diff options
Diffstat (limited to 'modules/luci-base/htdocs/luci-static')
5 files changed, 464 insertions, 160 deletions
diff --git a/modules/luci-base/htdocs/luci-static/resources/cbi.js b/modules/luci-base/htdocs/luci-static/resources/cbi.js index 324a91403f..65ea6bce3c 100644 --- a/modules/luci-base/htdocs/luci-static/resources/cbi.js +++ b/modules/luci-base/htdocs/luci-static/resources/cbi.js @@ -764,72 +764,14 @@ function cbi_update_table(table, data, placeholder) { if (!isElem(target)) return; - target.querySelectorAll('tr.table-titles, .tr.table-titles, .cbi-section-table-titles').forEach(function(thead) { - var titles = []; + var t = L.dom.findClassInstance(target); - thead.querySelectorAll('th, .th').forEach(function(th) { - titles.push(th); - }); - - if (Array.isArray(data)) { - var n = 0, rows = target.querySelectorAll('tr, .tr'), trows = []; - - data.forEach(function(row) { - var trow = E('tr', { 'class': 'tr' }); - - for (var i = 0; i < titles.length; i++) { - var text = (titles[i].innerText || '').trim(); - var td = trow.appendChild(E('td', { - 'class': titles[i].className, - 'data-title': (text !== '') ? text : null - }, (row[i] != null) ? row[i] : '')); - - td.classList.remove('th'); - td.classList.add('td'); - } - - trow.classList.add('cbi-rowstyle-%d'.format((n++ % 2) ? 2 : 1)); - - trows[n] = trow; - }); - - for (var i = 1; i <= n; i++) { - if (rows[i]) - target.replaceChild(trows[i], rows[i]); - else - target.appendChild(trows[i]); - } - - while (rows[++n]) - target.removeChild(rows[n]); - - if (placeholder && target.firstElementChild === target.lastElementChild) { - var trow = target.appendChild(E('tr', { 'class': 'tr placeholder' })); - var td = trow.appendChild(E('td', { 'class': titles[0].className }, placeholder)); - - td.classList.remove('th'); - td.classList.add('td'); - } - } - else { - thead.parentNode.style.display = 'none'; - - thead.parentNode.querySelectorAll('tr, .tr, .cbi-section-table-row').forEach(function(trow) { - if (trow !== thead) { - var n = 0; - trow.querySelectorAll('th, td, .th, .td').forEach(function(td) { - if (n < titles.length) { - var text = (titles[n++].innerText || '').trim(); - if (text !== '') - td.setAttribute('data-title', text); - } - }); - } - }); + if (!(t instanceof L.ui.Table)) { + t = new L.ui.Table(target); + L.dom.bindClassInstance(target, t); + } - thead.parentNode.style.display = ''; - } - }); + t.update(data, placeholder); } function showModal(title, children) diff --git a/modules/luci-base/htdocs/luci-static/resources/form.js b/modules/luci-base/htdocs/luci-static/resources/form.js index 23cc0b1cb5..c17f8ff0ca 100644 --- a/modules/luci-base/htdocs/luci-static/resources/form.js +++ b/modules/luci-base/htdocs/luci-static/resources/form.js @@ -2615,7 +2615,8 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p if (has_titles) { var trEl = E('tr', { 'class': 'tr cbi-section-table-titles ' + anon_class, - 'data-title': (!this.anonymous || this.sectiontitle) ? _('Name') : null + 'data-title': (!this.anonymous || this.sectiontitle) ? _('Name') : null, + 'click': this.sortable ? ui.createHandlerFn(this, 'handleSort') : null }); for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) { @@ -2624,7 +2625,8 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p trEl.appendChild(E('th', { 'class': 'th cbi-section-table-cell', - 'data-widget': opt.__name__ + 'data-widget': opt.__name__, + 'data-sortable-row': this.sortable ? '' : null })); if (opt.width != null) @@ -3034,6 +3036,68 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p .catch(function() {}); }, + /** @private */ + handleSort: function(ev) { + if (!ev.target.matches('th[data-sortable-row]')) + return; + + var th = ev.target, + descending = (th.getAttribute('data-sort-direction') == 'desc'), + config_name = this.uciconfig || this.map.config, + index = 0, + list = []; + + ev.currentTarget.querySelectorAll('th').forEach(function(other_th, i) { + if (other_th !== th) + other_th.removeAttribute('data-sort-direction'); + else + index = i; + }); + + ev.currentTarget.parentNode.querySelectorAll('tr.cbi-section-table-row').forEach(L.bind(function(tr, i) { + var sid = tr.getAttribute('data-sid'), + opt = tr.childNodes[index].getAttribute('data-name'), + val = this.cfgvalue(sid, opt); + + tr.querySelectorAll('.flash').forEach(function(n) { + n.classList.remove('flash') + }); + + list.push([ + ui.Table.prototype.deriveSortKey((val != null) ? val.trim() : ''), + tr + ]); + }, this)); + + list.sort(function(a, b) { + if (a[0] < b[0]) + return descending ? 1 : -1; + + if (a[0] > b[0]) + return descending ? -1 : 1; + + return 0; + }); + + window.requestAnimationFrame(L.bind(function() { + var ref_sid, cur_sid; + + for (var i = 0; i < list.length; i++) { + list[i][1].childNodes[index].classList.add('flash'); + th.parentNode.parentNode.appendChild(list[i][1]); + + cur_sid = list[i][1].getAttribute('data-sid'); + + if (ref_sid) + this.map.data.move(config_name, cur_sid, ref_sid, true); + + ref_sid = cur_sid; + } + + th.setAttribute('data-sort-direction', descending ? 'asc' : 'desc'); + }, this)); + }, + /** * Add further options to the per-section instanced modal popup. * diff --git a/modules/luci-base/htdocs/luci-static/resources/luci.js b/modules/luci-base/htdocs/luci-static/resources/luci.js index 78e8b8b30b..529a33ca3b 100644 --- a/modules/luci-base/htdocs/luci-static/resources/luci.js +++ b/modules/luci-base/htdocs/luci-static/resources/luci.js @@ -695,115 +695,117 @@ * The resulting HTTP response. */ request: function(target, options) { - var state = { xhr: new XMLHttpRequest(), url: this.expandURL(target), start: Date.now() }, - opt = Object.assign({}, options, state), - content = null, - contenttype = null, - callback = this.handleReadyStateChange; - - return new Promise(function(resolveFn, rejectFn) { - opt.xhr.onreadystatechange = callback.bind(opt, resolveFn, rejectFn); - opt.method = String(opt.method || 'GET').toUpperCase(); - - if ('query' in opt) { - var q = (opt.query != null) ? Object.keys(opt.query).map(function(k) { - if (opt.query[k] != null) { - var v = (typeof(opt.query[k]) == 'object') - ? JSON.stringify(opt.query[k]) - : String(opt.query[k]); - - return '%s=%s'.format(encodeURIComponent(k), encodeURIComponent(v)); - } - else { - return encodeURIComponent(k); - } - }).join('&') : ''; - - if (q !== '') { - switch (opt.method) { - case 'GET': - case 'HEAD': - case 'OPTIONS': - opt.url += ((/\?/).test(opt.url) ? '&' : '?') + q; - break; - - default: - if (content == null) { - content = q; - contenttype = 'application/x-www-form-urlencoded'; + return Promise.resolve(target).then((function(url) { + var state = { xhr: new XMLHttpRequest(), url: this.expandURL(url), start: Date.now() }, + opt = Object.assign({}, options, state), + content = null, + contenttype = null, + callback = this.handleReadyStateChange; + + return new Promise(function(resolveFn, rejectFn) { + opt.xhr.onreadystatechange = callback.bind(opt, resolveFn, rejectFn); + opt.method = String(opt.method || 'GET').toUpperCase(); + + if ('query' in opt) { + var q = (opt.query != null) ? Object.keys(opt.query).map(function(k) { + if (opt.query[k] != null) { + var v = (typeof(opt.query[k]) == 'object') + ? JSON.stringify(opt.query[k]) + : String(opt.query[k]); + + return '%s=%s'.format(encodeURIComponent(k), encodeURIComponent(v)); + } + else { + return encodeURIComponent(k); + } + }).join('&') : ''; + + if (q !== '') { + switch (opt.method) { + case 'GET': + case 'HEAD': + case 'OPTIONS': + opt.url += ((/\?/).test(opt.url) ? '&' : '?') + q; + break; + + default: + if (content == null) { + content = q; + contenttype = 'application/x-www-form-urlencoded'; + } } } } - } - if (!opt.cache) - opt.url += ((/\?/).test(opt.url) ? '&' : '?') + (new Date()).getTime(); + if (!opt.cache) + opt.url += ((/\?/).test(opt.url) ? '&' : '?') + (new Date()).getTime(); - if (isQueueableRequest(opt)) { - requestQueue.push([opt, rejectFn, resolveFn]); - requestAnimationFrame(flushRequestQueue); - return; - } + if (isQueueableRequest(opt)) { + requestQueue.push([opt, rejectFn, resolveFn]); + requestAnimationFrame(flushRequestQueue); + return; + } - if ('username' in opt && 'password' in opt) - opt.xhr.open(opt.method, opt.url, true, opt.username, opt.password); - else - opt.xhr.open(opt.method, opt.url, true); + if ('username' in opt && 'password' in opt) + opt.xhr.open(opt.method, opt.url, true, opt.username, opt.password); + else + opt.xhr.open(opt.method, opt.url, true); - opt.xhr.responseType = opt.responseType || 'text'; + opt.xhr.responseType = opt.responseType || 'text'; - if ('overrideMimeType' in opt.xhr) - opt.xhr.overrideMimeType('application/octet-stream'); + if ('overrideMimeType' in opt.xhr) + opt.xhr.overrideMimeType('application/octet-stream'); - if ('timeout' in opt) - opt.xhr.timeout = +opt.timeout; + if ('timeout' in opt) + opt.xhr.timeout = +opt.timeout; - if ('credentials' in opt) - opt.xhr.withCredentials = !!opt.credentials; + if ('credentials' in opt) + opt.xhr.withCredentials = !!opt.credentials; - if (opt.content != null) { - switch (typeof(opt.content)) { - case 'function': - content = opt.content(opt.xhr); - break; + if (opt.content != null) { + switch (typeof(opt.content)) { + case 'function': + content = opt.content(opt.xhr); + break; - case 'object': - if (!(opt.content instanceof FormData)) { - content = JSON.stringify(opt.content); - contenttype = 'application/json'; - } - else { - content = opt.content; - } - break; + case 'object': + if (!(opt.content instanceof FormData)) { + content = JSON.stringify(opt.content); + contenttype = 'application/json'; + } + else { + content = opt.content; + } + break; - default: - content = String(opt.content); + default: + content = String(opt.content); + } } - } - if ('headers' in opt) - for (var header in opt.headers) - if (opt.headers.hasOwnProperty(header)) { - if (header.toLowerCase() != 'content-type') - opt.xhr.setRequestHeader(header, opt.headers[header]); - else - contenttype = opt.headers[header]; - } + if ('headers' in opt) + for (var header in opt.headers) + if (opt.headers.hasOwnProperty(header)) { + if (header.toLowerCase() != 'content-type') + opt.xhr.setRequestHeader(header, opt.headers[header]); + else + contenttype = opt.headers[header]; + } - if ('progress' in opt && 'upload' in opt.xhr) - opt.xhr.upload.addEventListener('progress', opt.progress); + if ('progress' in opt && 'upload' in opt.xhr) + opt.xhr.upload.addEventListener('progress', opt.progress); - if (contenttype != null) - opt.xhr.setRequestHeader('Content-Type', contenttype); + if (contenttype != null) + opt.xhr.setRequestHeader('Content-Type', contenttype); - try { - opt.xhr.send(content); - } - catch (e) { - rejectFn.call(opt, e); - } - }); + try { + opt.xhr.send(content); + } + catch (e) { + rejectFn.call(opt, e); + } + }); + }).bind(this)); }, handleReadyStateChange: function(resolveFn, rejectFn, ev) { diff --git a/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js b/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js index 10b65be8ec..ae17d8197d 100644 --- a/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js +++ b/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js @@ -187,9 +187,10 @@ var CBIZoneSelect = form.ListValue.extend({ emptyval.setAttribute('data-value', ''); } - L.dom.content(emptyval.querySelector('span'), [ - E('strong', _('Device')), E('span', ' (%s)'.format(_('input'))) - ]); + if (opt[0].allowlocal) + L.dom.content(emptyval.querySelector('span'), [ + E('strong', _('Device')), E('span', ' (%s)'.format(_('input'))) + ]); L.dom.content(anyval.querySelector('span'), [ E('strong', _('Any zone')), E('span', ' (%s)'.format(_('forward'))) diff --git a/modules/luci-base/htdocs/luci-static/resources/ui.js b/modules/luci-base/htdocs/luci-static/resources/ui.js index ac158f5260..5abd3b388d 100644 --- a/modules/luci-base/htdocs/luci-static/resources/ui.js +++ b/modules/luci-base/htdocs/luci-static/resources/ui.js @@ -3140,6 +3140,299 @@ var UIMenu = baseclass.singleton(/** @lends LuCI.ui.menu.prototype */ { } }); +var UITable = baseclass.extend(/** @lends LuCI.ui.table.prototype */ { + __init__: function(captions, options, placeholder) { + if (!Array.isArray(captions)) { + this.initFromMarkup(captions); + + return; + } + + var id = options.id || 'table%08x'.format(Math.random() * 0xffffffff); + + var table = E('table', { 'id': id, 'class': 'table' }, [ + E('tr', { 'class': 'tr table-titles', 'click': UI.prototype.createHandlerFn(this, 'handleSort') }) + ]); + + this.id = id; + this.node = table + this.options = options; + + var sorting = this.getActiveSortState(); + + for (var i = 0; i < captions.length; i++) { + if (captions[i] == null) + continue; + + var th = E('th', { 'class': 'th' }, [ captions[i] ]); + + if (typeof(options.captionClasses) == 'object') + DOMTokenList.prototype.add.apply(th.classList, L.toArray(options.captionClasses[i])); + + if (options.sortable !== false && (typeof(options.sortable) != 'object' || options.sortable[i] !== false)) { + th.setAttribute('data-sortable-row', true); + + if (sorting && sorting[0] == i) + th.setAttribute('data-sort-direction', sorting[1] ? 'desc' : 'asc'); + } + + table.firstElementChild.appendChild(th); + } + + if (placeholder) { + var trow = table.appendChild(E('tr', { 'class': 'tr placeholder' })), + 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(); + + if (!Array.isArray(data)) + return; + + if (sorting) { + var list = data.map(L.bind(function(row) { + return [ this.deriveSortKey(row[sorting[0]], sorting[0]), row ]; + }, this)); + + list.sort(function(a, b) { + if (a[0] < b[0]) + return sorting[1] ? 1 : -1; + + if (a[0] > b[0]) + return sorting[1] ? -1 : 1; + + return 0; + }); + + data.length = 0; + + list.forEach(function(item) { + data.push(item[1]); + }); + } + + this.data = data; + this.placeholder = placeholder; + + var n = 0, + rows = this.node.querySelectorAll('tr'), + trows = [], + headings = [].slice.call(this.node.firstElementChild.querySelectorAll('th')), + captionClasses = this.options.captionClasses; + + data.forEach(function(row) { + trows[n] = E('tr', { 'class': 'tr' }); + + for (var i = 0; i < headings.length; i++) { + var text = (headings[i].innerText || '').trim(); + var td = trows[n].appendChild(E('td', { + 'class': 'td', + 'data-title': (text !== '') ? text : null + }, (row[i] != null) ? row[i] : '')); + + if (typeof(captionClasses) == 'object') + DOMTokenList.prototype.add.apply(td.classList, L.toArray(captionClasses[i])); + + if (!td.classList.contains('cbi-section-actions')) + headings[i].setAttribute('data-sortable-row', true); + } + + trows[n].classList.add('cbi-rowstyle-%d'.format((n++ % 2) ? 2 : 1)); + }); + + for (var i = 0; i < n; i++) { + if (rows[i+1]) + this.node.replaceChild(trows[i], rows[i+1]); + else + this.node.appendChild(trows[i]); + } + + while (rows[++n]) + this.node.removeChild(rows[n]); + + if (placeholder && this.node.firstElementChild === this.node.lastElementChild) { + var trow = this.node.appendChild(E('tr', { 'class': 'tr placeholder' })), + td = trow.appendChild(E('td', { 'class': 'td' }, placeholder)); + + if (typeof(captionClasses) == 'object') + DOMTokenList.prototype.add.apply(td.classList, L.toArray(captionClasses[0])); + } + + return this.node; + }, + + render: function() { + return this.node; + }, + + /** @private */ + initFromMarkup: function(node) { + if (!dom.elem(node)) + node = document.querySelector(node); + + if (!node) + throw 'Invalid table selector'; + + var options = {}, + headrow = node.querySelector('tr, .tr'); + + if (!headrow) + return; + + options.classes = [].slice.call(node.classList).filter(function(c) { return c != 'table' }); + options.sortable = []; + options.captionClasses = []; + + headrow.querySelectorAll('th, .th').forEach(function(th, i) { + options.sortable[i] = !th.classList.contains('cbi-section-actions'); + options.captionClasses[i] = [].slice.call(th.classList).filter(function(c) { return c != 'th' }); + }); + + headrow.addEventListener('click', UI.prototype.createHandlerFn(this, 'handleSort')); + + this.id = node.id; + this.node = node; + this.options = options; + }, + + /** @private */ + deriveSortKey: function(value, index) { + var opts = this.options || {}, + hint, m; + + if (opts.sortable == true || opts.sortable == null) + hint = 'auto'; + else if (typeof( opts.sortable) == 'object') + hint = opts.sortable[index]; + + if (dom.elem(value)) + value = value.innerText.trim(); + + switch (hint || 'auto') { + case true: + case 'auto': + m = /^([0-9a-fA-F:.]+)(?:\/([0-9a-fA-F:.]+))?$/.exec(value); + + if (m) { + var addr, mask; + + addr = validation.parseIPv6(m[1]); + mask = m[2] ? validation.parseIPv6(m[2]) : null; + + if (addr && mask != null) + return '%04x%04x%04x%04x%04x%04x%04x%04x%04x%04x%04x%04x%04x%04x%04x%04x'.format( + addr[0], addr[1], addr[2], addr[3], addr[4], addr[5], addr[6], addr[7], + mask[0], mask[1], mask[2], mask[3], mask[4], mask[5], mask[6], mask[7] + ); + else if (addr) + return '%04x%04x%04x%04x%04x%04x%04x%04x%02x'.format( + addr[0], addr[1], addr[2], addr[3], addr[4], addr[5], addr[6], addr[7], + m[2] ? +m[2] : 128 + ); + + addr = validation.parseIPv4(m[1]); + mask = m[2] ? validation.parseIPv4(m[2]) : null; + + if (addr && mask != null) + return '%03d%03d%03d%03d%03d%03d%03d%03d'.format( + addr[0], addr[1], addr[2], addr[3], + mask[0], mask[1], mask[2], mask[3] + ); + else if (addr) + return '%03d%03d%03d%03d%02d'.format( + addr[0], addr[1], addr[2], addr[3], + m[2] ? +m[2] : 32 + ); + } + + m = /^(?:(\d+)d )?(\d+)h (\d+)m (\d+)s$/.exec(value); + + if (m) + return '%05d%02d%02d%02d'.format(+m[1], +m[2], +m[3], +m[4]); + + m = /^(\d+)\b(\D*)$/.exec(value); + + if (m) + return '%010d%s'.format(+m[1], m[2]); + + return String(value); + + case 'ignorecase': + return String(value).toLowerCase(); + + case 'numeric': + return +value; + + default: + return String(value); + } + }, + + /** @private */ + getActiveSortState: function() { + if (this.sortState) + return this.sortState; + + var page = document.body.getAttribute('data-page'), + key = page + '.' + this.id, + state = session.getLocalData('tablesort'); + + if (L.isObject(state) && Array.isArray(state[key])) + return state[key]; + + return null; + }, + + /** @private */ + setActiveSortState: function(index, descending) { + this.sortState = [ index, descending ]; + + if (!this.options.id) + return; + + var page = document.body.getAttribute('data-page'), + key = page + '.' + this.id, + state = session.getLocalData('tablesort'); + + if (!L.isObject(state)) + state = {}; + + state[key] = this.sortState; + + session.setLocalData('tablesort', state); + }, + + /** @private */ + handleSort: function(ev) { + if (!ev.target.matches('th[data-sortable-row]')) + return; + + var th = ev.target, + direction = (th.getAttribute('data-sort-direction') == 'asc'), + index = 0; + + this.node.firstElementChild.querySelectorAll('th').forEach(function(other_th, i) { + if (other_th !== th) + other_th.removeAttribute('data-sort-direction'); + else + index = i; + }); + + th.setAttribute('data-sort-direction', direction ? 'desc' : 'asc'); + + this.setActiveSortState(index, direction); + this.update(this.data, this.placeholder); + } +}); + /** * @class ui * @memberof LuCI @@ -4519,6 +4812,8 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ { menu: UIMenu, + Table: UITable, + AbstractElement: UIElement, /* Widgets */ |