From: Jo-Philipp Wich Date: Sat, 20 Oct 2018 15:56:02 +0000 (+0200) Subject: luci-base, themes: rework dynlist and dropdown widgets X-Git-Url: http://git.openwrt.org/?p=project%2Fluci.git;a=commitdiff_plain;h=7c78218339ac914f097db79c343b07ea86c7313a luci-base, themes: rework dynlist and dropdown widgets Signed-off-by: Jo-Philipp Wich --- diff --git a/modules/luci-base/htdocs/luci-static/resources/cbi.js b/modules/luci-base/htdocs/luci-static/resources/cbi.js index 2efa024859..3ace96f322 100644 --- a/modules/luci-base/htdocs/luci-static/resources/cbi.js +++ b/modules/luci-base/htdocs/luci-static/resources/cbi.js @@ -794,95 +794,41 @@ function cbi_init() { cbi_d_update(); } -function cbi_combobox(id, values, def, man, focus) { - var selid = "cbi.combobox." + id; - if (document.getElementById(selid)) { - return - } - - var obj = document.getElementById(id) - var sel = document.createElement("select"); - sel.id = selid; - sel.index = obj.index; - sel.classList.remove('cbi-input-text'); - sel.classList.add('cbi-input-select'); - - if (obj.nextSibling) - obj.parentNode.insertBefore(sel, obj.nextSibling); - else - obj.parentNode.appendChild(sel); - - var dt = obj.getAttribute('cbi_datatype'); - var op = obj.getAttribute('cbi_optional'); - - if (!values[obj.value]) { - if (obj.value == "") { - var optdef = document.createElement("option"); - optdef.value = ""; - optdef.appendChild(document.createTextNode(typeof(def) === 'string' ? def : _('-- Please choose --'))); - sel.appendChild(optdef); - } - else { - var opt = document.createElement("option"); - opt.value = obj.value; - opt.selected = "selected"; - opt.appendChild(document.createTextNode(obj.value)); - sel.appendChild(opt); - } +function cbi_combobox_init(id, values, def, man) { + var obj = (typeof(id) === 'string') ? document.getElementById(id) : id; + var sb = E('div', { + 'name': obj.name, + 'class': 'cbi-dropdown', + 'display-items': 5, + 'optional': obj.getAttribute('data-optional'), + 'placeholder': _('-- Please choose --') + }, [ E('ul') ]); + + if (!(obj.value in values) && obj.value.length) { + sb.lastElementChild.appendChild(E('li', { + 'data-value': obj.value, + 'selected': '' + }, obj.value.length ? obj.value : (def || _('-- Please choose --')))); } for (var i in values) { - var opt = document.createElement("option"); - opt.value = i; - - if (obj.value == i) - opt.selected = "selected"; - - opt.appendChild(document.createTextNode(values[i])); - sel.appendChild(opt); - } - - var optman = document.createElement("option"); - optman.value = ""; - optman.appendChild(document.createTextNode(typeof(man) === 'string' ? man : _('-- custom --'))); - sel.appendChild(optman); - - obj.style.display = "none"; - - if (dt) - cbi_validate_field(sel, op == 'true', dt); - - sel.addEventListener("change", function() { - if (sel.selectedIndex == sel.options.length - 1) { - obj.style.display = "inline"; - sel.blur(); - sel.parentNode.removeChild(sel); - obj.focus(); - } - else { - obj.value = sel.options[sel.selectedIndex].value; - } - - try { - cbi_d_update(); - } catch (e) { - //Do nothing - } - }) - - // Retrigger validation in select - if (focus) { - sel.focus(); - sel.blur(); + sb.lastElementChild.appendChild(E('li', { + 'data-value': i, + 'selected': (i == obj.value) ? '' : null + }, values[i])); } -} -function cbi_combobox_init(id, values, def, man) { - var obj = (typeof(id) === 'string') ? document.getElementById(id) : id; - obj.addEventListener("blur", function() { - cbi_combobox(obj.id, values, def, man, true); - }); - cbi_combobox(obj.id, values, def, man, false); + sb.lastElementChild.appendChild(E('li', { 'data-value': '-' }, [ + E('input', { + 'type': 'text', + 'class': 'create-item-input', + 'data-type': obj.getAttribute('data-type'), + 'data-optional': true, + 'placeholder': (man || _('-- custom --')) + }) + ])); + + obj.parentNode.replaceChild(sb, obj); } function cbi_filebrowser(id, defpath) { @@ -912,229 +858,151 @@ function cbi_browser_init(id, resource, defpath) btn.addEventListener('click', cbi_browser_btnclick); } -function cbi_dynlist_init(parent, datatype, optional, choices) -{ - var prefix = parent.getAttribute('data-prefix'); - var holder = parent.getAttribute('data-placeholder'); - - var values; - - function cbi_dynlist_redraw(focus, add, del) - { - values = [ ]; +CBIDynamicList = { + 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('input', { + 'type': 'hidden', + 'name': dl.getAttribute('data-prefix'), + 'value': value })]); + + dl.querySelectorAll('.item, .add-item').forEach(function(item) { + if (exists) + return; - while (parent.firstChild) { - var n = parent.firstChild; - var i = +n.index; + var hidden = item.querySelector('input[type="hidden"]'); - if (i != del) { - if (matchesElem(n, 'input')) - values.push(n.value || ''); - else if (matchesElem(n, 'select')) - values[values.length-1] = n.options[n.selectedIndex].value; - } + if (hidden && hidden.value === value) + exists = true; + else if (!hidden || hidden.value >= value) + exists = !!item.parentNode.insertBefore(new_item, item); + }); + }, - parent.removeChild(n); - } + removeItem: function(dl, item) { + var sb = dl.querySelector('.cbi-dropdown'); + if (sb) { + var value = item.querySelector('input[type="hidden"]').value; - if (add >= 0) { - focus = add+1; - values.splice(focus, 0, ''); - } - else if (values.length == 0) { - focus = 0; - values.push(''); + sb.querySelectorAll('ul > li').forEach(function(li) { + if (li.getAttribute('data-value') === value) + li.removeAttribute('unselectable'); + }); } - for (var i = 0; i < values.length; i++) { - var t = document.createElement('input'); - t.id = prefix + '.' + (i+1); - t.name = prefix; - t.value = values[i]; - t.type = 'text'; - t.index = i; - t.className = 'cbi-input-text'; - - if (i == 0 && holder) - t.placeholder = holder; - - var b = E('div', { - class: 'cbi-button cbi-button-' + ((i+1) < values.length ? 'remove' : 'add') - }, (i+1) < values.length ? '×' : '+'); - - parent.appendChild(t); - parent.appendChild(b); - - if (datatype == 'file') - cbi_browser_init(t.id, null, parent.getAttribute('data-browser-path')); - - parent.appendChild(document.createElement('br')); - - if (datatype) - cbi_validate_field(t.id, ((i+1) == values.length) || optional, datatype); - - if (choices) { - cbi_combobox_init(t.id, choices, '', _('-- custom --')); - b.index = i; - - b.addEventListener('keydown', cbi_dynlist_keydown); - b.addEventListener('keypress', cbi_dynlist_keypress); - - if (i == focus || -i == focus) - b.focus(); - } - else { - t.addEventListener('keydown', cbi_dynlist_keydown); - t.addEventListener('keypress', cbi_dynlist_keypress); - - if (i == focus) { - t.focus(); - } - else if (-i == focus) { - t.focus(); + item.parentNode.removeChild(item); + }, - /* force cursor to end */ - var v = t.value; - t.value = ' ' - t.value = v; - } - } + handleClick: function(ev) { + var dl = ev.currentTarget, + item = findParent(ev.target, '.item'); - b.addEventListener('click', cbi_dynlist_btnclick); + if (item) { + this.removeItem(dl, item); } - } - - function cbi_dynlist_keypress(ev) - { - ev = ev ? ev : window.event; - - var se = ev.target ? ev.target : ev.srcElement; - - if (se.nodeType == 3) - se = se.parentNode; - - switch (ev.keyCode) { - /* backspace, delete */ - case 8: - case 46: - if (se.value.length == 0) { - if (ev.preventDefault) - ev.preventDefault(); - - return false; - } - - return true; - - /* enter, arrow up, arrow down */ - case 13: - case 38: - case 40: - if (ev.preventDefault) - ev.preventDefault(); - - return false; + else if (matchesElem(ev.target, '.cbi-button-add')) { + var input = ev.target.previousElementSibling; + if (input.value.length && !input.classList.contains('cbi-input-invalid')) { + this.addItem(dl, input.value, null, true); + input.value = ''; + } } + }, - return true; - } - - function cbi_dynlist_keydown(ev) - { - ev = ev ? ev : window.event; - - var se = ev.target ? ev.target : ev.srcElement; - - if (se.nodeType == 3) - se = se.parentNode; - - var prev = se.previousSibling; - while (prev && prev.name != prefix) - prev = prev.previousSibling; - - var next = se.nextSibling; - while (next && next.name != prefix) - next = next.nextSibling; - - /* advance one further in combobox case */ - if (next && next.nextSibling.name == prefix) - next = next.nextSibling; - - switch (ev.keyCode) { - /* backspace, delete */ - case 8: - case 46: - var del = (matchesElem(se, 'select')) - ? true : (se.value.length == 0); + handleDropdownChange: function(ev) { + var dl = ev.currentTarget, + sbIn = ev.detail.instance, + sbEl = ev.detail.element, + sbVal = ev.detail.value; - if (del) { - if (ev.preventDefault) - ev.preventDefault(); + if (sbVal === null) + return; - var focus = se.index; - if (ev.keyCode == 8) - focus = -focus+1; + sbIn.setValues(sbEl, null); + sbVal.element.setAttribute('unselectable', ''); - cbi_dynlist_redraw(focus, -1, se.index); + this.addItem(dl, sbVal.value, sbVal.text, true); + }, - return false; - } + handleKeydown: function(ev) { + var dl = ev.currentTarget, + item = findParent(ev.target, '.item'); - break; + if (item) { + switch (ev.keyCode) { + case 8: /* backspace */ + if (item.previousElementSibling) + item.previousElementSibling.focus(); - /* enter */ - case 13: - cbi_dynlist_redraw(-1, se.index, -1); + this.removeItem(dl, item); break; - /* arrow up */ - case 38: - if (prev) - prev.focus(); + case 46: /* delete */ + if (item.nextElementSibling) { + if (item.nextElementSibling.classList.contains('item')) + item.nextElementSibling.focus(); + else + item.nextElementSibling.firstElementChild.focus(); + } + this.removeItem(dl, item); break; + } + } + else if (matchesElem(ev.target, '.cbi-input-text')) { + switch (ev.keyCode) { + case 13: /* enter */ + if (ev.target.value.length && !ev.target.classList.contains('cbi-input-invalid')) { + this.addItem(dl, ev.target.value, null, true); + ev.target.value = ''; + ev.target.blur(); + ev.target.focus(); + } - /* arrow down */ - case 40: - if (next) - next.focus(); - + ev.preventDefault(); break; + } } - - return true; } +}; - function cbi_dynlist_btnclick(ev) - { - ev = ev ? ev : window.event; +function cbi_dynlist_init(dl, datatype, optional, choices) +{ + if (!(this instanceof cbi_dynlist_init)) + return new cbi_dynlist_init(dl, datatype, optional, choices); + + dl.classList.add('cbi-dynlist'); + dl.appendChild(E('div', { 'class': 'add-item' }, E('input', { + 'type': 'text', + 'name': 'cbi.dynlist.' + dl.getAttribute('data-prefix'), + 'class': 'cbi-input-text', + 'data-type': datatype, + 'data-optional': true + }))); + + if (choices) + cbi_combobox_init(dl.lastElementChild.lastElementChild, choices, '', _('-- custom --')); + else + dl.lastElementChild.appendChild(E('div', { 'class': 'cbi-button cbi-button-add' }, '+')); - var se = ev.target ? ev.target : ev.srcElement; - var input = se.previousSibling; - while (input && input.name != prefix) - input = input.previousSibling; + dl.addEventListener('click', this.handleClick.bind(this)); + dl.addEventListener('keydown', this.handleKeydown.bind(this)); + dl.addEventListener('cbi-dropdown-change', this.handleDropdownChange.bind(this)); - if (se.classList.contains('cbi-button-remove')) { - input.value = ''; - - cbi_dynlist_keydown({ - target: input, - keyCode: 8 - }); - } - else { - cbi_dynlist_keydown({ - target: input, - keyCode: 13 - }); - } + try { + var values = JSON.parse(dl.getAttribute('data-values') || '[]'); - return false; + if (typeof(values) === 'object' && Array.isArray(values)) + for (var i = 0; i < values.length; i++) + this.addItem(dl, values[i], choices ? choices[values[i]] : null); } - - cbi_dynlist_redraw(NaN, -1, -1); + catch (e) {} } +cbi_dynlist_init.prototype = CBIDynamicList; + function cbi_t_add(section, tab) { var t = document.getElementById('tab.' + section + '.' + tab); diff --git a/themes/luci-theme-bootstrap/htdocs/luci-static/bootstrap/cascade.css b/themes/luci-theme-bootstrap/htdocs/luci-static/bootstrap/cascade.css index 67e19e7d07..73e6c3bed6 100644 --- a/themes/luci-theme-bootstrap/htdocs/luci-static/bootstrap/cascade.css +++ b/themes/luci-theme-bootstrap/htdocs/luci-static/bootstrap/cascade.css @@ -492,12 +492,47 @@ select, box-sizing: border-box; } -.cbi-dropdown { +.cbi-dropdown, +.cbi-dynlist { min-width: 210px; max-width: 400px; width: auto; } +.cbi-dynlist { + height: auto; + min-height: 30px; + display: inline-flex; + flex-direction: column; +} + +.cbi-dynlist > .item { + margin-bottom: 4px; + box-shadow: 0 0 2px #ccc; + background: #fff; + padding: 2px 2em 2px 4px; + border: 1px solid #ccc; + border-radius: 3px; + position: relative; + pointer-events: none; +} + +.cbi-dynlist > .item::after { + content: "×"; + position: absolute; + display: inline-flex; + align-items: center; + top: -1px; + right: -1px; + bottom: -1px; + padding: 0 6px; + border: 1px solid #ccc; + border-radius: 0 3px 3px 0; + font-weight: bold; + color: #c44; + pointer-events: auto; +} + select { padding: initial; background: #fff; @@ -548,7 +583,8 @@ textarea { .td > input[type=text], .td > input[type=password], .td > select, -.td > .cbi-dropdown { +.td > .cbi-dropdown, +.cbi-dynlist > .add-item > .cbi-dropdown { width: 100%; } @@ -568,11 +604,12 @@ textarea { color: #bfbfbf; } -.btn, .cbi-button, input, textarea { +.item::after, .btn, .cbi-button, input, textarea { transition: border linear 0.2s, box-shadow linear 0.2s; box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); } +.item:hover::after, .btn:hover, .cbi-button:hover, input:focus, textarea:focus { outline: 0; @@ -1206,6 +1243,7 @@ footer { border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); } +.item::after, .btn, .cbi-button { cursor: pointer; @@ -1318,6 +1356,7 @@ footer { color: #404040; } +.cbi-dynlist > .item:focus, .cbi-dropdown:focus { outline: 2px solid #4b6e9b; } @@ -1354,6 +1393,7 @@ footer { font-weight: bold; text-shadow: 1px 1px 0px #fff; display: none; + justify-content: center; } .cbi-dropdown > ul > li { @@ -1454,6 +1494,14 @@ footer { border-bottom: none; } +.cbi-dropdown[open] > ul.dropdown > li[unselectable] { + opacity: 0.7; +} + +.cbi-dropdown[open] > ul.dropdown > li > input.create-item-input:first-child:last-child { + width: 100%; +} + .cbi-dropdown[disabled] { pointer-events: none; opacity: .6; diff --git a/themes/luci-theme-material/htdocs/luci-static/material/cascade.css b/themes/luci-theme-material/htdocs/luci-static/material/cascade.css index a4d71eeb67..e62c9be216 100644 --- a/themes/luci-theme-material/htdocs/luci-static/material/cascade.css +++ b/themes/luci-theme-material/htdocs/luci-static/material/cascade.css @@ -153,7 +153,9 @@ input, } select:not([multiple="multiple"]):focus, -input:focus { +input:focus, +.cbi-dropdown:focus, +.cbi-dynlist > .item:focus { border-color: var(--main-color, #0099CC); } @@ -642,7 +644,7 @@ td > table > tbody > tr > td, /* button style */ -.btn, .cbi-button { +.btn, .cbi-button, .item::after { -webkit-appearance: none; text-transform: uppercase; color: rgba(0, 0, 0, 0.87); @@ -675,6 +677,7 @@ td > table > tbody > tr > td, .cbi-button:hover, .cbi-button:focus, .cbi-button:active, +.item:hover::after, .cbi-page-actions .cbi-button-apply + .cbi-button-save:hover, .cbi-page-actions .cbi-button-apply + .cbi-button-save:focus, .cbi-page-actions .cbi-button-apply + .cbi-button-save:active { @@ -958,6 +961,7 @@ td > table > tbody > tr > td, } +.cbi-dynlist, .cbi-dropdown { display: inline-flex; cursor: pointer; @@ -966,10 +970,6 @@ td > table > tbody > tr > td, height: auto; } -.cbi-dropdown:focus { - outline: 2px solid #4b6e9b; -} - .cbi-dropdown > ul { margin: 0 !important; padding: 0; @@ -1109,6 +1109,22 @@ td > table > tbody > tr > td, border-bottom: none; } +.cbi-dropdown[open] > ul.dropdown > li[unselectable] { + opacity: 0.7; +} + +.cbi-dropdown[open] > ul.dropdown > li > input.create-item-input:first-child:last-child { + width: 100%; +} + +.cbi-dropdown[open] > ul.dropdown > li[unselectable] { + opacity: 0.7; +} + +.cbi-dropdown[open] > ul.dropdown > li > input.create-item-input:first-child:last-child { + width: 100%; +} + .cbi-dropdown[disabled] { pointer-events: none; opacity: .6; @@ -1122,6 +1138,68 @@ td > table > tbody > tr > td, width: auto; } +.cbi-dynlist { + height: auto; + min-height: 30px; + display: inline-flex; + flex-direction: column; +} + +.cbi-dynlist > .item { + margin: 0 2em 4px 0; + padding: 2px 4px; + border-bottom: 2px solid rgba(0, 0, 0, .26); + position: relative; + pointer-events: none; + cursor: default; +} + +.cbi-dynlist > .item::after { + content: "×"; + position: absolute; + display: inline-flex; + align-items: center; + top: 0; + right: -2em; + bottom: 0; + padding: 0 6px; + border: 1px solid #c44; + font-weight: bold; + color: #c44; + pointer-events: auto; +} + +.cbi-dynlist { + height: auto; + min-height: 30px; + display: inline-flex; + flex-direction: column; +} + +.cbi-dynlist > .item { + margin: 0 2em 4px 0; + padding: 2px 4px; + border-bottom: 2px solid rgba(0, 0, 0, .26); + position: relative; + pointer-events: none; + cursor: default; +} + +.cbi-dynlist > .item::after { + content: "×"; + position: absolute; + display: inline-flex; + align-items: center; + top: 0; + right: -2em; + bottom: 0; + padding: 0 6px; + border: 1px solid #c44; + font-weight: bold; + color: #c44; + pointer-events: auto; +} + /* luci */ diff --git a/themes/luci-theme-openwrt/htdocs/luci-static/openwrt.org/cascade.css b/themes/luci-theme-openwrt/htdocs/luci-static/openwrt.org/cascade.css index 94d6b57296..e650aa55a9 100644 --- a/themes/luci-theme-openwrt/htdocs/luci-static/openwrt.org/cascade.css +++ b/themes/luci-theme-openwrt/htdocs/luci-static/openwrt.org/cascade.css @@ -516,7 +516,8 @@ input.cbi-input-password + img { .td select, .td .cbi-dropdown, -.td input[type=text] { +.td input[type=text], +.cbi-dynlist > .add-item > .cbi-dropdown { width: 100%; } @@ -531,7 +532,7 @@ img.cbi-image-button { vertical-align: middle; } -.btn, .cbi-button { +.btn, .cbi-button, .item::after { padding: 0 .5em; border-radius: 3px; border: 1px solid #aaa; @@ -545,9 +546,11 @@ img.cbi-image-button { font-weight: bold; line-height: 13pt; height: 16pt; + box-sizing: border-box; + cursor: pointer; } -.btn:hover, .cbi-button:hover { +.btn:hover, .cbi-button:hover, .item:hover::after { box-shadow: 0 0 3px #37c; } @@ -1009,7 +1012,8 @@ ul.cbi-tabmenu li.cbi-tab { background: #fff; } -.cbi-dropdown:focus { +.cbi-dropdown:focus, +.cbi-dynlist > .item:focus { outline: 2px solid #4b6e9b; } @@ -1150,11 +1154,60 @@ ul.cbi-tabmenu li.cbi-tab { border-bottom: none; } +.cbi-dropdown[open] > ul.dropdown > li[unselectable] { + opacity: 0.7; +} + +.cbi-dropdown[open] > ul.dropdown > li > input.create-item-input:first-child:last-child { + width: 100%; +} + .cbi-dropdown[disabled] { pointer-events: none; opacity: .6; } +.cbi-dynlist { + height: auto; + min-height: 30px; + min-width: 210px; + max-width: 100%; + width: auto; + display: inline-flex; + flex-direction: column; +} + +.cbi-dynlist > .item { + margin-bottom: 4px; + background: #eee; + padding: 2px 2em 2px 4px; + border: 1px outset #000; + border-radius: 3px; + position: relative; + pointer-events: none; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.cbi-dynlist > .item::after { + content: "×"; + position: absolute; + display: inline-flex; + align-items: center; + top: -1px; + right: -1px; + bottom: -1px; + padding: 0 6px; + border: 1px outset #000; + background: #fff; + border-radius: 0 3px 3px 0; + font-weight: bold; + color: #c44; + pointer-events: auto; + height: auto; +} + input[type="text"] + .cbi-button, input[type="password"] + .cbi-button, select + .cbi-button { @@ -1695,13 +1748,14 @@ select + .cbi-button { height: 1.4em; } - [data-dynlist] > input, input.cbi-input-password { width: calc(100% - 20px); } + .cbi-dynlist, .cbi-dropdown { min-width: 100%; + display: flex; } .btn, .cbi-button {