├── .gitignore ├── .versions ├── LICENSE.md ├── README.md ├── lib ├── css │ ├── jquery.fileupload-ui.css │ └── jquery.fileupload.css ├── img │ ├── loading.gif │ └── progressbar.gif ├── jquery.fileupload.js ├── jquery.iframe-transport.js └── vendor │ └── jquery.ui.widget.js ├── main.css ├── package.js ├── templates ├── buttons.html ├── buttons.js ├── dropzone.html ├── dropzone.js ├── queueItem.html ├── queueItem.js ├── upload.html └── upload.js ├── uploader.js └── versions.json /.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | .idea 3 | -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | babel-compiler@6.5.2 2 | babel-runtime@0.1.6 3 | base64@1.0.6 4 | blaze@2.1.5 5 | blaze-tools@1.0.6 6 | caching-compiler@1.0.2 7 | caching-html-compiler@1.0.4 8 | check@1.1.2 9 | deps@1.0.10 10 | diff-sequence@1.0.3 11 | ecmascript@0.4.1 12 | ecmascript-runtime@0.2.8 13 | ejson@1.0.9 14 | html-tools@1.0.7 15 | htmljs@1.0.7 16 | id-map@1.0.5 17 | jquery@1.11.6 18 | meteor@1.1.12 19 | minifier-js@1.1.9 20 | modules@0.5.1 21 | modules-runtime@0.6.1 22 | mongo-id@1.0.2 23 | observe-sequence@1.0.9 24 | promise@0.6.5 25 | random@1.0.7 26 | reactive-var@1.0.7 27 | spacebars@1.0.9 28 | spacebars-compiler@1.0.9 29 | templating@1.1.7 30 | templating-tools@1.0.2 31 | tomi:upload-jquery@2.4.0 32 | tracker@1.0.11 33 | underscore@1.0.6 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Tomas Trescak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a client interface package for the [Meteor Uploads](https://github.com/tomitrescak/meteor-uploads) 2 | 3 | Use via {{> upload_bootstrap}} 4 | 5 | If you are seeking for the comfort of this package but with uploads to S3, please go to: [https://github.com/tomitrescak/meteor-tomi-uploads-s3](https://github.com/tomitrescak/meteor-tomi-uploads-s3) 6 | 7 | For the full documentation and instructions and **issues** on use go to [https://github.com/tomitrescak/meteor-uploads](https://github.com/tomitrescak/meteor-uploads). 8 | 9 | [](https://atmospherejs.com/tomi/upload-jquery) 10 | 11 | ####Version Info 12 | 13 | * 2.4.0 - Now fully compatible with Meteor 1.3 14 | * 2.0.0 - Big update 15 | * Full support for Semantic UI 16 | * Breaking change in 'getFileInfo', 'getDirectory' and 'finished' callbacks, when fileInfo is now passed in the callback. See the documentation. 17 | * Update the DEMO application to showcase all current possibilities 18 | * Possibility to automatically create directories on server 19 | * Possibility to delete files on server with UploadServer.delete(path) 20 | * 1.1.1 - Bugfixed 'multiple' parameter 21 | * 1.1.0 - Added the drag and drop zone support 22 | * 1.0.7 - Bugfixes 23 | * 1.0.6 - Added possibility to add extra form elements 24 | * 1.0.5 - Bugfixes 25 | * 1.0.4 - Bugfixes 26 | * 1.0.3 - Complete redesign, Allowing upload of multiple files, supporting only Bootstrap 27 | * 1.0.2 - Added reactive variables for the uploaded file info 28 | * 1.0.1 - Added Semantic.ui support 29 | * 1.0.0 - Initial Version with Bootstrap support 30 | -------------------------------------------------------------------------------- /lib/css/jquery.fileupload-ui.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /* 3 | * jQuery File Upload UI Plugin CSS 9.0.0 4 | * https://github.com/blueimp/jQuery-File-Upload 5 | * 6 | * Copyright 2010, Sebastian Tschan 7 | * https://blueimp.net 8 | * 9 | * Licensed under the MIT license: 10 | * http://www.opensource.org/licenses/MIT 11 | */ 12 | 13 | .fileupload-buttonbar .btn, 14 | .fileupload-buttonbar .toggle { 15 | margin-bottom: 5px; 16 | } 17 | .progress-animated .progress-bar, 18 | .progress-animated .bar { 19 | background: url("../img/progressbar.gif") !important; 20 | filter: none; 21 | } 22 | .fileupload-process { 23 | float: right; 24 | display: none; 25 | } 26 | .fileupload-processing .fileupload-process, 27 | .files .processing .preview { 28 | display: block; 29 | width: 32px; 30 | height: 32px; 31 | background: url("../img/loading.gif") center no-repeat; 32 | background-size: contain; 33 | } 34 | .files audio, 35 | .files video { 36 | max-width: 300px; 37 | } 38 | 39 | @media (max-width: 767px) { 40 | .fileupload-buttonbar .toggle, 41 | .files .toggle, 42 | .files .btn span { 43 | display: none; 44 | } 45 | .files .name { 46 | width: 80px; 47 | word-wrap: break-word; 48 | } 49 | .files audio, 50 | .files video { 51 | max-width: 80px; 52 | } 53 | .files img, 54 | .files canvas { 55 | max-width: 100%; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/css/jquery.fileupload.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /* 3 | * jQuery File Upload Plugin CSS 1.3.0 4 | * https://github.com/blueimp/jQuery-File-Upload 5 | * 6 | * Copyright 2013, Sebastian Tschan 7 | * https://blueimp.net 8 | * 9 | * Licensed under the MIT license: 10 | * http://www.opensource.org/licenses/MIT 11 | */ 12 | 13 | .fileinput-button { 14 | position: relative; 15 | overflow: hidden; 16 | } 17 | .fileinput-button input { 18 | position: absolute; 19 | top: 0; 20 | right: 0; 21 | margin: 0; 22 | opacity: 0; 23 | -ms-filter: 'alpha(opacity=0)'; 24 | font-size: 200px; 25 | direction: ltr; 26 | cursor: pointer; 27 | } 28 | 29 | /* Fixes for IE < 8 */ 30 | @media screen\9 { 31 | .fileinput-button input { 32 | filter: alpha(opacity=0); 33 | font-size: 100%; 34 | height: 100%; 35 | } 36 | } 37 | 38 | /* SEMANTIC UI */ 39 | 40 | .progressOuter { 41 | display: table-cell; width: 100%; vertical-align: top 42 | } 43 | 44 | .progressInner { 45 | height: 33px; 46 | } 47 | 48 | .semantic.progressInner { 49 | background: none repeat scroll 0 0 rgba(0, 0, 0, 0.03); 50 | border: 1px solid rgba(39, 41, 43, 0.15); 51 | box-shadow: none; 52 | display: block; 53 | max-width: 100%; 54 | padding: 0.2857em; 55 | position: relative; 56 | } 57 | 58 | .semantic.progressInner .bar { 59 | animation: 0s ease 0s normal none 1 running none !important; 60 | background-color: #5bbd72 !important; 61 | height: 1.75em; 62 | background: none repeat scroll 0 0 #888; 63 | border-radius: 0.2857rem; 64 | display: block; 65 | line-height: 1; 66 | min-width: 2em; 67 | position: relative; 68 | transition: width 0.3s ease 0s, background-color 0.3s ease 0s; 69 | width: 0; 70 | } 71 | 72 | .rightButton { 73 | float: right; 74 | margin-left: 3px!important; 75 | } 76 | 77 | 78 | -------------------------------------------------------------------------------- /lib/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomitrescak/meteor-tomi-upload-jquery/60ce3eb1237e38325a0c367dc84b6a38404eb10b/lib/img/loading.gif -------------------------------------------------------------------------------- /lib/img/progressbar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomitrescak/meteor-tomi-upload-jquery/60ce3eb1237e38325a0c367dc84b6a38404eb10b/lib/img/progressbar.gif -------------------------------------------------------------------------------- /lib/jquery.fileupload.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload Plugin 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2010, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * http://www.opensource.org/licenses/MIT 10 | */ 11 | 12 | /* jshint nomen:false */ 13 | /* global define, require, window, document, location, Blob, FormData */ 14 | 15 | (function (factory) { 16 | 'use strict'; 17 | if (typeof define === 'function' && define.amd) { 18 | // Register as an anonymous AMD module: 19 | define([ 20 | 'jquery', 21 | 'jquery.ui.widget' 22 | ], factory); 23 | } else if (typeof exports === 'object') { 24 | // Node/CommonJS: 25 | factory( 26 | require('jquery'), 27 | require('./vendor/jquery.ui.widget') 28 | ); 29 | } else { 30 | // Browser globals: 31 | factory(window.jQuery); 32 | } 33 | }(function ($) { 34 | 'use strict'; 35 | 36 | // Detect file input support, based on 37 | // http://viljamis.com/blog/2012/file-upload-support-on-mobile/ 38 | $.support.fileInput = !(new RegExp( 39 | // Handle devices which give false positives for the feature detection: 40 | '(Android (1\\.[0156]|2\\.[01]))' + 41 | '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' + 42 | '|(w(eb)?OSBrowser)|(webOS)' + 43 | '|(Kindle/(1\\.0|2\\.[05]|3\\.0))' 44 | ).test(window.navigator.userAgent) || 45 | // Feature detection for all other devices: 46 | $('').prop('disabled')); 47 | 48 | // The FileReader API is not actually used, but works as feature detection, 49 | // as some Safari versions (5?) support XHR file uploads via the FormData API, 50 | // but not non-multipart XHR file uploads. 51 | // window.XMLHttpRequestUpload is not available on IE10, so we check for 52 | // window.ProgressEvent instead to detect XHR2 file upload capability: 53 | $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader); 54 | $.support.xhrFormDataFileUpload = !!window.FormData; 55 | 56 | // Detect support for Blob slicing (required for chunked uploads): 57 | $.support.blobSlice = window.Blob && (Blob.prototype.slice || 58 | Blob.prototype.webkitSlice || Blob.prototype.mozSlice); 59 | 60 | // Helper function to create drag handlers for dragover/dragenter/dragleave: 61 | function getDragHandler(type) { 62 | var isDragOver = type === 'dragover'; 63 | return function (e) { 64 | e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; 65 | var dataTransfer = e.dataTransfer; 66 | if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1 && 67 | this._trigger( 68 | type, 69 | $.Event(type, {delegatedEvent: e}) 70 | ) !== false) { 71 | e.preventDefault(); 72 | if (isDragOver) { 73 | dataTransfer.dropEffect = 'copy'; 74 | } 75 | } 76 | }; 77 | } 78 | 79 | // The fileupload widget listens for change events on file input fields defined 80 | // via fileInput setting and paste or drop events of the given dropZone. 81 | // In addition to the default jQuery Widget methods, the fileupload widget 82 | // exposes the "add" and "send" methods, to add or directly send files using 83 | // the fileupload API. 84 | // By default, files added via file input selection, paste, drag & drop or 85 | // "add" method are uploaded immediately, but it is possible to override 86 | // the "add" callback option to queue file uploads. 87 | $.widget('blueimp.fileupload', { 88 | 89 | options: { 90 | // The drop target element(s), by the default the complete document. 91 | // Set to null to disable drag & drop support: 92 | dropZone: $(document), 93 | // The paste target element(s), by the default undefined. 94 | // Set to a DOM node or jQuery object to enable file pasting: 95 | pasteZone: undefined, 96 | // The file input field(s), that are listened to for change events. 97 | // If undefined, it is set to the file input fields inside 98 | // of the widget element on plugin initialization. 99 | // Set to null to disable the change listener. 100 | fileInput: undefined, 101 | // By default, the file input field is replaced with a clone after 102 | // each input field change event. This is required for iframe transport 103 | // queues and allows change events to be fired for the same file 104 | // selection, but can be disabled by setting the following option to false: 105 | replaceFileInput: true, 106 | // The parameter name for the file form data (the request argument name). 107 | // If undefined or empty, the name property of the file input field is 108 | // used, or "files[]" if the file input name property is also empty, 109 | // can be a string or an array of strings: 110 | paramName: undefined, 111 | // By default, each file of a selection is uploaded using an individual 112 | // request for XHR type uploads. Set to false to upload file 113 | // selections in one request each: 114 | singleFileUploads: true, 115 | // To limit the number of files uploaded with one XHR request, 116 | // set the following option to an integer greater than 0: 117 | limitMultiFileUploads: undefined, 118 | // The following option limits the number of files uploaded with one 119 | // XHR request to keep the request size under or equal to the defined 120 | // limit in bytes: 121 | limitMultiFileUploadSize: undefined, 122 | // Multipart file uploads add a number of bytes to each uploaded file, 123 | // therefore the following option adds an overhead for each file used 124 | // in the limitMultiFileUploadSize configuration: 125 | limitMultiFileUploadSizeOverhead: 512, 126 | // Set the following option to true to issue all file upload requests 127 | // in a sequential order: 128 | sequentialUploads: false, 129 | // To limit the number of concurrent uploads, 130 | // set the following option to an integer greater than 0: 131 | limitConcurrentUploads: undefined, 132 | // Set the following option to true to force iframe transport uploads: 133 | forceIframeTransport: false, 134 | // Set the following option to the location of a redirect url on the 135 | // origin server, for cross-domain iframe transport uploads: 136 | redirect: undefined, 137 | // The parameter name for the redirect url, sent as part of the form 138 | // data and set to 'redirect' if this option is empty: 139 | redirectParamName: undefined, 140 | // Set the following option to the location of a postMessage window, 141 | // to enable postMessage transport uploads: 142 | postMessage: undefined, 143 | // By default, XHR file uploads are sent as multipart/form-data. 144 | // The iframe transport is always using multipart/form-data. 145 | // Set to false to enable non-multipart XHR uploads: 146 | multipart: true, 147 | // To upload large files in smaller chunks, set the following option 148 | // to a preferred maximum chunk size. If set to 0, null or undefined, 149 | // or the browser does not support the required Blob API, files will 150 | // be uploaded as a whole. 151 | maxChunkSize: undefined, 152 | // When a non-multipart upload or a chunked multipart upload has been 153 | // aborted, this option can be used to resume the upload by setting 154 | // it to the size of the already uploaded bytes. This option is most 155 | // useful when modifying the options object inside of the "add" or 156 | // "send" callbacks, as the options are cloned for each file upload. 157 | uploadedBytes: undefined, 158 | // By default, failed (abort or error) file uploads are removed from the 159 | // global progress calculation. Set the following option to false to 160 | // prevent recalculating the global progress data: 161 | recalculateProgress: true, 162 | // Interval in milliseconds to calculate and trigger progress events: 163 | progressInterval: 100, 164 | // Interval in milliseconds to calculate progress bitrate: 165 | bitrateInterval: 500, 166 | // By default, uploads are started automatically when adding files: 167 | autoUpload: true, 168 | 169 | // Error and info messages: 170 | messages: { 171 | uploadedBytes: 'Uploaded bytes exceed file size' 172 | }, 173 | 174 | // Translation function, gets the message key to be translated 175 | // and an object with context specific data as arguments: 176 | i18n: function (message, context) { 177 | message = this.messages[message] || message.toString(); 178 | if (context) { 179 | $.each(context, function (key, value) { 180 | message = message.replace('{' + key + '}', value); 181 | }); 182 | } 183 | return message; 184 | }, 185 | 186 | // Additional form data to be sent along with the file uploads can be set 187 | // using this option, which accepts an array of objects with name and 188 | // value properties, a function returning such an array, a FormData 189 | // object (for XHR file uploads), or a simple object. 190 | // The form of the first fileInput is given as parameter to the function: 191 | formData: function (form) { 192 | return form.serializeArray(); 193 | }, 194 | 195 | // The add callback is invoked as soon as files are added to the fileupload 196 | // widget (via file input selection, drag & drop, paste or add API call). 197 | // If the singleFileUploads option is enabled, this callback will be 198 | // called once for each file in the selection for XHR file uploads, else 199 | // once for each file selection. 200 | // 201 | // The upload starts when the submit method is invoked on the data parameter. 202 | // The data object contains a files property holding the added files 203 | // and allows you to override plugin options as well as define ajax settings. 204 | // 205 | // Listeners for this callback can also be bound the following way: 206 | // .bind('fileuploadadd', func); 207 | // 208 | // data.submit() returns a Promise object and allows to attach additional 209 | // handlers using jQuery's Deferred callbacks: 210 | // data.submit().done(func).fail(func).always(func); 211 | add: function (e, data) { 212 | if (e.isDefaultPrevented()) { 213 | return false; 214 | } 215 | if (data.autoUpload || (data.autoUpload !== false && 216 | $(this).fileupload('option', 'autoUpload'))) { 217 | data.process().done(function () { 218 | data.submit(); 219 | }); 220 | } 221 | }, 222 | 223 | // Other callbacks: 224 | 225 | // Callback for the submit event of each file upload: 226 | // submit: function (e, data) {}, // .bind('fileuploadsubmit', func); 227 | 228 | // Callback for the start of each file upload request: 229 | // send: function (e, data) {}, // .bind('fileuploadsend', func); 230 | 231 | // Callback for successful uploads: 232 | // done: function (e, data) {}, // .bind('fileuploaddone', func); 233 | 234 | // Callback for failed (abort or error) uploads: 235 | // fail: function (e, data) {}, // .bind('fileuploadfail', func); 236 | 237 | // Callback for completed (success, abort or error) requests: 238 | // always: function (e, data) {}, // .bind('fileuploadalways', func); 239 | 240 | // Callback for upload progress events: 241 | // progress: function (e, data) {}, // .bind('fileuploadprogress', func); 242 | 243 | // Callback for global upload progress events: 244 | // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func); 245 | 246 | // Callback for uploads start, equivalent to the global ajaxStart event: 247 | // start: function (e) {}, // .bind('fileuploadstart', func); 248 | 249 | // Callback for uploads stop, equivalent to the global ajaxStop event: 250 | // stop: function (e) {}, // .bind('fileuploadstop', func); 251 | 252 | // Callback for change events of the fileInput(s): 253 | // change: function (e, data) {}, // .bind('fileuploadchange', func); 254 | 255 | // Callback for paste events to the pasteZone(s): 256 | // paste: function (e, data) {}, // .bind('fileuploadpaste', func); 257 | 258 | // Callback for drop events of the dropZone(s): 259 | // drop: function (e, data) {}, // .bind('fileuploaddrop', func); 260 | 261 | // Callback for dragover events of the dropZone(s): 262 | // dragover: function (e) {}, // .bind('fileuploaddragover', func); 263 | 264 | // Callback for the start of each chunk upload request: 265 | // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func); 266 | 267 | // Callback for successful chunk uploads: 268 | // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func); 269 | 270 | // Callback for failed (abort or error) chunk uploads: 271 | // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func); 272 | 273 | // Callback for completed (success, abort or error) chunk upload requests: 274 | // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func); 275 | 276 | // The plugin options are used as settings object for the ajax calls. 277 | // The following are jQuery ajax settings required for the file uploads: 278 | processData: false, 279 | contentType: false, 280 | cache: false, 281 | timeout: 0 282 | }, 283 | 284 | // A list of options that require reinitializing event listeners and/or 285 | // special initialization code: 286 | _specialOptions: [ 287 | 'fileInput', 288 | 'dropZone', 289 | 'pasteZone', 290 | 'multipart', 291 | 'forceIframeTransport' 292 | ], 293 | 294 | _blobSlice: $.support.blobSlice && function () { 295 | var slice = this.slice || this.webkitSlice || this.mozSlice; 296 | return slice.apply(this, arguments); 297 | }, 298 | 299 | _BitrateTimer: function () { 300 | this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime()); 301 | this.loaded = 0; 302 | this.bitrate = 0; 303 | this.getBitrate = function (now, loaded, interval) { 304 | var timeDiff = now - this.timestamp; 305 | if (!this.bitrate || !interval || timeDiff > interval) { 306 | this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8; 307 | this.loaded = loaded; 308 | this.timestamp = now; 309 | } 310 | return this.bitrate; 311 | }; 312 | }, 313 | 314 | _isXHRUpload: function (options) { 315 | return !options.forceIframeTransport && 316 | ((!options.multipart && $.support.xhrFileUpload) || 317 | $.support.xhrFormDataFileUpload); 318 | }, 319 | 320 | _getFormData: function (options) { 321 | var formData; 322 | if ($.type(options.formData) === 'function') { 323 | return options.formData(options.form); 324 | } 325 | if ($.isArray(options.formData)) { 326 | return options.formData; 327 | } 328 | if ($.type(options.formData) === 'object') { 329 | formData = []; 330 | $.each(options.formData, function (name, value) { 331 | formData.push({name: name, value: value}); 332 | }); 333 | return formData; 334 | } 335 | return []; 336 | }, 337 | 338 | _getTotal: function (files) { 339 | var total = 0; 340 | $.each(files, function (index, file) { 341 | total += file.size || 1; 342 | }); 343 | return total; 344 | }, 345 | 346 | _initProgressObject: function (obj) { 347 | var progress = { 348 | loaded: 0, 349 | total: 0, 350 | bitrate: 0 351 | }; 352 | if (obj._progress) { 353 | $.extend(obj._progress, progress); 354 | } else { 355 | obj._progress = progress; 356 | } 357 | }, 358 | 359 | _initResponseObject: function (obj) { 360 | var prop; 361 | if (obj._response) { 362 | for (prop in obj._response) { 363 | if (obj._response.hasOwnProperty(prop)) { 364 | delete obj._response[prop]; 365 | } 366 | } 367 | } else { 368 | obj._response = {}; 369 | } 370 | }, 371 | 372 | _onProgress: function (e, data) { 373 | if (e.lengthComputable) { 374 | var now = ((Date.now) ? Date.now() : (new Date()).getTime()), 375 | loaded; 376 | if (data._time && data.progressInterval && 377 | (now - data._time < data.progressInterval) && 378 | e.loaded !== e.total) { 379 | return; 380 | } 381 | data._time = now; 382 | loaded = Math.floor( 383 | e.loaded / e.total * (data.chunkSize || data._progress.total) 384 | ) + (data.uploadedBytes || 0); 385 | // Add the difference from the previously loaded state 386 | // to the global loaded counter: 387 | this._progress.loaded += (loaded - data._progress.loaded); 388 | this._progress.bitrate = this._bitrateTimer.getBitrate( 389 | now, 390 | this._progress.loaded, 391 | data.bitrateInterval 392 | ); 393 | data._progress.loaded = data.loaded = loaded; 394 | data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate( 395 | now, 396 | loaded, 397 | data.bitrateInterval 398 | ); 399 | // Trigger a custom progress event with a total data property set 400 | // to the file size(s) of the current upload and a loaded data 401 | // property calculated accordingly: 402 | this._trigger( 403 | 'progress', 404 | $.Event('progress', {delegatedEvent: e}), 405 | data 406 | ); 407 | // Trigger a global progress event for all current file uploads, 408 | // including ajax calls queued for sequential file uploads: 409 | this._trigger( 410 | 'progressall', 411 | $.Event('progressall', {delegatedEvent: e}), 412 | this._progress 413 | ); 414 | } 415 | }, 416 | 417 | _initProgressListener: function (options) { 418 | var that = this, 419 | xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(); 420 | // Accesss to the native XHR object is required to add event listeners 421 | // for the upload progress event: 422 | if (xhr.upload) { 423 | $(xhr.upload).bind('progress', function (e) { 424 | var oe = e.originalEvent; 425 | // Make sure the progress event properties get copied over: 426 | e.lengthComputable = oe.lengthComputable; 427 | e.loaded = oe.loaded; 428 | e.total = oe.total; 429 | that._onProgress(e, options); 430 | }); 431 | options.xhr = function () { 432 | return xhr; 433 | }; 434 | } 435 | }, 436 | 437 | _isInstanceOf: function (type, obj) { 438 | // Cross-frame instanceof check 439 | return Object.prototype.toString.call(obj) === '[object ' + type + ']'; 440 | }, 441 | 442 | _initXHRData: function (options) { 443 | var that = this, 444 | formData, 445 | file = options.files[0], 446 | // Ignore non-multipart setting if not supported: 447 | multipart = options.multipart || !$.support.xhrFileUpload, 448 | paramName = $.type(options.paramName) === 'array' ? 449 | options.paramName[0] : options.paramName; 450 | options.headers = $.extend({}, options.headers); 451 | if (options.contentRange) { 452 | options.headers['Content-Range'] = options.contentRange; 453 | } 454 | if (!multipart || options.blob || !this._isInstanceOf('File', file)) { 455 | options.headers['Content-Disposition'] = 'attachment; filename="' + 456 | encodeURI(file.name) + '"'; 457 | } 458 | if (!multipart) { 459 | options.contentType = file.type || 'application/octet-stream'; 460 | options.data = options.blob || file; 461 | } else if ($.support.xhrFormDataFileUpload) { 462 | if (options.postMessage) { 463 | // window.postMessage does not allow sending FormData 464 | // objects, so we just add the File/Blob objects to 465 | // the formData array and let the postMessage window 466 | // create the FormData object out of this array: 467 | formData = this._getFormData(options); 468 | if (options.blob) { 469 | formData.push({ 470 | name: paramName, 471 | value: options.blob 472 | }); 473 | } else { 474 | $.each(options.files, function (index, file) { 475 | formData.push({ 476 | name: ($.type(options.paramName) === 'array' && 477 | options.paramName[index]) || paramName, 478 | value: file 479 | }); 480 | }); 481 | } 482 | } else { 483 | if (that._isInstanceOf('FormData', options.formData)) { 484 | formData = options.formData; 485 | } else { 486 | formData = new FormData(); 487 | $.each(this._getFormData(options), function (index, field) { 488 | formData.append(field.name, field.value); 489 | }); 490 | } 491 | if (options.blob) { 492 | formData.append(paramName, options.blob, file.name); 493 | } else { 494 | $.each(options.files, function (index, file) { 495 | // This check allows the tests to run with 496 | // dummy objects: 497 | if (that._isInstanceOf('File', file) || 498 | that._isInstanceOf('Blob', file)) { 499 | formData.append( 500 | ($.type(options.paramName) === 'array' && 501 | options.paramName[index]) || paramName, 502 | file, 503 | file.uploadName || file.name 504 | ); 505 | } 506 | }); 507 | } 508 | } 509 | options.data = formData; 510 | } 511 | // Blob reference is not needed anymore, free memory: 512 | options.blob = null; 513 | }, 514 | 515 | _initIframeSettings: function (options) { 516 | var targetHost = $('').prop('href', options.url).prop('host'); 517 | // Setting the dataType to iframe enables the iframe transport: 518 | options.dataType = 'iframe ' + (options.dataType || ''); 519 | // The iframe transport accepts a serialized array as form data: 520 | options.formData = this._getFormData(options); 521 | // Add redirect url to form data on cross-domain uploads: 522 | if (options.redirect && targetHost && targetHost !== location.host) { 523 | options.formData.push({ 524 | name: options.redirectParamName || 'redirect', 525 | value: options.redirect 526 | }); 527 | } 528 | }, 529 | 530 | _initDataSettings: function (options) { 531 | if (this._isXHRUpload(options)) { 532 | if (!this._chunkedUpload(options, true)) { 533 | if (!options.data) { 534 | this._initXHRData(options); 535 | } 536 | this._initProgressListener(options); 537 | } 538 | if (options.postMessage) { 539 | // Setting the dataType to postmessage enables the 540 | // postMessage transport: 541 | options.dataType = 'postmessage ' + (options.dataType || ''); 542 | } 543 | } else { 544 | this._initIframeSettings(options); 545 | } 546 | }, 547 | 548 | _getParamName: function (options) { 549 | var fileInput = $(options.fileInput), 550 | paramName = options.paramName; 551 | if (!paramName) { 552 | paramName = []; 553 | fileInput.each(function () { 554 | var input = $(this), 555 | name = input.prop('name') || 'files[]', 556 | i = (input.prop('files') || [1]).length; 557 | while (i) { 558 | paramName.push(name); 559 | i -= 1; 560 | } 561 | }); 562 | if (!paramName.length) { 563 | paramName = [fileInput.prop('name') || 'files[]']; 564 | } 565 | } else if (!$.isArray(paramName)) { 566 | paramName = [paramName]; 567 | } 568 | return paramName; 569 | }, 570 | 571 | _initFormSettings: function (options) { 572 | // Retrieve missing options from the input field and the 573 | // associated form, if available: 574 | if (!options.form || !options.form.length) { 575 | options.form = $(options.fileInput.prop('form')); 576 | // If the given file input doesn't have an associated form, 577 | // use the default widget file input's form: 578 | if (!options.form.length) { 579 | options.form = $(this.options.fileInput.prop('form')); 580 | } 581 | } 582 | options.paramName = this._getParamName(options); 583 | if (!options.url) { 584 | options.url = options.form.prop('action') || location.href; 585 | } 586 | // The HTTP request method must be "POST" or "PUT": 587 | options.type = (options.type || 588 | ($.type(options.form.prop('method')) === 'string' && 589 | options.form.prop('method')) || '' 590 | ).toUpperCase(); 591 | if (options.type !== 'POST' && options.type !== 'PUT' && 592 | options.type !== 'PATCH') { 593 | options.type = 'POST'; 594 | } 595 | if (!options.formAcceptCharset) { 596 | options.formAcceptCharset = options.form.attr('accept-charset'); 597 | } 598 | }, 599 | 600 | _getAJAXSettings: function (data) { 601 | var options = $.extend({}, this.options, data); 602 | this._initFormSettings(options); 603 | this._initDataSettings(options); 604 | return options; 605 | }, 606 | 607 | // jQuery 1.6 doesn't provide .state(), 608 | // while jQuery 1.8+ removed .isRejected() and .isResolved(): 609 | _getDeferredState: function (deferred) { 610 | if (deferred.state) { 611 | return deferred.state(); 612 | } 613 | if (deferred.isResolved()) { 614 | return 'resolved'; 615 | } 616 | if (deferred.isRejected()) { 617 | return 'rejected'; 618 | } 619 | return 'pending'; 620 | }, 621 | 622 | // Maps jqXHR callbacks to the equivalent 623 | // methods of the given Promise object: 624 | _enhancePromise: function (promise) { 625 | promise.success = promise.done; 626 | promise.error = promise.fail; 627 | promise.complete = promise.always; 628 | return promise; 629 | }, 630 | 631 | // Creates and returns a Promise object enhanced with 632 | // the jqXHR methods abort, success, error and complete: 633 | _getXHRPromise: function (resolveOrReject, context, args) { 634 | var dfd = $.Deferred(), 635 | promise = dfd.promise(); 636 | context = context || this.options.context || promise; 637 | if (resolveOrReject === true) { 638 | dfd.resolveWith(context, args); 639 | } else if (resolveOrReject === false) { 640 | dfd.rejectWith(context, args); 641 | } 642 | promise.abort = dfd.promise; 643 | return this._enhancePromise(promise); 644 | }, 645 | 646 | // Adds convenience methods to the data callback argument: 647 | _addConvenienceMethods: function (e, data) { 648 | var that = this, 649 | getPromise = function (args) { 650 | return $.Deferred().resolveWith(that, args).promise(); 651 | }; 652 | data.process = function (resolveFunc, rejectFunc) { 653 | if (resolveFunc || rejectFunc) { 654 | data._processQueue = this._processQueue = 655 | (this._processQueue || getPromise([this])).pipe( 656 | function () { 657 | if (data.errorThrown) { 658 | return $.Deferred() 659 | .rejectWith(that, [data]).promise(); 660 | } 661 | return getPromise(arguments); 662 | } 663 | ).pipe(resolveFunc, rejectFunc); 664 | } 665 | return this._processQueue || getPromise([this]); 666 | }; 667 | data.submit = function () { 668 | if (this.state() !== 'pending') { 669 | data.jqXHR = this.jqXHR = 670 | (that._trigger( 671 | 'submit', 672 | $.Event('submit', {delegatedEvent: e}), 673 | this 674 | ) !== false) && that._onSend(e, this); 675 | } 676 | return this.jqXHR || that._getXHRPromise(); 677 | }; 678 | data.abort = function () { 679 | if (this.jqXHR) { 680 | return this.jqXHR.abort(); 681 | } 682 | this.errorThrown = 'abort'; 683 | that._trigger('fail', null, this); 684 | return that._getXHRPromise(false); 685 | }; 686 | data.state = function () { 687 | if (this.jqXHR) { 688 | return that._getDeferredState(this.jqXHR); 689 | } 690 | if (this._processQueue) { 691 | return that._getDeferredState(this._processQueue); 692 | } 693 | }; 694 | data.processing = function () { 695 | return !this.jqXHR && this._processQueue && that 696 | ._getDeferredState(this._processQueue) === 'pending'; 697 | }; 698 | data.progress = function () { 699 | return this._progress; 700 | }; 701 | data.response = function () { 702 | return this._response; 703 | }; 704 | }, 705 | 706 | // Parses the Range header from the server response 707 | // and returns the uploaded bytes: 708 | _getUploadedBytes: function (jqXHR) { 709 | var range = jqXHR.getResponseHeader('Range'), 710 | parts = range && range.split('-'), 711 | upperBytesPos = parts && parts.length > 1 && 712 | parseInt(parts[1], 10); 713 | return upperBytesPos && upperBytesPos + 1; 714 | }, 715 | 716 | // Uploads a file in multiple, sequential requests 717 | // by splitting the file up in multiple blob chunks. 718 | // If the second parameter is true, only tests if the file 719 | // should be uploaded in chunks, but does not invoke any 720 | // upload requests: 721 | _chunkedUpload: function (options, testOnly) { 722 | options.uploadedBytes = options.uploadedBytes || 0; 723 | var that = this, 724 | file = options.files[0], 725 | fs = file.size, 726 | ub = options.uploadedBytes, 727 | mcs = options.maxChunkSize || fs, 728 | slice = this._blobSlice, 729 | dfd = $.Deferred(), 730 | promise = dfd.promise(), 731 | jqXHR, 732 | upload; 733 | if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) || 734 | options.data) { 735 | return false; 736 | } 737 | if (testOnly) { 738 | return true; 739 | } 740 | if (ub >= fs) { 741 | file.error = options.i18n('uploadedBytes'); 742 | return this._getXHRPromise( 743 | false, 744 | options.context, 745 | [null, 'error', file.error] 746 | ); 747 | } 748 | // The chunk upload method: 749 | upload = function () { 750 | // Clone the options object for each chunk upload: 751 | var o = $.extend({}, options), 752 | currentLoaded = o._progress.loaded; 753 | o.blob = slice.call( 754 | file, 755 | ub, 756 | ub + mcs, 757 | file.type 758 | ); 759 | // Store the current chunk size, as the blob itself 760 | // will be dereferenced after data processing: 761 | o.chunkSize = o.blob.size; 762 | // Expose the chunk bytes position range: 763 | o.contentRange = 'bytes ' + ub + '-' + 764 | (ub + o.chunkSize - 1) + '/' + fs; 765 | // Process the upload data (the blob and potential form data): 766 | that._initXHRData(o); 767 | // Add progress listeners for this chunk upload: 768 | that._initProgressListener(o); 769 | jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) || 770 | that._getXHRPromise(false, o.context)) 771 | .done(function (result, textStatus, jqXHR) { 772 | ub = that._getUploadedBytes(jqXHR) || 773 | (ub + o.chunkSize); 774 | // Create a progress event if no final progress event 775 | // with loaded equaling total has been triggered 776 | // for this chunk: 777 | if (currentLoaded + o.chunkSize - o._progress.loaded) { 778 | that._onProgress($.Event('progress', { 779 | lengthComputable: true, 780 | loaded: ub - o.uploadedBytes, 781 | total: ub - o.uploadedBytes 782 | }), o); 783 | } 784 | options.uploadedBytes = o.uploadedBytes = ub; 785 | o.result = result; 786 | o.textStatus = textStatus; 787 | o.jqXHR = jqXHR; 788 | that._trigger('chunkdone', null, o); 789 | that._trigger('chunkalways', null, o); 790 | if (ub < fs) { 791 | // File upload not yet complete, 792 | // continue with the next chunk: 793 | upload(); 794 | } else { 795 | dfd.resolveWith( 796 | o.context, 797 | [result, textStatus, jqXHR] 798 | ); 799 | } 800 | }) 801 | .fail(function (jqXHR, textStatus, errorThrown) { 802 | o.jqXHR = jqXHR; 803 | o.textStatus = textStatus; 804 | o.errorThrown = errorThrown; 805 | that._trigger('chunkfail', null, o); 806 | that._trigger('chunkalways', null, o); 807 | dfd.rejectWith( 808 | o.context, 809 | [jqXHR, textStatus, errorThrown] 810 | ); 811 | }); 812 | }; 813 | this._enhancePromise(promise); 814 | promise.abort = function () { 815 | return jqXHR.abort(); 816 | }; 817 | upload(); 818 | return promise; 819 | }, 820 | 821 | _beforeSend: function (e, data) { 822 | if (this._active === 0) { 823 | // the start callback is triggered when an upload starts 824 | // and no other uploads are currently running, 825 | // equivalent to the global ajaxStart event: 826 | this._trigger('start'); 827 | // Set timer for global bitrate progress calculation: 828 | this._bitrateTimer = new this._BitrateTimer(); 829 | // Reset the global progress values: 830 | this._progress.loaded = this._progress.total = 0; 831 | this._progress.bitrate = 0; 832 | } 833 | // Make sure the container objects for the .response() and 834 | // .progress() methods on the data object are available 835 | // and reset to their initial state: 836 | this._initResponseObject(data); 837 | this._initProgressObject(data); 838 | data._progress.loaded = data.loaded = data.uploadedBytes || 0; 839 | data._progress.total = data.total = this._getTotal(data.files) || 1; 840 | data._progress.bitrate = data.bitrate = 0; 841 | this._active += 1; 842 | // Initialize the global progress values: 843 | this._progress.loaded += data.loaded; 844 | this._progress.total += data.total; 845 | }, 846 | 847 | _onDone: function (result, textStatus, jqXHR, options) { 848 | var total = options._progress.total, 849 | response = options._response; 850 | if (options._progress.loaded < total) { 851 | // Create a progress event if no final progress event 852 | // with loaded equaling total has been triggered: 853 | this._onProgress($.Event('progress', { 854 | lengthComputable: true, 855 | loaded: total, 856 | total: total 857 | }), options); 858 | } 859 | response.result = options.result = result; 860 | response.textStatus = options.textStatus = textStatus; 861 | response.jqXHR = options.jqXHR = jqXHR; 862 | this._trigger('done', null, options); 863 | }, 864 | 865 | _onFail: function (jqXHR, textStatus, errorThrown, options) { 866 | var response = options._response; 867 | if (options.recalculateProgress) { 868 | // Remove the failed (error or abort) file upload from 869 | // the global progress calculation: 870 | this._progress.loaded -= options._progress.loaded; 871 | this._progress.total -= options._progress.total; 872 | } 873 | response.jqXHR = options.jqXHR = jqXHR; 874 | response.textStatus = options.textStatus = textStatus; 875 | response.errorThrown = options.errorThrown = errorThrown; 876 | this._trigger('fail', null, options); 877 | }, 878 | 879 | _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) { 880 | // jqXHRorResult, textStatus and jqXHRorError are added to the 881 | // options object via done and fail callbacks 882 | this._trigger('always', null, options); 883 | }, 884 | 885 | _onSend: function (e, data) { 886 | if (!data.submit) { 887 | this._addConvenienceMethods(e, data); 888 | } 889 | var that = this, 890 | jqXHR, 891 | aborted, 892 | slot, 893 | pipe, 894 | options = that._getAJAXSettings(data), 895 | send = function () { 896 | that._sending += 1; 897 | // Set timer for bitrate progress calculation: 898 | options._bitrateTimer = new that._BitrateTimer(); 899 | jqXHR = jqXHR || ( 900 | ((aborted || that._trigger( 901 | 'send', 902 | $.Event('send', {delegatedEvent: e}), 903 | options 904 | ) === false) && 905 | that._getXHRPromise(false, options.context, aborted)) || 906 | that._chunkedUpload(options) || $.ajax(options) 907 | ).done(function (result, textStatus, jqXHR) { 908 | that._onDone(result, textStatus, jqXHR, options); 909 | }).fail(function (jqXHR, textStatus, errorThrown) { 910 | that._onFail(jqXHR, textStatus, errorThrown, options); 911 | }).always(function (jqXHRorResult, textStatus, jqXHRorError) { 912 | that._onAlways( 913 | jqXHRorResult, 914 | textStatus, 915 | jqXHRorError, 916 | options 917 | ); 918 | that._sending -= 1; 919 | that._active -= 1; 920 | if (options.limitConcurrentUploads && 921 | options.limitConcurrentUploads > that._sending) { 922 | // Start the next queued upload, 923 | // that has not been aborted: 924 | var nextSlot = that._slots.shift(); 925 | while (nextSlot) { 926 | if (that._getDeferredState(nextSlot) === 'pending') { 927 | nextSlot.resolve(); 928 | break; 929 | } 930 | nextSlot = that._slots.shift(); 931 | } 932 | } 933 | if (that._active === 0) { 934 | // The stop callback is triggered when all uploads have 935 | // been completed, equivalent to the global ajaxStop event: 936 | that._trigger('stop'); 937 | } 938 | }); 939 | return jqXHR; 940 | }; 941 | this._beforeSend(e, options); 942 | if (this.options.sequentialUploads || 943 | (this.options.limitConcurrentUploads && 944 | this.options.limitConcurrentUploads <= this._sending)) { 945 | if (this.options.limitConcurrentUploads > 1) { 946 | slot = $.Deferred(); 947 | this._slots.push(slot); 948 | pipe = slot.pipe(send); 949 | } else { 950 | this._sequence = this._sequence.pipe(send, send); 951 | pipe = this._sequence; 952 | } 953 | // Return the piped Promise object, enhanced with an abort method, 954 | // which is delegated to the jqXHR object of the current upload, 955 | // and jqXHR callbacks mapped to the equivalent Promise methods: 956 | pipe.abort = function () { 957 | aborted = [undefined, 'abort', 'abort']; 958 | if (!jqXHR) { 959 | if (slot) { 960 | slot.rejectWith(options.context, aborted); 961 | } 962 | return send(); 963 | } 964 | return jqXHR.abort(); 965 | }; 966 | return this._enhancePromise(pipe); 967 | } 968 | return send(); 969 | }, 970 | 971 | _onAdd: function (e, data) { 972 | var that = this, 973 | result = true, 974 | options = $.extend({}, this.options, data), 975 | files = data.files, 976 | filesLength = files.length, 977 | limit = options.limitMultiFileUploads, 978 | limitSize = options.limitMultiFileUploadSize, 979 | overhead = options.limitMultiFileUploadSizeOverhead, 980 | batchSize = 0, 981 | paramName = this._getParamName(options), 982 | paramNameSet, 983 | paramNameSlice, 984 | fileSet, 985 | i, 986 | j = 0; 987 | if (!filesLength) { 988 | return false; 989 | } 990 | if (limitSize && files[0].size === undefined) { 991 | limitSize = undefined; 992 | } 993 | if (!(options.singleFileUploads || limit || limitSize) || 994 | !this._isXHRUpload(options)) { 995 | fileSet = [files]; 996 | paramNameSet = [paramName]; 997 | } else if (!(options.singleFileUploads || limitSize) && limit) { 998 | fileSet = []; 999 | paramNameSet = []; 1000 | for (i = 0; i < filesLength; i += limit) { 1001 | fileSet.push(files.slice(i, i + limit)); 1002 | paramNameSlice = paramName.slice(i, i + limit); 1003 | if (!paramNameSlice.length) { 1004 | paramNameSlice = paramName; 1005 | } 1006 | paramNameSet.push(paramNameSlice); 1007 | } 1008 | } else if (!options.singleFileUploads && limitSize) { 1009 | fileSet = []; 1010 | paramNameSet = []; 1011 | for (i = 0; i < filesLength; i = i + 1) { 1012 | batchSize += files[i].size + overhead; 1013 | if (i + 1 === filesLength || 1014 | ((batchSize + files[i + 1].size + overhead) > limitSize) || 1015 | (limit && i + 1 - j >= limit)) { 1016 | fileSet.push(files.slice(j, i + 1)); 1017 | paramNameSlice = paramName.slice(j, i + 1); 1018 | if (!paramNameSlice.length) { 1019 | paramNameSlice = paramName; 1020 | } 1021 | paramNameSet.push(paramNameSlice); 1022 | j = i + 1; 1023 | batchSize = 0; 1024 | } 1025 | } 1026 | } else { 1027 | paramNameSet = paramName; 1028 | } 1029 | data.originalFiles = files; 1030 | $.each(fileSet || files, function (index, element) { 1031 | var newData = $.extend({}, data); 1032 | newData.files = fileSet ? element : [element]; 1033 | newData.paramName = paramNameSet[index]; 1034 | that._initResponseObject(newData); 1035 | that._initProgressObject(newData); 1036 | that._addConvenienceMethods(e, newData); 1037 | result = that._trigger( 1038 | 'add', 1039 | $.Event('add', {delegatedEvent: e}), 1040 | newData 1041 | ); 1042 | return result; 1043 | }); 1044 | return result; 1045 | }, 1046 | 1047 | _replaceFileInput: function (data) { 1048 | var input = data.fileInput, 1049 | inputClone = input.clone(true), 1050 | restoreFocus = input.is(document.activeElement); 1051 | // Add a reference for the new cloned file input to the data argument: 1052 | data.fileInputClone = inputClone; 1053 | $('
').append(inputClone)[0].reset(); 1054 | // Detaching allows to insert the fileInput on another form 1055 | // without loosing the file input value: 1056 | input.after(inputClone).detach(); 1057 | // If the fileInput had focus before it was detached, 1058 | // restore focus to the inputClone. 1059 | if (restoreFocus) { 1060 | inputClone.focus(); 1061 | } 1062 | // Avoid memory leaks with the detached file input: 1063 | $.cleanData(input.unbind('remove')); 1064 | // Replace the original file input element in the fileInput 1065 | // elements set with the clone, which has been copied including 1066 | // event handlers: 1067 | this.options.fileInput = this.options.fileInput.map(function (i, el) { 1068 | if (el === input[0]) { 1069 | return inputClone[0]; 1070 | } 1071 | return el; 1072 | }); 1073 | // If the widget has been initialized on the file input itself, 1074 | // override this.element with the file input clone: 1075 | if (input[0] === this.element[0]) { 1076 | this.element = inputClone; 1077 | } 1078 | }, 1079 | 1080 | _handleFileTreeEntry: function (entry, path) { 1081 | var that = this, 1082 | dfd = $.Deferred(), 1083 | errorHandler = function (e) { 1084 | if (e && !e.entry) { 1085 | e.entry = entry; 1086 | } 1087 | // Since $.when returns immediately if one 1088 | // Deferred is rejected, we use resolve instead. 1089 | // This allows valid files and invalid items 1090 | // to be returned together in one set: 1091 | dfd.resolve([e]); 1092 | }, 1093 | successHandler = function (entries) { 1094 | that._handleFileTreeEntries( 1095 | entries, 1096 | path + entry.name + '/' 1097 | ).done(function (files) { 1098 | dfd.resolve(files); 1099 | }).fail(errorHandler); 1100 | }, 1101 | readEntries = function () { 1102 | dirReader.readEntries(function (results) { 1103 | if (!results.length) { 1104 | successHandler(entries); 1105 | } else { 1106 | entries = entries.concat(results); 1107 | readEntries(); 1108 | } 1109 | }, errorHandler); 1110 | }, 1111 | dirReader, entries = []; 1112 | path = path || ''; 1113 | if (entry.isFile) { 1114 | if (entry._file) { 1115 | // Workaround for Chrome bug #149735 1116 | entry._file.relativePath = path; 1117 | dfd.resolve(entry._file); 1118 | } else { 1119 | entry.file(function (file) { 1120 | file.relativePath = path; 1121 | dfd.resolve(file); 1122 | }, errorHandler); 1123 | } 1124 | } else if (entry.isDirectory) { 1125 | dirReader = entry.createReader(); 1126 | readEntries(); 1127 | } else { 1128 | // Return an empy list for file system items 1129 | // other than files or directories: 1130 | dfd.resolve([]); 1131 | } 1132 | return dfd.promise(); 1133 | }, 1134 | 1135 | _handleFileTreeEntries: function (entries, path) { 1136 | var that = this; 1137 | return $.when.apply( 1138 | $, 1139 | $.map(entries, function (entry) { 1140 | return that._handleFileTreeEntry(entry, path); 1141 | }) 1142 | ).pipe(function () { 1143 | return Array.prototype.concat.apply( 1144 | [], 1145 | arguments 1146 | ); 1147 | }); 1148 | }, 1149 | 1150 | _getDroppedFiles: function (dataTransfer) { 1151 | dataTransfer = dataTransfer || {}; 1152 | var items = dataTransfer.items; 1153 | if (items && items.length && (items[0].webkitGetAsEntry || 1154 | items[0].getAsEntry)) { 1155 | return this._handleFileTreeEntries( 1156 | $.map(items, function (item) { 1157 | var entry; 1158 | if (item.webkitGetAsEntry) { 1159 | entry = item.webkitGetAsEntry(); 1160 | if (entry) { 1161 | // Workaround for Chrome bug #149735: 1162 | entry._file = item.getAsFile(); 1163 | } 1164 | return entry; 1165 | } 1166 | return item.getAsEntry(); 1167 | }) 1168 | ); 1169 | } 1170 | return $.Deferred().resolve( 1171 | $.makeArray(dataTransfer.files) 1172 | ).promise(); 1173 | }, 1174 | 1175 | _getSingleFileInputFiles: function (fileInput) { 1176 | fileInput = $(fileInput); 1177 | var entries = fileInput.prop('webkitEntries') || 1178 | fileInput.prop('entries'), 1179 | files, 1180 | value; 1181 | if (entries && entries.length) { 1182 | return this._handleFileTreeEntries(entries); 1183 | } 1184 | files = $.makeArray(fileInput.prop('files')); 1185 | if (!files.length) { 1186 | value = fileInput.prop('value'); 1187 | if (!value) { 1188 | return $.Deferred().resolve([]).promise(); 1189 | } 1190 | // If the files property is not available, the browser does not 1191 | // support the File API and we add a pseudo File object with 1192 | // the input value as name with path information removed: 1193 | files = [{name: value.replace(/^.*\\/, '')}]; 1194 | } else if (files[0].name === undefined && files[0].fileName) { 1195 | // File normalization for Safari 4 and Firefox 3: 1196 | $.each(files, function (index, file) { 1197 | file.name = file.fileName; 1198 | file.size = file.fileSize; 1199 | }); 1200 | } 1201 | return $.Deferred().resolve(files).promise(); 1202 | }, 1203 | 1204 | _getFileInputFiles: function (fileInput) { 1205 | if (!(fileInput instanceof $) || fileInput.length === 1) { 1206 | return this._getSingleFileInputFiles(fileInput); 1207 | } 1208 | return $.when.apply( 1209 | $, 1210 | $.map(fileInput, this._getSingleFileInputFiles) 1211 | ).pipe(function () { 1212 | return Array.prototype.concat.apply( 1213 | [], 1214 | arguments 1215 | ); 1216 | }); 1217 | }, 1218 | 1219 | _onChange: function (e) { 1220 | var that = this, 1221 | data = { 1222 | fileInput: $(e.target), 1223 | form: $(e.target.form) 1224 | }; 1225 | this._getFileInputFiles(data.fileInput).always(function (files) { 1226 | data.files = files; 1227 | if (that.options.replaceFileInput) { 1228 | that._replaceFileInput(data); 1229 | } 1230 | if (that._trigger( 1231 | 'change', 1232 | $.Event('change', {delegatedEvent: e}), 1233 | data 1234 | ) !== false) { 1235 | that._onAdd(e, data); 1236 | } 1237 | }); 1238 | }, 1239 | 1240 | _onPaste: function (e) { 1241 | var items = e.originalEvent && e.originalEvent.clipboardData && 1242 | e.originalEvent.clipboardData.items, 1243 | data = {files: []}; 1244 | if (items && items.length) { 1245 | $.each(items, function (index, item) { 1246 | var file = item.getAsFile && item.getAsFile(); 1247 | if (file) { 1248 | data.files.push(file); 1249 | } 1250 | }); 1251 | if (this._trigger( 1252 | 'paste', 1253 | $.Event('paste', {delegatedEvent: e}), 1254 | data 1255 | ) !== false) { 1256 | this._onAdd(e, data); 1257 | } 1258 | } 1259 | }, 1260 | 1261 | _onDrop: function (e) { 1262 | e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; 1263 | var that = this, 1264 | dataTransfer = e.dataTransfer, 1265 | data = {}; 1266 | if (dataTransfer && dataTransfer.files && dataTransfer.files.length) { 1267 | e.preventDefault(); 1268 | this._getDroppedFiles(dataTransfer).always(function (files) { 1269 | data.files = files; 1270 | if (that._trigger( 1271 | 'drop', 1272 | $.Event('drop', {delegatedEvent: e}), 1273 | data 1274 | ) !== false) { 1275 | that._onAdd(e, data); 1276 | } 1277 | }); 1278 | } 1279 | }, 1280 | 1281 | _onDragOver: getDragHandler('dragover'), 1282 | 1283 | _onDragEnter: getDragHandler('dragenter'), 1284 | 1285 | _onDragLeave: getDragHandler('dragleave'), 1286 | 1287 | _initEventHandlers: function () { 1288 | if (this._isXHRUpload(this.options)) { 1289 | this._on(this.options.dropZone, { 1290 | dragover: this._onDragOver, 1291 | drop: this._onDrop, 1292 | // event.preventDefault() on dragenter is required for IE10+: 1293 | dragenter: this._onDragEnter, 1294 | // dragleave is not required, but added for completeness: 1295 | dragleave: this._onDragLeave 1296 | }); 1297 | this._on(this.options.pasteZone, { 1298 | paste: this._onPaste 1299 | }); 1300 | } 1301 | if ($.support.fileInput) { 1302 | this._on(this.options.fileInput, { 1303 | change: this._onChange 1304 | }); 1305 | } 1306 | }, 1307 | 1308 | _destroyEventHandlers: function () { 1309 | this._off(this.options.dropZone, 'dragenter dragleave dragover drop'); 1310 | this._off(this.options.pasteZone, 'paste'); 1311 | this._off(this.options.fileInput, 'change'); 1312 | }, 1313 | 1314 | _setOption: function (key, value) { 1315 | var reinit = $.inArray(key, this._specialOptions) !== -1; 1316 | if (reinit) { 1317 | this._destroyEventHandlers(); 1318 | } 1319 | this._super(key, value); 1320 | if (reinit) { 1321 | this._initSpecialOptions(); 1322 | this._initEventHandlers(); 1323 | } 1324 | }, 1325 | 1326 | _initSpecialOptions: function () { 1327 | var options = this.options; 1328 | if (options.fileInput === undefined) { 1329 | options.fileInput = this.element.is('input[type="file"]') ? 1330 | this.element : this.element.find('input[type="file"]'); 1331 | } else if (!(options.fileInput instanceof $)) { 1332 | options.fileInput = $(options.fileInput); 1333 | } 1334 | if (!(options.dropZone instanceof $)) { 1335 | options.dropZone = $(options.dropZone); 1336 | } 1337 | if (!(options.pasteZone instanceof $)) { 1338 | options.pasteZone = $(options.pasteZone); 1339 | } 1340 | }, 1341 | 1342 | _getRegExp: function (str) { 1343 | var parts = str.split('/'), 1344 | modifiers = parts.pop(); 1345 | parts.shift(); 1346 | return new RegExp(parts.join('/'), modifiers); 1347 | }, 1348 | 1349 | _isRegExpOption: function (key, value) { 1350 | return key !== 'url' && $.type(value) === 'string' && 1351 | /^\/.*\/[igm]{0,3}$/.test(value); 1352 | }, 1353 | 1354 | _initDataAttributes: function () { 1355 | var that = this, 1356 | options = this.options, 1357 | data = this.element.data(); 1358 | // Initialize options set via HTML5 data-attributes: 1359 | $.each( 1360 | this.element[0].attributes, 1361 | function (index, attr) { 1362 | var key = attr.name.toLowerCase(), 1363 | value; 1364 | if (/^data-/.test(key)) { 1365 | // Convert hyphen-ated key to camelCase: 1366 | key = key.slice(5).replace(/-[a-z]/g, function (str) { 1367 | return str.charAt(1).toUpperCase(); 1368 | }); 1369 | value = data[key]; 1370 | if (that._isRegExpOption(key, value)) { 1371 | value = that._getRegExp(value); 1372 | } 1373 | options[key] = value; 1374 | } 1375 | } 1376 | ); 1377 | }, 1378 | 1379 | _create: function () { 1380 | this._initDataAttributes(); 1381 | this._initSpecialOptions(); 1382 | this._slots = []; 1383 | this._sequence = this._getXHRPromise(true); 1384 | this._sending = this._active = 0; 1385 | this._initProgressObject(this); 1386 | this._initEventHandlers(); 1387 | }, 1388 | 1389 | // This method is exposed to the widget API and allows to query 1390 | // the number of active uploads: 1391 | active: function () { 1392 | return this._active; 1393 | }, 1394 | 1395 | // This method is exposed to the widget API and allows to query 1396 | // the widget upload progress. 1397 | // It returns an object with loaded, total and bitrate properties 1398 | // for the running uploads: 1399 | progress: function () { 1400 | return this._progress; 1401 | }, 1402 | 1403 | // This method is exposed to the widget API and allows adding files 1404 | // using the fileupload API. The data parameter accepts an object which 1405 | // must have a files property and can contain additional options: 1406 | // .fileupload('add', {files: filesList}); 1407 | add: function (data) { 1408 | var that = this; 1409 | if (!data || this.options.disabled) { 1410 | return; 1411 | } 1412 | if (data.fileInput && !data.files) { 1413 | this._getFileInputFiles(data.fileInput).always(function (files) { 1414 | data.files = files; 1415 | that._onAdd(null, data); 1416 | }); 1417 | } else { 1418 | data.files = $.makeArray(data.files); 1419 | this._onAdd(null, data); 1420 | } 1421 | }, 1422 | 1423 | // This method is exposed to the widget API and allows sending files 1424 | // using the fileupload API. The data parameter accepts an object which 1425 | // must have a files or fileInput property and can contain additional options: 1426 | // .fileupload('send', {files: filesList}); 1427 | // The method returns a Promise object for the file upload call. 1428 | send: function (data) { 1429 | if (data && !this.options.disabled) { 1430 | if (data.fileInput && !data.files) { 1431 | var that = this, 1432 | dfd = $.Deferred(), 1433 | promise = dfd.promise(), 1434 | jqXHR, 1435 | aborted; 1436 | promise.abort = function () { 1437 | aborted = true; 1438 | if (jqXHR) { 1439 | return jqXHR.abort(); 1440 | } 1441 | dfd.reject(null, 'abort', 'abort'); 1442 | return promise; 1443 | }; 1444 | this._getFileInputFiles(data.fileInput).always( 1445 | function (files) { 1446 | if (aborted) { 1447 | return; 1448 | } 1449 | if (!files.length) { 1450 | dfd.reject(); 1451 | return; 1452 | } 1453 | data.files = files; 1454 | jqXHR = that._onSend(null, data); 1455 | jqXHR.then( 1456 | function (result, textStatus, jqXHR) { 1457 | dfd.resolve(result, textStatus, jqXHR); 1458 | }, 1459 | function (jqXHR, textStatus, errorThrown) { 1460 | dfd.reject(jqXHR, textStatus, errorThrown); 1461 | } 1462 | ); 1463 | } 1464 | ); 1465 | return this._enhancePromise(promise); 1466 | } 1467 | data.files = $.makeArray(data.files); 1468 | if (data.files.length) { 1469 | return this._onSend(null, data); 1470 | } 1471 | } 1472 | return this._getXHRPromise(false, data && data.context); 1473 | } 1474 | 1475 | }); 1476 | 1477 | })); 1478 | -------------------------------------------------------------------------------- /lib/jquery.iframe-transport.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Iframe Transport Plugin 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2011, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * http://www.opensource.org/licenses/MIT 10 | */ 11 | 12 | /* global define, require, window, document */ 13 | 14 | (function (factory) { 15 | 'use strict'; 16 | if (typeof define === 'function' && define.amd) { 17 | // Register as an anonymous AMD module: 18 | define(['jquery'], factory); 19 | } else if (typeof exports === 'object') { 20 | // Node/CommonJS: 21 | factory(require('jquery')); 22 | } else { 23 | // Browser globals: 24 | factory(window.jQuery); 25 | } 26 | }(function ($) { 27 | 'use strict'; 28 | 29 | // Helper variable to create unique names for the transport iframes: 30 | var counter = 0; 31 | 32 | // The iframe transport accepts four additional options: 33 | // options.fileInput: a jQuery collection of file input fields 34 | // options.paramName: the parameter name for the file form data, 35 | // overrides the name property of the file input field(s), 36 | // can be a string or an array of strings. 37 | // options.formData: an array of objects with name and value properties, 38 | // equivalent to the return data of .serializeArray(), e.g.: 39 | // [{name: 'a', value: 1}, {name: 'b', value: 2}] 40 | // options.initialIframeSrc: the URL of the initial iframe src, 41 | // by default set to "javascript:false;" 42 | $.ajaxTransport('iframe', function (options) { 43 | if (options.async) { 44 | // javascript:false as initial iframe src 45 | // prevents warning popups on HTTPS in IE6: 46 | /*jshint scripturl: true */ 47 | var initialIframeSrc = options.initialIframeSrc || 'javascript:false;', 48 | /*jshint scripturl: false */ 49 | form, 50 | iframe, 51 | addParamChar; 52 | return { 53 | send: function (_, completeCallback) { 54 | form = $(''); 55 | form.attr('accept-charset', options.formAcceptCharset); 56 | addParamChar = /\?/.test(options.url) ? '&' : '?'; 57 | // XDomainRequest only supports GET and POST: 58 | if (options.type === 'DELETE') { 59 | options.url = options.url + addParamChar + '_method=DELETE'; 60 | options.type = 'POST'; 61 | } else if (options.type === 'PUT') { 62 | options.url = options.url + addParamChar + '_method=PUT'; 63 | options.type = 'POST'; 64 | } else if (options.type === 'PATCH') { 65 | options.url = options.url + addParamChar + '_method=PATCH'; 66 | options.type = 'POST'; 67 | } 68 | // IE versions below IE8 cannot set the name property of 69 | // elements that have already been added to the DOM, 70 | // so we set the name along with the iframe HTML markup: 71 | counter += 1; 72 | iframe = $( 73 | '' 75 | ).bind('load', function () { 76 | var fileInputClones, 77 | paramNames = $.isArray(options.paramName) ? 78 | options.paramName : [options.paramName]; 79 | iframe 80 | .unbind('load') 81 | .bind('load', function () { 82 | var response; 83 | // Wrap in a try/catch block to catch exceptions thrown 84 | // when trying to access cross-domain iframe contents: 85 | try { 86 | response = iframe.contents(); 87 | // Google Chrome and Firefox do not throw an 88 | // exception when calling iframe.contents() on 89 | // cross-domain requests, so we unify the response: 90 | if (!response.length || !response[0].firstChild) { 91 | throw new Error(); 92 | } 93 | } catch (e) { 94 | response = undefined; 95 | } 96 | // The complete callback returns the 97 | // iframe content document as response object: 98 | completeCallback( 99 | 200, 100 | 'success', 101 | {'iframe': response} 102 | ); 103 | // Fix for IE endless progress bar activity bug 104 | // (happens on form submits to iframe targets): 105 | $('') 106 | .appendTo(form); 107 | window.setTimeout(function () { 108 | // Removing the form in a setTimeout call 109 | // allows Chrome's developer tools to display 110 | // the response result 111 | form.remove(); 112 | }, 0); 113 | }); 114 | form 115 | .prop('target', iframe.prop('name')) 116 | .prop('action', options.url) 117 | .prop('method', options.type); 118 | if (options.formData) { 119 | $.each(options.formData, function (index, field) { 120 | $('') 121 | .prop('name', field.name) 122 | .val(field.value) 123 | .appendTo(form); 124 | }); 125 | } 126 | if (options.fileInput && options.fileInput.length && 127 | options.type === 'POST') { 128 | fileInputClones = options.fileInput.clone(); 129 | // Insert a clone for each file input field: 130 | options.fileInput.after(function (index) { 131 | return fileInputClones[index]; 132 | }); 133 | if (options.paramName) { 134 | options.fileInput.each(function (index) { 135 | $(this).prop( 136 | 'name', 137 | paramNames[index] || options.paramName 138 | ); 139 | }); 140 | } 141 | // Appending the file input fields to the hidden form 142 | // removes them from their original location: 143 | form 144 | .append(options.fileInput) 145 | .prop('enctype', 'multipart/form-data') 146 | // enctype must be set as encoding for IE: 147 | .prop('encoding', 'multipart/form-data'); 148 | // Remove the HTML5 form attribute from the input(s): 149 | options.fileInput.removeAttr('form'); 150 | } 151 | form.submit(); 152 | // Insert the file input fields at their original location 153 | // by replacing the clones with the originals: 154 | if (fileInputClones && fileInputClones.length) { 155 | options.fileInput.each(function (index, input) { 156 | var clone = $(fileInputClones[index]); 157 | // Restore the original name and form properties: 158 | $(input) 159 | .prop('name', clone.prop('name')) 160 | .attr('form', clone.attr('form')); 161 | clone.replaceWith(input); 162 | }); 163 | } 164 | }); 165 | form.append(iframe).appendTo(document.body); 166 | }, 167 | abort: function () { 168 | if (iframe) { 169 | // javascript:false as iframe src aborts the request 170 | // and prevents warning popups on HTTPS in IE6. 171 | // concat is used to avoid the "Script URL" JSLint error: 172 | iframe 173 | .unbind('load') 174 | .prop('src', initialIframeSrc); 175 | } 176 | if (form) { 177 | form.remove(); 178 | } 179 | } 180 | }; 181 | } 182 | }); 183 | 184 | // The iframe transport returns the iframe content document as response. 185 | // The following adds converters from iframe to text, json, html, xml 186 | // and script. 187 | // Please note that the Content-Type for JSON responses has to be text/plain 188 | // or text/html, if the browser doesn't include application/json in the 189 | // Accept header, else IE will show a download dialog. 190 | // The Content-Type for XML responses on the other hand has to be always 191 | // application/xml or text/xml, so IE properly parses the XML response. 192 | // See also 193 | // https://github.com/blueimp/jQuery-File-Upload/wiki/Setup#content-type-negotiation 194 | $.ajaxSetup({ 195 | converters: { 196 | 'iframe text': function (iframe) { 197 | return iframe && $(iframe[0].body).text(); 198 | }, 199 | 'iframe json': function (iframe) { 200 | return iframe && $.parseJSON($(iframe[0].body).text()); 201 | }, 202 | 'iframe html': function (iframe) { 203 | return iframe && $(iframe[0].body).html(); 204 | }, 205 | 'iframe xml': function (iframe) { 206 | var xmlDoc = iframe && iframe[0]; 207 | return xmlDoc && $.isXMLDoc(xmlDoc) ? xmlDoc : 208 | $.parseXML((xmlDoc.XMLDocument && xmlDoc.XMLDocument.xml) || 209 | $(xmlDoc.body).html()); 210 | }, 211 | 'iframe script': function (iframe) { 212 | return iframe && $.globalEval($(iframe[0].body).text()); 213 | } 214 | } 215 | }); 216 | 217 | })); 218 | -------------------------------------------------------------------------------- /lib/vendor/jquery.ui.widget.js: -------------------------------------------------------------------------------- 1 | /*! jQuery UI - v1.11.4+CommonJS - 2015-08-28 2 | * http://jqueryui.com 3 | * Includes: widget.js 4 | * Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */ 5 | 6 | (function( factory ) { 7 | if ( typeof define === "function" && define.amd ) { 8 | 9 | // AMD. Register as an anonymous module. 10 | define([ "jquery" ], factory ); 11 | 12 | } else if ( typeof exports === "object" ) { 13 | 14 | // Node/CommonJS 15 | factory( require( "jquery" ) ); 16 | 17 | } else { 18 | 19 | // Browser globals 20 | factory( jQuery ); 21 | } 22 | }(function( $ ) { 23 | /*! 24 | * jQuery UI Widget 1.11.4 25 | * http://jqueryui.com 26 | * 27 | * Copyright jQuery Foundation and other contributors 28 | * Released under the MIT license. 29 | * http://jquery.org/license 30 | * 31 | * http://api.jqueryui.com/jQuery.widget/ 32 | */ 33 | 34 | 35 | var widget_uuid = 0, 36 | widget_slice = Array.prototype.slice; 37 | 38 | $.cleanData = (function( orig ) { 39 | return function( elems ) { 40 | var events, elem, i; 41 | for ( i = 0; (elem = elems[i]) != null; i++ ) { 42 | try { 43 | 44 | // Only trigger remove when necessary to save time 45 | events = $._data( elem, "events" ); 46 | if ( events && events.remove ) { 47 | $( elem ).triggerHandler( "remove" ); 48 | } 49 | 50 | // http://bugs.jquery.com/ticket/8235 51 | } catch ( e ) {} 52 | } 53 | orig( elems ); 54 | }; 55 | })( $.cleanData ); 56 | 57 | $.widget = function( name, base, prototype ) { 58 | var fullName, existingConstructor, constructor, basePrototype, 59 | // proxiedPrototype allows the provided prototype to remain unmodified 60 | // so that it can be used as a mixin for multiple widgets (#8876) 61 | proxiedPrototype = {}, 62 | namespace = name.split( "." )[ 0 ]; 63 | 64 | name = name.split( "." )[ 1 ]; 65 | fullName = namespace + "-" + name; 66 | 67 | if ( !prototype ) { 68 | prototype = base; 69 | base = $.Widget; 70 | } 71 | 72 | // create selector for plugin 73 | $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { 74 | return !!$.data( elem, fullName ); 75 | }; 76 | 77 | $[ namespace ] = $[ namespace ] || {}; 78 | existingConstructor = $[ namespace ][ name ]; 79 | constructor = $[ namespace ][ name ] = function( options, element ) { 80 | // allow instantiation without "new" keyword 81 | if ( !this._createWidget ) { 82 | return new constructor( options, element ); 83 | } 84 | 85 | // allow instantiation without initializing for simple inheritance 86 | // must use "new" keyword (the code above always passes args) 87 | if ( arguments.length ) { 88 | this._createWidget( options, element ); 89 | } 90 | }; 91 | // extend with the existing constructor to carry over any static properties 92 | $.extend( constructor, existingConstructor, { 93 | version: prototype.version, 94 | // copy the object used to create the prototype in case we need to 95 | // redefine the widget later 96 | _proto: $.extend( {}, prototype ), 97 | // track widgets that inherit from this widget in case this widget is 98 | // redefined after a widget inherits from it 99 | _childConstructors: [] 100 | }); 101 | 102 | basePrototype = new base(); 103 | // we need to make the options hash a property directly on the new instance 104 | // otherwise we'll modify the options hash on the prototype that we're 105 | // inheriting from 106 | basePrototype.options = $.widget.extend( {}, basePrototype.options ); 107 | $.each( prototype, function( prop, value ) { 108 | if ( !$.isFunction( value ) ) { 109 | proxiedPrototype[ prop ] = value; 110 | return; 111 | } 112 | proxiedPrototype[ prop ] = (function() { 113 | var _super = function() { 114 | return base.prototype[ prop ].apply( this, arguments ); 115 | }, 116 | _superApply = function( args ) { 117 | return base.prototype[ prop ].apply( this, args ); 118 | }; 119 | return function() { 120 | var __super = this._super, 121 | __superApply = this._superApply, 122 | returnValue; 123 | 124 | this._super = _super; 125 | this._superApply = _superApply; 126 | 127 | returnValue = value.apply( this, arguments ); 128 | 129 | this._super = __super; 130 | this._superApply = __superApply; 131 | 132 | return returnValue; 133 | }; 134 | })(); 135 | }); 136 | constructor.prototype = $.widget.extend( basePrototype, { 137 | // TODO: remove support for widgetEventPrefix 138 | // always use the name + a colon as the prefix, e.g., draggable:start 139 | // don't prefix for widgets that aren't DOM-based 140 | widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name 141 | }, proxiedPrototype, { 142 | constructor: constructor, 143 | namespace: namespace, 144 | widgetName: name, 145 | widgetFullName: fullName 146 | }); 147 | 148 | // If this widget is being redefined then we need to find all widgets that 149 | // are inheriting from it and redefine all of them so that they inherit from 150 | // the new version of this widget. We're essentially trying to replace one 151 | // level in the prototype chain. 152 | if ( existingConstructor ) { 153 | $.each( existingConstructor._childConstructors, function( i, child ) { 154 | var childPrototype = child.prototype; 155 | 156 | // redefine the child widget using the same prototype that was 157 | // originally used, but inherit from the new version of the base 158 | $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); 159 | }); 160 | // remove the list of existing child constructors from the old constructor 161 | // so the old child constructors can be garbage collected 162 | delete existingConstructor._childConstructors; 163 | } else { 164 | base._childConstructors.push( constructor ); 165 | } 166 | 167 | $.widget.bridge( name, constructor ); 168 | 169 | return constructor; 170 | }; 171 | 172 | $.widget.extend = function( target ) { 173 | var input = widget_slice.call( arguments, 1 ), 174 | inputIndex = 0, 175 | inputLength = input.length, 176 | key, 177 | value; 178 | for ( ; inputIndex < inputLength; inputIndex++ ) { 179 | for ( key in input[ inputIndex ] ) { 180 | value = input[ inputIndex ][ key ]; 181 | if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { 182 | // Clone objects 183 | if ( $.isPlainObject( value ) ) { 184 | target[ key ] = $.isPlainObject( target[ key ] ) ? 185 | $.widget.extend( {}, target[ key ], value ) : 186 | // Don't extend strings, arrays, etc. with objects 187 | $.widget.extend( {}, value ); 188 | // Copy everything else by reference 189 | } else { 190 | target[ key ] = value; 191 | } 192 | } 193 | } 194 | } 195 | return target; 196 | }; 197 | 198 | $.widget.bridge = function( name, object ) { 199 | var fullName = object.prototype.widgetFullName || name; 200 | $.fn[ name ] = function( options ) { 201 | var isMethodCall = typeof options === "string", 202 | args = widget_slice.call( arguments, 1 ), 203 | returnValue = this; 204 | 205 | if ( isMethodCall ) { 206 | this.each(function() { 207 | var methodValue, 208 | instance = $.data( this, fullName ); 209 | if ( options === "instance" ) { 210 | returnValue = instance; 211 | return false; 212 | } 213 | if ( !instance ) { 214 | return $.error( "cannot call methods on " + name + " prior to initialization; " + 215 | "attempted to call method '" + options + "'" ); 216 | } 217 | if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { 218 | return $.error( "no such method '" + options + "' for " + name + " widget instance" ); 219 | } 220 | methodValue = instance[ options ].apply( instance, args ); 221 | if ( methodValue !== instance && methodValue !== undefined ) { 222 | returnValue = methodValue && methodValue.jquery ? 223 | returnValue.pushStack( methodValue.get() ) : 224 | methodValue; 225 | return false; 226 | } 227 | }); 228 | } else { 229 | 230 | // Allow multiple hashes to be passed on init 231 | if ( args.length ) { 232 | options = $.widget.extend.apply( null, [ options ].concat(args) ); 233 | } 234 | 235 | this.each(function() { 236 | var instance = $.data( this, fullName ); 237 | if ( instance ) { 238 | instance.option( options || {} ); 239 | if ( instance._init ) { 240 | instance._init(); 241 | } 242 | } else { 243 | $.data( this, fullName, new object( options, this ) ); 244 | } 245 | }); 246 | } 247 | 248 | return returnValue; 249 | }; 250 | }; 251 | 252 | $.Widget = function( /* options, element */ ) {}; 253 | $.Widget._childConstructors = []; 254 | 255 | $.Widget.prototype = { 256 | widgetName: "widget", 257 | widgetEventPrefix: "", 258 | defaultElement: "