luci-base, themes: rework dynlist and dropdown widgets
authorJo-Philipp Wich <jo@mein.io>
Sat, 20 Oct 2018 15:56:02 +0000 (17:56 +0200)
committerJo-Philipp Wich <jo@mein.io>
Mon, 5 Nov 2018 10:05:12 +0000 (11:05 +0100)
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
modules/luci-base/htdocs/luci-static/resources/cbi.js
themes/luci-theme-bootstrap/htdocs/luci-static/bootstrap/cascade.css
themes/luci-theme-material/htdocs/luci-static/material/cascade.css
themes/luci-theme-openwrt/htdocs/luci-static/openwrt.org/cascade.css

index 2efa024859b55f9d577e73578fbb216ca8ca2284..3ace96f32250250620e9f71a8c7b6577a616c1d3 100644 (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
-       }
-
-       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);
index 67e19e7d0786bbd8b94c6ba5149bb5d9c75525ea..73e6c3bed6597ebfadf49194f546f294ec42119d 100644 (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;
index a4d71eeb673847568837896eb03aa7d04224c4ea..e62c9be2162542c5ffff58e3b2704d06b12d71cb 100644 (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 */
 
index 94d6b5729644068c1cb552a35f7568757d7992f6..e650aa55a92a658a485a22fe07452fdf700ba6f1 100644 (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 {