summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJo-Philipp Wich <jo@mein.io>2018-10-20 10:06:57 +0200
committerJo-Philipp Wich <jo@mein.io>2018-11-05 11:01:45 +0100
commitc2b570998811accb7a880fe42745ee0278f323e6 (patch)
tree52d6d406f56ea1b04a584f6172dae4423baf7f53
parent6b8fc99fd5897d9a0d959d567a02113ef5b2a328 (diff)
luci-base: cbi.js: rework dropdown implementation
- Refactor event handler closures into class methods and bind them instead - Fix quirk in dropdown placement calculation - Different dropdown placement strategy on touch devices - Broadcast custom "cbi-dropdown-change" event when value is changed - Implement setValues() method to alter dropdown selection - Prevent creating empty custom values Signed-off-by: Jo-Philipp Wich <jo@mein.io>
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/cbi.js405
1 files changed, 260 insertions, 145 deletions
diff --git a/modules/luci-base/htdocs/luci-static/resources/cbi.js b/modules/luci-base/htdocs/luci-static/resources/cbi.js
index 7248c51779..2efa024859 100644
--- a/modules/luci-base/htdocs/luci-static/resources/cbi.js
+++ b/modules/luci-base/htdocs/luci-static/resources/cbi.js
@@ -1687,16 +1687,30 @@ CBIDropdown = {
s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
});
- ul.style.maxHeight = mh + 'px';
sb.setAttribute('open', '');
- ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
+ if ('ontouchstart' in window) {
+ var scroll = document.documentElement.scrollTop,
+ vpWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
+ vpHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
+
+ ul.style.top = h + 'px';
+ ul.style.left = -rect.left + 'px';
+ ul.style.right = (rect.right - vpWidth) + 'px';
+
+ window.scrollTo(0, (scroll + rect.top - vpHeight * 0.6));
+ }
+ else {
+ ul.style.maxHeight = mh + 'px';
+ ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
+ ul.style.top = ul.style.bottom = '';
+ ul.style[((rect.top + rect.height + eh) > window.innerHeight) ? 'bottom' : 'top'] = rect.height + 'px';
+ }
+
ul.querySelectorAll('[selected] input[type="checkbox"]').forEach(function(c) {
c.checked = true;
});
- ul.style.top = ul.style.bottom = '';
- ul.style[((sb.getBoundingClientRect().top + eh) > window.innerHeight) ? 'bottom' : 'top'] = rect.height + 'px';
ul.classList.add('dropdown');
var pv = ul.cloneNode(true);
@@ -1827,27 +1841,78 @@ CBIDropdown = {
},
saveValues: function(sb, ul) {
- var sel = ul.querySelectorAll('[selected]'),
- div = sb.lastElementChild;
+ var sel = ul.querySelectorAll('li[selected]'),
+ div = sb.lastElementChild,
+ values = [];
while (div.lastElementChild)
div.removeChild(div.lastElementChild);
sel.forEach(function (s) {
+ if (s.hasAttribute('placeholder'))
+ return;
+
div.appendChild(E('input', {
type: 'hidden',
name: s.hasAttribute('name') ? s.getAttribute('name') : (sb.getAttribute('name') || ''),
value: s.hasAttribute('data-value') ? s.getAttribute('data-value') : s.innerText
}));
+
+ values.push({
+ text: s.innerText,
+ value: s.hasAttribute('data-value') ? s.getAttribute('data-value') : s.innerText,
+ element: s
+ });
});
+ var detail = {
+ instance: this,
+ element: sb
+ };
+
+ if (this.mult)
+ detail.values = values;
+ else
+ detail.value = values.length ? values[0] : null;
+
+ sb.dispatchEvent(new CustomEvent('cbi-dropdown-change', {
+ bubbles: true,
+ detail: detail
+ }));
+
cbi_d_update();
},
+ setValues: function(sb, values) {
+ var ul = sb.querySelector('ul');
+
+ if (this.multi) {
+ ul.querySelectorAll('li[data-value]').forEach(function(li) {
+ if (values === null || !(li.getAttribute('data-value') in values))
+ this.toggleItem(sb, li, false);
+ else
+ this.toggleItem(sb, li, true);
+ });
+ }
+ else {
+ var ph = ul.querySelector('li[placeholder]');
+ if (ph)
+ this.toggleItem(sb, ph);
+
+ ul.querySelectorAll('li[data-value]').forEach(function(li) {
+ if (values !== null && (li.getAttribute('data-value') in values))
+ this.toggleItem(sb, li);
+ });
+ }
+ },
+
setFocus: function(sb, elem, scroll) {
if (sb && sb.hasAttribute && sb.hasAttribute('locked-in'))
return;
+ if (sb.target && findParent(sb.target, 'ul.dropdown'))
+ return;
+
document.querySelectorAll('.focus').forEach(function(e) {
if (!matchesElem(e, 'input')) {
e.classList.remove('focus');
@@ -1872,6 +1937,9 @@ CBIDropdown = {
if (!sbox.multi)
val.length = Math.min(val.length, 1);
+ if (val.length === 1 && val[0].length === 0)
+ val.length = 0;
+
val.forEach(function(item) {
var new_item = null;
@@ -1914,6 +1982,166 @@ CBIDropdown = {
document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
});
+ },
+
+ handleClick: function(ev) {
+ var sb = ev.currentTarget;
+
+ if (!sb.hasAttribute('open')) {
+ if (!matchesElem(ev.target, 'input'))
+ this.openDropdown(sb);
+ }
+ else {
+ var li = findParent(ev.target, 'li');
+ if (li && li.parentNode.classList.contains('dropdown'))
+ this.toggleItem(sb, li);
+ }
+
+ ev.preventDefault();
+ ev.stopPropagation();
+ },
+
+ handleKeydown: function(ev) {
+ var sb = ev.currentTarget;
+
+ if (matchesElem(ev.target, 'input'))
+ return;
+
+ if (!sb.hasAttribute('open')) {
+ switch (ev.keyCode) {
+ case 37:
+ case 38:
+ case 39:
+ case 40:
+ this.openDropdown(sb);
+ ev.preventDefault();
+ }
+ }
+ else {
+ var active = findParent(document.activeElement, 'li');
+
+ switch (ev.keyCode) {
+ case 27:
+ this.closeDropdown(sb);
+ break;
+
+ case 13:
+ if (active) {
+ if (!active.hasAttribute('selected'))
+ this.toggleItem(sb, active);
+ this.closeDropdown(sb);
+ ev.preventDefault();
+ }
+ break;
+
+ case 32:
+ if (active) {
+ this.toggleItem(sb, active);
+ ev.preventDefault();
+ }
+ break;
+
+ case 38:
+ if (active && active.previousElementSibling) {
+ this.setFocus(sb, active.previousElementSibling);
+ ev.preventDefault();
+ }
+ break;
+
+ case 40:
+ if (active && active.nextElementSibling) {
+ this.setFocus(sb, active.nextElementSibling);
+ ev.preventDefault();
+ }
+ break;
+ }
+ }
+ },
+
+ handleDropdownClose: function(ev) {
+ var sb = ev.currentTarget;
+
+ this.closeDropdown(sb, true);
+ },
+
+ handleDropdownSelect: function(ev) {
+ var sb = ev.currentTarget,
+ li = findParent(ev.target, 'li');
+
+ if (!li)
+ return;
+
+ this.toggleItem(sb, li);
+ this.closeDropdown(sb, true);
+ },
+
+ handleMouseover: function(ev) {
+ var sb = ev.currentTarget;
+
+ if (!sb.hasAttribute('open'))
+ return;
+
+ var li = findParent(ev.target, 'li');
+
+ if (li && li.parentNode.classList.contains('dropdown'))
+ this.setFocus(sb, li);
+ },
+
+ handleFocus: function(ev) {
+ var sb = ev.currentTarget;
+
+ document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
+ if (s !== sb || sb.hasAttribute('open'))
+ s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
+ });
+ },
+
+ handleCanaryFocus: function(ev) {
+ this.closeDropdown(ev.currentTarget.parentNode);
+ },
+
+ handleCreateKeydown: function(ev) {
+ var input = ev.currentTarget,
+ sb = findParent(input, '.cbi-dropdown');
+
+ switch (ev.keyCode) {
+ case 13:
+ ev.preventDefault();
+
+ if (input.classList.contains('cbi-input-invalid'))
+ return;
+
+ this.createItems(sb, input.value);
+ input.value = '';
+ input.blur();
+ break;
+ }
+ },
+
+ handleCreateFocus: function(ev) {
+ var input = ev.currentTarget,
+ cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
+ sb = findParent(input, '.cbi-dropdown');
+
+ if (cbox)
+ cbox.checked = true;
+
+ sb.setAttribute('locked-in', '');
+ },
+
+ handleCreateBlur: function(ev) {
+ var input = ev.currentTarget,
+ cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
+ sb = findParent(input, '.cbi-dropdown');
+
+ if (cbox)
+ cbox.checked = false;
+
+ sb.removeAttribute('locked-in');
+ },
+
+ handleCreateClick: function(ev) {
+ ev.currentTarget.querySelector(this.create).focus();
}
};
@@ -1929,9 +2157,7 @@ function cbi_dropdown_init(sb) {
this.create = sb.getAttribute('item-create') || '.create-item-input';
this.template = sb.getAttribute('item-template') || 'script[type="item-template"]';
- var sbox = this,
- ul = sb.querySelector('ul'),
- items = ul.querySelectorAll('li'),
+ var ul = sb.querySelector('ul'),
more = sb.appendChild(E('span', { class: 'more', tabindex: -1 }, '···')),
open = sb.appendChild(E('span', { class: 'open', tabindex: -1 }, '▾')),
canary = sb.appendChild(E('div')),
@@ -1940,15 +2166,23 @@ function cbi_dropdown_init(sb) {
n = 0;
if (this.multi) {
+ var items = ul.querySelectorAll('li');
+
for (var i = 0; i < items.length; i++) {
- sbox.transformItem(sb, items[i]);
+ this.transformItem(sb, items[i]);
if (items[i].hasAttribute('selected') && ndisplay-- > 0)
items[i].setAttribute('display', n++);
}
}
else {
- var sel = sb.querySelectorAll('[selected]');
+ if (this.optional && !ul.querySelector('li[data-value=""]')) {
+ var placeholder = E('li', { placeholder: '' }, this.placeholder);
+ ul.firstChild ? ul.insertBefore(placeholder, ul.firstChild) : ul.appendChild(placeholder);
+ }
+
+ var items = ul.querySelectorAll('li'),
+ sel = sb.querySelectorAll('[selected]');
sel.forEach(function(s) {
s.removeAttribute('selected');
@@ -1961,14 +2195,9 @@ function cbi_dropdown_init(sb) {
}
ndisplay--;
-
- if (this.optional && !ul.querySelector('li[data-value=""]')) {
- var placeholder = E('li', { placeholder: '' }, this.placeholder);
- ul.firstChild ? ul.insertBefore(placeholder, ul.firstChild) : ul.appendChild(placeholder);
- }
}
- sbox.saveValues(sb, ul);
+ this.saveValues(sb, ul);
ul.setAttribute('tabindex', -1);
sb.setAttribute('tabindex', 0);
@@ -1986,148 +2215,34 @@ function cbi_dropdown_init(sb) {
more.innerHTML = (ndisplay === this.display_items) ? this.placeholder : '···';
- sb.addEventListener('click', function(ev) {
- if (!this.hasAttribute('open')) {
- if (!matchesElem(ev.target, 'input'))
- sbox.openDropdown(this);
- }
- else {
- var li = findParent(ev.target, 'li');
- if (li && li.parentNode.classList.contains('dropdown'))
- sbox.toggleItem(this, li);
- }
-
- ev.preventDefault();
- ev.stopPropagation();
- });
-
- sb.addEventListener('keydown', function(ev) {
- if (matchesElem(ev.target, 'input'))
- return;
-
- if (!this.hasAttribute('open')) {
- switch (ev.keyCode) {
- case 37:
- case 38:
- case 39:
- case 40:
- sbox.openDropdown(this);
- ev.preventDefault();
- }
- }
- else
- {
- var active = findParent(document.activeElement, 'li');
-
- switch (ev.keyCode) {
- case 27:
- sbox.closeDropdown(this);
- break;
-
- case 13:
- if (active) {
- if (!active.hasAttribute('selected'))
- sbox.toggleItem(this, active);
- sbox.closeDropdown(this);
- ev.preventDefault();
- }
- break;
-
- case 32:
- if (active) {
- sbox.toggleItem(this, active);
- ev.preventDefault();
- }
- break;
-
- case 38:
- if (active && active.previousElementSibling) {
- sbox.setFocus(this, active.previousElementSibling);
- ev.preventDefault();
- }
- break;
-
- case 40:
- if (active && active.nextElementSibling) {
- sbox.setFocus(this, active.nextElementSibling);
- ev.preventDefault();
- }
- break;
- }
- }
- });
-
- sb.addEventListener('cbi-dropdown-close', function(ev) {
- sbox.closeDropdown(this, true);
- });
+ sb.addEventListener('click', this.handleClick.bind(this));
+ sb.addEventListener('keydown', this.handleKeydown.bind(this));
+ sb.addEventListener('cbi-dropdown-close', this.handleDropdownClose.bind(this));
+ sb.addEventListener('cbi-dropdown-select', this.handleDropdownSelect.bind(this));
if ('ontouchstart' in window) {
sb.addEventListener('touchstart', function(ev) { ev.stopPropagation(); });
- window.addEventListener('touchstart', sbox.closeAllDropdowns);
+ window.addEventListener('touchstart', this.closeAllDropdowns);
}
else {
- sb.addEventListener('mouseover', function(ev) {
- if (!this.hasAttribute('open'))
- return;
-
- var li = findParent(ev.target, 'li');
- if (li) {
- if (li.parentNode.classList.contains('dropdown'))
- sbox.setFocus(this, li);
-
- ev.stopPropagation();
- }
- });
+ sb.addEventListener('mouseover', this.handleMouseover.bind(this));
+ sb.addEventListener('focus', this.handleFocus.bind(this));
- sb.addEventListener('focus', function(ev) {
- document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
- if (s !== this || this.hasAttribute('open'))
- s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
- });
- });
+ canary.addEventListener('focus', this.handleCanaryFocus.bind(this));
- canary.addEventListener('focus', function(ev) {
- sbox.closeDropdown(this.parentNode);
- });
-
- window.addEventListener('mouseover', sbox.setFocus);
- window.addEventListener('click', sbox.closeAllDropdowns);
+ window.addEventListener('mouseover', this.setFocus);
+ window.addEventListener('click', this.closeAllDropdowns);
}
if (create) {
- create.addEventListener('keydown', function(ev) {
- switch (ev.keyCode) {
- case 13:
- ev.preventDefault();
-
- if (this.classList.contains('cbi-input-invalid'))
- return;
-
- sbox.createItems(sb, this.value);
- this.value = '';
- this.blur();
- break;
- }
- });
-
- create.addEventListener('focus', function(ev) {
- var cbox = findParent(this, 'li').querySelector('input[type="checkbox"]');
- if (cbox) cbox.checked = true;
- sb.setAttribute('locked-in', '');
- });
-
- create.addEventListener('blur', function(ev) {
- var cbox = findParent(this, 'li').querySelector('input[type="checkbox"]');
- if (cbox) cbox.checked = false;
- sb.removeAttribute('locked-in');
- });
+ create.addEventListener('keydown', this.handleCreateKeydown.bind(this));
+ create.addEventListener('focus', this.handleCreateFocus.bind(this));
+ create.addEventListener('blur', this.handleCreateBlur.bind(this));
var li = findParent(create, 'li');
li.setAttribute('unselectable', '');
- li.addEventListener('click', function(ev) {
- this.querySelector(sbox.create).focus();
- });
+ li.addEventListener('click', this.handleCreateClick.bind(this));
}
}