7 var UIElement = L.Class.extend({
9 if (L.dom.matches(this.node, 'select') || L.dom.matches(this.node, 'input'))
10 return this.node.value;
15 setValue: function(value) {
16 if (L.dom.matches(this.node, 'select') || L.dom.matches(this.node, 'input'))
17 this.node.value = value;
24 registerEvents: function(targetNode, synevent, events) {
25 var dispatchFn = L.bind(function(ev) {
26 this.node.dispatchEvent(new CustomEvent(synevent, { bubbles: true }));
29 for (var i = 0; i < events.length; i++)
30 targetNode.addEventListener(events[i], dispatchFn);
33 setUpdateEvents: function(targetNode /*, ... */) {
34 this.registerEvents(targetNode, 'widget-update', this.varargs(arguments, 1));
37 setChangeEvents: function(targetNode /*, ... */) {
38 this.registerEvents(targetNode, 'widget-change', this.varargs(arguments, 1));
42 var UIDropdown = UIElement.extend({
43 __init__: function(value, choices, options) {
44 if (typeof(choices) != 'object')
47 if (!Array.isArray(value))
48 this.values = (value != null) ? [ value ] : [];
52 this.choices = choices;
53 this.options = Object.assign({
55 multi: Array.isArray(value),
57 select_placeholder: _('-- Please choose --'),
58 custom_placeholder: _('-- custom --'),
62 create_query: '.create-item-input',
63 create_template: 'script[type="item-template"]'
69 'id': this.options.id,
70 'class': 'cbi-dropdown',
71 'multiple': this.options.multi ? '' : null,
72 'optional': this.options.optional ? '' : null,
75 var keys = Object.keys(this.choices);
77 if (this.options.sort === true)
79 else if (Array.isArray(this.options.sort))
80 keys = this.options.sort;
82 if (this.options.create)
83 for (var i = 0; i < this.values.length; i++)
84 if (!this.choices.hasOwnProperty(this.values[i]))
85 keys.push(this.values[i]);
87 for (var i = 0; i < keys.length; i++)
88 sb.lastElementChild.appendChild(E('li', {
89 'data-value': keys[i],
90 'selected': (this.values.indexOf(keys[i]) > -1) ? '' : null
91 }, this.choices[keys[i]] || keys[i]));
93 if (this.options.create) {
94 var createEl = E('input', {
96 'class': 'create-item-input',
97 'placeholder': this.options.custom_placeholder || this.options.placeholder
100 if (this.options.datatype)
101 L.ui.addValidator(createEl, this.options.datatype, true, 'blur', 'keyup');
103 sb.lastElementChild.appendChild(E('li', { 'data-value': '-' }, createEl));
106 return this.bind(sb);
110 var o = this.options;
112 o.multi = sb.hasAttribute('multiple');
113 o.optional = sb.hasAttribute('optional');
114 o.placeholder = sb.getAttribute('placeholder') || o.placeholder;
115 o.display_items = parseInt(sb.getAttribute('display-items') || o.display_items);
116 o.dropdown_items = parseInt(sb.getAttribute('dropdown-items') || o.dropdown_items);
117 o.create_query = sb.getAttribute('item-create') || o.create_query;
118 o.create_template = sb.getAttribute('item-template') || o.create_template;
120 var ul = sb.querySelector('ul'),
121 more = sb.appendChild(E('span', { class: 'more', tabindex: -1 }, '···')),
122 open = sb.appendChild(E('span', { class: 'open', tabindex: -1 }, '▾')),
123 canary = sb.appendChild(E('div')),
124 create = sb.querySelector(this.options.create_query),
125 ndisplay = this.options.display_items,
128 if (this.options.multi) {
129 var items = ul.querySelectorAll('li');
131 for (var i = 0; i < items.length; i++) {
132 this.transformItem(sb, items[i]);
134 if (items[i].hasAttribute('selected') && ndisplay-- > 0)
135 items[i].setAttribute('display', n++);
139 if (this.options.optional && !ul.querySelector('li[data-value=""]')) {
140 var placeholder = E('li', { placeholder: '' },
141 this.options.select_placeholder || this.options.placeholder);
144 ? ul.insertBefore(placeholder, ul.firstChild)
145 : ul.appendChild(placeholder);
148 var items = ul.querySelectorAll('li'),
149 sel = sb.querySelectorAll('[selected]');
151 sel.forEach(function(s) {
152 s.removeAttribute('selected');
155 var s = sel[0] || items[0];
157 s.setAttribute('selected', '');
158 s.setAttribute('display', n++);
164 this.saveValues(sb, ul);
166 ul.setAttribute('tabindex', -1);
167 sb.setAttribute('tabindex', 0);
170 sb.setAttribute('more', '')
172 sb.removeAttribute('more');
174 if (ndisplay == this.options.display_items)
175 sb.setAttribute('empty', '')
177 sb.removeAttribute('empty');
179 more.innerHTML = (ndisplay == this.options.display_items)
180 ? (this.options.select_placeholder || this.options.placeholder) : '···';
183 sb.addEventListener('click', this.handleClick.bind(this));
184 sb.addEventListener('keydown', this.handleKeydown.bind(this));
185 sb.addEventListener('cbi-dropdown-close', this.handleDropdownClose.bind(this));
186 sb.addEventListener('cbi-dropdown-select', this.handleDropdownSelect.bind(this));
188 if ('ontouchstart' in window) {
189 sb.addEventListener('touchstart', function(ev) { ev.stopPropagation(); });
190 window.addEventListener('touchstart', this.closeAllDropdowns);
193 sb.addEventListener('mouseover', this.handleMouseover.bind(this));
194 sb.addEventListener('focus', this.handleFocus.bind(this));
196 canary.addEventListener('focus', this.handleCanaryFocus.bind(this));
198 window.addEventListener('mouseover', this.setFocus);
199 window.addEventListener('click', this.closeAllDropdowns);
203 create.addEventListener('keydown', this.handleCreateKeydown.bind(this));
204 create.addEventListener('focus', this.handleCreateFocus.bind(this));
205 create.addEventListener('blur', this.handleCreateBlur.bind(this));
207 var li = findParent(create, 'li');
209 li.setAttribute('unselectable', '');
210 li.addEventListener('click', this.handleCreateClick.bind(this));
215 this.setUpdateEvents(sb, 'cbi-dropdown-open', 'cbi-dropdown-close');
216 this.setChangeEvents(sb, 'cbi-dropdown-change', 'cbi-dropdown-close');
218 L.dom.bindClassInstance(sb, this);
223 openDropdown: function(sb) {
224 var st = window.getComputedStyle(sb, null),
225 ul = sb.querySelector('ul'),
226 li = ul.querySelectorAll('li'),
227 fl = findParent(sb, '.cbi-value-field'),
228 sel = ul.querySelector('[selected]'),
229 rect = sb.getBoundingClientRect(),
230 items = Math.min(this.options.dropdown_items, li.length);
232 document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
233 s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
236 sb.setAttribute('open', '');
238 var pv = ul.cloneNode(true);
239 pv.classList.add('preview');
242 fl.classList.add('cbi-dropdown-open');
244 if ('ontouchstart' in window) {
245 var vpWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
246 vpHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0),
247 scrollFrom = window.pageYOffset,
248 scrollTo = scrollFrom + rect.top - vpHeight * 0.5,
251 ul.style.top = sb.offsetHeight + 'px';
252 ul.style.left = -rect.left + 'px';
253 ul.style.right = (rect.right - vpWidth) + 'px';
254 ul.style.maxHeight = (vpHeight * 0.5) + 'px';
255 ul.style.WebkitOverflowScrolling = 'touch';
257 var scrollStep = function(timestamp) {
260 ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
263 var duration = Math.max(timestamp - start, 1);
264 if (duration < 100) {
265 document.body.scrollTop = scrollFrom + (scrollTo - scrollFrom) * (duration / 100);
266 window.requestAnimationFrame(scrollStep);
269 document.body.scrollTop = scrollTo;
273 window.requestAnimationFrame(scrollStep);
276 ul.style.maxHeight = '1px';
277 ul.style.top = ul.style.bottom = '';
279 window.requestAnimationFrame(function() {
280 var height = items * li[Math.max(0, li.length - 2)].offsetHeight;
282 ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
283 ul.style[((rect.top + rect.height + height) > window.innerHeight) ? 'bottom' : 'top'] = rect.height + 'px';
284 ul.style.maxHeight = height + 'px';
288 var cboxes = ul.querySelectorAll('[selected] input[type="checkbox"]');
289 for (var i = 0; i < cboxes.length; i++) {
290 cboxes[i].checked = true;
291 cboxes[i].disabled = (cboxes.length == 1 && !this.options.optional);
294 ul.classList.add('dropdown');
296 sb.insertBefore(pv, ul.nextElementSibling);
298 li.forEach(function(l) {
299 l.setAttribute('tabindex', 0);
302 sb.lastElementChild.setAttribute('tabindex', 0);
304 this.setFocus(sb, sel || li[0], true);
307 closeDropdown: function(sb, no_focus) {
308 if (!sb.hasAttribute('open'))
311 var pv = sb.querySelector('ul.preview'),
312 ul = sb.querySelector('ul.dropdown'),
313 li = ul.querySelectorAll('li'),
314 fl = findParent(sb, '.cbi-value-field');
316 li.forEach(function(l) { l.removeAttribute('tabindex'); });
317 sb.lastElementChild.removeAttribute('tabindex');
320 sb.removeAttribute('open');
321 sb.style.width = sb.style.height = '';
323 ul.classList.remove('dropdown');
324 ul.style.top = ul.style.bottom = ul.style.maxHeight = '';
327 fl.classList.remove('cbi-dropdown-open');
330 this.setFocus(sb, sb);
332 this.saveValues(sb, ul);
335 toggleItem: function(sb, li, force_state) {
336 if (li.hasAttribute('unselectable'))
339 if (this.options.multi) {
340 var cbox = li.querySelector('input[type="checkbox"]'),
341 items = li.parentNode.querySelectorAll('li'),
342 label = sb.querySelector('ul.preview'),
343 sel = li.parentNode.querySelectorAll('[selected]').length,
344 more = sb.querySelector('.more'),
345 ndisplay = this.options.display_items,
348 if (li.hasAttribute('selected')) {
349 if (force_state !== true) {
350 if (sel > 1 || this.options.optional) {
351 li.removeAttribute('selected');
352 cbox.checked = cbox.disabled = false;
356 cbox.disabled = true;
361 if (force_state !== false) {
362 li.setAttribute('selected', '');
364 cbox.disabled = false;
369 while (label && label.firstElementChild)
370 label.removeChild(label.firstElementChild);
372 for (var i = 0; i < items.length; i++) {
373 items[i].removeAttribute('display');
374 if (items[i].hasAttribute('selected')) {
375 if (ndisplay-- > 0) {
376 items[i].setAttribute('display', n++);
378 label.appendChild(items[i].cloneNode(true));
380 var c = items[i].querySelector('input[type="checkbox"]');
382 c.disabled = (sel == 1 && !this.options.optional);
387 sb.setAttribute('more', '');
389 sb.removeAttribute('more');
391 if (ndisplay === this.options.display_items)
392 sb.setAttribute('empty', '');
394 sb.removeAttribute('empty');
396 more.innerHTML = (ndisplay === this.options.display_items)
397 ? (this.options.select_placeholder || this.options.placeholder) : '···';
400 var sel = li.parentNode.querySelector('[selected]');
402 sel.removeAttribute('display');
403 sel.removeAttribute('selected');
406 li.setAttribute('display', 0);
407 li.setAttribute('selected', '');
409 this.closeDropdown(sb, true);
412 this.saveValues(sb, li.parentNode);
415 transformItem: function(sb, li) {
416 var cbox = E('form', {}, E('input', { type: 'checkbox', tabindex: -1, onclick: 'event.preventDefault()' })),
419 while (li.firstChild)
420 label.appendChild(li.firstChild);
422 li.appendChild(cbox);
423 li.appendChild(label);
426 saveValues: function(sb, ul) {
427 var sel = ul.querySelectorAll('li[selected]'),
428 div = sb.lastElementChild,
429 name = this.options.name,
433 while (div.lastElementChild)
434 div.removeChild(div.lastElementChild);
436 sel.forEach(function (s) {
437 if (s.hasAttribute('placeholder'))
442 value: s.hasAttribute('data-value') ? s.getAttribute('data-value') : s.innerText,
446 div.appendChild(E('input', {
454 strval += strval.length ? ' ' + v.value : v.value;
462 if (this.options.multi)
463 detail.values = values;
465 detail.value = values.length ? values[0] : null;
469 sb.dispatchEvent(new CustomEvent('cbi-dropdown-change', {
475 setValues: function(sb, values) {
476 var ul = sb.querySelector('ul');
478 if (this.options.create) {
479 for (var value in values) {
480 this.createItems(sb, value);
482 if (!this.options.multi)
487 if (this.options.multi) {
488 var lis = ul.querySelectorAll('li[data-value]');
489 for (var i = 0; i < lis.length; i++) {
490 var value = lis[i].getAttribute('data-value');
491 if (values === null || !(value in values))
492 this.toggleItem(sb, lis[i], false);
494 this.toggleItem(sb, lis[i], true);
498 var ph = ul.querySelector('li[placeholder]');
500 this.toggleItem(sb, ph);
502 var lis = ul.querySelectorAll('li[data-value]');
503 for (var i = 0; i < lis.length; i++) {
504 var value = lis[i].getAttribute('data-value');
505 if (values !== null && (value in values))
506 this.toggleItem(sb, lis[i]);
511 setFocus: function(sb, elem, scroll) {
512 if (sb && sb.hasAttribute && sb.hasAttribute('locked-in'))
515 if (sb.target && findParent(sb.target, 'ul.dropdown'))
518 document.querySelectorAll('.focus').forEach(function(e) {
519 if (!matchesElem(e, 'input')) {
520 e.classList.remove('focus');
527 elem.classList.add('focus');
530 elem.parentNode.scrollTop = elem.offsetTop - elem.parentNode.offsetTop;
534 createItems: function(sb, value) {
536 val = (value || '').trim(),
537 ul = sb.querySelector('ul');
539 if (!sbox.options.multi)
540 val = val.length ? [ val ] : [];
542 val = val.length ? val.split(/\s+/) : [];
544 val.forEach(function(item) {
547 ul.childNodes.forEach(function(li) {
548 if (li.getAttribute && li.getAttribute('data-value') === item)
554 tpl = sb.querySelector(sbox.options.create_template);
557 markup = (tpl.textContent || tpl.innerHTML || tpl.firstChild.data).replace(/^<!--|-->$/, '').trim();
559 markup = '<li data-value="{{value}}">{{value}}</li>';
561 new_item = E(markup.replace(/{{value}}/g, item));
563 if (sbox.options.multi) {
564 sbox.transformItem(sb, new_item);
567 var old = ul.querySelector('li[created]');
571 new_item.setAttribute('created', '');
574 new_item = ul.insertBefore(new_item, ul.lastElementChild);
577 sbox.toggleItem(sb, new_item, true);
578 sbox.setFocus(sb, new_item, true);
582 closeAllDropdowns: function() {
583 document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
584 s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
588 handleClick: function(ev) {
589 var sb = ev.currentTarget;
591 if (!sb.hasAttribute('open')) {
592 if (!matchesElem(ev.target, 'input'))
593 this.openDropdown(sb);
596 var li = findParent(ev.target, 'li');
597 if (li && li.parentNode.classList.contains('dropdown'))
598 this.toggleItem(sb, li);
599 else if (li && li.parentNode.classList.contains('preview'))
600 this.closeDropdown(sb);
604 ev.stopPropagation();
607 handleKeydown: function(ev) {
608 var sb = ev.currentTarget;
610 if (matchesElem(ev.target, 'input'))
613 if (!sb.hasAttribute('open')) {
614 switch (ev.keyCode) {
619 this.openDropdown(sb);
624 var active = findParent(document.activeElement, 'li');
626 switch (ev.keyCode) {
628 this.closeDropdown(sb);
633 if (!active.hasAttribute('selected'))
634 this.toggleItem(sb, active);
635 this.closeDropdown(sb);
642 this.toggleItem(sb, active);
648 if (active && active.previousElementSibling) {
649 this.setFocus(sb, active.previousElementSibling);
655 if (active && active.nextElementSibling) {
656 this.setFocus(sb, active.nextElementSibling);
664 handleDropdownClose: function(ev) {
665 var sb = ev.currentTarget;
667 this.closeDropdown(sb, true);
670 handleDropdownSelect: function(ev) {
671 var sb = ev.currentTarget,
672 li = findParent(ev.target, 'li');
677 this.toggleItem(sb, li);
678 this.closeDropdown(sb, true);
681 handleMouseover: function(ev) {
682 var sb = ev.currentTarget;
684 if (!sb.hasAttribute('open'))
687 var li = findParent(ev.target, 'li');
689 if (li && li.parentNode.classList.contains('dropdown'))
690 this.setFocus(sb, li);
693 handleFocus: function(ev) {
694 var sb = ev.currentTarget;
696 document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
697 if (s !== sb || sb.hasAttribute('open'))
698 s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
702 handleCanaryFocus: function(ev) {
703 this.closeDropdown(ev.currentTarget.parentNode);
706 handleCreateKeydown: function(ev) {
707 var input = ev.currentTarget,
708 sb = findParent(input, '.cbi-dropdown');
710 switch (ev.keyCode) {
714 if (input.classList.contains('cbi-input-invalid'))
717 this.createItems(sb, input.value);
724 handleCreateFocus: function(ev) {
725 var input = ev.currentTarget,
726 cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
727 sb = findParent(input, '.cbi-dropdown');
732 sb.setAttribute('locked-in', '');
735 handleCreateBlur: function(ev) {
736 var input = ev.currentTarget,
737 cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
738 sb = findParent(input, '.cbi-dropdown');
741 cbox.checked = false;
743 sb.removeAttribute('locked-in');
746 handleCreateClick: function(ev) {
747 ev.currentTarget.querySelector(this.options.create_query).focus();
750 setValue: function(values) {
751 if (this.options.multi) {
752 if (!Array.isArray(values))
753 values = (values != null) ? [ values ] : [];
757 for (var i = 0; i < values.length; i++)
760 this.setValues(this.node, v);
765 if (values != null) {
766 if (Array.isArray(values))
772 this.setValues(this.node, v);
776 getValue: function() {
777 var div = this.node.lastElementChild,
778 h = div.querySelectorAll('input[type="hidden"]'),
781 for (var i = 0; i < h.length; i++)
784 return this.options.multi ? v : v[0];
788 var UICombobox = UIDropdown.extend({
789 __init__: function(value, choices, options) {
790 this.super('__init__', [ value, choices, Object.assign({
791 select_placeholder: _('-- Please choose --'),
792 custom_placeholder: _('-- custom --'),
803 var UIDynamicList = UIElement.extend({
804 __init__: function(values, choices, options) {
805 if (!Array.isArray(values))
806 values = (values != null) ? [ values ] : [];
808 if (typeof(choices) != 'object')
811 this.values = values;
812 this.choices = choices;
813 this.options = Object.assign({}, options, {
821 'id': this.options.id,
822 'class': 'cbi-dynlist'
823 }, E('div', { 'class': 'add-item' }));
826 var cbox = new UICombobox(null, this.choices, this.options);
827 dl.lastElementChild.appendChild(cbox.render());
830 var inputEl = E('input', {
832 'class': 'cbi-input-text',
833 'placeholder': this.options.placeholder
836 dl.lastElementChild.appendChild(inputEl);
837 dl.lastElementChild.appendChild(E('div', { 'class': 'cbi-button cbi-button-add' }, '+'));
839 L.ui.addValidator(inputEl, this.options.datatype, true, 'blue', 'keyup');
842 for (var i = 0; i < this.values.length; i++)
843 this.addItem(dl, this.values[i],
844 this.choices ? this.choices[this.values[i]] : null);
846 return this.bind(dl);
850 dl.addEventListener('click', L.bind(this.handleClick, this));
851 dl.addEventListener('keydown', L.bind(this.handleKeydown, this));
852 dl.addEventListener('cbi-dropdown-change', L.bind(this.handleDropdownChange, this));
856 this.setUpdateEvents(dl, 'cbi-dynlist-change');
857 this.setChangeEvents(dl, 'cbi-dynlist-change');
859 L.dom.bindClassInstance(dl, this);
864 addItem: function(dl, value, text, flash) {
866 new_item = E('div', { 'class': flash ? 'item flash' : 'item', 'tabindex': 0 }, [
867 E('span', {}, text || value),
870 'name': this.options.name,
873 dl.querySelectorAll('.item, .add-item').forEach(function(item) {
877 var hidden = item.querySelector('input[type="hidden"]');
879 if (hidden && hidden.parentNode !== item)
882 if (hidden && hidden.value === value)
884 else if (!hidden || hidden.value >= value)
885 exists = !!item.parentNode.insertBefore(new_item, item);
888 dl.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
899 removeItem: function(dl, item) {
900 var value = item.querySelector('input[type="hidden"]').value;
901 var sb = dl.querySelector('.cbi-dropdown');
903 sb.querySelectorAll('ul > li').forEach(function(li) {
904 if (li.getAttribute('data-value') === value) {
905 if (li.hasAttribute('dynlistcustom'))
906 li.parentNode.removeChild(li);
908 li.removeAttribute('unselectable');
912 item.parentNode.removeChild(item);
914 dl.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
925 handleClick: function(ev) {
926 var dl = ev.currentTarget,
927 item = findParent(ev.target, '.item');
930 this.removeItem(dl, item);
932 else if (matchesElem(ev.target, '.cbi-button-add')) {
933 var input = ev.target.previousElementSibling;
934 if (input.value.length && !input.classList.contains('cbi-input-invalid')) {
935 this.addItem(dl, input.value, null, true);
941 handleDropdownChange: function(ev) {
942 var dl = ev.currentTarget,
943 sbIn = ev.detail.instance,
944 sbEl = ev.detail.element,
945 sbVal = ev.detail.value;
950 sbIn.setValues(sbEl, null);
951 sbVal.element.setAttribute('unselectable', '');
953 if (sbVal.element.hasAttribute('created')) {
954 sbVal.element.removeAttribute('created');
955 sbVal.element.setAttribute('dynlistcustom', '');
958 this.addItem(dl, sbVal.value, sbVal.text, true);
961 handleKeydown: function(ev) {
962 var dl = ev.currentTarget,
963 item = findParent(ev.target, '.item');
966 switch (ev.keyCode) {
967 case 8: /* backspace */
968 if (item.previousElementSibling)
969 item.previousElementSibling.focus();
971 this.removeItem(dl, item);
974 case 46: /* delete */
975 if (item.nextElementSibling) {
976 if (item.nextElementSibling.classList.contains('item'))
977 item.nextElementSibling.focus();
979 item.nextElementSibling.firstElementChild.focus();
982 this.removeItem(dl, item);
986 else if (matchesElem(ev.target, '.cbi-input-text')) {
987 switch (ev.keyCode) {
989 if (ev.target.value.length && !ev.target.classList.contains('cbi-input-invalid')) {
990 this.addItem(dl, ev.target.value, null, true);
991 ev.target.value = '';
1002 getValue: function() {
1003 var items = this.node.querySelectorAll('.item > input[type="hidden"]'),
1006 for (var i = 0; i < items.length; i++)
1007 v.push(items[i].value);
1012 setValue: function(values) {
1013 if (!Array.isArray(values))
1014 values = (values != null) ? [ values ] : [];
1016 var items = this.node.querySelectorAll('.item');
1018 for (var i = 0; i < items.length; i++)
1019 if (items[i].parentNode === this.node)
1020 this.removeItem(this.node, items[i]);
1022 for (var i = 0; i < values.length; i++)
1023 this.addItem(this.node, values[i],
1024 this.choices ? this.choices[values[i]] : null);
1029 return L.Class.extend({
1030 __init__: function() {
1031 modalDiv = document.body.appendChild(
1032 L.dom.create('div', { id: 'modal_overlay' },
1033 L.dom.create('div', { class: 'modal', role: 'dialog', 'aria-modal': true })));
1035 tooltipDiv = document.body.appendChild(
1036 L.dom.create('div', { class: 'cbi-tooltip' }));
1038 /* setup old aliases */
1039 L.showModal = this.showModal;
1040 L.hideModal = this.hideModal;
1041 L.showTooltip = this.showTooltip;
1042 L.hideTooltip = this.hideTooltip;
1043 L.itemlist = this.itemlist;
1045 document.addEventListener('mouseover', this.showTooltip.bind(this), true);
1046 document.addEventListener('mouseout', this.hideTooltip.bind(this), true);
1047 document.addEventListener('focus', this.showTooltip.bind(this), true);
1048 document.addEventListener('blur', this.hideTooltip.bind(this), true);
1050 document.addEventListener('luci-loaded', this.tabs.init.bind(this.tabs));
1054 showModal: function(title, children) {
1055 var dlg = modalDiv.firstElementChild;
1057 dlg.setAttribute('class', 'modal');
1059 L.dom.content(dlg, L.dom.create('h4', {}, title));
1060 L.dom.append(dlg, children);
1062 document.body.classList.add('modal-overlay-active');
1067 hideModal: function() {
1068 document.body.classList.remove('modal-overlay-active');
1072 showTooltip: function(ev) {
1073 var target = findParent(ev.target, '[data-tooltip]');
1078 if (tooltipTimeout !== null) {
1079 window.clearTimeout(tooltipTimeout);
1080 tooltipTimeout = null;
1083 var rect = target.getBoundingClientRect(),
1084 x = rect.left + window.pageXOffset,
1085 y = rect.top + rect.height + window.pageYOffset;
1087 tooltipDiv.className = 'cbi-tooltip';
1088 tooltipDiv.innerHTML = '▲ ';
1089 tooltipDiv.firstChild.data += target.getAttribute('data-tooltip');
1091 if (target.hasAttribute('data-tooltip-style'))
1092 tooltipDiv.classList.add(target.getAttribute('data-tooltip-style'));
1094 if ((y + tooltipDiv.offsetHeight) > (window.innerHeight + window.pageYOffset)) {
1095 y -= (tooltipDiv.offsetHeight + target.offsetHeight);
1096 tooltipDiv.firstChild.data = '▼ ' + tooltipDiv.firstChild.data.substr(2);
1099 tooltipDiv.style.top = y + 'px';
1100 tooltipDiv.style.left = x + 'px';
1101 tooltipDiv.style.opacity = 1;
1103 tooltipDiv.dispatchEvent(new CustomEvent('tooltip-open', {
1105 detail: { target: target }
1109 hideTooltip: function(ev) {
1110 if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv ||
1111 tooltipDiv.contains(ev.target) || tooltipDiv.contains(ev.relatedTarget))
1114 if (tooltipTimeout !== null) {
1115 window.clearTimeout(tooltipTimeout);
1116 tooltipTimeout = null;
1119 tooltipDiv.style.opacity = 0;
1120 tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250);
1122 tooltipDiv.dispatchEvent(new CustomEvent('tooltip-close', { bubbles: true }));
1126 itemlist: function(node, items, separators) {
1129 if (!Array.isArray(separators))
1130 separators = [ separators || E('br') ];
1132 for (var i = 0; i < items.length; i += 2) {
1133 if (items[i+1] !== null && items[i+1] !== undefined) {
1134 var sep = separators[(i/2) % separators.length],
1137 children.push(E('span', { class: 'nowrap' }, [
1138 items[i] ? E('strong', items[i] + ': ') : '',
1142 if ((i+2) < items.length)
1143 children.push(L.dom.elem(sep) ? sep.cloneNode(true) : sep);
1147 L.dom.content(node, children);
1153 tabs: L.Class.singleton({
1155 var groups = [], prevGroup = null, currGroup = null;
1157 document.querySelectorAll('[data-tab]').forEach(function(tab) {
1158 var parent = tab.parentNode;
1160 if (!parent.hasAttribute('data-tab-group'))
1161 parent.setAttribute('data-tab-group', groups.length);
1163 currGroup = +parent.getAttribute('data-tab-group');
1165 if (currGroup !== prevGroup) {
1166 prevGroup = currGroup;
1168 if (!groups[currGroup])
1169 groups[currGroup] = [];
1172 groups[currGroup].push(tab);
1175 for (var i = 0; i < groups.length; i++)
1176 this.initTabGroup(groups[i]);
1178 document.addEventListener('dependency-update', this.updateTabs.bind(this));
1183 this.setActiveTabId(-1, -1);
1186 initTabGroup: function(panes) {
1187 if (typeof(panes) != 'object' || !('length' in panes) || panes.length === 0)
1190 var menu = E('ul', { 'class': 'cbi-tabmenu' }),
1191 group = panes[0].parentNode,
1192 groupId = +group.getAttribute('data-tab-group'),
1195 for (var i = 0, pane; pane = panes[i]; i++) {
1196 var name = pane.getAttribute('data-tab'),
1197 title = pane.getAttribute('data-tab-title'),
1198 active = pane.getAttribute('data-tab-active') === 'true';
1200 menu.appendChild(E('li', {
1201 'class': active ? 'cbi-tab' : 'cbi-tab-disabled',
1205 'click': this.switchTab.bind(this)
1212 group.parentNode.insertBefore(menu, group);
1214 if (selected === null) {
1215 selected = this.getActiveTabId(groupId);
1217 if (selected < 0 || selected >= panes.length)
1220 menu.childNodes[selected].classList.add('cbi-tab');
1221 menu.childNodes[selected].classList.remove('cbi-tab-disabled');
1222 panes[selected].setAttribute('data-tab-active', 'true');
1224 this.setActiveTabId(groupId, selected);
1228 getActiveTabState: function() {
1229 var page = document.body.getAttribute('data-page');
1232 var val = JSON.parse(window.sessionStorage.getItem('tab'));
1233 if (val.page === page && Array.isArray(val.groups))
1238 window.sessionStorage.removeItem('tab');
1239 return { page: page, groups: [] };
1242 getActiveTabId: function(groupId) {
1243 return +this.getActiveTabState().groups[groupId] || 0;
1246 setActiveTabId: function(groupId, tabIndex) {
1248 var state = this.getActiveTabState();
1249 state.groups[groupId] = tabIndex;
1251 window.sessionStorage.setItem('tab', JSON.stringify(state));
1253 catch (e) { return false; }
1258 updateTabs: function(ev) {
1259 document.querySelectorAll('[data-tab-title]').forEach(function(pane) {
1260 var menu = pane.parentNode.previousElementSibling,
1261 tab = menu.querySelector('[data-tab="%s"]'.format(pane.getAttribute('data-tab'))),
1262 n_errors = pane.querySelectorAll('.cbi-input-invalid').length;
1264 if (!pane.firstElementChild) {
1265 tab.style.display = 'none';
1266 tab.classList.remove('flash');
1268 else if (tab.style.display === 'none') {
1269 tab.style.display = '';
1270 requestAnimationFrame(function() { tab.classList.add('flash') });
1274 tab.setAttribute('data-errors', n_errors);
1275 tab.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors));
1276 tab.setAttribute('data-tooltip-style', 'error');
1279 tab.removeAttribute('data-errors');
1280 tab.removeAttribute('data-tooltip');
1285 switchTab: function(ev) {
1286 var tab = ev.target.parentNode,
1287 name = tab.getAttribute('data-tab'),
1288 menu = tab.parentNode,
1289 group = menu.nextElementSibling,
1290 groupId = +group.getAttribute('data-tab-group'),
1293 ev.preventDefault();
1295 if (!tab.classList.contains('cbi-tab-disabled'))
1298 menu.querySelectorAll('[data-tab]').forEach(function(tab) {
1299 tab.classList.remove('cbi-tab');
1300 tab.classList.remove('cbi-tab-disabled');
1302 tab.getAttribute('data-tab') === name ? 'cbi-tab' : 'cbi-tab-disabled');
1305 group.childNodes.forEach(function(pane) {
1306 if (L.dom.matches(pane, '[data-tab]')) {
1307 if (pane.getAttribute('data-tab') === name) {
1308 pane.setAttribute('data-tab-active', 'true');
1309 L.ui.tabs.setActiveTabId(groupId, index);
1312 pane.setAttribute('data-tab-active', 'false');
1321 addValidator: function(field, type, optional /*, ... */) {
1325 var events = this.varargs(arguments, 3);
1326 if (events.length == 0)
1327 events.push('blur', 'keyup');
1330 var cbiValidator = new CBIValidator(field, type, optional),
1331 validatorFn = cbiValidator.validate.bind(cbiValidator);
1333 for (var i = 0; i < events.length; i++)
1334 field.addEventListener(events[i], validatorFn);
1342 Dropdown: UIDropdown,
1343 DynamicList: UIDynamicList,
1344 Combobox: UICombobox