@kubex/zinc 1.1.185 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.browserslistrc +2 -0
- package/.github/workflows/js_build_and_deploy.yaml +54 -1
- package/.github/workflows/test.yaml +27 -0
- package/dist/chunks/zn.UWJLIXDT.js +1 -0
- package/dist/custom-elements.json +9998 -2345
- package/dist/vscode.html-custom-data.json +1022 -67
- package/dist/web-types.json +2129 -177
- package/dist/zn.d.ts +2293 -416
- package/dist/zn.min.css +1 -1
- package/dist/zn.min.js +2310 -1637
- package/docs/_includes/default.njk +4 -1
- package/docs/assets/scripts/docs.js +22 -6
- package/docs/assets/styles/docs.css +9 -2
- package/docs/data/report-table-day-compare.json +380 -0
- package/docs/data/report-table.json +314 -0
- package/docs/pages/components/avatar-group.md +57 -0
- package/docs/pages/components/badge.md +67 -0
- package/docs/pages/components/button.md +41 -1
- package/docs/pages/components/chat-message.md +16 -0
- package/docs/pages/components/checkbox.md +1 -1
- package/docs/pages/components/chip.md +25 -13
- package/docs/pages/components/color-picker.md +37 -0
- package/docs/pages/components/copy-button.md +2 -2
- package/docs/pages/components/data-table-filter.md +33 -50
- package/docs/pages/components/data-table-search.md +4 -4
- package/docs/pages/components/data-table.md +27 -3
- package/docs/pages/components/date-range.md +116 -0
- package/docs/pages/components/dialog.md +29 -37
- package/docs/pages/components/diff-row.md +28 -0
- package/docs/pages/components/diff.md +46 -0
- package/docs/pages/components/editor.md +55 -0
- package/docs/pages/components/icon.md +46 -1
- package/docs/pages/components/input.md +30 -11
- package/docs/pages/components/item.md +35 -0
- package/docs/pages/components/key-value.md +25 -0
- package/docs/pages/components/menu.md +74 -7
- package/docs/pages/components/nav-item.md +15 -0
- package/docs/pages/components/nav-list.md +132 -0
- package/docs/pages/components/navbar.md +100 -1
- package/docs/pages/components/page.md +242 -74
- package/docs/pages/components/priority-list.md +43 -5
- package/docs/pages/components/query-builder.md +23 -0
- package/docs/pages/components/radio-group.md +33 -1
- package/docs/pages/components/radio.md +1 -1
- package/docs/pages/components/report-table.md +202 -0
- package/docs/pages/components/stat.md +30 -0
- package/docs/pages/components/tile.md +11 -0
- package/docs/pages/components/tooltip.md +1 -1
- package/docs/pages/components/translation-grid.md +86 -0
- package/docs/pages/components/vertical-stepper.md +108 -64
- package/package.json +2 -1
- package/scss/_global-spacing.scss +2 -2
- package/scss/_root.scss +11 -171
- package/scss/boot.scss +14 -0
- package/scss/mixins/_helper-mixins.scss +24 -2
- package/scss/mixins/_motion-mixins.scss +126 -0
- package/scss/mixins/_spacing-mixins.scss +4 -3
- package/scss/mixins/_tab-indicator-mixins.scss +12 -0
- package/scss/mixins/_typography-mixins.scss +1 -1
- package/scss/mixins/index.scss +14 -0
- package/scss/shared/_base.scss +32 -1
- package/scss/shared/_button-mixin.scss +7 -5
- package/scss/shared/_table.scss +35 -4
- package/scss/shared/nav.scss +4 -0
- package/scss/shared/shimmer.scss +1 -1
- package/scss/table-controls.scss +206 -0
- package/scss/themes/_dark.scss +1 -3
- package/scss/themes/_index.scss +1 -1
- package/scss/themes/_light.scss +46 -6
- package/scss/themes/_modes.scss +11 -11
- package/scss/themes/_openbyte.scss +944 -0
- package/scss/themes/_typography.scss +4 -2
- package/scss/variables/index.scss +0 -9
- package/src/components/alert/alert.component.ts +14 -4
- package/src/components/alert/alert.scss +55 -69
- package/src/components/alert/alert.test.ts +8 -0
- package/src/components/animated-button/animated-button.component.ts +3 -2
- package/src/components/animated-button/animated-button.scss +6 -23
- package/src/components/animated-button/animated-button.test.ts +4 -3
- package/src/components/avatar-group/avatar-group.component.ts +84 -0
- package/src/components/avatar-group/avatar-group.scss +54 -0
- package/src/components/avatar-group/avatar-group.test.ts +59 -0
- package/src/components/avatar-group/index.ts +12 -0
- package/src/components/badge/badge.component.ts +49 -0
- package/src/components/badge/badge.scss +80 -0
- package/src/components/badge/badge.test.ts +34 -0
- package/src/components/badge/index.ts +12 -0
- package/src/components/bulk-actions/bulk-actions.component.ts +14 -2
- package/src/components/button/button.component.ts +228 -8
- package/src/components/button/button.scss +115 -32
- package/src/components/button/button.test.ts +122 -0
- package/src/components/button-menu/button-menu.component.ts +32 -11
- package/src/components/button-menu/button-menu.scss +5 -1
- package/src/components/channel-tile/channel-tile.component.ts +34 -27
- package/src/components/channel-tile/channel-tile.scss +76 -43
- package/src/components/chart/builders.test.ts +46 -0
- package/src/components/chart/builders.ts +27 -3
- package/src/components/chart/chart-live.test.ts +4 -6
- package/src/components/chart/chart.component.ts +125 -5
- package/src/components/chart/chart.scss +121 -0
- package/src/components/chart/chart.test.ts +81 -0
- package/src/components/chat-message/chat-message.component.ts +21 -1
- package/src/components/chat-message/chat-message.scss +13 -2
- package/src/components/chat-message/chat-message.test.ts +9 -0
- package/src/components/checkbox/checkbox.component.ts +61 -27
- package/src/components/checkbox/checkbox.scss +58 -25
- package/src/components/checkbox-group/checkbox-group.component.ts +7 -1
- package/src/components/chip/chip.component.ts +11 -1
- package/src/components/chip/chip.scss +91 -26
- package/src/components/chip/chip.test.ts +17 -0
- package/src/components/collapsible/collapsible.component.ts +17 -3
- package/src/components/collapsible/collapsible.scss +19 -10
- package/src/components/color-picker/color-picker.component.ts +277 -0
- package/src/components/color-picker/color-picker.scss +141 -0
- package/src/components/color-picker/color-picker.test.ts +57 -0
- package/src/components/color-picker/index.ts +12 -0
- package/src/components/cols/cols.component.ts +10 -0
- package/src/components/cols/cols.test.ts +17 -0
- package/src/components/confirm/confirm.component.ts +20 -6
- package/src/components/confirm/confirm.test.ts +52 -0
- package/src/components/content-block/content-block.component.ts +38 -9
- package/src/components/content-block/content-block.scss +11 -5
- package/src/components/content-block/content-block.test.ts +26 -0
- package/src/components/copy-button/copy-button.component.ts +2 -2
- package/src/components/copy-button/copy-button.scss +17 -4
- package/src/components/data-select/data-select.component.ts +3 -1
- package/src/components/data-table/data-table.component.ts +362 -140
- package/src/components/data-table/data-table.scss +133 -60
- package/src/components/data-table/data-table.test.ts +356 -4
- package/src/components/data-table-filter/data-table-filter.component.ts +426 -406
- package/src/components/data-table-filter/data-table-filter.scss +176 -148
- package/src/components/data-table-filter/data-table-filter.test.ts +271 -273
- package/src/components/data-table-search/data-table-search.component.ts +4 -2
- package/src/components/data-table-search/data-table-search.scss +8 -1
- package/src/components/data-table-search/data-table-search.test.ts +29 -0
- package/src/components/date-range/date-range-math.test.ts +78 -0
- package/src/components/date-range/date-range-math.ts +105 -0
- package/src/components/date-range/date-range.component.ts +375 -0
- package/src/components/date-range/date-range.scss +272 -0
- package/src/components/date-range/date-range.test.ts +247 -0
- package/src/components/date-range/index.ts +12 -0
- package/src/components/datepicker/datepicker.component.ts +44 -7
- package/src/components/datepicker/datepicker.scss +9 -0
- package/src/components/datepicker/datepicker.test.ts +2 -2
- package/src/components/dialog/dialog.component.ts +38 -19
- package/src/components/dialog/dialog.scss +65 -36
- package/src/components/dialog/dialog.test.ts +14 -0
- package/src/components/diff/diff.component.ts +55 -0
- package/src/components/diff/diff.scss +46 -0
- package/src/components/diff/diff.test.ts +15 -0
- package/src/components/diff/index.ts +12 -0
- package/src/components/diff-row/diff-row.component.ts +75 -0
- package/src/components/diff-row/diff-row.scss +70 -0
- package/src/components/diff-row/diff-row.test.ts +37 -0
- package/src/components/diff-row/index.ts +12 -0
- package/src/components/dropdown/dropdown.component.ts +12 -10
- package/src/components/dropdown/dropdown.scss +2 -0
- package/src/components/editor/editor.component.ts +159 -11
- package/src/components/editor/editor.scss +250 -1
- package/src/components/editor/editor.test.ts +31 -3
- package/src/components/editor/modules/ai/panel/ai-panel.scss +0 -1
- package/src/components/editor/modules/attachment/attachment.ts +53 -31
- package/src/components/editor/modules/context-menu/context-menu.ts +17 -0
- package/src/components/editor/modules/dialog/dialog.component.ts +23 -1
- package/src/components/editor/modules/dialog/dialog.test.ts +28 -0
- package/src/components/editor/modules/emoji/emoji.ts +4 -5
- package/src/components/editor/modules/merge-field/merge-field.ts +83 -0
- package/src/components/editor/modules/toolbar/toolbar.component.ts +121 -68
- package/src/components/editor/modules/toolbar/toolbar.scss +38 -0
- package/src/components/editor/modules/toolbar/toolbar.ts +25 -1
- package/src/components/empty-state/empty-state.component.ts +6 -1
- package/src/components/empty-state/empty-state.scss +21 -4
- package/src/components/expanding-action/expanding-action.component.ts +22 -34
- package/src/components/expanding-action/expanding-action.scss +3 -45
- package/src/components/expanding-action/expanding-action.test.ts +2 -1
- package/src/components/file/file.component.ts +30 -2
- package/src/components/file/file.scss +6 -0
- package/src/components/flow-builder/flow-builder.component.ts +36 -3
- package/src/components/flow-builder/flow-builder.scss +0 -6
- package/src/components/flow-builder/modules/flow-canvas/flow-canvas.component.ts +8 -0
- package/src/components/flow-builder/modules/flow-canvas/flow-canvas.scss +10 -5
- package/src/components/flow-builder/modules/flow-node/flow-node.component.ts +17 -0
- package/src/components/flow-builder/modules/flow-node/flow-node.scss +10 -0
- package/src/components/form-actions/form-actions.component.ts +48 -2
- package/src/components/form-actions/form-actions.scss +1 -1
- package/src/components/form-group/form-group.component.ts +10 -3
- package/src/components/form-group/form-group.scss +37 -4
- package/src/components/form-group/form-group.test.ts +10 -0
- package/src/components/header/header.component.ts +11 -4
- package/src/components/header/header.scss +47 -5
- package/src/components/hover-container/hover-container.component.ts +4 -5
- package/src/components/icon/icon.component.ts +95 -9
- package/src/components/icon/icon.scss +70 -6
- package/src/components/icon/icon.test.ts +55 -0
- package/src/components/icon-picker/icon-picker.component.ts +35 -1
- package/src/components/inline-edit/inline-edit.component.ts +38 -8
- package/src/components/inline-edit/inline-edit.scss +128 -19
- package/src/components/input/input.component.ts +173 -297
- package/src/components/input/input.scss +65 -43
- package/src/components/input/input.test.ts +86 -4
- package/src/components/item/item.component.ts +12 -0
- package/src/components/item/item.scss +102 -3
- package/src/components/item/item.test.ts +23 -0
- package/src/components/key/key.component.ts +2 -2
- package/src/components/key/key.scss +11 -0
- package/src/components/key-value/index.ts +12 -0
- package/src/components/key-value/key-value.component.ts +160 -0
- package/src/components/key-value/key-value.scss +20 -0
- package/src/components/key-value/key-value.test.ts +22 -0
- package/src/components/markdown-editor/markdown-editor.component.ts +8 -1
- package/src/components/menu/menu.component.ts +21 -2
- package/src/components/menu/menu.scss +43 -23
- package/src/components/menu/menu.test.ts +33 -0
- package/src/components/menu-item/menu-item.component.ts +13 -2
- package/src/components/menu-item/menu-item.scss +56 -23
- package/src/components/menu-item/menu-item.test.ts +31 -0
- package/src/components/menu-item/submenu-controller.ts +25 -15
- package/src/components/menu-label/index.ts +12 -0
- package/src/components/menu-label/menu-label.component.ts +30 -0
- package/src/components/menu-label/menu-label.scss +18 -0
- package/src/components/nav-item/index.ts +12 -0
- package/src/components/nav-item/nav-item.component.ts +83 -0
- package/src/components/nav-item/nav-item.scss +128 -0
- package/src/components/nav-list/index.ts +12 -0
- package/src/components/nav-list/nav-list.component.ts +313 -0
- package/src/components/nav-list/nav-list.scss +87 -0
- package/src/components/nav-list/nav-list.test.ts +250 -0
- package/src/components/navbar/navbar.component.ts +235 -171
- package/src/components/navbar/navbar.scss +333 -169
- package/src/components/navbar/navbar.test.ts +63 -143
- package/src/components/page/page.component.ts +114 -29
- package/src/components/page/page.scss +223 -95
- package/src/components/page/page.test.ts +214 -3
- package/src/components/page-builder/page-builder.component.ts +51 -5
- package/src/components/page-builder/page-builder.scss +6 -1
- package/src/components/page-nav/page-nav.component.ts +25 -3
- package/src/components/page-nav/page-nav.scss +10 -2
- package/src/components/pagination/pagination.scss +1 -1
- package/src/components/panel/panel.component.ts +14 -13
- package/src/components/panel/panel.scss +14 -10
- package/src/components/panel/panel.test.ts +42 -0
- package/src/components/popup/popup.component.ts +20 -13
- package/src/components/popup/popup.test.ts +21 -1
- package/src/components/priority-list/priority-list.component.ts +157 -172
- package/src/components/priority-list/priority-list.scss +113 -17
- package/src/components/priority-list/priority-list.test.ts +301 -3
- package/src/components/progress-bar/progress-bar.component.ts +1 -1
- package/src/components/progress-bar/progress-bar.scss +16 -4
- package/src/components/progress-tile/progress-tile.scss +1 -1
- package/src/components/query-builder/query-builder.component.ts +85 -10
- package/src/components/query-builder/query-builder.scss +7 -1
- package/src/components/query-builder/query-builder.test.ts +40 -0
- package/src/components/radio/radio.component.ts +57 -18
- package/src/components/radio/radio.scss +101 -26
- package/src/components/radio-group/radio-group.component.ts +45 -7
- package/src/components/radio-group/radio-group.scss +17 -0
- package/src/components/radio-group/radio-group.test.ts +63 -2
- package/src/components/rating/rating.component.ts +23 -0
- package/src/components/rating/rating.scss +1 -1
- package/src/components/remarkd-editor/remarkd-editor.component.ts +60 -5
- package/src/components/remarkd-editor/remarkd-editor.scss +12 -4
- package/src/components/report-table/index.ts +12 -0
- package/src/components/report-table/report-format.test.ts +86 -0
- package/src/components/report-table/report-format.ts +72 -0
- package/src/components/report-table/report-math.test.ts +74 -0
- package/src/components/report-table/report-math.ts +252 -0
- package/src/components/report-table/report-table.component.ts +681 -0
- package/src/components/report-table/report-table.scss +328 -0
- package/src/components/report-table/report-table.test.ts +702 -0
- package/src/components/reveal/reveal.component.ts +15 -1
- package/src/components/reveal/reveal.scss +5 -0
- package/src/components/schedule-builder/schedule-builder.component.ts +23 -8
- package/src/components/scroll-container/scroll-container.component.ts +19 -1
- package/src/components/scroll-container/scroll-container.scss +6 -1
- package/src/components/select/select.component.ts +52 -36
- package/src/components/select/select.scss +42 -25
- package/src/components/select/select.test.ts +14 -2
- package/src/components/settings-container/settings-container.scss +18 -5
- package/src/components/sidebar/sidebar.component.ts +8 -1
- package/src/components/sidebar/sidebar.scss +6 -1
- package/src/components/skeleton/skeleton.component.ts +3 -3
- package/src/components/skeleton/skeleton.scss +6 -6
- package/src/components/skeleton/skeleton.test.ts +9 -0
- package/src/components/slash-menu/slash-menu.scss +1 -1
- package/src/components/slideout/slideout.component.ts +20 -3
- package/src/components/slideout/slideout.scss +13 -6
- package/src/components/slideout/slideout.test.ts +19 -0
- package/src/components/split-button/split-button.component.ts +1 -1
- package/src/components/split-pane/split-pane.component.ts +71 -8
- package/src/components/split-pane/split-pane.scss +145 -41
- package/src/components/split-pane/split-pane.test.ts +23 -3
- package/src/components/stat/stat.component.ts +209 -14
- package/src/components/stat/stat.scss +305 -5
- package/src/components/stat/stat.test.ts +143 -2
- package/src/components/status-indicator/status-indicator.scss +22 -21
- package/src/components/stepper/stepper.scss +3 -3
- package/src/components/tab/tab.component.ts +1 -0
- package/src/components/table/table.component.ts +12 -1
- package/src/components/table/table.scss +21 -13
- package/src/components/table/table.test.ts +17 -0
- package/src/components/tabs/tabs.component.ts +150 -37
- package/src/components/tabs/tabs.scss +48 -7
- package/src/components/tabs/tabs.test.ts +169 -0
- package/src/components/textarea/textarea.component.ts +25 -3
- package/src/components/textarea/textarea.scss +10 -0
- package/src/components/textarea/textarea.test.ts +8 -4
- package/src/components/theme-editor/theme-editor.scss +0 -6
- package/src/components/tile/tile.component.ts +32 -2
- package/src/components/tile/tile.scss +40 -7
- package/src/components/tile/tile.test.ts +10 -0
- package/src/components/toggle/toggle.component.ts +23 -7
- package/src/components/toggle/toggle.scss +36 -29
- package/src/components/toggle/toggle.test.ts +17 -0
- package/src/components/tooltip/tooltip.component.ts +3 -0
- package/src/components/tooltip/tooltip.scss +11 -6
- package/src/components/tooltip/tooltip.test.ts +12 -0
- package/src/components/translation-field/index.ts +12 -0
- package/src/components/translation-field/translation-field.component.ts +55 -0
- package/src/components/translation-field/translation-field.scss +101 -0
- package/src/components/translation-fields/index.ts +12 -0
- package/src/components/translation-fields/translation-fields.component.ts +80 -0
- package/src/components/translation-fields/translation-fields.scss +60 -0
- package/src/components/translation-grid/index.ts +12 -0
- package/src/components/translation-grid/translation-grid.component.ts +157 -0
- package/src/components/translation-grid/translation-grid.scss +138 -0
- package/src/components/translation-grid/translation-grid.test.ts +28 -0
- package/src/components/vertical-stepper/vertical-stepper.component.ts +35 -9
- package/src/components/vertical-stepper/vertical-stepper.scss +110 -39
- package/src/components/vertical-stepper/vertical-stepper.test.ts +35 -0
- package/src/components/well/well.scss +2 -2
- package/src/events/events.ts +2 -0
- package/src/events/zn-chart-click.ts +13 -0
- package/src/events/zn-report-change.ts +7 -0
- package/src/form-control.scss +36 -3
- package/src/internal/anchored-popover.ts +175 -0
- package/src/internal/animate.ts +19 -5
- package/src/internal/cosmic-border.ts +15 -0
- package/src/internal/form.ts +1 -1
- package/src/internal/hover-intent.ts +15 -0
- package/src/internal/input-modality.ts +15 -0
- package/src/internal/pointer-intent.ts +37 -0
- package/src/internal/reorder.ts +625 -0
- package/src/internal/scroll.ts +51 -3
- package/src/internal/selection-card.ts +1 -1
- package/src/internal/tab-indicator.ts +92 -0
- package/src/internal/table-sort.ts +25 -0
- package/src/internal/table-tabs.ts +108 -0
- package/src/internal/test-drag.ts +35 -0
- package/src/internal/theme-openbyte.test.ts +126 -0
- package/src/internal/theme.ts +4 -4
- package/src/internal/zinc-element.ts +2 -2
- package/src/selection-card.scss +25 -0
- package/src/types/web-test-runner-commands.d.ts +1 -0
- package/src/utilities/color.test.ts +43 -0
- package/src/utilities/color.ts +189 -0
- package/src/zinc.ts +14 -0
- package/.github/workflows/eleventy_build.yml +0 -42
- package/scss/themes/_aura.scss +0 -114
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import {AnchoredPopoverController} from '../../internal/anchored-popover';
|
|
1
2
|
import {classMap} from 'lit/directives/class-map.js';
|
|
2
3
|
import {type CSSResultGroup, html, nothing, type PropertyValues, unsafeCSS} from 'lit';
|
|
3
4
|
import {FormControlController, validValidityState} from "../../internal/form";
|
|
4
5
|
import {
|
|
5
6
|
operatorText,
|
|
6
|
-
type QueryBuilderData,
|
|
7
7
|
type QueryBuilderDateSubmitFormat,
|
|
8
8
|
type QueryBuilderItem,
|
|
9
9
|
QueryBuilderOperators,
|
|
@@ -12,92 +12,114 @@ import {
|
|
|
12
12
|
import {property, state} from 'lit/decorators.js';
|
|
13
13
|
import ZincElement, {type ZincFormControl} from '../../internal/zinc-element';
|
|
14
14
|
import ZnButton from "../button";
|
|
15
|
-
import
|
|
16
|
-
import ZnDropdown from "../dropdown";
|
|
15
|
+
import ZnIcon from "../icon";
|
|
17
16
|
import ZnInput from "../input";
|
|
18
|
-
import
|
|
19
|
-
import
|
|
17
|
+
import ZnOptGroup from "../opt-group";
|
|
18
|
+
import ZnOption from "../option";
|
|
19
|
+
import ZnSelect from "../select";
|
|
20
20
|
import type {ZnInputEvent} from "../../events/zn-input";
|
|
21
21
|
|
|
22
22
|
import styles from './data-table-filter.scss';
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
/** A field offered by the filter: a zn-query-builder filter whose operators may be left out. */
|
|
25
|
+
export type FilterField = Omit<QueryBuilderItem, 'operators'> & {operators?: QueryBuilderItem['operators'] | string[]};
|
|
26
|
+
|
|
27
|
+
interface FilterRule {
|
|
25
28
|
key: string;
|
|
26
|
-
comparator:
|
|
29
|
+
comparator: string;
|
|
27
30
|
value: string;
|
|
28
|
-
/**
|
|
29
|
-
label?: string;
|
|
30
|
-
/** Kept so a date can be re-encoded when the comparator changes. */
|
|
31
|
+
/** Kept so a date can be re-encoded when the operator changes. */
|
|
31
32
|
timestamp?: number;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
export const FILTER_OPERATOR_LABELS: Record<string, string> = {
|
|
36
|
+
eq: 'is',
|
|
37
|
+
neq: 'is not',
|
|
38
|
+
in: 'is any of',
|
|
39
|
+
nin: 'is none of',
|
|
40
|
+
contains: 'contains',
|
|
41
|
+
like: 'is like',
|
|
42
|
+
starts: 'starts with',
|
|
43
|
+
gt: 'is more than',
|
|
44
|
+
gte: 'is at least',
|
|
45
|
+
lt: 'is less than',
|
|
46
|
+
lte: 'is at most',
|
|
47
|
+
before: 'is before',
|
|
48
|
+
after: 'is after',
|
|
49
|
+
};
|
|
50
|
+
|
|
34
51
|
const BOOLEAN_OPTIONS: QueryBuilderOptions = {1: 'True', 0: 'False'};
|
|
35
|
-
const TEXT_DEBOUNCE = 350;
|
|
36
52
|
const SUGGESTION_LIMIT = 8;
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
// Pills have to stay short, so the wordier operators get a compact label
|
|
40
|
-
const OPERATOR_LABELS: Partial<Record<QueryBuilderOperators, string>> = {
|
|
41
|
-
[QueryBuilderOperators.Eq]: 'is',
|
|
42
|
-
[QueryBuilderOperators.Neq]: 'is not',
|
|
43
|
-
[QueryBuilderOperators.Gt]: '>',
|
|
44
|
-
[QueryBuilderOperators.Gte]: '\u2265',
|
|
45
|
-
[QueryBuilderOperators.Lt]: '<',
|
|
46
|
-
[QueryBuilderOperators.Lte]: '\u2264',
|
|
47
|
-
[QueryBuilderOperators.In]: 'is any of',
|
|
48
|
-
[QueryBuilderOperators.Nin]: 'is none of',
|
|
49
|
-
[QueryBuilderOperators.Before]: 'before',
|
|
50
|
-
[QueryBuilderOperators.After]: 'after',
|
|
51
|
-
};
|
|
53
|
+
const FIELD_SEARCH_THRESHOLD = 8;
|
|
52
54
|
|
|
53
55
|
/**
|
|
54
|
-
* @summary
|
|
56
|
+
* @summary The filter for data tables: a funnel that opens a field / operator / value builder, and chips for the
|
|
57
|
+
* rules that apply.
|
|
55
58
|
* @documentation https://zinc.style/components/data-table-filter
|
|
56
59
|
* @status experimental
|
|
57
60
|
* @since 1.0
|
|
58
61
|
*
|
|
59
62
|
* @dependency zn-button
|
|
60
|
-
* @dependency zn-
|
|
61
|
-
* @dependency zn-dropdown
|
|
63
|
+
* @dependency zn-icon
|
|
62
64
|
* @dependency zn-input
|
|
63
|
-
* @dependency zn-
|
|
64
|
-
* @dependency zn-
|
|
65
|
+
* @dependency zn-opt-group
|
|
66
|
+
* @dependency zn-option
|
|
67
|
+
* @dependency zn-select
|
|
65
68
|
*
|
|
66
|
-
* @event zn-filter-change - Emitted when the
|
|
69
|
+
* @event zn-filter-change - Emitted when the applied rules change. Read the encoded query off `value`.
|
|
70
|
+
* @event zn-clear - Emitted when `clear()` removes every rule.
|
|
67
71
|
*/
|
|
68
72
|
export default class ZnDataTableFilter extends ZincElement implements ZincFormControl {
|
|
69
73
|
static styles: CSSResultGroup = unsafeCSS(styles);
|
|
70
74
|
static dependencies = {
|
|
71
75
|
'zn-button': ZnButton,
|
|
72
|
-
'zn-
|
|
73
|
-
'zn-dropdown': ZnDropdown,
|
|
76
|
+
'zn-icon': ZnIcon,
|
|
74
77
|
'zn-input': ZnInput,
|
|
75
|
-
'zn-
|
|
76
|
-
'zn-
|
|
78
|
+
'zn-opt-group': ZnOptGroup,
|
|
79
|
+
'zn-option': ZnOption,
|
|
80
|
+
'zn-select': ZnSelect,
|
|
77
81
|
};
|
|
78
82
|
|
|
79
|
-
|
|
80
|
-
private
|
|
81
|
-
|
|
83
|
+
// A chips-only filter mirrors another, so it must not submit a second copy of its value
|
|
84
|
+
private _formController: FormControlController = new FormControlController(this, {
|
|
85
|
+
disabled: () => !!this.source,
|
|
86
|
+
});
|
|
82
87
|
|
|
83
|
-
|
|
88
|
+
private readonly _popover = new AnchoredPopoverController(this, {
|
|
89
|
+
trigger: '.filter__toggle',
|
|
90
|
+
popover: '.filter__pop',
|
|
91
|
+
width: () => Math.min(704, window.innerWidth * 0.94),
|
|
92
|
+
align: 'end',
|
|
93
|
+
focus: ['.filter__rule:last-of-type zn-select[aria-label="Filter field"]'],
|
|
94
|
+
onHide: () => this.discard(),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
private _mirrors = new Set<ZnDataTableFilter>();
|
|
98
|
+
private _emitted?: string;
|
|
99
|
+
|
|
100
|
+
@property({type: Array}) filters: FilterField[] = [];
|
|
84
101
|
|
|
85
102
|
@property() name: string = "data-table-filter";
|
|
86
103
|
|
|
87
104
|
@property() value: string = '';
|
|
88
105
|
|
|
89
|
-
/**
|
|
106
|
+
/** Fields to offer an empty rule for when the builder opens with nothing applied. */
|
|
90
107
|
@property({attribute: 'default-filters'}) defaultFilters: string = '';
|
|
91
108
|
|
|
92
|
-
/** Typeahead values per
|
|
109
|
+
/** Typeahead values per field. `zn-data-table` fills this from the rows it has loaded. */
|
|
93
110
|
@property({attribute: false}) suggestions: Record<string, string[]> = {};
|
|
94
111
|
|
|
95
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Another filter to show the chips of. This element then renders only that filter's chips, and that filter stops
|
|
114
|
+
* rendering its own, so a table can keep the funnel in its header and the chips under it.
|
|
115
|
+
*/
|
|
116
|
+
@property({attribute: false}) source: ZnDataTableFilter | null = null;
|
|
117
|
+
|
|
118
|
+
@state() private _rules: FilterRule[] = [];
|
|
96
119
|
|
|
97
|
-
|
|
98
|
-
@state() private _typing: Record<string, string> = {};
|
|
120
|
+
@state() private _typing: {index: number; text: string} | null = null;
|
|
99
121
|
|
|
100
|
-
@state() private
|
|
122
|
+
@state() private _fieldSearch: number | null = null;
|
|
101
123
|
|
|
102
124
|
get validationMessage(): string {
|
|
103
125
|
return '';
|
|
@@ -123,235 +145,222 @@ export default class ZnDataTableFilter extends ZincElement implements ZincFormCo
|
|
|
123
145
|
this._formController.updateValidity();
|
|
124
146
|
}
|
|
125
147
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
this.
|
|
148
|
+
/** Number of rules that apply. */
|
|
149
|
+
get activeCount(): number {
|
|
150
|
+
return this.committed().length;
|
|
129
151
|
}
|
|
130
152
|
|
|
131
|
-
|
|
132
|
-
|
|
153
|
+
/** Opens the filter builder. */
|
|
154
|
+
show() {
|
|
155
|
+
if (this._rules.length === 0) {
|
|
156
|
+
const defaults = this.defaultFilters.split(',').map(key => key.trim()).filter(key => this.field(key));
|
|
157
|
+
this._rules = defaults.length > 0
|
|
158
|
+
? defaults.map(key => ({key, comparator: this.operators(this.field(key)!)[0], value: ''}))
|
|
159
|
+
: [{key: '', comparator: '', value: ''}];
|
|
160
|
+
}
|
|
161
|
+
this._popover.show();
|
|
162
|
+
}
|
|
133
163
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
this.
|
|
164
|
+
/** Closes the filter builder. */
|
|
165
|
+
hide() {
|
|
166
|
+
this._popover.hide();
|
|
167
|
+
}
|
|
137
168
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
169
|
+
/** Removes every rule. */
|
|
170
|
+
clear() {
|
|
171
|
+
this.setRules([]);
|
|
172
|
+
this.emit('zn-clear');
|
|
173
|
+
}
|
|
142
174
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
175
|
+
protected firstUpdated(changed: PropertyValues) {
|
|
176
|
+
super.firstUpdated(changed);
|
|
177
|
+
this._formController.updateValidity();
|
|
146
178
|
}
|
|
147
179
|
|
|
148
180
|
protected willUpdate(changed: PropertyValues) {
|
|
149
181
|
super.willUpdate(changed);
|
|
150
182
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
this.hydrateFromValue();
|
|
183
|
+
if (changed.has('source')) {
|
|
184
|
+
this.toggleAttribute('data-chips', !!this.source);
|
|
185
|
+
(changed.get('source') as ZnDataTableFilter | null | undefined)?.unmirror(this);
|
|
186
|
+
this.source?.mirror(this);
|
|
156
187
|
}
|
|
157
188
|
|
|
158
|
-
if (changed.has('
|
|
159
|
-
this.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
.filter(key => key.length > 0)
|
|
163
|
-
.forEach(key => this.addFilter(key, false));
|
|
189
|
+
if (changed.has('value') && this.value !== this._emitted) {
|
|
190
|
+
this.readValue();
|
|
191
|
+
} else if (changed.has('filters')) {
|
|
192
|
+
this._rules = this._rules.map(rule => rule.timestamp ? rule : this.ruleFrom(rule));
|
|
164
193
|
}
|
|
165
194
|
}
|
|
166
195
|
|
|
196
|
+
protected updated(changed: PropertyValues) {
|
|
197
|
+
super.updated(changed);
|
|
198
|
+
this._mirrors.forEach(mirror => mirror.requestUpdate());
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
connectedCallback() {
|
|
202
|
+
super.connectedCallback();
|
|
203
|
+
this.source?.mirror(this);
|
|
204
|
+
}
|
|
205
|
+
|
|
167
206
|
disconnectedCallback() {
|
|
168
207
|
super.disconnectedCallback();
|
|
169
|
-
|
|
170
|
-
window.clearTimeout(this._textTimeout);
|
|
171
|
-
}
|
|
208
|
+
this.source?.unmirror(this);
|
|
172
209
|
}
|
|
173
210
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
211
|
+
private mirror(chips: ZnDataTableFilter) {
|
|
212
|
+
this._mirrors.add(chips);
|
|
213
|
+
this.requestUpdate();
|
|
177
214
|
}
|
|
178
215
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
this._active = [];
|
|
182
|
-
this.emitChange();
|
|
183
|
-
this.emit('zn-clear');
|
|
216
|
+
private unmirror(chips: ZnDataTableFilter) {
|
|
217
|
+
if (this._mirrors.delete(chips)) this.requestUpdate();
|
|
184
218
|
}
|
|
185
219
|
|
|
186
|
-
private
|
|
187
|
-
|
|
220
|
+
private readValue() {
|
|
221
|
+
const pending = this._popover.open ? this._rules.filter(rule => !this.isComplete(rule)) : [];
|
|
222
|
+
this._rules = [...this.decoded(), ...pending];
|
|
223
|
+
this._emitted = this.value;
|
|
188
224
|
}
|
|
189
225
|
|
|
190
|
-
|
|
191
|
-
|
|
226
|
+
/** The complete rules in `value`, which the builder's edits only reach once they are applied. */
|
|
227
|
+
private committed(): FilterRule[] {
|
|
228
|
+
return this.decoded().filter(rule => this.isComplete(rule));
|
|
192
229
|
}
|
|
193
230
|
|
|
194
|
-
private
|
|
195
|
-
|
|
196
|
-
|
|
231
|
+
private decoded(): FilterRule[] {
|
|
232
|
+
let decoded: unknown = [];
|
|
233
|
+
try {
|
|
234
|
+
decoded = this.value ? JSON.parse(atob(this.value)) : [];
|
|
235
|
+
} catch {
|
|
236
|
+
decoded = [];
|
|
237
|
+
}
|
|
197
238
|
|
|
198
|
-
|
|
199
|
-
|
|
239
|
+
return Array.isArray(decoded)
|
|
240
|
+
? decoded
|
|
241
|
+
.filter((entry: Partial<FilterRule> | null) => typeof entry?.key === 'string' && entry.key)
|
|
242
|
+
.map((entry: {key: string; comparator?: string; value?: unknown}) => this.ruleFrom(entry))
|
|
243
|
+
: [];
|
|
200
244
|
}
|
|
201
245
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
246
|
+
private ruleFrom(entry: {key: string; comparator?: string; value?: unknown}): FilterRule {
|
|
247
|
+
const value = Array.isArray(entry.value) ? entry.value.join(' ') : String(entry.value ?? '');
|
|
248
|
+
const rule: FilterRule = {key: entry.key, comparator: entry.comparator ?? '', value};
|
|
249
|
+
const field = this.field(entry.key);
|
|
250
|
+
if (field && this.isDate(field) && value) {
|
|
251
|
+
rule.timestamp = this.parseDate(value, rule.comparator, field.dateSubmitFormat);
|
|
252
|
+
}
|
|
253
|
+
return rule;
|
|
205
254
|
}
|
|
206
255
|
|
|
207
|
-
private
|
|
208
|
-
|
|
209
|
-
if (filter.type === 'bool' || filter.type === 'boolean') return BOOLEAN_OPTIONS;
|
|
210
|
-
return undefined;
|
|
256
|
+
private field(key: string): FilterField | undefined {
|
|
257
|
+
return this.filters.find(field => field.id === key);
|
|
211
258
|
}
|
|
212
259
|
|
|
213
|
-
private
|
|
214
|
-
|
|
260
|
+
private operators(field: FilterField): string[] {
|
|
261
|
+
return field.operators?.length ? field.operators : [QueryBuilderOperators.Eq];
|
|
262
|
+
}
|
|
215
263
|
|
|
216
|
-
|
|
217
|
-
|
|
264
|
+
private operatorLabel(op: string): string {
|
|
265
|
+
return FILTER_OPERATOR_LABELS[op] ?? operatorText[op as QueryBuilderOperators]?.toLowerCase() ?? op;
|
|
266
|
+
}
|
|
218
267
|
|
|
219
|
-
|
|
268
|
+
private optionsFor(field: FilterField): QueryBuilderOptions | undefined {
|
|
269
|
+
if (field.options) return field.options;
|
|
270
|
+
if (field.type === 'bool' || field.type === 'boolean') return BOOLEAN_OPTIONS;
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
220
273
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
this._pendingOpen = key;
|
|
224
|
-
this.emitChange();
|
|
225
|
-
}
|
|
274
|
+
private isMultiple(comparator: string): boolean {
|
|
275
|
+
return comparator === 'in' || comparator === 'nin';
|
|
226
276
|
}
|
|
227
277
|
|
|
228
|
-
private
|
|
229
|
-
|
|
230
|
-
this.emitChange();
|
|
278
|
+
private isDate(field: FilterField): boolean {
|
|
279
|
+
return field.type === 'date' || field.type === 'dateTime';
|
|
231
280
|
}
|
|
232
281
|
|
|
233
|
-
private
|
|
234
|
-
|
|
235
|
-
this.emitChange();
|
|
282
|
+
private humanize(value: string) {
|
|
283
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
236
284
|
}
|
|
237
285
|
|
|
238
|
-
private
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
? this.selectedValues(filter, active)[0] ?? ''
|
|
242
|
-
: active.value;
|
|
286
|
+
private isComplete(rule: FilterRule): boolean {
|
|
287
|
+
return !!this.field(rule.key) && !!rule.comparator && rule.value !== '';
|
|
288
|
+
}
|
|
243
289
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
value: item.timestamp
|
|
249
|
-
? this.serializeDate(item.timestamp, comparator, filter.dateSubmitFormat)
|
|
250
|
-
: value,
|
|
251
|
-
}
|
|
252
|
-
: item);
|
|
253
|
-
this.emitChange();
|
|
290
|
+
// Option keys keep the space-separated wire format the query builder uses; typed values can contain spaces of
|
|
291
|
+
// their own, so they only split on commas
|
|
292
|
+
private values(field: FilterField | undefined, value: string): string[] {
|
|
293
|
+
return value.split(field && this.optionsFor(field) ? ' ' : ',').map(item => item.trim()).filter(Boolean);
|
|
254
294
|
}
|
|
255
295
|
|
|
256
|
-
private
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
296
|
+
private ruleText(rule: FilterRule): string {
|
|
297
|
+
const field = this.field(rule.key);
|
|
298
|
+
const name = field ? this.humanize(field.name) : rule.key;
|
|
299
|
+
const options = field && this.optionsFor(field);
|
|
300
|
+
let value = rule.value;
|
|
301
|
+
if (field && this.isDate(field) && rule.timestamp) {
|
|
302
|
+
value = this.dateInputValue(field, rule.timestamp);
|
|
303
|
+
} else if (options) {
|
|
304
|
+
const items = this.isMultiple(rule.comparator) ? this.values(field, rule.value) : [rule.value];
|
|
305
|
+
value = items.map(item => options[item] ?? item).join(', ');
|
|
260
306
|
}
|
|
261
|
-
|
|
262
|
-
const selected = this.selectedValues(filter, active);
|
|
263
|
-
const next = selected.includes(option)
|
|
264
|
-
? selected.filter(item => item !== option)
|
|
265
|
-
: [...selected, option];
|
|
266
|
-
|
|
267
|
-
this.setValue(active.key, next.join(' '));
|
|
307
|
+
return `${name} ${this.operatorLabel(rule.comparator)} ${value}`;
|
|
268
308
|
}
|
|
269
309
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
private selectedValues(filter: QueryBuilderItem, active: ActiveFilter): string[] {
|
|
273
|
-
return active.value
|
|
274
|
-
.split(this.optionsFor(filter) ? ' ' : ',')
|
|
275
|
-
.map(item => item.trim())
|
|
276
|
-
.filter(item => item.length > 0);
|
|
310
|
+
private setLocal(rules: FilterRule[]) {
|
|
311
|
+
this._rules = rules;
|
|
277
312
|
}
|
|
278
313
|
|
|
279
|
-
private
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
.
|
|
314
|
+
private setRules(rules: FilterRule[]) {
|
|
315
|
+
this.setLocal(rules);
|
|
316
|
+
const complete = rules
|
|
317
|
+
.filter(rule => this.isComplete(rule))
|
|
318
|
+
.map(rule => ({key: rule.key, comparator: rule.comparator, value: rule.value}));
|
|
319
|
+
const encoded = complete.length > 0 ? btoa(JSON.stringify(complete)) : '';
|
|
320
|
+
if (encoded === this.value) return;
|
|
283
321
|
|
|
284
|
-
this.
|
|
322
|
+
this._emitted = encoded;
|
|
323
|
+
this.value = encoded;
|
|
285
324
|
this._formController.updateValidity();
|
|
286
325
|
this.emit('zn-filter-change');
|
|
287
326
|
}
|
|
288
327
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
decoded.forEach((entry) => {
|
|
310
|
-
if (typeof entry !== 'object' || entry === null) return;
|
|
311
|
-
const cond = entry as {key?: string; comparator?: string; value?: unknown};
|
|
312
|
-
if (typeof cond.key !== 'string') return;
|
|
313
|
-
|
|
314
|
-
const filter = this.definition(cond.key);
|
|
315
|
-
if (!filter) return;
|
|
316
|
-
|
|
317
|
-
const comparator = (cond.comparator as QueryBuilderOperators) || this.comparatorFor(filter);
|
|
318
|
-
// Multi-value filters serialize as a joined string; tolerate an array form too.
|
|
319
|
-
const value = Array.isArray(cond.value) ? cond.value.join(' ') : String(cond.value ?? '');
|
|
320
|
-
|
|
321
|
-
active.push({key: cond.key, comparator, value});
|
|
328
|
+
private updateRule(index: number, change: Partial<FilterRule>) {
|
|
329
|
+
const rules = this._rules.map((rule, i) => {
|
|
330
|
+
if (i !== index) return rule;
|
|
331
|
+
const next = {...rule, ...change};
|
|
332
|
+
const field = this.field(next.key);
|
|
333
|
+
|
|
334
|
+
// A new field resets the operator and value, which may not apply to it
|
|
335
|
+
if (change.key !== undefined && change.key !== rule.key) {
|
|
336
|
+
next.comparator = field ? this.operators(field)[0] : 'eq';
|
|
337
|
+
next.value = '';
|
|
338
|
+
next.timestamp = undefined;
|
|
339
|
+
this._typing = null;
|
|
340
|
+
} else if (change.comparator !== undefined && field) {
|
|
341
|
+
if (next.timestamp) {
|
|
342
|
+
next.value = this.serializeDate(next.timestamp, next.comparator, field.dateSubmitFormat);
|
|
343
|
+
} else if (this.isMultiple(rule.comparator) && !this.isMultiple(next.comparator)) {
|
|
344
|
+
next.value = this.values(field, rule.value)[0] ?? '';
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return next;
|
|
322
348
|
});
|
|
323
|
-
|
|
324
|
-
if (active.length > 0) this._active = active;
|
|
349
|
+
this.setLocal(rules);
|
|
325
350
|
}
|
|
326
351
|
|
|
327
|
-
private
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (!timestamp) {
|
|
332
|
-
this.setValue(active.key, '');
|
|
352
|
+
private dateChange(index: number, field: FilterField, input: string) {
|
|
353
|
+
if (!input) {
|
|
354
|
+
this.updateRule(index, {value: '', timestamp: undefined});
|
|
333
355
|
return;
|
|
334
356
|
}
|
|
335
|
-
|
|
336
|
-
this.
|
|
337
|
-
|
|
338
|
-
this.serializeDate(timestamp, active.comparator, filter.dateSubmitFormat),
|
|
339
|
-
picker.value as string,
|
|
340
|
-
timestamp
|
|
341
|
-
);
|
|
342
|
-
|
|
343
|
-
// A time picker needs the panel to stay put while the time is set
|
|
344
|
-
if (filter.type !== 'dateTime') {
|
|
345
|
-
void picker.closest('zn-dropdown')?.hide();
|
|
346
|
-
}
|
|
357
|
+
const timestamp = new Date(field.type === 'dateTime' ? input : `${input}T00:00`).getTime();
|
|
358
|
+
const comparator = this._rules[index].comparator;
|
|
359
|
+
this.updateRule(index, {timestamp, value: this.serializeDate(timestamp, comparator, field.dateSubmitFormat)});
|
|
347
360
|
}
|
|
348
361
|
|
|
349
362
|
// Mirrors zn-query-builder's date serialization so backends built against it keep working
|
|
350
|
-
private serializeDate(
|
|
351
|
-
timestamp: number,
|
|
352
|
-
comparator: QueryBuilderOperators,
|
|
353
|
-
format: QueryBuilderDateSubmitFormat = 'legacy'
|
|
354
|
-
): string {
|
|
363
|
+
private serializeDate(timestamp: number, comparator: string, format: QueryBuilderDateSubmitFormat = 'legacy'): string {
|
|
355
364
|
if (format === 'timestamp') return timestamp.toString();
|
|
356
365
|
|
|
357
366
|
if (format === 'iso') {
|
|
@@ -359,251 +368,262 @@ export default class ZnDataTableFilter extends ZincElement implements ZincFormCo
|
|
|
359
368
|
return new Date(date.getTime() - date.getTimezoneOffset() * 60_000).toISOString();
|
|
360
369
|
}
|
|
361
370
|
|
|
362
|
-
if (comparator ===
|
|
371
|
+
if (comparator === 'eq' || comparator === 'neq') {
|
|
363
372
|
return (timestamp / 1000).toString();
|
|
364
373
|
}
|
|
365
374
|
|
|
366
375
|
// Everything else goes as minutes from now, negated for `before`
|
|
367
|
-
const multiplier = comparator ===
|
|
376
|
+
const multiplier = comparator === 'before' ? -1 : 1;
|
|
368
377
|
return (Math.floor((Date.now() - timestamp) / 1000 / 60) * multiplier).toString();
|
|
369
378
|
}
|
|
370
379
|
|
|
371
|
-
private
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
380
|
+
private parseDate(value: string, comparator: string, format: QueryBuilderDateSubmitFormat = 'legacy'): number | undefined {
|
|
381
|
+
let timestamp: number;
|
|
382
|
+
if (format === 'timestamp') {
|
|
383
|
+
timestamp = Number(value);
|
|
384
|
+
} else if (format === 'iso') {
|
|
385
|
+
const date = new Date(value);
|
|
386
|
+
timestamp = date.getTime() + date.getTimezoneOffset() * 60_000;
|
|
387
|
+
} else if (comparator === 'eq' || comparator === 'neq') {
|
|
388
|
+
timestamp = Number(value) * 1000;
|
|
389
|
+
} else {
|
|
390
|
+
const multiplier = comparator === 'before' ? -1 : 1;
|
|
391
|
+
timestamp = Date.now() - Number(value) * multiplier * 60_000;
|
|
392
|
+
}
|
|
393
|
+
return Number.isFinite(timestamp) ? timestamp : undefined;
|
|
379
394
|
}
|
|
380
395
|
|
|
381
|
-
private
|
|
382
|
-
const
|
|
383
|
-
|
|
396
|
+
private dateInputValue(field: FilterField, timestamp: number): string {
|
|
397
|
+
const date = new Date(timestamp);
|
|
398
|
+
const pad = (n: number) => String(n).padStart(2, '0');
|
|
399
|
+
const day = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
400
|
+
return field.type === 'dateTime' ? `${day}T${pad(date.getHours())}:${pad(date.getMinutes())}` : day;
|
|
401
|
+
}
|
|
384
402
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const
|
|
403
|
+
private suggestionsFor(field: FilterField, rule: FilterRule, index: number): string[] {
|
|
404
|
+
if (this._typing?.index !== index) return [];
|
|
405
|
+
const available = this.suggestions[rule.key] ?? [];
|
|
406
|
+
const multiple = this.isMultiple(rule.comparator);
|
|
407
|
+
const typed = (multiple ? this._typing.text.split(',').pop()! : this._typing.text).trim().toLowerCase();
|
|
408
|
+
const chosen = multiple ? this.values(field, this._typing.text).map(item => item.toLowerCase()) : [];
|
|
388
409
|
|
|
389
410
|
return available
|
|
390
|
-
.filter(value => value.toLowerCase().includes(typed) && !
|
|
411
|
+
.filter(value => value.toLowerCase().includes(typed) && !chosen.includes(value.toLowerCase())
|
|
412
|
+
&& value.toLowerCase() !== typed)
|
|
391
413
|
.slice(0, SUGGESTION_LIMIT);
|
|
392
414
|
}
|
|
393
415
|
|
|
394
|
-
private applySuggestion(
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
? [...this.selectedValues(filter, active), value].join(',')
|
|
416
|
+
private applySuggestion(index: number, field: FilterField, rule: FilterRule, value: string) {
|
|
417
|
+
const typed = this._typing?.text ?? rule.value;
|
|
418
|
+
const next = this.isMultiple(rule.comparator)
|
|
419
|
+
? [...this.values(field, typed).slice(0, typed.trimEnd().endsWith(',') ? undefined : -1), value].join(',')
|
|
399
420
|
: value;
|
|
400
|
-
|
|
401
|
-
this.
|
|
402
|
-
this.setValue(active.key, next);
|
|
403
|
-
|
|
404
|
-
if (!this.isMultiple(active.comparator)) {
|
|
405
|
-
void (event.currentTarget as HTMLElement).closest('zn-dropdown')?.hide();
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
private humanize(value: string) {
|
|
410
|
-
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
421
|
+
this._typing = null;
|
|
422
|
+
this.updateRule(index, {value: next});
|
|
411
423
|
}
|
|
412
424
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
425
|
+
// A removed control takes focus with it, so hand focus to the first selector that matches after the re-render
|
|
426
|
+
private async keepFocus(owner: ZnDataTableFilter, ...selectors: string[]) {
|
|
427
|
+
const focused = this.shadowRoot?.activeElement;
|
|
428
|
+
if (!focused) return;
|
|
429
|
+
await owner.updateComplete;
|
|
430
|
+
await this.updateComplete;
|
|
431
|
+
if (focused.isConnected) return;
|
|
418
432
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
if (selected.length > 1) return `${name}${operator} (${selected.length})`;
|
|
433
|
+
const target = selectors
|
|
434
|
+
.map(selector => this.shadowRoot?.querySelector(selector) ?? owner.shadowRoot?.querySelector(selector))
|
|
435
|
+
.find(Boolean);
|
|
436
|
+
(target instanceof ZnButton ? target.button : target as HTMLElement | undefined)?.focus();
|
|
437
|
+
}
|
|
425
438
|
|
|
426
|
-
|
|
427
|
-
|
|
439
|
+
private toggle = () => {
|
|
440
|
+
if (this._popover.open) {
|
|
441
|
+
this.hide();
|
|
442
|
+
} else {
|
|
443
|
+
this.show();
|
|
444
|
+
}
|
|
445
|
+
};
|
|
428
446
|
|
|
429
|
-
|
|
447
|
+
private apply() {
|
|
448
|
+
this.setRules(this._rules);
|
|
449
|
+
this.hide();
|
|
430
450
|
}
|
|
431
451
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
return html`
|
|
437
|
-
<div class="filter-bar__operators">
|
|
438
|
-
${operators.map(operator => html`
|
|
439
|
-
<zn-button class="${classMap({
|
|
440
|
-
'filter-bar__operator': true,
|
|
441
|
-
'filter-bar__operator--selected': operator === active.comparator,
|
|
442
|
-
})}"
|
|
443
|
-
outline
|
|
444
|
-
@click="${() => this.setComparator(filter, active, operator)}">
|
|
445
|
-
${this.operatorLabel(operator)}
|
|
446
|
-
</zn-button>`)}
|
|
447
|
-
</div>`;
|
|
452
|
+
// Closing without applying abandons the builder's edits
|
|
453
|
+
private discard() {
|
|
454
|
+
this._rules = this.committed();
|
|
455
|
+
this._typing = null;
|
|
448
456
|
}
|
|
449
457
|
|
|
450
|
-
private
|
|
451
|
-
const
|
|
452
|
-
if (!filter) return nothing;
|
|
458
|
+
private renderToggle() {
|
|
459
|
+
const applied = this.activeCount;
|
|
453
460
|
|
|
454
|
-
|
|
461
|
+
return html`
|
|
462
|
+
<zn-button icon-button="small"
|
|
463
|
+
icon="funnel@lu"
|
|
464
|
+
icon-size="18"
|
|
465
|
+
tooltip="Filter"
|
|
466
|
+
aria-label="Filter"
|
|
467
|
+
muted-notifications
|
|
468
|
+
notification="${applied || nothing}"
|
|
469
|
+
aria-expanded="${this._popover.open}"
|
|
470
|
+
aria-haspopup="dialog"
|
|
471
|
+
class="${classMap({
|
|
472
|
+
'filter__toggle': true,
|
|
473
|
+
'filter__toggle--active': applied > 0,
|
|
474
|
+
'filter__toggle--open': this._popover.open,
|
|
475
|
+
})}"
|
|
476
|
+
@click="${this.toggle}">
|
|
477
|
+
</zn-button>`;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private renderPopover() {
|
|
481
|
+
if (!this._popover.open) return nothing;
|
|
482
|
+
|
|
483
|
+
const incomplete = this._rules.filter(rule => !this.isComplete(rule)).length;
|
|
484
|
+
const hint = incomplete === 0
|
|
485
|
+
? 'Show records that match all of these'
|
|
486
|
+
: incomplete === 1 ? 'Choose a value for the empty filter to apply it' : 'Choose a value for each empty filter to apply it';
|
|
487
|
+
const used = new Set(this._rules.map(rule => rule.key));
|
|
488
|
+
const canAdd = this.filters.some(field => !used.has(field.id));
|
|
455
489
|
|
|
456
490
|
return html`
|
|
457
|
-
<div class="
|
|
458
|
-
<
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
491
|
+
<div class="filter__pop" role="dialog" aria-label="Filters" style="${this._popover.style}">
|
|
492
|
+
<div class="filter__pop__head">
|
|
493
|
+
<b>Filters</b>
|
|
494
|
+
<span class="filter__pop__meta">${hint}</span>
|
|
495
|
+
</div>
|
|
496
|
+
<div class="filter__pop__body">
|
|
497
|
+
${this._rules.map((rule, index) => this.renderRule(rule, index, used))}
|
|
498
|
+
</div>
|
|
499
|
+
<div class="filter__pop__foot">
|
|
500
|
+
<zn-button class="filter__add" size="small" color="secondary" icon="plus@lu" icon-size="14" ?disabled="${!canAdd}"
|
|
501
|
+
@click="${() => this.setLocal([...this._rules, {key: '', comparator: '', value: ''}])}">
|
|
502
|
+
Add filter
|
|
468
503
|
</zn-button>
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
@zn-menu-select="${() => this.setComparator(filter, active, operator)}">
|
|
478
|
-
${this.operatorLabel(operator)}
|
|
479
|
-
</zn-menu-item>`)}
|
|
480
|
-
<div class="filter-bar__separator"></div>` : nothing}
|
|
481
|
-
${this.renderOptionSearch(filter, active)}
|
|
482
|
-
${this.renderOptionItems(filter, active)}
|
|
483
|
-
</zn-menu>`
|
|
484
|
-
: html`
|
|
485
|
-
<div class="filter-bar__editor">
|
|
486
|
-
${this.renderOperators(filter, active)}
|
|
487
|
-
${this.renderValueInput(filter, active)}
|
|
488
|
-
</div>`}
|
|
489
|
-
</zn-dropdown>
|
|
490
|
-
<zn-button class="filter-bar__remove"
|
|
491
|
-
icon-button="small"
|
|
492
|
-
plain
|
|
493
|
-
icon="x@lu"
|
|
494
|
-
icon-size="16"
|
|
495
|
-
label="Remove ${this.humanize(filter.name)} filter"
|
|
496
|
-
@click="${() => this.removeFilter(active.key)}">
|
|
497
|
-
</zn-button>
|
|
504
|
+
<span class="filter__pop__spacer"></span>
|
|
505
|
+
${this._rules.some(rule => this.isComplete(rule)) ? html`
|
|
506
|
+
<zn-button size="small" color="transparent" @click="${() => {
|
|
507
|
+
this.keepFocus(this, '.filter__add');
|
|
508
|
+
this.setLocal([]);
|
|
509
|
+
}}">Clear all</zn-button>` : nothing}
|
|
510
|
+
<zn-button size="small" color="primary" @click="${() => this.apply()}">Apply</zn-button>
|
|
511
|
+
</div>
|
|
498
512
|
</div>`;
|
|
499
513
|
}
|
|
500
514
|
|
|
501
|
-
private
|
|
502
|
-
|
|
515
|
+
private renderRule(rule: FilterRule, index: number, used: Set<string>) {
|
|
516
|
+
const field = this.field(rule.key);
|
|
517
|
+
const fields = this.filters.filter(f => f.id === rule.key || !used.has(f.id));
|
|
518
|
+
const searchable = this.filters.length > FIELD_SEARCH_THRESHOLD;
|
|
519
|
+
const placeholder = searchable && this._fieldSearch === index
|
|
520
|
+
? `Search ${this.filters.length} fields...`
|
|
521
|
+
: 'Please select...';
|
|
522
|
+
|
|
523
|
+
return html`
|
|
524
|
+
<div class="filter__rule">
|
|
525
|
+
<zn-select placeholder="${placeholder}" value="${rule.key}" aria-label="Filter field" ?search="${searchable}"
|
|
526
|
+
@zn-show="${() => { this._fieldSearch = index; }}"
|
|
527
|
+
@zn-hide="${() => { this._fieldSearch = null; }}"
|
|
528
|
+
@zn-change="${(e: Event) => this.updateRule(index, {key: (e.target as ZnSelect).value as string})}">
|
|
529
|
+
<zn-opt-group label="Filter by">
|
|
530
|
+
${fields.map(f => html`<zn-option value="${f.id}">${this.humanize(f.name)}</zn-option>`)}
|
|
531
|
+
</zn-opt-group>
|
|
532
|
+
</zn-select>
|
|
533
|
+
<zn-select value="${rule.comparator}" ?disabled="${!field}" aria-label="Filter operator"
|
|
534
|
+
@zn-change="${(e: Event) => this.updateRule(index, {comparator: (e.target as ZnSelect).value as string})}">
|
|
535
|
+
${field ? this.operators(field).map(op => html`<zn-option value="${op}">${this.operatorLabel(op)}</zn-option>`) : nothing}
|
|
536
|
+
</zn-select>
|
|
537
|
+
${this.renderValue(rule, index, field)}
|
|
538
|
+
<zn-button class="filter__remove" icon="trash-2@lu" icon-size="16" icon-color="grey" icon-button="small" plain
|
|
539
|
+
aria-label="Remove filter"
|
|
540
|
+
@click="${() => {
|
|
541
|
+
this.keepFocus(this, '.filter__rule:last-of-type .filter__remove', '.filter__add');
|
|
542
|
+
this.setLocal(this._rules.filter((_, i) => i !== index));
|
|
543
|
+
}}"></zn-button>
|
|
544
|
+
${field ? this.renderSuggestions(field, rule, index) : nothing}
|
|
545
|
+
</div>`;
|
|
503
546
|
}
|
|
504
547
|
|
|
505
|
-
private
|
|
506
|
-
|
|
548
|
+
private renderValue(rule: FilterRule, index: number, field?: FilterField) {
|
|
549
|
+
const options = field && this.optionsFor(field);
|
|
550
|
+
if (field && options) {
|
|
507
551
|
return html`
|
|
508
|
-
<zn-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
552
|
+
<zn-select placeholder="Value" value="${rule.value}" aria-label="Filter value"
|
|
553
|
+
?multiple="${this.isMultiple(rule.comparator)}"
|
|
554
|
+
@zn-change="${(e: Event) => {
|
|
555
|
+
const value = (e.target as ZnSelect).value;
|
|
556
|
+
this.updateRule(index, {value: Array.isArray(value) ? value.join(' ') : value});
|
|
557
|
+
}}">
|
|
558
|
+
${Object.entries(options).map(([value, label]) => html`<zn-option value="${value}">${label}</zn-option>`)}
|
|
559
|
+
</zn-select>`;
|
|
513
560
|
}
|
|
514
561
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
return html`
|
|
518
|
-
<zn-input type="${filter.type === 'number' ? 'number' : 'text'}"
|
|
519
|
-
no-spin-buttons
|
|
520
|
-
placeholder="${this.humanize(filter.name)}"
|
|
521
|
-
value="${active.value}"
|
|
522
|
-
@zn-input="${(e: ZnInputEvent) => this.handleTextInput(active.key, e)}">
|
|
523
|
-
</zn-input>
|
|
524
|
-
${suggestions.length > 0 ? html`
|
|
525
|
-
<div class="filter-bar__suggestions">
|
|
526
|
-
${suggestions.map(value => html`
|
|
527
|
-
<zn-button class="filter-bar__suggestion"
|
|
528
|
-
text
|
|
529
|
-
@click="${(e: Event) => this.applySuggestion(filter, active, value, e)}">${value}
|
|
530
|
-
</zn-button>`)}
|
|
531
|
-
</div>` : nothing}`;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
private handleOptionSearch(key: string, event: ZnInputEvent) {
|
|
535
|
-
const input = event.target as ZnInput;
|
|
536
|
-
this._optionQuery = {...this._optionQuery, [key]: input.value as string};
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
private renderOptionSearch(filter: QueryBuilderItem, active: ActiveFilter) {
|
|
540
|
-
if (Object.keys(this.optionsFor(filter)!).length <= OPTION_SEARCH_THRESHOLD) return nothing;
|
|
541
|
-
|
|
542
|
-
return html`
|
|
543
|
-
<zn-input class="filter-bar__option-search"
|
|
544
|
-
size="small"
|
|
545
|
-
placeholder="Search ${this.humanize(filter.name)}"
|
|
546
|
-
value="${this._optionQuery[active.key] ?? ''}"
|
|
547
|
-
@zn-input="${(e: ZnInputEvent) => this.handleOptionSearch(active.key, e)}"
|
|
548
|
-
@keydown="${(e: KeyboardEvent) => e.stopPropagation()}">
|
|
549
|
-
</zn-input>`;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
private renderOptionItems(filter: QueryBuilderItem, active: ActiveFilter) {
|
|
553
|
-
const options = this.optionsFor(filter)!;
|
|
554
|
-
const selected = this.selectedValues(filter, active);
|
|
555
|
-
const query = (this._optionQuery[active.key] ?? '').trim().toLowerCase();
|
|
556
|
-
const matches = Object.keys(options).filter(key => String(options[key]).toLowerCase().includes(query));
|
|
557
|
-
|
|
558
|
-
if (matches.length === 0) {
|
|
562
|
+
if (field && this.isDate(field)) {
|
|
559
563
|
return html`
|
|
560
|
-
<zn-
|
|
564
|
+
<zn-input type="${field.type === 'dateTime' ? 'datetime-local' : 'date'}" aria-label="Filter value"
|
|
565
|
+
value="${rule.timestamp ? this.dateInputValue(field, rule.timestamp) : ''}"
|
|
566
|
+
@zn-change="${(e: Event) => this.dateChange(index, field, (e.target as ZnInput).value as string)}"></zn-input>`;
|
|
561
567
|
}
|
|
562
568
|
|
|
563
|
-
return
|
|
564
|
-
<zn-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
+
return html`
|
|
570
|
+
<zn-input type="${field?.type === 'number' ? 'number' : 'text'}" no-spin-buttons placeholder="Value"
|
|
571
|
+
value="${rule.value}" ?disabled="${!field}" aria-label="Filter value"
|
|
572
|
+
@zn-input="${(e: ZnInputEvent) => { this._typing = {index, text: (e.target as ZnInput).value as string}; }}"
|
|
573
|
+
@zn-change="${(e: Event) => {
|
|
574
|
+
this._typing = null;
|
|
575
|
+
this.updateRule(index, {value: (e.target as ZnInput).value as string});
|
|
576
|
+
}}"></zn-input>`;
|
|
569
577
|
}
|
|
570
578
|
|
|
571
|
-
private
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
579
|
+
private renderSuggestions(field: FilterField, rule: FilterRule, index: number) {
|
|
580
|
+
const suggestions = this.suggestionsFor(field, rule, index);
|
|
581
|
+
if (suggestions.length === 0) return nothing;
|
|
582
|
+
|
|
583
|
+
return html`
|
|
584
|
+
<div class="filter__suggestions" role="group" aria-label="Suggestions">
|
|
585
|
+
${suggestions.map(value => html`
|
|
586
|
+
<button class="filter__suggestion"
|
|
587
|
+
@mousedown="${(e: Event) => e.preventDefault()}"
|
|
588
|
+
@click="${() => this.applySuggestion(index, field, rule, value)}">${value}</button>`)}
|
|
589
|
+
</div>`;
|
|
590
|
+
}
|
|
575
591
|
|
|
576
|
-
|
|
592
|
+
private renderChips(owner: ZnDataTableFilter) {
|
|
593
|
+
const applied = owner.committed();
|
|
594
|
+
if (applied.length === 0) return nothing;
|
|
577
595
|
|
|
578
596
|
return html`
|
|
579
|
-
<
|
|
580
|
-
<
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
597
|
+
<div class="filter__chips">
|
|
598
|
+
<span class="filter__chips__label">Filters:</span>
|
|
599
|
+
${applied.map(rule => html`
|
|
600
|
+
<span class="filter__chip">
|
|
601
|
+
<button class="filter__chip__text" @click="${(e: Event) => {
|
|
602
|
+
e.stopPropagation();
|
|
603
|
+
owner.show();
|
|
604
|
+
}}">${owner.ruleText(rule)}</button>
|
|
605
|
+
<button class="filter__chip__remove" aria-label="Remove filter: ${owner.ruleText(rule)}"
|
|
606
|
+
@click="${() => {
|
|
607
|
+
this.keepFocus(owner, '.filter__chip:last-of-type .filter__chip__remove', '.filter__toggle');
|
|
608
|
+
owner.setRules(applied.filter(r => r !== rule));
|
|
609
|
+
}}">
|
|
610
|
+
<zn-icon src="x@lu" size="14"></zn-icon>
|
|
611
|
+
</button>
|
|
612
|
+
</span>`)}
|
|
613
|
+
<button class="filter__chips__clear" @click="${() => {
|
|
614
|
+
this.keepFocus(owner, '.filter__toggle');
|
|
615
|
+
owner.setRules([]);
|
|
616
|
+
}}">Clear all</button>
|
|
617
|
+
</div>`;
|
|
595
618
|
}
|
|
596
619
|
|
|
597
620
|
render() {
|
|
621
|
+
if (this.source) return this.renderChips(this.source);
|
|
598
622
|
if (this.filters.length === 0) return nothing;
|
|
599
623
|
|
|
600
624
|
return html`
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
${this._active.length > 0 ? html`
|
|
605
|
-
<zn-button class="filter-bar__clear" text @click="${this.clear}">Clear</zn-button>` : nothing}
|
|
606
|
-
</div>
|
|
607
|
-
`;
|
|
625
|
+
${this.renderToggle()}
|
|
626
|
+
${this.renderPopover()}
|
|
627
|
+
${this._mirrors.size === 0 ? this.renderChips(this) : nothing}`;
|
|
608
628
|
}
|
|
609
629
|
}
|