'require uci';
'require fs';
-var modalDiv = null,
- tooltipDiv = null,
- indicatorDiv = null,
- tooltipTimeout = null;
+let modalDiv = null;
+let tooltipDiv = null;
+let indicatorDiv = null;
+let tooltipTimeout = null;
/**
* @class AbstractElement
* it in external JavaScript, use `L.require("ui").then(...)` and access the
* `AbstractElement` property of the class instance value.
*/
-var UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype */ {
+const UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype */ {
/**
* @typedef {Object} InitOptions
* @memberof LuCI.ui.AbstractElement
* string. Complex widgets such as `DynamicList` instances may result in
* an array of strings or `null` for unset values.
*/
- getValue: function() {
+ getValue() {
if (dom.matches(this.node, 'select') || dom.matches(this.node, 'input'))
return this.node.value;
* Complex widgets such as `DynamicList` instances may accept string array
* or `null` values.
*/
- setValue: function(value) {
+ setValue(value) {
if (dom.matches(this.node, 'select') || dom.matches(this.node, 'input'))
this.node.value = value;
},
* The placeholder to set for the input element. Only applicable to text
* inputs, not to radio buttons, selects or similar.
*/
- setPlaceholder: function(value) {
- var node = this.node ? this.node.querySelector('input,textarea') : null;
+ setPlaceholder(value) {
+ const node = this.node ? this.node.querySelector('input,textarea') : null;
if (node) {
- switch (node.getAttribute('type') || 'text') {
+ switch (node.getAttribute('type') ?? 'text') {
case 'password':
case 'search':
case 'tel':
* value and changes it back to the original state, it is still reported
* as changed.
*/
- isChanged: function() {
+ isChanged() {
return (this.node ? this.node.getAttribute('data-changed') : null) == 'true';
},
* Returns `true` if the current input value is valid or `false` if it does
* not meet the validation constraints.
*/
- isValid: function() {
+ isValid() {
return (this.validState !== false);
},
* @returns {string}
* The validation error at this time
*/
- getValidationError: function() {
- return this.validationError || '';
+ getValidationError() {
+ return this.validationError ?? '';
},
/**
* @instance
* @memberof LuCI.ui.AbstractElement
*/
- triggerValidation: function() {
+ triggerValidation() {
if (typeof(this.vfunc) != 'function')
return false;
- var wasValid = this.isValid();
+ const wasValid = this.isValid();
this.vfunc();
* @param {string[]} events
* The native DOM events for which event handlers should be registered.
*/
- registerEvents: function(targetNode, synevent, events) {
- var dispatchFn = L.bind(function(ev) {
+ registerEvents(targetNode, synevent, events) {
+ const dispatchFn = L.bind(function(ev) {
this.node.dispatchEvent(new CustomEvent(synevent, { bubbles: true }));
}, this);
- for (var i = 0; i < events.length; i++)
+ for (let i = 0; i < events.length; i++)
targetNode.addEventListener(events[i], dispatchFn);
},
* @param {...string} events
* The DOM events for which event handlers should be registered.
*/
- setUpdateEvents: function(targetNode /*, ... */) {
- var datatype = this.options.datatype,
- optional = this.options.hasOwnProperty('optional') ? this.options.optional : true,
- validate = this.options.validate,
- events = this.varargs(arguments, 1);
+ setUpdateEvents(targetNode, ...events) {
+ const datatype = this.options.datatype;
+ const optional = this.options.hasOwnProperty('optional') ? this.options.optional : true;
+ const validate = this.options.validate;
this.registerEvents(targetNode, 'widget-update', events);
if (!datatype && !validate)
return;
- this.vfunc = UI.prototype.addValidator.apply(UI.prototype, [
- targetNode, datatype || 'string',
+ this.vfunc = UI.prototype.addValidator(...[
+ targetNode, datatype ?? 'string',
optional, validate
].concat(events));
* @param {...string} events
* The DOM events for which event handlers should be registered.
*/
- setChangeEvents: function(targetNode /*, ... */) {
- var tag_changed = L.bind(function(ev) { this.setAttribute('data-changed', true) }, this.node);
+ setChangeEvents(targetNode, ...events) {
+ const 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);
+ for (let i = 0; i < events.length; i++)
+ targetNode.addEventListener(events[i], tag_changed);
- this.registerEvents(targetNode, 'widget-change', this.varargs(arguments, 1));
+ this.registerEvents(targetNode, 'widget-change', events);
},
/**
* Returns a DOM Node or DocumentFragment containing the rendered
* widget markup.
*/
- render: function() {}
+ render() {}
});
/**
* @param {LuCI.ui.Textfield.InitOptions} [options]
* Object describing the widget specific options to initialize the input.
*/
-var UITextfield = UIElement.extend(/** @lends LuCI.ui.Textfield.prototype */ {
+const UITextfield = UIElement.extend(/** @lends LuCI.ui.Textfield.prototype */ {
/**
* In addition to the [AbstractElement.InitOptions]{@link LuCI.ui.AbstractElement.InitOptions}
* the following properties are recognized:
* Specifies the HTML `placeholder` attribute which is displayed when the
* corresponding `<input>` element is empty.
*/
- __init__: function(value, options) {
+ __init__(value, options) {
this.value = value;
this.options = Object.assign({
optional: true,
},
/** @override */
- render: function() {
- var frameEl = E('div', { 'id': this.options.id });
- var inputEl = E('input', {
- 'id': this.options.id ? 'widget.' + this.options.id : null,
+ render() {
+ const frameEl = E('div', { 'id': this.options.id });
+ const inputEl = E('input', {
+ 'id': this.options.id ? `widget.${this.options.id}` : null,
'name': this.options.name,
'type': 'text',
'class': this.options.password ? 'cbi-input-password' : 'cbi-input-text',
'disabled': this.options.disabled ? '' : null,
'maxlength': this.options.maxlength,
'placeholder': this.options.placeholder,
- 'autocomplete': this.options.password ? 'new-password' : null,
'value': this.value,
});
'title': _('Reveal/hide password'),
'aria-label': _('Reveal/hide password'),
'click': function(ev) {
- var e = this.previousElementSibling;
+ const e = this.previousElementSibling;
e.type = (e.type === 'password') ? 'text' : 'password';
ev.preventDefault();
}
}, '∗')
]));
- window.requestAnimationFrame(function() { inputEl.type = 'password' });
+ window.requestAnimationFrame(() => { inputEl.type = 'password' });
}
else {
frameEl.appendChild(inputEl);
},
/** @private */
- bind: function(frameEl) {
- var inputEl = frameEl.querySelector('input');
+ bind(frameEl) {
+ const inputEl = frameEl.querySelector('input');
this.node = frameEl;
},
/** @override */
- getValue: function() {
- var inputEl = this.node.querySelector('input');
+ getValue() {
+ const inputEl = this.node.querySelector('input');
return inputEl.value;
},
/** @override */
- setValue: function(value) {
- var inputEl = this.node.querySelector('input');
+ setValue(value) {
+ const inputEl = this.node.querySelector('input');
inputEl.value = value;
}
});
* @param {LuCI.ui.Textarea.InitOptions} [options]
* Object describing the widget specific options to initialize the input.
*/
-var UITextarea = UIElement.extend(/** @lends LuCI.ui.Textarea.prototype */ {
+const UITextarea = UIElement.extend(/** @lends LuCI.ui.Textarea.prototype */ {
/**
* In addition to the [AbstractElement.InitOptions]{@link LuCI.ui.AbstractElement.InitOptions}
* the following properties are recognized:
* @property {boolean} [wrap=false]
* Specifies whether the HTML `wrap` attribute should be set.
*/
- __init__: function(value, options) {
+ __init__(value, options) {
this.value = value;
this.options = Object.assign({
optional: true,
},
/** @override */
- render: function() {
- var style = !this.options.cols ? 'width:100%' : null,
- frameEl = E('div', { 'id': this.options.id, 'style': style }),
- value = (this.value != null) ? String(this.value) : '';
+ render() {
+ const style = !this.options.cols ? 'width:100%' : null;
+ const frameEl = E('div', { 'id': this.options.id, 'style': style });
+ const value = (this.value != null) ? String(this.value) : '';
frameEl.appendChild(E('textarea', {
- 'id': this.options.id ? 'widget.' + this.options.id : null,
+ 'id': this.options.id ? `widget.${this.options.id}` : null,
'name': this.options.name,
'class': 'cbi-input-textarea',
'readonly': this.options.readonly ? '' : null,
},
/** @private */
- bind: function(frameEl) {
- var inputEl = frameEl.firstElementChild;
+ bind(frameEl) {
+ const inputEl = frameEl.firstElementChild;
this.node = frameEl;
},
/** @override */
- getValue: function() {
+ getValue() {
return this.node.firstElementChild.value;
},
/** @override */
- setValue: function(value) {
+ setValue(value) {
this.node.firstElementChild.value = value;
}
});
* @param {LuCI.ui.Checkbox.InitOptions} [options]
* Object describing the widget specific options to initialize the input.
*/
-var UICheckbox = UIElement.extend(/** @lends LuCI.ui.Checkbox.prototype */ {
+const UICheckbox = UIElement.extend(/** @lends LuCI.ui.Checkbox.prototype */ {
/**
* In addition to the [AbstractElement.InitOptions]{@link LuCI.ui.AbstractElement.InitOptions}
* the following properties are recognized:
* checkbox. This is a legacy property existing for compatibility reasons,
* it is required for HTML based form submissions.
*/
- __init__: function(value, options) {
+ __init__(value, options) {
this.value = value;
this.options = Object.assign({
value_enabled: '1',
},
/** @override */
- render: function() {
- var id = 'cb%08x'.format(Math.random() * 0xffffffff);
- var frameEl = E('div', {
+ render() {
+ const id = 'cb%08x'.format(Math.random() * 0xffffffff);
+ const frameEl = E('div', {
'id': this.options.id,
'class': 'cbi-checkbox'
});
'value': this.options.value_enabled,
'checked': (this.value == this.options.value_enabled) ? '' : null,
'disabled': this.options.disabled ? '' : null,
- 'data-widget-id': this.options.id ? 'widget.' + this.options.id : null
+ 'data-widget-id': this.options.id ? `widget.${this.options.id}` : null
}));
frameEl.appendChild(E('label', { 'for': id }));
if (this.options.tooltip != null) {
- var icon = "⚠️";
+ let icon = "⚠️";
if (this.options.tooltipicon != null)
icon = this.options.tooltipicon;
},
/** @private */
- bind: function(frameEl) {
+ bind(frameEl) {
this.node = frameEl;
- var input = frameEl.querySelector('input[type="checkbox"]');
+ const input = frameEl.querySelector('input[type="checkbox"]');
this.setUpdateEvents(input, 'click', 'blur');
this.setChangeEvents(input, 'change');
* @returns {boolean}
* Returns `true` when the checkbox is currently checked, otherwise `false`.
*/
- isChecked: function() {
+ isChecked() {
return this.node.querySelector('input[type="checkbox"]').checked;
},
/** @override */
- getValue: function() {
+ getValue() {
return this.isChecked()
? this.options.value_enabled
: this.options.value_disabled;
},
/** @override */
- setValue: function(value) {
+ setValue(value) {
this.node.querySelector('input[type="checkbox"]').checked = (value == this.options.value_enabled);
}
});
* @param {LuCI.ui.Select.InitOptions} [options]
* Object describing the widget specific options to initialize the inputs.
*/
-var UISelect = UIElement.extend(/** @lends LuCI.ui.Select.prototype */ {
+const UISelect = UIElement.extend(/** @lends LuCI.ui.Select.prototype */ {
/**
* In addition to the [AbstractElement.InitOptions]{@link LuCI.ui.AbstractElement.InitOptions}
* the following properties are recognized:
* Specifies a placeholder text which is displayed when no choice is
* selected yet. Only applicable to the `select` widget type.
*/
- __init__: function(value, choices, options) {
+ __init__(value, choices, options) {
if (!L.isObject(choices))
choices = {};
},
/** @override */
- render: function() {
- var frameEl = E('div', { 'id': this.options.id }),
- keys = Object.keys(this.choices);
+ render() {
+ const frameEl = E('div', { 'id': this.options.id });
+ let keys = Object.keys(this.choices);
if (this.options.sort === true)
keys.sort(L.naturalCompare);
if (this.options.widget != 'radio' && this.options.widget != 'checkbox') {
frameEl.appendChild(E('select', {
- 'id': this.options.id ? 'widget.' + this.options.id : null,
+ 'id': this.options.id ? `widget.${this.options.id}` : null,
'name': this.options.name,
'size': this.options.size,
'class': 'cbi-input-select',
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++) {
+ for (let i = 0; i < keys.length; i++) {
if (keys[i] == null || keys[i] == '')
continue;
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 {
- var brEl = (this.options.orientation === 'horizontal') ? document.createTextNode(' \xa0 ') : E('br');
+ const brEl = (this.options.orientation === 'horizontal') ? document.createTextNode(' \xa0 ') : E('br');
- for (var i = 0; i < keys.length; i++) {
+ for (let i = 0; i < keys.length; i++) {
frameEl.appendChild(E('span', {
'class': 'cbi-%s'.format(this.options.multiple ? 'checkbox' : 'radio')
}, [
E('input', {
'id': this.options.id ? 'widget.%s.%d'.format(this.options.id, i) : null,
- 'name': this.options.id || this.options.name,
+ 'name': this.options.id ?? this.options.name,
'type': this.options.multiple ? 'checkbox' : 'radio',
'class': this.options.multiple ? 'cbi-input-checkbox' : 'cbi-input-radio',
'value': keys[i],
'click': function(ev) {
ev.currentTarget.previousElementSibling.previousElementSibling.click();
}
- }, [ this.choices[keys[i]] || keys[i] ])
+ }, [ this.choices[keys[i]] ?? keys[i] ])
]));
frameEl.appendChild(brEl.cloneNode());
},
/** @private */
- bind: function(frameEl) {
+ bind(frameEl) {
this.node = frameEl;
if (this.options.widget != 'radio' && this.options.widget != 'checkbox') {
this.setChangeEvents(frameEl.firstChild, 'change');
}
else {
- var radioEls = frameEl.querySelectorAll('input[type="radio"]');
- for (var i = 0; i < radioEls.length; i++) {
+ const radioEls = frameEl.querySelectorAll('input[type="radio"]');
+ for (let i = 0; i < radioEls.length; i++) {
this.setUpdateEvents(radioEls[i], 'change', 'click', 'blur');
this.setChangeEvents(radioEls[i], 'change', 'click', 'blur');
}
},
/** @override */
- getValue: function() {
+ getValue() {
if (this.options.widget != 'radio' && this.options.widget != 'checkbox')
return this.node.firstChild.value;
- var radioEls = this.node.querySelectorAll('input[type="radio"]');
- for (var i = 0; i < radioEls.length; i++)
+ const radioEls = this.node.querySelectorAll('input[type="radio"]');
+ for (let i = 0; i < radioEls.length; i++)
if (radioEls[i].checked)
return radioEls[i].value;
},
/** @override */
- setValue: function(value) {
+ setValue(value) {
if (this.options.widget != 'radio' && this.options.widget != 'checkbox') {
if (value == null)
value = '';
- for (var i = 0; i < this.node.firstChild.options.length; i++)
+ for (let i = 0; i < this.node.firstChild.options.length; i++)
this.node.firstChild.options[i].selected = (this.node.firstChild.options[i].value == value);
return;
}
- var radioEls = frameEl.querySelectorAll('input[type="radio"]');
- for (var i = 0; i < radioEls.length; i++)
+ const radioEls = frameEl.querySelectorAll('input[type="radio"]');
+ for (let i = 0; i < radioEls.length; i++)
radioEls[i].checked = (radioEls[i].value == value);
}
});
* @param {LuCI.ui.Dropdown.InitOptions} [options]
* Object describing the widget specific options to initialize the dropdown.
*/
-var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
+const UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
/**
* In addition to the [AbstractElement.InitOptions]{@link LuCI.ui.AbstractElement.InitOptions}
* the following properties are recognized:
* compatibility reasons. It is usually better to `maxlength(N)` validation
* expression. Only applicable when `create` is `true`.
*/
- __init__: function(value, choices, options) {
+ __init__(value, choices, options) {
if (typeof(choices) != 'object')
choices = {};
},
/** @override */
- render: function() {
- var sb = E('div', {
+ render() {
+ const sb = E('div', {
'id': this.options.id,
'class': 'cbi-dropdown',
'multiple': this.options.multiple ? '' : null,
'tabindex': -1
}, E('ul'));
- var keys = Object.keys(this.choices);
+ let keys = Object.keys(this.choices);
if (this.options.sort === true)
keys.sort(L.naturalCompare);
keys = this.options.sort;
if (this.options.create)
- for (var i = 0; i < this.values.length; i++)
+ for (let i = 0; i < this.values.length; i++)
if (!this.choices.hasOwnProperty(this.values[i]))
keys.push(this.values[i]);
- for (var i = 0; i < keys.length; i++) {
- var label = this.choices[keys[i]];
+ for (let i = 0; i < keys.length; i++) {
+ let label = this.choices[keys[i]];
if (dom.elem(label))
label = label.cloneNode(true);
sb.lastElementChild.appendChild(E('li', {
'data-value': keys[i],
'selected': (this.values.indexOf(keys[i]) > -1) ? '' : null
- }, [ label || keys[i] ]));
+ }, [ label ?? keys[i] ]));
}
if (this.options.create) {
- var createEl = E('input', {
+ const createEl = E('input', {
'type': 'text',
'class': 'create-item-input',
'readonly': this.options.readonly ? '' : null,
'maxlength': this.options.maxlength,
- 'placeholder': this.options.custom_placeholder || this.options.placeholder
+ 'placeholder': this.options.custom_placeholder ?? this.options.placeholder
});
if (this.options.datatype || this.options.validate)
- UI.prototype.addValidator(createEl, this.options.datatype || 'string',
+ UI.prototype.addValidator(createEl, this.options.datatype ?? 'string',
true, this.options.validate, 'blur', 'keyup');
sb.lastElementChild.appendChild(E('li', { 'data-value': '-' }, createEl));
},
/** @private */
- bind: function(sb) {
- var o = this.options;
+ bind(sb) {
+ const o = this.options;
o.multiple = sb.hasAttribute('multiple');
o.optional = sb.hasAttribute('optional');
- o.placeholder = sb.getAttribute('placeholder') || o.placeholder;
- o.display_items = parseInt(sb.getAttribute('display-items') || o.display_items);
- o.dropdown_items = parseInt(sb.getAttribute('dropdown-items') || o.dropdown_items);
- o.create_query = sb.getAttribute('item-create') || o.create_query;
- o.create_template = sb.getAttribute('item-template') || o.create_template;
-
- var ul = sb.querySelector('ul'),
- more = sb.appendChild(E('span', { class: 'more', tabindex: -1 }, '···')),
- open = sb.appendChild(E('span', { class: 'open', tabindex: -1 }, '▾')),
- canary = sb.appendChild(E('div')),
- create = sb.querySelector(this.options.create_query),
- ndisplay = this.options.display_items,
- n = 0;
+ o.placeholder = sb.getAttribute('placeholder') ?? o.placeholder;
+ o.display_items = parseInt(sb.getAttribute('display-items') ?? o.display_items);
+ o.dropdown_items = parseInt(sb.getAttribute('dropdown-items') ?? o.dropdown_items);
+ o.create_query = sb.getAttribute('item-create') ?? o.create_query;
+ o.create_template = sb.getAttribute('item-template') ?? o.create_template;
+
+ const ul = sb.querySelector('ul');
+ const more = sb.appendChild(E('span', { class: 'more', tabindex: -1 }, '···'));
+ const open = sb.appendChild(E('span', { class: 'open', tabindex: -1 }, '▾'));
+ const canary = sb.appendChild(E('div'));
+ const create = sb.querySelector(this.options.create_query);
+ let ndisplay = this.options.display_items;
+ let n = 0;
if (this.options.multiple) {
- var items = ul.querySelectorAll('li');
+ let items = ul.querySelectorAll('li');
- for (var i = 0; i < items.length; i++) {
+ for (let i = 0; i < items.length; i++) {
this.transformItem(sb, items[i]);
if (items[i].hasAttribute('selected') && ndisplay-- > 0)
}
else {
if (this.options.optional && !ul.querySelector('li[data-value=""]')) {
- var placeholder = E('li', { placeholder: '' },
- this.options.select_placeholder || this.options.placeholder);
+ const placeholder = E('li', { placeholder: '' },
+ this.options.select_placeholder ?? this.options.placeholder);
ul.firstChild
? ul.insertBefore(placeholder, ul.firstChild)
: ul.appendChild(placeholder);
}
- var items = ul.querySelectorAll('li'),
- sel = sb.querySelectorAll('[selected]');
+ let items = ul.querySelectorAll('li');
+ const sel = sb.querySelectorAll('[selected]');
- sel.forEach(function(s) {
+ sel.forEach(s => {
s.removeAttribute('selected');
});
- var s = sel[0] || items[0];
+ const s = sel[0] ?? items[0];
if (s) {
s.setAttribute('selected', '');
s.setAttribute('display', n++);
sb.removeAttribute('empty');
dom.content(more, (ndisplay == this.options.display_items)
- ? (this.options.select_placeholder || this.options.placeholder) : '···');
+ ? (this.options.select_placeholder ?? this.options.placeholder) : '···');
sb.addEventListener('click', this.handleClick.bind(this));
sb.addEventListener('cbi-dropdown-select', this.handleDropdownSelect.bind(this));
if ('ontouchstart' in window) {
- sb.addEventListener('touchstart', function(ev) { ev.stopPropagation(); });
+ sb.addEventListener('touchstart', ev => ev.stopPropagation());
window.addEventListener('touchstart', this.closeAllDropdowns);
}
else {
create.addEventListener('focus', this.handleCreateFocus.bind(this));
create.addEventListener('blur', this.handleCreateBlur.bind(this));
- var li = findParent(create, 'li');
+ const li = findParent(create, 'li');
li.setAttribute('unselectable', '');
li.addEventListener('click', this.handleCreateClick.bind(this));
},
/** @private */
- getScrollParent: function(element) {
- var parent = element,
- style = getComputedStyle(element),
- excludeStaticParent = (style.position === 'absolute');
+ getScrollParent(element) {
+ let parent = element;
+ let style = getComputedStyle(element);
+ const excludeStaticParent = (style.position === 'absolute');
if (style.position === 'fixed')
return document.body;
},
/** @private */
- openDropdown: function(sb) {
- var st = window.getComputedStyle(sb, null),
- ul = sb.querySelector('ul'),
- li = ul.querySelectorAll('li'),
- fl = findParent(sb, '.cbi-value-field'),
- sel = ul.querySelector('[selected]'),
- rect = sb.getBoundingClientRect(),
- items = Math.min(this.options.dropdown_items, li.length),
- scrollParent = this.getScrollParent(sb);
-
- document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
+ openDropdown(sb) {
+ const st = window.getComputedStyle(sb, null);
+ const ul = sb.querySelector('ul');
+ const li = ul.querySelectorAll('li');
+ const fl = findParent(sb, '.cbi-value-field');
+ const sel = ul.querySelector('[selected]');
+ const rect = sb.getBoundingClientRect();
+ const items = Math.min(this.options.dropdown_items, li.length);
+ const scrollParent = this.getScrollParent(sb);
+
+ document.querySelectorAll('.cbi-dropdown[open]').forEach(s => {
s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
});
sb.setAttribute('open', '');
- var pv = ul.cloneNode(true);
+ const pv = ul.cloneNode(true);
pv.classList.add('preview');
if (fl)
fl.classList.add('cbi-dropdown-open');
if ('ontouchstart' in window) {
- var vpWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
- vpHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0),
- start = null;
-
- ul.style.top = sb.offsetHeight + 'px';
- ul.style.left = -rect.left + 'px';
- ul.style.right = (rect.right - vpWidth) + 'px';
- ul.style.maxHeight = (vpHeight * 0.5) + 'px';
+ const vpWidth = Math.max(document.documentElement.clientWidth, window.innerWidth ?? 0);
+ const vpHeight = Math.max(document.documentElement.clientHeight, window.innerHeight ?? 0);
+ let start = null;
+
+ ul.style.top = `${sb.offsetHeight}px`;
+ ul.style.left = `${-rect.left}px`;
+ ul.style.right = `${rect.right - vpWidth}px`;
+ ul.style.maxHeight = `${vpHeight * 0.5}px`;
ul.style.WebkitOverflowScrolling = 'touch';
- var scrollFrom = scrollParent.scrollTop,
- scrollTo = scrollFrom + rect.top - vpHeight * 0.5;
+ const scrollFrom = scrollParent.scrollTop;
+ const scrollTo = scrollFrom + rect.top - vpHeight * 0.5;
- var scrollStep = function(timestamp) {
+ const scrollStep = timestamp => {
if (!start) {
start = timestamp;
ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
}
- var duration = Math.max(timestamp - start, 1);
+ const duration = Math.max(timestamp - start, 1);
if (duration < 100) {
scrollParent.scrollTop = scrollFrom + (scrollTo - scrollFrom) * (duration / 100);
window.requestAnimationFrame(scrollStep);
ul.style.maxHeight = '1px';
ul.style.top = ul.style.bottom = '';
- window.requestAnimationFrame(function() {
- var containerRect = scrollParent.getBoundingClientRect(),
- itemHeight = li[Math.max(0, li.length - 2)].getBoundingClientRect().height,
- fullHeight = 0,
- spaceAbove = rect.top - containerRect.top,
- spaceBelow = containerRect.bottom - rect.bottom;
+ window.requestAnimationFrame(() => {
+ const containerRect = scrollParent.getBoundingClientRect();
+ const itemHeight = li[Math.max(0, li.length - 2)].getBoundingClientRect().height;
+ let fullHeight = 0;
+ const spaceAbove = rect.top - containerRect.top;
+ const spaceBelow = containerRect.bottom - rect.bottom;
- for (var i = 0; i < (items == -1 ? li.length : items); i++)
+ for (let i = 0; i < (items == -1 ? li.length : items); i++)
fullHeight += li[i].getBoundingClientRect().height;
if (fullHeight <= spaceBelow) {
- ul.style.top = rect.height + 'px';
- ul.style.maxHeight = spaceBelow + 'px';
+ ul.style.top = `${rect.height}px`;
+ ul.style.maxHeight = `${spaceBelow}px`;
}
else if (fullHeight <= spaceAbove) {
- ul.style.bottom = rect.height + 'px';
- ul.style.maxHeight = spaceAbove + 'px';
+ ul.style.bottom = `${rect.height}px`;
+ ul.style.maxHeight = `${spaceAbove}px`;
}
else if (spaceBelow >= spaceAbove) {
- ul.style.top = rect.height + 'px';
- ul.style.maxHeight = (spaceBelow - (spaceBelow % itemHeight)) + 'px';
+ ul.style.top = `${rect.height}px`;
+ ul.style.maxHeight = `${spaceBelow - (spaceBelow % itemHeight)}px`;
}
else {
- ul.style.bottom = rect.height + 'px';
- ul.style.maxHeight = (spaceAbove - (spaceAbove % itemHeight)) + 'px';
+ ul.style.bottom = `${rect.height}px`;
+ ul.style.maxHeight = `${spaceAbove - (spaceAbove % itemHeight)}px`;
}
ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
});
}
- var cboxes = ul.querySelectorAll('[selected] input[type="checkbox"]');
- for (var i = 0; i < cboxes.length; i++) {
+ const cboxes = ul.querySelectorAll('[selected] input[type="checkbox"]');
+ for (let i = 0; i < cboxes.length; i++) {
cboxes[i].checked = true;
cboxes[i].disabled = (cboxes.length == 1 && !this.options.optional);
- };
+ }
ul.classList.add('dropdown');
sb.insertBefore(pv, ul.nextElementSibling);
- li.forEach(function(l) {
+ li.forEach(l => {
if (!l.hasAttribute('unselectable'))
l.setAttribute('tabindex', 0);
});
sb.lastElementChild.setAttribute('tabindex', 0);
- var focusFn = L.bind(function(el) {
+ const focusFn = L.bind(function(el) {
this.setFocus(sb, el, true);
ul.removeEventListener('transitionend', focusFn);
- }, this, sel || li[0]);
+ }, this, sel ?? li[0]);
ul.addEventListener('transitionend', focusFn);
},
/** @private */
- closeDropdown: function(sb, no_focus) {
+ closeDropdown(sb, no_focus) {
if (!sb.hasAttribute('open'))
return;
- var pv = sb.querySelector('ul.preview'),
- ul = sb.querySelector('ul.dropdown'),
- li = ul.querySelectorAll('li'),
- fl = findParent(sb, '.cbi-value-field');
+ const pv = sb.querySelector('ul.preview');
+ const ul = sb.querySelector('ul.dropdown');
+ const li = ul.querySelectorAll('li');
+ const fl = findParent(sb, '.cbi-value-field');
- li.forEach(function(l) { l.removeAttribute('tabindex'); });
+ li.forEach(l => l.removeAttribute('tabindex'));
sb.lastElementChild.removeAttribute('tabindex');
sb.removeChild(pv);
},
/** @private */
- toggleItem: function(sb, li, force_state) {
- var ul = li.parentNode;
+ toggleItem(sb, li, force_state) {
+ const ul = li.parentNode;
if (li.hasAttribute('unselectable'))
return;
if (this.options.multiple) {
- var cbox = li.querySelector('input[type="checkbox"]'),
- items = li.parentNode.querySelectorAll('li'),
- label = sb.querySelector('ul.preview'),
- sel = li.parentNode.querySelectorAll('[selected]').length,
- more = sb.querySelector('.more'),
- ndisplay = this.options.display_items,
- n = 0;
+ const cbox = li.querySelector('input[type="checkbox"]');
+ const items = li.parentNode.querySelectorAll('li');
+ const label = sb.querySelector('ul.preview');
+ let sel = li.parentNode.querySelectorAll('[selected]').length;
+ const more = sb.querySelector('.more');
+ let ndisplay = this.options.display_items;
+ let n = 0;
if (li.hasAttribute('selected')) {
if (force_state !== true) {
while (label && label.firstElementChild)
label.removeChild(label.firstElementChild);
- for (var i = 0; i < items.length; i++) {
+ for (let i = 0; i < items.length; i++) {
items[i].removeAttribute('display');
if (items[i].hasAttribute('selected')) {
if (ndisplay-- > 0) {
if (label)
label.appendChild(items[i].cloneNode(true));
}
- var c = items[i].querySelector('input[type="checkbox"]');
+ const c = items[i].querySelector('input[type="checkbox"]');
if (c)
c.disabled = (sel == 1 && !this.options.optional);
}
sb.removeAttribute('empty');
dom.content(more, (ndisplay === this.options.display_items)
- ? (this.options.select_placeholder || this.options.placeholder) : '···');
+ ? (this.options.select_placeholder ?? this.options.placeholder) : '···');
}
else {
- var sel = li.parentNode.querySelector('[selected]');
+ let sel = li.parentNode.querySelector('[selected]');
if (sel) {
sel.removeAttribute('display');
sel.removeAttribute('selected');
},
/** @private */
- transformItem: function(sb, li) {
- var cbox = E('form', {}, E('input', { type: 'checkbox', tabindex: -1, onclick: 'event.preventDefault()' })),
- label = E('label');
+ transformItem(sb, li) {
+ const cbox = E('form', {}, E('input', { type: 'checkbox', tabindex: -1, onclick: 'event.preventDefault()' }));
+ const label = E('label');
while (li.firstChild)
label.appendChild(li.firstChild);
},
/** @private */
- saveValues: function(sb, ul) {
- var sel = ul.querySelectorAll('li[selected]'),
- div = sb.lastElementChild,
- name = this.options.name,
- strval = '',
- values = [];
+ saveValues(sb, ul) {
+ const sel = ul.querySelectorAll('li[selected]');
+ const div = sb.lastElementChild;
+ const name = this.options.name;
+ let strval = '';
+ const values = [];
while (div.lastElementChild)
div.removeChild(div.lastElementChild);
- sel.forEach(function (s) {
+ sel.forEach(s => {
if (s.hasAttribute('placeholder'))
return;
- var v = {
+ const v = {
text: s.innerText,
value: s.hasAttribute('data-value') ? s.getAttribute('data-value') : s.innerText,
element: s
values.push(v);
- strval += strval.length ? ' ' + v.value : v.value;
+ strval += strval.length ? ` ${v.value}` : v.value;
});
- var detail = {
+ const detail = {
instance: this,
element: sb
};
},
/** @private */
- setValues: function(sb, values) {
- var ul = sb.querySelector('ul');
+ setValues(sb, values) {
+ const ul = sb.querySelector('ul');
if (this.options.create) {
- for (var value in values) {
+ for (const value in values) {
this.createItems(sb, value);
if (!this.options.multiple)
}
if (this.options.multiple) {
- var lis = ul.querySelectorAll('li[data-value]');
- for (var i = 0; i < lis.length; i++) {
- var value = lis[i].getAttribute('data-value');
+ const lis = ul.querySelectorAll('li[data-value]');
+ for (let i = 0; i < lis.length; i++) {
+ const value = lis[i].getAttribute('data-value');
if (values === null || !(value in values))
this.toggleItem(sb, lis[i], false);
else
}
}
else {
- var ph = ul.querySelector('li[placeholder]');
+ const ph = ul.querySelector('li[placeholder]');
if (ph)
this.toggleItem(sb, ph);
- var lis = ul.querySelectorAll('li[data-value]');
- for (var i = 0; i < lis.length; i++) {
- var value = lis[i].getAttribute('data-value');
+ const lis = ul.querySelectorAll('li[data-value]');
+ for (let i = 0; i < lis.length; i++) {
+ const value = lis[i].getAttribute('data-value');
if (values !== null && (value in values))
this.toggleItem(sb, lis[i]);
}
},
/** @private */
- setFocus: function(sb, elem, scroll) {
+ setFocus(sb, elem, scroll) {
if (sb.hasAttribute('locked-in'))
return;
- sb.querySelectorAll('.focus').forEach(function(e) {
+ sb.querySelectorAll('.focus').forEach(e => {
e.classList.remove('focus');
});
},
/** @private */
- createChoiceElement: function(sb, value, label) {
- var tpl = sb.querySelector(this.options.create_template),
- markup = null;
+ handleMouseout(ev) {
+ const sb = ev.currentTarget;
+
+ if (!sb.hasAttribute('open'))
+ return;
+
+ sb.querySelectorAll('.focus').forEach(e => {
+ e.classList.remove('focus');
+ });
+
+ sb.querySelector('ul.dropdown').focus();
+ },
+
+ /** @private */
+ createChoiceElement(sb, value, label) {
+ const tpl = sb.querySelector(this.options.create_template);
+ let markup = null;
if (tpl)
- markup = (tpl.textContent || tpl.innerHTML || tpl.firstChild.data).replace(/^<!--|--!?>$/, '').trim();
+ 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]');
+ const new_item = E(markup.replace(/{{value}}/g, '%h'.format(value)));
+ const placeholder = new_item.querySelector('[data-label-placeholder]');
if (placeholder) {
- var content = E('span', {}, label || this.choices[value] || [ value ]);
+ const content = E('span', {}, label ?? this.choices[value] ?? [ value ]);
while (content.firstChild)
placeholder.parentNode.insertBefore(content.firstChild, placeholder);
},
/** @private */
- createItems: function(sb, value) {
- var sbox = this,
- val = (value || '').trim(),
- ul = sb.querySelector('ul');
+ createItems(sb, value) {
+ const sbox = this;
+ let val = (value ?? '').trim();
+ const ul = sb.querySelector('ul');
if (!sbox.options.multiple)
val = val.length ? [ val ] : [];
else
val = val.length ? val.split(/\s+/) : [];
- val.forEach(function(item) {
- var new_item = null;
+ val.forEach(item => {
+ let new_item = null;
- ul.childNodes.forEach(function(li) {
+ ul.childNodes.forEach(li => {
if (li.getAttribute && li.getAttribute('data-value') === item)
new_item = li;
});
new_item = sbox.createChoiceElement(sb, item);
if (!sbox.options.multiple) {
- var old = ul.querySelector('li[created]');
+ const old = ul.querySelector('li[created]');
if (old)
ul.removeChild(old);
* If set to `true`, deselect and remove selected choices as well instead
* of keeping them.
*/
- 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');
+ clearChoices(reset_value) {
+ const ul = this.node.querySelector('ul');
+ const lis = ul ? ul.querySelectorAll('li[data-value]') : [];
+ const len = lis.length - (this.options.create ? 1 : 0);
+ const val = reset_value ? null : this.getValue();
+
+ for (let i = 0; i < len; i++) {
+ const lival = lis[i].getAttribute('data-value');
if (val == null ||
(!this.options.multiple && val != lival) ||
(this.options.multiple && val.indexOf(lival) == -1))
* as label text. Choice labels may be any valid value accepted by
* {@link LuCI.dom#content}.
*/
- addChoices: function(values, labels) {
- var sb = this.node,
- ul = sb.querySelector('ul'),
- lis = ul ? ul.querySelectorAll('li[data-value]') : [];
+ addChoices(values, labels) {
+ const sb = this.node;
+ const ul = sb.querySelector('ul');
+ const 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 (let i = 0; i < values.length; i++) {
+ let found = false;
- for (var j = 0; j < lis.length; j++) {
+ for (let j = 0; j < lis.length; j++) {
if (lis[j].getAttribute('data-value') === values[i]) {
found = true;
break;
/**
* Close all open dropdown widgets in the current document.
*/
- closeAllDropdowns: function() {
- document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
+ closeAllDropdowns() {
+ document.querySelectorAll('.cbi-dropdown[open]').forEach(s => {
s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
});
},
/** @private */
- handleClick: function(ev) {
- var sb = ev.currentTarget;
+ handleClick(ev) {
+ const sb = ev.currentTarget;
if (!sb.hasAttribute('open')) {
if (!matchesElem(ev.target, 'input'))
this.openDropdown(sb);
}
else {
- var li = findParent(ev.target, 'li');
+ const li = findParent(ev.target, 'li');
if (li && li.parentNode.classList.contains('dropdown'))
this.toggleItem(sb, li);
else if (li && li.parentNode.classList.contains('preview'))
},
/** @private */
- handleKeydown: function(ev) {
- var sb = ev.currentTarget,
- ul = sb.querySelector('ul.dropdown');
+ handleKeydown(ev) {
+ const sb = ev.currentTarget;
+ const ul = sb.querySelector('ul.dropdown');
if (matchesElem(ev.target, 'input'))
return;
}
}
else {
- var active = findParent(document.activeElement, 'li');
+ const active = findParent(document.activeElement, 'li');
switch (ev.keyCode) {
case 27:
},
/** @private */
- handleDropdownClose: function(ev) {
- var sb = ev.currentTarget;
+ handleDropdownClose(ev) {
+ const sb = ev.currentTarget;
this.closeDropdown(sb, true);
},
/** @private */
- handleDropdownSelect: function(ev) {
- var sb = ev.currentTarget,
- li = findParent(ev.target, 'li');
+ handleDropdownSelect(ev) {
+ const sb = ev.currentTarget;
+ const li = findParent(ev.target, 'li');
if (!li)
return;
},
/** @private */
- handleFocus: function(ev) {
- var sb = ev.currentTarget;
+ handleMouseover(ev) {
+ const sb = ev.currentTarget;
+
+ if (!sb.hasAttribute('open'))
+ return;
+
+ const li = findParent(ev.target, 'li');
+
+ if (li && li.parentNode.classList.contains('dropdown'))
+ this.setFocus(sb, li);
+ },
+
+ /** @private */
+ handleFocus(ev) {
+ const sb = ev.currentTarget;
- document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
+ document.querySelectorAll('.cbi-dropdown[open]').forEach(s => {
if (s !== sb || sb.hasAttribute('open'))
s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
});
},
/** @private */
- handleCanaryFocus: function(ev) {
+ handleCanaryFocus(ev) {
this.closeDropdown(ev.currentTarget.parentNode);
},
/** @private */
- handleCreateKeydown: function(ev) {
- var input = ev.currentTarget,
- li = findParent(input, 'li'),
- sb = findParent(li, '.cbi-dropdown');
+ handleCreateKeydown(ev) {
+ const input = ev.currentTarget;
+ const li = findParent(input, 'li');
+ const sb = findParent(li, '.cbi-dropdown');
switch (ev.keyCode) {
case 13:
},
/** @private */
- handleCreateFocus: function(ev) {
- var input = ev.currentTarget,
- li = findParent(input, 'li'),
- cbox = li.querySelector('input[type="checkbox"]'),
- sb = findParent(input, '.cbi-dropdown');
+ handleCreateFocus(ev) {
+ const input = ev.currentTarget;
+ const li = findParent(input, 'li');
+ const cbox = li.querySelector('input[type="checkbox"]');
+ const sb = findParent(input, '.cbi-dropdown');
if (cbox)
cbox.checked = true;
},
/** @private */
- handleCreateBlur: function(ev) {
- var input = ev.currentTarget,
- cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
- sb = findParent(input, '.cbi-dropdown');
+ handleCreateBlur(ev) {
+ const input = ev.currentTarget;
+ const cbox = findParent(input, 'li').querySelector('input[type="checkbox"]');
+ const sb = findParent(input, '.cbi-dropdown');
if (cbox)
cbox.checked = false;
},
/** @private */
- handleCreateClick: function(ev) {
+ handleCreateClick(ev) {
ev.currentTarget.querySelector(this.options.create_query).focus();
},
/** @override */
- setValue: function(values) {
+ setValue(values) {
if (this.options.multiple) {
if (!Array.isArray(values))
values = (values != null && values != '') ? [ values ] : [];
- var v = {};
+ const v = {};
- for (var i = 0; i < values.length; i++)
+ for (let i = 0; i < values.length; i++)
v[values[i]] = true;
this.setValues(this.node, v);
}
else {
- var v = {};
+ const v = {};
if (values != null) {
if (Array.isArray(values))
},
/** @override */
- getValue: function() {
- var div = this.node.lastElementChild,
- h = div.querySelectorAll('input[type="hidden"]'),
- v = [];
+ getValue() {
+ const div = this.node.lastElementChild;
+ const h = div.querySelectorAll('input[type="hidden"]');
+ const v = [];
- for (var i = 0; i < h.length; i++)
+ for (let i = 0; i < h.length; i++)
v.push(h[i].value);
return this.options.multiple ? v : v[0];
* @param {LuCI.ui.Combobox.InitOptions} [options]
* Object describing the widget specific options to initialize the dropdown.
*/
-var UICombobox = UIDropdown.extend(/** @lends LuCI.ui.Combobox.prototype */ {
+const UICombobox = UIDropdown.extend(/** @lends LuCI.ui.Combobox.prototype */ {
/**
* Comboboxes support the same properties as
* [Dropdown.InitOptions]{@link LuCI.ui.Dropdown.InitOptions} but enforce
* Since Comboboxes are always optional, this property is forcibly set to
* `true`.
*/
- __init__: function(value, choices, options) {
+ __init__(value, choices, options) {
this.super('__init__', [ value, choices, Object.assign({
select_placeholder: _('-- Please choose --'),
custom_placeholder: _('-- custom --'),
* @param {LuCI.ui.ComboButton.InitOptions} [options]
* Object describing the widget specific options to initialize the button.
*/
-var UIComboButton = UIDropdown.extend(/** @lends LuCI.ui.ComboButton.prototype */ {
+const UIComboButton = UIDropdown.extend(/** @lends LuCI.ui.ComboButton.prototype */ {
/**
* ComboButtons support the same properties as
* [Dropdown.InitOptions]{@link LuCI.ui.Dropdown.InitOptions} but enforce
* and receive the DOM click event as first as well as the selected action
* choice value as second argument.
*/
- __init__: function(value, choices, options) {
+ __init__(value, choices, options) {
this.super('__init__', [ value, choices, Object.assign({
sort: true
}, options, {
},
/** @override */
- render: function(/* ... */) {
- var node = UIDropdown.prototype.render.apply(this, arguments),
- val = this.getValue();
+ render(...args) {
+ const node = UIDropdown.prototype.render.call(this, ...args);
+ const val = this.getValue();
if (L.isObject(this.options.classes) && this.options.classes.hasOwnProperty(val))
- node.setAttribute('class', 'cbi-dropdown ' + this.options.classes[val]);
+ node.setAttribute('class', `cbi-dropdown ${this.options.classes[val]}`);
return node;
},
/** @private */
- handleClick: function(ev) {
- var sb = ev.currentTarget,
- t = ev.target;
+ handleClick(ev, ...args) {
+ const sb = ev.currentTarget;
+ const t = ev.target;
if (sb.hasAttribute('open') || dom.matches(t, '.cbi-dropdown > span.open'))
- return UIDropdown.prototype.handleClick.apply(this, arguments);
+ return UIDropdown.prototype.handleClick.call(this, ev, ...args);
if (this.options.click)
return this.options.click.call(sb, ev, this.getValue());
},
/** @private */
- toggleItem: function(sb /*, ... */) {
- var rv = UIDropdown.prototype.toggleItem.apply(this, arguments),
- val = this.getValue();
+ toggleItem(sb, ...args) {
+ const rv = UIDropdown.prototype.toggleItem.call(this, sb, ...args);
+ const val = this.getValue();
if (L.isObject(this.options.classes) && this.options.classes.hasOwnProperty(val))
- sb.setAttribute('class', 'cbi-dropdown ' + this.options.classes[val]);
+ sb.setAttribute('class', `cbi-dropdown ${this.options.classes[val]}`);
else
sb.setAttribute('class', 'cbi-dropdown');
* @param {LuCI.ui.DynamicList.InitOptions} [options]
* Object describing the widget specific options to initialize the dynamic list.
*/
-var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */ {
+const UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */ {
/**
* In case choices are passed to the dynamic list constructor, the widget
* supports the same properties as [Dropdown.InitOptions]{@link LuCI.ui.Dropdown.InitOptions}
* predefined choice values, the dropdown must be made optional to allow
* it to remain unselected.
*/
- __init__: function(values, choices, options) {
+ __init__(values, choices, options) {
if (!Array.isArray(values))
values = (values != null && values != '') ? [ values ] : [];
},
/** @override */
- render: function() {
- var dl = E('div', {
+ render() {
+ const dl = E('div', {
'id': this.options.id,
'class': 'cbi-dynlist',
'disabled': this.options.disabled ? '' : null
if (this.options.placeholder != null)
this.options.select_placeholder = this.options.placeholder;
- var cbox = new UICombobox(null, this.choices, this.options);
+ const cbox = new UICombobox(null, this.choices, this.options);
dl.lastElementChild.appendChild(cbox.render());
}
else {
- var inputEl = E('input', {
- 'id': this.options.id ? 'widget.' + this.options.id : null,
+ const inputEl = E('input', {
+ 'id': this.options.id ? `widget.${this.options.id}` : null,
'type': 'text',
'class': 'cbi-input-text',
'placeholder': this.options.placeholder,
dl.lastElementChild.appendChild(E('div', { 'class': 'btn cbi-button cbi-button-add' }, '+'));
if (this.options.datatype || this.options.validate)
- UI.prototype.addValidator(inputEl, this.options.datatype || 'string',
+ UI.prototype.addValidator(inputEl, this.options.datatype ?? 'string',
true, this.options.validate, 'blur', 'keyup');
}
- for (var i = 0; i < this.values.length; i++) {
- var label = this.choices ? this.choices[this.values[i]] : null;
+ for (let i = 0; i < this.values.length; i++) {
+ let label = this.choices ? this.choices[this.values[i]] : null;
if (dom.elem(label))
label = label.cloneNode(true);
},
/** @private */
- initDragAndDrop: function(dl) {
+ initDragAndDrop(dl) {
let draggedItem = null;
let placeholder = null;
},
/** @private */
- addItem: function(dl, value, text, flash) {
- var exists = false,
- new_item = E('div', { 'class': flash ? 'item flash' : 'item', 'tabindex': 0, 'draggable': true }, [
- E('span', {}, [ text || value ]),
- E('input', {
- 'type': 'hidden',
- 'name': this.options.name,
- 'value': value })]);
+ addItem(dl, value, text, flash) {
+ let exists = false;
- dl.querySelectorAll('.item').forEach(function(item) {
+ const new_item = E('div', { 'class': flash ? 'item flash' : 'item', 'tabindex': 0, 'draggable': true }, [
+ E('span', {}, [ text ?? value ]),
+ E('input', {
+ 'type': 'hidden',
+ 'name': this.options.name,
+ 'value': value })]);
+
+ dl.querySelectorAll('.item').forEach(item => {
if (exists)
return;
- var hidden = item.querySelector('input[type="hidden"]');
+ let hidden = item.querySelector('input[type="hidden"]');
if (hidden && hidden.parentNode !== item)
hidden = null;
});
if (!exists) {
- var ai = dl.querySelector('.add-item');
+ const ai = dl.querySelector('.add-item');
ai.parentNode.insertBefore(new_item, ai);
}
},
/** @private */
- removeItem: function(dl, item) {
- var value = item.querySelector('input[type="hidden"]').value;
- var sb = dl.querySelector('.cbi-dropdown');
+ removeItem(dl, item) {
+ const value = item.querySelector('input[type="hidden"]').value;
+ const sb = dl.querySelector('.cbi-dropdown');
if (sb)
- sb.querySelectorAll('ul > li').forEach(function(li) {
+ sb.querySelectorAll('ul > li').forEach(li => {
if (li.getAttribute('data-value') === value) {
if (li.hasAttribute('dynlistcustom'))
li.parentNode.removeChild(li);
},
/** @private */
- handleClick: function(ev) {
- var dl = ev.currentTarget,
- item = findParent(ev.target, '.item');
+ handleClick(ev) {
+ const dl = ev.currentTarget;
+ const item = findParent(ev.target, '.item');
if (this.options.disabled)
return;
}
}
else if (matchesElem(ev.target, '.cbi-button-add')) {
- var input = ev.target.previousElementSibling;
+ const input = ev.target.previousElementSibling;
if (input.value.length && !input.classList.contains('cbi-input-invalid')) {
this.addItem(dl, input.value, null, true);
input.value = '';
},
/** @private */
- handleDropdownChange: function(ev) {
- var dl = ev.currentTarget,
- sbIn = ev.detail.instance,
- sbEl = ev.detail.element,
- sbVal = ev.detail.value;
+ handleDropdownChange(ev) {
+ const dl = ev.currentTarget;
+ const sbIn = ev.detail.instance;
+ const sbEl = ev.detail.element;
+ const sbVal = ev.detail.value;
if (sbVal === null)
return;
sbVal.element.setAttribute('dynlistcustom', '');
}
- var label = sbVal.text;
+ let label = sbVal.text;
if (sbVal.element) {
label = E([]);
- for (var i = 0; i < sbVal.element.childNodes.length; i++)
+ for (let i = 0; i < sbVal.element.childNodes.length; i++)
label.appendChild(sbVal.element.childNodes[i].cloneNode(true));
}
},
/** @private */
- handleKeydown: function(ev) {
- var dl = ev.currentTarget,
- item = findParent(ev.target, '.item');
+ handleKeydown(ev) {
+ const dl = ev.currentTarget;
+ const item = findParent(ev.target, '.item');
if (item) {
switch (ev.keyCode) {
},
/** @override */
- getValue: function() {
- var items = this.node.querySelectorAll('.item > input[type="hidden"]'),
- input = this.node.querySelector('.add-item > input[type="text"]'),
- v = [];
+ getValue() {
+ const items = this.node.querySelectorAll('.item > input[type="hidden"]');
+ const input = this.node.querySelector('.add-item > input[type="text"]');
+ const v = [];
- for (var i = 0; i < items.length; i++)
+ for (let i = 0; i < items.length; i++)
v.push(items[i].value);
if (input && input.value != null && input.value.match(/\S/) &&
input.classList.contains('cbi-input-invalid') == false &&
- v.filter(function(s) { return s == input.value }).length == 0)
+ v.filter(s => s == input.value).length == 0)
v.push(input.value);
return v;
},
/** @override */
- setValue: function(values) {
+ setValue(values) {
if (!Array.isArray(values))
values = (values != null && values != '') ? [ values ] : [];
- var items = this.node.querySelectorAll('.item');
+ const items = this.node.querySelectorAll('.item');
- for (var i = 0; i < items.length; i++)
+ for (let i = 0; i < items.length; i++)
if (items[i].parentNode === this.node)
this.removeItem(this.node, items[i]);
- for (var i = 0; i < values.length; i++)
+ for (let i = 0; i < values.length; i++)
this.addItem(this.node, values[i],
this.choices ? this.choices[values[i]] : null);
},
* as label text. Choice labels may be any valid value accepted by
* {@link LuCI.dom#content}.
*/
- addChoices: function(values, labels) {
- var dl = this.node.lastElementChild.firstElementChild;
+ addChoices(values, labels) {
+ const dl = this.node.lastElementChild.firstElementChild;
dom.callClassMethod(dl, 'addChoices', values, labels);
},
* @instance
* @memberof LuCI.ui.DynamicList
*/
- clearChoices: function() {
- var dl = this.node.lastElementChild.firstElementChild;
+ clearChoices() {
+ const dl = this.node.lastElementChild.firstElementChild;
dom.callClassMethod(dl, 'clearChoices');
}
});
* @param {LuCI.ui.AbstractElement.InitOptions} [options]
* Object describing the widget specific options to initialize the hidden input.
*/
-var UIHiddenfield = UIElement.extend(/** @lends LuCI.ui.Hiddenfield.prototype */ {
- __init__: function(value, options) {
+const UIHiddenfield = UIElement.extend(/** @lends LuCI.ui.Hiddenfield.prototype */ {
+ __init__(value, options) {
this.value = value;
this.options = Object.assign({
},
/** @override */
- render: function() {
- var hiddenEl = E('input', {
+ render() {
+ const hiddenEl = E('input', {
'id': this.options.id,
'type': 'hidden',
'value': this.value
},
/** @private */
- bind: function(hiddenEl) {
+ bind(hiddenEl) {
this.node = hiddenEl;
dom.bindClassInstance(hiddenEl, this);
},
/** @override */
- getValue: function() {
+ getValue() {
return this.node.value;
},
/** @override */
- setValue: function(value) {
+ setValue(value) {
this.node.value = value;
}
});
* Object describing the widget specific options to initialize the file
* upload control.
*/
-var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
+const UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
/**
* In addition to the [AbstractElement.InitOptions]{@link LuCI.ui.AbstractElement.InitOptions}
* the following properties are recognized:
* Whether remote directories are browsable or not solely depends on the
* ACL setup for the current session.
*/
- __init__: function(value, options) {
+ __init__(value, options) {
this.value = value;
this.options = Object.assign({
browser: false,
},
/** @private */
- bind: function(browserEl) {
+ bind(browserEl) {
this.node = browserEl;
this.setUpdateEvents(browserEl, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
/** @override */
render: function() {
- var renderFileBrowser = L.resolveDefault(this.value != null ? fs.stat(this.value) : null).then(L.bind(function(stat) {
- var label;
+ const renderFileBrowser = L.resolveDefault(this.value != null ? fs.stat(this.value) : null).then(L.bind(function(stat) {
+ let label;
if (L.isObject(stat) && stat.type != 'directory')
this.stat = stat;
},
/** @private */
- truncatePath: function(path) {
+ truncatePath(path) {
if (path.length > 50)
- path = path.substring(0, 25) + '…' + path.substring(path.length - 25);
+ path = `${path.substring(0, 25)}…${path.substring(path.length - 25)}`;
return path;
},
/** @private */
- iconForType: function(type) {
+ iconForType(type) {
switch (type) {
case 'symlink':
return E('img', {
},
/** @private */
- canonicalizePath: function(path) {
+ canonicalizePath(path) {
return path.replace(/\/{2,}/, '/')
.replace(/\/\.(\/|$)/g, '/')
.replace(/[^\/]+\/\.\.(\/|$)/g, '/')
},
/** @private */
- splitPath: function(path) {
- var croot = this.canonicalizePath(this.options.root_directory || '/'),
- cpath = this.canonicalizePath(path || '/');
+ splitPath(path) {
+ const croot = this.canonicalizePath(this.options.root_directory ?? '/');
+ const 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(/\//);
+ const parts = cpath.substring(croot.length + 1).split(/\//);
parts.unshift(croot);
},
/** @private */
- 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();
+ handleUpload(path, list, ev) {
+ const form = ev.target.parentNode;
+ const fileinput = form.querySelector('input[type="file"]');
+ const nameinput = form.querySelector('input[type="text"]');
+ const 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];
+ const existing = list.filter(e => 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();
+ const data = new FormData();
data.append('sessionid', L.env.sessionid);
- data.append('filename', path + '/' + filename);
+ data.append('filename', `${path}/${filename}`);
data.append('filedata', fileinput.files[0]);
- return request.post(L.env.cgi_base + '/cgi-upload', data, {
- progress: L.bind(function(btn, ev) {
+ return request.post(`${L.env.cgi_base}/cgi-upload`, data, {
+ progress: L.bind((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();
+ const reply = res.json();
if (L.isObject(reply) && reply.failure)
alert(_('Upload request failed: %s').format(reply.message));
},
/** @private */
- handleDelete: function(path, fileStat, ev) {
- var parent = path.replace(/\/[^\/]+$/, '') || '/',
- name = path.replace(/^.+\//, ''),
- msg;
+ handleDelete(path, fileStat, ev) {
+ const parent = path.replace(/\/[^\/]+$/, '') ?? '/';
+ const name = path.replace(/^.+\//, '');
+ let msg;
ev.preventDefault();
msg = _('Do you really want to delete "%s" ?').format(name);
if (confirm(msg)) {
- var button = this.node.firstElementChild,
- hidden = this.node.lastElementChild;
+ const button = this.node.firstElementChild;
+ const hidden = this.node.lastElementChild;
if (path == hidden.value) {
dom.content(button, _('Select file…'));
return fs.remove(path).then(L.bind(function(parent, ev) {
return this.handleSelect(parent, null, ev);
- }, this, parent, ev)).catch(function(err) {
+ }, this, parent, ev)).catch(err => {
alert(_('Delete request failed: %s').format(err.message));
});
}
},
/** @private */
- renderUpload: function(path, list) {
+ renderUpload(path, list) {
if (!this.options.enable_upload)
return E([]);
'href': '#',
'class': 'btn cbi-button-positive',
'click': function(ev) {
- var uploadForm = ev.target.nextElementSibling,
- fileInput = uploadForm.querySelector('input[type="file"]');
+ const uploadForm = ev.target.nextElementSibling;
+ const fileInput = uploadForm.querySelector('input[type="file"]');
ev.target.style.display = 'none';
uploadForm.style.display = '';
'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');
+ const nameinput = ev.target.parentNode.querySelector('input[type="text"]');
+ const uploadbtn = ev.target.parentNode.querySelector('button.cbi-button-save');
nameinput.value = ev.target.value.replace(/^.+[\/\\]/, '');
uploadbtn.disabled = false;
},
/** @private */
- renderListing: function(container, path, list) {
- var breadcrumb = E('p'),
- rows = E('ul');
+ renderListing(container, path, list) {
+ const breadcrumb = E('p');
+ const rows = E('ul');
- list.sort(function(a, b) {
+ list.sort((a, b) => {
return L.naturalCompare(a.type == 'directory', b.type == 'directory') ||
- L.naturalCompare(a.name, b.name);
+ L.naturalCompare(a.name, b.name);
});
- for (var i = 0; i < list.length; i++) {
+ for (let 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);
+ const entrypath = this.canonicalizePath(`${path}/${list[i].name}`);
+ const selected = (entrypath == this.node.lastElementChild.value);
+ const mtime = new Date(list[i].mtime * 1000);
rows.appendChild(E('li', [
E('div', { 'class': 'name' }, [
if (!rows.firstElementChild)
rows.appendChild(E('em', _('No entries in this directory')));
- var dirs = this.splitPath(path),
- cur = '';
+ const dirs = this.splitPath(path);
+ let cur = '';
- for (var i = 0; i < dirs.length; i++) {
- cur = cur ? cur + '/' + dirs[i] : dirs[i];
+ for (let i = 0; i < dirs.length; i++) {
+ cur = cur ? `${cur}/${dirs[i]}` : dirs[i];
dom.append(breadcrumb, [
i ? ' » ' : '',
E('a', {
'href': '#',
- 'click': UI.prototype.createHandlerFn(this, 'handleSelect', cur || '/', null)
+ 'click': UI.prototype.createHandlerFn(this, 'handleSelect', cur ?? '/', null)
}, dirs[i] != '' ? '%h'.format(dirs[i]) : E('em', '(root)')),
]);
}
},
/** @private */
- handleCancel: function(ev) {
- var button = this.node.firstElementChild,
- browser = button.nextElementSibling;
+ handleCancel(ev) {
+ const button = this.node.firstElementChild;
+ const browser = button.nextElementSibling;
browser.classList.remove('open');
button.style.display = '';
},
/** @private */
- handleReset: function(ev) {
- var button = this.node.firstElementChild,
- hidden = this.node.lastElementChild;
+ handleReset(ev) {
+ const button = this.node.firstElementChild;
+ const hidden = this.node.lastElementChild;
hidden.value = '';
dom.content(button, _('Select file…'));
},
/** @private */
- handleDownload: function(path, fileStat, ev) {
+ handleDownload(path, fileStat, ev) {
fs.read_direct(path, 'blob').then(function (blob) {
- var url = window.URL.createObjectURL(blob);
- var a = document.createElement('a');
+ const url = window.URL.createObjectURL(blob);
+ let a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = fileStat.name;
},
/** @private */
- handleSelect: function(path, fileStat, ev) {
- var browser = dom.parent(ev.target, '.cbi-filebrowser'),
- ul = browser.querySelector('ul');
+ handleSelect(path, fileStat, ev) {
+ const browser = dom.parent(ev.target, '.cbi-filebrowser');
+ const ul = browser.querySelector('ul');
if (fileStat == null) {
dom.content(ul, E('em', { 'class': 'spinning' }, _('Loading directory contents…')));
L.resolveDefault(fs.list(path), []).then(L.bind(this.renderListing, this, browser, path));
}
else if (!this.options.browser) {
- var button = this.node.firstElementChild,
- hidden = this.node.lastElementChild;
+ const button = this.node.firstElementChild;
+ const hidden = this.node.lastElementChild;
path = this.canonicalizePath(path);
},
/** @private */
- handleFileBrowser: function(ev) {
- var button = ev.target,
- browser = button.nextElementSibling,
- path = this.stat ? this.stat.path.replace(/\/[^\/]+$/, '') : (this.options.initial_directory || this.options.root_directory);
+ handleFileBrowser(ev) {
+ const button = ev.target;
+ const browser = button.nextElementSibling;
+ let path = this.stat ? this.stat.path.replace(/\/[^\/]+$/, '') : (this.options.initial_directory ?? this.options.root_directory);
if (path.indexOf(this.options.root_directory) != 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) {
+ document.querySelectorAll('.cbi-filebrowser.open').forEach(browserEl => {
dom.findClassInstance(browserEl).handleCancel(ev);
});
},
/** @override */
- getValue: function() {
+ getValue() {
return this.node.lastElementChild.value;
},
/** @override */
- setValue: function(value) {
+ setValue(value) {
this.node.lastElementChild.value = value;
}
});
function scrubMenu(node) {
- var hasSatisfiedChild = false;
+ let hasSatisfiedChild = false;
if (L.isObject(node.children)) {
- for (var k in node.children) {
- var child = scrubMenu(node.children[k]);
+ for (const k in node.children) {
+ const child = scrubMenu(node.children[k]);
if (child.title && !child.firstchild_ineligible)
- hasSatisfiedChild = hasSatisfiedChild || child.satisfied;
+ hasSatisfiedChild ||= child.satisfied;
}
}
if (L.isObject(node.action) &&
- node.action.type == 'firstchild' &&
- hasSatisfiedChild == false)
+ node.action.type == 'firstchild' &&
+ hasSatisfiedChild == false)
node.satisfied = false;
return node;
-};
+}
/**
* Handle menu.
*
* Handles menus.
*/
-var UIMenu = baseclass.singleton(/** @lends LuCI.ui.menu.prototype */ {
+const UIMenu = baseclass.singleton(/** @lends LuCI.ui.menu.prototype */ {
/**
* @typedef {Object} MenuNode
* @memberof LuCI.ui.menu
* @returns {Promise<LuCI.ui.menu.MenuNode>}
* Returns a promise resolving to the root element of the menu tree.
*/
- load: function() {
+ load() {
if (this.menu == null)
this.menu = session.getLocalData('menu');
* Flush the internal menu cache to force loading a new structure on the
* next page load.
*/
- flushCache: function() {
+ flushCache() {
session.setLocalData('menu', null);
},
* @returns {LuCI.ui.menu.MenuNode[]}
* Returns an array of child menu nodes.
*/
- getChildren: function(node) {
- var children = [];
+ getChildren(node) {
+ const children = [];
if (node == null)
node = this.menu;
- for (var k in node.children) {
+ for (const k in node.children) {
if (!node.children.hasOwnProperty(k))
continue;
if (!node.children[k].hasOwnProperty('title'))
continue;
- var subnode = Object.assign(node.children[k], { name: k });
+ let subnode = Object.assign(node.children[k], { name: k });
if (L.isObject(subnode.action) && subnode.action.path != null &&
- (subnode.action.type == 'alias' || subnode.action.type == 'rewrite')) {
- var root = this.menu,
- path = subnode.action.path.split('/');
+ (subnode.action.type == 'alias' || subnode.action.type == 'rewrite')) {
+ let root = this.menu;
+ const path = subnode.action.path.split('/');
- for (var i = 0; root != null && i < path.length; i++)
+ for (let i = 0; root != null && i < path.length; i++)
root = L.isObject(root.children) ? root.children[path[i]] : null;
if (root)
children.push(subnode);
}
- return children.sort(function(a, b) {
- var wA = a.order || 1000,
- wB = b.order || 1000;
+ return children.sort((a, b) => {
+ const wA = a.order ?? 1000;
+ const wB = b.order ?? 1000;
if (wA != wB)
return wA - wB;
}
});
-var UITable = baseclass.extend(/** @lends LuCI.ui.table.prototype */ {
- __init__: function(captions, options, placeholder) {
+const UITable = baseclass.extend(/** @lends LuCI.ui.table.prototype */ {
+ __init__(captions, options, placeholder) {
if (!Array.isArray(captions)) {
this.initFromMarkup(captions);
return;
}
- var id = options.id || 'table%08x'.format(Math.random() * 0xffffffff);
+ const id = options.id ?? 'table%08x'.format(Math.random() * 0xffffffff);
- var table = E('table', { 'id': id, 'class': 'table' }, [
+ const table = E('table', { 'id': id, 'class': 'table' }, [
E('tr', { 'class': 'tr table-titles', 'click': UI.prototype.createHandlerFn(this, 'handleSort') })
]);
this.node = table
this.options = options;
- var sorting = this.getActiveSortState();
+ const sorting = this.getActiveSortState();
- for (var i = 0; i < captions.length; i++) {
+ for (let i = 0; i < captions.length; i++) {
if (captions[i] == null)
continue;
- var th = E('th', { 'class': 'th' }, [ captions[i] ]);
+ const th = E('th', { 'class': 'th' }, [ captions[i] ]);
if (typeof(options.captionClasses) == 'object')
DOMTokenList.prototype.add.apply(th.classList, L.toArray(options.captionClasses[i]));
}
if (placeholder) {
- var trow = table.appendChild(E('tr', { 'class': 'tr placeholder' })),
- td = trow.appendChild(E('td', { 'class': 'td' }, placeholder));
+ const trow = table.appendChild(E('tr', { 'class': 'tr placeholder' }));
+ const td = trow.appendChild(E('td', { 'class': 'td' }, placeholder));
if (typeof(captionClasses) == 'object')
DOMTokenList.prototype.add.apply(td.classList, L.toArray(captionClasses[0]));
DOMTokenList.prototype.add.apply(table.classList, L.toArray(options.classes));
},
- update: function(data, placeholder) {
- var placeholder = placeholder || this.options.placeholder || _('No data', 'empty table placeholder'),
- sorting = this.getActiveSortState();
+ update(data, placeholderText) {
+ const placeholder = placeholderText ?? this.options.placeholder ?? _('No data', 'empty table placeholder');
+ const sorting = this.getActiveSortState();
if (!Array.isArray(data))
return;
- this.data = data;
- this.placeholder = placeholder;
-
- var n = 0,
- rows = this.node.querySelectorAll('tr, .tr'),
- trows = [],
- headings = [].slice.call(this.node.firstElementChild.querySelectorAll('th, .th')),
- captionClasses = this.options.captionClasses,
- trTag = (rows[0] && rows[0].nodeName == 'DIV') ? 'div' : 'tr',
- tdTag = (headings[0] && headings[0].nodeName == 'DIV') ? 'div' : 'td';
+ const headings = [].slice.call(this.node.firstElementChild.querySelectorAll('th, .th'));
if (sorting) {
- var list = data.map(L.bind(function(row) {
+ const list = data.map(L.bind(function(row) {
return [ this.deriveSortKey(row[sorting[0]], sorting[0]), row ];
}, this));
- list.sort(function(a, b) {
+ list.sort((a, b) => {
return sorting[1]
? -L.naturalCompare(a[0], b[0])
: L.naturalCompare(a[0], b[0]);
data.length = 0;
- list.forEach(function(item) {
+ list.forEach(item => {
data.push(item[1]);
});
});
}
- data.forEach(function(row) {
+ this.data = data;
+ this.placeholder = placeholder;
+
+ let n = 0;
+ const rows = this.node.querySelectorAll('tr, .tr');
+ const trows = [];
+ const captionClasses = this.options.captionClasses;
+ const trTag = (rows[0] && rows[0].nodeName == 'DIV') ? 'div' : 'tr';
+ const tdTag = (headings[0] && headings[0].nodeName == 'DIV') ? 'div' : 'td';
+
+ data.forEach(row => {
trows[n] = E(trTag, { 'class': 'tr' });
- for (var i = 0; i < headings.length; i++) {
- var text = (headings[i].innerText || '').trim();
- var raw_val = Array.isArray(row[i]) ? row[i][0] : null;
- var disp_val = Array.isArray(row[i]) ? row[i][1] : row[i];
- var td = trows[n].appendChild(E(tdTag, {
+ for (let i = 0; i < headings.length; i++) {
+ const text = (headings[i].innerText ?? '').trim();
+ const raw_val = Array.isArray(row[i]) ? row[i][0] : null;
+ const disp_val = Array.isArray(row[i]) ? row[i][1] : row[i];
+ const td = trows[n].appendChild(E(tdTag, {
'class': 'td',
'data-title': (text !== '') ? text : null,
'data-value': raw_val
trows[n].classList.add('cbi-rowstyle-%d'.format((n++ % 2) ? 2 : 1));
});
- for (var i = 0; i < n; i++) {
+ for (let i = 0; i < n; i++) {
if (rows[i+1])
this.node.replaceChild(trows[i], rows[i+1]);
else
this.node.removeChild(rows[n]);
if (placeholder && this.node.firstElementChild === this.node.lastElementChild) {
- var trow = this.node.appendChild(E(trTag, { 'class': 'tr placeholder' })),
- td = trow.appendChild(E(tdTag, { 'class': 'td' }, placeholder));
+ const trow = this.node.appendChild(E(trTag, { 'class': 'tr placeholder' }));
+ const td = trow.appendChild(E(tdTag, { 'class': 'td' }, placeholder));
if (typeof(captionClasses) == 'object')
DOMTokenList.prototype.add.apply(td.classList, L.toArray(captionClasses[0]));
return this.node;
},
- render: function() {
+ render() {
return this.node;
},
/** @private */
- initFromMarkup: function(node) {
+ initFromMarkup(node) {
if (!dom.elem(node))
node = document.querySelector(node);
if (!node)
throw 'Invalid table selector';
- var options = {},
- headrow = node.querySelector('tr, .tr');
+ const options = {};
+ const headrow = node.querySelector('tr, .tr');
if (!headrow)
return;
options.id = node.id;
- options.classes = [].slice.call(node.classList).filter(function(c) { return c != 'table' });
+ options.classes = [].slice.call(node.classList).filter(c => c != 'table');
options.sortable = [];
options.captionClasses = [];
- headrow.querySelectorAll('th, .th').forEach(function(th, i) {
+ headrow.querySelectorAll('th, .th').forEach((th, i) => {
options.sortable[i] = !th.classList.contains('cbi-section-actions');
- options.captionClasses[i] = [].slice.call(th.classList).filter(function(c) { return c != 'th' });
+ options.captionClasses[i] = [].slice.call(th.classList).filter(c => c != 'th');
});
headrow.addEventListener('click', UI.prototype.createHandlerFn(this, 'handleSort'));
},
/** @private */
- deriveSortKey: function(value, index) {
- var opts = this.options || {},
- hint, m;
+ deriveSortKey(value, index) {
+ const opts = this.options ?? {};
+ let hint;
+ let m;
if (opts.sortable == true || opts.sortable == null)
hint = 'auto';
if (value.hasAttribute('data-value'))
value = value.getAttribute('data-value');
else
- value = (value.innerText || '').trim();
+ value = (value.innerText ?? '').trim();
}
- switch (hint || 'auto') {
+ switch (hint ?? 'auto') {
case true:
case 'auto':
m = /^([0-9a-fA-F:.]+)(?:\/([0-9a-fA-F:.]+))?$/.exec(value);
if (m) {
- var addr, mask;
+ let addr;
+ let mask;
addr = validation.parseIPv6(m[1]);
mask = m[2] ? validation.parseIPv6(m[2]) : null;
},
/** @private */
- getActiveSortState: function() {
+ getActiveSortState() {
if (this.sortState)
return this.sortState;
if (!this.options.id)
return null;
- var page = document.body.getAttribute('data-page'),
- key = page + '.' + this.options.id,
- state = session.getLocalData('tablesort');
+ const page = document.body.getAttribute('data-page');
+ const key = `${page}.${this.id}`;
+ const state = session.getLocalData('tablesort');
if (L.isObject(state) && Array.isArray(state[key]))
return state[key];
},
/** @private */
- setActiveSortState: function(index, descending) {
+ setActiveSortState(index, descending) {
this.sortState = [ index, descending ];
if (!this.options.id)
return;
- var page = document.body.getAttribute('data-page'),
- key = page + '.' + this.options.id,
- state = session.getLocalData('tablesort');
+ const page = document.body.getAttribute('data-page');
+ const key = `${page}.${this.options.id}`;
+ let state = session.getLocalData('tablesort');
if (!L.isObject(state))
state = {};
},
/** @private */
- handleSort: function(ev) {
+ handleSort(ev) {
if (!ev.target.matches('th[data-sortable-row]'))
return;
- var index, direction;
+ const th = ev.target;
+ const direction = (th.getAttribute('data-sort-direction') == 'asc');
+ let index = 0;
- this.node.firstElementChild.querySelectorAll('th, .th').forEach(function(th, i) {
- if (th === ev.target) {
+ this.node.firstElementChild.querySelectorAll('th').forEach((other_th, i) => {
+ if (other_th !== th)
+ other_th.removeAttribute('data-sort-direction');
+ else
index = i;
- direction = th.getAttribute('data-sort-direction') == 'asc';
- }
});
this.setActiveSortState(index, direction);
* To import the class in views, use `'require ui'`, to import it in
* external JavaScript, use `L.require("ui").then(...)`.
*/
-var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
- __init__: function() {
+const UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
+ __init__() {
modalDiv = document.body.appendChild(
dom.create('div', {
id: 'modal_overlay',
* @returns {Node}
* Returns a DOM Node representing the modal dialog element.
*/
- showModal: function(title, children /* , ... */) {
- var dlg = modalDiv.firstElementChild;
+ showModal(title, children, ...classes) {
+ const dlg = modalDiv.firstElementChild;
dlg.setAttribute('class', 'modal');
-
- for (var i = 2; i < arguments.length; i++)
- dlg.classList.add(arguments[i]);
+ dlg.classList.add(...classes);
dom.content(dlg, dom.create('h4', {}, title));
dom.append(dlg, children);
* will not invoke other class functions so it is suitable to be used as event
* handler as-is without the need to bind it first.
*/
- hideModal: function() {
+ hideModal() {
document.body.classList.remove('modal-overlay-active');
modalDiv.blur();
},
/** @private */
- cancelModal: function(ev) {
+ cancelModal(ev) {
if (ev.key == 'Escape') {
- var btn = modalDiv.querySelector('.right > button, .right > .btn, .button-row > .btn');
+ const btn = modalDiv.querySelector('.right > button, .right > .btn, .button-row > .btn');
if (btn)
btn.click();
},
/** @private */
- showTooltip: function(ev) {
- var target = findParent(ev.target, '[data-tooltip]');
+ showTooltip(ev) {
+ const target = findParent(ev.target, '[data-tooltip]');
if (!target)
return;
tooltipTimeout = null;
}
- var rect = target.getBoundingClientRect(),
- x = rect.left + window.pageXOffset,
- y = rect.top + rect.height + window.pageYOffset,
- above = false;
+ const rect = target.getBoundingClientRect();
+ const x = rect.left + window.pageXOffset;
+ let y = rect.top + rect.height + window.pageYOffset;
+ let above = false;
tooltipDiv.className = 'cbi-tooltip';
tooltipDiv.innerHTML = '▲ ';
if ((y + tooltipDiv.offsetHeight) > (window.innerHeight + window.pageYOffset))
above = true;
- var dropdown = target.querySelector('ul.dropdown[style]:first-child');
+ const dropdown = target.querySelector('ul.dropdown[style]:first-child');
if (dropdown && dropdown.style.top)
above = true;
if (above) {
y -= (tooltipDiv.offsetHeight + target.offsetHeight);
- tooltipDiv.firstChild.data = '▼ ' + tooltipDiv.firstChild.data.substr(2);
+ tooltipDiv.firstChild.data = `▼ ${tooltipDiv.firstChild.data.substr(2)}`;
}
- tooltipDiv.style.top = y + 'px';
- tooltipDiv.style.left = x + 'px';
+ tooltipDiv.style.top = `${y}px`;
+ tooltipDiv.style.left = `${x}px`;
tooltipDiv.style.opacity = 1;
tooltipDiv.dispatchEvent(new CustomEvent('tooltip-open', {
},
/** @private */
- hideTooltip: function(ev) {
+ hideTooltip(ev) {
if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv ||
tooltipDiv.contains(ev.target) || tooltipDiv.contains(ev.relatedTarget))
return;
}
tooltipDiv.style.opacity = 0;
- tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250);
+ tooltipTimeout = window.setTimeout(() => tooltipDiv.removeAttribute('style'), 250);
tooltipDiv.dispatchEvent(new CustomEvent('tooltip-close', { bubbles: true }));
},
* @returns {Node}
* Returns a DOM Node representing the notification banner element.
*/
- addNotification: function(title, children, timeout, ...classes) {
- var mc = document.querySelector('#maincontent') || document.body;
- var msg = E('div', {
+ addNotification(title, children, timeout, ...classes) {
+ const mc = document.querySelector('#maincontent') ?? document.body;
+ const msg = E('div', {
'class': 'alert-message fade-in',
'style': 'display:flex',
'transitionend': function(ev) {
- var node = ev.currentTarget;
+ const node = ev.currentTarget;
if (node.parentNode && node.classList.contains('fade-out'))
node.parentNode.removeChild(node);
}
dom.append(msg.firstElementChild, children);
- classes.forEach(cls => msg.classList.add(cls));
+ msg.classList.add(...classes);
mc.insertBefore(msg, mc.firstElementChild);
* Returns `true` when the indicator has been updated or `false` when no
* changes were made.
*/
- showIndicator: function(id, label, handler, style) {
+ showIndicator(id, label, handler, style) {
if (indicatorDiv == null) {
indicatorDiv = document.body.querySelector('#indicators');
return false;
}
- var handlerFn = (typeof(handler) == 'function') ? handler : null,
- indicatorElem = indicatorDiv.querySelector('span[data-indicator="%s"]'.format(id));
+ const handlerFn = (typeof(handler) == 'function') ? handler : null;
+ let indicatorElem = indicatorDiv.querySelector('span[data-indicator="%s"]'.format(id));
if (indicatorElem == null) {
- var beforeElem = null;
+ let beforeElem = null;
for (beforeElem = indicatorDiv.firstElementChild;
beforeElem != null;
* Returns `true` when the indicator has been removed or `false` when the
* requested indicator was not found.
*/
- hideIndicator: function(id) {
- var indicatorElem = indicatorDiv ? indicatorDiv.querySelector('span[data-indicator="%s"]'.format(id)) : null;
+ hideIndicator(id) {
+ const indicatorElem = indicatorDiv ? indicatorDiv.querySelector('span[data-indicator="%s"]'.format(id)) : null;
if (indicatorElem == null)
return false;
* @returns {Node}
* Returns the parent DOM node the formatted markup has been added to.
*/
- itemlist: function(node, items, separators) {
- var children = [];
+ itemlist(node, items, separators) {
+ const children = [];
if (!Array.isArray(separators))
- separators = [ separators || E('br') ];
+ separators = [ separators ?? E('br') ];
- for (var i = 0; i < items.length; i += 2) {
+ for (let i = 0; i < items.length; i += 2) {
if (items[i+1] !== null && items[i+1] !== undefined) {
- var sep = separators[(i/2) % separators.length],
- cld = [];
+ const sep = separators[(i/2) % separators.length];
+ const cld = [];
children.push(E('span', { class: 'nowrap' }, [
- items[i] ? E('strong', items[i] + ': ') : '',
+ items[i] ? E('strong', `${items[i]}: `) : '',
items[i+1]
]));
*/
tabs: baseclass.singleton(/* @lends LuCI.ui.tabs.prototype */ {
/** @private */
- init: function() {
- var groups = [], prevGroup = null, currGroup = null;
+ init() {
+ const groups = [];
+ let prevGroup = null;
+ let currGroup = null;
- document.querySelectorAll('[data-tab]').forEach(function(tab) {
- var parent = tab.parentNode;
+ document.querySelectorAll('[data-tab]').forEach(tab => {
+ const parent = tab.parentNode;
if (dom.matches(tab, 'li') && dom.matches(parent, 'ul.cbi-tabmenu'))
return;
groups[currGroup].push(tab);
});
- for (var i = 0; i < groups.length; i++)
+ for (let i = 0; i < groups.length; i++)
this.initTabGroup(groups[i]);
document.addEventListener('dependency-update', this.updateTabs.bind(this));
* of a `querySelectorAll()` call or the `.childNodes` property of a
* DOM node.
*/
- initTabGroup: function(panes) {
+ initTabGroup(panes) {
if (typeof(panes) != 'object' || !('length' in panes) || panes.length === 0)
return;
- var menu = E('ul', { 'class': 'cbi-tabmenu' }),
- group = panes[0].parentNode,
- groupId = +group.getAttribute('data-tab-group'),
- selected = null;
+ const menu = E('ul', { 'class': 'cbi-tabmenu' });
+ const group = panes[0].parentNode;
+ const groupId = +group.getAttribute('data-tab-group');
+ let 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'),
- active = pane.getAttribute('data-tab-active') === 'true';
+ for (let i = 0, pane; pane = panes[i]; i++) {
+ const name = pane.getAttribute('data-tab');
+ const title = pane.getAttribute('data-tab-title');
+ const active = pane.getAttribute('data-tab-active') === 'true';
menu.appendChild(E('li', {
'style': this.isEmptyPane(pane) ? 'display:none' : null,
selected = this.getActiveTabId(panes[0]);
if (selected < 0 || selected >= panes.length || this.isEmptyPane(panes[selected])) {
- for (var i = 0; i < panes.length; i++) {
+ for (let i = 0; i < panes.length; i++) {
if (!this.isEmptyPane(panes[i])) {
selected = i;
break;
this.setActiveTabId(panes[selected], selected);
}
- requestAnimationFrame(L.bind(function(pane) {
+ requestAnimationFrame(L.bind(pane => {
pane.dispatchEvent(new CustomEvent('cbi-tab-active', {
detail: { tab: pane.getAttribute('data-tab') }
}));
* @returns {boolean}
* Returns `true` if the pane is empty, else `false`.
*/
- isEmptyPane: function(pane) {
- return dom.isEmpty(pane, function(n) { return n.classList.contains('cbi-tab-descr') });
+ isEmptyPane(pane) {
+ return dom.isEmpty(pane, n => n.classList.contains('cbi-tab-descr'));
},
/** @private */
- getPathForPane: function(pane) {
- var path = [], node = null;
+ getPathForPane(pane) {
+ const path = [];
+ let node = null;
for (node = pane ? pane.parentNode : null;
node != null && node.hasAttribute != null;
},
/** @private */
- getActiveTabState: function() {
- var page = document.body.getAttribute('data-page'),
- state = session.getLocalData('tab');
+ getActiveTabState() {
+ const page = document.body.getAttribute('data-page');
+ const state = session.getLocalData('tab');
if (L.isObject(state) && state.page === page && L.isObject(state.paths))
return state;
},
/** @private */
- getActiveTabId: function(pane) {
- var path = this.getPathForPane(pane);
- return +this.getActiveTabState().paths[path] || 0;
+ getActiveTabId(pane) {
+ const path = this.getPathForPane(pane);
+ return +this.getActiveTabState().paths[path] ?? 0;
},
/** @private */
- setActiveTabId: function(pane, tabIndex) {
- var path = this.getPathForPane(pane),
- state = this.getActiveTabState();
+ setActiveTabId(pane, tabIndex) {
+ const path = this.getPathForPane(pane);
+ const state = this.getActiveTabState();
state.paths[path] = tabIndex;
},
/** @private */
- updateTabs: function(ev, root) {
- (root || document).querySelectorAll('[data-tab-title]').forEach(L.bind(function(pane) {
- var menu = pane.parentNode.previousElementSibling,
- tab = menu ? menu.querySelector('[data-tab="%s"]'.format(pane.getAttribute('data-tab'))) : null,
- n_errors = pane.querySelectorAll('.cbi-input-invalid').length;
+ updateTabs(ev, root) {
+ (root ?? document).querySelectorAll('[data-tab-title]').forEach(L.bind(function(pane) {
+ const menu = pane.parentNode.previousElementSibling;
+ const tab = menu ? menu.querySelector('[data-tab="%s"]'.format(pane.getAttribute('data-tab'))) : null;
+ const n_errors = pane.querySelectorAll('.cbi-input-invalid').length;
if (!menu || !tab)
return;
}
else if (tab.style.display === 'none') {
tab.style.display = '';
- requestAnimationFrame(function() { tab.classList.add('flash') });
+ requestAnimationFrame(() => tab.classList.add('flash'));
}
if (n_errors) {
},
/** @private */
- switchTab: function(ev) {
- var tab = ev.target.parentNode,
- name = tab.getAttribute('data-tab'),
- menu = tab.parentNode,
- group = menu.nextElementSibling,
- groupId = +group.getAttribute('data-tab-group'),
- index = 0;
+ switchTab(ev) {
+ const tab = ev.target.parentNode;
+ const name = tab.getAttribute('data-tab');
+ const menu = tab.parentNode;
+ const group = menu.nextElementSibling;
+ const groupId = +group.getAttribute('data-tab-group');
+ let index = 0;
ev.preventDefault();
if (!tab.classList.contains('cbi-tab-disabled'))
return;
- menu.querySelectorAll('[data-tab]').forEach(function(tab) {
+ menu.querySelectorAll('[data-tab]').forEach(tab => {
tab.classList.remove('cbi-tab');
tab.classList.remove('cbi-tab-disabled');
tab.classList.add(
tab.getAttribute('data-tab') === name ? 'cbi-tab' : 'cbi-tab-disabled');
});
- group.childNodes.forEach(function(pane) {
+ group.childNodes.forEach(pane => {
if (dom.matches(pane, '[data-tab]')) {
if (pane.getAttribute('data-tab') === name) {
pane.setAttribute('data-tab-active', 'true');
* or rejecting with an error in case the upload failed or has been
* cancelled by the user.
*/
- uploadFile: function(path, progressStatusNode) {
- return new Promise(function(resolveFn, rejectFn) {
+ uploadFile(path, progressStatusNode) {
+ return new Promise((resolveFn, rejectFn) => {
UI.prototype.showModal(_('Uploading file…'), [
E('p', _('Please select the file to upload.')),
- E('div', { 'class': 'button-row' }, [
- E('button', {
- 'class': 'btn cbi-button',
- 'click': function() {
- UI.prototype.hideModal();
- rejectFn(new Error(_('Upload has been cancelled')));
- }
- }, [ _('Cancel') ]),
- E('input', {
- type: 'file',
- style: 'display:none',
- change: function(ev) {
- var modal = 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;
-
- 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 cbi-button',
- 'click': function(ev) {
- ev.target.previousElementSibling.click();
- }
- }, [ _('Browse…') ]),
- E('button', {
- 'class': 'btn cbi-button-action important',
- 'disabled': true,
- 'click': function(ev) {
- var input = 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' }));
-
- UI.prototype.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;
-
- 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();
-
+ E('div', { 'style': 'display:flex' }, [
+ E('div', { 'class': 'left', 'style': 'flex:1' }, [
+ E('input', {
+ type: 'file',
+ style: 'display:none',
+ change(ev) {
+ const modal = dom.parent(ev.target, '.modal');
+ const body = modal.querySelector('p');
+ const upload = modal.querySelector('.cbi-button-action.important');
+ const file = ev.currentTarget.files[0];
+
+ if (file == null)
+ return;
+
+ 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 cbi-button',
+ 'click': function(ev) {
+ ev.target.previousElementSibling.click();
+ }
+ }, [ _('Browse…') ])
+ ]),
+ E('div', { 'class': 'right', 'style': 'flex:1' }, [
+ E('button', {
+ 'class': 'btn',
+ 'click': function() {
UI.prototype.hideModal();
-
- if (L.isObject(reply) && reply.failure) {
- UI.prototype.addNotification(null, E('p', _('Upload request failed: %s').format(reply.message)));
- rejectFn(new Error(reply.failure));
- }
- else {
- reply.name = filename;
- resolveFn(reply);
- }
- }, function(err) {
- UI.prototype.hideModal();
- rejectFn(err);
- });
- }
- }, [ _('Upload') ])
+ rejectFn(new Error(_('Upload has been cancelled')));
+ }
+ }, [ _('Cancel') ]),
+ ' ',
+ E('button', {
+ 'class': 'btn cbi-button-action important',
+ 'disabled': true,
+ 'click': function(ev) {
+ const input = dom.parent(ev.target, '.modal').querySelector('input[type="file"]');
+
+ if (!input.files[0])
+ return;
+
+ const progress = E('div', { 'class': 'cbi-progressbar', 'title': '0%' }, E('div', { 'style': 'width:0' }));
+
+ UI.prototype.showModal(_('Uploading file…'), [ progress ]);
+
+ const data = new FormData();
+
+ data.append('sessionid', rpc.getSessionID());
+ data.append('filename', path);
+ data.append('filedata', input.files[0]);
+
+ const filename = input.files[0].name;
+
+ request.post(`${L.env.cgi_base}/cgi-upload`, data, {
+ timeout: 0,
+ progress(pev) {
+ const 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(res => {
+ const reply = res.json();
+
+ UI.prototype.hideModal();
+
+ if (L.isObject(reply) && reply.failure) {
+ UI.prototype.addNotification(null, E('p', _('Upload request failed: %s').format(reply.message)));
+ rejectFn(new Error(reply.failure));
+ }
+ else {
+ reply.name = filename;
+ resolveFn(reply);
+ }
+ }, err => {
+ UI.prototype.hideModal();
+ rejectFn(err);
+ });
+ }
+ }, [ _('Upload') ])
+ ])
])
]);
});
* reachable or rejecting with an `error` event in case it is not reachable
* or rejecting with `null` when the connectivity check timed out.
*/
- pingDevice: function(proto, ipaddr) {
- var target = '%s://%s%s?%s'.format(proto || 'http', ipaddr || window.location.host, L.resource('icons/loading.gif'), Math.random());
+ pingDevice(proto, ipaddr) {
+ const 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();
+ return new Promise((resolveFn, rejectFn) => {
+ const img = new Image();
img.onload = resolveFn;
img.onerror = rejectFn;
* If omitted, the current value of `window.location.host` is used by
* default.
*/
- awaitReconnect: function(/* ... */) {
- var ipaddrs = arguments.length ? arguments : [ window.location.host ];
+ awaitReconnect(...hosts) {
+ const ipaddrs = hosts.length ? hosts : [ window.location.host ];
window.setTimeout(L.bind(function() {
poll.add(L.bind(function() {
- var tasks = [], reachable = false;
+ const tasks = [];
+ let reachable = false;
- for (var i = 0; i < 2; i++)
- for (var j = 0; j < ipaddrs.length; j++)
+ for (let i = 0; i < 2; i++)
+ for (let 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() {}));
+ .then(ev => { reachable = ev.target.src.replace(/^(https?:\/\/[^\/]+).*$/, '$1/') }, () => {}));
- return Promise.all(tasks).then(function() {
+ return Promise.all(tasks).then(() => {
if (reachable) {
poll.stop();
window.location = reachable;
* `changes` property of the class instance value.
*/
changes: baseclass.singleton(/* @lends LuCI.ui.changes.prototype */ {
- init: function() {
+ init() {
if (!L.env.sessionid)
return;
* @param {number} n
* The number of changes to indicate.
*/
- setIndicator: function(n) {
+ setIndicator(n) {
if (n > 0) {
UI.prototype.showIndicator('uci-changes',
'%s: %d'.format(_('Unsaved Changes'), n),
* @param {Object<string, Array<LuCI.uci.ChangeRecord>>} changes
* The UCI changeset to count.
*/
- renderChangeIndicator: function(changes) {
- var n_changes = 0;
+ renderChangeIndicator(changes) {
+ let n_changes = 0;
- for (var config in changes)
+ for (const config in changes)
if (changes.hasOwnProperty(config))
n_changes += changes[config].length;
* @instance
* @memberof LuCI.ui.changes
*/
- displayChanges: function() {
- var list = E('div', { 'class': 'uci-change-list' }),
- dlg = UI.prototype.showModal(_('Configuration') + ' / ' + _('Changes'), [
- E('div', { 'class': 'cbi-section' }, [
- E('strong', _('Legend:')),
- E('div', { 'class': 'uci-change-legend' }, [
- E('div', { 'class': 'uci-change-legend-label' }, [
- E('ins', ' '), ' ', _('Section added') ]),
- E('div', { 'class': 'uci-change-legend-label' }, [
- E('del', ' '), ' ', _('Section removed') ]),
- E('div', { 'class': 'uci-change-legend-label' }, [
- E('var', {}, E('ins', ' ')), ' ', _('Option changed') ]),
- E('div', { 'class': 'uci-change-legend-label' }, [
- E('var', {}, E('del', ' ')), ' ', _('Option removed') ])]),
- E('br'),
- list,
- ]),
- E('div', { 'class': 'button-row' }, [
+ displayChanges() {
+ const list = E('div', { 'class': 'uci-change-list' });
+
+ const dlg = UI.prototype.showModal(`${_('Configuration')} / ${_('Changes')}`, [
+ E('div', { 'class': 'cbi-section' }, [
+ E('strong', _('Legend:')),
+ E('div', { 'class': 'uci-change-legend' }, [
+ E('div', { 'class': 'uci-change-legend-label' }, [
+ E('ins', ' '), ' ', _('Section added') ]),
+ E('div', { 'class': 'uci-change-legend-label' }, [
+ E('del', ' '), ' ', _('Section removed') ]),
+ E('div', { 'class': 'uci-change-legend-label' }, [
+ E('var', {}, E('ins', ' ')), ' ', _('Option changed') ]),
+ E('div', { 'class': 'uci-change-legend-label' }, [
+ E('var', {}, E('del', ' ')), ' ', _('Option removed') ])]),
+ E('br'), list,
+ E('div', { 'class': 'right' }, [ //button-row?
E('button', {
'class': 'btn cbi-button',
'click': UI.prototype.hideModal
E('button', {
'class': 'btn cbi-button cbi-button-reset',
'click': L.bind(this.revert, this)
- }, [ _('Revert') ])
- ])
- ]);
+ }, [ _('Revert') ])])])
+ ]);
- for (var config in this.changes) {
+ for (const config in this.changes) {
if (!this.changes.hasOwnProperty(config))
continue;
list.appendChild(E('h5', '# /etc/config/%s'.format(config)));
- for (var i = 0, added = null; i < this.changes[config].length; i++) {
- var chg = this.changes[config][i],
- tpl = this.changeTemplates['%s-%d'.format(chg[0], chg.length)];
+ for (let i = 0, added = null; i < this.changes[config].length; i++) {
+ const chg = this.changes[config][i];
+ const tpl = this.changeTemplates['%s-%d'.format(chg[0], chg.length)];
- list.appendChild(E(tpl.replace(/%([01234])/g, function(m0, m1) {
+ list.appendChild(E(tpl.replace(/%([01234])/g, (m0, m1) => {
switch (+m1) {
case 0:
return config;
case 2:
if (added != null && chg[1] == added[0])
- return '@' + added[1] + '[-1]';
+ return `@${added[1]}[-1]`;
else
return chg[1];
},
/** @private */
- displayStatus: function(type, content) {
+ displayStatus(type, content) {
if (type) {
- var message = UI.prototype.showModal('', '');
+ const message = UI.prototype.showModal('', '');
message.classList.add('alert-message');
DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
},
/** @private */
- checkConnectivityAffected: function() {
+ checkConnectivityAffected() {
return L.resolveDefault(fs.exec_direct('/usr/libexec/luci-peeraddr', null, 'json')).then(L.bind(function(info) {
if (L.isObject(info) && Array.isArray(info.inbound_interfaces)) {
- for (var i = 0; i < info.inbound_interfaces.length; i++) {
- var iif = info.inbound_interfaces[i];
+ for (let i = 0; i < info.inbound_interfaces.length; i++) {
+ const iif = info.inbound_interfaces[i];
- for (var j = 0; this.changes && this.changes.network && j < this.changes.network.length; j++) {
- var chg = this.changes.network[j];
+ for (let j = 0; this.changes && this.changes.network && j < this.changes.network.length; j++) {
+ const chg = this.changes.network[j];
if (chg[0] == 'set' && chg[1] == iif &&
((chg[2] == 'disabled' && chg[3] == '1') || chg[2] == 'proto' || chg[2] == 'ipaddr' || chg[2] == 'netmask'))
},
/** @private */
- rollback: function(checked) {
+ rollback(checked) {
if (checked) {
this.displayStatus('warning spinning',
E('p', _('Failed to confirm apply within %ds, waiting for rollback…')
.format(L.env.apply_rollback)));
- var call = function(r) {
+ const call = (r, data, duration) => {
if (r.status === 204) {
UI.prototype.changes.displayStatus('warning', [
E('h4', _('Configuration changes have been rolled back!')),
return;
}
- var delay = isNaN(r.duration) ? 0 : Math.max(1000 - r.duration, 0);
- window.setTimeout(function() {
+ const delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
+ window.setTimeout(() => {
request.request(L.url('admin/uci/confirm'), {
method: 'post',
timeout: L.env.apply_timeout * 1000,
},
/** @private */
- confirm: function(checked, deadline, override_token) {
- var tt;
- var ts = Date.now();
+ confirm(checked, deadline, override_token) {
+ let tt;
+ let ts = Date.now();
this.displayStatus('notice');
if (override_token)
this.confirm_auth = { token: override_token };
- var call = function(r) {
+ const call = (r, data, duration) => {
if (Date.now() >= deadline) {
window.clearTimeout(tt);
UI.prototype.changes.rollback(checked);
E('p', _('Configuration changes applied.')));
window.clearTimeout(tt);
- window.setTimeout(function() {
+ window.setTimeout(() => {
//UI.prototype.changes.displayStatus(false);
window.location = window.location.href.split('#')[0];
}, L.env.apply_display * 1000);
return;
}
- var delay = isNaN(r.duration) ? 0 : Math.max(1000 - r.duration, 0);
- window.setTimeout(function() {
+ const delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
+ window.setTimeout(() => {
request.request(L.url('admin/uci/confirm'), {
method: 'post',
timeout: L.env.apply_timeout * 1000,
}, delay);
};
- var tick = function() {
- var now = Date.now();
+ const tick = () => {
+ const now = Date.now();
UI.prototype.changes.displayStatus('notice spinning',
E('p', _('Applying configuration changes… %ds')
* will begin to roll back the changes in order to restore the previous
* settings.
*/
- apply: function(checked) {
+ apply(checked) {
this.displayStatus('notice spinning',
E('p', _('Starting configuration apply…')));
- (new Promise(function(resolveFn, rejectFn) {
+ (new Promise((resolveFn, rejectFn) => {
if (!checked)
return resolveFn(false);
- UI.prototype.changes.checkConnectivityAffected().then(function(affected) {
+ UI.prototype.changes.checkConnectivityAffected().then(affected => {
if (!affected)
return resolveFn(true);
])
]);
});
- })).then(function(checked) {
+ })).then(checked => {
request.request(L.url('admin/uci', checked ? 'apply_rollback' : 'apply_unchecked'), {
method: 'post',
query: { sid: L.env.sessionid, token: L.env.token }
- }).then(function(r) {
+ }).then(r => {
if (r.status === (checked ? 200 : 204)) {
- var tok = null; try { tok = r.json(); } catch(e) {}
+ let tok = null; try { tok = r.json(); } catch(e) {}
if (checked && tok !== null && typeof(tok) === 'object' && typeof(tok.token) === 'string')
UI.prototype.changes.confirm_auth = tok;
UI.prototype.changes.displayStatus('notice',
E('p', _('There are no changes to apply')));
- window.setTimeout(function() {
+ window.setTimeout(() => {
UI.prototype.changes.displayStatus(false);
}, L.env.apply_display * 1000);
}
else {
UI.prototype.changes.displayStatus('warning',
E('p', _('Apply request failed with status <code>%h</code>')
- .format(r.responseText || r.statusText || r.status)));
+ .format(r.responseText ?? r.statusText ?? r.status)));
- window.setTimeout(function() {
+ window.setTimeout(() => {
UI.prototype.changes.displayStatus(false);
}, L.env.apply_display * 1000);
}
* @instance
* @memberof LuCI.ui.changes
*/
- revert: function() {
+ revert() {
this.displayStatus('notice spinning',
E('p', _('Reverting configuration…')));
request.request(L.url('admin/uci/revert'), {
method: 'post',
query: { sid: L.env.sessionid, token: L.env.token }
- }).then(function(r) {
+ }).then(r => {
if (r.status === 200) {
document.dispatchEvent(new CustomEvent('uci-reverted'));
UI.prototype.changes.displayStatus('notice',
E('p', _('Changes have been reverted.')));
- window.setTimeout(function() {
+ window.setTimeout(() => {
//UI.prototype.changes.displayStatus(false);
window.location = window.location.href.split('#')[0];
}, L.env.apply_display * 1000);
else {
UI.prototype.changes.displayStatus('warning',
E('p', _('Revert request failed with status <code>%h</code>')
- .format(r.statusText || r.status)));
+ .format(r.statusText ?? r.status)));
- window.setTimeout(function() {
+ window.setTimeout(() => {
UI.prototype.changes.displayStatus(false);
}, L.env.apply_display * 1000);
}
*
* @see LuCI.validation
*/
- addValidator: function(field, type, optional, vfunc /*, ... */) {
+ addValidator(field, type, optional, vfunc, ...events) {
if (type == null)
return;
- var events = this.varargs(arguments, 3);
if (events.length == 0)
events.push('blur', 'keyup');
try {
- var cbiValidator = validation.create(field, type, optional, vfunc),
- validatorFn = cbiValidator.validate.bind(cbiValidator);
+ const cbiValidator = validation.create(field, type, optional, vfunc);
+ const validatorFn = cbiValidator.validate.bind(cbiValidator);
- for (var i = 0; i < events.length; i++)
+ for (let i = 0; i < events.length; i++)
field.addEventListener(events[i], validatorFn);
validatorFn();
* a string which could not be found in `ctx` or if `ctx[fn]` is not a
* valid function value.
*/
- createHandlerFn: function(ctx, fn /*, ... */) {
+ createHandlerFn(ctx, fn, ...args) {
if (typeof(fn) == 'string')
fn = ctx[fn];
if (typeof(fn) != 'function')
return null;
- var arg_offset = arguments.length - 2;
-
- return Function.prototype.bind.apply(function() {
- var t = arguments[arg_offset].currentTarget;
+ return L.bind(function() {
+ const t = arguments[args.length].currentTarget;
t.classList.add('spinning');
t.disabled = true;
if (t.blur)
t.blur();
- Promise.resolve(fn.apply(ctx, arguments)).finally(function() {
+ Promise.resolve(fn.apply(ctx, arguments)).finally(() => {
t.classList.remove('spinning');
t.disabled = false;
});
- }, this.varargs(arguments, 2, ctx));
+ }, ctx, ...args);
},
/**
* @returns {Promise<LuCI.view>}
* Returns a promise resolving to the loaded view instance.
*/
- instantiateView: function(path) {
- var className = 'view.%s'.format(path.replace(/\//g, '.'));
+ instantiateView(path) {
+ const className = 'view.%s'.format(path.replace(/\//g, '.'));
- return L.require(className).then(function(view) {
+ return L.require(className).then(view => {
if (!(view instanceof View))
throw new TypeError('Loaded class %s is not a descendant of View'.format(className));
return view;
- }).catch(function(err) {
+ }).catch(err => {
dom.content(document.querySelector('#view'), null);
L.error(err);
});