@mono-lit/helper 0.0.0-stage → 0.0.2
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/LICENSE.md +55 -0
- package/README.md +35 -3
- package/dist/breakpoints-CAbhDUht.js +84 -0
- package/dist/checkbox-Cqpie9Ap.js +4 -0
- package/dist/chip-DBUUa2eB.js +4 -0
- package/dist/chip-limits-BMoO31JM.js +213 -0
- package/dist/chip-strip-Cw3gyz0q.js +213 -0
- package/dist/color-DHYrfXsX.js +253 -0
- package/dist/css-class-BRKRzHx-.js +60 -0
- package/dist/css-size-DhHSVZJK.js +37 -0
- package/dist/data/theme/presets.json.d.ts +140 -0
- package/dist/data-source-controller-B_rgHCjw.js +241 -0
- package/dist/data-source-controller-Dbb4fd8I.js +241 -0
- package/dist/data-source-options-CP-X62Pa.js +535 -0
- package/dist/data-source-options-Jqj7LU3Z.js +537 -0
- package/dist/data-source-read-B4DGp6UQ.js +216 -0
- package/dist/data-source-read-DL88aH8t.js +217 -0
- package/dist/decorate-DEXtNagw.js +141 -0
- package/dist/element-props-CLB6yvbm.js +114 -0
- package/dist/esm-Ptok4SQZ.js +2026 -0
- package/dist/excel-CY6zGkcx.js +608 -0
- package/dist/excel-DopRbEMK.js +608 -0
- package/dist/export-BiGVyxt6.js +2302 -0
- package/dist/export.d.ts +1 -0
- package/dist/export.js +2302 -0
- package/dist/field-icons-DELrSdqJ.js +86 -0
- package/dist/field-path-C92eGLg3.js +251 -0
- package/dist/filter-builder-Bke3qOgJ.js +205 -0
- package/dist/filter-builder-CE4zE0V5.js +205 -0
- package/dist/filter-eval-DIfU2EAn.js +173 -0
- package/dist/filter-odata-C1vSGZaY.js +591 -0
- package/dist/form-control-core-B8d7k6vk.js +223 -0
- package/dist/form-control-core-eeRr0zLU.js +223 -0
- package/dist/hydration-flush-RB5Tz5nl.js +1001 -0
- package/dist/icon-CJkTlmTb.js +55 -0
- package/dist/icon-dVhYJUIH.js +17 -0
- package/dist/import-BgJdkwBI.js +606 -0
- package/dist/import.d.ts +1 -0
- package/dist/import.js +606 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +16 -0
- package/dist/index.node-BIqEGehH.js +396 -0
- package/dist/index.node.d.ts +1 -0
- package/dist/index.node.js +16 -0
- package/dist/input-zGOULADt.js +4 -0
- package/dist/layout-var-CZnzMC0s.js +73 -0
- package/dist/light-slots-BVJwyg_2.js +244 -0
- package/dist/light-slots-DW1WgfgT.js +254 -0
- package/dist/markdown-DfqrfOJN.js +20146 -0
- package/dist/mono-button-simcTQdW.js +873 -0
- package/dist/mono-button.shadow-B_rKOY9N.js +784 -0
- package/dist/mono-data-chart-CEv29_NS.js +779 -0
- package/dist/mono-data-dropdown-Wnoke6zO.js +357 -0
- package/dist/mono-data-grid-CjkEqxgE.js +3913 -0
- package/dist/mono-data-grid-YjR3n9Ar.js +3915 -0
- package/dist/mono-element-B0kP_96P.js +232 -0
- package/dist/mono-event-Bi1qP9uN.js +129 -0
- package/dist/mono-form-controller-5PfsU4qa.js +409 -0
- package/dist/mono-modal-controller-BMO3n3rP.js +223 -0
- package/dist/mono-ui-CPV7rrdo.js +358 -0
- package/dist/normalize-error-Gcgmgh5i.js +871 -0
- package/dist/notifier-CE4yxMUQ.js +88 -0
- package/dist/nuxt.d.ts +1 -0
- package/dist/nuxt.js +130 -0
- package/dist/popup-portal-BziRX1yG.js +465 -0
- package/dist/popup-stack-CEEMib__.js +189 -0
- package/dist/rate-limit-BBa2PO79.js +521 -0
- package/dist/reactive-D3EERqgO.js +47 -0
- package/dist/reactive-Di3mTMnV.js +1087 -0
- package/dist/search.d.ts +1 -0
- package/dist/search.js +97 -0
- package/dist/src/components/accordion/accordion-core.d.ts +62 -0
- package/dist/src/components/accordion/accordion-types.d.ts +62 -0
- package/dist/src/components/accordion/index.d.ts +3 -0
- package/dist/src/components/accordion/index.shadow.d.ts +5 -0
- package/dist/src/components/accordion/mono-accordion.d.ts +55 -0
- package/dist/src/components/accordion/mono-accordion.shadow.d.ts +56 -0
- package/dist/src/components/alert/alert-core.d.ts +46 -0
- package/dist/src/components/alert/alert-types.d.ts +44 -0
- package/dist/src/components/alert/index.d.ts +3 -0
- package/dist/src/components/alert/index.shadow.d.ts +5 -0
- package/dist/src/components/alert/mono-alert.d.ts +44 -0
- package/dist/src/components/alert/mono-alert.shadow.d.ts +34 -0
- package/dist/src/components/breadcrumb/breadcrumb-core.d.ts +61 -0
- package/dist/src/components/breadcrumb/breadcrumb-list-core.d.ts +49 -0
- package/dist/src/components/breadcrumb/breadcrumb-render.d.ts +31 -0
- package/dist/src/components/breadcrumb/breadcrumb-types.d.ts +117 -0
- package/dist/src/components/breadcrumb/breadcrumb-utils.d.ts +43 -0
- package/dist/src/components/breadcrumb/index.d.ts +6 -0
- package/dist/src/components/breadcrumb/index.shadow.d.ts +8 -0
- package/dist/src/components/breadcrumb/mono-breadcrumb-list.d.ts +29 -0
- package/dist/src/components/breadcrumb/mono-breadcrumb-list.shadow.d.ts +48 -0
- package/dist/src/components/breadcrumb/mono-breadcrumb.d.ts +38 -0
- package/dist/src/components/breadcrumb/mono-breadcrumb.shadow.d.ts +73 -0
- package/dist/src/components/button/button-core.d.ts +90 -0
- package/dist/src/components/button/button-dropdown-core.d.ts +57 -0
- package/dist/src/components/button/button-dropdown-types.d.ts +129 -0
- package/dist/src/components/button/button-types.d.ts +219 -0
- package/dist/src/components/button/button-utils.d.ts +18 -0
- package/dist/src/components/button/index.button-dropdown.d.ts +4 -0
- package/dist/src/components/button/index.button-dropdown.shadow.d.ts +4 -0
- package/dist/src/components/button/index.d.ts +8 -0
- package/dist/src/components/button/index.shadow.d.ts +5 -0
- package/dist/src/components/button/mono-button-dropdown.d.ts +35 -0
- package/dist/src/components/button/mono-button-dropdown.shadow.d.ts +45 -0
- package/dist/src/components/button/mono-button.d.ts +123 -0
- package/dist/src/components/button/mono-button.shadow.d.ts +56 -0
- package/dist/src/components/card/card-core.d.ts +75 -0
- package/dist/src/components/card/card-types.d.ts +137 -0
- package/dist/src/components/card/card-utils.d.ts +23 -0
- package/dist/src/components/card/index.d.ts +4 -0
- package/dist/src/components/card/index.shadow.d.ts +6 -0
- package/dist/src/components/card/mono-card.d.ts +56 -0
- package/dist/src/components/card/mono-card.shadow.d.ts +52 -0
- package/dist/src/components/chart/chart-core.d.ts +43 -0
- package/dist/src/components/chart/chart-odata.d.ts +52 -0
- package/dist/src/components/chart/chart-types.d.ts +356 -0
- package/dist/src/components/chart/index.d.ts +8 -0
- package/dist/src/components/chart/index.shadow.d.ts +7 -0
- package/dist/src/components/chart/mono-chart-presets.d.ts +43 -0
- package/dist/src/components/chart/mono-chart-presets.shadow.d.ts +43 -0
- package/dist/src/components/chart/mono-chart.d.ts +30 -0
- package/dist/src/components/chart/mono-chart.shadow.d.ts +22 -0
- package/dist/src/components/chart/mono-data-chart.d.ts +27 -0
- package/dist/src/components/checkbox/checkbox-core.d.ts +53 -0
- package/dist/src/components/checkbox/checkbox-types.d.ts +69 -0
- package/dist/src/components/checkbox/checkbox-utils.d.ts +35 -0
- package/dist/src/components/checkbox/index.d.ts +4 -0
- package/dist/src/components/checkbox/index.shadow.d.ts +5 -0
- package/dist/src/components/checkbox/mono-checkbox.d.ts +34 -0
- package/dist/src/components/checkbox/mono-checkbox.shadow.d.ts +45 -0
- package/dist/src/components/chip/chip-core.d.ts +58 -0
- package/dist/src/components/chip/chip-types.d.ts +142 -0
- package/dist/src/components/chip/chip-utils.d.ts +24 -0
- package/dist/src/components/chip/index.d.ts +8 -0
- package/dist/src/components/chip/index.shadow.d.ts +6 -0
- package/dist/src/components/chip/mono-chip.d.ts +48 -0
- package/dist/src/components/chip/mono-chip.shadow.d.ts +33 -0
- package/dist/src/components/chip/mono-status-dot.d.ts +34 -0
- package/dist/src/components/date/date-core.d.ts +80 -0
- package/dist/src/components/date/date-types.d.ts +163 -0
- package/dist/src/components/date/index.d.ts +3 -0
- package/dist/src/components/date/index.shadow.d.ts +4 -0
- package/dist/src/components/date/mono-date.d.ts +22 -0
- package/dist/src/components/date/mono-date.shadow.d.ts +31 -0
- package/dist/src/components/drawer/drawer-core.d.ts +99 -0
- package/dist/src/components/drawer/drawer-types.d.ts +153 -0
- package/dist/src/components/drawer/index.d.ts +3 -0
- package/dist/src/components/drawer/index.shadow.d.ts +4 -0
- package/dist/src/components/drawer/mono-drawer.d.ts +45 -0
- package/dist/src/components/drawer/mono-drawer.shadow.d.ts +46 -0
- package/dist/src/components/dropdown/dropdown-core.d.ts +33 -0
- package/dist/src/components/dropdown/dropdown-types.d.ts +88 -0
- package/dist/src/components/dropdown/index.d.ts +3 -0
- package/dist/src/components/dropdown/index.shadow.d.ts +4 -0
- package/dist/src/components/dropdown/mono-dropdown.d.ts +34 -0
- package/dist/src/components/dropdown/mono-dropdown.shadow.d.ts +28 -0
- package/dist/src/components/dropdown-table/dropdown-table-core.d.ts +65 -0
- package/dist/src/components/dropdown-table/dropdown-table-types.d.ts +278 -0
- package/dist/src/components/dropdown-table/index.d.ts +5 -0
- package/dist/src/components/dropdown-table/index.shadow.d.ts +6 -0
- package/dist/src/components/dropdown-table/mono-data-dropdown.d.ts +152 -0
- package/dist/src/components/dropdown-table/mono-dropdown-table.d.ts +46 -0
- package/dist/src/components/dropdown-table/mono-dropdown-table.shadow.d.ts +22 -0
- package/dist/src/components/file-upload/file-upload-core.d.ts +60 -0
- package/dist/src/components/file-upload/file-upload-types.d.ts +145 -0
- package/dist/src/components/file-upload/file-upload-utils.d.ts +16 -0
- package/dist/src/components/file-upload/index.d.ts +4 -0
- package/dist/src/components/file-upload/index.shadow.d.ts +5 -0
- package/dist/src/components/file-upload/mono-file-upload.d.ts +33 -0
- package/dist/src/components/file-upload/mono-file-upload.shadow.d.ts +50 -0
- package/dist/src/components/filter/filter-builder-core.d.ts +36 -0
- package/dist/src/components/filter/filter-builder.d.ts +23 -0
- package/dist/src/components/filter/filter-odata.d.ts +27 -0
- package/dist/src/components/filter/filter-operators.d.ts +13 -0
- package/dist/src/components/filter/filter-types.d.ts +253 -0
- package/dist/src/components/filter/index.d.ts +6 -0
- package/dist/src/components/filter/index.shadow.d.ts +7 -0
- package/dist/src/components/filter/mono-filter-builder.d.ts +21 -0
- package/dist/src/components/filter/mono-filter-builder.shadow.d.ts +17 -0
- package/dist/src/components/form/form-control-core.d.ts +37 -0
- package/dist/src/components/form/form-rules.d.ts +25 -0
- package/dist/src/components/form/form-types.d.ts +307 -0
- package/dist/src/components/form/index.d.ts +6 -0
- package/dist/src/components/form/mono-form-controller.d.ts +16 -0
- package/dist/src/components/input/index.d.ts +6 -0
- package/dist/src/components/input/index.shadow.d.ts +8 -0
- package/dist/src/components/input/input-core.d.ts +91 -0
- package/dist/src/components/input/input-format.d.ts +118 -0
- package/dist/src/components/input/input-types.d.ts +179 -0
- package/dist/src/components/input/input-utils.d.ts +10 -0
- package/dist/src/components/input/mono-input.d.ts +46 -0
- package/dist/src/components/input/mono-input.shadow.d.ts +34 -0
- package/dist/src/components/menu/index.d.ts +6 -0
- package/dist/src/components/menu/index.shadow.d.ts +7 -0
- package/dist/src/components/menu/menu-core.d.ts +65 -0
- package/dist/src/components/menu/menu-render.d.ts +55 -0
- package/dist/src/components/menu/menu-types.d.ts +195 -0
- package/dist/src/components/menu/menu-utils.d.ts +53 -0
- package/dist/src/components/menu/mono-menu-list.d.ts +118 -0
- package/dist/src/components/menu/mono-menu.d.ts +39 -0
- package/dist/src/components/menu/mono-menu.shadow.d.ts +40 -0
- package/dist/src/components/modal/index.d.ts +4 -0
- package/dist/src/components/modal/index.shadow.d.ts +5 -0
- package/dist/src/components/modal/modal-core.d.ts +110 -0
- package/dist/src/components/modal/modal-types.d.ts +304 -0
- package/dist/src/components/modal/mono-modal-controller.d.ts +4 -0
- package/dist/src/components/modal/mono-modal.d.ts +45 -0
- package/dist/src/components/modal/mono-modal.shadow.d.ts +50 -0
- package/dist/src/components/nav/index.d.ts +4 -0
- package/dist/src/components/nav/index.shadow.d.ts +6 -0
- package/dist/src/components/nav/mono-nav.d.ts +41 -0
- package/dist/src/components/nav/mono-nav.shadow.d.ts +21 -0
- package/dist/src/components/nav/nav-core.d.ts +34 -0
- package/dist/src/components/nav/nav-types.d.ts +48 -0
- package/dist/src/components/nav/nav-utils.d.ts +12 -0
- package/dist/src/components/radio/index.d.ts +4 -0
- package/dist/src/components/radio/index.shadow.d.ts +5 -0
- package/dist/src/components/radio/mono-radio.d.ts +31 -0
- package/dist/src/components/radio/mono-radio.shadow.d.ts +35 -0
- package/dist/src/components/radio/radio-core.d.ts +45 -0
- package/dist/src/components/radio/radio-types.d.ts +58 -0
- package/dist/src/components/radio/radio-utils.d.ts +12 -0
- package/dist/src/components/rich-text-editor/index.d.ts +5 -0
- package/dist/src/components/rich-text-editor/index.shadow.d.ts +6 -0
- package/dist/src/components/rich-text-editor/mono-rich-text-editor.d.ts +31 -0
- package/dist/src/components/rich-text-editor/mono-rich-text-editor.shadow.d.ts +34 -0
- package/dist/src/components/rich-text-editor/rich-text-editor-core.d.ts +100 -0
- package/dist/src/components/rich-text-editor/rich-text-editor-loader.d.ts +17 -0
- package/dist/src/components/rich-text-editor/rich-text-editor-toolbar.d.ts +65 -0
- package/dist/src/components/rich-text-editor/rich-text-editor-types.d.ts +231 -0
- package/dist/src/components/select/index.d.ts +4 -0
- package/dist/src/components/select/index.shadow.d.ts +5 -0
- package/dist/src/components/select/mono-select.d.ts +69 -0
- package/dist/src/components/select/mono-select.shadow.d.ts +48 -0
- package/dist/src/components/select/select-core.d.ts +98 -0
- package/dist/src/components/select/select-types.d.ts +284 -0
- package/dist/src/components/select/select-utils.d.ts +7 -0
- package/dist/src/components/sidebar/index.d.ts +4 -0
- package/dist/src/components/sidebar/index.shadow.d.ts +6 -0
- package/dist/src/components/sidebar/mono-sidebar.d.ts +34 -0
- package/dist/src/components/sidebar/mono-sidebar.shadow.d.ts +27 -0
- package/dist/src/components/sidebar/sidebar-core.d.ts +51 -0
- package/dist/src/components/sidebar/sidebar-types.d.ts +119 -0
- package/dist/src/components/sidebar/sidebar-utils.d.ts +55 -0
- package/dist/src/components/switch/index.d.ts +4 -0
- package/dist/src/components/switch/index.shadow.d.ts +5 -0
- package/dist/src/components/switch/mono-switch.d.ts +31 -0
- package/dist/src/components/switch/mono-switch.shadow.d.ts +36 -0
- package/dist/src/components/switch/switch-core.d.ts +49 -0
- package/dist/src/components/switch/switch-types.d.ts +64 -0
- package/dist/src/components/switch/switch-utils.d.ts +12 -0
- package/dist/src/components/table/array-source.d.ts +44 -0
- package/dist/src/components/table/date-filter-tree.d.ts +93 -0
- package/dist/src/components/table/grouping.d.ts +55 -0
- package/dist/src/components/table/index.d.ts +33 -0
- package/dist/src/components/table/index.shadow.d.ts +47 -0
- package/dist/src/components/table/mono-data-grid.d.ts +1756 -0
- package/dist/src/components/table/mono-table-checkbox-core.d.ts +76 -0
- package/dist/src/components/table/mono-table-checkbox.d.ts +34 -0
- package/dist/src/components/table/mono-table-checkbox.shadow.d.ts +26 -0
- package/dist/src/components/table/mono-table-detail-core.d.ts +87 -0
- package/dist/src/components/table/mono-table-detail.d.ts +32 -0
- package/dist/src/components/table/mono-table-detail.shadow.d.ts +31 -0
- package/dist/src/components/table/mono-table-empty-core.d.ts +60 -0
- package/dist/src/components/table/mono-table-empty.d.ts +35 -0
- package/dist/src/components/table/mono-table-empty.shadow.d.ts +40 -0
- package/dist/src/components/table/mono-table-error-core.d.ts +84 -0
- package/dist/src/components/table/mono-table-error.d.ts +28 -0
- package/dist/src/components/table/mono-table-error.shadow.d.ts +21 -0
- package/dist/src/components/table/mono-table-info-core.d.ts +13 -0
- package/dist/src/components/table/mono-table-info.d.ts +19 -0
- package/dist/src/components/table/mono-table-info.shadow.d.ts +14 -0
- package/dist/src/components/table/mono-table-loading-core.d.ts +41 -0
- package/dist/src/components/table/mono-table-loading.d.ts +50 -0
- package/dist/src/components/table/mono-table-loading.shadow.d.ts +14 -0
- package/dist/src/components/table/mono-table-page-size-core.d.ts +14 -0
- package/dist/src/components/table/mono-table-page-size.d.ts +16 -0
- package/dist/src/components/table/mono-table-page-size.shadow.d.ts +11 -0
- package/dist/src/components/table/mono-table-paging-core.d.ts +22 -0
- package/dist/src/components/table/mono-table-paging-group-core.d.ts +18 -0
- package/dist/src/components/table/mono-table-paging-group.d.ts +20 -0
- package/dist/src/components/table/mono-table-paging-group.shadow.d.ts +13 -0
- package/dist/src/components/table/mono-table-paging.d.ts +16 -0
- package/dist/src/components/table/mono-table-paging.shadow.d.ts +11 -0
- package/dist/src/components/table/mono-table-search-core.d.ts +74 -0
- package/dist/src/components/table/mono-table-search.d.ts +34 -0
- package/dist/src/components/table/mono-table-search.shadow.d.ts +21 -0
- package/dist/src/components/table/mono-table-sort-core.d.ts +35 -0
- package/dist/src/components/table/mono-table-sort.d.ts +38 -0
- package/dist/src/components/table/mono-table-sort.shadow.d.ts +20 -0
- package/dist/src/components/table/mono-table-summary-core.d.ts +26 -0
- package/dist/src/components/table/mono-table-summary.d.ts +24 -0
- package/dist/src/components/table/mono-table-summary.shadow.d.ts +14 -0
- package/dist/src/components/table/mono-table-th-core.d.ts +56 -0
- package/dist/src/components/table/mono-table-th.d.ts +41 -0
- package/dist/src/components/table/mono-table-th.shadow.d.ts +19 -0
- package/dist/src/components/table/store-group-source.d.ts +24 -0
- package/dist/src/components/table/table-controller-core.d.ts +40 -0
- package/dist/src/components/table/table-filter-icons.d.ts +3 -0
- package/dist/src/components/table/table-menu-core.d.ts +65 -0
- package/dist/src/components/table/table-overlay.d.ts +165 -0
- package/dist/src/components/table/table-sort-icons.d.ts +4 -0
- package/dist/src/components/table/table-sort-utils.d.ts +20 -0
- package/dist/src/components/table/table-types.d.ts +475 -0
- package/dist/src/components/tabs/index.d.ts +3 -0
- package/dist/src/components/tabs/index.shadow.d.ts +4 -0
- package/dist/src/components/tabs/mono-tabs.d.ts +33 -0
- package/dist/src/components/tabs/mono-tabs.shadow.d.ts +37 -0
- package/dist/src/components/tabs/tabs-core.d.ts +57 -0
- package/dist/src/components/tabs/tabs-types.d.ts +65 -0
- package/dist/src/components/tag-input/index.d.ts +4 -0
- package/dist/src/components/tag-input/index.shadow.d.ts +5 -0
- package/dist/src/components/tag-input/mono-tag-input.d.ts +66 -0
- package/dist/src/components/tag-input/mono-tag-input.shadow.d.ts +40 -0
- package/dist/src/components/tag-input/tag-input-core.d.ts +108 -0
- package/dist/src/components/tag-input/tag-input-types.d.ts +420 -0
- package/dist/src/components/tag-input/tag-input-utils.d.ts +8 -0
- package/dist/src/components/textarea/index.d.ts +4 -0
- package/dist/src/components/textarea/index.shadow.d.ts +5 -0
- package/dist/src/components/textarea/mono-textarea.d.ts +29 -0
- package/dist/src/components/textarea/mono-textarea.shadow.d.ts +31 -0
- package/dist/src/components/textarea/textarea-core.d.ts +60 -0
- package/dist/src/components/textarea/textarea-types.d.ts +131 -0
- package/dist/src/components/textarea/textarea-utils.d.ts +7 -0
- package/dist/src/components/tooltip/control-tooltip.d.ts +36 -0
- package/dist/src/components/tooltip/index.d.ts +6 -0
- package/dist/src/components/tooltip/tooltip-config.d.ts +27 -0
- package/dist/src/components/tooltip/tooltip-declarative.d.ts +41 -0
- package/dist/src/components/tooltip/tooltip-loader.d.ts +1 -0
- package/dist/src/components/tooltip/tooltip-types.d.ts +110 -0
- package/dist/src/composables/breakpoints.d.ts +59 -0
- package/dist/src/composables/chip-limits.d.ts +48 -0
- package/dist/src/composables/chip-strip.d.ts +93 -0
- package/dist/src/composables/color.d.ts +103 -0
- package/dist/src/composables/css-class.d.ts +27 -0
- package/dist/src/composables/css-size.d.ts +40 -0
- package/dist/src/composables/data-source-controller.d.ts +113 -0
- package/dist/src/composables/element-props.d.ts +46 -0
- package/dist/src/composables/field-icons.d.ts +15 -0
- package/dist/src/composables/hybird-prop.d.ts +79 -0
- package/dist/src/composables/hydration-flush.d.ts +34 -0
- package/dist/src/composables/icon.d.ts +17 -0
- package/dist/src/composables/layout-var.d.ts +34 -0
- package/dist/src/composables/light-slots.d.ts +133 -0
- package/dist/src/composables/mdi-glyphs.d.ts +4 -0
- package/dist/src/composables/mono-element.d.ts +9 -0
- package/dist/src/composables/mono-event.d.ts +55 -0
- package/dist/src/composables/mono-pending.d.ts +8 -0
- package/dist/src/composables/mono-skeleton.d.ts +193 -0
- package/dist/src/composables/mono-ui.d.ts +58 -0
- package/dist/src/composables/notifier.d.ts +46 -0
- package/dist/src/composables/popup-portal.d.ts +226 -0
- package/dist/src/composables/popup-stack.d.ts +70 -0
- package/dist/src/composables/rate-limit.d.ts +108 -0
- package/dist/src/composables/reactive.d.ts +53 -0
- package/dist/src/composables/shadow-css.d.ts +42 -0
- package/dist/src/composables/visibility.d.ts +36 -0
- package/dist/src/data/theme/index.d.ts +30 -0
- package/dist/src/data/theme/presets.d.ts +94 -0
- package/dist/src/entries/index.d.ts +2 -0
- package/dist/src/entries/index.node.d.ts +42 -0
- package/dist/src/entries/search.d.ts +3 -0
- package/dist/src/export/detail.d.ts +29 -0
- package/dist/src/export/download.d.ts +9 -0
- package/dist/src/export/excel/index.d.ts +27 -0
- package/dist/src/export/excel/nodes.d.ts +123 -0
- package/dist/src/export/expression.d.ts +36 -0
- package/dist/src/export/helpers.d.ts +6 -0
- package/dist/src/export/index.d.ts +13 -0
- package/dist/src/export/markdown.d.ts +52 -0
- package/dist/src/export/resolve-data.d.ts +31 -0
- package/dist/src/export/sentinel.d.ts +97 -0
- package/dist/src/export/sheet-name.d.ts +45 -0
- package/dist/src/export/styles.d.ts +33 -0
- package/dist/src/export/template.d.ts +10 -0
- package/dist/src/export/types.d.ts +259 -0
- package/dist/src/import/coerce.d.ts +38 -0
- package/dist/src/import/index.d.ts +16 -0
- package/dist/src/import/match.d.ts +54 -0
- package/dist/src/import/parse.d.ts +15 -0
- package/dist/src/import/types.d.ts +153 -0
- package/dist/src/nuxt/index.d.ts +94 -0
- package/dist/src/search/data-search.d.ts +207 -0
- package/dist/src/search/field-path.d.ts +107 -0
- package/dist/src/search/filter-eval.d.ts +30 -0
- package/dist/src/search/index.d.ts +22 -0
- package/dist/src/search/odata-search.d.ts +18 -0
- package/dist/src/search/search-expr.d.ts +160 -0
- package/dist/src/utils/data-source-apply.d.ts +97 -0
- package/dist/src/utils/data-source-options.d.ts +116 -0
- package/dist/src/utils/data-source-read.d.ts +85 -0
- package/dist/src/utils/data-source-summary.d.ts +55 -0
- package/dist/src/utils/normalize-error.d.ts +90 -0
- package/dist/src/vite/index.d.ts +48 -0
- package/dist/src/vite/mono-client-only.d.ts +35 -0
- package/dist/src/vite/mono-ssr-stub.d.ts +20 -0
- package/dist/src/vite/shadow-manifest.d.ts +8 -0
- package/dist/styles-BIJ31TM4.js +20146 -0
- package/dist/tooltip-DQqNpqk2.js +795 -0
- package/dist/tooltip.d.ts +1 -0
- package/dist/tooltip.js +2 -0
- package/dist/ui/accordion.d.ts +1 -0
- package/dist/ui/accordion.js +466 -0
- package/dist/ui/alert.d.ts +1 -0
- package/dist/ui/alert.js +221 -0
- package/dist/ui/breadcrumb.d.ts +1 -0
- package/dist/ui/breadcrumb.js +902 -0
- package/dist/ui/button-dropdown.d.ts +1 -0
- package/dist/ui/button-dropdown.js +679 -0
- package/dist/ui/button.d.ts +1 -0
- package/dist/ui/button.js +63 -0
- package/dist/ui/card.d.ts +1 -0
- package/dist/ui/card.js +524 -0
- package/dist/ui/chart.d.ts +1 -0
- package/dist/ui/chart.js +885 -0
- package/dist/ui/checkbox.d.ts +1 -0
- package/dist/ui/checkbox.js +413 -0
- package/dist/ui/chip.d.ts +1 -0
- package/dist/ui/chip.js +558 -0
- package/dist/ui/date.d.ts +1 -0
- package/dist/ui/date.js +686 -0
- package/dist/ui/drawer.d.ts +1 -0
- package/dist/ui/drawer.js +901 -0
- package/dist/ui/dropdown-table.d.ts +1 -0
- package/dist/ui/dropdown-table.js +1291 -0
- package/dist/ui/dropdown.d.ts +1 -0
- package/dist/ui/dropdown.js +586 -0
- package/dist/ui/file-upload.d.ts +1 -0
- package/dist/ui/file-upload.js +688 -0
- package/dist/ui/filter.d.ts +1 -0
- package/dist/ui/filter.js +400 -0
- package/dist/ui/font/files/geist-47c091c9.woff2 +0 -0
- package/dist/ui/font/files/geist-8dd77544.woff2 +0 -0
- package/dist/ui/font/files/geist-b9248b1e.woff2 +0 -0
- package/dist/ui/font/files/geist-d40d74b4.woff2 +0 -0
- package/dist/ui/font/files/geist-f493429a.woff2 +0 -0
- package/dist/ui/font/files/geistmono-03737868.woff2 +0 -0
- package/dist/ui/font/files/geistmono-421a7e38.woff2 +0 -0
- package/dist/ui/font/files/geistmono-48ef518d.woff2 +0 -0
- package/dist/ui/font/files/geistmono-5df35d39.woff2 +0 -0
- package/dist/ui/font/files/geistmono-7de289a4.woff2 +0 -0
- package/dist/ui/font/files/geistmono-e976128f.woff2 +0 -0
- package/dist/ui/font/files/inter-05cf802c.woff2 +0 -0
- package/dist/ui/font/files/inter-2b2f25a4.woff2 +0 -0
- package/dist/ui/font/files/inter-467acc7a.woff2 +0 -0
- package/dist/ui/font/files/inter-6e85163c.woff2 +0 -0
- package/dist/ui/font/files/inter-7048add3.woff2 +0 -0
- package/dist/ui/font/files/inter-9be0371e.woff2 +0 -0
- package/dist/ui/font/files/inter-a28b7e31.woff2 +0 -0
- package/dist/ui/font/files/poppins-054df55a.woff2 +0 -0
- package/dist/ui/font/files/poppins-0ee5dc9a.woff2 +0 -0
- package/dist/ui/font/files/poppins-0f44cb18.woff2 +0 -0
- package/dist/ui/font/files/poppins-0ff0d30c.woff2 +0 -0
- package/dist/ui/font/files/poppins-13045147.woff2 +0 -0
- package/dist/ui/font/files/poppins-1db57cd9.woff2 +0 -0
- package/dist/ui/font/files/poppins-2cf2a902.woff2 +0 -0
- package/dist/ui/font/files/poppins-39e7f079.woff2 +0 -0
- package/dist/ui/font/files/poppins-9607042d.woff2 +0 -0
- package/dist/ui/font/files/poppins-9a88fc1f.woff2 +0 -0
- package/dist/ui/font/files/poppins-b8f4f752.woff2 +0 -0
- package/dist/ui/font/files/poppins-f5e57db4.woff2 +0 -0
- package/dist/ui/font/geist-mono.css +104 -0
- package/dist/ui/font/geist.css +168 -0
- package/dist/ui/font/inter.css +232 -0
- package/dist/ui/font/poppins.css +104 -0
- package/dist/ui/form.d.ts +1 -0
- package/dist/ui/form.js +3 -0
- package/dist/ui/index.css +21989 -0
- package/dist/ui/input.d.ts +1 -0
- package/dist/ui/input.js +1176 -0
- package/dist/ui/menu.d.ts +1 -0
- package/dist/ui/menu.js +1289 -0
- package/dist/ui/modal.d.ts +1 -0
- package/dist/ui/modal.js +996 -0
- package/dist/ui/nav.d.ts +1 -0
- package/dist/ui/nav.js +295 -0
- package/dist/ui/radio.d.ts +1 -0
- package/dist/ui/radio.js +379 -0
- package/dist/ui/rich-text-editor.d.ts +1 -0
- package/dist/ui/rich-text-editor.js +1338 -0
- package/dist/ui/select.d.ts +1 -0
- package/dist/ui/select.js +2200 -0
- package/dist/ui/shadow/accordion.js +442 -0
- package/dist/ui/shadow/alert.js +218 -0
- package/dist/ui/shadow/breadcrumb.js +919 -0
- package/dist/ui/shadow/button-dropdown.js +696 -0
- package/dist/ui/shadow/button.js +63 -0
- package/dist/ui/shadow/card.js +538 -0
- package/dist/ui/shadow/chart.js +1685 -0
- package/dist/ui/shadow/checkbox.js +457 -0
- package/dist/ui/shadow/chip.js +559 -0
- package/dist/ui/shadow/date.js +712 -0
- package/dist/ui/shadow/drawer.js +869 -0
- package/dist/ui/shadow/dropdown-table.js +1620 -0
- package/dist/ui/shadow/dropdown.js +547 -0
- package/dist/ui/shadow/file-upload.js +731 -0
- package/dist/ui/shadow/filter.js +402 -0
- package/dist/ui/shadow/input.js +1150 -0
- package/dist/ui/shadow/menu.js +899 -0
- package/dist/ui/shadow/modal.js +1194 -0
- package/dist/ui/shadow/nav.js +240 -0
- package/dist/ui/shadow/radio.js +408 -0
- package/dist/ui/shadow/rich-text-editor.js +1344 -0
- package/dist/ui/shadow/select.js +2167 -0
- package/dist/ui/shadow/sidebar.js +606 -0
- package/dist/ui/shadow/switch.js +428 -0
- package/dist/ui/shadow/table.js +4755 -0
- package/dist/ui/shadow/tabs.js +372 -0
- package/dist/ui/shadow/tag-input.js +2974 -0
- package/dist/ui/shadow/textarea.js +816 -0
- package/dist/ui/sidebar.d.ts +1 -0
- package/dist/ui/sidebar.js +659 -0
- package/dist/ui/switch.d.ts +1 -0
- package/dist/ui/switch.js +398 -0
- package/dist/ui/table.d.ts +1 -0
- package/dist/ui/table.js +4654 -0
- package/dist/ui/tabs.d.ts +1 -0
- package/dist/ui/tabs.js +379 -0
- package/dist/ui/tag-input.d.ts +1 -0
- package/dist/ui/tag-input.js +2947 -0
- package/dist/ui/textarea.d.ts +1 -0
- package/dist/ui/textarea.js +819 -0
- package/dist/ui/theme/legacy.css +293 -0
- package/dist/ui/theme/luma.css +743 -0
- package/dist/ui/theme/lyra.css +754 -0
- package/dist/ui/theme/maia.css +673 -0
- package/dist/ui/theme/mira.css +802 -0
- package/dist/ui/theme/nova.css +662 -0
- package/dist/ui/theme/one.css +699 -0
- package/dist/ui/theme/rhea.css +742 -0
- package/dist/ui/theme/sera.css +845 -0
- package/dist/ui/theme/tokens.css +261 -0
- package/dist/ui/theme/vega.css +467 -0
- package/dist/vite-C5iUK7so.js +329 -0
- package/dist/vite.d.ts +1 -0
- package/dist/vite.js +2 -0
- package/dist/vue.d.ts +282 -0
- package/package.json +411 -3
|
@@ -0,0 +1,3913 @@
|
|
|
1
|
+
import { t as createNotifier } from "./notifier-CE4yxMUQ.js";
|
|
2
|
+
import { a as parseFieldPath, c as toODataClause, l as toODataSelector, n as isPath, o as projectFields, r as mergePatch, s as setFieldValue, t as getFieldValue } from "./field-path-C92eGLg3.js";
|
|
3
|
+
import { A as compileFilterPredicate, D as searchEntryFor, E as resolveSearchEntries, O as andFilters, S as hasWildcardSearch, T as plainSearchColumns, a as describeError, h as mergeSearchFields, j as joinFilters, k as andPredicates, l as isCanceled, n as unwrapReactive, p as DEFAULT_SEARCH_FIELDS, v as customPredicate, x as hasCustomSearch, y as customRemoteClause } from "./reactive-Di3mTMnV.js";
|
|
4
|
+
import { a as buildGroups, c as isGroupNode, i as searchFilterOf, o as collectGroupPaths, r as readAllRows, s as flattenLeaves } from "./data-source-read-B4DGp6UQ.js";
|
|
5
|
+
import { a as resolveDataSourceOptions, c as composeSummaryApply, d as createApplyGate, f as isRolledUp, h as monoArraySource, i as normalizeSortList, l as summaryAliases, m as storeRows, n as extraLoadOptions, o as toList, p as loadApply, s as buildSummaryApply, t as dataSourceOptionsKey, u as summaryFilterOf } from "./data-source-options-CP-X62Pa.js";
|
|
6
|
+
import { t as arrayToODataString } from "./filter-odata-C1vSGZaY.js";
|
|
7
|
+
import { t as monoFilterBuilder } from "./filter-builder-CE4zE0V5.js";
|
|
8
|
+
//#region src/components/table/date-filter-tree.ts
|
|
9
|
+
var DATE_LEVELS = [
|
|
10
|
+
"year",
|
|
11
|
+
"month",
|
|
12
|
+
"day",
|
|
13
|
+
"hour",
|
|
14
|
+
"minute",
|
|
15
|
+
"second"
|
|
16
|
+
];
|
|
17
|
+
/** A `{ from, to }` pair of Dates (or ISO strings, which `setColumnFilter` also accepts). */
|
|
18
|
+
function isDateRange(v) {
|
|
19
|
+
if (!v || typeof v !== "object") return false;
|
|
20
|
+
const o = v;
|
|
21
|
+
const ok = (x) => x instanceof Date || typeof x === "string";
|
|
22
|
+
return ok(o.from) && ok(o.to);
|
|
23
|
+
}
|
|
24
|
+
var BLANK_KEY = "blank";
|
|
25
|
+
/**
|
|
26
|
+
* A column value as a Date, or `null` for a blank / unparsable one. Accepts a
|
|
27
|
+
* `Date`, a number (epoch ms), an ISO string, and a bare `YYYY-MM-DD` — the
|
|
28
|
+
* last is parsed as a LOCAL date on purpose (`new Date('2026-03-05')` would be
|
|
29
|
+
* UTC midnight and shift a day west of Greenwich).
|
|
30
|
+
*/
|
|
31
|
+
function parseDateValue(v) {
|
|
32
|
+
if (v == null || v === "") return null;
|
|
33
|
+
if (v instanceof Date) return Number.isNaN(v.getTime()) ? null : v;
|
|
34
|
+
if (typeof v === "number") {
|
|
35
|
+
const d = new Date(v);
|
|
36
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
37
|
+
}
|
|
38
|
+
if (typeof v !== "string") return null;
|
|
39
|
+
const s = v.trim();
|
|
40
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
|
|
41
|
+
if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
42
|
+
const d = new Date(s);
|
|
43
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
44
|
+
}
|
|
45
|
+
/** `[year, month(1-12), day, hour, minute, second]` of a Date, local or UTC. */
|
|
46
|
+
function datePartsOf(d, utc = false) {
|
|
47
|
+
return utc ? [
|
|
48
|
+
d.getUTCFullYear(),
|
|
49
|
+
d.getUTCMonth() + 1,
|
|
50
|
+
d.getUTCDate(),
|
|
51
|
+
d.getUTCHours(),
|
|
52
|
+
d.getUTCMinutes(),
|
|
53
|
+
d.getUTCSeconds()
|
|
54
|
+
] : [
|
|
55
|
+
d.getFullYear(),
|
|
56
|
+
d.getMonth() + 1,
|
|
57
|
+
d.getDate(),
|
|
58
|
+
d.getHours(),
|
|
59
|
+
d.getMinutes(),
|
|
60
|
+
d.getSeconds()
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
/** The Date at the START of the period the parts describe (missing parts = their minimum). */
|
|
64
|
+
function dateFromParts(parts, utc) {
|
|
65
|
+
const [y, mo = 1, d = 1, h = 0, mi = 0, s = 0] = parts;
|
|
66
|
+
return utc ? new Date(Date.UTC(y, mo - 1, d, h, mi, s)) : new Date(y, mo - 1, d, h, mi, s);
|
|
67
|
+
}
|
|
68
|
+
/** The period `[from, to)` of a node at `level` with these parts. */
|
|
69
|
+
function periodOf(parts, level, utc = false) {
|
|
70
|
+
const i = DATE_LEVELS.indexOf(level);
|
|
71
|
+
const own = parts.slice(0, i + 1);
|
|
72
|
+
const next = [...own];
|
|
73
|
+
next[i] += 1;
|
|
74
|
+
return {
|
|
75
|
+
from: dateFromParts(own, utc),
|
|
76
|
+
to: dateFromParts(next, utc)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function labelFor(parts, level, utc, locale) {
|
|
80
|
+
const d = dateFromParts(parts, utc);
|
|
81
|
+
const tz = utc ? { timeZone: "UTC" } : {};
|
|
82
|
+
const two = (n) => String(n).padStart(2, "0");
|
|
83
|
+
switch (level) {
|
|
84
|
+
case "year": return String(parts[0]);
|
|
85
|
+
case "month": return new Intl.DateTimeFormat(locale, {
|
|
86
|
+
month: "long",
|
|
87
|
+
...tz
|
|
88
|
+
}).format(d);
|
|
89
|
+
case "day": return `${two(parts[2])} (${new Intl.DateTimeFormat(locale, {
|
|
90
|
+
weekday: "short",
|
|
91
|
+
...tz
|
|
92
|
+
}).format(d)})`;
|
|
93
|
+
case "hour": return `${two(parts[3])}:00`;
|
|
94
|
+
case "minute": return `${two(parts[3])}:${two(parts[4])}`;
|
|
95
|
+
case "second": return `${two(parts[3])}:${two(parts[4])}:${two(parts[5])}`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Fold distinct timestamps into the tree. Only periods that occur are built,
|
|
100
|
+
* counts are summed up the branches, siblings are in chronological order, and
|
|
101
|
+
* a blank value becomes one `(Blanks)` node at the top (never expandable).
|
|
102
|
+
*/
|
|
103
|
+
function buildDateTree(values, options = {}) {
|
|
104
|
+
const depth = DATE_LEVELS.indexOf(options.depth ?? "month");
|
|
105
|
+
const utc = options.utc ?? false;
|
|
106
|
+
const locale = options.locale;
|
|
107
|
+
const roots = /* @__PURE__ */ new Map();
|
|
108
|
+
let blanks = 0;
|
|
109
|
+
for (const v of values) {
|
|
110
|
+
const d = parseDateValue(v.value);
|
|
111
|
+
const count = Number(v.count) || 0;
|
|
112
|
+
if (!d) {
|
|
113
|
+
blanks += count;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const parts = datePartsOf(d, utc);
|
|
117
|
+
let siblings = roots;
|
|
118
|
+
for (let i = 0; i <= depth; i++) {
|
|
119
|
+
const level = DATE_LEVELS[i];
|
|
120
|
+
const own = parts.slice(0, i + 1);
|
|
121
|
+
let n = siblings.get(parts[i]);
|
|
122
|
+
if (!n) {
|
|
123
|
+
const { from, to } = periodOf(own, level, utc);
|
|
124
|
+
n = {
|
|
125
|
+
key: own.join("-"),
|
|
126
|
+
level,
|
|
127
|
+
parts: own,
|
|
128
|
+
label: labelFor(own, level, utc, locale),
|
|
129
|
+
count: 0,
|
|
130
|
+
from,
|
|
131
|
+
to,
|
|
132
|
+
children: []
|
|
133
|
+
};
|
|
134
|
+
siblings.set(parts[i], n);
|
|
135
|
+
}
|
|
136
|
+
n.count += count;
|
|
137
|
+
siblings = childMap(n);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const finish = (map) => [...map.entries()].sort(([a], [b]) => a - b).map(([, n]) => {
|
|
141
|
+
n.children = finish(childMap(n));
|
|
142
|
+
childMaps.delete(n);
|
|
143
|
+
return n;
|
|
144
|
+
});
|
|
145
|
+
const out = finish(roots);
|
|
146
|
+
if (blanks > 0) out.unshift({
|
|
147
|
+
key: BLANK_KEY,
|
|
148
|
+
level: "year",
|
|
149
|
+
parts: [],
|
|
150
|
+
label: "(Blanks)",
|
|
151
|
+
count: blanks,
|
|
152
|
+
from: null,
|
|
153
|
+
to: null,
|
|
154
|
+
children: []
|
|
155
|
+
});
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
var childMaps = /* @__PURE__ */ new WeakMap();
|
|
159
|
+
function childMap(n) {
|
|
160
|
+
let m = childMaps.get(n);
|
|
161
|
+
if (!m) {
|
|
162
|
+
m = /* @__PURE__ */ new Map();
|
|
163
|
+
childMaps.set(n, m);
|
|
164
|
+
}
|
|
165
|
+
return m;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* A node's tick state from the set of ticked keys. A key in the set means the
|
|
169
|
+
* WHOLE node is ticked (so every descendant is on too); otherwise the children
|
|
170
|
+
* decide — all on → on, none → off, some (or a partly-ticked child) → mixed.
|
|
171
|
+
*
|
|
172
|
+
* Pass `roots` to have a ticked ANCESTOR count (the node is then on) — the
|
|
173
|
+
* renderer walks top-down and passes `inheritedOn` instead, which is the same
|
|
174
|
+
* answer without a path lookup per row.
|
|
175
|
+
*/
|
|
176
|
+
function checkState(node, checked, roots, inheritedOn = false) {
|
|
177
|
+
if (inheritedOn || checked.has(node.key)) return "on";
|
|
178
|
+
if (roots && pathTo(node.key, roots).slice(0, -1).some((a) => checked.has(a.key))) return "on";
|
|
179
|
+
if (!node.children.length) return "off";
|
|
180
|
+
let on = 0;
|
|
181
|
+
let mixed = false;
|
|
182
|
+
for (const c of node.children) {
|
|
183
|
+
const s = checkState(c, checked);
|
|
184
|
+
if (s === "on") on++;
|
|
185
|
+
else if (s === "mixed") mixed = true;
|
|
186
|
+
}
|
|
187
|
+
if (on === node.children.length) return "on";
|
|
188
|
+
return on > 0 || mixed ? "mixed" : "off";
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Tick or untick a node: the node's own key goes in (and every descendant key
|
|
192
|
+
* comes out — the node covers them) or the node and every descendant come out.
|
|
193
|
+
* Ticking a child of a fully-ticked parent first expands the parent's tick to its
|
|
194
|
+
* siblings, so unticking one day of a ticked month leaves the other days ticked.
|
|
195
|
+
*/
|
|
196
|
+
function setChecked(node, on, checked, roots) {
|
|
197
|
+
const next = new Set(checked);
|
|
198
|
+
const path = pathTo(node.key, roots);
|
|
199
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
200
|
+
const anc = path[i];
|
|
201
|
+
if (next.has(anc.key)) {
|
|
202
|
+
next.delete(anc.key);
|
|
203
|
+
for (const c of anc.children) next.add(c.key);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const strip = (n) => {
|
|
207
|
+
next.delete(n.key);
|
|
208
|
+
n.children.forEach(strip);
|
|
209
|
+
};
|
|
210
|
+
strip(node);
|
|
211
|
+
if (on) next.add(node.key);
|
|
212
|
+
for (let i = path.length - 2; i >= 0; i--) {
|
|
213
|
+
const anc = path[i];
|
|
214
|
+
if (anc.children.length && anc.children.every((c) => next.has(c.key))) {
|
|
215
|
+
anc.children.forEach((c) => next.delete(c.key));
|
|
216
|
+
next.add(anc.key);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return next;
|
|
220
|
+
}
|
|
221
|
+
function pathTo(key, roots) {
|
|
222
|
+
const walk = (nodes, trail) => {
|
|
223
|
+
for (const n of nodes) {
|
|
224
|
+
if (n.key === key) return [...trail, n];
|
|
225
|
+
const found = walk(n.children, [...trail, n]);
|
|
226
|
+
if (found) return found;
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
};
|
|
230
|
+
return walk(roots, []) ?? [];
|
|
231
|
+
}
|
|
232
|
+
/** Tick everything (every root) or nothing. */
|
|
233
|
+
function setAllChecked(roots, on) {
|
|
234
|
+
return on ? new Set(roots.map((r) => r.key)) : /* @__PURE__ */ new Set();
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* The ticked periods as filter values — one `{ from, to }` per fully-ticked node,
|
|
238
|
+
* walking down only where a node is partly ticked. `(Blanks)` ticked adds a
|
|
239
|
+
* `null` value (the column filter's "is blank"), matching what the plain header
|
|
240
|
+
* filter sends for a blank.
|
|
241
|
+
*/
|
|
242
|
+
function minimize(roots, checked) {
|
|
243
|
+
const out = [];
|
|
244
|
+
const walk = (n) => {
|
|
245
|
+
if (checked.has(n.key)) {
|
|
246
|
+
out.push(n.from && n.to ? {
|
|
247
|
+
from: n.from,
|
|
248
|
+
to: n.to
|
|
249
|
+
} : null);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
n.children.forEach(walk);
|
|
253
|
+
};
|
|
254
|
+
roots.forEach(walk);
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The tick set that reproduces an applied filter (`table.columnFilter(field)`):
|
|
259
|
+
* a node is ticked when one of the ranges covers its whole period; a blank value
|
|
260
|
+
* ticks `(Blanks)`. A range that only partly covers a node ticks the children it
|
|
261
|
+
* covers instead.
|
|
262
|
+
*/
|
|
263
|
+
function seedFromRanges(roots, values) {
|
|
264
|
+
const ranges = [];
|
|
265
|
+
let blank = false;
|
|
266
|
+
for (const v of values) if (v == null || v === "") blank = true;
|
|
267
|
+
else if (isDateRange(v)) {
|
|
268
|
+
const from = v.from instanceof Date ? v.from : new Date(v.from);
|
|
269
|
+
const to = v.to instanceof Date ? v.to : new Date(v.to);
|
|
270
|
+
if (!Number.isNaN(from.getTime()) && !Number.isNaN(to.getTime())) ranges.push({
|
|
271
|
+
from,
|
|
272
|
+
to
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
const out = /* @__PURE__ */ new Set();
|
|
276
|
+
const covers = (n) => !!n.from && !!n.to && ranges.some((r) => r.from.getTime() <= n.from.getTime() && r.to.getTime() >= n.to.getTime());
|
|
277
|
+
const walk = (n) => {
|
|
278
|
+
if (n.key === "blank") {
|
|
279
|
+
if (blank) out.add(n.key);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (covers(n)) {
|
|
283
|
+
out.add(n.key);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
n.children.forEach(walk);
|
|
287
|
+
};
|
|
288
|
+
roots.forEach(walk);
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
/** Whether a Date falls in `[from, to)`. */
|
|
292
|
+
function inRange(d, r) {
|
|
293
|
+
const t = d.getTime();
|
|
294
|
+
return t >= r.from.getTime() && t < r.to.getTime();
|
|
295
|
+
}
|
|
296
|
+
//#endregion
|
|
297
|
+
//#region src/components/table/store-group-source.ts
|
|
298
|
+
/**
|
|
299
|
+
* Build a {@link MonoServerGroupSource} that drives **server-side** group paging
|
|
300
|
+
* straight from a devextreme `DataSource`'s store — no extra fetcher, no custom
|
|
301
|
+
* callbacks. `loadGroups` issues one cheap grouped query (group keys + counts +
|
|
302
|
+
* optional summaries, no rows); `loadRows` fetches a single group's page with
|
|
303
|
+
* `$filter` + `$skip`/`$top`. Returns `null` if the source has no usable store
|
|
304
|
+
* (e.g. a plain in-memory array), so the caller can fall back to client grouping.
|
|
305
|
+
*/
|
|
306
|
+
function storeGroupSource(source, config) {
|
|
307
|
+
const store = source.store?.();
|
|
308
|
+
if (!store || typeof store.load !== "function") return null;
|
|
309
|
+
const { groupField, select, searchExpr, searchOperation, groupSummary } = config;
|
|
310
|
+
const summaryDesc = groupSummary ? Object.entries(groupSummary).map(([selector, summaryType]) => ({
|
|
311
|
+
selector,
|
|
312
|
+
summaryType
|
|
313
|
+
})) : void 0;
|
|
314
|
+
/**
|
|
315
|
+
* Build a devextreme filter expression for the active search (or undefined).
|
|
316
|
+
* Delegates to the shared builder, which takes the search state off a source;
|
|
317
|
+
* here the search arrives per-call via `ctx`, so a minimal stand-in carries it.
|
|
318
|
+
*/
|
|
319
|
+
function searchFilter(search) {
|
|
320
|
+
if (!search || !searchExpr) return void 0;
|
|
321
|
+
return searchFilterOf({
|
|
322
|
+
load: () => [],
|
|
323
|
+
items: () => [],
|
|
324
|
+
searchValue: () => search,
|
|
325
|
+
searchExpr: () => searchExpr,
|
|
326
|
+
searchOperation: () => searchOperation ?? "contains"
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
/** devextreme `store.load` returns either an array or `{ data, … }`. */
|
|
330
|
+
const rows = (res) => Array.isArray(res) ? res : res?.data ?? [];
|
|
331
|
+
return {
|
|
332
|
+
async loadGroups(ctx) {
|
|
333
|
+
const filter = joinFilters([ctx.filter, searchFilter(ctx.search)], "and");
|
|
334
|
+
return rows(await store.load({
|
|
335
|
+
group: [{
|
|
336
|
+
selector: groupField,
|
|
337
|
+
isExpanded: false
|
|
338
|
+
}],
|
|
339
|
+
...summaryDesc ? { groupSummary: summaryDesc } : {},
|
|
340
|
+
requireGroupCount: true,
|
|
341
|
+
...filter ? { filter } : {},
|
|
342
|
+
sort: ctx.sort ?? [{ selector: groupField }],
|
|
343
|
+
...ctx.loadOptions ?? {}
|
|
344
|
+
})).map((g) => ({
|
|
345
|
+
key: g.key,
|
|
346
|
+
count: Number(g.count ?? (Array.isArray(g.items) ? g.items.length : 0)) || 0,
|
|
347
|
+
aggregates: summaryDesc && Array.isArray(g.summary) ? Object.fromEntries(summaryDesc.map((s, i) => [s.selector, Number(g.summary[i]) || 0])) : void 0
|
|
348
|
+
}));
|
|
349
|
+
},
|
|
350
|
+
async loadRows(key, ctx) {
|
|
351
|
+
const groupFilter = [
|
|
352
|
+
groupField,
|
|
353
|
+
"=",
|
|
354
|
+
key
|
|
355
|
+
];
|
|
356
|
+
const rowSelect = select ?? ctx.select;
|
|
357
|
+
return rows(await store.load({
|
|
358
|
+
filter: joinFilters([
|
|
359
|
+
groupFilter,
|
|
360
|
+
ctx.filter,
|
|
361
|
+
searchFilter(ctx.search)
|
|
362
|
+
], "and"),
|
|
363
|
+
...rowSelect ? { select: rowSelect } : {},
|
|
364
|
+
...ctx.sort ? { sort: ctx.sort } : {},
|
|
365
|
+
skip: ctx.skip,
|
|
366
|
+
take: ctx.take,
|
|
367
|
+
requireTotalCount: false,
|
|
368
|
+
...ctx.loadOptions ?? {}
|
|
369
|
+
}));
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
//#endregion
|
|
374
|
+
//#region src/components/table/mono-data-grid.ts
|
|
375
|
+
function monoDataGrid(ds = null, opts = {}) {
|
|
376
|
+
let bound = null;
|
|
377
|
+
let searchFieldsOverride;
|
|
378
|
+
let searchFieldsCache = null;
|
|
379
|
+
/**
|
|
380
|
+
* The configured search fields, or `undefined` when none of the four spellings
|
|
381
|
+
* was given. `undefined` is load-bearing: it means "not configured", which lets
|
|
382
|
+
* a data source keep using its own `searchExpr` instead of being handed `[]`.
|
|
383
|
+
*
|
|
384
|
+
* An override set through {@link setSearchExpr} (i.e. by `<mono-table-search>`)
|
|
385
|
+
* wins over the options — the element is the more specific declaration.
|
|
386
|
+
*/
|
|
387
|
+
function searchFields() {
|
|
388
|
+
if (searchFieldsOverride !== void 0) return searchFieldsOverride;
|
|
389
|
+
const keys = [
|
|
390
|
+
opts.searchExpr,
|
|
391
|
+
opts["search-expr"],
|
|
392
|
+
opts.searchValue,
|
|
393
|
+
opts["search-value"]
|
|
394
|
+
];
|
|
395
|
+
if (searchFieldsCache && searchFieldsCache.keys.length === keys.length && searchFieldsCache.keys.every((k, i) => k === keys[i])) return searchFieldsCache.entries;
|
|
396
|
+
const entries = mergeSearchFields(opts);
|
|
397
|
+
searchFieldsCache = {
|
|
398
|
+
keys,
|
|
399
|
+
entries
|
|
400
|
+
};
|
|
401
|
+
return entries;
|
|
402
|
+
}
|
|
403
|
+
/** Whether the CONSUMER declared fields (any spelling, or via `setSearchValue`). */
|
|
404
|
+
function searchFieldsConfigured() {
|
|
405
|
+
return searchFields() !== void 0;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Whether the bound SOURCE carries its own column list.
|
|
409
|
+
*
|
|
410
|
+
* A devextreme DataSource can be built with `searchExpr` of its own, and a grid
|
|
411
|
+
* that declares nothing relies on that source folding the search itself. The
|
|
412
|
+
* `'*'` default must not take that over — it would replace a correct server-side
|
|
413
|
+
* search with one derived from the loaded page.
|
|
414
|
+
*/
|
|
415
|
+
function sourceDeclaresSearchFields() {
|
|
416
|
+
const e = bound?.searchExpr?.();
|
|
417
|
+
return Array.isArray(e) ? e.length > 0 : !!e;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* The fields actually searched: what the consumer configured, else what the
|
|
421
|
+
* source declares (left to the source — see above), else the **default `'*'`**,
|
|
422
|
+
* meaning every top-level field.
|
|
423
|
+
*
|
|
424
|
+
* `'*'` is resolved against real rows by `searchEntries()` and never leaves the
|
|
425
|
+
* client as a literal — it expands to concrete column names first.
|
|
426
|
+
*/
|
|
427
|
+
function effectiveSearchFields() {
|
|
428
|
+
const configured = searchFields();
|
|
429
|
+
if (configured !== void 0) return configured;
|
|
430
|
+
if (sourceDeclaresSearchFields()) return void 0;
|
|
431
|
+
return [...DEFAULT_SEARCH_FIELDS];
|
|
432
|
+
}
|
|
433
|
+
let groupFields = toFields(opts.group);
|
|
434
|
+
const collapsed = /* @__PURE__ */ new Set();
|
|
435
|
+
let groupPageSize = Math.max(1, opts.pageSize ?? 10);
|
|
436
|
+
let groupPageIndex = 0;
|
|
437
|
+
let pageSizeAll = false;
|
|
438
|
+
const chunkSize = Math.max(1, opts.chunkSize ?? 100);
|
|
439
|
+
let suppressSync = false;
|
|
440
|
+
let allRows = [];
|
|
441
|
+
let scrollMode = "off";
|
|
442
|
+
let accumulated = [];
|
|
443
|
+
let scrollNextPage = 0;
|
|
444
|
+
let scrollHasMore = false;
|
|
445
|
+
let scrollLoading = false;
|
|
446
|
+
let scrollToken = 0;
|
|
447
|
+
let preferredPageSize = null;
|
|
448
|
+
let sourcePageSize = null;
|
|
449
|
+
let vStart = 0;
|
|
450
|
+
let vEnd = 0;
|
|
451
|
+
let vPadTop = 0;
|
|
452
|
+
let vPadBottom = 0;
|
|
453
|
+
const groupRowPaging = /* @__PURE__ */ new Map();
|
|
454
|
+
const defaultGroupRowPageSize = opts.groupRowPageSize && opts.groupRowPageSize > 0 ? Math.floor(opts.groupRowPageSize) : 0;
|
|
455
|
+
/** Effective row paging for a group: explicit registration, else the default. */
|
|
456
|
+
function groupRowPagingFor(path) {
|
|
457
|
+
const explicit = groupRowPaging.get(path);
|
|
458
|
+
if (explicit) return explicit;
|
|
459
|
+
return defaultGroupRowPageSize ? {
|
|
460
|
+
pageIndex: 0,
|
|
461
|
+
pageSize: defaultGroupRowPageSize
|
|
462
|
+
} : null;
|
|
463
|
+
}
|
|
464
|
+
const pathIndex = /* @__PURE__ */ new Map();
|
|
465
|
+
let serverGroup = opts.serverGroup ?? null;
|
|
466
|
+
let serverGroups = [];
|
|
467
|
+
const groupRows = /* @__PURE__ */ new Map();
|
|
468
|
+
const groupRowsKey = /* @__PURE__ */ new Map();
|
|
469
|
+
const groupRowLoading = /* @__PURE__ */ new Set();
|
|
470
|
+
let searchValue = null;
|
|
471
|
+
let groupSeq = 0;
|
|
472
|
+
const isGrouped = () => groupFields.length > 0;
|
|
473
|
+
/** Server mode requires a single group field + a `serverGroup` provider. */
|
|
474
|
+
const serverMode = () => !!serverGroup && groupFields.length === 1;
|
|
475
|
+
function clearRowCaches() {
|
|
476
|
+
groupRows.clear();
|
|
477
|
+
groupRowsKey.clear();
|
|
478
|
+
groupRowLoading.clear();
|
|
479
|
+
}
|
|
480
|
+
const formOps = [];
|
|
481
|
+
let formKey = null;
|
|
482
|
+
let formShowForm = false;
|
|
483
|
+
let formTempSeq = 0;
|
|
484
|
+
let formHandle = null;
|
|
485
|
+
const MONO_ROW_KEY = Symbol("monoFormRowKey");
|
|
486
|
+
const EDITABLE_MARK = "Editable";
|
|
487
|
+
const ctrl = {
|
|
488
|
+
items: [],
|
|
489
|
+
mapped: [],
|
|
490
|
+
displayRows: [],
|
|
491
|
+
loading: false,
|
|
492
|
+
hasLoaded: false,
|
|
493
|
+
error: null,
|
|
494
|
+
totalCount: 0,
|
|
495
|
+
pageIndex: 0,
|
|
496
|
+
pageSize: 0,
|
|
497
|
+
pageSizeAll: false,
|
|
498
|
+
pageCount: 0,
|
|
499
|
+
searchTerms: [],
|
|
500
|
+
sorts: [],
|
|
501
|
+
sortField: null,
|
|
502
|
+
sortOrder: null,
|
|
503
|
+
grouped: groupFields.length > 0,
|
|
504
|
+
scrollMode: "off",
|
|
505
|
+
hasMore: false,
|
|
506
|
+
loadedCount: 0,
|
|
507
|
+
virtualStart: 0,
|
|
508
|
+
virtualEnd: 0,
|
|
509
|
+
virtualPadTop: 0,
|
|
510
|
+
virtualPadBottom: 0,
|
|
511
|
+
dataSource: null,
|
|
512
|
+
editingKey: null,
|
|
513
|
+
editableTrigger: opts.editableTrigger ?? "click",
|
|
514
|
+
editorNavKeys: opts.editorNavKeys ?? "tab-arrows",
|
|
515
|
+
onCellChange: null,
|
|
516
|
+
pendingData: formOps,
|
|
517
|
+
load,
|
|
518
|
+
reload,
|
|
519
|
+
clear,
|
|
520
|
+
setPage,
|
|
521
|
+
setScrollPaging,
|
|
522
|
+
loadNext,
|
|
523
|
+
setVirtualWindow,
|
|
524
|
+
setPreferredPageSize,
|
|
525
|
+
setPageSize,
|
|
526
|
+
setSearch,
|
|
527
|
+
setSearchTerms,
|
|
528
|
+
setSearchValue: setSearchExpr,
|
|
529
|
+
setSearchExpr,
|
|
530
|
+
searchValue: effectiveSearchFields,
|
|
531
|
+
searchExpr: effectiveSearchFields,
|
|
532
|
+
setFilter,
|
|
533
|
+
setDataSourceOptions,
|
|
534
|
+
setOdataOptions,
|
|
535
|
+
refresh,
|
|
536
|
+
resolvedDataSourceOptions,
|
|
537
|
+
setColumnFilter,
|
|
538
|
+
columnFilterCombining,
|
|
539
|
+
errorBehaviour,
|
|
540
|
+
setErrorBehaviour,
|
|
541
|
+
columnFilter,
|
|
542
|
+
filteredColumns,
|
|
543
|
+
distinctValues,
|
|
544
|
+
setSort,
|
|
545
|
+
sortCombining,
|
|
546
|
+
sortOf,
|
|
547
|
+
sortIndex,
|
|
548
|
+
clearSort,
|
|
549
|
+
columnMap,
|
|
550
|
+
setData,
|
|
551
|
+
setGroup,
|
|
552
|
+
toggleGroup,
|
|
553
|
+
isGroupCollapsed,
|
|
554
|
+
expandAllGroups,
|
|
555
|
+
collapseAllGroups,
|
|
556
|
+
setGroupPageSize,
|
|
557
|
+
setGroupPage,
|
|
558
|
+
clearGroupPaging,
|
|
559
|
+
groupPageInfo,
|
|
560
|
+
groupNode,
|
|
561
|
+
getData,
|
|
562
|
+
summary,
|
|
563
|
+
summaryText,
|
|
564
|
+
setSummaryResolver,
|
|
565
|
+
registerSummary,
|
|
566
|
+
unregisterSummary,
|
|
567
|
+
summaries,
|
|
568
|
+
buildGroups: buildGroupTree,
|
|
569
|
+
export: exportReport,
|
|
570
|
+
import: importSheet,
|
|
571
|
+
isCellImported,
|
|
572
|
+
isRowImported,
|
|
573
|
+
bind,
|
|
574
|
+
subscribe,
|
|
575
|
+
dispose,
|
|
576
|
+
registerColumn,
|
|
577
|
+
unregisterColumn,
|
|
578
|
+
detail,
|
|
579
|
+
check,
|
|
580
|
+
registerDetail,
|
|
581
|
+
unregisterDetail,
|
|
582
|
+
columns,
|
|
583
|
+
props,
|
|
584
|
+
registeredColumns,
|
|
585
|
+
editableColumns,
|
|
586
|
+
moveEditor,
|
|
587
|
+
bindRowTrigger,
|
|
588
|
+
beginEditRow,
|
|
589
|
+
cancelEdit,
|
|
590
|
+
isEditingRow,
|
|
591
|
+
isEditingCell,
|
|
592
|
+
commitCell,
|
|
593
|
+
editorKeydown,
|
|
594
|
+
stageCell,
|
|
595
|
+
cellValue,
|
|
596
|
+
isCellDirty,
|
|
597
|
+
isRowDirty,
|
|
598
|
+
pendingCount,
|
|
599
|
+
hasChanges,
|
|
600
|
+
changes,
|
|
601
|
+
discardChanges,
|
|
602
|
+
saveChanges,
|
|
603
|
+
form,
|
|
604
|
+
rowKey
|
|
605
|
+
};
|
|
606
|
+
if (opts.filterBuilder) ctrl.filterBuilder = monoFilterBuilder({
|
|
607
|
+
...opts.filterBuilder,
|
|
608
|
+
dataGrid: ctrl
|
|
609
|
+
});
|
|
610
|
+
const columnEls = /* @__PURE__ */ new Set();
|
|
611
|
+
const pending = /* @__PURE__ */ new Map();
|
|
612
|
+
function normalizeSummary(cfg) {
|
|
613
|
+
if (!cfg) return {
|
|
614
|
+
specs: [],
|
|
615
|
+
recalcSearching: false,
|
|
616
|
+
recalcChangedData: true,
|
|
617
|
+
resolve: null
|
|
618
|
+
};
|
|
619
|
+
if (Array.isArray(cfg)) return {
|
|
620
|
+
specs: [...cfg],
|
|
621
|
+
recalcSearching: false,
|
|
622
|
+
recalcChangedData: true,
|
|
623
|
+
resolve: null
|
|
624
|
+
};
|
|
625
|
+
const specs = [];
|
|
626
|
+
for (const [field, spec] of Object.entries(cfg.fields ?? {})) {
|
|
627
|
+
const list = Array.isArray(spec) ? spec : [spec];
|
|
628
|
+
for (const s of list) specs.push({
|
|
629
|
+
field,
|
|
630
|
+
...s
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
specs,
|
|
635
|
+
recalcSearching: cfg.recalculate?.searching ?? false,
|
|
636
|
+
recalcChangedData: cfg.recalculate?.changedData ?? true,
|
|
637
|
+
resolve: cfg.resolve ?? null
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
const _summaryCfg = normalizeSummary(opts.summary);
|
|
641
|
+
const summarySpecs = _summaryCfg.specs;
|
|
642
|
+
for (const col of opts.props?.th ?? []) {
|
|
643
|
+
if (!col?.field || !col.summary) continue;
|
|
644
|
+
const spec = {
|
|
645
|
+
field: col.field,
|
|
646
|
+
...col.summary
|
|
647
|
+
};
|
|
648
|
+
if (!summarySpecs.some((s) => s.field === spec.field && (s.type ?? "sum") === (spec.type ?? "sum") && s.name === spec.name)) summarySpecs.push(spec);
|
|
649
|
+
}
|
|
650
|
+
const recalcSearching = _summaryCfg.recalcSearching;
|
|
651
|
+
const recalcChangedData = _summaryCfg.recalcChangedData;
|
|
652
|
+
let summaryResolve = _summaryCfg.resolve;
|
|
653
|
+
const summaryCache = /* @__PURE__ */ new Map();
|
|
654
|
+
/**
|
|
655
|
+
* The `$apply`-through-the-store switch, shared by the summary footer and the
|
|
656
|
+
* header filter: off by option, or off for good once the backend rejected one.
|
|
657
|
+
*/
|
|
658
|
+
const applyGate = createApplyGate(opts.serverApply !== false);
|
|
659
|
+
let errorMode = opts.onError === "keep-list" ? "keep-list" : "clear-list";
|
|
660
|
+
function errorBehaviour() {
|
|
661
|
+
return errorMode;
|
|
662
|
+
}
|
|
663
|
+
function setErrorBehaviour(next) {
|
|
664
|
+
errorMode = next === "keep-list" ? "keep-list" : "clear-list";
|
|
665
|
+
}
|
|
666
|
+
const summaryKey = (spec) => `${spec.field ?? "*"}|${spec.type ?? "sum"}|${spec.name ?? ""}`;
|
|
667
|
+
/** Find the spec that a `(field, type)` read refers to. Type is matched only when given. */
|
|
668
|
+
function findSummarySpec(field, type) {
|
|
669
|
+
return summarySpecs.find((s) => (s.field ?? "") === field && (type === void 0 || (s.type ?? "sum") === type));
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* How many elements currently want each spec.
|
|
673
|
+
*
|
|
674
|
+
* Registration is REFCOUNTED because several `<mono-table-summary>` cells can
|
|
675
|
+
* declare the same field (a footer total and a toolbar total, say). Only the
|
|
676
|
+
* first pushes the spec; previously the first to unmount removed the shared spec
|
|
677
|
+
* out from under the others, and since nothing re-runs their registration they
|
|
678
|
+
* rendered an empty value forever.
|
|
679
|
+
*/
|
|
680
|
+
const summaryRefs = /* @__PURE__ */ new Map();
|
|
681
|
+
function registerSummary(spec) {
|
|
682
|
+
const key = summaryKey(spec);
|
|
683
|
+
summaryRefs.set(key, (summaryRefs.get(key) ?? 0) + 1);
|
|
684
|
+
if (summarySpecs.some((s) => summaryKey(s) === key)) return;
|
|
685
|
+
summarySpecs.push(spec);
|
|
686
|
+
recomputeSummaries(true);
|
|
687
|
+
}
|
|
688
|
+
function unregisterSummary(spec) {
|
|
689
|
+
const key = summaryKey(spec);
|
|
690
|
+
const refs = (summaryRefs.get(key) ?? 1) - 1;
|
|
691
|
+
if (refs > 0) {
|
|
692
|
+
summaryRefs.set(key, refs);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
summaryRefs.delete(key);
|
|
696
|
+
const i = summarySpecs.findIndex((s) => summaryKey(s) === key);
|
|
697
|
+
if (i >= 0) summarySpecs.splice(i, 1);
|
|
698
|
+
}
|
|
699
|
+
function summaries() {
|
|
700
|
+
return summarySpecs;
|
|
701
|
+
}
|
|
702
|
+
/** Build the public result object for one spec from the cache. */
|
|
703
|
+
function summaryResult(spec) {
|
|
704
|
+
const value = summaryCache.get(summaryKey(spec)) ?? null;
|
|
705
|
+
return {
|
|
706
|
+
field: spec.field,
|
|
707
|
+
type: spec.type ?? "sum",
|
|
708
|
+
name: spec.name,
|
|
709
|
+
value,
|
|
710
|
+
text: formatSummary(value, spec)
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
const summaryHandle = {
|
|
714
|
+
get(field, type) {
|
|
715
|
+
const spec = findSummarySpec(field, type);
|
|
716
|
+
return spec ? summaryResult(spec) : null;
|
|
717
|
+
},
|
|
718
|
+
getAll() {
|
|
719
|
+
return summarySpecs.map(summaryResult);
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
function summary(field, type) {
|
|
723
|
+
if (field === void 0) return summaryHandle;
|
|
724
|
+
let spec = findSummarySpec(field, type);
|
|
725
|
+
if (!spec) {
|
|
726
|
+
spec = {
|
|
727
|
+
field,
|
|
728
|
+
type: type ?? "sum"
|
|
729
|
+
};
|
|
730
|
+
registerSummary(spec);
|
|
731
|
+
}
|
|
732
|
+
return summaryCache.get(summaryKey(spec)) ?? null;
|
|
733
|
+
}
|
|
734
|
+
let summaryVersion = 0;
|
|
735
|
+
let summaryComputed = -1;
|
|
736
|
+
/**
|
|
737
|
+
* Bumped on EVERY row-set change, unconditionally — `summaryVersion` is gated
|
|
738
|
+
* by the `recalculate` config, so it can't be used to answer "is the row set
|
|
739
|
+
* still the one I last read?". `check().selectAll()` uses this to know whether
|
|
740
|
+
* its cached drain is still valid.
|
|
741
|
+
*/
|
|
742
|
+
let rowSetVersion = 0;
|
|
743
|
+
const bumpSummary = (reason) => {
|
|
744
|
+
rowSetVersion++;
|
|
745
|
+
if (reason === "search" && !recalcSearching) return;
|
|
746
|
+
if (reason === "data" && !recalcChangedData && summaryComputed >= 0) return;
|
|
747
|
+
summaryVersion++;
|
|
748
|
+
};
|
|
749
|
+
function summaryText(field, type) {
|
|
750
|
+
const spec = findSummarySpec(field, type) ?? {
|
|
751
|
+
field,
|
|
752
|
+
type: type ?? "sum"
|
|
753
|
+
};
|
|
754
|
+
return formatSummary(summary(field, type), spec);
|
|
755
|
+
}
|
|
756
|
+
function formatSummary(value, spec) {
|
|
757
|
+
if (spec.format) return spec.format(value, ctrl.items);
|
|
758
|
+
if (value === null || Number.isNaN(value)) return spec.emptyText ?? "—";
|
|
759
|
+
const body = spec.precision != null ? value.toLocaleString(void 0, {
|
|
760
|
+
minimumFractionDigits: spec.precision,
|
|
761
|
+
maximumFractionDigits: spec.precision
|
|
762
|
+
}) : value.toLocaleString();
|
|
763
|
+
return `${spec.prefix ?? ""}${body}${spec.suffix ?? ""}`;
|
|
764
|
+
}
|
|
765
|
+
/** Aggregate one spec over `rows`. Non-numeric values are skipped. */
|
|
766
|
+
function aggregate(spec, rows) {
|
|
767
|
+
const type = spec.type ?? "sum";
|
|
768
|
+
if (type === "count") return rows.length;
|
|
769
|
+
const field = spec.field;
|
|
770
|
+
if (!field) return null;
|
|
771
|
+
const valuesOf = (row) => {
|
|
772
|
+
const raw = getFieldValue(row, field);
|
|
773
|
+
return Array.isArray(raw) ? raw : [raw];
|
|
774
|
+
};
|
|
775
|
+
if (type === "countDistinct") {
|
|
776
|
+
const seen = /* @__PURE__ */ new Set();
|
|
777
|
+
for (const row of rows) for (const v of valuesOf(row)) if (v != null && v !== "") seen.add(v);
|
|
778
|
+
return seen.size;
|
|
779
|
+
}
|
|
780
|
+
const nums = [];
|
|
781
|
+
for (const row of rows) for (const raw of valuesOf(row)) {
|
|
782
|
+
if (raw == null || raw === "") continue;
|
|
783
|
+
const n = Number(raw);
|
|
784
|
+
if (Number.isFinite(n)) nums.push(n);
|
|
785
|
+
}
|
|
786
|
+
if (!nums.length) return null;
|
|
787
|
+
switch (type) {
|
|
788
|
+
case "sum": return nums.reduce((a, b) => a + b, 0);
|
|
789
|
+
case "avg": return nums.reduce((a, b) => a + b, 0) / nums.length;
|
|
790
|
+
case "min": return Math.min(...nums);
|
|
791
|
+
case "max": return Math.max(...nums);
|
|
792
|
+
default: return null;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Run the consumer's resolver, normalised to one value per spec.
|
|
797
|
+
*
|
|
798
|
+
* Declines (`null`) for every case the caller should not have to think about:
|
|
799
|
+
* no resolver, no bound source, an ARRAY source (already in memory — a network
|
|
800
|
+
* round trip for rows we hold would be absurd), a spec set no `$apply` can
|
|
801
|
+
* express, or a resolver that returned the wrong shape.
|
|
802
|
+
*
|
|
803
|
+
* A THROW is caught and reported once rather than propagated: a failed totals
|
|
804
|
+
* request should degrade to a slower total, not break the grid. It is warned
|
|
805
|
+
* about exactly once per grid so a persistent fault is visible without
|
|
806
|
+
* flooding the console on every reload.
|
|
807
|
+
*/
|
|
808
|
+
let _summaryResolveWarned = false;
|
|
809
|
+
/**
|
|
810
|
+
* Swap the resolver after construction.
|
|
811
|
+
*
|
|
812
|
+
* The option form covers a grid you build yourself. This covers the other
|
|
813
|
+
* case, which is at least as common: a grid built by a shared factory or
|
|
814
|
+
* composable that does not forward `summary` — the consumer still owns the
|
|
815
|
+
* fetcher, and needs somewhere to hand it over.
|
|
816
|
+
*
|
|
817
|
+
* Forces a recompute rather than waiting for the next data change, so a footer
|
|
818
|
+
* already on screen with a locally-drained total corrects itself, and clears
|
|
819
|
+
* the one-shot warning so a newly attached resolver gets a fair hearing.
|
|
820
|
+
*/
|
|
821
|
+
function setSummaryResolver(resolve) {
|
|
822
|
+
summaryResolve = resolve ?? null;
|
|
823
|
+
_summaryResolveWarned = false;
|
|
824
|
+
recomputeSummaries(true);
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* A resolver's (or the server's) answer → one value per spec, in spec order.
|
|
828
|
+
* Positional, or keyed by alias — falling back to the plain field name, which
|
|
829
|
+
* is what an `$apply` aliases to whenever a field carries one aggregate (the
|
|
830
|
+
* ordinary case). `null` when the shape cannot be read.
|
|
831
|
+
*/
|
|
832
|
+
function summaryOutToValues(out) {
|
|
833
|
+
const num = (v) => {
|
|
834
|
+
const n = Number(v);
|
|
835
|
+
return Number.isFinite(n) ? n : null;
|
|
836
|
+
};
|
|
837
|
+
if (Array.isArray(out)) return out.length === summarySpecs.length ? out.map(num) : null;
|
|
838
|
+
if (!out || typeof out !== "object") return null;
|
|
839
|
+
const row = out;
|
|
840
|
+
const aliases = summaryAliases(summarySpecs);
|
|
841
|
+
return summarySpecs.map((spec, i) => {
|
|
842
|
+
const key = aliases[i];
|
|
843
|
+
if (key in row) return num(row[key]);
|
|
844
|
+
if (spec.field && spec.field in row) return num(row[spec.field]);
|
|
845
|
+
return null;
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
async function resolveSummaries() {
|
|
849
|
+
if (!summaryResolve || !bound || Array.isArray(bound)) return null;
|
|
850
|
+
const apply = buildSummaryApply(summarySpecs);
|
|
851
|
+
if (!apply) return null;
|
|
852
|
+
const filter = summaryFilterOf(bound);
|
|
853
|
+
const aliases = summaryAliases(summarySpecs);
|
|
854
|
+
try {
|
|
855
|
+
const out = await summaryResolve({
|
|
856
|
+
specs: summarySpecs,
|
|
857
|
+
aliases,
|
|
858
|
+
filter,
|
|
859
|
+
odataFilter: arrayToODataString(filter) ?? "",
|
|
860
|
+
apply,
|
|
861
|
+
applyWithFilter: composeSummaryApply(apply, filter),
|
|
862
|
+
source: bound
|
|
863
|
+
});
|
|
864
|
+
if (!out) return null;
|
|
865
|
+
return summaryOutToValues(out);
|
|
866
|
+
} catch (err) {
|
|
867
|
+
if (!_summaryResolveWarned) {
|
|
868
|
+
_summaryResolveWarned = true;
|
|
869
|
+
console.warn("[mono-table] summary resolver failed; falling back to reading every row. Return \"null\" from the resolver to fall back without this warning.", err);
|
|
870
|
+
}
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* The totals as ONE `$apply=filter(…)/aggregate(…)` request through the bound
|
|
876
|
+
* source's own store — the same clause the consumer resolver is offered, sent
|
|
877
|
+
* by the grid itself when there is no resolver (or it declined).
|
|
878
|
+
*
|
|
879
|
+
* `null` is the decline: no store, the path switched off, a store that cannot
|
|
880
|
+
* carry an `$apply` (`loadApply`), a response that is not one aggregated row
|
|
881
|
+
* (a server that ignored the clause hands back entities, which carry none of
|
|
882
|
+
* the aliases), or a failed request — which also reports to the gate, so a
|
|
883
|
+
* backend that rejects `$apply` is asked exactly once.
|
|
884
|
+
*/
|
|
885
|
+
async function resolveSummariesViaStore() {
|
|
886
|
+
if (applyGate.skip || !bound || Array.isArray(bound) || !hasStore()) return null;
|
|
887
|
+
const apply = buildSummaryApply(summarySpecs);
|
|
888
|
+
if (!apply) return null;
|
|
889
|
+
let rows;
|
|
890
|
+
try {
|
|
891
|
+
rows = await loadApply(bound.store(), composeSummaryApply(apply, summaryFilterOf(bound)));
|
|
892
|
+
} catch (err) {
|
|
893
|
+
applyGate.reject(err);
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
if (!rows || rows.length !== 1 || !isRolledUp(rows, summaryAliases(summarySpecs))) return null;
|
|
897
|
+
return summaryOutToValues(rows[0]);
|
|
898
|
+
}
|
|
899
|
+
let _summaryRun = 0;
|
|
900
|
+
/**
|
|
901
|
+
* Recompute every registered summary over the full filtered set and notify.
|
|
902
|
+
* No-op when nothing is registered (an unused grid never fetches) or — unless
|
|
903
|
+
* `force`d — when the set hasn't changed since the last compute (so sort/paging
|
|
904
|
+
* don't refetch). A run token guards against overlapping async reads landing
|
|
905
|
+
* out of order.
|
|
906
|
+
*
|
|
907
|
+
* BURST COALESCING: a footer mounts many `<mono-table-summary>` elements and each
|
|
908
|
+
* registration forces a recompute — Lit elements upgrade across separate tasks, so a
|
|
909
|
+
* 48-column footer used to fire one resolver request PER element, each carrying only
|
|
910
|
+
* the specs registered so far (the `$apply` aggregate list visibly grew 1 → 2 → N).
|
|
911
|
+
* While one recompute is in flight, later ones only mark `_summaryQueued` and return;
|
|
912
|
+
* a single trailing recompute then runs with the FULL spec set. A mount burst is
|
|
913
|
+
* capped at leading + trailing requests whatever N is, and the trailing run's cache
|
|
914
|
+
* write is what the footer finally shows.
|
|
915
|
+
*/
|
|
916
|
+
let _summaryInFlight = false;
|
|
917
|
+
let _summaryQueued = false;
|
|
918
|
+
async function recomputeSummaries(force = false) {
|
|
919
|
+
if (!summarySpecs.length) return;
|
|
920
|
+
if (!force && summaryComputed === summaryVersion) return;
|
|
921
|
+
if (_summaryInFlight) {
|
|
922
|
+
_summaryQueued = true;
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
_summaryInFlight = true;
|
|
926
|
+
const run = ++_summaryRun;
|
|
927
|
+
const captured = summaryVersion;
|
|
928
|
+
try {
|
|
929
|
+
let resolved = await resolveSummaries();
|
|
930
|
+
if (run !== _summaryRun) return;
|
|
931
|
+
if (!resolved) {
|
|
932
|
+
resolved = await resolveSummariesViaStore();
|
|
933
|
+
if (run !== _summaryRun) return;
|
|
934
|
+
}
|
|
935
|
+
if (resolved) {
|
|
936
|
+
summarySpecs.forEach((spec, i) => {
|
|
937
|
+
summaryCache.set(summaryKey(spec), resolved[i] ?? null);
|
|
938
|
+
});
|
|
939
|
+
summaryComputed = captured;
|
|
940
|
+
notify();
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
const rows = await getData();
|
|
944
|
+
if (run !== _summaryRun) return;
|
|
945
|
+
for (const spec of summarySpecs) summaryCache.set(summaryKey(spec), aggregate(spec, rows));
|
|
946
|
+
summaryComputed = captured;
|
|
947
|
+
notify();
|
|
948
|
+
} finally {
|
|
949
|
+
_summaryInFlight = false;
|
|
950
|
+
if (_summaryQueued) {
|
|
951
|
+
_summaryQueued = false;
|
|
952
|
+
recomputeSummaries(true);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
function registerColumn(el) {
|
|
957
|
+
columnEls.add(el);
|
|
958
|
+
}
|
|
959
|
+
function unregisterColumn(el) {
|
|
960
|
+
columnEls.delete(el);
|
|
961
|
+
if (columnEls.size === 0) unbindRowTrigger();
|
|
962
|
+
}
|
|
963
|
+
const detailEls = /* @__PURE__ */ new Set();
|
|
964
|
+
function registerDetail(el) {
|
|
965
|
+
detailEls.add(el);
|
|
966
|
+
}
|
|
967
|
+
function unregisterDetail(el) {
|
|
968
|
+
detailEls.delete(el);
|
|
969
|
+
}
|
|
970
|
+
const detailHandle = {
|
|
971
|
+
expandAll() {
|
|
972
|
+
for (const el of detailEls) el.open = true;
|
|
973
|
+
notify();
|
|
974
|
+
},
|
|
975
|
+
collapseAll() {
|
|
976
|
+
for (const el of detailEls) if (!el.stayOpen) el.open = false;
|
|
977
|
+
notify();
|
|
978
|
+
},
|
|
979
|
+
collapseOthers(except) {
|
|
980
|
+
let changed = false;
|
|
981
|
+
for (const el of detailEls) {
|
|
982
|
+
if (el === except || el.stayOpen || !el.open) continue;
|
|
983
|
+
el.open = false;
|
|
984
|
+
changed = true;
|
|
985
|
+
}
|
|
986
|
+
if (changed) notify();
|
|
987
|
+
},
|
|
988
|
+
openCount() {
|
|
989
|
+
let open = 0;
|
|
990
|
+
for (const el of detailEls) if (el.open) open++;
|
|
991
|
+
return open;
|
|
992
|
+
},
|
|
993
|
+
getAll: () => domOrdered(Array.from(detailEls))
|
|
994
|
+
};
|
|
995
|
+
function detail() {
|
|
996
|
+
return detailHandle;
|
|
997
|
+
}
|
|
998
|
+
const checkedRows = /* @__PURE__ */ new Map();
|
|
999
|
+
let checkConfig = {};
|
|
1000
|
+
let checkPending = false;
|
|
1001
|
+
/** `keyValue` as a list (empty = project nothing, i.e. keep the whole row). */
|
|
1002
|
+
function checkKeys() {
|
|
1003
|
+
const k = checkConfig.keyValue;
|
|
1004
|
+
if (!k) return [];
|
|
1005
|
+
return (Array.isArray(k) ? k : [k]).filter(Boolean);
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Columns to fetch while draining, or `null` for "everything".
|
|
1009
|
+
*
|
|
1010
|
+
* `keyExpr` is normally included, even when the consumer only asked for other
|
|
1011
|
+
* fields: `rowKeyOf` reads `row[keyExpr]` and falls back to the array INDEX
|
|
1012
|
+
* when it's missing, so a drain without it would key every row by its position
|
|
1013
|
+
* and collide with the loaded page. A `[*]` path can't be expressed as
|
|
1014
|
+
* `$select` at all (it needs an expand), so any wildcard drops the optimisation.
|
|
1015
|
+
*
|
|
1016
|
+
* EXCEPT when `keyExpr` names a field the rows do not have — a misconfigured
|
|
1017
|
+
* (or defaulted) key would otherwise put a non-existent column on the wire and
|
|
1018
|
+
* the server rejects the whole drain: OData answers `$select=Id` on an entity
|
|
1019
|
+
* without `Id` with a 400. A wrong key should cost a wider fetch, not a failed
|
|
1020
|
+
* one, so drop the optimisation and read whole rows instead.
|
|
1021
|
+
*/
|
|
1022
|
+
function checkSelect() {
|
|
1023
|
+
const keys = checkKeys();
|
|
1024
|
+
if (!keys.length) return null;
|
|
1025
|
+
const cols = [];
|
|
1026
|
+
for (const key of keys) {
|
|
1027
|
+
if (parseFieldPath(key).hasWildcard) return null;
|
|
1028
|
+
const selector = toODataSelector(key);
|
|
1029
|
+
if (!selector) return null;
|
|
1030
|
+
cols.push(selector);
|
|
1031
|
+
}
|
|
1032
|
+
if (!cols.includes(keyExpr)) {
|
|
1033
|
+
if (!rowsHaveKeyExpr()) {
|
|
1034
|
+
warnMissingKeyExpr();
|
|
1035
|
+
return null;
|
|
1036
|
+
}
|
|
1037
|
+
cols.push(keyExpr);
|
|
1038
|
+
}
|
|
1039
|
+
return cols;
|
|
1040
|
+
}
|
|
1041
|
+
/** A positive integer limit, or undefined when the config has none. */
|
|
1042
|
+
function limitOf(value) {
|
|
1043
|
+
if (value == null) return void 0;
|
|
1044
|
+
const n = Math.floor(Number(value));
|
|
1045
|
+
return Number.isFinite(n) && n >= 0 ? n : void 0;
|
|
1046
|
+
}
|
|
1047
|
+
function checkLimits() {
|
|
1048
|
+
const max = limitOf(checkConfig.max);
|
|
1049
|
+
const min = limitOf(checkConfig.min);
|
|
1050
|
+
return {
|
|
1051
|
+
...max !== void 0 ? { max } : {},
|
|
1052
|
+
...min !== void 0 ? { min } : {}
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Add rows up to the cap. Rows already selected never count against the room
|
|
1057
|
+
* left — re-selecting a drained set the user has partly unticked must still be
|
|
1058
|
+
* a no-op for what is there. When `capped` is false (`replace`) the cap is
|
|
1059
|
+
* ignored: a value the developer pushes in is theirs.
|
|
1060
|
+
*/
|
|
1061
|
+
function addChecked(rows, capped = true) {
|
|
1062
|
+
const max = capped ? checkLimits().max : void 0;
|
|
1063
|
+
rows.forEach((row, i) => {
|
|
1064
|
+
const key = rowKeyOf(row, i);
|
|
1065
|
+
if (!checkedRows.has(key) && max !== void 0 && checkedRows.size >= max) return;
|
|
1066
|
+
checkedRows.set(key, row);
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* The last completed drain, so "select all → untick one → select all again"
|
|
1071
|
+
* costs nothing instead of walking the whole source a second time.
|
|
1072
|
+
*
|
|
1073
|
+
* Deliberately NOT an OData `not in (…)` query over the already-selected keys:
|
|
1074
|
+
* the exclusion list is largest exactly when the selection is largest, so
|
|
1075
|
+
* re-selecting after unticking one of 1,085 rows would mean putting 1,084 ids
|
|
1076
|
+
* in a URL — past every practical URL limit, and slower than the paged drain it
|
|
1077
|
+
* replaces. Remembering the drain is the cheap direction.
|
|
1078
|
+
*
|
|
1079
|
+
* One entry, invalidated whenever the ROW SET could differ: a different
|
|
1080
|
+
* filter/search (compared by reference, since the controller builds a fresh
|
|
1081
|
+
* expression for every real change), a different `$select` projection, or any
|
|
1082
|
+
* `bumpSummary` (bind / reload / setData / search / filter). Sorting and paging
|
|
1083
|
+
* deliberately do NOT invalidate — they reorder or window the same set.
|
|
1084
|
+
*/
|
|
1085
|
+
let drainedRows = null;
|
|
1086
|
+
let drainedFilter = null;
|
|
1087
|
+
let drainedSearch = null;
|
|
1088
|
+
let drainedTerms = "";
|
|
1089
|
+
let drainedSelect = "";
|
|
1090
|
+
let drainedVersion = -1;
|
|
1091
|
+
function drainIsReusable() {
|
|
1092
|
+
if (!drainedRows) return false;
|
|
1093
|
+
return Object.is(drainedFilter, bound?.filter?.() ?? null) && Object.is(drainedSearch, bound?.searchValue?.() ?? null) && drainedTerms === JSON.stringify(ctrl.searchTerms) && drainedSelect === JSON.stringify(checkSelect()) && drainedVersion === rowSetVersion;
|
|
1094
|
+
}
|
|
1095
|
+
function rememberDrain(rows) {
|
|
1096
|
+
drainedRows = rows;
|
|
1097
|
+
drainedFilter = bound?.filter?.() ?? null;
|
|
1098
|
+
drainedSearch = bound?.searchValue?.() ?? null;
|
|
1099
|
+
drainedTerms = JSON.stringify(ctrl.searchTerms);
|
|
1100
|
+
drainedSelect = JSON.stringify(checkSelect());
|
|
1101
|
+
drainedVersion = rowSetVersion;
|
|
1102
|
+
}
|
|
1103
|
+
const checkHandle = {
|
|
1104
|
+
getAll() {
|
|
1105
|
+
const keys = checkKeys();
|
|
1106
|
+
return [...checkedRows.values()].map((row) => projectFields(row, keys));
|
|
1107
|
+
},
|
|
1108
|
+
count: () => checkedRows.size,
|
|
1109
|
+
rows: () => [...checkedRows.values()],
|
|
1110
|
+
isChecked(row) {
|
|
1111
|
+
return checkedRows.has(typeof row === "string" ? row : rowKey(row));
|
|
1112
|
+
},
|
|
1113
|
+
toggle(item, checked) {
|
|
1114
|
+
if (item == null) return;
|
|
1115
|
+
const key = rowKey(item);
|
|
1116
|
+
const has = checkedRows.has(key);
|
|
1117
|
+
const next = checked === void 0 ? !has : checked;
|
|
1118
|
+
const { max, min } = checkLimits();
|
|
1119
|
+
if (!(next && !has && max !== void 0 && checkedRows.size >= max || !next && has && min !== void 0 && checkedRows.size <= min)) if (next) checkedRows.set(key, item);
|
|
1120
|
+
else checkedRows.delete(key);
|
|
1121
|
+
notify();
|
|
1122
|
+
},
|
|
1123
|
+
selectPage() {
|
|
1124
|
+
addChecked(ctrl.items);
|
|
1125
|
+
notify();
|
|
1126
|
+
},
|
|
1127
|
+
replace(rows) {
|
|
1128
|
+
checkedRows.clear();
|
|
1129
|
+
addChecked(rows, false);
|
|
1130
|
+
notify();
|
|
1131
|
+
},
|
|
1132
|
+
async selectAll() {
|
|
1133
|
+
if (checkPending) return;
|
|
1134
|
+
if (drainIsReusable()) {
|
|
1135
|
+
addChecked(drainedRows);
|
|
1136
|
+
notify();
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
checkPending = true;
|
|
1140
|
+
notify();
|
|
1141
|
+
try {
|
|
1142
|
+
const select = checkSelect();
|
|
1143
|
+
const rows = await readAllRows(bound, {
|
|
1144
|
+
chunkSize: Math.max(1, Math.floor(checkConfig.chunk ?? 100)),
|
|
1145
|
+
...select ? { select } : {},
|
|
1146
|
+
sort: mergedSort()
|
|
1147
|
+
});
|
|
1148
|
+
rememberDrain(rows);
|
|
1149
|
+
addChecked(rows);
|
|
1150
|
+
clearError();
|
|
1151
|
+
} catch (err) {
|
|
1152
|
+
captureError(err, "selectAll");
|
|
1153
|
+
throw err;
|
|
1154
|
+
} finally {
|
|
1155
|
+
checkPending = false;
|
|
1156
|
+
notify();
|
|
1157
|
+
}
|
|
1158
|
+
},
|
|
1159
|
+
clear() {
|
|
1160
|
+
if (!checkedRows.size) return;
|
|
1161
|
+
const min = checkLimits().min ?? 0;
|
|
1162
|
+
if (min > 0) {
|
|
1163
|
+
if (checkedRows.size <= min) {
|
|
1164
|
+
notify();
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
const keep = [...checkedRows.entries()].slice(0, min);
|
|
1168
|
+
checkedRows.clear();
|
|
1169
|
+
for (const [key, row] of keep) checkedRows.set(key, row);
|
|
1170
|
+
} else checkedRows.clear();
|
|
1171
|
+
notify();
|
|
1172
|
+
},
|
|
1173
|
+
limits: checkLimits,
|
|
1174
|
+
get atMax() {
|
|
1175
|
+
const max = checkLimits().max;
|
|
1176
|
+
return max !== void 0 && checkedRows.size >= max;
|
|
1177
|
+
},
|
|
1178
|
+
get atMin() {
|
|
1179
|
+
const min = checkLimits().min;
|
|
1180
|
+
return min !== void 0 && checkedRows.size <= min;
|
|
1181
|
+
},
|
|
1182
|
+
get pending() {
|
|
1183
|
+
return checkPending;
|
|
1184
|
+
},
|
|
1185
|
+
get allChecked() {
|
|
1186
|
+
const rows = ctrl.items;
|
|
1187
|
+
return rows.length > 0 && rows.every((row, i) => checkedRows.has(rowKeyOf(row, i)));
|
|
1188
|
+
},
|
|
1189
|
+
get someChecked() {
|
|
1190
|
+
const rows = ctrl.items;
|
|
1191
|
+
if (!rows.length || !checkedRows.size) return false;
|
|
1192
|
+
return !rows.every((row, i) => checkedRows.has(rowKeyOf(row, i)));
|
|
1193
|
+
},
|
|
1194
|
+
configure(config) {
|
|
1195
|
+
for (const [k, v] of Object.entries(config)) if (v === null) delete checkConfig[k];
|
|
1196
|
+
else if (v !== void 0) checkConfig[k] = v;
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
1199
|
+
function check() {
|
|
1200
|
+
return checkHandle;
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Sort elements by document order so a handle reads left→right / top→bottom as
|
|
1204
|
+
* displayed. Falls back to insertion order where `compareDocumentPosition` is
|
|
1205
|
+
* unavailable (SSR / no DOM).
|
|
1206
|
+
*/
|
|
1207
|
+
function domOrdered(els) {
|
|
1208
|
+
return els.sort((a, b) => {
|
|
1209
|
+
if (typeof a.compareDocumentPosition !== "function") return 0;
|
|
1210
|
+
const pos = a.compareDocumentPosition(b);
|
|
1211
|
+
if (pos & 4) return -1;
|
|
1212
|
+
if (pos & 2) return 1;
|
|
1213
|
+
return 0;
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
function orderedColumnEls() {
|
|
1217
|
+
return Array.from(columnEls).sort((a, b) => {
|
|
1218
|
+
if (typeof a.compareDocumentPosition !== "function") return 0;
|
|
1219
|
+
const pos = a.compareDocumentPosition(b);
|
|
1220
|
+
if (pos & 4) return -1;
|
|
1221
|
+
if (pos & 2) return 1;
|
|
1222
|
+
return 0;
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
function registeredColumns() {
|
|
1226
|
+
return orderedColumnEls().map((el) => ({
|
|
1227
|
+
field: el.field,
|
|
1228
|
+
caption: el.caption,
|
|
1229
|
+
sort: el.sort,
|
|
1230
|
+
editable: el.editable,
|
|
1231
|
+
headerFilter: el.headerFilter,
|
|
1232
|
+
dateFilter: el.dateFilter
|
|
1233
|
+
}));
|
|
1234
|
+
}
|
|
1235
|
+
function editableColumns() {
|
|
1236
|
+
return registeredColumns().filter((c) => !!c.editable && !!c.field);
|
|
1237
|
+
}
|
|
1238
|
+
const columnDefs = [...opts.columns ?? []];
|
|
1239
|
+
const columnsHandle = {
|
|
1240
|
+
get: (field) => columnDefs.find((c) => c.field === field) ?? null,
|
|
1241
|
+
getAll: () => columnDefs
|
|
1242
|
+
};
|
|
1243
|
+
const elementProps = { ...opts.props ?? {} };
|
|
1244
|
+
{
|
|
1245
|
+
const declared = opts.props?.th ?? [];
|
|
1246
|
+
const byField = /* @__PURE__ */ new Map();
|
|
1247
|
+
for (const c of columnDefs) if (c?.field) byField.set(c.field, { ...c });
|
|
1248
|
+
for (const c of declared) {
|
|
1249
|
+
if (!c?.field) continue;
|
|
1250
|
+
byField.set(c.field, {
|
|
1251
|
+
...byField.get(c.field) ?? {},
|
|
1252
|
+
...c
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
const ordered = [];
|
|
1256
|
+
for (const c of declared) if (c?.field && byField.has(c.field)) ordered.push(byField.get(c.field));
|
|
1257
|
+
for (const [field, c] of byField) if (!ordered.some((o) => o.field === field)) ordered.push(c);
|
|
1258
|
+
elementProps.th = ordered;
|
|
1259
|
+
}
|
|
1260
|
+
function props() {
|
|
1261
|
+
return elementProps;
|
|
1262
|
+
}
|
|
1263
|
+
/** Snapshot written into `opts.state` on every notify (see `notify`). */
|
|
1264
|
+
function propsSnapshot() {
|
|
1265
|
+
return {
|
|
1266
|
+
...elementProps,
|
|
1267
|
+
th: (elementProps.th ?? []).map((c) => ({ ...c }))
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
function columns() {
|
|
1271
|
+
return columnsHandle;
|
|
1272
|
+
}
|
|
1273
|
+
function indexOfRowKey(rowKey) {
|
|
1274
|
+
return ctrl.items.findIndex((row, i) => rowKeyOf(row, i) === rowKey);
|
|
1275
|
+
}
|
|
1276
|
+
function rowByKey(rowKey) {
|
|
1277
|
+
const i = indexOfRowKey(rowKey);
|
|
1278
|
+
return i >= 0 ? ctrl.items[i] : void 0;
|
|
1279
|
+
}
|
|
1280
|
+
function _cssEscape(value) {
|
|
1281
|
+
return typeof CSS !== "undefined" && CSS.escape ? CSS.escape(value) : value;
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Is this pointer event a press on `el`'s scrollbar rather than on its content?
|
|
1285
|
+
*
|
|
1286
|
+
* Overlay scrollbars are painted over the content box, so the event target is
|
|
1287
|
+
* the element underneath and only the coordinates can tell the two apart. A
|
|
1288
|
+
* band is live only on an axis that actually overflows, which keeps a genuine
|
|
1289
|
+
* click on the last row from being mistaken for a scrollbar grab.
|
|
1290
|
+
*/
|
|
1291
|
+
function _inScrollbarBand(el, e) {
|
|
1292
|
+
const { clientX: x, clientY: y } = e;
|
|
1293
|
+
if (typeof x !== "number" || typeof y !== "number") return false;
|
|
1294
|
+
const BAND = 17;
|
|
1295
|
+
const r = el.getBoundingClientRect();
|
|
1296
|
+
const inX = x >= r.left && x <= r.right;
|
|
1297
|
+
const inY = y >= r.top && y <= r.bottom;
|
|
1298
|
+
if (el.scrollWidth > el.clientWidth + 1 && inX && y >= r.bottom - BAND && y <= r.bottom) return true;
|
|
1299
|
+
if (el.scrollHeight > el.clientHeight + 1 && inY && x >= r.right - BAND && x <= r.right) return true;
|
|
1300
|
+
return false;
|
|
1301
|
+
}
|
|
1302
|
+
let _outsidePointer = null;
|
|
1303
|
+
function installOutsideExit() {
|
|
1304
|
+
if (typeof document === "undefined" || _outsidePointer) return;
|
|
1305
|
+
_outsidePointer = (e) => {
|
|
1306
|
+
const key = ctrl.editingKey;
|
|
1307
|
+
if (key == null) return;
|
|
1308
|
+
const target = e.target;
|
|
1309
|
+
if (!target?.closest) return;
|
|
1310
|
+
if (target.closest(`[data-row-key="${_cssEscape(key)}"]`) || target.closest("[data-mono-popup-portal], .flatpickr-calendar")) return;
|
|
1311
|
+
let node = target;
|
|
1312
|
+
while (node) {
|
|
1313
|
+
const s = getComputedStyle(node);
|
|
1314
|
+
if (/(auto|scroll)/.test(s.overflowX + s.overflowY) && _inScrollbarBand(node, e)) return;
|
|
1315
|
+
node = node.parentElement;
|
|
1316
|
+
}
|
|
1317
|
+
setTimeout(() => {
|
|
1318
|
+
if (ctrl.editingKey === key) cancelEdit();
|
|
1319
|
+
}, 0);
|
|
1320
|
+
};
|
|
1321
|
+
document.addEventListener("pointerdown", _outsidePointer, true);
|
|
1322
|
+
}
|
|
1323
|
+
function removeOutsideExit() {
|
|
1324
|
+
if (!_outsidePointer || typeof document === "undefined") return;
|
|
1325
|
+
document.removeEventListener("pointerdown", _outsidePointer, true);
|
|
1326
|
+
_outsidePointer = null;
|
|
1327
|
+
}
|
|
1328
|
+
let _triggerEl = null;
|
|
1329
|
+
let _triggerHandler = null;
|
|
1330
|
+
let _triggerType = null;
|
|
1331
|
+
const _onRowTrigger = (e) => {
|
|
1332
|
+
const target = e.target;
|
|
1333
|
+
if (!target?.closest) return;
|
|
1334
|
+
if (target.closest("[data-edit-cell]")) return;
|
|
1335
|
+
if (target.closest("button, a, input, select, textarea, label, [contenteditable=\"true\"]")) return;
|
|
1336
|
+
const rowEl = target.closest("[data-row-key]");
|
|
1337
|
+
if (!rowEl) return;
|
|
1338
|
+
const key = rowEl.getAttribute("data-row-key");
|
|
1339
|
+
if (key == null || ctrl.editingKey === key) return;
|
|
1340
|
+
if (!editableColumns().length) return;
|
|
1341
|
+
if (isEditableRow(rowByKey(key))) return;
|
|
1342
|
+
beginEditRow(key, e);
|
|
1343
|
+
};
|
|
1344
|
+
/**
|
|
1345
|
+
* Point the row trigger at the `<table>` that owns this grid's rows. Called by
|
|
1346
|
+
* every `mono-table-th` on update; idempotent, so only a changed table element
|
|
1347
|
+
* or a changed `editableTrigger` actually re-binds.
|
|
1348
|
+
*/
|
|
1349
|
+
function bindRowTrigger(tableEl) {
|
|
1350
|
+
if (typeof document === "undefined") return;
|
|
1351
|
+
const type = ctrl.editableTrigger === "double-click" ? "dblclick" : "click";
|
|
1352
|
+
if (tableEl === _triggerEl && type === _triggerType) return;
|
|
1353
|
+
unbindRowTrigger();
|
|
1354
|
+
if (!tableEl) return;
|
|
1355
|
+
_triggerHandler = _onRowTrigger;
|
|
1356
|
+
_triggerType = type;
|
|
1357
|
+
_triggerEl = tableEl;
|
|
1358
|
+
tableEl.addEventListener(type, _triggerHandler);
|
|
1359
|
+
}
|
|
1360
|
+
function unbindRowTrigger() {
|
|
1361
|
+
if (_triggerEl && _triggerHandler && _triggerType) _triggerEl.removeEventListener(_triggerType, _triggerHandler);
|
|
1362
|
+
_triggerEl = null;
|
|
1363
|
+
_triggerHandler = null;
|
|
1364
|
+
_triggerType = null;
|
|
1365
|
+
}
|
|
1366
|
+
function beginEditRow(rowKey, event) {
|
|
1367
|
+
if (ctrl.editingKey === rowKey) return;
|
|
1368
|
+
ctrl.editingKey = rowKey;
|
|
1369
|
+
installOutsideExit();
|
|
1370
|
+
installEditorKeys();
|
|
1371
|
+
notify();
|
|
1372
|
+
if (event) focusRowCell(event.target, rowKey, "first", { preferClicked: true });
|
|
1373
|
+
}
|
|
1374
|
+
function cancelEdit() {
|
|
1375
|
+
removeOutsideExit();
|
|
1376
|
+
removeEditorKeys();
|
|
1377
|
+
if (ctrl.editingKey === null) return;
|
|
1378
|
+
ctrl.editingKey = null;
|
|
1379
|
+
notify();
|
|
1380
|
+
}
|
|
1381
|
+
function isEditingRow(rowKey) {
|
|
1382
|
+
return ctrl.editingKey === rowKey;
|
|
1383
|
+
}
|
|
1384
|
+
/** A `form({ showForm: true })` row inserted for inline data entry. */
|
|
1385
|
+
function isEditableRow(row) {
|
|
1386
|
+
return !!row && row._Type === EDITABLE_MARK;
|
|
1387
|
+
}
|
|
1388
|
+
function isEditingCell(rowKeyStr, field) {
|
|
1389
|
+
if (!registeredColumns().some((c) => c.field === field && !!c.editable)) return false;
|
|
1390
|
+
return ctrl.editingKey === rowKeyStr || isEditableRow(rowByKey(rowKeyStr));
|
|
1391
|
+
}
|
|
1392
|
+
function commitCell(rowKey, field, value) {
|
|
1393
|
+
ctrl.onCellChange?.({
|
|
1394
|
+
rowKey,
|
|
1395
|
+
field,
|
|
1396
|
+
value,
|
|
1397
|
+
row: rowByKey(rowKey)
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
function serverKeyOf(rowKey) {
|
|
1401
|
+
const row = rowByKey(rowKey);
|
|
1402
|
+
return row ? row[keyExpr] ?? rowKey : rowKey;
|
|
1403
|
+
}
|
|
1404
|
+
function stageCell(rowKey, field, value) {
|
|
1405
|
+
const row = rowByKey(rowKey);
|
|
1406
|
+
if (isEditableRow(row)) {
|
|
1407
|
+
if (isPath(field)) setFieldValue(row, field, value);
|
|
1408
|
+
else row[field] = value;
|
|
1409
|
+
notify();
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
const patch = pending.get(rowKey) ?? {};
|
|
1413
|
+
patch[field] = value;
|
|
1414
|
+
pending.set(rowKey, patch);
|
|
1415
|
+
clearImported(rowKey, field);
|
|
1416
|
+
notify();
|
|
1417
|
+
}
|
|
1418
|
+
const importedCells = /* @__PURE__ */ new Map();
|
|
1419
|
+
function markImported(rowKey, fields) {
|
|
1420
|
+
const set = importedCells.get(rowKey) ?? /* @__PURE__ */ new Set();
|
|
1421
|
+
for (const f of fields) set.add(f);
|
|
1422
|
+
importedCells.set(rowKey, set);
|
|
1423
|
+
}
|
|
1424
|
+
function clearImported(rowKey, field) {
|
|
1425
|
+
const set = importedCells.get(rowKey);
|
|
1426
|
+
if (!set) return;
|
|
1427
|
+
set.delete(field);
|
|
1428
|
+
if (!set.size) importedCells.delete(rowKey);
|
|
1429
|
+
}
|
|
1430
|
+
function isCellImported(rowKey, field) {
|
|
1431
|
+
return !!importedCells.get(rowKey)?.has(field);
|
|
1432
|
+
}
|
|
1433
|
+
function isRowImported(rowKey) {
|
|
1434
|
+
return !!importedCells.get(rowKey)?.size;
|
|
1435
|
+
}
|
|
1436
|
+
/** Staged value for a cell, or `fallback` when the cell isn't staged. */
|
|
1437
|
+
function cellValue(rowKey, field, fallback) {
|
|
1438
|
+
const row = rowByKey(rowKey);
|
|
1439
|
+
if (isEditableRow(row)) {
|
|
1440
|
+
const v = isPath(field) ? getFieldValue(row, field) : row[field];
|
|
1441
|
+
return v === void 0 ? fallback : v;
|
|
1442
|
+
}
|
|
1443
|
+
const patch = pending.get(rowKey);
|
|
1444
|
+
return patch && field in patch ? patch[field] : fallback;
|
|
1445
|
+
}
|
|
1446
|
+
function isCellDirty(rowKey, field) {
|
|
1447
|
+
const patch = pending.get(rowKey);
|
|
1448
|
+
return !!patch && field in patch;
|
|
1449
|
+
}
|
|
1450
|
+
function isRowDirty(rowKey) {
|
|
1451
|
+
return pending.has(rowKey);
|
|
1452
|
+
}
|
|
1453
|
+
function pendingCount() {
|
|
1454
|
+
return pending.size;
|
|
1455
|
+
}
|
|
1456
|
+
function hasChanges() {
|
|
1457
|
+
return pending.size > 0;
|
|
1458
|
+
}
|
|
1459
|
+
/** The staged change set — for a consumer that sends its own (bulk) request. */
|
|
1460
|
+
function changes() {
|
|
1461
|
+
return Array.from(pending.entries()).map(([rowKey, patch]) => ({
|
|
1462
|
+
rowKey,
|
|
1463
|
+
key: serverKeyOf(rowKey),
|
|
1464
|
+
patch: { ...patch },
|
|
1465
|
+
row: rowByKey(rowKey)
|
|
1466
|
+
}));
|
|
1467
|
+
}
|
|
1468
|
+
/** Drop the staged buffer and the import marks that shadow it. */
|
|
1469
|
+
function clearPending() {
|
|
1470
|
+
pending.clear();
|
|
1471
|
+
importedCells.clear();
|
|
1472
|
+
}
|
|
1473
|
+
function discardChanges() {
|
|
1474
|
+
if (pending.size === 0) return;
|
|
1475
|
+
clearPending();
|
|
1476
|
+
notify();
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Flush every staged row to the bound source, then clear + reflect. Remote
|
|
1480
|
+
* DataSource → `store.update(key, patch)` per row + `reload()`; array source →
|
|
1481
|
+
* per-row `update`; else a `setData` merge of the current page. On error the
|
|
1482
|
+
* buffer is KEPT so the save can be retried.
|
|
1483
|
+
*/
|
|
1484
|
+
async function saveChanges() {
|
|
1485
|
+
if (pending.size === 0) return;
|
|
1486
|
+
const entries = Array.from(pending.entries());
|
|
1487
|
+
const s = bound;
|
|
1488
|
+
const store = s?.store?.();
|
|
1489
|
+
if (store && typeof store.update === "function") {
|
|
1490
|
+
for (const [rowKey, patch] of entries) {
|
|
1491
|
+
if (Object.keys(patch).some(isPath)) console.warn("[monoDataGrid] saveChanges: a remote store.update patch contains a path key; nested PATCH bodies are not auto-built — send the nested shape your backend expects.");
|
|
1492
|
+
await store.update(serverKeyOf(rowKey), patch);
|
|
1493
|
+
}
|
|
1494
|
+
clearPending();
|
|
1495
|
+
await reload();
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
if (s && typeof s.update === "function") {
|
|
1499
|
+
for (const [rowKey, patch] of entries) await s.update(serverKeyOf(rowKey), patch);
|
|
1500
|
+
clearPending();
|
|
1501
|
+
notify();
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
if (s && typeof s.setData === "function") {
|
|
1505
|
+
const next = (s.data?.() ?? ctrl.items ?? []).map((row, i) => {
|
|
1506
|
+
const patch = pending.get(rowKeyOf(row, i));
|
|
1507
|
+
return patch ? mergePatch(row, patch) : row;
|
|
1508
|
+
});
|
|
1509
|
+
clearPending();
|
|
1510
|
+
await setData(next);
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
clearPending();
|
|
1514
|
+
notify();
|
|
1515
|
+
}
|
|
1516
|
+
/** First existing row to infer an empty-row shape from (array backing → page → items). */
|
|
1517
|
+
function formSample() {
|
|
1518
|
+
const fromData = (bound?.data?.())?.[0];
|
|
1519
|
+
const fromItems = (bound?.items?.())?.[0];
|
|
1520
|
+
return fromData ?? fromItems ?? ctrl.items[0];
|
|
1521
|
+
}
|
|
1522
|
+
/** Build a typed-empty row from a sample: number→0, string→'', bool→false, array→[], Date→null, object→{}. */
|
|
1523
|
+
function emptyLike(sample) {
|
|
1524
|
+
const out = {};
|
|
1525
|
+
for (const k in sample) {
|
|
1526
|
+
const v = sample[k];
|
|
1527
|
+
if (v == null) out[k] = null;
|
|
1528
|
+
else if (typeof v === "number") out[k] = 0;
|
|
1529
|
+
else if (typeof v === "string") out[k] = "";
|
|
1530
|
+
else if (typeof v === "boolean") out[k] = false;
|
|
1531
|
+
else if (Array.isArray(v)) out[k] = [];
|
|
1532
|
+
else if (v instanceof Date) out[k] = null;
|
|
1533
|
+
else if (typeof v === "object") out[k] = {};
|
|
1534
|
+
else out[k] = null;
|
|
1535
|
+
}
|
|
1536
|
+
return out;
|
|
1537
|
+
}
|
|
1538
|
+
/** Strip OData response metadata off a staged row before it enters the data. */
|
|
1539
|
+
function cleanRow(row) {
|
|
1540
|
+
if (row && typeof row === "object") {
|
|
1541
|
+
const r = row;
|
|
1542
|
+
if ("@odata.context" in r) delete r["@odata.context"];
|
|
1543
|
+
if ("@odata.url" in r) delete r["@odata.url"];
|
|
1544
|
+
}
|
|
1545
|
+
return row;
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* The row to actually commit: OData metadata stripped, and — for a `showForm`
|
|
1549
|
+
* row — the `_Type` marker removed (the non-enumerable identity symbol is dropped
|
|
1550
|
+
* by the spread). Returns a clean object; never carries the editable marker.
|
|
1551
|
+
*/
|
|
1552
|
+
function commitRow(row) {
|
|
1553
|
+
cleanRow(row);
|
|
1554
|
+
if (isEditableRow(row)) {
|
|
1555
|
+
const { _Type, ...rest } = row;
|
|
1556
|
+
return rest;
|
|
1557
|
+
}
|
|
1558
|
+
return row;
|
|
1559
|
+
}
|
|
1560
|
+
const looseEq = (a, b) => a == b;
|
|
1561
|
+
/** Replay one op into a full array (array source path). */
|
|
1562
|
+
function replayArray(arr, op, key) {
|
|
1563
|
+
const idxOf = (k) => arr.findIndex((e) => looseEq(e?.[key], k));
|
|
1564
|
+
if (op.op === "add") for (const src of op.rows) {
|
|
1565
|
+
const row = commitRow(src);
|
|
1566
|
+
const i = idxOf(row?.[key]);
|
|
1567
|
+
if (i >= 0) arr.splice(i, 1, row);
|
|
1568
|
+
else arr.unshift(row);
|
|
1569
|
+
}
|
|
1570
|
+
else if (op.op === "edit") {
|
|
1571
|
+
const i = idxOf(op.key);
|
|
1572
|
+
if (i >= 0) arr.splice(i, 1, {
|
|
1573
|
+
...arr[i],
|
|
1574
|
+
...op.row
|
|
1575
|
+
});
|
|
1576
|
+
} else {
|
|
1577
|
+
const i = idxOf(op.key);
|
|
1578
|
+
if (i >= 0) arr.splice(i, 1);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
/** Replay one op into the live page collection (remote source path). Returns the totalCount delta. */
|
|
1582
|
+
function replayDs(coll, op, key, pushes) {
|
|
1583
|
+
const idxOf = (k) => coll.findIndex((e) => looseEq(e?.[key], k));
|
|
1584
|
+
if (op.op === "add") {
|
|
1585
|
+
let delta = 0;
|
|
1586
|
+
for (const src of op.rows) {
|
|
1587
|
+
const row = commitRow(src);
|
|
1588
|
+
const k = row?.[key];
|
|
1589
|
+
const i = idxOf(k);
|
|
1590
|
+
if (i >= 0) Object.assign(coll[i], row);
|
|
1591
|
+
else {
|
|
1592
|
+
coll.unshift(row);
|
|
1593
|
+
pushes.push({
|
|
1594
|
+
type: "insert",
|
|
1595
|
+
data: row,
|
|
1596
|
+
key: k
|
|
1597
|
+
});
|
|
1598
|
+
delta++;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
return delta;
|
|
1602
|
+
}
|
|
1603
|
+
if (op.op === "edit") {
|
|
1604
|
+
const i = idxOf(op.key);
|
|
1605
|
+
if (i >= 0) {
|
|
1606
|
+
Object.assign(coll[i], op.row);
|
|
1607
|
+
pushes.push({
|
|
1608
|
+
type: "update",
|
|
1609
|
+
key: op.key,
|
|
1610
|
+
data: op.row
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
return 0;
|
|
1614
|
+
}
|
|
1615
|
+
const i = idxOf(op.key);
|
|
1616
|
+
if (i >= 0) {
|
|
1617
|
+
coll.splice(i, 1);
|
|
1618
|
+
pushes.push({
|
|
1619
|
+
type: "remove",
|
|
1620
|
+
key: op.key
|
|
1621
|
+
});
|
|
1622
|
+
return -1;
|
|
1623
|
+
}
|
|
1624
|
+
return 0;
|
|
1625
|
+
}
|
|
1626
|
+
/** Commit all staged form ops into the rendered data. Local only — no server call. Final: no undo. */
|
|
1627
|
+
async function applyForm() {
|
|
1628
|
+
if (formOps.length === 0) return;
|
|
1629
|
+
const key = formKey ?? keyExpr;
|
|
1630
|
+
const ops = formOps.splice(0, formOps.length);
|
|
1631
|
+
const s = bound;
|
|
1632
|
+
if (s && typeof s.setData === "function") {
|
|
1633
|
+
const next = [...s.data?.() ?? ctrl.items.filter((r) => !isEditableRow(r)) ?? []];
|
|
1634
|
+
for (const op of ops) replayArray(next, op, key);
|
|
1635
|
+
await setData(next);
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
const store = s?.store?.();
|
|
1639
|
+
if (s && store) {
|
|
1640
|
+
if (isGrouped() || serverMode()) console.warn("[monoDataGrid] form().apply() on a grouped/server grid updates only the flat page; reload to refresh groups.");
|
|
1641
|
+
const coll = s.items?.() ?? [];
|
|
1642
|
+
const pushes = [];
|
|
1643
|
+
let delta = 0;
|
|
1644
|
+
for (const op of ops) delta += replayDs(coll, op, key, pushes);
|
|
1645
|
+
if (typeof store.push === "function" && pushes.length) store.push(pushes);
|
|
1646
|
+
ctrl.items = [...coll];
|
|
1647
|
+
ctrl.totalCount = Math.max(0, ctrl.totalCount + delta);
|
|
1648
|
+
render();
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
notify();
|
|
1652
|
+
}
|
|
1653
|
+
/** Tag a `showForm` row: visible marker + stable non-enumerable identity. */
|
|
1654
|
+
function tagEditable(row) {
|
|
1655
|
+
row._Type = EDITABLE_MARK;
|
|
1656
|
+
Object.defineProperty(row, MONO_ROW_KEY, {
|
|
1657
|
+
value: `__form_${++formTempSeq}`,
|
|
1658
|
+
enumerable: false,
|
|
1659
|
+
configurable: true,
|
|
1660
|
+
writable: true
|
|
1661
|
+
});
|
|
1662
|
+
return row;
|
|
1663
|
+
}
|
|
1664
|
+
/** {@link MonoTableController.form} — memoized, chainable handle over one shared buffer. */
|
|
1665
|
+
function form(config) {
|
|
1666
|
+
if (config?.key) formKey = config.key;
|
|
1667
|
+
formShowForm = !!config?.showForm;
|
|
1668
|
+
if (formHandle) return formHandle;
|
|
1669
|
+
const handle = {
|
|
1670
|
+
add(row) {
|
|
1671
|
+
let rows;
|
|
1672
|
+
if (row === void 0) {
|
|
1673
|
+
const sample = formSample();
|
|
1674
|
+
rows = [sample ? emptyLike(sample) : {}];
|
|
1675
|
+
} else rows = Array.isArray(row) ? row : [row];
|
|
1676
|
+
if (formShowForm) {
|
|
1677
|
+
rows.forEach(tagEditable);
|
|
1678
|
+
ctrl.items.unshift(...rows);
|
|
1679
|
+
ctrl.totalCount += rows.length;
|
|
1680
|
+
render();
|
|
1681
|
+
}
|
|
1682
|
+
formOps.push({
|
|
1683
|
+
op: "add",
|
|
1684
|
+
rows
|
|
1685
|
+
});
|
|
1686
|
+
return handle;
|
|
1687
|
+
},
|
|
1688
|
+
edit(k, row) {
|
|
1689
|
+
formOps.push({
|
|
1690
|
+
op: "edit",
|
|
1691
|
+
key: k,
|
|
1692
|
+
row
|
|
1693
|
+
});
|
|
1694
|
+
return handle;
|
|
1695
|
+
},
|
|
1696
|
+
delete(k) {
|
|
1697
|
+
formOps.push({
|
|
1698
|
+
op: "delete",
|
|
1699
|
+
key: k
|
|
1700
|
+
});
|
|
1701
|
+
return handle;
|
|
1702
|
+
},
|
|
1703
|
+
apply: applyForm,
|
|
1704
|
+
revert(key) {
|
|
1705
|
+
const k = String(key);
|
|
1706
|
+
const items = ctrl.items;
|
|
1707
|
+
const idx = items.findIndex((r, i) => rowKeyOf(r, i) === k);
|
|
1708
|
+
if (idx >= 0 && isEditableRow(items[idx])) {
|
|
1709
|
+
items.splice(idx, 1);
|
|
1710
|
+
ctrl.totalCount = Math.max(0, ctrl.totalCount - 1);
|
|
1711
|
+
}
|
|
1712
|
+
for (let i = formOps.length - 1; i >= 0; i--) {
|
|
1713
|
+
const op = formOps[i];
|
|
1714
|
+
if (op.op !== "add") continue;
|
|
1715
|
+
op.rows = op.rows.filter((r) => rowKeyOf(r, -1) !== k);
|
|
1716
|
+
if (op.rows.length === 0) formOps.splice(i, 1);
|
|
1717
|
+
}
|
|
1718
|
+
render();
|
|
1719
|
+
return handle;
|
|
1720
|
+
},
|
|
1721
|
+
revertAll() {
|
|
1722
|
+
const shown = ctrl.items.filter((r) => isEditableRow(r)).length;
|
|
1723
|
+
if (shown) {
|
|
1724
|
+
ctrl.items = ctrl.items.filter((r) => !isEditableRow(r));
|
|
1725
|
+
ctrl.totalCount = Math.max(0, ctrl.totalCount - shown);
|
|
1726
|
+
}
|
|
1727
|
+
for (let i = formOps.length - 1; i >= 0; i--) {
|
|
1728
|
+
const op = formOps[i];
|
|
1729
|
+
if (op.op !== "add") continue;
|
|
1730
|
+
op.rows = op.rows.filter((r) => !isEditableRow(r));
|
|
1731
|
+
if (op.rows.length === 0) formOps.splice(i, 1);
|
|
1732
|
+
}
|
|
1733
|
+
render();
|
|
1734
|
+
return handle;
|
|
1735
|
+
},
|
|
1736
|
+
changes: () => formOps.map((o) => ({ ...o })),
|
|
1737
|
+
discard(key) {
|
|
1738
|
+
if (key === void 0) {
|
|
1739
|
+
formOps.length = 0;
|
|
1740
|
+
const shown = ctrl.items.filter((r) => isEditableRow(r)).length;
|
|
1741
|
+
if (shown) {
|
|
1742
|
+
ctrl.items = ctrl.items.filter((r) => !isEditableRow(r));
|
|
1743
|
+
ctrl.totalCount = Math.max(0, ctrl.totalCount - shown);
|
|
1744
|
+
render();
|
|
1745
|
+
}
|
|
1746
|
+
return handle;
|
|
1747
|
+
}
|
|
1748
|
+
const k = String(key);
|
|
1749
|
+
const items = ctrl.items;
|
|
1750
|
+
const idx = items.findIndex((r, i) => rowKeyOf(r, i) === k);
|
|
1751
|
+
if (idx >= 0 && isEditableRow(items[idx])) {
|
|
1752
|
+
items.splice(idx, 1);
|
|
1753
|
+
ctrl.totalCount = Math.max(0, ctrl.totalCount - 1);
|
|
1754
|
+
}
|
|
1755
|
+
for (let i = formOps.length - 1; i >= 0; i--) {
|
|
1756
|
+
const op = formOps[i];
|
|
1757
|
+
if (op.op === "add") {
|
|
1758
|
+
op.rows = op.rows.filter((r) => rowKeyOf(r, -1) !== k);
|
|
1759
|
+
if (op.rows.length === 0) formOps.splice(i, 1);
|
|
1760
|
+
} else if (String(op.key) === k) formOps.splice(i, 1);
|
|
1761
|
+
}
|
|
1762
|
+
render();
|
|
1763
|
+
return handle;
|
|
1764
|
+
},
|
|
1765
|
+
discardAll() {
|
|
1766
|
+
handle.discard();
|
|
1767
|
+
clearPending();
|
|
1768
|
+
cancelEdit();
|
|
1769
|
+
render();
|
|
1770
|
+
return handle;
|
|
1771
|
+
}
|
|
1772
|
+
};
|
|
1773
|
+
formHandle = handle;
|
|
1774
|
+
return handle;
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* Focus an editor in `rowKey`.
|
|
1778
|
+
*
|
|
1779
|
+
* `preventScroll` is the important part: a bare `.focus()` scrolls the element
|
|
1780
|
+
* into view, and this runs inside a `requestAnimationFrame` — one frame AFTER
|
|
1781
|
+
* the row re-rendered — so the browser scrolls `.mono-table-scroll`
|
|
1782
|
+
* (`overflow-x: auto`) or the page a moment after the click lands. That
|
|
1783
|
+
* delayed nudge is what reads as the editor "jumping".
|
|
1784
|
+
*
|
|
1785
|
+
* `reveal` opts back into a MINIMAL scroll for the cross-row Tab, where the
|
|
1786
|
+
* next row genuinely may be off screen. `block: 'nearest'` moves the least
|
|
1787
|
+
* possible amount and does nothing when the row is already visible — unlike
|
|
1788
|
+
* the default focus scroll, which can centre it.
|
|
1789
|
+
*
|
|
1790
|
+
* `preferClicked` focuses the editor in the cell the user actually clicked
|
|
1791
|
+
* (falling back to `edge` when that cell has none — an ID or actions column,
|
|
1792
|
+
* or a click on the row's padding). Without it, opening a row by clicking its
|
|
1793
|
+
* Job cell would drop the caret in Name, and the user has to Tab back across
|
|
1794
|
+
* the row to reach the field they aimed at.
|
|
1795
|
+
*/
|
|
1796
|
+
function focusRowCell(fromEl, rowKey, edge, opts = {}) {
|
|
1797
|
+
const from = fromEl;
|
|
1798
|
+
const table = from?.closest?.("table");
|
|
1799
|
+
if (!table || typeof requestAnimationFrame === "undefined") return;
|
|
1800
|
+
const clickedCell = opts.preferClicked ? from?.closest?.("td") : null;
|
|
1801
|
+
requestAnimationFrame(() => {
|
|
1802
|
+
const escaped = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(rowKey) : rowKey;
|
|
1803
|
+
const cells = table.querySelectorAll(`[data-row-key="${escaped}"] [data-edit-cell]`);
|
|
1804
|
+
const clicked = clickedCell?.closest(`[data-row-key="${escaped}"]`) && table.contains(clickedCell) ? clickedCell.querySelector("[data-edit-cell]") : null;
|
|
1805
|
+
const target = (opts.field ? table.querySelector(`[data-row-key="${escaped}"] [data-edit-cell="${_cssEscape(opts.field)}"]`) : null) ?? clicked ?? (edge === "first" ? cells[0] : cells[cells.length - 1]);
|
|
1806
|
+
const definesOwnFocus = typeof target?.focus === "function" && typeof HTMLElement !== "undefined" && target.focus !== HTMLElement.prototype.focus;
|
|
1807
|
+
const focusable = target?.matches?.("input, textarea, select, button") ? target : definesOwnFocus ? target : target?.querySelector?.("input, textarea, select, button") ?? target;
|
|
1808
|
+
if (opts.reveal) revealCell(target);
|
|
1809
|
+
focusable?.focus?.({ preventScroll: true });
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
/** The nearest scrollable ancestor of `el` — the table's own scroll region first. */
|
|
1813
|
+
function scrollParentOf(el) {
|
|
1814
|
+
const region = el.closest(".mono-table-scroll, [mono-table-scroll]");
|
|
1815
|
+
if (region) return region;
|
|
1816
|
+
let node = el.parentElement;
|
|
1817
|
+
while (node) {
|
|
1818
|
+
const s = getComputedStyle(node);
|
|
1819
|
+
if (/(auto|scroll)/.test(s.overflowX + s.overflowY)) return node;
|
|
1820
|
+
node = node.parentElement;
|
|
1821
|
+
}
|
|
1822
|
+
return null;
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* Scroll `cell` into view by the SMALLEST delta that works — and no further.
|
|
1826
|
+
*
|
|
1827
|
+
* `scrollIntoView({ block: 'nearest', inline: 'nearest' })` looks like the
|
|
1828
|
+
* answer but isn't: it knows nothing about pinned columns, so on a wide grid
|
|
1829
|
+
* it happily parks the cell *underneath* a `.mono-table-sticky-left` column or
|
|
1830
|
+
* the sticky header, leaving the user typing into something they can't see.
|
|
1831
|
+
* So compute the genuinely visible band from the pinned elements' own rects
|
|
1832
|
+
* and scroll only as far as that band requires. Each axis is handled
|
|
1833
|
+
* independently, so stepping sideways never nudges the page vertically.
|
|
1834
|
+
*
|
|
1835
|
+
* This has to be explicit now: the keyboard path focuses with
|
|
1836
|
+
* `preventScroll: true`, which is what stopped the browser from revealing the
|
|
1837
|
+
* editor for us.
|
|
1838
|
+
*/
|
|
1839
|
+
function revealCell(cell) {
|
|
1840
|
+
if (!cell || typeof getComputedStyle === "undefined") return;
|
|
1841
|
+
const scroller = scrollParentOf(cell);
|
|
1842
|
+
if (!scroller) return;
|
|
1843
|
+
const cellBox = cell.getBoundingClientRect();
|
|
1844
|
+
const view = scroller.getBoundingClientRect();
|
|
1845
|
+
const row = cell.closest("tr");
|
|
1846
|
+
const table = cell.closest("table");
|
|
1847
|
+
const MARGIN = 8;
|
|
1848
|
+
const edgeOf = (sel, side) => {
|
|
1849
|
+
const rects = Array.from((row ?? table)?.querySelectorAll(sel) ?? []).filter((n) => !n.contains(cell)).map((n) => n.getBoundingClientRect());
|
|
1850
|
+
if (!rects.length) return null;
|
|
1851
|
+
if (side === "left") return Math.max(...rects.map((r) => r.right));
|
|
1852
|
+
if (side === "right") return Math.min(...rects.map((r) => r.left));
|
|
1853
|
+
if (side === "top") return Math.max(...rects.map((r) => r.bottom));
|
|
1854
|
+
return Math.min(...rects.map((r) => r.top));
|
|
1855
|
+
};
|
|
1856
|
+
const left = Math.max(view.left, edgeOf(".mono-table-sticky-left, [mono-sticky-left]", "left") ?? -Infinity);
|
|
1857
|
+
const right = Math.min(view.right, edgeOf(".mono-table-sticky-right, [mono-sticky-right]", "right") ?? Infinity);
|
|
1858
|
+
if (cellBox.left < left + MARGIN) scroller.scrollLeft -= left + MARGIN - cellBox.left;
|
|
1859
|
+
else if (cellBox.right > right - MARGIN) scroller.scrollLeft += cellBox.right - (right - MARGIN);
|
|
1860
|
+
if (scroller.scrollHeight <= scroller.clientHeight + 1) return;
|
|
1861
|
+
const heads = Array.from(table?.querySelectorAll(".mono-table-sticky-head thead th, [mono-sticky-head] thead th, thead th") ?? []).map((n) => n.getBoundingClientRect());
|
|
1862
|
+
const foots = Array.from(table?.querySelectorAll(".mono-table-sticky-foot tfoot th, .mono-table-sticky-foot tfoot td, [mono-sticky-foot] tfoot th, [mono-sticky-foot] tfoot td") ?? []).map((n) => n.getBoundingClientRect());
|
|
1863
|
+
const top = Math.max(view.top, ...heads.map((r) => r.bottom).filter((v) => v <= view.bottom));
|
|
1864
|
+
const bottom = Math.min(view.bottom, ...foots.map((r) => r.top).filter((v) => v >= view.top));
|
|
1865
|
+
if (cellBox.top < top + MARGIN) scroller.scrollTop -= top + MARGIN - cellBox.top;
|
|
1866
|
+
else if (cellBox.bottom > bottom - MARGIN) scroller.scrollTop += cellBox.bottom - (bottom - MARGIN);
|
|
1867
|
+
}
|
|
1868
|
+
/** Which `[data-edit-cell]` holds the caret right now. */
|
|
1869
|
+
function activeEditorCell(fromEl) {
|
|
1870
|
+
const byEvent = fromEl?.closest?.("[data-edit-cell]");
|
|
1871
|
+
if (byEvent) return byEvent;
|
|
1872
|
+
return (typeof document !== "undefined" ? document.activeElement : null)?.closest?.("[data-edit-cell]") ?? null;
|
|
1873
|
+
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Move the editor one cell and focus it. Clamps on both axes and returns
|
|
1876
|
+
* `false` when the move is blocked, so the caller can swallow the key without
|
|
1877
|
+
* anything happening — arrow navigation must NOT wrap around an edge. Only the
|
|
1878
|
+
* plain Tab tap passes `wrap`, which lets left/right cross into the
|
|
1879
|
+
* previous/next row in reading order (the long-standing Tab behaviour).
|
|
1880
|
+
*/
|
|
1881
|
+
function moveEditor(dir, fromEl, opts = {}) {
|
|
1882
|
+
const rowKey = ctrl.editingKey;
|
|
1883
|
+
if (rowKey == null) return false;
|
|
1884
|
+
const cell = activeEditorCell(fromEl);
|
|
1885
|
+
const field = cell?.getAttribute("data-edit-cell");
|
|
1886
|
+
const cols = editableColumns();
|
|
1887
|
+
const colIdx = field ? cols.findIndex((c) => c.field === field) : -1;
|
|
1888
|
+
const rowIdx = indexOfRowKey(rowKey);
|
|
1889
|
+
if (colIdx === -1 || rowIdx === -1 || !cols.length) return false;
|
|
1890
|
+
const rows = ctrl.items;
|
|
1891
|
+
const anchor = cell ?? fromEl;
|
|
1892
|
+
if (dir === "up" || dir === "down") {
|
|
1893
|
+
const nextIdx = rowIdx + (dir === "down" ? 1 : -1);
|
|
1894
|
+
const next = rows[nextIdx];
|
|
1895
|
+
if (!next) return false;
|
|
1896
|
+
const nextKey = rowKeyOf(next, nextIdx);
|
|
1897
|
+
beginEditRow(nextKey);
|
|
1898
|
+
focusRowCell(anchor, nextKey, "first", {
|
|
1899
|
+
reveal: true,
|
|
1900
|
+
field: cols[colIdx].field
|
|
1901
|
+
});
|
|
1902
|
+
return true;
|
|
1903
|
+
}
|
|
1904
|
+
const nextCol = colIdx + (dir === "right" ? 1 : -1);
|
|
1905
|
+
if (nextCol >= 0 && nextCol < cols.length) {
|
|
1906
|
+
focusRowCell(anchor, rowKey, "first", {
|
|
1907
|
+
reveal: true,
|
|
1908
|
+
field: cols[nextCol].field
|
|
1909
|
+
});
|
|
1910
|
+
return true;
|
|
1911
|
+
}
|
|
1912
|
+
if (!opts.wrap) return false;
|
|
1913
|
+
const nextIdx = rowIdx + (dir === "right" ? 1 : -1);
|
|
1914
|
+
const next = rows[nextIdx];
|
|
1915
|
+
if (!next) return false;
|
|
1916
|
+
const nextKey = rowKeyOf(next, nextIdx);
|
|
1917
|
+
beginEditRow(nextKey);
|
|
1918
|
+
focusRowCell(anchor, nextKey, dir === "right" ? "first" : "last", { reveal: true });
|
|
1919
|
+
return true;
|
|
1920
|
+
}
|
|
1921
|
+
const ARROW_DIRS = {
|
|
1922
|
+
ArrowLeft: "left",
|
|
1923
|
+
ArrowRight: "right",
|
|
1924
|
+
ArrowUp: "up",
|
|
1925
|
+
ArrowDown: "down"
|
|
1926
|
+
};
|
|
1927
|
+
let _editorKeys = null;
|
|
1928
|
+
let _tabHeld = false;
|
|
1929
|
+
let _tabConsumed = false;
|
|
1930
|
+
let _tabShift = false;
|
|
1931
|
+
/**
|
|
1932
|
+
* The control inside `cell` that owns a popup. `data-edit-cell` usually sits on
|
|
1933
|
+
* the control itself, but allow a wrapper. Custom elements only, and `open`
|
|
1934
|
+
* must be a METHOD — `<details>` and `<dialog>` both carry a boolean `open`
|
|
1935
|
+
* property that isn't callable.
|
|
1936
|
+
*/
|
|
1937
|
+
function popupEditorOf(cell) {
|
|
1938
|
+
if (!cell) return null;
|
|
1939
|
+
return [cell, ...Array.from(cell.querySelectorAll("*"))].find((el) => el.tagName.includes("-") && typeof el.open === "function") ?? null;
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* `Enter` / `Escape`, shared by the document-level machine and the per-editor
|
|
1943
|
+
* `editorKeydown` so the two modes behave identically. Returns whether the key
|
|
1944
|
+
* belonged to us.
|
|
1945
|
+
*/
|
|
1946
|
+
function handleEditorActionKey(e, cell) {
|
|
1947
|
+
if (e.key === "Escape") {
|
|
1948
|
+
const host = popupEditorOf(cell);
|
|
1949
|
+
if (host?.isOpen && typeof host.close === "function") {
|
|
1950
|
+
e.preventDefault();
|
|
1951
|
+
e.stopPropagation();
|
|
1952
|
+
host.close();
|
|
1953
|
+
return true;
|
|
1954
|
+
}
|
|
1955
|
+
cancelEdit();
|
|
1956
|
+
return true;
|
|
1957
|
+
}
|
|
1958
|
+
if (e.key !== "Enter") return false;
|
|
1959
|
+
const active = e.target;
|
|
1960
|
+
if (active?.tagName === "TEXTAREA" || active?.isContentEditable) return false;
|
|
1961
|
+
const host = popupEditorOf(cell);
|
|
1962
|
+
if (host?.isOpen) return false;
|
|
1963
|
+
e.preventDefault();
|
|
1964
|
+
if (host) {
|
|
1965
|
+
e.stopPropagation();
|
|
1966
|
+
host.open();
|
|
1967
|
+
}
|
|
1968
|
+
return true;
|
|
1969
|
+
}
|
|
1970
|
+
/**
|
|
1971
|
+
* The very last cell in the grid (or the very first, going back). Tab is left
|
|
1972
|
+
* alone there so a keyboard user can still leave the table — trapping focus in
|
|
1973
|
+
* the grid would be worse than losing Tab+Arrow from that one cell.
|
|
1974
|
+
*/
|
|
1975
|
+
function atGridEdge(cell, back) {
|
|
1976
|
+
const cols = editableColumns();
|
|
1977
|
+
const colIdx = cols.findIndex((c) => c.field === cell.getAttribute("data-edit-cell"));
|
|
1978
|
+
const rowIdx = ctrl.editingKey != null ? indexOfRowKey(ctrl.editingKey) : -1;
|
|
1979
|
+
if (colIdx === -1 || rowIdx === -1) return true;
|
|
1980
|
+
return back ? colIdx === 0 && rowIdx === 0 : colIdx === cols.length - 1 && rowIdx === ctrl.items.length - 1;
|
|
1981
|
+
}
|
|
1982
|
+
function installEditorKeys() {
|
|
1983
|
+
if (typeof document === "undefined" || _editorKeys) return;
|
|
1984
|
+
if (ctrl.editorNavKeys !== "tab-arrows") return;
|
|
1985
|
+
const editingCell = (e) => {
|
|
1986
|
+
const key = ctrl.editingKey;
|
|
1987
|
+
if (key == null) return null;
|
|
1988
|
+
const cell = e.target?.closest?.("[data-edit-cell]");
|
|
1989
|
+
return cell?.closest(`[data-row-key="${_cssEscape(key)}"]`) ? cell : null;
|
|
1990
|
+
};
|
|
1991
|
+
const down = (evt) => {
|
|
1992
|
+
const e = evt;
|
|
1993
|
+
const cell = editingCell(e);
|
|
1994
|
+
if (!cell) return;
|
|
1995
|
+
if (handleEditorActionKey(e, cell)) return;
|
|
1996
|
+
if (e.key === "Tab") {
|
|
1997
|
+
if (atGridEdge(cell, e.shiftKey)) {
|
|
1998
|
+
_tabHeld = false;
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
e.preventDefault();
|
|
2002
|
+
if (!e.repeat) {
|
|
2003
|
+
_tabHeld = true;
|
|
2004
|
+
_tabConsumed = false;
|
|
2005
|
+
_tabShift = e.shiftKey;
|
|
2006
|
+
}
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
if (!_tabHeld) return;
|
|
2010
|
+
const dir = ARROW_DIRS[e.key];
|
|
2011
|
+
if (!dir) return;
|
|
2012
|
+
e.preventDefault();
|
|
2013
|
+
_tabConsumed = true;
|
|
2014
|
+
moveEditor(dir, e.target);
|
|
2015
|
+
};
|
|
2016
|
+
const up = (evt) => {
|
|
2017
|
+
const e = evt;
|
|
2018
|
+
if (e.key !== "Tab" || !_tabHeld) return;
|
|
2019
|
+
_tabHeld = false;
|
|
2020
|
+
if (_tabConsumed) return;
|
|
2021
|
+
moveEditor(_tabShift ? "left" : "right", e.target, { wrap: true });
|
|
2022
|
+
};
|
|
2023
|
+
document.addEventListener("keydown", down, true);
|
|
2024
|
+
document.addEventListener("keyup", up, true);
|
|
2025
|
+
_editorKeys = {
|
|
2026
|
+
down,
|
|
2027
|
+
up
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
function removeEditorKeys() {
|
|
2031
|
+
_tabHeld = false;
|
|
2032
|
+
_tabConsumed = false;
|
|
2033
|
+
if (!_editorKeys || typeof document === "undefined") return;
|
|
2034
|
+
document.removeEventListener("keydown", _editorKeys.down, true);
|
|
2035
|
+
document.removeEventListener("keyup", _editorKeys.up, true);
|
|
2036
|
+
_editorKeys = null;
|
|
2037
|
+
}
|
|
2038
|
+
function editorKeydown(event, rowKey, field) {
|
|
2039
|
+
if (ctrl.editorNavKeys === "tab-arrows") return;
|
|
2040
|
+
if (event.key === "Escape" || event.key === "Enter") {
|
|
2041
|
+
const cell = event.target?.closest?.("[data-edit-cell]");
|
|
2042
|
+
handleEditorActionKey(event, cell);
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
if (event.key !== "Tab") return;
|
|
2046
|
+
const cols = editableColumns();
|
|
2047
|
+
const idx = cols.findIndex((c) => c.field === field);
|
|
2048
|
+
if (idx === -1) return;
|
|
2049
|
+
const rowIdx = indexOfRowKey(rowKey);
|
|
2050
|
+
if (rowIdx === -1) return;
|
|
2051
|
+
if (event.shiftKey) {
|
|
2052
|
+
if (idx > 0) return;
|
|
2053
|
+
const prev = ctrl.items[rowIdx - 1];
|
|
2054
|
+
if (!prev) return;
|
|
2055
|
+
event.preventDefault();
|
|
2056
|
+
const prevKey = rowKeyOf(prev, rowIdx - 1);
|
|
2057
|
+
beginEditRow(prevKey);
|
|
2058
|
+
focusRowCell(event.target, prevKey, "last", { reveal: true });
|
|
2059
|
+
} else {
|
|
2060
|
+
if (idx < cols.length - 1) return;
|
|
2061
|
+
const nextRow = ctrl.items[rowIdx + 1];
|
|
2062
|
+
if (!nextRow) return;
|
|
2063
|
+
event.preventDefault();
|
|
2064
|
+
const nextKey = rowKeyOf(nextRow, rowIdx + 1);
|
|
2065
|
+
beginEditRow(nextKey);
|
|
2066
|
+
focusRowCell(event.target, nextKey, "first", { reveal: true });
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
function toFields(group) {
|
|
2070
|
+
if (!group) return [];
|
|
2071
|
+
return (Array.isArray(group) ? group : [group]).filter(Boolean);
|
|
2072
|
+
}
|
|
2073
|
+
/**
|
|
2074
|
+
* Sort descriptor sent to the source. When grouping, the group fields lead
|
|
2075
|
+
* (so each fetched/sliced page holds contiguous, ordered groups); the active
|
|
2076
|
+
* column sort follows — applied to the group field itself if it is one.
|
|
2077
|
+
*/
|
|
2078
|
+
function effectiveSort() {
|
|
2079
|
+
const keys = ctrl.sorts.map((s) => ({
|
|
2080
|
+
selector: s.field,
|
|
2081
|
+
desc: s.order === "desc"
|
|
2082
|
+
}));
|
|
2083
|
+
if (!groupFields.length) return keys.length ? keys : null;
|
|
2084
|
+
const list = groupFields.map((f) => ({
|
|
2085
|
+
selector: f,
|
|
2086
|
+
desc: keys.find((k) => k.selector === f)?.desc ?? false
|
|
2087
|
+
}));
|
|
2088
|
+
for (const k of keys) if (!groupFields.includes(k.selector)) list.push(k);
|
|
2089
|
+
return list;
|
|
2090
|
+
}
|
|
2091
|
+
/**
|
|
2092
|
+
* Push the merged sort onto the bound source — the column sort plus the base
|
|
2093
|
+
* sort as tiebreakers (see `mergedSort`). Remembers what it wrote so a sort the
|
|
2094
|
+
* consumer sets on the source afterwards can be told apart and adopted.
|
|
2095
|
+
*/
|
|
2096
|
+
function applySourceSort() {
|
|
2097
|
+
const s = bound;
|
|
2098
|
+
if (!s || typeof s.sort !== "function") return;
|
|
2099
|
+
s.sort(mergedSort());
|
|
2100
|
+
lastWrittenSort = s.sort() ?? null;
|
|
2101
|
+
}
|
|
2102
|
+
const notifier = createNotifier({ onFlush: () => {
|
|
2103
|
+
syncMapped();
|
|
2104
|
+
if (opts.state) opts.state.value = propsSnapshot();
|
|
2105
|
+
} });
|
|
2106
|
+
const notify = notifier.notify;
|
|
2107
|
+
const keyExpr = opts.keyExpr ?? "Id";
|
|
2108
|
+
const rowKeyOf = (row, i) => String(row?.[MONO_ROW_KEY] ?? row?.[keyExpr] ?? i);
|
|
2109
|
+
/**
|
|
2110
|
+
* Last-resort identity for a row we cannot place: stable per object, and never
|
|
2111
|
+
* equal to another row's. See `rowKey` for why "-1 for everyone" is not an option.
|
|
2112
|
+
*/
|
|
2113
|
+
const orphanKeys = /* @__PURE__ */ new WeakMap();
|
|
2114
|
+
let orphanSeq = 0;
|
|
2115
|
+
/**
|
|
2116
|
+
* Does the loaded row set actually carry `keyExpr`? Drives the drain + the warning.
|
|
2117
|
+
*
|
|
2118
|
+
* `ctrl.items` is not always rows. Once grouping is on it holds group NODES
|
|
2119
|
+
* (`{ key, items, count, level, path, … }`), and a node has none of the row's fields — so
|
|
2120
|
+
* sampling it would report every correctly-configured grouped table as misconfigured. Dig one
|
|
2121
|
+
* level down to the first group that actually has rows loaded instead; in server-group mode a
|
|
2122
|
+
* group's rows arrive only when it is on screen, so "no rows yet" means not-yet-known, not
|
|
2123
|
+
* missing, and takes the same benefit of the doubt as an empty ungrouped table.
|
|
2124
|
+
*/
|
|
2125
|
+
function rowsHaveKeyExpr() {
|
|
2126
|
+
/** A group node, not a row — the shape `syncGrouped` / `renderServer` put in `ctrl.items`. */
|
|
2127
|
+
const isNode = (x) => !!x && typeof x === "object" && Array.isArray(x.items) && "path" in x;
|
|
2128
|
+
/** First real row under `list`, descending as many group levels as there are. */
|
|
2129
|
+
const firstRow = (list, depth = 0) => {
|
|
2130
|
+
if (depth > 8) return void 0;
|
|
2131
|
+
for (const entry of list) {
|
|
2132
|
+
if (!isNode(entry)) return entry;
|
|
2133
|
+
const found = firstRow(entry.items ?? [], depth + 1);
|
|
2134
|
+
if (found != null) return found;
|
|
2135
|
+
}
|
|
2136
|
+
};
|
|
2137
|
+
const sample = firstRow(ctrl.items);
|
|
2138
|
+
if (sample == null || typeof sample !== "object") return true;
|
|
2139
|
+
return keyExpr in sample;
|
|
2140
|
+
}
|
|
2141
|
+
let keyExprWarned = false;
|
|
2142
|
+
/** Dev-only, once per controller: a wrong `keyExpr` otherwise fails completely silently. */
|
|
2143
|
+
function warnMissingKeyExpr() {
|
|
2144
|
+
if (keyExprWarned || ctrl.items.length === 0 || rowsHaveKeyExpr()) return;
|
|
2145
|
+
keyExprWarned = true;
|
|
2146
|
+
console.warn(`[mono-table] keyExpr "${keyExpr}" is not a property of the loaded rows${opts.keyExpr ? "" : " (nothing set one, so it defaulted to \"Id\")"}. Row keys fall back to identity, and a remote drain cannot \$select it. Set \`keyExpr\` to the row key field.`);
|
|
2147
|
+
}
|
|
2148
|
+
/**
|
|
2149
|
+
* Public stable row key — prefers the `showForm` identity, else `keyExpr`.
|
|
2150
|
+
*
|
|
2151
|
+
* `unwrapReactive` first: Vue hands components a reactive PROXY of the row while
|
|
2152
|
+
* `ctrl.items` holds the raw object, so a bare `indexOf(proxy)` is always `-1`.
|
|
2153
|
+
* With `keyExpr` also missing that made EVERY row key `"-1"` — tick one row and
|
|
2154
|
+
* the whole grid reads as ticked. A row we still cannot place gets its own
|
|
2155
|
+
* WeakMap id rather than a shared sentinel, so two rows can never collide
|
|
2156
|
+
* whatever the config. A row with a real index keeps its index-derived key, so
|
|
2157
|
+
* array grids with no `keyExpr` are untouched.
|
|
2158
|
+
*/
|
|
2159
|
+
function rowKey(row) {
|
|
2160
|
+
const raw = unwrapReactive(row);
|
|
2161
|
+
const i = ctrl.items.indexOf(raw);
|
|
2162
|
+
if (i >= 0) return rowKeyOf(raw, i);
|
|
2163
|
+
const direct = rowKeyOf(raw, NaN);
|
|
2164
|
+
if (direct !== "NaN") return direct;
|
|
2165
|
+
warnMissingKeyExpr();
|
|
2166
|
+
const obj = raw;
|
|
2167
|
+
if (obj == null || typeof obj !== "object") return String(raw);
|
|
2168
|
+
let id = orphanKeys.get(obj);
|
|
2169
|
+
if (!id) {
|
|
2170
|
+
id = `mono-orphan:${orphanSeq += 1}`;
|
|
2171
|
+
orphanKeys.set(obj, id);
|
|
2172
|
+
}
|
|
2173
|
+
return id;
|
|
2174
|
+
}
|
|
2175
|
+
/** Columns that declare a per-column `map`, resolved once per mapping pass. */
|
|
2176
|
+
function mapColumns() {
|
|
2177
|
+
const out = [];
|
|
2178
|
+
for (const c of elementProps.th ?? []) if (c?.field && typeof c.map === "function") out.push({
|
|
2179
|
+
field: c.field,
|
|
2180
|
+
map: c.map
|
|
2181
|
+
});
|
|
2182
|
+
return out;
|
|
2183
|
+
}
|
|
2184
|
+
/** True when anything at all would transform a row (lets the fast path skip). */
|
|
2185
|
+
function hasMap() {
|
|
2186
|
+
return typeof opts.map === "function" || mapColumns().length > 0;
|
|
2187
|
+
}
|
|
2188
|
+
/** One column's `map`, for the header filter's value labels. */
|
|
2189
|
+
function columnMap(field) {
|
|
2190
|
+
return mapColumns().find((c) => c.field === field)?.map;
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* Raw row → presentation row. The grid-level `map` builds the row, then each
|
|
2194
|
+
* column's `map` overwrites its own field — reading the RAW value, deliberately
|
|
2195
|
+
* not the grid map's output, so a column map behaves identically here and in the
|
|
2196
|
+
* header-filter list (which can only hand it a raw distinct value).
|
|
2197
|
+
*
|
|
2198
|
+
* Returns the SAME object when nothing is configured, so no-map tables allocate
|
|
2199
|
+
* nothing and `mapped === items` holds by reference.
|
|
2200
|
+
*/
|
|
2201
|
+
function mapRow(row, index, cols = mapColumns()) {
|
|
2202
|
+
const rowMap = typeof opts.map === "function" ? opts.map : null;
|
|
2203
|
+
if (!rowMap && cols.length === 0) return row;
|
|
2204
|
+
let out = rowMap ? rowMap(row, index) : { ...row };
|
|
2205
|
+
if (cols.length && out && typeof out === "object") for (const c of cols) out[c.field] = c.map(row?.[c.field], row, index);
|
|
2206
|
+
return out;
|
|
2207
|
+
}
|
|
2208
|
+
/** Recompute `ctrl.mapped` from `ctrl.items`. Cheap no-op without a map. */
|
|
2209
|
+
function syncMapped() {
|
|
2210
|
+
if (!hasMap()) {
|
|
2211
|
+
ctrl.mapped = ctrl.items;
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
const cols = mapColumns();
|
|
2215
|
+
ctrl.mapped = ctrl.items.map((row, i) => mapRow(row, i, cols));
|
|
2216
|
+
}
|
|
2217
|
+
/** Flatten `ctrl.items` into the ready-to-render, keyed `displayRows` list. */
|
|
2218
|
+
function buildDisplayRows() {
|
|
2219
|
+
const out = [];
|
|
2220
|
+
const cols = mapColumns();
|
|
2221
|
+
if (!isGrouped()) {
|
|
2222
|
+
ctrl.items.forEach((row, i) => out.push({
|
|
2223
|
+
key: `r:${rowKeyOf(row, i)}`,
|
|
2224
|
+
kind: "row",
|
|
2225
|
+
level: 0,
|
|
2226
|
+
row,
|
|
2227
|
+
mapped: mapRow(row, i, cols)
|
|
2228
|
+
}));
|
|
2229
|
+
return out;
|
|
2230
|
+
}
|
|
2231
|
+
const footerOn = !pageSizeAll && (serverMode() || defaultGroupRowPageSize > 0 || groupRowPaging.size > 0);
|
|
2232
|
+
const walk = (nodes) => {
|
|
2233
|
+
for (const node of nodes) {
|
|
2234
|
+
out.push({
|
|
2235
|
+
key: `g:${node.path}`,
|
|
2236
|
+
kind: "group",
|
|
2237
|
+
level: node.level,
|
|
2238
|
+
node
|
|
2239
|
+
});
|
|
2240
|
+
if (node.collapsed) continue;
|
|
2241
|
+
const children = node.items;
|
|
2242
|
+
if (children.length > 0 && isGroupNode(children[0])) walk(children);
|
|
2243
|
+
else {
|
|
2244
|
+
children.forEach((row, i) => out.push({
|
|
2245
|
+
key: `r:${node.path}:${rowKeyOf(row, i)}`,
|
|
2246
|
+
kind: "row",
|
|
2247
|
+
level: node.level + 1,
|
|
2248
|
+
row,
|
|
2249
|
+
mapped: mapRow(row, i, cols)
|
|
2250
|
+
}));
|
|
2251
|
+
if (footerOn) out.push({
|
|
2252
|
+
key: `f:${node.path}`,
|
|
2253
|
+
kind: "footer",
|
|
2254
|
+
level: node.level,
|
|
2255
|
+
node
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
};
|
|
2260
|
+
walk(ctrl.items);
|
|
2261
|
+
return out;
|
|
2262
|
+
}
|
|
2263
|
+
/** Rebuild `displayRows` from the current `items`, then notify subscribers. */
|
|
2264
|
+
function render() {
|
|
2265
|
+
ctrl.displayRows = buildDisplayRows();
|
|
2266
|
+
notify();
|
|
2267
|
+
}
|
|
2268
|
+
/** Build the full group tree from every matched row the source holds. */
|
|
2269
|
+
function fullGroups() {
|
|
2270
|
+
return buildGroups(flattenLeaves(bound ? [...bound.items?.() ?? []] : []), groupFields, collapsed);
|
|
2271
|
+
}
|
|
2272
|
+
/** Index every group node by path (full, unsliced) for `groupPageInfo`. */
|
|
2273
|
+
function indexPaths(nodes) {
|
|
2274
|
+
for (const node of nodes) {
|
|
2275
|
+
pathIndex.set(node.path, node);
|
|
2276
|
+
if (node.items.length && isGroupNode(node.items[0])) indexPaths(node.items);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
/** Clone nodes, slicing each group's items to its registered group-page. */
|
|
2280
|
+
function toDisplay(nodes) {
|
|
2281
|
+
return nodes.map((node) => {
|
|
2282
|
+
let items = node.items.length > 0 && isGroupNode(node.items[0]) ? toDisplay(node.items) : node.items;
|
|
2283
|
+
const pg = pageSizeAll ? null : groupRowPagingFor(node.path);
|
|
2284
|
+
if (pg) {
|
|
2285
|
+
const start = pg.pageIndex * pg.pageSize;
|
|
2286
|
+
items = items.slice(start, start + pg.pageSize);
|
|
2287
|
+
}
|
|
2288
|
+
return {
|
|
2289
|
+
...node,
|
|
2290
|
+
items
|
|
2291
|
+
};
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2294
|
+
function syncGrouped() {
|
|
2295
|
+
const all = fullGroups();
|
|
2296
|
+
pathIndex.clear();
|
|
2297
|
+
indexPaths(all);
|
|
2298
|
+
const total = all.length;
|
|
2299
|
+
const size = pageSizeAll ? Math.max(1, total) : Math.max(1, groupPageSize);
|
|
2300
|
+
const pageCount = Math.max(1, Math.ceil(total / size));
|
|
2301
|
+
if (groupPageIndex > pageCount - 1) groupPageIndex = pageCount - 1;
|
|
2302
|
+
if (groupPageIndex < 0) groupPageIndex = 0;
|
|
2303
|
+
const start = groupPageIndex * size;
|
|
2304
|
+
ctrl.grouped = true;
|
|
2305
|
+
ctrl.items = toDisplay(all.slice(start, start + size));
|
|
2306
|
+
ctrl.totalCount = total;
|
|
2307
|
+
ctrl.pageSize = size;
|
|
2308
|
+
ctrl.pageSizeAll = pageSizeAll;
|
|
2309
|
+
ctrl.pageIndex = groupPageIndex;
|
|
2310
|
+
ctrl.pageCount = pageCount;
|
|
2311
|
+
render();
|
|
2312
|
+
}
|
|
2313
|
+
/** Effective row paging for a group in server mode (always has a size). */
|
|
2314
|
+
function serverRowPaging(path) {
|
|
2315
|
+
return groupRowPagingFor(path) ?? {
|
|
2316
|
+
pageIndex: 0,
|
|
2317
|
+
pageSize: defaultGroupRowPageSize || groupPageSize
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
/** Re-render from the in-memory group list + loaded row pages (no fetch). */
|
|
2321
|
+
function renderServer() {
|
|
2322
|
+
pathIndex.clear();
|
|
2323
|
+
const total = serverGroups.length;
|
|
2324
|
+
const size = pageSizeAll ? Math.max(1, total) : Math.max(1, groupPageSize);
|
|
2325
|
+
const pageCount = Math.max(1, Math.ceil(total / size));
|
|
2326
|
+
if (groupPageIndex > pageCount - 1) groupPageIndex = pageCount - 1;
|
|
2327
|
+
if (groupPageIndex < 0) groupPageIndex = 0;
|
|
2328
|
+
const start = groupPageIndex * size;
|
|
2329
|
+
const nodes = serverGroups.slice(start, start + size).map((meta) => {
|
|
2330
|
+
const path = String(meta.key);
|
|
2331
|
+
const node = {
|
|
2332
|
+
key: meta.key,
|
|
2333
|
+
items: collapsed.has(path) ? [] : groupRows.get(path) ?? [],
|
|
2334
|
+
count: meta.count,
|
|
2335
|
+
level: 0,
|
|
2336
|
+
path,
|
|
2337
|
+
collapsed: collapsed.has(path),
|
|
2338
|
+
meta
|
|
2339
|
+
};
|
|
2340
|
+
pathIndex.set(path, node);
|
|
2341
|
+
return node;
|
|
2342
|
+
});
|
|
2343
|
+
ctrl.grouped = true;
|
|
2344
|
+
ctrl.items = nodes;
|
|
2345
|
+
ctrl.totalCount = total;
|
|
2346
|
+
ctrl.pageSize = size;
|
|
2347
|
+
ctrl.pageSizeAll = pageSizeAll;
|
|
2348
|
+
ctrl.pageIndex = groupPageIndex;
|
|
2349
|
+
ctrl.pageCount = pageCount;
|
|
2350
|
+
ctrl.loading = groupRowLoading.size > 0;
|
|
2351
|
+
render();
|
|
2352
|
+
}
|
|
2353
|
+
/** Fetch a group's current page — or, in "all" mode, every row (chunked). */
|
|
2354
|
+
async function loadGroupRows(meta, pg, want) {
|
|
2355
|
+
if (!serverGroup) return;
|
|
2356
|
+
const path = String(meta.key);
|
|
2357
|
+
groupRowLoading.add(path);
|
|
2358
|
+
renderServer();
|
|
2359
|
+
try {
|
|
2360
|
+
if (pageSizeAll) {
|
|
2361
|
+
const acc = [];
|
|
2362
|
+
for (let skip = 0;; skip += chunkSize) {
|
|
2363
|
+
const chunk = await serverGroup.loadRows(meta.key, {
|
|
2364
|
+
skip,
|
|
2365
|
+
take: chunkSize,
|
|
2366
|
+
search: searchValue,
|
|
2367
|
+
...serverCtx()
|
|
2368
|
+
});
|
|
2369
|
+
acc.push(...chunk ?? []);
|
|
2370
|
+
if (!chunk || chunk.length < chunkSize || acc.length >= meta.count) break;
|
|
2371
|
+
if (skip > 1e7) break;
|
|
2372
|
+
}
|
|
2373
|
+
groupRows.set(path, acc);
|
|
2374
|
+
} else {
|
|
2375
|
+
const rows = await serverGroup.loadRows(meta.key, {
|
|
2376
|
+
skip: pg.pageIndex * pg.pageSize,
|
|
2377
|
+
take: pg.pageSize,
|
|
2378
|
+
search: searchValue,
|
|
2379
|
+
...serverCtx()
|
|
2380
|
+
});
|
|
2381
|
+
groupRows.set(path, rows ?? []);
|
|
2382
|
+
}
|
|
2383
|
+
groupRowsKey.set(path, want);
|
|
2384
|
+
} catch (err) {
|
|
2385
|
+
captureError(err, "groupRows");
|
|
2386
|
+
} finally {
|
|
2387
|
+
groupRowLoading.delete(path);
|
|
2388
|
+
renderServer();
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
/** Fetch rows for every visible, expanded group missing its current page. */
|
|
2392
|
+
async function fetchVisibleRows() {
|
|
2393
|
+
if (!serverGroup) return;
|
|
2394
|
+
const size = pageSizeAll ? Math.max(1, serverGroups.length) : Math.max(1, groupPageSize);
|
|
2395
|
+
const start = groupPageIndex * size;
|
|
2396
|
+
const visible = serverGroups.slice(start, start + size);
|
|
2397
|
+
const jobs = [];
|
|
2398
|
+
for (const meta of visible) {
|
|
2399
|
+
const path = String(meta.key);
|
|
2400
|
+
if (collapsed.has(path)) continue;
|
|
2401
|
+
const pg = serverRowPaging(path);
|
|
2402
|
+
const want = pageSizeAll ? "all" : `${pg.pageIndex}:${pg.pageSize}`;
|
|
2403
|
+
if (groupRowsKey.get(path) === want || groupRowLoading.has(path)) continue;
|
|
2404
|
+
jobs.push(loadGroupRows(meta, pg, want));
|
|
2405
|
+
}
|
|
2406
|
+
if (jobs.length) await Promise.all(jobs);
|
|
2407
|
+
}
|
|
2408
|
+
/** Reload the group list (cheap groupby), then the visible groups' rows. */
|
|
2409
|
+
async function loadGroupsServer() {
|
|
2410
|
+
if (!serverGroup) return;
|
|
2411
|
+
if (bound) applyBase(bound);
|
|
2412
|
+
const seq = ++groupSeq;
|
|
2413
|
+
ctrl.loading = true;
|
|
2414
|
+
notify();
|
|
2415
|
+
let metas = [];
|
|
2416
|
+
try {
|
|
2417
|
+
metas = await serverGroup.loadGroups({
|
|
2418
|
+
search: searchValue,
|
|
2419
|
+
...serverCtx()
|
|
2420
|
+
}) ?? [];
|
|
2421
|
+
clearError();
|
|
2422
|
+
} catch (err) {
|
|
2423
|
+
captureError(err, "group");
|
|
2424
|
+
metas = [];
|
|
2425
|
+
}
|
|
2426
|
+
if (seq !== groupSeq) return;
|
|
2427
|
+
serverGroups = metas;
|
|
2428
|
+
clearRowCaches();
|
|
2429
|
+
renderServer();
|
|
2430
|
+
await fetchVisibleRows();
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Republish `ctrl` from the bound source.
|
|
2434
|
+
*
|
|
2435
|
+
* `initial` marks the one call `bind()` makes for itself. Every OTHER call
|
|
2436
|
+
* happens because the source produced a result — a load, a search, a filter, a
|
|
2437
|
+
* sort, a page — so this is the funnel where `hasLoaded` latches, rather than
|
|
2438
|
+
* the `changed` handler: a source that resolves `load()` without emitting still
|
|
2439
|
+
* reaches here, and `bind()`'s own pass must NOT count, because reflecting
|
|
2440
|
+
* whatever a source already holds is not the same as having asked it for
|
|
2441
|
+
* anything.
|
|
2442
|
+
*/
|
|
2443
|
+
function sync(initial = false) {
|
|
2444
|
+
if (!initial) ctrl.hasLoaded = true;
|
|
2445
|
+
warnMissingKeyExpr();
|
|
2446
|
+
if (serverMode()) {
|
|
2447
|
+
renderServer();
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
if (isGrouped()) {
|
|
2451
|
+
syncGrouped();
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
if (scrollMode !== "off") {
|
|
2455
|
+
syncScroll();
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
if (pageSizeAll) {
|
|
2459
|
+
ctrl.grouped = false;
|
|
2460
|
+
ctrl.items = allRows;
|
|
2461
|
+
ctrl.pageIndex = 0;
|
|
2462
|
+
ctrl.pageSize = allRows.length || 1;
|
|
2463
|
+
ctrl.pageSizeAll = true;
|
|
2464
|
+
ctrl.totalCount = allRows.length;
|
|
2465
|
+
ctrl.pageCount = 1;
|
|
2466
|
+
render();
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
const s = bound;
|
|
2470
|
+
const rawItems = s ? [...s.items?.() ?? []] : [];
|
|
2471
|
+
const total = s?.totalCount?.();
|
|
2472
|
+
const pageSize = s?.pageSize?.() ?? rawItems.length;
|
|
2473
|
+
const pageIndex = s?.pageIndex?.() ?? 0;
|
|
2474
|
+
const hasTotal = typeof total === "number" && total >= 0;
|
|
2475
|
+
ctrl.grouped = false;
|
|
2476
|
+
ctrl.items = rawItems;
|
|
2477
|
+
ctrl.pageIndex = pageIndex;
|
|
2478
|
+
ctrl.pageSize = pageSize;
|
|
2479
|
+
ctrl.pageSizeAll = false;
|
|
2480
|
+
ctrl.totalCount = hasTotal ? total : rawItems.length;
|
|
2481
|
+
if (hasTotal && pageSize > 0) ctrl.pageCount = Math.max(1, Math.ceil(total / pageSize));
|
|
2482
|
+
else if (s?.isLastPage?.()) ctrl.pageCount = pageIndex + 1;
|
|
2483
|
+
else ctrl.pageCount = pageIndex + 2;
|
|
2484
|
+
render();
|
|
2485
|
+
}
|
|
2486
|
+
function syncLoading() {
|
|
2487
|
+
ctrl.loading = bound?.isLoading?.() ?? false;
|
|
2488
|
+
notify();
|
|
2489
|
+
}
|
|
2490
|
+
const onChanged = () => {
|
|
2491
|
+
if (suppressSync) return;
|
|
2492
|
+
sync();
|
|
2493
|
+
recomputeSummaries();
|
|
2494
|
+
};
|
|
2495
|
+
const onLoadingChanged = () => syncLoading();
|
|
2496
|
+
function detach() {
|
|
2497
|
+
if (!bound) return;
|
|
2498
|
+
bound.off("changed", onChanged);
|
|
2499
|
+
bound.off("loadingChanged", onLoadingChanged);
|
|
2500
|
+
bound.off("loadError", onLoadingChanged);
|
|
2501
|
+
bound = null;
|
|
2502
|
+
ctrl.hasLoaded = false;
|
|
2503
|
+
ctrl.error = null;
|
|
2504
|
+
}
|
|
2505
|
+
function bind(next) {
|
|
2506
|
+
const source = Array.isArray(next) ? monoArraySource(next, {
|
|
2507
|
+
pageSize: opts.pageSize,
|
|
2508
|
+
keyExpr: opts.keyExpr,
|
|
2509
|
+
searchExpr: plainSearchColumns(searchFields()),
|
|
2510
|
+
group: groupFields
|
|
2511
|
+
}) : next;
|
|
2512
|
+
if (bound === source) return;
|
|
2513
|
+
detach();
|
|
2514
|
+
bound = source;
|
|
2515
|
+
ctrl.dataSource = source;
|
|
2516
|
+
bumpSummary("data");
|
|
2517
|
+
externalBase = source?.filter?.() ?? null;
|
|
2518
|
+
lastWrittenFilter = externalBase;
|
|
2519
|
+
lastComposed = void 0;
|
|
2520
|
+
composeMemo = null;
|
|
2521
|
+
lastWrittenSort = source?.sort?.() ?? null;
|
|
2522
|
+
externalSort = normalizeSortList(lastWrittenSort);
|
|
2523
|
+
resolvedKey = null;
|
|
2524
|
+
appliedKnobs.clear();
|
|
2525
|
+
if (!source) {
|
|
2526
|
+
sync(true);
|
|
2527
|
+
return;
|
|
2528
|
+
}
|
|
2529
|
+
rebuildGridExpr();
|
|
2530
|
+
if (!opts.serverGroup && groupFields.length === 1 && typeof source.store === "function") serverGroup = storeGroupSource(source, {
|
|
2531
|
+
groupField: groupFields[0],
|
|
2532
|
+
select: opts.select,
|
|
2533
|
+
searchExpr: plainSearchColumns(searchFields()),
|
|
2534
|
+
searchOperation: opts.searchOperation,
|
|
2535
|
+
groupSummary: opts.groupSummary
|
|
2536
|
+
});
|
|
2537
|
+
source.on("changed", onChanged);
|
|
2538
|
+
source.on("loadingChanged", onLoadingChanged);
|
|
2539
|
+
source.on("loadError", onLoadingChanged);
|
|
2540
|
+
if (preferredPageSize != null) {
|
|
2541
|
+
const cur = source.pageSize?.();
|
|
2542
|
+
if (typeof cur === "number" && cur > 0) sourcePageSize = cur;
|
|
2543
|
+
source.pageSize?.(preferredPageSize);
|
|
2544
|
+
}
|
|
2545
|
+
if (groupFields.length && !serverMode()) applySourceSort();
|
|
2546
|
+
sync(true);
|
|
2547
|
+
}
|
|
2548
|
+
/**
|
|
2549
|
+
* Record a failed operation so something can render it.
|
|
2550
|
+
*
|
|
2551
|
+
* A FRESH object every time, never a reused one. `<mono-table-error>` dismisses
|
|
2552
|
+
* per failure rather than per message — it remembers the object it dismissed
|
|
2553
|
+
* and re-shows when a different one arrives — so two identical failures have to
|
|
2554
|
+
* be distinguishable, and object identity is the only thing that reliably is
|
|
2555
|
+
* (a store rejecting with the plain string `'Network error'` gives `===` values
|
|
2556
|
+
* for two entirely unrelated attempts).
|
|
2557
|
+
*/
|
|
2558
|
+
function captureError(raw, source) {
|
|
2559
|
+
const { message, status, detail } = describeError(raw, opts.errorMessages);
|
|
2560
|
+
ctrl.error = {
|
|
2561
|
+
raw,
|
|
2562
|
+
message,
|
|
2563
|
+
status,
|
|
2564
|
+
detail,
|
|
2565
|
+
source,
|
|
2566
|
+
at: Date.now()
|
|
2567
|
+
};
|
|
2568
|
+
if (errorMode === "clear-list" && source !== "selectAll") {
|
|
2569
|
+
clear();
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
notify();
|
|
2573
|
+
}
|
|
2574
|
+
/**
|
|
2575
|
+
* Drop a recorded error because an operation just succeeded.
|
|
2576
|
+
*
|
|
2577
|
+
* Called at each operation's own success point rather than from the render
|
|
2578
|
+
* funnels (`sync` / `renderServer`), and that is not a stylistic choice:
|
|
2579
|
+
* `loadGroupsServer` calls `renderServer()` immediately AFTER its catch, so a
|
|
2580
|
+
* clear there would wipe the error it had just captured, every time.
|
|
2581
|
+
*
|
|
2582
|
+
* Guarded, so a table that has never failed does not publish a notify on every
|
|
2583
|
+
* successful load for a field nobody changed.
|
|
2584
|
+
*/
|
|
2585
|
+
function clearError() {
|
|
2586
|
+
if (!ctrl.error) return;
|
|
2587
|
+
ctrl.error = null;
|
|
2588
|
+
notify();
|
|
2589
|
+
}
|
|
2590
|
+
async function runLoad(fn, source = "load") {
|
|
2591
|
+
if (!bound) return;
|
|
2592
|
+
try {
|
|
2593
|
+
await Promise.resolve(fn());
|
|
2594
|
+
clearError();
|
|
2595
|
+
} catch (err) {
|
|
2596
|
+
if (isCanceled(err)) return;
|
|
2597
|
+
captureError(err, source);
|
|
2598
|
+
throw err;
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
/**
|
|
2602
|
+
* Whether the source pages. Client group mode pages the groups in memory, so
|
|
2603
|
+
* it loads everything; otherwise `dataSourceOptions.paginate` decides, and the
|
|
2604
|
+
* default is to page — a flat table always did.
|
|
2605
|
+
*/
|
|
2606
|
+
function sourcePaginate() {
|
|
2607
|
+
if (isGrouped()) return false;
|
|
2608
|
+
return resolvedOpts.paginate ?? true;
|
|
2609
|
+
}
|
|
2610
|
+
async function load() {
|
|
2611
|
+
const s = bound;
|
|
2612
|
+
if (s) applyBase(s);
|
|
2613
|
+
if (serverMode()) {
|
|
2614
|
+
await loadGroupsServer();
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
if (scrollActive()) {
|
|
2618
|
+
await loadScrollPage(true);
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
if (pageSizeAll && !isGrouped()) {
|
|
2622
|
+
await loadAllChunked();
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
if (!s) return;
|
|
2626
|
+
s.paginate?.(sourcePaginate());
|
|
2627
|
+
await runLoad(() => s.load());
|
|
2628
|
+
}
|
|
2629
|
+
async function reload() {
|
|
2630
|
+
bumpSummary("data");
|
|
2631
|
+
const s = bound;
|
|
2632
|
+
if (s) applyBase(s);
|
|
2633
|
+
if (serverMode()) {
|
|
2634
|
+
clearRowCaches();
|
|
2635
|
+
await loadGroupsServer();
|
|
2636
|
+
return;
|
|
2637
|
+
}
|
|
2638
|
+
if (scrollActive()) {
|
|
2639
|
+
await loadScrollPage(true);
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
if (pageSizeAll && !isGrouped()) {
|
|
2643
|
+
await loadAllChunked();
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
if (!s) return;
|
|
2647
|
+
s.paginate?.(sourcePaginate());
|
|
2648
|
+
await runLoad(() => s.reload ? s.reload() : s.load(), "reload");
|
|
2649
|
+
}
|
|
2650
|
+
function clear() {
|
|
2651
|
+
scrollToken++;
|
|
2652
|
+
suppressSync = false;
|
|
2653
|
+
accumulated = [];
|
|
2654
|
+
allRows = [];
|
|
2655
|
+
scrollNextPage = 0;
|
|
2656
|
+
scrollHasMore = false;
|
|
2657
|
+
vStart = vEnd = vPadTop = vPadBottom = 0;
|
|
2658
|
+
serverGroups = [];
|
|
2659
|
+
clearRowCaches();
|
|
2660
|
+
ctrl.items = [];
|
|
2661
|
+
ctrl.loading = false;
|
|
2662
|
+
ctrl.totalCount = 0;
|
|
2663
|
+
ctrl.pageIndex = 0;
|
|
2664
|
+
ctrl.pageCount = 1;
|
|
2665
|
+
ctrl.loadedCount = 0;
|
|
2666
|
+
ctrl.hasMore = false;
|
|
2667
|
+
ctrl.virtualStart = ctrl.virtualEnd = 0;
|
|
2668
|
+
ctrl.virtualPadTop = ctrl.virtualPadBottom = 0;
|
|
2669
|
+
render();
|
|
2670
|
+
}
|
|
2671
|
+
async function setPage(pageIndex) {
|
|
2672
|
+
if (isGrouped()) {
|
|
2673
|
+
groupPageIndex = Math.max(0, pageIndex);
|
|
2674
|
+
sync();
|
|
2675
|
+
if (serverMode()) await fetchVisibleRows();
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
const s = bound;
|
|
2679
|
+
if (!s) return;
|
|
2680
|
+
s.pageIndex?.(Math.max(0, pageIndex));
|
|
2681
|
+
await load();
|
|
2682
|
+
}
|
|
2683
|
+
async function setPageSize(pageSize) {
|
|
2684
|
+
const all = pageSize === "all";
|
|
2685
|
+
pageSizeAll = all;
|
|
2686
|
+
if (isGrouped()) {
|
|
2687
|
+
if (!all) groupPageSize = Math.max(1, pageSize);
|
|
2688
|
+
groupPageIndex = 0;
|
|
2689
|
+
sync();
|
|
2690
|
+
if (serverMode()) await fetchVisibleRows();
|
|
2691
|
+
return;
|
|
2692
|
+
}
|
|
2693
|
+
const s = bound;
|
|
2694
|
+
if (!s) return;
|
|
2695
|
+
if (all) {
|
|
2696
|
+
await loadAllChunked();
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
s.pageSize?.(pageSize);
|
|
2700
|
+
s.pageIndex?.(0);
|
|
2701
|
+
await load();
|
|
2702
|
+
}
|
|
2703
|
+
/**
|
|
2704
|
+
* Ungrouped "all": pull every row, in `chunkSize` requests, accumulate, and
|
|
2705
|
+
* show on one page. Works with a capped backend (e.g. 100 rows/request).
|
|
2706
|
+
*/
|
|
2707
|
+
async function loadAllChunked() {
|
|
2708
|
+
const s = bound;
|
|
2709
|
+
if (!s) {
|
|
2710
|
+
allRows = [];
|
|
2711
|
+
sync();
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2714
|
+
suppressSync = true;
|
|
2715
|
+
ctrl.loading = true;
|
|
2716
|
+
notify();
|
|
2717
|
+
const acc = [];
|
|
2718
|
+
try {
|
|
2719
|
+
applyBase(s);
|
|
2720
|
+
s.paginate?.(true);
|
|
2721
|
+
s.pageSize?.(chunkSize);
|
|
2722
|
+
let page = 0;
|
|
2723
|
+
for (;;) {
|
|
2724
|
+
s.pageIndex?.(page);
|
|
2725
|
+
await runLoad(() => s.load());
|
|
2726
|
+
const items = [...s.items?.() ?? []];
|
|
2727
|
+
acc.push(...items);
|
|
2728
|
+
const total = s.totalCount?.();
|
|
2729
|
+
const reachedTotal = typeof total === "number" && total >= 0 && acc.length >= total;
|
|
2730
|
+
if ((s.isLastPage?.() ?? items.length < chunkSize) || reachedTotal || items.length === 0) break;
|
|
2731
|
+
page += 1;
|
|
2732
|
+
if (page > 1e5) break;
|
|
2733
|
+
}
|
|
2734
|
+
} finally {
|
|
2735
|
+
suppressSync = false;
|
|
2736
|
+
}
|
|
2737
|
+
allRows = acc;
|
|
2738
|
+
ctrl.loading = bound?.isLoading?.() ?? false;
|
|
2739
|
+
sync();
|
|
2740
|
+
}
|
|
2741
|
+
/** Scroll modes only apply to a flat (ungrouped, non-server) grid. */
|
|
2742
|
+
function scrollActive() {
|
|
2743
|
+
return scrollMode !== "off" && !isGrouped() && !serverMode();
|
|
2744
|
+
}
|
|
2745
|
+
/** Map the accumulator (+ virtual window) onto `ctrl` and render. */
|
|
2746
|
+
function syncScroll() {
|
|
2747
|
+
const s = bound;
|
|
2748
|
+
const total = s?.totalCount?.();
|
|
2749
|
+
const hasTotal = typeof total === "number" && total >= 0;
|
|
2750
|
+
ctrl.grouped = false;
|
|
2751
|
+
ctrl.pageSizeAll = false;
|
|
2752
|
+
ctrl.scrollMode = scrollMode;
|
|
2753
|
+
ctrl.loading = s?.isLoading?.() ?? false;
|
|
2754
|
+
ctrl.loadedCount = accumulated.length;
|
|
2755
|
+
ctrl.hasMore = scrollHasMore;
|
|
2756
|
+
ctrl.totalCount = hasTotal ? total : accumulated.length;
|
|
2757
|
+
ctrl.pageIndex = 0;
|
|
2758
|
+
ctrl.pageSize = accumulated.length || 1;
|
|
2759
|
+
ctrl.pageCount = 1;
|
|
2760
|
+
if (scrollMode === "virtual") {
|
|
2761
|
+
const end = Math.min(Math.max(vStart, vEnd), accumulated.length);
|
|
2762
|
+
const start = Math.min(Math.max(0, vStart), end);
|
|
2763
|
+
ctrl.items = accumulated.slice(start, end);
|
|
2764
|
+
ctrl.virtualStart = start;
|
|
2765
|
+
ctrl.virtualEnd = end;
|
|
2766
|
+
ctrl.virtualPadTop = Math.max(0, vPadTop);
|
|
2767
|
+
ctrl.virtualPadBottom = Math.max(0, vPadBottom);
|
|
2768
|
+
} else {
|
|
2769
|
+
ctrl.items = accumulated;
|
|
2770
|
+
ctrl.virtualStart = 0;
|
|
2771
|
+
ctrl.virtualEnd = accumulated.length;
|
|
2772
|
+
ctrl.virtualPadTop = 0;
|
|
2773
|
+
ctrl.virtualPadBottom = 0;
|
|
2774
|
+
}
|
|
2775
|
+
render();
|
|
2776
|
+
}
|
|
2777
|
+
/**
|
|
2778
|
+
* Load one source page and append it to the accumulator. `reset` clears the
|
|
2779
|
+
* accumulator and starts from page 0 (a fresh query / mode enable).
|
|
2780
|
+
*
|
|
2781
|
+
* Concurrency: every entry claims a `scrollToken`. Only the holder of the newest one may
|
|
2782
|
+
* append, advance the page counter or release the shared load flags -- a reset (filter change,
|
|
2783
|
+
* search, rebind) routinely lands while a `loadNext()` is still awaiting its page, and
|
|
2784
|
+
* `runLoad` swallows the cancellation that would otherwise stop the loser.
|
|
2785
|
+
*/
|
|
2786
|
+
async function loadScrollPage(reset) {
|
|
2787
|
+
const s = bound;
|
|
2788
|
+
if (!s) {
|
|
2789
|
+
accumulated = [];
|
|
2790
|
+
scrollNextPage = 0;
|
|
2791
|
+
scrollHasMore = false;
|
|
2792
|
+
syncScroll();
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
const token = ++scrollToken;
|
|
2796
|
+
const mine = () => token === scrollToken;
|
|
2797
|
+
if (reset) {
|
|
2798
|
+
applyBase(s);
|
|
2799
|
+
accumulated = [];
|
|
2800
|
+
scrollNextPage = 0;
|
|
2801
|
+
vStart = vEnd = vPadTop = vPadBottom = 0;
|
|
2802
|
+
ctrl.items = [];
|
|
2803
|
+
ctrl.loadedCount = 0;
|
|
2804
|
+
ctrl.hasMore = false;
|
|
2805
|
+
ctrl.virtualStart = ctrl.virtualEnd = 0;
|
|
2806
|
+
ctrl.virtualPadTop = ctrl.virtualPadBottom = 0;
|
|
2807
|
+
}
|
|
2808
|
+
suppressSync = true;
|
|
2809
|
+
ctrl.loading = true;
|
|
2810
|
+
notify();
|
|
2811
|
+
try {
|
|
2812
|
+
s.paginate?.(true);
|
|
2813
|
+
const page = scrollNextPage;
|
|
2814
|
+
s.pageIndex?.(page);
|
|
2815
|
+
await runLoad(() => s.load());
|
|
2816
|
+
if (!mine()) return;
|
|
2817
|
+
const rows = [...s.items?.() ?? []];
|
|
2818
|
+
accumulated.push(...rows);
|
|
2819
|
+
const total = s.totalCount?.();
|
|
2820
|
+
const size = s.pageSize?.() ?? rows.length;
|
|
2821
|
+
const reachedTotal = typeof total === "number" && total >= 0 && accumulated.length >= total;
|
|
2822
|
+
const isLast = s.isLastPage?.() ?? rows.length < (size || 1);
|
|
2823
|
+
scrollHasMore = !(reachedTotal || isLast || rows.length === 0);
|
|
2824
|
+
scrollNextPage = page + 1;
|
|
2825
|
+
} finally {
|
|
2826
|
+
if (mine()) suppressSync = false;
|
|
2827
|
+
}
|
|
2828
|
+
if (!mine()) return;
|
|
2829
|
+
ctrl.loading = bound?.isLoading?.() ?? false;
|
|
2830
|
+
syncScroll();
|
|
2831
|
+
}
|
|
2832
|
+
async function setScrollPaging(mode, opts = {}) {
|
|
2833
|
+
const reload = opts.reload !== false;
|
|
2834
|
+
if (mode !== "off" && (isGrouped() || serverMode())) {
|
|
2835
|
+
if (typeof console !== "undefined") console.warn(`[mono-table] scroll paging ("${mode}") requires a flat (ungrouped) table; keeping standard paging.`);
|
|
2836
|
+
if (scrollMode !== "off") {
|
|
2837
|
+
scrollMode = "off";
|
|
2838
|
+
ctrl.scrollMode = "off";
|
|
2839
|
+
}
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
if (scrollMode === mode) return;
|
|
2843
|
+
scrollMode = mode;
|
|
2844
|
+
ctrl.scrollMode = mode;
|
|
2845
|
+
if (mode === "off") {
|
|
2846
|
+
if (!reload) {
|
|
2847
|
+
syncScroll();
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
accumulated = [];
|
|
2851
|
+
await load();
|
|
2852
|
+
return;
|
|
2853
|
+
}
|
|
2854
|
+
if (!bound) {
|
|
2855
|
+
syncScroll();
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
2858
|
+
if (accumulated.length) {
|
|
2859
|
+
syncScroll();
|
|
2860
|
+
return;
|
|
2861
|
+
}
|
|
2862
|
+
await loadScrollPage(true);
|
|
2863
|
+
}
|
|
2864
|
+
async function loadNext() {
|
|
2865
|
+
if (!scrollActive() || !scrollHasMore || scrollLoading) return;
|
|
2866
|
+
scrollLoading = true;
|
|
2867
|
+
try {
|
|
2868
|
+
await loadScrollPage(false);
|
|
2869
|
+
} finally {
|
|
2870
|
+
scrollLoading = false;
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
function setVirtualWindow(start, end, padTop, padBottom) {
|
|
2874
|
+
if (scrollMode !== "virtual") return;
|
|
2875
|
+
const ns = Math.max(0, Math.floor(start));
|
|
2876
|
+
const ne = Math.max(ns, Math.floor(end));
|
|
2877
|
+
const nt = Math.max(0, Math.round(padTop));
|
|
2878
|
+
const nb = Math.max(0, Math.round(padBottom));
|
|
2879
|
+
if (ns === vStart && ne === vEnd && nt === vPadTop && nb === vPadBottom) return;
|
|
2880
|
+
vStart = ns;
|
|
2881
|
+
vEnd = ne;
|
|
2882
|
+
vPadTop = nt;
|
|
2883
|
+
vPadBottom = nb;
|
|
2884
|
+
syncScroll();
|
|
2885
|
+
}
|
|
2886
|
+
async function setPreferredPageSize(pageSize) {
|
|
2887
|
+
const v = typeof pageSize === "number" && pageSize > 0 ? Math.floor(pageSize) : null;
|
|
2888
|
+
if (v === preferredPageSize) return;
|
|
2889
|
+
const wasOverriding = preferredPageSize != null;
|
|
2890
|
+
preferredPageSize = v;
|
|
2891
|
+
const s = bound;
|
|
2892
|
+
if (!s) return;
|
|
2893
|
+
if (v != null) {
|
|
2894
|
+
if (!wasOverriding) {
|
|
2895
|
+
const cur = s.pageSize?.();
|
|
2896
|
+
sourcePageSize = typeof cur === "number" && cur > 0 ? cur : null;
|
|
2897
|
+
}
|
|
2898
|
+
s.pageSize?.(v);
|
|
2899
|
+
} else {
|
|
2900
|
+
const base = resolvedOpts.pageSize;
|
|
2901
|
+
if (typeof base === "number" && base > 0) s.pageSize?.(base);
|
|
2902
|
+
else if (sourcePageSize != null) s.pageSize?.(sourcePageSize);
|
|
2903
|
+
sourcePageSize = null;
|
|
2904
|
+
}
|
|
2905
|
+
s.pageIndex?.(0);
|
|
2906
|
+
await load();
|
|
2907
|
+
}
|
|
2908
|
+
/**
|
|
2909
|
+
* Rows a `*` pattern resolves against. The full backing array when the source
|
|
2910
|
+
* has one (an in-memory source exposes `data()`), else the loaded page — which
|
|
2911
|
+
* is all a remote source can offer, and is enough to read the shape.
|
|
2912
|
+
*/
|
|
2913
|
+
function searchSampleRows() {
|
|
2914
|
+
return bound?.data?.() ?? ctrl.items ?? [];
|
|
2915
|
+
}
|
|
2916
|
+
/**
|
|
2917
|
+
* searchExpr as a list of entries — plain column names, `{ field, custom }` and
|
|
2918
|
+
* `*` patterns resolved against the data. `textOnly` for a remote source, whose
|
|
2919
|
+
* `$filter` can only `contains` a string column.
|
|
2920
|
+
*/
|
|
2921
|
+
function searchEntries() {
|
|
2922
|
+
return resolveSearchEntries(effectiveSearchFields(), {
|
|
2923
|
+
rows: searchSampleRows(),
|
|
2924
|
+
textOnly: hasStore()
|
|
2925
|
+
});
|
|
2926
|
+
}
|
|
2927
|
+
/**
|
|
2928
|
+
* The entries an unbound term fans out over, or the single entry a
|
|
2929
|
+
* column-bound term targets. A bound field with no `searchExpr` entry keeps the
|
|
2930
|
+
* old behaviour and is treated as a plain column.
|
|
2931
|
+
*/
|
|
2932
|
+
function entriesForTerm(field, all) {
|
|
2933
|
+
if (!field) return all;
|
|
2934
|
+
return [searchEntryFor(searchFields(), field) ?? field];
|
|
2935
|
+
}
|
|
2936
|
+
/** Whether a remote search must bypass devextreme folding (a path/wildcard column). */
|
|
2937
|
+
function searchIsPath() {
|
|
2938
|
+
return hasStore() && plainSearchColumns(searchFields()).some(isPath);
|
|
2939
|
+
}
|
|
2940
|
+
/** Current live search string, held when the controller owns the search filter (path mode). */
|
|
2941
|
+
let searchTerm = null;
|
|
2942
|
+
/**
|
|
2943
|
+
* Remote OData search filter expr, built by the controller when a searchExpr
|
|
2944
|
+
* column is a path/wildcard (devextreme's own search folding can't emit `Job/Name`
|
|
2945
|
+
* navigation or `Nav/any(...)` lambdas). Returns `null` when folding handles it.
|
|
2946
|
+
*/
|
|
2947
|
+
/** Join expressions with `and` / `or`, returning the lone one unwrapped. */
|
|
2948
|
+
function joinExprs(parts, join) {
|
|
2949
|
+
const kept = parts.filter((p) => p != null);
|
|
2950
|
+
if (!kept.length) return null;
|
|
2951
|
+
if (kept.length === 1) return kept[0];
|
|
2952
|
+
const out = [];
|
|
2953
|
+
kept.forEach((p, i) => {
|
|
2954
|
+
if (i) out.push(join);
|
|
2955
|
+
out.push(p);
|
|
2956
|
+
});
|
|
2957
|
+
return out;
|
|
2958
|
+
}
|
|
2959
|
+
/**
|
|
2960
|
+
* Group the live terms by `field` — a term with no field covers every
|
|
2961
|
+
* `searchExpr` column. Preserves first-seen order so the emitted filter is
|
|
2962
|
+
* stable and diffable.
|
|
2963
|
+
*/
|
|
2964
|
+
function groupedSearchTerms() {
|
|
2965
|
+
const order = [];
|
|
2966
|
+
const byField = /* @__PURE__ */ new Map();
|
|
2967
|
+
for (const t of ctrl.searchTerms) {
|
|
2968
|
+
const v = String(t?.value ?? "");
|
|
2969
|
+
if (!v) continue;
|
|
2970
|
+
const key = t.field || void 0;
|
|
2971
|
+
if (!byField.has(key)) {
|
|
2972
|
+
byField.set(key, []);
|
|
2973
|
+
order.push(key);
|
|
2974
|
+
}
|
|
2975
|
+
byField.get(key).push(v);
|
|
2976
|
+
}
|
|
2977
|
+
return order.map((field) => ({
|
|
2978
|
+
field,
|
|
2979
|
+
values: byField.get(field)
|
|
2980
|
+
}));
|
|
2981
|
+
}
|
|
2982
|
+
/**
|
|
2983
|
+
* Remote OData search filter built by the controller rather than devextreme's
|
|
2984
|
+
* own `searchValue` folding. Used when folding CAN'T express the search:
|
|
2985
|
+
* - a `searchExpr` column is a path/wildcard (`Job/Name`, `Nav/any(...)`), or
|
|
2986
|
+
* - there is more than one term, or any term is bound to a single column —
|
|
2987
|
+
* devextreme's `searchValue` holds exactly one string across one column set.
|
|
2988
|
+
*
|
|
2989
|
+
* Shape: OR within a field, AND across fields.
|
|
2990
|
+
*/
|
|
2991
|
+
function remoteSearchExpr() {
|
|
2992
|
+
const groups = groupedSearchTerms();
|
|
2993
|
+
if (!groups.length || !controllerOwnsSearch()) return null;
|
|
2994
|
+
const op = opts.searchOperation ?? "contains";
|
|
2995
|
+
const all = searchEntries();
|
|
2996
|
+
return joinExprs(groups.map((g) => {
|
|
2997
|
+
const entries = entriesForTerm(g.field, all);
|
|
2998
|
+
const clauses = [];
|
|
2999
|
+
for (const value of g.values) {
|
|
3000
|
+
const expr = joinExprs(entries.map((e) => typeof e === "string" ? toODataClause(e, op, value) : customRemoteClause(e, value, op)).filter((c) => c != null), "or");
|
|
3001
|
+
if (expr != null) clauses.push(expr);
|
|
3002
|
+
}
|
|
3003
|
+
return joinExprs(clauses, "or");
|
|
3004
|
+
}), "and");
|
|
3005
|
+
}
|
|
3006
|
+
/**
|
|
3007
|
+
* Whether the controller (not devextreme) must build the search filter. One
|
|
3008
|
+
* plain unbound term over plain columns can still ride devextreme's folding.
|
|
3009
|
+
*/
|
|
3010
|
+
function controllerOwnsSearch() {
|
|
3011
|
+
const groups = groupedSearchTerms();
|
|
3012
|
+
if (!groups.length) return false;
|
|
3013
|
+
if (!hasStore()) return false;
|
|
3014
|
+
if (searchIsPath()) return true;
|
|
3015
|
+
if (hasCustomSearch(effectiveSearchFields()) || hasWildcardSearch(effectiveSearchFields())) return true;
|
|
3016
|
+
return !(groups.length === 1 && groups[0].values.length === 1 && !groups[0].field);
|
|
3017
|
+
}
|
|
3018
|
+
/** Client-side equivalent: OR within a field, AND across fields. */
|
|
3019
|
+
function searchPredicate() {
|
|
3020
|
+
const groups = groupedSearchTerms();
|
|
3021
|
+
if (!groups.length) return null;
|
|
3022
|
+
const op = opts.searchOperation ?? "contains";
|
|
3023
|
+
const matches = (row, col, needle) => {
|
|
3024
|
+
const raw = isPath(col) ? getFieldValue(row, col) : row?.[col];
|
|
3025
|
+
return (Array.isArray(raw) ? raw : [raw]).some((v) => {
|
|
3026
|
+
const s = String(v ?? "").toLowerCase();
|
|
3027
|
+
const n = needle.toLowerCase();
|
|
3028
|
+
if (op === "startswith") return s.startsWith(n);
|
|
3029
|
+
if (op === "endswith") return s.endsWith(n);
|
|
3030
|
+
if (op === "=" || op === "equals") return s === n;
|
|
3031
|
+
return s.includes(n);
|
|
3032
|
+
});
|
|
3033
|
+
};
|
|
3034
|
+
const all = searchEntries();
|
|
3035
|
+
const perGroup = groups.map((g) => entriesForTerm(g.field, all));
|
|
3036
|
+
return (row) => groups.every((g, i) => {
|
|
3037
|
+
const entries = perGroup[i];
|
|
3038
|
+
if (!entries.length) return true;
|
|
3039
|
+
return g.values.some((needle) => entries.some((e) => {
|
|
3040
|
+
if (typeof e === "string") return matches(row, e, needle);
|
|
3041
|
+
const pred = customPredicate(e, needle, op);
|
|
3042
|
+
return pred ? pred(row) : false;
|
|
3043
|
+
}));
|
|
3044
|
+
});
|
|
3045
|
+
}
|
|
3046
|
+
/**
|
|
3047
|
+
* Whether an ARRAY source needs the controller's predicate. One plain unbound
|
|
3048
|
+
* term can stay on the source's own `searchValue`/`searchExpr` — which matters
|
|
3049
|
+
* when `searchExpr` was configured on `monoArraySource` and not on the grid.
|
|
3050
|
+
*/
|
|
3051
|
+
function controllerOwnsArraySearch() {
|
|
3052
|
+
const groups = groupedSearchTerms();
|
|
3053
|
+
if (!groups.length) return false;
|
|
3054
|
+
if (hasCustomSearch(effectiveSearchFields()) || hasWildcardSearch(effectiveSearchFields())) return true;
|
|
3055
|
+
if (groups.length === 1 && groups[0].values.length === 1 && !groups[0].field) return false;
|
|
3056
|
+
return true;
|
|
3057
|
+
}
|
|
3058
|
+
/** Single-term shorthand — the classic search box. */
|
|
3059
|
+
async function setSearch(value) {
|
|
3060
|
+
await setSearchTerms(value ? [{ value }] : []);
|
|
3061
|
+
}
|
|
3062
|
+
/**
|
|
3063
|
+
* Replace the searched fields. `undefined`/`null` drops the override so the
|
|
3064
|
+
* `searchExpr` / `search-value` option takes over again.
|
|
3065
|
+
*
|
|
3066
|
+
* Only re-runs the query when a search is actually live — changing the fields
|
|
3067
|
+
* with an empty box has nothing to re-filter, and a needless `load()` would
|
|
3068
|
+
* make `<mono-table-search>` refetch on every connect.
|
|
3069
|
+
*/
|
|
3070
|
+
async function setSearchExpr(expr) {
|
|
3071
|
+
searchFieldsOverride = expr == null ? void 0 : mergeSearchFields({ searchExpr: expr });
|
|
3072
|
+
if (!ctrl.searchTerms.length) {
|
|
3073
|
+
notify();
|
|
3074
|
+
return;
|
|
3075
|
+
}
|
|
3076
|
+
await setSearchTerms(ctrl.searchTerms);
|
|
3077
|
+
}
|
|
3078
|
+
async function setSearchTerms(terms) {
|
|
3079
|
+
ctrl.searchTerms = (terms ?? []).filter((t) => t && String(t.value ?? "") !== "");
|
|
3080
|
+
groupPageIndex = 0;
|
|
3081
|
+
bumpSummary("search");
|
|
3082
|
+
if (serverMode()) {
|
|
3083
|
+
searchValue = ctrl.searchTerms.map((t) => t.value).join(" ") || null;
|
|
3084
|
+
notify();
|
|
3085
|
+
await loadGroupsServer();
|
|
3086
|
+
return;
|
|
3087
|
+
}
|
|
3088
|
+
const s = bound;
|
|
3089
|
+
if (!s) return;
|
|
3090
|
+
searchTerm = ctrl.searchTerms[0]?.value ?? null;
|
|
3091
|
+
if (!hasStore()) {
|
|
3092
|
+
if (controllerOwnsArraySearch()) s.searchValue?.(null);
|
|
3093
|
+
else {
|
|
3094
|
+
s.searchOperation?.(opts.searchOperation ?? "contains");
|
|
3095
|
+
if (searchFieldsConfigured()) s.searchExpr?.(plainSearchColumns(searchFields()));
|
|
3096
|
+
s.searchValue?.(searchTerm);
|
|
3097
|
+
}
|
|
3098
|
+
applyFilter();
|
|
3099
|
+
} else if (controllerOwnsSearch()) {
|
|
3100
|
+
s.searchValue?.(null);
|
|
3101
|
+
applyFilter();
|
|
3102
|
+
} else {
|
|
3103
|
+
s.searchOperation?.(opts.searchOperation ?? "contains");
|
|
3104
|
+
if (searchFieldsConfigured()) s.searchExpr?.(plainSearchColumns(searchFields()));
|
|
3105
|
+
s.searchValue?.(searchTerm);
|
|
3106
|
+
applyFilter();
|
|
3107
|
+
}
|
|
3108
|
+
s.pageIndex?.(0);
|
|
3109
|
+
notify();
|
|
3110
|
+
await load();
|
|
3111
|
+
}
|
|
3112
|
+
/**
|
|
3113
|
+
* The grid's explicit filter — ONE layer, not a replacement.
|
|
3114
|
+
*
|
|
3115
|
+
* This is what the slotted filter builder writes, and it used to be a bare
|
|
3116
|
+
* `s.filter(x)`: it replaced the consumer's own source filter, the column
|
|
3117
|
+
* filters and the search in one go, and the next keystroke then replaced IT
|
|
3118
|
+
* with the search alone. It is now composed like everything else — under the
|
|
3119
|
+
* base, beside the columns and the search — and `null` clears only this layer.
|
|
3120
|
+
* That is also why it is NOT the adopted external base: the builder's *Clear*
|
|
3121
|
+
* would otherwise wipe the scope the consumer set on the source.
|
|
3122
|
+
*/
|
|
3123
|
+
async function setFilter(filter) {
|
|
3124
|
+
if (!bound) return;
|
|
3125
|
+
bumpSummary("search");
|
|
3126
|
+
explicitFilter = hasStore() ? filter ?? null : compileOrPredicate(filter);
|
|
3127
|
+
applyFilter();
|
|
3128
|
+
groupPageIndex = 0;
|
|
3129
|
+
await load();
|
|
3130
|
+
}
|
|
3131
|
+
const columnFilters = /* @__PURE__ */ new Map();
|
|
3132
|
+
/** The combine gesture, remembered — see `MonoSetColumnFilterOptions.sticky`. */
|
|
3133
|
+
let filterCombining = false;
|
|
3134
|
+
/** Whether the bound source is a remote DataSource (has a devextreme store). */
|
|
3135
|
+
function hasStore() {
|
|
3136
|
+
return typeof bound?.store === "function" && !!bound.store();
|
|
3137
|
+
}
|
|
3138
|
+
/**
|
|
3139
|
+
* OR-join a column's value clauses into a devextreme filter expression (path-aware).
|
|
3140
|
+
*
|
|
3141
|
+
* A value is normally a plain equality; a `{ from, to }` range (what the DATE
|
|
3142
|
+
* filter stores) becomes `[[f,'>=',from],'and',[f,'<',to]]` — half-open, so
|
|
3143
|
+
* a "March" range never touches April 1st 00:00:00.
|
|
3144
|
+
*/
|
|
3145
|
+
function orClauses(field, values) {
|
|
3146
|
+
const clauses = values.map((v) => {
|
|
3147
|
+
if (isDateRange(v)) {
|
|
3148
|
+
const ge = toODataClause(field, ">=", v.from);
|
|
3149
|
+
const lt = toODataClause(field, "<", v.to);
|
|
3150
|
+
return ge && lt ? [
|
|
3151
|
+
ge,
|
|
3152
|
+
"and",
|
|
3153
|
+
lt
|
|
3154
|
+
] : null;
|
|
3155
|
+
}
|
|
3156
|
+
return toODataClause(field, "=", v);
|
|
3157
|
+
}).filter((c) => c != null);
|
|
3158
|
+
if (!clauses.length) return null;
|
|
3159
|
+
if (clauses.length === 1) return clauses[0];
|
|
3160
|
+
const out = [];
|
|
3161
|
+
clauses.forEach((c, i) => {
|
|
3162
|
+
if (i) out.push("or");
|
|
3163
|
+
out.push(c);
|
|
3164
|
+
});
|
|
3165
|
+
return out;
|
|
3166
|
+
}
|
|
3167
|
+
/**
|
|
3168
|
+
* Rebuild the grid's OWN filter layers from the column filters and the
|
|
3169
|
+
* controller-owned search — `columnExpr` (columns only, what a server-group
|
|
3170
|
+
* source gets) and `gridExpr` (columns AND search, what the source gets).
|
|
3171
|
+
*
|
|
3172
|
+
* Only builds; it writes nothing. Until this was split out, the write was here
|
|
3173
|
+
* too, and it wrote THIS expression alone — so a filter the consumer had put on
|
|
3174
|
+
* the source was gone on the first search keystroke and "restored" to nothing
|
|
3175
|
+
* on clear. The write now goes through `writeFilter()`, which composes these on
|
|
3176
|
+
* top of the base.
|
|
3177
|
+
*/
|
|
3178
|
+
/** The active column filters, optionally leaving one column out (its own panel). */
|
|
3179
|
+
function activeColumnFilters(except) {
|
|
3180
|
+
return [...columnFilters.entries()].filter(([f, v]) => v.length && f !== except);
|
|
3181
|
+
}
|
|
3182
|
+
/** Remote: the column filters as ONE devextreme expression — AND across columns, OR within. */
|
|
3183
|
+
function columnFilterExpr(except) {
|
|
3184
|
+
const cols = activeColumnFilters(except).map(([f, v]) => orClauses(f, v)).filter((c) => c != null);
|
|
3185
|
+
if (!cols.length) return null;
|
|
3186
|
+
if (cols.length === 1) return cols[0];
|
|
3187
|
+
const out = [];
|
|
3188
|
+
cols.forEach((c, i) => {
|
|
3189
|
+
if (i) out.push("and");
|
|
3190
|
+
out.push(c);
|
|
3191
|
+
});
|
|
3192
|
+
return out;
|
|
3193
|
+
}
|
|
3194
|
+
/**
|
|
3195
|
+
* Array / client: the same as a predicate. Path fields resolve nested/wildcard
|
|
3196
|
+
* values (wildcard = match any element).
|
|
3197
|
+
*/
|
|
3198
|
+
function columnFilterPredicate(except) {
|
|
3199
|
+
const active = activeColumnFilters(except);
|
|
3200
|
+
if (!active.length) return null;
|
|
3201
|
+
return (row) => active.every(([f, vals]) => {
|
|
3202
|
+
const raw = isPath(f) ? getFieldValue(row, f) : row?.[f];
|
|
3203
|
+
return (Array.isArray(raw) ? raw : [raw]).some((rv) => vals.some((v) => {
|
|
3204
|
+
if (isDateRange(v)) {
|
|
3205
|
+
const d = parseDateValue(rv);
|
|
3206
|
+
if (!d) return false;
|
|
3207
|
+
return inRange(d, {
|
|
3208
|
+
from: v.from instanceof Date ? v.from : new Date(v.from),
|
|
3209
|
+
to: v.to instanceof Date ? v.to : new Date(v.to)
|
|
3210
|
+
});
|
|
3211
|
+
}
|
|
3212
|
+
return String(rv) === String(v);
|
|
3213
|
+
}));
|
|
3214
|
+
});
|
|
3215
|
+
}
|
|
3216
|
+
function rebuildGridExpr() {
|
|
3217
|
+
if (hasStore()) {
|
|
3218
|
+
const expr = columnFilterExpr();
|
|
3219
|
+
columnExpr = expr;
|
|
3220
|
+
gridExpr = andFilters(expr, remoteSearchExpr());
|
|
3221
|
+
} else {
|
|
3222
|
+
const colPred = columnFilterPredicate();
|
|
3223
|
+
const sPred = controllerOwnsArraySearch() ? searchPredicate() : null;
|
|
3224
|
+
columnExpr = colPred;
|
|
3225
|
+
gridExpr = andPredicates(colPred, sPred);
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
/**
|
|
3229
|
+
* A grid-driven filter change: search, column filter, `setFilter`. Adopts
|
|
3230
|
+
* anything the consumer changed on the source in the meantime, rebuilds the
|
|
3231
|
+
* grid's layers, writes the composed filter and goes back to page 0.
|
|
3232
|
+
*/
|
|
3233
|
+
function applyFilter() {
|
|
3234
|
+
const s = bound;
|
|
3235
|
+
if (!s) return;
|
|
3236
|
+
syncExternalFilter(s);
|
|
3237
|
+
rebuildGridExpr();
|
|
3238
|
+
writeFilter(s);
|
|
3239
|
+
s.pageIndex?.(0);
|
|
3240
|
+
}
|
|
3241
|
+
async function setColumnFilter(field, values, options) {
|
|
3242
|
+
if (!field) return;
|
|
3243
|
+
const multi = options?.multi === true;
|
|
3244
|
+
if (!multi) columnFilters.clear();
|
|
3245
|
+
if (!values || !values.length) columnFilters.delete(field);
|
|
3246
|
+
else columnFilters.set(field, [...values]);
|
|
3247
|
+
filterCombining = columnFilters.size > 0 && multi && (options?.sticky === true || filterCombining);
|
|
3248
|
+
groupPageIndex = 0;
|
|
3249
|
+
bumpSummary("search");
|
|
3250
|
+
notify();
|
|
3251
|
+
applyFilter();
|
|
3252
|
+
await load();
|
|
3253
|
+
}
|
|
3254
|
+
function columnFilter(field) {
|
|
3255
|
+
return [...columnFilters.get(field) ?? []];
|
|
3256
|
+
}
|
|
3257
|
+
function columnFilterCombining() {
|
|
3258
|
+
return filterCombining;
|
|
3259
|
+
}
|
|
3260
|
+
function filteredColumns() {
|
|
3261
|
+
return [...columnFilters.entries()].filter(([, v]) => v.length).map(([f]) => f);
|
|
3262
|
+
}
|
|
3263
|
+
let dsOptionsSource = opts.dataSourceOptions;
|
|
3264
|
+
let odataOptionsSource = opts.odataOptions;
|
|
3265
|
+
/** The merged, normalised options as of the last resolve. */
|
|
3266
|
+
let resolvedOpts = {};
|
|
3267
|
+
/** Change key of `resolvedOpts` — `null` until the first resolve, which always counts. */
|
|
3268
|
+
let resolvedKey = null;
|
|
3269
|
+
/** A function-valued `filter` cannot go through the key; compared by reference instead. */
|
|
3270
|
+
let optionsFilterRef = void 0;
|
|
3271
|
+
let optionsFilter = null;
|
|
3272
|
+
let optionsSort = [];
|
|
3273
|
+
/** Knobs `applyBaseOptions` has written, so a knob that DISAPPEARS is cleared, not left behind. */
|
|
3274
|
+
const appliedKnobs = /* @__PURE__ */ new Set();
|
|
3275
|
+
let externalBase = null;
|
|
3276
|
+
let explicitFilter = null;
|
|
3277
|
+
let columnExpr = null;
|
|
3278
|
+
let gridExpr = null;
|
|
3279
|
+
/** What the source HELD after the grid's last write (read back — a source may normalise). */
|
|
3280
|
+
let lastWrittenFilter = null;
|
|
3281
|
+
/** What the grid last composed and handed to the source. */
|
|
3282
|
+
let lastComposed = void 0;
|
|
3283
|
+
let composeMemo = null;
|
|
3284
|
+
let externalSort = [];
|
|
3285
|
+
let lastWrittenSort = null;
|
|
3286
|
+
let rawFilterWarned = false;
|
|
3287
|
+
/** Array-source filter layer: a predicate as given, an array compiled, a raw OData string dropped. */
|
|
3288
|
+
function compileOrPredicate(filter) {
|
|
3289
|
+
if (filter == null) return null;
|
|
3290
|
+
if (typeof filter === "function") return filter;
|
|
3291
|
+
if (Array.isArray(filter) && filter.length === 1 && typeof filter[0] === "string") {
|
|
3292
|
+
if (!rawFilterWarned) {
|
|
3293
|
+
rawFilterWarned = true;
|
|
3294
|
+
console.warn("[monoDataGrid] a raw OData $filter string cannot filter an array source — ignored.");
|
|
3295
|
+
}
|
|
3296
|
+
return null;
|
|
3297
|
+
}
|
|
3298
|
+
return compileFilterPredicate(filter);
|
|
3299
|
+
}
|
|
3300
|
+
/**
|
|
3301
|
+
* Re-read the two option bags. Returns whether anything changed since the last
|
|
3302
|
+
* resolve — the getters hand back a fresh object every time, so the comparison
|
|
3303
|
+
* is by content, not identity.
|
|
3304
|
+
*/
|
|
3305
|
+
function resolveDsOptions() {
|
|
3306
|
+
const merged = resolveDataSourceOptions(dsOptionsSource, odataOptionsSource);
|
|
3307
|
+
const fnFilter = typeof merged.filter === "function" ? merged.filter : void 0;
|
|
3308
|
+
const key = dataSourceOptionsKey(merged);
|
|
3309
|
+
if (key === resolvedKey && fnFilter === optionsFilterRef) return false;
|
|
3310
|
+
resolvedKey = key;
|
|
3311
|
+
optionsFilterRef = fnFilter;
|
|
3312
|
+
resolvedOpts = merged;
|
|
3313
|
+
optionsFilter = hasStore() ? merged.filter ?? null : compileOrPredicate(merged.filter);
|
|
3314
|
+
optionsSort = normalizeSortList(merged.sort);
|
|
3315
|
+
return true;
|
|
3316
|
+
}
|
|
3317
|
+
/**
|
|
3318
|
+
* Adopt a filter the consumer set on the source directly: anything the source
|
|
3319
|
+
* holds that is not what the grid last put there. One comparison is enough
|
|
3320
|
+
* because `writeFilter` records what the source HOLDS after a write, not what
|
|
3321
|
+
* was passed — so a source that ignores writes (a getter-only fake) reads back
|
|
3322
|
+
* as its own last write and is never adopted, and a source that normalises
|
|
3323
|
+
* compares against its normalised value.
|
|
3324
|
+
*/
|
|
3325
|
+
function syncExternalFilter(s) {
|
|
3326
|
+
if (typeof s.filter !== "function") return false;
|
|
3327
|
+
const live = s.filter() ?? null;
|
|
3328
|
+
if (live === lastWrittenFilter) return false;
|
|
3329
|
+
externalBase = live;
|
|
3330
|
+
return true;
|
|
3331
|
+
}
|
|
3332
|
+
/** The same for `ds.sort(...)`. */
|
|
3333
|
+
function syncExternalSort(s) {
|
|
3334
|
+
if (typeof s.sort !== "function") return false;
|
|
3335
|
+
const live = s.sort() ?? null;
|
|
3336
|
+
if (live === lastWrittenSort) return false;
|
|
3337
|
+
lastWrittenSort = live;
|
|
3338
|
+
externalSort = normalizeSortList(live);
|
|
3339
|
+
return true;
|
|
3340
|
+
}
|
|
3341
|
+
/** options ∧ external ∧ explicit — everything under the grid's own layers. */
|
|
3342
|
+
function baseFilter() {
|
|
3343
|
+
return hasStore() ? andFilters(andFilters(optionsFilter, externalBase), explicitFilter) : andPredicates(optionsFilter, externalBase, explicitFilter);
|
|
3344
|
+
}
|
|
3345
|
+
/** What a server-group source gets: the base plus the column filters, NOT the search (it folds that itself). */
|
|
3346
|
+
function baseFilterWithColumns() {
|
|
3347
|
+
return hasStore() ? andFilters(baseFilter(), columnExpr) : andPredicates(baseFilter(), columnExpr);
|
|
3348
|
+
}
|
|
3349
|
+
function composeFilter() {
|
|
3350
|
+
const m = composeMemo;
|
|
3351
|
+
if (m && m.o === optionsFilter && m.e === externalBase && m.x === explicitFilter && m.g === gridExpr) return m.out;
|
|
3352
|
+
const out = hasStore() ? andFilters(baseFilter(), gridExpr) : andPredicates(baseFilter(), gridExpr);
|
|
3353
|
+
composeMemo = {
|
|
3354
|
+
o: optionsFilter,
|
|
3355
|
+
e: externalBase,
|
|
3356
|
+
x: explicitFilter,
|
|
3357
|
+
g: gridExpr,
|
|
3358
|
+
out
|
|
3359
|
+
};
|
|
3360
|
+
return out;
|
|
3361
|
+
}
|
|
3362
|
+
/** Write the composed filter — only when it is not what the source already holds. */
|
|
3363
|
+
function writeFilter(s) {
|
|
3364
|
+
if (typeof s.filter !== "function") return false;
|
|
3365
|
+
const next = composeFilter();
|
|
3366
|
+
if (next === lastComposed && (s.filter() ?? null) === lastWrittenFilter) return false;
|
|
3367
|
+
s.filter(next);
|
|
3368
|
+
lastComposed = next;
|
|
3369
|
+
lastWrittenFilter = s.filter() ?? null;
|
|
3370
|
+
return true;
|
|
3371
|
+
}
|
|
3372
|
+
/**
|
|
3373
|
+
* The sort actually sent: the user's column sort first (it wins), then the
|
|
3374
|
+
* options' sort, then whatever the consumer set on the source — later entries
|
|
3375
|
+
* only for selectors not already sorted, so they act as tiebreakers. Clearing
|
|
3376
|
+
* the column sort therefore falls back to the base instead of to nothing.
|
|
3377
|
+
*/
|
|
3378
|
+
function mergedSort() {
|
|
3379
|
+
let keys = effectiveSort() ?? [];
|
|
3380
|
+
if (hasStore()) keys = keys.map((s) => {
|
|
3381
|
+
if (!isPath(s.selector)) return s;
|
|
3382
|
+
const selector = toODataSelector(s.selector);
|
|
3383
|
+
if (selector) return {
|
|
3384
|
+
...s,
|
|
3385
|
+
selector
|
|
3386
|
+
};
|
|
3387
|
+
console.warn(`[monoDataGrid] sort: path "${s.selector}" is not an OData selector — skipped.`);
|
|
3388
|
+
return null;
|
|
3389
|
+
}).filter((s) => !!s);
|
|
3390
|
+
const out = [...keys];
|
|
3391
|
+
for (const e of [...optionsSort, ...externalSort]) if (!out.some((k) => k.selector === e.selector)) out.push({
|
|
3392
|
+
selector: e.selector,
|
|
3393
|
+
desc: !!e.desc
|
|
3394
|
+
});
|
|
3395
|
+
return out.length ? out : null;
|
|
3396
|
+
}
|
|
3397
|
+
/** Push the non-filter knobs of `resolvedOpts` onto the source. Only called when they changed. */
|
|
3398
|
+
function applyBaseOptions(s) {
|
|
3399
|
+
const o = resolvedOpts;
|
|
3400
|
+
const knob = (name, present, write) => {
|
|
3401
|
+
if (present) {
|
|
3402
|
+
write(false);
|
|
3403
|
+
appliedKnobs.add(name);
|
|
3404
|
+
} else if (appliedKnobs.has(name)) {
|
|
3405
|
+
write(true);
|
|
3406
|
+
appliedKnobs.delete(name);
|
|
3407
|
+
}
|
|
3408
|
+
};
|
|
3409
|
+
knob("select", o.select !== void 0, (clear) => s.select?.(clear ? null : toList(o.select) ?? null));
|
|
3410
|
+
knob("requireTotalCount", typeof o.requireTotalCount === "boolean", (clear) => s.requireTotalCount?.(clear ? false : !!o.requireTotalCount));
|
|
3411
|
+
if (!searchFieldsConfigured()) {
|
|
3412
|
+
knob("searchExpr", o.searchExpr !== void 0, (clear) => s.searchExpr?.(clear ? null : o.searchExpr));
|
|
3413
|
+
knob("searchOperation", typeof o.searchOperation === "string", (clear) => s.searchOperation?.(clear ? "contains" : String(o.searchOperation)));
|
|
3414
|
+
}
|
|
3415
|
+
if (preferredPageSize == null && typeof o.pageSize === "number" && o.pageSize > 0) s.pageSize?.(o.pageSize);
|
|
3416
|
+
const lo = typeof s.loadOptions === "function" ? s.loadOptions() : null;
|
|
3417
|
+
if (lo && typeof lo === "object") {
|
|
3418
|
+
const extras = extraLoadOptions(o);
|
|
3419
|
+
for (const k of Object.keys(extras)) knob(`lo:${k}`, extras[k] !== void 0, (clear) => {
|
|
3420
|
+
if (clear) delete lo[k];
|
|
3421
|
+
else lo[k] = extras[k];
|
|
3422
|
+
});
|
|
3423
|
+
for (const name of [...appliedKnobs]) if (name.startsWith("lo:") && !(name.slice(3) in extras)) {
|
|
3424
|
+
delete lo[name.slice(3)];
|
|
3425
|
+
appliedKnobs.delete(name);
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
if (optionsSort.length || appliedKnobs.has("sort")) {
|
|
3429
|
+
applySourceSort();
|
|
3430
|
+
if (optionsSort.length) appliedKnobs.add("sort");
|
|
3431
|
+
else appliedKnobs.delete("sort");
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
/**
|
|
3435
|
+
* The one call every load funnel makes first: re-read the options, adopt what
|
|
3436
|
+
* the consumer changed on the source, push the knobs, write the composed
|
|
3437
|
+
* filter. Never loads — the funnel does that — and writes nothing when nothing
|
|
3438
|
+
* moved, so a plain `setPage(n)` costs no filter write and keeps its page.
|
|
3439
|
+
*/
|
|
3440
|
+
function applyBase(s) {
|
|
3441
|
+
const optChanged = resolveDsOptions();
|
|
3442
|
+
const extChanged = syncExternalFilter(s);
|
|
3443
|
+
const sortAdopted = syncExternalSort(s);
|
|
3444
|
+
if (optChanged) applyBaseOptions(s);
|
|
3445
|
+
else if (sortAdopted && (ctrl.sorts.length || optionsSort.length)) applySourceSort();
|
|
3446
|
+
if (optChanged || extChanged) {
|
|
3447
|
+
bumpSummary("search");
|
|
3448
|
+
groupPageIndex = 0;
|
|
3449
|
+
}
|
|
3450
|
+
writeFilter(s);
|
|
3451
|
+
return optChanged || extChanged;
|
|
3452
|
+
}
|
|
3453
|
+
/** The `MonoServerGroupCtx` for the current query. */
|
|
3454
|
+
function serverCtx() {
|
|
3455
|
+
const loadOptions = {};
|
|
3456
|
+
if (resolvedOpts.expand !== void 0) loadOptions.expand = resolvedOpts.expand;
|
|
3457
|
+
if (resolvedOpts.customQueryParams !== void 0) loadOptions.customQueryParams = resolvedOpts.customQueryParams;
|
|
3458
|
+
const filter = baseFilterWithColumns();
|
|
3459
|
+
const select = opts.select ?? toList(resolvedOpts.select);
|
|
3460
|
+
return {
|
|
3461
|
+
sort: mergedSort(),
|
|
3462
|
+
...filter != null ? { filter } : {},
|
|
3463
|
+
...select ? { select } : {},
|
|
3464
|
+
...Object.keys(loadOptions).length ? { loadOptions } : {}
|
|
3465
|
+
};
|
|
3466
|
+
}
|
|
3467
|
+
async function setDataSourceOptions(next) {
|
|
3468
|
+
dsOptionsSource = next;
|
|
3469
|
+
await refresh();
|
|
3470
|
+
}
|
|
3471
|
+
async function setOdataOptions(next) {
|
|
3472
|
+
odataOptionsSource = next;
|
|
3473
|
+
await refresh();
|
|
3474
|
+
}
|
|
3475
|
+
async function refresh() {
|
|
3476
|
+
const s = bound;
|
|
3477
|
+
if (!s) return;
|
|
3478
|
+
if (applyBase(s)) s.pageIndex?.(0);
|
|
3479
|
+
await load();
|
|
3480
|
+
}
|
|
3481
|
+
function resolvedDataSourceOptions() {
|
|
3482
|
+
return resolvedOpts;
|
|
3483
|
+
}
|
|
3484
|
+
/** The panel's order: nulls first, numeric before lexical. Shared by both distinct paths. */
|
|
3485
|
+
function sortColumnValues(values) {
|
|
3486
|
+
return values.sort((a, b) => {
|
|
3487
|
+
const av = a.value, bv = b.value;
|
|
3488
|
+
if (av == null) return bv == null ? 0 : -1;
|
|
3489
|
+
if (bv == null) return 1;
|
|
3490
|
+
const an = Number(av), bn = Number(bv);
|
|
3491
|
+
if (!Number.isNaN(an) && !Number.isNaN(bn)) return an - bn;
|
|
3492
|
+
return String(av).localeCompare(String(bv));
|
|
3493
|
+
});
|
|
3494
|
+
}
|
|
3495
|
+
/**
|
|
3496
|
+
* Distinct values as ONE `$apply=groupby((field),aggregate($count as count))`
|
|
3497
|
+
* request through the store, or `null` when that is not on.
|
|
3498
|
+
*
|
|
3499
|
+
* `null` — not `[]` — is the fallback signal, and it comes from four places:
|
|
3500
|
+
* the path switched off (`serverApply: false`, or a backend that already
|
|
3501
|
+
* rejected an `$apply` — the gate remembers), a wildcard path (there is no
|
|
3502
|
+
* selector to group by), a store that cannot carry an `$apply` at all (a
|
|
3503
|
+
* CustomStore — `loadApply` sends nothing) or a request that failed, and a
|
|
3504
|
+
* response that is not grouped (a server that ignored the clause hands back
|
|
3505
|
+
* plain entities, which have no `count`). An EMPTY grouped result is a real
|
|
3506
|
+
* answer (nothing in scope) and is returned as such.
|
|
3507
|
+
*
|
|
3508
|
+
* The request carries `$apply` and nothing else: `$select` beside `$apply` is
|
|
3509
|
+
* rejected by most servers, a sibling `$filter` would be applied a second time
|
|
3510
|
+
* (the base folds INSIDE the clause), and `$top` only if the column asked for
|
|
3511
|
+
* a cap. `loadApply` puts the clause INTO the url (`urlOverride` —
|
|
3512
|
+
* `customQueryParams` is quoted as a literal by devextreme and becomes a
|
|
3513
|
+
* function call on v4), so this rides the store's URL, `beforeSend` and auth
|
|
3514
|
+
* exactly as every other request does.
|
|
3515
|
+
*/
|
|
3516
|
+
async function distinctViaApply(store, field, options) {
|
|
3517
|
+
if (applyGate.skip) return null;
|
|
3518
|
+
if (parseFieldPath(field).hasWildcard) return null;
|
|
3519
|
+
const selector = isPath(field) ? toODataSelector(field) : field;
|
|
3520
|
+
if (!selector) return null;
|
|
3521
|
+
const apply = `groupby((${selector}),aggregate($count as count))`;
|
|
3522
|
+
const odataFilter = options?.filter != null ? arrayToODataString(options.filter) : "";
|
|
3523
|
+
const clause = odataFilter ? `filter(${odataFilter})/${apply}` : apply;
|
|
3524
|
+
const top = typeof options?.take === "number" && options.take >= 0 ? options.take : void 0;
|
|
3525
|
+
let rows;
|
|
3526
|
+
try {
|
|
3527
|
+
rows = await loadApply(store, clause, { top });
|
|
3528
|
+
} catch (err) {
|
|
3529
|
+
applyGate.reject(err);
|
|
3530
|
+
return null;
|
|
3531
|
+
}
|
|
3532
|
+
if (!rows) return null;
|
|
3533
|
+
if (!rows.length) return [];
|
|
3534
|
+
const first = rows[0];
|
|
3535
|
+
if (!first || typeof first !== "object" || typeof first.count !== "number") return null;
|
|
3536
|
+
if (!isRolledUp(rows, [field, "count"])) return null;
|
|
3537
|
+
const path = isPath(field);
|
|
3538
|
+
return sortColumnValues(rows.map((r) => ({
|
|
3539
|
+
value: path ? getFieldValue(r, field) : r[field],
|
|
3540
|
+
count: Number(r.count) || 0
|
|
3541
|
+
})));
|
|
3542
|
+
}
|
|
3543
|
+
/** Count occurrences of `field` across `rows` into a sorted `{value,count}[]`. */
|
|
3544
|
+
function distinctFromRows(rows, field) {
|
|
3545
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3546
|
+
const bump = (v) => {
|
|
3547
|
+
counts.set(v, (counts.get(v) ?? 0) + 1);
|
|
3548
|
+
};
|
|
3549
|
+
const path = isPath(field);
|
|
3550
|
+
for (const r of rows) if (path) {
|
|
3551
|
+
const v = getFieldValue(r, field);
|
|
3552
|
+
if (Array.isArray(v)) v.forEach(bump);
|
|
3553
|
+
else bump(v);
|
|
3554
|
+
} else bump(r?.[field]);
|
|
3555
|
+
return sortColumnValues([...counts.entries()].map(([value, count]) => ({
|
|
3556
|
+
value,
|
|
3557
|
+
count
|
|
3558
|
+
})));
|
|
3559
|
+
}
|
|
3560
|
+
async function distinctValues(field, options) {
|
|
3561
|
+
if (!field) return [];
|
|
3562
|
+
resolveDsOptions();
|
|
3563
|
+
if (bound) syncExternalFilter(bound);
|
|
3564
|
+
const base = baseFilter();
|
|
3565
|
+
const cascade = opts.headerFilterCascade !== false;
|
|
3566
|
+
let filter;
|
|
3567
|
+
if (hasStore()) {
|
|
3568
|
+
filter = andFilters(base, options?.filter ?? null);
|
|
3569
|
+
if (cascade) {
|
|
3570
|
+
const search = remoteSearchExpr() ?? searchFilterOf(bound) ?? null;
|
|
3571
|
+
filter = andFilters(filter, andFilters(columnFilterExpr(field), search));
|
|
3572
|
+
}
|
|
3573
|
+
} else {
|
|
3574
|
+
filter = andPredicates(base, typeof options?.filter === "function" ? options.filter : null);
|
|
3575
|
+
if (cascade) filter = andPredicates(andPredicates(filter, columnFilterPredicate(field)), searchPredicate());
|
|
3576
|
+
}
|
|
3577
|
+
const scoped = filter == null ? options : {
|
|
3578
|
+
...options ?? {},
|
|
3579
|
+
filter
|
|
3580
|
+
};
|
|
3581
|
+
if (typeof opts.distinctValues === "function") {
|
|
3582
|
+
const filter = hasStore() ? scoped?.filter ?? null : null;
|
|
3583
|
+
const odataFilter = filter != null ? arrayToODataString(filter) : "";
|
|
3584
|
+
const apply = `groupby((${isPath(field) ? toODataSelector(field) ?? field : field}),aggregate($count as count))`;
|
|
3585
|
+
const ctx = {
|
|
3586
|
+
filter,
|
|
3587
|
+
odataFilter,
|
|
3588
|
+
apply,
|
|
3589
|
+
applyWithFilter: odataFilter ? `filter(${odataFilter})/${apply}` : apply,
|
|
3590
|
+
source: bound
|
|
3591
|
+
};
|
|
3592
|
+
return (await opts.distinctValues(field, scoped, ctx) ?? []).map((r) => {
|
|
3593
|
+
const o = r;
|
|
3594
|
+
return o && typeof o === "object" && "value" in o ? {
|
|
3595
|
+
value: o.value,
|
|
3596
|
+
count: Number(o.count ?? 0) || 0
|
|
3597
|
+
} : {
|
|
3598
|
+
value: r,
|
|
3599
|
+
count: 0
|
|
3600
|
+
};
|
|
3601
|
+
});
|
|
3602
|
+
}
|
|
3603
|
+
const s = bound;
|
|
3604
|
+
if (!hasStore()) {
|
|
3605
|
+
let rows = s?.data?.() ?? ctrl.items;
|
|
3606
|
+
if (typeof scoped?.filter === "function") rows = rows.filter(scoped.filter);
|
|
3607
|
+
if (typeof scoped?.take === "number" && scoped.take >= 0) rows = rows.slice(0, scoped.take);
|
|
3608
|
+
return distinctFromRows(rows, field);
|
|
3609
|
+
}
|
|
3610
|
+
const store = s.store();
|
|
3611
|
+
const grouped = await distinctViaApply(store, field, scoped);
|
|
3612
|
+
if (grouped) return grouped;
|
|
3613
|
+
const loadOpts = {};
|
|
3614
|
+
if (!isPath(field)) loadOpts.select = [field];
|
|
3615
|
+
for (const [key, value] of Object.entries(scoped ?? {})) if (value == null) delete loadOpts[key];
|
|
3616
|
+
else loadOpts[key] = value;
|
|
3617
|
+
return distinctFromRows(storeRows(await store.load(loadOpts)), field);
|
|
3618
|
+
}
|
|
3619
|
+
async function setData(next) {
|
|
3620
|
+
const s = bound;
|
|
3621
|
+
if (typeof s?.setData !== "function") return;
|
|
3622
|
+
bumpSummary("data");
|
|
3623
|
+
await runLoad(() => s.setData(next));
|
|
3624
|
+
if (scrollActive()) await loadScrollPage(true);
|
|
3625
|
+
}
|
|
3626
|
+
/** The combine gesture, remembered — see `MonoSetSortOptions.sticky`. */
|
|
3627
|
+
let sortCombineMode = false;
|
|
3628
|
+
function sortCombining() {
|
|
3629
|
+
return sortCombineMode;
|
|
3630
|
+
}
|
|
3631
|
+
/** This column's direction, or null when it isn't part of the sort. */
|
|
3632
|
+
function sortOf(field) {
|
|
3633
|
+
return ctrl.sorts.find((s) => s.field === field)?.order ?? null;
|
|
3634
|
+
}
|
|
3635
|
+
/** 1-based precedence in `sorts`; `0` when the column isn't sorted. */
|
|
3636
|
+
function sortIndex(field) {
|
|
3637
|
+
return ctrl.sorts.findIndex((s) => s.field === field) + 1;
|
|
3638
|
+
}
|
|
3639
|
+
/**
|
|
3640
|
+
* The 1-based precedence a column DECLARED via `sort.index`, if any. Central
|
|
3641
|
+
* `props.th` config wins over the element's own `:sort.prop`, matching how every
|
|
3642
|
+
* other key resolves.
|
|
3643
|
+
*/
|
|
3644
|
+
function declaredSortIndex(field) {
|
|
3645
|
+
const fromProps = (elementProps.th ?? []).find((c) => c?.field === field)?.sort;
|
|
3646
|
+
if (typeof fromProps === "object" && typeof fromProps?.index === "number") return fromProps.index;
|
|
3647
|
+
for (const el of columnEls) if (el.field === field && typeof el.sort === "object" && typeof el.sort?.index === "number") return el.sort.index;
|
|
3648
|
+
}
|
|
3649
|
+
/**
|
|
3650
|
+
* Re-order `sorts` so any column declaring `sort.index` sits at that slot.
|
|
3651
|
+
* Unindexed columns keep their click order and fill the remaining slots — the
|
|
3652
|
+
* comparator falls back to the current position, so the sort is stable.
|
|
3653
|
+
* No-ops (and allocates nothing) when no column declares an index.
|
|
3654
|
+
*/
|
|
3655
|
+
function applyDeclaredSortOrder() {
|
|
3656
|
+
if (ctrl.sorts.length < 2) return;
|
|
3657
|
+
const marked = ctrl.sorts.map((s, i) => ({
|
|
3658
|
+
s,
|
|
3659
|
+
i,
|
|
3660
|
+
hint: declaredSortIndex(s.field)
|
|
3661
|
+
}));
|
|
3662
|
+
if (!marked.some((m) => typeof m.hint === "number")) return;
|
|
3663
|
+
marked.sort((a, b) => {
|
|
3664
|
+
const ah = a.hint ?? Number.POSITIVE_INFINITY;
|
|
3665
|
+
const bh = b.hint ?? Number.POSITIVE_INFINITY;
|
|
3666
|
+
return ah === bh ? a.i - b.i : ah - bh;
|
|
3667
|
+
});
|
|
3668
|
+
ctrl.sorts = marked.map((m) => m.s);
|
|
3669
|
+
}
|
|
3670
|
+
/** Keep the single-value mirrors pointing at the primary key. */
|
|
3671
|
+
function syncPrimarySort() {
|
|
3672
|
+
ctrl.sortField = ctrl.sorts[0]?.field ?? null;
|
|
3673
|
+
ctrl.sortOrder = ctrl.sorts[0]?.order ?? null;
|
|
3674
|
+
}
|
|
3675
|
+
function clearSort() {
|
|
3676
|
+
return setSort(null);
|
|
3677
|
+
}
|
|
3678
|
+
async function setSort(field, order, options) {
|
|
3679
|
+
if (!field) {
|
|
3680
|
+
ctrl.sorts = [];
|
|
3681
|
+
sortCombineMode = false;
|
|
3682
|
+
} else {
|
|
3683
|
+
const multi = options?.multi === true;
|
|
3684
|
+
const current = sortOf(field);
|
|
3685
|
+
const next = order !== void 0 ? order : current === "asc" ? "desc" : current === "desc" ? null : "asc";
|
|
3686
|
+
if (!next) ctrl.sorts = multi ? ctrl.sorts.filter((s) => s.field !== field) : [];
|
|
3687
|
+
else if (multi) ctrl.sorts = ctrl.sorts.findIndex((s) => s.field === field) >= 0 ? ctrl.sorts.map((s) => s.field === field ? {
|
|
3688
|
+
field,
|
|
3689
|
+
order: next
|
|
3690
|
+
} : s) : [...ctrl.sorts, {
|
|
3691
|
+
field,
|
|
3692
|
+
order: next
|
|
3693
|
+
}];
|
|
3694
|
+
else ctrl.sorts = [{
|
|
3695
|
+
field,
|
|
3696
|
+
order: next
|
|
3697
|
+
}];
|
|
3698
|
+
sortCombineMode = ctrl.sorts.length > 0 && multi && (options?.sticky === true || sortCombineMode);
|
|
3699
|
+
}
|
|
3700
|
+
applyDeclaredSortOrder();
|
|
3701
|
+
syncPrimarySort();
|
|
3702
|
+
notify();
|
|
3703
|
+
groupPageIndex = 0;
|
|
3704
|
+
if (serverMode()) {
|
|
3705
|
+
await loadGroupsServer();
|
|
3706
|
+
return;
|
|
3707
|
+
}
|
|
3708
|
+
const s = bound;
|
|
3709
|
+
if (!s) return;
|
|
3710
|
+
applySourceSort();
|
|
3711
|
+
s.pageIndex?.(0);
|
|
3712
|
+
await load();
|
|
3713
|
+
}
|
|
3714
|
+
async function setGroup(group) {
|
|
3715
|
+
groupFields = toFields(group);
|
|
3716
|
+
ctrl.grouped = groupFields.length > 0;
|
|
3717
|
+
collapsed.clear();
|
|
3718
|
+
groupRowPaging.clear();
|
|
3719
|
+
groupPageIndex = 0;
|
|
3720
|
+
if (serverMode()) {
|
|
3721
|
+
clearRowCaches();
|
|
3722
|
+
await loadGroupsServer();
|
|
3723
|
+
return;
|
|
3724
|
+
}
|
|
3725
|
+
const s = bound;
|
|
3726
|
+
if (!s) {
|
|
3727
|
+
sync();
|
|
3728
|
+
return;
|
|
3729
|
+
}
|
|
3730
|
+
applySourceSort();
|
|
3731
|
+
s.pageIndex?.(0);
|
|
3732
|
+
await load();
|
|
3733
|
+
}
|
|
3734
|
+
function toggleGroup(target) {
|
|
3735
|
+
const path = typeof target === "string" ? target : target?.path;
|
|
3736
|
+
if (!path) return;
|
|
3737
|
+
if (collapsed.has(path)) collapsed.delete(path);
|
|
3738
|
+
else collapsed.add(path);
|
|
3739
|
+
sync();
|
|
3740
|
+
if (serverMode()) fetchVisibleRows();
|
|
3741
|
+
}
|
|
3742
|
+
function isGroupCollapsed(path) {
|
|
3743
|
+
return collapsed.has(path);
|
|
3744
|
+
}
|
|
3745
|
+
function expandAllGroups() {
|
|
3746
|
+
if (!collapsed.size) return;
|
|
3747
|
+
collapsed.clear();
|
|
3748
|
+
sync();
|
|
3749
|
+
if (serverMode()) fetchVisibleRows();
|
|
3750
|
+
}
|
|
3751
|
+
function collapseAllGroups() {
|
|
3752
|
+
if (!groupFields.length) return;
|
|
3753
|
+
if (serverMode()) {
|
|
3754
|
+
for (const meta of serverGroups) collapsed.add(String(meta.key));
|
|
3755
|
+
sync();
|
|
3756
|
+
return;
|
|
3757
|
+
}
|
|
3758
|
+
const tree = buildGroups(flattenLeaves(bound ? [...bound.items?.() ?? []] : []), groupFields);
|
|
3759
|
+
for (const path of collectGroupPaths(tree)) collapsed.add(path);
|
|
3760
|
+
sync();
|
|
3761
|
+
}
|
|
3762
|
+
function setGroupPageSize(path, pageSize) {
|
|
3763
|
+
if (!path) return;
|
|
3764
|
+
const size = Math.max(1, Math.floor(pageSize) || 1);
|
|
3765
|
+
const cur = groupRowPaging.get(path);
|
|
3766
|
+
if (cur && cur.pageSize === size) return;
|
|
3767
|
+
groupRowPaging.set(path, {
|
|
3768
|
+
pageIndex: 0,
|
|
3769
|
+
pageSize: size
|
|
3770
|
+
});
|
|
3771
|
+
if (serverMode()) {
|
|
3772
|
+
groupRowsKey.delete(path);
|
|
3773
|
+
sync();
|
|
3774
|
+
fetchVisibleRows();
|
|
3775
|
+
return;
|
|
3776
|
+
}
|
|
3777
|
+
sync();
|
|
3778
|
+
}
|
|
3779
|
+
function setGroupPage(path, pageIndex) {
|
|
3780
|
+
if (!path) return;
|
|
3781
|
+
const pageSize = groupRowPaging.get(path)?.pageSize ?? (defaultGroupRowPageSize || groupPageSize);
|
|
3782
|
+
groupRowPaging.set(path, {
|
|
3783
|
+
pageIndex: Math.max(0, pageIndex),
|
|
3784
|
+
pageSize
|
|
3785
|
+
});
|
|
3786
|
+
if (serverMode()) {
|
|
3787
|
+
groupRowsKey.delete(path);
|
|
3788
|
+
sync();
|
|
3789
|
+
fetchVisibleRows();
|
|
3790
|
+
return;
|
|
3791
|
+
}
|
|
3792
|
+
sync();
|
|
3793
|
+
}
|
|
3794
|
+
function clearGroupPaging(path) {
|
|
3795
|
+
if (groupRowPaging.delete(path)) if (serverMode()) {
|
|
3796
|
+
groupRowsKey.delete(path);
|
|
3797
|
+
sync();
|
|
3798
|
+
fetchVisibleRows();
|
|
3799
|
+
} else sync();
|
|
3800
|
+
}
|
|
3801
|
+
function groupPageInfo(path) {
|
|
3802
|
+
if (pageSizeAll) {
|
|
3803
|
+
const node = pathIndex.get(path);
|
|
3804
|
+
const total = serverMode() ? serverGroups.find((g) => String(g.key) === path)?.count ?? 0 : node?.items.length ?? 0;
|
|
3805
|
+
return {
|
|
3806
|
+
pageIndex: 0,
|
|
3807
|
+
pageCount: 1,
|
|
3808
|
+
pageSize: Math.max(1, total),
|
|
3809
|
+
total
|
|
3810
|
+
};
|
|
3811
|
+
}
|
|
3812
|
+
if (serverMode()) {
|
|
3813
|
+
const total = serverGroups.find((g) => String(g.key) === path)?.count ?? 0;
|
|
3814
|
+
const pg = groupRowPagingFor(path);
|
|
3815
|
+
const pageSize = Math.max(1, pg?.pageSize ?? (defaultGroupRowPageSize || total || 1));
|
|
3816
|
+
const pageCount = Math.max(1, Math.ceil(total / pageSize));
|
|
3817
|
+
return {
|
|
3818
|
+
pageIndex: Math.min(Math.max(0, pg?.pageIndex ?? 0), pageCount - 1),
|
|
3819
|
+
pageCount,
|
|
3820
|
+
pageSize,
|
|
3821
|
+
total
|
|
3822
|
+
};
|
|
3823
|
+
}
|
|
3824
|
+
const node = pathIndex.get(path);
|
|
3825
|
+
const total = node ? node.items.length : 0;
|
|
3826
|
+
const pg = groupRowPagingFor(path);
|
|
3827
|
+
const pageSize = Math.max(1, pg?.pageSize ?? total ?? 1);
|
|
3828
|
+
const pageCount = Math.max(1, Math.ceil(total / pageSize));
|
|
3829
|
+
return {
|
|
3830
|
+
pageIndex: Math.min(Math.max(0, pg?.pageIndex ?? 0), pageCount - 1),
|
|
3831
|
+
pageCount,
|
|
3832
|
+
pageSize,
|
|
3833
|
+
total
|
|
3834
|
+
};
|
|
3835
|
+
}
|
|
3836
|
+
function groupNode(path) {
|
|
3837
|
+
return pathIndex.get(path);
|
|
3838
|
+
}
|
|
3839
|
+
/**
|
|
3840
|
+
* Every row matching the current query, without disturbing what's on screen.
|
|
3841
|
+
*
|
|
3842
|
+
* The chunked read itself lives in `readAllRows` (shared with the report
|
|
3843
|
+
* engine's `data` resolver), which also reconstructs the DataSource's active
|
|
3844
|
+
* **search** — `searchValue` is a DataSource-level option the store knows
|
|
3845
|
+
* nothing about, so reading the store directly would otherwise hand back rows
|
|
3846
|
+
* the user filtered away.
|
|
3847
|
+
*
|
|
3848
|
+
* `suppressSync` wraps the call because an array-backed source has to be
|
|
3849
|
+
* re-paged to be read: the detour fires `changed`, and subscribers must not
|
|
3850
|
+
* see the intermediate full-list state.
|
|
3851
|
+
*/
|
|
3852
|
+
async function getData() {
|
|
3853
|
+
const s = bound;
|
|
3854
|
+
if (!s) return [];
|
|
3855
|
+
suppressSync = true;
|
|
3856
|
+
try {
|
|
3857
|
+
return await readAllRows(s, {
|
|
3858
|
+
chunkSize,
|
|
3859
|
+
select: opts.select ?? toList(resolvedOpts.select),
|
|
3860
|
+
sort: mergedSort()
|
|
3861
|
+
});
|
|
3862
|
+
} finally {
|
|
3863
|
+
suppressSync = false;
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
/**
|
|
3867
|
+
* Group `rows` (or everything {@link getData} returns) by the grid's own
|
|
3868
|
+
* group fields. Named `buildGroupTree` internally so it doesn't shadow the
|
|
3869
|
+
* imported `buildGroups` helper that `syncGrouped` and friends rely on.
|
|
3870
|
+
*/
|
|
3871
|
+
async function buildGroupTree(rows) {
|
|
3872
|
+
if (!groupFields.length) return [];
|
|
3873
|
+
return buildGroups(rows ?? await getData(), groupFields);
|
|
3874
|
+
}
|
|
3875
|
+
async function importSheet(options) {
|
|
3876
|
+
const { importTable } = await import("./import-BgJdkwBI.js");
|
|
3877
|
+
return importTable(options, {
|
|
3878
|
+
getData,
|
|
3879
|
+
rowKeyOf,
|
|
3880
|
+
serverKeyOf: (row) => row?.[keyExpr] ?? rowKeyOf(row, -1),
|
|
3881
|
+
stageImported: (changes) => {
|
|
3882
|
+
for (const change of changes) {
|
|
3883
|
+
const patch = pending.get(change.rowKey) ?? {};
|
|
3884
|
+
Object.assign(patch, change.patch);
|
|
3885
|
+
pending.set(change.rowKey, patch);
|
|
3886
|
+
markImported(change.rowKey, Object.keys(change.patch));
|
|
3887
|
+
}
|
|
3888
|
+
notify();
|
|
3889
|
+
}
|
|
3890
|
+
});
|
|
3891
|
+
}
|
|
3892
|
+
async function exportReport(options) {
|
|
3893
|
+
const { exportTable } = await import("./export-BiGVyxt6.js");
|
|
3894
|
+
return exportTable(options);
|
|
3895
|
+
}
|
|
3896
|
+
function subscribe(cb) {
|
|
3897
|
+
return notifier.subscribe(cb);
|
|
3898
|
+
}
|
|
3899
|
+
function dispose() {
|
|
3900
|
+
detach();
|
|
3901
|
+
removeOutsideExit();
|
|
3902
|
+
unbindRowTrigger();
|
|
3903
|
+
clearRowCaches();
|
|
3904
|
+
formOps.length = 0;
|
|
3905
|
+
formHandle = null;
|
|
3906
|
+
ctrl.filterBuilder?.dispose();
|
|
3907
|
+
notifier.clear();
|
|
3908
|
+
}
|
|
3909
|
+
if (ds) bind(ds);
|
|
3910
|
+
return ctrl;
|
|
3911
|
+
}
|
|
3912
|
+
//#endregion
|
|
3913
|
+
export { seedFromRanges as a, minimize as i, buildDateTree as n, setAllChecked as o, checkState as r, setChecked as s, monoDataGrid as t };
|