Failed to save the file to the "xx" directory.

Failed to save the file to the "ll" directory.

Failed to save the file to the "mm" directory.

Failed to save the file to the "wp" directory.

403WebShell
403Webshell
Server IP : 66.29.132.124  /  Your IP : 3.17.78.184
Web Server : LiteSpeed
System : Linux business141.web-hosting.com 4.18.0-553.lve.el8.x86_64 #1 SMP Mon May 27 15:27:34 UTC 2024 x86_64
User : wavevlvu ( 1524)
PHP Version : 7.4.33
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : ON  |  Sudo : OFF  |  Pkexec : OFF
Directory :  /home/wavevlvu/wavestudioz.com/wp-content/plugins/wp-file-manager/lib/js/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /home/wavevlvu/wavestudioz.com/wp-content/plugins/wp-file-manager/lib/js//elFinder.js
/**
 * @class elFinder - file manager for web
 *
 * @author Dmitry (dio) Levashov
 **/
 var elFinder = function(elm, opts, bootCallback) {
	"use strict";
	//this.time('load');
	var self = this,
		
		/**
		 * Objects array of jQuery.Deferred that calls before elFinder boot up
		 * 
		 * @type Array
		 */
		dfrdsBeforeBootup = [],
		
		/**
		 * Plugin name to check for conflicts with bootstrap etc
		 *
		 * @type Array
		 **/
		conflictChecks = ['button', 'tooltip'],
		
		/**
		 * Node on which elfinder creating
		 *
		 * @type jQuery
		 **/
		node = jQuery(elm),
		
		/**
		 * Object of events originally registered in this node
		 * 
		 * @type Object
		 */
		prevEvents = jQuery.extend(true, {}, jQuery._data(node.get(0), 'events')),
		
		/**
		 * Store node contents.
		 *
		 * @see this.destroy
		 * @type jQuery
		 **/
		prevContent = jQuery('<div></div>').append(node.contents()).attr('class', node.attr('class') || '').attr('style', node.attr('style') || ''),
		
		/**
		 * Instance ID. Required to get/set cookie
		 *
		 * @type String
		 **/
		id = node.attr('id') || node.attr('id', 'elfauto' + jQuery('.elfinder').length).attr('id'),
		
		/**
		 * Events namespace
		 *
		 * @type String
		 **/
		namespace = 'elfinder-' + id,
		
		/**
		 * Mousedown event
		 *
		 * @type String
		 **/
		mousedown = 'mousedown.'+namespace,
		
		/**
		 * Keydown event
		 *
		 * @type String
		 **/
		keydown = 'keydown.'+namespace,
		
		/**
		 * Keypress event
		 *
		 * @type String
		 **/
		keypress = 'keypress.'+namespace,
		
		/**
		 * Keypup event
		 *
		 * @type String
		 **/
		keyup    = 'keyup.'+namespace,

		/**
		 * Is shortcuts/commands enabled
		 *
		 * @type Boolean
		 **/
		enabled = false,
		
		/**
		 * Store enabled value before ajax request
		 *
		 * @type Boolean
		 **/
		prevEnabled = false,
		
		/**
		 * List of build-in events which mapped into methods with same names
		 *
		 * @type Array
		 **/
		events = ['enable', 'disable', 'load', 'open', 'reload', 'select',  'add', 'remove', 'change', 'dblclick', 'getfile', 'lockfiles', 'unlockfiles', 'selectfiles', 'unselectfiles', 'dragstart', 'dragstop', 'search', 'searchend', 'viewchange'],
		
		/**
		 * Rules to validate data from backend
		 *
		 * @type Object
		 **/
		rules = {},
		
		/**
		 * Current working directory hash
		 *
		 * @type String
		 **/
		cwd = '',
		
		/**
		 * Current working directory options default
		 *
		 * @type Object
		 **/
		cwdOptionsDefault = {
			path          : '',
			url           : '',
			tmbUrl        : '',
			disabled      : [],
			separator     : '/',
			archives      : [],
			extract       : [],
			copyOverwrite : true,
			uploadOverwrite : true,
			uploadMaxSize : 0,
			jpgQuality    : 100,
			tmbCrop       : false,
			tmbReqCustomData : false,
			tmb           : false // old API
		},
		
		/**
		 * Current working directory options
		 *
		 * @type Object
		 **/
		cwdOptions = {},
		
		/**
		 * Files/dirs cache
		 *
		 * @type Object
		 **/
		files = {},
		
		/**
		 * Hidden Files/dirs cache
		 *
		 * @type Object
		 **/
		hiddenFiles = {},

		/**
		 * Files/dirs hash cache of each dirs
		 *
		 * @type Object
		 **/
		ownFiles = {},
		
		/**
		 * Selected files hashes
		 *
		 * @type Array
		 **/
		selected = [],
		
		/**
		 * Events listeners
		 *
		 * @type Object
		 **/
		listeners = {},
		
		/**
		 * Shortcuts
		 *
		 * @type Object
		 **/
		shortcuts = {},
		
		/**
		 * Buffer for copied files
		 *
		 * @type Array
		 **/
		clipboard = [],
		
		/**
		 * Copied/cuted files hashes
		 * Prevent from remove its from cache.
		 * Required for dispaly correct files names in error messages
		 *
		 * @type Object
		 **/
		remember = {},
		
		/**
		 * Queue for 'open' requests
		 *
		 * @type Array
		 **/
		queue = [],
		
		/**
		 * Queue for only cwd requests e.g. `tmb`
		 *
		 * @type Array
		 **/
		cwdQueue = [],
		
		/**
		 * Commands prototype
		 *
		 * @type Object
		 **/
		base = new self.command(self),
		
		/**
		 * elFinder node width
		 *
		 * @type String
		 * @default "auto"
		 **/
		width  = 'auto',
		
		/**
		 * elFinder node height
		 * Number: pixcel or String: Number + "%"
		 *
		 * @type Number | String
		 * @default 400
		 **/
		height = 400,
		
		/**
		 * Base node object or selector
		 * Element which is the reference of the height percentage
		 *
		 * @type Object|String
		 * @default null | jQuery(window) (if height is percentage)
		 **/
		heightBase = null,
		
		/**
		 * MIME type list(Associative array) handled as a text file
		 * 
		 * @type Object|null
		 */
		textMimes = null,
		
		/**
		 * elfinder path for sound played on remove
		 * @type String
		 * @default ./sounds/
		 **/
		soundPath = '../wp-content/plugins/wp-file-manager/lib/sounds/',
		
		/**
		 * JSON.stringify of previous fm.sorters
		 * @type String
		 */
		prevSorterStr = '',

		/**
		 * Map table of file extention to MIME-Type
		 * @type Object
		 */
		extToMimeTable,

		/**
		 * Disabled page unload function
		 * @type Boolean
		 */
		diableUnloadCheck = false,

		beeper = jQuery(document.createElement('audio')).hide().appendTo('body')[0],
			
		syncInterval,
		autoSyncStop = 0,
		
		uiCmdMapPrev = '',
		
		gcJobRes = null,
		
		open = function(data) {
			// NOTES: Do not touch data object
		
			var volumeid, contextmenu, emptyDirs = {}, stayDirs = {},
				rmClass, hashes, calc, gc, collapsed, prevcwd, sorterStr, diff;
			
			if (self.api >= 2.1) {
				// support volume driver option `uiCmdMap`
				self.commandMap = (data.options.uiCmdMap && Object.keys(data.options.uiCmdMap).length)? data.options.uiCmdMap : {};
				if (uiCmdMapPrev !== JSON.stringify(self.commandMap)) {
					uiCmdMapPrev = JSON.stringify(self.commandMap);
				}
			} else {
				self.options.sync = 0;
			}
			
			if (data.init) {
				// init - reset cache
				files = {};
				ownFiles = {};
			} else {
				// remove only files from prev cwd
				// and collapsed directory (included 100+ directories) to empty for perfomance tune in DnD
				prevcwd = cwd;
				rmClass = 'elfinder-subtree-loaded ' + self.res('class', 'navexpand');
				collapsed = self.res('class', 'navcollapse');
				hashes = Object.keys(files);
				calc = function(i) {
					if (!files[i]) {
						return true;
					}
					
					var isDir = (files[i].mime === 'directory'),
						phash = files[i].phash,
						pnav;
						
					if (
						(!isDir
							|| emptyDirs[phash]
							|| (!stayDirs[phash]
								&& self.navHash2Elm(files[i].hash).is(':hidden')
								&& self.navHash2Elm(phash).next('.elfinder-navbar-subtree').children().length > 100
							)
						)
						&& (isDir || phash !== cwd)
						&& ! remember[i]
					) {
						if (isDir && !emptyDirs[phash]) {
							emptyDirs[phash] = true;
							self.navHash2Elm(phash)
							 .removeClass(rmClass)
							 .next('.elfinder-navbar-subtree').empty();
						}
						deleteCache(files[i]);
					} else if (isDir) {
						stayDirs[phash] = true;
					}
				};
				gc = function() {
					if (hashes.length) {
						gcJobRes && gcJobRes._abort();
						gcJobRes = self.asyncJob(calc, hashes, {
							interval : 20,
							numPerOnce : 100
						}).done(function() {
							var hd = self.storage('hide') || {items: {}};
							if (Object.keys(hiddenFiles).length) {
								jQuery.each(hiddenFiles, function(h) {
									if (!hd.items[h]) {
										delete hiddenFiles[h];
									}
								});
							}
						});
					}
				};
				
				self.trigger('filesgc').one('filesgc', function() {
					hashes = [];
				});
				
				self.one('opendone', function() {
					if (prevcwd !== cwd) {
						if (! node.data('lazycnt')) {
							gc();
						} else {
							self.one('lazydone', gc);
						}
					}
				});
			}

			self.sorters = {};
			cwd = data.cwd.hash;
			cache(data.files);
			if (!files[cwd]) {
				cache([data.cwd]);
			} else {
				diff = self.diff([data.cwd], true);
				if (diff.changed.length) {
					cache(diff.changed, 'change');
					self.change({changed: diff.changed});
				}
			}
			data.changed && data.changed.length && cache(data.changed, 'change');

			// trigger event 'sorterupdate'
			sorterStr = JSON.stringify(self.sorters);
			if (prevSorterStr !== sorterStr) {
				self.trigger('sorterupdate');
				prevSorterStr = sorterStr;
			}

			self.lastDir(cwd);
			
			self.autoSync();
		},
		
		/**
		 * Store info about files/dirs in "files" object.
		 *
		 * @param  Array  files
		 * @param  String data type
		 * @return void
		 **/
		cache = function(data, type) {
			var type      = type || 'files',
				keeps = ['sizeInfo', 'encoding'],
				defsorter = { name: true, perm: true, date: true,  size: true, kind: true },
				sorterChk = !self.sorters._checked && (type === 'files'),
				l         = data.length,
				setSorter = function(file) {
					var f = file || {},
						sorters = [];
					jQuery.each(self.sortRules, function(key) {
						if (defsorter[key] || typeof f[key] !== 'undefined' || (key === 'mode' && typeof f.perm !== 'undefined')) {
							sorters.push(key);
						}
					});
					self.sorters = self.arrayFlip(sorters, true);
					self.sorters._checked = true;
				},
				changedParents = {},
				hideData = self.storage('hide') || {},
				hides = hideData.items || {},
				f, i, i1, keepProp, parents, hidden;

			for (i = 0; i < l; i++) {
				f = Object.assign({}, data[i]);
				hidden = (!hideData.show && hides[f.hash])? true : false;
				if (f.name && f.hash && f.mime) {
					if (!hidden) {
						if (sorterChk && f.phash === cwd) {
							setSorter(f);
							sorterChk = false;
						}
						
						if (f.phash && (type === 'add' || (type === 'change' && (!files[f.hash] || f.size !== files[f.hash])))) {
							if (parents = self.parents(f.phash)) {
								jQuery.each(parents, function() {
									changedParents[this] = true;
								});
							}
						}
					}

					if (files[f.hash]) {
						for (i1 =0; i1 < keeps.length; i1++) {
							if(files[f.hash][keeps[i1]] && ! f[keeps[i1]]) {
								f[keeps[i1]] = files[f.hash][keeps[i1]];
							}
						}
						if (f.sizeInfo && !f.size) {
							f.size = f.sizeInfo.size;
						}
						deleteCache(files[f.hash], true);
					}
					if (hides[f.hash]) {
						hiddenFiles[f.hash] = f;
					}
					if (hidden) {
						l--;
						data.splice(i--, 1);
					} else {
						files[f.hash] = f;
						if (f.mime === 'directory' && !ownFiles[f.hash]) {
							ownFiles[f.hash] = {};
						}
						if (f.phash) {
							if (!ownFiles[f.phash]) {
								ownFiles[f.phash] = {};
							}
							ownFiles[f.phash][f.hash] = true;
						}
					}
				}
			}
			// delete sizeInfo cache
			jQuery.each(Object.keys(changedParents), function() {
				var target = files[this];
				if (target && target.sizeInfo) {
					delete target.sizeInfo;
				}
			});
			
			// for empty folder
			sorterChk && setSorter();
		},
		
		/**
		 * Delete file object from files caches
		 * 
		 * @param  Array  removed hashes
		 * @return void
		 */
		remove = function(removed) {
			var l       = removed.length,
				roots   = {},
				rm      = function(hash) {
					var file = files[hash], i;
					if (file) {
						if (file.mime === 'directory') {
							if (roots[hash]) {
								delete self.roots[roots[hash]];
							}
							// restore stats of deleted root parent directory
							jQuery.each(self.leafRoots, function(phash, roots) {
								var idx, pdir;
								if ((idx = jQuery.inArray(hash, roots))!== -1) {
									if (roots.length === 1) {
										if ((pdir = Object.assign({}, files[phash])) && pdir._realStats) {
											jQuery.each(pdir._realStats, function(k, v) {
												pdir[k] = v;
											});
											remove(files[phash]._realStats);
											self.change({ changed: [pdir] });
										}
										delete self.leafRoots[phash];
									} else {
										self.leafRoots[phash].splice(idx, 1);
									}
								}
							});
							if (self.searchStatus.state < 2) {
								jQuery.each(files, function(h, f) {
									f.phash == hash && rm(h);
								});
							}
						}
						if (file.phash) {
							if (parents = self.parents(file.phash)) {
								jQuery.each(parents, function() {
									changedParents[this] = true;
								});
							}
						}
						deleteCache(files[hash]);
					}
				},
				changedParents = {},
				parents;
		
			jQuery.each(self.roots, function(k, v) {
				roots[v] = k;
			});
			while (l--) {
				rm(removed[l]);
			}
			// delete sizeInfo cache
			jQuery.each(Object.keys(changedParents), function() {
				var target = files[this];
				if (target && target.sizeInfo) {
					delete target.sizeInfo;
				}
			});
		},
		
		/**
		 * Update file object in files caches
		 * 
		 * @param  Array  changed file objects
		 * @return void
		 * @deprecated should be use `cache(updatesArrayData, 'change');`
		 */
		change = function(changed) {
			jQuery.each(changed, function(i, file) {
				var hash = file.hash;
				if (files[hash]) {
					jQuery.each(Object.keys(files[hash]), function(i, v){
						if (typeof file[v] === 'undefined') {
							delete files[hash][v];
						}
					});
				}
				files[hash] = files[hash] ? Object.assign(files[hash], file) : file;
			});
		},
		
		/**
		 * Delete cache data of files, ownFiles and self.optionsByHashes
		 * 
		 * @param  Object  file
		 * @param  Boolean update
		 * @return void
		 */
		deleteCache = function(file, update) {
			var hash = file.hash,
				phash = file.phash;
			
			if (phash && ownFiles[phash]) {
				 delete ownFiles[phash][hash];
			}
			if (!update) {
				ownFiles[hash] && delete ownFiles[hash];
				self.optionsByHashes[hash] && delete self.optionsByHashes[hash];
			}
			delete files[hash];
		},
		
		/**
		 * Maximum number of concurrent connections on request
		 * 
		 * @type Number
		 */
		requestMaxConn,
		
		/**
		 * Current number of connections
		 * 
		 * @type Number
		 */
		requestCnt = 0,
		
		/**
		 * Queue waiting for connection
		 * 
		 * @type Array
		 */
		requestQueue = [],
		
		/**
		 * Current open command instance
		 * 
		 * @type Object
		 */
		currentOpenCmd = null,

		/**
		 * Exec shortcut
		 *
		 * @param  jQuery.Event  keydown/keypress event
		 * @return void
		 */
		execShortcut = function(e) {
			var code    = e.keyCode,
				ctrlKey = !!(e.ctrlKey || e.metaKey),
				isMousedown = e.type === 'mousedown',
				ddm;

			!isMousedown && (self.keyState.keyCode = code);
			self.keyState.ctrlKey  = ctrlKey;
			self.keyState.shiftKey = e.shiftKey;
			self.keyState.metaKey  = e.metaKey;
			self.keyState.altKey   = e.altKey;
			if (isMousedown) {
				return;
			} else if (e.type === 'keyup') {
				self.keyState.keyCode = null;
				return;
			}

			if (enabled) {

				jQuery.each(shortcuts, function(i, shortcut) {
					if (shortcut.type    == e.type 
					&& shortcut.keyCode  == code 
					&& shortcut.shiftKey == e.shiftKey 
					&& shortcut.ctrlKey  == ctrlKey 
					&& shortcut.altKey   == e.altKey) {
						e.preventDefault();
						e.stopPropagation();
						shortcut.callback(e, self);
						self.debug('shortcut-exec', i+' : '+shortcut.description);
					}
				});
				
				// prevent tab out of elfinder
				if (code == jQuery.ui.keyCode.TAB && !jQuery(e.target).is(':input')) {
					e.preventDefault();
				}
				
				// cancel any actions by [Esc] key
				if (e.type === 'keydown' && code == jQuery.ui.keyCode.ESCAPE) {
					// copy or cut 
					if (! node.find('.ui-widget:visible').length) {
						self.clipboard().length && self.clipboard([]);
					}
					// dragging
					if (jQuery.ui.ddmanager) {
						ddm = jQuery.ui.ddmanager.current;
						ddm && ddm.helper && ddm.cancel();
					}
					// button menus
					self.toHide(node.find('.ui-widget.elfinder-button-menu.elfinder-frontmost:visible'));
					// trigger keydownEsc
					self.trigger('keydownEsc', e);
				}

			}
		},
		date = new Date(),
		utc,
		i18n,
		inFrame = (window.parent !== window),
		parentIframe = (function() {
			var pifm, ifms;
			if (inFrame) {
				try {
					ifms = jQuery('iframe', window.parent.document);
					if (ifms.length) {
						jQuery.each(ifms, function(i, ifm) {
							if (ifm.contentWindow === window) {
								pifm = jQuery(ifm);
								return false;
							}
						});
					}
				} catch(e) {}
			}
			return pifm;
		})(),
		/**
		 * elFinder boot up function
		 * 
		 * @type Function
		 */
		bootUp,
		/**
		 * Original function of XMLHttpRequest.prototype.send
		 * 
		 * @type Function
		 */
		savedXhrSend;
	
	// opts must be an object
	if (!opts) {
		opts = {};
	}
	
	// set UA.Angle, UA.Rotated for mobile devices
	if (self.UA.Mobile) {
		jQuery(window).on('orientationchange.'+namespace, function() {
			var a = ((screen && screen.orientation && screen.orientation.angle) || window.orientation || 0) + 0;
			if (a === -90) {
				a = 270;
			}
			self.UA.Angle = a;
			self.UA.Rotated = a % 180 === 0? false : true;
		}).trigger('orientationchange.'+namespace);
	}
	
	// check opt.bootCallback
	if (opts.bootCallback && typeof opts.bootCallback === 'function') {
		(function() {
			var func = bootCallback,
				opFunc = opts.bootCallback;
			bootCallback = function(fm, extraObj) {
				func && typeof func === 'function' && func.call(this, fm, extraObj);
				opFunc.call(this, fm, extraObj);
			};
		})();
	}
	delete opts.bootCallback;

	/**
	 * Protocol version
	 *
	 * @type String
	 **/
	this.api = null;
	
	/**
	 * elFinder use new api
	 *
	 * @type Boolean
	 **/
	this.newAPI = false;
	
	/**
	 * elFinder use old api
	 *
	 * @type Boolean
	 **/
	this.oldAPI = false;
	
	/**
	 * Net drivers names
	 *
	 * @type Array
	 **/
	this.netDrivers = [];
	
	/**
	 * Base URL of elfFinder library starting from Manager HTML
	 * 
	 * @type String
	 */
	this.baseUrl = '';
	
	/**
	 * Base URL of i18n js files
	 * baseUrl + "js/i18n/" when empty value
	 * 
	 * @type String
	 */
	this.i18nBaseUrl = '';

	/**
	 * Base URL of worker js files
	 * baseUrl + "js/worker/" when empty value
	 * 
	 * @type String
	 */
	 this.workerBaseUrl = '';

	/**
	 * Is elFinder CSS loaded
	 * 
	 * @type Boolean
	 */
	this.cssloaded = false;
	
	/**
	 * Current theme object
	 * 
	 * @type Object|Null
	 */
	this.theme = null;

	this.mimesCanMakeEmpty = {};

	/**
	 * Callback function at boot up that option specified at elFinder starting
	 * 
	 * @type Function
	 */
	this.bootCallback;

	/**
	 * Callback function at reload(restart) elFinder 
	 * 
	 * @type Function
	 */
	this.reloadCallback;

	/**
	 * ID. Required to create unique cookie name
	 *
	 * @type String
	 **/
	this.id = id;

	/**
	 * Method to store/fetch data
	 *
	 * @type Function
	 **/
	this.storage = (function() {
		try {
			if ('localStorage' in window && window.localStorage !== null) {
				if (self.UA.Safari) {
					// check for Mac/iOS safari private browsing mode
					window.localStorage.setItem('elfstoragecheck', 1);
					window.localStorage.removeItem('elfstoragecheck');
				}
				return self.localStorage;
			} else {
				return self.cookie;
			}
		} catch (e) {
			return self.cookie;
		}
	})();

	/**
	 * Set pause page unload check function or Get state
	 *
	 * @param      Boolean   state   To set state
	 * @param      Boolean   keep    Keep disabled
	 * @return     Boolean|void
	 */
	this.pauseUnloadCheck = function(state, keep) {
		if (typeof state === 'undefined') {
			return diableUnloadCheck;
		} else {
			diableUnloadCheck = !!state;
			if (state && !keep) {
				requestAnimationFrame(function() {
					diableUnloadCheck = false;
				});
			}
		}
	};

	/**
	 * Configuration options
	 *
	 * @type Object
	 **/
	//this.options = jQuery.extend(true, {}, this._options, opts);
	this.options = Object.assign({}, this._options);
	
	// for old type configuration
	if (opts.uiOptions) {
		if (opts.uiOptions.toolbar && Array.isArray(opts.uiOptions.toolbar)) {
			if (jQuery.isPlainObject(opts.uiOptions.toolbar[opts.uiOptions.toolbar.length - 1])) {
				self.options.uiOptions.toolbarExtra = Object.assign(self.options.uiOptions.toolbarExtra || {}, opts.uiOptions.toolbar.pop());
			}
		}
	}
	
	// Overwrite if opts value is an array
	(function() {
		var arrOv = function(obj, base) {
			if (jQuery.isPlainObject(obj)) {
				jQuery.each(obj, function(k, v) {
					if (jQuery.isPlainObject(v)) {
						if (!base[k]) {
							base[k] = {};
						}
						arrOv(v, base[k]);
					} else {
						base[k] = v;
					}
				});
			}
		};
		arrOv(opts, self.options);
	})();
	
	// join toolbarExtra to toolbar
	this.options.uiOptions.toolbar.push(this.options.uiOptions.toolbarExtra);
	delete this.options.uiOptions.toolbarExtra;

	/**
	 * Arrays that has to unbind events
	 * 
	 * @type Object
	 */
	this.toUnbindEvents = {};
	
	/**
	 * Attach listener to events
	 * To bind to multiply events at once, separate events names by space
	 * 
	 * @param  String  event(s) name(s)
	 * @param  Object  event handler or {done: handler}
	 * @param  Boolean priority first
	 * @return elFinder
	 */
	this.bind = function(event, callback, priorityFirst) {
		var i, len;
		
		if (callback && (typeof callback === 'function' || typeof callback.done === 'function')) {
			event = ('' + event).toLowerCase().replace(/^\s+|\s+$/g, '').split(/\s+/);
			
			len = event.length;
			for (i = 0; i < len; i++) {
				if (listeners[event[i]] === void(0)) {
					listeners[event[i]] = [];
				}
				listeners[event[i]][priorityFirst? 'unshift' : 'push'](callback);
			}
		}
		return this;
	};
	
	/**
	 * Remove event listener if exists
	 * To un-bind to multiply events at once, separate events names by space
	 *
	 * @param  String    event(s) name(s)
	 * @param  Function  callback
	 * @return elFinder
	 */
	this.unbind = function(event, callback) {
		var i, len, l, ci;
		
		event = ('' + event).toLowerCase().split(/\s+/);
		
		len = event.length;
		for (i = 0; i < len; i++) {
			if (l = listeners[event[i]]) {
				ci = jQuery.inArray(callback, l);
				ci > -1 && l.splice(ci, 1);
			}
		}
		
		callback = null;
		return this;
	};
	
	/**
	 * Fire event - send notification to all event listeners
	 * In the callback `this` becames an event object
	 *
	 * @param  String   event type
	 * @param  Object   data to send across event
	 * @param  Boolean  allow modify data (call by reference of data) default: true
	 * @return elFinder
	 */
	this.trigger = function(evType, data, allowModify) {
		var type      = evType.toLowerCase(),
			isopen    = (type === 'open'),
			dataIsObj = (typeof data === 'object'),
			handlers  = listeners[type] || [],
			dones     = [],
			i, l, jst, event;
		
		this.debug('event-'+type, data);
		
		if (! dataIsObj || typeof allowModify === 'undefined') {
			allowModify = true;
		}
		if (l = handlers.length) {
			event = jQuery.Event(type);
			if (data) {
				data._getEvent = function() {
					return event;
				};
			}
			if (allowModify) {
				event.data = data;
			}

			for (i = 0; i < l; i++) {
				if (! handlers[i]) {
					// probably un-binded this handler
					continue;
				}

				// handler is jQuery.Deferred(), call all functions upon completion
				if (handlers[i].done) {
					dones.push(handlers[i].done);
					continue;
				}
				
				// set `event.data` only callback has argument
				if (handlers[i].length) {
					if (!allowModify) {
						// to avoid data modifications. remember about "sharing" passing arguments in js :) 
						if (typeof jst === 'undefined') {
							try {
								jst = JSON.stringify(data);
							} catch(e) {
								jst = false;
							}
						}
						event.data = jst? JSON.parse(jst) : data;
					}
				}

				try {
					if (handlers[i].call(event, event, this) === false || event.isDefaultPrevented()) {
						this.debug('event-stoped', event.type);
						break;
					}
				} catch (ex) {
					window.console && window.console.log && window.console.log(ex);
				}
				
			}
			
			// call done functions
			if (l = dones.length) {
				for (i = 0; i < l; i++) {
					try {
						if (dones[i].call(event, event, this) === false || event.isDefaultPrevented()) {
							this.debug('event-stoped', event.type + '(done)');
							break;
						}
					} catch (ex) {
						window.console && window.console.log && window.console.log(ex);
					}
				}
			}

			if (this.toUnbindEvents[type] && this.toUnbindEvents[type].length) {
				jQuery.each(this.toUnbindEvents[type], function(i, v) {
					self.unbind(v.type, v.callback);
				});
				delete this.toUnbindEvents[type];
			}
		}
		return this;
	};
	
	/**
	 * Get event listeners
	 *
	 * @param  String   event type
	 * @return Array    listed event functions
	 */
	this.getListeners = function(event) {
		return event? listeners[event.toLowerCase()] : listeners;
	};

	// set fm.baseUrl
	this.baseUrl = (function() {
		var myTag, base, baseUrl;
		
		if (self.options.baseUrl) {
			return self.options.baseUrl;
		} else {
			baseUrl = '';
			myTag = null;
			jQuery('head > script').each(function() {
				if (this.src && this.src.match(/js\/elfinder(?:-[a-z0-9_-]+)?\.(?:min|full)\.js(?:$|\?)/i)) {
					myTag = jQuery(this);
					return false;
				}
			});
			if (myTag) {
				baseUrl = myTag.attr('src').replace(/js\/[^\/]+$/, '');
				if (! baseUrl.match(/^(https?\/\/|\/)/)) {
					// check <base> tag
					if (base = jQuery('head > base[href]').attr('href')) {
						baseUrl = base.replace(/\/$/, '') + '/' + baseUrl; 
					}
				}
			}
			if (baseUrl !== '') {
				self.options.baseUrl = baseUrl;
			} else {
				if (! self.options.baseUrl) {
					self.options.baseUrl = './';
				}
				baseUrl = self.options.baseUrl;
			}
			return baseUrl;
		}
	})();
	
	this.i18nBaseUrl = (this.options.i18nBaseUrl || this.baseUrl + 'js/i18n').replace(/\/$/, '') + '/';
	this.workerBaseUrl = (this.options.workerBaseUrl || this.baseUrl + 'js/worker').replace(/\/$/, '') + '/';

	this.options.maxErrorDialogs = Math.max(1, parseInt(this.options.maxErrorDialogs || 5));

	// set dispInlineRegex
	cwdOptionsDefault.dispInlineRegex = this.options.dispInlineRegex;

	// auto load required CSS
	if (this.options.cssAutoLoad) {
		(function() {
			var baseUrl = self.baseUrl,
				myCss = jQuery('head > link[href$="css/elfinder.min.css"],link[href$="css/elfinder.full.css"]:first').length,
				rmTag = function() {
					if (node.data('cssautoloadHide')) {
						node.data('cssautoloadHide').remove();
						node.removeData('cssautoloadHide');
					}
				},
				loaded = function() {
					if (!self.cssloaded) {
						rmTag();
						self.cssloaded = true;
						self.trigger('cssloaded');
					}
				};
			
			if (! myCss) {
				// to request CSS auto loading
				self.cssloaded = null;
			}

			// additional CSS files
			if (Array.isArray(self.options.cssAutoLoad)) {
				if (!self.options.themes.default) {
					// set as default theme
					self.options.themes = Object.assign({
						'default' : {
							'name': 'default',
							'cssurls': self.options.cssAutoLoad
						}
					}, self.options.themes);
					if (!self.options.theme) {
						self.options.theme = 'default';
					}
				} else {
					if (self.cssloaded === true) {
						self.loadCss(self.options.cssAutoLoad);
					} else {
						self.bind('cssloaded', function() {
							self.loadCss(self.options.cssAutoLoad);
						});
					}
				}
			}

			// try to load main css
			if (self.cssloaded === null) {
				// hide elFinder node while css loading
				node.addClass('elfinder')
					.data('cssautoloadHide', jQuery('<style>.elfinder{visibility:hidden;overflow:hidden}</style>'));
				jQuery('head').append(node.data('cssautoloadHide'));

				// set default theme
				if (!self.options.themes.default) {
					self.options.themes = Object.assign({
						'default' : {
							'name': 'default',
							'cssurls': '',
							'author': 'elFinder Project',
							'license': '3-clauses BSD'
						}
					}, self.options.themes);
					if (!self.options.theme) {
						self.options.theme = 'default';
					}
				}

				// Delay 'visibility' check it required for browsers such as Safari
				requestAnimationFrame(function() {
					if (node.css('visibility') === 'hidden') {
						// load CSS
						self.loadCss([baseUrl+'css/elfinder.min.css'], {
							dfd: jQuery.Deferred().done(function() {
								loaded();
							}).fail(function() {
								rmTag();
								if (!self.cssloaded) {
									self.cssloaded = false;
									self.bind('init', function() {
										if (!self.cssloaded) {
											self.error(['errRead', 'CSS (elfinder.min)']);
										}
									});
								}
							})
						});
					} else {
						loaded();
					}
				});
			}
		})();
	}

	// load theme if exists
	(function() {
		var theme,
			themes = self.options.themes,
			ids = Object.keys(themes || {});
		if (ids.length) {
			theme = self.storage('theme') || self.options.theme;
			if (!themes[theme]) {
				theme = ids[0];
			}
			if (self.cssloaded) {
				self.changeTheme(theme);
			} else {
				self.bind('cssloaded', function() {
					self.changeTheme(theme);
				});
			}
		}
	})();
	
	/**
	 * Volume option to set the properties of the root Stat
	 * 
	 * @type Object
	 */
	this.optionProperties = {
		icon: void(0),
		csscls: void(0),
		tmbUrl: void(0),
		uiCmdMap: {},
		netkey: void(0),
		disabled: []
	};
	
	if (! inFrame && ! this.options.enableAlways && jQuery('body').children().length === 2) { // only node and beeper
		this.options.enableAlways = true;
	}
	
	// make options.debug
	if (this.options.debug === true) {
		this.options.debug = 'all';
	} else if (Array.isArray(this.options.debug)) {
		(function() {
			var d = {};
			jQuery.each(self.options.debug, function() {
				d[this] = true;
			});
			self.options.debug = d;
		})();
	} else {
		this.options.debug = false;
	}
	
	/**
	 * Original functions evacuated by conflict check
	 * 
	 * @type Object
	 */
	this.noConflicts = {};
	
	/**
	 * Check and save conflicts with bootstrap etc
	 * 
	 * @type Function
	 */
	this.noConflict = function() {
		jQuery.each(conflictChecks, function(i, p) {
			if (jQuery.fn[p] && typeof jQuery.fn[p].noConflict === 'function') {
				self.noConflicts[p] = jQuery.fn[p].noConflict();
			}
		});
	};
	// do check conflict
	this.noConflict();
	
	/**
	 * Is elFinder over CORS
	 *
	 * @type Boolean
	 **/
	this.isCORS = false;
	
	// configure for CORS
	(function(){
		if (typeof self.options.cors !== 'undefined' && self.options.cors !== null) {
			self.isCORS = self.options.cors? true : false;
		} else {
			var parseUrl = document.createElement('a'),
				parseUploadUrl,
				selfProtocol = window.location.protocol,
				portReg = function(protocol) {
					protocol = (!protocol || protocol === ':')? selfProtocol : protocol;
					return protocol === 'https:'? /\:443$/ : /\:80$/;
				},
				selfHost = window.location.host.replace(portReg(selfProtocol), '');
			parseUrl.href = opts.url;
			if (opts.urlUpload && (opts.urlUpload !== opts.url)) {
				parseUploadUrl = document.createElement('a');
				parseUploadUrl.href = opts.urlUpload;
			}
			if (selfHost !== parseUrl.host.replace(portReg(parseUrl.protocol), '')
				|| (parseUrl.protocol !== ':'&& parseUrl.protocol !== '' && (selfProtocol !== parseUrl.protocol))
				|| (parseUploadUrl && 
					(selfHost !== parseUploadUrl.host.replace(portReg(parseUploadUrl.protocol), '')
					|| (parseUploadUrl.protocol !== ':' && parseUploadUrl.protocol !== '' && (selfProtocol !== parseUploadUrl.protocol))
					)
				)
			) {
				self.isCORS = true;
			}
		}
		if (self.isCORS) {
			if (!jQuery.isPlainObject(self.options.customHeaders)) {
				self.options.customHeaders = {};
			}
			if (!jQuery.isPlainObject(self.options.xhrFields)) {
				self.options.xhrFields = {};
			}
			self.options.requestType = 'post';
			self.options.customHeaders['X-Requested-With'] = 'XMLHttpRequest';
			self.options.xhrFields['withCredentials'] = true;
		}
	})();

	/**
	 * Ajax request type
	 *
	 * @type String
	 * @default "get"
	 **/
	this.requestType = /^(get|post)$/i.test(this.options.requestType) ? this.options.requestType.toLowerCase() : 'get';
	
	// set `requestMaxConn` by option
	requestMaxConn = Math.max(parseInt(this.options.requestMaxConn), 1);
	
	/**
	 * Custom data that given as options
	 * 
	 * @type Object
	 * @default {}
	 */
	this.optsCustomData = jQuery.isPlainObject(this.options.customData) ? this.options.customData : {};

	/**
	 * Any data to send across every ajax request
	 *
	 * @type Object
	 * @default {}
	 **/
	this.customData = Object.assign({}, this.optsCustomData);

	/**
	 * Previous custom data from connector
	 * 
	 * @type Object|null
	 */
	this.prevCustomData = null;

	/**
	 * Any custom headers to send across every ajax request
	 *
	 * @type Object
	 * @default {}
	*/
	this.customHeaders = jQuery.isPlainObject(this.options.customHeaders) ? this.options.customHeaders : {};

	/**
	 * Any custom xhrFields to send across every ajax request
	 *
	 * @type Object
	 * @default {}
	 */
	this.xhrFields = jQuery.isPlainObject(this.options.xhrFields) ? this.options.xhrFields : {};

	/**
	 * Replace XMLHttpRequest.prototype.send to extended function for 3rd party libs XHR request etc.
	 * 
	 * @type Function
	 */
	this.replaceXhrSend = function() {
		if (! savedXhrSend) {
			savedXhrSend = XMLHttpRequest.prototype.send;
		}
		XMLHttpRequest.prototype.send = function() {
			var xhr = this;
			// set request headers
			if (self.customHeaders) {
				jQuery.each(self.customHeaders, function(key) {
					xhr.setRequestHeader(key, this);
				});
			}
			// set xhrFields
			if (self.xhrFields) {
				jQuery.each(self.xhrFields, function(key) {
					if (key in xhr) {
						xhr[key] = this;
					}
				});
			}
			return savedXhrSend.apply(this, arguments);
		};
	};
	
	/**
	 * Restore saved original XMLHttpRequest.prototype.send
	 * 
	 * @type Function
	 */
	this.restoreXhrSend = function() {
		savedXhrSend && (XMLHttpRequest.prototype.send = savedXhrSend);
	};

	/**
	 * command names for into queue for only cwd requests
	 * these commands aborts before `open` request
	 *
	 * @type Array
	 * @default ['tmb', 'parents']
	 */
	this.abortCmdsOnOpen = this.options.abortCmdsOnOpen || ['tmb', 'parents'];

	/**
	 * ui.nav id prefix
	 * 
	 * @type String
	 */
	this.navPrefix = 'nav' + (elFinder.prototype.uniqueid? elFinder.prototype.uniqueid : '') + '-';
	
	/**
	 * ui.cwd id prefix
	 * 
	 * @type String
	 */
	this.cwdPrefix = elFinder.prototype.uniqueid? ('cwd' + elFinder.prototype.uniqueid + '-') : '';
	
	// Increment elFinder.prototype.uniqueid
	++elFinder.prototype.uniqueid;
	
	/**
	 * URL to upload files
	 *
	 * @type String
	 **/
	this.uploadURL = opts.urlUpload || opts.url;
	
	/**
	 * Events namespace
	 *
	 * @type String
	 **/
	this.namespace = namespace;

	/**
	 * Today timestamp
	 *
	 * @type Number
	 **/
	this.today = (new Date(date.getFullYear(), date.getMonth(), date.getDate())).getTime()/1000;
	
	/**
	 * Yesterday timestamp
	 *
	 * @type Number
	 **/
	this.yesterday = this.today - 86400;
	
	utc = this.options.UTCDate ? 'UTC' : '';
	
	this.getHours    = 'get'+utc+'Hours';
	this.getMinutes  = 'get'+utc+'Minutes';
	this.getSeconds  = 'get'+utc+'Seconds';
	this.getDate     = 'get'+utc+'Date';
	this.getDay      = 'get'+utc+'Day';
	this.getMonth    = 'get'+utc+'Month';
	this.getFullYear = 'get'+utc+'FullYear';
	
	/**
	 * elFinder node z-index (auto detect on elFinder load)
	 *
	 * @type null | Number
	 **/
	this.zIndex;

	/**
	 * Current search status
	 * 
	 * @type Object
	 */
	this.searchStatus = {
		state  : 0, // 0: search ended, 1: search started, 2: in search result
		query  : '',
		target : '',
		mime   : '',
		mixed  : false, // in multi volumes search: false or Array that target volume ids
		ininc  : false // in incremental search
	};

	/**
	 * Interface language
	 *
	 * @type String
	 * @default "en"
	 **/
	this.lang = this.storage('lang') || this.options.lang;
	if (this.lang === 'jp') {
		this.lang = this.options.lang = 'ja';
	}

	this.viewType = this.storage('view') || this.options.defaultView || 'icons';

	this.sortType = this.storage('sortType') || this.options.sortType || 'name';
	
	this.sortOrder = this.storage('sortOrder') || this.options.sortOrder || 'asc';

	this.sortStickFolders = this.storage('sortStickFolders');
	if (this.sortStickFolders === null) {
		this.sortStickFolders = !!this.options.sortStickFolders;
	} else {
		this.sortStickFolders = !!this.sortStickFolders;
	}

	this.sortAlsoTreeview = this.storage('sortAlsoTreeview');
	if (this.sortAlsoTreeview === null || this.options.sortAlsoTreeview === null) {
		this.sortAlsoTreeview = !!this.options.sortAlsoTreeview;
	} else {
		this.sortAlsoTreeview = !!this.sortAlsoTreeview;
	}

	this.sortRules = jQuery.extend(true, {}, this._sortRules, this.options.sortRules);
	
	jQuery.each(this.sortRules, function(name, method) {
		if (typeof method != 'function') {
			delete self.sortRules[name];
		} 
	});
	
	this.compare = jQuery.proxy(this.compare, this);
	
	/**
	 * Delay in ms before open notification dialog
	 *
	 * @type Number
	 * @default 500
	 **/
	this.notifyDelay = this.options.notifyDelay > 0 ? parseInt(this.options.notifyDelay) : 500;
	
	/**
	 * Dragging UI Helper object
	 *
	 * @type jQuery | null
	 **/
	this.draggingUiHelper = null;
	
	/**
	 * Base droppable options
	 *
	 * @type Object
	 **/
	this.droppable = {
		greedy     : true,
		tolerance  : 'pointer',
		accept     : '.elfinder-cwd-file-wrapper,.elfinder-navbar-dir,.elfinder-cwd-file,.elfinder-cwd-filename',
		hoverClass : this.res('class', 'adroppable'),
		classes    : { // Deprecated hoverClass jQueryUI>=1.12.0
			'ui-droppable-hover': this.res('class', 'adroppable')
		},
		autoDisable: true, // elFinder original, see jquery.elfinder.js
		drop : function(e, ui) {
			var dst     = jQuery(this),
				targets = jQuery.grep(ui.helper.data('files')||[], function(h) { return h? true : false; }),
				result  = [],
				dups    = [],
				faults  = [],
				isCopy  = ui.helper.hasClass('elfinder-drag-helper-plus'),
				c       = 'class',
				cnt, hash, i, h;
			
			if (typeof e.button === 'undefined' || ui.helper.data('namespace') !== namespace || ! self.insideWorkzone(e.pageX, e.pageY)) {
				return false;
			}
			if (dst.hasClass(self.res(c, 'cwdfile'))) {
				hash = self.cwdId2Hash(dst.attr('id'));
			} else if (dst.hasClass(self.res(c, 'navdir'))) {
				hash = self.navId2Hash(dst.attr('id'));
			} else {
				hash = cwd;
			}

			cnt = targets.length;
			
			while (cnt--) {
				h = targets[cnt];
				// ignore drop into itself or in own location
				if (h != hash && files[h].phash != hash) {
					result.push(h);
				} else {
					((isCopy && h !== hash && files[hash].write)? dups : faults).push(h);
				}
			}
			
			if (faults.length) {
				return false;
			}
			
			ui.helper.data('droped', true);
			
			if (dups.length) {
				ui.helper.hide();
				self.exec('duplicate', dups, {_userAction: true});
			}
			
			if (result.length) {
				ui.helper.hide();
				self.clipboard(result, !isCopy);
				self.exec('paste', hash, {_userAction: true}, hash).always(function(){
					self.clipboard([]);
					self.trigger('unlockfiles', {files : targets});
				});
				self.trigger('drop', {files : targets});
			}
		}
	};
	
	/**
	 * Return true if filemanager is active
	 *
	 * @return Boolean
	 **/
	this.enabled = function() {
		return enabled && this.visible();
	};
	
	/**
	 * Return true if filemanager is visible
	 *
	 * @return Boolean
	 **/
	this.visible = function() {
		return node[0].elfinder && node.is(':visible');
	};
	
	/**
	 * Return file is root?
	 * 
	 * @param  Object  target file object
	 * @return Boolean
	 */
	this.isRoot = function(file) {
		return (file.isroot || ! file.phash)? true : false;
	};
	
	/**
	 * Return root dir hash for current working directory
	 * 
	 * @param  String   target hash
	 * @param  Boolean  include fake parent (optional)
	 * @return String
	 */
	this.root = function(hash, fake) {
		hash = hash || cwd;
		var dir, i;
		
		if (! fake) {
			jQuery.each(self.roots, function(id, rhash) {
				if (hash.indexOf(id) === 0) {
					dir = rhash;
					return false;
				}
			});
			if (dir) {
				return dir;
			}
		}
		
		dir = files[hash];
		while (dir && dir.phash && (fake || ! dir.isroot)) {
			dir = files[dir.phash];
		}
		if (dir) {
			return dir.hash;
		}
		
		while (i in files && files.hasOwnProperty(i)) {
			dir = files[i];
			if (dir.mime === 'directory' && !dir.phash && dir.read) {
				return dir.hash;
			}
		}
		
		return '';
	};
	
	/**
	 * Return current working directory info
	 * 
	 * @return Object
	 */
	this.cwd = function() {
		return files[cwd] || {};
	};
	
	/**
	 * Return required cwd option
	 * 
	 * @param  String  option name
	 * @param  String  target hash (optional)
	 * @return mixed
	 */
	this.option = function(name, target) {
		var res, item;
		target = target || cwd;
		if (self.optionsByHashes[target] && typeof self.optionsByHashes[target][name] !== 'undefined') {
			return self.optionsByHashes[target][name];
		}
		if (self.hasVolOptions && cwd !== target && (!(item = self.file(target)) || item.phash !== cwd)) {
			res = '';
			jQuery.each(self.volOptions, function(id, opt) {
				if (target.indexOf(id) === 0) {
					res = opt[name] || '';
					return false;
				}
			});
			return res;
		} else {
			return cwdOptions[name] || '';
		}
	};
	
	/**
	 * Return disabled commands by each folder
	 * 
	 * @param  Array  target hashes
	 * @return Array
	 */
	this.getDisabledCmds = function(targets, flip) {
		var disabled = {'hidden': true};
		if (! Array.isArray(targets)) {
			targets = [ targets ];
		}
		jQuery.each(targets, function(i, h) {
			var disCmds = self.option('disabledFlip', h);
			if (disCmds) {
				Object.assign(disabled, disCmds);
			}
		});
		return flip? disabled : Object.keys(disabled);
	};
	
	/**
	 * Return file data from current dir or tree by it's hash
	 * 
	 * @param  String  file hash
	 * @return Object
	 */
	this.file = function(hash, alsoHidden) { 
		return hash? (files[hash] || (alsoHidden? hiddenFiles[hash] : void(0))) : void(0); 
	};
	
	/**
	 * Return all cached files
	 * 
	 * @param  String  parent hash
	 * @return Object
	 */
	this.files = function(phash) {
		var items = {};
		if (phash) {
			if (!ownFiles[phash]) {
				return {};
			}
			jQuery.each(ownFiles[phash], function(h) {
				if (files[h]) {
					items[h] = files[h];
				} else {
					delete ownFiles[phash][h];
				}
			});
			return Object.assign({}, items);
		}
		return Object.assign({}, files);
	};
	
	/**
	 * Return list of file parents hashes include file hash
	 * 
	 * @param  String  file hash
	 * @return Array
	 */
	this.parents = function(hash) {
		var parents = [],
			dir;
		
		while (hash && (dir = this.file(hash))) {
			parents.unshift(dir.hash);
			hash = dir.phash;
		}
		return parents;
	};
	
	this.path2array = function(hash, i18) {
		var file, 
			path = [];
			
		while (hash) {
			if ((file = files[hash]) && file.hash) {
				path.unshift(i18 && file.i18 ? file.i18 : file.name);
				hash = file.isroot? null : file.phash;
			} else {
				path = [];
				break;
			}
		}
			
		return path;
	};
	
	/**
	 * Return file path or Get path async with jQuery.Deferred
	 * 
	 * @param  Object  file
	 * @param  Boolean i18
	 * @param  Object  asyncOpt
	 * @return String|jQuery.Deferred
	 */
	this.path = function(hash, i18, asyncOpt) { 
		var path = files[hash] && files[hash].path
			? files[hash].path
			: this.path2array(hash, i18).join(cwdOptions.separator);
		if (! asyncOpt || ! files[hash]) {
			return path;
		} else {
			asyncOpt = Object.assign({notify: {type : 'parents', cnt : 1, hideCnt : true}}, asyncOpt);
			
			var dfd    = jQuery.Deferred(),
				notify = asyncOpt.notify,
				noreq  = false,
				req    = function() {
					self.request({
						data : {cmd : 'parents', target : files[hash].phash},
						notify : notify,
						preventFail : true
					})
					.done(done)
					.fail(function() {
						dfd.reject();
					});
				},
				done   = function() {
					self.one('parentsdone', function() {
						path = self.path(hash, i18);
						if (path === '' && noreq) {
							//retry with request
							noreq = false;
							req();
						} else {
							if (notify) {
								clearTimeout(ntftm);
								notify.cnt = -(parseInt(notify.cnt || 0));
								self.notify(notify);
							}
							dfd.resolve(path);
						}
					});
				},
				ntftm;
		
			if (path) {
				return dfd.resolve(path);
			} else {
				if (self.ui['tree']) {
					// try as no request
					if (notify) {
						ntftm = setTimeout(function() {
							self.notify(notify);
						}, self.notifyDelay);
					}
					noreq = true;
					done(true);
				} else {
					req();
				}
				return dfd;
			}
		}
	};
	
	/**
	 * Return file url if set
	 * 
	 * @param  String  file hash
	 * @param  Object  Options
	 * @return String|Object of jQuery Deferred
	 */
	this.url = function(hash, o) {
		var file   = files[hash],
			opts   = o || {},
			async  = opts.async || false,
			temp   = opts.temporary || false,
			onetm  = (opts.onetime && self.option('onetimeUrl', hash)) || false,
			absurl = opts.absurl || false,
			dfrd   = (async || onetm)? jQuery.Deferred() : null,
			filter = function(url) {
				if (url && absurl) {
					url = self.convAbsUrl(url);
				}
				return url;
			},
			getUrl = function(url) {
				if (url) {
					return filter(url);
				}
				if (file.url) {
					return filter(file.url);
				}
				
				if (typeof baseUrl === 'undefined') {
					baseUrl = getBaseUrl();
				}
				
				if (baseUrl) {
					return filter(baseUrl + jQuery.map(self.path2array(hash), function(n) { return encodeURIComponent(n); }).slice(1).join('/'));
				}

				var params = Object.assign({}, self.customData, {
					cmd: 'file',
					target: file.hash
				});
				if (self.oldAPI) {
					params.cmd = 'open';
					params.current = file.phash;
				}
				return filter(self.options.url + (self.options.url.indexOf('?') === -1 ? '?' : '&') + jQuery.param(params, true));
			},
			getBaseUrl = function() {
				return self.option('url', (!self.isRoot(file) && file.phash) || file.hash);
			},
			baseUrl, res;
		
		if (!file || !file.read) {
			return async? dfrd.resolve('') : '';
		}
		
		if (onetm && (!file.url || file.url == '1') && !(baseUrl = getBaseUrl())) {
			async = true;
			this.request({
				data : { cmd : 'url', target : hash, options : { onetime: 1 } },
				preventDefault : true,
				options: {async: async},
				notify: {type : 'file', cnt : 1, hideCnt : true},
				progressBar: opts.progressBar
			}).done(function(data) {
				dfrd.resolve(filter(data.url || ''));
			}).fail(function() {
				dfrd.resolve('');
			});
		} else {
			if (file.url == '1' || (temp && !file.url && !(baseUrl = getBaseUrl()))) {
				this.request({
					data : { cmd : 'url', target : hash, options : { temporary: temp? 1 : 0 } },
					preventDefault : true,
					options: {async: async},
					notify: async? {type : temp? 'file' : 'url', cnt : 1, hideCnt : true} : {},
					progressBar: opts.progressBar
				})
				.done(function(data) {
					file.url = data.url || '';
				})
				.fail(function() {
					file.url = '';
				})
				.always(function() {
					var url;
					if (file.url && temp) {
						url = file.url;
						file.url = '1'; // restore
					}
					if (async) {
						dfrd.resolve(getUrl(url));
					} else {
						return getUrl(url);
					}
				});
			} else {
				if (async) {
					dfrd.resolve(getUrl());
				} else {
					return getUrl();
				}
			}
		}
		if (async) {
			return dfrd;
		}
	};
	
	/**
	 * Return file url for the extarnal service
	 *
	 * @param      String  hash     The hash
	 * @param      Object  options  The options
	 * @return     Object  jQuery Deferred
	 */
	this.forExternalUrl = function(hash, options) {
		var onetime = self.option('onetimeUrl', hash),
			opts = {
				async: true,
				absurl: true
			};

		opts[onetime? 'onetime' : 'temporary'] = true;
		return self.url(hash, Object.assign({}, options, opts));
	};

	/**
	 * Return file url for open in elFinder
	 * 
	 * @param  String  file hash
	 * @param  Boolean for download link
	 * @param      Object  requestOpts   The request options
	 * @return String
	 */
	this.openUrl = function(hash, download, callback, requestOpts) {
		var file = files[hash],
			url  = '',
			onetimeSize = (requestOpts || {}).onetimeSize || (5 * 1024 * 1024);
		
		if (!file || !file.read) {
			return '';
		}
		
		if (!download || download === 'sameorigin') {
			if (file.url) {
				if (file.url != 1) {
					url = file.url;
				}
			} else if (cwdOptions.url && file.hash.indexOf(self.cwd().volumeid) === 0) {
				url = cwdOptions.url + jQuery.map(this.path2array(hash), function(n) { return encodeURIComponent(n); }).slice(1).join('/');
			}
			if (!download || this.isSameOrigin(url)) {
				if (url) {
					url += (url.match(/\?/)? '&' : '?') + '_'.repeat((url.match(/[\?&](_+)t=/g) || ['&t=']).sort().shift().match(/[\?&](_*)t=/)[1].length + 1) + 't=' + (file.ts || parseInt(+new Date()/1000));
					if (callback) {
						callback(url);
						return;
					} else {
						return url;
					}
				}
			}
		}
		
		if (callback && this.hasParrotHeaders()) {
			if (!requestOpts) {
				requestOpts = {};
			} else {
				delete requestOpts.onetimeSize;
			}
			if (!requestOpts.onetime && !requestOpts.temporary && file.size > onetimeSize) {
				if (file.mime.match(/^video|audio/)) {
					requestOpts.temporary = true;
				} else {
					requestOpts.onetime = true;
				}
			}
			if (requestOpts.onetime || requestOpts.temporary) {
				return this.url(file.hash, Object.assign({
					async: true
				}, requestOpts)).done(function(url) {
					callback(url);
				}).fail(function() {
					callback('');
				});
			} else {
				return this.getContents(hash, 'blob', requestOpts).done(function(blob){
					url = (window.URL || window.webkitURL).createObjectURL(blob);
					callback(url);
				}).fail(function() {
					callback('');
				});
			}
		} else {
			url = this.options.url;
			url = url + (url.indexOf('?') === -1 ? '?' : '&')
				+ (this.oldAPI ? 'cmd=open&current='+file.phash : 'cmd=file')
				+ '&target=' + file.hash
				+ '&_t=' + (file.ts || parseInt(+new Date()/1000));
			
			if (download === true) {
				url += '&download=1';
			}
			
			jQuery.each(this.customData, function(key, val) {
				url += '&' + encodeURIComponent(key) + '=' + encodeURIComponent(val);
			});
			if (callback) {
				callback(url);
				return;
			} else {
				return url;
			}
		}
	};
	
	/**
	 * Return thumbnail url
	 * 
	 * @param  Object  file object
	 * @return String
	 */
	this.tmb = function(file) {
		var tmbUrl, tmbCrop,
			cls    = 'elfinder-cwd-bgurl',
			url    = '',
			cData  = {},
			n      = 0;

		if (jQuery.isPlainObject(file)) {
			if (self.searchStatus.state && file.hash.indexOf(self.cwd().volumeid) !== 0) {
				tmbUrl = self.option('tmbUrl', file.hash);
				tmbCrop = self.option('tmbCrop', file.hash);
			} else {
				tmbUrl = cwdOptions.tmbUrl;
				tmbCrop = cwdOptions.tmbCrop;
			}
			if (tmbCrop) {
				cls += ' elfinder-cwd-bgurl-crop';
			}
			if (tmbUrl === 'self' && file.mime.indexOf('image/') === 0) {
				url = self.openUrl(file.hash);
				cls += ' elfinder-cwd-bgself';
			} else if ((self.oldAPI || tmbUrl) && file && file.tmb && file.tmb != 1) {
				url = tmbUrl + file.tmb;
			} else if (self.newAPI && file && file.tmb && file.tmb != 1) {
				url = file.tmb;
			}
			if (url) {
				if (tmbUrl !== 'self') {
					if (file.ts) {
						cData._t = file.ts;
					}
					if (cwdOptions.tmbReqCustomData && Object.keys(this.customData).length) {
						cData = Object.assign(cData, this.customData);
					}
					if (Object.keys(cData).length) {
						url += (url.match(/\?/) ? '&' : '?');
						jQuery.each(cData, function (key, val) {
							url += ((n++ === 0)? '' : '&') + encodeURIComponent(key) + '=' + encodeURIComponent(val);
						});
					}
				}
				return { url: url, className: cls };
			}
		}
		
		return false;
	};
	
	/**
	 * Return selected files hashes
	 *
	 * @return Array
	 **/
	this.selected = function() {
		return selected.slice(0);
	};
	
	/**
	 * Return selected files info
	 * 
	 * @return Array
	 */
	this.selectedFiles = function() {
		return jQuery.map(selected, function(hash) { return files[hash] ? Object.assign({}, files[hash]) : null; });
	};
	
	/**
	 * Return true if file with required name existsin required folder
	 * 
	 * @param  String  file name
	 * @param  String  parent folder hash
	 * @return Boolean
	 */
	this.fileByName = function(name, phash) {
		var hash;
	
		for (hash in files) {
			if (files.hasOwnProperty(hash) && files[hash].phash == phash && files[hash].name == name) {
				return files[hash];
			}
		}
	};
	
	/**
	 * Valid data for required command based on rules
	 * 
	 * @param  String  command name
	 * @param  Object  cammand's data
	 * @return Boolean
	 */
	this.validResponse = function(cmd, data) {
		return data.error || this.rules[this.rules[cmd] ? cmd : 'defaults'](data);
	};
	
	/**
	 * Return bytes from ini formated size
	 * 
	 * @param  String  ini formated size
	 * @return Integer
	 */
	this.returnBytes = function(val) {
		var last;
		if (isNaN(val)) {
			if (! val) {
				val = '';
			}
			// for ex. 1mb, 1KB
			val = val.replace(/b$/i, '');
			last = val.charAt(val.length - 1).toLowerCase();
			val = val.replace(/[tgmk]$/i, '');
			if (last == 't') {
				val = val * 1024 * 1024 * 1024 * 1024;
			} else if (last == 'g') {
				val = val * 1024 * 1024 * 1024;
			} else if (last == 'm') {
				val = val * 1024 * 1024;
			} else if (last == 'k') {
				val = val * 1024;
			}
			val = isNaN(val)? 0 : parseInt(val);
		} else {
			val = parseInt(val);
			if (val < 1) val = 0;
		}
		return val;
	};
	
	/**
	 * Process ajax request.
	 * Fired events :
	 * @todo
	 * @example
	 * @todo
	 * @return jQuery.Deferred
	 */
	this.request = function(opts) { 
		var self     = this,
			o        = this.options,
			dfrd     = jQuery.Deferred(),
			// request ID
			reqId    = (+ new Date()).toString(16) + Math.floor(1000 * Math.random()).toString(16), 
			// request data
			data     = Object.assign({}, self.customData, {mimes : o.onlyMimes}, opts.data || opts),
			// command name
			cmd      = data.cmd,
			// request type is binary
			isBinary = (opts.options || {}).dataType === 'binary',
			// current cmd is "open"
			isOpen   = (!opts.asNotOpen && cmd === 'open'),
			// call default fail callback (display error dialog) ?
			deffail  = !(isBinary || opts.preventDefault || opts.preventFail),
			// call default success callback ?
			defdone  = !(isBinary || opts.preventDefault || opts.preventDone),
			// current progress of receive data
			prog     = opts.progressVal || 20,
			// timer of fake progress
			progTm   = null,
			// whether the notification dialog is currently displayed
			hasNotify= false,
			// options for notify dialog
			notify   = !opts.progressBar? (opts.notify? Object.assign({progress: prog * opts.notify.cnt}, opts.notify) : {}) : {},
			// make cancel button
			cancel   = !!opts.cancel,
			// do not normalize data - return as is
			raw      = isBinary || !!opts.raw,
			// sync files on request fail
			syncOnFail = opts.syncOnFail,
			// use lazy()
			lazy     = !!opts.lazy,
			// prepare function before done()
			prepare  = opts.prepare,
			// navigate option object when cmd done
			navigate = opts.navigate,
			// open notify dialog timeout
			timeout,
			// use browser cache
			useCache = (opts.options || {}).cache,
			// request options
			options = Object.assign({
				url      : o.url,
				async    : true,
				type     : this.requestType,
				dataType : 'json',
				cache    : (self.api >= 2.1029), // api >= 2.1029 has unique request ID
				data     : data,
				headers  : this.customHeaders,
				xhrFields: this.xhrFields,
				progress : function(e) {
					var p = e.loaded / e.total * 100;
					progTm && clearTimeout(progTm);
					if (opts.progressBar) {
						try {
							opts.progressBar.width(p + '%');
						} catch(e) {}
					} else {
						if (hasNotify && notify.type) {
							p = p * notify.cnt;
							if (prog < p) {
								self.notify({
									type: notify.type,
									progress: p - prog,
									cnt: 0,
									hideCnt: notify.hideCnt
								});
								prog = p;
							}
						}
					}
					if (opts.progress) {
						try {
							opts.progress(e);
						} catch(e) {}
					}
				}
			}, opts.options || {}),
			/**
			 * Default success handler. 
			 * Call default data handlers and fire event with command name.
			 *
			 * @param Object  normalized response data
			 * @return void
			 **/
			done = function(data) {
				data.warning && self.error(data.warning);
				
				if (isOpen) {
					open(data);
				} else {
					self.updateCache(data);
				}
				
				self.lazy(function() {
					// fire some event to update cache/ui
					data.removed && data.removed.length && self.remove(data);
					data.added   && data.added.length   && self.add(data);
					data.changed && data.changed.length && self.change(data);
				}).then(function() {
					// fire event with command name
					return self.lazy(function() {
						self.trigger(cmd, data, false);
					});
				}).then(function() {
					// fire event with command name + 'done'
					return self.lazy(function() {
						self.trigger(cmd + 'done');
					});
				}).then(function() {
					// make toast message
					if (data.toasts && Array.isArray(data.toasts)) {
						jQuery.each(data.toasts, function() {
							this.msg && self.toast(this);
						});
					}
					// force update content
					data.sync && self.sync();
				});
			},
			/**
			 * Request error handler. Reject dfrd with correct error message.
			 *
			 * @param jqxhr  request object
			 * @param String request status
			 * @return void
			 **/
			error = function(xhr, status) {
				var error, data, 
					d = self.options.debug;
				
				switch (status) {
					case 'abort':
						error = xhr.quiet ? '' : ['errConnect', 'errAbort'];
						break;
					case 'timeout':	    
						error = ['errConnect', 'errTimeout'];
						break;
					case 'parsererror': 
						error = ['errResponse', 'errDataNotJSON'];
						if (xhr.responseText) {
							if (! cwd || (d && (d === 'all' || d['backend-error']))) {
								error.push(xhr.responseText);
							}
						}
						break;
					default:
						if (xhr.responseText) {
							// check responseText, Is that JSON?
							try {
								data = JSON.parse(xhr.responseText);
								if (data && data.error) {
									error = data.error;
								}
							} catch(e) {}
						}
						if (! error) {
							if (xhr.status == 403) {
								error = ['errConnect', 'errAccess', 'HTTP error ' + xhr.status];
							} else if (xhr.status == 404) {
								error = ['errConnect', 'errNotFound', 'HTTP error ' + xhr.status];
							} else if (xhr.status >= 500) {
								error = ['errResponse', 'errServerError', 'HTTP error ' + xhr.status];
							} else {
								if (xhr.status == 414 && options.type === 'get') {
									// retry by POST method
									options.type = 'post';
									self.abortXHR(xhr);
									dfrd.xhr = xhr = self.transport.send(options).fail(error).done(success);
									return;
								}
								error = xhr.quiet ? '' : ['errConnect', 'HTTP error ' + xhr.status];
							} 
						}
				}
				
				self.trigger(cmd + 'done');
				dfrd.reject({error: error}, xhr, status);
			},
			/**
			 * Request success handler. Valid response data and reject/resolve dfrd.
			 *
			 * @param Object  response data
			 * @param String request status
			 * @return void
			 **/
			success = function(response) {
				// Set currrent request command name
				self.currentReqCmd = cmd;
				
				response.debug && self.responseDebug(response);
				
				self.setCustomHeaderByXhr(xhr);

				if (raw) {
					self.abortXHR(xhr);
					response && response.debug && self.debug('backend-debug', response);
					return dfrd.resolve(response);
				}
				
				if (!response) {
					return dfrd.reject({error :['errResponse', 'errDataEmpty']}, xhr, response);
				} else if (!jQuery.isPlainObject(response)) {
					return dfrd.reject({error :['errResponse', 'errDataNotJSON']}, xhr, response);
				} else if (response.error) {
					if (isOpen) {
						// check leafRoots
						jQuery.each(self.leafRoots, function(phash, roots) {
							self.leafRoots[phash] = jQuery.grep(roots, function(h) { return h !== data.target; });
						});
					}
					return dfrd.reject({error :response.error}, xhr, response);
				}
				
				var resolve = function() {
					var pushLeafRoots = function(name) {
						if (self.leafRoots[data.target] && response[name]) {
							jQuery.each(self.leafRoots[data.target], function(i, h) {
								var root;
								if (root = self.file(h)) {
									response[name].push(root);
								}
							});
						}
					},
					setTextMimes = function() {
						self.textMimes = {};
						jQuery.each(self.res('mimes', 'text'), function() {
							self.textMimes[this.toLowerCase()] = true;
						});
					},
					actionTarget;
					
					if (isOpen) {
						pushLeafRoots('files');
					} else if (cmd === 'tree') {
						pushLeafRoots('tree');
					}
					
					response = self.normalize(response);
					
					if (!self.validResponse(cmd, response)) {
						return dfrd.reject({error :(response.norError || 'errResponse')}, xhr, response);
					}
					
					if (isOpen) {
						if (!self.api) {
							self.api    = response.api || 1;
							if (self.api == '2.0' && typeof response.options.uploadMaxSize !== 'undefined') {
								self.api = '2.1';
							}
							self.newAPI = self.api >= 2;
							self.oldAPI = !self.newAPI;
						}
						
						if (response.textMimes && Array.isArray(response.textMimes)) {
							self.resources.mimes.text = response.textMimes;
							setTextMimes();
						}
						!self.textMimes && setTextMimes();
						
						if (response.options) {
							cwdOptions = Object.assign({}, cwdOptionsDefault, response.options);
						}

						if (response.netDrivers) {
							self.netDrivers = response.netDrivers;
						}

						if (response.maxTargets) {
							self.maxTargets = response.maxTargets;
						}

						if (!!data.init) {
							self.uplMaxSize = self.returnBytes(response.uplMaxSize);
							self.uplMaxFile = !!response.uplMaxFile? Math.min(parseInt(response.uplMaxFile), 50) : 20;
						}
					}

					if (typeof prepare === 'function') {
						prepare(response);
					}
					
					if (navigate) {
						actionTarget = navigate.target || 'added';
						if (response[actionTarget] && response[actionTarget].length) {
							self.one(cmd + 'done', function() {
								var targets  = response[actionTarget],
									newItems = self.findCwdNodes(targets),
									inCwdHashes = function() {
										var cwdHash = self.cwd().hash;
										return jQuery.map(targets, function(f) { return (f.phash && cwdHash === f.phash)? f.hash : null; });
									},
									hashes   = inCwdHashes(),
									makeToast  = function(t) {
										var node = void(0),
											data = t.action? t.action.data : void(0),
											cmd, msg, done;
										if ((data || hashes.length) && t.action && (msg = t.action.msg) && (cmd = t.action.cmd) && (!t.action.cwdNot || t.action.cwdNot !== self.cwd().hash)) {
											done = t.action.done;
											data = t.action.data;
											node = jQuery('<div></div>')
												.append(
													jQuery('<button type="button" class="ui-button ui-widget ui-state-default ui-corner-all elfinder-tabstop"><span class="ui-button-text">'
														+self.i18n(msg)
														+'</span></button>')
													.on('mouseenter mouseleave', function(e) { 
														jQuery(this).toggleClass('ui-state-hover', e.type == 'mouseenter');
													})
													.on('click', function() {
														self.exec(cmd, data || hashes, {_userAction: true, _currentType: 'toast', _currentNode: jQuery(this) });
														if (done) {
															self.one(cmd+'done', function() {
																if (typeof done === 'function') {
																	done();
																} else if (done === 'select') {
																	self.trigger('selectfiles', {files : inCwdHashes()});
																}
															});
														}
													})
												);
										}
										delete t.action;
										t.extNode = node;
										return t;
									};
								
								if (! navigate.toast) {
									navigate.toast = {};
								}
								
								!navigate.noselect && self.trigger('selectfiles', {files : self.searchStatus.state > 1 ? jQuery.map(targets, function(f) { return f.hash; }) : hashes});
								
								if (newItems.length) {
									if (!navigate.noscroll) {
										newItems.first().trigger('scrolltoview', {blink : false});
										self.resources.blink(newItems, 'lookme');
									}
									if (jQuery.isPlainObject(navigate.toast.incwd)) {
										self.toast(makeToast(navigate.toast.incwd));
									}
								} else {
									if (jQuery.isPlainObject(navigate.toast.inbuffer)) {
										self.toast(makeToast(navigate.toast.inbuffer));
									}
								}
							});
						}
					}
					
					dfrd.resolve(response);
					
					response.debug && self.debug('backend-debug', response);
				};
				self.abortXHR(xhr);
				lazy? self.lazy(resolve) : resolve();
			},
			xhr, _xhr,
			xhrAbort = function(e) {
				if (xhr && xhr.state() === 'pending') {
					self.abortXHR(xhr, { quiet: true , abort: true });
					if (!e || (e.type !== 'unload' && e.type !== 'destroy')) {
						self.autoSync();
					}
				}
			},
			abort = function(e){
				self.trigger(cmd + 'done');
				if (e.type == 'autosync') {
					if (e.data.action != 'stop') return;
				} else if (e.type != 'unload' && e.type != 'destroy' && e.type != 'openxhrabort') {
					if (!e.data.added || !e.data.added.length) {
						return;
					}
				}
				xhrAbort(e);
			},
			request = function(mode) {
				var queueAbort = function() {
					syncOnFail = false;
					dfrd.reject();
				};
				
				if (mode) {
					if (mode === 'cmd') {
						return cmd;
					}
				}
				
				if (isOpen) {
					if (currentOpenCmd && currentOpenCmd.state() === 'pending') {
						if (currentOpenCmd._target === data.target) {
							return dfrd.reject('openabort');
						} else {
							if (currentOpenCmd.xhr) {
								currentOpenCmd.xhr.queueAbort();
							} else {
								currentOpenCmd.reject('openabort');
							}
						}
					}
					currentOpenCmd = dfrd;
					currentOpenCmd._target = data.target;
				}
				
				dfrd.always(function() {
					delete options.headers['X-elFinderReqid'];
					if (isOpen) {
						currentOpenCmd = null;
					}
				}).fail(function(error, xhr, response) {
					var errData, errMsg;

					if (isOpen && error === 'openabort') {
						error = '';
						syncOnFail = false;
					}

					errData = {
						cmd: cmd,
						err: error,
						xhr: xhr,
						rc: response
					};

					// unset this cmd queue when user canceling
					// see notify : function - `cancel.reject(0);`
					if (error === 0) {
						if (requestQueue.length) {
							requestQueue = jQuery.grep(requestQueue, function(req) {
								return (req('cmd') === cmd) ? false : true;
							});
						}
					}
					// trigger "requestError" event
					self.trigger('requestError', errData);
					if (errData._getEvent && errData._getEvent().isDefaultPrevented()) {
						deffail = false;
						syncOnFail = false;
						if (error) {
							error.error = '';
						}
					}
					// abort xhr
					xhrAbort();
					if (isOpen) {
						openDir = self.file(data.target);
						openDir && openDir.volumeid && self.isRoot(openDir) && delete self.volumeExpires[openDir.volumeid];
					}
					self.trigger(cmd + 'fail', response);
					errMsg = (typeof error === 'object')? error.error : error;
					if (errMsg) {
						deffail ? self.error(errMsg) : self.debug('error', self.i18n(errMsg));
					}
					syncOnFail && self.sync();
				});

				if (!cmd) {
					syncOnFail = false;
					return dfrd.reject({error :'errCmdReq'});
				}
				
				if (self.maxTargets && data.targets && data.targets.length > self.maxTargets) {
					syncOnFail = false;
					return dfrd.reject({error :['errMaxTargets', self.maxTargets]});
				}

				defdone && dfrd.done(done);
				
				// quiet abort not completed "open" requests
				if (isOpen) {
					while ((_xhr = queue.pop())) {
						_xhr.queueAbort();
					}
					if (cwd !== data.target) {
						while ((_xhr = cwdQueue.pop())) {
							_xhr.queueAbort();
						}
					}
				}

				// trigger abort autoSync for commands to add the item
				if (jQuery.inArray(cmd, (self.cmdsToAdd + ' autosync').split(' ')) !== -1) {
					if (cmd !== 'autosync') {
						self.autoSync('stop');
						dfrd.always(function() {
							self.autoSync();
						});
					}
					self.trigger('openxhrabort');
				}

				delete options.preventFail;

				if (self.api >= 2.1029) {
					if (useCache) {
						options.headers['X-elFinderReqid'] = reqId;
					} else {
						Object.assign(options.data, { reqid : reqId });
					}
				}
				
				// function for set value of this syncOnFail
				dfrd.syncOnFail = function(state) {
					syncOnFail = !!state;
				};

				requestCnt++;

				dfrd.xhr = xhr = self.transport.send(options).always(function() {
					// set responseURL from native xhr object
					if (options._xhr && typeof options._xhr.responseURL !== 'undefined') {
						xhr.responseURL = options._xhr.responseURL || '';
					}
					--requestCnt;
					if (requestQueue.length) {
						requestQueue.shift()();
					}
				}).fail(error).done(success);
				
				if (self.api >= 2.1029) {
					xhr._requestId = reqId;
				}
				
				if (isOpen || (data.compare && cmd === 'info')) {
					// regist function queueAbort
					xhr.queueAbort = queueAbort;
					// add autoSync xhr into queue
					queue.unshift(xhr);
					// bind abort()
					data.compare && self.bind(self.cmdsToAdd + ' autosync openxhrabort', abort);
					dfrd.always(function() {
						var ndx = jQuery.inArray(xhr, queue);
						data.compare && self.unbind(self.cmdsToAdd + ' autosync openxhrabort', abort);
						ndx !== -1 && queue.splice(ndx, 1);
					});
				} else if (jQuery.inArray(cmd, self.abortCmdsOnOpen) !== -1) {
					// regist function queueAbort
					xhr.queueAbort = queueAbort;
					// add "open" xhr, only cwd xhr into queue
					cwdQueue.unshift(xhr);
					dfrd.always(function() {
						var ndx = jQuery.inArray(xhr, cwdQueue);
						ndx !== -1 && cwdQueue.splice(ndx, 1);
					});
				}
				
				// abort pending xhr on window unload or elFinder destroy
				self.bind('unload destroy', abort);
				dfrd.always(function() {
					self.unbind('unload destroy', abort);
				});
				
				return dfrd;
			},
			queueingRequest = function() {
				// show notify
				if (notify.type && notify.cnt) {
					if (cancel) {
						notify.cancel = dfrd;
						opts.eachCancel && (notify.id = +new Date());
					}
					timeout = setTimeout(function() {
						// start fake count up
						progTm = setTimeout(progFakeUp, 1000);
						self.notify(notify);
						hasNotify = true;
						dfrd.always(function() {
							notify.cnt = -(parseInt(notify.cnt)||0);
							self.notify(notify);
							hasNotify = false;
						});
					}, self.notifyDelay);
					
					dfrd.always(function() {
						clearTimeout(timeout);
					});
				}
				// queueing
				if (requestCnt < requestMaxConn) {
					// do request
					return request();
				} else {
					if (isOpen) {
						requestQueue.unshift(request);
					} else {
						requestQueue.push(request);
					}
					return dfrd;
				}
			},
			progFakeUp = function() {
				var add;
				if (hasNotify && progTm) {
					add = 1 * notify.cnt;
					progTm = null;
					self.notify({
						type: notify.type,
						progress: add,
						cnt: 0,
						hideCnt: notify.hideCnt
					});
					prog += add;
					if ((prog / notify.cnt) < 80) {
						progTm = setTimeout(progFakeUp, 500);
					}
				}
			},
			bindData = {opts: opts, result: true},
			openDir;
		
		// prevent request initial request is completed
		if (!self.api && !data.init) {
			syncOnFail = false;
			return dfrd.reject();
		}

		// trigger "request.cmd" that callback be able to cancel request by substituting "false" for "event.data.result"
		self.trigger('request.' + cmd, bindData, true);
		
		if (! bindData.result) {
			self.trigger(cmd + 'done');
			return dfrd.reject();
		} else if (typeof bindData.result === 'object' && bindData.result.promise) {
			bindData.result
				.done(queueingRequest)
				.fail(function() {
					self.trigger(cmd + 'done');
					dfrd.reject();
				});
			return dfrd;
		}
		
		return queueingRequest();
	};
	
	/**
	 * Call cache()
	 * Store info about files/dirs in "files" object.
	 *
	 * @param  Array  files
	 * @param  String type
	 * @return void
	 */
	this.cache = function(dataArray, type) {
		if (! Array.isArray(dataArray)) {
			dataArray = [ dataArray ];
		}
		cache(dataArray, type);
	};
	
	/**
	 * Update file object caches by respose data object
	 * 
	 * @param  Object  respose data object
	 * @return void
	 */
	this.updateCache = function(data) {
		if (jQuery.isPlainObject(data)) {
			data.files && data.files.length && cache(data.files, 'files');
			data.tree && data.tree.length && cache(data.tree, 'tree');
			data.removed && data.removed.length && remove(data.removed);
			data.added && data.added.length && cache(data.added, 'add');
			data.changed && data.changed.length && cache(data.changed, 'change');
		}
	};
	
	/**
	 * Compare current files cache with new files and return diff
	 * 
	 * @param  Array   new files
	 * @param  String  target folder hash
	 * @param  Array   exclude properties to compare
	 * @return Object
	 */
	this.diff = function(incoming, onlydir, excludeProps) {
		var raw       = {},
			added     = [],
			removed   = [],
			changed   = [],
			excludes  = null,
			isChanged = function(hash) {
				var l = changed.length;

				while (l--) {
					if (changed[l].hash == hash) {
						return true;
					}
				}
			};
		
		jQuery.each(incoming, function(i, f) {
			raw[f.hash] = f;
		});
		
		// make excludes object
		if (excludeProps && excludeProps.length) {
			excludes = {};
			jQuery.each(excludeProps, function() {
				excludes[this] = true;
			});
		}
		
		// find removed
		jQuery.each(files, function(hash, f) {
			if (! raw[hash] && (! onlydir || f.phash === onlydir)) {
				removed.push(hash);
			}
		});
		
		// compare files
		jQuery.each(raw, function(hash, file) {
			var origin  = files[hash],
				orgKeys = {},
				chkKeyLen;

			if (!origin) {
				added.push(file);
			} else {
				// make orgKeys object
				jQuery.each(Object.keys(origin), function() {
					orgKeys[this] = true;
				});
				jQuery.each(file, function(prop) {
					delete orgKeys[prop];
					if (! excludes || ! excludes[prop]) {
						if (file[prop] !== origin[prop]) {
							changed.push(file);
							orgKeys = {};
							return false;
						}
					}
				});
				chkKeyLen = Object.keys(orgKeys).length;
				if (chkKeyLen !== 0) {
					if (excludes) {
						jQuery.each(orgKeys, function(prop) {
							if (excludes[prop]) {
								--chkKeyLen;
							}
						});
					}
					(chkKeyLen !== 0) && changed.push(file);
				}
			}
		});
		
		// parents of removed dirs mark as changed (required for tree correct work)
		jQuery.each(removed, function(i, hash) {
			var file  = files[hash], 
				phash = file.phash;

			if (phash 
			&& file.mime == 'directory' 
			&& jQuery.inArray(phash, removed) === -1 
			&& raw[phash] 
			&& !isChanged(phash)) {
				changed.push(raw[phash]);
			}
		});
		
		return {
			added   : added,
			removed : removed,
			changed : changed
		};
	};
	
	/**
	 * Sync content
	 * 
	 * @return jQuery.Deferred
	 */
	this.sync = function(onlydir, polling) {
		this.autoSync('stop');
		var self  = this,
			compare = function(){
				var c = '', cnt = 0, mtime = 0;
				if (onlydir && polling) {
					jQuery.each(files, function(h, f) {
						if (f.phash && f.phash === onlydir) {
							++cnt;
							mtime = Math.max(mtime, f.ts);
						}
						c = cnt+':'+mtime;
					});
				}
				return c;
			},
			comp  = compare(),
			dfrd  = jQuery.Deferred().always(function() { !reqFail && self.trigger('sync'); }),
			opts = [this.request({
				data           : {cmd : 'open', reload : 1, target : cwd, tree : (! onlydir && this.ui.tree) ? 1 : 0, compare : comp},
				preventDefault : true
			})],
			exParents = function() {
				var parents = [],
					curRoot = self.file(self.root(cwd)),
					curId = curRoot? curRoot.volumeid : null,
					phash = self.cwd().phash,
					isroot,pdir;
				
				while(phash) {
					if (pdir = self.file(phash)) {
						if (phash.indexOf(curId) !== 0) {
							parents.push( {target: phash, cmd: 'tree'} );
							if (! self.isRoot(pdir)) {
								parents.push( {target: phash, cmd: 'parents'} );
							}
							curRoot = self.file(self.root(phash));
							curId = curRoot? curRoot.volumeid : null;
						}
						phash = pdir.phash;
					} else {
						phash = null;
					}
				}
				return parents;
			},
			reqFail;
		
		if (! onlydir && self.api >= 2) {
			(cwd !== this.root()) && opts.push(this.request({
				data           : {cmd : 'parents', target : cwd},
				preventDefault : true
			}));
			jQuery.each(exParents(), function(i, data) {
				opts.push(self.request({
					data           : {cmd : data.cmd, target : data.target},
					preventDefault : true
				}));
			});
		}
		jQuery.when.apply($, opts)
		.fail(function(error, xhr) {
			reqFail = (xhr && xhr.status != 200);
			if (! polling || jQuery.inArray('errOpen', error) !== -1) {
				dfrd.reject(error);
				self.parseError(error) && self.request({
					data   : {cmd : 'open', target : (self.lastDir('') || self.root()), tree : 1, init : 1},
					notify : {type : 'open', cnt : 1, hideCnt : true}
				});
			} else {
				dfrd.reject((error && xhr.status != 0)? error : void 0);
			}
		})
		.done(function(odata) {
			var pdata, argLen, i;
			
			if (odata.cwd.compare) {
				if (comp === odata.cwd.compare) {
					return dfrd.reject();
				}
			}
			
			// for 2nd and more requests
			pdata = {tree : []};
			
			// results marge of 2nd and more requests
			argLen = arguments.length;
			if (argLen > 1) {
				for(i = 1; i < argLen; i++) {
					if (arguments[i].tree && arguments[i].tree.length) {
						pdata.tree.push.apply(pdata.tree, arguments[i].tree);
					}
				}
			}
			
			if (self.api < 2.1) {
				if (! pdata.tree) {
					pdata.tree = [];
				}
				pdata.tree.push(odata.cwd);
			}
			
			// data normalize
			odata = self.normalize(odata);
			if (!self.validResponse('open', odata)) {
				return dfrd.reject((odata.norError || 'errResponse'));
			}
			pdata = self.normalize(pdata);
			if (!self.validResponse('tree', pdata)) {
				return dfrd.reject((pdata.norError || 'errResponse'));
			}
			
			var diff = self.diff(odata.files.concat(pdata && pdata.tree ? pdata.tree : []), onlydir);

			diff.added.push(odata.cwd);
			
			self.updateCache(diff);
			
			// trigger events
			diff.removed.length && self.remove(diff);
			diff.added.length   && self.add(diff);
			diff.changed.length && self.change(diff);
			return dfrd.resolve(diff);
		})
		.always(function() {
			self.autoSync();
		});
		
		return dfrd;
	};
	
	this.upload = function(files) {
		return this.transport.upload(files, this);
	};
	
	/**
	 * Bind keybord shortcut to keydown event
	 *
	 * @example
	 *    elfinder.shortcut({ 
	 *       pattern : 'ctrl+a', 
	 *       description : 'Select all files', 
	 *       callback : function(e) { ... }, 
	 *       keypress : true|false (bind to keypress instead of keydown) 
	 *    })
	 *
	 * @param  Object  shortcut config
	 * @return elFinder
	 */
	this.shortcut = function(s) {
		var patterns, pattern, code, i, parts;
		
		if (this.options.allowShortcuts && s.pattern && jQuery.isFunction(s.callback)) {
			patterns = s.pattern.toUpperCase().split(/\s+/);
			
			for (i= 0; i < patterns.length; i++) {
				pattern = patterns[i];
				parts   = pattern.split('+');
				code    = (code = parts.pop()).length == 1 
					? (code > 0 ? code : code.charCodeAt(0))
					: (code > 0 ? code : jQuery.ui.keyCode[code]);

				if (code && !shortcuts[pattern]) {
					shortcuts[pattern] = {
						keyCode     : code,
						altKey      : jQuery.inArray('ALT', parts)   != -1,
						ctrlKey     : jQuery.inArray('CTRL', parts)  != -1,
						shiftKey    : jQuery.inArray('SHIFT', parts) != -1,
						type        : s.type || 'keydown',
						callback    : s.callback,
						description : s.description,
						pattern     : pattern
					};
				}
			}
		}
		return this;
	};
	
	/**
	 * Registered shortcuts
	 *
	 * @type Object
	 **/
	this.shortcuts = function() {
		var ret = [];
		
		jQuery.each(shortcuts, function(i, s) {
			ret.push([s.pattern, self.i18n(s.description)]);
		});
		return ret;
	};
	
	/**
	 * Get/set clipboard content.
	 * Return new clipboard content.
	 *
	 * @example
	 *   this.clipboard([]) - clean clipboard
	 *   this.clipboard([{...}, {...}], true) - put 2 files in clipboard and mark it as cutted
	 * 
	 * @param  Array    new files hashes
	 * @param  Boolean  cut files?
	 * @return Array
	 */
	this.clipboard = function(hashes, cut) {
		var map = function() { return jQuery.map(clipboard, function(f) { return f.hash; }); };

		if (hashes !== void(0)) {
			clipboard.length && this.trigger('unlockfiles', {files : map()});
			remember = {};
			
			clipboard = jQuery.map(hashes||[], function(hash) {
				var file = files[hash];
				if (file) {
					
					remember[hash] = true;
					
					return {
						hash   : hash,
						phash  : file.phash,
						name   : file.name,
						mime   : file.mime,
						read   : file.read,
						locked : file.locked,
						cut    : !!cut
					};
				}
				return null;
			});
			this.trigger('changeclipboard', {clipboard : clipboard.slice(0, clipboard.length)});
			cut && this.trigger('lockfiles', {files : map()});
		}

		// return copy of clipboard instead of refrence
		return clipboard.slice(0, clipboard.length);
	};
	
	/**
	 * Return true if command enabled
	 * 
	 * @param  String       command name
	 * @param  String|void  hash for check of own volume's disabled cmds
	 * @return Boolean
	 */
	this.isCommandEnabled = function(name, dstHash) {
		var disabled, cmd,
			cvid = self.cwd().volumeid || '';
		
		// In serach results use selected item hash to check
		if (!dstHash && self.searchStatus.state > 1 && self.selected().length) {
			dstHash = self.selected()[0];
		}
		if (dstHash && (! cvid || dstHash.indexOf(cvid) !== 0)) {
			disabled = self.option('disabledFlip', dstHash);
			//if (! disabled) {
			//	disabled = {};
			//}
		} else {
			disabled = cwdOptions.disabledFlip/* || {}*/;
		}
		cmd = this._commands[name];
		return cmd ? (cmd.alwaysEnabled || !disabled[name]) : false;
	};
	
	/**
	 * Exec command and return result;
	 *
	 * @param  String         command name
	 * @param  String|Array   usualy files hashes
	 * @param  String|Array   command options
	 * @param  String|void    hash for enabled check of own volume's disabled cmds
	 * @return jQuery.Deferred
	 */		
	this.exec = function(cmd, files, opts, dstHash) {
		var dfrd, resType;
		
		// apply commandMap for keyboard shortcut
		if (!dstHash && this.commandMap[cmd] && this.commandMap[cmd] !== 'hidden') {
			cmd = this.commandMap[cmd];
		}

		if (cmd === 'open') {
			if (this.searchStatus.state || this.searchStatus.ininc) {
				this.trigger('searchend', { noupdate: true });
			}
			this.autoSync('stop');
		}
		if (!dstHash && files) {
			if (jQuery.isArray(files)) {
				if (files.length) {
					dstHash = files[0];
				}
			} else {
				dstHash = files;
			}
		}
		dfrd = this._commands[cmd] && this.isCommandEnabled(cmd, dstHash) 
			? this._commands[cmd].exec(files, opts) 
			: jQuery.Deferred().reject('errUnknownCmd');
		
		resType = typeof dfrd;
		if (!(resType === 'object' && dfrd.promise)) {
			self.debug('warning', '"cmd.exec()" should be returned "jQuery.Deferred" but cmd "' + cmd + '" returned "' + resType + '"');
			dfrd = jQuery.Deferred().resolve();
		}
		
		this.trigger('exec', { dfrd : dfrd, cmd : cmd, files : files, opts : opts, dstHash : dstHash });
		return dfrd;
	};
	
	/**
	 * Create and return dialog.
	 *
	 * @param  String|DOMElement  dialog content
	 * @param  Object             dialog options
	 * @return jQuery
	 */
	this.dialog = function(content, options) {
		var dialog = jQuery('<div></div>').append(content).appendTo(node).elfinderdialog(options, self),
			dnode  = dialog.closest('.ui-dialog'),
			resize = function(){
				! dialog.data('draged') && dialog.is(':visible') && dialog.elfinderdialog('posInit');
			};
		if (dnode.length) {
			self.bind('resize', resize);
			dnode.on('remove', function() {
				self.unbind('resize', resize);
			});
		}
		return dialog;
	};
	
	/**
	 * Create and return toast.
	 *
	 * @param  Object  toast options - see ui/toast.js
	 * @return jQuery
	 */
	this.toast = function(options) {
		return jQuery('<div class="ui-front"></div>').appendTo(this.ui.toast).elfindertoast(options || {}, this);
	};
	
	/**
	 * Return UI widget or node
	 *
	 * @param  String  ui name
	 * @return jQuery
	 */
	this.getUI = function(ui) {
		return ui? (this.ui[ui] || jQuery()) : node;
	};
	
	/**
	 * Return elFinder.command instance or instances array
	 *
	 * @param  String  command name
	 * @return Object | Array
	 */
	this.getCommand = function(name) {
		return name === void(0) ? this._commands : this._commands[name];
	};
	
	/**
	 * Resize elfinder node
	 * 
	 * @param  String|Number  width
	 * @param  String|Number  height
	 * @return void
	 */
	this.resize = function(w, h) {
		var getMargin = function() {
				var m = node.outerHeight(true) - node.innerHeight(),
					p = node;
				
				while(p.get(0) !== heightBase.get(0)) {
					p = p.parent();
					m += p.outerHeight(true) - p.innerHeight();
					if (! p.parent().length) {
						// reached the document
						break;
					}
				}
				return m;
			},
			fit = ! node.hasClass('ui-resizable'),
			prv = node.data('resizeSize') || {w: 0, h: 0},
			mt, size = {};

		if (heightBase && heightBase.data('resizeTm')) {
			clearTimeout(heightBase.data('resizeTm'));
		}
		
		if (! self.options.noResizeBySelf) {
			if (typeof h === 'string') {
				if (mt = h.match(/^([0-9.]+)%$/)) {
					// setup heightBase
					if (! heightBase || ! heightBase.length) {
						heightBase = jQuery(window);
					}
					if (! heightBase.data('marginToMyNode')) {
						heightBase.data('marginToMyNode', getMargin());
					}
					if (! heightBase.data('fitToBaseFunc')) {
						heightBase.data('fitToBaseFunc', function(e) {
							var tm = heightBase.data('resizeTm');
							e.preventDefault();
							e.stopPropagation();
							tm && cancelAnimationFrame(tm);
							if (! node.hasClass('elfinder-fullscreen') && (!self.UA.Mobile || heightBase.data('rotated') !== self.UA.Rotated)) {
								heightBase.data('rotated', self.UA.Rotated);
								heightBase.data('resizeTm', requestAnimationFrame(function() {
									self.restoreSize();
								}));
							}
						});
					}
					if (typeof heightBase.data('rotated') === 'undefined') {
						heightBase.data('rotated', self.UA.Rotated);
					}
					h = heightBase.height() * (mt[1] / 100) - heightBase.data('marginToMyNode');
					
					heightBase.off('resize.' + self.namespace, heightBase.data('fitToBaseFunc'));
					fit && heightBase.on('resize.' + self.namespace, heightBase.data('fitToBaseFunc'));
				}
			}
			
			node.css({ width : w, height : parseInt(h) });
		}

		size.w = Math.round(node.width());
		size.h = Math.round(node.height());
		node.data('resizeSize', size);
		if (size.w !== prv.w || size.h !== prv.h) {
			node.trigger('resize');
			this.trigger('resize', {width : size.w, height : size.h});
		}
	};
	
	/**
	 * Restore elfinder node size
	 * 
	 * @return elFinder
	 */
	this.restoreSize = function() {
		this.resize(width, height);
	};
	
	this.show = function() {
		node.show();
		this.enable().trigger('show');
	};
	
	this.hide = function() {
		if (this.options.enableAlways) {
			prevEnabled = enabled;
			enabled = false;
		}
		this.disable();
		this.trigger('hide');
		node.hide();
	};
	
	/**
	 * Lazy execution function
	 * 
	 * @param  Object  function
	 * @param  Number  delay
	 * @param  Object  options
	 * @return Object  jQuery.Deferred
	 */
	this.lazy = function(func, delay, opts) {
		var busy = function(state) {
				var cnt = node.data('lazycnt'),
					repaint;
				
				if (state) {
					repaint = node.data('lazyrepaint')? false : opts.repaint;
					if (! cnt) {
						node.data('lazycnt', 1)
							.addClass('elfinder-processing');
					} else {
						node.data('lazycnt', ++cnt);
					}
					if (repaint) {
						node.data('lazyrepaint', true).css('display'); // force repaint
					}
				} else {
					if (cnt && cnt > 1) {
						node.data('lazycnt', --cnt);
					} else {
						repaint = node.data('lazyrepaint');
						node.data('lazycnt', 0)
							.removeData('lazyrepaint')
							.removeClass('elfinder-processing');
						repaint && node.css('display'); // force repaint;
						self.trigger('lazydone');
					}
				}
			},
			dfd  = jQuery.Deferred(),
			callFunc = function() {
				dfd.resolve(func.call(dfd));
				busy(false);
			};
		
		delay = delay || 0;
		opts = opts || {};
		busy(true);
		
		if (delay) {
			setTimeout(callFunc, delay);
		} else {
			requestAnimationFrame(callFunc);
		}
		
		return dfd;
	};
	
	/**
	 * Destroy this elFinder instance
	 *
	 * @return void
	 **/
	this.destroy = function() {
		if (node && node[0].elfinder) {
			node.hasClass('elfinder-fullscreen') && self.toggleFullscreen(node);
			this.options.syncStart = false;
			this.autoSync('forcestop');
			this.trigger('destroy').disable();
			clipboard = [];
			selected = [];
			listeners = {};
			shortcuts = {};
			jQuery(window).off('.' + namespace);
			jQuery(document).off('.' + namespace);
			self.trigger = function(){};
			jQuery(beeper).remove();
			node.off()
				.removeData()
				.empty()
				.append(prevContent.contents())
				.attr('class', prevContent.attr('class'))
				.attr('style', prevContent.attr('style'));
			delete node[0].elfinder;
			// restore kept events
			jQuery.each(prevEvents, function(n, arr) {
				jQuery.each(arr, function(i, o) {
					node.on(o.type + (o.namespace? '.'+o.namespace : ''), o.selector, o.handler);
				});
			});
		}
	};
	
	/**
	 * Start or stop auto sync
	 * 
	 * @param  String|Bool  stop
	 * @return void
	 */
	this.autoSync = function(mode) {
		var sync;
		if (self.options.sync >= 1000) {
			if (syncInterval) {
				clearTimeout(syncInterval);
				syncInterval = null;
				self.trigger('autosync', {action : 'stop'});
			}
			
			if (mode === 'stop') {
				++autoSyncStop;
			} else {
				autoSyncStop = Math.max(0, --autoSyncStop);
			}
			
			if (autoSyncStop || mode === 'forcestop' || ! self.options.syncStart) {
				return;
			} 
			
			// run interval sync
			sync = function(start){
				var timeout;
				if (cwdOptions.syncMinMs && (start || syncInterval)) {
					start && self.trigger('autosync', {action : 'start'});
					timeout = Math.max(self.options.sync, cwdOptions.syncMinMs);
					syncInterval && clearTimeout(syncInterval);
					syncInterval = setTimeout(function() {
						var dosync = true, hash = cwd, cts;
						if (cwdOptions.syncChkAsTs && files[hash] && (cts = files[hash].ts)) {
							self.request({
								data : {cmd : 'info', targets : [hash], compare : cts, reload : 1},
								preventDefault : true
							})
							.done(function(data){
								var ts;
								dosync = true;
								if (data.compare) {
									ts = data.compare;
									if (ts == cts) {
										dosync = false;
									}
								}
								if (dosync) {
									self.sync(hash).always(function(){
										if (ts) {
											// update ts for cache clear etc.
											files[hash].ts = ts;
										}
										sync();
									});
								} else {
									sync();
								}
							})
							.fail(function(error, xhr){
								var err = self.parseError(error);
								if (err && xhr.status != 0) {
									self.error(err);
									if (Array.isArray(err) && jQuery.inArray('errOpen', err) !== -1) {
										self.request({
											data   : {cmd : 'open', target : (self.lastDir('') || self.root()), tree : 1, init : 1},
											notify : {type : 'open', cnt : 1, hideCnt : true}
										});
									}
								} else {
									syncInterval = setTimeout(function() {
										sync();
									}, timeout);
								}
							});
						} else {
							self.sync(cwd, true).always(function(){
								sync();
							});
						}
					}, timeout);
				}
			};
			sync(true);
		}
	};
	
	/**
	 * Return bool is inside work zone of specific point
	 * 
	 * @param  Number event.pageX
	 * @param  Number event.pageY
	 * @return Bool
	 */
	this.insideWorkzone = function(x, y, margin) {
		var rectangle = this.getUI('workzone').data('rectangle');
		
		margin = margin || 1;
		if (x < rectangle.left + margin
		|| x > rectangle.left + rectangle.width + margin
		|| y < rectangle.top + margin
		|| y > rectangle.top + rectangle.height + margin) {
			return false;
		}
		return true;
	};
	
	/**
	 * Target ui node move to last of children of elFinder node fot to show front
	 * 
	 * @param  Object  target    Target jQuery node object
	 */
	this.toFront = function(target) {
		var nodes = node.children('.ui-front').removeClass('elfinder-frontmost'),
			lastnode = nodes.last();
		nodes.css('z-index', '');
		jQuery(target).addClass('ui-front elfinder-frontmost').css('z-index', lastnode.css('z-index') + 1);
	};
	
	/**
	 * Remove class 'elfinder-frontmost' and hide() to target ui node
	 *
	 * @param      Object   target  Target jQuery node object
	 * @param      Boolean  nohide  Do not hide
	 */
	this.toHide =function(target, nohide) {
		var tgt = jQuery(target),
			last;

		!nohide && tgt.hide();
		if (tgt.hasClass('elfinder-frontmost')) {
			tgt.removeClass('elfinder-frontmost');
			last = node.children('.ui-front:visible:not(.elfinder-frontmost)').last();
			if (last.length) {
				requestAnimationFrame(function() {
					if (!node.children('.elfinder-frontmost:visible').length) {
						self.toFront(last);
						last.trigger('frontmost');
					}
				});
			}
		}
	};

	/**
	 * Return css object for maximize
	 * 
	 * @return Object
	 */
	this.getMaximizeCss = function() {
		return {
			width   : '100%',
			height  : '100%',
			margin  : 0,
			top     : 0,
			left    : 0,
			display : 'block',
			position: 'fixed',
			zIndex  : Math.max(self.zIndex? (self.zIndex + 1) : 0 , 1000),
			maxWidth : '',
			maxHeight: ''
		};
	};
	
	// Closure for togglefullscreen
	(function() {
		// check is in iframe
		if (inFrame && self.UA.Fullscreen) {
			self.UA.Fullscreen = false;
			if (parentIframe && typeof parentIframe.attr('allowfullscreen') !== 'undefined') {
				self.UA.Fullscreen = true;
			}
		}

		var orgStyle, bodyOvf, resizeTm, fullElm, exitFull, toFull, funcObj,
			cls = 'elfinder-fullscreen',
			clsN = 'elfinder-fullscreen-native',
			checkDialog = function() {
				var t = 0,
					l = 0;
				jQuery.each(node.children('.ui-dialog,.ui-draggable'), function(i, d) {
					var $d = jQuery(d),
						pos = $d.position();
					
					if (pos.top < 0) {
						$d.css('top', t);
						t += 20;
					}
					if (pos.left < 0) {
						$d.css('left', l);
						l += 20;
					}
				});
			},
			setFuncObj = function() {
				var useFullscreen = self.storage('useFullscreen');
				funcObj = self.UA.Fullscreen && (useFullscreen? useFullscreen > 0 : self.options.commandsOptions.fullscreen.mode === 'screen') ? {
					// native full screen mode
					
					fullElm: function() {
						return document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement || null;
					},
					
					exitFull: function() {
						if (document.exitFullscreen) {
							return document.exitFullscreen();
						} else if (document.webkitExitFullscreen) {
							return document.webkitExitFullscreen();
						} else if (document.mozCancelFullScreen) {
							return document.mozCancelFullScreen();
						} else if (document.msExitFullscreen) {
							return document.msExitFullscreen();
						}
					},
					
					toFull: function(elem) {
						if (elem.requestFullscreen) {
							return elem.requestFullscreen();
						} else if (elem.webkitRequestFullscreen) {
							return elem.webkitRequestFullscreen();
						} else if (elem.mozRequestFullScreen) {
							return elem.mozRequestFullScreen();
						} else if (elem.msRequestFullscreen) {
							return elem.msRequestFullscreen();
						}
						return false;
					}
				} : {
					// node element maximize mode
					
					fullElm: function() {
						var full;
						if (node.hasClass(cls)) {
							return node.get(0);
						} else {
							full = node.find('.' + cls);
							if (full.length) {
								return full.get(0);
							}
						}
						return null;
					},
					
					exitFull: function() {
						var elm;
						
						jQuery(window).off('resize.' + namespace, resize);
						if (bodyOvf !== void(0)) {
							jQuery('body').css('overflow', bodyOvf);
						}
						bodyOvf = void(0);
						
						if (orgStyle) {
							elm = orgStyle.elm;
							restoreStyle(elm);
							jQuery(elm).trigger('resize', {fullscreen: 'off'});
						}
						
						jQuery(window).trigger('resize');
					},
					
					toFull: function(elem) {
						bodyOvf = jQuery('body').css('overflow') || '';
						jQuery('body').css('overflow', 'hidden');
						
						jQuery(elem).css(self.getMaximizeCss())
							.addClass(cls)
							.trigger('resize', {fullscreen: 'on'});
						
						checkDialog();
						
						jQuery(window).on('resize.' + namespace, resize).trigger('resize');
						
						return true;
					}
				};
			},
			restoreStyle = function(elem) {
				if (orgStyle && orgStyle.elm == elem) {
					jQuery(elem).removeClass(cls + ' ' + clsN).attr('style', orgStyle.style);
					orgStyle = null;
				}
			},
			resize = function(e) {
				var elm;
				if (e.target === window) {
					resizeTm && cancelAnimationFrame(resizeTm);
					resizeTm = requestAnimationFrame(function() {
						if (elm = funcObj.fullElm()) {
							jQuery(elm).trigger('resize', {fullscreen: 'on'});
						}
					});
				}
			};
		
		setFuncObj();

		jQuery(document).on('fullscreenchange.' + namespace + ' webkitfullscreenchange.' + namespace + ' mozfullscreenchange.' + namespace + ' MSFullscreenChange.' + namespace, function(e){
			if (self.UA.Fullscreen) {
				var elm = funcObj.fullElm(),
					win = jQuery(window);
				
				resizeTm && cancelAnimationFrame(resizeTm);
				if (elm === null) {
					win.off('resize.' + namespace, resize);
					if (orgStyle) {
						elm = orgStyle.elm;
						restoreStyle(elm);
						jQuery(elm).trigger('resize', {fullscreen: 'off'});
					}
				} else {
					jQuery(elm).addClass(cls + ' ' + clsN)
						.attr('style', 'width:100%; height:100%; margin:0; padding:0;')
						.trigger('resize', {fullscreen: 'on'});
					win.on('resize.' + namespace, resize);
					checkDialog();
				}
				win.trigger('resize');
			}
		});
		
		/**
		 * Toggle Full Scrren Mode
		 * 
		 * @param  Object target
		 * @param  Bool   full
		 * @return Object | Null  DOM node object of current full scrren
		 */
		self.toggleFullscreen = function(target, full) {
			var elm = jQuery(target).get(0),
				curElm = null;
			
			curElm = funcObj.fullElm();
			if (curElm) {
				if (curElm == elm) {
					if (full === true) {
						return curElm;
					}
				} else {
					if (full === false) {
						return curElm;
					}
				}
				funcObj.exitFull();
				return null;
			} else {
				if (full === false) {
					return null;
				}
			}
			
			setFuncObj();
			orgStyle = {elm: elm, style: jQuery(elm).attr('style')};
			if (funcObj.toFull(elm) !== false) {
				return elm;
			} else {
				orgStyle = null;
				return null;
			}
		};
	})();
	
	// Closure for toggleMaximize
	(function(){
		var cls = 'elfinder-maximized',
		resizeTm,
		resize = function(e) {
			if (e.target === window && e.data && e.data.elm) {
				var elm = e.data.elm;
				resizeTm && cancelAnimationFrame(resizeTm);
				resizeTm = requestAnimationFrame(function() {
					elm.trigger('resize', {maximize: 'on'});
				});
			}
		},
		exitMax = function(elm) {
			jQuery(window).off('resize.' + namespace, resize);
			jQuery('body').css('overflow', elm.data('bodyOvf'));
			elm.removeClass(cls)
				.attr('style', elm.data('orgStyle'))
				.removeData('bodyOvf')
				.removeData('orgStyle');
			elm.trigger('resize', {maximize: 'off'});
		},
		toMax = function(elm) {
			elm.data('bodyOvf', jQuery('body').css('overflow') || '')
				.data('orgStyle', elm.attr('style'))
				.addClass(cls)
				.css(self.getMaximizeCss());
			jQuery('body').css('overflow', 'hidden');
			jQuery(window).on('resize.' + namespace, {elm: elm}, resize);
			elm.trigger('resize', {maximize: 'on'});
		};
		
		/**
		 * Toggle Maximize target node
		 * 
		 * @param  Object target
		 * @param  Bool   max
		 * @return void
		 */
		self.toggleMaximize = function(target, max) {
			var elm = jQuery(target),
				maximized = elm.hasClass(cls);
			
			if (maximized) {
				if (max === true) {
					return;
				}
				exitMax(elm);
			} else {
				if (max === false) {
					return;
				}
				toMax(elm);
			}
		};
	})();
	
	/*************  init stuffs  ****************/
	Object.assign(jQuery.ui.keyCode, {
		'F1' : 112,
		'F2' : 113,
		'F3' : 114,
		'F4' : 115,
		'F5' : 116,
		'F6' : 117,
		'F7' : 118,
		'F8' : 119,
		'F9' : 120,
		'F10' : 121,
		'F11' : 122,
		'F12' : 123,
		'DIG0' : 48,
		'DIG1' : 49,
		'DIG2' : 50,
		'DIG3' : 51,
		'DIG4' : 52,
		'DIG5' : 53,
		'DIG6' : 54,
		'DIG7' : 55,
		'DIG8' : 56,
		'DIG9' : 57,
		'NUM0' : 96,
		'NUM1' : 97,
		'NUM2' : 98,
		'NUM3' : 99,
		'NUM4' : 100,
		'NUM5' : 101,
		'NUM6' : 102,
		'NUM7' : 103,
		'NUM8' : 104,
		'NUM9' : 105,
		'CONTEXTMENU' : 93,
		'DOT'  : 190
	});
	
	this.dragUpload = false;
	this.xhrUpload  = (typeof XMLHttpRequestUpload != 'undefined' || typeof XMLHttpRequestEventTarget != 'undefined') && typeof File != 'undefined' && typeof FormData != 'undefined';
	
	// configure transport object
	this.transport = {};

	if (typeof(this.options.transport) == 'object') {
		this.transport = this.options.transport;
		if (typeof(this.transport.init) == 'function') {
			this.transport.init(this);
		}
	}
	
	if (typeof(this.transport.send) != 'function') {
		this.transport.send = function(opts) {
			if (!self.UA.IE) {
				// keep native xhr object for handling property responseURL
				opts._xhr = new XMLHttpRequest();
				opts.xhr = function() { 
					if (opts.progress) {
						opts._xhr.addEventListener('progress', opts.progress); 
					}
					return opts._xhr;
				};
			}
			return jQuery.ajax(opts);
		};
	}
	
	if (this.transport.upload == 'iframe') {
		this.transport.upload = jQuery.proxy(this.uploads.iframe, this);
	} else if (typeof(this.transport.upload) == 'function') {
		this.dragUpload = !!this.options.dragUploadAllow;
	} else if (this.xhrUpload && !!this.options.dragUploadAllow) {
		this.transport.upload = jQuery.proxy(this.uploads.xhr, this);
		this.dragUpload = true;
	} else {
		this.transport.upload = jQuery.proxy(this.uploads.iframe, this);
	}

	/**
	 * Decoding 'raw' string converted to unicode
	 * 
	 * @param  String str
	 * @return String
	 */
	this.decodeRawString = function(str) {
		var charCodes = function(str) {
			var i, len, arr;
			for (i=0,len=str.length,arr=[]; i<len; i++) {
				arr.push(str.charCodeAt(i));
			}
			return arr;
		},
		scalarValues = function(arr) {
			var scalars = [], i, len, c;
			if (typeof arr === 'string') {arr = charCodes(arr);}
			for (i=0,len=arr.length; c=arr[i],i<len; i++) {
				if (c >= 0xd800 && c <= 0xdbff) {
					scalars.push((c & 1023) + 64 << 10 | arr[++i] & 1023);
				} else {
					scalars.push(c);
				}
			}
			return scalars;
		},
		decodeUTF8 = function(arr) {
			var i, len, c, str, char = String.fromCharCode;
			for (i=0,len=arr.length,str=""; c=arr[i],i<len; i++) {
				if (c <= 0x7f) {
					str += char(c);
				} else if (c <= 0xdf && c >= 0xc2) {
					str += char((c&31)<<6 | arr[++i]&63);
				} else if (c <= 0xef && c >= 0xe0) {
					str += char((c&15)<<12 | (arr[++i]&63)<<6 | arr[++i]&63);
				} else if (c <= 0xf7 && c >= 0xf0) {
					str += char(
						0xd800 | ((c&7)<<8 | (arr[++i]&63)<<2 | arr[++i]>>>4&3) - 64,
						0xdc00 | (arr[i++]&15)<<6 | arr[i]&63
					);
				} else {
					str += char(0xfffd);
				}
			}
			return str;
		};
		
		return decodeUTF8(scalarValues(str));
	};

	/**
	 * Gets target file contents by file.hash
	 *
	 * @param      String  hash          The hash
	 * @param      String  responseType  'blob' or 'arraybuffer' (default)
	 * @param      Object  requestOpts   The request options
	 * @return     arraybuffer|blob  The contents.
	 */
	this.getContents = function(hash, responseType, requestOpts) {
		var self = this,
			dfd = jQuery.Deferred(),
			type = responseType || 'arraybuffer',
			url, req;

		dfd.fail(function() {
			req && req.state() === 'pending' && req.reject();
		});

		url = self.openUrl(hash);
		if (!self.isSameOrigin(url)) {
			url = self.openUrl(hash, true);
		}
		req = self.request(Object.assign({
			data    : {cmd : 'get'},
			options : {
				url: url,
				type: 'get',
				cache : true,
				dataType : 'binary',
				responseType : type,
				processData: false
			},
			notify : {
				type: 'file',
				cnt: 1,
				hideCnt: true
			},
			cancel : true
		}, requestOpts || {}))
		.fail(function() {
			dfd.reject();
		})
		.done(function(data) {
			dfd.resolve(data);
		});

		return dfd;
	};

	/**
	 * Gets the binary by url.
	 *
	 * @param      {Object}    opts      The options
	 * @param      {Function}  callback  The callback
	 * @param      {Object}    requestOpts The request options
	 * @return     arraybuffer|blob  The contents.
	 */
	this.getBinaryByUrl = function(opts, callback, requestOpts) {
		var self = this,
			dfd = jQuery.Deferred(),
			url, req;

		dfd.fail(function() {
			req && req.state() === 'pending' && req.reject();
		});

		req = self.request(Object.assign({
			data    : {cmd : 'get'},
			options : Object.assign({
				type: 'get',
				cache : true,
				dataType : 'binary',
				responseType : 'blob',
				processData: false
			}, opts)
		}, requestOpts || {}))
		.fail(function() {
			dfd.reject();
		})
		.done(function(data) {
			callback && callback(data);
			dfd.resolve(data);
		});

		return dfd;
	};

	/**
	 * Gets the mimetype.
	 *
	 * @param      {string}  name     The name
	 * @param      {string}  orgMime  The organization mime
	 * @return     {string}  The mimetype.
	 */
	this.getMimetype = function(name, orgMime) {
		var mime = orgMime,
			ext, m;
		m = (name + '').match(/\.([^.]+)$/);
		if (m && (ext = m[1])) {
			if (!extToMimeTable) {
				extToMimeTable = self.arrayFlip(self.mimeTypes);
			}
			if (!(mime = extToMimeTable[ext.toLowerCase()])) {
				mime = orgMime;
			}
		}
		return mime;
	};

	/**
	 * Supported check hash algorisms
	 * 
	 * @type Array
	 */
	self.hashCheckers = [];

	/**
	 * Closure of getContentsHashes()
	 */
	(function(self) {
		var hashLibs = {};

		if (window.Worker && window.ArrayBuffer) {
			// make fm.hashCheckers
			if (self.options.cdns.sparkmd5) {
				hashLibs.SparkMD5 = true;
				self.hashCheckers.push('md5');
			}
			if (self.options.cdns.jssha) {
				hashLibs.jsSHA = true;
				self.hashCheckers = self.hashCheckers.concat(['sha1', 'sha224', 'sha256', 'sha384', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512', 'shake128', 'shake256']);
			}
		}

		/**
		 * Gets the contents hashes.
		 *
		 * @param      String  target      target file.hash
		 * @param      Object  needHashes  need hash lib names
		 * @param      Object  requestOpts The request options
		 * @return     Object  hashes with lib name as key
		 */
		self.getContentsHashes = function(target, needHashes, hashOpts, requestOpts) {
			var dfd = jQuery.Deferred(),
				needs = self.arrayFlip(needHashes || ['md5'], true),
				libs = [],
				jobs = [],
				res = {},
				opts = hashOpts? hashOpts : {
					shake128len : 256,
					shake256len : 512
				},
				req;

			dfd.fail(function() {
				req && req.reject();
			});

			if (Object.keys(hashLibs).length) {
				req = self.getContents(target, 'arraybuffer', requestOpts).done(function(arrayBuffer) {
					if (needs.md5 && hashLibs.SparkMD5) {
						jobs.push((function() {
							var job = jQuery.Deferred();
							try {
								var wk = self.getWorker();
								job.fail(function() {
									wk && wk.terminate();
								});
								wk.onmessage = function(ans) {
									wk && wk.terminate();
									if (ans.data.hash) {
										var f;
										res.md5 = ans.data.hash;
										if (f = self.file(target)) {
											f.md5 = res.md5;
										}
									} else if (ans.data.error) {
										res.md5 = ans.data.error;
									}
									dfd.notify(res);
									job.resolve();
								};
								wk.onerror = function(e) {
									job.reject();
								};
								wk.postMessage({
									scripts: [self.options.cdns.sparkmd5, self.getWorkerUrl('calcfilehash.js')],
									data: { type: 'md5', bin: arrayBuffer }
								});
								dfd.fail(function() {
									job.reject();
								});
							} catch(e) {
								job.reject();
								delete hashLibs.SparkMD5;
							}
							return job;
						})());
					}
					if (hashLibs.jsSHA) {
						jQuery.each(['1', '224', '256', '384', '512', '3-224', '3-256', '3-384', '3-512', 'ke128', 'ke256'], function(i, v) {
							if (needs['sha' + v]) {
								jobs.push((function() {
									var job = jQuery.Deferred();
									try {
										var wk = self.getWorker();
										job.fail(function() {
											wk && wk.terminate();
										});
										wk.onmessage = function(ans) {
											wk && wk.terminate();
											if (ans.data.hash) {
												var f;
												res['sha' + v] = ans.data.hash;
												if (f = self.file(target)) {
													f['sha' + v] = res['sha' + v];
												}
											} else if (ans.data.error) {
												res['sha' + v] = ans.data.error;
											}
											dfd.notify(res);
											job.resolve();
										};
										wk.onerror = function(e) {
											job.reject();
										};
										wk.postMessage({
											scripts: [self.options.cdns.jssha, self.getWorkerUrl('calcfilehash.js')],
											data: { type: v, bin: arrayBuffer, hashOpts: opts }
										});
										dfd.fail(function() {
											job.reject();
										});
									} catch(e) {
										job.reject();
										delete hashLibs.jsSHA;
									}
									return job;
								})());
							}
						});
					}
					if (jobs.length) {
						jQuery.when.apply(null, jobs).always(function() {
							dfd.resolve(res);
						});
					} else {
						dfd.reject();
					}
				}).fail(function() {
					dfd.reject();
				});
			} else {
				dfd.reject();
			}

			return dfd;
		};
	})(this);

	/**
	 * Parse error value to display
	 *
	 * @param  Mixed  error
	 * @return Mixed  parsed error
	 */
	this.parseError = function(error) {
		var arg = error;
		if (jQuery.isPlainObject(arg)) {
			arg = arg.error;
		}
		return arg;
	};

	/**
	 * Alias for this.trigger('error', {error : 'message'})
	 *
	 * @param  String  error message
	 * @return elFinder
	 **/
	this.error = function() {
		var arg = arguments[0],
			opts = arguments[1] || null,
			err;
		if (arguments.length == 1 && typeof(arg) === 'function') {
			return self.bind('error', arg);
		} else {
			err = this.parseError(arg);
			return (err === true || !err)? this : self.trigger('error', {error: err, opts : opts});
		}
	};
	
	// create bind/trigger aliases for build-in events
	jQuery.each(events, function(i, name) {
		self[name] = function() {
			var arg = arguments[0];
			return arguments.length == 1 && typeof(arg) == 'function'
				? self.bind(name, arg)
				: self.trigger(name, jQuery.isPlainObject(arg) ? arg : {});
		};
	});

	// bind core event handlers
	this
		.enable(function() {
			if (!enabled && self.api && self.visible() && self.ui.overlay.is(':hidden') && ! node.children('.elfinder-dialog.' + self.res('class', 'editing') + ':visible').length) {
				enabled = true;
				document.activeElement && document.activeElement.blur();
				node.removeClass('elfinder-disabled');
			}
		})
		.disable(function() {
			prevEnabled = enabled;
			enabled = false;
			node.addClass('elfinder-disabled');
		})
		.open(function() {
			selected = [];
		})
		.select(function(e) {
			var cnt = 0,
				unselects = [];
			selected = jQuery.grep(e.data.selected || e.data.value|| [], function(hash) {
				if (unselects.length || (self.maxTargets && ++cnt > self.maxTargets)) {
					unselects.push(hash);
					return false;
				} else {
					return files[hash] ? true : false;
				}
			});
			if (unselects.length) {
				self.trigger('unselectfiles', {files: unselects, inselect: true});
				self.toast({mode: 'warning', msg: self.i18n(['errMaxTargets', self.maxTargets])});
			}
		})
		.error(function(e) { 
			var opts  = {
					cssClass  : 'elfinder-dialog-error',
					title     : self.i18n('error'),
					resizable : false,
					destroyOnClose : true,
					buttons   : {}
				},
				node = self.getUI(),
				cnt = node.children('.elfinder-dialog-error').length,
				last, counter;
			
			if (cnt < self.options.maxErrorDialogs) {
				opts.buttons[self.i18n(self.i18n('btnClose'))] = function() { jQuery(this).elfinderdialog('close'); };

				if (e.data.opts && jQuery.isPlainObject(e.data.opts)) {
					Object.assign(opts, e.data.opts);
				}

				self.dialog('<span class="elfinder-dialog-icon elfinder-dialog-icon-error"></span>'+self.i18n(e.data.error), opts);
			} else {
				last = node.children('.elfinder-dialog-error:last').children('.ui-dialog-content:first');
				counter = last.children('.elfinder-error-counter');
				if (counter.length) {
					counter.data('cnt', parseInt(counter.data('cnt')) + 1).html(self.i18n(['moreErrors', counter.data('cnt')]));
				} else {
					counter = jQuery('<span class="elfinder-error-counter">'+ self.i18n(['moreErrors', 1]) +'</span>').data('cnt', 1);
					last.append('<br/>', counter);
				}
			}
		})
		.bind('tmb', function(e) {
			jQuery.each(e.data.images||[], function(hash, tmb) {
				if (files[hash]) {
					files[hash].tmb = tmb;
				}
			});
		})
		.bind('searchstart', function(e) {
			Object.assign(self.searchStatus, e.data);
			self.searchStatus.state = 1;
		})
		.bind('search', function(e) {
			self.searchStatus.state = 2;
		})
		.bind('searchend', function() {
			self.searchStatus.state = 0;
			self.searchStatus.ininc = false;
			self.searchStatus.mixed = false;
		})
		.bind('canMakeEmptyFile', function(e) {
			var data = e.data,
				obj = {};
			if (data && Array.isArray(data.mimes)) {
				if (!data.unshift) {
					obj = self.mimesCanMakeEmpty;
				}
				jQuery.each(data.mimes, function() {
					if (!obj[this]) {
						obj[this] = self.mimeTypes[this];
					}
				});
				if (data.unshift) {
					self.mimesCanMakeEmpty = Object.assign(obj, self.mimesCanMakeEmpty);
				}
			}
		})
		.bind('themechange', function() {
			requestAnimationFrame(function() {
				self.trigger('uiresize');
			});
		})
		;

	// We listen and emit a sound on delete according to option
	if (true === this.options.sound) {
		this.bind('playsound', function(e) {
			var play  = beeper.canPlayType && beeper.canPlayType('audio/wav; codecs="1"'),
				file = e.data && e.data.soundFile;

			play && file && play != '' && play != 'no' && jQuery(beeper).html('<source src="' + soundPath + file + '" type="audio/wav">')[0].play();
		});
	}

	// bind external event handlers
	jQuery.each(this.options.handlers, function(event, callback) {
		self.bind(event, callback);
	});

	/**
	 * History object. Store visited folders
	 *
	 * @type Object
	 **/
	this.history = new this.history(this);
	
	/**
	 * Root hashed
	 * 
	 * @type Object
	 */
	this.roots = {};
	
	/**
	 * leaf roots
	 * 
	 * @type Object
	 */
	this.leafRoots = {};
	
	this.volumeExpires = {};

	/**
	 * Loaded commands
	 *
	 * @type Object
	 **/
	this._commands = {};
	
	if (!Array.isArray(this.options.commands)) {
		this.options.commands = [];
	}
	
	if (jQuery.inArray('*', this.options.commands) !== -1) {
		this.options.commands = Object.keys(this.commands);
	}
	
	/**
	 * UI command map of cwd volume ( That volume driver option `uiCmdMap` )
	 *
	 * @type Object
	 **/
	this.commandMap = {};
	
	/**
	 * cwd options of each volume
	 * key: volumeid
	 * val: options object
	 * 
	 * @type Object
	 */
	this.volOptions = {};

	/**
	 * Has volOptions data
	 * 
	 * @type Boolean
	 */
	this.hasVolOptions = false;

	/**
	 * Hash of trash holders
	 * key: trash folder hash
	 * val: source volume hash
	 * 
	 * @type Object
	 */
	this.trashes = {};

	/**
	 * cwd options of each folder/file
	 * key: hash
	 * val: options object
	 *
	 * @type Object
	 */
	this.optionsByHashes = {};
	
	/**
	 * UI Auto Hide Functions
	 * Each auto hide function mast be call to `fm.trigger('uiautohide')` at end of process
	 *
	 * @type Array
	 **/
	this.uiAutoHide = [];
	
	// trigger `uiautohide`
	this.one('open', function() {
		if (self.uiAutoHide.length) {
			setTimeout(function() {
				self.trigger('uiautohide');
			}, 500);
		}
	});
	
	// Auto Hide Functions sequential processing start
	this.bind('uiautohide', function() {
		if (self.uiAutoHide.length) {
			self.uiAutoHide.shift()();
		}
	});

	if (this.options.width) {
		width = this.options.width;
	}
	
	if (this.options.height) {
		height = this.options.height;
	}
	
	if (this.options.heightBase) {
		heightBase = jQuery(this.options.heightBase);
	}
	
	if (this.options.soundPath) {
		soundPath = this.options.soundPath.replace(/\/+$/, '') + '/';
	} else {
		soundPath = this.baseUrl + soundPath;
	}
	
	if (this.options.parrotHeaders && Array.isArray(this.options.parrotHeaders) && this.options.parrotHeaders.length) {
		this.parrotHeaders = this.options.parrotHeaders;
		// check sessionStorage
		jQuery.each(this.parrotHeaders, function(i, h) {
			var v = self.sessionStorage('core-ph:' + h);
			if (v) {
				self.customHeaders[h] = v;
			}
		});
	} else {
		this.parrotHeaders = [];
	}

	self.one('opendone', function() {
		var tm;
		// attach events to document
		jQuery(document)
			// disable elfinder on click outside elfinder
			.on('click.'+namespace, function(e) { enabled && ! self.options.enableAlways && !jQuery(e.target).closest(node).length && self.disable(); })
			// exec shortcuts
			.on(keydown+' '+keypress+' '+keyup+' '+mousedown, execShortcut);
		
		// attach events to window
		self.options.useBrowserHistory && jQuery(window)
			.on('popstate.' + namespace, function(ev) {
				var state = ev.originalEvent.state || {},
					hasThash = state.thash? true : false,
					dialog = node.find('.elfinder-frontmost:visible'),
					input = node.find('.elfinder-navbar-dir,.elfinder-cwd-filename').find('input,textarea'),
					onOpen, toast;
				if (!hasThash) {
					state = { thash: self.cwd().hash };
					// scroll to elFinder node
					jQuery('html,body').animate({ scrollTop: node.offset().top });
				}
				if (dialog.length || input.length) {
					history.pushState(state, null, location.pathname + location.search + '#elf_' + state.thash);
					if (dialog.length) {
						if (!dialog.hasClass(self.res('class', 'preventback'))) {
							if (dialog.hasClass('elfinder-contextmenu')) {
								jQuery(document).trigger(jQuery.Event('keydown', { keyCode: jQuery.ui.keyCode.ESCAPE, ctrlKey : false, shiftKey : false, altKey : false, metaKey : false }));
							} else if (dialog.hasClass('elfinder-dialog')) {
								dialog.elfinderdialog('close');
							} else {
								dialog.trigger('close');
							}
						}
					} else {
						input.trigger(jQuery.Event('keydown', { keyCode: jQuery.ui.keyCode.ESCAPE, ctrlKey : false, shiftKey : false, altKey : false, metaKey : false }));
					}
				} else {
					if (hasThash) {
						!jQuery.isEmptyObject(self.files()) && self.request({
							data   : {cmd  : 'open', target : state.thash, onhistory : 1},
							notify : {type : 'open', cnt : 1, hideCnt : true},
							syncOnFail : true
						});
					} else {
						onOpen = function() {
							toast.trigger('click');
						};
						self.one('open', onOpen, true);
						toast = self.toast({
							msg: self.i18n('pressAgainToExit'),
							onHidden: function() {
								self.unbind('open', onOpen);
								history.pushState(state, null, location.pathname + location.search + '#elf_' + state.thash);
							}
						});
					}
				}
			});
		
		jQuery(window).on('resize.' + namespace, function(e){
			if (e.target === this) {
				tm && cancelAnimationFrame(tm);
				tm = requestAnimationFrame(function() {
					var prv = node.data('resizeSize') || {w: 0, h: 0},
						size = {w: Math.round(node.width()), h: Math.round(node.height())};
					node.data('resizeSize', size);
					if (size.w !== prv.w || size.h !== prv.h) {
						node.trigger('resize');
						self.trigger('resize', {width : size.w, height : size.h});
					}
				});
			}
		})
		.on('beforeunload.' + namespace,function(e){
			var msg, cnt;
			if (!self.pauseUnloadCheck()) {
				if (node.is(':visible')) {
					if (self.ui.notify.children().length && jQuery.inArray('hasNotifyDialog', self.options.windowCloseConfirm) !== -1) {
						msg = self.i18n('ntfsmth');
					} else if (node.find('.'+self.res('class', 'editing')).length && jQuery.inArray('editingFile', self.options.windowCloseConfirm) !== -1) {
						msg = self.i18n('editingFile');
					} else if ((cnt = Object.keys(self.selected()).length) && jQuery.inArray('hasSelectedItem', self.options.windowCloseConfirm) !== -1) {
						msg = self.i18n('hasSelected', ''+cnt);
					} else if ((cnt = Object.keys(self.clipboard()).length) && jQuery.inArray('hasClipboardData', self.options.windowCloseConfirm) !== -1) {
						msg = self.i18n('hasClipboard', ''+cnt);
					}
					if (msg) {
						e.returnValue = msg;
						return msg;
					}
				}
				self.trigger('unload');
			}
		});

		// bind window onmessage for CORS
		jQuery(window).on('message.' + namespace, function(e){
			var res = e.originalEvent || null,
				obj, data;
			if (res && (self.convAbsUrl(self.options.url).indexOf(res.origin) === 0 || self.convAbsUrl(self.uploadURL).indexOf(res.origin) === 0)) {
				try {
					obj = JSON.parse(res.data);
					data = obj.data || null;
					if (data) {
						if (data.error) {
							if (obj.bind) {
								self.trigger(obj.bind+'fail', data);
							}
							self.error(data.error);
						} else {
							data.warning && self.error(data.warning);
							self.updateCache(data);
							data.removed && data.removed.length && self.remove(data);
							data.added   && data.added.length   && self.add(data);
							data.changed && data.changed.length && self.change(data);
							if (obj.bind) {
								self.trigger(obj.bind, data);
								self.trigger(obj.bind+'done');
							}
							data.sync && self.sync();
						}
					}
				} catch (e) {
					self.sync();
				}
			}
		});

		// elFinder enable always
		if (self.options.enableAlways) {
			jQuery(window).on('focus.' + namespace, function(e){
				(e.target === this) && self.enable();
			});
			if (inFrame) {
				jQuery(window.top).on('focus.' + namespace, function() {
					if (self.enable() && (! parentIframe || parentIframe.is(':visible'))) {
						requestAnimationFrame(function() {
							jQuery(window).trigger('focus');
						});
					}
				});
			}
		} else if (inFrame) {
			jQuery(window).on('blur.' + namespace, function(e){
				enabled && e.target === this && self.disable();
			});
		}

		// return focus to the window on click (elFInder in the frame)
		if (inFrame) {
			node.on('click', function(e) {
				jQuery(window).trigger('focus');
			});
		}
		
		// elFinder to enable by mouse over
		if (self.options.enableByMouseOver) {
			node.on('mouseenter touchstart', function(e) {
				(inFrame) && jQuery(window).trigger('focus');
				! self.enabled() && self.enable();
			});
		}

		// When the browser tab turn to foreground/background
		jQuery(window).on('visibilitychange.' + namespace, function(e) {
			var background = document.hidden || document.webkitHidden || document.msHidden;
			// AutoSync turn On/Off
			if (self.options.syncStart) {
				self.autoSync(background? 'stop' : void(0));
			}
		});
	});

	// store instance in node
	node[0].elfinder = this;

	// auto load language file
	dfrdsBeforeBootup.push((function() {
		var lang   = self.lang,
			langJs = self.i18nBaseUrl + 'elfinder.' + lang + '.js',
			dfd    = jQuery.Deferred().done(function() {
				if (self.i18[lang]) {
					self.lang = lang;
				}
				self.trigger('i18load');
				i18n = self.lang === 'en' 
					? self.i18['en'] 
					: jQuery.extend(true, {}, self.i18['en'], self.i18[self.lang]);
			});
		
		if (!self.i18[lang]) {
			self.lang = 'en';
			if (self.hasRequire) {
				require([langJs], function() {
					dfd.resolve();
				}, function() {
					dfd.resolve();
				});
			} else {
				self.loadScript([langJs], function() {
					dfd.resolve();
				}, {
					loadType: 'tag',
					error : function() {
						dfd.resolve();
					}
				});
			}
		} else {
			dfd.resolve();
		}
		return dfd;
	})());
	
	// elFinder boot up function
	bootUp = function() {
		var columnNames;

		/**
		 * i18 messages
		 *
		 * @type Object
		 **/
		self.messages = i18n.messages;
		
		// check jquery ui
		if (!(jQuery.fn.selectable && jQuery.fn.draggable && jQuery.fn.droppable && jQuery.fn.resizable && jQuery.fn.button && jQuery.fn.slider)) {
			return alert(self.i18n('errJqui'));
		}
		
		// check node
		if (!node.length) {
			return alert(self.i18n('errNode'));
		}
		// check connector url
		if (!self.options.url) {
			return alert(self.i18n('errURL'));
		}
		
		// column key/name map for fm.getColumnName()
		columnNames = Object.assign({
			name : self.i18n('name'),
			perm : self.i18n('perms'),
			date : self.i18n('modify'),
			size : self.i18n('size'),
			kind : self.i18n('kind'),
			modestr : self.i18n('mode'),
			modeoct : self.i18n('mode'),
			modeboth : self.i18n('mode')
		}, self.options.uiOptions.cwd.listView.columnsCustomName);

		/**
		 * Gets the column name of cwd list view
		 *
		 * @param      String  key     The key
		 * @return     String  The column name.
		 */
		self.getColumnName = function(key) {
			var res = columnNames[key] || self.i18n(key);
			return typeof res === 'function'? res() : res;
		};

		/**
		 * Interface direction
		 *
		 * @type String
		 * @default "ltr"
		 **/
		self.direction = i18n.direction;
		
		/**
		 * Date/time format
		 *
		 * @type String
		 * @default "m.d.Y"
		 **/
		self.dateFormat = self.options.dateFormat || i18n.dateFormat;
		
		/**
		 * Date format like "Yesterday 10:20:12"
		 *
		 * @type String
		 * @default "{day} {time}"
		 **/
		self.fancyFormat = self.options.fancyDateFormat || i18n.fancyDateFormat;
		
		/**
		 * Date format for if upload file has not original unique name
		 * e.g. Clipboard image data, Image data taken with iOS
		 *
		 * @type String
		 * @default "ymd-His"
		 **/
		self.nonameDateFormat = (self.options.nonameDateFormat || i18n.nonameDateFormat).replace(/[\/\\]/g, '_');

		/**
		 * Css classes 
		 *
		 * @type String
		 **/
		self.cssClass = 'ui-helper-reset ui-helper-clearfix ui-widget ui-widget-content ui-corner-all elfinder elfinder-'
				+(self.direction == 'rtl' ? 'rtl' : 'ltr')
				+(self.UA.Touch? (' elfinder-touch' + (self.options.resizable ? ' touch-punch' : '')) : '')
				+(self.UA.Mobile? ' elfinder-mobile' : '')
				+(self.UA.iOS? ' elfinder-ios' : '')
				+' '+self.options.cssClass;

		// prepare node
		node.addClass(self.cssClass)
			.on(mousedown, function() {
				!enabled && self.enable();
			});

		// draggable closure
		(function() {
			var ltr, wzRect, wzBottom, wzBottom2, nodeStyle,
				keyEvt = keydown + 'draggable' + ' keyup.' + namespace + 'draggable';
			
			/**
			 * Base draggable options
			 *
			 * @type Object
			 **/
			self.draggable = {
				appendTo   : node,
				addClasses : false,
				distance   : 4,
				revert     : true,
				refreshPositions : false,
				cursor     : 'crosshair',
				cursorAt   : {left : 50, top : 47},
				scroll     : false,
				start      : function(e, ui) {
					var helper   = ui.helper,
						targets  = jQuery.grep(helper.data('files')||[], function(h) {
							if (h) {
								remember[h] = true;
								return true;
							}
							return false;
						}),
						locked   = false,
						cnt, h;
					
					// fix node size
					nodeStyle = node.attr('style');
					node.width(node.width()).height(node.height());
					
					// set var for drag()
					ltr = (self.direction === 'ltr');
					wzRect = self.getUI('workzone').data('rectangle');
					wzBottom = wzRect.top + wzRect.height;
					wzBottom2 = wzBottom - self.getUI('navdock').outerHeight(true);
					
					self.draggingUiHelper = helper;
					cnt = targets.length;
					while (cnt--) {
						h = targets[cnt];
						if (files[h].locked) {
							locked = true;
							helper.data('locked', true);
							break;
						}
					}
					!locked && self.trigger('lockfiles', {files : targets});
		
					helper.data('autoScrTm', setInterval(function() {
						if (helper.data('autoScr')) {
							self.autoScroll[helper.data('autoScr')](helper.data('autoScrVal'));
						}
					}, 50));
				},
				drag       : function(e, ui) {
					var helper = ui.helper,
						autoScr, autoUp, bottom;
					
					if ((autoUp = wzRect.top > e.pageY) || wzBottom2 < e.pageY) {
						if (wzRect.cwdEdge > e.pageX) {
							autoScr = (ltr? 'navbar' : 'cwd') + (autoUp? 'Up' : 'Down');
						} else {
							autoScr = (ltr? 'cwd' : 'navbar') + (autoUp? 'Up' : 'Down');
						}
						if (!autoUp) {
							if (autoScr.substr(0, 3) === 'cwd') {
								if (wzBottom < e.pageY) {
									bottom = wzBottom;
								} else {
									autoScr = null;
								}
							} else {
								bottom = wzBottom2;
							}
						}
						if (autoScr) {
							helper.data('autoScr', autoScr);
							helper.data('autoScrVal', Math.pow((autoUp? wzRect.top - e.pageY : e.pageY - bottom), 1.3));
						}
					}
					if (! autoScr) {
						if (helper.data('autoScr')) {
							helper.data('refreshPositions', 1).data('autoScr', null);
						}
					}
					if (helper.data('refreshPositions') && jQuery(this).elfUiWidgetInstance('draggable')) {
						if (helper.data('refreshPositions') > 0) {
							jQuery(this).draggable('option', { refreshPositions : true, elfRefresh : true });
							helper.data('refreshPositions', -1);
						} else {
							jQuery(this).draggable('option', { refreshPositions : false, elfRefresh : false });
							helper.data('refreshPositions', null);
						}
					}
				},
				stop       : function(e, ui) {
					var helper = ui.helper,
						files;
					
					jQuery(document).off(keyEvt);
					jQuery(this).elfUiWidgetInstance('draggable') && jQuery(this).draggable('option', { refreshPositions : false });
					self.draggingUiHelper = null;
					self.trigger('focus').trigger('dragstop');
					if (! helper.data('droped')) {
						files = jQuery.grep(helper.data('files')||[], function(h) { return h? true : false ;});
						self.trigger('unlockfiles', {files : files});
						self.trigger('selectfiles', {files : self.selected()});
					}
					self.enable();
					
					// restore node style
					node.attr('style', nodeStyle);
					
					helper.data('autoScrTm') && clearInterval(helper.data('autoScrTm'));
				},
				helper     : function(e, ui) {
					var element = this.id ? jQuery(this) : jQuery(this).parents('[id]:first'),
						helper  = jQuery('<div class="elfinder-drag-helper"><span class="elfinder-drag-helper-icon-status"></span></div>'),
						icon    = function(f) {
							var mime = f.mime, i, tmb = self.tmb(f);
							i = '<div class="elfinder-cwd-icon elfinder-cwd-icon-drag '+self.mime2class(mime)+' ui-corner-all"></div>';
							if (tmb) {
								i = jQuery(i).addClass(tmb.className).css('background-image', "url('"+tmb.url+"')").get(0).outerHTML;
							} else if (f.icon) {
								i = jQuery(i).css(self.getIconStyle(f, true)).get(0).outerHTML;
							}
							if (f.csscls) {
								i = '<div class="'+f.csscls+'">' + i + '</div>';
							}
							return i;
						},
						hashes, l, ctr;
					
					self.draggingUiHelper && self.draggingUiHelper.stop(true, true);
					
					self.trigger('dragstart', {target : element[0], originalEvent : e}, true);
					
					hashes = element.hasClass(self.res('class', 'cwdfile')) 
						? self.selected() 
						: [self.navId2Hash(element.attr('id'))];
					
					helper.append(icon(files[hashes[0]])).data('files', hashes).data('locked', false).data('droped', false).data('namespace', namespace).data('dropover', 0);
		
					if ((l = hashes.length) > 1) {
						helper.append(icon(files[hashes[l-1]]) + '<span class="elfinder-drag-num">'+l+'</span>');
					}
					
					jQuery(document).on(keyEvt, function(e){
						if (self._commands.copy) {
							var chk = (e.shiftKey||e.ctrlKey||e.metaKey);
							if (ctr !== chk) {
								ctr = chk;
								if (helper.is(':visible') && helper.data('dropover') && ! helper.data('droped')) {
									helper.toggleClass('elfinder-drag-helper-plus', helper.data('locked')? true : ctr);
									self.trigger(ctr? 'unlockfiles' : 'lockfiles', {files : hashes, helper: helper});
								}
							}
						}
					});
					
					return helper;
				}
			};
		})();

		// in getFileCallback set - change default actions on double click/enter/ctrl+enter
		if (self.commands.getfile) {
			if (typeof(self.options.getFileCallback) == 'function') {
				self.bind('dblclick', function(e) {
					e.preventDefault();
					self.exec('getfile').fail(function() {
						self.exec('open', e.data && e.data.file? [ e.data.file ]: void(0));
					});
				});
				self.shortcut({
					pattern     : 'enter',
					description : self.i18n('cmdgetfile'),
					callback    : function() { self.exec('getfile').fail(function() { self.exec(self.OS == 'mac' ? 'rename' : 'open'); }); }
				})
				.shortcut({
					pattern     : 'ctrl+enter',
					description : self.i18n(self.OS == 'mac' ? 'cmdrename' : 'cmdopen'),
					callback    : function() { self.exec(self.OS == 'mac' ? 'rename' : 'open'); }
				});
			} else {
				self.options.getFileCallback = null;
			}
		}

		// load commands
		jQuery.each(self.commands, function(name, cmd) {
			var proto = Object.assign({}, cmd.prototype),
				extendsCmd, opts;
			if (jQuery.isFunction(cmd) && !self._commands[name] && (cmd.prototype.forceLoad || jQuery.inArray(name, self.options.commands) !== -1)) {
				extendsCmd = cmd.prototype.extendsCmd || '';
				if (extendsCmd) {
					if (jQuery.isFunction(self.commands[extendsCmd])) {
						cmd.prototype = Object.assign({}, base, new self.commands[extendsCmd](), cmd.prototype);
					} else {
						return true;
					}
				} else {
					cmd.prototype = Object.assign({}, base, cmd.prototype);
				}
				self._commands[name] = new cmd();
				cmd.prototype = proto;
				opts = self.options.commandsOptions[name] || {};
				if (extendsCmd && self.options.commandsOptions[extendsCmd]) {
					opts = jQuery.extend(true, {}, self.options.commandsOptions[extendsCmd], opts);
				}
				self._commands[name].setup(name, opts);
				// setup linked commands
				if (self._commands[name].linkedCmds.length) {
					jQuery.each(self._commands[name].linkedCmds, function(i, n) {
						var lcmd = self.commands[n];
						if (jQuery.isFunction(lcmd) && !self._commands[n]) {
							lcmd.prototype = base;
							self._commands[n] = new lcmd();
							self._commands[n].setup(n, self.options.commandsOptions[n]||{});
						}
					});
				}
			}
		});

		/**
		 * UI nodes
		 *
		 * @type Object
		 **/
		self.ui = {
			// container for nav panel and current folder container
			workzone : jQuery('<div></div>').appendTo(node).elfinderworkzone(self),
			// contaainer for folders tree / places
			navbar : jQuery('<div></div>').appendTo(node).elfindernavbar(self, self.options.uiOptions.navbar || {}),
			// container for for preview etc at below the navbar
			navdock : jQuery('<div></div>').appendTo(node).elfindernavdock(self, self.options.uiOptions.navdock || {}),
			// contextmenu
			contextmenu : jQuery('<div></div>').appendTo(node).elfindercontextmenu(self),
			// overlay
			overlay : jQuery('<div></div>').appendTo(node).elfinderoverlay({
				show : function() { self.disable(); },
				hide : function() { prevEnabled && self.enable(); }
			}),
			// current folder container
			cwd : jQuery('<div></div>').appendTo(node).elfindercwd(self, self.options.uiOptions.cwd || {}),
			// notification dialog window
			notify : self.dialog('', {
				cssClass      : 'elfinder-dialog-notify' + (self.options.notifyDialog.canClose? '' : ' elfinder-titlebar-button-hide'),
				position      : self.options.notifyDialog.position,
				absolute      : true,
				resizable     : false,
				autoOpen      : false,
				allowMinimize : true,
				closeOnEscape : self.options.notifyDialog.canClose? true : false,
				title         : '&nbsp;',
				width         : self.options.notifyDialog.width? parseInt(self.options.notifyDialog.width) : null,
				minHeight     : null,
				minimize      : function() { self.ui.notify.trigger('minimize'); }
			}),
			statusbar : jQuery('<div class="ui-widget-header ui-helper-clearfix ui-corner-bottom elfinder-statusbar"></div>').hide().appendTo(node),
			toast : jQuery('<div class="elfinder-toast"></div>').appendTo(node),
			bottomtray : jQuery('<div class="elfinder-bottomtray">').appendTo(node),
			progressbar : jQuery('<div class="elfinder-ui-progressbar">').appendTo(node)
		};

		self.trigger('uiready');

		// load required ui
		jQuery.each(self.options.ui || [], function(i, ui) {
			var name = 'elfinder'+ui,
				opts = self.options.uiOptions[ui] || {};
	
			if (!self.ui[ui] && jQuery.fn[name]) {
				// regist to self.ui before make instance
				self.ui[ui] = jQuery('<'+(opts.tag || 'div')+'/>').appendTo(node);
				self.ui[ui][name](self, opts);
			}
		});

		self.ui.progressbar.appendTo(self.ui.workzone);
		self.ui.notify.prev('.ui-dialog-titlebar').append('<div class="elfinder-ui-progressbar"></div>');

		// update size	
		self.resize(width, height);
		
		// make node resizable
		if (self.options.resizable) {
			node.resizable({
				resize    : function(e, ui) {
					self.resize(ui.size.width, ui.size.height);
				},
				handles   : 'se',
				minWidth  : 300,
				minHeight : 200
			});
			if (self.UA.Touch) {
				node.addClass('touch-punch');
			}
		}

		(function() {
			var navbar = self.getUI('navbar'),
				cwd    = self.getUI('cwd').parent();
			
			self.autoScroll = {
				navbarUp   : function(v) {
					navbar.scrollTop(Math.max(0, navbar.scrollTop() - v));
				},
				navbarDown : function(v) {
					navbar.scrollTop(navbar.scrollTop() + v);
				},
				cwdUp     : function(v) {
					cwd.scrollTop(Math.max(0, cwd.scrollTop() - v));
				},
				cwdDown   : function(v) {
					cwd.scrollTop(cwd.scrollTop() + v);
				}
			};
		})();

		// Swipe on the touch devices to show/hide of toolbar or navbar
		if (self.UA.Touch) {
			(function() {
				var lastX, lastY, nodeOffset, nodeWidth, nodeTop, navbarW, toolbarH,
					navbar = self.getUI('navbar'),
					toolbar = self.getUI('toolbar'),
					moveEv = 'touchmove.stopscroll',
					moveTm,
					moveUpOn = function(e) {
						var touches = e.originalEvent.touches || [{}],
							y = touches[0].pageY || null;
						if (!lastY || y < lastY) {
							e.preventDefault();
							moveTm && clearTimeout(moveTm);
						}
					},
					moveDownOn = function(e) {
						e.preventDefault();
						moveTm && clearTimeout(moveTm);
					},
					moveOff = function() {
						moveTm = setTimeout(function() {
							node.off(moveEv);
						}, 100);
					},
					handleW, handleH = 50;

				navbar = navbar.children().length? navbar : null;
				toolbar = toolbar.length? toolbar : null;
				node.on('touchstart touchmove touchend', function(e) {
					if (e.type === 'touchend') {
						lastX = false;
						lastY = false;
						moveOff();
						return;
					}
					
					var touches = e.originalEvent.touches || [{}],
						x = touches[0].pageX || null,
						y = touches[0].pageY || null,
						ltr = (self.direction === 'ltr'),
						navbarMode, treeWidth, swipeX, moveX, toolbarT, mode;
					
					if (x === null || y === null || (e.type === 'touchstart' && touches.length > 1)) {
						return;
					}
					
					if (e.type === 'touchstart') {
						nodeOffset = node.offset();
						nodeWidth = node.width();
						if (navbar) {
							lastX = false;
							if (navbar.is(':hidden')) {
								if (! handleW) {
									handleW = Math.max(50, nodeWidth / 10);
								}
								if ((ltr? (x - nodeOffset.left) : (nodeWidth + nodeOffset.left - x)) < handleW) {
									lastX = x;
								}
							} else if (! e.originalEvent._preventSwipeX) {
								navbarW = navbar.width();
								if (ltr) {
									swipeX = (x < nodeOffset.left + navbarW);
								} else {
									swipeX = (x > nodeOffset.left + nodeWidth - navbarW);
								}
								if (swipeX) {
									handleW = Math.max(50, nodeWidth / 10);
									lastX = x;
								} else {
									lastX = false;
								}
							}
						}
						if (toolbar) {
							lastY = false;
							if (! e.originalEvent._preventSwipeY) {
								toolbarH = toolbar.height();
								nodeTop = nodeOffset.top;
								if (y - nodeTop < (toolbar.is(':hidden')? handleH : (toolbarH + 30))) {
									lastY = y;
									node.on(moveEv, toolbar.is(':hidden')? moveDownOn: moveUpOn);
								}
							}
						}
					} else {
						if (navbar && lastX !== false) {
							navbarMode = (ltr? (lastX > x) : (lastX < x))? 'navhide' : 'navshow';
							moveX = Math.abs(lastX - x);
							if (navbarMode === 'navhide' && moveX > navbarW * 0.6
								|| (moveX > (navbarMode === 'navhide'? navbarW / 3 : 45)
									&& (navbarMode === 'navshow'
										|| (ltr? x < nodeOffset.left + 20 : x > nodeOffset.left + nodeWidth - 20)
									))
							) {
								self.getUI('navbar').trigger(navbarMode, {handleW: handleW});
								lastX = false;
							}
						}
						if (toolbar && lastY !== false ) {
							toolbarT = toolbar.offset().top;
							if (Math.abs(lastY - y) > Math.min(45, toolbarH / 3)) {
								mode = (lastY > y)? 'slideUp' : 'slideDown';
								if (mode === 'slideDown' || toolbarT + 20 > y) {
									if (toolbar.is(mode === 'slideDown' ? ':hidden' : ':visible')) {
										toolbar.stop(true, true).trigger('toggle', {duration: 100, handleH: handleH});
									}
									lastY = false;
								}
							}
						}
					}
				});
			})();
		}

		if (self.dragUpload) {
			// add event listener for HTML5 DnD upload
			(function() {
				var isin = function(e) {
					return (e.target.nodeName !== 'TEXTAREA' && e.target.nodeName !== 'INPUT' && jQuery(e.target).closest('div.ui-dialog-content').length === 0);
				},
				ent       = 'native-drag-enter',
				disable   = 'native-drag-disable',
				c         = 'class',
				navdir    = self.res(c, 'navdir'),
				droppable = self.res(c, 'droppable'),
				dropover  = self.res(c, 'adroppable'),
				arrow     = self.res(c, 'navarrow'),
				clDropActive = self.res(c, 'adroppable'),
				wz        = self.getUI('workzone'),
				ltr       = (self.direction === 'ltr'),
				clearTm   = function() {
					autoScrTm && cancelAnimationFrame(autoScrTm);
					autoScrTm = null;
				},
				wzRect, autoScrFn, autoScrTm;
				
				node.on('dragenter', function(e) {
					clearTm();
					if (isin(e)) {
						e.preventDefault();
						e.stopPropagation();
						wzRect = wz.data('rectangle');
					}
				})
				.on('dragleave', function(e) {
					clearTm();
					if (isin(e)) {
						e.preventDefault();
						e.stopPropagation();
					}
				})
				.on('dragover', function(e) {
					var autoUp;
					if (isin(e)) {
						e.preventDefault();
						e.stopPropagation();
						e.originalEvent.dataTransfer.dropEffect = 'none';
						if (! autoScrTm) {
							autoScrTm = requestAnimationFrame(function() {
								var wzBottom = wzRect.top + wzRect.height,
									wzBottom2 = wzBottom - self.getUI('navdock').outerHeight(true),
									fn;
								if ((autoUp = e.pageY < wzRect.top) || e.pageY > wzBottom2 ) {
									if (wzRect.cwdEdge > e.pageX) {
										fn = (ltr? 'navbar' : 'cwd') + (autoUp? 'Up' : 'Down');
									} else {
										fn = (ltr? 'cwd' : 'navbar') + (autoUp? 'Up' : 'Down');
									}
									if (!autoUp) {
										if (fn.substr(0, 3) === 'cwd') {
											if (wzBottom < e.pageY) {
												wzBottom2 = wzBottom;
											} else {
												fn = '';
											}
										}
									}
									fn && self.autoScroll[fn](Math.pow((autoUp? wzRect.top - e.pageY : e.pageY - wzBottom2), 1.3));
								}
								autoScrTm = null;
							});
						}
					} else {
						clearTm();
					}
				})
				.on('drop', function(e) {
					clearTm();
					if (isin(e)) {
						e.stopPropagation();
						e.preventDefault();
					}
				});
				
				node.on('dragenter', '.native-droppable', function(e){
					if (e.originalEvent.dataTransfer) {
						var $elm = jQuery(e.currentTarget),
							id   = e.currentTarget.id || null,
							cwd  = null,
							elfFrom;
						if (!id) { // target is cwd
							cwd = self.cwd();
							$elm.data(disable, false);
							try {
								jQuery.each(e.originalEvent.dataTransfer.types, function(i, v){
									if (v.substr(0, 13) === 'elfinderfrom:') {
										elfFrom = v.substr(13).toLowerCase();
									}
								});
							} catch(e) {}
						}
						if (!cwd || (cwd.write && (!elfFrom || elfFrom !== (window.location.href + cwd.hash).toLowerCase()))) {
							e.preventDefault();
							e.stopPropagation();
							$elm.data(ent, true);
							$elm.addClass(clDropActive);
						} else {
							$elm.data(disable, true);
						}
					}
				})
				.on('dragleave', '.native-droppable', function(e){
					if (e.originalEvent.dataTransfer) {
						var $elm = jQuery(e.currentTarget);
						e.preventDefault();
						e.stopPropagation();
						if ($elm.data(ent)) {
							$elm.data(ent, false);
						} else {
							$elm.removeClass(clDropActive);
						}
					}
				})
				.on('dragover', '.native-droppable', function(e){
					if (e.originalEvent.dataTransfer) {
						var $elm = jQuery(e.currentTarget);
						e.preventDefault();
						e.stopPropagation();
						e.originalEvent.dataTransfer.dropEffect = $elm.data(disable)? 'none' : 'copy';
						$elm.data(ent, false);
					}
				})
				.on('drop', '.native-droppable', function(e){
					if (e.originalEvent && e.originalEvent.dataTransfer) {
						var $elm = jQuery(e.currentTarget),
							id;
						e.preventDefault();
						e.stopPropagation();
						$elm.removeClass(clDropActive);
						if (e.currentTarget.id) {
							id = $elm.hasClass(navdir)? self.navId2Hash(e.currentTarget.id) : self.cwdId2Hash(e.currentTarget.id);
						} else {
							id = self.cwd().hash;
						}
						e.originalEvent._target = id;
						self.exec('upload', {dropEvt: e.originalEvent, target: id}, void 0, id);
					}
				});
			})();
		}

		// trigger event cssloaded if cssAutoLoad disabled
		if (self.cssloaded === false) {
			self.cssloaded = true;
			self.trigger('cssloaded');
		}

		// calculate elFinder node z-index
		self.zIndexCalc();

		// send initial request and start to pray >_<
		self.trigger('init')
			.request({
				data        : {cmd : 'open', target : self.startDir(), init : 1, tree : 1}, 
				preventDone : true,
				notify      : {type : 'open', cnt : 1, hideCnt : true},
				freeze      : true
			})
			.fail(function() {
				self.trigger('fail').disable().lastDir('');
				listeners = {};
				shortcuts = {};
				jQuery(document).add(node).off('.'+namespace);
				self.trigger = function() { };
			})
			.done(function(data) {
				var trashDisable = function(th) {
						var src = self.file(self.trashes[th]),
							d = self.options.debug,
							error;
						
						if (src && src.volumeid) {
							delete self.volOptions[src.volumeid].trashHash;
						}
						self.trashes[th] = false;
						self.debug('backend-error', 'Trash hash "'+th+'" was not found or not writable.');
					},
					toChkTh = {};
				
				// regist rawStringDecoder
				if (self.options.rawStringDecoder) {
					self.registRawStringDecoder(self.options.rawStringDecoder);
				}

				// re-calculate elFinder node z-index
				self.zIndexCalc();
				
				self.load().debug('api', self.api);
				// update ui's size after init
				node.trigger('resize');
				// initial open
				open(data);
				self.trigger('open', data, false);
				self.trigger('opendone');
				
				if (inFrame && self.options.enableAlways) {
					jQuery(window).trigger('focus');
				}
				
				// check self.trashes
				jQuery.each(self.trashes, function(th) {
					var dir = self.file(th),
						src;
					if (! dir) {
						toChkTh[th] = true;
					} else if (dir.mime !== 'directory' || ! dir.write) {
						trashDisable(th);
					}
				});
				if (Object.keys(toChkTh).length) {
					self.request({
						data : {cmd : 'info', targets : Object.keys(toChkTh)},
						preventDefault : true
					}).done(function(data) {
						if (data && data.files) {
							jQuery.each(data.files, function(i, dir) {
								if (dir.mime === 'directory' && dir.write) {
									delete toChkTh[dir.hash];
								}
							});
						}
					}).always(function() {
						jQuery.each(toChkTh, trashDisable);
					});
				}
				// to enable / disable
				self[self.options.enableAlways? 'enable' : 'disable']();
			});
		
		// self.timeEnd('load');
		// End of bootUp()
	};
	
	// call bootCallback function with elFinder instance, extraObject - { dfrdsBeforeBootup: dfrdsBeforeBootup }
	if (bootCallback && typeof bootCallback === 'function') {
		self.bootCallback = bootCallback;
		bootCallback.call(node.get(0), self, { dfrdsBeforeBootup: dfrdsBeforeBootup });
	}
	
	// call dfrdsBeforeBootup functions then boot up elFinder
	jQuery.when.apply(null, dfrdsBeforeBootup).done(function() {
		bootUp();
	}).fail(function(error) {
		self.error(error);
	});
};

//register elFinder to global scope
if (typeof toGlobal === 'undefined' || toGlobal) {
	window.elFinder = elFinder;
}

/**
 * Prototype
 * 
 * @type  Object
 */
elFinder.prototype = {
	
	uniqueid : 0,
	
	res : function(type, id) {
		return this.resources[type] && this.resources[type][id];
	}, 

	/**
	 * User os. Required to bind native shortcuts for open/rename
	 *
	 * @type String
	 **/
	OS : navigator.userAgent.indexOf('Mac') !== -1 ? 'mac' : navigator.userAgent.indexOf('Win') !== -1  ? 'win' : 'other',
	
	/**
	 * User browser UA.
	 * jQuery.browser: version deprecated: 1.3, removed: 1.9
	 *
	 * @type Object
	 **/
	UA : (function(){
		var self = this,
			webkit = !document.unqueID && !window.opera && !window.sidebar && 'localStorage' in window && 'WebkitAppearance' in document.documentElement.style,
			chrome = webkit && window.chrome,
			/*setRotated = function() {
				var a = ((screen && screen.orientation && screen.orientation.angle) || window.orientation || 0) + 0;
				if (a === -90) {
					a = 270;
				}
				UA.Angle = a;
				UA.Rotated = a % 180 === 0? false : true;
			},*/
			UA = {
				// Browser IE <= IE 6
				ltIE6   : typeof window.addEventListener == "undefined" && typeof document.documentElement.style.maxHeight == "undefined",
				// Browser IE <= IE 7
				ltIE7   : typeof window.addEventListener == "undefined" && typeof document.querySelectorAll == "undefined",
				// Browser IE <= IE 8
				ltIE8   : typeof window.addEventListener == "undefined" && typeof document.getElementsByClassName == "undefined",
				// Browser IE <= IE 9
				ltIE9  : document.uniqueID && document.documentMode <= 9,
				// Browser IE <= IE 10
				ltIE10  : document.uniqueID && document.documentMode <= 10,
				// Browser IE >= IE 11
				gtIE11  : document.uniqueID && document.documentMode >= 11,
				IE      : document.uniqueID,
				Firefox : window.sidebar,
				Opera   : window.opera,
				Webkit  : webkit,
				Chrome  : chrome,
				Edge    : (chrome && window.msCredentials)? true : false,
				Safari  : webkit && !window.chrome,
				Mobile  : typeof window.orientation != "undefined",
				Touch   : typeof window.ontouchstart != "undefined",
				iOS     : navigator.platform.match(/^iP(?:[ao]d|hone)/),
				Mac     : navigator.platform.match(/^Mac/),
				Fullscreen : (typeof (document.exitFullscreen || document.webkitExitFullscreen || document.mozCancelFullScreen || document.msExitFullscreen) !== 'undefined'),
				Angle   : 0,
				Rotated : false,
				CSS : (function() {
					var aStyle = document.createElement('a').style,
						pStyle = document.createElement('p').style,
						css;
					css = 'position:sticky;position:-webkit-sticky;';
					css += 'width:-webkit-max-content;width:-moz-max-content;width:-ms-max-content;width:max-content;';
					aStyle.cssText = css;
					return {
						positionSticky : aStyle.position.indexOf('sticky')!==-1,
						widthMaxContent : aStyle.width.indexOf('max-content')!==-1,
						flex : typeof pStyle.flex !== 'undefined'
					};
				})()
			};
			return UA;
	})(),
	
	/**
	 * Is cookie enabled
	 * 
	 * @type Boolean
	 */
	cookieEnabled : window.navigator.cookieEnabled,

	/**
	 * Has RequireJS?
	 * 
	 * @type Boolean
	 */
	hasRequire : (typeof define === 'function' && define.amd),
	
	/**
	 * Current request command
	 * 
	 * @type  String
	 */
	currentReqCmd : '',
	
	/**
	 * Current keyboard state
	 * 
	 * @type  Object
	 */
	keyState : {},
	
	/**
	 * Internationalization object
	 * 
	 * @type  Object
	 */
	i18 : {
		en : {
			translator      : '',
			language        : 'English',
			direction       : 'ltr',
			dateFormat      : 'd.m.Y H:i',
			fancyDateFormat : '$1 H:i',
			nonameDateFormat : 'ymd-His',
			messages        : {}
		},
		months : ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
		monthsShort : ['msJan', 'msFeb', 'msMar', 'msApr', 'msMay', 'msJun', 'msJul', 'msAug', 'msSep', 'msOct', 'msNov', 'msDec'],

		days : ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
		daysShort : ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
	},
	
	/**
	 * File mimetype to kind mapping
	 * 
	 * @type  Object
	 */
	kinds : 	{
			'unknown'                       : 'Unknown',
			'directory'                     : 'Folder',
			'group'                         : 'Selects',
			'symlink'                       : 'Alias',
			'symlink-broken'                : 'AliasBroken',
			'application/x-empty'           : 'TextPlain',
			'application/postscript'        : 'Postscript',
			'application/vnd.ms-office'     : 'MsOffice',
			'application/msword'            : 'MsWord',
			'application/vnd.ms-word'       : 'MsWord',
			'application/vnd.openxmlformats-officedocument.wordprocessingml.document' : 'MsWord',
			'application/vnd.ms-word.document.macroEnabled.12'                        : 'MsWord',
			'application/vnd.openxmlformats-officedocument.wordprocessingml.template' : 'MsWord',
			'application/vnd.ms-word.template.macroEnabled.12'                        : 'MsWord',
			'application/vnd.ms-excel'      : 'MsExcel',
			'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'       : 'MsExcel',
			'application/vnd.ms-excel.sheet.macroEnabled.12'                          : 'MsExcel',
			'application/vnd.openxmlformats-officedocument.spreadsheetml.template'    : 'MsExcel',
			'application/vnd.ms-excel.template.macroEnabled.12'                       : 'MsExcel',
			'application/vnd.ms-excel.sheet.binary.macroEnabled.12'                   : 'MsExcel',
			'application/vnd.ms-excel.addin.macroEnabled.12'                          : 'MsExcel',
			'application/vnd.ms-powerpoint' : 'MsPP',
			'application/vnd.openxmlformats-officedocument.presentationml.presentation' : 'MsPP',
			'application/vnd.ms-powerpoint.presentation.macroEnabled.12'              : 'MsPP',
			'application/vnd.openxmlformats-officedocument.presentationml.slideshow'  : 'MsPP',
			'application/vnd.ms-powerpoint.slideshow.macroEnabled.12'                 : 'MsPP',
			'application/vnd.openxmlformats-officedocument.presentationml.template'   : 'MsPP',
			'application/vnd.ms-powerpoint.template.macroEnabled.12'                  : 'MsPP',
			'application/vnd.ms-powerpoint.addin.macroEnabled.12'                     : 'MsPP',
			'application/vnd.openxmlformats-officedocument.presentationml.slide'      : 'MsPP',
			'application/vnd.ms-powerpoint.slide.macroEnabled.12'                     : 'MsPP',
			'application/pdf'               : 'PDF',
			'application/xml'               : 'XML',
			'application/vnd.oasis.opendocument.text' : 'OO',
			'application/vnd.oasis.opendocument.text-template'         : 'OO',
			'application/vnd.oasis.opendocument.text-web'              : 'OO',
			'application/vnd.oasis.opendocument.text-master'           : 'OO',
			'application/vnd.oasis.opendocument.graphics'              : 'OO',
			'application/vnd.oasis.opendocument.graphics-template'     : 'OO',
			'application/vnd.oasis.opendocument.presentation'          : 'OO',
			'application/vnd.oasis.opendocument.presentation-template' : 'OO',
			'application/vnd.oasis.opendocument.spreadsheet'           : 'OO',
			'application/vnd.oasis.opendocument.spreadsheet-template'  : 'OO',
			'application/vnd.oasis.opendocument.chart'                 : 'OO',
			'application/vnd.oasis.opendocument.formula'               : 'OO',
			'application/vnd.oasis.opendocument.database'              : 'OO',
			'application/vnd.oasis.opendocument.image'                 : 'OO',
			'application/vnd.openofficeorg.extension'                  : 'OO',
			'application/x-shockwave-flash' : 'AppFlash',
			'application/flash-video'       : 'Flash video',
			'application/x-bittorrent'      : 'Torrent',
			'application/javascript'        : 'JS',
			'application/rtf'               : 'RTF',
			'application/rtfd'              : 'RTF',
			'application/x-font-ttf'        : 'TTF',
			'application/x-font-otf'        : 'OTF',
			'application/x-rpm'             : 'RPM',
			'application/x-web-config'      : 'TextPlain',
			'application/xhtml+xml'         : 'HTML',
			'application/docbook+xml'       : 'DOCBOOK',
			'application/x-awk'             : 'AWK',
			'application/x-gzip'            : 'GZIP',
			'application/x-bzip2'           : 'BZIP',
			'application/x-xz'              : 'XZ',
			'application/zip'               : 'ZIP',
			'application/x-zip'               : 'ZIP',
			'application/x-rar'             : 'RAR',
			'application/x-tar'             : 'TAR',
			'application/x-7z-compressed'   : '7z',
			'application/x-jar'             : 'JAR',
			'text/plain'                    : 'TextPlain',
			'text/x-php'                    : 'PHP',
			'text/html'                     : 'HTML',
			'text/javascript'               : 'JS',
			'text/css'                      : 'CSS',
			'text/rtf'                      : 'RTF',
			'text/rtfd'                     : 'RTF',
			'text/x-c'                      : 'C',
			'text/x-csrc'                   : 'C',
			'text/x-chdr'                   : 'CHeader',
			'text/x-c++'                    : 'CPP',
			'text/x-c++src'                 : 'CPP',
			'text/x-c++hdr'                 : 'CPPHeader',
			'text/x-shellscript'            : 'Shell',
			'application/x-csh'             : 'Shell',
			'text/x-python'                 : 'Python',
			'text/x-java'                   : 'Java',
			'text/x-java-source'            : 'Java',
			'text/x-ruby'                   : 'Ruby',
			'text/x-perl'                   : 'Perl',
			'text/x-sql'                    : 'SQL',
			'text/xml'                      : 'XML',
			'text/x-comma-separated-values' : 'CSV',
			'text/x-markdown'               : 'Markdown',
			'image/x-ms-bmp'                : 'BMP',
			'image/jpeg'                    : 'JPEG',
			'image/gif'                     : 'GIF',
			'image/png'                     : 'PNG',
			'image/tiff'                    : 'TIFF',
			'image/x-targa'                 : 'TGA',
			'image/vnd.adobe.photoshop'     : 'PSD',
			'image/xbm'                     : 'XBITMAP',
			'image/pxm'                     : 'PXM',
			'audio/mpeg'                    : 'AudioMPEG',
			'audio/midi'                    : 'AudioMIDI',
			'audio/ogg'                     : 'AudioOGG',
			'audio/mp4'                     : 'AudioMPEG4',
			'audio/x-m4a'                   : 'AudioMPEG4',
			'audio/wav'                     : 'AudioWAV',
			'audio/x-mp3-playlist'          : 'AudioPlaylist',
			'video/x-dv'                    : 'VideoDV',
			'video/mp4'                     : 'VideoMPEG4',
			'video/mpeg'                    : 'VideoMPEG',
			'video/x-msvideo'               : 'VideoAVI',
			'video/quicktime'               : 'VideoMOV',
			'video/x-ms-wmv'                : 'VideoWM',
			'video/x-flv'                   : 'VideoFlash',
			'video/x-matroska'              : 'VideoMKV',
			'video/ogg'                     : 'VideoOGG'
		},
	
	/**
	 * File mimetype to file extention mapping
	 * 
	 * @type  Object
	 * @see   elFinder.mimetypes.js
	 */
	mimeTypes : {},
	
	/**
	 * Ajax request data validation rules
	 * 
	 * @type  Object
	 */
	rules : {
		defaults : function(data) {
			if (!data
			|| (data.added && !Array.isArray(data.added))
			||  (data.removed && !Array.isArray(data.removed))
			||  (data.changed && !Array.isArray(data.changed))) {
				return false;
			}
			return true;
		},
		open    : function(data) { return data && data.cwd && data.files && jQuery.isPlainObject(data.cwd) && Array.isArray(data.files); },
		tree    : function(data) { return data && data.tree && Array.isArray(data.tree); },
		parents : function(data) { return data && data.tree && Array.isArray(data.tree); },
		tmb     : function(data) { return data && data.images && (jQuery.isPlainObject(data.images) || Array.isArray(data.images)); },
		upload  : function(data) { return data && (jQuery.isPlainObject(data.added) || Array.isArray(data.added));},
		search  : function(data) { return data && data.files && Array.isArray(data.files); }
	},
	
	/**
	 * Commands costructors
	 *
	 * @type Object
	 */
	commands : {},
	
	/**
	 * Commands to add the item (space delimited)
	 * 
	 * @type String
	 */
	cmdsToAdd : 'archive duplicate extract mkdir mkfile paste rm upload',
	
	parseUploadData : function(text) {
		var self = this,
			data;
		
		if (!jQuery.trim(text)) {
			return {error : ['errResponse', 'errDataEmpty']};
		}
		
		try {
			data = JSON.parse(text);
		} catch (e) {
			return {error : ['errResponse', 'errDataNotJSON']};
		}
		
		data = self.normalize(data);
		if (!self.validResponse('upload', data)) {
			return {error : (data.norError || ['errResponse'])};
		}
		data.removed = jQuery.merge((data.removed || []), jQuery.map(data.added || [], function(f) { return self.file(f.hash)? f.hash : null; }));
		return data;
		
	},
	
	iframeCnt : 0,
	
	uploads : {
		// xhr muiti uploading flag
		xhrUploading: false,
		
		// Timer of request fail to sync
		failSyncTm: null,
		
		// current chunkfail requesting chunk
		chunkfailReq: {},
		
		// check file/dir exists
		checkExists: function(files, target, fm, isDir) {
			var dfrd = jQuery.Deferred(),
				names, renames = [], hashes = {}, chkFiles = [],
				cancel = function() {
					var i = files.length;
					while (--i > -1) {
						files[i]._remove = true;
					}
				},
				resolve = function() {
					dfrd.resolve(renames, hashes);
				},
				check = function() {
					var existed = [], exists = [], i, c,
						pathStr = target !== fm.cwd().hash? fm.path(target, true) + fm.option('separator', target) : '',
						confirm = function(ndx) {
							var last = ndx == exists.length-1,
								opts = {
									cssClass : 'elfinder-confirm-upload',
									title  : fm.i18n('cmdupload'),
									text   : ['errExists', pathStr + exists[ndx].name, 'confirmRepl'], 
									all    : !last,
									accept : {
										label    : 'btnYes',
										callback : function(all) {
											!last && !all
												? confirm(++ndx)
												: resolve();
										}
									},
									reject : {
										label    : 'btnNo',
										callback : function(all) {
											var i;
			
											if (all) {
												i = exists.length;
												while (ndx < i--) {
													files[exists[i].i]._remove = true;
												}
											} else {
												files[exists[ndx].i]._remove = true;
											}
			
											!last && !all
												? confirm(++ndx)
												: resolve();
										}
									},
									cancel : {
										label    : 'btnCancel',
										callback : function() {
											cancel();
											resolve();
										}
									},
									buttons : [
										{
											label : 'btnBackup',
											cssClass : 'elfinder-confirm-btn-backup',
											callback : function(all) {
												var i;
												if (all) {
													i = exists.length;
													while (ndx < i--) {
														renames.push(exists[i].name);
													}
												} else {
													renames.push(exists[ndx].name);
												}
												!last && !all
													? confirm(++ndx)
													: resolve();
											}
										}
									]
								};
							
							if (!isDir) {
								opts.buttons.push({
									label : 'btnRename' + (last? '' : 'All'),
									cssClass : 'elfinder-confirm-btn-rename',
									callback : function() {
										renames = null;
										resolve();
									}
								});
							}
							if (fm.iframeCnt > 0) {
								delete opts.reject;
							}
							fm.confirm(opts);
						};
					
					if (! fm.file(target).read) {
						// for dropbox type
						resolve();
						return;
					}
					
					names = jQuery.map(files, function(file, i) { return file.name && (!fm.UA.iOS || file.name !== 'image.jpg')? {i: i, name: file.name} : null ;});
					
					fm.request({
						data : {cmd : 'ls', target : target, intersect : jQuery.map(names, function(item) { return item.name;})},
						notify : {type : 'preupload', cnt : 1, hideCnt : true},
						preventDefault : true
					})
					.done(function(data) {
						var existedArr, cwdItems;
						if (data) {
							if (data.error) {
								cancel();
							} else {
								if (fm.options.overwriteUploadConfirm && fm.option('uploadOverwrite', target)) {
									if (data.list) {
										if (Array.isArray(data.list)) {
											existed = data.list || [];
										} else {
											existedArr = [];
											existed = jQuery.map(data.list, function(n) {
												if (typeof n === 'string') {
													return n;
												} else {
													// support to >=2.1.11 plugin Normalizer, Sanitizer
													existedArr = existedArr.concat(n);
													return false;
												}
											});
											if (existedArr.length) {
												existed = existed.concat(existedArr);
											}
											hashes = data.list;
										}
										exists = jQuery.grep(names, function(name){
											return jQuery.inArray(name.name, existed) !== -1 ? true : false ;
										});
										if (exists.length && existed.length && target == fm.cwd().hash) {
											cwdItems = jQuery.map(fm.files(target), function(file) { return file.name; } );
											if (jQuery.grep(existed, function(n) { 
												return jQuery.inArray(n, cwdItems) === -1? true : false;
											}).length){
												fm.sync();
											}
										}
									}
								}
							}
						}
						if (exists.length > 0) {
							confirm(0);
						} else {
							resolve();
						}
					})
					.fail(function(error) {
						cancel();
						resolve();
						error && fm.error(error);
					});
				};
			if (fm.api >= 2.1 && typeof files[0] == 'object') {
				check();
			} else {
				resolve();
			}
			return dfrd;
		},
		
		// check droped contents
		checkFile : function(data, fm, target) {
			if (!!data.checked || data.type == 'files') {
				return data.files;
			} else if (data.type == 'data') {
				var dfrd = jQuery.Deferred(),
				scanDfd = jQuery.Deferred(),
				files = [],
				paths = [],
				dirctorys = [],
				processing = 0,
				items,
				mkdirs = [],
				cancel = false,
				toArray = function(list) {
					return Array.prototype.slice.call(list || [], 0);
				},
				doScan = function(items) {
					var entry, readEntries,
						excludes = fm.options.folderUploadExclude[fm.OS] || null,
						length = items.length,
						check = function() {
							if (--processing < 1 && scanDfd.state() === 'pending') {
								scanDfd.resolve();
							}
						},
						pushItem = function(file) {
							if (! excludes || ! file.name.match(excludes)) {
								paths.push(entry.fullPath || '');
								files.push(file);
							}
							check();
						},
						readEntries = function(dirReader) {
							var entries = [],
								read = function() {
									dirReader.readEntries(function(results) {
										if (cancel || !results.length) {
											for (var i = 0; i < entries.length; i++) {
												if (cancel) {
													scanDfd.reject();
													break;
												}
												doScan([entries[i]]);
											}
											check();
										} else {
											entries = entries.concat(toArray(results));
											read();
										}
									}, check);
								};
							read();
						};
					
					processing++;
					for (var i = 0; i < length; i++) {
						if (cancel) {
							scanDfd.reject();
							break;
						}
						entry = items[i];
						if (entry) {
							if (entry.isFile) {
								processing++;
								entry.file(pushItem, check);
							} else if (entry.isDirectory) {
								if (fm.api >= 2.1) {
									processing++;
									mkdirs.push(entry.fullPath);
									readEntries(entry.createReader()); // Start reading dirs.
								}
							}
						}
					}
					check();
					return scanDfd;
				}, hasDirs;
				
				items = jQuery.map(data.files.items, function(item){
					return item.getAsEntry? item.getAsEntry() : item.webkitGetAsEntry();
				});
				jQuery.each(items, function(i, item) {
					if (item.isDirectory) {
						hasDirs = true;
						return false;
					}
				});
				if (items.length > 0) {
					fm.uploads.checkExists(items, target, fm, hasDirs).done(function(renames, hashes){
						var dfds = [];
						if (fm.options.overwriteUploadConfirm && fm.option('uploadOverwrite', target)) {
							if (renames === null) {
								data.overwrite = 0;
								renames = [];
							}
							items = jQuery.grep(items, function(item){
								var i, bak, hash, dfd, hi;
								if (item.isDirectory && renames.length) {
									i = jQuery.inArray(item.name, renames);
									if (i !== -1) {
										renames.splice(i, 1);
										bak = fm.uniqueName(item.name + fm.options.backupSuffix , null, '');
										jQuery.each(hashes, function(h, name) {
											if (item.name == name) {
												hash = h;
												return false;
											}
										});
										if (! hash) {
											hash = fm.fileByName(item.name, target).hash;
										}
										fm.lockfiles({files : [hash]});
										dfd = fm.request({
											data   : {cmd : 'rename', target : hash, name : bak},
											notify : {type : 'rename', cnt : 1}
										})
										.fail(function() {
											item._remove = true;
											fm.sync();
										})
										.always(function() {
											fm.unlockfiles({files : [hash]});
										});
										dfds.push(dfd);
									}
								}
								return !item._remove? true : false;
							});
						}
						jQuery.when.apply($, dfds).done(function(){
							var notifyto, msg,
								id = +new Date();
							
							if (items.length > 0) {
								msg = fm.escape(items[0].name);
								if (items.length > 1) {
									msg += ' ... ' + items.length + fm.i18n('items');
								}
								notifyto = setTimeout(function() {
									fm.notify({
										type : 'readdir',
										id : id,
										cnt : 1,
										hideCnt: true,
										msg : fm.i18n('ntfreaddir') + ' (' + msg + ')',
										cancel: function() {
											cancel = true;
										}
									});
								}, fm.options.notifyDelay);
								doScan(items).done(function() {
									notifyto && clearTimeout(notifyto);
									fm.notify({type : 'readdir', id: id, cnt : -1});
									if (cancel) {
										dfrd.reject();
									} else {
										dfrd.resolve([files, paths, renames, hashes, mkdirs]);
									}
								}).fail(function() {
									dfrd.reject();
								});
							} else {
								dfrd.reject();
							}
						});
					});
					return dfrd.promise();
				} else {
					return dfrd.reject();
				}
			} else {
				var ret = [];
				var check = [];
				var str = data.files[0];
				if (data.type == 'html') {
					var tmp = jQuery("<html></html>").append(jQuery.parseHTML(str.replace(/ src=/ig, ' _elfsrc='))),
						atag;
					jQuery('img[_elfsrc]', tmp).each(function(){
						var url, purl,
						self = jQuery(this),
						pa = self.closest('a');
						if (pa && pa.attr('href') && pa.attr('href').match(/\.(?:jpe?g|gif|bmp|png)/i)) {
							purl = pa.attr('href');
						}
						url = self.attr('_elfsrc');
						if (url) {
							if (purl) {
								jQuery.inArray(purl, ret) == -1 && ret.push(purl);
								jQuery.inArray(url, check) == -1 &&  check.push(url);
							} else {
								jQuery.inArray(url, ret) == -1 && ret.push(url);
							}
						}
						// Probably it's clipboard data
						if (ret.length === 1 && ret[0].match(/^data:image\/png/)) {
							data.clipdata = true;
						}
					});
					atag = jQuery('a[href]', tmp);
					atag.each(function(){
						var text, loc,
							parseUrl = function(url) {
								var a = document.createElement('a');
								a.href = url;
								return a;
							};
						if (text = jQuery(this).text()) {
							loc = parseUrl(jQuery(this).attr('href'));
							if (loc.href && loc.href.match(/^(?:ht|f)tp/i) && (atag.length === 1 || ! loc.pathname.match(/(?:\.html?|\/[^\/.]*)$/i) || jQuery.trim(text).match(/\.[a-z0-9-]{1,10}$/i))) {
								if (jQuery.inArray(loc.href, ret) == -1 && jQuery.inArray(loc.href, check) == -1) ret.push(loc.href);
							}
						}
					});
				} else {
					var regex, m, url;
					regex = /((?:ht|f)tps?:\/\/[-_.!~*\'()a-z0-9;/?:\@&=+\$,%#\*\[\]]+)/ig;
					while (m = regex.exec(str)) {
						url = m[1].replace(/&amp;/g, '&');
						if (jQuery.inArray(url, ret) == -1) ret.push(url);
					}
				}
				return ret;
			}
		},

		// upload transport using XMLHttpRequest
		xhr : function(data, fm) { 
			var self   = fm ? fm : this,
				node        = self.getUI(),
				xhr         = new XMLHttpRequest(),
				notifyto    = null,
				notifyto1   = null,
				notifyto2   = null,
				dataChecked = data.checked,
				isDataType  = (data.isDataType || data.type == 'data'),
				target      = (data.target || self.cwd().hash),
				dropEvt     = (data.dropEvt || null),
				extraData   = data.extraData || null,
				chunkEnable = (self.option('uploadMaxConn', target) != -1),
				multiMax    = Math.min(5, Math.max(1, self.option('uploadMaxConn', target))),
				retryWait   = 10000, // 10 sec
				retryMax    = 30, // 10 sec * 30 = 300 secs (Max 5 mins)
				retry       = 0,
				getFile     = function(files) {
					var dfd = jQuery.Deferred(),
						file;
					if (files.promise) {
						files.always(function(f) {
							dfd.resolve(Array.isArray(f) && f.length? (isDataType? f[0][0] : f[0]) : {});
						});
					} else {
						dfd.resolve(files.length? (isDataType? files[0][0] : files[0]) : {});
					}
					return dfd;
				},
				dfrd   = jQuery.Deferred()
					.fail(function(err) {
						var error = self.parseError(err),
							userAbort;
						if (error === 'userabort') {
							userAbort = true;
							error = void 0;
						}
						if (files && (self.uploads.xhrUploading || userAbort)) {
							// send request om fail
							getFile(files).done(function(file) {
								if (!userAbort) {
									triggerError(error, file);
								}
								if (! file._cid) {
									// send sync request
									self.uploads.failSyncTm && clearTimeout(self.uploads.failSyncTm);
									self.uploads.failSyncTm = setTimeout(function() {
										self.sync(target);
									}, 1000);
								} else if (! self.uploads.chunkfailReq[file._cid]) {
									// send chunkfail request
									self.uploads.chunkfailReq[file._cid] = true;
									setTimeout(function() {
										fm.request({
											data : {
												cmd: 'upload',
												target: target,
												chunk: file._chunk,
												cid: file._cid,
												upload: ['chunkfail'],
												mimes: 'chunkfail'
											},
											options : {
												type: 'post',
												url: self.uploadURL
											},
											preventDefault: true
										}).always(function() {
											delete self.uploads.chunkfailReq[file._chunk];
										});
									}, 1000);
								}
							});
						} else {
							triggerError(error);
						}
						!userAbort && self.sync();
						self.uploads.xhrUploading = false;
						files = null;
					})
					.done(function(data) {
						self.uploads.xhrUploading = false;
						files = null;
						if (data) {
							self.currentReqCmd = 'upload';
							data.warning && triggerError(data.warning);
							self.updateCache(data);
							data.removed && data.removed.length && self.remove(data);
							data.added   && data.added.length   && self.add(data);
							data.changed && data.changed.length && self.change(data);
							self.trigger('upload', data, false);
							self.trigger('uploaddone');
							if (data.toasts && Array.isArray(data.toasts)) {
								jQuery.each(data.toasts, function() {
									this.msg && self.toast(this);
								});
							}
							data.sync && self.sync();
							if (data.debug) {
								self.responseDebug(data);
								fm.debug('backend-debug', data);
							}
						}
					})
					.always(function() {
						self.abortXHR(xhr);
						// unregist fnAbort function
						node.off('uploadabort', fnAbort);
						jQuery(window).off('unload', fnAbort);
						notifyto && clearTimeout(notifyto);
						notifyto1 && clearTimeout(notifyto1);
						notifyto2 && clearTimeout(notifyto2);
						dataChecked && !data.multiupload && checkNotify() && self.notify({type : 'upload', cnt : -cnt, progress : 0, size : 0});
						notifyto1 && uploadedNtf && self.notify({type : 'chunkmerge', cnt : -cnt});
						chunkMerge && notifyElm.children('.elfinder-notify-chunkmerge').length && self.notify({type : 'chunkmerge', cnt : -1});
					}),
				formData    = new FormData(),
				files       = data.input ? data.input.files : self.uploads.checkFile(data, self, target), 
				cnt         = data.checked? (isDataType? files[0].length : files.length) : files.length,
				isChunked   = false,
				loaded      = 0,
				prev        = 0,
				filesize    = 0,
				notify      = false,
				notifyElm   = self.ui.notify,
				cancelBtn   = true,
				uploadedNtf = false,
				abort       = false,
				checkNotify = function() {
					if (!notify && (ntfUpload = notifyElm.children('.elfinder-notify-upload')).length) {
						notify = true;
					}
					return notify;
				},
				fnAbort     = function(e, error) {
					abort = true;
					self.abortXHR(xhr, { quiet: true, abort: true });
					dfrd.reject(error);
					if (checkNotify()) {
						self.notify({type : 'upload', cnt : ntfUpload.data('cnt') * -1, progress : 0, size : 0});
					}
				},
				cancelToggle = function(show, hasChunk) {
					ntfUpload.children('.elfinder-notify-cancel')[show? 'show':'hide']();
					cancelBtn = show;
				},
				startNotify = function(size) {
					if (!size) size = filesize;
					return setTimeout(function() {
						notify = true;
						self.notify({type : 'upload', cnt : cnt, progress : loaded - prev, size : size,
							cancel: function() {
								node.trigger('uploadabort', 'userabort');
							}
						});
						ntfUpload = notifyElm.children('.elfinder-notify-upload');
						prev = loaded;
						if (data.multiupload) {
							cancelBtn && cancelToggle(true);
						} else {
							cancelToggle(cancelBtn && loaded < size);
						}
					}, self.options.notifyDelay);
				},
				doRetry = function() {
					if (retry++ <= retryMax) {
						if (checkNotify() && prev) {
							self.notify({type : 'upload', cnt : 0, progress : 0, size : prev});
						}
						self.abortXHR(xhr, { quiet: true });
						prev = loaded = 0;
						setTimeout(function() {
							var reqId;
							if (! abort) {
								xhr.open('POST', self.uploadURL, true);
								if (self.api >= 2.1029) {
									reqId = (+ new Date()).toString(16) + Math.floor(1000 * Math.random()).toString(16);
									(typeof formData['delete'] === 'function') && formData['delete']('reqid');
									formData.append('reqid', reqId);
									xhr._requestId = reqId;
								}
								xhr.send(formData);
							}
						}, retryWait);
					} else {
						node.trigger('uploadabort', ['errAbort', 'errTimeout']);
					}
				},
				progress = function() {
					var node;
					if (notify) {
						dfrd.notifyWith(ntfUpload, [{
							cnt: ntfUpload.data('cnt'),
							progress: ntfUpload.data('progress'),
							total: ntfUpload.data('total')
						}]);
					}
				},
				triggerError = function(err, file, unite) {
					err && self.trigger('xhruploadfail', { error: err, file: file });
					if (unite) {
						if (err) {
							if (errCnt < self.options.maxErrorDialogs) {
								if (Array.isArray(err)) {
									errors = errors.concat(err);
								} else {
									errors.push(err);
								}
							}
							errCnt++;
						}
					} else {
						if (err) {
							self.error(err);
						} else {
							if (errors.length) {
								if (errCnt >= self.options.maxErrorDialogs) {
									errors = errors.concat('moreErrors', errCnt - self.options.maxErrorDialogs);
								}
								self.error(errors);
							}
							errors = [];
							errCnt = 0;
						}
					}
				},
				errors = [],
				errCnt = 0,
				renames = (data.renames || null),
				hashes = (data.hashes || null),
				chunkMerge = false,
				ntfUpload = jQuery();
			
			// regist fnAbort function
			node.one('uploadabort', fnAbort);
			jQuery(window).one('unload.' + fm.namespace, fnAbort);
			
			!chunkMerge && (prev = loaded);
			
			if (!isDataType && !cnt) {
				return dfrd.reject(['errUploadNoFiles']);
			}
			
			xhr.addEventListener('error', function() {
				if (xhr.status == 0) {
					if (abort) {
						dfrd.reject();
					} else {
						// ff bug while send zero sized file
						// for safari - send directory
						if (!isDataType && data.files && jQuery.grep(data.files, function(f){return ! f.type && f.size === (self.UA.Safari? 1802 : 0)? true : false;}).length) {
							dfrd.reject(['errAbort', 'errFolderUpload']);
						} else if (data.input && jQuery.grep(data.input.files, function(f){return ! f.type && f.size === (self.UA.Safari? 1802 : 0)? true : false;}).length) {
							dfrd.reject(['errUploadNoFiles']);
						} else {
							doRetry();
						}
					}
				} else {
					node.trigger('uploadabort', 'errConnect');
				}
			}, false);
			
			xhr.addEventListener('load', function(e) {
				var status = xhr.status, res, curr = 0, error = '', errData, errObj;
				
				self.setCustomHeaderByXhr(xhr);

				if (status >= 400) {
					if (status > 500) {
						error = 'errResponse';
					} else {
						error = ['errResponse', 'errServerError'];
					}
				} else {
					if (!xhr.responseText) {
						error = ['errResponse', 'errDataEmpty'];
					}
				}
				
				if (error) {
					node.trigger('uploadabort');
					getFile(files || {}).done(function(file) {
						return dfrd.reject(file._cid? null : error);
					});
				}
				
				loaded = filesize;
				
				if (checkNotify() && (curr = loaded - prev)) {
					self.notify({type : 'upload', cnt : 0, progress : curr, size : 0});
					progress();
				}

				res = self.parseUploadData(xhr.responseText);
				
				// chunked upload commit
				if (res._chunkmerged) {
					formData = new FormData();
					var _file = [{_chunkmerged: res._chunkmerged, _name: res._name, _mtime: res._mtime}];
					chunkMerge = true;
					node.off('uploadabort', fnAbort);
					notifyto2 = setTimeout(function() {
						self.notify({type : 'chunkmerge', cnt : 1});
					}, self.options.notifyDelay);
					isDataType? send(_file, files[1]) : send(_file);
					return;
				}
				
				res._multiupload = data.multiupload? true : false;
				if (res.error) {
					errData = {
						cmd: 'upload',
						err: res,
						xhr: xhr,
						rc: xhr.status
					};
					self.trigger('uploadfail', res);
					// trigger "requestError" event
					self.trigger('requestError', errData);
					if (errData._getEvent && errData._getEvent().isDefaultPrevented()) {
						res.error = '';
					}
					if (res._chunkfailure || res._multiupload) {
						abort = true;
						self.uploads.xhrUploading = false;
						notifyto && clearTimeout(notifyto);
						if (ntfUpload.length) {
							self.notify({type : 'upload', cnt : -cnt, progress : 0, size : 0});
							dfrd.reject(res);
						} else {
							// for multi connection
							dfrd.reject();
						}
					} else {
						dfrd.reject(res);
					}
				} else {
					dfrd.resolve(res);
				}
			}, false);
			
			xhr.upload.addEventListener('loadstart', function(e) {
				if (!chunkMerge && e.lengthComputable) {
					loaded = e.loaded;
					retry && (loaded = 0);
					filesize = e.total;
					if (!loaded) {
						loaded = parseInt(filesize * 0.05);
					}
					if (checkNotify()) {
						self.notify({type : 'upload', cnt : 0, progress : loaded - prev, size : data.multiupload? 0 : filesize});
						prev = loaded;
						progress();
					}
				}
			}, false);
			
			xhr.upload.addEventListener('progress', function(e) {
				var curr;

				if (e.lengthComputable && !chunkMerge && xhr.readyState < 2) {
					
					loaded = e.loaded;

					// to avoid strange bug in safari (not in chrome) with drag&drop.
					// bug: macos finder opened in any folder,
					// reset safari cache (option+command+e), reload elfinder page,
					// drop file from finder
					// on first attempt request starts (progress callback called ones) but never ends.
					// any next drop - successfull.
					if (!data.checked && loaded > 0 && !notifyto) {
						notifyto = startNotify(xhr._totalSize - loaded);
					}
					
					if (!filesize) {
						filesize = e.total;
						if (!loaded) {
							loaded = parseInt(filesize * 0.05);
						}
					}
					
					curr = loaded - prev;
					if (checkNotify() && (curr/e.total) >= 0.05) {
						self.notify({type : 'upload', cnt : 0, progress : curr, size : 0});
						prev = loaded;
						progress();
					}
					
					if (!uploadedNtf && loaded >= filesize && !isChunked) {
						// Use "chunkmerge" for "server-in-process" notification
						uploadedNtf = true;
						notifyto1 = setTimeout(function() {
							self.notify({type : 'chunkmerge', cnt : cnt});
						}, self.options.notifyDelay);
					}

					if (cancelBtn && ! data.multiupload && loaded >= filesize) {
						checkNotify() && cancelToggle(false);
					}
				}
			}, false);
			
			var send = function(files, paths){
				var size = 0,
				fcnt = 1,
				sfiles = [],
				c = 0,
				total = cnt,
				maxFileSize,
				totalSize = 0,
				chunked = [],
				chunkID = new Date().getTime().toString().substr(-9), // for take care of the 32bit backend system
				BYTES_PER_CHUNK = Math.min((fm.uplMaxSize? fm.uplMaxSize : 2097152) - 8190, fm.options.uploadMaxChunkSize), // uplMaxSize margin 8kb or options.uploadMaxChunkSize
				blobSlice = chunkEnable? false : '',
				blobSize, blobMtime, blobName, i, start, end, chunks, blob, chunk, added, done, last, failChunk,
				multi = function(files, num){
					var sfiles = [], cid, sfilesLen = 0, cancelChk, hasChunk;
					if (!abort) {
						while(files.length && sfiles.length < num) {
							sfiles.push(files.shift());
						}
						sfilesLen = sfiles.length;
						if (sfilesLen) {
							cancelChk = sfilesLen;
							for (var i=0; i < sfilesLen; i++) {
								if (abort) {
									break;
								}
								cid = isDataType? (sfiles[i][0][0]._cid || null) : (sfiles[i][0]._cid || null);
								hasChunk = (hasChunk || cid)? true : false;
								if (!!failChunk[cid]) {
									last--;
									continue;
								}
								fm.exec('upload', {
									type: data.type,
									isDataType: isDataType,
									files: sfiles[i],
									checked: true,
									target: target,
									dropEvt: dropEvt,
									renames: renames,
									hashes: hashes,
									multiupload: true,
									overwrite: data.overwrite === 0? 0 : void 0,
									clipdata: data.clipdata
								}, void 0, target)
								.fail(function(error) {
									if (error && error === 'No such command') {
										abort = true;
										fm.error(['errUpload', 'errPerm']);
									}
									if (cid) {	
										failChunk[cid] = true;
									}
								})
								.always(function(e) {
									if (e && e.added) added = jQuery.merge(added, e.added);
									if (last <= ++done) {
										fm.trigger('multiupload', {added: added});
										notifyto && clearTimeout(notifyto);
										if (checkNotify()) {
											self.notify({type : 'upload', cnt : -cnt, progress : 0, size : 0});
										}
									}
									if (files.length) {
										multi(files, 1); // Next one
									} else {
										if (--cancelChk <= 1) {
											if (cancelBtn) {
												cancelToggle(false, hasChunk);
											}
										}
										dfrd.resolve();
									}
								});
							}
						}
					}
					if (sfiles.length < 1 || abort) {
						if (abort) {
							notifyto && clearTimeout(notifyto);
							if (cid) {
								failChunk[cid] = true;
							}
							dfrd.reject();
						} else {
							dfrd.resolve();
							self.uploads.xhrUploading = false;
						}
					}
				},
				check = function(){
					if (!self.uploads.xhrUploading) {
						self.uploads.xhrUploading = true;
						multi(sfiles, multiMax); // Max connection: 3
					} else {
						setTimeout(check, 100);
					}
				},
				reqId, err;

				if (! dataChecked && (isDataType || data.type == 'files')) {
					if (! (maxFileSize = fm.option('uploadMaxSize', target))) {
						maxFileSize = 0;
					}
					for (i=0; i < files.length; i++) {
						try {
							blob = files[i];
							blobSize = blob.size;
							if (blobSlice === false) {
								blobSlice = '';
								if (self.api >= 2.1) {
									if ('slice' in blob) {
										blobSlice = 'slice';
									} else if ('mozSlice' in blob) {
										blobSlice = 'mozSlice';
									} else if ('webkitSlice' in blob) {
										blobSlice = 'webkitSlice';
									}
								}
							}
						} catch(e) {
							cnt--;
							total--;
							continue;
						}
						
						// file size check
						if ((maxFileSize && blobSize > maxFileSize) || (!blobSlice && fm.uplMaxSize && blobSize > fm.uplMaxSize)) {
							triggerError(['errUploadFile', blob.name, 'errUploadFileSize'], blob, true);
							cnt--;
							total--;
							continue;
						}
						
						// file mime check
						if (blob.type && ! self.uploadMimeCheck(blob.type, target)) {
							triggerError(['errUploadFile', blob.name, 'errUploadMime', '(' + blob.type + ')'], blob, true);
							cnt--;
							total--;
							continue;
						}
						
						if (blobSlice && blobSize > BYTES_PER_CHUNK) {
							start = 0;
							end = BYTES_PER_CHUNK;
							chunks = -1;
							total = Math.floor((blobSize - 1) / BYTES_PER_CHUNK);
							blobMtime = blob.lastModified? Math.round(blob.lastModified/1000) : 0;
							blobName = data.clipdata? fm.date(fm.nonameDateFormat) + '.png' : blob.name;

							totalSize += blobSize;
							chunked[chunkID] = 0;
							while(start < blobSize) {
								chunk = blob[blobSlice](start, end);
								chunk._chunk = blobName + '.' + (++chunks) + '_' + total + '.part';
								chunk._cid   = chunkID;
								chunk._range = start + ',' + chunk.size + ',' + blobSize;
								chunk._mtime = blobMtime;
								chunked[chunkID]++;
								
								if (size) {
									c++;
								}
								if (typeof sfiles[c] == 'undefined') {
									sfiles[c] = [];
									if (isDataType) {
										sfiles[c][0] = [];
										sfiles[c][1] = [];
									}
								}
								size = BYTES_PER_CHUNK;
								fcnt = 1;
								if (isDataType) {
									sfiles[c][0].push(chunk);
									sfiles[c][1].push(paths[i]);
								} else {
									sfiles[c].push(chunk);
								}

								start = end;
								end = start + BYTES_PER_CHUNK;
							}
							if (chunk == null) {
								triggerError(['errUploadFile', blob.name, 'errUploadFileSize'], blob, true);
								cnt--;
								total--;
							} else {
								total += chunks;
								size = 0;
								fcnt = 1;
								c++;
							}
							continue;
						}
						if ((fm.uplMaxSize && size + blobSize > fm.uplMaxSize) || fcnt > fm.uplMaxFile) {
							size = 0;
							fcnt = 1;
							c++;
						}
						if (typeof sfiles[c] == 'undefined') {
							sfiles[c] = [];
							if (isDataType) {
								sfiles[c][0] = [];
								sfiles[c][1] = [];
							}
						}
						if (isDataType) {
							sfiles[c][0].push(blob);
							sfiles[c][1].push(paths[i]);
						} else {
							sfiles[c].push(blob);
						}
						size += blobSize;
						totalSize += blobSize;
						fcnt++;
					}
					
					if (errors.length) {
						triggerError();
					}

					if (sfiles.length == 0) {
						// no data
						data.checked = true;
						return false;
					}
					
					if (sfiles.length > 1) {
						// multi upload
						notifyto = startNotify(totalSize);
						added = [];
						done = 0;
						last = sfiles.length;
						failChunk = [];
						check();
						return true;
					}
					
					// single upload
					if (isDataType) {
						files = sfiles[0][0];
						paths = sfiles[0][1];
					} else {
						files = sfiles[0];
					}
				}
				
				if (!dataChecked) {
					if (!fm.UA.Safari || !data.files) {
						notifyto = startNotify(totalSize);
					} else {
						xhr._totalSize = totalSize;
					}
				}
				
				dataChecked = true;
				
				if (! files.length) {
					dfrd.reject(['errUploadNoFiles']);
				}
				
				xhr.open('POST', self.uploadURL, true);
				
				// set request headers
				if (fm.customHeaders) {
					jQuery.each(fm.customHeaders, function(key) {
						xhr.setRequestHeader(key, this);
					});
				}
				
				// set xhrFields
				if (fm.xhrFields) {
					jQuery.each(fm.xhrFields, function(key) {
						if (key in xhr) {
							xhr[key] = this;
						}
					});
				}

				if (self.api >= 2.1029) {
					// request ID
					reqId = (+ new Date()).toString(16) + Math.floor(1000 * Math.random()).toString(16);
					formData.append('reqid', reqId);
					xhr._requestId = reqId;
				}
				formData.append('cmd', 'upload');
				formData.append(self.newAPI ? 'target' : 'current', target);
				if (renames && renames.length) {
					jQuery.each(renames, function(i, v) {
						formData.append('renames[]', v);
					});
					formData.append('suffix', fm.options.backupSuffix);
				}
				if (hashes) {
					jQuery.each(hashes, function(i, v) {
						formData.append('hashes['+ i +']', v);
					});
				}
				jQuery.each(self.customData, function(key, val) {
					formData.append(key, val);
				});
				jQuery.each(self.options.onlyMimes, function(i, mime) {
					formData.append('mimes[]', mime);
				});
				
				jQuery.each(files, function(i, file) {
					var name, relpath;
					if (file._chunkmerged) {
						formData.append('chunk', file._chunkmerged);
						formData.append('upload[]', file._name);
						formData.append('mtime[]', file._mtime);
						data.clipdata && formData.append('overwrite', 0);
						isChunked = true;
					} else {
						if (file._chunkfail) {
							formData.append('upload[]', 'chunkfail');
							formData.append('mimes', 'chunkfail');
						} else {
							if (data.clipdata) {
								if (!file._chunk) {
									data.overwrite = 0;
									name = fm.date(fm.nonameDateFormat) + '.png';
								}
							} else {
								if (file.name) {
									name = file.name;
									if (fm.UA.iOS) {
										if (name.match(/^image\.jpe?g$/i)) {
											data.overwrite = 0;
											name = fm.date(fm.nonameDateFormat) + '.jpg';
										} else if (name.match(/^capturedvideo\.mov$/i)) {
											data.overwrite = 0;
											name = fm.date(fm.nonameDateFormat) + '.mov';
										}
									}
									relpath = (file.webkitRelativePath || file.relativePath || file._relativePath || '').replace(/[^\/]+$/, '');
									name = relpath + name;
								}
							}
							name? formData.append('upload[]', file, name) : formData.append('upload[]', file);
						}
						if (file._chunk) {
							formData.append('chunk', file._chunk);
							formData.append('cid'  , file._cid);
							formData.append('range', file._range);
							formData.append('mtime[]', file._mtime);
							isChunked = true;
						} else {
							formData.append('mtime[]', file.lastModified? Math.round(file.lastModified/1000) : 0);
						}
					}
				});
				
				if (isDataType) {
					jQuery.each(paths, function(i, path) {
						formData.append('upload_path[]', path);
					});
				}
				
				if (data.overwrite === 0) {
					formData.append('overwrite', 0);
				}
				
				// send int value that which meta key was pressed when dropped  as `dropWith`
				if (dropEvt) {
					formData.append('dropWith', parseInt(
						(dropEvt.altKey  ? '1' : '0')+
						(dropEvt.ctrlKey ? '1' : '0')+
						(dropEvt.metaKey ? '1' : '0')+
						(dropEvt.shiftKey? '1' : '0'), 2));
				}
				
				// set extraData on current request
				if (extraData) {
					jQuery.each(extraData, function(key, val) {
						formData.append(key, val);
					});
				}

				xhr.send(formData);
				
				return true;
			};
			
			if (! isDataType) {
				if (files.length > 0) {
					if (! data.clipdata && renames == null) {
						var mkdirs = [],
							paths = [],
							excludes = fm.options.folderUploadExclude[fm.OS] || null;
						jQuery.each(files, function(i, file) {
							var relPath = file.webkitRelativePath || file.relativePath || '',
								idx, rootDir;
							if (! relPath) {
								return false;
							}
							if (excludes && file.name.match(excludes)) {
								file._remove = true;
								relPath = void(0);
							} else {
								// add '/' as prefix to make same to folder uploading with DnD, see #2607
								relPath = '/' + relPath.replace(/\/[^\/]*$/, '').replace(/^\//, '');
								if (relPath && jQuery.inArray(relPath, mkdirs) === -1) {
									mkdirs.push(relPath);
									// checking the root directory to supports <input type="file" webkitdirectory> see #2378
									idx = relPath.substr(1).indexOf('/');
									if (idx !== -1 && (rootDir = relPath.substr(0, idx + 1)) && jQuery.inArray(rootDir, mkdirs) === -1) {
										mkdirs.unshift(rootDir);
									}
								}
							}
							paths.push(relPath);
						});
						renames = [];
						hashes = {};
						if (mkdirs.length) {
							(function() {
								var checkDirs = jQuery.map(mkdirs, function(name) { return name.substr(1).indexOf('/') === -1 ? {name: name.substr(1)} : null;}),
									cancelDirs = [];
								fm.uploads.checkExists(checkDirs, target, fm, true).done(
									function(res, res2) {
										var dfds = [], dfd, bak, hash;
										if (fm.options.overwriteUploadConfirm && fm.option('uploadOverwrite', target)) {
											cancelDirs = jQuery.map(checkDirs, function(dir) { return dir._remove? dir.name : null ;} );
											checkDirs = jQuery.grep(checkDirs, function(dir) { return !dir._remove? true : false ;} );
										}
										if (cancelDirs.length) {
											jQuery.each(paths.concat(), function(i, path) {
												if (jQuery.inArray(path, cancelDirs) === 0) {
													files[i]._remove = true;
													paths[i] = void(0);
												}
											});
										}
										files = jQuery.grep(files, function(file) { return file._remove? false : true; });
										paths = jQuery.grep(paths, function(path) { return path === void 0 ? false : true; });
										if (checkDirs.length) {
											dfd = jQuery.Deferred();
											if (res.length) {
												jQuery.each(res, function(i, existName) {
													// backup
													bak = fm.uniqueName(existName + fm.options.backupSuffix , null, '');
													jQuery.each(res2, function(h, name) {
														if (res[0] == name) {
															hash = h;
															return false;
														}
													});
													if (! hash) {
														hash = fm.fileByName(res[0], target).hash;
													}
													fm.lockfiles({files : [hash]});
													dfds.push(
														fm.request({
															data   : {cmd : 'rename', target : hash, name : bak},
															notify : {type : 'rename', cnt : 1}
														})
														.fail(function(error) {
															dfrd.reject(error);
															fm.sync();
														})
														.always(function() {
															fm.unlockfiles({files : [hash]});
														})
													);
												});
											} else {
												dfds.push(null);
											}
											
											jQuery.when.apply($, dfds).done(function() {
												// ensure directories
												fm.request({
													data   : {cmd : 'mkdir', target : target, dirs : mkdirs},
													notify : {type : 'mkdir', cnt : mkdirs.length},
													preventFail: true
												})
												.fail(function(error) {
													error = error || ['errUnknown'];
													if (error[0] === 'errCmdParams') {
														multiMax = 1;
													} else {
														multiMax = 0;
														dfrd.reject(error);
													}
												})
												.done(function(data) {
													var rm = false;
													if (!data.hashes) {
														data.hashes = {};
													}
													paths = jQuery.map(paths.concat(), function(p, i) {
														if (p === '/') {
															return target;
														} else {
															if (data.hashes[p]) {
																return data.hashes[p];
															} else {
																rm = true;
																files[i]._remove = true;
																return null;
															}
														}
													});
													if (rm) {
														files = jQuery.grep(files, function(file) { return file._remove? false : true; });
													}
												})
												.always(function(data) {
													if (multiMax) {
														isDataType = true;
														if (! send(files, paths)) {
															dfrd.reject();
														}
													}
												});
											});
										} else {
											dfrd.reject();
										}
									}
								);
							})();
						} else {
							fm.uploads.checkExists(files, target, fm).done(
								function(res, res2){
									if (fm.options.overwriteUploadConfirm && fm.option('uploadOverwrite', target)) {
										hashes = res2;
										if (res === null) {
											data.overwrite = 0;
										} else {
											renames = res;
										}
										files = jQuery.grep(files, function(file){return !file._remove? true : false ;});
									}
									cnt = files.length;
									if (cnt > 0) {
										if (! send(files)) {
											dfrd.reject();
										}
									} else {
										dfrd.reject();
									}
								}
							);
						}
					} else {
						if (! send(files)) {
							dfrd.reject();
						}
					}
				} else {
					dfrd.reject();
				}
			} else {
				if (dataChecked) {
					send(files[0], files[1]);
				} else {
					files.done(function(result) { // result: [files, paths, renames, hashes, mkdirs]
						renames = [];
						cnt = result[0].length;
						if (cnt) {
							if (result[4] && result[4].length) {
								// ensure directories
								fm.request({
									data   : {cmd : 'mkdir', target : target, dirs : result[4]},
									notify : {type : 'mkdir', cnt : result[4].length},
									preventFail: true
								})
								.fail(function(error) {
									error = error || ['errUnknown'];
									if (error[0] === 'errCmdParams') {
										multiMax = 1;
									} else {
										multiMax = 0;
										dfrd.reject(error);
									}
								})
								.done(function(data) {
									var rm = false;
									if (!data.hashes) {
										data.hashes = {};
									}
									result[1] = jQuery.map(result[1], function(p, i) {
										result[0][i]._relativePath = p.replace(/^\//, '');
										p = p.replace(/\/[^\/]*$/, '');
										if (p === '') {
											return target;
										} else {
											if (data.hashes[p]) {
												return data.hashes[p];
											} else {
												rm = true;
												result[0][i]._remove = true;
												return null;
											}
										}
									});
									if (rm) {
										result[0] = jQuery.grep(result[0], function(file) { return file._remove? false : true; });
									}
								})
								.always(function(data) {
									if (multiMax) {
										renames = result[2];
										hashes = result[3];
										send(result[0], result[1]);
									}
								});
								return;
							} else {
								result[1] = jQuery.map(result[1], function() { return target; });
							}
							renames = result[2];
							hashes = result[3];
							send(result[0], result[1]);
						} else {
							dfrd.reject(['errUploadNoFiles']);
						}
					}).fail(function(){
						dfrd.reject();
					});
				}
			}

			return dfrd;
		},
		
		// upload transport using iframe
		iframe : function(data, fm) { 
			var self   = fm ? fm : this,
				input  = data.input? data.input : false,
				files  = !input ? self.uploads.checkFile(data, self) : false,
				dfrd   = jQuery.Deferred()
					.fail(function(error) {
						error && self.error(error);
					}),
				name = 'iframe-'+fm.namespace+(++self.iframeCnt),
				form = jQuery('<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>'),
				msie = this.UA.IE,
				// clear timeouts, close notification dialog, remove form/iframe
				onload = function() {
					abortto  && clearTimeout(abortto);
					notifyto && clearTimeout(notifyto);
					notify   && self.notify({type : 'upload', cnt : -cnt});
					
					setTimeout(function() {
						msie && jQuery('<iframe src="javascript:false;"></iframe>').appendTo(form);
						form.remove();
						iframe.remove();
					}, 100);
				},
				iframe = jQuery('<iframe src="'+(msie ? 'javascript:false;' : 'about:blank')+'" name="'+name+'" style="position:absolute;left:-1000px;top:-1000px" ></iframe>')
					.on('load', function() {
						iframe.off('load')
							.on('load', function() {
								onload();
								// data will be processed in callback response or window onmessage
								dfrd.resolve();
							});
							
							// notify dialog
							notifyto = setTimeout(function() {
								notify = true;
								self.notify({type : 'upload', cnt : cnt});
							}, self.options.notifyDelay);
							
							// emulate abort on timeout
							if (self.options.iframeTimeout > 0) {
								abortto = setTimeout(function() {
									onload();
									dfrd.reject(['errConnect', 'errTimeout']);
								}, self.options.iframeTimeout);
							}
							
							form.submit();
					}),
				target  = (data.target || self.cwd().hash),
				names   = [],
				dfds    = [],
				renames = [],
				hashes  = {},
				cnt, notify, notifyto, abortto;

			if (files && files.length) {
				jQuery.each(files, function(i, val) {
					form.append('<input type="hidden" name="upload[]" value="'+val+'"/>');
				});
				cnt = 1;
			} else if (input && jQuery(input).is(':file') && jQuery(input).val()) {
				if (fm.options.overwriteUploadConfirm && fm.option('uploadOverwrite', target)) {
					names = input.files? input.files : [{ name: jQuery(input).val().replace(/^(?:.+[\\\/])?([^\\\/]+)$/, '$1') }];
					//names = jQuery.map(names, function(file){return file.name? { name: file.name } : null ;});
					dfds.push(self.uploads.checkExists(names, target, self).done(
						function(res, res2){
							hashes = res2;
							if (res === null) {
								data.overwrite = 0;
							} else{
								renames = res;
								cnt = jQuery.grep(names, function(file){return !file._remove? true : false ;}).length;
								if (cnt != names.length) {
									cnt = 0;
								}
							}
						}
					));
				}
				cnt = input.files ? input.files.length : 1;
				form.append(input);
			} else {
				return dfrd.reject();
			}
			
			jQuery.when.apply($, dfds).done(function() {
				if (cnt < 1) {
					return dfrd.reject();
				}
				form.append('<input type="hidden" name="'+(self.newAPI ? 'target' : 'current')+'" value="'+target+'"/>')
					.append('<input type="hidden" name="html" value="1"/>')
					.append('<input type="hidden" name="node" value="'+self.id+'"/>')
					.append(jQuery(input).attr('name', 'upload[]'));
				
				if (renames.length > 0) {
					jQuery.each(renames, function(i, rename) {
						form.append('<input type="hidden" name="renames[]" value="'+self.escape(rename)+'"/>');
					});
					form.append('<input type="hidden" name="suffix" value="'+fm.options.backupSuffix+'"/>');
				}
				if (hashes) {
					jQuery.each(renames, function(i, v) {
						form.append('<input type="hidden" name="['+i+']" value="'+self.escape(v)+'"/>');
					});
				}
				
				if (data.overwrite === 0) {
					form.append('<input type="hidden" name="overwrite" value="0"/>');
				}
				
				jQuery.each(self.options.onlyMimes||[], function(i, mime) {
					form.append('<input type="hidden" name="mimes[]" value="'+self.escape(mime)+'"/>');
				});
				
				jQuery.each(self.customData, function(key, val) {
					form.append('<input type="hidden" name="'+key+'" value="'+self.escape(val)+'"/>');
				});
				
				form.appendTo('body');
				iframe.appendTo('body');
			});
			
			return dfrd;
		}
	},
	
	
	/**
	 * Bind callback to event(s) The callback is executed at most once per event.
	 * To bind to multiply events at once, separate events names by space
	 *
	 * @param  String    event name
	 * @param  Function  callback
	 * @param  Boolan    priority first
	 * @return elFinder
	 */
	one : function(ev, callback, priorityFirst) {
		var self  = this,
			event = ev.toLowerCase(),
			h     = function(e, f) {
				if (!self.toUnbindEvents[event]) {
					self.toUnbindEvents[event] = [];
				}
				self.toUnbindEvents[event].push({
					type: event,
					callback: h
				});
				return (callback.done? callback.done : callback).apply(this, arguments);
			};
		if (callback.done) {
			h = {done: h};
		}
		return this.bind(event, h, priorityFirst);
	},
	
	/**
	 * Set/get data into/from localStorage
	 *
	 * @param  String       key
	 * @param  String|void  value
	 * @return String|null
	 */
	localStorage : function(key, val) {
		var self   = this,
			s      = window.localStorage,
			oldkey = 'elfinder-'+(key || '')+this.id, // old key of elFinder < 2.1.6
			prefix = window.location.pathname+'-elfinder-',
			suffix = this.id,
			clrs   = [],
			retval, oldval, t, precnt, sufcnt;

		// reset this node data
		if (typeof(key) === 'undefined') {
			precnt = prefix.length;
			sufcnt = suffix.length * -1;
			jQuery.each(s, function(key) {
				if (key.substr(0, precnt) === prefix && key.substr(sufcnt) === suffix) {
					clrs.push(key);
				}
			});
			jQuery.each(clrs, function(i, key) {
				s.removeItem(key);
			});
			return true;
		}
		
		// new key of elFinder >= 2.1.6
		key = prefix+key+suffix;
		
		if (val === null) {
			return s.removeItem(key);
		}
		
		if (val === void(0) && !(retval = s.getItem(key)) && (oldval = s.getItem(oldkey))) {
			val = oldval;
			s.removeItem(oldkey);
		}
		
		if (val !== void(0)) {
			t = typeof val;
			if (t !== 'string' && t !== 'number') {
				val = JSON.stringify(val);
			}
			try {
				s.setItem(key, val);
			} catch (e) {
				try {
					s.clear();
					s.setItem(key, val);
				} catch (e) {
					self.debug('error', e.toString());
				}
			}
			retval = s.getItem(key);
		}

		if (retval && (retval.substr(0,1) === '{' || retval.substr(0,1) === '[')) {
			try {
				return JSON.parse(retval);
			} catch(e) {}
		}
		return retval;
	},

	/**
	 * Set/get data into/from sessionStorage
	 *
	 * @param  String       key
	 * @param  String|void  value
	 * @return String|null
	 */
	sessionStorage : function(key, val) {
		var self   = this,
			s, retval, t;

		try {
			s = window.sessionStorage;
		} catch(e) {}

		if (!s) {
			return;
		}

		if (val === null) {
			return s.removeItem(key);
		}

		if (val !== void(0)) {
			t = typeof val;
			if (t !== 'string' && t !== 'number') {
				val = JSON.stringify(val);
			}
			try {
				s.setItem(key, val);
			} catch (e) {
				try {
					s.clear();
					s.setItem(key, val);
				} catch (e) {
					self.debug('error', e.toString());
				}
			}
		}
		retval = s.getItem(key);

		if (retval && (retval.substr(0,1) === '{' || retval.substr(0,1) === '[')) {
			try {
				return JSON.parse(retval);
			} catch(e) {}
		}
		return retval;
	},

	/**
	 * Get/set cookie
	 *
	 * @param  String       cookie name
	 * @param  String|void  cookie value
	 * @return String|null
	 */
	cookie : function(name, value) {
		var d, o, c, i, retval, t;

		name = 'elfinder-'+name+this.id;

		if (value === void(0)) {
			if (this.cookieEnabled && document.cookie && document.cookie != '') {
				c = document.cookie.split(';');
				name += '=';
				for (i=0; i<c.length; i++) {
					c[i] = jQuery.trim(c[i]);
					if (c[i].substring(0, name.length) == name) {
						retval = decodeURIComponent(c[i].substring(name.length));
						if (retval.substr(0,1) === '{' || retval.substr(0,1) === '[') {
							try {
								return JSON.parse(retval);
							} catch(e) {}
						}
						return retval;
					}
				}
			}
			return null;
		}

		if (!this.cookieEnabled) {
			return '';
		}

		o = Object.assign({}, this.options.cookie);
		if (value === null) {
			value = '';
			o.expires = -1;
		} else {
			t = typeof value;
			if (t !== 'string' && t !== 'number') {
				value = JSON.stringify(value);
			}
		}
		if (typeof(o.expires) == 'number') {
			d = new Date();
			d.setTime(d.getTime()+(o.expires * 86400000));
			o.expires = d;
		}
		document.cookie = name+'='+encodeURIComponent(value)+'; expires='+o.expires.toUTCString()+(o.path ? '; path='+o.path : '')+(o.domain ? '; domain='+o.domain : '')+(o.secure ? '; secure' : '')+(o.samesite ? '; samesite='+o.samesite : '');
		if (value && (value.substr(0,1) === '{' || value.substr(0,1) === '[')) {
			try {
				return JSON.parse(value);
			} catch(e) {}
		}
		return value;
	},
	
	/**
	 * Get start directory (by location.hash or last opened directory)
	 * 
	 * @return String
	 */
	startDir : function() {
		var locHash = window.location.hash;
		if (locHash && locHash.match(/^#elf_/)) {
			return locHash.replace(/^#elf_/, '');
		} else if (this.options.startPathHash) {
			return this.options.startPathHash;
		} else {
			return this.lastDir();
		}
	},
	
	/**
	 * Get/set last opened directory
	 * 
	 * @param  String|undefined  dir hash
	 * @return String
	 */
	lastDir : function(hash) { 
		return this.options.rememberLastDir ? this.storage('lastdir', hash) : '';
	},
	
	/**
	 * Node for escape html entities in texts
	 * 
	 * @type jQuery
	 */
	_node : jQuery('<span></c.length;>'),
	
	/**
	 * Replace not html-safe symbols to html entities
	 * 
	 * @param  String  text to escape
	 * @return String
	 */
	escape : function(name) {
		return this._node.text(name).html().replace(/"/g, '&quot;').replace(/'/g, '&#039;');
	},
	
	/**
	 * Cleanup ajax data.
	 * For old api convert data into new api format
	 * 
	 * @param  String  command name
	 * @param  Object  data from backend
	 * @return Object
	 */
	normalize : function(data) {
		var self   = this,
			fileFilter = (function() {
				var func, filter;
				if (filter = self.options.fileFilter) {
					if (typeof filter === 'function') {
						func = function(file) {
							return filter.call(self, file);
						};
					} else if (filter instanceof RegExp) {
						func = function(file) {
							return filter.test(file.name);
						};
					}
				}
				return func? func : null;
			})(),
			chkCmdMap = function(opts) {
				// Disable command to replace with other command
				var disabled;
				if (opts.uiCmdMap) {
					if (jQuery.isPlainObject(opts.uiCmdMap) && Object.keys(opts.uiCmdMap).length) {
						if (!opts.disabledFlip) {
							opts.disabledFlip = {};
						}
						disabled = opts.disabledFlip;
						jQuery.each(opts.uiCmdMap, function(f, t) {
							if (t === 'hidden' && !disabled[f]) {
								opts.disabled.push(f);
								opts.disabledFlip[f] = true;
							}
						});
					} else {
						delete opts.uiCmdMap;
					}
				}
			},
			normalizeOptions = function(opts) {
				var getType = function(v) {
						var type = typeof v;
						if (type === 'object' && Array.isArray(v)) {
							type = 'array';
						}
						return type;
					};
				jQuery.each(self.optionProperties, function(k, empty) {
					if (empty !== void(0)) {
						if (opts[k] && getType(opts[k]) !== getType(empty)) {
							opts[k] = empty;
						}
					}
				});
				if (opts.disabled) {
					opts.disabledFlip = self.arrayFlip(opts.disabled, true);
					jQuery.each(self.options.disabledCmdsRels, function(com, rels) {
						var m, flg;
						if (opts.disabledFlip[com]) {
							flg = true;
						} else if (m = com.match(/^([^&]+)&([^=]+)=(.*)$/)) {
							if (opts.disabledFlip[m[1]] && opts[m[2]] == m[3]) {
								flg = true;
							}
						}
						if (flg) {
							jQuery.each(rels, function(i, rel) {
								if (!opts.disabledFlip[rel]) {
									opts.disabledFlip[rel] = true;
									opts.disabled.push(rel);
								}
							});
						}
					});
				} else {
					opts.disabledFlip = {};
				}
				return opts;
			},
			filter = function(file, asMap, type) { 
				var res = asMap? file : true,
					ign = asMap? null : false,
					vid, targetOptions, isRoot, rootNames;
				
				if (file && file.hash && file.name && file.mime) {
					if (file.mime === 'application/x-empty') {
						file.mime = 'text/plain';
					}
					
					isRoot = self.isRoot(file);
					if (isRoot && ! file.volumeid) {
						self.debug('warning', 'The volume root statuses requires `volumeid` property.');
					}
					if (isRoot || file.mime === 'directory') {
						// Prevention of circular reference
						if (file.phash) {
							if (file.phash === file.hash) {
								error = error.concat(['Parent folder of "$1" is itself.', file.name]);
								return ign;
							}
							if (isRoot && file.volumeid && file.phash.indexOf(file.volumeid) === 0) {
								error = error.concat(['Parent folder of "$1" is inner itself.', file.name]);
								return ign;
							}
						}
						
						// set options, tmbUrls for each volume
						if (file.volumeid) {
							vid = file.volumeid;
							
							if (isRoot) {
								// make or update of leaf roots cache
								if (file.phash) {
									if (! self.leafRoots[file.phash]) {
										self.leafRoots[file.phash] = [ file.hash ];
									} else {
										if (jQuery.inArray(file.hash, self.leafRoots[file.phash]) === -1) {
											self.leafRoots[file.phash].push(file.hash);
										}
									}
								}

								self.hasVolOptions = true;
								if (! self.volOptions[vid]) {
									self.volOptions[vid] = {
										// set dispInlineRegex
										dispInlineRegex: self.options.dispInlineRegex
									};
								}
								
								targetOptions = self.volOptions[vid];
								
								if (file.options) {
									// >= v.2.1.14 has file.options
									Object.assign(targetOptions, file.options);
								}
								
								// for compat <= v2.1.13
								if (file.disabled) {
									targetOptions.disabled = file.disabled;
									targetOptions.disabledFlip = self.arrayFlip(file.disabled, true);
								}
								if (file.tmbUrl) {
									targetOptions.tmbUrl = file.tmbUrl;
								}
								
								// '/' required at the end of url
								if (targetOptions.url && targetOptions.url.substr(-1) !== '/') {
									targetOptions.url += '/';
								}

								// check uiCmdMap
								chkCmdMap(targetOptions);
								
								// check trash bin hash
								if (targetOptions.trashHash) {
									if (self.trashes[targetOptions.trashHash] === false) {
										delete targetOptions.trashHash;
									} else {
										self.trashes[targetOptions.trashHash] = file.hash;
									}
								}
								
								// set immediate properties
								jQuery.each(self.optionProperties, function(k) {
									if (targetOptions[k]) {
										file[k] = targetOptions[k];
									}
								});

								// regist fm.roots
								if (type !== 'cwd') {
									self.roots[vid] = file.hash;
								}

								// regist fm.volumeExpires
								if (file.expires) {
									self.volumeExpires[vid] = file.expires;
								}
							}
							
							if (prevId !== vid) {
								prevId = vid;
								i18nFolderName = self.option('i18nFolderName', vid);
							}
						}
						
						// volume root i18n name
						if (isRoot && ! file.i18) {
							name = 'volume_' + file.name,
							i18 = self.i18n(false, name);
	
							if (name !== i18) {
								file.i18 = i18;
							}
						}
						
						// i18nFolderName
						if (i18nFolderName && ! file.i18) {
							name = 'folder_' + file.name,
							i18 = self.i18n(false, name);
	
							if (name !== i18) {
								file.i18 = i18;
							}
						}
						
						if (isRoot) {
							if (rootNames = self.storage('rootNames')) {
								if (rootNames[file.hash]) {
									file._name = file.name;
									file._i18 = file.i18;
									file.name = rootNames[file.hash] = rootNames[file.hash];
									delete file.i18;
								}
								self.storage('rootNames', rootNames);
							}
						}

						// lock trash bins holder
						if (self.trashes[file.hash]) {
							file.locked = true;
						}
					} else {
						if (fileFilter) {
							try {
								if (! fileFilter(file)) {
									return ign;
								}
							} catch(e) {
								self.debug(e);
							}
						}
						if (file.size == 0) {
							file.mime = self.getMimetype(file.name, file.mime);
						}
					}
					
					if (file.options) {
						self.optionsByHashes[file.hash] = normalizeOptions(file.options);
					}
					
					delete file.options;
					
					return res;
				}
				return ign;
			},
			getDescendants = function(hashes) {
				var res = [];
				jQuery.each(self.files(), function(h, f) {
					jQuery.each(self.parents(h), function(i, ph) {
						if (jQuery.inArray(ph, hashes) !== -1 && jQuery.inArray(h, hashes) === -1) {
							res.push(h);
							return false;
						}
					});
				});
				return res;
			},
			applyLeafRootStats = function(dataArr, type) {
				jQuery.each(dataArr, function(i, f) {
					var pfile, done;
					if (self.leafRoots[f.hash]) {
						self.applyLeafRootStats(f);
					}
					// update leaf root parent stat
					if (type !== 'change' && f.phash && self.isRoot(f) && (pfile = self.file(f.phash))) {
						self.applyLeafRootStats(pfile);
						// add to data.changed
						if (!data.changed) {
							data.changed = [pfile];
						} else {
							jQuery.each(data.changed, function(i, f) {
								if (f.hash === pfile.hash) {
									data.changed[i] = pfile;
									done = true;
									return false;
								}
							});
							if (!done) {
								data.changed.push(pfile);
							}
						}
					}
				});
			},
			error = [],
			name, i18, i18nFolderName, prevId, cData;
		
		// set cunstom data
		if (data.customData && (!self.prevCustomData || (JSON.stringify(data.customData) !== JSON.stringify(self.prevCustomData)))) {
			self.prevCustomData = data.customData;
			try {
				cData = JSON.parse(data.customData);
				if (jQuery.isPlainObject(cData)) {
					self.prevCustomData = cData;
					jQuery.each(Object.keys(cData), function(i, key) {
						if (cData[key] === null) {
							delete cData[key];
							delete self.optsCustomData[key];
						}
					});
					self.customData = Object.assign({}, self.optsCustomData, cData);
				}
			} catch(e) {}
		}

		if (data.options) {
			normalizeOptions(data.options);
		}
		
		if (data.cwd) {
			if (data.cwd.volumeid && data.options && Object.keys(data.options).length && self.isRoot(data.cwd)) {
				self.hasVolOptions = true;
				self.volOptions[data.cwd.volumeid] = data.options;
			}
			data.cwd = filter(data.cwd, true, 'cwd');
		}
		if (data.files) {
			data.files = jQuery.grep(data.files, filter);
		} 
		if (data.tree) {
			data.tree = jQuery.grep(data.tree, filter);
		}
		if (data.added) {
			data.added = jQuery.grep(data.added, filter);
		}
		if (data.changed) {
			data.changed = jQuery.grep(data.changed, filter);
		}
		if (data.removed && data.removed.length && self.searchStatus.state === 2) {
			data.removed = data.removed.concat(getDescendants(data.removed));
		}
		if (data.api) {
			data.init = true;
		}

		if (Object.keys(self.leafRoots).length) {
			data.files && applyLeafRootStats(data.files);
			data.tree && applyLeafRootStats(data.tree);
			data.added && applyLeafRootStats(data.added);
			data.changed && applyLeafRootStats(data.changed, 'change');
		}

		// merge options that apply only to cwd
		if (data.cwd && data.cwd.options && data.options) {
			Object.assign(data.options, normalizeOptions(data.cwd.options));
		}

		// '/' required at the end of url
		if (data.options && data.options.url && data.options.url.substr(-1) !== '/') {
			data.options.url += '/';
		}
		
		// check error
		if (error.length) {
			data.norError = ['errResponse'].concat(error);
		}
		
		return data;
	},
	
	/**
	 * Update sort options
	 *
	 * @param {String} sort type
	 * @param {String} sort order
	 * @param {Boolean} show folder first
	 */
	setSort : function(type, order, stickFolders, alsoTreeview) {
		this.storage('sortType', (this.sortType = this.sortRules[type] ? type : 'name'));
		this.storage('sortOrder', (this.sortOrder = /asc|desc/.test(order) ? order : 'asc'));
		this.storage('sortStickFolders', (this.sortStickFolders = !!stickFolders) ? 1 : '');
		this.storage('sortAlsoTreeview', (this.sortAlsoTreeview = !!alsoTreeview) ? 1 : '');
		this.trigger('sortchange');
	},
	
	_sortRules : {
		name : function(file1, file2) {
			return elFinder.prototype.naturalCompare(file1.i18 || file1.name, file2.i18 || file2.name);
		},
		size : function(file1, file2) { 
			var size1 = parseInt(file1.size) || 0,
				size2 = parseInt(file2.size) || 0;
				
			return size1 === size2 ? 0 : size1 > size2 ? 1 : -1;
		},
		kind : function(file1, file2) {
			return elFinder.prototype.naturalCompare(file1.mime, file2.mime);
		},
		date : function(file1, file2) { 
			var date1 = file1.ts || file1.date || 0,
				date2 = file2.ts || file2.date || 0;

			return date1 === date2 ? 0 : date1 > date2 ? 1 : -1;
		},
		perm : function(file1, file2) { 
			var val = function(file) { return (file.write? 2 : 0) + (file.read? 1 : 0); },
				v1  = val(file1),
				v2  = val(file2);
			return v1 === v2 ? 0 : v1 > v2 ? 1 : -1;
		},
		mode : function(file1, file2) { 
			var v1 = file1.mode || (file1.perm || ''),
				v2 = file2.mode || (file2.perm || '');
			return elFinder.prototype.naturalCompare(v1, v2);
		},
		owner : function(file1, file2) { 
			var v1 = file1.owner || '',
				v2 = file2.owner || '';
			return elFinder.prototype.naturalCompare(v1, v2);
		},
		group : function(file1, file2) { 
			var v1 = file1.group || '',
				v2 = file2.group || '';
			return elFinder.prototype.naturalCompare(v1, v2);
		}
	},
	
	/**
	 * Valid sort rule names
	 * 
	 * @type Object
	 */
	sorters : {},
	
	/**
	 * Compare strings for natural sort
	 *
	 * @param  String
	 * @param  String
	 * @return Number
	 */
	naturalCompare : function(a, b) {
		var self = elFinder.prototype.naturalCompare;
		if (typeof self.loc == 'undefined') {
			self.loc = (navigator.userLanguage || navigator.browserLanguage || navigator.language || 'en-US');
		}
		if (typeof self.sort == 'undefined') {
			if ('11'.localeCompare('2', self.loc, {numeric: true}) > 0) {
				// Native support
				if (window.Intl && window.Intl.Collator) {
					self.sort = new Intl.Collator(self.loc, {numeric: true}).compare;
				} else {
					self.sort = function(a, b) {
						return a.localeCompare(b, self.loc, {numeric: true});
					};
				}
			} else {
				/*
				 * Edited for elFinder (emulates localeCompare() by numeric) by Naoki Sawada aka nao-pon
				 */
				/*
				 * Huddle/javascript-natural-sort (https://github.com/Huddle/javascript-natural-sort)
				 */
				/*
				 * Natural Sort algorithm for Javascript - Version 0.7 - Released under MIT license
				 * Author: Jim Palmer (based on chunking idea from Dave Koelle)
				 * http://opensource.org/licenses/mit-license.php
				 */
				self.sort = function(a, b) {
					var re = /(^-?[0-9]+(\.?[0-9]*)[df]?e?[0-9]?$|^0x[0-9a-f]+$|[0-9]+)/gi,
					sre = /(^[ ]*|[ ]*$)/g,
					dre = /(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/,
					hre = /^0x[0-9a-f]+$/i,
					ore = /^0/,
					syre = /^[\x01\x21-\x2f\x3a-\x40\x5b-\x60\x7b-\x7e]/, // symbol first - (Naoki Sawada)
					i = function(s) { return self.sort.insensitive && (''+s).toLowerCase() || ''+s; },
					// convert all to strings strip whitespace
					// first character is "_", it's smallest - (Naoki Sawada)
					x = i(a).replace(sre, '').replace(/^_/, "\x01") || '',
					y = i(b).replace(sre, '').replace(/^_/, "\x01") || '',
					// chunk/tokenize
					xN = x.replace(re, '\0$1\0').replace(/\0$/,'').replace(/^\0/,'').split('\0'),
					yN = y.replace(re, '\0$1\0').replace(/\0$/,'').replace(/^\0/,'').split('\0'),
					// numeric, hex or date detection
					xD = parseInt(x.match(hre)) || (xN.length != 1 && x.match(dre) && Date.parse(x)),
					yD = parseInt(y.match(hre)) || xD && y.match(dre) && Date.parse(y) || null,
					oFxNcL, oFyNcL,
					locRes = 0;

					// first try and sort Hex codes or Dates
					if (yD) {
						if ( xD < yD ) return -1;
						else if ( xD > yD ) return 1;
					}
					// natural sorting through split numeric strings and default strings
					for(var cLoc=0, numS=Math.max(xN.length, yN.length); cLoc < numS; cLoc++) {

						// find floats not starting with '0', string or 0 if not defined (Clint Priest)
						oFxNcL = !(xN[cLoc] || '').match(ore) && parseFloat(xN[cLoc]) || xN[cLoc] || 0;
						oFyNcL = !(yN[cLoc] || '').match(ore) && parseFloat(yN[cLoc]) || yN[cLoc] || 0;

						// handle numeric vs string comparison - number < string - (Kyle Adams)
						// but symbol first < number - (Naoki Sawada)
						if (isNaN(oFxNcL) !== isNaN(oFyNcL)) {
							if (isNaN(oFxNcL) && (typeof oFxNcL !== 'string' || ! oFxNcL.match(syre))) {
								return 1;
							} else if (typeof oFyNcL !== 'string' || ! oFyNcL.match(syre)) {
								return -1;
							}
						}

						// use decimal number comparison if either value is string zero
						if (parseInt(oFxNcL, 10) === 0) oFxNcL = 0;
						if (parseInt(oFyNcL, 10) === 0) oFyNcL = 0;

						// rely on string comparison if different types - i.e. '02' < 2 != '02' < '2'
						if (typeof oFxNcL !== typeof oFyNcL) {
							oFxNcL += '';
							oFyNcL += '';
						}

						// use locale sensitive sort for strings when case insensitive
						// note: localeCompare interleaves uppercase with lowercase (e.g. A,a,B,b)
						if (self.sort.insensitive && typeof oFxNcL === 'string' && typeof oFyNcL === 'string') {
							locRes = oFxNcL.localeCompare(oFyNcL, self.loc);
							if (locRes !== 0) return locRes;
						}

						if (oFxNcL < oFyNcL) return -1;
						if (oFxNcL > oFyNcL) return 1;
					}
					return 0;
				};
				self.sort.insensitive = true;
			}
		}
		return self.sort(a, b);
	},
	
	/**
	 * Compare files based on elFinder.sort
	 *
	 * @param  Object  file
	 * @param  Object  file
	 * @return Number
	 */
	compare : function(file1, file2) {
		var self  = this,
			type  = self.sortType,
			asc   = self.sortOrder == 'asc',
			stick = self.sortStickFolders,
			rules = self.sortRules,
			sort  = rules[type],
			d1    = file1.mime == 'directory',
			d2    = file2.mime == 'directory',
			res;
			
		if (stick) {
			if (d1 && !d2) {
				return -1;
			} else if (!d1 && d2) {
				return 1;
			}
		}
		
		res = asc ? sort(file1, file2) : sort(file2, file1);
		
		return type !== 'name' && res === 0
			? res = asc ? rules.name(file1, file2) : rules.name(file2, file1)
			: res;
	},
	
	/**
	 * Sort files based on config
	 *
	 * @param  Array  files
	 * @return Array
	 */
	sortFiles : function(files) {
		return files.sort(this.compare);
	},
	
	/**
	 * Open notification dialog 
	 * and append/update message for required notification type.
	 *
	 * @param  Object  options
	 * @example  
	 * this.notify({
	 *    type : 'copy',
	 *    msg : 'Copy files', // not required for known types @see this.notifyType
	 *    cnt : 3,
	 *    hideCnt  : false,   // true for not show count
	 *    progress : 10,      // progress bar percents (use cnt : 0 to update progress bar)
	 *    cancel   : callback // callback function for cancel button
	 * })
	 * @return elFinder
	 */
	notify : function(opts) {
		var self     = this,
			type     = opts.type,
			id       = opts.id? 'elfinder-notify-'+opts.id : '',
			msg      = this.i18n((typeof opts.msg !== 'undefined')? opts.msg : (this.messages['ntf'+type] ? 'ntf'+type : 'ntfsmth')),
			hiddens  = this.arrayFlip(this.options.notifyDialog.hiddens || []),
			ndialog  = this.ui.notify,
			dialog   = ndialog.closest('.ui-dialog'),
			notify   = ndialog.children('.elfinder-notify-'+type+(id? ('.'+id) : '')),
			button   = notify.children('div.elfinder-notify-cancel').children('button'),
			ntpl     = '<div class="elfinder-notify elfinder-notify-{type}'+(id? (' '+id) : '')+'"><span class="elfinder-dialog-icon elfinder-dialog-icon-{type}"></span><span class="elfinder-notify-msg">{msg}</span> <span class="elfinder-notify-cnt"></span><div class="elfinder-notify-progressbar"><div class="elfinder-notify-progress"></div></div><div class="elfinder-notify-cancel"></div></div>',
			delta    = opts.cnt + 0,
			size     = (typeof opts.size != 'undefined')? parseInt(opts.size) : null,
			progress = (typeof opts.progress != 'undefined' && opts.progress >= 0) ? opts.progress : null,
			fakeint  = opts.fakeinterval || 200,
			cancel   = opts.cancel,
			clhover  = 'ui-state-hover',
			close    = function() {
				var prog = notify.find('.elfinder-notify-progress'),
					rm = function() {
						notify.remove();
						if (!ndialog.children(dialog.data('minimized')? void(0) : ':visible').length) {
							if (dialog.data('minimized')) {
								dialog.data('minimized').hide();
							} else {
								ndialog.elfinderdialog('close');
							}
						}
						setProgressbar();
					};
				notify._esc && jQuery(document).off('keydown', notify._esc);
				if (notify.data('cur') < 100) {
					prog.animate({
						width : '100%'
					}, 50, function() { requestAnimationFrame(function() { rm(); }); });
				} else {
					rm();
				}
			},
			fakeUp = function(interval) {
				var cur;
				if (notify.length) {
					cur = notify.data('cur') + 1;
					if (cur <= 98) {
						notify.find('.elfinder-notify-progress').width(cur + '%');
						notify.data('cur', cur);
						setProgressbar();
						setTimeout(function() {
							interval *= 1.05; 
							fakeUp(interval);
						}, interval);
					}
				}
			},
			setProgressbar = function() {
				var cnt = 0,
					val = 0,
					ntfs = ndialog.children('.elfinder-notify'),
					w;
				if (ntfs.length) {
					ntfs.each(function() {
						cnt++;
						val += Math.min(jQuery(this).data('cur'), 100);
					});
					w = cnt? Math.floor(val / (cnt * 100) * 100) + '%' : 0;
					self.ui.progressbar.width(w);
					if (dialog.data('minimized')) {
						dialog.data('minimized').title(w);
						dialog.data('minimized').dialog().children('.ui-dialog-titlebar').children('.elfinder-ui-progressbar').width(w);
					}
				} else {
					self.ui.progressbar.width(0);
					dialog.data('minimized') && dialog.data('minimized').hide();
				}
			},
			cnt, total, prc;

		if (!type) {
			return this;
		}
		
		if (!notify.length) {
			notify = jQuery(ntpl.replace(/\{type\}/g, type).replace(/\{msg\}/g, msg));
			if (hiddens[type]) {
				notify.hide();
			} else {
				ndialog.on('minimize', function(e) {
					dialog.data('minimized') && setProgressbar();
				});
			}
			notify.appendTo(ndialog).data('cnt', 0);

			if (progress != null) {
				notify.data({progress : 0, total : 0, cur : 0});
			} else {
				notify.data({cur : 0});
				fakeUp(fakeint);
			}

			if (cancel) {
				button = jQuery('<span class="elfinder-notify-button ui-icon ui-icon-close" title="'+this.i18n('btnCancel')+'"></span>')
					.on('mouseenter mouseleave', function(e) { 
						jQuery(this).toggleClass(clhover, e.type === 'mouseenter');
					});
				notify.children('div.elfinder-notify-cancel').append(button);
			}
			ndialog.trigger('resize');
		} else if (typeof opts.msg !== 'undefined') {
			notify.children('span.elfinder-notify-msg').html(msg);
		}

		cnt = delta + parseInt(notify.data('cnt'));
		
		if (cnt > 0) {
			if (cancel && button.length) {
				if (jQuery.isFunction(cancel) || (typeof cancel === 'object' && cancel.promise)) {
					notify._esc = function(e) {
						if (e.type == 'keydown' && e.keyCode != jQuery.ui.keyCode.ESCAPE) {
							return;
						}
						e.preventDefault();
						e.stopPropagation();
						close();
						if (cancel.promise) {
							cancel.reject(0); // 0 is canceling flag
						} else {
							cancel(e);
						}
					};
					button.on('click', function(e) {
						notify._esc(e);
					});
					jQuery(document).on('keydown.' + this.namespace, notify._esc);
				}
			}
			
			!opts.hideCnt && notify.children('.elfinder-notify-cnt').text('('+cnt+')');
			if (delta > 0 && ndialog.is(':hidden') && !hiddens[type]) {
				if (dialog.data('minimized')) {
					dialog.data('minimized').show();
				} else {
					ndialog.elfinderdialog('open', this).height('auto');
				}
			}
			notify.data('cnt', cnt);
			
			if ((progress != null)
			&& (total = notify.data('total')) >= 0
			&& (prc = notify.data('progress')) >= 0) {

				total += size != null? size : delta;
				prc   += progress;
				(size == null && delta < 0) && (prc += delta * 100);
				notify.data({progress : prc, total : total});
				if (size != null) {
					prc *= 100;
					total = Math.max(1, total);
				}
				progress = Math.min(parseInt(prc/total), 100);
				
				notify.find('.elfinder-notify-progress')
					.animate({
						width : (progress < 100 ? progress : 100)+'%'
					}, 20, function() {
						notify.data('cur', progress);
						setProgressbar();
					});
			}
			
		} else {
			close();
		}
		
		return this;
	},
	
	/**
	 * Open confirmation dialog 
	 *
	 * @param  Object  options
	 * @example  
	 * this.confirm({
	 *    cssClass : 'elfinder-confirm-mydialog',
	 *    title : 'Remove files',
	 *    text  : 'Here is question text',
	 *    accept : {  // accept callback - required
	 *      label : 'Continue',
	 *      callback : function(applyToAll) { fm.log('Ok') }
	 *    },
	 *    cancel : { // cancel callback - required
	 *      label : 'Cancel',
	 *      callback : function() { fm.log('Cancel')}
	 *    },
	 *    reject : { // reject callback - optionally
	 *      label : 'No',
	 *      callback : function(applyToAll) { fm.log('No')}
	 *    },
	 *    buttons : [ // additional buttons callback - optionally
	 *      {
	 *        label : 'Btn1',
	 *        callback : function(applyToAll) { fm.log('Btn1')}
	 *      }
	 *    ],
	 *    all : true  // display checkbox "Apply to all"
	 * })
	 * @return elFinder
	 */
	confirm : function(opts) {
		var self     = this,
			complete = false,
			options = {
				cssClass  : 'elfinder-dialog-confirm',
				modal     : true,
				resizable : false,
				title     : this.i18n(opts.title || 'confirmReq'),
				buttons   : {},
				close     : function() { 
					!complete && opts.cancel.callback();
					jQuery(this).elfinderdialog('destroy');
				}
			},
			apply = this.i18n('apllyAll'),
			label, checkbox, btnNum;

		if (opts.cssClass) {
			options.cssClass += ' ' + opts.cssClass;
		}
		options.buttons[this.i18n(opts.accept.label)] = function() {
			opts.accept.callback(!!(checkbox && checkbox.prop('checked')));
			complete = true;
			jQuery(this).elfinderdialog('close');
		};
		options.buttons[this.i18n(opts.accept.label)]._cssClass = 'elfinder-confirm-accept';
		
		if (opts.reject) {
			options.buttons[this.i18n(opts.reject.label)] = function() {
				opts.reject.callback(!!(checkbox && checkbox.prop('checked')));
				complete = true;
				jQuery(this).elfinderdialog('close');
			};
			options.buttons[this.i18n(opts.reject.label)]._cssClass = 'elfinder-confirm-reject';
		}
		
		if (opts.buttons && opts.buttons.length > 0) {
			btnNum = 1;
			jQuery.each(opts.buttons, function(i, v){
				options.buttons[self.i18n(v.label)] = function() {
					v.callback(!!(checkbox && checkbox.prop('checked')));
					complete = true;
					jQuery(this).elfinderdialog('close');
				};
				options.buttons[self.i18n(v.label)]._cssClass = 'elfinder-confirm-extbtn' + (btnNum++);
				if (v.cssClass) {
					options.buttons[self.i18n(v.label)]._cssClass += ' ' + v.cssClass;
				}
			});
		}
		
		options.buttons[this.i18n(opts.cancel.label)] = function() {
			jQuery(this).elfinderdialog('close');
		};
		options.buttons[this.i18n(opts.cancel.label)]._cssClass = 'elfinder-confirm-cancel';
		
		if (opts.all) {
			options.create = function() {
				var base = jQuery('<div class="elfinder-dialog-confirm-applyall"></div>');
				checkbox = jQuery('<input type="checkbox" />');
				jQuery(this).next().find('.ui-dialog-buttonset')
					.prepend(base.append(jQuery('<label>'+apply+'</label>').prepend(checkbox)));
			};
		}
		
		if (opts.optionsCallback && jQuery.isFunction(opts.optionsCallback)) {
			opts.optionsCallback(options);
		}
		
		return this.dialog('<span class="elfinder-dialog-icon elfinder-dialog-icon-confirm"></span>' + this.i18n(opts.text), options);
	},
	
	/**
	 * Create unique file name in required dir
	 * 
	 * @param  String  file name
	 * @param  String  parent dir hash
	 * @param  String  glue
	 * @return String
	 */
	uniqueName : function(prefix, phash, glue) {
		var i = 0, ext = '', p, name;
		
		prefix = this.i18n(false, prefix);
		phash = phash || this.cwd().hash;
		glue = (typeof glue === 'undefined')? ' ' : glue;

		if (p = prefix.match(/^(.+)(\.[^.]+)$/)) {
			ext    = p[2];
			prefix = p[1];
		}
		
		name   = prefix+ext;
		
		if (!this.fileByName(name, phash)) {
			return name;
		}
		while (i < 10000) {
			name = prefix + glue + (++i) + ext;
			if (!this.fileByName(name, phash)) {
				return name;
			}
		}
		return prefix + Math.random() + ext;
	},
	
	/**
	 * Return message translated onto current language
	 * Allowed accept HTML element that was wrapped in jQuery object
	 * To be careful to XSS vulnerability of HTML element Ex. You should use `fm.escape(file.name)`
	 *
	 * @param  String|Array  message[s]|Object jQuery
	 * @return String
	 **/
	i18n : function() {
		var self = this,
			messages = this.messages, 
			input    = [],
			ignore   = [], 
			message = function(m) {
				var file;
				if (m.indexOf('#') === 0) {
					if ((file = self.file(m.substr(1)))) {
						return file.name;
					}
				}
				return m;
			},
			i, j, m, escFunc, start = 0, isErr;
		
		if (arguments.length && arguments[0] === false) {
			escFunc = function(m){ return m; };
			start = 1;
		}
		for (i = start; i< arguments.length; i++) {
			m = arguments[i];
			
			if (Array.isArray(m)) {
				for (j = 0; j < m.length; j++) {
					if (m[j] instanceof jQuery) {
						// jQuery object is HTML element
						input.push(m[j]);
					} else if (typeof m[j] !== 'undefined'){
						input.push(message('' + m[j]));
					}
				}
			} else if (m instanceof jQuery) {
				// jQuery object is HTML element
				input.push(m[j]);
			} else if (typeof m !== 'undefined'){
				input.push(message('' + m));
			}
		}
		
		for (i = 0; i < input.length; i++) {
			// dont translate placeholders
			if (jQuery.inArray(i, ignore) !== -1) {
				continue;
			}
			m = input[i];
			if (typeof m == 'string') {
				isErr = !!(messages[m] && m.match(/^err/));
				// translate message
				m = messages[m] || (escFunc? escFunc(m) : self.escape(m));
				// replace placeholders in message
				m = m.replace(/\$(\d+)/g, function(match, placeholder) {
					var res;
					placeholder = i + parseInt(placeholder);
					if (placeholder > 0 && input[placeholder]) {
						ignore.push(placeholder);
					}
					res = escFunc? escFunc(input[placeholder]) : self.escape(input[placeholder]);
					if (isErr) {
						res = '<span class="elfinder-err-var elfinder-err-var' + placeholder + '">' + res + '</span>';
					}
					return res;
				});
			} else {
				// get HTML from jQuery object
				m = m.get(0).outerHTML;
			}

			input[i] = m;
		}

		return jQuery.grep(input, function(m, i) { return jQuery.inArray(i, ignore) === -1 ? true : false; }).join('<br>');
	},
	
	/**
	 * Get icon style from file.icon
	 * 
	 * @param  Object  elFinder file object
	 * @return String|Object
	 */
	getIconStyle : function(file, asObject) {
		var self = this,
			template = {
				'background' : 'url(\'{url}\') 0 0 no-repeat',
				'background-size' : 'contain'
			},
			style = '',
			cssObj = {},
			i = 0;
		if (file.icon) {
			style = 'style="';
			jQuery.each(template, function(k, v) {
				if (i++ === 0) {
					v = v.replace('{url}', self.escape(file.icon));
				}
				if (asObject) {
					cssObj[k] = v;
				} else {
					style += k+':'+v+';';
				}
			});
			style += '"';
		}
		return asObject? cssObj : style;
	},
	
	/**
	 * Convert mimetype into css classes
	 * 
	 * @param  String  file mimetype
	 * @return String
	 */
	mime2class : function(mimeType) {
		var prefix = 'elfinder-cwd-icon-',
			mime   = mimeType.toLowerCase(),
			isText = this.textMimes[mime];
		
		mime = mime.split('/');
		if (isText) {
			mime[0] += ' ' + prefix + 'text';
		} else if (mime[1] && mime[1].match(/\+xml$/)) {
			mime[0] += ' ' + prefix + 'xml';
		}
		
		return prefix + mime[0] + (mime[1] ? ' ' + prefix + mime[1].replace(/(\.|\+)/g, '-') : '');
	},
	
	/**
	 * Return localized kind of file
	 * 
	 * @param  Object|String  file or file mimetype
	 * @return String
	 */
	mime2kind : function(f) {
		var isObj = typeof(f) == 'object' ? true : false,
			mime  = isObj ? f.mime : f,
			kind;
		

		if (isObj && f.alias && mime != 'symlink-broken') {
			kind = 'Alias';
		} else if (this.kinds[mime]) {
			if (isObj && mime === 'directory' && (! f.phash || f.isroot)) {
				kind = 'Root';
			} else {
				kind = this.kinds[mime];
			}
		} else if (this.mimeTypes[mime]) {
			kind = this.mimeTypes[mime].toUpperCase();
			if (!this.messages['kind'+kind]) {
				kind = null;
			}
		}
		if (! kind) {
			if (mime.indexOf('text') === 0) {
				kind = 'Text';
			} else if (mime.indexOf('image') === 0) {
				kind = 'Image';
			} else if (mime.indexOf('audio') === 0) {
				kind = 'Audio';
			} else if (mime.indexOf('video') === 0) {
				kind = 'Video';
			} else if (mime.indexOf('application') === 0) {
				kind = 'App';
			} else {
				kind = mime;
			}
		}
		
		return this.messages['kind'+kind] ? this.i18n('kind'+kind) : mime;
	},
	
	/**
	 * Return boolean Is mime-type text file
	 * 
	 * @param  String  mime-type
	 * @return Boolean
	 */
	mimeIsText : function(mime) {
		return (this.textMimes[mime.toLowerCase()] || (mime.indexOf('text/') === 0 && mime.substr(5, 3) !== 'rtf') || mime.match(/^application\/.+\+xml$/))? true : false;
	},
	
	/**
	 * Returns a date string formatted according to the given format string
	 * 
	 * @param  String  format string
	 * @param  Object  Date object
	 * @return String
	 */
	date : function(format, date) {
		var self = this,
			output, d, dw, m, y, h, g, i, s;
		
		if (! date) {
			date = new Date();
		}
		
		h  = date[self.getHours]();
		g  = h > 12 ? h - 12 : h;
		i  = date[self.getMinutes]();
		s  = date[self.getSeconds]();
		d  = date[self.getDate]();
		dw = date[self.getDay]();
		m  = date[self.getMonth]() + 1;
		y  = date[self.getFullYear]();
		
		output = format.replace(/[a-z]/gi, function(val) {
			switch (val) {
				case 'd': return d > 9 ? d : '0'+d;
				case 'j': return d;
				case 'D': return self.i18n(self.i18.daysShort[dw]);
				case 'l': return self.i18n(self.i18.days[dw]);
				case 'm': return m > 9 ? m : '0'+m;
				case 'n': return m;
				case 'M': return self.i18n(self.i18.monthsShort[m-1]);
				case 'F': return self.i18n(self.i18.months[m-1]);
				case 'Y': return y;
				case 'y': return (''+y).substr(2);
				case 'H': return h > 9 ? h : '0'+h;
				case 'G': return h;
				case 'g': return g;
				case 'h': return g > 9 ? g : '0'+g;
				case 'a': return h >= 12 ? 'pm' : 'am';
				case 'A': return h >= 12 ? 'PM' : 'AM';
				case 'i': return i > 9 ? i : '0'+i;
				case 's': return s > 9 ? s : '0'+s;
			}
			return val;
		});
		
		return output;
	},
	
	/**
	 * Return localized date
	 * 
	 * @param  Object  file object
	 * @return String
	 */
	formatDate : function(file, t) {
		var self = this, 
			ts   = t || file.ts, 
			i18  = self.i18,
			date, format, output, d, dw, m, y, h, g, i, s;

		if (self.options.clientFormatDate && ts > 0) {

			date = new Date(ts*1000);
			format = ts >= this.yesterday 
				? this.fancyFormat 
				: this.dateFormat;

			output = self.date(format, date);
			
			return ts >= this.yesterday
				? output.replace('$1', this.i18n(ts >= this.today ? 'Today' : 'Yesterday'))
				: output;
		} else if (file.date) {
			return file.date.replace(/([a-z]+)\s/i, function(a1, a2) { return self.i18n(a2)+' '; });
		}
		
		return self.i18n('dateUnknown');
	},
	
	/**
	 * Return localized number string
	 * 
	 * @param  Number
	 * @return String
	 */
	toLocaleString : function(num) {
		var v = new Number(num);
		if (v) {
			if (v.toLocaleString) {
				return v.toLocaleString();
			} else {
				return String(num).replace( /(\d)(?=(\d\d\d)+(?!\d))/g, '$1,');
			}
		}
		return num;
	},
	
	/**
	 * Return css class marks file permissions
	 * 
	 * @param  Object  file 
	 * @return String
	 */
	perms2class : function(o) {
		var c = '';
		
		if (!o.read && !o.write) {
			c = 'elfinder-na';
		} else if (!o.read) {
			c = 'elfinder-wo';
		} else if (!o.write) {
			c = 'elfinder-ro';
		}
		
		if (o.type) {
			c += ' elfinder-' + this.escape(o.type);
		}
		
		return c;
	},
	
	/**
	 * Return localized string with file permissions
	 * 
	 * @param  Object  file
	 * @return String
	 */
	formatPermissions : function(f) {
		var p  = [];
			
		f.read && p.push(this.i18n('read'));
		f.write && p.push(this.i18n('write'));	

		return p.length ? p.join(' '+this.i18n('and')+' ') : this.i18n('noaccess');
	},
	
	/**
	 * Return formated file size
	 * 
	 * @param  Number  file size
	 * @return String
	 */
	formatSize : function(s) {
		var n = 1, u = 'b';
		
		if (s == 'unknown') {
			return this.i18n('unknown');
		}
		
		if (s > 1073741824) {
			n = 1073741824;
			u = 'GB';
		} else if (s > 1048576) {
			n = 1048576;
			u = 'MB';
		} else if (s > 1024) {
			n = 1024;
			u = 'KB';
		}
		s = s/n;
		return (s > 0 ? n >= 1048576 ? s.toFixed(2) : Math.round(s) : 0) +' '+u;
	},
	
	/**
	 * Return formated file mode by options.fileModeStyle
	 * 
	 * @param  String  file mode
	 * @param  String  format style
	 * @return String
	 */
	formatFileMode : function(p, style) {
		var i, o, s, b, sticy, suid, sgid, str, oct;
		
		if (!style) {
			style = this.options.fileModeStyle.toLowerCase();
		}
		p = jQuery.trim(p);
		if (p.match(/[rwxs-]{9}$/i)) {
			str = p = p.substr(-9);
			if (style == 'string') {
				return str;
			}
			oct = '';
			s = 0;
			for (i=0; i<7; i=i+3) {
				o = p.substr(i, 3);
				b = 0;
				if (o.match(/[r]/i)) {
					b += 4;
				}
				if (o.match(/[w]/i)) {
					b += 2;
				}
				if (o.match(/[xs]/i)) {
					if (o.match(/[xs]/)) {
						b += 1;
					}
					if (o.match(/[s]/i)) {
						if (i == 0) {
							s += 4;
						} else if (i == 3) {
							s += 2;
						}
					}
				}
				oct += b.toString(8);
			}
			if (s) {
				oct = s.toString(8) + oct;
			}
		} else {
			p = parseInt(p, 8);
			oct = p? p.toString(8) : '';
			if (!p || style == 'octal') {
				return oct;
			}
			o = p.toString(8);
			s = 0;
			if (o.length > 3) {
				o = o.substr(-4);
				s = parseInt(o.substr(0, 1), 8);
				o = o.substr(1);
			}
			sticy = ((s & 1) == 1); // not support
			sgid = ((s & 2) == 2);
			suid = ((s & 4) == 4);
			str = '';
			for(i=0; i<3; i++) {
				if ((parseInt(o.substr(i, 1), 8) & 4) == 4) {
					str += 'r';
				} else {
					str += '-';
				}
				if ((parseInt(o.substr(i, 1), 8) & 2) == 2) {
					str += 'w';
				} else {
					str += '-';
				}
				if ((parseInt(o.substr(i, 1), 8) & 1) == 1) {
					str += ((i==0 && suid)||(i==1 && sgid))? 's' : 'x';
				} else {
					str += '-';
				}
			}
		}
		if (style == 'both') {
			return str + ' (' + oct + ')';
		} else if (style == 'string') {
			return str;
		} else {
			return oct;
		}
	},
	
	/**
	 * Regist this.decodeRawString function
	 * 
	 * @return void
	 */
	registRawStringDecoder : function(rawStringDecoder) {
		if (jQuery.isFunction(rawStringDecoder)) {
			this.decodeRawString = this.options.rawStringDecoder = rawStringDecoder;
		}
	},
	
	/**
	 * Return boolean that uploadable MIME type into target folder
	 * 
	 * @param  String  mime    MIME type
	 * @param  String  target  target folder hash
	 * @return Bool
	 */
	uploadMimeCheck : function(mime, target) {
		target = target || this.cwd().hash;
		var res   = true, // default is allow
			mimeChecker = this.option('uploadMime', target),
			allow,
			deny,
			check = function(checker) {
				var ret = false;
				if (typeof checker === 'string' && checker.toLowerCase() === 'all') {
					ret = true;
				} else if (Array.isArray(checker) && checker.length) {
					jQuery.each(checker, function(i, v) {
						v = v.toLowerCase();
						if (v === 'all' || mime.indexOf(v) === 0) {
							ret = true;
							return false;
						}
					});
				}
				return ret;
			};
		if (mime && jQuery.isPlainObject(mimeChecker)) {
			mime = mime.toLowerCase();
			allow = check(mimeChecker.allow);
			deny = check(mimeChecker.deny);
			if (mimeChecker.firstOrder === 'allow') {
				res = false; // default is deny
				if (! deny && allow === true) { // match only allow
					res = true;
				}
			} else {
				res = true; // default is allow
				if (deny === true && ! allow) { // match only deny
					res = false;
				}
			}
		}
		return res;
	},
	
	/**
	 * call chained sequence of async deferred functions
	 * 
	 * @param  Array   tasks async functions
	 * @return Object  jQuery.Deferred
	 */
	sequence : function(tasks) {
		var l = tasks.length,
			chain = function(task, idx) {
				++idx;
				if (tasks[idx]) {
					return chain(task.then(tasks[idx]), idx);
				} else {
					return task;
				}
			};
		if (l > 1) {
			return chain(tasks[0](), 0);
		} else {
			return tasks[0]();
		}
	},
	
	/**
	 * Reload contents of target URL for clear browser cache
	 * 
	 * @param  String  url target URL
	 * @return Object  jQuery.Deferred
	 */
	reloadContents : function(url) {
		var dfd = jQuery.Deferred(),
			ifm;
		try {
			ifm = jQuery('<iframe width="1" height="1" scrolling="no" frameborder="no" style="position:absolute; top:-1px; left:-1px" crossorigin="use-credentials">')
				.attr('src', url)
				.one('load', function() {
					var ifm = jQuery(this);
					try {
						this.contentDocument.location.reload(true);
						ifm.one('load', function() {
							ifm.remove();
							dfd.resolve();
						});
					} catch(e) {
						ifm.attr('src', '').attr('src', url).one('load', function() {
							ifm.remove();
							dfd.resolve();
						});
					}
				})
				.appendTo('body');
		} catch(e) {
			ifm && ifm.remove();
			dfd.reject();
		}
		return dfd;
	},
	
	/**
	 * Make netmount option for OAuth2
	 * 
	 * @param  String   protocol
	 * @param  String   name
	 * @param  String   host
	 * @param  Object   opts  Default {noOffline: false, root: 'root', pathI18n: 'folderId', folders: true}
			}
	 * 
	 * @return Object
	 */
	makeNetmountOptionOauth : function(protocol, name, host, opt) {
		var noOffline = typeof opt === 'boolean'? opt : null, // for backward compat
			opts = Object.assign({
				noOffline : false,
				root      : 'root',
				pathI18n  : 'folderId',
				folders   : true
			}, (noOffline === null? (opt || {}) : {noOffline : noOffline})),
			addFolders = function(fm, bro, folders) {
				var self = this,
					cnt  = Object.keys(jQuery.isPlainObject(folders)? folders : {}).length,
					select;
				
				bro.next().remove();
				if (cnt) {
					select = jQuery('<select class="ui-corner-all elfinder-tabstop" style="max-width:200px;">').append(
						jQuery(jQuery.map(folders, function(n,i){return '<option value="'+fm.escape((i+'').trim())+'">'+fm.escape(n)+'</option>';}).join(''))
					).on('change click', function(e){
						var node = jQuery(this),
							path = node.val(),
							spn;
						self.inputs.path.val(path);
						if (opts.folders && (e.type === 'change' || node.data('current') !== path)) {
							node.next().remove();
							node.data('current', path);
							if (path != opts.root) {
								spn = spinner();
								if (xhr && xhr.state() === 'pending') {
									fm.abortXHR(xhr, { quiet: true , abort: true });
								}
								node.after(spn);
								xhr = fm.request({
									data : {cmd : 'netmount', protocol: protocol, host: host, user: 'init', path: path, pass: 'folders'},
									preventDefault : true
								}).done(function(data){
									addFolders.call(self, fm, node, data.folders);
								}).always(function() {
									fm.abortXHR(xhr, { quiet: true });
									spn.remove();
								}).xhr;
							}
						}
					});
					bro.after(jQuery('<div></div>').append(select))
						.closest('.ui-dialog').trigger('tabstopsInit');
					select.trigger('focus');
				}
			},
			spinner = function() {
				return jQuery('<div class="elfinder-netmount-spinner"></div>').append('<span class="elfinder-spinner"></span>');
			},
			xhr;
		return {
			vars : {},
			name : name,
			inputs: {
				offline  : jQuery('<input type="checkbox"/>').on('change', function() {
					jQuery(this).parents('table.elfinder-netmount-tb').find('select:first').trigger('change', 'reset');
				}),
				host     : jQuery('<span><span class="elfinder-spinner"></span></span><input type="hidden"/>'),
				path     : jQuery('<input type="text" value="'+opts.root+'"/>'),
				user     : jQuery('<input type="hidden"/>'),
				pass     : jQuery('<input type="hidden"/>'),
				mnt2res  : jQuery('<input type="hidden"/>')
			},
			select: function(fm, ev, d){
				var f = this.inputs,
					oline = f.offline,
					f0 = jQuery(f.host[0]),
					data = d || null;
				this.vars.mbtn = f.host.closest('.ui-dialog').children('.ui-dialog-buttonpane:first').find('button.elfinder-btncnt-0');
				if (! f0.data('inrequest')
						&& (f0.find('span.elfinder-spinner').length
							|| data === 'reset'
							|| (data === 'winfocus' && ! f0.siblings('span.elfinder-button-icon-reload').length))
							)
				{
					if (oline.parent().children().length === 1) {
						f.path.parent().prev().html(fm.i18n(opts.pathI18n));
						oline.attr('title', fm.i18n('offlineAccess'));
						oline.uniqueId().after(jQuery('<label></label>').attr('for', oline.attr('id')).html(' '+fm.i18n('offlineAccess')));
					}
					f0.data('inrequest', true).empty().addClass('elfinder-spinner')
						.parent().find('span.elfinder-button-icon').remove();
					fm.request({
						data : {cmd : 'netmount', protocol: protocol, host: host, user: 'init', options: {id: fm.id, offline: oline.prop('checked')? 1:0, pass: f.host[1].value}},
						preventDefault : true
					}).done(function(data){
						f0.removeClass("elfinder-spinner").html(data.body.replace(/\{msg:([^}]+)\}/g, function(whole,s1){return fm.i18n(s1, host);}));
					});
					opts.noOffline && oline.closest('tr').hide();
				} else {
					oline.closest('tr')[(opts.noOffline || f.user.val())? 'hide':'show']();
					f0.data('funcexpup') && f0.data('funcexpup')();
				}
				this.vars.mbtn[jQuery(f.host[1]).val()? 'show':'hide']();
			},
			done: function(fm, data){
				var f = this.inputs,
					p = this.protocol,
					f0 = jQuery(f.host[0]),
					f1 = jQuery(f.host[1]),
					expires = '&nbsp;',
					vars = this.vars,
					chk = function() {
						if (vars.oauthW && !document.hasFocus() && --vars.chkCnt) {
							p.trigger('change', 'winfocus');
							vars.tm = setTimeout(chk, 3000);
						}
					},
					btn;
				
				opts.noOffline && f.offline.closest('tr').hide();
				if (data.mode == 'makebtn') {
					f0.removeClass('elfinder-spinner').removeData('expires').removeData('funcexpup');
					btn = f.host.find('input').on('mouseenter mouseleave', function(){jQuery(this).toggleClass('ui-state-hover');});
					if (data.url) {
						btn.on('click', function() {
							vars.tm && clearTimeout(vars.tm);
							vars.oauthW = window.open(data.url);
							// To correspond to safari, authentication tab sometimes not closing in CORS environment.
							// This may be a safari bug and may improve in the future.
							if ((fm.UA.iOS || fm.UA.Mac) && fm.isCORS && !vars.chkdone) {
								vars.chkCnt = 60;
								vars.tm = setTimeout(chk, 5000);
							}
						});
					}
					f1.val('');
					f.path.val(opts.root).next().remove();
					f.user.val('');
					f.pass.val('');
					! opts.noOffline && f.offline.closest('tr').show();
					vars.mbtn.hide();
				} else if (data.mode == 'folders') {
					if (data.folders) {
						addFolders.call(this, fm, f.path.nextAll(':last'), data.folders);
					}
				} else {
					if (vars.oauthW) {
						vars.tm && clearTimeout(vars.tm);
						vars.oauthW.close();
						delete vars.oauthW;
						// The problem that Safari's authentication tab doesn't close only affects the first time.
						vars.chkdone = true;
					}
					if (data.expires) {
						expires = '()';
						f0.data('expires', data.expires);
					}
					f0.html(host + expires).removeClass('elfinder-spinner');
					if (data.expires) {
						f0.data('funcexpup', function() {
							var rem = Math.floor((f0.data('expires') - (+new Date()) / 1000) / 60);
							if (rem < 3) {
								f0.parent().children('.elfinder-button-icon-reload').click();
							} else {
								f0.text(f0.text().replace(/\(.*\)/, '('+fm.i18n(['minsLeft', rem])+')'));
								setTimeout(function() {
									if (f0.is(':visible')) {
										f0.data('funcexpup')();
									}
								}, 60000);
							}
						});
						f0.data('funcexpup')();
					}
					if (data.reset) {
						p.trigger('change', 'reset');
						return;
					}
					f0.parent().append(jQuery('<span class="elfinder-button-icon elfinder-button-icon-reload" title="'+fm.i18n('reAuth')+'">')
						.on('click', function() {
							f1.val('reauth');
							p.trigger('change', 'reset');
						}));
					f1.val(protocol);
					vars.mbtn.show();
					if (data.folders) {
						addFolders.call(this, fm, f.path, data.folders);
					}
					if (data.mnt2res) {
						f.mnt2res.val('1');
					}
					f.user.val('done');
					f.pass.val('done');
					f.offline.closest('tr').hide();
				}
				f0.removeData('inrequest');
			},
			fail: function(fm, err){
				jQuery(this.inputs.host[0]).removeData('inrequest');
				this.protocol.trigger('change', 'reset');
			},
			integrateInfo: opts.integrate
		};
	},
	
	/**
	 * Find cwd's nodes from files
	 * 
	 * @param  Array    files
	 * @param  Object   opts   {firstOnly: true|false}
	 */
	findCwdNodes : function(files, opts) {
		var self    = this,
			cwd     = this.getUI('cwd'),
			cwdHash = this.cwd().hash,
			newItem = jQuery();
		
		opts = opts || {};
		
		jQuery.each(files, function(i, f) {
			if (f.phash === cwdHash || self.searchStatus.state > 1) {
				newItem = newItem.add(self.cwdHash2Elm(f.hash));
				if (opts.firstOnly) {
					return false;
				}
			}
		});
		
		return newItem;
	},
	
	/**
	 * Convert from relative URL to abstract URL based on current URL
	 * 
	 * @param  String  URL
	 * @return String
	 */
	convAbsUrl : function(url) {
		if (url.match(/^http/i)) {
			return url;
		}
		if (url.substr(0,2) === '//') {
			return window.location.protocol + url;
		}
		var root = window.location.protocol + '//' + window.location.host,
			reg  = /[^\/]+\/\.\.\//,
			ret;
		if (url.substr(0, 1) === '/') {
			ret = root + url;
		} else {
			ret = root + window.location.pathname.replace(/\/[^\/]+$/, '/') + url;
		}
		ret = ret.replace('/./', '/');
		while(reg.test(ret)) {
			ret = ret.replace(reg, '');
		}
		return ret;
	},
	
	/**
	 * Is same origin to current site
	 * 
	 * @param  String  check url
	 * @return Boolean
	 */
	isSameOrigin : function (checkUrl) {
		var url;
		checkUrl = this.convAbsUrl(checkUrl);
		if (location.origin && window.URL) {
			try {
				url = new URL(checkUrl);
				return location.origin === url.origin;
			} catch(e) {}
		}
		url = document.createElement('a');
		url.href = checkUrl;
		return location.protocol === url.protocol && location.host === url.host && location.port && url.port;
	},
	
	navHash2Id : function(hash) {
		return this.navPrefix + hash;
	},
	
	navId2Hash : function(id) {
		return typeof(id) == 'string' ? id.substr(this.navPrefix.length) : false;
	},
	
	cwdHash2Id : function(hash) {
		return this.cwdPrefix + hash;
	},
	
	cwdId2Hash : function(id) {
		return typeof(id) == 'string' ? id.substr(this.cwdPrefix.length) : false;
	},
	
	/**
	 * navHash to jQuery element object
	 *
	 * @param      String  hash    nav hash
	 * @return     Object  jQuery element object
	 */
	navHash2Elm : function(hash) {
		return jQuery(document.getElementById(this.navHash2Id(hash)));
	},

	/**
	 * cwdHash to jQuery element object
	 *
	 * @param      String  hash    cwd hash
	 * @return     Object  jQuery element object
	 */
	cwdHash2Elm : function(hash) {
		return jQuery(document.getElementById(this.cwdHash2Id(hash)));
	},

	isInWindow : function(elem, nochkHide) {
		var elm, rect;
		if (! (elm = elem.get(0))) {
			return false;
		}
		if (! nochkHide && elm.offsetParent === null) {
			return false;
		}
		rect = elm.getBoundingClientRect();
		return document.elementFromPoint(rect.left, rect.top)? true : false;
	},
	
	/**
	 * calculate elFinder node z-index
	 * 
	 * @return void
	 */
	zIndexCalc : function() {
		var self = this,
			node = this.getUI(),
			ni = node.css('z-index');
		if (ni && ni !== 'auto' && ni !== 'inherit') {
			self.zIndex = ni;
		} else {
			node.parents().each(function(i, n) {
				var z = jQuery(n).css('z-index');
				if (z !== 'auto' && z !== 'inherit' && (z = parseInt(z))) {
					self.zIndex = z;
					return false;
				}
			});
		}
	},
	
	/**
	 * Load JavaScript files
	 * 
	 * @param  Array    urls      to load JavaScript file URLs
	 * @param  Function callback  call back function on script loaded
	 * @param  Object   opts      Additional options to jQuery.ajax OR {loadType: 'tag'} to load by script tag
	 * @param  Object   check     { obj: (Object)ParentObject, name: (String)"Attribute name", timeout: (Integer)milliseconds }
	 * @return elFinder
	 */
	loadScript : function(urls, callback, opts, check) {
		var defOpts = {
				dataType : 'script',
				cache    : true
			},
			success, cnt, scripts = {}, results = {};
		
		opts = opts || {};
		if (opts.tryRequire && this.hasRequire) {
			require(urls, callback, opts.error);
		} else {
			success = function() {
				var cnt, fi, hasError;
				jQuery.each(results, function(i, status) {
					if (status !== 'success' && status !== 'notmodified') {
						hasError = true;
						return false;
					}
				});
				if (!hasError) {
					if (jQuery.isFunction(callback)) {
						if (check) {
							if (typeof check.obj[check.name] === 'undefined') {
								cnt = check.timeout? (check.timeout / 10) : 1;
								fi = setInterval(function() {
									if (--cnt < 0 || typeof check.obj[check.name] !== 'undefined') {
										clearInterval(fi);
										callback();
									}
								}, 10);
							} else {
								callback();
							}
						} else {
							callback();
						}
					}
				} else {
					if (opts.error && jQuery.isFunction(opts.error)) {
						opts.error({ loadResults: results });
					}
				}
			};

			if (opts.loadType === 'tag') {
				jQuery('head > script').each(function() {
					scripts[this.src] = this;
				});
				cnt = urls.length;
				jQuery.each(urls, function(i, url) {
					var done = false,
						script;
					
					if (scripts[url]) {
						results[i] = scripts[url]._error || 'success';
						(--cnt < 1) && success();
					} else {
						script = document.createElement('script');
						script.charset = opts.charset || 'UTF-8';
						jQuery('head').append(script);
						script.onload = script.onreadystatechange = function() {
							if ( !done && (!this.readyState ||
									this.readyState === 'loaded' || this.readyState === 'complete') ) {
								done = true;
								results[i] = 'success';
								(--cnt < 1) && success();
							}
						};
						script.onerror = function(err) {
							results[i] = script._error = (err && err.type)? err.type : 'error';
							(--cnt < 1) && success();
						};
						script.src = url;
					}
				});
			} else {
				opts = jQuery.isPlainObject(opts)? Object.assign(defOpts, opts) : defOpts;
				cnt = 0;
				(function appendScript(d, status) {
					if (d !== void(0)) {
						results[cnt++] = status;
					}
					if (urls.length) {
						jQuery.ajax(Object.assign({}, opts, {
							url: urls.shift(),
							success: appendScript,
							error: appendScript
						}));
					} else {
						success();
					}
				})();
			}
		}
		return this;
	},
	
	/**
	 * Load CSS files
	 * 
	 * @param  Array    to load CSS file URLs
	 * @param  Object   options
	 * @return elFinder
	 */
	loadCss : function(urls, opts) {
		var self = this,
			clName, dfds;
		if (typeof urls === 'string') {
			urls = [ urls ];
		}
		if (opts) {
			if (opts.className) {
				clName = opts.className;
			}
			if (opts.dfd && opts.dfd.promise) {
				dfds = [];
			}
		}
		jQuery.each(urls, function(i, url) {
			var link, df;
			url = self.convAbsUrl(url).replace(/^https?:/i, '');
			if (dfds) {
				dfds[i] = jQuery.Deferred();
			}
			if (! jQuery('head > link[href="' + self.escape(url) + '"]').length) {
				link = document.createElement('link');
				link.type = 'text/css';
				link.rel = 'stylesheet';
				link.href = url;
				if (clName) {
					link.className = clName;
				}
				if (dfds) {
					link.onload = function() {
						dfds[i].resolve();
					};
					link.onerror = function() {
						dfds[i].reject();
					};
				}
				jQuery('head').append(link);
			} else {
				dfds && dfds[i].resolve();
			}
		});
		if (dfds) {
			jQuery.when.apply(null, dfds).done(function() {
				opts.dfd.resolve();
			}).fail(function() {
				opts.dfd.reject();
			});
		}
		return this;
	},
	
	/**
	 * Abortable async job performer
	 * 
	 * @param func Function
	 * @param arr  Array
	 * @param opts Object
	 * 
	 * @return Object jQuery.Deferred that has an extended method _abort()
	 */
	asyncJob : function(func, arr, opts) {
		var dfrd = jQuery.Deferred(),
			abortFlg = false,
			parms = Object.assign({
				interval : 0,
				numPerOnce : 1
			}, opts || {}),
			resArr = [],
			vars =[],
			curVars = [],
			exec,
			tm;
		
		dfrd._abort = function(resolve) {
			tm && clearTimeout(tm);
			vars = [];
			abortFlg = true;
			if (dfrd.state() === 'pending') {
				dfrd[resolve? 'resolve' : 'reject'](resArr);
			}
		};
		
		dfrd.fail(function() {
			dfrd._abort();
		}).always(function() {
			dfrd._abort = function() {};
		});

		if (typeof func === 'function' && Array.isArray(arr)) {
			vars = arr.concat();
			exec = function() {
				var i, len, res;
				if (abortFlg) {
					return;
				}
				curVars = vars.splice(0, parms.numPerOnce);
				len = curVars.length;
				for (i = 0; i < len; i++) {
					if (abortFlg) {
						break;
					}
					res = func(curVars[i]);
					(res !== null) && resArr.push(res);
				}
				if (abortFlg) {
					return;
				}
				if (vars.length) {
					tm = setTimeout(exec, parms.interval);
				} else {
					dfrd.resolve(resArr);
				}
			};
			if (vars.length) {
				tm = setTimeout(exec, 0);
			} else {
				dfrd.resolve(resArr);
			}
		} else {
			dfrd.reject();
		}
		return dfrd;
	},
	
	getSize : function(targets) {
		var self = this,
			reqs = [],
			tgtlen = targets.length,
			dfrd = jQuery.Deferred().fail(function() {
				jQuery.each(reqs, function(i, req) {
					if (req) {
						req.syncOnFail && req.syncOnFail(false);
						req.reject();
					}
				});
			}),
			getLeafRoots = function(file) {
				var targets = [];
				if (file.mime === 'directory') {
					jQuery.each(self.leafRoots, function(hash, roots) {
						var phash;
						if (hash === file.hash) {
							targets.push.apply(targets, roots);
						} else {
							phash = (self.file(hash) || {}).phash;
							while(phash) {
								if (phash === file.hash) {
									targets.push.apply(targets, roots);
								}
								phash = (self.file(phash) || {}).phash;
							}
						}
					});
				}
				return targets;
			},
			checkPhash = function(hash) {
				var dfd = jQuery.Deferred(),
					dir = self.file(hash),
					target = dir? dir.phash : hash;
				if (target && ! self.file(target)) {
					self.request({
						data : {
							cmd    : 'parents',
							target : target
						},
						preventFail : true
					}).done(function() {
						self.one('parentsdone', function() {
							dfd.resolve();
						});
					}).fail(function() {
						dfd.resolve();
					});
				} else {
					dfd.resolve();
				}
				return dfd;
			},
			cache = function() {
				var dfd = jQuery.Deferred(),
					cnt = Object.keys(self.leafRoots).length;
				
				if (cnt > 0) {
					jQuery.each(self.leafRoots, function(hash) {
						checkPhash(hash).done(function() {
							--cnt;
							if (cnt < 1) {
								dfd.resolve();
							}
						});
					});
				} else {
					dfd.resolve();
				}
				return dfd;
			};

		self.autoSync('stop');
		cache().done(function() {
			var files = [], grps = {}, dfds = [], cache = [], singles = {};
			
			jQuery.each(targets, function() {
				files.push.apply(files, getLeafRoots(self.file(this)));
			});
			targets.push.apply(targets, files);
			
			jQuery.each(targets, function() {
				var root = self.root(this),
					file = self.file(this);
				if (file && (file.sizeInfo || file.mime !== 'directory')) {
					cache.push(jQuery.Deferred().resolve(file.sizeInfo? file.sizeInfo : {size: file.size, dirCnt: 0, fileCnt : 1}));
				} else {
					if (! grps[root]) {
						grps[root] = [ this.toString() ];
					} else {
						grps[root].push(this.toString());
					}
				}
			});
			
			jQuery.each(grps, function() {
				var idx = dfds.length;
				if (this.length === 1) {
					singles[idx] = this[0];
				}
				dfds.push(self.request({
					data : {cmd : 'size', targets : this},
					preventDefault : true
				}));
			});
			reqs.push.apply(reqs, dfds);
			dfds.push.apply(dfds, cache);
			
			jQuery.when.apply($, dfds).fail(function() {
				dfrd.reject();
			}).done(function() {
				var cache = function(h, data) {
						var file;
						if (file = self.file(h)) {
							file.sizeInfo = { isCache: true };
							jQuery.each(['size', 'dirCnt', 'fileCnt'], function() {
								file.sizeInfo[this] = data[this] || 0;
							});
							file.size = parseInt(file.sizeInfo.size);
							changed.push(file);
						}
					},
					size = 0,
					fileCnt = 0,
					dirCnt = 0,
					argLen = arguments.length,
					cnts = [],
					cntsTxt = '',
					changed = [],
					i, file, data;
				
				for (i = 0; i < argLen; i++) {
					data = arguments[i];
					file = null;
					if (!data.isCache) {
						if (singles[i] && (file = self.file(singles[i]))) {
							cache(singles[i], data);
						} else if (data.sizes && jQuery.isPlainObject(data.sizes)) {
							jQuery.each(data.sizes, function(h, sizeInfo) {
								cache(h, sizeInfo);
							});
						}
					}
					size += parseInt(data.size);
					if (fileCnt !== false) {
						if (typeof data.fileCnt === 'undefined') {
							fileCnt = false;
						} else {
							fileCnt += parseInt(data.fileCnt || 0);
						}
					}
					if (dirCnt !== false) {
						if (typeof data.dirCnt === 'undefined') {
							dirCnt = false;
						} else {
							dirCnt += parseInt(data.dirCnt || 0);
						}
					}
				}
				changed.length && self.change({changed: changed});
				
				if (dirCnt !== false){
					cnts.push(self.i18n('folders') + ': ' + (dirCnt - (tgtlen > 1? 0 : 1)));
				}
				if (fileCnt !== false){
					cnts.push(self.i18n('files') + ': ' + fileCnt);
				}
				if (cnts.length) {
					cntsTxt = '<br>' + cnts.join(', ');
				}
				dfrd.resolve({
					size: size,
					fileCnt: fileCnt,
					dirCnt: dirCnt,
					formated: (size >= 0 ? self.formatSize(size) : self.i18n('unknown')) + cntsTxt
				});
			});
			
			self.autoSync();
		});
		
		return dfrd;
	},

	/**
	 * Worker Object URL for Blob URL of getWorker()
	 */
	wkObjUrl : null,

	/**
	 * Gets the web worker.
	 *
	 * @param      {Object}  options  The options
	 * @return     {Worker}  The worker.
	 */
	getWorker : function(options){
		// for to make blob URL
		function woker() {
			self.onmessage = function(e) {
				var d = e.data;
				try {
					self.data = d.data;
					if (d.scripts) {
						for(var i = 0; i < d.scripts.length; i++) {
							importScripts(d.scripts[i]);
						}
					}
					self.postMessage(self.res);
				} catch (e) {
					self.postMessage({error: e.toString()});
				}
			};
		}
		// get woker
		var wk;
		try {
			if (!this.wkObjUrl) {
				this.wkObjUrl = (window.URL || window.webkitURL).createObjectURL(new Blob(
					[woker.toString().replace(/\s+/g, ' ').replace(/ *([^\w]) */g, '$1').replace(/^function\b.+?\{|\}$/g, '')],
					{ type:'text/javascript' }
				));
			}
			wk = new Worker(this.wkObjUrl, options);
		} catch(e) {
			this.debug('error', e.toString());
		}
		return wk;
	},

	/**
	 * Get worker absolute URL by filename
	 *
	 * @param      {string}  filename  The filename
	 * @return     {<type>}  The worker url.
	 */
	getWorkerUrl : function(filename) {
		return this.convAbsUrl(this.baseUrl + 'js/worker/' + filename);
	},

	/**
	 * Gets the theme object by settings of options.themes
	 *
	 * @param  String  themeid  The themeid
	 * @return Object  jQuery.Deferred
	 */
	getTheme : function(themeid) {
		var self = this,
			dfd = jQuery.Deferred(),
			absUrl = function(url, base) {
				if (!base) {
					base = self.convAbsUrl(self.baseUrl);
				}
				if (Array.isArray(url)) {
					return jQuery.map(url, function(v) {
						return absUrl(v, base);
					});
				} else {
					return url.match(/^(?:http|\/\/)/i)? url : base + url.replace(/^(?:\.\/|\/)/, '');
				}
			},
			themeObj, m;
		if (themeid && (themeObj = self.options.themes[themeid])) {
			if (typeof themeObj === 'string') {
				url = absUrl(themeObj);
				if (m = url.match(/^(.+\/)[^/]+\.json$/i)) {
					jQuery.getJSON(url).done(function(data) {
						themeObj = data;
						themeObj.id = themeid;
						if (themeObj.cssurls) {
							themeObj.cssurls = absUrl(themeObj.cssurls, m[1]);
						}
						dfd.resolve(themeObj);
					}).fail(function() {
						dfd.reject();
					});
				} else {
					dfd.resolve({
						id: themeid,
						name: themeid,
						cssurls: [url]
					});
				}
			} else if (jQuery.isPlainObject(themeObj) && themeObj.cssurls) {
				themeObj.id = themeid;
				themeObj.cssurls = absUrl(themeObj.cssurls);
				if (!Array.isArray(themeObj.cssurls)) {
					themeObj.cssurls = [themeObj.cssurls];
				}
				if (!themeObj.name) {
					themeObj.name = themeid;
				}
				dfd.resolve(themeObj);
			} else {
				dfd.reject();
			}
		} else {
			dfd.reject();
		}
		return dfd;
	},

	/**
	 * Change current theme
	 *
	 * @param  String  themeid  The themeid
	 * @return Object  this elFinder instance
	 */
	changeTheme : function(themeid) {
		var self = this;
		if (themeid) {
			if (self.options.themes[themeid] && (!self.theme || self.theme.id !== themeid)) {
				self.getTheme(themeid).done(function(themeObj) {
					if (themeObj.cssurls) {
						jQuery('head>link.elfinder-theme-ext').remove();
						self.loadCss(themeObj.cssurls, {
							className: 'elfinder-theme-ext',
							dfd: jQuery.Deferred().done(function() {
								self.theme = themeObj;
								self.trigger && self.trigger('themechange');
							})
						});
					}
				});
			} else if (themeid === 'default' && self.theme && self.theme.id !== 'default') {
				jQuery('head>link.elfinder-theme-ext').remove();
				self.theme = null;
				self.trigger && self.trigger('themechange');
			}
		}
		return this;
	},

	/**
	 * Apply leaf root stats to target directory
	 *
	 * @param      object     dir     object of target directory
	 * @param      boolean    update  is force update
	 * 
	 * @return     boolean    dir object was chenged 
	 */
	applyLeafRootStats : function(dir, update) {
		var self = this,
			prev = update? dir : (self.file(dir.hash) || dir),
			prevTs = prev.ts,
			change = false;
		// backup original stats
		if (update || !dir._realStats) {
			dir._realStats = {
				locked: dir.locked || 0,
				dirs: dir.dirs || 0,
				ts: dir.ts
			};
		}
		// set lock
		dir.locked = 1;
		if (!prev.locked) {
			change = true;
		}
		// has leaf root to `dirs: 1`
		dir.dirs = 1;
		if (!prev.dirs) {
			change = true;
		}
		// set ts
		jQuery.each(self.leafRoots[dir.hash], function() {
			var f = self.file(this);
			if (f && f.ts && (dir.ts || 0) < f.ts) {
				dir.ts = f.ts;
			}
		});
		if (prevTs !== dir.ts) {
			change = true;
		}

		return change;
	},

	/**
	 * To aborted XHR object
	 * 
	 * @param Object xhr
	 * @param Object opts
	 * 
	 * @return void
	 */
	abortXHR : function(xhr, o) {
		var opts = o || {};
		
		if (xhr) {
			opts.quiet && (xhr.quiet = true);
			if (opts.abort && xhr._requestId) {
				this.request({
					data: {
						cmd: 'abort',
						id: xhr._requestId
					},
					preventDefault: true
				});
			}
			xhr.abort();
			xhr = void 0;
		}
	},

	/**
	 * Sets the custom header by xhr response header with options.parrotHeaders
	 *
	 * @param Object xhr
	 * 
	 * @return void
	 */
	setCustomHeaderByXhr : function(xhr) {
		var self = this;
		if (xhr.getResponseHeader && self.parrotHeaders && self.parrotHeaders.length) {
			jQuery.each(self.parrotHeaders, function(i, h) {
				var val = xhr.getResponseHeader(h);
				if (val) {
					self.customHeaders[h] = val;
					self.sessionStorage('core-ph:'+h, val);
				} else if (typeof val === 'string') {
					delete self.customHeaders[h];
					self.sessionStorage('core-ph:'+h, null);
				}
			});
		}
	},

	/**
	 * Determines if parrot headers.
	 *
	 * @return     {boolean}  True if parrot headers, False otherwise.
	 */
	hasParrotHeaders : function() {
		var res = false,
			phs = this.parrotHeaders;
		if (Object.keys(this.customHeaders).length) {
			for (var i = 0; i < phs.length; i++) {
				if (this.customHeaders[phs[i]]) {
					res = true;
					break;
				}
			}
		}
		return res;
	},

	/**
	 * Gets the request identifier
	 *
	 * @return  String  The request identifier.
	 */
	getRequestId : function() {
		return (+ new Date()).toString(16) + Math.floor(1000 * Math.random()).toString(16);
	},
	
	/**
	 * Flip key and value of array or object
	 * 
	 * @param  Array | Object  { a: 1, b: 1, c: 2 }
	 * @param  Mixed           Static value
	 * @return Object          { 1: "b", 2: "c" }
	 */
	arrayFlip : function (trans, val) {
		var key,
			tmpArr = {},
			isArr = jQuery.isArray(trans);
		for (key in trans) {
			if (isArr || trans.hasOwnProperty(key)) {
				tmpArr[trans[key]] = val || key;
			}
		}
		return tmpArr;
	},
	
	/**
	 * Return array ["name without extention", "extention"]
	 * 
	 * @param String name
	 * 
	 * @return Array
	 * 
	 */
	splitFileExtention : function(name) {
		var m;
		if (m = name.match(/^(.+?)?\.((?:tar\.(?:gz|bz|bz2|z|lzo))|cpio\.gz|ps\.gz|xcf\.(?:gz|bz2)|[a-z0-9]{1,10})$/i)) {
			if (typeof m[1] === 'undefined') {
				m[1] = '';
			}
			return [m[1], m[2]];
		} else {
			return [name, ''];
		}
	},
	
	/**
	 * Slice the ArrayBuffer by sliceSize
	 *
	 * @param      arraybuffer  arrayBuffer  The array buffer
	 * @param      Number       sliceSize    The slice size
	 * @return     Array   Array of sleced arraybuffer
	 */
	sliceArrayBuffer : function(arrayBuffer, sliceSize) {
		var segments= [],
			fi = 0;
		while(fi * sliceSize < arrayBuffer.byteLength){
			segments.push(arrayBuffer.slice(fi * sliceSize, (fi + 1) * sliceSize));
			fi++;
		}
		return segments;
	},

	arrayBufferToBase64 : function(ab) {
		if (!window.btoa) {
			return '';
		}
		var dView = new Uint8Array(ab), // Get a byte view
			arr = Array.prototype.slice.call(dView), // Create a normal array
			arr1 = arr.map(function(item) {
				return String.fromCharCode(item); // Convert
			});
	    return window.btoa(arr1.join('')); // Form a string
	},

	log : function(m) { window.console && window.console.log && window.console.log(m); return this; },
	
	debug : function(type, m) {
		var self = this,
			d = this.options.debug,
			tb = this.options.toastBackendWarn,
			tbOpts, showlog;

		if (type === 'backend-error') {
			if (! this.cwd().hash || (d && (d === 'all' || d['backend-error']))) {
				m = Array.isArray(m)? m : [ m ];
				this.error(m);
			}
		} else if (type === 'backend-warning') {
			showlog = true;
			if (tb) {
				tbOpts = jQuery.isPlainObject(tb)? tb : {};
				jQuery.each(Array.isArray(m)? m : [ m ], function(i, m) {
					self.toast(Object.assign({
						mode : 'warning',
						msg: m
					}, tbOpts));
				});
			}
		} else if (type === 'backend-debug') {
			this.trigger('backenddebug', m);
		}
		
		if (showlog || (d && (d === 'all' || d[type]))) {
			window.console && window.console.log && window.console.log('elfinder debug: ['+type+'] ['+this.id+']', m);
		}

		return this;
	},

	/**
	 * Parse response.debug and trigger debug
	 *
	 * @param      Object  response  The response
	 */
	responseDebug : function(response) {
		var rd = response.debug,
			d;
		if (rd) {
			// set options.debug
			d = this.options.debug;
			if (!d || d !== 'all') {
				if (!d) {
					d = this.options.debug = {};
				}
				d['backend-error'] = true;
				d['warning'] = true;
			}
			if (rd.mountErrors && (typeof rd.mountErrors === 'string' || (Array.isArray(rd.mountErrors) && rd.mountErrors.length))) {
				this.debug('backend-error', rd.mountErrors);
			}
			if (rd.backendErrors && (typeof rd.backendErrors === 'string' || (Array.isArray(rd.backendErrors) && rd.backendErrors.length))) {
				this.debug('backend-warning', rd.backendErrors);
			}
		}
	},

	time : function(l) { window.console && window.console.time && window.console.time(l); },
	timeEnd : function(l) { window.console && window.console.timeEnd && window.console.timeEnd(l); }
	

};

/**
 * for conpat ex. ie8...
 *
 * Object.keys() - JavaScript | MDN
 * https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
 */
if (!Object.keys) {
	Object.keys = (function () {
		var hasOwnProperty = Object.prototype.hasOwnProperty,
				hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
				dontEnums = [
					'toString',
					'toLocaleString',
					'valueOf',
					'hasOwnProperty',
					'isPrototypeOf',
					'propertyIsEnumerable',
					'constructor'
				],
				dontEnumsLength = dontEnums.length;

		return function (obj) {
			if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) throw new TypeError('Object.keys called on non-object');

			var result = [];

			for (var prop in obj) {
				if (hasOwnProperty.call(obj, prop)) result.push(prop);
			}

			if (hasDontEnumBug) {
				for (var i=0; i < dontEnumsLength; i++) {
					if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]);
				}
			}
			return result;
		};
	})();
}
// Array.isArray
if (!Array.isArray) {
	Array.isArray = function(arr) {
		return jQuery.isArray(arr);
	};
}
// Object.assign
if (!Object.assign) {
	Object.assign = function() {
		return jQuery.extend.apply(null, arguments);
	};
}
// String.repeat
if (!String.prototype.repeat) {
	String.prototype.repeat = function(count) {
		'use strict';
		if (this == null) {
			throw new TypeError('can\'t convert ' + this + ' to object');
		}
		var str = '' + this;
		count = +count;
		if (count != count) {
			count = 0;
		}
		if (count < 0) {
			throw new RangeError('repeat count must be non-negative');
		}
		if (count == Infinity) {
			throw new RangeError('repeat count must be less than infinity');
		}
		count = Math.floor(count);
		if (str.length == 0 || count == 0) {
			return '';
		}
		// Ensuring count is a 31-bit integer allows us to heavily optimize the
		// main part. But anyway, most current (August 2014) browsers can't handle
		// strings 1 << 28 chars or longer, so:
		if (str.length * count >= 1 << 28) {
			throw new RangeError('repeat count must not overflow maximum string size');
		}
		var rpt = '';
		for (var i = 0; i < count; i++) {
			rpt += str;
		}
		return rpt;
	};
}
// String.trim
if (!String.prototype.trim) {
	String.prototype.trim = function() {
		return this.replace(/^\s+|\s+$/g, '');
	};
}
// Array.apply
(function () {
	try {
		Array.apply(null, {});
		return;
	} catch (e) { }

	var toString = Object.prototype.toString,
		arrayType = '[object Array]',
		_apply = Function.prototype.apply,
		slice = /*@cc_on @if (@_jscript_version <= 5.8)
			function () {
				var a = [], i = this.length;
				while (i-- > 0) a[i] = this[i];
				return a;
			}@else@*/Array.prototype.slice/*@end@*/;

	Function.prototype.apply = function apply(thisArg, argArray) {
		return _apply.call(this, thisArg,
			toString.call(argArray) === arrayType ? argArray : slice.call(argArray));
	};
})();
// Array.from
if (!Array.from) {
	Array.from = function(obj) {
		return obj.length === 1 ? [obj[0]] : Array.apply(null, obj);
	};
}
// window.requestAnimationFrame and window.cancelAnimationFrame
if (!window.cancelAnimationFrame) {
// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
// requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel
// MIT license
(function() {
    var lastTime = 0;
    var vendors = ['ms', 'moz', 'webkit', 'o'];
    for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
        window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
        window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] 
                                   || window[vendors[x]+'CancelRequestAnimationFrame'];
    }
 
    if (!window.requestAnimationFrame)
        window.requestAnimationFrame = function(callback, element) {
            var currTime = new Date().getTime();
            var timeToCall = Math.max(0, 16 - (currTime - lastTime));
            var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 
              timeToCall);
            lastTime = currTime + timeToCall;
            return id;
        };
 
    if (!window.cancelAnimationFrame)
        window.cancelAnimationFrame = function(id) {
            clearTimeout(id);
        };
}());
}

Youez - 2016 - github.com/yon3zu
LinuXploit