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>
This commit is contained in:
parent
6b8fc99fd5
commit
c2b5709988
1 changed files with 260 additions and 145 deletions
|
@ -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;
|
||||
sb.addEventListener('mouseover', this.handleMouseover.bind(this));
|
||||
sb.addEventListener('focus', this.handleFocus.bind(this));
|
||||
|
||||
var li = findParent(ev.target, 'li');
|
||||
if (li) {
|
||||
if (li.parentNode.classList.contains('dropdown'))
|
||||
sbox.setFocus(this, li);
|
||||
canary.addEventListener('focus', this.handleCanaryFocus.bind(this));
|
||||
|
||||
ev.stopPropagation();
|
||||
}
|
||||
});
|
||||
|
||||
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', 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue