angular-file-upload.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. /**!
  2. * AngularJS file upload/drop directive and service with progress and abort
  3. * @author Danial <danial.farid@gmail.com>
  4. * @version 3.2.4
  5. */
  6. (function () {
  7. var key, i;
  8. function patchXHR(fnName, newFn) {
  9. window.XMLHttpRequest.prototype[fnName] = newFn(window.XMLHttpRequest.prototype[fnName]);
  10. }
  11. if (window.XMLHttpRequest && !window.XMLHttpRequest.__isFileAPIShim) {
  12. patchXHR('setRequestHeader', function (orig) {
  13. return function (header, value) {
  14. if (header === '__setXHR_') {
  15. var val = value(this);
  16. // fix for angular < 1.2.0
  17. if (val instanceof Function) {
  18. val(this);
  19. }
  20. } else {
  21. orig.apply(this, arguments);
  22. }
  23. }
  24. });
  25. }
  26. var angularFileUpload = angular.module('angularFileUpload', []);
  27. angularFileUpload.version = '3.2.4';
  28. angularFileUpload.service('$upload', ['$http', '$q', '$timeout', function ($http, $q, $timeout) {
  29. function sendHttp(config) {
  30. config.method = config.method || 'POST';
  31. config.headers = config.headers || {};
  32. config.transformRequest = config.transformRequest || function (data, headersGetter) {
  33. if (window.ArrayBuffer && data instanceof window.ArrayBuffer) {
  34. return data;
  35. }
  36. return $http.defaults.transformRequest[0](data, headersGetter);
  37. };
  38. var deferred = $q.defer();
  39. var promise = deferred.promise;
  40. config.headers['__setXHR_'] = function () {
  41. return function (xhr) {
  42. if (!xhr) return;
  43. config.__XHR = xhr;
  44. config.xhrFn && config.xhrFn(xhr);
  45. xhr.upload.addEventListener('progress', function (e) {
  46. e.config = config;
  47. deferred.notify ? deferred.notify(e) : promise.progress_fn && $timeout(function () {
  48. promise.progress_fn(e)
  49. });
  50. }, false);
  51. //fix for firefox not firing upload progress end, also IE8-9
  52. xhr.upload.addEventListener('load', function (e) {
  53. if (e.lengthComputable) {
  54. e.config = config;
  55. deferred.notify ? deferred.notify(e) : promise.progress_fn && $timeout(function () {
  56. promise.progress_fn(e)
  57. });
  58. }
  59. }, false);
  60. };
  61. };
  62. $http(config).then(function (r) {
  63. deferred.resolve(r)
  64. }, function (e) {
  65. deferred.reject(e)
  66. }, function (n) {
  67. deferred.notify(n)
  68. });
  69. promise.success = function (fn) {
  70. promise.then(function (response) {
  71. fn(response.data, response.status, response.headers, config);
  72. });
  73. return promise;
  74. };
  75. promise.error = function (fn) {
  76. promise.then(null, function (response) {
  77. fn(response.data, response.status, response.headers, config);
  78. });
  79. return promise;
  80. };
  81. promise.progress = function (fn) {
  82. promise.progress_fn = fn;
  83. promise.then(null, null, function (update) {
  84. fn(update);
  85. });
  86. return promise;
  87. };
  88. promise.abort = function () {
  89. if (config.__XHR) {
  90. $timeout(function () {
  91. config.__XHR.abort();
  92. });
  93. }
  94. return promise;
  95. };
  96. promise.xhr = function (fn) {
  97. config.xhrFn = (function (origXhrFn) {
  98. return function () {
  99. origXhrFn && origXhrFn.apply(promise, arguments);
  100. fn.apply(promise, arguments);
  101. }
  102. })(config.xhrFn);
  103. return promise;
  104. };
  105. return promise;
  106. }
  107. this.upload = function (config) {
  108. config.headers = config.headers || {};
  109. config.headers['Content-Type'] = undefined;
  110. config.transformRequest = config.transformRequest ?
  111. (Object.prototype.toString.call(config.transformRequest) === '[object Array]' ?
  112. config.transformRequest : [config.transformRequest]) : [];
  113. config.transformRequest.push(function (data) {
  114. var formData = new FormData();
  115. var allFields = {};
  116. for (key in config.fields) {
  117. if (config.fields.hasOwnProperty(key)) {
  118. allFields[key] = config.fields[key];
  119. }
  120. }
  121. if (data) allFields['data'] = data;
  122. if (config.formDataAppender) {
  123. for (key in allFields) {
  124. if (allFields.hasOwnProperty(key)) {
  125. config.formDataAppender(formData, key, allFields[key]);
  126. }
  127. }
  128. } else {
  129. for (key in allFields) {
  130. if (allFields.hasOwnProperty(key)) {
  131. var val = allFields[key];
  132. if (val !== undefined) {
  133. if (Object.prototype.toString.call(val) === '[object String]') {
  134. formData.append(key, val);
  135. } else {
  136. if (config.sendObjectsAsJsonBlob && typeof val === 'object') {
  137. formData.append(key, new Blob([val], {type: 'application/json'}));
  138. } else {
  139. formData.append(key, JSON.stringify(val));
  140. }
  141. }
  142. }
  143. }
  144. }
  145. }
  146. if (config.file != null) {
  147. var fileFormName = config.fileFormDataName || 'file';
  148. if (Object.prototype.toString.call(config.file) === '[object Array]') {
  149. var isFileFormNameString = Object.prototype.toString.call(fileFormName) === '[object String]';
  150. for (i = 0; i < config.file.length; i++) {
  151. formData.append(isFileFormNameString ? fileFormName : fileFormName[i], config.file[i],
  152. (config.fileName && config.fileName[i]) || config.file[i].name);
  153. }
  154. } else {
  155. formData.append(fileFormName, config.file, config.fileName || config.file.name);
  156. }
  157. }
  158. return formData;
  159. });
  160. return sendHttp(config);
  161. };
  162. this.http = function (config) {
  163. return sendHttp(config);
  164. };
  165. }]);
  166. angularFileUpload.directive('ngFileSelect', ['$parse', '$timeout', '$compile',
  167. function ($parse, $timeout, $compile) {
  168. return {
  169. restrict: 'AEC',
  170. require: '?ngModel',
  171. link: function (scope, elem, attr, ngModel) {
  172. linkFileSelect(scope, elem, attr, ngModel, $parse, $timeout, $compile);
  173. }
  174. }
  175. }]);
  176. function linkFileSelect(scope, elem, attr, ngModel, $parse, $timeout, $compile) {
  177. function isInputTypeFile() {
  178. return elem[0].tagName.toLowerCase() === 'input' && elem.attr('type') && elem.attr('type').toLowerCase() === 'file';
  179. }
  180. var isUpdating = false;
  181. function changeFn(evt) {
  182. if (!isUpdating) {
  183. isUpdating = true;
  184. try {
  185. var fileList = evt.__files_ || (evt.target && evt.target.files);
  186. var files = [], rejFiles = [];
  187. var accept = $parse(attr.ngAccept);
  188. for (i = 0; i < fileList.length; i++) {
  189. var file = fileList.item(i);
  190. if (isAccepted(scope, accept, file, evt)) {
  191. files.push(file);
  192. } else {
  193. rejFiles.push(file);
  194. }
  195. }
  196. updateModel($parse, $timeout, scope, ngModel, attr,
  197. attr.ngFileChange || attr.ngFileSelect, files, rejFiles, evt);
  198. if (files.length == 0) evt.target.value = files;
  199. if (evt.target && evt.target.getAttribute('__ngf_gen__')) {
  200. angular.element(evt.target).remove();
  201. }
  202. } finally {
  203. isUpdating = false;
  204. }
  205. }
  206. }
  207. function bindAttrToFileInput(fileElem) {
  208. if (attr.ngMultiple) fileElem.attr('multiple', $parse(attr.ngMultiple)(scope));
  209. if (attr['accept']) fileElem.attr('accept', attr['accept']);
  210. if (attr.ngCapture) fileElem.attr('capture', $parse(attr.ngCapture)(scope));
  211. if (attr.ngDisabled) fileElem.attr('disabled', $parse(attr.ngDisabled)(scope));
  212. fileElem.bind('change', changeFn);
  213. }
  214. function createFileInput(evt) {
  215. if (elem.attr('disabled')) {
  216. return;
  217. }
  218. var fileElem = angular.element('<input type="file">');
  219. for (var i = 0; i < elem[0].attributes.length; i++) {
  220. var attribute = elem[0].attributes[i];
  221. fileElem.attr(attribute.name, attribute.value);
  222. }
  223. if (isInputTypeFile()) {
  224. elem.replaceWith(fileElem);
  225. elem = fileElem;
  226. } else {
  227. fileElem.css('width', '0px').css('height', '0px').css('position', 'absolute')
  228. .css('padding', 0).css('margin', 0).css('overflow', 'hidden')
  229. .attr('tabindex', '-1').css('opacity', 0).attr('__ngf_gen__', true);
  230. if (elem.__ngf_ref_elem__) elem.__ngf_ref_elem__.remove();
  231. elem.__ngf_ref_elem__ = fileElem;
  232. elem.parent()[0].insertBefore(fileElem[0], elem[0]);
  233. elem.css('overflow', 'hidden');
  234. }
  235. bindAttrToFileInput(fileElem);
  236. return fileElem;
  237. }
  238. function resetModel(evt) {
  239. updateModel($parse, $timeout, scope, ngModel, attr,
  240. attr.ngFileChange || attr.ngFileSelect, [], [], evt, true);
  241. }
  242. function clickHandler(evt) {
  243. var fileElem = createFileInput(evt);
  244. if (fileElem) {
  245. resetModel(evt);
  246. fileElem[0].click();
  247. }
  248. if (isInputTypeFile()) {
  249. elem.bind('click', clickHandler);
  250. evt.preventDefault()
  251. }
  252. }
  253. if (window.FileAPI && window.FileAPI.ngfFixIE) {
  254. window.FileAPI.ngfFixIE(elem, createFileInput, changeFn, resetModel);
  255. } else {
  256. elem.bind('click', clickHandler);
  257. }
  258. }
  259. angularFileUpload.directive('ngFileDrop', ['$parse', '$timeout', '$location', function ($parse, $timeout, $location) {
  260. return {
  261. restrict: 'AEC',
  262. require: '?ngModel',
  263. link: function (scope, elem, attr, ngModel) {
  264. linkDrop(scope, elem, attr, ngModel, $parse, $timeout, $location);
  265. }
  266. }
  267. }]);
  268. angularFileUpload.directive('ngNoFileDrop', function () {
  269. return function (scope, elem) {
  270. if (dropAvailable()) elem.css('display', 'none')
  271. }
  272. });
  273. //for backward compatibility
  274. angularFileUpload.directive('ngFileDropAvailable', ['$parse', '$timeout', function ($parse, $timeout) {
  275. return function (scope, elem, attr) {
  276. if (dropAvailable()) {
  277. var fn = $parse(attr['ngFileDropAvailable']);
  278. $timeout(function () {
  279. fn(scope);
  280. });
  281. }
  282. }
  283. }]);
  284. function linkDrop(scope, elem, attr, ngModel, $parse, $timeout, $location) {
  285. var available = dropAvailable();
  286. if (attr['dropAvailable']) {
  287. $timeout(function () {
  288. scope.dropAvailable ? scope.dropAvailable.value = available :
  289. scope.dropAvailable = available;
  290. });
  291. }
  292. if (!available) {
  293. if ($parse(attr.hideOnDropNotAvailable)(scope) == true) {
  294. elem.css('display', 'none');
  295. }
  296. return;
  297. }
  298. var leaveTimeout = null;
  299. var stopPropagation = $parse(attr.stopPropagation);
  300. var dragOverDelay = 1;
  301. var accept = $parse(attr.ngAccept);
  302. var disabled = $parse(attr.ngDisabled);
  303. var actualDragOverClass;
  304. elem[0].addEventListener('dragover', function (evt) {
  305. if (disabled(scope)) return;
  306. evt.preventDefault();
  307. if (stopPropagation(scope)) evt.stopPropagation();
  308. // handling dragover events from the Chrome download bar
  309. if (navigator.userAgent.indexOf("Chrome") > -1) {
  310. var b = evt.dataTransfer.effectAllowed;
  311. evt.dataTransfer.dropEffect = ('move' === b || 'linkMove' === b) ? 'move' : 'copy';
  312. }
  313. $timeout.cancel(leaveTimeout);
  314. if (!scope.actualDragOverClass) {
  315. actualDragOverClass = calculateDragOverClass(scope, attr, evt);
  316. }
  317. elem.addClass(actualDragOverClass);
  318. }, false);
  319. elem[0].addEventListener('dragenter', function (evt) {
  320. if (disabled(scope)) return;
  321. evt.preventDefault();
  322. if (stopPropagation(scope)) evt.stopPropagation();
  323. }, false);
  324. elem[0].addEventListener('dragleave', function () {
  325. if (disabled(scope)) return;
  326. leaveTimeout = $timeout(function () {
  327. elem.removeClass(actualDragOverClass);
  328. actualDragOverClass = null;
  329. }, dragOverDelay || 1);
  330. }, false);
  331. elem[0].addEventListener('drop', function (evt) {
  332. if (disabled(scope)) return;
  333. evt.preventDefault();
  334. if (stopPropagation(scope)) evt.stopPropagation();
  335. elem.removeClass(actualDragOverClass);
  336. actualDragOverClass = null;
  337. extractFiles(evt, function (files, rejFiles) {
  338. updateModel($parse, $timeout, scope, ngModel, attr,
  339. attr.ngFileChange || attr.ngFileDrop, files, rejFiles, evt)
  340. }, $parse(attr.allowDir)(scope) != false, attr.multiple || $parse(attr.ngMultiple)(scope));
  341. }, false);
  342. function calculateDragOverClass(scope, attr, evt) {
  343. var accepted = true;
  344. var items = evt.dataTransfer.items;
  345. if (items != null) {
  346. for (i = 0; i < items.length && accepted; i++) {
  347. accepted = accepted
  348. && (items[i].kind == 'file' || items[i].kind == '')
  349. && isAccepted(scope, accept, items[i], evt);
  350. }
  351. }
  352. var clazz = $parse(attr.dragOverClass)(scope, {$event: evt});
  353. if (clazz) {
  354. if (clazz.delay) dragOverDelay = clazz.delay;
  355. if (clazz.accept) clazz = accepted ? clazz.accept : clazz.reject;
  356. }
  357. return clazz || attr['dragOverClass'] || 'dragover';
  358. }
  359. function extractFiles(evt, callback, allowDir, multiple) {
  360. var files = [], rejFiles = [], items = evt.dataTransfer.items, processing = 0;
  361. function addFile(file) {
  362. if (isAccepted(scope, accept, file, evt)) {
  363. files.push(file);
  364. } else {
  365. rejFiles.push(file);
  366. }
  367. }
  368. if (items && items.length > 0 && $location.protocol() != 'file') {
  369. for (i = 0; i < items.length; i++) {
  370. if (items[i].webkitGetAsEntry && items[i].webkitGetAsEntry() && items[i].webkitGetAsEntry().isDirectory) {
  371. var entry = items[i].webkitGetAsEntry();
  372. if (entry.isDirectory && !allowDir) {
  373. continue;
  374. }
  375. if (entry != null) {
  376. traverseFileTree(files, entry);
  377. }
  378. } else {
  379. var f = items[i].getAsFile();
  380. if (f != null) addFile(f);
  381. }
  382. if (!multiple && files.length > 0) break;
  383. }
  384. } else {
  385. var fileList = evt.dataTransfer.files;
  386. if (fileList != null) {
  387. for (i = 0; i < fileList.length; i++) {
  388. addFile(fileList.item(i));
  389. if (!multiple && files.length > 0) break;
  390. }
  391. }
  392. }
  393. var delays = 0;
  394. (function waitForProcess(delay) {
  395. $timeout(function () {
  396. if (!processing) {
  397. if (!multiple && files.length > 1) {
  398. i = 0;
  399. while (files[i].type == 'directory') i++;
  400. files = [files[i]];
  401. }
  402. callback(files, rejFiles);
  403. } else {
  404. if (delays++ * 10 < 20 * 1000) {
  405. waitForProcess(10);
  406. }
  407. }
  408. }, delay || 0)
  409. })();
  410. function traverseFileTree(files, entry, path) {
  411. if (entry != null) {
  412. if (entry.isDirectory) {
  413. var filePath = (path || '') + entry.name;
  414. addFile({name: entry.name, type: 'directory', path: filePath});
  415. var dirReader = entry.createReader();
  416. var entries = [];
  417. processing++;
  418. var readEntries = function () {
  419. dirReader.readEntries(function (results) {
  420. try {
  421. if (!results.length) {
  422. for (i = 0; i < entries.length; i++) {
  423. traverseFileTree(files, entries[i], (path ? path : '') + entry.name + '/');
  424. }
  425. processing--;
  426. } else {
  427. entries = entries.concat(Array.prototype.slice.call(results || [], 0));
  428. readEntries();
  429. }
  430. } catch (e) {
  431. processing--;
  432. console.error(e);
  433. }
  434. }, function () {
  435. processing--;
  436. });
  437. };
  438. readEntries();
  439. } else {
  440. processing++;
  441. entry.file(function (file) {
  442. try {
  443. processing--;
  444. file.path = (path ? path : '') + file.name;
  445. addFile(file);
  446. } catch (e) {
  447. processing--;
  448. console.error(e);
  449. }
  450. }, function () {
  451. processing--;
  452. });
  453. }
  454. }
  455. }
  456. }
  457. }
  458. function dropAvailable() {
  459. var div = document.createElement('div');
  460. return ('draggable' in div) && ('ondrop' in div);
  461. }
  462. function updateModel($parse, $timeout, scope, ngModel, attr, fileChange, files, rejFiles, evt, noDelay) {
  463. function update() {
  464. if (ngModel) {
  465. $parse(attr.ngModel).assign(scope, files);
  466. $timeout(function () {
  467. ngModel && ngModel.$setViewValue(files != null && files.length == 0 ? null : files);
  468. });
  469. }
  470. if (attr.ngModelRejected) {
  471. $parse(attr.ngModelRejected).assign(scope, rejFiles);
  472. }
  473. if (fileChange) {
  474. $parse(fileChange)(scope, {
  475. $files: files,
  476. $rejectedFiles: rejFiles,
  477. $event: evt
  478. });
  479. }
  480. }
  481. if (noDelay) {
  482. update();
  483. } else {
  484. $timeout(function () {
  485. update();
  486. });
  487. }
  488. }
  489. function isAccepted(scope, accept, file, evt) {
  490. var val = accept(scope, {$file: file, $event: evt});
  491. if (val == null) {
  492. return true;
  493. }
  494. if (angular.isString(val)) {
  495. var regexp = new RegExp(globStringToRegex(val), 'gi')
  496. val = (file.type != null && file.type.match(regexp)) ||
  497. (file.name != null && file.name.match(regexp));
  498. }
  499. return val;
  500. }
  501. function globStringToRegex(str) {
  502. if (str.length > 2 && str[0] === '/' && str[str.length - 1] === '/') {
  503. return str.substring(1, str.length - 1);
  504. }
  505. var split = str.split(','), result = '';
  506. if (split.length > 1) {
  507. for (i = 0; i < split.length; i++) {
  508. result += '(' + globStringToRegex(split[i]) + ')';
  509. if (i < split.length - 1) {
  510. result += '|'
  511. }
  512. }
  513. } else {
  514. if (str.indexOf('.') == 0) {
  515. str = '*' + str;
  516. }
  517. result = '^' + str.replace(new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\' + '-]', 'g'), '\\$&') + '$';
  518. result = result.replace(/\\\*/g, '.*').replace(/\\\?/g, '.');
  519. }
  520. return result;
  521. }
  522. var ngFileUpload = angular.module('ngFileUpload', []);
  523. for (key in angularFileUpload) {
  524. if (angularFileUpload.hasOwnProperty(key)) {
  525. ngFileUpload[key] = angularFileUpload[key];
  526. }
  527. }
  528. })();