/backend/Metis/src/assets/lib/elFinder/js/elFinder.js

https://gitlab.com/project-store/theme · JavaScript · 2742 lines · 1631 code · 353 blank · 758 comment · 500 complexity · 76bcca0dd66f5f0a77d2a35d278fd2cd MD5 · raw file

Large files are truncated click here to view the full file

  1. "use strict";
  2. /**
  3. * @class elFinder - file manager for web
  4. *
  5. * @author Dmitry (dio) Levashov
  6. **/
  7. window.elFinder = function(node, opts) {
  8. this.time('load');
  9. var self = this,
  10. /**
  11. * Node on which elfinder creating
  12. *
  13. * @type jQuery
  14. **/
  15. node = $(node),
  16. /**
  17. * Store node contents.
  18. *
  19. * @see this.destroy
  20. * @type jQuery
  21. **/
  22. prevContent = $('<div/>').append(node.contents()),
  23. /**
  24. * Store node inline styles
  25. *
  26. * @see this.destroy
  27. * @type String
  28. **/
  29. prevStyle = node.attr('style'),
  30. /**
  31. * Instance ID. Required to get/set cookie
  32. *
  33. * @type String
  34. **/
  35. id = node.attr('id') || '',
  36. /**
  37. * Events namespace
  38. *
  39. * @type String
  40. **/
  41. namespace = 'elfinder-'+(id || Math.random().toString().substr(2, 7)),
  42. /**
  43. * Mousedown event
  44. *
  45. * @type String
  46. **/
  47. mousedown = 'mousedown.'+namespace,
  48. /**
  49. * Keydown event
  50. *
  51. * @type String
  52. **/
  53. keydown = 'keydown.'+namespace,
  54. /**
  55. * Keypress event
  56. *
  57. * @type String
  58. **/
  59. keypress = 'keypress.'+namespace,
  60. /**
  61. * Is shortcuts/commands enabled
  62. *
  63. * @type Boolean
  64. **/
  65. enabled = true,
  66. /**
  67. * Store enabled value before ajax requiest
  68. *
  69. * @type Boolean
  70. **/
  71. prevEnabled = true,
  72. /**
  73. * List of build-in events which mapped into methods with same names
  74. *
  75. * @type Array
  76. **/
  77. events = ['enable', 'disable', 'load', 'open', 'reload', 'select', 'add', 'remove', 'change', 'dblclick', 'getfile', 'lockfiles', 'unlockfiles', 'dragstart', 'dragstop'],
  78. /**
  79. * Rules to validate data from backend
  80. *
  81. * @type Object
  82. **/
  83. rules = {},
  84. /**
  85. * Current working directory hash
  86. *
  87. * @type String
  88. **/
  89. cwd = '',
  90. /**
  91. * Current working directory options
  92. *
  93. * @type Object
  94. **/
  95. cwdOptions = {
  96. path : '',
  97. url : '',
  98. tmbUrl : '',
  99. disabled : [],
  100. separator : '/',
  101. archives : [],
  102. extract : [],
  103. copyOverwrite : true,
  104. tmb : false // old API
  105. },
  106. /**
  107. * Files/dirs cache
  108. *
  109. * @type Object
  110. **/
  111. files = {},
  112. /**
  113. * Selected files hashes
  114. *
  115. * @type Array
  116. **/
  117. selected = [],
  118. /**
  119. * Events listeners
  120. *
  121. * @type Object
  122. **/
  123. listeners = {},
  124. /**
  125. * Shortcuts
  126. *
  127. * @type Object
  128. **/
  129. shortcuts = {},
  130. /**
  131. * Buffer for copied files
  132. *
  133. * @type Array
  134. **/
  135. clipboard = [],
  136. /**
  137. * Copied/cuted files hashes
  138. * Prevent from remove its from cache.
  139. * Required for dispaly correct files names in error messages
  140. *
  141. * @type Array
  142. **/
  143. remember = [],
  144. /**
  145. * Queue for 'open' requests
  146. *
  147. * @type Array
  148. **/
  149. queue = [],
  150. /**
  151. * Commands prototype
  152. *
  153. * @type Object
  154. **/
  155. base = new self.command(self),
  156. /**
  157. * elFinder node width
  158. *
  159. * @type String
  160. * @default "auto"
  161. **/
  162. width = 'auto',
  163. /**
  164. * elFinder node height
  165. *
  166. * @type Number
  167. * @default 400
  168. **/
  169. height = 400,
  170. beeper = $(document.createElement('audio')).hide().appendTo('body')[0],
  171. syncInterval,
  172. open = function(data) {
  173. if (data.init) {
  174. // init - reset cache
  175. files = {};
  176. } else {
  177. // remove only files from prev cwd
  178. for (var i in files) {
  179. if (files.hasOwnProperty(i)
  180. && files[i].mime != 'directory'
  181. && files[i].phash == cwd
  182. && $.inArray(i, remember) === -1) {
  183. delete files[i];
  184. }
  185. }
  186. }
  187. cwd = data.cwd.hash;
  188. cache(data.files);
  189. if (!files[cwd]) {
  190. cache([data.cwd]);
  191. }
  192. self.lastDir(cwd);
  193. },
  194. /**
  195. * Store info about files/dirs in "files" object.
  196. *
  197. * @param Array files
  198. * @return void
  199. **/
  200. cache = function(data) {
  201. var l = data.length, f;
  202. while (l--) {
  203. f = data[l];
  204. if (f.name && f.hash && f.mime) {
  205. files[f.hash] = f;
  206. }
  207. }
  208. },
  209. /**
  210. * Exec shortcut
  211. *
  212. * @param jQuery.Event keydown/keypress event
  213. * @return void
  214. */
  215. execShortcut = function(e) {
  216. var code = e.keyCode,
  217. ctrlKey = !!(e.ctrlKey || e.metaKey);
  218. if (enabled) {
  219. $.each(shortcuts, function(i, shortcut) {
  220. if (shortcut.type == e.type
  221. && shortcut.keyCode == code
  222. && shortcut.shiftKey == e.shiftKey
  223. && shortcut.ctrlKey == ctrlKey
  224. && shortcut.altKey == e.altKey) {
  225. e.preventDefault()
  226. e.stopPropagation();
  227. shortcut.callback(e, self);
  228. self.debug('shortcut-exec', i+' : '+shortcut.description);
  229. }
  230. });
  231. // prevent tab out of elfinder
  232. code == 9 && e.preventDefault();
  233. }
  234. },
  235. date = new Date(),
  236. utc,
  237. i18n
  238. ;
  239. /**
  240. * Protocol version
  241. *
  242. * @type String
  243. **/
  244. this.api = null;
  245. /**
  246. * elFinder use new api
  247. *
  248. * @type Boolean
  249. **/
  250. this.newAPI = false;
  251. /**
  252. * elFinder use old api
  253. *
  254. * @type Boolean
  255. **/
  256. this.oldAPI = false;
  257. /**
  258. * User os. Required to bind native shortcuts for open/rename
  259. *
  260. * @type String
  261. **/
  262. this.OS = navigator.userAgent.indexOf('Mac') !== -1 ? 'mac' : navigator.userAgent.indexOf('Win') !== -1 ? 'win' : 'other';
  263. /**
  264. * Configuration options
  265. *
  266. * @type Object
  267. **/
  268. this.options = $.extend(true, {}, this._options, opts||{});
  269. if (opts.ui) {
  270. this.options.ui = opts.ui;
  271. }
  272. if (opts.commands) {
  273. this.options.commands = opts.commands;
  274. }
  275. if (opts.uiOptions && opts.uiOptions.toolbar) {
  276. this.options.uiOptions.toolbar = opts.uiOptions.toolbar;
  277. }
  278. $.extend(this.options.contextmenu, opts.contextmenu);
  279. /**
  280. * Ajax request type
  281. *
  282. * @type String
  283. * @default "get"
  284. **/
  285. this.requestType = /^(get|post)$/i.test(this.options.requestType) ? this.options.requestType.toLowerCase() : 'get',
  286. /**
  287. * Any data to send across every ajax request
  288. *
  289. * @type Object
  290. * @default {}
  291. **/
  292. this.customData = $.isPlainObject(this.options.customData) ? this.options.customData : {};
  293. /**
  294. * ID. Required to create unique cookie name
  295. *
  296. * @type String
  297. **/
  298. this.id = id;
  299. /**
  300. * URL to upload files
  301. *
  302. * @type String
  303. **/
  304. this.uploadURL = opts.urlUpload || opts.url;
  305. /**
  306. * Events namespace
  307. *
  308. * @type String
  309. **/
  310. this.namespace = namespace;
  311. /**
  312. * Interface language
  313. *
  314. * @type String
  315. * @default "en"
  316. **/
  317. this.lang = this.i18[this.options.lang] && this.i18[this.options.lang].messages ? this.options.lang : 'en';
  318. i18n = this.lang == 'en'
  319. ? this.i18['en']
  320. : $.extend(true, {}, this.i18['en'], this.i18[this.lang]);
  321. /**
  322. * Interface direction
  323. *
  324. * @type String
  325. * @default "ltr"
  326. **/
  327. this.direction = i18n.direction;
  328. /**
  329. * i18 messages
  330. *
  331. * @type Object
  332. **/
  333. this.messages = i18n.messages;
  334. /**
  335. * Date/time format
  336. *
  337. * @type String
  338. * @default "m.d.Y"
  339. **/
  340. this.dateFormat = this.options.dateFormat || i18n.dateFormat;
  341. /**
  342. * Date format like "Yesterday 10:20:12"
  343. *
  344. * @type String
  345. * @default "{day} {time}"
  346. **/
  347. this.fancyFormat = this.options.fancyDateFormat || i18n.fancyDateFormat;
  348. /**
  349. * Today timestamp
  350. *
  351. * @type Number
  352. **/
  353. this.today = (new Date(date.getFullYear(), date.getMonth(), date.getDate())).getTime()/1000;
  354. /**
  355. * Yesterday timestamp
  356. *
  357. * @type Number
  358. **/
  359. this.yesterday = this.today - 86400;
  360. utc = this.options.UTCDate ? 'UTC' : '';
  361. this.getHours = 'get'+utc+'Hours';
  362. this.getMinutes = 'get'+utc+'Minutes';
  363. this.getSeconds = 'get'+utc+'Seconds';
  364. this.getDate = 'get'+utc+'Date';
  365. this.getDay = 'get'+utc+'Day';
  366. this.getMonth = 'get'+utc+'Month';
  367. this.getFullYear = 'get'+utc+'FullYear';
  368. /**
  369. * Css classes
  370. *
  371. * @type String
  372. **/
  373. this.cssClass = 'ui-helper-reset ui-helper-clearfix ui-widget ui-widget-content ui-corner-all elfinder elfinder-'+(this.direction == 'rtl' ? 'rtl' : 'ltr')+' '+this.options.cssClass;
  374. /**
  375. * Method to store/fetch data
  376. *
  377. * @type Function
  378. **/
  379. this.storage = (function() {
  380. try {
  381. return 'localStorage' in window && window['localStorage'] !== null ? self.localStorage : self.cookie;
  382. } catch (e) {
  383. return self.cookie;
  384. }
  385. })();
  386. /**
  387. * Delay in ms before open notification dialog
  388. *
  389. * @type Number
  390. * @default 500
  391. **/
  392. this.notifyDelay = this.options.notifyDelay > 0 ? parseInt(this.options.notifyDelay) : 500;
  393. /**
  394. * Base draggable options
  395. *
  396. * @type Object
  397. **/
  398. this.draggable = {
  399. appendTo : 'body',
  400. addClasses : true,
  401. delay : 30,
  402. revert : true,
  403. refreshPositions : true,
  404. cursor : 'move',
  405. cursorAt : {left : 50, top : 47},
  406. drag : function(e, ui) { ui.helper.toggleClass('elfinder-drag-helper-plus', e.shiftKey||e.ctrlKey||e.metaKey); },
  407. stop : function() { self.trigger('focus').trigger('dragstop'); },
  408. helper : function(e, ui) {
  409. var element = this.id ? $(this) : $(this).parents('[id]:first'),
  410. helper = $('<div class="elfinder-drag-helper"><span class="elfinder-drag-helper-icon-plus"/></div>'),
  411. icon = function(mime) { return '<div class="elfinder-cwd-icon '+self.mime2class(mime)+' ui-corner-all"/>'; },
  412. hashes, l;
  413. self.trigger('dragstart', {target : element[0], originalEvent : e});
  414. hashes = element.is('.'+self.res('class', 'cwdfile'))
  415. ? self.selected()
  416. : [self.navId2Hash(element.attr('id'))];
  417. helper.append(icon(files[hashes[0]].mime)).data('files', hashes);
  418. if ((l = hashes.length) > 1) {
  419. helper.append(icon(files[hashes[l-1]].mime) + '<span class="elfinder-drag-num">'+l+'</span>');
  420. }
  421. return helper;
  422. }
  423. };
  424. /**
  425. * Base droppable options
  426. *
  427. * @type Object
  428. **/
  429. this.droppable = {
  430. tolerance : 'pointer',
  431. accept : '.elfinder-cwd-file-wrapper,.elfinder-navbar-dir,.elfinder-cwd-file',
  432. hoverClass : this.res('class', 'adroppable'),
  433. drop : function(e, ui) {
  434. var dst = $(this),
  435. targets = $.map(ui.helper.data('files')||[], function(h) { return h || null }),
  436. result = [],
  437. c = 'class',
  438. cnt, hash, i, h;
  439. if (dst.is('.'+self.res(c, 'cwd'))) {
  440. hash = cwd;
  441. } else if (dst.is('.'+self.res(c, 'cwdfile'))) {
  442. hash = dst.attr('id');
  443. } else if (dst.is('.'+self.res(c, 'navdir'))) {
  444. hash = self.navId2Hash(dst.attr('id'));
  445. }
  446. cnt = targets.length;
  447. while (cnt--) {
  448. h = targets[cnt];
  449. // ignore drop into itself or in own location
  450. h != hash && files[h].phash != hash && result.push(h);
  451. }
  452. if (result.length) {
  453. ui.helper.hide();
  454. self.clipboard(result, !(e.ctrlKey||e.shiftKey||e.metaKey));
  455. self.exec('paste', hash).always(function() { self.clipboard([]); });
  456. self.trigger('drop', {files : targets});
  457. }
  458. }
  459. };
  460. /**
  461. * Return true if filemanager is active
  462. *
  463. * @return Boolean
  464. **/
  465. this.enabled = function() {
  466. return node.is(':visible') && enabled;
  467. }
  468. /**
  469. * Return true if filemanager is visible
  470. *
  471. * @return Boolean
  472. **/
  473. this.visible = function() {
  474. return node.is(':visible');
  475. }
  476. /**
  477. * Return root dir hash for current working directory
  478. *
  479. * @return String
  480. */
  481. this.root = function(hash) {
  482. var dir = files[hash || cwd], i;
  483. while (dir && dir.phash) {
  484. dir = files[dir.phash]
  485. }
  486. if (dir) {
  487. return dir.hash;
  488. }
  489. while (i in files && files.hasOwnProperty(i)) {
  490. dir = files[i]
  491. if (!dir.phash && !dir.mime == 'directory' && dir.read) {
  492. return dir.hash
  493. }
  494. }
  495. return '';
  496. }
  497. /**
  498. * Return current working directory info
  499. *
  500. * @return Object
  501. */
  502. this.cwd = function() {
  503. return files[cwd] || {};
  504. }
  505. /**
  506. * Return required cwd option
  507. *
  508. * @param String option name
  509. * @return mixed
  510. */
  511. this.option = function(name) {
  512. return cwdOptions[name]||'';
  513. }
  514. /**
  515. * Return file data from current dir or tree by it's hash
  516. *
  517. * @param String file hash
  518. * @return Object
  519. */
  520. this.file = function(hash) {
  521. return files[hash];
  522. };
  523. /**
  524. * Return all cached files
  525. *
  526. * @return Array
  527. */
  528. this.files = function() {
  529. return $.extend(true, {}, files);
  530. }
  531. /**
  532. * Return list of file parents hashes include file hash
  533. *
  534. * @param String file hash
  535. * @return Array
  536. */
  537. this.parents = function(hash) {
  538. var parents = [],
  539. dir;
  540. while ((dir = this.file(hash))) {
  541. parents.unshift(dir.hash);
  542. hash = dir.phash;
  543. }
  544. return parents;
  545. }
  546. this.path2array = function(hash) {
  547. var file,
  548. path = [];
  549. while (hash && (file = files[hash]) && file.hash) {
  550. path.unshift(file.name);
  551. hash = file.phash;
  552. }
  553. return path;
  554. }
  555. /**
  556. * Return file path
  557. *
  558. * @param Object file
  559. * @return String
  560. */
  561. this.path = function(hash) {
  562. return files[hash] && files[hash].path
  563. ? files[hash].path
  564. : this.path2array(hash).join(cwdOptions.separator);
  565. }
  566. /**
  567. * Return file url if set
  568. *
  569. * @param Object file
  570. * @return String
  571. */
  572. this.url = function(hash) {
  573. var file = files[hash];
  574. if (!file || !file.read) {
  575. return '';
  576. }
  577. if (file.url) {
  578. return file.url;
  579. }
  580. if (cwdOptions.url) {
  581. return cwdOptions.url + $.map(this.path2array(hash), function(n) { return encodeURIComponent(n); }).slice(1).join('/')
  582. }
  583. var params = $.extend({}, this.customData, {
  584. cmd: 'file',
  585. target: file.hash
  586. });
  587. if (this.oldAPI) {
  588. params.cmd = 'open';
  589. params.current = file.phash;
  590. }
  591. return this.options.url + (this.options.url.indexOf('?') === -1 ? '?' : '&') + $.param(params, true);
  592. }
  593. /**
  594. * Return thumbnail url
  595. *
  596. * @param String file hash
  597. * @return String
  598. */
  599. this.tmb = function(hash) {
  600. var file = files[hash],
  601. url = file && file.tmb && file.tmb != 1 ? cwdOptions['tmbUrl'] + file.tmb : '';
  602. if (url && ($.browser.opera || $.browser.msie)) {
  603. url += '?_=' + new Date().getTime();
  604. }
  605. return url;
  606. }
  607. /**
  608. * Return selected files hashes
  609. *
  610. * @return Array
  611. **/
  612. this.selected = function() {
  613. return selected.slice(0);
  614. }
  615. /**
  616. * Return selected files info
  617. *
  618. * @return Array
  619. */
  620. this.selectedFiles = function() {
  621. return $.map(selected, function(hash) { return files[hash] || null });
  622. };
  623. /**
  624. * Return true if file with required name existsin required folder
  625. *
  626. * @param String file name
  627. * @param String parent folder hash
  628. * @return Boolean
  629. */
  630. this.fileByName = function(name, phash) {
  631. var hash;
  632. for (hash in files) {
  633. if (files.hasOwnProperty(hash) && files[hash].phash == phash && files[hash].name == name) {
  634. return files[hash];
  635. }
  636. }
  637. };
  638. /**
  639. * Valid data for required command based on rules
  640. *
  641. * @param String command name
  642. * @param Object cammand's data
  643. * @return Boolean
  644. */
  645. this.validResponse = function(cmd, data) {
  646. return data.error || this.rules[this.rules[cmd] ? cmd : 'defaults'](data);
  647. }
  648. /**
  649. * Proccess ajax request.
  650. * Fired events :
  651. * @todo
  652. * @example
  653. * @todo
  654. * @return $.Deferred
  655. */
  656. this.request = function(options) {
  657. var self = this,
  658. o = this.options,
  659. dfrd = $.Deferred(),
  660. // request data
  661. data = $.extend({}, o.customData, {mimes : o.onlyMimes}, options.data || options),
  662. // command name
  663. cmd = data.cmd,
  664. // call default fail callback (display error dialog) ?
  665. deffail = !(options.preventDefault || options.preventFail),
  666. // call default success callback ?
  667. defdone = !(options.preventDefault || options.preventDone),
  668. // options for notify dialog
  669. notify = $.extend({}, options.notify),
  670. // do not normalize data - return as is
  671. raw = !!options.raw,
  672. // sync files on request fail
  673. syncOnFail = options.syncOnFail,
  674. // open notify dialog timeout
  675. timeout,
  676. // request options
  677. options = $.extend({
  678. url : o.url,
  679. async : true,
  680. type : this.requestType,
  681. dataType : 'json',
  682. cache : false,
  683. // timeout : 100,
  684. data : data
  685. }, options.options || {}),
  686. /**
  687. * Default success handler.
  688. * Call default data handlers and fire event with command name.
  689. *
  690. * @param Object normalized response data
  691. * @return void
  692. **/
  693. done = function(data) {
  694. data.warning && self.error(data.warning);
  695. cmd == 'open' && open($.extend(true, {}, data));
  696. // fire some event to update cache/ui
  697. data.removed && data.removed.length && self.remove(data);
  698. data.added && data.added.length && self.add(data);
  699. data.changed && data.changed.length && self.change(data);
  700. // fire event with command name
  701. self.trigger(cmd, data);
  702. // force update content
  703. data.sync && self.sync();
  704. },
  705. /**
  706. * Request error handler. Reject dfrd with correct error message.
  707. *
  708. * @param jqxhr request object
  709. * @param String request status
  710. * @return void
  711. **/
  712. error = function(xhr, status) {
  713. var error;
  714. switch (status) {
  715. case 'abort':
  716. error = xhr.quiet ? '' : ['errConnect', 'errAbort'];
  717. break;
  718. case 'timeout':
  719. error = ['errConnect', 'errTimeout'];
  720. break;
  721. case 'parsererror':
  722. error = ['errResponse', 'errDataNotJSON'];
  723. break;
  724. default:
  725. if (xhr.status == 403) {
  726. error = ['errConnect', 'errAccess'];
  727. } else if (xhr.status == 404) {
  728. error = ['errConnect', 'errNotFound'];
  729. } else {
  730. error = 'errConnect';
  731. }
  732. }
  733. dfrd.reject(error, xhr, status);
  734. },
  735. /**
  736. * Request success handler. Valid response data and reject/resolve dfrd.
  737. *
  738. * @param Object response data
  739. * @param String request status
  740. * @return void
  741. **/
  742. success = function(response) {
  743. if (raw) {
  744. return dfrd.resolve(response);
  745. }
  746. if (!response) {
  747. return dfrd.reject(['errResponse', 'errDataEmpty'], xhr);
  748. } else if (!$.isPlainObject(response)) {
  749. return dfrd.reject(['errResponse', 'errDataNotJSON'], xhr);
  750. } else if (response.error) {
  751. return dfrd.reject(response.error, xhr);
  752. } else if (!self.validResponse(cmd, response)) {
  753. return dfrd.reject('errResponse', xhr);
  754. }
  755. response = self.normalize(response);
  756. if (!self.api) {
  757. self.api = response.api || 1;
  758. self.newAPI = self.api >= 2;
  759. self.oldAPI = !self.newAPI;
  760. }
  761. if (response.options) {
  762. cwdOptions = $.extend({}, cwdOptions, response.options);
  763. }
  764. dfrd.resolve(response);
  765. response.debug && self.debug('backend-debug', response.debug);
  766. },
  767. xhr, _xhr
  768. ;
  769. defdone && dfrd.done(done);
  770. dfrd.fail(function(error) {
  771. if (error) {
  772. deffail ? self.error(error) : self.debug('error', self.i18n(error));
  773. }
  774. })
  775. if (!cmd) {
  776. return dfrd.reject('errCmdReq');
  777. }
  778. if (syncOnFail) {
  779. dfrd.fail(function(error) {
  780. error && self.sync();
  781. });
  782. }
  783. if (notify.type && notify.cnt) {
  784. timeout = setTimeout(function() {
  785. self.notify(notify);
  786. dfrd.always(function() {
  787. notify.cnt = -(parseInt(notify.cnt)||0);
  788. self.notify(notify);
  789. })
  790. }, self.notifyDelay)
  791. dfrd.always(function() {
  792. clearTimeout(timeout);
  793. });
  794. }
  795. // quiet abort not completed "open" requests
  796. if (cmd == 'open') {
  797. while ((_xhr = queue.pop())) {
  798. if (!_xhr.isRejected() && !_xhr.isResolved()) {
  799. _xhr.quiet = true;
  800. _xhr.abort();
  801. }
  802. }
  803. }
  804. delete options.preventFail
  805. xhr = this.transport.send(options).fail(error).done(success);
  806. // this.transport.send(options)
  807. // add "open" xhr into queue
  808. if (cmd == 'open') {
  809. queue.unshift(xhr);
  810. dfrd.always(function() {
  811. var ndx = $.inArray(xhr, queue);
  812. ndx !== -1 && queue.splice(ndx, 1);
  813. });
  814. }
  815. return dfrd;
  816. };
  817. /**
  818. * Compare current files cache with new files and return diff
  819. *
  820. * @param Array new files
  821. * @return Object
  822. */
  823. this.diff = function(incoming) {
  824. var raw = {},
  825. added = [],
  826. removed = [],
  827. changed = [],
  828. isChanged = function(hash) {
  829. var l = changed.length;
  830. while (l--) {
  831. if (changed[l].hash == hash) {
  832. return true;
  833. }
  834. }
  835. };
  836. $.each(incoming, function(i, f) {
  837. raw[f.hash] = f;
  838. });
  839. // find removed
  840. $.each(files, function(hash, f) {
  841. !raw[hash] && removed.push(hash);
  842. });
  843. // compare files
  844. $.each(raw, function(hash, file) {
  845. var origin = files[hash];
  846. if (!origin) {
  847. added.push(file);
  848. } else {
  849. $.each(file, function(prop) {
  850. if (file[prop] != origin[prop]) {
  851. changed.push(file)
  852. return false;
  853. }
  854. });
  855. }
  856. });
  857. // parents of removed dirs mark as changed (required for tree correct work)
  858. $.each(removed, function(i, hash) {
  859. var file = files[hash],
  860. phash = file.phash;
  861. if (phash
  862. && file.mime == 'directory'
  863. && $.inArray(phash, removed) === -1
  864. && raw[phash]
  865. && !isChanged(phash)) {
  866. changed.push(raw[phash]);
  867. }
  868. });
  869. return {
  870. added : added,
  871. removed : removed,
  872. changed : changed
  873. };
  874. }
  875. /**
  876. * Sync content
  877. *
  878. * @return jQuery.Deferred
  879. */
  880. this.sync = function() {
  881. var self = this,
  882. dfrd = $.Deferred().done(function() { self.trigger('sync'); }),
  883. opts1 = {
  884. data : {cmd : 'open', init : 1, target : cwd, tree : this.ui.tree ? 1 : 0},
  885. preventDefault : true
  886. },
  887. opts2 = {
  888. data : {cmd : 'parents', target : cwd},
  889. preventDefault : true
  890. };
  891. $.when(
  892. this.request(opts1),
  893. this.request(opts2)
  894. )
  895. .fail(function(error) {
  896. dfrd.reject(error);
  897. error && self.request({
  898. data : {cmd : 'open', target : self.lastDir(''), tree : 1, init : 1},
  899. notify : {type : 'open', cnt : 1, hideCnt : true}
  900. });
  901. })
  902. .done(function(odata, pdata) {
  903. var diff = self.diff(odata.files.concat(pdata && pdata.tree ? pdata.tree : []));
  904. diff.removed.length && self.remove(diff);
  905. diff.added.length && self.add(diff);
  906. diff.changed.length && self.change(diff);
  907. return dfrd.resolve(diff);
  908. });
  909. return dfrd;
  910. }
  911. this.upload = function(files) {
  912. return this.transport.upload(files, this);
  913. }
  914. /**
  915. * Attach listener to events
  916. * To bind to multiply events at once, separate events names by space
  917. *
  918. * @param String event(s) name(s)
  919. * @param Object event handler
  920. * @return elFinder
  921. */
  922. this.bind = function(event, callback) {
  923. var i;
  924. if (typeof(callback) == 'function') {
  925. event = ('' + event).toLowerCase().split(/\s+/);
  926. for (i = 0; i < event.length; i++) {
  927. if (listeners[event[i]] === void(0)) {
  928. listeners[event[i]] = [];
  929. }
  930. listeners[event[i]].push(callback);
  931. }
  932. }
  933. return this;
  934. };
  935. /**
  936. * Remove event listener if exists
  937. *
  938. * @param String event name
  939. * @param Function callback
  940. * @return elFinder
  941. */
  942. this.unbind = function(event, callback) {
  943. var l = listeners[('' + event).toLowerCase()] || [],
  944. i = l.indexOf(callback);
  945. i > -1 && l.splice(i, 1);
  946. //delete callback; // need this?
  947. callback = null
  948. return this;
  949. };
  950. /**
  951. * Fire event - send notification to all event listeners
  952. *
  953. * @param String event type
  954. * @param Object data to send across event
  955. * @return elFinder
  956. */
  957. this.trigger = function(event, data) {
  958. var event = event.toLowerCase(),
  959. handlers = listeners[event] || [], i, j;
  960. this.debug('event-'+event, data)
  961. if (handlers.length) {
  962. event = $.Event(event);
  963. for (i = 0; i < handlers.length; i++) {
  964. // to avoid data modifications. remember about "sharing" passing arguments in js :)
  965. event.data = $.extend(true, {}, data);
  966. try {
  967. if (handlers[i](event, this) === false
  968. || event.isDefaultPrevented()) {
  969. this.debug('event-stoped', event.type);
  970. break;
  971. }
  972. } catch (ex) {
  973. window.console && window.console.log && window.console.log(ex);
  974. }
  975. }
  976. }
  977. return this;
  978. }
  979. /**
  980. * Bind keybord shortcut to keydown event
  981. *
  982. * @example
  983. * elfinder.shortcut({
  984. * pattern : 'ctrl+a',
  985. * description : 'Select all files',
  986. * callback : function(e) { ... },
  987. * keypress : true|false (bind to keypress instead of keydown)
  988. * })
  989. *
  990. * @param Object shortcut config
  991. * @return elFinder
  992. */
  993. this.shortcut = function(s) {
  994. var patterns, pattern, code, i, parts;
  995. if (this.options.allowShortcuts && s.pattern && $.isFunction(s.callback)) {
  996. patterns = s.pattern.toUpperCase().split(/\s+/);
  997. for (i= 0; i < patterns.length; i++) {
  998. pattern = patterns[i]
  999. parts = pattern.split('+');
  1000. code = (code = parts.pop()).length == 1
  1001. ? code > 0 ? code : code.charCodeAt(0)
  1002. : $.ui.keyCode[code];
  1003. if (code && !shortcuts[pattern]) {
  1004. shortcuts[pattern] = {
  1005. keyCode : code,
  1006. altKey : $.inArray('ALT', parts) != -1,
  1007. ctrlKey : $.inArray('CTRL', parts) != -1,
  1008. shiftKey : $.inArray('SHIFT', parts) != -1,
  1009. type : s.type || 'keydown',
  1010. callback : s.callback,
  1011. description : s.description,
  1012. pattern : pattern
  1013. };
  1014. }
  1015. }
  1016. }
  1017. return this;
  1018. }
  1019. /**
  1020. * Registered shortcuts
  1021. *
  1022. * @type Object
  1023. **/
  1024. this.shortcuts = function() {
  1025. var ret = [];
  1026. $.each(shortcuts, function(i, s) {
  1027. ret.push([s.pattern, self.i18n(s.description)]);
  1028. });
  1029. return ret;
  1030. };
  1031. /**
  1032. * Get/set clipboard content.
  1033. * Return new clipboard content.
  1034. *
  1035. * @example
  1036. * this.clipboard([]) - clean clipboard
  1037. * this.clipboard([{...}, {...}], true) - put 2 files in clipboard and mark it as cutted
  1038. *
  1039. * @param Array new files hashes
  1040. * @param Boolean cut files?
  1041. * @return Array
  1042. */
  1043. this.clipboard = function(hashes, cut) {
  1044. var map = function() { return $.map(clipboard, function(f) { return f.hash }); }
  1045. if (hashes !== void(0)) {
  1046. clipboard.length && this.trigger('unlockfiles', {files : map()});
  1047. remember = [];
  1048. clipboard = $.map(hashes||[], function(hash) {
  1049. var file = files[hash];
  1050. if (file) {
  1051. remember.push(hash);
  1052. return {
  1053. hash : hash,
  1054. phash : file.phash,
  1055. name : file.name,
  1056. mime : file.mime,
  1057. read : file.read,
  1058. locked : file.locked,
  1059. cut : !!cut
  1060. }
  1061. }
  1062. return null;
  1063. });
  1064. this.trigger('changeclipboard', {clipboard : clipboard.slice(0, clipboard.length)});
  1065. cut && this.trigger('lockfiles', {files : map()});
  1066. }
  1067. // return copy of clipboard instead of refrence
  1068. return clipboard.slice(0, clipboard.length);
  1069. }
  1070. /**
  1071. * Return true if command enabled
  1072. *
  1073. * @param String command name
  1074. * @return Boolean
  1075. */
  1076. this.isCommandEnabled = function(name) {
  1077. return this._commands[name] ? $.inArray(name, cwdOptions.disabled) === -1 : false;
  1078. }
  1079. /**
  1080. * Exec command and return result;
  1081. *
  1082. * @param String command name
  1083. * @param String|Array usualy files hashes
  1084. * @param String|Array command options
  1085. * @return $.Deferred
  1086. */
  1087. this.exec = function(cmd, files, opts) {
  1088. return this._commands[cmd] && this.isCommandEnabled(cmd)
  1089. ? this._commands[cmd].exec(files, opts)
  1090. : $.Deferred().reject('No such command');
  1091. }
  1092. /**
  1093. * Create and return dialog.
  1094. *
  1095. * @param String|DOMElement dialog content
  1096. * @param Object dialog options
  1097. * @return jQuery
  1098. */
  1099. this.dialog = function(content, options) {
  1100. return $('<div/>').append(content).appendTo(node).elfinderdialog(options);
  1101. }
  1102. /**
  1103. * Return UI widget or node
  1104. *
  1105. * @param String ui name
  1106. * @return jQuery
  1107. */
  1108. this.getUI = function(ui) {
  1109. return this.ui[ui] || node;
  1110. }
  1111. this.command = function(name) {
  1112. return name === void(0) ? this._commands : this._commands[name];
  1113. }
  1114. /**
  1115. * Resize elfinder node
  1116. *
  1117. * @param String|Number width
  1118. * @param Number height
  1119. * @return void
  1120. */
  1121. this.resize = function(w, h) {
  1122. node.css('width', w).height(h).trigger('resize');
  1123. this.trigger('resize', {width : node.width(), height : node.height()});
  1124. }
  1125. /**
  1126. * Restore elfinder node size
  1127. *
  1128. * @return elFinder
  1129. */
  1130. this.restoreSize = function() {
  1131. this.resize(width, height);
  1132. }
  1133. this.show = function() {
  1134. node.show();
  1135. this.enable().trigger('show');
  1136. }
  1137. this.hide = function() {
  1138. this.disable().trigger('hide');
  1139. node.hide();
  1140. }
  1141. /**
  1142. * Destroy this elFinder instance
  1143. *
  1144. * @return void
  1145. **/
  1146. this.destroy = function() {
  1147. if (node && node[0].elfinder) {
  1148. this.trigger('destroy').disable();
  1149. listeners = {};
  1150. shortcuts = {};
  1151. $(document).add(node).unbind('.'+this.namespace);
  1152. self.trigger = function() { }
  1153. node.children().remove();
  1154. node.append(prevContent.contents()).removeClass(this.cssClass).attr('style', prevStyle);
  1155. node[0].elfinder = null;
  1156. if (syncInterval) {
  1157. clearInterval(syncInterval);
  1158. }
  1159. }
  1160. }
  1161. /************* init stuffs ****************/
  1162. // set sort variant
  1163. this.setSort(this.options.sort, this.options.sortDirect);
  1164. // check jquery ui
  1165. if (!($.fn.selectable && $.fn.draggable && $.fn.droppable)) {
  1166. return alert(this.i18n('errJqui'));
  1167. }
  1168. // check node
  1169. if (!node.length) {
  1170. return alert(this.i18n('errNode'));
  1171. }
  1172. // check connector url
  1173. if (!this.options.url) {
  1174. return alert(this.i18n('errURL'));
  1175. }
  1176. $.extend($.ui.keyCode, {
  1177. 'F1' : 112,
  1178. 'F2' : 113,
  1179. 'F3' : 114,
  1180. 'F4' : 115,
  1181. 'F5' : 116,
  1182. 'F6' : 117,
  1183. 'F7' : 118,
  1184. 'F8' : 119,
  1185. 'F9' : 120
  1186. });
  1187. this.dragUpload = false;
  1188. this.xhrUpload = typeof XMLHttpRequestUpload != 'undefined' && typeof File != 'undefined' && typeof FormData != 'undefined';
  1189. // configure transport object
  1190. this.transport = {}
  1191. if (typeof(this.options.transport) == 'object') {
  1192. this.transport = this.options.transport;
  1193. if (typeof(this.transport.init) == 'function') {
  1194. this.transport.init(this)
  1195. }
  1196. }
  1197. if (typeof(this.transport.send) != 'function') {
  1198. this.transport.send = function(opts) { return $.ajax(opts); }
  1199. }
  1200. if (this.transport.upload == 'iframe') {
  1201. this.transport.upload = $.proxy(this.uploads.iframe, this);
  1202. } else if (typeof(this.transport.upload) == 'function') {
  1203. this.dragUpload = !!this.options.dragUploadAllow;
  1204. } else if (this.xhrUpload) {
  1205. this.transport.upload = $.proxy(this.uploads.xhr, this);
  1206. this.dragUpload = true;
  1207. } else {
  1208. this.transport.upload = $.proxy(this.uploads.iframe, this);
  1209. }
  1210. /**
  1211. * Alias for this.trigger('error', {error : 'message'})
  1212. *
  1213. * @param String error message
  1214. * @return elFinder
  1215. **/
  1216. this.error = function() {
  1217. var arg = arguments[0];
  1218. return arguments.length == 1 && typeof(arg) == 'function'
  1219. ? self.bind('error', arg)
  1220. : self.trigger('error', {error : arg});
  1221. }
  1222. // create bind/trigger aliases for build-in events
  1223. $.each(['enable', 'disable', 'load', 'open', 'reload', 'select', 'add', 'remove', 'change', 'dblclick', 'getfile', 'lockfiles', 'unlockfiles', 'dragstart', 'dragstop', 'search', 'searchend', 'viewchange'], function(i, name) {
  1224. self[name] = function() {
  1225. var arg = arguments[0];
  1226. return arguments.length == 1 && typeof(arg) == 'function'
  1227. ? self.bind(name, arg)
  1228. : self.trigger(name, $.isPlainObject(arg) ? arg : {});
  1229. }
  1230. });
  1231. // bind core event handlers
  1232. this
  1233. .enable(function() {
  1234. if (!enabled && self.visible() && self.ui.overlay.is(':hidden')) {
  1235. enabled = true;
  1236. $('texarea:focus,input:focus,button').blur();
  1237. node.removeClass('elfinder-disabled');
  1238. }
  1239. })
  1240. .disable(function() {
  1241. prevEnabled = enabled;
  1242. enabled = false;
  1243. node.addClass('elfinder-disabled');
  1244. })
  1245. .open(function() {
  1246. selected = [];
  1247. })
  1248. .select(function(e) {
  1249. selected = $.map(e.data.selected || e.data.value|| [], function(hash) { return files[hash] ? hash : null; });
  1250. })
  1251. .error(function(e) {
  1252. var opts = {
  1253. cssClass : 'elfinder-dialog-error',
  1254. title : self.i18n(self.i18n('error')),
  1255. resizable : false,
  1256. destroyOnClose : true,
  1257. buttons : {}
  1258. };
  1259. opts.buttons[self.i18n(self.i18n('btnClose'))] = function() { $(this).elfinderdialog('close'); };
  1260. self.dialog('<span class="elfinder-dialog-icon elfinder-dialog-icon-error"/>'+self.i18n(e.data.error), opts);
  1261. })
  1262. .bind('tree parents', function(e) {
  1263. cache(e.data.tree || []);
  1264. })
  1265. .bind('tmb', function(e) {
  1266. $.each(e.data.images||[], function(hash, tmb) {
  1267. if (files[hash]) {
  1268. files[hash].tmb = tmb;
  1269. }
  1270. })
  1271. })
  1272. .add(function(e) {
  1273. cache(e.data.added||[]);
  1274. })
  1275. .change(function(e) {
  1276. $.each(e.data.changed||[], function(i, file) {
  1277. var hash = file.hash;
  1278. files[hash] = files[hash] ? $.extend(files[hash], file) : file;
  1279. });
  1280. })
  1281. .remove(function(e) {
  1282. var removed = e.data.removed||[],
  1283. l = removed.length,
  1284. rm = function(hash) {
  1285. var file = files[hash];
  1286. if (file) {
  1287. if (file.mime == 'directory' && file.dirs) {
  1288. $.each(files, function(h, f) {
  1289. f.phash == hash && rm(h);
  1290. });
  1291. }
  1292. delete files[hash];
  1293. }
  1294. };
  1295. while (l--) {
  1296. rm(removed[l]);
  1297. }
  1298. })
  1299. .bind('search', function(e) {
  1300. cache(e.data.files);
  1301. })
  1302. .bind('rm', function(e) {
  1303. var play = beeper.canPlayType && beeper.canPlayType('audio/wav; codecs="1"');
  1304. play && play != '' && play != 'no' && $(beeper).html('<source src="./sounds/rm.wav" type="audio/wav">')[0].play()
  1305. })
  1306. ;
  1307. // bind external event handlers
  1308. $.each(this.options.handlers, function(event, callback) {
  1309. self.bind(event, callback);
  1310. });
  1311. /**
  1312. * History object. Store visited folders
  1313. *
  1314. * @type Object
  1315. **/
  1316. this.history = new this.history(this);
  1317. // in getFileCallback set - change default actions on duble click/enter/ctrl+enter
  1318. if (typeof(this.options.getFileCallback) == 'function' && this.commands.getfile) {
  1319. this.bind('dblclick', function(e) {
  1320. e.preventDefault();
  1321. self.exec('getfile').fail(function() {
  1322. self.exec('open');
  1323. });
  1324. });
  1325. this.shortcut({
  1326. pattern : 'enter',
  1327. description : this.i18n('cmdgetfile'),
  1328. callback : function() { self.exec('getfile').fail(function() { self.exec(self.OS == 'mac' ? 'rename' : 'open') }) }
  1329. })
  1330. .shortcut({
  1331. pattern : 'ctrl+enter',
  1332. description : this.i18n(this.OS == 'mac' ? 'cmdrename' : 'cmdopen'),
  1333. callback : function() { self.exec(self.OS == 'mac' ? 'rename' : 'open') }
  1334. });
  1335. }
  1336. /**
  1337. * Loaded commands
  1338. *
  1339. * @type Object
  1340. **/
  1341. this._commands = {};
  1342. if (!$.isArray(this.options.commands)) {
  1343. this.options.commands = [];
  1344. }
  1345. // check required commands
  1346. $.each(['open', 'reload', 'back', 'forward', 'up', 'home', 'info', 'quicklook', 'getfile', 'help'], function(i, cmd) {
  1347. $.inArray(cmd, self.options.commands) === -1 && self.options.commands.push(cmd);
  1348. });
  1349. // load commands
  1350. $.each(this.options.commands, function(i, name) {
  1351. var cmd = self.commands[name];
  1352. if ($.isFunction(cmd) && !self._commands[name]) {
  1353. cmd.prototype = base;
  1354. self._commands[name] = new cmd();
  1355. self._commands[name].setup(name, self.options.commandsOptions[name]||{});
  1356. }
  1357. });
  1358. // prepare node
  1359. node.addClass(this.cssClass)
  1360. .bind(mousedown, function() {
  1361. !enabled && self.enable();
  1362. });
  1363. /**
  1364. * UI nodes
  1365. *
  1366. * @type Object
  1367. **/
  1368. this.ui = {
  1369. // container for nav panel and current folder container
  1370. workzone : $('<div/>').appendTo(node).elfinderworkzone(this),
  1371. // container for folders tree / places
  1372. navbar : $('<div/>').appendTo(node).elfindernavbar(this, this.options.uiOptions.navbar || {}),
  1373. // contextmenu
  1374. contextmenu : $('<div/>').appendTo(node).elfindercontextmenu(this),
  1375. // overlay
  1376. overlay : $('<div/>').appendTo(node).elfinderoverlay({
  1377. show : function() { self.disable(); },
  1378. hide : function() { prevEnabled && self.enable(); }
  1379. }),
  1380. // current folder container
  1381. cwd : $('<div/>').appendTo(node).elfindercwd(this),
  1382. // notification dialog window
  1383. notify : this.dialog('', {
  1384. cssClass : 'elfinder-dialog-notify',
  1385. position : {top : '12px', right : '12px'},
  1386. resizable : false,
  1387. autoOpen : false,
  1388. title : '&nbsp;',
  1389. width : 280
  1390. }),
  1391. statusbar : $('<div class="ui-widget-header ui-helper-clearfix ui-corner-bottom elfinder-statusbar"/>').hide().appendTo(node)
  1392. }
  1393. // load required ui
  1394. $.each(this.options.ui || [], function(i, ui) {
  1395. var name = 'elfinder'+ui,
  1396. opts = self.options.uiOptions[ui] || {};
  1397. if (!self.ui[ui] && $.fn[name]) {
  1398. self.ui[ui] = $('<'+(opts.tag || 'div')+'/>').appendTo(node)[name](self, opts);
  1399. }
  1400. });
  1401. // store instance in node
  1402. node[0].elfinder = this;
  1403. // make node resizable
  1404. this.options.resizable
  1405. && $.fn.resizable
  1406. && node.resizable({
  1407. handles : 'se',
  1408. minWidth : 300,
  1409. minHeight : 200
  1410. });
  1411. if (this.options.width) {
  1412. width = this.options.width;
  1413. }
  1414. if (this.options.height) {
  1415. height = parseInt(this.options.height);
  1416. }
  1417. // update size
  1418. self.resize(width, height);
  1419. // attach events to document
  1420. $(document)
  1421. // disable elfinder on click outside elfinder
  1422. .bind('click.'+this.namespace, function(e) { enabled && !$(e.target).closest(node).length && self.disable(); })
  1423. // exec shortcuts
  1424. .bind(keydown+' '+keypress, execShortcut);
  1425. // send initial request and start to pray >_<
  1426. this.trigger('init')
  1427. .request({
  1428. data : {cmd : 'open', target : self.lastDir(), init : 1, tree : this.ui.tree ? 1 : 0},
  1429. preventDone : true,
  1430. notify : {type : 'open', cnt : 1, hideCnt : true},
  1431. freeze : true
  1432. })
  1433. .fail(function() {
  1434. self.trigger('fail').disable().lastDir('');
  1435. listeners = {};
  1436. shortcuts = {};
  1437. $(document).add(node).unbind('.'+this.namespace);
  1438. self.trigger = function() { };
  1439. })
  1440. .done(function(data) {
  1441. self.load().debug('api', self.api);
  1442. data = $.extend(true, {}, data);
  1443. open(data);
  1444. self.trigger('open', data);
  1445. });
  1446. // update ui's size after init
  1447. this.one('load', function() {
  1448. node.trigger('resize');
  1449. if (self.options.sync > 1000) {
  1450. syncInterval = setInterval(function() {
  1451. self.sync();
  1452. }, self.options.sync)
  1453. }
  1454. });
  1455. // self.timeEnd('load');
  1456. }
  1457. /**
  1458. * Prototype
  1459. *
  1460. * @type Object
  1461. */
  1462. elFinder.prototype = {
  1463. res : function(type, id) {
  1464. return this.resources[type] && this.resources[type][id];
  1465. },
  1466. /**
  1467. * Internationalization object
  1468. *
  1469. * @type Object
  1470. */
  1471. i18 : {
  1472. en : {
  1473. translator : '',
  1474. language : 'English',
  1475. direction : 'ltr',
  1476. dateFormat : 'd.m.Y H:i',
  1477. fancyDateFormat : '$1 H:i',
  1478. messages : {}
  1479. },
  1480. months : ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
  1481. monthsShort : ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
  1482. days : ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
  1483. daysShort : ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
  1484. },
  1485. /**
  1486. * File mimetype to kind mapping
  1487. *
  1488. * @type Object
  1489. */
  1490. kinds : {
  1491. 'unknown' : 'Unknown',
  1492. 'directory' : 'Folder',
  1493. 'symlink' : 'Alias',
  1494. 'symlink-broken' : 'AliasBroken',
  1495. 'application/x-empty' : 'TextPlain',
  1496. 'application/postscript' : 'Postscript',
  1497. 'application/vnd.ms-office' : 'MsOffice',
  1498. 'application/vnd.ms-word' : 'MsWord',
  1499. 'application/vnd.ms-excel' : 'MsExcel',
  1500. 'application/vnd.ms-powerpoint' : 'MsPP',
  1501. 'application/pdf' : 'PDF',
  1502. 'application/xml' : 'XML',
  1503. 'application/vnd.oasis.opendocument.text' : 'OO',
  1504. 'application/x-shockwave-flash' : 'AppFlash',
  1505. 'application/flash-video' : 'Flash video',
  1506. 'application/x-bittorrent' : 'Torrent',
  1507. 'application/javascript' : 'JS',
  1508. 'application/rtf' : 'RTF',
  1509. 'application/rtfd' : 'RTF',
  1510. 'application/x-font-ttf' : 'TTF',
  1511. 'application/x-font-otf' : 'OTF',
  1512. 'application/x-rpm' : 'RPM',
  1513. 'application/x-web-config' : 'TextPlain',
  1514. 'application/xhtml+xml' : 'HTML',
  1515. 'application/docbook+xml' : 'DOCBOOK',
  1516. 'application/x-awk' : 'AWK',
  1517. 'application/x-gzip' : 'GZIP',
  1518. 'application/x-bzip2' : 'BZIP',
  1519. 'application/zip' : 'ZIP',
  1520. 'application/x-zip' : 'ZIP',
  1521. 'application/x-rar' : 'RAR',
  1522. 'application/x-tar' : 'TAR',
  1523. 'application/x-7z-compressed' : '7z',
  1524. 'application/x-jar' : 'JAR',
  1525. 'text/plain' : 'TextPlain',
  1526. 'text/x-php' : 'PHP',
  1527. 'text/html' : 'HTML',
  1528. 'text/javascript' : 'JS',
  1529. 'text/css' : 'CSS',
  1530. 'text/rtf' : 'RTF',
  1531. 'text/rtfd' : 'RTF',
  1532. 'text/x-c' : 'C',
  1533. 'text/x-csrc' : 'C',
  1534. 'text/x-chdr' : 'CHeader',
  1535. 'text/x-c++' : 'CPP',
  1536. 'text/x-c++src' : 'CPP',
  1537. 'text/x-c++hdr' : 'CPPHeader',
  1538. 'text/x-shellscript' : 'Shell',
  1539. 'application/x-csh' : 'Shell',
  1540. 'text/x-python' : 'Python',
  1541. 'text/x-java' : 'Java',
  1542. 'text/x-java-source' : 'Java',
  1543. 'text/x-ruby' : 'Ruby',
  1544. 'text/x-perl' : 'Perl',
  1545. 'text/x-sql' : 'SQL',
  1546. 'text/xml' : 'XML',
  1547. 'text/x-comma-separated-values' : 'CSV',
  1548. 'image/x-ms-bmp' : 'BMP',
  1549. 'image/jpeg' : 'JPEG',
  1550. 'image/gif' : 'GIF',
  1551. 'image/png' : 'PNG',
  1552. 'image/tiff' : 'TIFF',
  1553. 'image/x-targa' : 'TGA',
  1554. 'image/vnd.adobe.photoshop' : 'PSD',
  1555. 'image/xbm' : 'XBITMAP',
  1556. 'image/pxm' : 'PXM',
  1557. 'audio/mpeg' : 'AudioMPEG',
  1558. 'audio/midi' : 'AudioMIDI',
  1559. 'audio/ogg' : 'AudioOGG',
  1560. 'audio/mp4' : 'AudioMPEG4',
  1561. 'audio/x-m4a' : 'AudioMPEG4',
  1562. 'audio/wav' : 'AudioWAV',
  1563. 'audio/x-mp3-playlist' : 'AudioPlaylist',
  1564. 'video/x-dv' : 'VideoDV',
  1565. 'video/mp4' : 'VideoMPEG4',
  1566. 'video/mpeg' : 'VideoMPEG',
  1567. 'video/x-msvideo' : 'VideoAVI',
  1568. 'video/quicktime' : 'VideoMOV',
  1569. 'video/x-ms-wmv' : 'VideoWM',
  1570. 'video/x-flv' : 'VideoFlash',
  1571. 'video/x-matroska' : 'VideoMKV',
  1572. 'video/ogg' : 'VideoOGG'
  1573. },
  1574. /**
  1575. * Ajax request data validation rules
  1576. *
  1577. * @type Object
  1578. */
  1579. rules : {
  1580. defaults : function(data) {
  1581. if (!data
  1582. || (data.added && !$.isArray(data.added))
  1583. || (data.removed && !$.isArray(data.removed))
  1584. || (data.changed && !$.isArray(data.changed))) {
  1585. return false;
  1586. }
  1587. return true;
  1588. },
  1589. open : function(data) { return data && data.cwd && data.files && $.isPlainObject(data.cwd) && $.isArray(data.files); },
  1590. tree : function(data) { return data && data.tree && $.isArray(data.tree); },
  1591. parents : function(data) { return data && data.tree && $.isArray(data.tree); },
  1592. tmb : function(data) { return data && data.images && ($.isPlainObject(data.images) || $.isArray(data.images)); },
  1593. upload : function(data) { return data && ($.isPlainObject(data.added) || $.isArray(data.added));},
  1594. search : function(data) { return data && data.files && $.isArray(data.files)}
  1595. },
  1596. /**
  1597. * Sort types for current directory content
  1598. *
  1599. * @type Object
  1600. */
  1601. sorts : {
  1602. nameDirsFirst : 1,
  1603. kindDirsFirst : 2,
  1604. sizeDirsFirst : 3,
  1605. dateDirsFirst : 4,
  1606. name : 5,
  1607. kind : 6,
  1608. size : 7,
  1609. date : 8
  1610. },
  1611. setSort : function(type, dir) {
  1612. this.sort = this.sorts[type] || 1;
  1613. this.sortDirect = dir == 'asc' || dir == 'desc' ? dir : 'asc';
  1614. this.trigger('sortchange');
  1615. },
  1616. /**
  1617. * Commands costructors
  1618. *
  1619. * @type Object
  1620. */
  1621. commands : {},
  1622. parseUploadData : function(text) {
  1623. var data;
  1624. if (!$.trim(text)) {
  1625. return {error : ['errResponse', 'errDataEmpty']};
  1626. }
  1627. try {
  1628. data = $.parseJSON(text);
  1629. } catch (e) {
  1630. return {error : ['errResponse', 'errDataNotJSON']}
  1631. }
  1632. if (!this.validResponse('upload', data)) {
  1633. return {error : ['errResponse']};
  1634. }
  1635. data = this.normalize(data);
  1636. data.removed = $.map(data.added||[], function(f) { return f.hash; })
  1637. return data;
  1638. },
  1639. iframeCnt : 0,
  1640. uploads : {
  1641. // upload transport using iframe
  1642. iframe : function(data, fm) {
  1643. var self = fm ? fm : this,
  1644. input = data.input,
  1645. dfrd = $.Deferred()
  1646. .fail(function(error) {
  1647. error && self.error(error);
  1648. })
  1649. .done(function(data) {
  1650. data.warning && self.error(data.warning);
  1651. data.removed && self.remove(data);
  1652. data.added && self.add(data);
  1653. data.changed && self.change(data);
  1654. self.trigger('upload', data);
  1655. data.sync && self.sync();
  1656. }),
  1657. name = 'iframe-'+self.namespace+(++self.iframeCnt),
  1658. form = $('<form action="'+self.uploadURL+'" method="post" enctype="multipart/form-data" encoding="multipart/form-data" target="'+name+'" style="display:none"><input type="hidden" name="cmd" value="upload" /></form>'),
  1659. msie = $.browser.msie,
  1660. // clear timeouts, close notification dialog, remove form/iframe
  1661. onload = function() {
  1662. abortto && clearTimeout(abortto);
  1663. notifyto && clearTimeout(notifyto);
  1664. notify && self.notify({type : 'upload', cnt : -cnt});
  1665. setTimeout(function() {
  1666. msie && $('<iframe src="javascript:false;"/>').appendTo(form);
  1667. form.remove();
  1668. iframe.remove();
  1669. }, 100);
  1670. },
  1671. iframe = $('<iframe src="'+(msie ? 'javascript:false;' : 'about:blank')+'" name="'+name+'" style="position:absolute;left:-1000px;top:-1000px" />')
  1672. .bind('load', function() {
  1673. iframe.unbind('load')
  1674. .bind('load', function() {
  1675. var data = self.parseUploadData(iframe.contents().text());
  1676. onload();
  1677. data.error ? dfrd.reject(data.error) : dfrd.resolve(data);
  1678. });
  1679. // notify dialog
  1680. notifyto = setTimeout(function() {
  1681. notify = true;
  1682. self.notify({type : 'upload', cnt : cnt});
  1683. }, self.options.notifyDelay);
  1684. // emulate abort on timeout
  1685. if (self.options.iframeTimeout > 0) {
  1686. abortto = setTimeout(function() {
  1687. onload();
  1688. dfrd.reject([errors.connect, errors.timeout]);
  1689. }, self.options.iframeTimeout);
  1690. }
  1691. form.submit();
  1692. }),
  1693. cnt, notify, notifyto, abortto
  1694. ;
  1695. if (input && $(input).is(':file') && $(input).val()) {
  1696. form.append(input);
  1697. } else {
  1698. return dfrd.reject();
  1699. }
  1700. cnt = input.files ? input.files.length : 1;
  1701. form.append('<input type="hidden" name="'+(self.newAPI ? 'target' : 'current')+'" value="'+self.cwd().hash+'"/>')
  1702. .append('<input type="hidden" name="html" value="1"/>')
  1703. .append($(input).attr('name', 'upload[]'));
  1704. $.each(self.options.onlyMimes||[], function(i, mime) {
  1705. form.append('<input type="hidden" name="mimes[]" value="'+mime+'"/>');
  1706. });
  1707. $.each(self.options.customData, function(key, val) {
  1708. form.append('<input type="hidden" name="'+key+'" value="'+val+'"/>');
  1709. });
  1710. form.appendTo('body');
  1711. iframe.appendTo('body');
  1712. return dfrd;
  1713. },
  1714. // upload transport using XMLHttpRequest
  1715. xhr : function(data, fm) {
  1716. var self = fm ? fm : this,
  1717. dfrd = $.Deferred()
  1718. .fail(function(error) {
  1719. error && self.error(error);
  1720. })
  1721. .done(function(data) {
  1722. data.warning && self.error(data.warning);
  1723. data.removed && self.remove(data);
  1724. data.added && self.add(data);
  1725. data.changed && self.change(data);
  1726. self.trigger('upload', data);
  1727. data.sync && self.sync();
  1728. })
  1729. .always(function() {
  1730. notifyto && clearTimeout(notifyto);
  1731. notify && self.notify({type : 'upload', cnt : -cnt, progress : 100*cnt});
  1732. }),
  1733. xhr = new XMLHttpRequest(),
  1734. formData = new FormData(),
  1735. files = data.input ? data.input.files : data.files,
  1736. cnt = files.length,
  1737. loaded = 5,
  1738. notify = false,
  1739. startNotify = function() {
  1740. return setTimeout(function() {
  1741. notify = true;
  1742. self.notify({type : 'upload', cnt : cnt, progress : loaded*cnt});
  1743. }, self.options.notifyDelay);
  1744. },
  1745. notifyto;
  1746. if (!cnt) {
  1747. return dfrd.reject();
  1748. }
  1749. xhr.addEventListener('error', function() {
  1750. dfrd.reject('errConnect');
  1751. }, false);
  1752. xhr.addEventListener('abort', function() {
  1753. dfrd.reject(['errConnect', 'errAbort']);
  1754. }, false);
  1755. xhr.addEventListener('load', function() {
  1756. var status = xhr.status, data;
  1757. if (status > 500) {
  1758. return dfrd.reject('errResponse');
  1759. }
  1760. if (status != 200) {
  1761. return dfrd.reject('errConnect');
  1762. }
  1763. if (xhr.readyState != 4) {
  1764. return dfrd.reject(['errConnect', 'errTimeout']); // am i right?
  1765. }
  1766. if (!xhr.responseText) {
  1767. return dfrd.reject(['errResponse', 'errDataEmpty']);
  1768. }
  1769. data = self.parseUploadData(xhr.responseText);
  1770. data.error ? dfrd.reject(data.error) : dfrd.resolve(data);
  1771. }, false);
  1772. xhr.upload.addEventListener('progress', function(e) {
  1773. var prev = loaded, curr;
  1774. if (e.lengthComputable) {
  1775. curr = parseInt(e.loaded*100 / e.total);
  1776. // to avoid strange bug in safari (not in chrome) with drag&drop.
  1777. // bug: macos finder opened in any folder,
  1778. // reset safari cache (option+command+e), reload elfinder page,
  1779. // drop file from finder
  1780. // on first attempt request starts (progress callback called ones) but never ends.
  1781. // any next drop - successfull.
  1782. if (curr > 0 && !notifyto) {
  1783. notifyto = startNotify();
  1784. }
  1785. if (curr - prev > 4) {
  1786. loaded = curr;
  1787. notify && self.notify({type : 'upload', cnt : 0, progress : (loaded - prev)*cnt});
  1788. }
  1789. }
  1790. }, false);
  1791. xhr.open('POST', self.uploadURL, true);
  1792. formData.append('cmd', 'upload');
  1793. formData.append(self.newAPI ? 'target' : 'current', self.cwd().hash);
  1794. $.each(self.options.customData, function(key, val) {