text: /**
   * @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 = '';
  
  	/**
  	 * 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.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 (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(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) {
  			return columnNames[key] || self.i18n(key);
  		};
  
  		/**
  		 * 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){
  						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 : (function() {
  		var res = false,
  			test = 'elftest=';
  		document.cookie = test + '1';
  		res = document.cookie.split(test).length === 2;
  		document.cookie = test + ';max-age=0';
  		return res;
  	})(),
  
  	/**
  	 * 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' : '');
  		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];
  			}
  		}
  		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);
          };
  }());
  }
  