diff options
author | Jo-Philipp Wich <jo@mein.io> | 2019-02-07 18:29:29 +0100 |
---|---|---|
committer | Jo-Philipp Wich <jo@mein.io> | 2019-07-07 15:25:49 +0200 |
commit | 558f8c3b2a40d187068e339be70d4173b555d4a5 (patch) | |
tree | 6a77bb88b7cae7347a30472097521be887b2021f /modules/luci-base | |
parent | e4c29c6db4da21e5d6abb7b89969ebbbae74566c (diff) |
luci-base: move dropdown, combox and dynlist widget code to L.ui
Move the widget code to the ui class and replace it with compatibility
shims in cbi.js
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
Diffstat (limited to 'modules/luci-base')
-rw-r--r-- | modules/luci-base/htdocs/luci-static/resources/cbi.js | 862 | ||||
-rw-r--r-- | modules/luci-base/htdocs/luci-static/resources/ui.js | 1051 |
2 files changed, 1083 insertions, 830 deletions
diff --git a/modules/luci-base/htdocs/luci-static/resources/cbi.js b/modules/luci-base/htdocs/luci-static/resources/cbi.js index 3131ccbaa..745f01244 100644 --- a/modules/luci-base/htdocs/luci-static/resources/cbi.js +++ b/modules/luci-base/htdocs/luci-static/resources/cbi.js @@ -738,6 +738,11 @@ function cbi_d_update() { function cbi_init() { var nodes; + document.querySelectorAll('.cbi-dropdown').forEach(function(node) { + cbi_dropdown_init(node); + node.addEventListener('cbi-dropdown-change', cbi_d_update); + }); + nodes = document.querySelectorAll('[data-strings]'); for (var i = 0, node; (node = nodes[i]) !== undefined; i++) { @@ -772,8 +777,8 @@ function cbi_init() { nodes = document.querySelectorAll('[data-choices]'); for (var i = 0, node; (node = nodes[i]) !== undefined; i++) { - var choices = JSON.parse(node.getAttribute('data-choices')); - var options = {}; + var choices = JSON.parse(node.getAttribute('data-choices')), + options = {}; for (var j = 0; j < choices[0].length; j++) options[choices[0][j]] = choices[1][j]; @@ -781,15 +786,24 @@ function cbi_init() { var def = (node.getAttribute('data-optional') === 'true') ? node.placeholder || '' : null; - cbi_combobox_init(node, options, def, - node.getAttribute('data-manual')); + var cb = new L.ui.Combobox(node.value, options, { + name: node.getAttribute('name'), + sort: choices[0], + select_placeholder: def || _('-- Please choose --'), + custom_placeholder: node.getAttribute('data-manual') || _('-- custom --') + }); + + var n = cb.render(); + n.addEventListener('cbi-dropdown-change', cbi_d_update); + node.parentNode.replaceChild(n, node); } nodes = document.querySelectorAll('[data-dynlist]'); for (var i = 0, node; (node = nodes[i]) !== undefined; i++) { - var choices = JSON.parse(node.getAttribute('data-dynlist')); - var options = null; + var choices = JSON.parse(node.getAttribute('data-dynlist')), + values = JSON.parse(node.getAttribute('data-values') || '[]'), + options = null; if (choices[0] && choices[0].length) { options = {}; @@ -798,8 +812,17 @@ function cbi_init() { options[choices[0][j]] = choices[1][j]; } - cbi_dynlist_init(node, choices[2], choices[3], options); - node.addEventListener('cbi-dynlist-change', cbi_d_update); + var dl = new L.ui.DynamicList(values, options, { + name: node.getAttribute('data-prefix'), + sort: choices[0], + datatype: choices[2], + optional: choices[3], + placeholder: node.getAttribute('data-placeholder') + }); + + var n = dl.render(); + n.addEventListener('cbi-dynlist-change', cbi_d_update); + node.parentNode.replaceChild(n, node); } nodes = document.querySelectorAll('[data-type]'); @@ -809,11 +832,6 @@ function cbi_init() { node.getAttribute('data-type')); } - document.querySelectorAll('.cbi-dropdown').forEach(function(node) { - cbi_dropdown_init(node); - node.addEventListener('cbi-dropdown-change', cbi_d_update); - }); - document.querySelectorAll('[data-browser]').forEach(cbi_browser_init); document.querySelectorAll('.cbi-tooltip:not(:empty)').forEach(function(s) { @@ -835,46 +853,6 @@ function cbi_init() { cbi_d_update(); } -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 --'), - 'data-type': obj.getAttribute('data-type'), - 'data-optional': obj.getAttribute('data-optional') - }, [ 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) { - sb.lastElementChild.appendChild(E('li', { - 'data-value': i, - 'selected': (i == obj.value) ? '' : null - }, values[i])); - } - - 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 --')) - }) - ])); - - sb.value = obj.value; - obj.parentNode.replaceChild(sb, obj); -} - function cbi_filebrowser(id, defpath) { var field = L.dom.elem(id) ? id : document.getElementById(id); var browser = window.open( @@ -898,171 +876,6 @@ function cbi_browser_init(field) }), field.nextSibling); } -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); - }); - - dl.dispatchEvent(new CustomEvent('cbi-dynlist-change', { - bubbles: true, - detail: { - instance: this, - element: dl, - value: value, - add: true - } - })); - }, - - removeItem: function(dl, item) { - var value = item.querySelector('input[type="hidden"]').value; - var sb = dl.querySelector('.cbi-dropdown'); - if (sb) - sb.querySelectorAll('ul > li').forEach(function(li) { - if (li.getAttribute('data-value') === value) - li.removeAttribute('unselectable'); - }); - - item.parentNode.removeChild(item); - - dl.dispatchEvent(new CustomEvent('cbi-dynlist-change', { - bubbles: true, - detail: { - instance: this, - element: dl, - value: value, - remove: true - } - })); - }, - - 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) -{ - 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', - 'placeholder': dl.getAttribute('data-placeholder'), - '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' }, '+')); - - dl.addEventListener('click', this.handleClick.bind(this)); - dl.addEventListener('keydown', this.handleKeydown.bind(this)); - dl.addEventListener('cbi-dropdown-change', this.handleDropdownChange.bind(this)); - - try { - var values = JSON.parse(dl.getAttribute('data-values') || '[]'); - - 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); - } - catch (e) {} -} - -cbi_dynlist_init.prototype = CBIDynamicList; - - function cbi_validate_form(form, errmsg) { /* if triggered by a section removal or addition, don't validate */ @@ -1445,620 +1258,11 @@ if (typeof(window.CustomEvent) !== 'function') { window.CustomEvent = CustomEvent; } -CBIDropdown = { - openDropdown: function(sb) { - var st = window.getComputedStyle(sb, null), - ul = sb.querySelector('ul'), - li = ul.querySelectorAll('li'), - fl = findParent(sb, '.cbi-value-field'), - sel = ul.querySelector('[selected]'), - rect = sb.getBoundingClientRect(), - items = Math.min(this.dropdown_items, li.length); - - document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) { - s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {})); - }); - - sb.setAttribute('open', ''); - - var pv = ul.cloneNode(true); - pv.classList.add('preview'); - - if (fl) - fl.classList.add('cbi-dropdown-open'); - - if ('ontouchstart' in window) { - var vpWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0), - vpHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0), - scrollFrom = window.pageYOffset, - scrollTo = scrollFrom + rect.top - vpHeight * 0.5, - start = null; - - ul.style.top = sb.offsetHeight + 'px'; - ul.style.left = -rect.left + 'px'; - ul.style.right = (rect.right - vpWidth) + 'px'; - ul.style.maxHeight = (vpHeight * 0.5) + 'px'; - ul.style.WebkitOverflowScrolling = 'touch'; - - var scrollStep = function(timestamp) { - if (!start) { - start = timestamp; - ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0; - } - - var duration = Math.max(timestamp - start, 1); - if (duration < 100) { - document.body.scrollTop = scrollFrom + (scrollTo - scrollFrom) * (duration / 100); - window.requestAnimationFrame(scrollStep); - } - else { - document.body.scrollTop = scrollTo; - } - }; - - window.requestAnimationFrame(scrollStep); - } - else { - ul.style.maxHeight = '1px'; - ul.style.top = ul.style.bottom = ''; - - window.requestAnimationFrame(function() { - var height = items * li[Math.max(0, li.length - 2)].offsetHeight; - - ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0; - ul.style[((rect.top + rect.height + height) > window.innerHeight) ? 'bottom' : 'top'] = rect.height + 'px'; - ul.style.maxHeight = height + 'px'; - }); - } - - ul.querySelectorAll('[selected] input[type="checkbox"]').forEach(function(c) { - c.checked = true; - }); - - ul.classList.add('dropdown'); - - sb.insertBefore(pv, ul.nextElementSibling); - - li.forEach(function(l) { - l.setAttribute('tabindex', 0); - }); - - sb.lastElementChild.setAttribute('tabindex', 0); - - this.setFocus(sb, sel || li[0], true); - }, - - closeDropdown: function(sb, no_focus) { - if (!sb.hasAttribute('open')) - return; - - var pv = sb.querySelector('ul.preview'), - ul = sb.querySelector('ul.dropdown'), - li = ul.querySelectorAll('li'), - fl = findParent(sb, '.cbi-value-field'); - - li.forEach(function(l) { l.removeAttribute('tabindex'); }); - sb.lastElementChild.removeAttribute('tabindex'); - - sb.removeChild(pv); - sb.removeAttribute('open'); - sb.style.width = sb.style.height = ''; - - ul.classList.remove('dropdown'); - ul.style.top = ul.style.bottom = ul.style.maxHeight = ''; - - if (fl) - fl.classList.remove('cbi-dropdown-open'); - - if (!no_focus) - this.setFocus(sb, sb); - - this.saveValues(sb, ul); - }, - - toggleItem: function(sb, li, force_state) { - if (li.hasAttribute('unselectable')) - return; - - if (this.multi) { - var cbox = li.querySelector('input[type="checkbox"]'), - items = li.parentNode.querySelectorAll('li'), - label = sb.querySelector('ul.preview'), - sel = li.parentNode.querySelectorAll('[selected]').length, - more = sb.querySelector('.more'), - ndisplay = this.display_items, - n = 0; - - if (li.hasAttribute('selected')) { - if (force_state !== true) { - if (sel > 1 || this.optional) { - li.removeAttribute('selected'); - cbox.checked = cbox.disabled = false; - sel--; - } - else { - cbox.disabled = true; - } - } - } - else { - if (force_state !== false) { - li.setAttribute('selected', ''); - cbox.checked = true; - cbox.disabled = false; - sel++; - } - } - - while (label.firstElementChild) - label.removeChild(label.firstElementChild); - - for (var i = 0; i < items.length; i++) { - items[i].removeAttribute('display'); - if (items[i].hasAttribute('selected')) { - if (ndisplay-- > 0) { - items[i].setAttribute('display', n++); - label.appendChild(items[i].cloneNode(true)); - } - var c = items[i].querySelector('input[type="checkbox"]'); - if (c) - c.disabled = (sel == 1 && !this.optional); - } - } - - if (ndisplay < 0) - sb.setAttribute('more', ''); - else - sb.removeAttribute('more'); - - if (ndisplay === this.display_items) - sb.setAttribute('empty', ''); - else - sb.removeAttribute('empty'); - - more.innerHTML = (ndisplay === this.display_items) ? this.placeholder : '···'; - } - else { - var sel = li.parentNode.querySelector('[selected]'); - if (sel) { - sel.removeAttribute('display'); - sel.removeAttribute('selected'); - } - - li.setAttribute('display', 0); - li.setAttribute('selected', ''); - - this.closeDropdown(sb, true); - } - - this.saveValues(sb, li.parentNode); - }, - - transformItem: function(sb, li) { - var cbox = E('form', {}, E('input', { type: 'checkbox', tabindex: -1, onclick: 'event.preventDefault()' })), - label = E('label'); - - while (li.firstChild) - label.appendChild(li.firstChild); - - li.appendChild(cbox); - li.appendChild(label); - }, - - saveValues: function(sb, ul) { - var sel = ul.querySelectorAll('li[selected]'), - div = sb.lastElementChild, - strval = '', - values = []; - - while (div.lastElementChild) - div.removeChild(div.lastElementChild); - - sel.forEach(function (s) { - if (s.hasAttribute('placeholder')) - return; - - var v = { - text: s.innerText, - value: s.hasAttribute('data-value') ? s.getAttribute('data-value') : s.innerText, - element: s - }; - - div.appendChild(E('input', { - type: 'hidden', - name: s.hasAttribute('name') ? s.getAttribute('name') : (sb.getAttribute('name') || ''), - value: v.value - })); - - values.push(v); - - strval += strval.length ? ' ' + v.value : v.value; - }); - - var detail = { - instance: this, - element: sb - }; - - if (this.multi) - detail.values = values; - else - detail.value = values.length ? values[0] : null; - - sb.value = strval; - - sb.dispatchEvent(new CustomEvent('cbi-dropdown-change', { - bubbles: true, - detail: detail - })); - }, - - 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'); - e.blur(); - } - }); - - if (elem) { - elem.focus(); - elem.classList.add('focus'); - - if (scroll) - elem.parentNode.scrollTop = elem.offsetTop - elem.parentNode.offsetTop; - } - }, - - createItems: function(sb, value) { - var sbox = this, - val = (value || '').trim(), - ul = sb.querySelector('ul'); - - if (!sbox.multi) - val = val.length ? [ val ] : []; - else - val = val.length ? val.split(/\s+/) : []; - - val.forEach(function(item) { - var new_item = null; - - ul.childNodes.forEach(function(li) { - if (li.getAttribute && li.getAttribute('data-value') === item) - new_item = li; - }); - - if (!new_item) { - var markup, - tpl = sb.querySelector(sbox.template); - - if (tpl) - markup = (tpl.textContent || tpl.innerHTML || tpl.firstChild.data).replace(/^<!--|-->$/, '').trim(); - else - markup = '<li data-value="{{value}}">{{value}}</li>'; - - new_item = E(markup.replace(/{{value}}/g, item)); - - if (sbox.multi) { - sbox.transformItem(sb, new_item); - } - else { - var old = ul.querySelector('li[created]'); - if (old) - ul.removeChild(old); - - new_item.setAttribute('created', ''); - } - - new_item = ul.insertBefore(new_item, ul.lastElementChild); - } - - sbox.toggleItem(sb, new_item, true); - sbox.setFocus(sb, new_item, true); - }); - }, - - closeAllDropdowns: function() { - 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); - else if (li && li.parentNode.classList.contains('preview')) - this.closeDropdown(sb); - } - - 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(); - } -}; - function cbi_dropdown_init(sb) { - if (!(this instanceof cbi_dropdown_init)) - return new cbi_dropdown_init(sb); - - this.multi = sb.hasAttribute('multiple'); - this.optional = sb.hasAttribute('optional'); - this.placeholder = sb.getAttribute('placeholder') || '---'; - this.display_items = parseInt(sb.getAttribute('display-items') || 3); - this.dropdown_items = parseInt(sb.getAttribute('dropdown-items') || 5); - this.create = sb.getAttribute('item-create') || '.create-item-input'; - this.template = sb.getAttribute('item-template') || 'script[type="item-template"]'; - - 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')), - create = sb.querySelector(this.create), - ndisplay = this.display_items, - n = 0; - - if (this.multi) { - var items = ul.querySelectorAll('li'); - - for (var i = 0; i < items.length; i++) { - this.transformItem(sb, items[i]); - - if (items[i].hasAttribute('selected') && ndisplay-- > 0) - items[i].setAttribute('display', n++); - } - } - else { - 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'); - }); - - var s = sel[0] || items[0]; - if (s) { - s.setAttribute('selected', ''); - s.setAttribute('display', n++); - } - - ndisplay--; - } - - this.saveValues(sb, ul); - - ul.setAttribute('tabindex', -1); - sb.setAttribute('tabindex', 0); - - if (ndisplay < 0) - sb.setAttribute('more', '') - else - sb.removeAttribute('more'); - - if (ndisplay === this.display_items) - sb.setAttribute('empty', '') - else - sb.removeAttribute('empty'); - - more.innerHTML = (ndisplay === this.display_items) ? this.placeholder : '···'; - - - 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', this.closeAllDropdowns); - } - else { - sb.addEventListener('mouseover', this.handleMouseover.bind(this)); - sb.addEventListener('focus', this.handleFocus.bind(this)); - - canary.addEventListener('focus', this.handleCanaryFocus.bind(this)); - - window.addEventListener('mouseover', this.setFocus); - window.addEventListener('click', this.closeAllDropdowns); - } - - if (create) { - 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', this.handleCreateClick.bind(this)); - } + var dl = new L.ui.Dropdown(sb, null, { name: sb.getAttribute('name') }); + return dl.bind(sb); } -cbi_dropdown_init.prototype = CBIDropdown; - function cbi_update_table(table, data, placeholder) { var target = isElem(table) ? table : document.querySelector(table); diff --git a/modules/luci-base/htdocs/luci-static/resources/ui.js b/modules/luci-base/htdocs/luci-static/resources/ui.js index c69f1cb0a..5914b5ee6 100644 --- a/modules/luci-base/htdocs/luci-static/resources/ui.js +++ b/modules/luci-base/htdocs/luci-static/resources/ui.js @@ -1,7 +1,1031 @@ +'use strict'; + var modalDiv = null, tooltipDiv = null, tooltipTimeout = null; +var UIElement = L.Class.extend({ + getValue: function() { + if (L.dom.matches(this.node, 'select') || L.dom.matches(this.node, 'input')) + return this.node.value; + + return null; + }, + + setValue: function(value) { + if (L.dom.matches(this.node, 'select') || L.dom.matches(this.node, 'input')) + this.node.value = value; + }, + + isValid: function() { + return true; + }, + + registerEvents: function(targetNode, synevent, events) { + var dispatchFn = L.bind(function(ev) { + this.node.dispatchEvent(new CustomEvent(synevent, { bubbles: true })); + }, this); + + for (var i = 0; i < events.length; i++) + targetNode.addEventListener(events[i], dispatchFn); + }, + + setUpdateEvents: function(targetNode /*, ... */) { + this.registerEvents(targetNode, 'widget-update', this.varargs(arguments, 1)); + }, + + setChangeEvents: function(targetNode /*, ... */) { + this.registerEvents(targetNode, 'widget-change', this.varargs(arguments, 1)); + } +}); + +var UIDropdown = UIElement.extend({ + __init__: function(value, choices, options) { + if (typeof(choices) != 'object') + choices = {}; + + if (!Array.isArray(value)) + this.values = (value != null) ? [ value ] : []; + else + this.values = value; + + this.choices = choices; + this.options = Object.assign({ + sort: true, + multi: Array.isArray(value), + optional: true, + select_placeholder: _('-- Please choose --'), + custom_placeholder: _('-- custom --'), + display_items: 3, + dropdown_items: 5, + create: false, + create_query: '.create-item-input', + create_template: 'script[type="item-template"]' + }, options); + }, + + render: function() { + var sb = E('div', { + 'id': this.options.id, + 'class': 'cbi-dropdown', + 'multiple': this.options.multi ? '' : null, + 'optional': this.options.optional ? '' : null, + }, E('ul')); + + var keys = Object.keys(this.choices); + + if (this.options.sort === true) + keys.sort(); + else if (Array.isArray(this.options.sort)) + keys = this.options.sort; + + if (this.options.create) + for (var i = 0; i < this.values.length; i++) + if (!this.choices.hasOwnProperty(this.values[i])) + keys.push(this.values[i]); + + for (var i = 0; i < keys.length; i++) + sb.lastElementChild.appendChild(E('li', { + 'data-value': keys[i], + 'selected': (this.values.indexOf(keys[i]) > -1) ? '' : null + }, this.choices[keys[i]] || keys[i])); + + if (this.options.create) { + var createEl = E('input', { + 'type': 'text', + 'class': 'create-item-input', + 'placeholder': this.options.custom_placeholder || this.options.placeholder + }); + + if (this.options.datatype) + L.ui.addValidator(createEl, this.options.datatype, true, 'blur', 'keyup'); + + sb.lastElementChild.appendChild(E('li', { 'data-value': '-' }, createEl)); + } + + return this.bind(sb); + }, + + bind: function(sb) { + var o = this.options; + + o.multi = sb.hasAttribute('multiple'); + o.optional = sb.hasAttribute('optional'); + o.placeholder = sb.getAttribute('placeholder') || o.placeholder; + o.display_items = parseInt(sb.getAttribute('display-items') || o.display_items); + o.dropdown_items = parseInt(sb.getAttribute('dropdown-items') || o.dropdown_items); + o.create_query = sb.getAttribute('item-create') || o.create_query; + o.create_template = sb.getAttribute('item-template') || o.create_template; + + 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')), + create = sb.querySelector(this.options.create_query), + ndisplay = this.options.display_items, + n = 0; + + if (this.options.multi) { + var items = ul.querySelectorAll('li'); + + for (var i = 0; i < items.length; i++) { + this.transformItem(sb, items[i]); + + if (items[i].hasAttribute('selected') && ndisplay-- > 0) + items[i].setAttribute('display', n++); + } + } + else { + if (this.options.optional && !ul.querySelector('li[data-value=""]')) { + var placeholder = E('li', { placeholder: '' }, + this.options.select_placeholder || this.options.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'); + }); + + var s = sel[0] || items[0]; + if (s) { + s.setAttribute('selected', ''); + s.setAttribute('display', n++); + } + + ndisplay--; + } + + this.saveValues(sb, ul); + + ul.setAttribute('tabindex', -1); + sb.setAttribute('tabindex', 0); + + if (ndisplay < 0) + sb.setAttribute('more', '') + else + sb.removeAttribute('more'); + + if (ndisplay == this.options.display_items) + sb.setAttribute('empty', '') + else + sb.removeAttribute('empty'); + + more.innerHTML = (ndisplay == this.options.display_items) + ? (this.options.select_placeholder || this.options.placeholder) : '···'; + + + 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', this.closeAllDropdowns); + } + else { + sb.addEventListener('mouseover', this.handleMouseover.bind(this)); + sb.addEventListener('focus', this.handleFocus.bind(this)); + + canary.addEventListener('focus', this.handleCanaryFocus.bind(this)); + + window.addEventListener('mouseover', this.setFocus); + window.addEventListener('click', this.closeAllDropdowns); + } + + if (create) { + 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', this.handleCreateClick.bind(this)); + } + + this.node = sb; + + this.setUpdateEvents(sb, 'cbi-dropdown-open', 'cbi-dropdown-close'); + this.setChangeEvents(sb, 'cbi-dropdown-change', 'cbi-dropdown-close'); + + L.dom.bindClassInstance(sb, this); + + return sb; + }, + + openDropdown: function(sb) { + var st = window.getComputedStyle(sb, null), + ul = sb.querySelector('ul'), + li = ul.querySelectorAll('li'), + fl = findParent(sb, '.cbi-value-field'), + sel = ul.querySelector('[selected]'), + rect = sb.getBoundingClientRect(), + items = Math.min(this.options.dropdown_items, li.length); + + document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) { + s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {})); + }); + + sb.setAttribute('open', ''); + + var pv = ul.cloneNode(true); + pv.classList.add('preview'); + + if (fl) + fl.classList.add('cbi-dropdown-open'); + + if ('ontouchstart' in window) { + var vpWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0), + vpHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0), + scrollFrom = window.pageYOffset, + scrollTo = scrollFrom + rect.top - vpHeight * 0.5, + start = null; + + ul.style.top = sb.offsetHeight + 'px'; + ul.style.left = -rect.left + 'px'; + ul.style.right = (rect.right - vpWidth) + 'px'; + ul.style.maxHeight = (vpHeight * 0.5) + 'px'; + ul.style.WebkitOverflowScrolling = 'touch'; + + var scrollStep = function(timestamp) { + if (!start) { + start = timestamp; + ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0; + } + + var duration = Math.max(timestamp - start, 1); + if (duration < 100) { + document.body.scrollTop = scrollFrom + (scrollTo - scrollFrom) * (duration / 100); + window.requestAnimationFrame(scrollStep); + } + else { + document.body.scrollTop = scrollTo; + } + }; + + window.requestAnimationFrame(scrollStep); + } + else { + ul.style.maxHeight = '1px'; + ul.style.top = ul.style.bottom = ''; + + window.requestAnimationFrame(function() { + var height = items * li[Math.max(0, li.length - 2)].offsetHeight; + + ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0; + ul.style[((rect.top + rect.height + height) > window.innerHeight) ? 'bottom' : 'top'] = rect.height + 'px'; + ul.style.maxHeight = height + 'px'; + }); + } + + var cboxes = ul.querySelectorAll('[selected] input[type="checkbox"]'); + for (var i = 0; i < cboxes.length; i++) { + cboxes[i].checked = true; + cboxes[i].disabled = (cboxes.length == 1 && !this.options.optional); + }; + + ul.classList.add('dropdown'); + + sb.insertBefore(pv, ul.nextElementSibling); + + li.forEach(function(l) { + l.setAttribute('tabindex', 0); + }); + + sb.lastElementChild.setAttribute('tabindex', 0); + + this.setFocus(sb, sel || li[0], true); + }, + + closeDropdown: function(sb, no_focus) { + if (!sb.hasAttribute('open')) + return; + + var pv = sb.querySelector('ul.preview'), + ul = sb.querySelector('ul.dropdown'), + li = ul.querySelectorAll('li'), + fl = findParent(sb, '.cbi-value-field'); + + li.forEach(function(l) { l.removeAttribute('tabindex'); }); + sb.lastElementChild.removeAttribute('tabindex'); + + sb.removeChild(pv); + sb.removeAttribute('open'); + sb.style.width = sb.style.height = ''; + + ul.classList.remove('dropdown'); + ul.style.top = ul.style.bottom = ul.style.maxHeight = ''; + + if (fl) + fl.classList.remove('cbi-dropdown-open'); + + if (!no_focus) + this.setFocus(sb, sb); + + this.saveValues(sb, ul); + }, + + toggleItem: function(sb, li, force_state) { + if (li.hasAttribute('unselectable')) + return; + + if (this.options.multi) { + var cbox = li.querySelector('input[type="checkbox"]'), + items = li.parentNode.querySelectorAll('li'), + label = sb.querySelector('ul.preview'), + sel = li.parentNode.querySelectorAll('[selected]').length, + more = sb.querySelector('.more'), + ndisplay = this.options.display_items, + n = 0; + + if (li.hasAttribute('selected')) { + if (force_state !== true) { + if (sel > 1 || this.options.optional) { + li.removeAttribute('selected'); + cbox.checked = cbox.disabled = false; + sel--; + } + else { + cbox.disabled = true; + } + } + } + else { + if (force_state !== false) { + li.setAttribute('selected', ''); + cbox.checked = true; + cbox.disabled = false; + sel++; + } + } + + while (label && label.firstElementChild) + label.removeChild(label.firstElementChild); + + for (var i = 0; i < items.length; i++) { + items[i].removeAttribute('display'); + if (items[i].hasAttribute('selected')) { + if (ndisplay-- > 0) { + items[i].setAttribute('display', n++); + if (label) + label.appendChild(items[i].cloneNode(true)); + } + var c = items[i].querySelector('input[type="checkbox"]'); + if (c) + c.disabled = (sel == 1 && !this.options.optional); + } + } + + if (ndisplay < 0) + sb.setAttribute('more', ''); + else + sb.removeAttribute('more'); + + if (ndisplay === this.options.display_items) + sb.setAttribute('empty', ''); + else + sb.removeAttribute('empty'); + + more.innerHTML = (ndisplay === this.options.display_items) + ? (this.options.select_placeholder || this.options.placeholder) : '···'; + } + else { + var sel = li.parentNode.querySelector('[selected]'); + if (sel) { + sel.removeAttribute('display'); + sel.removeAttribute('selected'); + } + + li.setAttribute('display', 0); + li.setAttribute('selected', ''); + + this.closeDropdown(sb, true); + } + + this.saveValues(sb, li.parentNode); + }, + + transformItem: function(sb, li) { + var cbox = E('form', {}, E('input', { type: 'checkbox', tabindex: -1, onclick: 'event.preventDefault()' })), + label = E('label'); + + while (li.firstChild) + label.appendChild(li.firstChild); + + li.appendChild(cbox); + li.appendChild(label); + }, + + saveValues: function(sb, ul) { + var sel = ul.querySelectorAll('li[selected]'), + div = sb.lastElementChild, + name = this.options.name, + strval = '', + values = []; + + while (div.lastElementChild) + div.removeChild(div.lastElementChild); + + sel.forEach(function (s) { + if (s.hasAttribute('placeholder')) + return; + + var v = { + text: s.innerText, + value: s.hasAttribute('data-value') ? s.getAttribute('data-value') : s.innerText, + element: s + }; + + div.appendChild(E('input', { + type: 'hidden', + name: name, + value: v.value + })); + + values.push(v); + + strval += strval.length ? ' ' + v.value : v.value; + }); + + var detail = { + instance: this, + element: sb + }; + + if (this.options.multi) + detail.values = values; + else + detail.value = values.length ? values[0] : null; + + sb.value = strval; + + sb.dispatchEvent(new CustomEvent('cbi-dropdown-change', { + bubbles: true, + detail: detail + })); + }, + + setValues: function(sb, values) { + var ul = sb.querySelector('ul'); + + if (this.options.create) { + for (var value in values) { + this.createItems(sb, value); + + if (!this.options.multi) + break; + } + } + + if (this.options.multi) { + var lis = ul.querySelectorAll('li[data-value]'); + for (var i = 0; i < lis.length; i++) { + var value = lis[i].getAttribute('data-value'); + if (values === null || !(value in values)) + this.toggleItem(sb, lis[i], false); + else + this.toggleItem(sb, lis[i], true); + } + } + else { + var ph = ul.querySelector('li[placeholder]'); + if (ph) + this.toggleItem(sb, ph); + + var lis = ul.querySelectorAll('li[data-value]'); + for (var i = 0; i < lis.length; i++) { + var value = lis[i].getAttribute('data-value'); + if (values !== null && (value in values)) + this.toggleItem(sb, lis[i]); + } + } + }, + + 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'); + e.blur(); + } + }); + + if (elem) { + elem.focus(); + elem.classList.add('focus'); + + if (scroll) + elem.parentNode.scrollTop = elem.offsetTop - elem.parentNode.offsetTop; + } + }, + + createItems: function(sb, value) { + var sbox = this, + val = (value || '').trim(), + ul = sb.querySelector('ul'); + + if (!sbox.options.multi) + val = val.length ? [ val ] : []; + else + val = val.length ? val.split(/\s+/) : []; + + val.forEach(function(item) { + var new_item = null; + + ul.childNodes.forEach(function(li) { + if (li.getAttribute && li.getAttribute('data-value') === item) + new_item = li; + }); + + if (!new_item) { + var markup, + tpl = sb.querySelector(sbox.options.create_template); + + if (tpl) + markup = (tpl.textContent || tpl.innerHTML || tpl.firstChild.data).replace(/^<!--|-->$/, '').trim(); + else + markup = '<li data-value="{{value}}">{{value}}</li>'; + + new_item = E(markup.replace(/{{value}}/g, item)); + + if (sbox.options.multi) { + sbox.transformItem(sb, new_item); + } + else { + var old = ul.querySelector('li[created]'); + if (old) + ul.removeChild(old); + + new_item.setAttribute('created', ''); + } + + new_item = ul.insertBefore(new_item, ul.lastElementChild); + } + + sbox.toggleItem(sb, new_item, true); + sbox.setFocus(sb, new_item, true); + }); + }, + + closeAllDropdowns: function() { + 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); + else if (li && li.parentNode.classList.contains('preview')) + this.closeDropdown(sb); + } + + 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.options.create_query).focus(); + }, + + setValue: function(values) { + if (this.options.multi) { + if (!Array.isArray(values)) + values = (values != null) ? [ values ] : []; + + var v = {}; + + for (var i = 0; i < values.length; i++) + v[values[i]] = true; + + this.setValues(this.node, v); + } + else { + var v = {}; + + if (values != null) { + if (Array.isArray(values)) + v[values[0]] = true; + else + v[values] = true; + } + + this.setValues(this.node, v); + } + }, + + getValue: function() { + var div = this.node.lastElementChild, + h = div.querySelectorAll('input[type="hidden"]'), + v = []; + + for (var i = 0; i < h.length; i++) + v.push(h[i].value); + + return this.options.multi ? v : v[0]; + } +}); + +var UICombobox = UIDropdown.extend({ + __init__: function(value, choices, options) { + this.super('__init__', [ value, choices, Object.assign({ + select_placeholder: _('-- Please choose --'), + custom_placeholder: _('-- custom --'), + dropdown_items: 5 + }, options, { + sort: true, + multi: false, + create: true, + optional: true + }) ]); + } +}); + +var UIDynamicList = UIElement.extend({ + __init__: function(values, choices, options) { + if (!Array.isArray(values)) + values = (values != null) ? [ values ] : []; + + if (typeof(choices) != 'object') + choices = null; + + this.values = values; + this.choices = choices; + this.options = Object.assign({}, options, { + multi: false, + optional: true + }); + }, + + render: function() { + var dl = E('div', { + 'id': this.options.id, + 'class': 'cbi-dynlist' + }, E('div', { 'class': 'add-item' })); + + if (this.choices) { + var cbox = new UICombobox(null, this.choices, this.options); + dl.lastElementChild.appendChild(cbox.render()); + } + else { + var inputEl = E('input', { + 'type': 'text', + 'class': 'cbi-input-text', + 'placeholder': this.options.placeholder + }); + + dl.lastElementChild.appendChild(inputEl); + dl.lastElementChild.appendChild(E('div', { 'class': 'cbi-button cbi-button-add' }, '+')); + + L.ui.addValidator(inputEl, this.options.datatype, true, 'blue', 'keyup'); + } + + for (var i = 0; i < this.values.length; i++) + this.addItem(dl, this.values[i], + this.choices ? this.choices[this.values[i]] : null); + + return this.bind(dl); + }, + + bind: function(dl) { + dl.addEventListener('click', L.bind(this.handleClick, this)); + dl.addEventListener('keydown', L.bind(this.handleKeydown, this)); + dl.addEventListener('cbi-dropdown-change', L.bind(this.handleDropdownChange, this)); + + this.node = dl; + + this.setUpdateEvents(dl, 'cbi-dynlist-change'); + this.setChangeEvents(dl, 'cbi-dynlist-change'); + + L.dom.bindClassInstance(dl, this); + + return dl; + }, + + 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': this.options.name, + 'value': value })]); + + dl.querySelectorAll('.item, .add-item').forEach(function(item) { + if (exists) + return; + + var hidden = item.querySelector('input[type="hidden"]'); + + if (hidden && hidden.parentNode !== item) + hidden = null; + + if (hidden && hidden.value === value) + exists = true; + else if (!hidden || hidden.value >= value) + exists = !!item.parentNode.insertBefore(new_item, item); + }); + + dl.dispatchEvent(new CustomEvent('cbi-dynlist-change', { + bubbles: true, + detail: { + instance: this, + element: dl, + value: value, + add: true + } + })); + }, + + removeItem: function(dl, item) { + var value = item.querySelector('input[type="hidden"]').value; + var sb = dl.querySelector('.cbi-dropdown'); + if (sb) + sb.querySelectorAll('ul > li').forEach(function(li) { + if (li.getAttribute('data-value') === value) { + if (li.hasAttribute('dynlistcustom')) + li.parentNode.removeChild(li); + else + li.removeAttribute('unselectable'); + } + }); + + item.parentNode.removeChild(item); + + dl.dispatchEvent(new CustomEvent('cbi-dynlist-change', { + bubbles: true, + detail: { + instance: this, + element: dl, + value: value, + remove: true + } + })); + }, + + 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', ''); + + if (sbVal.element.hasAttribute('created')) { + sbVal.element.removeAttribute('created'); + sbVal.element.setAttribute('dynlistcustom', ''); + } + + 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; + } + } + }, + + getValue: function() { + var items = this.node.querySelectorAll('.item > input[type="hidden"]'), + v = []; + + for (var i = 0; i < items.length; i++) + v.push(items[i].value); + + return v; + }, + + setValue: function(values) { + if (!Array.isArray(values)) + values = (values != null) ? [ values ] : []; + + var items = this.node.querySelectorAll('.item'); + + for (var i = 0; i < items.length; i++) + if (items[i].parentNode === this.node) + this.removeItem(this.node, items[i]); + + for (var i = 0; i < values.length; i++) + this.addItem(this.node, values[i], + this.choices ? this.choices[values[i]] : null); + } +}); + + return L.Class.extend({ __init__: function() { modalDiv = document.body.appendChild( @@ -292,5 +1316,30 @@ return L.Class.extend({ } }); } - }) + }), + + addValidator: function(field, type, optional /*, ... */) { + if (type == null) + return; + + var events = this.varargs(arguments, 3); + if (events.length == 0) + events.push('blur', 'keyup'); + + try { + var cbiValidator = new CBIValidator(field, type, optional), + validatorFn = cbiValidator.validate.bind(cbiValidator); + + for (var i = 0; i < events.length; i++) + field.addEventListener(events[i], validatorFn); + + validatorFn(); + } + catch (e) { } + }, + + /* Widgets */ + Dropdown: UIDropdown, + DynamicList: UIDynamicList, + Combobox: UICombobox }); |