jquery.ui.plupload.js 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307
  1. /**
  2. * jquery.ui.plupload.js
  3. *
  4. * Copyright 2013, Moxiecode Systems AB
  5. * Released under GPL License.
  6. *
  7. * License: http://www.plupload.com/license
  8. * Contributing: http://www.plupload.com/contributing
  9. *
  10. * Depends:
  11. * jquery.ui.core.js
  12. * jquery.ui.widget.js
  13. * jquery.ui.button.js
  14. * jquery.ui.progressbar.js
  15. *
  16. * Optionally:
  17. * jquery.ui.sortable.js
  18. */
  19. /* global jQuery:true */
  20. /**
  21. jQuery UI based implementation of the Plupload API - multi-runtime file uploading API.
  22. To use the widget you must include _jQuery_ and _jQuery UI_ bundle (including `ui.core`, `ui.widget`, `ui.button`,
  23. `ui.progressbar` and `ui.sortable`).
  24. In general the widget is designed the way that you do not usually need to do anything to it after you instantiate it.
  25. But! You still can intervenue, to some extent, in case you need to. Although, due to the fact that widget is based on
  26. _jQuery UI_ widget factory, there are some specifics. See examples below for more details.
  27. @example
  28. <!-- Instantiating: -->
  29. <div id="uploader">
  30. <p>Your browser doesn't have Flash, Silverlight or HTML5 support.</p>
  31. </div>
  32. <script>
  33. $('#uploader').plupload({
  34. url : '../upload.php',
  35. filters : [
  36. {title : "Image files", extensions : "jpg,gif,png"}
  37. ],
  38. rename: true,
  39. sortable: true,
  40. flash_swf_url : '../../js/Moxie.swf',
  41. silverlight_xap_url : '../../js/Moxie.xap',
  42. });
  43. </script>
  44. @example
  45. // Invoking methods:
  46. $('#uploader').plupload(options);
  47. // Display welcome message in the notification area
  48. $('#uploader').plupload('notify', 'info', "This might be obvious, but you need to click 'Add Files' to add some files.");
  49. @example
  50. // Subscribing to the events...
  51. // ... on initialization:
  52. $('#uploader').plupload({
  53. ...
  54. viewchanged: function(event, args) {
  55. // stuff ...
  56. }
  57. });
  58. // ... or after initialization
  59. $('#uploader').on("viewchanged", function(event, args) {
  60. // stuff ...
  61. });
  62. @class UI.Plupload
  63. @constructor
  64. @param {Object} settings For detailed information about each option check documentation.
  65. @param {String} settings.url URL of the server-side upload handler.
  66. @param {Number|String} [settings.chunk_size=0] Chunk size in bytes to slice the file into. Shorcuts with b, kb, mb, gb, tb suffixes also supported. `e.g. 204800 or "204800b" or "200kb"`. By default - disabled.
  67. @param {String} [settings.file_data_name="file"] Name for the file field in Multipart formated message.
  68. @param {Array} [settings.filters=[]] Set of file type filters, each one defined by hash of title and extensions. `e.g. {title : "Image files", extensions : "jpg,jpeg,gif,png"}`. Dispatches `plupload.FILE_EXTENSION_ERROR`
  69. @param {String} [settings.flash_swf_url] URL of the Flash swf.
  70. @param {Object} [settings.headers] Custom headers to send with the upload. Hash of name/value pairs.
  71. @param {Number|String} [settings.max_file_size] Maximum file size that the user can pick, in bytes. Optionally supports b, kb, mb, gb, tb suffixes. `e.g. "10mb" or "1gb"`. By default - not set. Dispatches `plupload.FILE_SIZE_ERROR`.
  72. @param {Number} [settings.max_retries=0] How many times to retry the chunk or file, before triggering Error event.
  73. @param {Boolean} [settings.multipart=true] Whether to send file and additional parameters as Multipart formated message.
  74. @param {Object} [settings.multipart_params] Hash of key/value pairs to send with every file upload.
  75. @param {Boolean} [settings.multi_selection=true] Enable ability to select multiple files at once in file dialog.
  76. @param {Boolean} [settings.prevent_duplicates=false] Do not let duplicates into the queue. Dispatches `plupload.FILE_DUPLICATE_ERROR`.
  77. @param {String|Object} [settings.required_features] Either comma-separated list or hash of required features that chosen runtime should absolutely possess.
  78. @param {Object} [settings.resize] Enable resizng of images on client-side. Applies to `image/jpeg` and `image/png` only. `e.g. {width : 200, height : 200, quality : 90, crop: true}`
  79. @param {Number} [settings.resize.width] If image is bigger, it will be resized.
  80. @param {Number} [settings.resize.height] If image is bigger, it will be resized.
  81. @param {Number} [settings.resize.quality=90] Compression quality for jpegs (1-100).
  82. @param {Boolean} [settings.resize.crop=false] Whether to crop images to exact dimensions. By default they will be resized proportionally.
  83. @param {String} [settings.runtimes="html5,flash,silverlight,html4"] Comma separated list of runtimes, that Plupload will try in turn, moving to the next if previous fails.
  84. @param {String} [settings.silverlight_xap_url] URL of the Silverlight xap.
  85. @param {Boolean} [settings.unique_names=false] If true will generate unique filenames for uploaded files.
  86. @param {Boolean} [settings.autostart=false] Whether to auto start uploading right after file selection.
  87. @param {Boolean} [settings.dragdrop=true] Enable ability to add file to the queue by drag'n'dropping them from the desktop.
  88. @param {Boolean} [settings.rename=false] Enable ability to rename files in the queue.
  89. @param {Boolean} [settings.sortable=false] Enable ability to sort files in the queue, changing their uploading priority.
  90. @param {Object} [settings.buttons] Control the visibility of functional buttons.
  91. @param {Boolean} [settings.buttons.browse=true] Display browse button.
  92. @param {Boolean} [settings.buttons.start=true] Display start button.
  93. @param {Boolean} [settings.buttons.stop=true] Display stop button.
  94. @param {Object} [settings.views] Control various views of the file queue.
  95. @param {Boolean} [settings.views.list=true] Enable list view.
  96. @param {Boolean} [settings.views.thumbs=false] Enable thumbs view.
  97. @param {String} [settings.views.default='list'] Default view.
  98. @param {Boolean} [settings.views.remember=true] Whether to remember the current view (requires jQuery Cookie plugin).
  99. @param {Boolean} [settings.multiple_queues=true] Re-activate the widget after each upload procedure.
  100. @param {Number} [settings.max_file_count=0] Limit the number of files user is able to upload in one go, autosets _multiple_queues_ to _false_ (default is 0 - no limit).
  101. */
  102. (function(window, document, plupload, o, $) {
  103. /**
  104. Dispatched when the widget is initialized and ready.
  105. @event ready
  106. @param {plupload.Uploader} uploader Uploader instance sending the event.
  107. */
  108. /**
  109. Dispatched when file dialog is closed.
  110. @event selected
  111. @param {plupload.Uploader} uploader Uploader instance sending the event.
  112. @param {Array} files Array of selected files represented by plupload.File objects
  113. */
  114. /**
  115. Dispatched when file dialog is closed.
  116. @event removed
  117. @param {plupload.Uploader} uploader Uploader instance sending the event.
  118. @param {Array} files Array of removed files represented by plupload.File objects
  119. */
  120. /**
  121. Dispatched when upload is started.
  122. @event start
  123. @param {plupload.Uploader} uploader Uploader instance sending the event.
  124. */
  125. /**
  126. Dispatched when upload is stopped.
  127. @event stop
  128. @param {plupload.Uploader} uploader Uploader instance sending the event.
  129. */
  130. /**
  131. Dispatched during the upload process.
  132. @event progress
  133. @param {plupload.Uploader} uploader Uploader instance sending the event.
  134. @param {plupload.File} file File that is being uploaded (includes loaded and percent properties among others).
  135. @param {Number} size Total file size in bytes.
  136. @param {Number} loaded Number of bytes uploaded of the files total size.
  137. @param {Number} percent Number of percentage uploaded of the file.
  138. */
  139. /**
  140. Dispatched when file is uploaded.
  141. @event uploaded
  142. @param {plupload.Uploader} uploader Uploader instance sending the event.
  143. @param {plupload.File} file File that was uploaded.
  144. @param {Enum} status Status constant matching the plupload states QUEUED, UPLOADING, FAILED, DONE.
  145. */
  146. /**
  147. Dispatched when upload of the whole queue is complete.
  148. @event complete
  149. @param {plupload.Uploader} uploader Uploader instance sending the event.
  150. @param {Array} files Array of uploaded files represented by plupload.File objects
  151. */
  152. /**
  153. Dispatched when the view is changed, e.g. from `list` to `thumbs` or vice versa.
  154. @event viewchanged
  155. @param {plupload.Uploader} uploader Uploader instance sending the event.
  156. @param {String} type Current view type.
  157. */
  158. /**
  159. Dispatched when error of some kind is detected.
  160. @event error
  161. @param {plupload.Uploader} uploader Uploader instance sending the event.
  162. @param {String} error Error message.
  163. @param {plupload.File} file File that was uploaded.
  164. @param {Enum} status Status constant matching the plupload states QUEUED, UPLOADING, FAILED, DONE.
  165. */
  166. var uploaders = {};
  167. function _(str) {
  168. return plupload.translate(str) || str;
  169. }
  170. function renderUI(obj) {
  171. obj.id = obj.attr('id');
  172. obj.html(
  173. '<div class="plupload_wrapper">' +
  174. '<div class="ui-widget-content plupload_container">' +
  175. '<div class="ui-state-default ui-widget-header plupload_header">' +
  176. '<div class="plupload_header_content">' +
  177. '<div class="plupload_logo"> </div>' +
  178. '<div class="plupload_header_title">' + _('Select files') + '</div>' +
  179. '<div class="plupload_header_text">' + _('Add files to the upload queue and click the start button.') + '</div>' +
  180. '<div class="plupload_view_switch">' +
  181. '<input type="radio" id="'+obj.id+'_view_list" name="view_mode_'+obj.id+'" checked="checked" /><label class="plupload_button" for="'+obj.id+'_view_list" data-view="list">' + _('List') + '</label>' +
  182. '<input type="radio" id="'+obj.id+'_view_thumbs" name="view_mode_'+obj.id+'" /><label class="plupload_button" for="'+obj.id+'_view_thumbs" data-view="thumbs">' + _('Thumbnails') + '</label>' +
  183. '</div>' +
  184. '</div>' +
  185. '</div>' +
  186. '<table class="plupload_filelist plupload_filelist_header ui-widget-header">' +
  187. '<tr>' +
  188. '<td class="plupload_cell plupload_file_name">' + _('Filename') + '</td>' +
  189. '<td class="plupload_cell plupload_file_status">' + _('Status') + '</td>' +
  190. '<td class="plupload_cell plupload_file_size">' + _('Size') + '</td>' +
  191. '<td class="plupload_cell plupload_file_action">&nbsp;</td>' +
  192. '</tr>' +
  193. '</table>' +
  194. '<div class="plupload_content">' +
  195. '<div class="plupload_droptext">' + _("Drag files here.") + '</div>' +
  196. '<ul class="plupload_filelist_content"> </ul>' +
  197. '<div class="plupload_clearer">&nbsp;</div>' +
  198. '</div>' +
  199. '<table class="plupload_filelist plupload_filelist_footer ui-widget-header">' +
  200. '<tr>' +
  201. '<td class="plupload_cell plupload_file_name">' +
  202. '<div class="plupload_buttons"><!-- Visible -->' +
  203. '<a class="plupload_button plupload_add">' + _('Add Files') + '</a>&nbsp;' +
  204. '<a class="plupload_button plupload_start">' + _('Start Upload') + '</a>&nbsp;' +
  205. '<a class="plupload_button plupload_stop plupload_hidden">'+_('Stop Upload') + '</a>&nbsp;' +
  206. '</div>' +
  207. '<div class="plupload_started plupload_hidden"><!-- Hidden -->' +
  208. '<div class="plupload_progress plupload_right">' +
  209. '<div class="plupload_progress_container"></div>' +
  210. '</div>' +
  211. '<div class="plupload_cell plupload_upload_status"></div>' +
  212. '<div class="plupload_clearer">&nbsp;</div>' +
  213. '</div>' +
  214. '</td>' +
  215. '<td class="plupload_file_status"><span class="plupload_total_status">0%</span></td>' +
  216. '<td class="plupload_file_size"><span class="plupload_total_file_size">0 kb</span></td>' +
  217. '<td class="plupload_file_action"></td>' +
  218. '</tr>' +
  219. '</table>' +
  220. '</div>' +
  221. '<input class="plupload_count" value="0" type="hidden">' +
  222. '</div>'
  223. );
  224. }
  225. $.widget("ui.plupload", {
  226. widgetEventPrefix: '',
  227. contents_bak: '',
  228. options: {
  229. browse_button_hover: 'ui-state-hover',
  230. browse_button_active: 'ui-state-active',
  231. // widget specific
  232. dragdrop : true,
  233. multiple_queues: true, // re-use widget by default
  234. buttons: {
  235. browse: true,
  236. start: true,
  237. stop: true
  238. },
  239. views: {
  240. list: true,
  241. thumbs: false,
  242. active: 'list',
  243. remember: true // requires: https://github.com/carhartl/jquery-cookie, otherwise disabled even if set to true
  244. },
  245. autostart: false,
  246. sortable: false,
  247. rename: false,
  248. max_file_count: 0 // unlimited
  249. },
  250. FILE_COUNT_ERROR: -9001,
  251. _create: function() {
  252. var id = this.element.attr('id');
  253. if (!id) {
  254. id = plupload.guid();
  255. this.element.attr('id', id);
  256. }
  257. this.id = id;
  258. // backup the elements initial state
  259. this.contents_bak = this.element.html();
  260. renderUI(this.element);
  261. // container, just in case
  262. this.container = $('.plupload_container', this.element).attr('id', id + '_container');
  263. this.content = $('.plupload_content', this.element);
  264. if ($.fn.resizable) {
  265. this.container.resizable({
  266. handles: 's',
  267. minHeight: 300
  268. });
  269. }
  270. // list of files, may become sortable
  271. this.filelist = $('.plupload_filelist_content', this.container)
  272. .attr({
  273. id: id + '_filelist',
  274. unselectable: 'on'
  275. });
  276. // buttons
  277. this.browse_button = $('.plupload_add', this.container).attr('id', id + '_browse');
  278. this.start_button = $('.plupload_start', this.container).attr('id', id + '_start');
  279. this.stop_button = $('.plupload_stop', this.container).attr('id', id + '_stop');
  280. this.thumbs_switcher = $('#' + id + '_view_thumbs');
  281. this.list_switcher = $('#' + id + '_view_list');
  282. if ($.ui.button) {
  283. this.browse_button.button({
  284. icons: { primary: 'ui-icon-circle-plus' },
  285. disabled: true
  286. });
  287. this.start_button.button({
  288. icons: { primary: 'ui-icon-circle-arrow-e' },
  289. disabled: true
  290. });
  291. this.stop_button.button({
  292. icons: { primary: 'ui-icon-circle-close' }
  293. });
  294. this.list_switcher.button({
  295. text: false,
  296. icons: { secondary: "ui-icon-grip-dotted-horizontal" }
  297. });
  298. this.thumbs_switcher.button({
  299. text: false,
  300. icons: { secondary: "ui-icon-image" }
  301. });
  302. }
  303. // progressbar
  304. this.progressbar = $('.plupload_progress_container', this.container);
  305. if ($.ui.progressbar) {
  306. this.progressbar.progressbar();
  307. }
  308. // counter
  309. this.counter = $('.plupload_count', this.element)
  310. .attr({
  311. id: id + '_count',
  312. name: id + '_count'
  313. });
  314. // initialize uploader instance
  315. this._initUploader();
  316. },
  317. _initUploader: function() {
  318. var self = this
  319. , id = this.id
  320. , uploader
  321. , options = {
  322. container: id + '_buttons',
  323. browse_button: id + '_browse'
  324. }
  325. ;
  326. $('.plupload_buttons', this.element).attr('id', id + '_buttons');
  327. if (self.options.dragdrop) {
  328. this.filelist.parent().attr('id', this.id + '_dropbox');
  329. options.drop_element = this.id + '_dropbox';
  330. }
  331. uploader = this.uploader = uploaders[id] = new plupload.Uploader($.extend(this.options, options));
  332. if (self.options.views.thumbs) {
  333. uploader.settings.required_features.display_media = true;
  334. }
  335. uploader.bind('Error', function(up, err) {
  336. var message, details = "";
  337. message = '<strong>' + err.message + '</strong>';
  338. switch (err.code) {
  339. case plupload.FILE_EXTENSION_ERROR:
  340. details = o.sprintf(_("File: %s"), err.file.name);
  341. break;
  342. case plupload.FILE_SIZE_ERROR:
  343. details = o.sprintf(_("File: %s, size: %d, max file size: %d"), err.file.name, err.file.size, plupload.parseSize(self.options.max_file_size));
  344. break;
  345. case plupload.FILE_DUPLICATE_ERROR:
  346. details = o.sprintf(_("%s already present in the queue."), err.file.name);
  347. break;
  348. case self.FILE_COUNT_ERROR:
  349. details = o.sprintf(_("Upload element accepts only %d file(s) at a time. Extra files were stripped."), self.options.max_file_count);
  350. break;
  351. case plupload.IMAGE_FORMAT_ERROR :
  352. details = _("Image format either wrong or not supported.");
  353. break;
  354. case plupload.IMAGE_MEMORY_ERROR :
  355. details = _("Runtime ran out of available memory.");
  356. break;
  357. /* // This needs a review
  358. case plupload.IMAGE_DIMENSIONS_ERROR :
  359. details = o.sprintf(_('Resoultion out of boundaries! <b>%s</b> runtime supports images only up to %wx%hpx.'), up.runtime, up.features.maxWidth, up.features.maxHeight);
  360. break; */
  361. case plupload.HTTP_ERROR:
  362. details = _("Upload URL might be wrong or doesn't exist.");
  363. break;
  364. }
  365. message += " <br /><i>" + details + "</i>";
  366. self._trigger('error', null, { up: up, error: err } );
  367. // do not show UI if no runtime can be initialized
  368. if (err.code === plupload.INIT_ERROR) {
  369. setTimeout(function() {
  370. self.destroy();
  371. }, 1);
  372. } else {
  373. self.notify('error', message);
  374. }
  375. });
  376. uploader.bind('PostInit', function(up) {
  377. // all buttons are optional, so they can be disabled and hidden
  378. if (!self.options.buttons.browse) {
  379. self.browse_button.button('disable').hide();
  380. up.disableBrowse(true);
  381. } else {
  382. self.browse_button.button('enable');
  383. }
  384. if (!self.options.buttons.start) {
  385. self.start_button.button('disable').hide();
  386. }
  387. if (!self.options.buttons.stop) {
  388. self.stop_button.button('disable').hide();
  389. }
  390. if (!self.options.unique_names && self.options.rename) {
  391. self._enableRenaming();
  392. }
  393. if (self.options.dragdrop && up.features.dragdrop) {
  394. self.filelist.parent().addClass('plupload_dropbox');
  395. }
  396. self._enableViewSwitcher();
  397. self.start_button.click(function(e) {
  398. if (!$(this).button('option', 'disabled')) {
  399. self.start();
  400. }
  401. e.preventDefault();
  402. });
  403. self.stop_button.click(function(e) {
  404. self.stop();
  405. e.preventDefault();
  406. });
  407. self._trigger('ready', null, { up: up });
  408. });
  409. // check if file count doesn't exceed the limit
  410. if (self.options.max_file_count) {
  411. self.options.multiple_queues = false; // one go only
  412. uploader.bind('FilesAdded', function(up, selectedFiles) {
  413. var selectedCount = selectedFiles.length
  414. , extraCount = up.files.length + selectedCount - self.options.max_file_count
  415. ;
  416. if (extraCount > 0) {
  417. selectedFiles.splice(selectedCount - extraCount, extraCount);
  418. up.trigger('Error', {
  419. code : self.FILE_COUNT_ERROR,
  420. message : _('File count error.')
  421. });
  422. }
  423. });
  424. }
  425. // uploader internal events must run first
  426. uploader.init();
  427. uploader.bind('FileFiltered', function(up, file) {
  428. self._addFiles(file);
  429. });
  430. uploader.bind('FilesAdded', function(up, files) {
  431. self._trigger('selected', null, { up: up, files: files } );
  432. // re-enable sortable
  433. if (self.options.sortable && $.ui.sortable) {
  434. self._enableSortingList();
  435. }
  436. self._trigger('updatelist', null, { filelist: self.filelist });
  437. if (self.options.autostart) {
  438. // set a little delay to make sure that QueueChanged triggered by the core has time to complete
  439. setTimeout(function() {
  440. self.start();
  441. }, 10);
  442. }
  443. });
  444. uploader.bind('FilesRemoved', function(up, files) {
  445. self._trigger('removed', null, { up: up, files: files } );
  446. });
  447. uploader.bind('QueueChanged StateChanged', function() {
  448. self._handleState();
  449. });
  450. uploader.bind('UploadFile', function(up, file) {
  451. self._handleFileStatus(file);
  452. });
  453. uploader.bind('FileUploaded', function(up, file) {
  454. self._handleFileStatus(file);
  455. self._trigger('uploaded', null, { up: up, file: file } );
  456. });
  457. uploader.bind('UploadProgress', function(up, file) {
  458. self._handleFileStatus(file);
  459. self._updateTotalProgress();
  460. self._trigger('progress', null, { up: up, file: file } );
  461. });
  462. uploader.bind('UploadComplete', function(up, files) {
  463. self._addFormFields();
  464. self._trigger('complete', null, { up: up, files: files } );
  465. });
  466. },
  467. _setOption: function(key, value) {
  468. var self = this;
  469. if (key == 'buttons' && typeof(value) == 'object') {
  470. value = $.extend(self.options.buttons, value);
  471. if (!value.browse) {
  472. self.browse_button.button('disable').hide();
  473. self.uploader.disableBrowse(true);
  474. } else {
  475. self.browse_button.button('enable').show();
  476. self.uploader.disableBrowse(false);
  477. }
  478. if (!value.start) {
  479. self.start_button.button('disable').hide();
  480. } else {
  481. self.start_button.button('enable').show();
  482. }
  483. if (!value.stop) {
  484. self.stop_button.button('disable').hide();
  485. } else {
  486. self.start_button.button('enable').show();
  487. }
  488. }
  489. self.uploader.settings[key] = value;
  490. },
  491. /**
  492. Start upload. Triggers `start` event.
  493. @method start
  494. */
  495. start: function() {
  496. this.uploader.start();
  497. this._trigger('start', null, { up: this.uploader });
  498. },
  499. /**
  500. Stop upload. Triggers `stop` event.
  501. @method stop
  502. */
  503. stop: function() {
  504. this.uploader.stop();
  505. this._trigger('stop', null, { up: this.uploader });
  506. },
  507. /**
  508. Enable browse button.
  509. @method enable
  510. */
  511. enable: function() {
  512. this.browse_button.button('enable');
  513. this.uploader.disableBrowse(false);
  514. },
  515. /**
  516. Disable browse button.
  517. @method disable
  518. */
  519. disable: function() {
  520. this.browse_button.button('disable');
  521. this.uploader.disableBrowse(true);
  522. },
  523. /**
  524. Retrieve file by it's unique id.
  525. @method getFile
  526. @param {String} id Unique id of the file
  527. @return {plupload.File}
  528. */
  529. getFile: function(id) {
  530. var file;
  531. if (typeof id === 'number') {
  532. file = this.uploader.files[id];
  533. } else {
  534. file = this.uploader.getFile(id);
  535. }
  536. return file;
  537. },
  538. /**
  539. Return array of files currently in the queue.
  540. @method getFiles
  541. @return {Array} Array of files in the queue represented by plupload.File objects
  542. */
  543. getFiles: function() {
  544. return this.uploader.files;
  545. },
  546. /**
  547. Remove the file from the queue.
  548. @method removeFile
  549. @param {plupload.File|String} file File to remove, might be specified directly or by it's unique id
  550. */
  551. removeFile: function(file) {
  552. if (plupload.typeOf(file) === 'string') {
  553. file = this.getFile(file);
  554. }
  555. this._removeFiles(file);
  556. },
  557. /**
  558. Clear the file queue.
  559. @method clearQueue
  560. */
  561. clearQueue: function() {
  562. this.uploader.splice();
  563. },
  564. /**
  565. Retrieve internal plupload.Uploader object (usually not required).
  566. @method getUploader
  567. @return {plupload.Uploader}
  568. */
  569. getUploader: function() {
  570. return this.uploader;
  571. },
  572. /**
  573. Trigger refresh procedure, specifically browse_button re-measure and re-position operations.
  574. Might get handy, when UI Widget is placed within the popup, that is constantly hidden and shown
  575. again - without calling this method after each show operation, dialog trigger might get displaced
  576. and disfunctional.
  577. @method refresh
  578. */
  579. refresh: function() {
  580. this.uploader.refresh();
  581. },
  582. /**
  583. Display a message in notification area.
  584. @method notify
  585. @param {Enum} type Type of the message, either `error` or `info`
  586. @param {String} message The text message to display.
  587. */
  588. notify: function(type, message) {
  589. var popup = $(
  590. '<div class="plupload_message">' +
  591. '<span class="plupload_message_close ui-icon ui-icon-circle-close" title="'+_('Close')+'"></span>' +
  592. '<p><span class="ui-icon"></span>' + message + '</p>' +
  593. '</div>'
  594. );
  595. popup
  596. .addClass('ui-state-' + (type === 'error' ? 'error' : 'highlight'))
  597. .find('p .ui-icon')
  598. .addClass('ui-icon-' + (type === 'error' ? 'alert' : 'info'))
  599. .end()
  600. .find('.plupload_message_close')
  601. .click(function() {
  602. popup.remove();
  603. })
  604. .end();
  605. $('.plupload_header', this.container).append(popup);
  606. },
  607. /**
  608. Destroy the widget, the uploader, free associated resources and bring back original html.
  609. @method destroy
  610. */
  611. destroy: function() {
  612. this._removeFiles([].slice.call(this.uploader.files));
  613. // destroy uploader instance
  614. this.uploader.destroy();
  615. // unbind all button events
  616. $('.plupload_button', this.element).unbind();
  617. // destroy buttons
  618. if ($.ui.button) {
  619. $('.plupload_add, .plupload_start, .plupload_stop', this.container)
  620. .button('destroy');
  621. }
  622. // destroy progressbar
  623. if ($.ui.progressbar) {
  624. this.progressbar.progressbar('destroy');
  625. }
  626. // destroy sortable behavior
  627. if ($.ui.sortable && this.options.sortable) {
  628. $('tbody', this.filelist).sortable('destroy');
  629. }
  630. // restore the elements initial state
  631. this.element
  632. .empty()
  633. .html(this.contents_bak);
  634. this.contents_bak = '';
  635. $.Widget.prototype.destroy.apply(this);
  636. },
  637. _handleState: function() {
  638. var up = this.uploader;
  639. if (up.state === plupload.STARTED) {
  640. $(this.start_button).button('disable');
  641. $([])
  642. .add(this.stop_button)
  643. .add('.plupload_started')
  644. .removeClass('plupload_hidden');
  645. $('.plupload_upload_status', this.element).html(o.sprintf(_('Uploaded %d/%d files'), up.total.uploaded, up.files.length));
  646. $('.plupload_header_content', this.element).addClass('plupload_header_content_bw');
  647. } else if (up.state === plupload.STOPPED) {
  648. $([])
  649. .add(this.stop_button)
  650. .add('.plupload_started')
  651. .addClass('plupload_hidden');
  652. if (this.options.multiple_queues) {
  653. $('.plupload_header_content', this.element).removeClass('plupload_header_content_bw');
  654. } else {
  655. $([])
  656. .add(this.browse_button)
  657. .add(this.start_button)
  658. .button('disable');
  659. up.disableBrowse();
  660. }
  661. if (up.files.length === (up.total.uploaded + up.total.failed)) {
  662. this.start_button.button('disable');
  663. } else {
  664. this.start_button.button('enable');
  665. }
  666. this._updateTotalProgress();
  667. }
  668. if (up.total.queued === 0) {
  669. $('.ui-button-text', this.browse_button).html(_('Add Files'));
  670. } else {
  671. $('.ui-button-text', this.browse_button).html(o.sprintf(_('%d files queued'), up.total.queued));
  672. }
  673. up.refresh();
  674. },
  675. _handleFileStatus: function(file) {
  676. var self = this, actionClass, iconClass;
  677. // since this method might be called asynchronously, file row might not yet be rendered
  678. if (!$('#' + file.id).length) {
  679. return;
  680. }
  681. switch (file.status) {
  682. case plupload.DONE:
  683. actionClass = 'plupload_done';
  684. iconClass = 'ui-icon ui-icon-circle-check';
  685. break;
  686. case plupload.FAILED:
  687. actionClass = 'ui-state-error plupload_failed';
  688. iconClass = 'ui-icon ui-icon-alert';
  689. break;
  690. case plupload.QUEUED:
  691. actionClass = 'plupload_delete';
  692. iconClass = 'ui-icon ui-icon-circle-minus';
  693. break;
  694. case plupload.UPLOADING:
  695. actionClass = 'ui-state-highlight plupload_uploading';
  696. iconClass = 'ui-icon ui-icon-circle-arrow-w';
  697. // scroll uploading file into the view if its bottom boundary is out of it
  698. var scroller = $('.plupload_scroll', this.container)
  699. , scrollTop = scroller.scrollTop()
  700. , scrollerHeight = scroller.height()
  701. , rowOffset = $('#' + file.id).position().top + $('#' + file.id).height()
  702. ;
  703. if (scrollerHeight < rowOffset) {
  704. scroller.scrollTop(scrollTop + rowOffset - scrollerHeight);
  705. }
  706. // Set file specific progress
  707. $('#' + file.id)
  708. .find('.plupload_file_percent')
  709. .html(file.percent + '%')
  710. .end()
  711. .find('.plupload_file_progress')
  712. .css('width', file.percent + '%')
  713. .end()
  714. .find('.plupload_file_size')
  715. .html(plupload.formatSize(file.size));
  716. break;
  717. }
  718. actionClass += ' ui-state-default plupload_file';
  719. $('#' + file.id)
  720. .attr('class', actionClass)
  721. .find('.ui-icon')
  722. .attr('class', iconClass)
  723. .end()
  724. .filter('.plupload_delete, .plupload_done, .plupload_failed')
  725. .find('.ui-icon')
  726. .click(function(e) {
  727. self._removeFiles(file);
  728. e.preventDefault();
  729. });
  730. },
  731. _updateTotalProgress: function() {
  732. var up = this.uploader;
  733. // Scroll to end of file list
  734. this.filelist[0].scrollTop = this.filelist[0].scrollHeight;
  735. this.progressbar.progressbar('value', up.total.percent);
  736. this.element
  737. .find('.plupload_total_status')
  738. .html(up.total.percent + '%')
  739. .end()
  740. .find('.plupload_total_file_size')
  741. .html(plupload.formatSize(up.total.size))
  742. .end()
  743. .find('.plupload_upload_status')
  744. .html(o.sprintf(_('Uploaded %d/%d files'), up.total.uploaded, up.files.length));
  745. },
  746. _displayThumbs: function() {
  747. var self = this
  748. , tw, th // thumb width/height
  749. , cols
  750. , num = 0 // number of simultaneously visible thumbs
  751. , thumbs = [] // array of thumbs to preload at any given moment
  752. , loading = false
  753. ;
  754. if (!this.options.views.thumbs) {
  755. return;
  756. }
  757. function onLast(el, eventName, cb) {
  758. var timer;
  759. el.on(eventName, function() {
  760. clearTimeout(timer);
  761. timer = setTimeout(function() {
  762. clearTimeout(timer);
  763. cb();
  764. }, 300);
  765. });
  766. }
  767. // calculate number of simultaneously visible thumbs
  768. function measure() {
  769. if (!tw || !th) {
  770. var wrapper = $('.plupload_file:eq(0)', self.filelist);
  771. tw = wrapper.outerWidth(true);
  772. th = wrapper.outerHeight(true);
  773. }
  774. var aw = self.content.width(), ah = self.content.height();
  775. cols = Math.floor(aw / tw);
  776. num = cols * (Math.ceil(ah / th) + 1);
  777. }
  778. function pickThumbsToLoad() {
  779. // calculate index of virst visible thumb
  780. var startIdx = Math.floor(self.content.scrollTop() / th) * cols;
  781. // get potentially visible thumbs that are not yet visible
  782. thumbs = $('.plupload_file', self.filelist)
  783. .slice(startIdx, startIdx + num)
  784. .filter(':not(.plupload_file_thumb_loaded)')
  785. .get();
  786. }
  787. function init() {
  788. function mpl() {
  789. if (self.view_mode !== 'thumbs') {
  790. return;
  791. }
  792. measure();
  793. pickThumbsToLoad();
  794. lazyLoad();
  795. }
  796. if ($.fn.resizable) {
  797. onLast(self.container, 'resize', mpl);
  798. }
  799. onLast(self.window, 'resize', mpl);
  800. onLast(self.content, 'scroll', mpl);
  801. self.element.on('viewchanged selected', mpl);
  802. mpl();
  803. }
  804. function preloadThumb(file, cb) {
  805. var img = new o.Image();
  806. img.onload = function() {
  807. var thumb = $('#' + file.id + ' .plupload_file_thumb', self.filelist).html('');
  808. this.embed(thumb[0], {
  809. width: 100,
  810. height: 60,
  811. crop: true,
  812. swf_url: o.resolveUrl(self.options.flash_swf_url),
  813. xap_url: o.resolveUrl(self.options.silverlight_xap_url)
  814. });
  815. };
  816. img.bind("embedded error", function() {
  817. $('#' + file.id, self.filelist).addClass('plupload_file_thumb_loaded');
  818. this.destroy();
  819. setTimeout(cb, 1); // detach, otherwise ui might hang (in SilverLight for example)
  820. });
  821. img.load(file.getSource());
  822. }
  823. function lazyLoad() {
  824. if (self.view_mode !== 'thumbs' || loading) {
  825. return;
  826. }
  827. pickThumbsToLoad();
  828. if (!thumbs.length) {
  829. return;
  830. }
  831. loading = true;
  832. preloadThumb(self.getFile($(thumbs.shift()).attr('id')), function() {
  833. loading = false;
  834. lazyLoad();
  835. });
  836. }
  837. // this has to run only once to measure structures and bind listeners
  838. this.element.on('selected', function onselected() {
  839. self.element.off('selected', onselected);
  840. init();
  841. });
  842. },
  843. _addFiles: function(files) {
  844. var self = this, file_html;
  845. file_html = '<li class="plupload_file ui-state-default" id="%id%">' +
  846. '<div class="plupload_file_thumb">' +
  847. '<div class="plupload_file_dummy ui-widget-content"><span class="ui-state-disabled">%ext%</span></div>' +
  848. '</div>' +
  849. '<div class="plupload_file_name" title="%name%"><span class="plupload_file_namespan">%name%</span></div>' +
  850. '<div class="plupload_file_action"><div class="ui-icon"> </div></div>' +
  851. '<div class="plupload_file_size">%size% </div>' +
  852. '<div class="plupload_file_status">' +
  853. '<div class="plupload_file_progress ui-widget-header" style="width: 0%"> </div>' +
  854. '<span class="plupload_file_percent">%percent% </span>' +
  855. '</div>' +
  856. '<div class="plupload_file_fields"> </div>' +
  857. '</li>';
  858. if (plupload.typeOf(files) !== 'array') {
  859. files = [files];
  860. }
  861. $.each(files, function(i, file) {
  862. var ext = o.Mime.getFileExtension(file.name) || 'none';
  863. self.filelist.append(file_html.replace(/%(\w+)%/g, function($0, $1) {
  864. if ('size' === $1) {
  865. return plupload.formatSize(file.size);
  866. } else if ('ext' === $1) {
  867. return ext;
  868. } else {
  869. return file[$1] || '';
  870. }
  871. }));
  872. self._handleFileStatus(file);
  873. });
  874. },
  875. _removeFiles: function(files) {
  876. var self = this, up = this.uploader;
  877. if (plupload.typeOf(files) !== 'array') {
  878. files = [files];
  879. }
  880. // destroy sortable if enabled
  881. if ($.ui.sortable && this.options.sortable) {
  882. $('tbody', self.filelist).sortable('destroy');
  883. }
  884. $.each(files, function(i, file) {
  885. $('#' + file.id).toggle("highlight", function() {
  886. this.remove();
  887. });
  888. up.removeFile(file);
  889. });
  890. if (up.files.length) {
  891. // re-initialize sortable
  892. if (this.options.sortable && $.ui.sortable) {
  893. this._enableSortingList();
  894. }
  895. }
  896. this._trigger('updatelist', null, { filelist: this.filelist });
  897. },
  898. _addFormFields: function() {
  899. var self = this;
  900. // re-add from fresh
  901. $('.plupload_file_fields', this.filelist).html('');
  902. plupload.each(this.uploader.files, function(file, count) {
  903. var fields = ''
  904. , id = self.id + '_' + count
  905. ;
  906. if (file.target_name) {
  907. fields += '<input type="hidden" name="' + id + '_tmpname" value="'+plupload.xmlEncode(file.target_name)+'" />';
  908. }
  909. fields += '<input type="hidden" name="' + id + '_name" value="'+plupload.xmlEncode(file.name)+'" />';
  910. fields += '<input type="hidden" name="' + id + '_status" value="' + (file.status === plupload.DONE ? 'done' : 'failed') + '" />';
  911. $('#' + file.id).find('.plupload_file_fields').html(fields);
  912. });
  913. this.counter.val(this.uploader.files.length);
  914. },
  915. _viewChanged: function(view) {
  916. // update or write a new cookie
  917. if (this.options.views.remember && $.cookie) {
  918. $.cookie('plupload_ui_view', view, { expires: 7, path: '/' });
  919. }
  920. // ugly fix for IE6 - make content area stretchable
  921. if (o.Env.browser === 'IE' && o.Env.version < 7) {
  922. this.content.attr('style', 'height:expression(document.getElementById("' + this.id + '_container' + '").clientHeight - ' + (view === 'list' ? 133 : 103) + ');');
  923. }
  924. this.container.removeClass('plupload_view_list plupload_view_thumbs').addClass('plupload_view_' + view);
  925. this.view_mode = view;
  926. this._trigger('viewchanged', null, { view: view });
  927. },
  928. _enableViewSwitcher: function() {
  929. var self = this
  930. , view
  931. , switcher = $('.plupload_view_switch', this.container)
  932. , buttons
  933. , button
  934. ;
  935. plupload.each(['list', 'thumbs'], function(view) {
  936. if (!self.options.views[view]) {
  937. switcher.find('[for="' + self.id + '_view_' + view + '"], #'+ self.id +'_view_' + view).remove();
  938. }
  939. });
  940. // check if any visible left
  941. buttons = switcher.find('.plupload_button');
  942. if (buttons.length === 1) {
  943. switcher.hide();
  944. view = buttons.eq(0).data('view');
  945. this._viewChanged(view);
  946. } else if ($.ui.button && buttons.length > 1) {
  947. if (this.options.views.remember && $.cookie) {
  948. view = $.cookie('plupload_ui_view');
  949. }
  950. // if wierd case, bail out to default
  951. if (!~plupload.inArray(view, ['list', 'thumbs'])) {
  952. view = this.options.views.active;
  953. }
  954. switcher
  955. .show()
  956. .buttonset()
  957. .find('.ui-button')
  958. .click(function(e) {
  959. view = $(this).data('view');
  960. self._viewChanged(view);
  961. e.preventDefault(); // avoid auto scrolling to widget in IE and FF (see #850)
  962. });
  963. // if view not active - happens when switcher wasn't clicked manually
  964. button = switcher.find('[for="' + self.id + '_view_'+view+'"]');
  965. if (button.length) {
  966. button.trigger('click');
  967. }
  968. } else {
  969. switcher.show();
  970. this._viewChanged(this.options.views.active);
  971. }
  972. // initialize thumb viewer if requested
  973. if (this.options.views.thumbs) {
  974. this._displayThumbs();
  975. }
  976. },
  977. _enableRenaming: function() {
  978. var self = this;
  979. this.filelist.dblclick(function(e) {
  980. var nameSpan = $(e.target), nameInput, file, parts, name, ext = "";
  981. if (!nameSpan.hasClass('plupload_file_namespan')) {
  982. return;
  983. }
  984. // Get file name and split out name and extension
  985. file = self.uploader.getFile(nameSpan.closest('.plupload_file')[0].id);
  986. name = file.name;
  987. parts = /^(.+)(\.[^.]+)$/.exec(name);
  988. if (parts) {
  989. name = parts[1];
  990. ext = parts[2];
  991. }
  992. // Display input element
  993. nameInput = $('<input class="plupload_file_rename" type="text" />').width(nameSpan.width()).insertAfter(nameSpan.hide());
  994. nameInput.val(name).blur(function() {
  995. nameSpan.show().parent().scrollLeft(0).end().next().remove();
  996. }).keydown(function(e) {
  997. var nameInput = $(this);
  998. if ($.inArray(e.keyCode, [13, 27]) !== -1) {
  999. e.preventDefault();
  1000. // Rename file and glue extension back on
  1001. if (e.keyCode === 13) {
  1002. file.name = nameInput.val() + ext;
  1003. nameSpan.html(file.name);
  1004. }
  1005. nameInput.blur();
  1006. }
  1007. })[0].focus();
  1008. });
  1009. },
  1010. _enableSortingList: function() {
  1011. var self = this;
  1012. if ($('.plupload_file', this.filelist).length < 2) {
  1013. return;
  1014. }
  1015. // destroy sortable if enabled
  1016. $('tbody', this.filelist).sortable('destroy');
  1017. // enable
  1018. this.filelist.sortable({
  1019. items: '.plupload_delete',
  1020. cancel: 'object, .plupload_clearer',
  1021. stop: function() {
  1022. var files = [];
  1023. $.each($(this).sortable('toArray'), function(i, id) {
  1024. files[files.length] = self.uploader.getFile(id);
  1025. });
  1026. files.unshift(files.length);
  1027. files.unshift(0);
  1028. // re-populate files array
  1029. Array.prototype.splice.apply(self.uploader.files, files);
  1030. }
  1031. });
  1032. }
  1033. });
  1034. } (window, document, plupload, mOxie, jQuery));