From 811467bc7a111728b7367f3e23807827237af68b Mon Sep 17 00:00:00 2001 From: Tsanie Lily Date: Mon, 24 Apr 2023 17:38:48 +0800 Subject: [PATCH] grid filter --- lib/ui/css/grid.scss | 68 ++++++++++++++++++++++++++++-- lib/ui/grid/grid.js | 98 ++++++++++++++++++++++++++++++++++++-------- lib/utility.js | 13 ------ 3 files changed, 146 insertions(+), 33 deletions(-) diff --git a/lib/ui/css/grid.scss b/lib/ui/css/grid.scss index 6cf7746..6515efe 100644 --- a/lib/ui/css/grid.scss +++ b/lib/ui/css/grid.scss @@ -8,7 +8,7 @@ overflow: visible; & { - --hover-bg-color: lightyellow; + --cell-hover-bg-color: lightyellow; --header-border-color: #adaba9; --header-bg-color: #fafafa; --header-fore-color: #000; @@ -46,6 +46,8 @@ --header-filter-padding: 4px 26px 4px 8px; --spacing-s: 4px; --spacing-cell: 6px 4px 6px 8px; + --filter-line-height: 30px; + --filter-item-padding: 0 4px; } @include outline(); @@ -369,7 +371,7 @@ position: absolute; line-height: var(--line-height); padding: var(--spacing-cell); - background-color: var(--hover-bg-color); + background-color: var(--cell-hover-bg-color); white-space: pre; display: flex; align-items: center; @@ -449,12 +451,72 @@ cursor: text; } } + + >.filter-item-list { + flex: 1 1 auto; + overflow-y: auto; + overflow-x: hidden; + position: relative; + user-select: none; + @include scrollbar(); + + >.filter-content { + position: absolute; + width: 100%; + } + + .filter-item { + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + box-sizing: border-box; + padding: var(--filter-item-padding); + + &:hover { + background-color: var(--hover-bg-color); + } + + .ui-check-wrapper { + height: var(--filter-line-height); + display: flex; + + .ui-check-inner+* { + font-size: var(--font-smaller-size); + } + } + } + } + + >.filter-function { + display: flex; + justify-content: flex-end; + padding: 4px; + + >button { + box-sizing: border-box; + margin-right: 10px; + min-width: 40px; + height: var(--filter-line-height); + border: none; + background-color: transparent; + cursor: pointer; + border-radius: 0; + transition: background-color .12s ease; + + @include outline(); + + &:hover { + background-color: var(--hover-bg-color); + } + } + } } } @media (prefers-color-scheme: dark) { .ui-grid { - --hover-bg-color: yellow; + --cell-hover-bg-color: yellow; --header-border-color: #525456; --header-bg-color: #050505; --header-fore-color: #fff; diff --git a/lib/ui/grid/grid.js b/lib/ui/grid/grid.js index e77b2df..26e973f 100644 --- a/lib/ui/grid/grid.js +++ b/lib/ui/grid/grid.js @@ -1,5 +1,5 @@ import '../css/grid.scss'; -import { global, isPositive, isMobile, throttle, truncate, distinct } from "../../utility"; +import { global, isPositive, isMobile, throttle, truncate } from "../../utility"; import { r } from "../../utility/lgres"; import { createElement } from "../../functions"; import { createIcon } from "../icon"; @@ -125,12 +125,17 @@ class Grid { } list = list.map(i => { return { values: i } }); this.#source = list; + this.#refreshSource(list); + } + + #refreshSource(list) { + list ??= this.#source; if (this.#colAttrs.__filtered === true) { this.#currentSource = list.filter(it => { for (let col of this.columns) { const f = this.#get(col.key, 'filter'); if (Array.isArray(f)) { - const v = this.#getItemValue(it, col.key, col.filter); + const v = this.#getItemValue(it.values, col.key, col.filter); if (f.indexOf(v) < 0) { return false; } @@ -375,8 +380,8 @@ class Grid { direction = 1; } comparer = (a, b) => { - a = this.#getItemValue(a, col.key, col.filter); - b = this.#getItemValue(b, col.key, col.filter); + a = this.#getItemValue(a.values, col.key, col.filter); + b = this.#getItemValue(b.values, col.key, col.filter); if (a == null && typeof b === 'number') { a = 0; } else if (typeof a === 'number' && b == null) { @@ -936,7 +941,7 @@ class Grid { if (typeof filter === 'function') { value = filter(item); } else { - value = item.values[key]; + value = item[key]; } return value?.value ?? value; } @@ -1005,7 +1010,8 @@ class Grid { document.addEventListener('mousedown', close); const panel = createElement('div', 'filter-panel'); panel.addEventListener('mousedown', e => e.stopPropagation()); - const th = e.currentTarget.parentElement; + const filter = e.currentTarget; + const th = filter.parentElement; const width = th.offsetWidth; panel.style.top = `${th.offsetHeight}px`; panel.style.left = (th.offsetLeft + (width > FilterPanelWidth ? width - FilterPanelWidth : 0)) + 'px'; @@ -1044,7 +1050,18 @@ class Grid { } else if (typeof col.filterSource === 'function') { array = col.filterSource.call(this, col); } else { - array = distinct(this.#currentSource, col.key, col.filter) + const dict = Object.create(null); + for (let item of this.#source) { + const val = this.#getItemValue(item.values, col.key, col.filter); + if (!Object.hasOwnProperty.call(dict, val)) { + const v = item.values[col.key]; + dict[val] = { + value: val, + displayValue: typeof col.filter === 'function' ? col.filter(item.values) : v?.displayValue ?? v + }; + } + } + array = Object.values(dict) .sort((a, b) => { a = a?.value ?? a; b = b?.value ?? b; @@ -1062,7 +1079,7 @@ class Grid { }; }); this.#fillFilterList(col, itemlist, array, itemall); - // TODO: check status + itemall.querySelector('input').checked = ![...itemlist.querySelectorAll('.filter-content input')].some(i => !i.checked); panel.appendChild(itemlist); if (searchbox != null) { searchbox.addEventListener('input', e => { @@ -1074,16 +1091,52 @@ class Grid { this.#fillFilterList(col, itemlist, items, itemall); }); } + // function + const functions = createElement('div', 'filter-function'); + functions.append( + createElement('button', ok => { + ok.innerText = this.langs.ok; + ok.addEventListener('click', () => { + const array = this.#get(col.key, 'filterSource').filter(i => i.__checked !== false); + if (typeof col.onFilterOk === 'function') { + col.onFilterOk.call(this, col, array); + } else { + this.#set(col.key, 'filter', array.map(a => a.value)); + } + this.#colAttrs.__filtered = true; + this.#refreshSource(); + if (typeof col.onFiltered === 'function') { + col.onFiltered.call(this, col); + } + filter.classList.add('active'); + this.#onCloseFilter(); + }); + }), + createElement('button', reset => { + reset.innerText = this.langs.reset; + reset.addEventListener('click', () => { + this.#set(col.key, 'filter', null); + // TODO: change __filtered + this.#refreshSource(); + if (typeof col.onFiltered === 'function') { + col.onFiltered.call(this, col); + } + filter.classList.remove('active'); + this.#onCloseFilter(); + }); + }) + ); + panel.appendChild(functions); this.#el.appendChild(panel); setTimeout(() => panel.classList.add('active'), 0); - this.#colAttrs.__filtering = e.currentTarget; - e.currentTarget.classList.add('hover'); + this.#colAttrs.__filtering = filter; + filter.classList.add('hover'); } #fillFilterList(col, list, array, all) { - list.querySelector('.filter-holder').remove(); - list.querySelector('.filter-content').remove(); + list.querySelector('.filter-holder')?.remove(); + list.querySelector('.filter-content')?.remove(); const rowHeight = this.filterRowHeight; const height = array.length * rowHeight; this.#set(col.key, 'filterHeight', height); @@ -1094,17 +1147,28 @@ class Grid { this.#set(col.key, 'filterSource', array); const filter = this.#get(col.key, 'filter'); for (let item of array) { - item.__checked = !Array.isArray(filter) || filter.indexOf(item.value ?? item); + item.__checked = !Array.isArray(filter) || filter.indexOf(item.value ?? item) >= 0; } if (array.length > 12) { array = array.slice(0, 12); } - this.#doFillFilterList(col, content, array, all); + this.#doFillFilterList(content, array, all); list.append(holder, content); } - #doFillFilterList(col, content, array, all) { - + #doFillFilterList(content, array, all) { + for (let item of array) { + const div = createElement('div', 'filter-item'); + div.appendChild(createCheckbox({ + checked: item.__checked, + label: item?.displayValue ?? item, + onchange: e => { + item.__checked = e.target.checked; + all.querySelector('input').checked = ![...content.querySelectorAll('input')].some(i => !i.checked); + } + })); + content.appendChild(div); + } } #onFilterScroll(col, list, top) { @@ -1132,7 +1196,7 @@ class Grid { } const content = list.querySelector('.filter-content'); content.replaceChildren(); - this.#doFillFilterList(col, content, array, list.querySelector('.filter-all>input')); + this.#doFillFilterList(content, array, list.querySelector('.filter-all>input')); content.style.top = `${top + rowHeight}px`; } } diff --git a/lib/utility.js b/lib/utility.js index 4a3c2b0..259d435 100644 --- a/lib/utility.js +++ b/lib/utility.js @@ -39,18 +39,6 @@ function truncate(v) { return (v > 0 ? Math.floor : Math.ceil)(v); } -function distinct(array, key, filter) { - const dict = Object.create(null); - for (let item of array) { - const v = typeof filter === 'function' ? filter(item) : item[key]; - const val = v?.value ?? v; - if (!Object.prototype.hasOwnProperty.call(dict, val)) { - dict[val] = v; - } - } - return Object.values(dict); -} - function isEmail(text) { return /^\w[-\w.+]*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(text); } @@ -87,7 +75,6 @@ export { throttle, debounce, truncate, - distinct, isEmail, isPhone } \ No newline at end of file