From: Jo-Philipp Wich Date: Sat, 20 Oct 2018 08:06:57 +0000 (+0200) Subject: luci-base: cbi.js: rework dropdown implementation X-Git-Url: http://git.openwrt.org/?a=commitdiff_plain;h=c2b570998811accb7a880fe42745ee0278f323e6;hp=6b8fc99fd5897d9a0d959d567a02113ef5b2a328;p=project%2Fluci.git 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 --- 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)); } }