'require rpc';
'require uci';
'require validation';
+'require fs';
var modalDiv = null,
tooltipDiv = null,
frameEl.lastChild.appendChild(E('option', {
'value': '',
'selected': (this.values.length == 0 || this.values[0] == '') ? '' : null
- }, this.choices[''] || this.options.placeholder || _('-- Please choose --')));
+ }, [ this.choices[''] || this.options.placeholder || _('-- Please choose --') ]));
for (var i = 0; i < keys.length; i++) {
if (keys[i] == null || keys[i] == '')
frameEl.lastChild.appendChild(E('option', {
'value': keys[i],
'selected': (this.values.indexOf(keys[i]) > -1) ? '' : null
- }, this.choices[keys[i]] || keys[i]));
+ }, [ this.choices[keys[i]] || keys[i] ]));
}
}
else {
if (!this.choices.hasOwnProperty(this.values[i]))
keys.push(this.values[i]);
- for (var i = 0; i < keys.length; i++)
+ for (var i = 0; i < keys.length; i++) {
+ var label = this.choices[keys[i]];
+
+ if (L.dom.elem(label))
+ label = label.cloneNode(true);
+
sb.lastElementChild.appendChild(E('li', {
'data-value': keys[i],
'selected': (this.values.indexOf(keys[i]) > -1) ? '' : null
- }, this.choices[keys[i]] || keys[i]));
+ }, [ label || keys[i] ]));
+ }
if (this.options.create) {
var createEl = E('input', {
'placeholder': this.options.custom_placeholder || this.options.placeholder
});
- if (this.options.datatype)
- L.ui.addValidator(createEl, this.options.datatype,
- true, null, 'blur', 'keyup');
+ if (this.options.datatype || this.options.validate)
+ L.ui.addValidator(createEl, this.options.datatype || 'string',
+ true, this.options.validate, 'blur', 'keyup');
sb.lastElementChild.appendChild(E('li', { 'data-value': '-' }, createEl));
}
}
},
+ createChoiceElement: function(sb, value, label) {
+ var tpl = sb.querySelector(this.options.create_template),
+ markup = null;
+
+ if (tpl)
+ markup = (tpl.textContent || tpl.innerHTML || tpl.firstChild.data).replace(/^<!--|-->$/, '').trim();
+ else
+ markup = '<li data-value="{{value}}"><span data-label-placeholder="true" /></li>';
+
+ var new_item = E(markup.replace(/{{value}}/g, '%h'.format(value))),
+ placeholder = new_item.querySelector('[data-label-placeholder]');
+
+ if (placeholder) {
+ var content = E('span', {}, label || this.choices[value] || [ value ]);
+
+ while (content.firstChild)
+ placeholder.parentNode.insertBefore(content.firstChild, placeholder);
+
+ placeholder.parentNode.removeChild(placeholder);
+ }
+
+ if (this.options.multiple)
+ this.transformItem(sb, new_item);
+
+ return new_item;
+ },
+
createItems: function(sb, value) {
var sbox = this,
val = (value || '').trim(),
});
if (!new_item) {
- var markup,
- tpl = sb.querySelector(sbox.options.create_template);
+ new_item = sbox.createChoiceElement(sb, item);
- if (tpl)
- markup = (tpl.textContent || tpl.innerHTML || tpl.firstChild.data).replace(/^<!--|-->$/, '').trim();
- else
- markup = '<li data-value="{{value}}">{{value}}</li>';
-
- new_item = E(markup.replace(/{{value}}/g, '%h'.format(item)));
-
- if (sbox.options.multiple) {
- sbox.transformItem(sb, new_item);
- }
- else {
+ if (!sbox.options.multiple) {
var old = ul.querySelector('li[created]');
if (old)
ul.removeChild(old);
});
},
+ clearChoices: function(reset_value) {
+ var ul = this.node.querySelector('ul'),
+ lis = ul ? ul.querySelectorAll('li[data-value]') : [],
+ len = lis.length - (this.options.create ? 1 : 0),
+ val = reset_value ? null : this.getValue();
+
+ for (var i = 0; i < len; i++) {
+ var lival = lis[i].getAttribute('data-value');
+ if (val == null ||
+ (!this.options.multiple && val != lival) ||
+ (this.options.multiple && val.indexOf(lival) == -1))
+ ul.removeChild(lis[i]);
+ }
+
+ if (reset_value)
+ this.setValues(this.node, {});
+ },
+
+ addChoices: function(values, labels) {
+ var sb = this.node,
+ ul = sb.querySelector('ul'),
+ lis = ul ? ul.querySelectorAll('li[data-value]') : [];
+
+ if (!Array.isArray(values))
+ values = L.toArray(values);
+
+ if (!L.isObject(labels))
+ labels = {};
+
+ for (var i = 0; i < values.length; i++) {
+ var found = false;
+
+ for (var j = 0; j < lis.length; j++) {
+ if (lis[j].getAttribute('data-value') === values[i]) {
+ found = true;
+ break;
+ }
+ }
+
+ if (found)
+ continue;
+
+ ul.insertBefore(
+ this.createChoiceElement(sb, values[i], labels[values[i]]),
+ ul.lastElementChild);
+ }
+ },
+
closeAllDropdowns: function() {
document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
}
});
+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))
}, E('div', { 'class': 'add-item' }));
if (this.choices) {
+ if (this.options.placeholder != null)
+ this.options.select_placeholder = this.options.placeholder;
+
var cbox = new UICombobox(null, this.choices, this.options);
+
dl.lastElementChild.appendChild(cbox.render());
}
else {
dl.lastElementChild.appendChild(inputEl);
dl.lastElementChild.appendChild(E('div', { 'class': 'cbi-button cbi-button-add' }, '+'));
- if (this.options.datatype)
- L.ui.addValidator(inputEl, this.options.datatype,
- true, null, 'blur', 'keyup');
+ if (this.options.datatype || this.options.validate)
+ L.ui.addValidator(inputEl, this.options.datatype || 'string',
+ true, this.options.validate, 'blur', 'keyup');
}
- for (var i = 0; i < this.values.length; i++)
- this.addItem(dl, this.values[i],
- this.choices ? this.choices[this.values[i]] : null);
+ for (var i = 0; i < this.values.length; i++) {
+ var label = this.choices ? this.choices[this.values[i]] : null;
+
+ if (L.dom.elem(label))
+ label = label.cloneNode(true);
+
+ this.addItem(dl, this.values[i], label);
+ }
return this.bind(dl);
},
addItem: function(dl, value, text, flash) {
var exists = false,
new_item = E('div', { 'class': flash ? 'item flash' : 'item', 'tabindex': 0 }, [
- E('span', {}, text || value),
+ E('span', {}, [ text || value ]),
E('input', {
'type': 'hidden',
'name': this.options.name,
sbVal.element.setAttribute('dynlistcustom', '');
}
- this.addItem(dl, sbVal.value, sbVal.text, true);
+ var label = sbVal.text;
+
+ if (sbVal.element) {
+ label = E([]);
+
+ for (var i = 0; i < sbVal.element.childNodes.length; i++)
+ label.appendChild(sbVal.element.childNodes[i].cloneNode(true));
+ }
+
+ this.addItem(dl, sbVal.value, label, true);
},
handleKeydown: function(ev) {
for (var i = 0; i < values.length; i++)
this.addItem(this.node, values[i],
this.choices ? this.choices[values[i]] : null);
+ },
+
+ addChoices: function(values, labels) {
+ var dl = this.node.lastElementChild.firstElementChild;
+ L.dom.callClassMethod(dl, 'addChoices', values, labels);
+ },
+
+ clearChoices: function() {
+ var dl = this.node.lastElementChild.firstElementChild;
+ L.dom.callClassMethod(dl, 'clearChoices');
}
});
}, options);
},
- callFileStat: rpc.declare({
- 'object': 'file',
- 'method': 'stat',
- 'params': [ 'path' ],
- 'expect': { '': {} }
- }),
-
- callFileList: rpc.declare({
- 'object': 'file',
- 'method': 'list',
- 'params': [ 'path' ],
- 'expect': { 'entries': [] }
- }),
-
- callFileRemove: rpc.declare({
- 'object': 'file',
- 'method': 'remove',
- 'params': [ 'path' ]
- }),
-
bind: function(browserEl) {
this.node = browserEl;
},
render: function() {
- return Promise.resolve(this.value != null ? this.callFileStat(this.value) : null).then(L.bind(function(stat) {
+ 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')
data.append('filename', path + '/' + filename);
data.append('filedata', fileinput.files[0]);
- return L.Request.post('/cgi-bin/cgi-upload', data, {
+ return L.Request.post(L.env.cgi_base + '/cgi-upload', data, {
progress: L.bind(function(btn, ev) {
btn.firstChild.data = '%.2f%%'.format((ev.loaded / ev.total) * 100);
}, this, ev.target)
hidden.value = '';
}
- return this.callFileRemove(path).then(L.bind(function(parent, ev, rc) {
- if (rc == 0)
- return this.handleSelect(parent, null, ev);
- else if (rc == 6)
- alert(_('Delete permission denied'));
- else
- alert(_('Delete request failed: %d %s').format(rc, rpc.getStatusText(rc)));
-
- }, this, parent, ev));
+ 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));
+ });
}
},
if (fileStat == null) {
L.dom.content(ul, E('em', { 'class': 'spinning' }, _('Loading directory contents…')));
- this.callFileList(path).then(L.bind(this.renderListing, this, browser, path));
+ L.resolveDefault(fs.list(path), []).then(L.bind(this.renderListing, this, browser, path));
}
else {
var button = this.node.firstElementChild,
ev.preventDefault();
- return this.callFileList(path).then(L.bind(function(button, browser, path, list) {
+ 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);
});
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]);
}
}),
+ /* 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(L.env.cgi_base + '/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() {
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('button', {
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;
}, this.varargs(arguments, 2, ctx));
},
+ AbstractElement: UIElement,
+
/* Widgets */
Textfield: UITextfield,
Textarea: UITextarea,
Dropdown: UIDropdown,
DynamicList: UIDynamicList,
Combobox: UICombobox,
+ ComboButton: UIComboButton,
Hiddenfield: UIHiddenfield,
FileUpload: UIFileUpload
});