ng-flow-standalone.js 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843
  1. /**
  2. * @license MIT
  3. */
  4. (function(window, document, undefined) {'use strict';
  5. // ie10+
  6. var ie10plus = window.navigator.msPointerEnabled;
  7. /**
  8. * Flow.js is a library providing multiple simultaneous, stable and
  9. * resumable uploads via the HTML5 File API.
  10. * @param [opts]
  11. * @param {number} [opts.chunkSize]
  12. * @param {bool} [opts.forceChunkSize]
  13. * @param {number} [opts.simultaneousUploads]
  14. * @param {bool} [opts.singleFile]
  15. * @param {string} [opts.fileParameterName]
  16. * @param {number} [opts.progressCallbacksInterval]
  17. * @param {number} [opts.speedSmoothingFactor]
  18. * @param {Object|Function} [opts.query]
  19. * @param {Object|Function} [opts.headers]
  20. * @param {bool} [opts.withCredentials]
  21. * @param {Function} [opts.preprocess]
  22. * @param {string} [opts.method]
  23. * @param {string|Function} [opts.testMethod]
  24. * @param {string|Function} [opts.uploadMethod]
  25. * @param {bool} [opts.prioritizeFirstAndLastChunk]
  26. * @param {string|Function} [opts.target]
  27. * @param {number} [opts.maxChunkRetries]
  28. * @param {number} [opts.chunkRetryInterval]
  29. * @param {Array.<number>} [opts.permanentErrors]
  30. * @param {Array.<number>} [opts.successStatuses]
  31. * @param {Function} [opts.generateUniqueIdentifier]
  32. * @constructor
  33. */
  34. function Flow(opts) {
  35. /**
  36. * Supported by browser?
  37. * @type {boolean}
  38. */
  39. this.support = (
  40. typeof File !== 'undefined' &&
  41. typeof Blob !== 'undefined' &&
  42. typeof FileList !== 'undefined' &&
  43. (
  44. !!Blob.prototype.slice || !!Blob.prototype.webkitSlice || !!Blob.prototype.mozSlice ||
  45. false
  46. ) // slicing files support
  47. );
  48. if (!this.support) {
  49. return ;
  50. }
  51. /**
  52. * Check if directory upload is supported
  53. * @type {boolean}
  54. */
  55. this.supportDirectory = /WebKit/.test(window.navigator.userAgent);
  56. /**
  57. * List of FlowFile objects
  58. * @type {Array.<FlowFile>}
  59. */
  60. this.files = [];
  61. /**
  62. * Default options for flow.js
  63. * @type {Object}
  64. */
  65. this.defaults = {
  66. chunkSize: 1024 * 1024,
  67. forceChunkSize: false,
  68. simultaneousUploads: 3,
  69. singleFile: false,
  70. fileParameterName: 'file',
  71. progressCallbacksInterval: 500,
  72. speedSmoothingFactor: 0.1,
  73. query: {},
  74. headers: {},
  75. withCredentials: false,
  76. preprocess: null,
  77. method: 'multipart',
  78. testMethod: 'GET',
  79. uploadMethod: 'POST',
  80. prioritizeFirstAndLastChunk: false,
  81. target: '/',
  82. testChunks: true,
  83. generateUniqueIdentifier: null,
  84. maxChunkRetries: 0,
  85. chunkRetryInterval: null,
  86. permanentErrors: [404, 415, 500, 501],
  87. successStatuses: [200, 201, 202],
  88. onDropStopPropagation: false
  89. };
  90. /**
  91. * Current options
  92. * @type {Object}
  93. */
  94. this.opts = {};
  95. /**
  96. * List of events:
  97. * key stands for event name
  98. * value array list of callbacks
  99. * @type {}
  100. */
  101. this.events = {};
  102. var $ = this;
  103. /**
  104. * On drop event
  105. * @function
  106. * @param {MouseEvent} event
  107. */
  108. this.onDrop = function (event) {
  109. if ($.opts.onDropStopPropagation) {
  110. event.stopPropagation();
  111. }
  112. event.preventDefault();
  113. var dataTransfer = event.dataTransfer;
  114. if (dataTransfer.items && dataTransfer.items[0] &&
  115. dataTransfer.items[0].webkitGetAsEntry) {
  116. $.webkitReadDataTransfer(event);
  117. } else {
  118. $.addFiles(dataTransfer.files, event);
  119. }
  120. };
  121. /**
  122. * Prevent default
  123. * @function
  124. * @param {MouseEvent} event
  125. */
  126. this.preventEvent = function (event) {
  127. event.preventDefault();
  128. };
  129. /**
  130. * Current options
  131. * @type {Object}
  132. */
  133. this.opts = Flow.extend({}, this.defaults, opts || {});
  134. }
  135. Flow.prototype = {
  136. /**
  137. * Set a callback for an event, possible events:
  138. * fileSuccess(file), fileProgress(file), fileAdded(file, event),
  139. * fileRetry(file), fileError(file, message), complete(),
  140. * progress(), error(message, file), pause()
  141. * @function
  142. * @param {string} event
  143. * @param {Function} callback
  144. */
  145. on: function (event, callback) {
  146. event = event.toLowerCase();
  147. if (!this.events.hasOwnProperty(event)) {
  148. this.events[event] = [];
  149. }
  150. this.events[event].push(callback);
  151. },
  152. /**
  153. * Remove event callback
  154. * @function
  155. * @param {string} [event] removes all events if not specified
  156. * @param {Function} [fn] removes all callbacks of event if not specified
  157. */
  158. off: function (event, fn) {
  159. if (event !== undefined) {
  160. event = event.toLowerCase();
  161. if (fn !== undefined) {
  162. if (this.events.hasOwnProperty(event)) {
  163. arrayRemove(this.events[event], fn);
  164. }
  165. } else {
  166. delete this.events[event];
  167. }
  168. } else {
  169. this.events = {};
  170. }
  171. },
  172. /**
  173. * Fire an event
  174. * @function
  175. * @param {string} event event name
  176. * @param {...} args arguments of a callback
  177. * @return {bool} value is false if at least one of the event handlers which handled this event
  178. * returned false. Otherwise it returns true.
  179. */
  180. fire: function (event, args) {
  181. // `arguments` is an object, not array, in FF, so:
  182. args = Array.prototype.slice.call(arguments);
  183. event = event.toLowerCase();
  184. var preventDefault = false;
  185. if (this.events.hasOwnProperty(event)) {
  186. each(this.events[event], function (callback) {
  187. preventDefault = callback.apply(this, args.slice(1)) === false || preventDefault;
  188. }, this);
  189. }
  190. if (event != 'catchall') {
  191. args.unshift('catchAll');
  192. preventDefault = this.fire.apply(this, args) === false || preventDefault;
  193. }
  194. return !preventDefault;
  195. },
  196. /**
  197. * Read webkit dataTransfer object
  198. * @param event
  199. */
  200. webkitReadDataTransfer: function (event) {
  201. var $ = this;
  202. var queue = event.dataTransfer.items.length;
  203. var files = [];
  204. each(event.dataTransfer.items, function (item) {
  205. var entry = item.webkitGetAsEntry();
  206. if (!entry) {
  207. decrement();
  208. return ;
  209. }
  210. if (entry.isFile) {
  211. // due to a bug in Chrome's File System API impl - #149735
  212. fileReadSuccess(item.getAsFile(), entry.fullPath);
  213. } else {
  214. entry.createReader().readEntries(readSuccess, readError);
  215. }
  216. });
  217. function readSuccess(entries) {
  218. queue += entries.length;
  219. each(entries, function(entry) {
  220. if (entry.isFile) {
  221. var fullPath = entry.fullPath;
  222. entry.file(function (file) {
  223. fileReadSuccess(file, fullPath);
  224. }, readError);
  225. } else if (entry.isDirectory) {
  226. entry.createReader().readEntries(readSuccess, readError);
  227. }
  228. });
  229. decrement();
  230. }
  231. function fileReadSuccess(file, fullPath) {
  232. // relative path should not start with "/"
  233. file.relativePath = fullPath.substring(1);
  234. files.push(file);
  235. decrement();
  236. }
  237. function readError(fileError) {
  238. throw fileError;
  239. }
  240. function decrement() {
  241. if (--queue == 0) {
  242. $.addFiles(files, event);
  243. }
  244. }
  245. },
  246. /**
  247. * Generate unique identifier for a file
  248. * @function
  249. * @param {FlowFile} file
  250. * @returns {string}
  251. */
  252. generateUniqueIdentifier: function (file) {
  253. var custom = this.opts.generateUniqueIdentifier;
  254. if (typeof custom === 'function') {
  255. return custom(file);
  256. }
  257. // Some confusion in different versions of Firefox
  258. var relativePath = file.relativePath || file.webkitRelativePath || file.fileName || file.name;
  259. return file.size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, '');
  260. },
  261. /**
  262. * Upload next chunk from the queue
  263. * @function
  264. * @returns {boolean}
  265. * @private
  266. */
  267. uploadNextChunk: function (preventEvents) {
  268. // In some cases (such as videos) it's really handy to upload the first
  269. // and last chunk of a file quickly; this let's the server check the file's
  270. // metadata and determine if there's even a point in continuing.
  271. var found = false;
  272. if (this.opts.prioritizeFirstAndLastChunk) {
  273. each(this.files, function (file) {
  274. if (!file.paused && file.chunks.length &&
  275. file.chunks[0].status() === 'pending' &&
  276. file.chunks[0].preprocessState === 0) {
  277. file.chunks[0].send();
  278. found = true;
  279. return false;
  280. }
  281. if (!file.paused && file.chunks.length > 1 &&
  282. file.chunks[file.chunks.length - 1].status() === 'pending' &&
  283. file.chunks[0].preprocessState === 0) {
  284. file.chunks[file.chunks.length - 1].send();
  285. found = true;
  286. return false;
  287. }
  288. });
  289. if (found) {
  290. return found;
  291. }
  292. }
  293. // Now, simply look for the next, best thing to upload
  294. each(this.files, function (file) {
  295. if (!file.paused) {
  296. each(file.chunks, function (chunk) {
  297. if (chunk.status() === 'pending' && chunk.preprocessState === 0) {
  298. chunk.send();
  299. found = true;
  300. return false;
  301. }
  302. });
  303. }
  304. if (found) {
  305. return false;
  306. }
  307. });
  308. if (found) {
  309. return true;
  310. }
  311. // The are no more outstanding chunks to upload, check is everything is done
  312. var outstanding = false;
  313. each(this.files, function (file) {
  314. if (!file.isComplete()) {
  315. outstanding = true;
  316. return false;
  317. }
  318. });
  319. if (!outstanding && !preventEvents) {
  320. // All chunks have been uploaded, complete
  321. async(function () {
  322. this.fire('complete');
  323. }, this);
  324. }
  325. return false;
  326. },
  327. /**
  328. * Assign a browse action to one or more DOM nodes.
  329. * @function
  330. * @param {Element|Array.<Element>} domNodes
  331. * @param {boolean} isDirectory Pass in true to allow directories to
  332. * @param {boolean} singleFile prevent multi file upload
  333. * @param {Object} attributes set custom attributes:
  334. * http://www.w3.org/TR/html-markup/input.file.html#input.file-attributes
  335. * eg: accept: 'image/*'
  336. * be selected (Chrome only).
  337. */
  338. assignBrowse: function (domNodes, isDirectory, singleFile, attributes) {
  339. if (typeof domNodes.length === 'undefined') {
  340. domNodes = [domNodes];
  341. }
  342. each(domNodes, function (domNode) {
  343. var input;
  344. if (domNode.tagName === 'INPUT' && domNode.type === 'file') {
  345. input = domNode;
  346. } else {
  347. input = document.createElement('input');
  348. input.setAttribute('type', 'file');
  349. // display:none - not working in opera 12
  350. extend(input.style, {
  351. visibility: 'hidden',
  352. position: 'absolute'
  353. });
  354. // for opera 12 browser, input must be assigned to a document
  355. domNode.appendChild(input);
  356. // https://developer.mozilla.org/en/using_files_from_web_applications)
  357. // event listener is executed two times
  358. // first one - original mouse click event
  359. // second - input.click(), input is inside domNode
  360. domNode.addEventListener('click', function() {
  361. input.click();
  362. }, false);
  363. }
  364. if (!this.opts.singleFile && !singleFile) {
  365. input.setAttribute('multiple', 'multiple');
  366. }
  367. if (isDirectory) {
  368. input.setAttribute('webkitdirectory', 'webkitdirectory');
  369. }
  370. each(attributes, function (value, key) {
  371. input.setAttribute(key, value);
  372. });
  373. // When new files are added, simply append them to the overall list
  374. var $ = this;
  375. input.addEventListener('change', function (e) {
  376. $.addFiles(e.target.files, e);
  377. e.target.value = '';
  378. }, false);
  379. }, this);
  380. },
  381. /**
  382. * Assign one or more DOM nodes as a drop target.
  383. * @function
  384. * @param {Element|Array.<Element>} domNodes
  385. */
  386. assignDrop: function (domNodes) {
  387. if (typeof domNodes.length === 'undefined') {
  388. domNodes = [domNodes];
  389. }
  390. each(domNodes, function (domNode) {
  391. domNode.addEventListener('dragover', this.preventEvent, false);
  392. domNode.addEventListener('dragenter', this.preventEvent, false);
  393. domNode.addEventListener('drop', this.onDrop, false);
  394. }, this);
  395. },
  396. /**
  397. * Un-assign drop event from DOM nodes
  398. * @function
  399. * @param domNodes
  400. */
  401. unAssignDrop: function (domNodes) {
  402. if (typeof domNodes.length === 'undefined') {
  403. domNodes = [domNodes];
  404. }
  405. each(domNodes, function (domNode) {
  406. domNode.removeEventListener('dragover', this.preventEvent);
  407. domNode.removeEventListener('dragenter', this.preventEvent);
  408. domNode.removeEventListener('drop', this.onDrop);
  409. }, this);
  410. },
  411. /**
  412. * Returns a boolean indicating whether or not the instance is currently
  413. * uploading anything.
  414. * @function
  415. * @returns {boolean}
  416. */
  417. isUploading: function () {
  418. var uploading = false;
  419. each(this.files, function (file) {
  420. if (file.isUploading()) {
  421. uploading = true;
  422. return false;
  423. }
  424. });
  425. return uploading;
  426. },
  427. /**
  428. * should upload next chunk
  429. * @function
  430. * @returns {boolean|number}
  431. */
  432. _shouldUploadNext: function () {
  433. var num = 0;
  434. var should = true;
  435. var simultaneousUploads = this.opts.simultaneousUploads;
  436. each(this.files, function (file) {
  437. each(file.chunks, function(chunk) {
  438. if (chunk.status() === 'uploading') {
  439. num++;
  440. if (num >= simultaneousUploads) {
  441. should = false;
  442. return false;
  443. }
  444. }
  445. });
  446. });
  447. // if should is true then return uploading chunks's length
  448. return should && num;
  449. },
  450. /**
  451. * Start or resume uploading.
  452. * @function
  453. */
  454. upload: function () {
  455. // Make sure we don't start too many uploads at once
  456. var ret = this._shouldUploadNext();
  457. if (ret === false) {
  458. return;
  459. }
  460. // Kick off the queue
  461. this.fire('uploadStart');
  462. var started = false;
  463. for (var num = 1; num <= this.opts.simultaneousUploads - ret; num++) {
  464. started = this.uploadNextChunk(true) || started;
  465. }
  466. if (!started) {
  467. async(function () {
  468. this.fire('complete');
  469. }, this);
  470. }
  471. },
  472. /**
  473. * Resume uploading.
  474. * @function
  475. */
  476. resume: function () {
  477. each(this.files, function (file) {
  478. file.resume();
  479. });
  480. },
  481. /**
  482. * Pause uploading.
  483. * @function
  484. */
  485. pause: function () {
  486. each(this.files, function (file) {
  487. file.pause();
  488. });
  489. },
  490. /**
  491. * Cancel upload of all FlowFile objects and remove them from the list.
  492. * @function
  493. */
  494. cancel: function () {
  495. for (var i = this.files.length - 1; i >= 0; i--) {
  496. this.files[i].cancel();
  497. }
  498. },
  499. /**
  500. * Returns a number between 0 and 1 indicating the current upload progress
  501. * of all files.
  502. * @function
  503. * @returns {number}
  504. */
  505. progress: function () {
  506. var totalDone = 0;
  507. var totalSize = 0;
  508. // Resume all chunks currently being uploaded
  509. each(this.files, function (file) {
  510. totalDone += file.progress() * file.size;
  511. totalSize += file.size;
  512. });
  513. return totalSize > 0 ? totalDone / totalSize : 0;
  514. },
  515. /**
  516. * Add a HTML5 File object to the list of files.
  517. * @function
  518. * @param {File} file
  519. * @param {Event} [event] event is optional
  520. */
  521. addFile: function (file, event) {
  522. this.addFiles([file], event);
  523. },
  524. /**
  525. * Add a HTML5 File object to the list of files.
  526. * @function
  527. * @param {FileList|Array} fileList
  528. * @param {Event} [event] event is optional
  529. */
  530. addFiles: function (fileList, event) {
  531. var files = [];
  532. each(fileList, function (file) {
  533. // Uploading empty file IE10/IE11 hangs indefinitely
  534. // see https://connect.microsoft.com/IE/feedback/details/813443/uploading-empty-file-ie10-ie11-hangs-indefinitely
  535. // Directories have size `0` and name `.`
  536. // Ignore already added files
  537. if ((!ie10plus || ie10plus && file.size > 0) && !(file.size % 4096 === 0 && (file.name === '.' || file.fileName === '.')) &&
  538. !this.getFromUniqueIdentifier(this.generateUniqueIdentifier(file))) {
  539. var f = new FlowFile(this, file);
  540. if (this.fire('fileAdded', f, event)) {
  541. files.push(f);
  542. }
  543. }
  544. }, this);
  545. if (this.fire('filesAdded', files, event)) {
  546. each(files, function (file) {
  547. if (this.opts.singleFile && this.files.length > 0) {
  548. this.removeFile(this.files[0]);
  549. }
  550. this.files.push(file);
  551. }, this);
  552. }
  553. this.fire('filesSubmitted', files, event);
  554. },
  555. /**
  556. * Cancel upload of a specific FlowFile object from the list.
  557. * @function
  558. * @param {FlowFile} file
  559. */
  560. removeFile: function (file) {
  561. for (var i = this.files.length - 1; i >= 0; i--) {
  562. if (this.files[i] === file) {
  563. this.files.splice(i, 1);
  564. file.abort();
  565. }
  566. }
  567. },
  568. /**
  569. * Look up a FlowFile object by its unique identifier.
  570. * @function
  571. * @param {string} uniqueIdentifier
  572. * @returns {boolean|FlowFile} false if file was not found
  573. */
  574. getFromUniqueIdentifier: function (uniqueIdentifier) {
  575. var ret = false;
  576. each(this.files, function (file) {
  577. if (file.uniqueIdentifier === uniqueIdentifier) {
  578. ret = file;
  579. }
  580. });
  581. return ret;
  582. },
  583. /**
  584. * Returns the total size of all files in bytes.
  585. * @function
  586. * @returns {number}
  587. */
  588. getSize: function () {
  589. var totalSize = 0;
  590. each(this.files, function (file) {
  591. totalSize += file.size;
  592. });
  593. return totalSize;
  594. },
  595. /**
  596. * Returns the total size uploaded of all files in bytes.
  597. * @function
  598. * @returns {number}
  599. */
  600. sizeUploaded: function () {
  601. var size = 0;
  602. each(this.files, function (file) {
  603. size += file.sizeUploaded();
  604. });
  605. return size;
  606. },
  607. /**
  608. * Returns remaining time to upload all files in seconds. Accuracy is based on average speed.
  609. * If speed is zero, time remaining will be equal to positive infinity `Number.POSITIVE_INFINITY`
  610. * @function
  611. * @returns {number}
  612. */
  613. timeRemaining: function () {
  614. var sizeDelta = 0;
  615. var averageSpeed = 0;
  616. each(this.files, function (file) {
  617. if (!file.paused && !file.error) {
  618. sizeDelta += file.size - file.sizeUploaded();
  619. averageSpeed += file.averageSpeed;
  620. }
  621. });
  622. if (sizeDelta && !averageSpeed) {
  623. return Number.POSITIVE_INFINITY;
  624. }
  625. if (!sizeDelta && !averageSpeed) {
  626. return 0;
  627. }
  628. return Math.floor(sizeDelta / averageSpeed);
  629. }
  630. };
  631. /**
  632. * FlowFile class
  633. * @name FlowFile
  634. * @param {Flow} flowObj
  635. * @param {File} file
  636. * @constructor
  637. */
  638. function FlowFile(flowObj, file) {
  639. /**
  640. * Reference to parent Flow instance
  641. * @type {Flow}
  642. */
  643. this.flowObj = flowObj;
  644. /**
  645. * Reference to file
  646. * @type {File}
  647. */
  648. this.file = file;
  649. /**
  650. * File name. Some confusion in different versions of Firefox
  651. * @type {string}
  652. */
  653. this.name = file.fileName || file.name;
  654. /**
  655. * File size
  656. * @type {number}
  657. */
  658. this.size = file.size;
  659. /**
  660. * Relative file path
  661. * @type {string}
  662. */
  663. this.relativePath = file.relativePath || file.webkitRelativePath || this.name;
  664. /**
  665. * File unique identifier
  666. * @type {string}
  667. */
  668. this.uniqueIdentifier = flowObj.generateUniqueIdentifier(file);
  669. /**
  670. * List of chunks
  671. * @type {Array.<FlowChunk>}
  672. */
  673. this.chunks = [];
  674. /**
  675. * Indicated if file is paused
  676. * @type {boolean}
  677. */
  678. this.paused = false;
  679. /**
  680. * Indicated if file has encountered an error
  681. * @type {boolean}
  682. */
  683. this.error = false;
  684. /**
  685. * Average upload speed
  686. * @type {number}
  687. */
  688. this.averageSpeed = 0;
  689. /**
  690. * Current upload speed
  691. * @type {number}
  692. */
  693. this.currentSpeed = 0;
  694. /**
  695. * Date then progress was called last time
  696. * @type {number}
  697. * @private
  698. */
  699. this._lastProgressCallback = Date.now();
  700. /**
  701. * Previously uploaded file size
  702. * @type {number}
  703. * @private
  704. */
  705. this._prevUploadedSize = 0;
  706. /**
  707. * Holds previous progress
  708. * @type {number}
  709. * @private
  710. */
  711. this._prevProgress = 0;
  712. this.bootstrap();
  713. }
  714. FlowFile.prototype = {
  715. /**
  716. * Update speed parameters
  717. * @link http://stackoverflow.com/questions/2779600/how-to-estimate-download-time-remaining-accurately
  718. * @function
  719. */
  720. measureSpeed: function () {
  721. var timeSpan = Date.now() - this._lastProgressCallback;
  722. if (!timeSpan) {
  723. return ;
  724. }
  725. var smoothingFactor = this.flowObj.opts.speedSmoothingFactor;
  726. var uploaded = this.sizeUploaded();
  727. // Prevent negative upload speed after file upload resume
  728. this.currentSpeed = Math.max((uploaded - this._prevUploadedSize) / timeSpan * 1000, 0);
  729. this.averageSpeed = smoothingFactor * this.currentSpeed + (1 - smoothingFactor) * this.averageSpeed;
  730. this._prevUploadedSize = uploaded;
  731. },
  732. /**
  733. * For internal usage only.
  734. * Callback when something happens within the chunk.
  735. * @function
  736. * @param {FlowChunk} chunk
  737. * @param {string} event can be 'progress', 'success', 'error' or 'retry'
  738. * @param {string} [message]
  739. */
  740. chunkEvent: function (chunk, event, message) {
  741. switch (event) {
  742. case 'progress':
  743. if (Date.now() - this._lastProgressCallback <
  744. this.flowObj.opts.progressCallbacksInterval) {
  745. break;
  746. }
  747. this.measureSpeed();
  748. this.flowObj.fire('fileProgress', this, chunk);
  749. this.flowObj.fire('progress');
  750. this._lastProgressCallback = Date.now();
  751. break;
  752. case 'error':
  753. this.error = true;
  754. this.abort(true);
  755. this.flowObj.fire('fileError', this, message, chunk);
  756. this.flowObj.fire('error', message, this, chunk);
  757. break;
  758. case 'success':
  759. if (this.error) {
  760. return;
  761. }
  762. this.measureSpeed();
  763. this.flowObj.fire('fileProgress', this, chunk);
  764. this.flowObj.fire('progress');
  765. this._lastProgressCallback = Date.now();
  766. if (this.isComplete()) {
  767. this.currentSpeed = 0;
  768. this.averageSpeed = 0;
  769. this.flowObj.fire('fileSuccess', this, message, chunk);
  770. }
  771. break;
  772. case 'retry':
  773. this.flowObj.fire('fileRetry', this, chunk);
  774. break;
  775. }
  776. },
  777. /**
  778. * Pause file upload
  779. * @function
  780. */
  781. pause: function() {
  782. this.paused = true;
  783. this.abort();
  784. },
  785. /**
  786. * Resume file upload
  787. * @function
  788. */
  789. resume: function() {
  790. this.paused = false;
  791. this.flowObj.upload();
  792. },
  793. /**
  794. * Abort current upload
  795. * @function
  796. */
  797. abort: function (reset) {
  798. this.currentSpeed = 0;
  799. this.averageSpeed = 0;
  800. var chunks = this.chunks;
  801. if (reset) {
  802. this.chunks = [];
  803. }
  804. each(chunks, function (c) {
  805. if (c.status() === 'uploading') {
  806. c.abort();
  807. this.flowObj.uploadNextChunk();
  808. }
  809. }, this);
  810. },
  811. /**
  812. * Cancel current upload and remove from a list
  813. * @function
  814. */
  815. cancel: function () {
  816. this.flowObj.removeFile(this);
  817. },
  818. /**
  819. * Retry aborted file upload
  820. * @function
  821. */
  822. retry: function () {
  823. this.bootstrap();
  824. this.flowObj.upload();
  825. },
  826. /**
  827. * Clear current chunks and slice file again
  828. * @function
  829. */
  830. bootstrap: function () {
  831. this.abort(true);
  832. this.error = false;
  833. // Rebuild stack of chunks from file
  834. this._prevProgress = 0;
  835. var round = this.flowObj.opts.forceChunkSize ? Math.ceil : Math.floor;
  836. var chunks = Math.max(
  837. round(this.file.size / this.flowObj.opts.chunkSize), 1
  838. );
  839. for (var offset = 0; offset < chunks; offset++) {
  840. this.chunks.push(
  841. new FlowChunk(this.flowObj, this, offset)
  842. );
  843. }
  844. },
  845. /**
  846. * Get current upload progress status
  847. * @function
  848. * @returns {number} from 0 to 1
  849. */
  850. progress: function () {
  851. if (this.error) {
  852. return 1;
  853. }
  854. if (this.chunks.length === 1) {
  855. this._prevProgress = Math.max(this._prevProgress, this.chunks[0].progress());
  856. return this._prevProgress;
  857. }
  858. // Sum up progress across everything
  859. var bytesLoaded = 0;
  860. each(this.chunks, function (c) {
  861. // get chunk progress relative to entire file
  862. bytesLoaded += c.progress() * (c.endByte - c.startByte);
  863. });
  864. var percent = bytesLoaded / this.size;
  865. // We don't want to lose percentages when an upload is paused
  866. this._prevProgress = Math.max(this._prevProgress, percent > 0.9999 ? 1 : percent);
  867. return this._prevProgress;
  868. },
  869. /**
  870. * Indicates if file is being uploaded at the moment
  871. * @function
  872. * @returns {boolean}
  873. */
  874. isUploading: function () {
  875. var uploading = false;
  876. each(this.chunks, function (chunk) {
  877. if (chunk.status() === 'uploading') {
  878. uploading = true;
  879. return false;
  880. }
  881. });
  882. return uploading;
  883. },
  884. /**
  885. * Indicates if file is has finished uploading and received a response
  886. * @function
  887. * @returns {boolean}
  888. */
  889. isComplete: function () {
  890. var outstanding = false;
  891. each(this.chunks, function (chunk) {
  892. var status = chunk.status();
  893. if (status === 'pending' || status === 'uploading' || chunk.preprocessState === 1) {
  894. outstanding = true;
  895. return false;
  896. }
  897. });
  898. return !outstanding;
  899. },
  900. /**
  901. * Count total size uploaded
  902. * @function
  903. * @returns {number}
  904. */
  905. sizeUploaded: function () {
  906. var size = 0;
  907. each(this.chunks, function (chunk) {
  908. size += chunk.sizeUploaded();
  909. });
  910. return size;
  911. },
  912. /**
  913. * Returns remaining time to finish upload file in seconds. Accuracy is based on average speed.
  914. * If speed is zero, time remaining will be equal to positive infinity `Number.POSITIVE_INFINITY`
  915. * @function
  916. * @returns {number}
  917. */
  918. timeRemaining: function () {
  919. if (this.paused || this.error) {
  920. return 0;
  921. }
  922. var delta = this.size - this.sizeUploaded();
  923. if (delta && !this.averageSpeed) {
  924. return Number.POSITIVE_INFINITY;
  925. }
  926. if (!delta && !this.averageSpeed) {
  927. return 0;
  928. }
  929. return Math.floor(delta / this.averageSpeed);
  930. },
  931. /**
  932. * Get file type
  933. * @function
  934. * @returns {string}
  935. */
  936. getType: function () {
  937. return this.file.type && this.file.type.split('/')[1];
  938. },
  939. /**
  940. * Get file extension
  941. * @function
  942. * @returns {string}
  943. */
  944. getExtension: function () {
  945. return this.name.substr((~-this.name.lastIndexOf(".") >>> 0) + 2).toLowerCase();
  946. }
  947. };
  948. /**
  949. * Class for storing a single chunk
  950. * @name FlowChunk
  951. * @param {Flow} flowObj
  952. * @param {FlowFile} fileObj
  953. * @param {number} offset
  954. * @constructor
  955. */
  956. function FlowChunk(flowObj, fileObj, offset) {
  957. /**
  958. * Reference to parent flow object
  959. * @type {Flow}
  960. */
  961. this.flowObj = flowObj;
  962. /**
  963. * Reference to parent FlowFile object
  964. * @type {FlowFile}
  965. */
  966. this.fileObj = fileObj;
  967. /**
  968. * File size
  969. * @type {number}
  970. */
  971. this.fileObjSize = fileObj.size;
  972. /**
  973. * File offset
  974. * @type {number}
  975. */
  976. this.offset = offset;
  977. /**
  978. * Indicates if chunk existence was checked on the server
  979. * @type {boolean}
  980. */
  981. this.tested = false;
  982. /**
  983. * Number of retries performed
  984. * @type {number}
  985. */
  986. this.retries = 0;
  987. /**
  988. * Pending retry
  989. * @type {boolean}
  990. */
  991. this.pendingRetry = false;
  992. /**
  993. * Preprocess state
  994. * @type {number} 0 = unprocessed, 1 = processing, 2 = finished
  995. */
  996. this.preprocessState = 0;
  997. /**
  998. * Bytes transferred from total request size
  999. * @type {number}
  1000. */
  1001. this.loaded = 0;
  1002. /**
  1003. * Total request size
  1004. * @type {number}
  1005. */
  1006. this.total = 0;
  1007. /**
  1008. * Size of a chunk
  1009. * @type {number}
  1010. */
  1011. var chunkSize = this.flowObj.opts.chunkSize;
  1012. /**
  1013. * Chunk start byte in a file
  1014. * @type {number}
  1015. */
  1016. this.startByte = this.offset * chunkSize;
  1017. /**
  1018. * Chunk end byte in a file
  1019. * @type {number}
  1020. */
  1021. this.endByte = Math.min(this.fileObjSize, (this.offset + 1) * chunkSize);
  1022. /**
  1023. * XMLHttpRequest
  1024. * @type {XMLHttpRequest}
  1025. */
  1026. this.xhr = null;
  1027. if (this.fileObjSize - this.endByte < chunkSize &&
  1028. !this.flowObj.opts.forceChunkSize) {
  1029. // The last chunk will be bigger than the chunk size,
  1030. // but less than 2*chunkSize
  1031. this.endByte = this.fileObjSize;
  1032. }
  1033. var $ = this;
  1034. /**
  1035. * Send chunk event
  1036. * @param event
  1037. * @param {...} args arguments of a callback
  1038. */
  1039. this.event = function (event, args) {
  1040. args = Array.prototype.slice.call(arguments);
  1041. args.unshift($);
  1042. $.fileObj.chunkEvent.apply($.fileObj, args);
  1043. };
  1044. /**
  1045. * Catch progress event
  1046. * @param {ProgressEvent} event
  1047. */
  1048. this.progressHandler = function(event) {
  1049. if (event.lengthComputable) {
  1050. $.loaded = event.loaded ;
  1051. $.total = event.total;
  1052. }
  1053. $.event('progress', event);
  1054. };
  1055. /**
  1056. * Catch test event
  1057. * @param {Event} event
  1058. */
  1059. this.testHandler = function(event) {
  1060. var status = $.status(true);
  1061. if (status === 'error') {
  1062. $.event(status, $.message());
  1063. $.flowObj.uploadNextChunk();
  1064. } else if (status === 'success') {
  1065. $.tested = true;
  1066. $.event(status, $.message());
  1067. $.flowObj.uploadNextChunk();
  1068. } else if (!$.fileObj.paused) {
  1069. // Error might be caused by file pause method
  1070. // Chunks does not exist on the server side
  1071. $.tested = true;
  1072. $.send();
  1073. }
  1074. };
  1075. /**
  1076. * Upload has stopped
  1077. * @param {Event} event
  1078. */
  1079. this.doneHandler = function(event) {
  1080. var status = $.status();
  1081. if (status === 'success' || status === 'error') {
  1082. $.event(status, $.message());
  1083. $.flowObj.uploadNextChunk();
  1084. } else {
  1085. $.event('retry', $.message());
  1086. $.pendingRetry = true;
  1087. $.abort();
  1088. $.retries++;
  1089. var retryInterval = $.flowObj.opts.chunkRetryInterval;
  1090. if (retryInterval !== null) {
  1091. setTimeout(function () {
  1092. $.send();
  1093. }, retryInterval);
  1094. } else {
  1095. $.send();
  1096. }
  1097. }
  1098. };
  1099. }
  1100. FlowChunk.prototype = {
  1101. /**
  1102. * Get params for a request
  1103. * @function
  1104. */
  1105. getParams: function () {
  1106. return {
  1107. flowChunkNumber: this.offset + 1,
  1108. flowChunkSize: this.flowObj.opts.chunkSize,
  1109. flowCurrentChunkSize: this.endByte - this.startByte,
  1110. flowTotalSize: this.fileObjSize,
  1111. flowIdentifier: this.fileObj.uniqueIdentifier,
  1112. flowFilename: this.fileObj.name,
  1113. flowRelativePath: this.fileObj.relativePath,
  1114. flowTotalChunks: this.fileObj.chunks.length
  1115. };
  1116. },
  1117. /**
  1118. * Get target option with query params
  1119. * @function
  1120. * @param params
  1121. * @returns {string}
  1122. */
  1123. getTarget: function(target, params){
  1124. if(target.indexOf('?') < 0) {
  1125. target += '?';
  1126. } else {
  1127. target += '&';
  1128. }
  1129. return target + params.join('&');
  1130. },
  1131. /**
  1132. * Makes a GET request without any data to see if the chunk has already
  1133. * been uploaded in a previous session
  1134. * @function
  1135. */
  1136. test: function () {
  1137. // Set up request and listen for event
  1138. this.xhr = new XMLHttpRequest();
  1139. this.xhr.addEventListener("load", this.testHandler, false);
  1140. this.xhr.addEventListener("error", this.testHandler, false);
  1141. var testMethod = evalOpts(this.flowObj.opts.testMethod, this.fileObj, this);
  1142. var data = this.prepareXhrRequest(testMethod, true);
  1143. this.xhr.send(data);
  1144. },
  1145. /**
  1146. * Finish preprocess state
  1147. * @function
  1148. */
  1149. preprocessFinished: function () {
  1150. this.preprocessState = 2;
  1151. this.send();
  1152. },
  1153. /**
  1154. * Uploads the actual data in a POST call
  1155. * @function
  1156. */
  1157. send: function () {
  1158. var preprocess = this.flowObj.opts.preprocess;
  1159. if (typeof preprocess === 'function') {
  1160. switch (this.preprocessState) {
  1161. case 0:
  1162. this.preprocessState = 1;
  1163. preprocess(this);
  1164. return;
  1165. case 1:
  1166. return;
  1167. }
  1168. }
  1169. if (this.flowObj.opts.testChunks && !this.tested) {
  1170. this.test();
  1171. return;
  1172. }
  1173. this.loaded = 0;
  1174. this.total = 0;
  1175. this.pendingRetry = false;
  1176. var func = (this.fileObj.file.slice ? 'slice' :
  1177. (this.fileObj.file.mozSlice ? 'mozSlice' :
  1178. (this.fileObj.file.webkitSlice ? 'webkitSlice' :
  1179. 'slice')));
  1180. var bytes = this.fileObj.file[func](this.startByte, this.endByte, this.fileObj.file.type);
  1181. // Set up request and listen for event
  1182. this.xhr = new XMLHttpRequest();
  1183. this.xhr.upload.addEventListener('progress', this.progressHandler, false);
  1184. this.xhr.addEventListener("load", this.doneHandler, false);
  1185. this.xhr.addEventListener("error", this.doneHandler, false);
  1186. var uploadMethod = evalOpts(this.flowObj.opts.uploadMethod, this.fileObj, this);
  1187. var data = this.prepareXhrRequest(uploadMethod, false, this.flowObj.opts.method, bytes);
  1188. this.xhr.send(data);
  1189. },
  1190. /**
  1191. * Abort current xhr request
  1192. * @function
  1193. */
  1194. abort: function () {
  1195. // Abort and reset
  1196. var xhr = this.xhr;
  1197. this.xhr = null;
  1198. if (xhr) {
  1199. xhr.abort();
  1200. }
  1201. },
  1202. /**
  1203. * Retrieve current chunk upload status
  1204. * @function
  1205. * @returns {string} 'pending', 'uploading', 'success', 'error'
  1206. */
  1207. status: function (isTest) {
  1208. if (this.pendingRetry || this.preprocessState === 1) {
  1209. // if pending retry then that's effectively the same as actively uploading,
  1210. // there might just be a slight delay before the retry starts
  1211. return 'uploading';
  1212. } else if (!this.xhr) {
  1213. return 'pending';
  1214. } else if (this.xhr.readyState < 4) {
  1215. // Status is really 'OPENED', 'HEADERS_RECEIVED'
  1216. // or 'LOADING' - meaning that stuff is happening
  1217. return 'uploading';
  1218. } else {
  1219. if (this.flowObj.opts.successStatuses.indexOf(this.xhr.status) > -1) {
  1220. // HTTP 200, perfect
  1221. // HTTP 202 Accepted - The request has been accepted for processing, but the processing has not been completed.
  1222. return 'success';
  1223. } else if (this.flowObj.opts.permanentErrors.indexOf(this.xhr.status) > -1 ||
  1224. !isTest && this.retries >= this.flowObj.opts.maxChunkRetries) {
  1225. // HTTP 415/500/501, permanent error
  1226. return 'error';
  1227. } else {
  1228. // this should never happen, but we'll reset and queue a retry
  1229. // a likely case for this would be 503 service unavailable
  1230. this.abort();
  1231. return 'pending';
  1232. }
  1233. }
  1234. },
  1235. /**
  1236. * Get response from xhr request
  1237. * @function
  1238. * @returns {String}
  1239. */
  1240. message: function () {
  1241. return this.xhr ? this.xhr.responseText : '';
  1242. },
  1243. /**
  1244. * Get upload progress
  1245. * @function
  1246. * @returns {number}
  1247. */
  1248. progress: function () {
  1249. if (this.pendingRetry) {
  1250. return 0;
  1251. }
  1252. var s = this.status();
  1253. if (s === 'success' || s === 'error') {
  1254. return 1;
  1255. } else if (s === 'pending') {
  1256. return 0;
  1257. } else {
  1258. return this.total > 0 ? this.loaded / this.total : 0;
  1259. }
  1260. },
  1261. /**
  1262. * Count total size uploaded
  1263. * @function
  1264. * @returns {number}
  1265. */
  1266. sizeUploaded: function () {
  1267. var size = this.endByte - this.startByte;
  1268. // can't return only chunk.loaded value, because it is bigger than chunk size
  1269. if (this.status() !== 'success') {
  1270. size = this.progress() * size;
  1271. }
  1272. return size;
  1273. },
  1274. /**
  1275. * Prepare Xhr request. Set query, headers and data
  1276. * @param {string} method GET or POST
  1277. * @param {bool} isTest is this a test request
  1278. * @param {string} [paramsMethod] octet or form
  1279. * @param {Blob} [blob] to send
  1280. * @returns {FormData|Blob|Null} data to send
  1281. */
  1282. prepareXhrRequest: function(method, isTest, paramsMethod, blob) {
  1283. // Add data from the query options
  1284. var query = evalOpts(this.flowObj.opts.query, this.fileObj, this, isTest);
  1285. query = extend(this.getParams(), query);
  1286. var target = evalOpts(this.flowObj.opts.target, this.fileObj, this, isTest);
  1287. var data = null;
  1288. if (method === 'GET' || paramsMethod === 'octet') {
  1289. // Add data from the query options
  1290. var params = [];
  1291. each(query, function (v, k) {
  1292. params.push([encodeURIComponent(k), encodeURIComponent(v)].join('='));
  1293. });
  1294. target = this.getTarget(target, params);
  1295. data = blob || null;
  1296. } else {
  1297. // Add data from the query options
  1298. data = new FormData();
  1299. each(query, function (v, k) {
  1300. data.append(k, v);
  1301. });
  1302. data.append(this.flowObj.opts.fileParameterName, blob, this.fileObj.file.name);
  1303. }
  1304. this.xhr.open(method, target, true);
  1305. this.xhr.withCredentials = this.flowObj.opts.withCredentials;
  1306. // Add data from header options
  1307. each(evalOpts(this.flowObj.opts.headers, this.fileObj, this, isTest), function (v, k) {
  1308. this.xhr.setRequestHeader(k, v);
  1309. }, this);
  1310. return data;
  1311. }
  1312. };
  1313. /**
  1314. * Remove value from array
  1315. * @param array
  1316. * @param value
  1317. */
  1318. function arrayRemove(array, value) {
  1319. var index = array.indexOf(value);
  1320. if (index > -1) {
  1321. array.splice(index, 1);
  1322. }
  1323. }
  1324. /**
  1325. * If option is a function, evaluate it with given params
  1326. * @param {*} data
  1327. * @param {...} args arguments of a callback
  1328. * @returns {*}
  1329. */
  1330. function evalOpts(data, args) {
  1331. if (typeof data === "function") {
  1332. // `arguments` is an object, not array, in FF, so:
  1333. args = Array.prototype.slice.call(arguments);
  1334. data = data.apply(null, args.slice(1));
  1335. }
  1336. return data;
  1337. }
  1338. Flow.evalOpts = evalOpts;
  1339. /**
  1340. * Execute function asynchronously
  1341. * @param fn
  1342. * @param context
  1343. */
  1344. function async(fn, context) {
  1345. setTimeout(fn.bind(context), 0);
  1346. }
  1347. /**
  1348. * Extends the destination object `dst` by copying all of the properties from
  1349. * the `src` object(s) to `dst`. You can specify multiple `src` objects.
  1350. * @function
  1351. * @param {Object} dst Destination object.
  1352. * @param {...Object} src Source object(s).
  1353. * @returns {Object} Reference to `dst`.
  1354. */
  1355. function extend(dst, src) {
  1356. each(arguments, function(obj) {
  1357. if (obj !== dst) {
  1358. each(obj, function(value, key){
  1359. dst[key] = value;
  1360. });
  1361. }
  1362. });
  1363. return dst;
  1364. }
  1365. Flow.extend = extend;
  1366. /**
  1367. * Iterate each element of an object
  1368. * @function
  1369. * @param {Array|Object} obj object or an array to iterate
  1370. * @param {Function} callback first argument is a value and second is a key.
  1371. * @param {Object=} context Object to become context (`this`) for the iterator function.
  1372. */
  1373. function each(obj, callback, context) {
  1374. if (!obj) {
  1375. return ;
  1376. }
  1377. var key;
  1378. // Is Array?
  1379. if (typeof(obj.length) !== 'undefined') {
  1380. for (key = 0; key < obj.length; key++) {
  1381. if (callback.call(context, obj[key], key) === false) {
  1382. return ;
  1383. }
  1384. }
  1385. } else {
  1386. for (key in obj) {
  1387. if (obj.hasOwnProperty(key) && callback.call(context, obj[key], key) === false) {
  1388. return ;
  1389. }
  1390. }
  1391. }
  1392. }
  1393. Flow.each = each;
  1394. /**
  1395. * FlowFile constructor
  1396. * @type {FlowFile}
  1397. */
  1398. Flow.FlowFile = FlowFile;
  1399. /**
  1400. * FlowFile constructor
  1401. * @type {FlowChunk}
  1402. */
  1403. Flow.FlowChunk = FlowChunk;
  1404. /**
  1405. * Library version
  1406. * @type {string}
  1407. */
  1408. Flow.version = '2.9.0';
  1409. if ( typeof module === "object" && module && typeof module.exports === "object" ) {
  1410. // Expose Flow as module.exports in loaders that implement the Node
  1411. // module pattern (including browserify). Do not create the global, since
  1412. // the user will be storing it themselves locally, and globals are frowned
  1413. // upon in the Node module world.
  1414. module.exports = Flow;
  1415. } else {
  1416. // Otherwise expose Flow to the global object as usual
  1417. window.Flow = Flow;
  1418. // Register as a named AMD module, since Flow can be concatenated with other
  1419. // files that may use define, but not via a proper concatenation script that
  1420. // understands anonymous AMD modules. A named AMD is safest and most robust
  1421. // way to register. Lowercase flow is used because AMD module names are
  1422. // derived from file names, and Flow is normally delivered in a lowercase
  1423. // file name. Do this after creating the global so that if an AMD module wants
  1424. // to call noConflict to hide this version of Flow, it will work.
  1425. if ( typeof define === "function" && define.amd ) {
  1426. define( "flow", [], function () { return Flow; } );
  1427. }
  1428. }
  1429. })(window, document);
  1430. /**
  1431. * @description
  1432. * var app = angular.module('App', ['flow.provider'], function(flowFactoryProvider){
  1433. * flowFactoryProvider.defaults = {target: '/'};
  1434. * });
  1435. * @name flowFactoryProvider
  1436. */
  1437. angular.module('flow.provider', [])
  1438. .provider('flowFactory', function() {
  1439. 'use strict';
  1440. /**
  1441. * Define the default properties for flow.js
  1442. * @name flowFactoryProvider.defaults
  1443. * @type {Object}
  1444. */
  1445. this.defaults = {};
  1446. /**
  1447. * Flow, MaybeFlow or NotFlow
  1448. * @name flowFactoryProvider.factory
  1449. * @type {function}
  1450. * @return {Flow}
  1451. */
  1452. this.factory = function (options) {
  1453. return new Flow(options);
  1454. };
  1455. /**
  1456. * Define the default events
  1457. * @name flowFactoryProvider.events
  1458. * @type {Array}
  1459. * @private
  1460. */
  1461. this.events = [];
  1462. /**
  1463. * Add default events
  1464. * @name flowFactoryProvider.on
  1465. * @function
  1466. * @param {string} event
  1467. * @param {Function} callback
  1468. */
  1469. this.on = function (event, callback) {
  1470. this.events.push([event, callback]);
  1471. };
  1472. this.$get = function() {
  1473. var fn = this.factory;
  1474. var defaults = this.defaults;
  1475. var events = this.events;
  1476. return {
  1477. 'create': function(opts) {
  1478. // combine default options with global options and options
  1479. var flow = fn(angular.extend({}, defaults, opts));
  1480. angular.forEach(events, function (event) {
  1481. flow.on(event[0], event[1]);
  1482. });
  1483. return flow;
  1484. }
  1485. };
  1486. };
  1487. });
  1488. angular.module('flow.init', ['flow.provider'])
  1489. .controller('flowCtrl', ['$scope', '$attrs', '$parse', 'flowFactory',
  1490. function ($scope, $attrs, $parse, flowFactory) {
  1491. var options = angular.extend({}, $scope.$eval($attrs.flowInit));
  1492. // use existing flow object or create a new one
  1493. var flow = $scope.$eval($attrs.flowObject) || flowFactory.create(options);
  1494. flow.on('catchAll', function (eventName) {
  1495. var args = Array.prototype.slice.call(arguments);
  1496. args.shift();
  1497. var event = $scope.$broadcast.apply($scope, ['flow::' + eventName, flow].concat(args));
  1498. if ({
  1499. 'progress':1, 'filesSubmitted':1, 'fileSuccess': 1, 'fileError': 1, 'complete': 1
  1500. }[eventName]) {
  1501. $scope.$apply();
  1502. }
  1503. if (event.defaultPrevented) {
  1504. return false;
  1505. }
  1506. });
  1507. $scope.$flow = flow;
  1508. if ($attrs.hasOwnProperty('flowName')) {
  1509. $parse($attrs.flowName).assign($scope, flow);
  1510. $scope.$on('$destroy', function () {
  1511. $parse($attrs.flowName).assign($scope);
  1512. });
  1513. }
  1514. }])
  1515. .directive('flowInit', [function() {
  1516. return {
  1517. scope: true,
  1518. controller: 'flowCtrl'
  1519. };
  1520. }]);
  1521. angular.module('flow.btn', ['flow.init'])
  1522. .directive('flowBtn', [function() {
  1523. return {
  1524. 'restrict': 'EA',
  1525. 'scope': false,
  1526. 'require': '^flowInit',
  1527. 'link': function(scope, element, attrs) {
  1528. var isDirectory = attrs.hasOwnProperty('flowDirectory');
  1529. var isSingleFile = attrs.hasOwnProperty('flowSingleFile');
  1530. var inputAttrs = attrs.hasOwnProperty('flowAttrs') && scope.$eval(attrs.flowAttrs);
  1531. scope.$flow.assignBrowse(element, isDirectory, isSingleFile, inputAttrs);
  1532. }
  1533. };
  1534. }]);
  1535. angular.module('flow.dragEvents', ['flow.init'])
  1536. /**
  1537. * @name flowPreventDrop
  1538. * Prevent loading files then dropped on element
  1539. */
  1540. .directive('flowPreventDrop', function() {
  1541. return {
  1542. 'scope': false,
  1543. 'link': function(scope, element, attrs) {
  1544. element.bind('drop dragover', function (event) {
  1545. event.preventDefault();
  1546. });
  1547. }
  1548. };
  1549. })
  1550. /**
  1551. * @name flowDragEnter
  1552. * executes `flowDragEnter` and `flowDragLeave` events
  1553. */
  1554. .directive('flowDragEnter', ['$timeout', function($timeout) {
  1555. return {
  1556. 'scope': false,
  1557. 'link': function(scope, element, attrs) {
  1558. var promise;
  1559. var enter = false;
  1560. element.bind('dragover', function (event) {
  1561. if (!isFileDrag(event)) {
  1562. return ;
  1563. }
  1564. if (!enter) {
  1565. scope.$apply(attrs.flowDragEnter);
  1566. enter = true;
  1567. }
  1568. $timeout.cancel(promise);
  1569. event.preventDefault();
  1570. });
  1571. element.bind('dragleave drop', function (event) {
  1572. $timeout.cancel(promise);
  1573. promise = $timeout(function () {
  1574. scope.$eval(attrs.flowDragLeave);
  1575. promise = null;
  1576. enter = false;
  1577. }, 100);
  1578. });
  1579. function isFileDrag(dragEvent) {
  1580. var fileDrag = false;
  1581. var dataTransfer = dragEvent.dataTransfer || dragEvent.originalEvent.dataTransfer;
  1582. angular.forEach(dataTransfer && dataTransfer.types, function(val) {
  1583. if (val === 'Files') {
  1584. fileDrag = true;
  1585. }
  1586. });
  1587. return fileDrag;
  1588. }
  1589. }
  1590. };
  1591. }]);
  1592. angular.module('flow.drop', ['flow.init'])
  1593. .directive('flowDrop', function() {
  1594. return {
  1595. 'scope': false,
  1596. 'require': '^flowInit',
  1597. 'link': function(scope, element, attrs) {
  1598. if (attrs.flowDropEnabled) {
  1599. scope.$watch(attrs.flowDropEnabled, function (value) {
  1600. if (value) {
  1601. assignDrop();
  1602. } else {
  1603. unAssignDrop();
  1604. }
  1605. });
  1606. } else {
  1607. assignDrop();
  1608. }
  1609. function assignDrop() {
  1610. scope.$flow.assignDrop(element);
  1611. }
  1612. function unAssignDrop() {
  1613. scope.$flow.unAssignDrop(element);
  1614. }
  1615. }
  1616. };
  1617. });
  1618. !function (angular) {'use strict';
  1619. var module = angular.module('flow.events', ['flow.init']);
  1620. var events = {
  1621. fileSuccess: ['$file', '$message'],
  1622. fileProgress: ['$file'],
  1623. fileAdded: ['$file', '$event'],
  1624. filesAdded: ['$files', '$event'],
  1625. filesSubmitted: ['$files', '$event'],
  1626. fileRetry: ['$file'],
  1627. fileError: ['$file', '$message'],
  1628. uploadStart: [],
  1629. complete: [],
  1630. progress: [],
  1631. error: ['$message', '$file']
  1632. };
  1633. angular.forEach(events, function (eventArgs, eventName) {
  1634. var name = 'flow' + capitaliseFirstLetter(eventName);
  1635. if (name == 'flowUploadStart') {
  1636. name = 'flowUploadStarted';// event alias
  1637. }
  1638. module.directive(name, [function() {
  1639. return {
  1640. require: '^flowInit',
  1641. controller: ['$scope', '$attrs', function ($scope, $attrs) {
  1642. $scope.$on('flow::' + eventName, function () {
  1643. var funcArgs = Array.prototype.slice.call(arguments);
  1644. var event = funcArgs.shift();// remove angular event
  1645. // remove flow object and ignore event if it is from parent directive
  1646. if ($scope.$flow !== funcArgs.shift()) {
  1647. return ;
  1648. }
  1649. var args = {};
  1650. angular.forEach(eventArgs, function(value, key) {
  1651. args[value] = funcArgs[key];
  1652. });
  1653. if ($scope.$eval($attrs[name], args) === false) {
  1654. event.preventDefault();
  1655. }
  1656. });
  1657. }]
  1658. };
  1659. }]);
  1660. });
  1661. function capitaliseFirstLetter(string) {
  1662. return string.charAt(0).toUpperCase() + string.slice(1);
  1663. }
  1664. }(angular);
  1665. angular.module('flow.img', ['flow.init'])
  1666. .directive('flowImg', [function() {
  1667. return {
  1668. 'scope': false,
  1669. 'require': '^flowInit',
  1670. 'link': function(scope, element, attrs) {
  1671. var file = attrs.flowImg;
  1672. scope.$watch(file, function (file) {
  1673. if (!file) {
  1674. return ;
  1675. }
  1676. var fileReader = new FileReader();
  1677. fileReader.readAsDataURL(file.file);
  1678. fileReader.onload = function (event) {
  1679. scope.$apply(function () {
  1680. attrs.$set('src', event.target.result);
  1681. });
  1682. };
  1683. });
  1684. }
  1685. };
  1686. }]);
  1687. angular.module('flow.transfers', ['flow.init'])
  1688. .directive('flowTransfers', [function() {
  1689. return {
  1690. 'scope': true,
  1691. 'require': '^flowInit',
  1692. 'link': function(scope) {
  1693. scope.transfers = scope.$flow.files;
  1694. }
  1695. };
  1696. }]);
  1697. angular.module('flow', ['flow.provider', 'flow.init', 'flow.events', 'flow.btn',
  1698. 'flow.drop', 'flow.transfers', 'flow.img', 'flow.dragEvents']);