luci-base, themes: rework dynlist and dropdown widgets

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
This commit is contained in:
Jo-Philipp Wich 2018-10-20 17:56:02 +02:00
parent bd614de514
commit 7c78218339
4 changed files with 358 additions and 310 deletions

View file

@ -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
}
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') ]);
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);
}
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);
sb.lastElementChild.appendChild(E('li', {
'data-value': i,
'selected': (i == obj.value) ? '' : null
}, values[i]));
}
var optman = document.createElement("option");
optman.value = "";
optman.appendChild(document.createTextNode(typeof(man) === 'string' ? man : _('-- custom --')));
sel.appendChild(optman);
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.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();
}
}
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);
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)
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;
var hidden = item.querySelector('input[type="hidden"]');
if (hidden && hidden.value === value)
exists = true;
else if (!hidden || hidden.value >= value)
exists = !!item.parentNode.insertBefore(new_item, item);
});
},
removeItem: function(dl, item) {
var sb = dl.querySelector('.cbi-dropdown');
if (sb) {
var value = item.querySelector('input[type="hidden"]').value;
sb.querySelectorAll('ul > li').forEach(function(li) {
if (li.getAttribute('data-value') === value)
li.removeAttribute('unselectable');
});
}
item.parentNode.removeChild(item);
},
handleClick: function(ev) {
var dl = ev.currentTarget,
item = findParent(ev.target, '.item');
if (item) {
this.removeItem(dl, item);
}
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 = '';
}
}
},
handleDropdownChange: function(ev) {
var dl = ev.currentTarget,
sbIn = ev.detail.instance,
sbEl = ev.detail.element,
sbVal = ev.detail.value;
if (sbVal === null)
return;
sbIn.setValues(sbEl, null);
sbVal.element.setAttribute('unselectable', '');
this.addItem(dl, sbVal.value, sbVal.text, true);
},
handleKeydown: function(ev) {
var dl = ev.currentTarget,
item = findParent(ev.target, '.item');
if (item) {
switch (ev.keyCode) {
case 8: /* backspace */
if (item.previousElementSibling)
item.previousElementSibling.focus();
this.removeItem(dl, item);
break;
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();
}
ev.preventDefault();
break;
}
}
}
};
function cbi_dynlist_init(dl, datatype, optional, choices)
{
var prefix = parent.getAttribute('data-prefix');
var holder = parent.getAttribute('data-placeholder');
if (!(this instanceof cbi_dynlist_init))
return new cbi_dynlist_init(dl, datatype, optional, choices);
var values;
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
})));
function cbi_dynlist_redraw(focus, add, del)
{
values = [ ];
if (choices)
cbi_combobox_init(dl.lastElementChild.lastElementChild, choices, '', _('-- custom --'));
else
dl.lastElementChild.appendChild(E('div', { 'class': 'cbi-button cbi-button-add' }, '+'));
while (parent.firstChild) {
var n = parent.firstChild;
var i = +n.index;
dl.addEventListener('click', this.handleClick.bind(this));
dl.addEventListener('keydown', this.handleKeydown.bind(this));
dl.addEventListener('cbi-dropdown-change', this.handleDropdownChange.bind(this));
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;
}
try {
var values = JSON.parse(dl.getAttribute('data-values') || '[]');
parent.removeChild(n);
}
if (add >= 0) {
focus = add+1;
values.splice(focus, 0, '');
}
else if (values.length == 0) {
focus = 0;
values.push('');
}
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();
/* force cursor to end */
var v = t.value;
t.value = ' '
t.value = v;
}
}
b.addEventListener('click', cbi_dynlist_btnclick);
}
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);
}
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;
}
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);
if (del) {
if (ev.preventDefault)
ev.preventDefault();
var focus = se.index;
if (ev.keyCode == 8)
focus = -focus+1;
cbi_dynlist_redraw(focus, -1, se.index);
return false;
}
break;
/* enter */
case 13:
cbi_dynlist_redraw(-1, se.index, -1);
break;
/* arrow up */
case 38:
if (prev)
prev.focus();
break;
/* arrow down */
case 40:
if (next)
next.focus();
break;
}
return true;
}
function cbi_dynlist_btnclick(ev)
{
ev = ev ? ev : window.event;
var se = ev.target ? ev.target : ev.srcElement;
var input = se.previousSibling;
while (input && input.name != prefix)
input = input.previousSibling;
if (se.classList.contains('cbi-button-remove')) {
input.value = '';
cbi_dynlist_keydown({
target: input,
keyCode: 8
});
}
else {
cbi_dynlist_keydown({
target: input,
keyCode: 13
});
}
return false;
}
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);

View file

@ -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;

View file

@ -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 */

View file

@ -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 {