'use strict';
+'require rpc';
+'require uci';
+'require validation';
+'require fs';
var modalDiv = null,
tooltipDiv = null,
},
isValid: function() {
- return true;
+ return (this.validState !== false);
+ },
+
+ triggerValidation: function() {
+ if (typeof(this.vfunc) != 'function')
+ return false;
+
+ var wasValid = this.isValid();
+
+ this.vfunc();
+
+ return (wasValid != this.isValid());
},
registerEvents: function(targetNode, synevent, events) {
},
setUpdateEvents: function(targetNode /*, ... */) {
- this.registerEvents(targetNode, 'widget-update', this.varargs(arguments, 1));
+ var datatype = this.options.datatype,
+ optional = this.options.hasOwnProperty('optional') ? this.options.optional : true,
+ validate = this.options.validate,
+ events = this.varargs(arguments, 1);
+
+ this.registerEvents(targetNode, 'widget-update', events);
+
+ if (!datatype && !validate)
+ return;
+
+ this.vfunc = L.ui.addValidator.apply(L.ui, [
+ targetNode, datatype || 'string',
+ optional, validate
+ ].concat(events));
+
+ this.node.addEventListener('validation-success', L.bind(function(ev) {
+ this.validState = true;
+ }, this));
+
+ this.node.addEventListener('validation-failure', L.bind(function(ev) {
+ this.validState = false;
+ }, this));
},
setChangeEvents: function(targetNode /*, ... */) {
+ var tag_changed = L.bind(function(ev) { this.setAttribute('data-changed', true) }, this.node);
+
+ for (var i = 1; i < arguments.length; i++)
+ targetNode.addEventListener(arguments[i], tag_changed);
+
this.registerEvents(targetNode, 'widget-change', this.varargs(arguments, 1));
}
});
+var UITextfield = UIElement.extend({
+ __init__: function(value, options) {
+ this.value = value;
+ this.options = Object.assign({
+ optional: true,
+ password: false
+ }, options);
+ },
+
+ render: function() {
+ var frameEl = E('div', { 'id': this.options.id });
+
+ if (this.options.password) {
+ frameEl.classList.add('nowrap');
+ frameEl.appendChild(E('input', {
+ 'type': 'password',
+ 'style': 'position:absolute; left:-100000px',
+ 'aria-hidden': true,
+ 'tabindex': -1,
+ 'name': this.options.name ? 'password.%s'.format(this.options.name) : null
+ }));
+ }
+
+ frameEl.appendChild(E('input', {
+ 'id': this.options.id ? 'widget.' + this.options.id : null,
+ 'name': this.options.name,
+ 'type': this.options.password ? 'password' : 'text',
+ 'class': this.options.password ? 'cbi-input-password' : 'cbi-input-text',
+ 'readonly': this.options.readonly ? '' : null,
+ 'maxlength': this.options.maxlength,
+ 'placeholder': this.options.placeholder,
+ 'value': this.value,
+ }));
+
+ if (this.options.password)
+ frameEl.appendChild(E('button', {
+ 'class': 'cbi-button cbi-button-neutral',
+ 'title': _('Reveal/hide password'),
+ 'aria-label': _('Reveal/hide password'),
+ 'click': function(ev) {
+ var e = this.previousElementSibling;
+ e.type = (e.type === 'password') ? 'text' : 'password';
+ ev.preventDefault();
+ }
+ }, '∗'));
+
+ return this.bind(frameEl);
+ },
+
+ bind: function(frameEl) {
+ var inputEl = frameEl.childNodes[+!!this.options.password];
+
+ this.node = frameEl;
+
+ this.setUpdateEvents(inputEl, 'keyup', 'blur');
+ this.setChangeEvents(inputEl, 'change');
+
+ L.dom.bindClassInstance(frameEl, this);
+
+ return frameEl;
+ },
+
+ getValue: function() {
+ var inputEl = this.node.childNodes[+!!this.options.password];
+ return inputEl.value;
+ },
+
+ setValue: function(value) {
+ var inputEl = this.node.childNodes[+!!this.options.password];
+ inputEl.value = value;
+ }
+});
+
+var UITextarea = UIElement.extend({
+ __init__: function(value, options) {
+ this.value = value;
+ this.options = Object.assign({
+ optional: true,
+ wrap: false,
+ cols: null,
+ rows: null
+ }, options);
+ },
+
+ render: function() {
+ var frameEl = E('div', { 'id': this.options.id }),
+ value = (this.value != null) ? String(this.value) : '';
+
+ frameEl.appendChild(E('textarea', {
+ 'id': this.options.id ? 'widget.' + this.options.id : null,
+ 'name': this.options.name,
+ 'class': 'cbi-input-textarea',
+ 'readonly': this.options.readonly ? '' : null,
+ 'placeholder': this.options.placeholder,
+ 'style': !this.options.cols ? 'width:100%' : null,
+ 'cols': this.options.cols,
+ 'rows': this.options.rows,
+ 'wrap': this.options.wrap ? '' : null
+ }, [ value ]));
+
+ if (this.options.monospace)
+ frameEl.firstElementChild.style.fontFamily = 'monospace';
+
+ return this.bind(frameEl);
+ },
+
+ bind: function(frameEl) {
+ var inputEl = frameEl.firstElementChild;
+
+ this.node = frameEl;
+
+ this.setUpdateEvents(inputEl, 'keyup', 'blur');
+ this.setChangeEvents(inputEl, 'change');
+
+ L.dom.bindClassInstance(frameEl, this);
+
+ return frameEl;
+ },
+
+ getValue: function() {
+ return this.node.firstElementChild.value;
+ },
+
+ setValue: function(value) {
+ this.node.firstElementChild.value = value;
+ }
+});
+
+var UICheckbox = UIElement.extend({
+ __init__: function(value, options) {
+ this.value = value;
+ this.options = Object.assign({
+ value_enabled: '1',
+ value_disabled: '0'
+ }, options);
+ },
+
+ render: function() {
+ var frameEl = E('div', {
+ 'id': this.options.id,
+ 'class': 'cbi-checkbox'
+ });
+
+ if (this.options.hiddenname)
+ frameEl.appendChild(E('input', {
+ 'type': 'hidden',
+ 'name': this.options.hiddenname,
+ 'value': 1
+ }));
+
+ frameEl.appendChild(E('input', {
+ 'id': this.options.id ? 'widget.' + this.options.id : null,
+ 'name': this.options.name,
+ 'type': 'checkbox',
+ 'value': this.options.value_enabled,
+ 'checked': (this.value == this.options.value_enabled) ? '' : null
+ }));
+
+ return this.bind(frameEl);
+ },
+
+ bind: function(frameEl) {
+ this.node = frameEl;
+
+ this.setUpdateEvents(frameEl.lastElementChild, 'click', 'blur');
+ this.setChangeEvents(frameEl.lastElementChild, 'change');
+
+ L.dom.bindClassInstance(frameEl, this);
+
+ return frameEl;
+ },
+
+ isChecked: function() {
+ return this.node.lastElementChild.checked;
+ },
+
+ getValue: function() {
+ return this.isChecked()
+ ? this.options.value_enabled
+ : this.options.value_disabled;
+ },
+
+ setValue: function(value) {
+ this.node.lastElementChild.checked = (value == this.options.value_enabled);
+ }
+});
+
+var UISelect = UIElement.extend({
+ __init__: function(value, choices, options) {
+ if (!L.isObject(choices))
+ choices = {};
+
+ if (!Array.isArray(value))
+ value = (value != null && value != '') ? [ value ] : [];
+
+ if (!options.multiple && value.length > 1)
+ value.length = 1;
+
+ this.values = value;
+ this.choices = choices;
+ this.options = Object.assign({
+ multiple: false,
+ widget: 'select',
+ orientation: 'horizontal'
+ }, options);
+
+ if (this.choices.hasOwnProperty(''))
+ this.options.optional = true;
+ },
+
+ render: function() {
+ var frameEl = E('div', { 'id': this.options.id }),
+ keys = Object.keys(this.choices);
+
+ if (this.options.sort === true)
+ keys.sort();
+ else if (Array.isArray(this.options.sort))
+ keys = this.options.sort;
+
+ if (this.options.widget == 'select') {
+ frameEl.appendChild(E('select', {
+ 'id': this.options.id ? 'widget.' + this.options.id : null,
+ 'name': this.options.name,
+ 'size': this.options.size,
+ 'class': 'cbi-input-select',
+ 'multiple': this.options.multiple ? '' : null
+ }));
+
+ if (this.options.optional)
+ frameEl.lastChild.appendChild(E('option', {
+ 'value': '',
+ 'selected': (this.values.length == 0 || this.values[0] == '') ? '' : null
+ }, [ this.choices[''] || this.options.placeholder || _('-- Please choose --') ]));
+
+ for (var 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] ]));
+ }
+ }
+ else {
+ var brEl = (this.options.orientation === 'horizontal') ? document.createTextNode(' ') : E('br');
+
+ for (var i = 0; i < keys.length; i++) {
+ frameEl.appendChild(E('label', {}, [
+ E('input', {
+ 'id': this.options.id ? 'widget.' + this.options.id : null,
+ '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],
+ 'checked': (this.values.indexOf(keys[i]) > -1) ? '' : null
+ }),
+ this.choices[keys[i]] || keys[i]
+ ]));
+
+ if (i + 1 == this.options.size)
+ frameEl.appendChild(brEl);
+ }
+ }
+
+ return this.bind(frameEl);
+ },
+
+ bind: function(frameEl) {
+ this.node = frameEl;
+
+ if (this.options.widget == 'select') {
+ this.setUpdateEvents(frameEl.firstChild, 'change', 'click', 'blur');
+ this.setChangeEvents(frameEl.firstChild, 'change');
+ }
+ else {
+ var radioEls = frameEl.querySelectorAll('input[type="radio"]');
+ for (var i = 0; i < radioEls.length; i++) {
+ this.setUpdateEvents(radioEls[i], 'change', 'click', 'blur');
+ this.setChangeEvents(radioEls[i], 'change', 'click', 'blur');
+ }
+ }
+
+ L.dom.bindClassInstance(frameEl, this);
+
+ return frameEl;
+ },
+
+ getValue: function() {
+ if (this.options.widget == 'select')
+ return this.node.firstChild.value;
+
+ var radioEls = frameEl.querySelectorAll('input[type="radio"]');
+ for (var i = 0; i < radioEls.length; i++)
+ if (radioEls[i].checked)
+ return radioEls[i].value;
+
+ return null;
+ },
+
+ setValue: function(value) {
+ if (this.options.widget == 'select') {
+ if (value == null)
+ value = '';
+
+ for (var 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++)
+ radioEls[i].checked = (radioEls[i].value == value);
+ }
+});
+
var UIDropdown = UIElement.extend({
__init__: function(value, choices, options) {
if (typeof(choices) != 'object')
choices = {};
if (!Array.isArray(value))
- this.values = (value != null) ? [ value ] : [];
+ this.values = (value != null && value != '') ? [ value ] : [];
else
this.values = value;
this.choices = choices;
this.options = Object.assign({
sort: true,
- multi: Array.isArray(value),
+ multiple: Array.isArray(value),
optional: true,
select_placeholder: _('-- Please choose --'),
custom_placeholder: _('-- custom --'),
display_items: 3,
- dropdown_items: 5,
+ dropdown_items: -1,
create: false,
create_query: '.create-item-input',
create_template: 'script[type="item-template"]'
var sb = E('div', {
'id': this.options.id,
'class': 'cbi-dropdown',
- 'multiple': this.options.multi ? '' : null,
+ 'multiple': this.options.multiple ? '' : null,
'optional': this.options.optional ? '' : null,
}, E('ul'));
if (!this.choices.hasOwnProperty(this.values[i]))
keys.push(this.values[i]);
- for (var i = 0; i < keys.length; i++)
+ for (var i = 0; i < keys.length; i++) {
+ var label = this.choices[keys[i]];
+
+ if (L.dom.elem(label))
+ label = label.cloneNode(true);
+
sb.lastElementChild.appendChild(E('li', {
'data-value': keys[i],
'selected': (this.values.indexOf(keys[i]) > -1) ? '' : null
- }, this.choices[keys[i]] || keys[i]));
+ }, [ label || keys[i] ]));
+ }
if (this.options.create) {
var createEl = E('input', {
'type': 'text',
'class': 'create-item-input',
+ 'readonly': this.options.readonly ? '' : null,
+ 'maxlength': this.options.maxlength,
'placeholder': this.options.custom_placeholder || this.options.placeholder
});
- if (this.options.datatype)
- L.ui.addValidator(createEl, this.options.datatype, true, 'blur', 'keyup');
+ if (this.options.datatype || this.options.validate)
+ L.ui.addValidator(createEl, this.options.datatype || 'string',
+ true, this.options.validate, 'blur', 'keyup');
sb.lastElementChild.appendChild(E('li', { 'data-value': '-' }, createEl));
}
+ if (this.options.create_markup)
+ sb.appendChild(E('script', { type: 'item-template' },
+ this.options.create_markup));
+
return this.bind(sb);
},
bind: function(sb) {
var o = this.options;
- o.multi = sb.hasAttribute('multiple');
+ 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);
ndisplay = this.options.display_items,
n = 0;
- if (this.options.multi) {
+ if (this.options.multiple) {
var items = ul.querySelectorAll('li');
for (var i = 0; i < items.length; i++) {
else
sb.removeAttribute('empty');
- more.innerHTML = (ndisplay == this.options.display_items)
- ? (this.options.select_placeholder || this.options.placeholder) : '···';
+ L.dom.content(more, (ndisplay == this.options.display_items)
+ ? (this.options.select_placeholder || this.options.placeholder) : '···');
sb.addEventListener('click', this.handleClick.bind(this));
ul.style.top = ul.style.bottom = '';
window.requestAnimationFrame(function() {
- var height = items * li[Math.max(0, li.length - 2)].offsetHeight;
+ var itemHeight = li[Math.max(0, li.length - 2)].getBoundingClientRect().height,
+ fullHeight = 0,
+ spaceAbove = rect.top,
+ spaceBelow = window.innerHeight - rect.height - rect.top;
+
+ for (var 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';
+ }
+ else if (fullHeight <= spaceAbove) {
+ 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';
+ }
+ else {
+ ul.style.bottom = rect.height + 'px';
+ ul.style.maxHeight = (spaceAbove - (spaceAbove % itemHeight)) + 'px';
+ }
ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
- ul.style[((rect.top + rect.height + height) > window.innerHeight) ? 'bottom' : 'top'] = rect.height + 'px';
- ul.style.maxHeight = height + 'px';
});
}
if (li.hasAttribute('unselectable'))
return;
- if (this.options.multi) {
+ if (this.options.multiple) {
var cbox = li.querySelector('input[type="checkbox"]'),
items = li.parentNode.querySelectorAll('li'),
label = sb.querySelector('ul.preview'),
else
sb.removeAttribute('empty');
- more.innerHTML = (ndisplay === this.options.display_items)
- ? (this.options.select_placeholder || this.options.placeholder) : '···';
+ L.dom.content(more, (ndisplay === this.options.display_items)
+ ? (this.options.select_placeholder || this.options.placeholder) : '···');
}
else {
var sel = li.parentNode.querySelector('[selected]');
element: sb
};
- if (this.options.multi)
+ if (this.options.multiple)
detail.values = values;
else
detail.value = values.length ? values[0] : null;
for (var value in values) {
this.createItems(sb, value);
- if (!this.options.multi)
+ if (!this.options.multiple)
break;
}
}
- if (this.options.multi) {
+ 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');
}
},
+ createChoiceElement: function(sb, value, label) {
+ var tpl = sb.querySelector(this.options.create_template),
+ markup = null;
+
+ if (tpl)
+ markup = (tpl.textContent || tpl.innerHTML || tpl.firstChild.data).replace(/^<!--|-->$/, '').trim();
+ else
+ markup = '<li data-value="{{value}}"><span data-label-placeholder="true" /></li>';
+
+ var new_item = E(markup.replace(/{{value}}/g, '%h'.format(value))),
+ placeholder = new_item.querySelector('[data-label-placeholder]');
+
+ if (placeholder) {
+ var content = E('span', {}, label || this.choices[value] || [ value ]);
+
+ while (content.firstChild)
+ placeholder.parentNode.insertBefore(content.firstChild, placeholder);
+
+ placeholder.parentNode.removeChild(placeholder);
+ }
+
+ if (this.options.multiple)
+ this.transformItem(sb, new_item);
+
+ return new_item;
+ },
+
createItems: function(sb, value) {
var sbox = this,
val = (value || '').trim(),
ul = sb.querySelector('ul');
- if (!sbox.options.multi)
+ if (!sbox.options.multiple)
val = val.length ? [ val ] : [];
else
val = val.length ? val.split(/\s+/) : [];
});
if (!new_item) {
- var markup,
- tpl = sb.querySelector(sbox.options.create_template);
-
- if (tpl)
- markup = (tpl.textContent || tpl.innerHTML || tpl.firstChild.data).replace(/^<!--|-->$/, '').trim();
- else
- markup = '<li data-value="{{value}}">{{value}}</li>';
-
- new_item = E(markup.replace(/{{value}}/g, item));
+ new_item = sbox.createChoiceElement(sb, item);
- if (sbox.options.multi) {
- sbox.transformItem(sb, new_item);
- }
- else {
+ if (!sbox.options.multiple) {
var old = ul.querySelector('li[created]');
if (old)
ul.removeChild(old);
});
},
+ clearChoices: function(reset_value) {
+ var ul = this.node.querySelector('ul'),
+ lis = ul ? ul.querySelectorAll('li[data-value]') : [],
+ len = lis.length - (this.options.create ? 1 : 0),
+ val = reset_value ? null : this.getValue();
+
+ for (var i = 0; i < len; i++) {
+ var lival = lis[i].getAttribute('data-value');
+ if (val == null ||
+ (!this.options.multiple && val != lival) ||
+ (this.options.multiple && val.indexOf(lival) == -1))
+ ul.removeChild(lis[i]);
+ }
+
+ if (reset_value)
+ this.setValues(this.node, {});
+ },
+
+ addChoices: function(values, labels) {
+ var sb = this.node,
+ ul = sb.querySelector('ul'),
+ lis = ul ? ul.querySelectorAll('li[data-value]') : [];
+
+ if (!Array.isArray(values))
+ values = L.toArray(values);
+
+ if (!L.isObject(labels))
+ labels = {};
+
+ for (var i = 0; i < values.length; i++) {
+ var found = false;
+
+ for (var j = 0; j < lis.length; j++) {
+ if (lis[j].getAttribute('data-value') === values[i]) {
+ found = true;
+ break;
+ }
+ }
+
+ if (found)
+ continue;
+
+ ul.insertBefore(
+ this.createChoiceElement(sb, values[i], labels[values[i]]),
+ ul.lastElementChild);
+ }
+ },
+
closeAllDropdowns: function() {
document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
this.toggleItem(sb, li);
else if (li && li.parentNode.classList.contains('preview'))
this.closeDropdown(sb);
+ else if (matchesElem(ev.target, 'span.open, span.more'))
+ this.closeDropdown(sb);
}
ev.preventDefault();
},
setValue: function(values) {
- if (this.options.multi) {
+ if (this.options.multiple) {
if (!Array.isArray(values))
- values = (values != null) ? [ values ] : [];
+ values = (values != null && values != '') ? [ values ] : [];
var v = {};
for (var i = 0; i < h.length; i++)
v.push(h[i].value);
- return this.options.multi ? v : v[0];
+ return this.options.multiple ? v : v[0];
}
});
this.super('__init__', [ value, choices, Object.assign({
select_placeholder: _('-- Please choose --'),
custom_placeholder: _('-- custom --'),
- dropdown_items: 5
+ dropdown_items: -1,
+ sort: true
}, options, {
- sort: true,
- multi: false,
+ multiple: false,
create: true,
optional: true
}) ]);
}
});
+var UIComboButton = UIDropdown.extend({
+ __init__: function(value, choices, options) {
+ this.super('__init__', [ value, choices, Object.assign({
+ sort: true
+ }, options, {
+ multiple: false,
+ create: false,
+ optional: false
+ }) ]);
+ },
+
+ render: function(/* ... */) {
+ var node = UIDropdown.prototype.render.apply(this, arguments),
+ val = this.getValue();
+
+ if (L.isObject(this.options.classes) && this.options.classes.hasOwnProperty(val))
+ node.setAttribute('class', 'cbi-dropdown ' + this.options.classes[val]);
+
+ return node;
+ },
+
+ handleClick: function(ev) {
+ var sb = ev.currentTarget,
+ t = ev.target;
+
+ if (sb.hasAttribute('open') || L.dom.matches(t, '.cbi-dropdown > span.open'))
+ return UIDropdown.prototype.handleClick.apply(this, arguments);
+
+ if (this.options.click)
+ return this.options.click.call(sb, ev, this.getValue());
+ },
+
+ toggleItem: function(sb /*, ... */) {
+ var rv = UIDropdown.prototype.toggleItem.apply(this, arguments),
+ val = this.getValue();
+
+ if (L.isObject(this.options.classes) && this.options.classes.hasOwnProperty(val))
+ sb.setAttribute('class', 'cbi-dropdown ' + this.options.classes[val]);
+ else
+ sb.setAttribute('class', 'cbi-dropdown');
+
+ return rv;
+ }
+});
+
var UIDynamicList = UIElement.extend({
__init__: function(values, choices, options) {
if (!Array.isArray(values))
- values = (values != null) ? [ values ] : [];
+ values = (values != null && values != '') ? [ values ] : [];
if (typeof(choices) != 'object')
choices = null;
this.values = values;
this.choices = choices;
this.options = Object.assign({}, options, {
- multi: false,
+ multiple: false,
optional: true
});
},
}, E('div', { 'class': 'add-item' }));
if (this.choices) {
+ if (this.options.placeholder != null)
+ this.options.select_placeholder = this.options.placeholder;
+
var cbox = new UICombobox(null, this.choices, this.options);
+
dl.lastElementChild.appendChild(cbox.render());
}
else {
var 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(inputEl);
dl.lastElementChild.appendChild(E('div', { 'class': 'cbi-button cbi-button-add' }, '+'));
- L.ui.addValidator(inputEl, this.options.datatype, true, 'blue', 'keyup');
+ if (this.options.datatype || this.options.validate)
+ L.ui.addValidator(inputEl, this.options.datatype || 'string',
+ true, this.options.validate, 'blur', 'keyup');
}
- for (var i = 0; i < this.values.length; i++)
- this.addItem(dl, this.values[i],
- this.choices ? this.choices[this.values[i]] : null);
+ for (var i = 0; i < this.values.length; i++) {
+ var label = this.choices ? this.choices[this.values[i]] : null;
+
+ if (L.dom.elem(label))
+ label = label.cloneNode(true);
+
+ this.addItem(dl, this.values[i], label);
+ }
return this.bind(dl);
},
addItem: function(dl, value, text, flash) {
var exists = false,
new_item = E('div', { 'class': flash ? 'item flash' : 'item', 'tabindex': 0 }, [
- E('span', {}, text || value),
+ E('span', {}, [ text || value ]),
E('input', {
'type': 'hidden',
'name': this.options.name,
'value': value })]);
- dl.querySelectorAll('.item, .add-item').forEach(function(item) {
+ dl.querySelectorAll('.item').forEach(function(item) {
if (exists)
return;
if (hidden && hidden.value === value)
exists = true;
- else if (!hidden || hidden.value >= value)
- exists = !!item.parentNode.insertBefore(new_item, item);
});
+ if (!exists) {
+ var ai = dl.querySelector('.add-item');
+ ai.parentNode.insertBefore(new_item, ai);
+ }
+
dl.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
bubbles: true,
detail: {
sbVal.element.setAttribute('dynlistcustom', '');
}
- this.addItem(dl, sbVal.value, sbVal.text, true);
+ var label = sbVal.text;
+
+ if (sbVal.element) {
+ label = E([]);
+
+ for (var i = 0; i < sbVal.element.childNodes.length; i++)
+ label.appendChild(sbVal.element.childNodes[i].cloneNode(true));
+ }
+
+ this.addItem(dl, sbVal.value, label, true);
},
handleKeydown: function(ev) {
getValue: function() {
var items = this.node.querySelectorAll('.item > input[type="hidden"]'),
+ input = this.node.querySelector('.add-item > input[type="text"]'),
v = [];
for (var 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.push(input.value);
+
return v;
},
setValue: function(values) {
if (!Array.isArray(values))
- values = (values != null) ? [ values ] : [];
+ values = (values != null && values != '') ? [ values ] : [];
var items = this.node.querySelectorAll('.item');
for (var i = 0; i < values.length; i++)
this.addItem(this.node, values[i],
this.choices ? this.choices[values[i]] : null);
+ },
+
+ addChoices: function(values, labels) {
+ var dl = this.node.lastElementChild.firstElementChild;
+ L.dom.callClassMethod(dl, 'addChoices', values, labels);
+ },
+
+ clearChoices: function() {
+ var dl = this.node.lastElementChild.firstElementChild;
+ L.dom.callClassMethod(dl, 'clearChoices');
+ }
+});
+
+var UIHiddenfield = UIElement.extend({
+ __init__: function(value, options) {
+ this.value = value;
+ this.options = Object.assign({
+
+ }, options);
+ },
+
+ render: function() {
+ var hiddenEl = E('input', {
+ 'id': this.options.id,
+ 'type': 'hidden',
+ 'value': this.value
+ });
+
+ return this.bind(hiddenEl);
+ },
+
+ bind: function(hiddenEl) {
+ this.node = hiddenEl;
+
+ L.dom.bindClassInstance(hiddenEl, this);
+
+ return hiddenEl;
+ },
+
+ getValue: function() {
+ return this.node.value;
+ },
+
+ setValue: function(value) {
+ this.node.value = value;
+ }
+});
+
+var UIFileUpload = UIElement.extend({
+ __init__: function(value, options) {
+ this.value = value;
+ this.options = Object.assign({
+ show_hidden: false,
+ enable_upload: true,
+ enable_remove: true,
+ root_directory: '/etc/luci-uploads'
+ }, options);
+ },
+
+ bind: function(browserEl) {
+ this.node = browserEl;
+
+ this.setUpdateEvents(browserEl, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
+ this.setChangeEvents(browserEl, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
+
+ L.dom.bindClassInstance(browserEl, this);
+
+ return browserEl;
+ },
+
+ render: function() {
+ return L.resolveDefault(this.value != null ? fs.stat(this.value) : null).then(L.bind(function(stat) {
+ var label;
+
+ if (L.isObject(stat) && stat.type != 'directory')
+ this.stat = stat;
+
+ if (this.stat != null)
+ label = [ this.iconForType(this.stat.type), ' %s (%1000mB)'.format(this.truncatePath(this.stat.path), this.stat.size) ];
+ else if (this.value != null)
+ label = [ this.iconForType('file'), ' %s (%s)'.format(this.truncatePath(this.value), _('File not accessible')) ];
+ else
+ label = [ _('Select file…') ];
+
+ return this.bind(E('div', { 'id': this.options.id }, [
+ E('button', {
+ 'class': 'btn',
+ 'click': L.ui.createHandlerFn(this, 'handleFileBrowser')
+ }, label),
+ E('div', {
+ 'class': 'cbi-filebrowser'
+ }),
+ E('input', {
+ 'type': 'hidden',
+ 'name': this.options.name,
+ 'value': this.value
+ })
+ ]));
+ }, this));
+ },
+
+ truncatePath: function(path) {
+ if (path.length > 50)
+ path = path.substring(0, 25) + '…' + path.substring(path.length - 25);
+
+ return path;
+ },
+
+ iconForType: function(type) {
+ switch (type) {
+ case 'symlink':
+ return E('img', {
+ 'src': L.resource('cbi/link.gif'),
+ 'title': _('Symbolic link'),
+ 'class': 'middle'
+ });
+
+ case 'directory':
+ return E('img', {
+ 'src': L.resource('cbi/folder.gif'),
+ 'title': _('Directory'),
+ 'class': 'middle'
+ });
+
+ default:
+ return E('img', {
+ 'src': L.resource('cbi/file.gif'),
+ 'title': _('File'),
+ 'class': 'middle'
+ });
+ }
+ },
+
+ canonicalizePath: function(path) {
+ return path.replace(/\/{2,}/, '/')
+ .replace(/\/\.(\/|$)/g, '/')
+ .replace(/[^\/]+\/\.\.(\/|$)/g, '/')
+ .replace(/\/$/, '');
+ },
+
+ splitPath: function(path) {
+ var croot = this.canonicalizePath(this.options.root_directory || '/'),
+ cpath = this.canonicalizePath(path || '/');
+
+ if (cpath.length <= croot.length)
+ return [ croot ];
+
+ if (cpath.charAt(croot.length) != '/')
+ return [ croot ];
+
+ var parts = cpath.substring(croot.length + 1).split(/\//);
+
+ parts.unshift(croot);
+
+ return parts;
+ },
+
+ handleUpload: function(path, list, ev) {
+ var form = ev.target.parentNode,
+ fileinput = form.querySelector('input[type="file"]'),
+ nameinput = form.querySelector('input[type="text"]'),
+ filename = (nameinput.value != null ? nameinput.value : '').trim();
+
+ ev.preventDefault();
+
+ if (filename == '' || filename.match(/\//) || fileinput.files[0] == null)
+ return;
+
+ var existing = list.filter(function(e) { return e.name == filename })[0];
+
+ if (existing != null && existing.type == 'directory')
+ return alert(_('A directory with the same name already exists.'));
+ else if (existing != null && !confirm(_('Overwrite existing file "%s" ?').format(filename)))
+ return;
+
+ var data = new FormData();
+
+ data.append('sessionid', L.env.sessionid);
+ data.append('filename', path + '/' + filename);
+ data.append('filedata', fileinput.files[0]);
+
+ return L.Request.post(L.env.cgi_base + '/cgi-upload', data, {
+ progress: L.bind(function(btn, ev) {
+ btn.firstChild.data = '%.2f%%'.format((ev.loaded / ev.total) * 100);
+ }, this, ev.target)
+ }).then(L.bind(function(path, ev, res) {
+ var reply = res.json();
+
+ if (L.isObject(reply) && reply.failure)
+ alert(_('Upload request failed: %s').format(reply.message));
+
+ return this.handleSelect(path, null, ev);
+ }, this, path, ev));
+ },
+
+ handleDelete: function(path, fileStat, ev) {
+ var parent = path.replace(/\/[^\/]+$/, '') || '/',
+ name = path.replace(/^.+\//, ''),
+ msg;
+
+ ev.preventDefault();
+
+ if (fileStat.type == 'directory')
+ msg = _('Do you really want to recursively delete the directory "%s" ?').format(name);
+ else
+ msg = _('Do you really want to delete "%s" ?').format(name);
+
+ if (confirm(msg)) {
+ var button = this.node.firstElementChild,
+ hidden = this.node.lastElementChild;
+
+ if (path == hidden.value) {
+ L.dom.content(button, _('Select file…'));
+ hidden.value = '';
+ }
+
+ return fs.remove(path).then(L.bind(function(parent, ev) {
+ return this.handleSelect(parent, null, ev);
+ }, this, parent, ev)).catch(function(err) {
+ alert(_('Delete request failed: %s').format(err.message));
+ });
+ }
+ },
+
+ renderUpload: function(path, list) {
+ if (!this.options.enable_upload)
+ return E([]);
+
+ return E([
+ E('a', {
+ 'href': '#',
+ 'class': 'btn cbi-button-positive',
+ 'click': function(ev) {
+ var uploadForm = ev.target.nextElementSibling,
+ fileInput = uploadForm.querySelector('input[type="file"]');
+
+ ev.target.style.display = 'none';
+ uploadForm.style.display = '';
+ fileInput.click();
+ }
+ }, _('Upload file…')),
+ E('div', { 'class': 'upload', 'style': 'display:none' }, [
+ E('input', {
+ 'type': 'file',
+ 'style': 'display:none',
+ 'change': function(ev) {
+ var nameinput = ev.target.parentNode.querySelector('input[type="text"]'),
+ uploadbtn = ev.target.parentNode.querySelector('button.cbi-button-save');
+
+ nameinput.value = ev.target.value.replace(/^.+[\/\\]/, '');
+ uploadbtn.disabled = false;
+ }
+ }),
+ E('button', {
+ 'class': 'btn',
+ 'click': function(ev) {
+ ev.preventDefault();
+ ev.target.previousElementSibling.click();
+ }
+ }, [ _('Browse…') ]),
+ E('div', {}, E('input', { 'type': 'text', 'placeholder': _('Filename') })),
+ E('button', {
+ 'class': 'btn cbi-button-save',
+ 'click': L.ui.createHandlerFn(this, 'handleUpload', path, list),
+ 'disabled': true
+ }, [ _('Upload file') ])
+ ])
+ ]);
+ },
+
+ renderListing: function(container, path, list) {
+ var breadcrumb = E('p'),
+ rows = E('ul');
+
+ list.sort(function(a, b) {
+ var isDirA = (a.type == 'directory'),
+ isDirB = (b.type == 'directory');
+
+ if (isDirA != isDirB)
+ return isDirA < isDirB;
+
+ return a.name > b.name;
+ });
+
+ for (var i = 0; i < list.length; i++) {
+ if (!this.options.show_hidden && list[i].name.charAt(0) == '.')
+ continue;
+
+ var entrypath = this.canonicalizePath(path + '/' + list[i].name),
+ selected = (entrypath == this.node.lastElementChild.value),
+ mtime = new Date(list[i].mtime * 1000);
+
+ rows.appendChild(E('li', [
+ E('div', { 'class': 'name' }, [
+ this.iconForType(list[i].type),
+ ' ',
+ E('a', {
+ 'href': '#',
+ 'style': selected ? 'font-weight:bold' : null,
+ 'click': L.ui.createHandlerFn(this, 'handleSelect',
+ entrypath, list[i].type != 'directory' ? list[i] : null)
+ }, '%h'.format(list[i].name))
+ ]),
+ E('div', { 'class': 'mtime hide-xs' }, [
+ ' %04d-%02d-%02d %02d:%02d:%02d '.format(
+ mtime.getFullYear(),
+ mtime.getMonth() + 1,
+ mtime.getDate(),
+ mtime.getHours(),
+ mtime.getMinutes(),
+ mtime.getSeconds())
+ ]),
+ E('div', [
+ selected ? E('button', {
+ 'class': 'btn',
+ 'click': L.ui.createHandlerFn(this, 'handleReset')
+ }, [ _('Deselect') ]) : '',
+ this.options.enable_remove ? E('button', {
+ 'class': 'btn cbi-button-negative',
+ 'click': L.ui.createHandlerFn(this, 'handleDelete', entrypath, list[i])
+ }, [ _('Delete') ]) : ''
+ ])
+ ]));
+ }
+
+ if (!rows.firstElementChild)
+ rows.appendChild(E('em', _('No entries in this directory')));
+
+ var dirs = this.splitPath(path),
+ cur = '';
+
+ for (var i = 0; i < dirs.length; i++) {
+ cur = cur ? cur + '/' + dirs[i] : dirs[i];
+ L.dom.append(breadcrumb, [
+ i ? ' » ' : '',
+ E('a', {
+ 'href': '#',
+ 'click': L.ui.createHandlerFn(this, 'handleSelect', cur || '/', null)
+ }, dirs[i] != '' ? '%h'.format(dirs[i]) : E('em', '(root)')),
+ ]);
+ }
+
+ L.dom.content(container, [
+ breadcrumb,
+ rows,
+ E('div', { 'class': 'right' }, [
+ this.renderUpload(path, list),
+ E('a', {
+ 'href': '#',
+ 'class': 'btn',
+ 'click': L.ui.createHandlerFn(this, 'handleCancel')
+ }, _('Cancel'))
+ ]),
+ ]);
+ },
+
+ handleCancel: function(ev) {
+ var button = this.node.firstElementChild,
+ browser = button.nextElementSibling;
+
+ browser.classList.remove('open');
+ button.style.display = '';
+
+ this.node.dispatchEvent(new CustomEvent('cbi-fileupload-cancel', {}));
+ },
+
+ handleReset: function(ev) {
+ var button = this.node.firstElementChild,
+ hidden = this.node.lastElementChild;
+
+ hidden.value = '';
+ L.dom.content(button, _('Select file…'));
+
+ this.handleCancel(ev);
+ },
+
+ handleSelect: function(path, fileStat, ev) {
+ var browser = L.dom.parent(ev.target, '.cbi-filebrowser'),
+ ul = browser.querySelector('ul');
+
+ if (fileStat == null) {
+ L.dom.content(ul, E('em', { 'class': 'spinning' }, _('Loading directory contents…')));
+ L.resolveDefault(fs.list(path), []).then(L.bind(this.renderListing, this, browser, path));
+ }
+ else {
+ var button = this.node.firstElementChild,
+ hidden = this.node.lastElementChild;
+
+ path = this.canonicalizePath(path);
+
+ L.dom.content(button, [
+ this.iconForType(fileStat.type),
+ ' %s (%1000mB)'.format(this.truncatePath(path), fileStat.size)
+ ]);
+
+ browser.classList.remove('open');
+ button.style.display = '';
+ hidden.value = path;
+
+ this.stat = Object.assign({ path: path }, fileStat);
+ this.node.dispatchEvent(new CustomEvent('cbi-fileupload-select', { detail: this.stat }));
+ }
+ },
+
+ handleFileBrowser: function(ev) {
+ var button = ev.target,
+ browser = button.nextElementSibling,
+ path = this.stat ? this.stat.path.replace(/\/[^\/]+$/, '') : this.options.root_directory;
+
+ if (this.options.root_directory.indexOf(path) != 0)
+ path = this.options.root_directory;
+
+ ev.preventDefault();
+
+ return L.resolveDefault(fs.list(path), []).then(L.bind(function(button, browser, path, list) {
+ document.querySelectorAll('.cbi-filebrowser.open').forEach(function(browserEl) {
+ L.dom.findClassInstance(browserEl).handleCancel(ev);
+ });
+
+ button.style.display = 'none';
+ browser.classList.add('open');
+
+ return this.renderListing(browser, path, list);
+ }, this, button, browser, path));
+ },
+
+ getValue: function() {
+ return this.node.lastElementChild.value;
+ },
+
+ setValue: function(value) {
+ this.node.lastElementChild.value = value;
}
});
document.addEventListener('blur', this.hideTooltip.bind(this), true);
document.addEventListener('luci-loaded', this.tabs.init.bind(this.tabs));
+ document.addEventListener('luci-loaded', this.changes.init.bind(this.changes));
+ document.addEventListener('uci-loaded', this.changes.init.bind(this.changes));
},
/* Modal dialog */
- showModal: function(title, children) {
+ showModal: function(title, children /* , ... */) {
var dlg = modalDiv.firstElementChild;
dlg.setAttribute('class', 'modal');
+ for (var i = 2; i < arguments.length; i++)
+ dlg.classList.add(arguments[i]);
+
L.dom.content(dlg, L.dom.create('h4', {}, title));
L.dom.append(dlg, children);
tooltipDiv.dispatchEvent(new CustomEvent('tooltip-close', { bubbles: true }));
},
+ addNotification: function(title, children /*, ... */) {
+ var mc = document.querySelector('#maincontent') || document.body;
+ var msg = E('div', {
+ 'class': 'alert-message fade-in',
+ 'style': 'display:flex',
+ 'transitionend': function(ev) {
+ var node = ev.currentTarget;
+ if (node.parentNode && node.classList.contains('fade-out'))
+ node.parentNode.removeChild(node);
+ }
+ }, [
+ E('div', { 'style': 'flex:10' }),
+ E('div', { 'style': 'flex:1 1 auto; display:flex' }, [
+ E('button', {
+ 'class': 'btn',
+ 'style': 'margin-left:auto; margin-top:auto',
+ 'click': function(ev) {
+ L.dom.parent(ev.target, '.alert-message').classList.add('fade-out');
+ },
+
+ }, [ _('Dismiss') ])
+ ])
+ ]);
+
+ if (title != null)
+ L.dom.append(msg.firstElementChild, E('h4', {}, title));
+
+ L.dom.append(msg.firstElementChild, children);
+
+ for (var i = 2; i < arguments.length; i++)
+ msg.classList.add(arguments[i]);
+
+ mc.insertBefore(msg, mc.firstElementChild);
+
+ return msg;
+ },
+
/* Widget helper */
itemlist: function(node, items, separators) {
var children = [];
document.querySelectorAll('[data-tab]').forEach(function(tab) {
var parent = tab.parentNode;
+ if (L.dom.matches(tab, 'li') && L.dom.matches(parent, 'ul.cbi-tabmenu'))
+ return;
+
if (!parent.hasAttribute('data-tab-group'))
parent.setAttribute('data-tab-group', groups.length);
document.addEventListener('dependency-update', this.updateTabs.bind(this));
this.updateTabs();
-
- if (!groups.length)
- this.setActiveTabId(-1, -1);
},
initTabGroup: function(panes) {
groupId = +group.getAttribute('data-tab-group'),
selected = null;
+ if (group.getAttribute('data-initialized') === 'true')
+ return;
+
for (var i = 0, pane; pane = panes[i]; i++) {
var name = pane.getAttribute('data-tab'),
title = pane.getAttribute('data-tab-title'),
active = pane.getAttribute('data-tab-active') === 'true';
menu.appendChild(E('li', {
+ 'style': this.isEmptyPane(pane) ? 'display:none' : null,
'class': active ? 'cbi-tab' : 'cbi-tab-disabled',
'data-tab': name
}, E('a', {
}
group.parentNode.insertBefore(menu, group);
+ group.setAttribute('data-initialized', true);
if (selected === null) {
- selected = this.getActiveTabId(groupId);
-
- if (selected < 0 || selected >= panes.length)
- selected = 0;
+ selected = this.getActiveTabId(panes[0]);
+
+ if (selected < 0 || selected >= panes.length || this.isEmptyPane(panes[selected])) {
+ for (var i = 0; i < panes.length; i++) {
+ if (!this.isEmptyPane(panes[i])) {
+ selected = i;
+ break;
+ }
+ }
+ }
menu.childNodes[selected].classList.add('cbi-tab');
menu.childNodes[selected].classList.remove('cbi-tab-disabled');
panes[selected].setAttribute('data-tab-active', 'true');
- this.setActiveTabId(groupId, selected);
+ this.setActiveTabId(panes[selected], selected);
+ }
+
+ this.updateTabs(group);
+ },
+
+ isEmptyPane: function(pane) {
+ return L.dom.isEmpty(pane, function(n) { return n.classList.contains('cbi-tab-descr') });
+ },
+
+ getPathForPane: function(pane) {
+ var path = [], node = null;
+
+ for (node = pane ? pane.parentNode : null;
+ node != null && node.hasAttribute != null;
+ node = node.parentNode)
+ {
+ if (node.hasAttribute('data-tab'))
+ path.unshift(node.getAttribute('data-tab'));
+ else if (node.hasAttribute('data-section-id'))
+ path.unshift(node.getAttribute('data-section-id'));
}
+
+ return path.join('/');
},
getActiveTabState: function() {
try {
var val = JSON.parse(window.sessionStorage.getItem('tab'));
- if (val.page === page && Array.isArray(val.groups))
+ if (val.page === page && L.isObject(val.paths))
return val;
}
catch(e) {}
window.sessionStorage.removeItem('tab');
- return { page: page, groups: [] };
+ return { page: page, paths: {} };
},
- getActiveTabId: function(groupId) {
- return +this.getActiveTabState().groups[groupId] || 0;
+ getActiveTabId: function(pane) {
+ var path = this.getPathForPane(pane);
+ return +this.getActiveTabState().paths[path] || 0;
},
- setActiveTabId: function(groupId, tabIndex) {
+ setActiveTabId: function(pane, tabIndex) {
+ var path = this.getPathForPane(pane);
+
try {
var state = this.getActiveTabState();
- state.groups[groupId] = tabIndex;
+ state.paths[path] = tabIndex;
window.sessionStorage.setItem('tab', JSON.stringify(state));
}
return true;
},
- updateTabs: function(ev) {
- document.querySelectorAll('[data-tab-title]').forEach(function(pane) {
+ updateTabs: function(ev, root) {
+ (root || document).querySelectorAll('[data-tab-title]').forEach(L.bind(function(pane) {
var menu = pane.parentNode.previousElementSibling,
- tab = menu.querySelector('[data-tab="%s"]'.format(pane.getAttribute('data-tab'))),
+ tab = menu ? menu.querySelector('[data-tab="%s"]'.format(pane.getAttribute('data-tab'))) : null,
n_errors = pane.querySelectorAll('.cbi-input-invalid').length;
- if (!pane.firstElementChild) {
+ if (!menu || !tab)
+ return;
+
+ if (this.isEmptyPane(pane)) {
tab.style.display = 'none';
tab.classList.remove('flash');
}
tab.removeAttribute('data-errors');
tab.removeAttribute('data-tooltip');
}
- });
+ }, this));
},
switchTab: function(ev) {
if (L.dom.matches(pane, '[data-tab]')) {
if (pane.getAttribute('data-tab') === name) {
pane.setAttribute('data-tab-active', 'true');
- L.ui.tabs.setActiveTabId(groupId, index);
+ L.ui.tabs.setActiveTabId(pane, index);
}
else {
pane.setAttribute('data-tab-active', 'false');
}
}),
- addValidator: function(field, type, optional /*, ... */) {
+ /* File uploading */
+ uploadFile: function(path, progressStatusNode) {
+ return new Promise(function(resolveFn, rejectFn) {
+ L.ui.showModal(_('Uploading file…'), [
+ E('p', _('Please select the file to upload.')),
+ E('div', { 'style': 'display:flex' }, [
+ E('div', { 'class': 'left', 'style': 'flex:1' }, [
+ E('input', {
+ type: 'file',
+ style: 'display:none',
+ change: function(ev) {
+ var modal = L.dom.parent(ev.target, '.modal'),
+ body = modal.querySelector('p'),
+ upload = modal.querySelector('.cbi-button-action.important'),
+ file = ev.currentTarget.files[0];
+
+ if (file == null)
+ return;
+
+ L.dom.content(body, [
+ E('ul', {}, [
+ E('li', {}, [ '%s: %s'.format(_('Name'), file.name.replace(/^.*[\\\/]/, '')) ]),
+ E('li', {}, [ '%s: %1024mB'.format(_('Size'), file.size) ])
+ ])
+ ]);
+
+ upload.disabled = false;
+ upload.focus();
+ }
+ }),
+ E('button', {
+ 'class': 'btn',
+ 'click': function(ev) {
+ ev.target.previousElementSibling.click();
+ }
+ }, [ _('Browse…') ])
+ ]),
+ E('div', { 'class': 'right', 'style': 'flex:1' }, [
+ E('button', {
+ 'class': 'btn',
+ 'click': function() {
+ L.ui.hideModal();
+ rejectFn(new Error('Upload has been cancelled'));
+ }
+ }, [ _('Cancel') ]),
+ ' ',
+ E('button', {
+ 'class': 'btn cbi-button-action important',
+ 'disabled': true,
+ 'click': function(ev) {
+ var input = L.dom.parent(ev.target, '.modal').querySelector('input[type="file"]');
+
+ if (!input.files[0])
+ return;
+
+ var progress = E('div', { 'class': 'cbi-progressbar', 'title': '0%' }, E('div', { 'style': 'width:0' }));
+
+ L.ui.showModal(_('Uploading file…'), [ progress ]);
+
+ var data = new FormData();
+
+ data.append('sessionid', rpc.getSessionID());
+ data.append('filename', path);
+ data.append('filedata', input.files[0]);
+
+ var filename = input.files[0].name;
+
+ L.Request.post(L.env.cgi_base + '/cgi-upload', data, {
+ timeout: 0,
+ progress: function(pev) {
+ var percent = (pev.loaded / pev.total) * 100;
+
+ if (progressStatusNode)
+ progressStatusNode.data = '%.2f%%'.format(percent);
+
+ progress.setAttribute('title', '%.2f%%'.format(percent));
+ progress.firstElementChild.style.width = '%.2f%%'.format(percent);
+ }
+ }).then(function(res) {
+ var reply = res.json();
+
+ L.ui.hideModal();
+
+ if (L.isObject(reply) && reply.failure) {
+ L.ui.addNotification(null, E('p', _('Upload request failed: %s').format(reply.message)));
+ rejectFn(new Error(reply.failure));
+ }
+ else {
+ reply.name = filename;
+ resolveFn(reply);
+ }
+ }, function(err) {
+ L.ui.hideModal();
+ rejectFn(err);
+ });
+ }
+ }, [ _('Upload') ])
+ ])
+ ])
+ ]);
+ });
+ },
+
+ /* Reconnect handling */
+ pingDevice: function(proto, ipaddr) {
+ var target = '%s://%s%s?%s'.format(proto || 'http', ipaddr || window.location.host, L.resource('icons/loading.gif'), Math.random());
+
+ return new Promise(function(resolveFn, rejectFn) {
+ var img = new Image();
+
+ img.onload = resolveFn;
+ img.onerror = rejectFn;
+
+ window.setTimeout(rejectFn, 1000);
+
+ img.src = target;
+ });
+ },
+
+ awaitReconnect: function(/* ... */) {
+ var ipaddrs = arguments.length ? arguments : [ window.location.host ];
+
+ window.setTimeout(L.bind(function() {
+ L.Poll.add(L.bind(function() {
+ var tasks = [], reachable = false;
+
+ for (var i = 0; i < 2; i++)
+ for (var j = 0; j < ipaddrs.length; j++)
+ tasks.push(this.pingDevice(i ? 'https' : 'http', ipaddrs[j])
+ .then(function(ev) { reachable = ev.target.src.replace(/^(https?:\/\/[^\/]+).*$/, '$1/') }, function() {}));
+
+ return Promise.all(tasks).then(function() {
+ if (reachable) {
+ L.Poll.stop();
+ window.location = reachable;
+ }
+ });
+ }, this));
+ }, this), 5000);
+ },
+
+ /* UCI Changes */
+ changes: L.Class.singleton({
+ init: function() {
+ if (!L.env.sessionid)
+ return;
+
+ return uci.changes().then(L.bind(this.renderChangeIndicator, this));
+ },
+
+ setIndicator: function(n) {
+ var i = document.querySelector('.uci_change_indicator');
+ if (i == null) {
+ var poll = document.getElementById('xhr_poll_status');
+ i = poll.parentNode.insertBefore(E('a', {
+ 'href': '#',
+ 'class': 'uci_change_indicator label notice',
+ 'click': L.bind(this.displayChanges, this)
+ }), poll);
+ }
+
+ if (n > 0) {
+ L.dom.content(i, [ _('Unsaved Changes'), ': ', n ]);
+ i.classList.add('flash');
+ i.style.display = '';
+ }
+ else {
+ i.classList.remove('flash');
+ i.style.display = 'none';
+ }
+ },
+
+ renderChangeIndicator: function(changes) {
+ var n_changes = 0;
+
+ for (var config in changes)
+ if (changes.hasOwnProperty(config))
+ n_changes += changes[config].length;
+
+ this.changes = changes;
+ this.setIndicator(n_changes);
+ },
+
+ changeTemplates: {
+ 'add-3': '<ins>uci add %0 <strong>%3</strong> # =%2</ins>',
+ 'set-3': '<ins>uci set %0.<strong>%2</strong>=%3</ins>',
+ 'set-4': '<var><ins>uci set %0.%2.%3=<strong>%4</strong></ins></var>',
+ 'remove-2': '<del>uci del %0.<strong>%2</strong></del>',
+ 'remove-3': '<var><del>uci del %0.%2.<strong>%3</strong></del></var>',
+ 'order-3': '<var>uci reorder %0.%2=<strong>%3</strong></var>',
+ 'list-add-4': '<var><ins>uci add_list %0.%2.%3=<strong>%4</strong></ins></var>',
+ 'list-del-4': '<var><del>uci del_list %0.%2.%3=<strong>%4</strong></del></var>',
+ 'rename-3': '<var>uci rename %0.%2=<strong>%3</strong></var>',
+ 'rename-4': '<var>uci rename %0.%2.%3=<strong>%4</strong></var>'
+ },
+
+ displayChanges: function() {
+ var list = E('div', { 'class': 'uci-change-list' }),
+ dlg = L.ui.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' }, [
+ E('button', {
+ 'class': 'btn',
+ 'click': L.ui.hideModal
+ }, [ _('Dismiss') ]), ' ',
+ E('button', {
+ 'class': 'cbi-button cbi-button-positive important',
+ 'click': L.bind(this.apply, this, true)
+ }, [ _('Save & Apply') ]), ' ',
+ E('button', {
+ 'class': 'cbi-button cbi-button-reset',
+ 'click': L.bind(this.revert, this)
+ }, [ _('Revert') ])])])
+ ]);
+
+ for (var 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)];
+
+ list.appendChild(E(tpl.replace(/%([01234])/g, function(m0, m1) {
+ switch (+m1) {
+ case 0:
+ return config;
+
+ case 2:
+ if (added != null && chg[1] == added[0])
+ return '@' + added[1] + '[-1]';
+ else
+ return chg[1];
+
+ case 4:
+ return "'%h'".format(chg[3].replace(/'/g, "'\"'\"'"));
+
+ default:
+ return chg[m1-1];
+ }
+ })));
+
+ if (chg[0] == 'add')
+ added = [ chg[1], chg[2] ];
+ }
+ }
+
+ list.appendChild(E('br'));
+ dlg.classList.add('uci-dialog');
+ },
+
+ displayStatus: function(type, content) {
+ if (type) {
+ var message = L.ui.showModal('', '');
+
+ message.classList.add('alert-message');
+ DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
+
+ if (content)
+ L.dom.content(message, content);
+
+ if (!this.was_polling) {
+ this.was_polling = L.Request.poll.active();
+ L.Request.poll.stop();
+ }
+ }
+ else {
+ L.ui.hideModal();
+
+ if (this.was_polling)
+ L.Request.poll.start();
+ }
+ },
+
+ rollback: function(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, data, duration) {
+ if (r.status === 204) {
+ L.ui.changes.displayStatus('warning', [
+ E('h4', _('Configuration changes have been rolled back!')),
+ E('p', _('The device could not be reached within %d seconds after applying the pending changes, which caused the configuration to be rolled back for safety reasons. If you believe that the configuration changes are correct nonetheless, perform an unchecked configuration apply. Alternatively, you can dismiss this warning and edit changes before attempting to apply again, or revert all pending changes to keep the currently working configuration state.').format(L.env.apply_rollback)),
+ E('div', { 'class': 'right' }, [
+ E('button', {
+ 'class': 'btn',
+ 'click': L.bind(L.ui.changes.displayStatus, L.ui.changes, false)
+ }, [ _('Dismiss') ]), ' ',
+ E('button', {
+ 'class': 'btn cbi-button-action important',
+ 'click': L.bind(L.ui.changes.revert, L.ui.changes)
+ }, [ _('Revert changes') ]), ' ',
+ E('button', {
+ 'class': 'btn cbi-button-negative important',
+ 'click': L.bind(L.ui.changes.apply, L.ui.changes, false)
+ }, [ _('Apply unchecked') ])
+ ])
+ ]);
+
+ return;
+ }
+
+ var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
+ window.setTimeout(function() {
+ L.Request.request(L.url('admin/uci/confirm'), {
+ method: 'post',
+ timeout: L.env.apply_timeout * 1000,
+ query: { sid: L.env.sessionid, token: L.env.token }
+ }).then(call);
+ }, delay);
+ };
+
+ call({ status: 0 });
+ }
+ else {
+ this.displayStatus('warning', [
+ E('h4', _('Device unreachable!')),
+ E('p', _('Could not regain access to the device after applying the configuration changes. You might need to reconnect if you modified network related settings such as the IP address or wireless security credentials.'))
+ ]);
+ }
+ },
+
+ confirm: function(checked, deadline, override_token) {
+ var tt;
+ var ts = Date.now();
+
+ this.displayStatus('notice');
+
+ if (override_token)
+ this.confirm_auth = { token: override_token };
+
+ var call = function(r, data, duration) {
+ if (Date.now() >= deadline) {
+ window.clearTimeout(tt);
+ L.ui.changes.rollback(checked);
+ return;
+ }
+ else if (r && (r.status === 200 || r.status === 204)) {
+ document.dispatchEvent(new CustomEvent('uci-applied'));
+
+ L.ui.changes.setIndicator(0);
+ L.ui.changes.displayStatus('notice',
+ E('p', _('Configuration changes applied.')));
+
+ window.clearTimeout(tt);
+ window.setTimeout(function() {
+ //L.ui.changes.displayStatus(false);
+ window.location = window.location.href.split('#')[0];
+ }, L.env.apply_display * 1000);
+
+ return;
+ }
+
+ var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
+ window.setTimeout(function() {
+ L.Request.request(L.url('admin/uci/confirm'), {
+ method: 'post',
+ timeout: L.env.apply_timeout * 1000,
+ query: L.ui.changes.confirm_auth
+ }).then(call, call);
+ }, delay);
+ };
+
+ var tick = function() {
+ var now = Date.now();
+
+ L.ui.changes.displayStatus('notice spinning',
+ E('p', _('Applying configuration changes… %ds')
+ .format(Math.max(Math.floor((deadline - Date.now()) / 1000), 0))));
+
+ if (now >= deadline)
+ return;
+
+ tt = window.setTimeout(tick, 1000 - (now - ts));
+ ts = now;
+ };
+
+ tick();
+
+ /* wait a few seconds for the settings to become effective */
+ window.setTimeout(call, Math.max(L.env.apply_holdoff * 1000 - ((ts + L.env.apply_rollback * 1000) - deadline), 1));
+ },
+
+ apply: function(checked) {
+ this.displayStatus('notice spinning',
+ E('p', _('Starting configuration apply…')));
+
+ L.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) {
+ if (r.status === (checked ? 200 : 204)) {
+ var tok = null; try { tok = r.json(); } catch(e) {}
+ if (checked && tok !== null && typeof(tok) === 'object' && typeof(tok.token) === 'string')
+ L.ui.changes.confirm_auth = tok;
+
+ L.ui.changes.confirm(checked, Date.now() + L.env.apply_rollback * 1000);
+ }
+ else if (checked && r.status === 204) {
+ L.ui.changes.displayStatus('notice',
+ E('p', _('There are no changes to apply')));
+
+ window.setTimeout(function() {
+ L.ui.changes.displayStatus(false);
+ }, L.env.apply_display * 1000);
+ }
+ else {
+ L.ui.changes.displayStatus('warning',
+ E('p', _('Apply request failed with status <code>%h</code>')
+ .format(r.responseText || r.statusText || r.status)));
+
+ window.setTimeout(function() {
+ L.ui.changes.displayStatus(false);
+ }, L.env.apply_display * 1000);
+ }
+ });
+ },
+
+ revert: function() {
+ this.displayStatus('notice spinning',
+ E('p', _('Reverting configuration…')));
+
+ L.Request.request(L.url('admin/uci/revert'), {
+ method: 'post',
+ query: { sid: L.env.sessionid, token: L.env.token }
+ }).then(function(r) {
+ if (r.status === 200) {
+ document.dispatchEvent(new CustomEvent('uci-reverted'));
+
+ L.ui.changes.setIndicator(0);
+ L.ui.changes.displayStatus('notice',
+ E('p', _('Changes have been reverted.')));
+
+ window.setTimeout(function() {
+ //L.ui.changes.displayStatus(false);
+ window.location = window.location.href.split('#')[0];
+ }, L.env.apply_display * 1000);
+ }
+ else {
+ L.ui.changes.displayStatus('warning',
+ E('p', _('Revert request failed with status <code>%h</code>')
+ .format(r.statusText || r.status)));
+
+ window.setTimeout(function() {
+ L.ui.changes.displayStatus(false);
+ }, L.env.apply_display * 1000);
+ }
+ });
+ }
+ }),
+
+ addValidator: function(field, type, optional, vfunc /*, ... */) {
if (type == null)
return;
events.push('blur', 'keyup');
try {
- var cbiValidator = new CBIValidator(field, type, optional),
+ var cbiValidator = L.validation.create(field, type, optional, vfunc),
validatorFn = cbiValidator.validate.bind(cbiValidator);
for (var i = 0; i < events.length; i++)
field.addEventListener(events[i], validatorFn);
validatorFn();
+
+ return validatorFn;
}
catch (e) { }
},
+ createHandlerFn: function(ctx, fn /*, ... */) {
+ 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].target;
+
+ t.classList.add('spinning');
+ t.disabled = true;
+
+ if (t.blur)
+ t.blur();
+
+ Promise.resolve(fn.apply(ctx, arguments)).finally(function() {
+ t.classList.remove('spinning');
+ t.disabled = false;
+ });
+ }, this.varargs(arguments, 2, ctx));
+ },
+
+ AbstractElement: UIElement,
+
/* Widgets */
+ Textfield: UITextfield,
+ Textarea: UITextarea,
+ Checkbox: UICheckbox,
+ Select: UISelect,
Dropdown: UIDropdown,
DynamicList: UIDynamicList,
- Combobox: UICombobox
+ Combobox: UICombobox,
+ ComboButton: UIComboButton,
+ Hiddenfield: UIHiddenfield,
+ FileUpload: UIFileUpload
});