` instead.
75 |
76 |
77 | Now you need to display uploaded files, all you need to do is to loop files array.
78 | Files array is attached to flow object named `$flow`.
79 | ````html
80 |
81 | {{$index+1}}
82 | {{file.name}}
83 |
84 | ````
85 |
86 | file is instance of [FlowFile](https://github.com/flowjs/flow.js#flowfile).
87 |
88 |
89 | ### Quick setup
90 | ````html
91 |
94 |
95 |
96 | Input OR Other element as upload button
97 |
Upload File
98 |
99 |
100 |
101 | {{$index+1}}
102 | {{file.name}}
103 | {{file.msg}}
104 |
105 |
106 |
107 | ````
108 |
109 | Need more examples?
110 | ============
111 | Clone this repository and go to "ng-flow/samples/basic/index.html".
112 | Single image upload "ng-flow/samples/image/index.html".
113 |
114 |
115 | How can I drop files?
116 | ============
117 |
118 | Use `flow-drop` directive:
119 | ````html
120 |
121 | Drag And Drop your file here
122 |
123 | ````
124 | Note: in most cases `flow-drop` must be used together with `flow-prevent-drop` directive on `body`
125 | element, because it prevents file from being loaded in the browser.
126 |
127 | ### Prevent dropping files on a document
128 | Use `flow-prevent-drop` directive on `body` element:
129 | ````html
130 |
131 |
132 |
133 | ````
134 |
135 | ### How to add some styles while dropping a file?
136 | Use `flow-drag-enter` directive:
137 | ````html
138 |
140 |
141 | ````
142 | Note: `flow-drag-leave` attribute can't be used alone, it is a part of `flow-drag-enter` directive.
143 |
144 | ### How to dynamically disable drop area?
145 | ````html
146 |
147 | Drag And Drop your file here
148 |
149 | ````
150 | See example at `samples/dataurl/`.
151 |
152 |
153 | How can I preview uploaded image?
154 | ============
155 |
156 | Use flow-img directive:
157 | ````html
158 |
159 | ````
160 |
161 | Image will be automatically updated once file is added. No need to start upload.
162 |
163 |
164 | How can I set options for flow.js?
165 | ============
166 |
167 | Use config:
168 | ````javascript
169 | var app = angular.module('app', ['flow'])
170 | .config(['flowFactoryProvider', function (flowFactoryProvider) {
171 | flowFactoryProvider.defaults = {
172 | target: '/upload',
173 | permanentErrors:[404, 500, 501]
174 | };
175 | // You can also set default events:
176 | flowFactoryProvider.on('catchAll', function (event) {
177 | ...
178 | });
179 | // Can be used with different implementations of Flow.js
180 | // flowFactoryProvider.factory = fustyFlowFactory;
181 | }]);
182 | ````
183 |
184 | also can be configured on "flow-init" directive:
185 | ````html
186 |
187 |
188 |
189 | ````
190 |
191 |
192 | How can I catch events?
193 | ============
194 |
195 | Events are listed inside `flow-init` directive:
196 | ````html
197 |
213 | ````
214 |
215 | ### How can I catch an event in a controller?
216 | If controller is on the same scope as `flow-init` directive or in a child scope,
217 | then we can catch events with `$on`. Events are prefixed with `flow::`.
218 | ````javascript
219 | $scope.$on('flow::fileAdded', function (event, $flow, flowFile) {
220 | event.preventDefault();//prevent file from uploading
221 | });
222 | ````
223 | second argument is always a `flow` instance and then follows event specific arguments.
224 |
225 | How can I assign flow to a parent scope?
226 | ============
227 |
228 | Use `flow-name` attribute and set it to any variable in the scope.
229 | ````html
230 |
231 | ... Flow is set to obj.flow ...
232 | I have uploaded files: #{{obj.flow.files.length}}
233 |
234 | ````
235 | ````javascript
236 | $scope.obj = {}; // variable "obj" must be initialized on the scope
237 | ````
238 |
239 | How can I initialize flow with an existing flow object ?
240 | ============
241 |
242 | Use `flow-object` attribute and set it with the existing flow object on scope.
243 | ````html
244 |
245 | ... Flow is initialized with existingFlowObject, no new Flow object is created ...
246 | There are already {{ existingFLowObject.files.length }} files uploaded,
247 | which is equal to {{ $flow.files.length }}.
248 |
249 | ````
250 |
251 | How can I support older browsers?
252 | ============
253 | Go to https://github.com/flowjs/fusty-flow.js
254 | and add to your config:
255 | ````javascript
256 | var app = angular.module('app', ['flow'])
257 | .config(['flowFactoryProvider', function (flowFactoryProvider) {
258 | flowFactoryProvider.factory = fustyFlowFactory;
259 | }]);
260 | ````
261 |
262 | Contribution
263 | ============
264 | To ensure consistency throughout the source code, keep these rules in mind as you are working:
265 |
266 | * All features or bug fixes must be tested by one or more specs.
267 |
268 | * With the exceptions listed below, we follow the rules contained in [Google's JavaScript Style Guide](http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml):
269 |
270 | * Wrap all code at 100 characters.
271 |
272 | * Instead of complex inheritance hierarchies, we prefer simple objects. We use prototypical
273 | inheritance only when absolutely necessary.
274 |
275 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ng-flow",
3 | "version": "2.7.8",
4 | "ignore": [
5 | "**/.*",
6 | "test",
7 | "samples",
8 | "build",
9 | "*.md",
10 | "karma.conf.js",
11 | "LICENSE",
12 | "Gruntfile.js",
13 | "package.json",
14 | "bower.json"
15 | ],
16 | "main": "./dist/ng-flow.js",
17 | "dependencies": {
18 | "angular": "~1",
19 | "flow.js": "~2"
20 | },
21 | "devDependencies": {
22 | "angular-mocks": "~1"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/dist/README.md:
--------------------------------------------------------------------------------
1 | `ng-flow-standalone.js` - Flow.js + ng-flow, everything that you need is in this one file.
2 |
3 | `ng-flow.js` - ng-flow only, you have to include flow.js yourself.
--------------------------------------------------------------------------------
/dist/ng-flow-standalone.js:
--------------------------------------------------------------------------------
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.} [opts.permanentErrors]
31 | * @param {Array.} [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 |
52 | if (!this.support) {
53 | return ;
54 | }
55 |
56 | /**
57 | * Check if directory upload is supported
58 | * @type {boolean}
59 | */
60 | this.supportDirectory = (
61 | /Chrome/.test(window.navigator.userAgent) ||
62 | /Firefox/.test(window.navigator.userAgent) ||
63 | /Edge/.test(window.navigator.userAgent)
64 | );
65 |
66 | /**
67 | * List of FlowFile objects
68 | * @type {Array.}
69 | */
70 | this.files = [];
71 |
72 | /**
73 | * Default options for flow.js
74 | * @type {Object}
75 | */
76 | this.defaults = {
77 | chunkSize: 1024 * 1024,
78 | forceChunkSize: false,
79 | simultaneousUploads: 3,
80 | singleFile: false,
81 | fileParameterName: 'file',
82 | progressCallbacksInterval: 500,
83 | speedSmoothingFactor: 0.1,
84 | query: {},
85 | headers: {},
86 | withCredentials: false,
87 | preprocess: null,
88 | method: 'multipart',
89 | testMethod: 'GET',
90 | uploadMethod: 'POST',
91 | prioritizeFirstAndLastChunk: false,
92 | allowDuplicateUploads: false,
93 | target: '/',
94 | testChunks: true,
95 | generateUniqueIdentifier: null,
96 | maxChunkRetries: 0,
97 | chunkRetryInterval: null,
98 | permanentErrors: [404, 413, 415, 500, 501],
99 | successStatuses: [200, 201, 202],
100 | onDropStopPropagation: false,
101 | initFileFn: null,
102 | readFileFn: webAPIFileRead
103 | };
104 |
105 | /**
106 | * Current options
107 | * @type {Object}
108 | */
109 | this.opts = {};
110 |
111 | /**
112 | * List of events:
113 | * key stands for event name
114 | * value array list of callbacks
115 | * @type {}
116 | */
117 | this.events = {};
118 |
119 | var $ = this;
120 |
121 | /**
122 | * On drop event
123 | * @function
124 | * @param {MouseEvent} event
125 | */
126 | this.onDrop = function (event) {
127 | if ($.opts.onDropStopPropagation) {
128 | event.stopPropagation();
129 | }
130 | event.preventDefault();
131 | var dataTransfer = event.dataTransfer;
132 | if (dataTransfer.items && dataTransfer.items[0] &&
133 | dataTransfer.items[0].webkitGetAsEntry) {
134 | $.webkitReadDataTransfer(event);
135 | } else {
136 | $.addFiles(dataTransfer.files, event);
137 | }
138 | };
139 |
140 | /**
141 | * Prevent default
142 | * @function
143 | * @param {MouseEvent} event
144 | */
145 | this.preventEvent = function (event) {
146 | event.preventDefault();
147 | };
148 |
149 |
150 | /**
151 | * Current options
152 | * @type {Object}
153 | */
154 | this.opts = Flow.extend({}, this.defaults, opts || {});
155 |
156 | }
157 |
158 | Flow.prototype = {
159 | /**
160 | * Set a callback for an event, possible events:
161 | * fileSuccess(file), fileProgress(file), fileAdded(file, event),
162 | * fileRemoved(file), fileRetry(file), fileError(file, message),
163 | * complete(), progress(), error(message, file), pause()
164 | * @function
165 | * @param {string} event
166 | * @param {Function} callback
167 | */
168 | on: function (event, callback) {
169 | event = event.toLowerCase();
170 | if (!this.events.hasOwnProperty(event)) {
171 | this.events[event] = [];
172 | }
173 | this.events[event].push(callback);
174 | },
175 |
176 | /**
177 | * Remove event callback
178 | * @function
179 | * @param {string} [event] removes all events if not specified
180 | * @param {Function} [fn] removes all callbacks of event if not specified
181 | */
182 | off: function (event, fn) {
183 | if (event !== undefined) {
184 | event = event.toLowerCase();
185 | if (fn !== undefined) {
186 | if (this.events.hasOwnProperty(event)) {
187 | arrayRemove(this.events[event], fn);
188 | }
189 | } else {
190 | delete this.events[event];
191 | }
192 | } else {
193 | this.events = {};
194 | }
195 | },
196 |
197 | /**
198 | * Fire an event
199 | * @function
200 | * @param {string} event event name
201 | * @param {...} args arguments of a callback
202 | * @return {bool} value is false if at least one of the event handlers which handled this event
203 | * returned false. Otherwise it returns true.
204 | */
205 | fire: function (event, args) {
206 | // `arguments` is an object, not array, in FF, so:
207 | args = Array.prototype.slice.call(arguments);
208 | event = event.toLowerCase();
209 | var preventDefault = false;
210 | if (this.events.hasOwnProperty(event)) {
211 | each(this.events[event], function (callback) {
212 | preventDefault = callback.apply(this, args.slice(1)) === false || preventDefault;
213 | }, this);
214 | }
215 | if (event != 'catchall') {
216 | args.unshift('catchAll');
217 | preventDefault = this.fire.apply(this, args) === false || preventDefault;
218 | }
219 | return !preventDefault;
220 | },
221 |
222 | /**
223 | * Read webkit dataTransfer object
224 | * @param event
225 | */
226 | webkitReadDataTransfer: function (event) {
227 | var $ = this;
228 | var queue = event.dataTransfer.items.length;
229 | var files = [];
230 | each(event.dataTransfer.items, function (item) {
231 | var entry = item.webkitGetAsEntry();
232 | if (!entry) {
233 | decrement();
234 | return ;
235 | }
236 | if (entry.isFile) {
237 | // due to a bug in Chrome's File System API impl - #149735
238 | fileReadSuccess(item.getAsFile(), entry.fullPath);
239 | } else {
240 | readDirectory(entry.createReader());
241 | }
242 | });
243 | function readDirectory(reader) {
244 | reader.readEntries(function (entries) {
245 | if (entries.length) {
246 | queue += entries.length;
247 | each(entries, function(entry) {
248 | if (entry.isFile) {
249 | var fullPath = entry.fullPath;
250 | entry.file(function (file) {
251 | fileReadSuccess(file, fullPath);
252 | }, readError);
253 | } else if (entry.isDirectory) {
254 | readDirectory(entry.createReader());
255 | }
256 | });
257 | readDirectory(reader);
258 | } else {
259 | decrement();
260 | }
261 | }, readError);
262 | }
263 | function fileReadSuccess(file, fullPath) {
264 | // relative path should not start with "/"
265 | file.relativePath = fullPath.substring(1);
266 | files.push(file);
267 | decrement();
268 | }
269 | function readError(fileError) {
270 | throw fileError;
271 | }
272 | function decrement() {
273 | if (--queue == 0) {
274 | $.addFiles(files, event);
275 | }
276 | }
277 | },
278 |
279 | /**
280 | * Generate unique identifier for a file
281 | * @function
282 | * @param {FlowFile} file
283 | * @returns {string}
284 | */
285 | generateUniqueIdentifier: function (file) {
286 | var custom = this.opts.generateUniqueIdentifier;
287 | if (typeof custom === 'function') {
288 | return custom(file);
289 | }
290 | // Some confusion in different versions of Firefox
291 | var relativePath = file.relativePath || file.webkitRelativePath || file.fileName || file.name;
292 | return file.size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, '');
293 | },
294 |
295 | /**
296 | * Upload next chunk from the queue
297 | * @function
298 | * @returns {boolean}
299 | * @private
300 | */
301 | uploadNextChunk: function (preventEvents) {
302 | // In some cases (such as videos) it's really handy to upload the first
303 | // and last chunk of a file quickly; this let's the server check the file's
304 | // metadata and determine if there's even a point in continuing.
305 | var found = false;
306 | if (this.opts.prioritizeFirstAndLastChunk) {
307 | each(this.files, function (file) {
308 | if (!file.paused && file.chunks.length &&
309 | file.chunks[0].status() === 'pending') {
310 | file.chunks[0].send();
311 | found = true;
312 | return false;
313 | }
314 | if (!file.paused && file.chunks.length > 1 &&
315 | file.chunks[file.chunks.length - 1].status() === 'pending') {
316 | file.chunks[file.chunks.length - 1].send();
317 | found = true;
318 | return false;
319 | }
320 | });
321 | if (found) {
322 | return found;
323 | }
324 | }
325 |
326 | // Now, simply look for the next, best thing to upload
327 | each(this.files, function (file) {
328 | if (!file.paused) {
329 | each(file.chunks, function (chunk) {
330 | if (chunk.status() === 'pending') {
331 | chunk.send();
332 | found = true;
333 | return false;
334 | }
335 | });
336 | }
337 | if (found) {
338 | return false;
339 | }
340 | });
341 | if (found) {
342 | return true;
343 | }
344 |
345 | // The are no more outstanding chunks to upload, check is everything is done
346 | var outstanding = false;
347 | each(this.files, function (file) {
348 | if (!file.isComplete()) {
349 | outstanding = true;
350 | return false;
351 | }
352 | });
353 | if (!outstanding && !preventEvents) {
354 | // All chunks have been uploaded, complete
355 | async(function () {
356 | this.fire('complete');
357 | }, this);
358 | }
359 | return false;
360 | },
361 |
362 |
363 | /**
364 | * Assign a browse action to one or more DOM nodes.
365 | * @function
366 | * @param {Element|Array.} domNodes
367 | * @param {boolean} isDirectory Pass in true to allow directories to
368 | * @param {boolean} singleFile prevent multi file upload
369 | * @param {Object} attributes set custom attributes:
370 | * http://www.w3.org/TR/html-markup/input.file.html#input.file-attributes
371 | * eg: accept: 'image/*'
372 | * be selected (Chrome only).
373 | */
374 | assignBrowse: function (domNodes, isDirectory, singleFile, attributes) {
375 | if (domNodes instanceof Element) {
376 | domNodes = [domNodes];
377 | }
378 |
379 | each(domNodes, function (domNode) {
380 | var input;
381 | if (domNode.tagName === 'INPUT' && domNode.type === 'file') {
382 | input = domNode;
383 | } else {
384 | input = document.createElement('input');
385 | input.setAttribute('type', 'file');
386 | // display:none - not working in opera 12
387 | extend(input.style, {
388 | visibility: 'hidden',
389 | position: 'absolute',
390 | width: '1px',
391 | height: '1px'
392 | });
393 | // for opera 12 browser, input must be assigned to a document
394 | domNode.appendChild(input);
395 | // https://developer.mozilla.org/en/using_files_from_web_applications)
396 | // event listener is executed two times
397 | // first one - original mouse click event
398 | // second - input.click(), input is inside domNode
399 | domNode.addEventListener('click', function() {
400 | input.click();
401 | }, false);
402 | }
403 | if (!this.opts.singleFile && !singleFile) {
404 | input.setAttribute('multiple', 'multiple');
405 | }
406 | if (isDirectory) {
407 | input.setAttribute('webkitdirectory', 'webkitdirectory');
408 | }
409 | each(attributes, function (value, key) {
410 | input.setAttribute(key, value);
411 | });
412 | // When new files are added, simply append them to the overall list
413 | var $ = this;
414 | input.addEventListener('change', function (e) {
415 | if (e.target.value) {
416 | $.addFiles(e.target.files, e);
417 | e.target.value = '';
418 | }
419 | }, false);
420 | }, this);
421 | },
422 |
423 | /**
424 | * Assign one or more DOM nodes as a drop target.
425 | * @function
426 | * @param {Element|Array.} domNodes
427 | */
428 | assignDrop: function (domNodes) {
429 | if (typeof domNodes.length === 'undefined') {
430 | domNodes = [domNodes];
431 | }
432 | each(domNodes, function (domNode) {
433 | domNode.addEventListener('dragover', this.preventEvent, false);
434 | domNode.addEventListener('dragenter', this.preventEvent, false);
435 | domNode.addEventListener('drop', this.onDrop, false);
436 | }, this);
437 | },
438 |
439 | /**
440 | * Un-assign drop event from DOM nodes
441 | * @function
442 | * @param domNodes
443 | */
444 | unAssignDrop: function (domNodes) {
445 | if (typeof domNodes.length === 'undefined') {
446 | domNodes = [domNodes];
447 | }
448 | each(domNodes, function (domNode) {
449 | domNode.removeEventListener('dragover', this.preventEvent);
450 | domNode.removeEventListener('dragenter', this.preventEvent);
451 | domNode.removeEventListener('drop', this.onDrop);
452 | }, this);
453 | },
454 |
455 | /**
456 | * Returns a boolean indicating whether or not the instance is currently
457 | * uploading anything.
458 | * @function
459 | * @returns {boolean}
460 | */
461 | isUploading: function () {
462 | var uploading = false;
463 | each(this.files, function (file) {
464 | if (file.isUploading()) {
465 | uploading = true;
466 | return false;
467 | }
468 | });
469 | return uploading;
470 | },
471 |
472 | /**
473 | * should upload next chunk
474 | * @function
475 | * @returns {boolean|number}
476 | */
477 | _shouldUploadNext: function () {
478 | var num = 0;
479 | var should = true;
480 | var simultaneousUploads = this.opts.simultaneousUploads;
481 | each(this.files, function (file) {
482 | each(file.chunks, function(chunk) {
483 | if (chunk.status() === 'uploading') {
484 | num++;
485 | if (num >= simultaneousUploads) {
486 | should = false;
487 | return false;
488 | }
489 | }
490 | });
491 | });
492 | // if should is true then return uploading chunks's length
493 | return should && num;
494 | },
495 |
496 | /**
497 | * Start or resume uploading.
498 | * @function
499 | */
500 | upload: function () {
501 | // Make sure we don't start too many uploads at once
502 | var ret = this._shouldUploadNext();
503 | if (ret === false) {
504 | return;
505 | }
506 | // Kick off the queue
507 | this.fire('uploadStart');
508 | var started = false;
509 | for (var num = 1; num <= this.opts.simultaneousUploads - ret; num++) {
510 | started = this.uploadNextChunk(true) || started;
511 | }
512 | if (!started) {
513 | async(function () {
514 | this.fire('complete');
515 | }, this);
516 | }
517 | },
518 |
519 | /**
520 | * Resume uploading.
521 | * @function
522 | */
523 | resume: function () {
524 | each(this.files, function (file) {
525 | if (!file.isComplete()) {
526 | file.resume();
527 | }
528 | });
529 | },
530 |
531 | /**
532 | * Pause uploading.
533 | * @function
534 | */
535 | pause: function () {
536 | each(this.files, function (file) {
537 | file.pause();
538 | });
539 | },
540 |
541 | /**
542 | * Cancel upload of all FlowFile objects and remove them from the list.
543 | * @function
544 | */
545 | cancel: function () {
546 | for (var i = this.files.length - 1; i >= 0; i--) {
547 | this.files[i].cancel();
548 | }
549 | },
550 |
551 | /**
552 | * Returns a number between 0 and 1 indicating the current upload progress
553 | * of all files.
554 | * @function
555 | * @returns {number}
556 | */
557 | progress: function () {
558 | var totalDone = 0;
559 | var totalSize = 0;
560 | // Resume all chunks currently being uploaded
561 | each(this.files, function (file) {
562 | totalDone += file.progress() * file.size;
563 | totalSize += file.size;
564 | });
565 | return totalSize > 0 ? totalDone / totalSize : 0;
566 | },
567 |
568 | /**
569 | * Add a HTML5 File object to the list of files.
570 | * @function
571 | * @param {File} file
572 | * @param {Event} [event] event is optional
573 | */
574 | addFile: function (file, event) {
575 | this.addFiles([file], event);
576 | },
577 |
578 | /**
579 | * Add a HTML5 File object to the list of files.
580 | * @function
581 | * @param {FileList|Array} fileList
582 | * @param {Event} [event] event is optional
583 | */
584 | addFiles: function (fileList, event) {
585 | var files = [];
586 | each(fileList, function (file) {
587 | // https://github.com/flowjs/flow.js/issues/55
588 | if ((!ie10plus || ie10plus && file.size > 0) && !(file.size % 4096 === 0 && (file.name === '.' || file.fileName === '.'))) {
589 | var uniqueIdentifier = this.generateUniqueIdentifier(file);
590 | if (this.opts.allowDuplicateUploads || !this.getFromUniqueIdentifier(uniqueIdentifier)) {
591 | var f = new FlowFile(this, file, uniqueIdentifier);
592 | if (this.fire('fileAdded', f, event)) {
593 | files.push(f);
594 | }
595 | }
596 | }
597 | }, this);
598 | if (this.fire('filesAdded', files, event)) {
599 | each(files, function (file) {
600 | if (this.opts.singleFile && this.files.length > 0) {
601 | this.removeFile(this.files[0]);
602 | }
603 | this.files.push(file);
604 | }, this);
605 | this.fire('filesSubmitted', files, event);
606 | }
607 | },
608 |
609 |
610 | /**
611 | * Cancel upload of a specific FlowFile object from the list.
612 | * @function
613 | * @param {FlowFile} file
614 | */
615 | removeFile: function (file) {
616 | for (var i = this.files.length - 1; i >= 0; i--) {
617 | if (this.files[i] === file) {
618 | this.files.splice(i, 1);
619 | file.abort();
620 | this.fire('fileRemoved', file);
621 | }
622 | }
623 | },
624 |
625 | /**
626 | * Look up a FlowFile object by its unique identifier.
627 | * @function
628 | * @param {string} uniqueIdentifier
629 | * @returns {boolean|FlowFile} false if file was not found
630 | */
631 | getFromUniqueIdentifier: function (uniqueIdentifier) {
632 | var ret = false;
633 | each(this.files, function (file) {
634 | if (file.uniqueIdentifier === uniqueIdentifier) {
635 | ret = file;
636 | }
637 | });
638 | return ret;
639 | },
640 |
641 | /**
642 | * Returns the total size of all files in bytes.
643 | * @function
644 | * @returns {number}
645 | */
646 | getSize: function () {
647 | var totalSize = 0;
648 | each(this.files, function (file) {
649 | totalSize += file.size;
650 | });
651 | return totalSize;
652 | },
653 |
654 | /**
655 | * Returns the total size uploaded of all files in bytes.
656 | * @function
657 | * @returns {number}
658 | */
659 | sizeUploaded: function () {
660 | var size = 0;
661 | each(this.files, function (file) {
662 | size += file.sizeUploaded();
663 | });
664 | return size;
665 | },
666 |
667 | /**
668 | * Returns remaining time to upload all files in seconds. Accuracy is based on average speed.
669 | * If speed is zero, time remaining will be equal to positive infinity `Number.POSITIVE_INFINITY`
670 | * @function
671 | * @returns {number}
672 | */
673 | timeRemaining: function () {
674 | var sizeDelta = 0;
675 | var averageSpeed = 0;
676 | each(this.files, function (file) {
677 | if (!file.paused && !file.error) {
678 | sizeDelta += file.size - file.sizeUploaded();
679 | averageSpeed += file.averageSpeed;
680 | }
681 | });
682 | if (sizeDelta && !averageSpeed) {
683 | return Number.POSITIVE_INFINITY;
684 | }
685 | if (!sizeDelta && !averageSpeed) {
686 | return 0;
687 | }
688 | return Math.floor(sizeDelta / averageSpeed);
689 | }
690 | };
691 |
692 |
693 |
694 |
695 |
696 |
697 | /**
698 | * FlowFile class
699 | * @name FlowFile
700 | * @param {Flow} flowObj
701 | * @param {File} file
702 | * @param {string} uniqueIdentifier
703 | * @constructor
704 | */
705 | function FlowFile(flowObj, file, uniqueIdentifier) {
706 |
707 | /**
708 | * Reference to parent Flow instance
709 | * @type {Flow}
710 | */
711 | this.flowObj = flowObj;
712 |
713 | /**
714 | * Used to store the bytes read
715 | * @type {Blob|string}
716 | */
717 | this.bytes = null;
718 |
719 | /**
720 | * Reference to file
721 | * @type {File}
722 | */
723 | this.file = file;
724 |
725 | /**
726 | * File name. Some confusion in different versions of Firefox
727 | * @type {string}
728 | */
729 | this.name = file.fileName || file.name;
730 |
731 | /**
732 | * File size
733 | * @type {number}
734 | */
735 | this.size = file.size;
736 |
737 | /**
738 | * Relative file path
739 | * @type {string}
740 | */
741 | this.relativePath = file.relativePath || file.webkitRelativePath || this.name;
742 |
743 | /**
744 | * File unique identifier
745 | * @type {string}
746 | */
747 | this.uniqueIdentifier = (uniqueIdentifier === undefined ? flowObj.generateUniqueIdentifier(file) : uniqueIdentifier);
748 |
749 | /**
750 | * List of chunks
751 | * @type {Array.}
752 | */
753 | this.chunks = [];
754 |
755 | /**
756 | * Indicated if file is paused
757 | * @type {boolean}
758 | */
759 | this.paused = false;
760 |
761 | /**
762 | * Indicated if file has encountered an error
763 | * @type {boolean}
764 | */
765 | this.error = false;
766 |
767 | /**
768 | * Average upload speed
769 | * @type {number}
770 | */
771 | this.averageSpeed = 0;
772 |
773 | /**
774 | * Current upload speed
775 | * @type {number}
776 | */
777 | this.currentSpeed = 0;
778 |
779 | /**
780 | * Date then progress was called last time
781 | * @type {number}
782 | * @private
783 | */
784 | this._lastProgressCallback = Date.now();
785 |
786 | /**
787 | * Previously uploaded file size
788 | * @type {number}
789 | * @private
790 | */
791 | this._prevUploadedSize = 0;
792 |
793 | /**
794 | * Holds previous progress
795 | * @type {number}
796 | * @private
797 | */
798 | this._prevProgress = 0;
799 |
800 | this.bootstrap();
801 | }
802 |
803 | FlowFile.prototype = {
804 | /**
805 | * Update speed parameters
806 | * @link http://stackoverflow.com/questions/2779600/how-to-estimate-download-time-remaining-accurately
807 | * @function
808 | */
809 | measureSpeed: function () {
810 | var timeSpan = Date.now() - this._lastProgressCallback;
811 | if (!timeSpan) {
812 | return ;
813 | }
814 | var smoothingFactor = this.flowObj.opts.speedSmoothingFactor;
815 | var uploaded = this.sizeUploaded();
816 | // Prevent negative upload speed after file upload resume
817 | this.currentSpeed = Math.max((uploaded - this._prevUploadedSize) / timeSpan * 1000, 0);
818 | this.averageSpeed = smoothingFactor * this.currentSpeed + (1 - smoothingFactor) * this.averageSpeed;
819 | this._prevUploadedSize = uploaded;
820 | },
821 |
822 | /**
823 | * For internal usage only.
824 | * Callback when something happens within the chunk.
825 | * @function
826 | * @param {FlowChunk} chunk
827 | * @param {string} event can be 'progress', 'success', 'error' or 'retry'
828 | * @param {string} [message]
829 | */
830 | chunkEvent: function (chunk, event, message) {
831 | switch (event) {
832 | case 'progress':
833 | if (Date.now() - this._lastProgressCallback <
834 | this.flowObj.opts.progressCallbacksInterval) {
835 | break;
836 | }
837 | this.measureSpeed();
838 | this.flowObj.fire('fileProgress', this, chunk);
839 | this.flowObj.fire('progress');
840 | this._lastProgressCallback = Date.now();
841 | break;
842 | case 'error':
843 | this.error = true;
844 | this.abort(true);
845 | this.flowObj.fire('fileError', this, message, chunk);
846 | this.flowObj.fire('error', message, this, chunk);
847 | break;
848 | case 'success':
849 | if (this.error) {
850 | return;
851 | }
852 | this.measureSpeed();
853 | this.flowObj.fire('fileProgress', this, chunk);
854 | this.flowObj.fire('progress');
855 | this._lastProgressCallback = Date.now();
856 | if (this.isComplete()) {
857 | this.currentSpeed = 0;
858 | this.averageSpeed = 0;
859 | this.flowObj.fire('fileSuccess', this, message, chunk);
860 | }
861 | break;
862 | case 'retry':
863 | this.flowObj.fire('fileRetry', this, chunk);
864 | break;
865 | }
866 | },
867 |
868 | /**
869 | * Pause file upload
870 | * @function
871 | */
872 | pause: function() {
873 | this.paused = true;
874 | this.abort();
875 | },
876 |
877 | /**
878 | * Resume file upload
879 | * @function
880 | */
881 | resume: function() {
882 | this.paused = false;
883 | this.flowObj.upload();
884 | },
885 |
886 | /**
887 | * Abort current upload
888 | * @function
889 | */
890 | abort: function (reset) {
891 | this.currentSpeed = 0;
892 | this.averageSpeed = 0;
893 | var chunks = this.chunks;
894 | if (reset) {
895 | this.chunks = [];
896 | }
897 | each(chunks, function (c) {
898 | if (c.status() === 'uploading') {
899 | c.abort();
900 | this.flowObj.uploadNextChunk();
901 | }
902 | }, this);
903 | },
904 |
905 | /**
906 | * Cancel current upload and remove from a list
907 | * @function
908 | */
909 | cancel: function () {
910 | this.flowObj.removeFile(this);
911 | },
912 |
913 | /**
914 | * Retry aborted file upload
915 | * @function
916 | */
917 | retry: function () {
918 | this.bootstrap();
919 | this.flowObj.upload();
920 | },
921 |
922 | /**
923 | * Clear current chunks and slice file again
924 | * @function
925 | */
926 | bootstrap: function () {
927 | if (typeof this.flowObj.opts.initFileFn === "function") {
928 | this.flowObj.opts.initFileFn(this);
929 | }
930 |
931 | this.abort(true);
932 | this.error = false;
933 | // Rebuild stack of chunks from file
934 | this._prevProgress = 0;
935 | var round = this.flowObj.opts.forceChunkSize ? Math.ceil : Math.floor;
936 | var chunks = Math.max(
937 | round(this.size / this.flowObj.opts.chunkSize), 1
938 | );
939 | for (var offset = 0; offset < chunks; offset++) {
940 | this.chunks.push(
941 | new FlowChunk(this.flowObj, this, offset)
942 | );
943 | }
944 | },
945 |
946 | /**
947 | * Get current upload progress status
948 | * @function
949 | * @returns {number} from 0 to 1
950 | */
951 | progress: function () {
952 | if (this.error) {
953 | return 1;
954 | }
955 | if (this.chunks.length === 1) {
956 | this._prevProgress = Math.max(this._prevProgress, this.chunks[0].progress());
957 | return this._prevProgress;
958 | }
959 | // Sum up progress across everything
960 | var bytesLoaded = 0;
961 | each(this.chunks, function (c) {
962 | // get chunk progress relative to entire file
963 | bytesLoaded += c.progress() * (c.endByte - c.startByte);
964 | });
965 | var percent = bytesLoaded / this.size;
966 | // We don't want to lose percentages when an upload is paused
967 | this._prevProgress = Math.max(this._prevProgress, percent > 0.9999 ? 1 : percent);
968 | return this._prevProgress;
969 | },
970 |
971 | /**
972 | * Indicates if file is being uploaded at the moment
973 | * @function
974 | * @returns {boolean}
975 | */
976 | isUploading: function () {
977 | var uploading = false;
978 | each(this.chunks, function (chunk) {
979 | if (chunk.status() === 'uploading') {
980 | uploading = true;
981 | return false;
982 | }
983 | });
984 | return uploading;
985 | },
986 |
987 | /**
988 | * Indicates if file is has finished uploading and received a response
989 | * @function
990 | * @returns {boolean}
991 | */
992 | isComplete: function () {
993 | var outstanding = false;
994 | each(this.chunks, function (chunk) {
995 | var status = chunk.status();
996 | if (status === 'pending' || status === 'uploading' || status === 'reading' || chunk.preprocessState === 1 || chunk.readState === 1) {
997 | outstanding = true;
998 | return false;
999 | }
1000 | });
1001 | return !outstanding;
1002 | },
1003 |
1004 | /**
1005 | * Count total size uploaded
1006 | * @function
1007 | * @returns {number}
1008 | */
1009 | sizeUploaded: function () {
1010 | var size = 0;
1011 | each(this.chunks, function (chunk) {
1012 | size += chunk.sizeUploaded();
1013 | });
1014 | return size;
1015 | },
1016 |
1017 | /**
1018 | * Returns remaining time to finish upload file in seconds. Accuracy is based on average speed.
1019 | * If speed is zero, time remaining will be equal to positive infinity `Number.POSITIVE_INFINITY`
1020 | * @function
1021 | * @returns {number}
1022 | */
1023 | timeRemaining: function () {
1024 | if (this.paused || this.error) {
1025 | return 0;
1026 | }
1027 | var delta = this.size - this.sizeUploaded();
1028 | if (delta && !this.averageSpeed) {
1029 | return Number.POSITIVE_INFINITY;
1030 | }
1031 | if (!delta && !this.averageSpeed) {
1032 | return 0;
1033 | }
1034 | return Math.floor(delta / this.averageSpeed);
1035 | },
1036 |
1037 | /**
1038 | * Get file type
1039 | * @function
1040 | * @returns {string}
1041 | */
1042 | getType: function () {
1043 | return this.file.type && this.file.type.split('/')[1];
1044 | },
1045 |
1046 | /**
1047 | * Get file extension
1048 | * @function
1049 | * @returns {string}
1050 | */
1051 | getExtension: function () {
1052 | return this.name.substr((~-this.name.lastIndexOf(".") >>> 0) + 2).toLowerCase();
1053 | }
1054 | };
1055 |
1056 | /**
1057 | * Default read function using the webAPI
1058 | *
1059 | * @function webAPIFileRead(fileObj, startByte, endByte, fileType, chunk)
1060 | *
1061 | */
1062 | function webAPIFileRead(fileObj, startByte, endByte, fileType, chunk) {
1063 | var function_name = 'slice';
1064 |
1065 | if (fileObj.file.slice)
1066 | function_name = 'slice';
1067 | else if (fileObj.file.mozSlice)
1068 | function_name = 'mozSlice';
1069 | else if (fileObj.file.webkitSlice)
1070 | function_name = 'webkitSlice';
1071 |
1072 | chunk.readFinished(fileObj.file[function_name](startByte, endByte, fileType));
1073 | }
1074 |
1075 |
1076 | /**
1077 | * Class for storing a single chunk
1078 | * @name FlowChunk
1079 | * @param {Flow} flowObj
1080 | * @param {FlowFile} fileObj
1081 | * @param {number} offset
1082 | * @constructor
1083 | */
1084 | function FlowChunk(flowObj, fileObj, offset) {
1085 |
1086 | /**
1087 | * Reference to parent flow object
1088 | * @type {Flow}
1089 | */
1090 | this.flowObj = flowObj;
1091 |
1092 | /**
1093 | * Reference to parent FlowFile object
1094 | * @type {FlowFile}
1095 | */
1096 | this.fileObj = fileObj;
1097 |
1098 | /**
1099 | * File offset
1100 | * @type {number}
1101 | */
1102 | this.offset = offset;
1103 |
1104 | /**
1105 | * Indicates if chunk existence was checked on the server
1106 | * @type {boolean}
1107 | */
1108 | this.tested = false;
1109 |
1110 | /**
1111 | * Number of retries performed
1112 | * @type {number}
1113 | */
1114 | this.retries = 0;
1115 |
1116 | /**
1117 | * Pending retry
1118 | * @type {boolean}
1119 | */
1120 | this.pendingRetry = false;
1121 |
1122 | /**
1123 | * Preprocess state
1124 | * @type {number} 0 = unprocessed, 1 = processing, 2 = finished
1125 | */
1126 | this.preprocessState = 0;
1127 |
1128 | /**
1129 | * Read state
1130 | * @type {number} 0 = not read, 1 = reading, 2 = finished
1131 | */
1132 | this.readState = 0;
1133 |
1134 |
1135 | /**
1136 | * Bytes transferred from total request size
1137 | * @type {number}
1138 | */
1139 | this.loaded = 0;
1140 |
1141 | /**
1142 | * Total request size
1143 | * @type {number}
1144 | */
1145 | this.total = 0;
1146 |
1147 | /**
1148 | * Size of a chunk
1149 | * @type {number}
1150 | */
1151 | this.chunkSize = this.flowObj.opts.chunkSize;
1152 |
1153 | /**
1154 | * Chunk start byte in a file
1155 | * @type {number}
1156 | */
1157 | this.startByte = this.offset * this.chunkSize;
1158 |
1159 | /**
1160 | * Compute the endbyte in a file
1161 | *
1162 | */
1163 | this.computeEndByte = function() {
1164 | var endByte = Math.min(this.fileObj.size, (this.offset + 1) * this.chunkSize);
1165 | if (this.fileObj.size - endByte < this.chunkSize && !this.flowObj.opts.forceChunkSize) {
1166 | // The last chunk will be bigger than the chunk size,
1167 | // but less than 2 * this.chunkSize
1168 | endByte = this.fileObj.size;
1169 | }
1170 | return endByte;
1171 | }
1172 |
1173 | /**
1174 | * Chunk end byte in a file
1175 | * @type {number}
1176 | */
1177 | this.endByte = this.computeEndByte();
1178 |
1179 | /**
1180 | * XMLHttpRequest
1181 | * @type {XMLHttpRequest}
1182 | */
1183 | this.xhr = null;
1184 |
1185 | var $ = this;
1186 |
1187 | /**
1188 | * Send chunk event
1189 | * @param event
1190 | * @param {...} args arguments of a callback
1191 | */
1192 | this.event = function (event, args) {
1193 | args = Array.prototype.slice.call(arguments);
1194 | args.unshift($);
1195 | $.fileObj.chunkEvent.apply($.fileObj, args);
1196 | };
1197 | /**
1198 | * Catch progress event
1199 | * @param {ProgressEvent} event
1200 | */
1201 | this.progressHandler = function(event) {
1202 | if (event.lengthComputable) {
1203 | $.loaded = event.loaded ;
1204 | $.total = event.total;
1205 | }
1206 | $.event('progress', event);
1207 | };
1208 |
1209 | /**
1210 | * Catch test event
1211 | * @param {Event} event
1212 | */
1213 | this.testHandler = function(event) {
1214 | var status = $.status(true);
1215 | if (status === 'error') {
1216 | $.event(status, $.message());
1217 | $.flowObj.uploadNextChunk();
1218 | } else if (status === 'success') {
1219 | $.tested = true;
1220 | $.event(status, $.message());
1221 | $.flowObj.uploadNextChunk();
1222 | } else if (!$.fileObj.paused) {
1223 | // Error might be caused by file pause method
1224 | // Chunks does not exist on the server side
1225 | $.tested = true;
1226 | $.send();
1227 | }
1228 | };
1229 |
1230 | /**
1231 | * Upload has stopped
1232 | * @param {Event} event
1233 | */
1234 | this.doneHandler = function(event) {
1235 | var status = $.status();
1236 | if (status === 'success' || status === 'error') {
1237 | delete this.data;
1238 | $.event(status, $.message());
1239 | $.flowObj.uploadNextChunk();
1240 | } else {
1241 | $.event('retry', $.message());
1242 | $.pendingRetry = true;
1243 | $.abort();
1244 | $.retries++;
1245 | var retryInterval = $.flowObj.opts.chunkRetryInterval;
1246 | if (retryInterval !== null) {
1247 | setTimeout(function () {
1248 | $.send();
1249 | }, retryInterval);
1250 | } else {
1251 | $.send();
1252 | }
1253 | }
1254 | };
1255 | }
1256 |
1257 | FlowChunk.prototype = {
1258 | /**
1259 | * Get params for a request
1260 | * @function
1261 | */
1262 | getParams: function () {
1263 | return {
1264 | flowChunkNumber: this.offset + 1,
1265 | flowChunkSize: this.flowObj.opts.chunkSize,
1266 | flowCurrentChunkSize: this.endByte - this.startByte,
1267 | flowTotalSize: this.fileObj.size,
1268 | flowIdentifier: this.fileObj.uniqueIdentifier,
1269 | flowFilename: this.fileObj.name,
1270 | flowRelativePath: this.fileObj.relativePath,
1271 | flowTotalChunks: this.fileObj.chunks.length
1272 | };
1273 | },
1274 |
1275 | /**
1276 | * Get target option with query params
1277 | * @function
1278 | * @param params
1279 | * @returns {string}
1280 | */
1281 | getTarget: function(target, params){
1282 | if(target.indexOf('?') < 0) {
1283 | target += '?';
1284 | } else {
1285 | target += '&';
1286 | }
1287 | return target + params.join('&');
1288 | },
1289 |
1290 | /**
1291 | * Makes a GET request without any data to see if the chunk has already
1292 | * been uploaded in a previous session
1293 | * @function
1294 | */
1295 | test: function () {
1296 | // Set up request and listen for event
1297 | this.xhr = new XMLHttpRequest();
1298 | this.xhr.addEventListener("load", this.testHandler, false);
1299 | this.xhr.addEventListener("error", this.testHandler, false);
1300 | var testMethod = evalOpts(this.flowObj.opts.testMethod, this.fileObj, this);
1301 | var data = this.prepareXhrRequest(testMethod, true);
1302 | this.xhr.send(data);
1303 | },
1304 |
1305 | /**
1306 | * Finish preprocess state
1307 | * @function
1308 | */
1309 | preprocessFinished: function () {
1310 | // Re-compute the endByte after the preprocess function to allow an
1311 | // implementer of preprocess to set the fileObj size
1312 | this.endByte = this.computeEndByte();
1313 |
1314 | this.preprocessState = 2;
1315 | this.send();
1316 | },
1317 |
1318 | /**
1319 | * Finish read state
1320 | * @function
1321 | */
1322 | readFinished: function (bytes) {
1323 | this.readState = 2;
1324 | this.bytes = bytes;
1325 | this.send();
1326 | },
1327 |
1328 |
1329 | /**
1330 | * Uploads the actual data in a POST call
1331 | * @function
1332 | */
1333 | send: function () {
1334 | var preprocess = this.flowObj.opts.preprocess;
1335 | var read = this.flowObj.opts.readFileFn;
1336 | if (typeof preprocess === 'function') {
1337 | switch (this.preprocessState) {
1338 | case 0:
1339 | this.preprocessState = 1;
1340 | preprocess(this);
1341 | return;
1342 | case 1:
1343 | return;
1344 | }
1345 | }
1346 | switch (this.readState) {
1347 | case 0:
1348 | this.readState = 1;
1349 | read(this.fileObj, this.startByte, this.endByte, this.fileObj.file.type, this);
1350 | return;
1351 | case 1:
1352 | return;
1353 | }
1354 | if (this.flowObj.opts.testChunks && !this.tested) {
1355 | this.test();
1356 | return;
1357 | }
1358 |
1359 | this.loaded = 0;
1360 | this.total = 0;
1361 | this.pendingRetry = false;
1362 |
1363 | // Set up request and listen for event
1364 | this.xhr = new XMLHttpRequest();
1365 | this.xhr.upload.addEventListener('progress', this.progressHandler, false);
1366 | this.xhr.addEventListener("load", this.doneHandler, false);
1367 | this.xhr.addEventListener("error", this.doneHandler, false);
1368 |
1369 | var uploadMethod = evalOpts(this.flowObj.opts.uploadMethod, this.fileObj, this);
1370 | var data = this.prepareXhrRequest(uploadMethod, false, this.flowObj.opts.method, this.bytes);
1371 | this.xhr.send(data);
1372 | },
1373 |
1374 | /**
1375 | * Abort current xhr request
1376 | * @function
1377 | */
1378 | abort: function () {
1379 | // Abort and reset
1380 | var xhr = this.xhr;
1381 | this.xhr = null;
1382 | if (xhr) {
1383 | xhr.abort();
1384 | }
1385 | },
1386 |
1387 | /**
1388 | * Retrieve current chunk upload status
1389 | * @function
1390 | * @returns {string} 'pending', 'uploading', 'success', 'error'
1391 | */
1392 | status: function (isTest) {
1393 | if (this.readState === 1) {
1394 | return 'reading';
1395 | } else if (this.pendingRetry || this.preprocessState === 1) {
1396 | // if pending retry then that's effectively the same as actively uploading,
1397 | // there might just be a slight delay before the retry starts
1398 | return 'uploading';
1399 | } else if (!this.xhr) {
1400 | return 'pending';
1401 | } else if (this.xhr.readyState < 4) {
1402 | // Status is really 'OPENED', 'HEADERS_RECEIVED'
1403 | // or 'LOADING' - meaning that stuff is happening
1404 | return 'uploading';
1405 | } else {
1406 | if (this.flowObj.opts.successStatuses.indexOf(this.xhr.status) > -1) {
1407 | // HTTP 200, perfect
1408 | // HTTP 202 Accepted - The request has been accepted for processing, but the processing has not been completed.
1409 | return 'success';
1410 | } else if (this.flowObj.opts.permanentErrors.indexOf(this.xhr.status) > -1 ||
1411 | !isTest && this.retries >= this.flowObj.opts.maxChunkRetries) {
1412 | // HTTP 413/415/500/501, permanent error
1413 | return 'error';
1414 | } else {
1415 | // this should never happen, but we'll reset and queue a retry
1416 | // a likely case for this would be 503 service unavailable
1417 | this.abort();
1418 | return 'pending';
1419 | }
1420 | }
1421 | },
1422 |
1423 | /**
1424 | * Get response from xhr request
1425 | * @function
1426 | * @returns {String}
1427 | */
1428 | message: function () {
1429 | return this.xhr ? this.xhr.responseText : '';
1430 | },
1431 |
1432 | /**
1433 | * Get upload progress
1434 | * @function
1435 | * @returns {number}
1436 | */
1437 | progress: function () {
1438 | if (this.pendingRetry) {
1439 | return 0;
1440 | }
1441 | var s = this.status();
1442 | if (s === 'success' || s === 'error') {
1443 | return 1;
1444 | } else if (s === 'pending') {
1445 | return 0;
1446 | } else {
1447 | return this.total > 0 ? this.loaded / this.total : 0;
1448 | }
1449 | },
1450 |
1451 | /**
1452 | * Count total size uploaded
1453 | * @function
1454 | * @returns {number}
1455 | */
1456 | sizeUploaded: function () {
1457 | var size = this.endByte - this.startByte;
1458 | // can't return only chunk.loaded value, because it is bigger than chunk size
1459 | if (this.status() !== 'success') {
1460 | size = this.progress() * size;
1461 | }
1462 | return size;
1463 | },
1464 |
1465 | /**
1466 | * Prepare Xhr request. Set query, headers and data
1467 | * @param {string} method GET or POST
1468 | * @param {bool} isTest is this a test request
1469 | * @param {string} [paramsMethod] octet or form
1470 | * @param {Blob} [blob] to send
1471 | * @returns {FormData|Blob|Null} data to send
1472 | */
1473 | prepareXhrRequest: function(method, isTest, paramsMethod, blob) {
1474 | // Add data from the query options
1475 | var query = evalOpts(this.flowObj.opts.query, this.fileObj, this, isTest);
1476 | query = extend(query, this.getParams());
1477 |
1478 | var target = evalOpts(this.flowObj.opts.target, this.fileObj, this, isTest);
1479 | var data = null;
1480 | if (method === 'GET' || paramsMethod === 'octet') {
1481 | // Add data from the query options
1482 | var params = [];
1483 | each(query, function (v, k) {
1484 | params.push([encodeURIComponent(k), encodeURIComponent(v)].join('='));
1485 | });
1486 | target = this.getTarget(target, params);
1487 | data = blob || null;
1488 | } else {
1489 | // Add data from the query options
1490 | data = new FormData();
1491 | each(query, function (v, k) {
1492 | data.append(k, v);
1493 | });
1494 | if (typeof blob !== "undefined") data.append(this.flowObj.opts.fileParameterName, blob, this.fileObj.file.name);
1495 | }
1496 |
1497 | this.xhr.open(method, target, true);
1498 | this.xhr.withCredentials = this.flowObj.opts.withCredentials;
1499 |
1500 | // Add data from header options
1501 | each(evalOpts(this.flowObj.opts.headers, this.fileObj, this, isTest), function (v, k) {
1502 | this.xhr.setRequestHeader(k, v);
1503 | }, this);
1504 |
1505 | return data;
1506 | }
1507 | };
1508 |
1509 | /**
1510 | * Remove value from array
1511 | * @param array
1512 | * @param value
1513 | */
1514 | function arrayRemove(array, value) {
1515 | var index = array.indexOf(value);
1516 | if (index > -1) {
1517 | array.splice(index, 1);
1518 | }
1519 | }
1520 |
1521 | /**
1522 | * If option is a function, evaluate it with given params
1523 | * @param {*} data
1524 | * @param {...} args arguments of a callback
1525 | * @returns {*}
1526 | */
1527 | function evalOpts(data, args) {
1528 | if (typeof data === "function") {
1529 | // `arguments` is an object, not array, in FF, so:
1530 | args = Array.prototype.slice.call(arguments);
1531 | data = data.apply(null, args.slice(1));
1532 | }
1533 | return data;
1534 | }
1535 | Flow.evalOpts = evalOpts;
1536 |
1537 | /**
1538 | * Execute function asynchronously
1539 | * @param fn
1540 | * @param context
1541 | */
1542 | function async(fn, context) {
1543 | setTimeout(fn.bind(context), 0);
1544 | }
1545 |
1546 | /**
1547 | * Extends the destination object `dst` by copying all of the properties from
1548 | * the `src` object(s) to `dst`. You can specify multiple `src` objects.
1549 | * @function
1550 | * @param {Object} dst Destination object.
1551 | * @param {...Object} src Source object(s).
1552 | * @returns {Object} Reference to `dst`.
1553 | */
1554 | function extend(dst, src) {
1555 | each(arguments, function(obj) {
1556 | if (obj !== dst) {
1557 | each(obj, function(value, key){
1558 | dst[key] = value;
1559 | });
1560 | }
1561 | });
1562 | return dst;
1563 | }
1564 | Flow.extend = extend;
1565 |
1566 | /**
1567 | * Iterate each element of an object
1568 | * @function
1569 | * @param {Array|Object} obj object or an array to iterate
1570 | * @param {Function} callback first argument is a value and second is a key.
1571 | * @param {Object=} context Object to become context (`this`) for the iterator function.
1572 | */
1573 | function each(obj, callback, context) {
1574 | if (!obj) {
1575 | return ;
1576 | }
1577 | var key;
1578 | // Is Array?
1579 | // Array.isArray won't work, not only arrays can be iterated by index https://github.com/flowjs/ng-flow/issues/236#
1580 | if (typeof(obj.length) !== 'undefined') {
1581 | for (key = 0; key < obj.length; key++) {
1582 | if (callback.call(context, obj[key], key) === false) {
1583 | return ;
1584 | }
1585 | }
1586 | } else {
1587 | for (key in obj) {
1588 | if (obj.hasOwnProperty(key) && callback.call(context, obj[key], key) === false) {
1589 | return ;
1590 | }
1591 | }
1592 | }
1593 | }
1594 | Flow.each = each;
1595 |
1596 | /**
1597 | * FlowFile constructor
1598 | * @type {FlowFile}
1599 | */
1600 | Flow.FlowFile = FlowFile;
1601 |
1602 | /**
1603 | * FlowFile constructor
1604 | * @type {FlowChunk}
1605 | */
1606 | Flow.FlowChunk = FlowChunk;
1607 |
1608 | /**
1609 | * Library version
1610 | * @type {string}
1611 | */
1612 | Flow.version = '2.13.0';
1613 |
1614 | if ( typeof module === "object" && module && typeof module.exports === "object" ) {
1615 | // Expose Flow as module.exports in loaders that implement the Node
1616 | // module pattern (including browserify). Do not create the global, since
1617 | // the user will be storing it themselves locally, and globals are frowned
1618 | // upon in the Node module world.
1619 | module.exports = Flow;
1620 | } else {
1621 | // Otherwise expose Flow to the global object as usual
1622 | window.Flow = Flow;
1623 |
1624 | // Register as a named AMD module, since Flow can be concatenated with other
1625 | // files that may use define, but not via a proper concatenation script that
1626 | // understands anonymous AMD modules. A named AMD is safest and most robust
1627 | // way to register. Lowercase flow is used because AMD module names are
1628 | // derived from file names, and Flow is normally delivered in a lowercase
1629 | // file name. Do this after creating the global so that if an AMD module wants
1630 | // to call noConflict to hide this version of Flow, it will work.
1631 | if ( typeof define === "function" && define.amd ) {
1632 | define( "flow", [], function () { return Flow; } );
1633 | }
1634 | }
1635 | })(window, document);
1636 |
1637 | /**
1638 | * @description
1639 | * var app = angular.module('App', ['flow.provider'], function(flowFactoryProvider){
1640 | * flowFactoryProvider.defaults = {target: '/'};
1641 | * });
1642 | * @name flowFactoryProvider
1643 | */
1644 | angular.module('flow.provider', [])
1645 | .provider('flowFactory', function() {
1646 | 'use strict';
1647 | /**
1648 | * Define the default properties for flow.js
1649 | * @name flowFactoryProvider.defaults
1650 | * @type {Object}
1651 | */
1652 | this.defaults = {};
1653 |
1654 | /**
1655 | * Flow, MaybeFlow or NotFlow
1656 | * @name flowFactoryProvider.factory
1657 | * @type {function}
1658 | * @return {Flow}
1659 | */
1660 | this.factory = function (options) {
1661 | return new Flow(options);
1662 | };
1663 |
1664 | /**
1665 | * Define the default events
1666 | * @name flowFactoryProvider.events
1667 | * @type {Array}
1668 | * @private
1669 | */
1670 | this.events = [];
1671 |
1672 | /**
1673 | * Add default events
1674 | * @name flowFactoryProvider.on
1675 | * @function
1676 | * @param {string} event
1677 | * @param {Function} callback
1678 | */
1679 | this.on = function (event, callback) {
1680 | this.events.push([event, callback]);
1681 | };
1682 |
1683 | this.$get = function() {
1684 | var fn = this.factory;
1685 | var defaults = this.defaults;
1686 | var events = this.events;
1687 | return {
1688 | 'create': function(opts) {
1689 | // combine default options with global options and options
1690 | var flow = fn(angular.extend({}, defaults, opts));
1691 | angular.forEach(events, function (event) {
1692 | flow.on(event[0], event[1]);
1693 | });
1694 | return flow;
1695 | }
1696 | };
1697 | };
1698 | });
1699 | angular.module('flow.init', ['flow.provider'])
1700 | .controller('flowCtrl', ['$scope', '$attrs', '$parse', 'flowFactory',
1701 | function ($scope, $attrs, $parse, flowFactory) {
1702 |
1703 | var options = angular.extend({}, $scope.$eval($attrs.flowInit));
1704 |
1705 | // use existing flow object or create a new one
1706 | var flow = $scope.$eval($attrs.flowObject) || flowFactory.create(options);
1707 |
1708 | var catchAllHandler = function(eventName){
1709 | var args = Array.prototype.slice.call(arguments);
1710 | args.shift();
1711 | var event = $scope.$broadcast.apply($scope, ['flow::' + eventName, flow].concat(args));
1712 | if ({
1713 | 'progress':1, 'filesSubmitted':1, 'fileSuccess': 1, 'fileError': 1, 'complete': 1
1714 | }[eventName]) {
1715 | $scope.$applyAsync();
1716 | }
1717 | if (event.defaultPrevented) {
1718 | return false;
1719 | }
1720 | };
1721 |
1722 | flow.on('catchAll', catchAllHandler);
1723 | $scope.$on('$destroy', function(){
1724 | flow.off('catchAll', catchAllHandler);
1725 | });
1726 |
1727 | $scope.$flow = flow;
1728 |
1729 | if ($attrs.hasOwnProperty('flowName')) {
1730 | $parse($attrs.flowName).assign($scope, flow);
1731 | $scope.$on('$destroy', function () {
1732 | $parse($attrs.flowName).assign($scope);
1733 | });
1734 | }
1735 | }])
1736 | .directive('flowInit', [function() {
1737 | return {
1738 | scope: true,
1739 | controller: 'flowCtrl'
1740 | };
1741 | }]);
1742 | angular.module('flow.btn', ['flow.init'])
1743 | .directive('flowBtn', [function() {
1744 | return {
1745 | 'restrict': 'EA',
1746 | 'scope': false,
1747 | 'require': '^flowInit',
1748 | 'link': function(scope, element, attrs) {
1749 | var isDirectory = attrs.hasOwnProperty('flowDirectory');
1750 | var isSingleFile = attrs.hasOwnProperty('flowSingleFile');
1751 | var inputAttrs = attrs.hasOwnProperty('flowAttrs') && scope.$eval(attrs.flowAttrs);
1752 | scope.$flow.assignBrowse(element, isDirectory, isSingleFile, inputAttrs);
1753 | }
1754 | };
1755 | }]);
1756 | angular.module('flow.dragEvents', ['flow.init'])
1757 | /**
1758 | * @name flowPreventDrop
1759 | * Prevent loading files then dropped on element
1760 | */
1761 | .directive('flowPreventDrop', function() {
1762 | return {
1763 | 'scope': false,
1764 | 'link': function(scope, element, attrs) {
1765 | element.bind('drop dragover', function (event) {
1766 | event.preventDefault();
1767 | });
1768 | }
1769 | };
1770 | })
1771 | /**
1772 | * @name flowDragEnter
1773 | * executes `flowDragEnter` and `flowDragLeave` events
1774 | */
1775 | .directive('flowDragEnter', ['$timeout', function($timeout) {
1776 | return {
1777 | 'scope': false,
1778 | 'link': function(scope, element, attrs) {
1779 | var promise;
1780 | var enter = false;
1781 | element.bind('dragover', function (event) {
1782 | if (!isFileDrag(event)) {
1783 | return ;
1784 | }
1785 | if (!enter) {
1786 | scope.$apply(attrs.flowDragEnter);
1787 | enter = true;
1788 | }
1789 | $timeout.cancel(promise);
1790 | event.preventDefault();
1791 | });
1792 | element.bind('dragleave drop', function (event) {
1793 | $timeout.cancel(promise);
1794 | promise = $timeout(function () {
1795 | scope.$eval(attrs.flowDragLeave);
1796 | promise = null;
1797 | enter = false;
1798 | }, 100);
1799 | });
1800 | function isFileDrag(dragEvent) {
1801 | var fileDrag = false;
1802 | var dataTransfer = dragEvent.dataTransfer || dragEvent.originalEvent.dataTransfer;
1803 | angular.forEach(dataTransfer && dataTransfer.types, function(val) {
1804 | if (val === 'Files') {
1805 | fileDrag = true;
1806 | }
1807 | });
1808 | return fileDrag;
1809 | }
1810 | }
1811 | };
1812 | }]);
1813 |
1814 | angular.module('flow.drop', ['flow.init'])
1815 | .directive('flowDrop', function() {
1816 | return {
1817 | 'scope': false,
1818 | 'require': '^flowInit',
1819 | 'link': function(scope, element, attrs) {
1820 | if (attrs.flowDropEnabled) {
1821 | scope.$watch(attrs.flowDropEnabled, function (value) {
1822 | if (value) {
1823 | assignDrop();
1824 | } else {
1825 | unAssignDrop();
1826 | }
1827 | });
1828 | } else {
1829 | assignDrop();
1830 | }
1831 | function assignDrop() {
1832 | scope.$flow.assignDrop(element);
1833 | }
1834 | function unAssignDrop() {
1835 | scope.$flow.unAssignDrop(element);
1836 | }
1837 | }
1838 | };
1839 | });
1840 |
1841 | !function (angular) {'use strict';
1842 | var module = angular.module('flow.events', ['flow.init']);
1843 | var events = {
1844 | fileSuccess: ['$file', '$message'],
1845 | fileProgress: ['$file'],
1846 | fileAdded: ['$file', '$event'],
1847 | filesAdded: ['$files', '$event'],
1848 | filesSubmitted: ['$files', '$event'],
1849 | fileRetry: ['$file'],
1850 | fileRemoved: ['$file'],
1851 | fileError: ['$file', '$message'],
1852 | uploadStart: [],
1853 | complete: [],
1854 | progress: [],
1855 | error: ['$message', '$file']
1856 | };
1857 |
1858 | angular.forEach(events, function (eventArgs, eventName) {
1859 | var name = 'flow' + capitaliseFirstLetter(eventName);
1860 | if (name == 'flowUploadStart') {
1861 | name = 'flowUploadStarted';// event alias
1862 | }
1863 | module.directive(name, [function() {
1864 | return {
1865 | require: '^flowInit',
1866 | controller: ['$scope', '$attrs', function ($scope, $attrs) {
1867 | $scope.$on('flow::' + eventName, function () {
1868 | var funcArgs = Array.prototype.slice.call(arguments);
1869 | var event = funcArgs.shift();// remove angular event
1870 | // remove flow object and ignore event if it is from parent directive
1871 | if ($scope.$flow !== funcArgs.shift()) {
1872 | return ;
1873 | }
1874 | var args = {};
1875 | angular.forEach(eventArgs, function(value, key) {
1876 | args[value] = funcArgs[key];
1877 | });
1878 | if ($scope.$eval($attrs[name], args) === false) {
1879 | event.preventDefault();
1880 | }
1881 | });
1882 | }]
1883 | };
1884 | }]);
1885 | });
1886 |
1887 | function capitaliseFirstLetter(string) {
1888 | return string.charAt(0).toUpperCase() + string.slice(1);
1889 | }
1890 | }(angular);
1891 |
1892 | angular.module('flow.img', ['flow.init'])
1893 | .directive('flowImg', [function() {
1894 | return {
1895 | 'scope': false,
1896 | 'require': '^flowInit',
1897 | 'link': function(scope, element, attrs) {
1898 | var file = attrs.flowImg;
1899 | scope.$watch(file, function (file) {
1900 | if (!file) {
1901 | return ;
1902 | }
1903 | var fileReader = new FileReader();
1904 | fileReader.readAsDataURL(file.file);
1905 | fileReader.onload = function (event) {
1906 | scope.$apply(function () {
1907 | attrs.$set('src', event.target.result);
1908 | });
1909 | };
1910 | });
1911 | }
1912 | };
1913 | }]);
1914 | angular.module('flow.transfers', ['flow.init'])
1915 | .directive('flowTransfers', [function() {
1916 | return {
1917 | 'scope': true,
1918 | 'require': '^flowInit',
1919 | 'link': function(scope) {
1920 | scope.transfers = scope.$flow.files;
1921 | }
1922 | };
1923 | }]);
1924 | angular.module('flow', ['flow.provider', 'flow.init', 'flow.events', 'flow.btn',
1925 | 'flow.drop', 'flow.transfers', 'flow.img', 'flow.dragEvents']);
--------------------------------------------------------------------------------
/dist/ng-flow-standalone.min.js:
--------------------------------------------------------------------------------
1 | /*! @flowjs/ng-flow 2.7.8 */
2 | !function(a,b,c){"use strict";function d(b){if(this.support=!("undefined"==typeof File||"undefined"==typeof Blob||"undefined"==typeof FileList||!Blob.prototype.slice&&!Blob.prototype.webkitSlice&&!Blob.prototype.mozSlice),this.support){this.supportDirectory=/Chrome/.test(a.navigator.userAgent)||/Firefox/.test(a.navigator.userAgent)||/Edge/.test(a.navigator.userAgent),this.files=[],this.defaults={chunkSize:1048576,forceChunkSize:!1,simultaneousUploads:3,singleFile:!1,fileParameterName:"file",progressCallbacksInterval:500,speedSmoothingFactor:.1,query:{},headers:{},withCredentials:!1,preprocess:null,method:"multipart",testMethod:"GET",uploadMethod:"POST",prioritizeFirstAndLastChunk:!1,allowDuplicateUploads:!1,target:"/",testChunks:!0,generateUniqueIdentifier:null,maxChunkRetries:0,chunkRetryInterval:null,permanentErrors:[404,413,415,500,501],successStatuses:[200,201,202],onDropStopPropagation:!1,initFileFn:null,readFileFn:f},this.opts={},this.events={};var c=this;this.onDrop=function(a){c.opts.onDropStopPropagation&&a.stopPropagation(),a.preventDefault();var b=a.dataTransfer;b.items&&b.items[0]&&b.items[0].webkitGetAsEntry?c.webkitReadDataTransfer(a):c.addFiles(b.files,a)},this.preventEvent=function(a){a.preventDefault()},this.opts=d.extend({},this.defaults,b||{})}}function e(a,b,d){this.flowObj=a,this.bytes=null,this.file=b,this.name=b.fileName||b.name,this.size=b.size,this.relativePath=b.relativePath||b.webkitRelativePath||this.name,this.uniqueIdentifier=d===c?a.generateUniqueIdentifier(b):d,this.chunks=[],this.paused=!1,this.error=!1,this.averageSpeed=0,this.currentSpeed=0,this._lastProgressCallback=Date.now(),this._prevUploadedSize=0,this._prevProgress=0,this.bootstrap()}function f(a,b,c,d,e){var f="slice";a.file.slice?f="slice":a.file.mozSlice?f="mozSlice":a.file.webkitSlice&&(f="webkitSlice"),e.readFinished(a.file[f](b,c,d))}function g(a,b,c){this.flowObj=a,this.fileObj=b,this.offset=c,this.tested=!1,this.retries=0,this.pendingRetry=!1,this.preprocessState=0,this.readState=0,this.loaded=0,this.total=0,this.chunkSize=this.flowObj.opts.chunkSize,this.startByte=this.offset*this.chunkSize,this.computeEndByte=function(){var a=Math.min(this.fileObj.size,(this.offset+1)*this.chunkSize);return this.fileObj.size-a-1&&a.splice(c,1)}function i(a,b){return"function"==typeof a&&(b=Array.prototype.slice.call(arguments),a=a.apply(null,b.slice(1))),a}function j(a,b){setTimeout(a.bind(b),0)}function k(a,b){return l(arguments,function(b){b!==a&&l(b,function(b,c){a[c]=b})}),a}function l(a,b,c){if(a){var d;if("undefined"!=typeof a.length){for(d=0;d1&&"pending"===a.chunks[a.chunks.length-1].status()?(a.chunks[a.chunks.length-1].send(),b=!0,!1):void 0}),b))return b;if(l(this.files,function(a){return a.paused||l(a.chunks,function(a){return"pending"===a.status()?(a.send(),b=!0,!1):void 0}),b?!1:void 0}),b)return!0;var c=!1;return l(this.files,function(a){return a.isComplete()?void 0:(c=!0,!1)}),c||a||j(function(){this.fire("complete")},this),!1},assignBrowse:function(a,c,d,e){a instanceof Element&&(a=[a]),l(a,function(a){var f;"INPUT"===a.tagName&&"file"===a.type?f=a:(f=b.createElement("input"),f.setAttribute("type","file"),k(f.style,{visibility:"hidden",position:"absolute",width:"1px",height:"1px"}),a.appendChild(f),a.addEventListener("click",function(){f.click()},!1)),this.opts.singleFile||d||f.setAttribute("multiple","multiple"),c&&f.setAttribute("webkitdirectory","webkitdirectory"),l(e,function(a,b){f.setAttribute(b,a)});var g=this;f.addEventListener("change",function(a){a.target.value&&(g.addFiles(a.target.files,a),a.target.value="")},!1)},this)},assignDrop:function(a){"undefined"==typeof a.length&&(a=[a]),l(a,function(a){a.addEventListener("dragover",this.preventEvent,!1),a.addEventListener("dragenter",this.preventEvent,!1),a.addEventListener("drop",this.onDrop,!1)},this)},unAssignDrop:function(a){"undefined"==typeof a.length&&(a=[a]),l(a,function(a){a.removeEventListener("dragover",this.preventEvent),a.removeEventListener("dragenter",this.preventEvent),a.removeEventListener("drop",this.onDrop)},this)},isUploading:function(){var a=!1;return l(this.files,function(b){return b.isUploading()?(a=!0,!1):void 0}),a},_shouldUploadNext:function(){var a=0,b=!0,c=this.opts.simultaneousUploads;return l(this.files,function(d){l(d.chunks,function(d){return"uploading"===d.status()&&(a++,a>=c)?(b=!1,!1):void 0})}),b&&a},upload:function(){var a=this._shouldUploadNext();if(a!==!1){this.fire("uploadStart");for(var b=!1,c=1;c<=this.opts.simultaneousUploads-a;c++)b=this.uploadNextChunk(!0)||b;b||j(function(){this.fire("complete")},this)}},resume:function(){l(this.files,function(a){a.isComplete()||a.resume()})},pause:function(){l(this.files,function(a){a.pause()})},cancel:function(){for(var a=this.files.length-1;a>=0;a--)this.files[a].cancel()},progress:function(){var a=0,b=0;return l(this.files,function(c){a+=c.progress()*c.size,b+=c.size}),b>0?a/b:0},addFile:function(a,b){this.addFiles([a],b)},addFiles:function(a,b){var c=[];l(a,function(a){if((!m||m&&a.size>0)&&(a.size%4096!==0||"."!==a.name&&"."!==a.fileName)){var d=this.generateUniqueIdentifier(a);if(this.opts.allowDuplicateUploads||!this.getFromUniqueIdentifier(d)){var f=new e(this,a,d);this.fire("fileAdded",f,b)&&c.push(f)}}},this),this.fire("filesAdded",c,b)&&(l(c,function(a){this.opts.singleFile&&this.files.length>0&&this.removeFile(this.files[0]),this.files.push(a)},this),this.fire("filesSubmitted",c,b))},removeFile:function(a){for(var b=this.files.length-1;b>=0;b--)this.files[b]===a&&(this.files.splice(b,1),a.abort(),this.fire("fileRemoved",a))},getFromUniqueIdentifier:function(a){var b=!1;return l(this.files,function(c){c.uniqueIdentifier===a&&(b=c)}),b},getSize:function(){var a=0;return l(this.files,function(b){a+=b.size}),a},sizeUploaded:function(){var a=0;return l(this.files,function(b){a+=b.sizeUploaded()}),a},timeRemaining:function(){var a=0,b=0;return l(this.files,function(c){c.paused||c.error||(a+=c.size-c.sizeUploaded(),b+=c.averageSpeed)}),a&&!b?Number.POSITIVE_INFINITY:a||b?Math.floor(a/b):0}},e.prototype={measureSpeed:function(){var a=Date.now()-this._lastProgressCallback;if(a){var b=this.flowObj.opts.speedSmoothingFactor,c=this.sizeUploaded();this.currentSpeed=Math.max((c-this._prevUploadedSize)/a*1e3,0),this.averageSpeed=b*this.currentSpeed+(1-b)*this.averageSpeed,this._prevUploadedSize=c}},chunkEvent:function(a,b,c){switch(b){case"progress":if(Date.now()-this._lastProgressCallbackc;c++)this.chunks.push(new g(this.flowObj,this,c))},progress:function(){if(this.error)return 1;if(1===this.chunks.length)return this._prevProgress=Math.max(this._prevProgress,this.chunks[0].progress()),this._prevProgress;var a=0;l(this.chunks,function(b){a+=b.progress()*(b.endByte-b.startByte)});var b=a/this.size;return this._prevProgress=Math.max(this._prevProgress,b>.9999?1:b),this._prevProgress},isUploading:function(){var a=!1;return l(this.chunks,function(b){return"uploading"===b.status()?(a=!0,!1):void 0}),a},isComplete:function(){var a=!1;return l(this.chunks,function(b){var c=b.status();return"pending"===c||"uploading"===c||"reading"===c||1===b.preprocessState||1===b.readState?(a=!0,!1):void 0}),!a},sizeUploaded:function(){var a=0;return l(this.chunks,function(b){a+=b.sizeUploaded()}),a},timeRemaining:function(){if(this.paused||this.error)return 0;var a=this.size-this.sizeUploaded();return a&&!this.averageSpeed?Number.POSITIVE_INFINITY:a||this.averageSpeed?Math.floor(a/this.averageSpeed):0},getType:function(){return this.file.type&&this.file.type.split("/")[1]},getExtension:function(){return this.name.substr((~-this.name.lastIndexOf(".")>>>0)+2).toLowerCase()}},g.prototype={getParams:function(){return{flowChunkNumber:this.offset+1,flowChunkSize:this.flowObj.opts.chunkSize,flowCurrentChunkSize:this.endByte-this.startByte,flowTotalSize:this.fileObj.size,flowIdentifier:this.fileObj.uniqueIdentifier,flowFilename:this.fileObj.name,flowRelativePath:this.fileObj.relativePath,flowTotalChunks:this.fileObj.chunks.length}},getTarget:function(a,b){return a+=a.indexOf("?")<0?"?":"&",a+b.join("&")},test:function(){this.xhr=new XMLHttpRequest,this.xhr.addEventListener("load",this.testHandler,!1),this.xhr.addEventListener("error",this.testHandler,!1);var a=i(this.flowObj.opts.testMethod,this.fileObj,this),b=this.prepareXhrRequest(a,!0);this.xhr.send(b)},preprocessFinished:function(){this.endByte=this.computeEndByte(),this.preprocessState=2,this.send()},readFinished:function(a){this.readState=2,this.bytes=a,this.send()},send:function(){var a=this.flowObj.opts.preprocess,b=this.flowObj.opts.readFileFn;if("function"==typeof a)switch(this.preprocessState){case 0:return this.preprocessState=1,void a(this);case 1:return}switch(this.readState){case 0:return this.readState=1,void b(this.fileObj,this.startByte,this.endByte,this.fileObj.file.type,this);case 1:return}if(this.flowObj.opts.testChunks&&!this.tested)return void this.test();this.loaded=0,this.total=0,this.pendingRetry=!1,this.xhr=new XMLHttpRequest,this.xhr.upload.addEventListener("progress",this.progressHandler,!1),this.xhr.addEventListener("load",this.doneHandler,!1),this.xhr.addEventListener("error",this.doneHandler,!1);var c=i(this.flowObj.opts.uploadMethod,this.fileObj,this),d=this.prepareXhrRequest(c,!1,this.flowObj.opts.method,this.bytes);this.xhr.send(d)},abort:function(){var a=this.xhr;this.xhr=null,a&&a.abort()},status:function(a){return 1===this.readState?"reading":this.pendingRetry||1===this.preprocessState?"uploading":this.xhr?this.xhr.readyState<4?"uploading":this.flowObj.opts.successStatuses.indexOf(this.xhr.status)>-1?"success":this.flowObj.opts.permanentErrors.indexOf(this.xhr.status)>-1||!a&&this.retries>=this.flowObj.opts.maxChunkRetries?"error":(this.abort(),"pending"):"pending"},message:function(){return this.xhr?this.xhr.responseText:""},progress:function(){if(this.pendingRetry)return 0;var a=this.status();return"success"===a||"error"===a?1:"pending"===a?0:this.total>0?this.loaded/this.total:0},sizeUploaded:function(){var a=this.endByte-this.startByte;return"success"!==this.status()&&(a=this.progress()*a),a},prepareXhrRequest:function(a,b,c,d){var e=i(this.flowObj.opts.query,this.fileObj,this,b);e=k(e,this.getParams());var f=i(this.flowObj.opts.target,this.fileObj,this,b),g=null;if("GET"===a||"octet"===c){var h=[];l(e,function(a,b){h.push([encodeURIComponent(b),encodeURIComponent(a)].join("="))}),f=this.getTarget(f,h),g=d||null}else g=new FormData,l(e,function(a,b){g.append(b,a)}),"undefined"!=typeof d&&g.append(this.flowObj.opts.fileParameterName,d,this.fileObj.file.name);return this.xhr.open(a,f,!0),this.xhr.withCredentials=this.flowObj.opts.withCredentials,l(i(this.flowObj.opts.headers,this.fileObj,this,b),function(a,b){this.xhr.setRequestHeader(b,a)},this),g}},d.evalOpts=i,d.extend=k,d.each=l,d.FlowFile=e,d.FlowChunk=g,d.version="2.13.0","object"==typeof module&&module&&"object"==typeof module.exports?module.exports=d:(a.Flow=d,"function"==typeof define&&define.amd&&define("flow",[],function(){return d}))}(window,document),angular.module("flow.provider",[]).provider("flowFactory",function(){"use strict";this.defaults={},this.factory=function(a){return new Flow(a)},this.events=[],this.on=function(a,b){this.events.push([a,b])},this.$get=function(){var a=this.factory,b=this.defaults,c=this.events;return{create:function(d){var e=a(angular.extend({},b,d));return angular.forEach(c,function(a){e.on(a[0],a[1])}),e}}}}),angular.module("flow.init",["flow.provider"]).controller("flowCtrl",["$scope","$attrs","$parse","flowFactory",function(a,b,c,d){var e=angular.extend({},a.$eval(b.flowInit)),f=a.$eval(b.flowObject)||d.create(e),g=function(b){var c=Array.prototype.slice.call(arguments);c.shift();var d=a.$broadcast.apply(a,["flow::"+b,f].concat(c));return{progress:1,filesSubmitted:1,fileSuccess:1,fileError:1,complete:1}[b]&&a.$applyAsync(),d.defaultPrevented?!1:void 0};f.on("catchAll",g),a.$on("$destroy",function(){f.off("catchAll",g)}),a.$flow=f,b.hasOwnProperty("flowName")&&(c(b.flowName).assign(a,f),a.$on("$destroy",function(){c(b.flowName).assign(a)}))}]).directive("flowInit",[function(){return{scope:!0,controller:"flowCtrl"}}]),angular.module("flow.btn",["flow.init"]).directive("flowBtn",[function(){return{restrict:"EA",scope:!1,require:"^flowInit",link:function(a,b,c){var d=c.hasOwnProperty("flowDirectory"),e=c.hasOwnProperty("flowSingleFile"),f=c.hasOwnProperty("flowAttrs")&&a.$eval(c.flowAttrs);a.$flow.assignBrowse(b,d,e,f)}}}]),angular.module("flow.dragEvents",["flow.init"]).directive("flowPreventDrop",function(){return{scope:!1,link:function(a,b,c){b.bind("drop dragover",function(a){a.preventDefault()})}}}).directive("flowDragEnter",["$timeout",function(a){return{scope:!1,link:function(b,c,d){function e(a){var b=!1,c=a.dataTransfer||a.originalEvent.dataTransfer;return angular.forEach(c&&c.types,function(a){"Files"===a&&(b=!0)}),b}var f,g=!1;c.bind("dragover",function(c){e(c)&&(g||(b.$apply(d.flowDragEnter),g=!0),a.cancel(f),c.preventDefault())}),c.bind("dragleave drop",function(c){a.cancel(f),f=a(function(){b.$eval(d.flowDragLeave),f=null,g=!1},100)})}}}]),angular.module("flow.drop",["flow.init"]).directive("flowDrop",function(){return{scope:!1,require:"^flowInit",link:function(a,b,c){function d(){a.$flow.assignDrop(b)}function e(){a.$flow.unAssignDrop(b)}c.flowDropEnabled?a.$watch(c.flowDropEnabled,function(a){a?d():e()}):d()}}}),!function(a){"use strict";function b(a){return a.charAt(0).toUpperCase()+a.slice(1)}var c=a.module("flow.events",["flow.init"]),d={fileSuccess:["$file","$message"],fileProgress:["$file"],fileAdded:["$file","$event"],filesAdded:["$files","$event"],filesSubmitted:["$files","$event"],fileRetry:["$file"],fileRemoved:["$file"],fileError:["$file","$message"],uploadStart:[],complete:[],progress:[],error:["$message","$file"]};a.forEach(d,function(d,e){var f="flow"+b(e);"flowUploadStart"==f&&(f="flowUploadStarted"),c.directive(f,[function(){return{require:"^flowInit",controller:["$scope","$attrs",function(b,c){b.$on("flow::"+e,function(){var e=Array.prototype.slice.call(arguments),g=e.shift();if(b.$flow===e.shift()){var h={};a.forEach(d,function(a,b){h[a]=e[b]}),b.$eval(c[f],h)===!1&&g.preventDefault()}})}]}}])})}(angular),angular.module("flow.img",["flow.init"]).directive("flowImg",[function(){return{scope:!1,require:"^flowInit",link:function(a,b,c){var d=c.flowImg;a.$watch(d,function(b){if(b){var d=new FileReader;d.readAsDataURL(b.file),d.onload=function(b){a.$apply(function(){c.$set("src",b.target.result)})}}})}}}]),angular.module("flow.transfers",["flow.init"]).directive("flowTransfers",[function(){return{scope:!0,require:"^flowInit",link:function(a){a.transfers=a.$flow.files}}}]),angular.module("flow",["flow.provider","flow.init","flow.events","flow.btn","flow.drop","flow.transfers","flow.img","flow.dragEvents"]);
--------------------------------------------------------------------------------
/dist/ng-flow.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @description
3 | * var app = angular.module('App', ['flow.provider'], function(flowFactoryProvider){
4 | * flowFactoryProvider.defaults = {target: '/'};
5 | * });
6 | * @name flowFactoryProvider
7 | */
8 | angular.module('flow.provider', [])
9 | .provider('flowFactory', function() {
10 | 'use strict';
11 | /**
12 | * Define the default properties for flow.js
13 | * @name flowFactoryProvider.defaults
14 | * @type {Object}
15 | */
16 | this.defaults = {};
17 |
18 | /**
19 | * Flow, MaybeFlow or NotFlow
20 | * @name flowFactoryProvider.factory
21 | * @type {function}
22 | * @return {Flow}
23 | */
24 | this.factory = function (options) {
25 | return new Flow(options);
26 | };
27 |
28 | /**
29 | * Define the default events
30 | * @name flowFactoryProvider.events
31 | * @type {Array}
32 | * @private
33 | */
34 | this.events = [];
35 |
36 | /**
37 | * Add default events
38 | * @name flowFactoryProvider.on
39 | * @function
40 | * @param {string} event
41 | * @param {Function} callback
42 | */
43 | this.on = function (event, callback) {
44 | this.events.push([event, callback]);
45 | };
46 |
47 | this.$get = function() {
48 | var fn = this.factory;
49 | var defaults = this.defaults;
50 | var events = this.events;
51 | return {
52 | 'create': function(opts) {
53 | // combine default options with global options and options
54 | var flow = fn(angular.extend({}, defaults, opts));
55 | angular.forEach(events, function (event) {
56 | flow.on(event[0], event[1]);
57 | });
58 | return flow;
59 | }
60 | };
61 | };
62 | });
63 | angular.module('flow.init', ['flow.provider'])
64 | .controller('flowCtrl', ['$scope', '$attrs', '$parse', 'flowFactory',
65 | function ($scope, $attrs, $parse, flowFactory) {
66 |
67 | var options = angular.extend({}, $scope.$eval($attrs.flowInit));
68 |
69 | // use existing flow object or create a new one
70 | var flow = $scope.$eval($attrs.flowObject) || flowFactory.create(options);
71 |
72 | var catchAllHandler = function(eventName){
73 | var args = Array.prototype.slice.call(arguments);
74 | args.shift();
75 | var event = $scope.$broadcast.apply($scope, ['flow::' + eventName, flow].concat(args));
76 | if ({
77 | 'progress':1, 'filesSubmitted':1, 'fileSuccess': 1, 'fileError': 1, 'complete': 1
78 | }[eventName]) {
79 | $scope.$applyAsync();
80 | }
81 | if (event.defaultPrevented) {
82 | return false;
83 | }
84 | };
85 |
86 | flow.on('catchAll', catchAllHandler);
87 | $scope.$on('$destroy', function(){
88 | flow.off('catchAll', catchAllHandler);
89 | });
90 |
91 | $scope.$flow = flow;
92 |
93 | if ($attrs.hasOwnProperty('flowName')) {
94 | $parse($attrs.flowName).assign($scope, flow);
95 | $scope.$on('$destroy', function () {
96 | $parse($attrs.flowName).assign($scope);
97 | });
98 | }
99 | }])
100 | .directive('flowInit', [function() {
101 | return {
102 | scope: true,
103 | controller: 'flowCtrl'
104 | };
105 | }]);
106 | angular.module('flow.btn', ['flow.init'])
107 | .directive('flowBtn', [function() {
108 | return {
109 | 'restrict': 'EA',
110 | 'scope': false,
111 | 'require': '^flowInit',
112 | 'link': function(scope, element, attrs) {
113 | var isDirectory = attrs.hasOwnProperty('flowDirectory');
114 | var isSingleFile = attrs.hasOwnProperty('flowSingleFile');
115 | var inputAttrs = attrs.hasOwnProperty('flowAttrs') && scope.$eval(attrs.flowAttrs);
116 | scope.$flow.assignBrowse(element, isDirectory, isSingleFile, inputAttrs);
117 | }
118 | };
119 | }]);
120 | angular.module('flow.dragEvents', ['flow.init'])
121 | /**
122 | * @name flowPreventDrop
123 | * Prevent loading files then dropped on element
124 | */
125 | .directive('flowPreventDrop', function() {
126 | return {
127 | 'scope': false,
128 | 'link': function(scope, element, attrs) {
129 | element.bind('drop dragover', function (event) {
130 | event.preventDefault();
131 | });
132 | }
133 | };
134 | })
135 | /**
136 | * @name flowDragEnter
137 | * executes `flowDragEnter` and `flowDragLeave` events
138 | */
139 | .directive('flowDragEnter', ['$timeout', function($timeout) {
140 | return {
141 | 'scope': false,
142 | 'link': function(scope, element, attrs) {
143 | var promise;
144 | var enter = false;
145 | element.bind('dragover', function (event) {
146 | if (!isFileDrag(event)) {
147 | return ;
148 | }
149 | if (!enter) {
150 | scope.$apply(attrs.flowDragEnter);
151 | enter = true;
152 | }
153 | $timeout.cancel(promise);
154 | event.preventDefault();
155 | });
156 | element.bind('dragleave drop', function (event) {
157 | $timeout.cancel(promise);
158 | promise = $timeout(function () {
159 | scope.$eval(attrs.flowDragLeave);
160 | promise = null;
161 | enter = false;
162 | }, 100);
163 | });
164 | function isFileDrag(dragEvent) {
165 | var fileDrag = false;
166 | var dataTransfer = dragEvent.dataTransfer || dragEvent.originalEvent.dataTransfer;
167 | angular.forEach(dataTransfer && dataTransfer.types, function(val) {
168 | if (val === 'Files') {
169 | fileDrag = true;
170 | }
171 | });
172 | return fileDrag;
173 | }
174 | }
175 | };
176 | }]);
177 |
178 | angular.module('flow.drop', ['flow.init'])
179 | .directive('flowDrop', function() {
180 | return {
181 | 'scope': false,
182 | 'require': '^flowInit',
183 | 'link': function(scope, element, attrs) {
184 | if (attrs.flowDropEnabled) {
185 | scope.$watch(attrs.flowDropEnabled, function (value) {
186 | if (value) {
187 | assignDrop();
188 | } else {
189 | unAssignDrop();
190 | }
191 | });
192 | } else {
193 | assignDrop();
194 | }
195 | function assignDrop() {
196 | scope.$flow.assignDrop(element);
197 | }
198 | function unAssignDrop() {
199 | scope.$flow.unAssignDrop(element);
200 | }
201 | }
202 | };
203 | });
204 |
205 | !function (angular) {'use strict';
206 | var module = angular.module('flow.events', ['flow.init']);
207 | var events = {
208 | fileSuccess: ['$file', '$message'],
209 | fileProgress: ['$file'],
210 | fileAdded: ['$file', '$event'],
211 | filesAdded: ['$files', '$event'],
212 | filesSubmitted: ['$files', '$event'],
213 | fileRetry: ['$file'],
214 | fileRemoved: ['$file'],
215 | fileError: ['$file', '$message'],
216 | uploadStart: [],
217 | complete: [],
218 | progress: [],
219 | error: ['$message', '$file']
220 | };
221 |
222 | angular.forEach(events, function (eventArgs, eventName) {
223 | var name = 'flow' + capitaliseFirstLetter(eventName);
224 | if (name == 'flowUploadStart') {
225 | name = 'flowUploadStarted';// event alias
226 | }
227 | module.directive(name, [function() {
228 | return {
229 | require: '^flowInit',
230 | controller: ['$scope', '$attrs', function ($scope, $attrs) {
231 | $scope.$on('flow::' + eventName, function () {
232 | var funcArgs = Array.prototype.slice.call(arguments);
233 | var event = funcArgs.shift();// remove angular event
234 | // remove flow object and ignore event if it is from parent directive
235 | if ($scope.$flow !== funcArgs.shift()) {
236 | return ;
237 | }
238 | var args = {};
239 | angular.forEach(eventArgs, function(value, key) {
240 | args[value] = funcArgs[key];
241 | });
242 | if ($scope.$eval($attrs[name], args) === false) {
243 | event.preventDefault();
244 | }
245 | });
246 | }]
247 | };
248 | }]);
249 | });
250 |
251 | function capitaliseFirstLetter(string) {
252 | return string.charAt(0).toUpperCase() + string.slice(1);
253 | }
254 | }(angular);
255 |
256 | angular.module('flow.img', ['flow.init'])
257 | .directive('flowImg', [function() {
258 | return {
259 | 'scope': false,
260 | 'require': '^flowInit',
261 | 'link': function(scope, element, attrs) {
262 | var file = attrs.flowImg;
263 | scope.$watch(file, function (file) {
264 | if (!file) {
265 | return ;
266 | }
267 | var fileReader = new FileReader();
268 | fileReader.readAsDataURL(file.file);
269 | fileReader.onload = function (event) {
270 | scope.$apply(function () {
271 | attrs.$set('src', event.target.result);
272 | });
273 | };
274 | });
275 | }
276 | };
277 | }]);
278 | angular.module('flow.transfers', ['flow.init'])
279 | .directive('flowTransfers', [function() {
280 | return {
281 | 'scope': true,
282 | 'require': '^flowInit',
283 | 'link': function(scope) {
284 | scope.transfers = scope.$flow.files;
285 | }
286 | };
287 | }]);
288 | angular.module('flow', ['flow.provider', 'flow.init', 'flow.events', 'flow.btn',
289 | 'flow.drop', 'flow.transfers', 'flow.img', 'flow.dragEvents']);
--------------------------------------------------------------------------------
/dist/ng-flow.min.js:
--------------------------------------------------------------------------------
1 | /*! @flowjs/ng-flow 2.7.8 */
2 | angular.module("flow.provider",[]).provider("flowFactory",function(){"use strict";this.defaults={},this.factory=function(a){return new Flow(a)},this.events=[],this.on=function(a,b){this.events.push([a,b])},this.$get=function(){var a=this.factory,b=this.defaults,c=this.events;return{create:function(d){var e=a(angular.extend({},b,d));return angular.forEach(c,function(a){e.on(a[0],a[1])}),e}}}}),angular.module("flow.init",["flow.provider"]).controller("flowCtrl",["$scope","$attrs","$parse","flowFactory",function(a,b,c,d){var e=angular.extend({},a.$eval(b.flowInit)),f=a.$eval(b.flowObject)||d.create(e),g=function(b){var c=Array.prototype.slice.call(arguments);c.shift();var d=a.$broadcast.apply(a,["flow::"+b,f].concat(c));return{progress:1,filesSubmitted:1,fileSuccess:1,fileError:1,complete:1}[b]&&a.$applyAsync(),d.defaultPrevented?!1:void 0};f.on("catchAll",g),a.$on("$destroy",function(){f.off("catchAll",g)}),a.$flow=f,b.hasOwnProperty("flowName")&&(c(b.flowName).assign(a,f),a.$on("$destroy",function(){c(b.flowName).assign(a)}))}]).directive("flowInit",[function(){return{scope:!0,controller:"flowCtrl"}}]),angular.module("flow.btn",["flow.init"]).directive("flowBtn",[function(){return{restrict:"EA",scope:!1,require:"^flowInit",link:function(a,b,c){var d=c.hasOwnProperty("flowDirectory"),e=c.hasOwnProperty("flowSingleFile"),f=c.hasOwnProperty("flowAttrs")&&a.$eval(c.flowAttrs);a.$flow.assignBrowse(b,d,e,f)}}}]),angular.module("flow.dragEvents",["flow.init"]).directive("flowPreventDrop",function(){return{scope:!1,link:function(a,b,c){b.bind("drop dragover",function(a){a.preventDefault()})}}}).directive("flowDragEnter",["$timeout",function(a){return{scope:!1,link:function(b,c,d){function e(a){var b=!1,c=a.dataTransfer||a.originalEvent.dataTransfer;return angular.forEach(c&&c.types,function(a){"Files"===a&&(b=!0)}),b}var f,g=!1;c.bind("dragover",function(c){e(c)&&(g||(b.$apply(d.flowDragEnter),g=!0),a.cancel(f),c.preventDefault())}),c.bind("dragleave drop",function(c){a.cancel(f),f=a(function(){b.$eval(d.flowDragLeave),f=null,g=!1},100)})}}}]),angular.module("flow.drop",["flow.init"]).directive("flowDrop",function(){return{scope:!1,require:"^flowInit",link:function(a,b,c){function d(){a.$flow.assignDrop(b)}function e(){a.$flow.unAssignDrop(b)}c.flowDropEnabled?a.$watch(c.flowDropEnabled,function(a){a?d():e()}):d()}}}),!function(a){"use strict";function b(a){return a.charAt(0).toUpperCase()+a.slice(1)}var c=a.module("flow.events",["flow.init"]),d={fileSuccess:["$file","$message"],fileProgress:["$file"],fileAdded:["$file","$event"],filesAdded:["$files","$event"],filesSubmitted:["$files","$event"],fileRetry:["$file"],fileRemoved:["$file"],fileError:["$file","$message"],uploadStart:[],complete:[],progress:[],error:["$message","$file"]};a.forEach(d,function(d,e){var f="flow"+b(e);"flowUploadStart"==f&&(f="flowUploadStarted"),c.directive(f,[function(){return{require:"^flowInit",controller:["$scope","$attrs",function(b,c){b.$on("flow::"+e,function(){var e=Array.prototype.slice.call(arguments),g=e.shift();if(b.$flow===e.shift()){var h={};a.forEach(d,function(a,b){h[a]=e[b]}),b.$eval(c[f],h)===!1&&g.preventDefault()}})}]}}])})}(angular),angular.module("flow.img",["flow.init"]).directive("flowImg",[function(){return{scope:!1,require:"^flowInit",link:function(a,b,c){var d=c.flowImg;a.$watch(d,function(b){if(b){var d=new FileReader;d.readAsDataURL(b.file),d.onload=function(b){a.$apply(function(){c.$set("src",b.target.result)})}}})}}}]),angular.module("flow.transfers",["flow.init"]).directive("flowTransfers",[function(){return{scope:!0,require:"^flowInit",link:function(a){a.transfers=a.$flow.files}}}]),angular.module("flow",["flow.provider","flow.init","flow.events","flow.btn","flow.drop","flow.transfers","flow.img","flow.dragEvents"]);
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /* for module loading using webpack or similar package bundlers */
2 | window.Flow = require('./dist/ng-flow-standalone');
3 | module.exports = 'flow';
4 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function(config) {
2 | // define SL browsers
3 | var customLaunchers = {
4 | sl_ie10: {
5 | base: 'SauceLabs',
6 | browserName: 'internet explorer',
7 | platform: 'Windows 8',
8 | version: '10.0'
9 | },
10 | sl_ie11: {
11 | base: 'SauceLabs',
12 | browserName: 'internet explorer',
13 | platform: 'Windows 10',
14 | version: '11.0'
15 | },
16 | sl_edge: {
17 | base: 'SauceLabs',
18 | browserName: 'microsoftedge',
19 | platform: 'Windows 10',
20 | version: '20.10240'
21 | },
22 | sl_chrome_1: {
23 | base: 'SauceLabs',
24 | browserName: 'chrome',
25 | platform: 'Linux',
26 | version: '26'
27 | },
28 | sl_chrome_2: {
29 | base: 'SauceLabs',
30 | browserName: 'chrome',
31 | platform: 'Linux',
32 | version: '46'
33 | },
34 | sl_firefox_1: {
35 | base: 'SauceLabs',
36 | browserName: 'firefox',
37 | platform: 'Linux',
38 | version: '13'
39 | },
40 | sl_firefox_2: {
41 | base: 'SauceLabs',
42 | browserName: 'firefox',
43 | platform: 'Linux',
44 | version: '42'
45 | },
46 | sl_android_1: {
47 | base: 'SauceLabs',
48 | browserName: 'android',
49 | platform: 'Linux',
50 | version: '4.4'
51 | },
52 | sl_android_2: {
53 | base: 'SauceLabs',
54 | browserName: 'android',
55 | platform: 'Linux',
56 | version: '5.1'
57 | },
58 | sl_iphone_1: {
59 | base: 'SauceLabs',
60 | browserName: 'iPhone',
61 | platform: 'OS X 10.10',
62 | deviceName: 'iPad Simulator',
63 | version: '7.1'
64 | },
65 | sl_iphone_2: {
66 | base: 'SauceLabs',
67 | browserName: 'iPhone',
68 | platform: 'OS X 10.10',
69 | deviceName: 'iPad Simulator',
70 | deviceOrientation: 'portrait',
71 | version: '9.2'
72 | },
73 | sl_safari_1: {
74 | base: 'SauceLabs',
75 | browserName: 'safari',
76 | platform: 'OS X 10.8',
77 | version: '6.0'
78 | },
79 | sl_safari_2: {
80 | base: 'SauceLabs',
81 | browserName: 'safari',
82 | platform: 'OS X 10.11',
83 | version: '9.0'
84 | }
85 | }
86 |
87 | config.set({
88 | // base path, that will be used to resolve files and exclude
89 | basePath: './',
90 |
91 | // frameworks to use
92 | frameworks: ['jasmine'],
93 |
94 | // list of files / patterns to load in the browser
95 | files: [
96 | 'bower_components/angular/angular.js',
97 | 'bower_components/angular-mocks/angular-mocks.js',
98 | 'bower_components/flow.js/dist/flow.js',
99 | 'src/**/*.js',
100 | 'test/*.spec.js'
101 | ],
102 |
103 | // list of files to exclude
104 | exclude: [
105 |
106 | ],
107 |
108 | // test results reporter to use
109 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
110 | reporters: ['progress', 'coverage', 'saucelabs'],
111 |
112 | // web server port
113 | port: 9876,
114 |
115 | // enable / disable colors in the output (reporters and logs)
116 | colors: true,
117 |
118 | // level of logging
119 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
120 | logLevel: config.LOG_INFO,
121 |
122 | // enable / disable watching file and executing tests whenever any file changes
123 | autoWatch: true,
124 |
125 | // If browser does not capture in given timeout [ms], kill it
126 | captureTimeout: 60000,
127 |
128 | // Continuous Integration mode
129 | // if true, it capture browsers, run tests and exit
130 | singleRun: true,
131 |
132 | customLaunchers: customLaunchers,
133 |
134 | browsers: Object.keys(customLaunchers),
135 | });
136 | };
137 |
--------------------------------------------------------------------------------
/package.js:
--------------------------------------------------------------------------------
1 | // package metadata file for Meteor.js
2 | var packageName = 'digimet:ng-flow';
3 | var where = 'client'; // where to install: 'client' or 'server'. For both, pass nothing.
4 | var version = '2.7.7';
5 | var summary = 'Flow.js html5 file upload extension on angular.js framework';
6 | var gitLink = 'https://github.com/flowjs/ng-flow.git';
7 | var documentationFile = 'README.md';
8 |
9 | // Meta-data
10 | Package.describe({
11 | name: packageName,
12 | version: version,
13 | summary: summary,
14 | git: gitLink,
15 | documentation: documentationFile
16 | });
17 |
18 | Package.onUse(function(api) {
19 | api.versionsFrom(['METEOR@0.9.0', 'METEOR@1.0']); // Meteor versions
20 |
21 | api.use('angular:angular@1.2.0', where); // Dependencies
22 | api.use('digimet:flowjs@2.13.0', where);
23 |
24 | api.addFiles('./dist/ng-flow.js', where); // Files in use
25 | });
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@flowjs/ng-flow",
3 | "version": "2.7.8",
4 | "description": "Flow.js html5 file upload extension on angular.js framework",
5 | "scripts": {
6 | "test": "grunt test"
7 | },
8 | "main": "dist/ng-flow.js",
9 | "repository": {
10 | "type": "git",
11 | "url": "git://github.com/flowjs/ng-flow.git"
12 | },
13 | "keywords": [
14 | "flow.js",
15 | "flow",
16 | "resumable.js",
17 | "resumable",
18 | "angular",
19 | "angular.js",
20 | "angular-upload",
21 | "file upload",
22 | "upload"
23 | ],
24 | "author": "Aidas Klimas",
25 | "license": "MIT",
26 | "readmeFilename": "README.md",
27 | "devDependencies": {
28 | "grunt": "1.4",
29 | "grunt-bump": "0.8.0",
30 | "grunt-contrib-clean": "2.0.0",
31 | "grunt-contrib-concat": "2.0.0",
32 | "grunt-contrib-uglify": "5.0",
33 | "grunt-karma": "4.0.2",
34 | "karma": "6.3",
35 | "karma-coverage": "2.1.0",
36 | "karma-firefox-launcher": "2.1.2",
37 | "karma-jasmine": "4.0.1",
38 | "karma-sauce-launcher": "4.3.6"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/samples/basic/app.js:
--------------------------------------------------------------------------------
1 | /*global angular */
2 | 'use strict';
3 |
4 | /**
5 | * The main app module
6 | * @name app
7 | * @type {angular.Module}
8 | */
9 | var app = angular.module('app', ['flow'])
10 | .config(['flowFactoryProvider', function (flowFactoryProvider) {
11 | flowFactoryProvider.defaults = {
12 | target: 'upload.php',
13 | permanentErrors: [404, 500, 501],
14 | maxChunkRetries: 1,
15 | chunkRetryInterval: 5000,
16 | simultaneousUploads: 4
17 | };
18 | flowFactoryProvider.on('catchAll', function (event) {
19 | console.log('catchAll', arguments);
20 | });
21 | // Can be used with different implementations of Flow.js
22 | // flowFactoryProvider.factory = fustyFlowFactory;
23 | }]);
--------------------------------------------------------------------------------
/samples/basic/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | basic
5 |
6 |
7 |
8 |
10 |
11 |
15 |
16 |
flow basic example
17 |
18 |
19 |
35 |
36 |
37 |
Transfers:
38 |
39 |
40 | Upload
41 | Pause
42 | Cancel
43 | Size: {{$flow.getSize()}}
44 | Is Uploading: {{$flow.isUploading()}}
45 |
46 |
47 |
48 |
49 | #
50 | Name
51 | Size
52 | Relative Path
53 | Unique Identifier
54 | #Chunks
55 | Progress
56 | Paused
57 | Uploading
58 | Completed
59 | Settings
60 |
61 |
62 |
63 |
64 | {{$index+1}}
65 | {{file.name}}
66 | {{file.size}}
67 | {{file.relativePath}}
68 | {{file.uniqueIdentifier}}
69 | {{file.chunks.length}}
70 | {{file.progress()}}
71 | {{file.paused}}
72 | {{file.isUploading()}}
73 | {{file.isComplete()}}
74 |
75 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
98 | Drag And Drop your file here
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/samples/basic/upload.php:
--------------------------------------------------------------------------------
1 | true,
19 | 'files' => $_FILES,
20 | 'get' => $_GET,
21 | 'post' => $_POST,
22 | //optional
23 | 'flowTotalSize' => isset($_FILES['file']) ? $_FILES['file']['size'] : $_GET['flowTotalSize'],
24 | 'flowIdentifier' => isset($_FILES['file']) ? $_FILES['file']['name'] . '-' . $_FILES['file']['size']
25 | : $_GET['flowIdentifier'],
26 | 'flowFilename' => isset($_FILES['file']) ? $_FILES['file']['name'] : $_GET['flowFilename'],
27 | 'flowRelativePath' => isset($_FILES['file']) ? $_FILES['file']['tmp_name'] : $_GET['flowRelativePath']
28 | ]);
29 |
--------------------------------------------------------------------------------
/samples/dataurl/app.js:
--------------------------------------------------------------------------------
1 | /*global angular */
2 | 'use strict';
3 |
4 | /**
5 | * The main app module
6 | * @name app
7 | * @type {angular.Module}
8 | */
9 | var app = angular.module('app', ['flow'])
10 | .config(['flowFactoryProvider', function (flowFactoryProvider) {
11 | flowFactoryProvider.defaults = {
12 | target: 'upload.php',
13 | permanentErrors: [404, 500, 501],
14 | maxChunkRetries: 1,
15 | chunkRetryInterval: 5000,
16 | simultaneousUploads: 4
17 | };
18 | flowFactoryProvider.on('catchAll', function (event) {
19 | console.log('catchAll', arguments);
20 | });
21 | // Can be used with different implementations of Flow.js
22 | // flowFactoryProvider.factory = fustyFlowFactory;
23 | }]).directive('appDownloadUrl', [function () {
24 | return {
25 | restrict: 'A',
26 | link: function(scope, element, attrs) {
27 | element.bind('dragstart', function (event) {
28 | var config = scope.$eval(attrs.appDownloadUrl);
29 | if (!config.disabled) {
30 | var data = config.mime + ':' + config.name + ':' + window.location.href + config.url;
31 | event.dataTransfer.setData('DownloadURL', data);
32 | }
33 | });
34 | }
35 | };
36 | }]).directive("appDragstart", [function () {
37 | return function(scope, element, attrs) {
38 | element.bind('dragstart', function (event) {
39 | scope.$eval(attrs.appDragstart);
40 | });
41 | }
42 | }]).directive("appDragend", [function () {
43 | return function(scope, element, attrs) {
44 | element.bind('dragend', function (event) {
45 | scope.$eval(attrs.appDragend);
46 | });
47 | }
48 | }]).run(function ($rootScope) {
49 | $rootScope.dropEnabled = true;
50 | });
--------------------------------------------------------------------------------
/samples/dataurl/flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flowjs/ng-flow/918c047f9a42065386e7446ae456f1f45e52bc6e/samples/dataurl/flow.png
--------------------------------------------------------------------------------
/samples/dataurl/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Download url
5 |
6 |
7 |
8 |
10 |
11 |
15 |
16 |
Flow drag & drop and drop to desktop feature
17 |
Chrome browser only
18 |
19 |
20 |
32 |
33 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/samples/image/app.js:
--------------------------------------------------------------------------------
1 | /*global angular */
2 | 'use strict';
3 |
4 | /**
5 | * The main app module
6 | * @name app
7 | * @type {angular.Module}
8 | */
9 | var app = angular.module('app', ['flow'])
10 | .config(['flowFactoryProvider', function (flowFactoryProvider) {
11 | flowFactoryProvider.defaults = {
12 | target: 'upload.php',
13 | permanentErrors: [404, 500, 501],
14 | maxChunkRetries: 1,
15 | chunkRetryInterval: 5000,
16 | simultaneousUploads: 4,
17 | singleFile: true
18 | };
19 | flowFactoryProvider.on('catchAll', function (event) {
20 | console.log('catchAll', arguments);
21 | });
22 | // Can be used with different implementations of Flow.js
23 | // flowFactoryProvider.factory = fustyFlowFactory;
24 | }]);
25 |
--------------------------------------------------------------------------------
/samples/image/index.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | Image
7 |
8 |
9 |
10 |
12 |
17 |
18 |
19 |
20 |
21 |
flow image example
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
39 |
40 | Only PNG,GIF,JPG files allowed.
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/samples/image/upload.php:
--------------------------------------------------------------------------------
1 | true,
18 | 'files' => $_FILES,
19 | 'get' => $_GET,
20 | 'post' => $_POST,
21 | //optional
22 | 'flowTotalSize' => isset($_FILES['file']) ? $_FILES['file']['size'] : $_GET['flowTotalSize'],
23 | 'flowIdentifier' => isset($_FILES['file']) ? $_FILES['file']['name'] . '-' . $_FILES['file']['size']
24 | : $_GET['flowIdentifier'],
25 | 'flowFilename' => isset($_FILES['file']) ? $_FILES['file']['name'] : $_GET['flowFilename'],
26 | 'flowRelativePath' => isset($_FILES['file']) ? $_FILES['file']['tmp_name'] : $_GET['flowRelativePath']
27 | ]);
--------------------------------------------------------------------------------
/src/angular-flow.js:
--------------------------------------------------------------------------------
1 | angular.module('flow', ['flow.provider', 'flow.init', 'flow.events', 'flow.btn',
2 | 'flow.drop', 'flow.transfers', 'flow.img', 'flow.dragEvents']);
--------------------------------------------------------------------------------
/src/directives/btn.js:
--------------------------------------------------------------------------------
1 | angular.module('flow.btn', ['flow.init'])
2 | .directive('flowBtn', [function() {
3 | return {
4 | 'restrict': 'EA',
5 | 'scope': false,
6 | 'require': '^flowInit',
7 | 'link': function(scope, element, attrs) {
8 | var isDirectory = attrs.hasOwnProperty('flowDirectory');
9 | var isSingleFile = attrs.hasOwnProperty('flowSingleFile');
10 | var inputAttrs = attrs.hasOwnProperty('flowAttrs') && scope.$eval(attrs.flowAttrs);
11 | scope.$flow.assignBrowse(element, isDirectory, isSingleFile, inputAttrs);
12 | }
13 | };
14 | }]);
--------------------------------------------------------------------------------
/src/directives/drag-events.js:
--------------------------------------------------------------------------------
1 | angular.module('flow.dragEvents', ['flow.init'])
2 | /**
3 | * @name flowPreventDrop
4 | * Prevent loading files then dropped on element
5 | */
6 | .directive('flowPreventDrop', function() {
7 | return {
8 | 'scope': false,
9 | 'link': function(scope, element, attrs) {
10 | element.bind('drop dragover', function (event) {
11 | event.preventDefault();
12 | });
13 | }
14 | };
15 | })
16 | /**
17 | * @name flowDragEnter
18 | * executes `flowDragEnter` and `flowDragLeave` events
19 | */
20 | .directive('flowDragEnter', ['$timeout', function($timeout) {
21 | return {
22 | 'scope': false,
23 | 'link': function(scope, element, attrs) {
24 | var promise;
25 | var enter = false;
26 | element.bind('dragover', function (event) {
27 | if (!isFileDrag(event)) {
28 | return ;
29 | }
30 | if (!enter) {
31 | scope.$apply(attrs.flowDragEnter);
32 | enter = true;
33 | }
34 | $timeout.cancel(promise);
35 | event.preventDefault();
36 | });
37 | element.bind('dragleave drop', function (event) {
38 | $timeout.cancel(promise);
39 | promise = $timeout(function () {
40 | scope.$eval(attrs.flowDragLeave);
41 | promise = null;
42 | enter = false;
43 | }, 100);
44 | });
45 | function isFileDrag(dragEvent) {
46 | var fileDrag = false;
47 | var dataTransfer = dragEvent.dataTransfer || dragEvent.originalEvent.dataTransfer;
48 | angular.forEach(dataTransfer && dataTransfer.types, function(val) {
49 | if (val === 'Files') {
50 | fileDrag = true;
51 | }
52 | });
53 | return fileDrag;
54 | }
55 | }
56 | };
57 | }]);
58 |
--------------------------------------------------------------------------------
/src/directives/drop.js:
--------------------------------------------------------------------------------
1 | angular.module('flow.drop', ['flow.init'])
2 | .directive('flowDrop', function() {
3 | return {
4 | 'scope': false,
5 | 'require': '^flowInit',
6 | 'link': function(scope, element, attrs) {
7 | if (attrs.flowDropEnabled) {
8 | scope.$watch(attrs.flowDropEnabled, function (value) {
9 | if (value) {
10 | assignDrop();
11 | } else {
12 | unAssignDrop();
13 | }
14 | });
15 | } else {
16 | assignDrop();
17 | }
18 | function assignDrop() {
19 | scope.$flow.assignDrop(element);
20 | }
21 | function unAssignDrop() {
22 | scope.$flow.unAssignDrop(element);
23 | }
24 | }
25 | };
26 | });
27 |
--------------------------------------------------------------------------------
/src/directives/events.js:
--------------------------------------------------------------------------------
1 | !function (angular) {'use strict';
2 | var module = angular.module('flow.events', ['flow.init']);
3 | var events = {
4 | fileSuccess: ['$file', '$message'],
5 | fileProgress: ['$file'],
6 | fileAdded: ['$file', '$event'],
7 | filesAdded: ['$files', '$event'],
8 | filesSubmitted: ['$files', '$event'],
9 | fileRetry: ['$file'],
10 | fileRemoved: ['$file'],
11 | fileError: ['$file', '$message'],
12 | uploadStart: [],
13 | complete: [],
14 | progress: [],
15 | error: ['$message', '$file']
16 | };
17 |
18 | angular.forEach(events, function (eventArgs, eventName) {
19 | var name = 'flow' + capitaliseFirstLetter(eventName);
20 | if (name == 'flowUploadStart') {
21 | name = 'flowUploadStarted';// event alias
22 | }
23 | module.directive(name, [function() {
24 | return {
25 | require: '^flowInit',
26 | controller: ['$scope', '$attrs', function ($scope, $attrs) {
27 | $scope.$on('flow::' + eventName, function () {
28 | var funcArgs = Array.prototype.slice.call(arguments);
29 | var event = funcArgs.shift();// remove angular event
30 | // remove flow object and ignore event if it is from parent directive
31 | if ($scope.$flow !== funcArgs.shift()) {
32 | return ;
33 | }
34 | var args = {};
35 | angular.forEach(eventArgs, function(value, key) {
36 | args[value] = funcArgs[key];
37 | });
38 | if ($scope.$eval($attrs[name], args) === false) {
39 | event.preventDefault();
40 | }
41 | });
42 | }]
43 | };
44 | }]);
45 | });
46 |
47 | function capitaliseFirstLetter(string) {
48 | return string.charAt(0).toUpperCase() + string.slice(1);
49 | }
50 | }(angular);
51 |
--------------------------------------------------------------------------------
/src/directives/img.js:
--------------------------------------------------------------------------------
1 | angular.module('flow.img', ['flow.init'])
2 | .directive('flowImg', [function() {
3 | return {
4 | 'scope': false,
5 | 'require': '^flowInit',
6 | 'link': function(scope, element, attrs) {
7 | var file = attrs.flowImg;
8 | scope.$watch(file, function (file) {
9 | if (!file) {
10 | return ;
11 | }
12 | var fileReader = new FileReader();
13 | fileReader.readAsDataURL(file.file);
14 | fileReader.onload = function (event) {
15 | scope.$apply(function () {
16 | attrs.$set('src', event.target.result);
17 | });
18 | };
19 | });
20 | }
21 | };
22 | }]);
--------------------------------------------------------------------------------
/src/directives/init.js:
--------------------------------------------------------------------------------
1 | angular.module('flow.init', ['flow.provider'])
2 | .controller('flowCtrl', ['$scope', '$attrs', '$parse', 'flowFactory',
3 | function ($scope, $attrs, $parse, flowFactory) {
4 |
5 | var options = angular.extend({}, $scope.$eval($attrs.flowInit));
6 |
7 | // use existing flow object or create a new one
8 | var flow = $scope.$eval($attrs.flowObject) || flowFactory.create(options);
9 |
10 | var catchAllHandler = function(eventName){
11 | var args = Array.prototype.slice.call(arguments);
12 | args.shift();
13 | var event = $scope.$broadcast.apply($scope, ['flow::' + eventName, flow].concat(args));
14 | if ({
15 | 'progress':1, 'filesSubmitted':1, 'fileSuccess': 1, 'fileError': 1, 'complete': 1
16 | }[eventName]) {
17 | $scope.$applyAsync();
18 | }
19 | if (event.defaultPrevented) {
20 | return false;
21 | }
22 | };
23 |
24 | flow.on('catchAll', catchAllHandler);
25 | $scope.$on('$destroy', function(){
26 | flow.off('catchAll', catchAllHandler);
27 | });
28 |
29 | $scope.$flow = flow;
30 |
31 | if ($attrs.hasOwnProperty('flowName')) {
32 | $parse($attrs.flowName).assign($scope, flow);
33 | $scope.$on('$destroy', function () {
34 | $parse($attrs.flowName).assign($scope);
35 | });
36 | }
37 | }])
38 | .directive('flowInit', [function() {
39 | return {
40 | scope: true,
41 | controller: 'flowCtrl'
42 | };
43 | }]);
--------------------------------------------------------------------------------
/src/directives/transfers.js:
--------------------------------------------------------------------------------
1 | angular.module('flow.transfers', ['flow.init'])
2 | .directive('flowTransfers', [function() {
3 | return {
4 | 'scope': true,
5 | 'require': '^flowInit',
6 | 'link': function(scope) {
7 | scope.transfers = scope.$flow.files;
8 | }
9 | };
10 | }]);
--------------------------------------------------------------------------------
/src/provider.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @description
3 | * var app = angular.module('App', ['flow.provider'], function(flowFactoryProvider){
4 | * flowFactoryProvider.defaults = {target: '/'};
5 | * });
6 | * @name flowFactoryProvider
7 | */
8 | angular.module('flow.provider', [])
9 | .provider('flowFactory', function() {
10 | 'use strict';
11 | /**
12 | * Define the default properties for flow.js
13 | * @name flowFactoryProvider.defaults
14 | * @type {Object}
15 | */
16 | this.defaults = {};
17 |
18 | /**
19 | * Flow, MaybeFlow or NotFlow
20 | * @name flowFactoryProvider.factory
21 | * @type {function}
22 | * @return {!Flow}
23 | */
24 | this.factory = function (options) {
25 | return new Flow(options);
26 | };
27 |
28 | /**
29 | * Define the default events
30 | * @name flowFactoryProvider.events
31 | * @type {Array}
32 | * @private
33 | */
34 | this.events = [];
35 |
36 | /**
37 | * Add default events
38 | * @name flowFactoryProvider.on
39 | * @function
40 | * @param {string} event
41 | * @param {Function} callback
42 | */
43 | this.on = function (event, callback) {
44 | this.events.push([event, callback]);
45 | };
46 |
47 | this.$get = function() {
48 | var fn = this.factory;
49 | var defaults = this.defaults;
50 | var events = this.events;
51 | return {
52 | 'create': function(opts) {
53 | // combine default options with global options and options
54 | var flow = fn(angular.extend({}, defaults, opts));
55 | angular.forEach(events, function (event) {
56 | flow.on(event[0], event[1]);
57 | });
58 | return flow;
59 | }
60 | };
61 | };
62 | });
63 |
--------------------------------------------------------------------------------
/test/events.spec.js:
--------------------------------------------------------------------------------
1 | describe('events', function () {
2 | var $compile;
3 | var $rootScope;
4 | var element;
5 | var elementScope;
6 |
7 | beforeEach(module('flow'));
8 |
9 | beforeEach(inject(function(_$compile_, _$rootScope_) {
10 | $compile = _$compile_;
11 | $rootScope = _$rootScope_;
12 | $rootScope.fileProgress = jasmine.createSpy('fileProgress');
13 | element = $compile('')($rootScope);
16 | $rootScope.$digest();
17 | elementScope = element.scope();
18 | }));
19 |
20 | describe('$broadcast events', function () {
21 | var ngFileProgress;
22 | var ngRootFileProgress;
23 | beforeEach(inject(function(){
24 | ngFileProgress = jasmine.createSpy('fileProgress');
25 | ngRootFileProgress = jasmine.createSpy('ngRootFileProgress');
26 | elementScope.$on('flow::fileProgress', ngFileProgress);
27 | $rootScope.$on('flow::fileProgress', ngRootFileProgress);
28 | elementScope.$flow.fire('fileProgress', 'file');
29 | }));
30 | it('should catch event on element scope', function () {
31 | expect(ngFileProgress).toHaveBeenCalled();
32 | expect(ngFileProgress.calls.count()).toBe(1);
33 | var args = ngFileProgress.calls.mostRecent().args;
34 | expect(args[1]).toBe(elementScope.$flow);
35 | expect(args[2]).toBe('file');
36 | });
37 | it('should not catch event on parent scope', function () {
38 | expect(ngRootFileProgress).not.toHaveBeenCalled();
39 | });
40 | });
41 |
42 | describe('uploadStart event should be aliased', function () {
43 | var uploadStart;
44 | beforeEach(inject(function(){
45 | $rootScope.uploadStart = jasmine.createSpy('uploadStart');
46 | element = $compile('' +
47 | '
')($rootScope);
48 | $rootScope.$digest();
49 | elementScope = element.scope();
50 | }));
51 | it('should catch broadcast event', function () {
52 | uploadStart = jasmine.createSpy('uploadStart');
53 | elementScope.$on('flow::uploadStart', uploadStart);
54 | elementScope.$flow.fire('uploadStart');
55 | expect(uploadStart.calls.count()).toBe(1);
56 | });
57 | it('should execute scope function', function () {
58 | elementScope.$flow.fire('uploadStart');
59 | expect($rootScope.uploadStart.calls.count()).toBe(1);
60 | });
61 | });
62 |
63 | it('should call event', function () {
64 | elementScope.$flow.fire('fileProgress', 'file');
65 | expect($rootScope.fileProgress).toHaveBeenCalledWith('file');
66 | expect($rootScope.fileProgress.calls.count()).toBe(2);
67 | });
68 |
69 | describe('nested flow directives', function () {
70 | var flowScope1;
71 | var flowScope2;
72 | beforeEach(inject(function() {
73 | $rootScope.fileProgress1 = jasmine.createSpy('fileProgress1');
74 | $rootScope.fileProgress2 = jasmine.createSpy('fileProgress2');
75 | element = $compile('')($rootScope);
78 | $rootScope.$digest();
79 | flowScope1 = element.scope();
80 | flowScope2 = flowScope1.$$childHead;
81 | }));
82 | it('should not call event of child directive', function () {
83 | flowScope1.$flow.fire('fileProgress', 'file1');
84 | expect($rootScope.fileProgress1).toHaveBeenCalledWith('file1');
85 | expect($rootScope.fileProgress2).not.toHaveBeenCalled();
86 | });
87 | it('should not call event of parent directive', function () {
88 | flowScope2.$flow.fire('fileProgress', 'file2');
89 | expect($rootScope.fileProgress2).toHaveBeenCalledWith('file2');
90 | expect($rootScope.fileProgress1).not.toHaveBeenCalled();
91 | });
92 | });
93 | });
94 |
--------------------------------------------------------------------------------
/test/init.spec.js:
--------------------------------------------------------------------------------
1 | describe('init', function() {
2 | var $compile;
3 | var $rootScope;
4 | var element;
5 | var elementScope;
6 | var flowFactory;
7 |
8 | beforeEach(module('flow'));
9 |
10 | beforeEach(inject(function(_$compile_, _$rootScope_, _flowFactory_){
11 | $compile = _$compile_;
12 | $rootScope = _$rootScope_;
13 | flowFactory = _flowFactory_;
14 | element = $compile('
')($rootScope);
15 | $rootScope.$digest();
16 | elementScope = element.scope();
17 | }));
18 |
19 | it('should assign $flow to element scope', function() {
20 | expect(elementScope.$flow).toBeDefined();
21 | expect($rootScope.$flow).toBeUndefined();
22 | });
23 |
24 | describe('flow-name', function () {
25 | beforeEach(function () {
26 | $rootScope.obj = {flow:''};
27 | element = $compile('
')($rootScope);
28 | $rootScope.$digest();
29 | elementScope = element.scope();
30 | });
31 |
32 | it('should assign $flow reference to element scope', function () {
33 | expect(elementScope.$flow).toBeDefined();
34 | expect($rootScope.$flow).toBeUndefined();
35 | });
36 |
37 | it('should assign $flow to given scope', function() {
38 | expect(elementScope.obj.flow).toBeDefined();
39 | expect(elementScope.obj.flow instanceof Flow).toBeTruthy();
40 | expect($rootScope.obj.flow).toBeDefined();
41 | expect($rootScope.obj.flow instanceof Flow).toBeTruthy();
42 | expect($rootScope.obj.flow).toBe(elementScope.obj.flow);
43 | });
44 |
45 | it('should destroy $flow', function() {
46 | expect($rootScope.obj.flow).toBeDefined();
47 | expect($rootScope.obj.flow instanceof Flow).toBeTruthy();
48 | elementScope.$destroy();
49 | expect($rootScope.obj.flow).toBeUndefined();
50 | });
51 | });
52 |
53 | describe('flow-object', function () {
54 | it('should create a new flow object', function () {
55 | spyOn(flowFactory, 'create').and.callThrough();
56 | $compile('
')($rootScope);
57 | $rootScope.$digest();
58 | expect(flowFactory.create).toHaveBeenCalled();
59 | });
60 | it('should init with the existing flow object', function () {
61 | $rootScope.existingFlow = flowFactory.create();
62 | spyOn(flowFactory, 'create').and.callThrough();
63 | element = $compile('
')($rootScope);
64 | elementScope = element.scope();
65 | $rootScope.$digest();
66 | expect(flowFactory.create).not.toHaveBeenCalled();
67 | expect($rootScope.existingFlow).toBe(elementScope.$flow);
68 | });
69 | it('should remove event handlers from flow when the scope is destroyed', function () {
70 | $rootScope.existingFlow = flowFactory.create();
71 | element = $compile('
')($rootScope);
72 | elementScope = element.scope();
73 |
74 | $compile('
')($rootScope);
75 | $rootScope.$digest();
76 |
77 | var scopePrototype = Object.getPrototypeOf(elementScope);
78 | spyOn(scopePrototype, '$broadcast').and.callThrough();
79 |
80 | $rootScope.existingFlow.fire('fileProgress', 'file');
81 | expect(elementScope.$broadcast.calls.count()).toEqual(2);
82 |
83 | elementScope.$destroy();
84 |
85 | elementScope.$broadcast.calls.reset();
86 |
87 | $rootScope.existingFlow.fire('fileProgress', 'file');
88 | expect(elementScope.$broadcast.calls.count()).toEqual(1);
89 | });
90 | });
91 | });
92 |
--------------------------------------------------------------------------------
/travis.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | if [ $TEST = "unit-tests" ]; then
6 | echo "Running unit-tests"
7 | grunt karma:coverage
8 | CODECLIMATE_REPO_TOKEN=9fcd24bf39f62bd186255d37ee66d0b1aaca8be7ea6616f074dd6c06a86720ab codeclimate-test-reporter < coverage/*/lcov.info
9 |
10 | elif [[ $TEST = "browser-tests" ]]; then
11 |
12 | echo "Running browser-tests"
13 | grunt karma:saucelabs
14 |
15 | fi
16 |
--------------------------------------------------------------------------------