From 41b1bbd7d60bcb23de92882d94675abdecc7218c Mon Sep 17 00:00:00 2001 From: Tsanie Lily Date: Fri, 31 Mar 2023 17:35:36 +0800 Subject: [PATCH] add dropdown component --- css/checkbox.scss | 26 +- css/dropdown.scss | 43 +--- css/tooltip.scss | 2 +- css/ui.min.css | 2 +- css/ui.scss | 1 + css/variables/definition.scss | 3 +- lib/ui.js | 5 +- lib/ui/checkbox.d.ts | 3 +- lib/ui/checkbox.html | 13 +- lib/ui/checkbox.js | 13 +- lib/ui/dropdown.d.ts | 32 +++ lib/ui/dropdown.js | 466 ++++++++++++++++++++++++++++++++++ lib/ui/tooltip.js | 4 +- lib/utility.js | 11 +- lib/utility/lgres.html | 2 +- lib/utility/lgres.js | 10 +- lib/utility/request.html | 2 +- lib/utility/strings.js | 5 +- main.js | 4 +- vite.build.js | 2 +- 20 files changed, 575 insertions(+), 74 deletions(-) create mode 100644 lib/ui/dropdown.d.ts create mode 100644 lib/ui/dropdown.js diff --git a/css/checkbox.scss b/css/checkbox.scss index e054816..fbd3a7e 100644 --- a/css/checkbox.scss +++ b/css/checkbox.scss @@ -34,19 +34,19 @@ .check-box-inner { flex: 0 0 auto; - } - >span { - flex: 1 1 auto; - font-weight: 400; - font-size: $mediumSize; - padding-left: 8px; - padding-right: 6px; - align-self: center; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - cursor: pointer; - color: #201f1e; + &+* { + flex: 1 1 auto; + font-weight: 400; + font-size: $mediumSize; + padding-left: 8px; + padding-right: 6px; + align-self: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: pointer; + color: $foreColor; + } } } \ No newline at end of file diff --git a/css/dropdown.scss b/css/dropdown.scss index c469044..c3fe4ca 100644 --- a/css/dropdown.scss +++ b/css/dropdown.scss @@ -14,6 +14,9 @@ $searchIconSize: 13px; $listMaxHeight: 210px; .dropdown-wrapper { + display: inline-block; + border: none; + @include border-radius(unset); @include user-select(none); position: relative; @@ -35,7 +38,7 @@ $listMaxHeight: 210px; outline: none; } - >.dropdown-select-container { + /*>.dropdown-select-container { flex: 1 1 auto; overflow-x: auto; white-space: nowrap; @@ -98,7 +101,7 @@ $listMaxHeight: 210px; font-size: .8125rem; color: $foreColor; } - } + }*/ >.dropdown-text { flex: 1 1 auto; @@ -132,11 +135,11 @@ $listMaxHeight: 210px; } &.disabled { - border-color: $disabledColor; - color: $disabledColor; + border-color: $disabledBgColor; + color: $disabledForeColor; &:focus { - border-color: $disabledColor; + border-color: $disabledBgColor; // @include box-shadow(none); } @@ -217,6 +220,7 @@ $listMaxHeight: 210px; >.dropdown-list { margin: 0; padding: 0; + padding-bottom: 6px; list-style: none; max-height: $listMaxHeight; overflow-y: auto; @@ -226,8 +230,8 @@ $listMaxHeight: 210px; } >li { - display: flex; - align-items: center; + // display: flex; + // align-items: center; line-height: $dropItemHeight; height: $dropItemHeight; padding: 0 10px; @@ -241,29 +245,8 @@ $listMaxHeight: 210px; background-color: $hoverColor; } - @include check-box(); - - >label { - display: flex; - align-items: center; - } - - .check-box-inner { - flex: 0 0 auto; - } - - .check-box-inner+* { - flex: 1 1 auto; - font-weight: 400; - font-size: .8125rem; - padding-left: 8px; - padding-right: 6px; - align-self: center; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - cursor: pointer; - color: $foreColor; + >.checkbox-wrapper { + height: $dropItemHeight; } } } diff --git a/css/tooltip.scss b/css/tooltip.scss index b5f1a1d..02b6041 100644 --- a/css/tooltip.scss +++ b/css/tooltip.scss @@ -46,7 +46,7 @@ font-size: $smallSize; line-height: 1rem; white-space: normal; - overflow: auto; + overflow: hidden; margin: 8px; height: calc(100% - 16px); @include user-select(none); diff --git a/css/ui.min.css b/css/ui.min.css index 1ec666f..c309a49 100644 --- a/css/ui.min.css +++ b/css/ui.min.css @@ -1 +1 @@ -.checkbox-image>input[type=checkbox]{display:none}.checkbox-image>input[type=checkbox]:checked~.checked{display:inline}.checkbox-image>input[type=checkbox]:checked~.unchecked{display:none}.checkbox-image>.checked{display:none}.checkbox-image>.unchecked{display:inline}.checkbox-wrapper{display:inline-flex;align-items:center;padding:0 8px;height:36px}.checkbox-wrapper .check-box-inner{position:relative;display:inline-block;padding:0;width:14px;height:14px;background-color:#fff;border:1px solid #999898;-moz-user-select:none;user-select:none;-webkit-user-select:none;border-radius:2px;transition:all .2s;cursor:pointer}.checkbox-wrapper .check-box-inner>svg{position:absolute;top:0;left:0;width:100%;height:100%;fill:#fff;transform:scale(0);opacity:0;transition:all .08s cubic-bezier(0.78, 0.14, 0.15, 0.86)}.checkbox-wrapper>input[type=checkbox]{display:none}.checkbox-wrapper>input[type=checkbox]:checked+.check-box-inner{border-color:#1890ff;background-color:#1890ff}.checkbox-wrapper>input[type=checkbox]:checked+.check-box-inner>svg{transform:scale(1);opacity:1}.checkbox-wrapper>input[type=checkbox]:disabled+.check-box-inner{border-color:#d9d9d9;cursor:default}.checkbox-wrapper>input[type=checkbox]:disabled:checked+.check-box-inner{border-color:#d9d9d9;background-color:#d9d9d9}.checkbox-wrapper>input[type=checkbox]:disabled~span{color:#d9d9d9;cursor:default}.checkbox-wrapper .check-box-inner{flex:0 0 auto}.checkbox-wrapper>span{flex:1 1 auto;font-weight:400;font-size:.875rem;padding-left:8px;padding-right:6px;align-self:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer;color:#201f1e}.tooltip-color{background-color:#fff;color:#323130;border-color:rgba(204,204,204,.8);outline:none}.tooltip-wrapper{position:fixed;word-wrap:break-word;height:auto;text-align:left;z-index:250;min-width:75px;max-width:480px;min-height:32px;border-radius:2px;box-shadow:0 3.2px 7.2px 0 rgba(0,0,0,.13),0 .6px 1.8px 0 rgba(0,0,0,.11);transition:visibility 0s linear 120ms,opacity 120ms ease}.tooltip-wrapper>.tooltip-pointer{box-sizing:border-box;box-shadow:0 5px 15px 2px rgba(0,0,0,.3);border:1px solid #fff;z-index:-1;width:16px;height:16px;position:absolute;left:calc(50% - 8px);bottom:-8px;transform:rotate(-45deg);transform-origin:center}.tooltip-wrapper>.tooltip-curtain{position:absolute;width:100%;height:100%;z-index:-1}.tooltip-wrapper>.tooltip-content{font-size:.8125rem;line-height:1rem;white-space:normal;overflow:auto;margin:8px;height:calc(100% - 16px);-moz-user-select:none;user-select:none;-webkit-user-select:none} \ No newline at end of file +.checkbox-image>input[type=checkbox]{display:none}.checkbox-image>input[type=checkbox]:checked~.checked{display:inline}.checkbox-image>input[type=checkbox]:checked~.unchecked{display:none}.checkbox-image>.checked{display:none}.checkbox-image>.unchecked{display:inline}.checkbox-wrapper{display:inline-flex;align-items:center;padding:0 8px;height:36px}.checkbox-wrapper .check-box-inner{position:relative;display:inline-block;padding:0;width:14px;height:14px;background-color:#fff;border:1px solid #999898;-moz-user-select:none;user-select:none;-webkit-user-select:none;border-radius:2px;transition:all .2s;cursor:pointer}.checkbox-wrapper .check-box-inner>svg{position:absolute;top:0;left:0;width:100%;height:100%;fill:#fff;transform:scale(0);opacity:0;transition:all .08s cubic-bezier(0.78, 0.14, 0.15, 0.86)}.checkbox-wrapper>input[type=checkbox]{display:none}.checkbox-wrapper>input[type=checkbox]:checked+.check-box-inner{border-color:#1890ff;background-color:#1890ff}.checkbox-wrapper>input[type=checkbox]:checked+.check-box-inner>svg{transform:scale(1);opacity:1}.checkbox-wrapper>input[type=checkbox]:disabled+.check-box-inner{border-color:#d9d9d9;cursor:default}.checkbox-wrapper>input[type=checkbox]:disabled:checked+.check-box-inner{border-color:#d9d9d9;background-color:#d9d9d9}.checkbox-wrapper>input[type=checkbox]:disabled~span{color:#d9d9d9;cursor:default}.checkbox-wrapper .check-box-inner{flex:0 0 auto}.checkbox-wrapper .check-box-inner+*{flex:1 1 auto;font-weight:400;font-size:.875rem;padding-left:8px;padding-right:6px;align-self:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer;color:#201f1e}.dropdown-wrapper{display:inline-block;border:none;border-radius:unset;-moz-user-select:none;user-select:none;-webkit-user-select:none;position:relative}.dropdown-wrapper>.dropdown-header{border:1px solid #d9d9d9;border-radius:2px;background-color:#fff;display:flex;height:26px;transition:all .3s}.dropdown-wrapper>.dropdown-header:focus{border-color:#b9b9b9}.dropdown-wrapper>.dropdown-header:focus,.dropdown-wrapper>.dropdown-header:focus-visible{outline:none}.dropdown-wrapper>.dropdown-header>.dropdown-text{flex:1 1 auto;cursor:pointer;font-size:.875rem;line-height:26px;padding:0 6px;overflow:hidden;text-overflow:ellipsis;border:none;outline:none}.dropdown-wrapper>.dropdown-header>.dropdown-caret{flex:0 0 auto;width:26px;display:flex;justify-content:center;align-items:center;cursor:pointer}.dropdown-wrapper>.dropdown-header>.dropdown-caret::after{display:block;content:"";border-top:4px solid;border-left:4px solid rgba(0,0,0,0);border-right:4px solid rgba(0,0,0,0);height:0;width:0}.dropdown-wrapper>.dropdown-header.disabled{border-color:#e9e9e9;color:#aaa}.dropdown-wrapper>.dropdown-header.disabled:focus{border-color:#e9e9e9}.dropdown-wrapper>.dropdown-header.disabled>.dropdown-text,.dropdown-wrapper>.dropdown-header.disabled>.dropdown-caret{cursor:default}.dropdown-wrapper>.dropdown-panel{position:absolute;visibility:hidden;opacity:0;transform:scaleY(0);transform-origin:top;background-color:#fff;top:28px;z-index:2;transition:transform 120ms ease,opacity 120ms ease,visibility 120ms ease;width:calc(100% + 2px);box-sizing:border-box;box-shadow:0 3px 6px -4px rgba(0,0,0,.12),0 6px 16px 0 rgba(0,0,0,.08),0 9px 28px 8px rgba(0,0,0,.05);left:-1px}.dropdown-wrapper>.dropdown-panel.slide-up{top:auto;transform-origin:bottom}.dropdown-wrapper>.dropdown-panel.active{visibility:visible;opacity:1;transform:scaleY(1)}.dropdown-wrapper>.dropdown-panel>.dropdown-search{box-sizing:border-box;height:40px;line-height:40px;padding:0 8px;position:relative}.dropdown-wrapper>.dropdown-panel>.dropdown-search>input[type=text]{box-sizing:border-box;width:100%;height:26px;outline:none;border:1px solid #d9d9d9;border-radius:2px;padding:0 6px 0 22px;color:#201f1e;transition:all .3s}.dropdown-wrapper>.dropdown-panel>.dropdown-search>input[type=text]:hover,.dropdown-wrapper>.dropdown-panel>.dropdown-search>input[type=text]:focus{border-color:#b9b9b9}.dropdown-wrapper>.dropdown-panel>.dropdown-search>svg{position:absolute;left:14px;width:13px;height:100%;cursor:text}.dropdown-wrapper>.dropdown-panel>.dropdown-list{margin:0;padding:0;padding-bottom:6px;list-style:none;max-height:210px;overflow-y:auto}.dropdown-wrapper>.dropdown-panel>.dropdown-list.filtered>li:first-child{background-color:#eee}.dropdown-wrapper>.dropdown-panel>.dropdown-list>li{line-height:30px;height:30px;padding:0 10px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dropdown-wrapper>.dropdown-panel>.dropdown-list>li:hover,.dropdown-wrapper>.dropdown-panel>.dropdown-list>li.selected{background-color:#eee}.dropdown-wrapper>.dropdown-panel>.dropdown-list>li>.checkbox-wrapper{height:30px}.tooltip-color{background-color:#fff;color:#323130;border-color:rgba(204,204,204,.8);outline:none}.tooltip-wrapper{position:fixed;word-wrap:break-word;height:auto;text-align:left;z-index:250;min-width:75px;max-width:480px;min-height:32px;border-radius:2px;box-shadow:0 3.2px 7.2px 0 rgba(0,0,0,.13),0 .6px 1.8px 0 rgba(0,0,0,.11);transition:visibility 0s linear 120ms,opacity 120ms ease}.tooltip-wrapper>.tooltip-pointer{box-sizing:border-box;box-shadow:0 5px 15px 2px rgba(0,0,0,.3);border:1px solid #fff;z-index:-1;width:16px;height:16px;position:absolute;left:calc(50% - 8px);bottom:-8px;transform:rotate(-45deg);transform-origin:center}.tooltip-wrapper>.tooltip-curtain{position:absolute;width:100%;height:100%;z-index:-1}.tooltip-wrapper>.tooltip-content{font-size:.8125rem;line-height:1rem;white-space:normal;overflow:hidden;margin:8px;height:calc(100% - 16px);-moz-user-select:none;user-select:none;-webkit-user-select:none} \ No newline at end of file diff --git a/css/ui.scss b/css/ui.scss index 04e352e..6109287 100644 --- a/css/ui.scss +++ b/css/ui.scss @@ -1,2 +1,3 @@ @import './checkbox.scss'; +@import './dropdown.scss'; @import './tooltip.scss'; \ No newline at end of file diff --git a/css/variables/definition.scss b/css/variables/definition.scss index 15a88a0..06077ef 100644 --- a/css/variables/definition.scss +++ b/css/variables/definition.scss @@ -1,7 +1,8 @@ // color $borderColor: #d9d9d9; $focusColor: #b9b9b9; -$disabledColor: #e9e9e9; +$disabledBgColor: #e9e9e9; +$disabledForeColor: #aaa; $hoverColor: #eee; $bgColor: #fff; $foreColor: #201f1e; diff --git a/lib/ui.js b/lib/ui.js index 2c5547d..d0c4f7d 100644 --- a/lib/ui.js +++ b/lib/ui.js @@ -1,6 +1,7 @@ import { createIcon, resolveIcon } from "./ui/icon"; import { createCheckbox, resolveCheckbox } from "./ui/checkbox"; import { setTooltip, resolveTooltip } from "./ui/tooltip"; +import Dropdown from "./ui/dropdown"; export { // icon @@ -11,5 +12,7 @@ export { resolveCheckbox, // tooltip setTooltip, - resolveTooltip + resolveTooltip, + // dropdown + Dropdown } diff --git a/lib/ui/checkbox.d.ts b/lib/ui/checkbox.d.ts index 52f2e6d..da8a0c5 100644 --- a/lib/ui/checkbox.d.ts +++ b/lib/ui/checkbox.d.ts @@ -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; } diff --git a/lib/ui/checkbox.html b/lib/ui/checkbox.html index a6ab2a4..baaf318 100644 --- a/lib/ui/checkbox.html +++ b/lib/ui/checkbox.html @@ -18,6 +18,7 @@ imageHeight?: Number; checkedNode?: HTMLElement; uncheckedNode?: HTMLElement; + customerAttributes?: { [key: string]: string }; onchange?: (this: HTMLInputElement, ev: Event) => any; }

@@ -25,9 +26,9 @@

复选框图标的样式,可选值目前有 fa-regularfa-lightfa-solid

-

label?: string

+

label?: string | HTMLElement

- 复选框的标签文本 + 复选框的标签文本,或者想要呈现的元素

checked?: boolean

@@ -49,6 +50,14 @@

为图片复选框时的未选中时显示的元素

+

customerAttributes?: { [key: string]: string }

+

+ 自定义属性,例如 +

{
+    'data-id': 'xxxxxx',
+    'disabled': ''
+}
+

onchange?: (this: HTMLInputElement, ev: Event) => any

复选框改变时触发的事件 diff --git a/lib/ui/checkbox.js b/lib/ui/checkbox.js index e75a480..965f7cc 100644 --- a/lib/ui/checkbox.js +++ b/lib/ui/checkbox.js @@ -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) { diff --git a/lib/ui/dropdown.d.ts b/lib/ui/dropdown.d.ts new file mode 100644 index 0000000..cf31b90 --- /dev/null +++ b/lib/ui/dropdown.d.ts @@ -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; \ No newline at end of file diff --git a/lib/ui/dropdown.js b/lib/ui/dropdown.js new file mode 100644 index 0000000..d42afbb --- /dev/null +++ b/lib/ui/dropdown.js @@ -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; \ No newline at end of file diff --git a/lib/ui/tooltip.js b/lib/ui/tooltip.js index 2fc8b75..e0fce9d 100644 --- a/lib/ui/tooltip.js +++ b/lib/ui/tooltip.js @@ -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'); diff --git a/lib/utility.js b/lib/utility.js index ed07ffd..c3e8c1b 100644 --- a/lib/utility.js +++ b/lib/utility.js @@ -3,6 +3,12 @@ import { init, r, lang } from "./utility/lgres"; import { get, post, upload } from "./utility/request"; import { nullOrEmpty, contains, endsWith, padStart } from "./utility/strings"; +let g = typeof globalThis !== 'undefined' ? globalThis : self; + +function isPositive(n) { + return !isNaN(n) && n > 0; +} + export { // cookie getCookie, @@ -20,5 +26,8 @@ export { nullOrEmpty, contains, endsWith, - padStart + padStart, + // variables + g as global, + isPositive } \ No newline at end of file diff --git a/lib/utility/lgres.html b/lib/utility/lgres.html index d1dd8dd..a4d5a00 100644 --- a/lib/utility/lgres.html +++ b/lib/utility/lgres.html @@ -65,7 +65,7 @@ lgres.init(document.body, { template: '/res.json', - callback: (res) => document.title = res.r('title', 'Default Title') + callback: res => document.title = res.r('title', 'Default Title') }).then(res => { document.querySelector('#header').innerText = res.r('header', 'My Header'); const msg = lgres.lang.unknownError; diff --git a/lib/utility/lgres.js b/lib/utility/lgres.js index 8ea6aa1..e38b1a7 100644 --- a/lib/utility/lgres.js +++ b/lib/utility/lgres.js @@ -66,6 +66,7 @@ async function refreshLgres(template, lgres) { Object.defineProperty(lgres, 'r', { writable: false, configurable: false, + enumerable: false, value: function (key, defaultValue) { return getLanguage(this, key, defaultValue); } @@ -76,16 +77,11 @@ async function refreshLgres(template, lgres) { function getLanguage(lgres, key, defaultValue) { let value = lgres[key]; - if (value == null) { - value = defaultValue; - } - return value; + return value ?? defaultValue; } function applyLanguage(dom, result) { - if (dom == null) { - dom = document.body; - } + dom ??= document.body; for (let text of dom.querySelectorAll('[data-lgid]')) { const key = text.getAttribute('data-lgid'); if (text.tagName === 'INPUT') { diff --git a/lib/utility/request.html b/lib/utility/request.html index b3d6ab9..5e90e7c 100644 --- a/lib/utility/request.html +++ b/lib/utility/request.html @@ -103,7 +103,7 @@ request.post('api/query', { id: 101 }) .then(result => console.log(result.data)); request.upload('api/upload', data, { - progress: (ev) => { + progress: ev => { console.log(`loaded: ${ev.loaded}, total: ${ev.total}`); } }) diff --git a/lib/utility/strings.js b/lib/utility/strings.js index d185291..eb10f9a 100644 --- a/lib/utility/strings.js +++ b/lib/utility/strings.js @@ -26,10 +26,7 @@ function padStart(s, num, char) { if (nullOrEmpty(s) || isNaN(num) || num <= s.length) { return s; } - if (char == null) { - char = ' '; - } - return char.repeat(num - s.length); + return (char ?? ' ').repeat(num - s.length); } export { diff --git a/main.js b/main.js index 7ec72c6..36d8176 100644 --- a/main.js +++ b/main.js @@ -25,7 +25,7 @@ function navigate(page) { }); } -document.querySelector('#directory').addEventListener('click', (ev) => { +document.querySelector('#directory').addEventListener('click', ev => { const page = ev.target.getAttribute('data-page'); if (typeof page === 'string') { location.hash = page; @@ -42,7 +42,7 @@ if (page.length > 1) { /* init(null, { template: '/res.json', - callback: (result) => console.log(result) + callback: result => console.log(result) }).then(() => { // document.querySelector('#create-icon').appendChild(createIcon('fa-solid', 'user-edit')) resolveIcon(document.querySelector('#create-icon')) diff --git a/vite.build.js b/vite.build.js index 91f295a..1334c36 100644 --- a/vite.build.js +++ b/vite.build.js @@ -11,7 +11,7 @@ const libraries = [ } ] -libraries.forEach(async (lib) => { +libraries.forEach(async lib => { await build({ build: { lib: {