add dropdown component
This commit is contained in:
466
lib/ui/dropdown.js
Normal file
466
lib/ui/dropdown.js
Normal file
@ -0,0 +1,466 @@
|
||||
// import { r, global, contains, isPositive, nullOrEmpty } from "../utility";
|
||||
import { r } from "../utility/lgres";
|
||||
import { contains, nullOrEmpty } from "../utility/strings";
|
||||
import { global, isPositive } from "../utility";
|
||||
import { createCheckbox } from "./checkbox";
|
||||
import { createIcon } from "./icon"
|
||||
|
||||
const SymbolDropdown = Symbol.for('ui-dropdown');
|
||||
const DropdownTitleHeight = 26;
|
||||
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 panel = document.querySelector('.dropdown-wrapper .dropdown-panel.active');
|
||||
if (panel == null) {
|
||||
return;
|
||||
}
|
||||
panel.classList.remove('active');
|
||||
const dropId = panel.parentElement.dataset.dropId;
|
||||
if (dropId == null) {
|
||||
return;
|
||||
}
|
||||
const dropdown = this[dropId];
|
||||
if (dropdown != null && 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('dropdown-panel')) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
dropdownGlobal.clear();
|
||||
});
|
||||
}
|
||||
|
||||
class Dropdown {
|
||||
#options;
|
||||
|
||||
#wrapper;
|
||||
#container;
|
||||
#label;
|
||||
|
||||
#allChecked;
|
||||
#source;
|
||||
#lastSelected;
|
||||
#selected;
|
||||
#selectedList;
|
||||
|
||||
sourceFilter;
|
||||
onselectedlist;
|
||||
onselected;
|
||||
onexpanded;
|
||||
|
||||
constructor(options) {
|
||||
options ??= {};
|
||||
options.searchplaceholder ??= r('searchHolder', 'Search...');
|
||||
options.textkey ??= 'text';
|
||||
options.valuekey ??= 'value';
|
||||
options.htmlkey ??= 'html';
|
||||
options.maxlength ??= 500;
|
||||
this.#options = options;
|
||||
}
|
||||
|
||||
create() {
|
||||
const options = this.#options;
|
||||
|
||||
// wrapper
|
||||
const wrapper = document.createElement('div');
|
||||
const dropId = String(Math.random()).substring(2);
|
||||
wrapper.dataset.dropId = dropId;
|
||||
wrapper.className = 'dropdown-wrapper';
|
||||
dropdownGlobal[dropId] = this;
|
||||
this.#wrapper = wrapper;
|
||||
|
||||
// header
|
||||
const header = document.createElement('div');
|
||||
header.className = 'dropdown-header';
|
||||
header.addEventListener('click', () => {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
const active = this.#expanded;
|
||||
if (active && this.#label.hasFocus()) {
|
||||
return;
|
||||
}
|
||||
this.#dropdown(!active);
|
||||
if (!active && typeof this.onexpanded === 'function') {
|
||||
setTimeout(() => this.onexpanded(), 120);
|
||||
}
|
||||
});
|
||||
|
||||
// label or input
|
||||
let label;
|
||||
let searchkeys = options.searchkeys;
|
||||
if (!Array.isArray(searchkeys) || searchkeys.length === 0) {
|
||||
searchkeys = [options.textkey];
|
||||
}
|
||||
if (options.input) {
|
||||
label = document.createElement('input');
|
||||
label.className = 'dropdown-text';
|
||||
label.setAttribute('type', 'text');
|
||||
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();
|
||||
let source = this.source;
|
||||
if (key.length > 0) {
|
||||
source = source.filter(it => {
|
||||
for (let k of searchkeys) {
|
||||
if (contains(it[k].toLowerCase(), key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
this.#filllist(source);
|
||||
this.#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 = document.createElement('label');
|
||||
label.className = 'dropdown-text';
|
||||
}
|
||||
this.#label = label;
|
||||
if (options.multiselect) {
|
||||
if (Array.isArray(options.selectedlist)) {
|
||||
this.selectlist(options.selectedlist, true);
|
||||
} else {
|
||||
this.#allChecked = true;
|
||||
label.innerText = r('allItem', '( All )');
|
||||
}
|
||||
} else if (options.selected != null) {
|
||||
this.select(options.selected, true);
|
||||
}
|
||||
header.appendChild(label);
|
||||
const caret = document.createElement('label');
|
||||
caret.className = 'dropdown-caret';
|
||||
header.appendChild(caret);
|
||||
wrapper.appendChild(header);
|
||||
|
||||
this.disabled = options.disabled || false;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
get multiselect() { return this.#options.multiselect }
|
||||
|
||||
get disabled() { return this.#wrapper == null || this.#wrapper.querySelector('.dropdown-header.disabled') != null }
|
||||
|
||||
set disabled(flag) {
|
||||
if (this.#wrapper == null) {
|
||||
return;
|
||||
}
|
||||
if (flag) {
|
||||
this.#wrapper.querySelector('.dropdown-header').classList.add('disabled');
|
||||
} else {
|
||||
this.#wrapper.querySelector('.dropdown-header').classList.remove('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
get source() {
|
||||
let source = this.#source;
|
||||
if (source == null || !Array.isArray(source)) {
|
||||
if (typeof this.sourceFilter === 'function') {
|
||||
source = this.sourceFilter();
|
||||
}
|
||||
if (!Array.isArray(source)) {
|
||||
source = [];
|
||||
}
|
||||
this.#source = source;
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
set source(list) {
|
||||
if (!Array.isArray(list)) {
|
||||
return;
|
||||
}
|
||||
this.#source = list;
|
||||
if (this.#expanded) {
|
||||
setTimeout(() => this.#dropdown(), 120);
|
||||
}
|
||||
}
|
||||
|
||||
get selected() { return this.#selected }
|
||||
|
||||
get selectedlist() { return this.#selectedList || [] }
|
||||
|
||||
select(selected, init) {
|
||||
if (this.#lastSelected === selected) {
|
||||
return false;
|
||||
}
|
||||
this.#lastSelected = selected;
|
||||
const valuekey = this.#options.valuekey;
|
||||
const textkey = this.#options.textkey;
|
||||
let item = this.source.find(it => it[valuekey] === selected);
|
||||
if (this.#options.input) {
|
||||
if (item == null) {
|
||||
item = {};
|
||||
item[valuekey] = selected;
|
||||
}
|
||||
this.#label.value = selected;
|
||||
} else {
|
||||
if (item == null) {
|
||||
this.#selected = null;
|
||||
this.#label.innerText = ' ';
|
||||
return false;
|
||||
}
|
||||
let text = item[textkey];
|
||||
if (nullOrEmpty(text)) {
|
||||
text = ' ';
|
||||
}
|
||||
this.#label.innerText = text;
|
||||
}
|
||||
this.#selected = item;
|
||||
if (!init && typeof this.onselected === 'function') {
|
||||
this.onselected(item);
|
||||
}
|
||||
}
|
||||
|
||||
selectlist(selectedlist, init) {
|
||||
const source = this.source;
|
||||
const valuekey = this.#options.valuekey;
|
||||
const textkey = this.#options.textkey;
|
||||
const itemlist = selectedlist.map(v => {
|
||||
let item = source.find(it => it[valuekey] === v);
|
||||
if (item == null) {
|
||||
item = {};
|
||||
item[valuekey] = v;
|
||||
item[textkey] = v;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
const none = r('noneItem', '( None )');
|
||||
if (itemlist.length === 0) {
|
||||
this.#selectedList = null;
|
||||
this.#label.innerText = none;
|
||||
return false;
|
||||
}
|
||||
const text = itemlist.map(it => it[textkey]).join(', ');
|
||||
if (nullOrEmpty(text)) {
|
||||
text = none;
|
||||
}
|
||||
this.#selectedList = itemlist;
|
||||
this.#label.innerText = text;
|
||||
if (!init && typeof this.onselectedlist === 'function') {
|
||||
this.onselectedlist(itemlist);
|
||||
}
|
||||
}
|
||||
|
||||
get #expanded() { return this.#container != null && this.#container.style.visibility === 'visible' }
|
||||
|
||||
#dropdown(flag) {
|
||||
flag ??= true;
|
||||
const options = this.#options;
|
||||
const textkey = options.textkey;
|
||||
let panel = this.#container;
|
||||
if (panel == null) {
|
||||
panel = document.createElement('div');
|
||||
panel.className = 'dropdown-panel';
|
||||
// search box
|
||||
if (options.search) {
|
||||
let searchkeys = options.searchkeys;
|
||||
if (!Array.isArray(searchkeys) || searchkeys.length === 0) {
|
||||
searchkeys = [textkey];
|
||||
}
|
||||
const search = document.createElement('div');
|
||||
search.className = 'dropdown-search';
|
||||
const input = document.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();
|
||||
let source = this.source;
|
||||
if (key.length > 0) {
|
||||
source = source.filter(it => {
|
||||
for (let k of searchkeys) {
|
||||
if (contains(it[k].toLowerCase(), key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
this.#filllist(source);
|
||||
})
|
||||
search.appendChild(input);
|
||||
search.appendChild(createIcon('fa-light', 'search'));
|
||||
panel.appendChild(search);
|
||||
}
|
||||
// list
|
||||
const list = document.createElement('ul');
|
||||
list.className = 'dropdown-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.#container = panel;
|
||||
this.#wrapper.appendChild(panel);
|
||||
this.#filllist(this.source);
|
||||
}
|
||||
if (flag) {
|
||||
if (!options.slidefixed) {
|
||||
let parent = options.parent ?? document.body;
|
||||
const height = panel.offsetHeight;
|
||||
if (this.#wrapper.offsetTop - parent.offsetTop + DropdownTitleHeight + height >= parent.offsetHeight) {
|
||||
panel.style.marginTop = -height - DropdownTitleHeight - 2;
|
||||
panel.classList.add('slide-up');
|
||||
} else {
|
||||
panel.style.marginTop = null;
|
||||
panel.classList.remove('slide-up');
|
||||
}
|
||||
}
|
||||
panel.classList.add('active');
|
||||
// search input
|
||||
// const inputSearch = panel.querySelector('.dropdown-search > input');
|
||||
// if (!nullOrEmpty(inputSearch.value)) {
|
||||
// const event = new InputEvent('type');
|
||||
// inputSearch.dispatchEvent(event);
|
||||
// }
|
||||
} else {
|
||||
panel.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
#filllist(source) {
|
||||
const list = this.#container.querySelector('.dropdown-list');
|
||||
list.replaceChildren();
|
||||
const multiselect = this.multiselect;
|
||||
const allchecked = this.#allChecked;
|
||||
if (multiselect) {
|
||||
const liall = document.createElement('li');
|
||||
const boxall = createCheckbox({
|
||||
label: r('allItem', '( All )'),
|
||||
checked: allchecked,
|
||||
customerAttributes: { 'isall': '1' },
|
||||
onchange: e => this.#triggerselect(e.target)
|
||||
});
|
||||
liall.appendChild(boxall);
|
||||
list.appendChild(liall);
|
||||
}
|
||||
// TODO: virtual mode
|
||||
const valuekey = this.#options.valuekey;
|
||||
const textkey = this.#options.textkey;
|
||||
const htmlkey = this.#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 = document.createElement('li');
|
||||
li.dataset.value = val;
|
||||
li.setAttribute('title', item[textkey]);
|
||||
let label;
|
||||
const html = item[htmlkey];
|
||||
if (html instanceof HTMLElement) {
|
||||
label = html;
|
||||
}
|
||||
if (multiselect) {
|
||||
const selected = selectedlist.some(s => s[valuekey] === val);
|
||||
if (label == null) {
|
||||
label = document.createElement('span');
|
||||
label.innerText = item[textkey];
|
||||
}
|
||||
const box = createCheckbox({
|
||||
label,
|
||||
checked: allchecked || selected,
|
||||
customerAttributes: {
|
||||
'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.#options.valuekey;
|
||||
const textkey = this.#options.textkey;
|
||||
if (checkbox.getAttribute('isall') === '1') {
|
||||
const allchecked = this.#allChecked = checkbox.checked;
|
||||
const boxes = this.#container.querySelectorAll('input.dataitem');
|
||||
boxes.forEach(box => box.checked = allchecked);
|
||||
list = [];
|
||||
} else if (checkbox.checked) {
|
||||
if (this.#container.querySelectorAll('input.dataitem:not(:checked)').length === 0) {
|
||||
this.#allChecked = true;
|
||||
this.#container.querySelector('input[isall="1"]').checked = true;
|
||||
list = [];
|
||||
} else {
|
||||
const source = this.source;
|
||||
list = [...this.#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.#allChecked) {
|
||||
this.#allChecked = false;
|
||||
this.#container.querySelector('input[isall="1"]').checked = false;
|
||||
list = this.source.filter(it => it[valuekey] !== val);
|
||||
} else {
|
||||
list = this.selectedlist.filter(it => it[valuekey] !== val);
|
||||
}
|
||||
}
|
||||
let text = this.#allChecked ? r('allItem', '( All )') : list.map(it => it[textkey]).join(', ');
|
||||
if (nullOrEmpty(text)) {
|
||||
text = r('noneItem', '( None )');
|
||||
}
|
||||
this.#selectedList = list;
|
||||
this.#label.innerText = text;
|
||||
if (typeof this.onselectedlist === 'function') {
|
||||
this.onselectedlist(itemlist);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Dropdown;
|
Reference in New Issue
Block a user