add dropdown component
This commit is contained in:
3
lib/ui/checkbox.d.ts
vendored
3
lib/ui/checkbox.d.ts
vendored
@ -1,11 +1,12 @@
|
||||
interface CheckboxOptions {
|
||||
type?: string;
|
||||
label?: string;
|
||||
label?: string | HTMLElement;
|
||||
checked?: boolean;
|
||||
isImage?: boolean;
|
||||
imageHeight?: Number;
|
||||
checkedNode?: HTMLElement;
|
||||
uncheckedNode?: HTMLElement;
|
||||
customerAttributes?: { [key: string]: string };
|
||||
onchange?: (this: HTMLInputElement, ev: Event) => any;
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@
|
||||
imageHeight?: Number;
|
||||
checkedNode?: HTMLElement;
|
||||
uncheckedNode?: HTMLElement;
|
||||
customerAttributes?: { [key: string]: string };
|
||||
onchange?: (this: HTMLInputElement, ev: Event) => any;
|
||||
}</pre>
|
||||
</p>
|
||||
@ -25,9 +26,9 @@
|
||||
<p>
|
||||
复选框图标的样式,可选值目前有 <code>fa-regular</code>、<code>fa-light</code>、<code>fa-solid</code>
|
||||
</p>
|
||||
<h3>label?: string</h3>
|
||||
<h3>label?: string | HTMLElement</h3>
|
||||
<p>
|
||||
复选框的标签文本
|
||||
复选框的标签文本,或者想要呈现的元素
|
||||
</p>
|
||||
<h3>checked?: boolean</h3>
|
||||
<p>
|
||||
@ -49,6 +50,14 @@
|
||||
<p>
|
||||
为图片复选框时的未选中时显示的元素
|
||||
</p>
|
||||
<h3>customerAttributes?: { [key: string]: string }</h3>
|
||||
<p>
|
||||
自定义属性,例如
|
||||
<pre>{
|
||||
'data-id': 'xxxxxx',
|
||||
'disabled': ''
|
||||
}</pre>
|
||||
</p>
|
||||
<h3>onchange?: (this: HTMLInputElement, ev: Event) => any</h3>
|
||||
<p>
|
||||
复选框改变时触发的事件
|
||||
|
@ -5,7 +5,9 @@ function fillCheckbox(container, type, label) {
|
||||
layer.className = 'check-box-inner';
|
||||
layer.appendChild(createIcon(type, 'check'));
|
||||
container.appendChild(layer);
|
||||
if (label != null && label.length > 0) {
|
||||
if (label instanceof HTMLElement) {
|
||||
container.appendChild(label);
|
||||
} else if (label != null && label.length > 0) {
|
||||
const span = document.createElement('span');
|
||||
span.innerText = label;
|
||||
container.appendChild(span);
|
||||
@ -21,6 +23,11 @@ function createCheckbox(opts) {
|
||||
if (opts.checked === true) {
|
||||
input.checked = true;
|
||||
}
|
||||
if (opts.customerAttributes != null) {
|
||||
for (let entry of Object.entries(opts.customerAttributes)) {
|
||||
input.setAttribute(entry[0], entry[1]);
|
||||
}
|
||||
}
|
||||
if (typeof opts.onchange === 'function') {
|
||||
input.addEventListener('change', opts.onchange);
|
||||
}
|
||||
@ -50,9 +57,7 @@ function createCheckbox(opts) {
|
||||
}
|
||||
|
||||
function resolveCheckbox(container, legacy) {
|
||||
if (container == null) {
|
||||
container = document.body;
|
||||
}
|
||||
container ??= document.body;
|
||||
if (legacy) {
|
||||
const checks = container.querySelectorAll('input[type="checkbox"]');
|
||||
for (let chk of checks) {
|
||||
|
32
lib/ui/dropdown.d.ts
vendored
Normal file
32
lib/ui/dropdown.d.ts
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
interface DropdownOptions {
|
||||
textkey?: string;
|
||||
valuekey?: string;
|
||||
htmlkey?: string;
|
||||
maxlength?: Number;
|
||||
multiselect?: boolean;
|
||||
selected?: any;
|
||||
selectedlist?: any[];
|
||||
disabled?: boolean;
|
||||
input?: boolean;
|
||||
search?: boolean;
|
||||
searchkeys?: string[];
|
||||
searchplaceholder?: string;
|
||||
tabindex?: Number;
|
||||
slidefixed?: boolean;
|
||||
parent?: HTMLElement
|
||||
}
|
||||
|
||||
interface Dropdown {
|
||||
create(): HTMLElement;
|
||||
get disabled(): boolean;
|
||||
set disabled(flag: boolean);
|
||||
readonly multiselect: boolean;
|
||||
readonly selected: any;
|
||||
}
|
||||
|
||||
declare var Dropdown: {
|
||||
prototype: Dropdown;
|
||||
new(options: DropdownOptions): Dropdown;
|
||||
};
|
||||
|
||||
export default Dropdown;
|
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;
|
@ -49,9 +49,7 @@ function setTooltip(container, content) {
|
||||
}
|
||||
|
||||
function resolveTooltip(container) {
|
||||
if (container == null) {
|
||||
container = document.body;
|
||||
}
|
||||
container ??= document.body;
|
||||
const tips = container.querySelectorAll('[title]');
|
||||
for (let tip of tips) {
|
||||
const title = tip.getAttribute('title');
|
||||
|
Reference in New Issue
Block a user