flow.js 44 KB

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