js-dos
Version:
Full-featured DOS player with multiple emulator backends
1,764 lines (1,322 loc) • 240 kB
JavaScript
// Folder and File Explorer. A pure, zero-dependencies Javascript widget.
// (C) 2020 CubicleSoft. All Rights Reserved.
(function() {
// Prevent multiple instances.
if (window.hasOwnProperty('FileExplorer')) return;
var EscapeHTML = function(text) {
var map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}
var FormatStr = function(format) {
var args = Array.prototype.slice.call(arguments, 1);
return format.replace(/{(\d+)}/g, function(match, number) {
return (typeof args[number] != 'undefined' ? args[number] : match);
});
};
var GetDisplayFilesize = function(numbytes, adjustprecision, units) {
if (numbytes == 0) return '0 Bytes';
if (numbytes == 1) return '1 Byte';
numbytes = Math.abs(numbytes);
var magnitude, abbreviations;
if (units && units.toLowerCase() === 'iec_formal')
{
magnitude = Math.pow(2, 10);
abbreviations = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
}
else if (units && units.toLowerCase() === 'si')
{
magnitude = Math.pow(10, 3);
abbreviations = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
}
else
{
magnitude = Math.pow(2, 10);
abbreviations = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
}
var pos = Math.floor(Math.log(numbytes) / Math.log(magnitude));
var result = (numbytes / Math.pow(magnitude, pos));
return (pos == 0 || (adjustprecision && result >= 99.995) ? result.toFixed(0) : result.toFixed(2)) + ' ' + abbreviations[pos];
};
var CreateNode = function(tag, classes, attrs, styles) {
var elem = document.createElement(tag);
if (classes)
{
if (typeof classes === 'string') elem.className = classes;
else elem.className = classes.join(' ');
}
if (attrs) Object.assign(elem, attrs);
if (styles) Object.assign(elem.style, styles);
return elem;
};
var DebounceAttributes = function(options) {
if (!(this instanceof DebounceAttributes)) return new DebounceAttributes(options);
var intervaltimer = null, numsame;
var $this = this;
var defaults = {
watchers: [],
interval: 50,
stopsame: 1,
callback: null,
intervalcallback: null
};
$this.settings = Object.assign({}, defaults, options);
var MainIntervalHandler = function() {
var nummatches = 0;
for (var x = 0; x < $this.settings.watchers.length; x++)
{
var watcher = $this.settings.watchers[x];
if (watcher.val === watcher.elem[watcher.attr]) nummatches++;
else watcher.val = watcher.elem[watcher.attr];
}
if (nummatches < $this.settings.watchers.length)
{
numsame = 0;
if ($this.settings.intervalcallback) $this.settings.intervalcallback.call($this);
}
else
{
numsame++;
if (numsame >= $this.settings.stopsame)
{
$this.Stop();
if ($this.settings.intervalcallback) $this.settings.intervalcallback.call($this);
if ($this.settings.callback) $this.settings.callback.call($this);
}
}
};
// Public functions.
$this.Start = function() {
if (!intervaltimer)
{
numsame = 0;
intervaltimer = setInterval(MainIntervalHandler, $this.settings.interval);
}
};
$this.Stop = function() {
if (intervaltimer)
{
clearInterval(intervaltimer);
intervaltimer = null;
}
};
$this.Destroy = function() {
$this.Stop();
$this = null;
}
};
function GetScrollLineHeight() {
var iframe = document.createElement('iframe');
iframe.src = '#';
document.body.appendChild(iframe);
var iwin = iframe.contentWindow;
var idoc = iwin.document;
idoc.open();
idoc.write('<!DOCTYPE html><html><head></head><body><span>a</span></body></html>');
idoc.close();
var span = idoc.body.firstElementChild;
var r = span.offsetHeight;
document.body.removeChild(iframe);
return r;
}
var scrolllineheight = GetScrollLineHeight();
// Clean up history stack.
var capturingrefs = 0, prevscrollrestore;
function HistoryPopStateHandler(e) {
if (!capturingrefs)
{
if (e.state && e.state._fileexplorer)
{
var prevscrollstatecopy = e.state._fileexplorerprevscroll;
window.history.back();
if (prevscrollstatecopy) prevscrollrestore = prevscrollstatecopy;
}
else if (prevscrollrestore)
{
setTimeout(function() {
window.history.scrollRestoration = prevscrollrestore;
prevscrollrestore = null;
}, 20);
}
}
}
window.addEventListener('popstate', HistoryPopStateHandler, true);
if (window.history.state && window.history.state._fileexplorer)
{
var prevscrollstatecopy = window.history.state._fileexplorerprevscroll;
window.history.back();
if (prevscrollstatecopy) prevscrollrestore = prevscrollstatecopy;
}
// Extracts and returns global drag group icon data for drag-and-drop operations.
function GetFileExplorerDragIconData(e, group) {
if (!e.dataTransfer) return false;
for (var x = 0; x < e.dataTransfer.types.length; x++)
{
if (e.dataTransfer.types[x].startsWith('application/file-explorer-icon;'))
{
try
{
var icondata = JSON.parse(e.dataTransfer.types[x].substring(31));
return (!group || group === icondata.group ? icondata : false);
}
catch (e)
{
}
}
}
return false;
}
// Creates a custom overlay that tracks with the position when entering the window.
var fe_numdragenters = 0, fe_dragiconoverlay, fe_dragiconoverlaypos = {};
function OverlayDragEnterHandler(e) {
if (!fe_numdragenters)
{
var icondata = GetFileExplorerDragIconData(e);
if (icondata !== false)
{
// Create the floating icon tracking overlay.
fe_dragiconoverlay = CreateNode('div', ['fe_fileexplorer_floating_drag_icon_wrap']);
var innernode = CreateNode('div', ['fe_fileexplorer_floating_drag_icon_wrap_inner']);
var iconnode = CreateNode('div', ['fe_fileexplorer_item_icon']);
if (icondata.numitems > 1) innernode.dataset.numitems = icondata.numitems;
iconnode.classList.add('fe_fileexplorer_item_icon_' + icondata.type);
innernode.appendChild(iconnode);
fe_dragiconoverlay.appendChild(innernode);
document.body.appendChild(fe_dragiconoverlay);
fe_dragiconoverlaypos = {
lastx: -9999,
lasty: 0,
xdiff: Math.floor(fe_dragiconoverlay.offsetWidth / 2),
ydiff: Math.floor(fe_dragiconoverlay.offsetHeight - 8)
};
}
}
fe_numdragenters++;
}
// Moves the overlay to the drag position.
function OverlayDragOverHandler(e) {
if (fe_dragiconoverlay && (fe_dragiconoverlaypos.lastx !== e.clientX || fe_dragiconoverlaypos.lasty !== e.clientY))
{
fe_dragiconoverlaypos.lastx = e.clientX;
fe_dragiconoverlaypos.lasty = e.clientY;
// Update the overlay position.
fe_dragiconoverlay.style.left = (e.clientX - fe_dragiconoverlaypos.xdiff) + 'px';
fe_dragiconoverlay.style.top = (e.clientY - fe_dragiconoverlaypos.ydiff) + 'px';
}
}
// Removes the custom overlay when leaving the window.
function OverlayDragLeaveHandler(e) {
fe_numdragenters--;
if (fe_numdragenters < 1)
{
fe_numdragenters = 0;
if (fe_dragiconoverlay)
{
fe_dragiconoverlay.parentNode.removeChild(fe_dragiconoverlay);
fe_dragiconoverlay = null;
}
fe_dragiconoverlaypos = {};
}
}
window.addEventListener('dragenter', OverlayDragEnterHandler, true);
window.addEventListener('dragover', OverlayDragOverHandler, true);
window.addEventListener('dragleave', OverlayDragLeaveHandler, true);
window.addEventListener('drop', OverlayDragLeaveHandler, true);
// Basic XMLHttpRequest (XHR) wrapper.
var PrepareXHR = function(options) {
if (!(this instanceof PrepareXHR)) return new PrepareXHR(options);
var sent = false;
var $this = this;
$this.xhr = new XMLHttpRequest();
var RequestEndedHandler = function(e) {
if ($this) $this.xhr = null;
};
$this.xhr.addEventListener('loadend', RequestEndedHandler);
if (options.onsuccess || options.onload) $this.xhr.addEventListener('load', options.onsuccess || options.onload);
if (options.onerror)
{
$this.xhr.addEventListener('error', options.onerror);
if (!options.onabort) $this.xhr.addEventListener('abort', options.onerror);
if (!options.ontimeout) $this.xhr.addEventListener('timeout', options.onerror);
}
if (options.onabort) $this.xhr.addEventListener('abort', options.onabort);
if (options.onloadstart) $this.xhr.addEventListener('loadstart', options.onloadstart);
if (options.onprogress) $this.xhr.addEventListener('progress', options.onprogress);
if (options.ontimeout) $this.xhr.addEventListener('timeout', options.ontimeout);
if (options.onloadend) $this.xhr.addEventListener('loadend', options.onloadend);
// Public functions.
// Transparently route event listener registration/removals.
$this.upload = {};
$this.upload.addEventListener = function(type, listener, options) {
if (!sent && $this && $this.xhr) $this.xhr.upload.addEventListener(type, listener, options);
};
$this.upload.removeEventListener = function(type, listener, options) {
if ($this && $this.xhr) $this.xhr.upload.removeEventListener(type, listener, options);
};
$this.addEventListener = function(type, listener, options) {
if (!sent && $this && $this.xhr) $this.xhr.addEventListener(type, listener, options);
};
$this.removeEventListener = function(type, listener, options) {
if ($this && $this.xhr) $this.xhr.removeEventListener(type, listener, options);
};
// Returns the calculated method.
$this.GetMethod = function() {
return (options.method || (options.params || options.body ? 'POST' : 'GET'));
};
$this.PrepareBody = function() {
if (options.body) return options.body;
var method = $this.GetMethod();
// Build a FormData object.
var xhrbody = (options.params || method === 'POST' ? new FormData() : null);
if (options.params)
{
if (options.params instanceof FormData)
{
xhrbody = options.params;
}
else if (Array.isArray(options.params))
{
for (var x = 0; x < options.params.length; x++) xhrbody.append(options.params[x].name, options.params[x].value);
}
else
{
for (var x in options.params)
{
if (options.params.hasOwnProperty(x))
{
if (typeof options.params[x] === 'string' || typeof options.params[x] === 'number') xhrbody.append(x, options.params[x]);
}
}
}
}
return xhrbody;
};
$this.Send = function(xhrbody) {
if (sent || !$this || !$this.xhr) return;
sent = true;
$this.xhr.open($this.GetMethod(), options.url);
// Set request headers.
if (options.headers)
{
for (var x in options.headers)
{
if (options.headers.hasOwnProperty(x) && typeof options.headers[x] === 'string') $this.xhr.setRequestHeader(x, options.headers[x]);
}
}
if (!xhrbody) xhrbody = $this.PrepareBody();
// Send the XHR request.
$this.xhr.send(xhrbody);
};
$this.Abort = function() {
if (!$this || !$this.xhr) return;
var tempxhr = $this.xhr;
$this.xhr = null;
if (sent) tempxhr.abort();
};
$this.Destroy = function() {
$this.Abort();
$this = null;
};
};
// Image loader. Items can be cancelled.
var ImageLoader = function(options) {
if (!(this instanceof ImageLoader)) return new ImageLoader(options);
var activequeue = {}, numactive = 0, queue = {}, numqueued = 0, minqueueid = 1, nextid = 1;
var $this = this;
var defaults = {
maxactive: 10
};
$this.settings = Object.assign({}, defaults, options);
var ImgLoadHandler = function(e) {
var id = e.target._ilid;
var opts = activequeue[id];
delete activequeue[id];
numactive--;
opts.img.onload = null;
opts.img.onerror = null;
if (opts.callback) opts.callback.call($this, opts, true, e);
$this.ProcessQueue();
};
var ImgErrorHandler = function(e) {
var id = e.target._ilid;
var opts = activequeue[id];
delete activequeue[id];
numactive--;
opts.img.onload = null;
opts.img.onerror = null;
if (opts.callback) opts.callback.call($this, opts, false, e);
$this.ProcessQueue();
};
// Public functions.
// Adds an image request to the queue.
// Required opts keys: src (image URL).
// Optional opts keys: width, height, callback.
// Reserved opts keys: id, started, img.
$this.AddToQueue = function(opts) {
// Ignore if an existing ID is already in a queue.
if (opts.id && ((opts.id in activequeue) || (opts.id in queue))) return;
opts.id = nextid;
queue[nextid] = opts;
numqueued++;
nextid++;
};
// Starts the next images in the queue up to maxactive.
$this.ProcessQueue = function() {
while (numactive < $this.settings.maxactive && numqueued)
{
while (minqueueid < nextid && !queue.hasOwnProperty(minqueueid)) minqueueid++;
// Move an item from the waiting queue to the active queue.
var opts = queue[minqueueid];
delete queue[minqueueid];
numqueued--;
activequeue[opts.id] = opts;
numactive++;
// Create the image.
var imgnode = (opts.width && opts.height ? new Image(opts.width, opts.height) : new Image());
imgnode._ilid = opts.id;
opts.started = Date.now();
opts.img = imgnode;
imgnode.onload = ImgLoadHandler;
imgnode.onerror = ImgErrorHandler;
imgnode.src = opts.src;
}
};
// Checks to see if the supplied ID is in the active queue.
$this.IsActive = function(id) {
return (id in activequeue);
};
// Remove an item from the queue it is in.
$this.RemoveFromQueue = function(id) {
if (id in queue)
{
delete queue[id];
numqueued--;
}
else if (id in activequeue)
{
var opts = activequeue[id];
// Cancel an active image download.
opts.img.onload = null;
opts.img.onerror = null;
opts.img.src = '';
delete opts.img;
delete activequeue[id];
numactive--;
}
};
$this.Destroy = function() {
for (var x in activequeue)
{
if (activequeue.hasOwnProperty(x))
{
activequeue[x].img.onload = null;
activequeue[x].img.onerror = null;
activequeue[x].img.src = '';
delete activequeue[x].img;
}
}
activequeue = {};
numactive = 0;
queue = {};
numqueued = 0;
$this = null;
};
};
// Single instance of ImageLoader for loading thumbnails.
var fe_thumbnailloader = new ImageLoader();
// Folder tracking. Manages information related to folders and files in the defined folder.
// Pass in an array of path segments to define the path. Each path segment is an array consisting of [id, value, attrs].
var Folder = function(path) {
if (!(this instanceof Folder)) return new Folder(path);
var triggers = {}, entries = [], entryidmap = {}, busyref = 0, busyqueue = [], autosort = true;
var $this = this;
if (!path[path.length - 1][2]) path[path.length - 1][2] = {};
// Internal functions.
var DispatchEvent = function(eventname, params) {
if (!triggers[eventname]) return;
triggers[eventname].forEach(function(callback) {
if (Array.isArray(params)) callback.apply($this, params);
else callback.call($this, params);
});
};
// Public DOM-style functions.
$this.addEventListener = function(eventname, callback) {
if (!triggers[eventname]) triggers[eventname] = [];
for (var x in triggers[eventname])
{
if (triggers[eventname][x] === callback) return;
}
triggers[eventname].push(callback);
};
$this.removeEventListener = function(eventname, callback) {
if (!triggers[eventname]) return;
for (var x in triggers[eventname])
{
if (triggers[eventname][x] === callback)
{
triggers[eventname].splice(x, 1);
return;
}
}
};
$this.hasEventListener = function(eventname) {
return (triggers[eventname] && triggers[eventname].length);
};
// Internal variables.
$this.lastrefresh = 0;
$this.waiting = true;
$this.refs = 0;
// Public functions.
// Add the value of newval to the folder busy state. Any queued changes will be applied when cleared.
$this.SetBusyRef = function(newval) {
busyref += newval;
if (busyref < 0) busyref = 0;
while (!busyref && busyqueue.length)
{
var item = busyqueue.shift();
item.callback.apply($this, item.callbackopts);
}
};
$this.IsBusy = function() {
return (busyref > 0);
};
$this.AddBusyQueueCallback = function(callback, callbackopts) {
busyqueue.push({ callback: callback, callbackopts: callbackopts });
$this.SetBusyRef(0);
};
// Internal function to only be used by FileExplorer.
$this.ClearBusyQueueCallbacks = function() {
busyqueue = [];
};
$this.GetPath = function() {
return path;
};
$this.GetPathIDs = function() {
var result = [];
for (var x = 0; x < path.length; x++) result.push(path[x][0]);
return result;
};
// Sets an object containing optional attributes for the path.
// Used primarily to disable some/all tools from functioning for specific folders.
$this.SetAttributes = function(newattrs) {
path[path.length - 1][2] = newattrs;
DispatchEvent('set_attributes');
};
$this.SetAttribute = function(key, value) {
path[path.length - 1][2][key] = value;
DispatchEvent('set_attributes', key);
};
$this.GetAttributes = function() {
return path[path.length - 1][2];
};
$this.SetAutoSort = function(newautosort) {
autosort = (newautosort ? true : false);
};
$this.SortEntries = function() {
if ($this.busy)
{
$this.busyqueue.push({ callback: $this.SortEntries, callbackopts: [] });
return;
}
var localeopts = { numeric: true, sensitivity: 'base' };
entries.sort(function(a, b) {
if (a.type !== b.type) return (a.type === 'folder' ? -1 : 1);
return a.name.localeCompare(b.name, undefined, localeopts);
});
};
// Sets an array of objects containing the folder entries.
// Required per-item object keys: id (unique string), name, type ('folder' or 'file'), hash (unique string).
// Optional per-item object keys: attrs, size, tooltip (tooltip string), thumb (thumbnail image URL), overlay (class name).
$this.SetEntries = function(newentries) {
if ($this.busy)
{
$this.busyqueue.push({ callback: $this.SetEntries, callbackopts: [newentries] });
return;
}
entries = newentries;
if (autosort) $this.SortEntries();
entryidmap = {};
for (var x = 0; x < entries.length; x++)
{
entryidmap[entries[x].id] = x;
}
$this.waiting = false;
DispatchEvent('set_entries');
};
// Creates/Updates multiple entries.
$this.UpdateEntries = function(updatedentries) {
if ($this.busy)
{
$this.busyqueue.push({ callback: $this.UpdateEntries, callbackopts: [updatedentries] });
return;
}
for (var x = 0; x < updatedentries.length; x++)
{
var entry = updatedentries[x];
if (!(entry.id in entryidmap)) entries.push(entry);
else entries[entryidmap[entry.id]] = entry;
}
if (autosort) $this.SortEntries();
entryidmap = {};
for (var x = 0; x < entries.length; x++)
{
entryidmap[entries[x].id] = x;
}
$this.waiting = false;
DispatchEvent('set_entries');
};
// Sets a single entry and triggers a full refresh.
$this.SetEntry = function(entry) {
if ($this.busy)
{
$this.busyqueue.push({ callback: $this.SetEntry, callbackopts: [entry] });
return;
}
if (!(entry.id in entryidmap)) entries.push(entry);
else entries[entryidmap[entry.id]] = entry;
if (autosort) $this.SortEntries();
entryidmap = {};
for (var x = 0; x < entries.length; x++)
{
entryidmap[entries[x].id] = x;
}
$this.waiting = false;
DispatchEvent('set_entries');
};
$this.RemoveEntry = function(id) {
if ($this.busy)
{
$this.busyqueue.push({ callback: $this.RemoveEntry, callbackopts: [id] });
return;
}
if (!(id in entryidmap)) return;
var pos = entryidmap[id];
delete entryidmap[id];
entries.splice(pos, 1);
for (var x = pos; x < entries.length; x++)
{
entryidmap[entries[x].id] = x;
}
DispatchEvent('remove_entry', pos);
};
$this.GetEntries = function() {
return entries;
};
$this.GetEntryIDMap = function() {
return entryidmap;
};
$this.Destroy = function() {
DispatchEvent('destroy');
triggers = {};
entries = [];
entryidmap = {};
busyref = 0
busyqueue = [];
$this.lastrefresh = 0;
$this.waiting = true;
$this.refs = 0;
$this = null;
path = null;
};
};
// Attaches a popup menu to the DOM.
var PopupMenu = function(parentelem, options) {
if (!(this instanceof PopupMenu)) return new PopupMenu(parentelem, options);
var triggers = {};
var $this = this;
var defaults = {
items: [],
resizewatchers: null,
onposition: null,
onselchanged: null,
onselected: null,
oncancel: null,
onleft: null,
onright: null,
ondestroy: null
};
$this.settings = Object.assign({}, defaults, options);
// Initialize the UI elements.
var elems = {
popupwrap: CreateNode('div', ['fe_fileexplorer_popup_wrap'], { tabIndex: 0 }),
innerwrap: CreateNode('div', ['fe_fileexplorer_popup_inner_wrap'])
};
// Track the last hovered/focused item.
var lastitem = false, itemidmap = {};
// Attach elements to DOM.
for (var x = 0; x < $this.settings.items.length; x++)
{
var item = $this.settings.items[x];
if (item === 'split')
{
var itemnode = CreateNode('div', ['fe_fileexplorer_popup_item_split']);
elems.innerwrap.appendChild(itemnode);
}
else
{
var itemnode = CreateNode('div', ['fe_fileexplorer_popup_item_wrap'], { tabIndex: -1 });
var itemicon = CreateNode('div', ['fe_fileexplorer_popup_item_icon']);
var itemiconinner = CreateNode('div', ['fe_fileexplorer_popup_item_icon_inner']);
var itemtext = CreateNode('div', ['fe_fileexplorer_popup_item_text']);
var enabled = (!('enabled' in item) || item.enabled);
if (!enabled) itemnode.classList.add('fe_fileexplorer_popup_item_disabled');
if ('icon' in item) itemiconinner.classList.add(item.icon);
itemtext.innerHTML = item.name;
itemicon.appendChild(itemiconinner);
itemnode.appendChild(itemicon);
itemnode.appendChild(itemtext);
itemnode.dataset.itemid = item.id;
itemidmap[item.id] = item;
elems.innerwrap.appendChild(itemnode);
}
}
elems.popupwrap.appendChild(elems.innerwrap);
parentelem.appendChild(elems.popupwrap);
// Internal functions.
var DispatchEvent = function(eventname, params) {
if (!triggers[eventname]) return;
triggers[eventname].forEach(function(callback) {
if (Array.isArray(params)) callback.apply($this, params);
else callback.call($this, params);
});
};
// Public DOM-style functions.
$this.addEventListener = function(eventname, callback) {
if (!triggers[eventname]) triggers[eventname] = [];
for (var x in triggers[eventname])
{
if (triggers[eventname][x] === callback) return;
}
triggers[eventname].push(callback);
};
$this.removeEventListener = function(eventname, callback) {
if (!triggers[eventname]) return;
for (var x in triggers[eventname])
{
if (triggers[eventname][x] === callback)
{
triggers[eventname].splice(x, 1);
return;
}
}
};
$this.hasEventListener = function(eventname) {
return (triggers[eventname] && triggers[eventname].length);
};
// Register settings callbacks.
if ($this.settings.onposition) $this.addEventListener('position', $this.settings.onposition);
if ($this.settings.onselchanged) $this.addEventListener('selection_changed', $this.settings.onselchanged);
if ($this.settings.onselected) $this.addEventListener('selected', $this.settings.onselected);
if ($this.settings.oncancel) $this.addEventListener('cancelled', $this.settings.oncancel);
if ($this.settings.onleft) $this.addEventListener('left', $this.settings.onleft);
if ($this.settings.onright) $this.addEventListener('right', $this.settings.onright);
if ($this.settings.ondestroy) $this.addEventListener('destroy', $this.settings.ondestroy);
// Set up focus changing closing rules.
var MainFocusHandler = function(e) {
if (!e.isTrusted) return;
var node = e.target;
while (node && node !== elems.popupwrap) node = node.parentNode;
if (node !== elems.popupwrap && allowcancel)
{
lastactiveelem = e.target;
$this.Cancel(e.type === 'focus' ? 'focus' : 'mouse');
}
};
window.addEventListener('mousedown', MainFocusHandler, true);
window.addEventListener('focus', MainFocusHandler, true);
var MainWindowBlurHander = function(e) {
if (e.target === window || e.target === document) $this.Cancel('blur');
};
window.addEventListener('blur', MainWindowBlurHander, true);
// Track mouse movement to update the last hovered/focused item.
var InnerWrapMoveHandler = function(e) {
if (!e.isTrusted) return;
e.preventDefault();
var node = e.target;
while (node && node.parentNode !== elems.innerwrap) node = node.parentNode;
if (node && (lastitem !== node || lastitem !== document.activeElement))
{
if (node.classList.contains('fe_fileexplorer_popup_item_wrap'))
{
node.tabIndex = 0;
node.focus();
if (lastitem !== false) lastitem.tabIndex = -1;
if (lastitem !== node) DispatchEvent('selection_changed', [node.dataset.itemid, itemidmap[node.dataset.itemid]]);
lastitem = node;
}
else if (elems.popupwrap !== document.activeElement)
{
elems.popupwrap.focus();
}
}
};
elems.innerwrap.addEventListener('mousemove', InnerWrapMoveHandler);
var InnerWrapLeaveHandler = function(e) {
if (!e.isTrusted) return;
elems.popupwrap.focus();
};
elems.innerwrap.addEventListener('mouseleave', InnerWrapLeaveHandler);
// Notify listeners that the last item was selected.
var lastactiveelem = document.activeElement;
var NotifySelected = function(etype) {
allowcancel = false;
DispatchEvent('selected', [itemidmap[lastitem.dataset.itemid].id, itemidmap[lastitem.dataset.itemid], lastactiveelem, etype]);
};
// Handle clicks.
var MainClickHandler = function(e) {
if (!e.isTrusted) return;
e.preventDefault();
if (e.button == 0 && lastitem !== false && lastitem === document.activeElement && !lastitem.classList.contains('fe_fileexplorer_popup_item_disabled')) NotifySelected('mouse');
};
elems.innerwrap.addEventListener('mouseup', MainClickHandler);
var StopContextMenu = function(e) {
if (!e.isTrusted) return;
e.preventDefault();
};
elems.innerwrap.addEventListener('contextmenu', StopContextMenu);
// Handle keyboard navigation.
var MainKeyHandler = function(e) {
// The keyboard is modal while the mouse is not. Stop propagation of all keyboard actions.
e.stopPropagation();
if (!e.isTrusted) return;
if (e.keyCode == 37)
{
// Left Arrow. Send event to registered caller (if any).
e.preventDefault();
DispatchEvent('left', lastactiveelem);
}
else if (e.keyCode == 39)
{
// Right Arrow. Send event to registered caller (if any).
e.preventDefault();
DispatchEvent('right', lastactiveelem);
}
else if (e.keyCode == 38)
{
// Up Arrow. Move to previous or last item.
e.preventDefault();
var node = (lastitem === false ? elems.innerwrap.lastChild : lastitem.previousSibling);
while (node && !node.classList.contains('fe_fileexplorer_popup_item_wrap')) node = node.previousSibling;
if (!node) node = elems.innerwrap.lastChild;
if (node)
{
node.tabIndex = 0;
node.focus();
if (lastitem !== false) lastitem.tabIndex = -1;
if (lastitem !== node) DispatchEvent('selection_changed', [node.dataset.itemid, itemidmap[node.dataset.itemid]]);
lastitem = node;
}
if (lastitem !== false) lastitem.focus();
}
else if (e.keyCode == 40)
{
// Down Arrow. Move to next or first item.
e.preventDefault();
var node = (lastitem === false ? elems.innerwrap.firstChild : lastitem.nextSibling);
while (node && !node.classList.contains('fe_fileexplorer_popup_item_wrap')) node = node.nextSibling;
if (!node) node = elems.innerwrap.firstChild;
if (node)
{
node.tabIndex = 0;
node.focus();
if (lastitem !== false) lastitem.tabIndex = -1;
if (lastitem !== node) DispatchEvent('selection_changed', [node.dataset.itemid, itemidmap[node.dataset.itemid]]);
lastitem = node;
}
if (lastitem !== false) lastitem.focus();
}
else if (e.keyCode == 13)
{
// Enter. Select item or cancel the popup if the item is disabled.
e.preventDefault();
if (lastitem === false || lastitem !== document.activeElement || lastitem.classList.contains('fe_fileexplorer_popup_item_disabled')) $this.Cancel('key');
else NotifySelected('key');
}
else if (e.keyCode == 27 || e.keyCode == 9 || e.altKey)
{
// Escape, Tab, or Alt. Cancel the popup.
e.preventDefault();
$this.Cancel('key');
}
};
elems.popupwrap.addEventListener('keydown', MainKeyHandler);
var IgnoreKeyHandler = function(e) {
e.stopPropagation();
if (!e.isTrusted) return;
};
elems.popupwrap.addEventListener('keyup', IgnoreKeyHandler);
elems.popupwrap.addEventListener('keypress', IgnoreKeyHandler);
// Public functions.
// Updates the position of the popup menu.
$this.UpdatePosition = function() {
elems.popupwrap.style.left = '-9999px';
DispatchEvent('position', elems.popupwrap);
};
$this.UpdatePosition();
// Set up a debounced element attribute watcher on window resize.
var updatepositionwatcher;
if (Array.isArray($this.settings.resizewatchers) && $this.settings.resizewatchers.length)
{
updatepositionwatcher = new DebounceAttributes({
watchers: $this.settings.resizewatchers,
interval: 100,
stopsame: 5,
callback: $this.UpdatePosition,
intervalcallback: $this.UpdatePosition
});
window.addEventListener('resize', updatepositionwatcher.Start, true);
}
// Dispatches the cancelled event.
var allowcancel = false;
$this.Cancel = function(etype) {
if (allowcancel)
{
allowcancel = false;
DispatchEvent('cancelled', [lastactiveelem, etype]);
}
};
// Prevents Cancel() from having any effect.
$this.PreventCancel = function() {
allowcancel = false;
};
// Destroys the popup menu.
$this.Destroy = function() {
DispatchEvent('destroy');
window.removeEventListener('mousedown', MainFocusHandler, true);
window.removeEventListener('focus', MainFocusHandler, true);
window.removeEventListener('blur', MainWindowBlurHander, true);
elems.innerwrap.removeEventListener('mousemove', InnerWrapMoveHandler);
elems.innerwrap.removeEventListener('mouseleave', InnerWrapLeaveHandler);
elems.innerwrap.removeEventListener('mouseup', MainClickHandler);
elems.innerwrap.removeEventListener('contextmenu', StopContextMenu);
elems.popupwrap.removeEventListener('keydown', MainKeyHandler);
elems.popupwrap.removeEventListener('keyup', IgnoreKeyHandler);
elems.popupwrap.removeEventListener('keypress', IgnoreKeyHandler);
if (Array.isArray($this.settings.resizewatchers) && $this.settings.resizewatchers.length)
{
window.removeEventListener('resize', updatepositionwatcher.Start, true);
updatepositionwatcher.Destroy();
}
for (var node in elems)
{
if (elems[node].parentNode) elems[node].parentNode.removeChild(elems[node]);
}
// Remaining cleanup.
elems = null;
lastactiveelem = null;
$this.settings = Object.assign({}, defaults);
$this = null;
parentelem = null;
options = null;
};
// Focus on the popup menu but do not select anything.
elems.popupwrap.focus();
allowcancel = true;
};
// Overlays a textarea placed into a parent element.
var TextareaOverlay = function(parentelem, options) {
if (!(this instanceof TextareaOverlay)) return new TextareaOverlay(parentelem, options);
var triggers = {};
var $this = this;
var defaults = {
capturetab: false,
multiline: false,
initvalue: '',
initselstart: -1,
initselend: -1,
resizewatchers: null,
onposition: null,
ondone: null,
oncancel: null,
ondestroy: null
};
$this.settings = Object.assign({}, defaults, options);
// Initialize the UI elements.
var elems = {
maintext: CreateNode('textarea', ['fe_fileexplorer_textarea'])
};
// Attach elements to DOM.
elems.maintext.value = $this.settings.initvalue;
parentelem.appendChild(elems.maintext);
// Internal functions.
var DispatchEvent = function(eventname, params) {
if (!triggers[eventname]) return;
triggers[eventname].forEach(function(callback) {
if (Array.isArray(params)) callback.apply($this, params);
else callback.call($this, params);
});
};
// Public DOM-style functions.
$this.addEventListener = function(eventname, callback) {
if (!triggers[eventname]) triggers[eventname] = [];
for (var x in triggers[eventname])
{
if (triggers[eventname][x] === callback) return;
}
triggers[eventname].push(callback);
};
$this.removeEventListener = function(eventname, callback) {
if (!triggers[eventname]) return;
for (var x in triggers[eventname])
{
if (triggers[eventname][x] === callback)
{
triggers[eventname].splice(x, 1);
return;
}
}
};
$this.hasEventListener = function(eventname) {
return (triggers[eventname] && triggers[eventname].length);
};
// Register settings callbacks.
if ($this.settings.onposition) $this.addEventListener('position', $this.settings.onposition);
if ($this.settings.ondone) $this.addEventListener('done', $this.settings.ondone);
if ($this.settings.oncancel) $this.addEventListener('cancelled', $this.settings.oncancel);
if ($this.settings.ondestroy) $this.addEventListener('destroy', $this.settings.ondestroy);
// Set up focus changing closing rules.
var MainFocusHandler = function(e) {
if (!e.isTrusted) return;
var node = e.target;
while (node && node !== elems.maintext) node = node.parentNode;
if (node !== elems.maintext && allowcanceldone)
{
lastactiveelem = e.target;
$this.Done(e.type === 'focus' ? 'focus' : 'mouse');
}
};
window.addEventListener('mousedown', MainFocusHandler, true);
window.addEventListener('focus', MainFocusHandler, true);
var MainWindowBlurHander = function(e) {
if (e.target === window || e.target === document) $this.Done('blur');
};
window.addEventListener('blur', MainWindowBlurHander, true);
var lastactiveelem = document.activeElement;
// Handle keyboard navigation.
var MainKeyHandler = function(e) {
// The keyboard is modal while the mouse is not. Stop propagation of all keyboard actions.
e.stopPropagation();
if (!e.isTrusted) return;
if (e.keyCode == 8)
{
// Backspace.
$this.UpdatePosition();
setTimeout($this.UpdatePosition, 0);
}
if (e.keyCode == 46)
{
// Delete.
$this.UpdatePosition();
setTimeout($this.UpdatePosition, 0);
}
else if (e.keyCode == 9)
{
// Tab.
e.preventDefault();
if (!$this.settings.capturetab || e.shiftKey) $this.Done('key');
else
{
var pos = elems.maintext.selectionStart;
elems.maintext.value = elems.maintext.value.substring(0, pos) + '\t' + elems.maintext.value.substring(elems.maintext.selectionEnd);
elems.maintext.selectionEnd = pos + 1;
$this.UpdatePosition();
setTimeout($this.UpdatePosition, 0);
}
}
else if (e.keyCode == 13)
{
// Enter. Complete the entry if not multiline.
if (!$this.settings.multiline)
{
e.preventDefault();
$this.Done('key');
}
}
else if (e.keyCode == 27)
{
// Escape. Cancel the textarea.
e.preventDefault();
$this.Cancel('key');
}
};
elems.maintext.addEventListener('keydown', MainKeyHandler);
var MainKeypressHandler = function(e) {
e.stopPropagation();
$this.UpdatePosition();
};
elems.maintext.addEventListener('keyup', MainKeypressHandler);
elems.maintext.addEventListener('keypress', MainKeypressHandler);
// Public functions.
// Updates the position of the textarea.
$this.UpdatePosition = function() {
elems.maintext.style.height = '1px';
DispatchEvent('position', elems.maintext);
};
$this.UpdatePosition();
// Set up a debounced element attribute watcher on window resize.
var updatepositionwatcher;
if (Array.isArray($this.settings.resizewatchers) && $this.settings.resizewatchers.length)
{
updatepositionwatcher = new DebounceAttributes({
watchers: $this.settings.resizewatchers,
interval: 100,
stopsame: 5,
callback: $this.UpdatePosition,
intervalcallback: $this.UpdatePosition
});
window.addEventListener('resize', updatepositionwatcher.Start, true);
}
// Dispatches the done event if the content has changed.
var allowcanceldone = false;
$this.Done = function(etype) {
if ($this.settings.initvalue === elems.maintext.value) $this.Cancel(etype);
else if (allowcanceldone)
{
allowcanceldone = false;
elems.maintext.readOnly = true;
DispatchEvent('done', [elems.maintext.value, lastactiveelem, etype]);
}
};
// Dispatches the cancelled event.
$this.Cancel = function(etype) {
if (allowcanceldone)
{
allowcanceldone = false;
elems.maintext.readOnly = true;
DispatchEvent('cancelled', [lastactiveelem, etype]);
}
};
// Resets the cancel/done status to true so another event can be dispatched.
$this.ResetAllowCancelDone = function() {
setTimeout(function() {
elems.maintext.readOnly = false;
elems.maintext.focus();
allowcanceldone = true;
}, 0);
};
// Destroys the textarea.
$this.Destroy = function() {
DispatchEvent('destroy');
window.removeEventListener('mousedown', MainFocusHandler, true);
window.removeEventListener('focus', MainFocusHandler, true);
window.removeEventListener('blur', MainWindowBlurHander, true);
elems.maintext.removeEventListener('keydown', MainKeyHandler);
elems.maintext.removeEventListener('keyup', MainKeypressHandler);
elems.maintext.removeEventListener('keypress', MainKeypressHandler);
if (Array.isArray($this.settings.resizewatchers) && $this.settings.resizewatchers.length)
{
window.removeEventListener('resize', updatepositionwatcher.Start, true);
updatepositionwatcher.Destroy();
}
for (var node in elems)
{
if (elems[node].parentNode) elems[node].parentNode.removeChild(elems[node]);
}
// Remaining cleanup.
elems = null;
lastactiveelem = null;
$this.settings = Object.assign({}, defaults);
$this = null;
parentelem = null;
options = null;
};
// Focus on the textarea.
elems.maintext.focus();
elems.maintext.setSelectionRange(($this.settings.initselstart > -1 ? $this.settings.initselstart : elems.maintext.value.length), ($this.settings.initselend > -1 ? $this.settings.initselend : elems.maintext.value.length));
allowcanceldone = true;
};
// File Explorer.
var nextmain_id = 1, coretools = [];
window.FileExplorer = function(parentelem, options) {
if (!(this instanceof FileExplorer)) return new FileExplorer(parentelem, options);
var triggers = {}, historystack = [], currhistory = -1, foldermap = {}, currfolder = false, destroyinprogress = false;
var $this = this;
// The internal ID needs to be fairly unique to identify source for clipboard paste and drag/drop targets.
var main_id = nextmain_id + '_fileexplorer_js_' + Date.now() + '_' + (window.crypto && window.crypto.getRandomValues ? window.crypto.getRandomValues(new Uint32Array(1))[0] : Math.random());
nextmain_id++;
var defaults = {
group: null,
alwaysfocused: false,
capturebrowser: false,
messagetimeout: 2000,
displayunits: 'iec_windows',
adjustprecision: true,
initpath: null,
onfocus: null,
onblur: null,
onrefresh: null,
onselchanged: null,
onrename: null,
onopenfile: null,
oninitupload: null,
onfinishedupload: null,
onuploaderror: null,
concurrentuploads: 4,
tools: {},
onnewfolder: null,
onnewfile: null,
oninitdownload: null,
ondownloadstarted: null,
ondownloaderror: null,
ondownloadurl: null,
oncopy: null,
onmove: null,
ondelete: null,
langmap: {}
};
$this.settings = Object.assign({}, defaults, options);
// If the group is not specified, set it to the unique main ID + the current time.
var main_group = (typeof $this.settings.group === 'string' ? $this.settings.group : main_id);
// Multilingual translation.
$this.Translate = function(str) {
return ($this.settings.langmap[str] ? $this.settings.langmap[str] : str);
};
// Initialize the UI elements.
var elems = {
mainwrap: CreateNode('div', ['fe_fileexplorer_wrap']),
dropzonewrap: CreateNode('div', ['fe_fileexplorer_dropzone_wrap']),
innerwrap: CreateNode('div', ['fe_fileexplorer_inner_wrap']),
toolbar: CreateNode('div', ['fe_fileexplorer_toolbar']),
navtools: CreateNode('div', ['fe_fileexplorer_navtools']),
navtool_back: CreateNode('button', ['fe_fileexplorer_navtool_back', 'fe_fileexplorer_disabled'], { title: $this.Translate('Back (Alt + Left Arrow)'), tabIndex: -1 }),
navtool_forward: CreateNode('button', ['fe_fileexplorer_navtool_forward', 'fe_fileexplorer_disabled'], { title: $this.Translate('Forward (Alt + Right Arrow)'), tabIndex: -1 }),
navtool_history: CreateNode('button', ['fe_fileexplorer_navtool_history'], { title: $this.Translate('Recent locations') }),
navtool_up: CreateNode('button', ['fe_fileexplorer_navtool_up', 'fe_fileexplorer_disabled'], { title: $this.Translate('Up (Alt + Up Arrow)'), tabIndex: -1 }),
pathwrap: CreateNode('div', ['fe_fileexplorer_path_wrap']),
pathicon: CreateNode('div', ['fe_fileexplorer_path_icon']),
pathiconinner: CreateNode('div', ['fe_fileexplorer_path_icon_inner']),
pathsegmentsscrollwrap: CreateNode('div', ['fe_fileexplorer_path_segments_scroll_wrap']),
pathsegmentswrap: CreateNode('div', ['fe_fileexplorer_path_segments_wrap']),
bodywrapouter: CreateNode('div', ['fe_fileexplorer_body_wrap_outer']),
bodywrap: CreateNode('div', ['fe_fileexplorer_body_wrap']),
bodytoolsscrollwrap: CreateNode('div', ['fe_fileexplorer_folder_tools_scroll_wrap', 'fe_fileexplorer_hidden']),
bodytoolbar: CreateNode('div', ['fe_fileexplorer_folder_tools']),
bodytools: [],
itemsscrollwrap: CreateNode('div', ['fe_fileexplorer_items_scroll_wrap'], { tabIndex: 0 }),
itemsscrollwrapinner: CreateNode('div', ['fe_fileexplorer_items_scroll_wrap_inner']),
itemsmessagewrap: CreateNode('div', ['fe_fileexplorer_items_message_wrap']),
itemswrap: CreateNode('div', ['fe_fileexplorer_items_wrap', 'fe_fileexplorer_hidden']),
itemsclipboardoverlaypastewrap: CreateNode('div', ['fe_fileexplorer_items_clipboard_overlay_paste_wrap', 'fe_fileexplorer_hidden']),
itemsclipboardoverlaypasteinnerwrap: CreateNode('div', ['fe_fileexplorer_items_clipboard_overlay_paste_inner_wrap']),
itemsclipboardoverlaypastetextwrap: CreateNode('div', ['fe_fileexplorer_items_clipboard_overlay_paste_text_wrap']),
itemsclipboardoverlaypastetext: CreateNode('div', ['fe_fileexplorer_items_clipboard_overlay_paste_text']),
itemsclipboardoverlaypastetextline: CreateNode('div', ['fe_fileexplorer_items_clipboard_overlay_paste_text_big'], { innerHTML: $this.Translate('Paste here') }),
itemsclipboardoverlaypastetexthint: CreateNode('div', ['fe_fileexplorer_items_clipboard_overlay_paste_text_small']),
itemsclipboardoverlay: CreateNode('textarea', ['fe_fileexplorer_items_clipboard_overlay'], { tabIndex: -1, inputMode: 'none', autocomplete: 'off', autocorrect: 'off', autocapitalize: 'off', spellcheck: false }),
statusbar: CreateNode('div', ['fe_fileexplorer_statusbar_wrap']),
statusbarmeasuresize: CreateNode('div', ['fe_fileexplorer_statusbar_measure_em_size']),
statusbartextwrap: CreateNode('div', ['fe_fileexplorer_statusbar_text_wrap']),
statusbartextsegments: [],
statusbartextsegmentmap: {},
statusbaractionwrap: CreateNode('div', ['fe_fileexplorer_action_wrap']),
statusbaractionprogresswrap: CreateNode('div', ['fe_fileexplorer_action_progress_wrap', 'fe_fileexplorer_hidden']),
statusbaractionprogressmessagewrap: CreateNode('div', ['fe_fileexplorer_action_progress_msg_wrap']),
statusbaractionprogressmessagewrap2: CreateNode('div', ['fe_fileexplorer_action_progress_msg_wrap', 'fe_fileexplorer_action_progress_msg_wrap_last']),
statusbaractionprogresscancelwrap: CreateNode('div', ['fe_fileexplorer_action_progress_cancel_wrap'], { title: $this.Translate('Cancel all'), tabIndex: 0 }),
};
// Sets a text segment's displayed text in the status bar.
$this.SetNamedStatusBarText = function(name, text, timeout) {
if (destroyinprogress) return;
if (!(name in elems.statusbartextsegmentmap))
{
elems.statusbartextsegmentmap[name] = { pos: elems.statusbartextsegments.length, timeout: null };
var node = CreateNode('div', ['fe_fileexplorer_statusbar_text_segment_wrap']);
elems.statusbartextsegments.push(node);
elems.statusbartextwrap.appendChild(node);
}
var currsegment = elems.statusbartextsegmentmap[name];
if (currsegment.timeout)
{
clearTimeout(currsegment.timeout);
currsegment.timeout = null;
}
var elem = elems.statusbartextsegments[currsegment.pos];
if (text === '')
{
elem.innerHTML = '';
elem.classList.add('fe_fileexplorer_hidden');
}
else
{
elem.innerHTML = text;
elem.classList.remove('fe_fileexplorer_hidden');
if (timeout)
{
elems.statusbartextsegmentmap[name].timeout = setTimeout(function() {
$this.SetNamedStatusBarText(name, '');
}, timeout);
// Recalculate widths.
var widthmap = [], totalwidth = 1.5 * elems.statusbarmeasuresize.offsetWidth;
for (var x = 0; x < elems.statusbartextsegments.length; x++)
{
var elem2 = elems.statusbartextsegments[x];
if (elem2.classList.contains('fe_fileexplorer_hidden')) widthmap.push(0);
else
{
var currstyle = elem2.currentStyle || window.getComputedStyle(elem2);
var elemwidth = elem2.offsetWidth + parseFloat(currstyle.marginLeft) + parseFloat(currstyle.marginRight);
widthmap.push(elemwidth);
totalwidth += elemwidth;
}
}
for (var x = elems.statusbartextsegments.length; totalwidth >= elems.statusbartextwrap.offsetWidth && x; x--)
{
if (widthmap[x - 1] && elem !== elems.statusbartextsegments[x - 1])
{
elems.statusbartextsegments[x - 1].classList.add('fe_fileexplorer_hidden');
totalwidth -= widthmap[x - 1];
}
}
}
}
// Adjust the last visible class.
elem = null;
elems.statusbartextsegments.forEach(function(elem2) {
if (!timeout && elem2.innerHTML !== '') elem2.classList.remove('fe_fileexplorer_hidden');
if (!elem2.classList.contains('fe_fileexplorer_hidden'))
{
elem2.classList.remove('fe_fileexplorer_statusbar_text_segment_wrap_last');
elem = elem2;
}
});
if (elem) elem.classList.add('fe_fileexplorer_statusbar_text_segment_wrap_last');
};
$this.SetNamedStatusBarText('folder', '');
$this.SetNamedStatusBarText('selected', '');
$this.SetNamedStatusBarText('message', '');
elems.itemsmessagewrap.innerHTML = EscapeHTML($this.Translate('Loading...'));
// Determine what text should show to the user when displaying the paste box.
if (matchMedia('(pointer: coarse)').matches) elems.itemsclipboardoverlaypastetexthint.innerHTML = EscapeHTML($this.Translate('Long-press + paste'));
else if (navigator.platform.indexOf('Mac') > -1) elems.itemsclipboardoverlaypastetexthint.innerHTML = EscapeHTML($this.Translate('\u2318 + V\u00A0\u00A0\u00A0/\u00A0\u00A0\u00A0Right-click + Paste'));
else elems.itemsclipboardoverlaypastetexthint.innerHTML = EscapeHTML($this.Translate('Ctrl + V\u00A0\u00A0\u00A0/\u00A0\u00A0\u00A0Right-click + Paste'));
// Attach elements to DOM.
elems.navtools.appendChild(elems.navtool_back);
elems.navtools.appendChild(elems.navtool_forward);
elems.navtools.appendChild(elems.navtool_history);
elems.navtools.appendChild(elems.navtool_up);
elems.pathicon.appendChild(elems.pathiconinner);
elems.pathwrap.appendChild(elems.pathicon);
elems.pathsegmentsscrollwrap.appendChild(elems.pathsegmentswrap);
elems.pathwrap.appendChild(elems.pathsegmentsscrollwrap);
elems.toolbar.appendChild(elems.navtools);
elems.toolbar.appendChild(elems.pathwrap);
elems.bodytoolsscrollwrap.appendChild(elems.bodytoolbar);
elems.itemsscrollwrapinner.appendChild(elems.itemsmessagewrap);
elems.itemsscrollwrapinner.appendChild(elems.itemswrap);
elems.itemsscrollwrap.appendChild(elems.itemsscrollwrapinner);
elems.itemsclipboardoverlaypastetext.appendChild(elems.itemsclipboardoverlaypastetextline);
elems.itemsclipboardoverlaypastetext.appendChild(elems.itemsclipboardoverlaypastetexthint);
elems.itemsclipboardoverlaypastetextwrap.appendChild(elems.itemsclipboardoverlaypastetext);
elems.itemsclipboardoverlaypasteinnerwrap.appendChild(elems.itemsclipboardoverlaypastetextwrap);
elems.itemsclipboardoverlaypasteinnerwrap.appendChild(elems.itemsclipboardoverlay);
elems.itemsclipboardoverlaypastewrap.appendChild(elems.itemsclipboardoverlaypasteinnerwrap);
elems.bodywrap.appendChild(elems.bodytoolsscrollwrap);
elems.bodywrap.appendChild(elems.itemsscrollwrap);
elems.bodywrapouter.appendChild(elems.bodywrap);
elems.bodywrapouter.appendChild(elems.itemsclipboardoverlaypastewrap);
elems.statusbaractionprogresswrap.appendChi