'use strict';
+'require rpc';
'require uci';
'require validation';
+'require fs';
var modalDiv = null,
tooltipDiv = null,
},
setChangeEvents: function(targetNode /*, ... */) {
+ var tag_changed = L.bind(function(ev) { this.setAttribute('data-changed', true) }, this.node);
+
+ for (var i = 1; i < arguments.length; i++)
+ targetNode.addEventListener(arguments[i], tag_changed);
+
this.registerEvents(targetNode, 'widget-change', this.varargs(arguments, 1));
}
});
}
});
+var UIComboButton = UIDropdown.extend({
+ __init__: function(value, choices, options) {
+ this.super('__init__', [ value, choices, Object.assign({
+ sort: true
+ }, options, {
+ multiple: false,
+ create: false,
+ optional: false
+ }) ]);
+ },
+
+ render: function(/* ... */) {
+ var node = UIDropdown.prototype.render.apply(this, arguments),
+ val = this.getValue();
+
+ if (L.isObject(this.options.classes) && this.options.classes.hasOwnProperty(val))
+ node.setAttribute('class', 'cbi-dropdown ' + this.options.classes[val]);
+
+ return node;
+ },
+
+ handleClick: function(ev) {
+ var sb = ev.currentTarget,
+ t = ev.target;
+
+ if (sb.hasAttribute('open') || L.dom.matches(t, '.cbi-dropdown > span.open'))
+ return UIDropdown.prototype.handleClick.apply(this, arguments);
+
+ if (this.options.click)
+ return this.options.click.call(sb, ev, this.getValue());
+ },
+
+ toggleItem: function(sb /*, ... */) {
+ var rv = UIDropdown.prototype.toggleItem.apply(this, arguments),
+ val = this.getValue();
+
+ if (L.isObject(this.options.classes) && this.options.classes.hasOwnProperty(val))
+ sb.setAttribute('class', 'cbi-dropdown ' + this.options.classes[val]);
+ else
+ sb.setAttribute('class', 'cbi-dropdown');
+
+ return rv;
+ }
+});
+
var UIDynamicList = UIElement.extend({
__init__: function(values, choices, options) {
if (!Array.isArray(values))
}
});
+var UIFileUpload = UIElement.extend({
+ __init__: function(value, options) {
+ this.value = value;
+ this.options = Object.assign({
+ show_hidden: false,
+ enable_upload: true,
+ enable_remove: true,
+ root_directory: '/etc/luci-uploads'
+ }, options);
+ },
+
+ bind: function(browserEl) {
+ this.node = browserEl;
+
+ this.setUpdateEvents(browserEl, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
+ this.setChangeEvents(browserEl, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
+
+ L.dom.bindClassInstance(browserEl, this);
+
+ return browserEl;
+ },
+
+ render: function() {
+ return L.resolveDefault(this.value != null ? fs.stat(this.value) : null).then(L.bind(function(stat) {
+ var label;
+
+ if (L.isObject(stat) && stat.type != 'directory')
+ this.stat = stat;
+
+ if (this.stat != null)
+ label = [ this.iconForType(this.stat.type), ' %s (%1000mB)'.format(this.truncatePath(this.stat.path), this.stat.size) ];
+ else if (this.value != null)
+ label = [ this.iconForType('file'), ' %s (%s)'.format(this.truncatePath(this.value), _('File not accessible')) ];
+ else
+ label = [ _('Select file…') ];
+
+ return this.bind(E('div', { 'id': this.options.id }, [
+ E('button', {
+ 'class': 'btn',
+ 'click': L.ui.createHandlerFn(this, 'handleFileBrowser')
+ }, label),
+ E('div', {
+ 'class': 'cbi-filebrowser'
+ }),
+ E('input', {
+ 'type': 'hidden',
+ 'name': this.options.name,
+ 'value': this.value
+ })
+ ]));
+ }, this));
+ },
+
+ truncatePath: function(path) {
+ if (path.length > 50)
+ path = path.substring(0, 25) + '…' + path.substring(path.length - 25);
+
+ return path;
+ },
+
+ iconForType: function(type) {
+ switch (type) {
+ case 'symlink':
+ return E('img', {
+ 'src': L.resource('cbi/link.gif'),
+ 'title': _('Symbolic link'),
+ 'class': 'middle'
+ });
+
+ case 'directory':
+ return E('img', {
+ 'src': L.resource('cbi/folder.gif'),
+ 'title': _('Directory'),
+ 'class': 'middle'
+ });
+
+ default:
+ return E('img', {
+ 'src': L.resource('cbi/file.gif'),
+ 'title': _('File'),
+ 'class': 'middle'
+ });
+ }
+ },
+
+ canonicalizePath: function(path) {
+ return path.replace(/\/{2,}/, '/')
+ .replace(/\/\.(\/|$)/g, '/')
+ .replace(/[^\/]+\/\.\.(\/|$)/g, '/')
+ .replace(/\/$/, '');
+ },
+
+ splitPath: function(path) {
+ var croot = this.canonicalizePath(this.options.root_directory || '/'),
+ cpath = this.canonicalizePath(path || '/');
+
+ if (cpath.length <= croot.length)
+ return [ croot ];
+
+ if (cpath.charAt(croot.length) != '/')
+ return [ croot ];
+
+ var parts = cpath.substring(croot.length + 1).split(/\//);
+
+ parts.unshift(croot);
+
+ return parts;
+ },
+
+ handleUpload: function(path, list, ev) {
+ var form = ev.target.parentNode,
+ fileinput = form.querySelector('input[type="file"]'),
+ nameinput = form.querySelector('input[type="text"]'),
+ filename = (nameinput.value != null ? nameinput.value : '').trim();
+
+ ev.preventDefault();
+
+ if (filename == '' || filename.match(/\//) || fileinput.files[0] == null)
+ return;
+
+ var existing = list.filter(function(e) { return e.name == filename })[0];
+
+ if (existing != null && existing.type == 'directory')
+ return alert(_('A directory with the same name already exists.'));
+ else if (existing != null && !confirm(_('Overwrite existing file "%s" ?').format(filename)))
+ return;
+
+ var data = new FormData();
+
+ data.append('sessionid', L.env.sessionid);
+ data.append('filename', path + '/' + filename);
+ data.append('filedata', fileinput.files[0]);
+
+ return L.Request.post('/cgi-bin/cgi-upload', data, {
+ progress: L.bind(function(btn, ev) {
+ btn.firstChild.data = '%.2f%%'.format((ev.loaded / ev.total) * 100);
+ }, this, ev.target)
+ }).then(L.bind(function(path, ev, res) {
+ var reply = res.json();
+
+ if (L.isObject(reply) && reply.failure)
+ alert(_('Upload request failed: %s').format(reply.message));
+
+ return this.handleSelect(path, null, ev);
+ }, this, path, ev));
+ },
+
+ handleDelete: function(path, fileStat, ev) {
+ var parent = path.replace(/\/[^\/]+$/, '') || '/',
+ name = path.replace(/^.+\//, ''),
+ msg;
+
+ ev.preventDefault();
+
+ if (fileStat.type == 'directory')
+ msg = _('Do you really want to recursively delete the directory "%s" ?').format(name);
+ else
+ msg = _('Do you really want to delete "%s" ?').format(name);
+
+ if (confirm(msg)) {
+ var button = this.node.firstElementChild,
+ hidden = this.node.lastElementChild;
+
+ if (path == hidden.value) {
+ L.dom.content(button, _('Select file…'));
+ hidden.value = '';
+ }
+
+ return fs.remove(path).then(L.bind(function(parent, ev) {
+ return this.handleSelect(parent, null, ev);
+ }, this, parent, ev)).catch(function(err) {
+ alert(_('Delete request failed: %s').format(err.message));
+ });
+ }
+ },
+
+ renderUpload: function(path, list) {
+ if (!this.options.enable_upload)
+ return E([]);
+
+ return E([
+ E('a', {
+ 'href': '#',
+ 'class': 'btn cbi-button-positive',
+ 'click': function(ev) {
+ var uploadForm = ev.target.nextElementSibling,
+ fileInput = uploadForm.querySelector('input[type="file"]');
+
+ ev.target.style.display = 'none';
+ uploadForm.style.display = '';
+ fileInput.click();
+ }
+ }, _('Upload file…')),
+ E('div', { 'class': 'upload', 'style': 'display:none' }, [
+ E('input', {
+ 'type': 'file',
+ 'style': 'display:none',
+ 'change': function(ev) {
+ var nameinput = ev.target.parentNode.querySelector('input[type="text"]'),
+ uploadbtn = ev.target.parentNode.querySelector('button.cbi-button-save');
+
+ nameinput.value = ev.target.value.replace(/^.+[\/\\]/, '');
+ uploadbtn.disabled = false;
+ }
+ }),
+ E('button', {
+ 'class': 'btn',
+ 'click': function(ev) {
+ ev.preventDefault();
+ ev.target.previousElementSibling.click();
+ }
+ }, [ _('Browse…') ]),
+ E('div', {}, E('input', { 'type': 'text', 'placeholder': _('Filename') })),
+ E('button', {
+ 'class': 'btn cbi-button-save',
+ 'click': L.ui.createHandlerFn(this, 'handleUpload', path, list),
+ 'disabled': true
+ }, [ _('Upload file') ])
+ ])
+ ]);
+ },
+
+ renderListing: function(container, path, list) {
+ var breadcrumb = E('p'),
+ rows = E('ul');
+
+ list.sort(function(a, b) {
+ var isDirA = (a.type == 'directory'),
+ isDirB = (b.type == 'directory');
+
+ if (isDirA != isDirB)
+ return isDirA < isDirB;
+
+ return a.name > b.name;
+ });
+
+ for (var i = 0; i < list.length; i++) {
+ if (!this.options.show_hidden && list[i].name.charAt(0) == '.')
+ continue;
+
+ var entrypath = this.canonicalizePath(path + '/' + list[i].name),
+ selected = (entrypath == this.node.lastElementChild.value),
+ mtime = new Date(list[i].mtime * 1000);
+
+ rows.appendChild(E('li', [
+ E('div', { 'class': 'name' }, [
+ this.iconForType(list[i].type),
+ ' ',
+ E('a', {
+ 'href': '#',
+ 'style': selected ? 'font-weight:bold' : null,
+ 'click': L.ui.createHandlerFn(this, 'handleSelect',
+ entrypath, list[i].type != 'directory' ? list[i] : null)
+ }, '%h'.format(list[i].name))
+ ]),
+ E('div', { 'class': 'mtime hide-xs' }, [
+ ' %04d-%02d-%02d %02d:%02d:%02d '.format(
+ mtime.getFullYear(),
+ mtime.getMonth() + 1,
+ mtime.getDate(),
+ mtime.getHours(),
+ mtime.getMinutes(),
+ mtime.getSeconds())
+ ]),
+ E('div', [
+ selected ? E('button', {
+ 'class': 'btn',
+ 'click': L.ui.createHandlerFn(this, 'handleReset')
+ }, [ _('Deselect') ]) : '',
+ this.options.enable_remove ? E('button', {
+ 'class': 'btn cbi-button-negative',
+ 'click': L.ui.createHandlerFn(this, 'handleDelete', entrypath, list[i])
+ }, [ _('Delete') ]) : ''
+ ])
+ ]));
+ }
+
+ if (!rows.firstElementChild)
+ rows.appendChild(E('em', _('No entries in this directory')));
+
+ var dirs = this.splitPath(path),
+ cur = '';
+
+ for (var i = 0; i < dirs.length; i++) {
+ cur = cur ? cur + '/' + dirs[i] : dirs[i];
+ L.dom.append(breadcrumb, [
+ i ? ' » ' : '',
+ E('a', {
+ 'href': '#',
+ 'click': L.ui.createHandlerFn(this, 'handleSelect', cur || '/', null)
+ }, dirs[i] != '' ? '%h'.format(dirs[i]) : E('em', '(root)')),
+ ]);
+ }
+
+ L.dom.content(container, [
+ breadcrumb,
+ rows,
+ E('div', { 'class': 'right' }, [
+ this.renderUpload(path, list),
+ E('a', {
+ 'href': '#',
+ 'class': 'btn',
+ 'click': L.ui.createHandlerFn(this, 'handleCancel')
+ }, _('Cancel'))
+ ]),
+ ]);
+ },
+
+ handleCancel: function(ev) {
+ var button = this.node.firstElementChild,
+ browser = button.nextElementSibling;
+
+ browser.classList.remove('open');
+ button.style.display = '';
+
+ this.node.dispatchEvent(new CustomEvent('cbi-fileupload-cancel', {}));
+ },
+
+ handleReset: function(ev) {
+ var button = this.node.firstElementChild,
+ hidden = this.node.lastElementChild;
+
+ hidden.value = '';
+ L.dom.content(button, _('Select file…'));
+
+ this.handleCancel(ev);
+ },
+
+ handleSelect: function(path, fileStat, ev) {
+ var browser = L.dom.parent(ev.target, '.cbi-filebrowser'),
+ ul = browser.querySelector('ul');
+
+ if (fileStat == null) {
+ L.dom.content(ul, E('em', { 'class': 'spinning' }, _('Loading directory contents…')));
+ L.resolveDefault(fs.list(path), []).then(L.bind(this.renderListing, this, browser, path));
+ }
+ else {
+ var button = this.node.firstElementChild,
+ hidden = this.node.lastElementChild;
+
+ path = this.canonicalizePath(path);
+
+ L.dom.content(button, [
+ this.iconForType(fileStat.type),
+ ' %s (%1000mB)'.format(this.truncatePath(path), fileStat.size)
+ ]);
+
+ browser.classList.remove('open');
+ button.style.display = '';
+ hidden.value = path;
+
+ this.stat = Object.assign({ path: path }, fileStat);
+ this.node.dispatchEvent(new CustomEvent('cbi-fileupload-select', { detail: this.stat }));
+ }
+ },
+
+ handleFileBrowser: function(ev) {
+ var button = ev.target,
+ browser = button.nextElementSibling,
+ path = this.stat ? this.stat.path.replace(/\/[^\/]+$/, '') : this.options.root_directory;
+
+ if (this.options.root_directory.indexOf(path) != 0)
+ path = this.options.root_directory;
+
+ ev.preventDefault();
+
+ return L.resolveDefault(fs.list(path), []).then(L.bind(function(button, browser, path, list) {
+ document.querySelectorAll('.cbi-filebrowser.open').forEach(function(browserEl) {
+ L.dom.findClassInstance(browserEl).handleCancel(ev);
+ });
+
+ button.style.display = 'none';
+ browser.classList.add('open');
+
+ return this.renderListing(browser, path, list);
+ }, this, button, browser, path));
+ },
+
+ getValue: function() {
+ return this.node.lastElementChild.value;
+ },
+
+ setValue: function(value) {
+ this.node.lastElementChild.value = value;
+ }
+});
+
return L.Class.extend({
__init__: function() {
tooltipDiv.dispatchEvent(new CustomEvent('tooltip-close', { bubbles: true }));
},
+ addNotification: function(title, children /*, ... */) {
+ var mc = document.querySelector('#maincontent') || document.body;
+ var msg = E('div', {
+ 'class': 'alert-message fade-in',
+ 'style': 'display:flex',
+ 'transitionend': function(ev) {
+ var node = ev.currentTarget;
+ if (node.parentNode && node.classList.contains('fade-out'))
+ node.parentNode.removeChild(node);
+ }
+ }, [
+ E('div', { 'style': 'flex:10' }),
+ E('div', { 'style': 'flex:1 1 auto; display:flex' }, [
+ E('button', {
+ 'class': 'btn',
+ 'style': 'margin-left:auto; margin-top:auto',
+ 'click': function(ev) {
+ L.dom.parent(ev.target, '.alert-message').classList.add('fade-out');
+ },
+
+ }, [ _('Dismiss') ])
+ ])
+ ]);
+
+ if (title != null)
+ L.dom.append(msg.firstElementChild, E('h4', {}, title));
+
+ L.dom.append(msg.firstElementChild, children);
+
+ for (var i = 2; i < arguments.length; i++)
+ msg.classList.add(arguments[i]);
+
+ mc.insertBefore(msg, mc.firstElementChild);
+
+ return msg;
+ },
+
/* Widget helper */
itemlist: function(node, items, separators) {
var children = [];
document.querySelectorAll('[data-tab]').forEach(function(tab) {
var parent = tab.parentNode;
+ if (L.dom.matches(tab, 'li') && L.dom.matches(parent, 'ul.cbi-tabmenu'))
+ return;
+
if (!parent.hasAttribute('data-tab-group'))
parent.setAttribute('data-tab-group', groups.length);
groupId = +group.getAttribute('data-tab-group'),
selected = null;
+ if (group.getAttribute('data-initialized') === 'true')
+ return;
+
for (var i = 0, pane; pane = panes[i]; i++) {
var name = pane.getAttribute('data-tab'),
title = pane.getAttribute('data-tab-title'),
}
group.parentNode.insertBefore(menu, group);
+ group.setAttribute('data-initialized', true);
if (selected === null) {
selected = this.getActiveTabId(panes[0]);
this.setActiveTabId(panes[selected], selected);
}
+
+ this.updateTabs(group);
},
isEmptyPane: function(pane) {
}
}),
+ /* File uploading */
+ uploadFile: function(path, progressStatusNode) {
+ return new Promise(function(resolveFn, rejectFn) {
+ L.ui.showModal(_('Uploading file…'), [
+ E('p', _('Please select the file to upload.')),
+ E('div', { 'style': 'display:flex' }, [
+ E('div', { 'class': 'left', 'style': 'flex:1' }, [
+ E('input', {
+ type: 'file',
+ style: 'display:none',
+ change: function(ev) {
+ var modal = L.dom.parent(ev.target, '.modal'),
+ body = modal.querySelector('p'),
+ upload = modal.querySelector('.cbi-button-action.important'),
+ file = ev.currentTarget.files[0];
+
+ if (file == null)
+ return;
+
+ L.dom.content(body, [
+ E('ul', {}, [
+ E('li', {}, [ '%s: %s'.format(_('Name'), file.name.replace(/^.*[\\\/]/, '')) ]),
+ E('li', {}, [ '%s: %1024mB'.format(_('Size'), file.size) ])
+ ])
+ ]);
+
+ upload.disabled = false;
+ upload.focus();
+ }
+ }),
+ E('button', {
+ 'class': 'btn',
+ 'click': function(ev) {
+ ev.target.previousElementSibling.click();
+ }
+ }, [ _('Browse…') ])
+ ]),
+ E('div', { 'class': 'right', 'style': 'flex:1' }, [
+ E('button', {
+ 'class': 'btn',
+ 'click': function() {
+ L.ui.hideModal();
+ rejectFn(new Error('Upload has been cancelled'));
+ }
+ }, [ _('Cancel') ]),
+ ' ',
+ E('button', {
+ 'class': 'btn cbi-button-action important',
+ 'disabled': true,
+ 'click': function(ev) {
+ var input = L.dom.parent(ev.target, '.modal').querySelector('input[type="file"]');
+
+ if (!input.files[0])
+ return;
+
+ var progress = E('div', { 'class': 'cbi-progressbar', 'title': '0%' }, E('div', { 'style': 'width:0' }));
+
+ L.ui.showModal(_('Uploading file…'), [ progress ]);
+
+ var data = new FormData();
+
+ data.append('sessionid', rpc.getSessionID());
+ data.append('filename', path);
+ data.append('filedata', input.files[0]);
+
+ var filename = input.files[0].name;
+
+ L.Request.post('/cgi-bin/cgi-upload', data, {
+ timeout: 0,
+ progress: function(pev) {
+ var percent = (pev.loaded / pev.total) * 100;
+
+ if (progressStatusNode)
+ progressStatusNode.data = '%.2f%%'.format(percent);
+
+ progress.setAttribute('title', '%.2f%%'.format(percent));
+ progress.firstElementChild.style.width = '%.2f%%'.format(percent);
+ }
+ }).then(function(res) {
+ var reply = res.json();
+
+ L.ui.hideModal();
+
+ if (L.isObject(reply) && reply.failure) {
+ L.ui.addNotification(null, E('p', _('Upload request failed: %s').format(reply.message)));
+ rejectFn(new Error(reply.failure));
+ }
+ else {
+ reply.name = filename;
+ resolveFn(reply);
+ }
+ }, function(err) {
+ L.ui.hideModal();
+ rejectFn(err);
+ });
+ }
+ }, [ _('Upload') ])
+ ])
+ ])
+ ]);
+ });
+ },
+
+ /* Reconnect handling */
+ pingDevice: function(proto, ipaddr) {
+ var target = '%s://%s%s?%s'.format(proto || 'http', ipaddr || window.location.host, L.resource('icons/loading.gif'), Math.random());
+
+ return new Promise(function(resolveFn, rejectFn) {
+ var img = new Image();
+
+ img.onload = resolveFn;
+ img.onerror = rejectFn;
+
+ window.setTimeout(rejectFn, 1000);
+
+ img.src = target;
+ });
+ },
+
+ awaitReconnect: function(/* ... */) {
+ var ipaddrs = arguments.length ? arguments : [ window.location.host ];
+
+ window.setTimeout(L.bind(function() {
+ L.Poll.add(L.bind(function() {
+ var tasks = [], reachable = false;
+
+ for (var i = 0; i < 2; i++)
+ for (var j = 0; j < ipaddrs.length; j++)
+ tasks.push(this.pingDevice(i ? 'https' : 'http', ipaddrs[j])
+ .then(function(ev) { reachable = ev.target.src.replace(/^(https?:\/\/[^\/]+).*$/, '$1/') }, function() {}));
+
+ return Promise.all(tasks).then(function() {
+ if (reachable) {
+ L.Poll.stop();
+ window.location = reachable;
+ }
+ });
+ }, this));
+ }, this), 5000);
+ },
+
/* UCI Changes */
changes: L.Class.singleton({
init: function() {
E('var', {}, E('del', ' ')), ' ', _('Option removed') ])]),
E('br'), list,
E('div', { 'class': 'right' }, [
- E('input', {
- 'type': 'button',
+ E('button', {
'class': 'btn',
- 'click': L.ui.hideModal,
- 'value': _('Dismiss')
- }), ' ',
- E('input', {
- 'type': 'button',
+ 'click': L.ui.hideModal
+ }, [ _('Dismiss') ]), ' ',
+ E('button', {
'class': 'cbi-button cbi-button-positive important',
- 'click': L.bind(this.apply, this, true),
- 'value': _('Save & Apply')
- }), ' ',
- E('input', {
- 'type': 'button',
+ 'click': L.bind(this.apply, this, true)
+ }, [ _('Save & Apply') ]), ' ',
+ E('button', {
'class': 'cbi-button cbi-button-reset',
- 'click': L.bind(this.revert, this),
- 'value': _('Revert')
- })])])
+ 'click': L.bind(this.revert, this)
+ }, [ _('Revert') ])])])
]);
for (var config in this.changes) {
var call = function(r, data, duration) {
if (r.status === 204) {
L.ui.changes.displayStatus('warning', [
- E('h4', _('Configuration has been rolled back!')),
+ E('h4', _('Configuration changes have been rolled back!')),
E('p', _('The device could not be reached within %d seconds after applying the pending changes, which caused the configuration to be rolled back for safety reasons. If you believe that the configuration changes are correct nonetheless, perform an unchecked configuration apply. Alternatively, you can dismiss this warning and edit changes before attempting to apply again, or revert all pending changes to keep the currently working configuration state.').format(L.env.apply_rollback)),
E('div', { 'class': 'right' }, [
- E('input', {
- 'type': 'button',
+ E('button', {
'class': 'btn',
- 'click': L.bind(L.ui.changes.displayStatus, L.ui.changes, false),
- 'value': _('Dismiss')
- }), ' ',
- E('input', {
- 'type': 'button',
+ 'click': L.bind(L.ui.changes.displayStatus, L.ui.changes, false)
+ }, [ _('Dismiss') ]), ' ',
+ E('button', {
'class': 'btn cbi-button-action important',
- 'click': L.bind(L.ui.changes.revert, L.ui.changes),
- 'value': _('Revert changes')
- }), ' ',
- E('input', {
- 'type': 'button',
+ 'click': L.bind(L.ui.changes.revert, L.ui.changes)
+ }, [ _('Revert changes') ]), ' ',
+ E('button', {
'class': 'btn cbi-button-negative important',
- 'click': L.bind(L.ui.changes.apply, L.ui.changes, false),
- 'value': _('Apply unchecked')
- })
+ 'click': L.bind(L.ui.changes.apply, L.ui.changes, false)
+ }, [ _('Apply unchecked') ])
])
]);
L.ui.changes.setIndicator(0);
L.ui.changes.displayStatus('notice',
- E('p', _('Configuration has been applied.')));
+ E('p', _('Configuration changes applied.')));
window.clearTimeout(tt);
window.setTimeout(function() {
var now = Date.now();
L.ui.changes.displayStatus('notice spinning',
- E('p', _('Waiting for configuration to get applied… %ds')
+ E('p', _('Applying configuration changes… %ds')
.format(Math.max(Math.floor((deadline - Date.now()) / 1000), 0))));
if (now >= deadline)
if (typeof(fn) != 'function')
return null;
+ var arg_offset = arguments.length - 2;
+
return Function.prototype.bind.apply(function() {
- var t = arguments[arguments.length - 1].target;
+ var t = arguments[arg_offset].target;
t.classList.add('spinning');
t.disabled = true;
if (t.blur)
t.blur();
- Promise.resolve(fn.apply(ctx, arguments)).then(function() {
+ Promise.resolve(fn.apply(ctx, arguments)).finally(function() {
t.classList.remove('spinning');
t.disabled = false;
});
Dropdown: UIDropdown,
DynamicList: UIDynamicList,
Combobox: UICombobox,
- Hiddenfield: UIHiddenfield
+ ComboButton: UIComboButton,
+ Hiddenfield: UIHiddenfield,
+ FileUpload: UIFileUpload
});