UNPKG

generator-gsndnn

Version:

Scaffolds DNN extensions, including Modules (Webforms, SPA, and MVC), Persona Bar, Skin Object, Library, Scheduler, and Hotcakes Commerce projects (based on the generator built by Matt Rutledge).

1,765 lines (1,620 loc) 54.5 kB
/** * @license MIT */ (function(window, document, undefined) {'use strict'; // ie10+ var ie10plus = window.navigator.msPointerEnabled; /** * Flow.js is a library providing multiple simultaneous, stable and * resumable uploads via the HTML5 File API. * @param [opts] * @param {number} [opts.chunkSize] * @param {bool} [opts.forceChunkSize] * @param {number} [opts.simultaneousUploads] * @param {bool} [opts.singleFile] * @param {string} [opts.fileParameterName] * @param {number} [opts.progressCallbacksInterval] * @param {number} [opts.speedSmoothingFactor] * @param {Object|Function} [opts.query] * @param {Object|Function} [opts.headers] * @param {bool} [opts.withCredentials] * @param {Function} [opts.preprocess] * @param {string} [opts.method] * @param {string|Function} [opts.testMethod] * @param {string|Function} [opts.uploadMethod] * @param {bool} [opts.prioritizeFirstAndLastChunk] * @param {bool} [opts.allowDuplicateUploads] * @param {string|Function} [opts.target] * @param {number} [opts.maxChunkRetries] * @param {number} [opts.chunkRetryInterval] * @param {Array.<number>} [opts.permanentErrors] * @param {Array.<number>} [opts.successStatuses] * @param {Function} [opts.initFileFn] * @param {Function} [opts.readFileFn] * @param {Function} [opts.generateUniqueIdentifier] * @constructor */ function Flow(opts) { /** * Supported by browser? * @type {boolean} */ this.support = ( typeof File !== 'undefined' && typeof Blob !== 'undefined' && typeof FileList !== 'undefined' && ( !!Blob.prototype.slice || !!Blob.prototype.webkitSlice || !!Blob.prototype.mozSlice || false ) // slicing files support ); if (!this.support) { return ; } /** * Check if directory upload is supported * @type {boolean} */ this.supportDirectory = /Chrome/.test(window.navigator.userAgent); /** * List of FlowFile objects * @type {Array.<FlowFile>} */ this.files = []; /** * Default options for flow.js * @type {Object} */ this.defaults = { chunkSize: 1024 * 1024, forceChunkSize: false, simultaneousUploads: 3, singleFile: false, fileParameterName: 'file', progressCallbacksInterval: 500, speedSmoothingFactor: 0.1, query: {}, headers: {}, withCredentials: false, preprocess: null, method: 'multipart', testMethod: 'GET', uploadMethod: 'POST', prioritizeFirstAndLastChunk: false, allowDuplicateUploads: false, target: '/', testChunks: true, generateUniqueIdentifier: null, maxChunkRetries: 0, chunkRetryInterval: null, permanentErrors: [404, 415, 500, 501], successStatuses: [200, 201, 202], onDropStopPropagation: false, initFileFn: null, readFileFn: webAPIFileRead }; /** * Current options * @type {Object} */ this.opts = {}; /** * List of events: * key stands for event name * value array list of callbacks * @type {} */ this.events = {}; var $ = this; /** * On drop event * @function * @param {MouseEvent} event */ this.onDrop = function (event) { if ($.opts.onDropStopPropagation) { event.stopPropagation(); } event.preventDefault(); var dataTransfer = event.dataTransfer; if (dataTransfer.items && dataTransfer.items[0] && dataTransfer.items[0].webkitGetAsEntry) { $.webkitReadDataTransfer(event); } else { $.addFiles(dataTransfer.files, event); } }; /** * Prevent default * @function * @param {MouseEvent} event */ this.preventEvent = function (event) { event.preventDefault(); }; /** * Current options * @type {Object} */ this.opts = Flow.extend({}, this.defaults, opts || {}); } Flow.prototype = { /** * Set a callback for an event, possible events: * fileSuccess(file), fileProgress(file), fileAdded(file, event), * fileRetry(file), fileError(file, message), complete(), * progress(), error(message, file), pause() * @function * @param {string} event * @param {Function} callback */ on: function (event, callback) { event = event.toLowerCase(); if (!this.events.hasOwnProperty(event)) { this.events[event] = []; } this.events[event].push(callback); }, /** * Remove event callback * @function * @param {string} [event] removes all events if not specified * @param {Function} [fn] removes all callbacks of event if not specified */ off: function (event, fn) { if (event !== undefined) { event = event.toLowerCase(); if (fn !== undefined) { if (this.events.hasOwnProperty(event)) { arrayRemove(this.events[event], fn); } } else { delete this.events[event]; } } else { this.events = {}; } }, /** * Fire an event * @function * @param {string} event event name * @param {...} args arguments of a callback * @return {bool} value is false if at least one of the event handlers which handled this event * returned false. Otherwise it returns true. */ fire: function (event, args) { // `arguments` is an object, not array, in FF, so: args = Array.prototype.slice.call(arguments); event = event.toLowerCase(); var preventDefault = false; if (this.events.hasOwnProperty(event)) { each(this.events[event], function (callback) { preventDefault = callback.apply(this, args.slice(1)) === false || preventDefault; }, this); } if (event != 'catchall') { args.unshift('catchAll'); preventDefault = this.fire.apply(this, args) === false || preventDefault; } return !preventDefault; }, /** * Read webkit dataTransfer object * @param event */ webkitReadDataTransfer: function (event) { var $ = this; var queue = event.dataTransfer.items.length; var files = []; each(event.dataTransfer.items, function (item) { var entry = item.webkitGetAsEntry(); if (!entry) { decrement(); return ; } if (entry.isFile) { // due to a bug in Chrome's File System API impl - #149735 fileReadSuccess(item.getAsFile(), entry.fullPath); } else { readDirectory(entry.createReader()); } }); function readDirectory(reader) { reader.readEntries(function (entries) { if (entries.length) { queue += entries.length; each(entries, function(entry) { if (entry.isFile) { var fullPath = entry.fullPath; entry.file(function (file) { fileReadSuccess(file, fullPath); }, readError); } else if (entry.isDirectory) { readDirectory(entry.createReader()); } }); readDirectory(reader); } else { decrement(); } }, readError); } function fileReadSuccess(file, fullPath) { // relative path should not start with "/" file.relativePath = fullPath.substring(1); files.push(file); decrement(); } function readError(fileError) { throw fileError; } function decrement() { if (--queue == 0) { $.addFiles(files, event); } } }, /** * Generate unique identifier for a file * @function * @param {FlowFile} file * @returns {string} */ generateUniqueIdentifier: function (file) { var custom = this.opts.generateUniqueIdentifier; if (typeof custom === 'function') { return custom(file); } // Some confusion in different versions of Firefox var relativePath = file.relativePath || file.webkitRelativePath || file.fileName || file.name; return file.size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, ''); }, /** * Upload next chunk from the queue * @function * @returns {boolean} * @private */ uploadNextChunk: function (preventEvents) { // In some cases (such as videos) it's really handy to upload the first // and last chunk of a file quickly; this let's the server check the file's // metadata and determine if there's even a point in continuing. var found = false; if (this.opts.prioritizeFirstAndLastChunk) { each(this.files, function (file) { if (!file.paused && file.chunks.length && file.chunks[0].status() === 'pending') { file.chunks[0].send(); found = true; return false; } if (!file.paused && file.chunks.length > 1 && file.chunks[file.chunks.length - 1].status() === 'pending') { file.chunks[file.chunks.length - 1].send(); found = true; return false; } }); if (found) { return found; } } // Now, simply look for the next, best thing to upload each(this.files, function (file) { if (!file.paused) { each(file.chunks, function (chunk) { if (chunk.status() === 'pending') { chunk.send(); found = true; return false; } }); } if (found) { return false; } }); if (found) { return true; } // The are no more outstanding chunks to upload, check is everything is done var outstanding = false; each(this.files, function (file) { if (!file.isComplete()) { outstanding = true; return false; } }); if (!outstanding && !preventEvents) { // All chunks have been uploaded, complete async(function () { this.fire('complete'); }, this); } return false; }, /** * Assign a browse action to one or more DOM nodes. * @function * @param {Element|Array.<Element>} domNodes * @param {boolean} isDirectory Pass in true to allow directories to * @param {boolean} singleFile prevent multi file upload * @param {Object} attributes set custom attributes: * http://www.w3.org/TR/html-markup/input.file.html#input.file-attributes * eg: accept: 'image/*' * be selected (Chrome only). */ assignBrowse: function (domNodes, isDirectory, singleFile, attributes) { if (typeof domNodes.length === 'undefined') { domNodes = [domNodes]; } each(domNodes, function (domNode) { var input; if (domNode.tagName === 'INPUT' && domNode.type === 'file') { input = domNode; } else { input = document.createElement('input'); input.setAttribute('type', 'file'); // display:none - not working in opera 12 extend(input.style, { visibility: 'hidden', position: 'absolute', width: '1px', height: '1px' }); // for opera 12 browser, input must be assigned to a document domNode.appendChild(input); // https://developer.mozilla.org/en/using_files_from_web_applications) // event listener is executed two times // first one - original mouse click event // second - input.click(), input is inside domNode domNode.addEventListener('click', function() { input.click(); }, false); } if (!this.opts.singleFile && !singleFile) { input.setAttribute('multiple', 'multiple'); } if (isDirectory) { input.setAttribute('webkitdirectory', 'webkitdirectory'); } each(attributes, function (value, key) { input.setAttribute(key, value); }); // When new files are added, simply append them to the overall list var $ = this; input.addEventListener('change', function (e) { if (e.target.value) { $.addFiles(e.target.files, e); e.target.value = ''; } }, false); }, this); }, /** * Assign one or more DOM nodes as a drop target. * @function * @param {Element|Array.<Element>} domNodes */ assignDrop: function (domNodes) { if (typeof domNodes.length === 'undefined') { domNodes = [domNodes]; } each(domNodes, function (domNode) { domNode.addEventListener('dragover', this.preventEvent, false); domNode.addEventListener('dragenter', this.preventEvent, false); domNode.addEventListener('drop', this.onDrop, false); }, this); }, /** * Un-assign drop event from DOM nodes * @function * @param domNodes */ unAssignDrop: function (domNodes) { if (typeof domNodes.length === 'undefined') { domNodes = [domNodes]; } each(domNodes, function (domNode) { domNode.removeEventListener('dragover', this.preventEvent); domNode.removeEventListener('dragenter', this.preventEvent); domNode.removeEventListener('drop', this.onDrop); }, this); }, /** * Returns a boolean indicating whether or not the instance is currently * uploading anything. * @function * @returns {boolean} */ isUploading: function () { var uploading = false; each(this.files, function (file) { if (file.isUploading()) { uploading = true; return false; } }); return uploading; }, /** * should upload next chunk * @function * @returns {boolean|number} */ _shouldUploadNext: function () { var num = 0; var should = true; var simultaneousUploads = this.opts.simultaneousUploads; each(this.files, function (file) { each(file.chunks, function(chunk) { if (chunk.status() === 'uploading') { num++; if (num >= simultaneousUploads) { should = false; return false; } } }); }); // if should is true then return uploading chunks's length return should && num; }, /** * Start or resume uploading. * @function */ upload: function () { // Make sure we don't start too many uploads at once var ret = this._shouldUploadNext(); if (ret === false) { return; } // Kick off the queue this.fire('uploadStart'); var started = false; for (var num = 1; num <= this.opts.simultaneousUploads - ret; num++) { started = this.uploadNextChunk(true) || started; } if (!started) { async(function () { this.fire('complete'); }, this); } }, /** * Resume uploading. * @function */ resume: function () { each(this.files, function (file) { file.resume(); }); }, /** * Pause uploading. * @function */ pause: function () { each(this.files, function (file) { file.pause(); }); }, /** * Cancel upload of all FlowFile objects and remove them from the list. * @function */ cancel: function () { for (var i = this.files.length - 1; i >= 0; i--) { this.files[i].cancel(); } }, /** * Returns a number between 0 and 1 indicating the current upload progress * of all files. * @function * @returns {number} */ progress: function () { var totalDone = 0; var totalSize = 0; // Resume all chunks currently being uploaded each(this.files, function (file) { totalDone += file.progress() * file.size; totalSize += file.size; }); return totalSize > 0 ? totalDone / totalSize : 0; }, /** * Add a HTML5 File object to the list of files. * @function * @param {File} file * @param {Event} [event] event is optional */ addFile: function (file, event) { this.addFiles([file], event); }, /** * Add a HTML5 File object to the list of files. * @function * @param {FileList|Array} fileList * @param {Event} [event] event is optional */ addFiles: function (fileList, event) { var files = []; each(fileList, function (file) { // https://github.com/flowjs/flow.js/issues/55 if ((!ie10plus || ie10plus && file.size > 0) && !(file.size % 4096 === 0 && (file.name === '.' || file.fileName === '.')) && (this.opts.allowDuplicateUploads || !this.getFromUniqueIdentifier(this.generateUniqueIdentifier(file)))) { var f = new FlowFile(this, file); if (this.fire('fileAdded', f, event)) { files.push(f); } } }, this); if (this.fire('filesAdded', files, event)) { each(files, function (file) { if (this.opts.singleFile && this.files.length > 0) { this.removeFile(this.files[0]); } this.files.push(file); }, this); } this.fire('filesSubmitted', files, event); }, /** * Cancel upload of a specific FlowFile object from the list. * @function * @param {FlowFile} file */ removeFile: function (file) { for (var i = this.files.length - 1; i >= 0; i--) { if (this.files[i] === file) { this.files.splice(i, 1); file.abort(); } } }, /** * Look up a FlowFile object by its unique identifier. * @function * @param {string} uniqueIdentifier * @returns {boolean|FlowFile} false if file was not found */ getFromUniqueIdentifier: function (uniqueIdentifier) { var ret = false; each(this.files, function (file) { if (file.uniqueIdentifier === uniqueIdentifier) { ret = file; } }); return ret; }, /** * Returns the total size of all files in bytes. * @function * @returns {number} */ getSize: function () { var totalSize = 0; each(this.files, function (file) { totalSize += file.size; }); return totalSize; }, /** * Returns the total size uploaded of all files in bytes. * @function * @returns {number} */ sizeUploaded: function () { var size = 0; each(this.files, function (file) { size += file.sizeUploaded(); }); return size; }, /** * Returns remaining time to upload all files in seconds. Accuracy is based on average speed. * If speed is zero, time remaining will be equal to positive infinity `Number.POSITIVE_INFINITY` * @function * @returns {number} */ timeRemaining: function () { var sizeDelta = 0; var averageSpeed = 0; each(this.files, function (file) { if (!file.paused && !file.error) { sizeDelta += file.size - file.sizeUploaded(); averageSpeed += file.averageSpeed; } }); if (sizeDelta && !averageSpeed) { return Number.POSITIVE_INFINITY; } if (!sizeDelta && !averageSpeed) { return 0; } return Math.floor(sizeDelta / averageSpeed); } }; /** * FlowFile class * @name FlowFile * @param {Flow} flowObj * @param {File} file * @constructor */ function FlowFile(flowObj, file) { /** * Reference to parent Flow instance * @type {Flow} */ this.flowObj = flowObj; /** * Used to store the bytes read * @type {Blob|string} */ this.bytes = null; /** * Reference to file * @type {File} */ this.file = file; /** * File name. Some confusion in different versions of Firefox * @type {string} */ this.name = file.fileName || file.name; /** * File size * @type {number} */ this.size = file.size; /** * Relative file path * @type {string} */ this.relativePath = file.relativePath || file.webkitRelativePath || this.name; /** * File unique identifier * @type {string} */ this.uniqueIdentifier = flowObj.generateUniqueIdentifier(file); /** * List of chunks * @type {Array.<FlowChunk>} */ this.chunks = []; /** * Indicated if file is paused * @type {boolean} */ this.paused = false; /** * Indicated if file has encountered an error * @type {boolean} */ this.error = false; /** * Average upload speed * @type {number} */ this.averageSpeed = 0; /** * Current upload speed * @type {number} */ this.currentSpeed = 0; /** * Date then progress was called last time * @type {number} * @private */ this._lastProgressCallback = Date.now(); /** * Previously uploaded file size * @type {number} * @private */ this._prevUploadedSize = 0; /** * Holds previous progress * @type {number} * @private */ this._prevProgress = 0; this.bootstrap(); } FlowFile.prototype = { /** * Update speed parameters * @link http://stackoverflow.com/questions/2779600/how-to-estimate-download-time-remaining-accurately * @function */ measureSpeed: function () { var timeSpan = Date.now() - this._lastProgressCallback; if (!timeSpan) { return ; } var smoothingFactor = this.flowObj.opts.speedSmoothingFactor; var uploaded = this.sizeUploaded(); // Prevent negative upload speed after file upload resume this.currentSpeed = Math.max((uploaded - this._prevUploadedSize) / timeSpan * 1000, 0); this.averageSpeed = smoothingFactor * this.currentSpeed + (1 - smoothingFactor) * this.averageSpeed; this._prevUploadedSize = uploaded; }, /** * For internal usage only. * Callback when something happens within the chunk. * @function * @param {FlowChunk} chunk * @param {string} event can be 'progress', 'success', 'error' or 'retry' * @param {string} [message] */ chunkEvent: function (chunk, event, message) { switch (event) { case 'progress': if (Date.now() - this._lastProgressCallback < this.flowObj.opts.progressCallbacksInterval) { break; } this.measureSpeed(); this.flowObj.fire('fileProgress', this, chunk); this.flowObj.fire('progress'); this._lastProgressCallback = Date.now(); break; case 'error': this.error = true; this.abort(true); this.flowObj.fire('fileError', this, message, chunk); this.flowObj.fire('error', message, this, chunk); break; case 'success': if (this.error) { return; } this.measureSpeed(); this.flowObj.fire('fileProgress', this, chunk); this.flowObj.fire('progress'); this._lastProgressCallback = Date.now(); if (this.isComplete()) { this.currentSpeed = 0; this.averageSpeed = 0; this.flowObj.fire('fileSuccess', this, message, chunk); } break; case 'retry': this.flowObj.fire('fileRetry', this, chunk); break; } }, /** * Pause file upload * @function */ pause: function() { this.paused = true; this.abort(); }, /** * Resume file upload * @function */ resume: function() { this.paused = false; this.flowObj.upload(); }, /** * Abort current upload * @function */ abort: function (reset) { this.currentSpeed = 0; this.averageSpeed = 0; var chunks = this.chunks; if (reset) { this.chunks = []; } each(chunks, function (c) { if (c.status() === 'uploading') { c.abort(); this.flowObj.uploadNextChunk(); } }, this); }, /** * Cancel current upload and remove from a list * @function */ cancel: function () { this.flowObj.removeFile(this); }, /** * Retry aborted file upload * @function */ retry: function () { this.bootstrap(); this.flowObj.upload(); }, /** * Clear current chunks and slice file again * @function */ bootstrap: function () { if (typeof this.flowObj.opts.initFileFn === "function") { this.flowObj.opts.initFileFn(this); } this.abort(true); this.error = false; // Rebuild stack of chunks from file this._prevProgress = 0; var round = this.flowObj.opts.forceChunkSize ? Math.ceil : Math.floor; var chunks = Math.max( round(this.size / this.flowObj.opts.chunkSize), 1 ); for (var offset = 0; offset < chunks; offset++) { this.chunks.push( new FlowChunk(this.flowObj, this, offset) ); } }, /** * Get current upload progress status * @function * @returns {number} from 0 to 1 */ progress: function () { if (this.error) { return 1; } if (this.chunks.length === 1) { this._prevProgress = Math.max(this._prevProgress, this.chunks[0].progress()); return this._prevProgress; } // Sum up progress across everything var bytesLoaded = 0; each(this.chunks, function (c) { // get chunk progress relative to entire file bytesLoaded += c.progress() * (c.endByte - c.startByte); }); var percent = bytesLoaded / this.size; // We don't want to lose percentages when an upload is paused this._prevProgress = Math.max(this._prevProgress, percent > 0.9999 ? 1 : percent); return this._prevProgress; }, /** * Indicates if file is being uploaded at the moment * @function * @returns {boolean} */ isUploading: function () { var uploading = false; each(this.chunks, function (chunk) { if (chunk.status() === 'uploading') { uploading = true; return false; } }); return uploading; }, /** * Indicates if file is has finished uploading and received a response * @function * @returns {boolean} */ isComplete: function () { var outstanding = false; each(this.chunks, function (chunk) { var status = chunk.status(); if (status === 'pending' || status === 'uploading' || status === 'reading' || chunk.preprocessState === 1 || chunk.readState === 1) { outstanding = true; return false; } }); return !outstanding; }, /** * Count total size uploaded * @function * @returns {number} */ sizeUploaded: function () { var size = 0; each(this.chunks, function (chunk) { size += chunk.sizeUploaded(); }); return size; }, /** * Returns remaining time to finish upload file in seconds. Accuracy is based on average speed. * If speed is zero, time remaining will be equal to positive infinity `Number.POSITIVE_INFINITY` * @function * @returns {number} */ timeRemaining: function () { if (this.paused || this.error) { return 0; } var delta = this.size - this.sizeUploaded(); if (delta && !this.averageSpeed) { return Number.POSITIVE_INFINITY; } if (!delta && !this.averageSpeed) { return 0; } return Math.floor(delta / this.averageSpeed); }, /** * Get file type * @function * @returns {string} */ getType: function () { return this.file.type && this.file.type.split('/')[1]; }, /** * Get file extension * @function * @returns {string} */ getExtension: function () { return this.name.substr((~-this.name.lastIndexOf(".") >>> 0) + 2).toLowerCase(); } }; /** * Default read function using the webAPI * * @function webAPIFileRead(fileObj, fileType, startByte, endByte, chunk) * */ function webAPIFileRead(fileObj, startByte, endByte, fileType, chunk) { var function_name = 'slice'; if (fileObj.file.slice) function_name = 'slice'; else if (fileObj.file.mozSlice) function_name = 'mozSlice'; else if (fileObj.file.webkitSlice) function_name = 'webkitSlice'; chunk.readFinished(fileObj.file[function_name](startByte, endByte, fileType)); } /** * Class for storing a single chunk * @name FlowChunk * @param {Flow} flowObj * @param {FlowFile} fileObj * @param {number} offset * @constructor */ function FlowChunk(flowObj, fileObj, offset) { /** * Reference to parent flow object * @type {Flow} */ this.flowObj = flowObj; /** * Reference to parent FlowFile object * @type {FlowFile} */ this.fileObj = fileObj; /** * File offset * @type {number} */ this.offset = offset; /** * Indicates if chunk existence was checked on the server * @type {boolean} */ this.tested = false; /** * Number of retries performed * @type {number} */ this.retries = 0; /** * Pending retry * @type {boolean} */ this.pendingRetry = false; /** * Preprocess state * @type {number} 0 = unprocessed, 1 = processing, 2 = finished */ this.preprocessState = 0; /** * Read state * @type {number} 0 = not read, 1 = reading, 2 = finished */ this.readState = 0; /** * Bytes transferred from total request size * @type {number} */ this.loaded = 0; /** * Total request size * @type {number} */ this.total = 0; /** * Size of a chunk * @type {number} */ this.chunkSize = this.flowObj.opts.chunkSize; /** * Chunk start byte in a file * @type {number} */ this.startByte = this.offset * this.chunkSize; /** * Compute the endbyte in a file * */ this.computeEndByte = function() { var endByte = Math.min(this.fileObj.size, (this.offset + 1) * this.chunkSize); if (this.fileObj.size - endByte < this.chunkSize && !this.flowObj.opts.forceChunkSize) { // The last chunk will be bigger than the chunk size, // but less than 2 * this.chunkSize endByte = this.fileObj.size; } return endByte; } /** * Chunk end byte in a file * @type {number} */ this.endByte = this.computeEndByte(); /** * XMLHttpRequest * @type {XMLHttpRequest} */ this.xhr = null; var $ = this; /** * Send chunk event * @param event * @param {...} args arguments of a callback */ this.event = function (event, args) { args = Array.prototype.slice.call(arguments); args.unshift($); $.fileObj.chunkEvent.apply($.fileObj, args); }; /** * Catch progress event * @param {ProgressEvent} event */ this.progressHandler = function(event) { if (event.lengthComputable) { $.loaded = event.loaded ; $.total = event.total; } $.event('progress', event); }; /** * Catch test event * @param {Event} event */ this.testHandler = function(event) { var status = $.status(true); if (status === 'error') { $.event(status, $.message()); $.flowObj.uploadNextChunk(); } else if (status === 'success') { $.tested = true; $.event(status, $.message()); $.flowObj.uploadNextChunk(); } else if (!$.fileObj.paused) { // Error might be caused by file pause method // Chunks does not exist on the server side $.tested = true; $.send(); } }; /** * Upload has stopped * @param {Event} event */ this.doneHandler = function(event) { var status = $.status(); if (status === 'success' || status === 'error') { delete this.data; $.event(status, $.message()); $.flowObj.uploadNextChunk(); } else { $.event('retry', $.message()); $.pendingRetry = true; $.abort(); $.retries++; var retryInterval = $.flowObj.opts.chunkRetryInterval; if (retryInterval !== null) { setTimeout(function () { $.send(); }, retryInterval); } else { $.send(); } } }; } FlowChunk.prototype = { /** * Get params for a request * @function */ 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 }; }, /** * Get target option with query params * @function * @param params * @returns {string} */ getTarget: function(target, params){ if(target.indexOf('?') < 0) { target += '?'; } else { target += '&'; } return target + params.join('&'); }, /** * Makes a GET request without any data to see if the chunk has already * been uploaded in a previous session * @function */ test: function () { // Set up request and listen for event this.xhr = new XMLHttpRequest(); this.xhr.addEventListener("load", this.testHandler, false); this.xhr.addEventListener("error", this.testHandler, false); var testMethod = evalOpts(this.flowObj.opts.testMethod, this.fileObj, this); var data = this.prepareXhrRequest(testMethod, true); this.xhr.send(data); }, /** * Finish preprocess state * @function */ preprocessFinished: function () { // Re-compute the endByte after the preprocess function to allow an // implementer of preprocess to set the fileObj size this.endByte = this.computeEndByte(); this.preprocessState = 2; this.send(); }, /** * Finish read state * @function */ readFinished: function (bytes) { this.readState = 2; this.bytes = bytes; this.send(); }, /** * Uploads the actual data in a POST call * @function */ send: function () { var preprocess = this.flowObj.opts.preprocess; var read = this.flowObj.opts.readFileFn; if (typeof preprocess === 'function') { switch (this.preprocessState) { case 0: this.preprocessState = 1; preprocess(this); return; case 1: return; } } switch (this.readState) { case 0: this.readState = 1; read(this.fileObj, this.startByte, this.endByte, this.fileType, this); return; case 1: return; } if (this.flowObj.opts.testChunks && !this.tested) { this.test(); return; } this.loaded = 0; this.total = 0; this.pendingRetry = false; // Set up request and listen for event this.xhr = new XMLHttpRequest(); this.xhr.upload.addEventListener('progress', this.progressHandler, false); this.xhr.addEventListener("load", this.doneHandler, false); this.xhr.addEventListener("error", this.doneHandler, false); var uploadMethod = evalOpts(this.flowObj.opts.uploadMethod, this.fileObj, this); var data = this.prepareXhrRequest(uploadMethod, false, this.flowObj.opts.method, this.bytes); this.xhr.send(data); }, /** * Abort current xhr request * @function */ abort: function () { // Abort and reset var xhr = this.xhr; this.xhr = null; if (xhr) { xhr.abort(); } }, /** * Retrieve current chunk upload status * @function * @returns {string} 'pending', 'uploading', 'success', 'error' */ status: function (isTest) { if (this.readState === 1) { return 'reading'; } else if (this.pendingRetry || this.preprocessState === 1) { // if pending retry then that's effectively the same as actively uploading, // there might just be a slight delay before the retry starts return 'uploading'; } else if (!this.xhr) { return 'pending'; } else if (this.xhr.readyState < 4) { // Status is really 'OPENED', 'HEADERS_RECEIVED' // or 'LOADING' - meaning that stuff is happening return 'uploading'; } else { if (this.flowObj.opts.successStatuses.indexOf(this.xhr.status) > -1) { // HTTP 200, perfect // HTTP 202 Accepted - The request has been accepted for processing, but the processing has not been completed. return 'success'; } else if (this.flowObj.opts.permanentErrors.indexOf(this.xhr.status) > -1 || !isTest && this.retries >= this.flowObj.opts.maxChunkRetries) { // HTTP 415/500/501, permanent error return 'error'; } else { // this should never happen, but we'll reset and queue a retry // a likely case for this would be 503 service unavailable this.abort(); return 'pending'; } } }, /** * Get response from xhr request * @function * @returns {String} */ message: function () { return this.xhr ? this.xhr.responseText : ''; }, /** * Get upload progress * @function * @returns {number} */ progress: function () { if (this.pendingRetry) { return 0; } var s = this.status(); if (s === 'success' || s === 'error') { return 1; } else if (s === 'pending') { return 0; } else { return this.total > 0 ? this.loaded / this.total : 0; } }, /** * Count total size uploaded * @function * @returns {number} */ sizeUploaded: function () { var size = this.endByte - this.startByte; // can't return only chunk.loaded value, because it is bigger than chunk size if (this.status() !== 'success') { size = this.progress() * size; } return size; }, /** * Prepare Xhr request. Set query, headers and data * @param {string} method GET or POST * @param {bool} isTest is this a test request * @param {string} [paramsMethod] octet or form * @param {Blob} [blob] to send * @returns {FormData|Blob|Null} data to send */ prepareXhrRequest: function(method, isTest, paramsMethod, blob) { // Add data from the query options var query = evalOpts(this.flowObj.opts.query, this.fileObj, this, isTest); query = extend(query, this.getParams()); var target = evalOpts(this.flowObj.opts.target, this.fileObj, this, isTest); var data = null; if (method === 'GET' || paramsMethod === 'octet') { // Add data from the query options var params = []; each(query, function (v, k) { params.push([encodeURIComponent(k), encodeURIComponent(v)].join('=')); }); target = this.getTarget(target, params); data = blob || null; } else { // Add data from the query options data = new FormData(); each(query, function (v, k) { data.append(k, v); }); data.append(this.flowObj.opts.fileParameterName, blob, this.fileObj.file.name); } this.xhr.open(method, target, true); this.xhr.withCredentials = this.flowObj.opts.withCredentials; // Add data from header options each(evalOpts(this.flowObj.opts.headers, this.fileObj, this, isTest), function (v, k) { this.xhr.setRequestHeader(k, v); }, this); return data; } }; /** * Remove value from array * @param array * @param value */ function arrayRemove(array, value) { var index = array.indexOf(value); if (index > -1) { array.splice(index, 1); } } /** * If option is a function, evaluate it with given params * @param {*} data * @param {...} args arguments of a callback * @returns {*} */ function evalOpts(data, args) { if (typeof data === "function") { // `arguments` is an object, not array, in FF, so: args = Array.prototype.slice.call(arguments); data = data.apply(null, args.slice(1)); } return data; } Flow.evalOpts = evalOpts; /** * Execute function asynchronously * @param fn * @param context */ function async(fn, context) { setTimeout(fn.bind(context), 0); } /** * Extends the destination object `dst` by copying all of the properties from * the `src` object(s) to `dst`. You can specify multiple `src` objects. * @function * @param {Object} dst Destination object. * @param {...Object} src Source object(s). * @returns {Object} Reference to `dst`. */ function extend(dst, src) { each(arguments, function(obj) { if (obj !== dst) { each(obj, function(value, key){ dst[key] = value; }); } }); return dst; } Flow.extend = extend; /** * Iterate each element of an object * @function * @param {Array|Object} obj object or an array to iterate * @param {Function} callback first argument is a value and second is a key. * @param {Object=} context Object to become context (`this`) for the iterator function. */ function each(obj, callback, context) { if (!obj) { return ; } var key; // Is Array? // Array.isArray won't work, not only arrays can be iterated by index https://github.com/flowjs/ng-flow/issues/236# if (typeof(obj.length) !== 'undefined') { for (key = 0; key < obj.length; key++) { if (callback.call(context, obj[key], key) === false) { return ; } } } else { for (key in obj) { if (obj.hasOwnProperty(key) && callback.call(context, obj[key], key) === false) { return ; } } } } Flow.each = each; /** * FlowFile constructor * @type {FlowFile} */ Flow.FlowFile = FlowFile; /** * FlowFile constructor * @type {FlowChunk} */ Flow.FlowChunk = FlowChunk; /** * Library version * @type {string} */ Flow.version = '2.10.1'; if ( typeof module === "object" && module && typeof module.exports === "object" ) { // Expose Flow as module.exports in loaders that implement the Node // module pattern (including browserify). Do not create the global, since // the user will be storing it themselves locally, and globals are frowned // upon in the Node module world. module.exports = Flow; } else { // Otherwise expose Flow to the global object as usual window.Flow = Flow; // Register as a named AMD module, since Flow can be concatenated with other // files that may use define, but not via a proper concatenation script that // understands anonymous AMD modules. A named AMD is safest and most robust // way to register. Lowercase flow is used because AMD module names are // derived from file names, and Flow is normally delivered in a lowercase // file name. Do this after creating the global so that if an AMD module wants // to call noConflict to hide this version of Flow, it will work. if ( typeof define === "function" && define.amd ) { define( "flow", [], function () { return Flow; } ); } } })(window, document); /** * @description * var app = angular.module('App', ['flow.provider'], function(flowFactoryProvider){ * flowFactoryProvider.defaults = {target: '/'}; * }); * @name flowFactoryProvider */ angular.module('flow.provider', []) .provider('flowFactory', function() { 'use strict'; /** * Define the default properties for flow.js * @name flowFactoryProvider.defaults * @type {Object} */ this.defaults = {}; /** * Flow, MaybeFlow or NotFlow * @name flowFactoryProvider.factory * @type {function} * @return {Flow} */ this.factory = function (options) { return new Flow(options); }; /** * Define the default events * @name flowFactoryProvider.events * @type {Array} * @private */ this.events = []; /** * Add default events * @name flowFactoryProvider.on * @function * @param {string} event * @param {Function} callback */ this.on = function (event, callback) { this.events.push([event, callback]); }; this.$get = function() { var fn = this.factory; var defaults = this.defaults; var events = this.events; return { 'create': function(opts) { // combine default options with global options and options var flow = fn(angular.extend({}, defaults, opts)); angular.forEach(events, function (event) { flow.on(event[0], event[1]); }); return flow; } }; }; }); angular.module('flow.init', ['flow.provider']) .controller('flowCtrl', ['$scope', '$attrs', '$parse', 'flowFactory', function ($scope, $attrs, $parse, flowFactory) { var options = angular.extend({}, $scope.$eval($attrs.flowInit)); // use existing flow object or create a new one var flow = $scope.$eval($attrs.flowObject) || flowFactory.create(options); var catchAllHandler = function(eventName){ var args = Array.prototype.slice.call(arguments); args.shift(); var event = $scope.$broadcast.apply($scope, ['flow::' + eventName, flow].concat(args)); if ({ 'progress':1, 'filesSubmitted':1, 'fileSuccess': 1, 'fileError': 1, 'complete': 1 }[eventName]) { $scope.$apply(); } if (event.defaultPrevented) { return false; } }; flow.on('catchAll', catchAllHandler); $scope.$on('$destroy', function(){ flow.off('catchAll', catchAllHandler); }); $scope.$flow = flow; if ($attrs.hasOwnProperty('flowName')) { $parse($attrs.flowName).assign($scope, flow); $scope.$on('$destroy', function () { $parse($attrs.flowName).assign($scope); }); } }]) .directive('flowInit', [function() { return { scope: true, controller: 'flowCtrl' }; }]); angular.module('flow.btn', ['flow.init']) .directive('flowBtn', [function() { return { 'restrict': 'EA', 'scope': false, 'require': '^flowInit', 'link': function(scope, element, attrs) { var isDirectory = attrs.hasOwnProperty('flowDirectory'); var isSingleFile = attrs.hasOwnProperty('flowSingleFile'); var inputAttrs = attrs.hasOwnProperty('flowAttrs') && scope.$eval(attrs.flowAttrs); scope.$flow.assignBrowse(element, isDirectory, isSingleFile, inputAttrs); } }; }]); angular.module('flow.dragEvents', ['flow.init']) /** * @name flowPreventDrop * Prevent loading files then dropped on element */ .directive('flowPreventDrop', function() { return { 'scope': false, 'link': function(scope, element, attrs) { element.bind('drop dragover', function (event) { event.preventDefault(); }); } }; }) /** * @name flowDragEnter * executes `flowDragEnter` and `flowDragLeave` events */ .directive('flowDragEnter', ['$timeout', function($timeout) {