// import { r, global, contains, isPositive, nullOrEmpty } from "../utility"; import './css/dropdown.scss'; import { r } from "../utility/lgres"; import { contains, nullOrEmpty } from "../utility/strings"; import { global, isPositive } from "../utility"; import { createElement } from "../functions"; import { createCheckbox } from "./checkbox"; import { createIcon } from "./icon" const SymbolDropdown = Symbol.for('ui-dropdown'); const DropdownItemHeight = 30; let dropdownGlobal = global[SymbolDropdown]; if (dropdownGlobal == null) { // init dropdownGlobal = {}; Object.defineProperty(dropdownGlobal, 'clear', { writable: false, configurable: false, enumerable: false, value: function () { const panels = document.querySelectorAll('.ui-drop-box.active'); for (let panel of [...panels]) { if (panel == null) { continue; } panel.classList.remove('active'); const dropId = panel.parentElement.dataset.dropId; if (dropId == null) { continue; } const dropdown = this[dropId]; if (dropdown?.multiselect && typeof dropdown.oncollapsed === 'function') { dropdown.oncollapsed(); } } } }) global[SymbolDropdown] = dropdownGlobal; document.addEventListener('mousedown', e => { let parent = e.target; while (parent != null) { if (parent.classList.contains('ui-drop-box')) { e.stopPropagation(); return; } parent = parent.parentElement; } dropdownGlobal.clear(); }); } function selectItems(label, itemlist, htmlkey, textkey) { const htmls = itemlist.map(it => it[htmlkey]); if (htmls.some(it => it instanceof HTMLElement)) { label.replaceChildren(...htmls.filter(it => it != null).map(it => it.cloneNode(true))); } else { let text = itemlist.map(it => it[textkey]).join(', '); if (nullOrEmpty(text)) { text = r('noneItem', '( None )'); } label.innerText = text; } } function filterSource(searchkeys, textkey, key, source) { if (!Array.isArray(searchkeys) || searchkeys.length === 0) { searchkeys = [textkey]; } if (key.length > 0) { source = source.filter(it => { for (let k of searchkeys) { if (contains(it[k].toLowerCase(), key)) { return true; } } return false; }); } return source; } export class Dropdown { _var = {}; // _var.options; // _var.wrapper; // _var.container; // _var.label; // _var.allChecked; // _var.source; // _var.lastSelected; // _var.selected; // _var.selectedList; sourceFilter; onselectedlist; onselected; onexpanded; constructor(options = {}) { options.searchPlaceholder ??= r('searchHolder', 'Search...'); options.textKey ??= 'text'; options.valueKey ??= 'value'; options.htmlKey ??= 'html'; options.maxLength ??= 500; this._var.options = options; } create() { const options = this._var.options; // wrapper const wrapper = createElement('div', 'ui-drop-wrapper'); const dropId = String(Math.random()).substring(2); wrapper.dataset.dropId = dropId; dropdownGlobal[dropId] = this; this._var.wrapper = wrapper; // header const header = createElement('div', 'ui-drop-header'); header.addEventListener('keypress', e => { if (e.key === ' ' || e.key === 'Enter') { header.dispatchEvent(new MouseEvent('click')); } }); header.addEventListener('keydown', e => { const up = e.key === 'ArrowUp'; const down = e.key === 'ArrowDown'; if (up || down) { const source = this.source; const count = source.length; const valuekey = this._var.options.valueKey; let index = source?.indexOf(this._var.selected); if (isNaN(index) || index < -1) { index = -1; } else if (index >= count) { index = count - 1; } if (up) { if (index > 0) { index--; } else { index = 0; } } else if (down) { if (index < 0) { index = 0; } else if (index < count) { index++; } else { index = count - 1; } } const target = source[index]?.[valuekey]; if (target != null) { this.select(target); } } else if (e.key === 'Tab') { this._dropdown(false); } }); header.addEventListener('click', () => { if (this.disabled) { return; } const active = this._expanded; const label = this._var.label; if (active && label.ownerDocument.activeElement === label) { return; } this._dropdown(!active); if (!active && typeof this.onexpanded === 'function') { setTimeout(() => this.onexpanded(), 120); } }); // label or input let label; if (options.input) { label = createElement('input', 'ui-drop-text'); label.setAttribute('type', 'text'); options.placeholder && label.setAttribute('placeholder', options.placeholder); isPositive(options.maxLength) && label.setAttribute('maxlength', options.maxLength); isPositive(options.tabIndex) && label.setAttribute('tabindex', options.tabIndex); label.addEventListener('input', e => { const key = e.target.value.toLowerCase(); const source = filterSource(options.searchKeys, options.textKey, key, this.source); this._filllist(source); this._var.container.classList.add('active'); }); label.addEventListener('blur', e => this.select(e.target.value)); label.addEventListener('mousedown', e => this._expanded && e.stopPropagation()); } else { isPositive(options.tabIndex) && header.setAttribute('tabindex', options.tabIndex); label = createElement('label', 'ui-drop-text'); } this._var.label = label; if (options.multiSelect) { if (Array.isArray(options.selectedList)) { this.selectlist(options.selectedList, true); } else { this._var.allChecked = true; label.innerText = r('allItem', '( All )'); } } else if (options.selected != null) { this.select(options.selected, true); } header.append(label, createElement('label', 'ui-drop-caret')); wrapper.appendChild(header); this.disabled = options.disabled || false; return wrapper; } get multiselect() { return this._var.options.multiSelect } get disabled() { return this._var.wrapper == null || this._var.wrapper.querySelector('.ui-drop-header.disabled') != null } set disabled(flag) { if (this._var.wrapper == null) { return; } if (flag) { this._var.wrapper.querySelector('.ui-drop-header').classList.add('disabled'); } else { this._var.wrapper.querySelector('.ui-drop-header').classList.remove('disabled'); } } get source() { let source = this._var.source; if (source == null || !Array.isArray(source)) { if (typeof this.sourceFilter === 'function') { source = this.sourceFilter(); } if (!Array.isArray(source)) { source = []; } this._var.source = source; } return source; } set source(list) { if (!Array.isArray(list)) { return; } this._var.source = list; if (this._expanded) { setTimeout(() => this._dropdown(), 120); } } get selected() { return this._var.selected } get selectedlist() { return this._var.selectedList || [] } select(selected, silence) { if (this._var.lastSelected === selected) { return false; } this._var.lastSelected = selected; const valuekey = this._var.options.valueKey; const textkey = this._var.options.textKey; const htmlkey = this._var.options.htmlKey; let item = this.source.find(it => it[valuekey] === selected); if (this._var.options.input) { if (item == null) { item = { [valuekey]: selected }; } this._var.label.value = selected; } else { const expanded = this._expanded; if (expanded) { this._var.container.querySelectorAll('li[data-value].selected').forEach(li => li.classList.remove('selected')); } if (item == null) { this._var.selected = null; this._var.label.innerText = ' '; return false; } const html = item[htmlkey]; if (html instanceof HTMLElement) { this._var.label.replaceChildren(html.cloneNode(true)); } else if (typeof html === 'string') { this._var.label.innerHTML = html; } else { let text = item[textkey]; if (nullOrEmpty(text)) { text = ' '; } this._var.label.innerText = text; } if (expanded) { const val = selected.replace(/"/g, '\\"'); const li = this._var.container.querySelector(`li[data-value="${val}"]`); if (li != null) { li.classList.add('selected'); } } } this._var.selected = item; if (!silence && typeof this.onselected === 'function') { this.onselected(item); } } selectlist(selectedlist, silence) { const source = this.source; const valuekey = this._var.options.valueKey; const textkey = this._var.options.textKey; const htmlkey = this._var.options.htmlKey; const itemlist = selectedlist.map(v => { let item = source.find(it => it[valuekey] === v); if (item == null) { item = { [valuekey]: v, [textkey]: v }; } return item; }); if (itemlist.length === 0) { this._var.selectedList = null; this._var.label.innerText = none; return false; } selectItems(this._var.label, itemlist, htmlkey, textkey); this._var.selectedList = itemlist; if (!silence && typeof this.onselectedlist === 'function') { this.onselectedlist(itemlist); } } get _expanded() { return this._var.container?.classList?.contains('active') } _dropdown(flag = true) { const options = this._var.options; let panel = this._var.container; if (panel == null) { panel = createElement('div', 'ui-drop-box'); // search box if (!options.input && options.search) { const search = createElement('div', 'ui-drop-search'); const input = createElement('input'); input.setAttribute('type', 'text'); isPositive(options.tabIndex) && input.setAttribute('tabindex', options.tabIndex); !nullOrEmpty(options.searchPlaceholder) && input.setAttribute('placeholder', options.searchPlaceholder); input.addEventListener('input', e => { const key = e.target.value.toLowerCase(); const source = filterSource(options.searchKeys, options.textKey, key, this.source); this._filllist(source); }) search.append(input, createIcon('fa-light', 'search')); panel.appendChild(search); } // list const list = createElement('ul', 'ui-drop-list'); if (!this.multiselect) { list.addEventListener('click', e => { let li = e.target; while (li.tagName !== 'LI') { li = li.parentElement; if (li == null) { return; } } const value = li.dataset.value; if (this.select(value) !== false) { dropdownGlobal.clear(); } }); } panel.appendChild(list); this._var.container = panel; if (options.wrapper instanceof HTMLElement) { options.wrapper.appendChild(panel); } else { this._var.wrapper.appendChild(panel); } } if (flag) { let source = this.source; if (!options.input && options.search) { const search = panel.querySelector('.ui-drop-search > input'); if (!nullOrEmpty(search?.value)) { source = filterSource(options.searchKeys, options.textKey, search.value, source); } } this._filllist(source); // slide direction if (!options.slideFixed) { const parent = options.wrapper ?? document.body; let p = this._var.wrapper; panel.style.minWidth = `${p.offsetWidth}px`; const headerHeight = p.offsetHeight; let top = p.offsetTop + headerHeight; let left = p.offsetLeft; if (p !== parent) { while ((p = p.parentElement) != null && p !== parent) { top -= p.scrollTop; left -= p.scrollLeft; } } p = this._var.wrapper; if (p !== parent) { while ((p = p.offsetParent) != null && p !== parent) { top += p.offsetTop; left += p.offsetLeft; } } const slideUp = top - parent.scrollTop + panel.offsetHeight >= parent.offsetHeight; if (options.wrapper instanceof HTMLElement) { if (slideUp) { panel.style.top = ''; panel.style.bottom = `${parent.offsetHeight - top + headerHeight - 4}px`; } else { panel.style.top = `${top}px`; panel.style.bottom = ''; } panel.style.left = `${left}px`; } if (slideUp) { panel.classList.add('slide-up'); } else { panel.classList.remove('slide-up'); } } panel.classList.add('active'); } else { panel.classList.remove('active'); } } _filllist(source) { const list = this._var.container.querySelector('.ui-drop-list'); list.replaceChildren(); const multiselect = this.multiselect; const allchecked = this._var.allChecked; if (multiselect) { list.appendChild( createElement('li', null, createCheckbox({ label: r('allItem', '( All )'), checked: allchecked, customAttributes: { 'isall': '1' }, onchange: e => this._triggerselect(e.target) }) ) ); } // TODO: virtual mode const valuekey = this._var.options.valueKey; const textkey = this._var.options.textKey; const htmlkey = this._var.options.htmlKey; const selected = this.selected; const selectedlist = this.selectedlist; let scrolled; source.slice(0, 200).forEach((item, i) => { const val = item[valuekey]; const li = createElement('li'); li.dataset.value = val; li.setAttribute('title', item[textkey]); let label; const html = item[htmlkey]; if (html instanceof HTMLElement) { label = html; } else if (typeof html === 'string') { label = createElement('span'); label.innerHTML = html; } if (multiselect) { const selected = selectedlist.some(s => s[valuekey] === val); if (label == null) { label = createElement('span'); label.innerText = item[textkey]; } const box = createCheckbox({ label, checked: allchecked || selected, customAttributes: { 'class': 'dataitem', 'data-value': val }, onchange: e => this._triggerselect(e.target) }); li.appendChild(box); } else { if (label == null) { li.innerText = item[textkey]; } else { li.appendChild(label); } if (selected != null && selected[valuekey] === val) { scrolled = DropdownItemHeight * i; li.classList.add('selected'); } } list.appendChild(li); }); if (scrolled != null) { setTimeout(() => list.scrollTop = scrolled, 10); } } _triggerselect(checkbox) { let list; const valuekey = this._var.options.valueKey; const textkey = this._var.options.textKey; const htmlkey = this._var.options.htmlKey; if (checkbox.getAttribute('isall') === '1') { const allchecked = this._var.allChecked = checkbox.checked; const boxes = this._var.container.querySelectorAll('input.dataitem'); boxes.forEach(box => box.checked = allchecked); list = []; } else if (checkbox.checked) { if (this._var.container.querySelectorAll('input.dataitem:not(:checked)').length === 0) { this._var.allChecked = true; this._var.container.querySelector('input[isall="1"]').checked = true; list = []; } else { const source = this.source; list = [...this._var.container.querySelectorAll('input.dataitem:checked')] .map(c => source.find(it => it[valuekey] === c.dataset.value)) .filter(it => it != null); } } else { const val = checkbox.dataset.value; if (this._var.allChecked) { this._var.allChecked = false; this._var.container.querySelector('input[isall="1"]').checked = false; list = this.source.filter(it => it[valuekey] !== val); } else { list = this.selectedlist.filter(it => it[valuekey] !== val); } } if (this._var.allChecked) { this._var.label.innerText = r('allItem', '( All )'); } else { selectItems(this._var.label, list, htmlkey, textkey); } this._var.selectedList = list; if (typeof this.onselectedlist === 'function') { this.onselectedlist(itemlist); } } static resolve(dom = document.body) { const selects = dom.querySelectorAll('select'); for (let sel of selects) { const source = [...sel.children].map(it => { return { value: it.value, text: it.innerText } }); const drop = new Dropdown({ selected: sel.value, disabled: sel.disabled, tabIndex: sel.tabIndex }); drop.source = source; sel.parentElement.replaceChild(drop.create(), sel); } return dom; } }