@mono-lit/helper 0.0.8 → 0.0.9

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/dist/index.js CHANGED
@@ -3,16 +3,16 @@ import { a as buildGroups, c as isGroupNode, o as collectGroupPaths, s as flatte
3
3
  import { _ as plainSearchFields, b as searchRemoteFilter, c as resolveErrorMessages, d as DEFAULT_SEARCH_OPERATION, f as MonoSourceSearch, g as parseSearchValue, h as mergeSearchFields, l as setErrorMessages, m as isFoldableSearch, n as describeError, p as SEARCH_FIELDS_KEYS, r as errorStatus, s as normalizeError, t as DEFAULT_ERROR_MESSAGES, v as readSearchField, x as searchRowPredicate, y as resolveSearchFields } from "./normalize-error-D93O4q8g.js";
4
4
  import { h as monoArraySource } from "./data-source-options-fxT5QIX4.js";
5
5
  import { t as resolveMaybeReactive } from "./reactive-Dtd-esHO.js";
6
- import { t as monoDataGrid } from "./mono-data-grid-Bb00QM72.js";
6
+ import { t as monoDataGrid } from "./mono-data-grid-llJCX9MC.js";
7
7
  import { d as OPERATOR_ARITY, l as treeToArray, m as operatorsFor, n as arrayToTree, s as odataStringToArray, t as arrayToODataString, u as DEFAULT_TEXTS } from "./filter-odata-Bi27XUpQ.js";
8
8
  import { t as monoFilterBuilder } from "./filter-builder-C0T3pMx2.js";
9
9
  import { C as resetMonoSkeleton, a as resetMonoUI, c as stopMonoSizeAttributes, i as getMonoUIStatus, m as isMonoSsrApp, n as createMonoUI, o as applyMonoSizeAttributes, p as getMonoSkeletonStatus, r as getMonoUI, s as startMonoSizeAttributes } from "./mono-ui-kvLVIf1V.js";
10
10
  import { t as monoForm } from "./mono-form-controller-BrA10erm.js";
11
11
  import { t as monoModal } from "./mono-modal-controller-ClObbHUs.js";
12
12
  import { i as composeApply, n as buildChartApply, r as buildChartOdataRequest, t as monoChart } from "./mono-data-chart-NKPdAgVq.js";
13
- import { t as monoDataDropdown } from "./mono-data-dropdown-DUXo_jxd.js";
13
+ import { t as monoDataDropdown } from "./mono-data-dropdown-B2iRpJuI.js";
14
14
  import { a as monoTooltip, c as getMonoTooltipGlobal, t as controlMonoTooltip } from "./control-tooltip-CoPD37_h.js";
15
15
  import { a as resetMonoTooltip, n as createMonoTooltip, r as destroyAllMonoTooltips } from "./tooltip-Cu5IrDS9.js";
16
- import { _ as DEFAULT_FLAVOR, a as flavors, b as themePresets, c as getCurrentFlavor, d as getThemeData, f as isValidFlavor, g as themes, h as resetTheme, i as createThemeClass, l as getCurrentTheme, m as resetFlavor, n as applyTheme, o as getAllFlavors, p as isValidTheme, r as createFlavorClass, s as getAllThemes, t as applyFlavor, u as getFlavorData, v as DEFAULT_THEME, y as flavorPresets } from "./index.node-CLqE8F8P.js";
16
+ import { _ as DEFAULT_FLAVOR, a as flavors, b as themePresets, c as getCurrentFlavor, d as getThemeData, f as isValidFlavor, g as themes, h as resetTheme, i as createThemeClass, l as getCurrentTheme, m as resetFlavor, n as applyTheme, o as getAllFlavors, p as isValidTheme, r as createFlavorClass, s as getAllThemes, t as applyFlavor, u as getFlavorData, v as DEFAULT_THEME, y as flavorPresets } from "./index.node-DTqDBca7.js";
17
17
 
18
18
  export { DEFAULT_ERROR_MESSAGES, DEFAULT_FLAVOR, DEFAULT_SEARCH_OPERATION, DEFAULT_THEME, OPERATOR_ARITY as MONO_FILTER_OPERATOR_ARITY, DEFAULT_TEXTS as MONO_FILTER_TEXTS, MonoSourceSearch, SEARCH_FIELDS_KEYS, applyFlavor, applyMonoSizeAttributes, applyTheme, arrayToODataString, arrayToTree, buildChartApply, buildChartOdataRequest, buildGroups, collectGroupPaths, composeApply, monoChart as controlMonoChart, monoChart, monoDataDropdown as controlMonoDataDropdown, monoDataDropdown, monoFilterBuilder as controlMonoFilterBuilder, monoFilterBuilder, monoForm as controlMonoForm, monoForm, monoModal as controlMonoModal, monoModal, monoDataGrid as controlMonoTable, monoDataGrid, controlMonoTooltip, createFlavorClass, createMonoTooltip, createMonoUI, createThemeClass, describeError, destroyAllMonoTooltips, errorStatus, flattenLeaves, flavorPresets, flavors, getAllFlavors, getAllThemes, getCurrentFlavor, getCurrentTheme, getFieldValue, getFlavorData, getMonoSkeletonStatus, getMonoTooltipGlobal, getMonoUI, getMonoUIStatus, getThemeData, isFoldableSearch, isGroupNode, isMonoSsrApp, isPath, isValidFlavor, isValidTheme, mergePatch, mergeSearchFields, monoArraySource, operatorsFor as monoFilterOperatorsFor, monoTooltip, normalizeError, odataStringToArray, parseFieldPath, parseSearchValue, plainSearchFields, readSearchField, resetFlavor, resetMonoSkeleton, resetMonoTooltip, resetMonoUI, resetTheme, resolveErrorMessages, resolveMaybeReactive, resolveSearchFields, searchRemoteFilter, searchRowPredicate, setErrorMessages, setFieldValue, startMonoSizeAttributes, stopMonoSizeAttributes, themePresets, themes, toODataClause, toODataSelector, treeToArray };
@@ -1,8 +1,8 @@
1
1
  import "./normalize-error-D93O4q8g.js";
2
- import "./mono-data-grid-Bb00QM72.js";
2
+ import "./mono-data-grid-llJCX9MC.js";
3
3
  import "./mono-ui-kvLVIf1V.js";
4
4
  import "./mono-data-chart-NKPdAgVq.js";
5
- import "./mono-data-dropdown-DUXo_jxd.js";
5
+ import "./mono-data-dropdown-B2iRpJuI.js";
6
6
  import "./tooltip-Cu5IrDS9.js";
7
7
 
8
8
  //#region src/data/theme/presets.json
@@ -3,16 +3,16 @@ import { a as buildGroups, c as isGroupNode, o as collectGroupPaths, s as flatte
3
3
  import { _ as plainSearchFields, b as searchRemoteFilter, c as resolveErrorMessages, d as DEFAULT_SEARCH_OPERATION, f as MonoSourceSearch, g as parseSearchValue, h as mergeSearchFields, l as setErrorMessages, m as isFoldableSearch, n as describeError, p as SEARCH_FIELDS_KEYS, r as errorStatus, s as normalizeError, t as DEFAULT_ERROR_MESSAGES, v as readSearchField, x as searchRowPredicate, y as resolveSearchFields } from "./normalize-error-D93O4q8g.js";
4
4
  import { h as monoArraySource } from "./data-source-options-fxT5QIX4.js";
5
5
  import { t as resolveMaybeReactive } from "./reactive-Dtd-esHO.js";
6
- import { t as monoDataGrid } from "./mono-data-grid-Bb00QM72.js";
6
+ import { t as monoDataGrid } from "./mono-data-grid-llJCX9MC.js";
7
7
  import { d as OPERATOR_ARITY, l as treeToArray, m as operatorsFor, n as arrayToTree, s as odataStringToArray, t as arrayToODataString, u as DEFAULT_TEXTS } from "./filter-odata-Bi27XUpQ.js";
8
8
  import { t as monoFilterBuilder } from "./filter-builder-C0T3pMx2.js";
9
9
  import { C as resetMonoSkeleton, a as resetMonoUI, c as stopMonoSizeAttributes, i as getMonoUIStatus, m as isMonoSsrApp, n as createMonoUI, o as applyMonoSizeAttributes, p as getMonoSkeletonStatus, r as getMonoUI, s as startMonoSizeAttributes } from "./mono-ui-kvLVIf1V.js";
10
10
  import { t as monoForm } from "./mono-form-controller-BrA10erm.js";
11
11
  import { t as monoModal } from "./mono-modal-controller-ClObbHUs.js";
12
12
  import { i as composeApply, n as buildChartApply, r as buildChartOdataRequest, t as monoChart } from "./mono-data-chart-NKPdAgVq.js";
13
- import { t as monoDataDropdown } from "./mono-data-dropdown-DUXo_jxd.js";
13
+ import { t as monoDataDropdown } from "./mono-data-dropdown-B2iRpJuI.js";
14
14
  import { a as monoTooltip, c as getMonoTooltipGlobal, t as controlMonoTooltip } from "./control-tooltip-CoPD37_h.js";
15
15
  import { a as resetMonoTooltip, n as createMonoTooltip, r as destroyAllMonoTooltips } from "./tooltip-Cu5IrDS9.js";
16
- import { _ as DEFAULT_FLAVOR, a as flavors, b as themePresets, c as getCurrentFlavor, d as getThemeData, f as isValidFlavor, g as themes, h as resetTheme, i as createThemeClass, l as getCurrentTheme, m as resetFlavor, n as applyTheme, o as getAllFlavors, p as isValidTheme, r as createFlavorClass, s as getAllThemes, t as applyFlavor, u as getFlavorData, v as DEFAULT_THEME, y as flavorPresets } from "./index.node-CLqE8F8P.js";
16
+ import { _ as DEFAULT_FLAVOR, a as flavors, b as themePresets, c as getCurrentFlavor, d as getThemeData, f as isValidFlavor, g as themes, h as resetTheme, i as createThemeClass, l as getCurrentTheme, m as resetFlavor, n as applyTheme, o as getAllFlavors, p as isValidTheme, r as createFlavorClass, s as getAllThemes, t as applyFlavor, u as getFlavorData, v as DEFAULT_THEME, y as flavorPresets } from "./index.node-DTqDBca7.js";
17
17
 
18
18
  export { DEFAULT_ERROR_MESSAGES, DEFAULT_FLAVOR, DEFAULT_SEARCH_OPERATION, DEFAULT_THEME, OPERATOR_ARITY as MONO_FILTER_OPERATOR_ARITY, DEFAULT_TEXTS as MONO_FILTER_TEXTS, MonoSourceSearch, SEARCH_FIELDS_KEYS, applyFlavor, applyMonoSizeAttributes, applyTheme, arrayToODataString, arrayToTree, buildChartApply, buildChartOdataRequest, buildGroups, collectGroupPaths, composeApply, monoChart as controlMonoChart, monoChart, monoDataDropdown as controlMonoDataDropdown, monoDataDropdown, monoFilterBuilder as controlMonoFilterBuilder, monoFilterBuilder, monoForm as controlMonoForm, monoForm, monoModal as controlMonoModal, monoModal, monoDataGrid as controlMonoTable, monoDataGrid, controlMonoTooltip, createFlavorClass, createMonoTooltip, createMonoUI, createThemeClass, describeError, destroyAllMonoTooltips, errorStatus, flattenLeaves, flavorPresets, flavors, getAllFlavors, getAllThemes, getCurrentFlavor, getCurrentTheme, getFieldValue, getFlavorData, getMonoSkeletonStatus, getMonoTooltipGlobal, getMonoUI, getMonoUIStatus, getThemeData, isFoldableSearch, isGroupNode, isMonoSsrApp, isPath, isValidFlavor, isValidTheme, mergePatch, mergeSearchFields, monoArraySource, operatorsFor as monoFilterOperatorsFor, monoTooltip, normalizeError, odataStringToArray, parseFieldPath, parseSearchValue, plainSearchFields, readSearchField, resetFlavor, resetMonoSkeleton, resetMonoTooltip, resetMonoUI, resetTheme, resolveErrorMessages, resolveMaybeReactive, resolveSearchFields, searchRemoteFilter, searchRowPredicate, setErrorMessages, setFieldValue, startMonoSizeAttributes, stopMonoSizeAttributes, themePresets, themes, toODataClause, toODataSelector, treeToArray };
@@ -1,4 +1,4 @@
1
- import { t as monoDataGrid } from "./mono-data-grid-Bb00QM72.js";
1
+ import { t as monoDataGrid } from "./mono-data-grid-llJCX9MC.js";
2
2
  import { t as createNotifier } from "./notifier-BIrqUzRh.js";
3
3
 
4
4
  //#region src/components/dropdown-table/mono-data-dropdown.ts
@@ -444,6 +444,9 @@ function monoDataGrid(ds = null, opts = {}) {
444
444
  const chunkSize = Math.max(1, opts.chunkSize ?? 100);
445
445
  let suppressSync = false;
446
446
  let allRows = [];
447
+ let browserRows = null;
448
+ let browserPageIndex = 0;
449
+ let browserPageSize = 0;
447
450
  let scrollMode = "off";
448
451
  let accumulated = [];
449
452
  let scrollNextPage = 0;
@@ -2147,16 +2150,21 @@ function monoDataGrid(ds = null, opts = {}) {
2147
2150
  * column sort follows — applied to the group field itself if it is one.
2148
2151
  */
2149
2152
  function effectiveSort() {
2150
- const keys = ctrl.sorts.map((s) => ({
2151
- selector: s.field,
2153
+ const keys = ctrl.sorts.filter((s) => !isBrowserSortKey(s.field)).map((s) => ({
2154
+ field: s.field,
2155
+ selector: declaredSortSelector(s.field) ?? s.field,
2152
2156
  desc: s.order === "desc"
2153
2157
  }));
2154
- if (!groupFields.length) return keys.length ? keys : null;
2158
+ const entry = (k) => ({
2159
+ selector: k.selector,
2160
+ desc: k.desc
2161
+ });
2162
+ if (!groupFields.length) return keys.length ? keys.map(entry) : null;
2155
2163
  const list = groupFields.map((f) => ({
2156
2164
  selector: f,
2157
- desc: keys.find((k) => k.selector === f)?.desc ?? false
2165
+ desc: keys.find((k) => k.field === f)?.desc ?? false
2158
2166
  }));
2159
- for (const k of keys) if (!groupFields.includes(k.selector)) list.push(k);
2167
+ for (const k of keys) if (!groupFields.includes(k.field)) list.push(entry(k));
2160
2168
  return list;
2161
2169
  }
2162
2170
  /**
@@ -2571,6 +2579,10 @@ function monoDataGrid(ds = null, opts = {}) {
2571
2579
  syncScroll();
2572
2580
  return;
2573
2581
  }
2582
+ if (browserRows && browserSortActive()) {
2583
+ syncBrowser();
2584
+ return;
2585
+ }
2574
2586
  if (pageSizeAll) {
2575
2587
  ctrl.grouped = false;
2576
2588
  ctrl.items = allRows;
@@ -2734,6 +2746,11 @@ function monoDataGrid(ds = null, opts = {}) {
2734
2746
  await loadScrollPage(true);
2735
2747
  return;
2736
2748
  }
2749
+ if (browserSortActive()) {
2750
+ await loadBrowserSorted();
2751
+ return;
2752
+ }
2753
+ browserRows = null;
2737
2754
  if (pageSizeAll && !isGrouped()) {
2738
2755
  await loadAllChunked();
2739
2756
  return;
@@ -2755,6 +2772,11 @@ function monoDataGrid(ds = null, opts = {}) {
2755
2772
  await loadScrollPage(true);
2756
2773
  return;
2757
2774
  }
2775
+ if (browserSortActive()) {
2776
+ await loadBrowserSorted();
2777
+ return;
2778
+ }
2779
+ browserRows = null;
2758
2780
  if (pageSizeAll && !isGrouped()) {
2759
2781
  await loadAllChunked();
2760
2782
  return;
@@ -2791,6 +2813,11 @@ function monoDataGrid(ds = null, opts = {}) {
2791
2813
  if (serverMode()) await fetchVisibleRows();
2792
2814
  return;
2793
2815
  }
2816
+ if (browserRows && browserSortActive()) {
2817
+ browserPageIndex = Math.max(0, pageIndex);
2818
+ syncBrowser();
2819
+ return;
2820
+ }
2794
2821
  const s = bound;
2795
2822
  if (!s) return;
2796
2823
  s.pageIndex?.(Math.max(0, pageIndex));
@@ -2808,6 +2835,15 @@ function monoDataGrid(ds = null, opts = {}) {
2808
2835
  }
2809
2836
  const s = bound;
2810
2837
  if (!s) return;
2838
+ if (browserRows && browserSortActive()) {
2839
+ if (!all) {
2840
+ browserPageSize = Math.max(1, pageSize);
2841
+ s.pageSize?.(browserPageSize);
2842
+ }
2843
+ browserPageIndex = 0;
2844
+ syncBrowser();
2845
+ return;
2846
+ }
2811
2847
  if (all) {
2812
2848
  await loadAllChunked();
2813
2849
  return;
@@ -2854,6 +2890,127 @@ function monoDataGrid(ds = null, opts = {}) {
2854
2890
  ctrl.loading = bound?.isLoading?.() ?? false;
2855
2891
  sync();
2856
2892
  }
2893
+ const BROWSER_SORT_MAX_ROWS = 5e3;
2894
+ /** The active sort keys when ANY of them sorts in the browser, else `null`. */
2895
+ function browserSortKeys() {
2896
+ if (!ctrl.sorts.some((s) => isBrowserSortKey(s.field))) return null;
2897
+ return ctrl.sorts.map((s) => {
2898
+ const browser = isBrowserSortKey(s.field);
2899
+ return {
2900
+ field: s.field,
2901
+ selector: declaredSortSelector(s.field) ?? s.field,
2902
+ desc: s.order === "desc",
2903
+ aggregate: browser ? declaredSortConfig(s.field).aggregate ?? "sum" : null
2904
+ };
2905
+ });
2906
+ }
2907
+ /** Browser sort applies: a browser-sorted column is active on a flat, paged, bound table. */
2908
+ function browserSortActive() {
2909
+ return !!bound && !isGrouped() && !serverMode() && !scrollActive() && !!browserSortKeys();
2910
+ }
2911
+ /** The smallest `maxRows` any active browser-sorted column declares (default 5000). */
2912
+ function browserSortCap() {
2913
+ const caps = ctrl.sorts.filter((s) => isBrowserSortKey(s.field)).map((s) => Number(declaredSortConfig(s.field).maxRows)).filter((n) => Number.isFinite(n) && n > 0);
2914
+ return caps.length ? Math.min(...caps) : BROWSER_SORT_MAX_ROWS;
2915
+ }
2916
+ /** A row's matches for `selector` reduced to one sortable value. */
2917
+ function browserSortValue(row, selector, aggregate) {
2918
+ const raw = getFieldValue(row, selector);
2919
+ if (!aggregate) return raw;
2920
+ const list = Array.isArray(raw) ? raw : raw == null ? [] : [raw];
2921
+ if (aggregate === "count") return list.length;
2922
+ const nums = list.map(Number).filter((n) => Number.isFinite(n));
2923
+ if (!nums.length) return aggregate === "sum" ? 0 : null;
2924
+ if (aggregate === "min") return Math.min(...nums);
2925
+ if (aggregate === "max") return Math.max(...nums);
2926
+ const sum = nums.reduce((a, b) => a + b, 0);
2927
+ return aggregate === "avg" ? sum / nums.length : sum;
2928
+ }
2929
+ /** Empties last in either direction; numbers numerically; text numeric-aware. */
2930
+ function compareBrowserValues(a, b) {
2931
+ const ea = a == null || a === "";
2932
+ const eb = b == null || b === "";
2933
+ if (ea || eb) return ea === eb ? 0 : ea ? 1 : -1;
2934
+ if (typeof a === "number" && typeof b === "number") return a - b;
2935
+ if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
2936
+ return String(a).localeCompare(String(b), void 0, { numeric: true });
2937
+ }
2938
+ /** Every matching row, sorted by the active keys (stable). */
2939
+ function sortBrowserRows(rows) {
2940
+ const keys = browserSortKeys() ?? [];
2941
+ const decorated = rows.map((row, i) => ({
2942
+ row,
2943
+ i,
2944
+ v: keys.map((k) => browserSortValue(row, k.selector, k.aggregate))
2945
+ }));
2946
+ decorated.sort((x, y) => {
2947
+ for (let k = 0; k < keys.length; k++) {
2948
+ const ex = x.v[k] == null || x.v[k] === "";
2949
+ const ey = y.v[k] == null || y.v[k] === "";
2950
+ const c = compareBrowserValues(x.v[k], y.v[k]);
2951
+ if (c) return ex || ey ? c : keys[k].desc ? -c : c;
2952
+ }
2953
+ return x.i - y.i;
2954
+ });
2955
+ return decorated.map((d) => d.row);
2956
+ }
2957
+ /** Too many rows to sort in the browser: drop those keys, warn, load normally. */
2958
+ async function refuseBrowserSort(count, cap) {
2959
+ const fields = ctrl.sorts.filter((s) => isBrowserSortKey(s.field)).map((s) => s.field);
2960
+ console.warn(`[monoDataGrid] sort: "${fields.join("\", \"")}" sorts in the browser and needs every row, but ${count} match (maxRows ${cap}) — sort cleared.`);
2961
+ ctrl.sorts = ctrl.sorts.filter((s) => !isBrowserSortKey(s.field));
2962
+ syncPrimarySort();
2963
+ browserRows = null;
2964
+ applySourceSort();
2965
+ notify();
2966
+ await load();
2967
+ }
2968
+ /** Read every matching row, sort it in memory, show the first page. */
2969
+ async function loadBrowserSorted() {
2970
+ const s = bound;
2971
+ if (!s) return;
2972
+ const cap = browserSortCap();
2973
+ const known = s.totalCount?.();
2974
+ if (typeof known === "number" && known > cap) {
2975
+ await refuseBrowserSort(known, cap);
2976
+ return;
2977
+ }
2978
+ if (!browserPageSize) browserPageSize = Math.max(1, s.pageSize?.() || ctrl.pageSize || 20);
2979
+ ctrl.loading = true;
2980
+ notify();
2981
+ let rows = [];
2982
+ try {
2983
+ await runLoad(async () => {
2984
+ rows = await readAllFromSource();
2985
+ });
2986
+ } finally {
2987
+ ctrl.loading = false;
2988
+ }
2989
+ if (rows.length > cap) {
2990
+ await refuseBrowserSort(rows.length, cap);
2991
+ return;
2992
+ }
2993
+ browserRows = sortBrowserRows(rows);
2994
+ browserPageIndex = 0;
2995
+ sync();
2996
+ recomputeSummaries();
2997
+ }
2998
+ /** Map the in-memory sorted rows (current page, or all) onto `ctrl`. */
2999
+ function syncBrowser() {
3000
+ const rows = browserRows ?? [];
3001
+ const size = pageSizeAll ? Math.max(1, rows.length) : Math.max(1, browserPageSize);
3002
+ const pageCount = Math.max(1, Math.ceil(rows.length / size));
3003
+ browserPageIndex = Math.min(Math.max(0, browserPageIndex), pageCount - 1);
3004
+ const start = browserPageIndex * size;
3005
+ ctrl.grouped = false;
3006
+ ctrl.items = rows.slice(start, start + size);
3007
+ ctrl.pageIndex = browserPageIndex;
3008
+ ctrl.pageSize = size;
3009
+ ctrl.pageSizeAll = pageSizeAll;
3010
+ ctrl.totalCount = rows.length;
3011
+ ctrl.pageCount = pageCount;
3012
+ render();
3013
+ }
2857
3014
  /** Scroll modes only apply to a flat (ungrouped, non-server) grid. */
2858
3015
  function scrollActive() {
2859
3016
  return scrollMode !== "off" && !isGrouped() && !serverMode();
@@ -3786,6 +3943,41 @@ function monoDataGrid(ds = null, opts = {}) {
3786
3943
  });
3787
3944
  ctrl.sorts = marked.map((m) => m.s);
3788
3945
  }
3946
+ /**
3947
+ * The source selector a column DECLARED via `sort.selector`, if any — what the
3948
+ * DataSource / array source orders by in place of the field. Resolved like
3949
+ * `declaredSortIndex`: central `props.th` config first, then the element's own
3950
+ * `:sort.prop`. A blank string counts as not declared.
3951
+ */
3952
+ function declaredSortSelector(field) {
3953
+ const sel = declaredSortConfig(field).selector;
3954
+ return typeof sel === "string" && sel.trim() ? sel.trim() : void 0;
3955
+ }
3956
+ /**
3957
+ * A column's declared sort OBJECT — the element's `:sort.prop` with central
3958
+ * `props.th` config merged over it (props win key by key, as everywhere). `{}`
3959
+ * for `sort: true` / no config.
3960
+ */
3961
+ function declaredSortConfig(field) {
3962
+ const asObj = (sort) => typeof sort === "object" && sort ? sort : {};
3963
+ let fromEl = {};
3964
+ for (const el of columnEls) if (el.field === field && typeof el.sort === "object" && el.sort) {
3965
+ fromEl = asObj(el.sort);
3966
+ break;
3967
+ }
3968
+ return {
3969
+ ...fromEl,
3970
+ ...asObj((elementProps.th ?? []).find((c) => c?.field === field)?.sort)
3971
+ };
3972
+ }
3973
+ /**
3974
+ * Whether a sorted column is sorted IN THE BROWSER — its selector fans out over a
3975
+ * collection (`[*]`, which no source can order by) or it declares an `aggregate`.
3976
+ */
3977
+ function isBrowserSortKey(field) {
3978
+ const sel = declaredSortSelector(field);
3979
+ return !!sel && parseFieldPath(sel).hasWildcard || !!declaredSortConfig(field).aggregate;
3980
+ }
3789
3981
  /** Keep the single-value mirrors pointing at the primary key. */
3790
3982
  function syncPrimarySort() {
3791
3983
  ctrl.sortField = ctrl.sorts[0]?.field ?? null;
@@ -3974,6 +4166,11 @@ function monoDataGrid(ds = null, opts = {}) {
3974
4166
  * see the intermediate full-list state.
3975
4167
  */
3976
4168
  async function getData() {
4169
+ if (browserRows && browserSortActive()) return [...browserRows];
4170
+ return readAllFromSource();
4171
+ }
4172
+ /** `getData`'s read — every matching row from the source, in its own order. */
4173
+ async function readAllFromSource() {
3977
4174
  const s = bound;
3978
4175
  if (!s) return [];
3979
4176
  const loadOptions = {};
@@ -445,6 +445,9 @@ function monoDataGrid(ds = null, opts = {}) {
445
445
  const chunkSize = Math.max(1, opts.chunkSize ?? 100);
446
446
  let suppressSync = false;
447
447
  let allRows = [];
448
+ let browserRows = null;
449
+ let browserPageIndex = 0;
450
+ let browserPageSize = 0;
448
451
  let scrollMode = "off";
449
452
  let accumulated = [];
450
453
  let scrollNextPage = 0;
@@ -2148,16 +2151,21 @@ function monoDataGrid(ds = null, opts = {}) {
2148
2151
  * column sort follows — applied to the group field itself if it is one.
2149
2152
  */
2150
2153
  function effectiveSort() {
2151
- const keys = ctrl.sorts.map((s) => ({
2152
- selector: s.field,
2154
+ const keys = ctrl.sorts.filter((s) => !isBrowserSortKey(s.field)).map((s) => ({
2155
+ field: s.field,
2156
+ selector: declaredSortSelector(s.field) ?? s.field,
2153
2157
  desc: s.order === "desc"
2154
2158
  }));
2155
- if (!groupFields.length) return keys.length ? keys : null;
2159
+ const entry = (k) => ({
2160
+ selector: k.selector,
2161
+ desc: k.desc
2162
+ });
2163
+ if (!groupFields.length) return keys.length ? keys.map(entry) : null;
2156
2164
  const list = groupFields.map((f) => ({
2157
2165
  selector: f,
2158
- desc: keys.find((k) => k.selector === f)?.desc ?? false
2166
+ desc: keys.find((k) => k.field === f)?.desc ?? false
2159
2167
  }));
2160
- for (const k of keys) if (!groupFields.includes(k.selector)) list.push(k);
2168
+ for (const k of keys) if (!groupFields.includes(k.field)) list.push(entry(k));
2161
2169
  return list;
2162
2170
  }
2163
2171
  /**
@@ -2572,6 +2580,10 @@ function monoDataGrid(ds = null, opts = {}) {
2572
2580
  syncScroll();
2573
2581
  return;
2574
2582
  }
2583
+ if (browserRows && browserSortActive()) {
2584
+ syncBrowser();
2585
+ return;
2586
+ }
2575
2587
  if (pageSizeAll) {
2576
2588
  ctrl.grouped = false;
2577
2589
  ctrl.items = allRows;
@@ -2735,6 +2747,11 @@ function monoDataGrid(ds = null, opts = {}) {
2735
2747
  await loadScrollPage(true);
2736
2748
  return;
2737
2749
  }
2750
+ if (browserSortActive()) {
2751
+ await loadBrowserSorted();
2752
+ return;
2753
+ }
2754
+ browserRows = null;
2738
2755
  if (pageSizeAll && !isGrouped()) {
2739
2756
  await loadAllChunked();
2740
2757
  return;
@@ -2756,6 +2773,11 @@ function monoDataGrid(ds = null, opts = {}) {
2756
2773
  await loadScrollPage(true);
2757
2774
  return;
2758
2775
  }
2776
+ if (browserSortActive()) {
2777
+ await loadBrowserSorted();
2778
+ return;
2779
+ }
2780
+ browserRows = null;
2759
2781
  if (pageSizeAll && !isGrouped()) {
2760
2782
  await loadAllChunked();
2761
2783
  return;
@@ -2792,6 +2814,11 @@ function monoDataGrid(ds = null, opts = {}) {
2792
2814
  if (serverMode()) await fetchVisibleRows();
2793
2815
  return;
2794
2816
  }
2817
+ if (browserRows && browserSortActive()) {
2818
+ browserPageIndex = Math.max(0, pageIndex);
2819
+ syncBrowser();
2820
+ return;
2821
+ }
2795
2822
  const s = bound;
2796
2823
  if (!s) return;
2797
2824
  s.pageIndex?.(Math.max(0, pageIndex));
@@ -2809,6 +2836,15 @@ function monoDataGrid(ds = null, opts = {}) {
2809
2836
  }
2810
2837
  const s = bound;
2811
2838
  if (!s) return;
2839
+ if (browserRows && browserSortActive()) {
2840
+ if (!all) {
2841
+ browserPageSize = Math.max(1, pageSize);
2842
+ s.pageSize?.(browserPageSize);
2843
+ }
2844
+ browserPageIndex = 0;
2845
+ syncBrowser();
2846
+ return;
2847
+ }
2812
2848
  if (all) {
2813
2849
  await loadAllChunked();
2814
2850
  return;
@@ -2855,6 +2891,127 @@ function monoDataGrid(ds = null, opts = {}) {
2855
2891
  ctrl.loading = bound?.isLoading?.() ?? false;
2856
2892
  sync();
2857
2893
  }
2894
+ const BROWSER_SORT_MAX_ROWS = 5e3;
2895
+ /** The active sort keys when ANY of them sorts in the browser, else `null`. */
2896
+ function browserSortKeys() {
2897
+ if (!ctrl.sorts.some((s) => isBrowserSortKey(s.field))) return null;
2898
+ return ctrl.sorts.map((s) => {
2899
+ const browser = isBrowserSortKey(s.field);
2900
+ return {
2901
+ field: s.field,
2902
+ selector: declaredSortSelector(s.field) ?? s.field,
2903
+ desc: s.order === "desc",
2904
+ aggregate: browser ? declaredSortConfig(s.field).aggregate ?? "sum" : null
2905
+ };
2906
+ });
2907
+ }
2908
+ /** Browser sort applies: a browser-sorted column is active on a flat, paged, bound table. */
2909
+ function browserSortActive() {
2910
+ return !!bound && !isGrouped() && !serverMode() && !scrollActive() && !!browserSortKeys();
2911
+ }
2912
+ /** The smallest `maxRows` any active browser-sorted column declares (default 5000). */
2913
+ function browserSortCap() {
2914
+ const caps = ctrl.sorts.filter((s) => isBrowserSortKey(s.field)).map((s) => Number(declaredSortConfig(s.field).maxRows)).filter((n) => Number.isFinite(n) && n > 0);
2915
+ return caps.length ? Math.min(...caps) : BROWSER_SORT_MAX_ROWS;
2916
+ }
2917
+ /** A row's matches for `selector` reduced to one sortable value. */
2918
+ function browserSortValue(row, selector, aggregate) {
2919
+ const raw = getFieldValue(row, selector);
2920
+ if (!aggregate) return raw;
2921
+ const list = Array.isArray(raw) ? raw : raw == null ? [] : [raw];
2922
+ if (aggregate === "count") return list.length;
2923
+ const nums = list.map(Number).filter((n) => Number.isFinite(n));
2924
+ if (!nums.length) return aggregate === "sum" ? 0 : null;
2925
+ if (aggregate === "min") return Math.min(...nums);
2926
+ if (aggregate === "max") return Math.max(...nums);
2927
+ const sum = nums.reduce((a, b) => a + b, 0);
2928
+ return aggregate === "avg" ? sum / nums.length : sum;
2929
+ }
2930
+ /** Empties last in either direction; numbers numerically; text numeric-aware. */
2931
+ function compareBrowserValues(a, b) {
2932
+ const ea = a == null || a === "";
2933
+ const eb = b == null || b === "";
2934
+ if (ea || eb) return ea === eb ? 0 : ea ? 1 : -1;
2935
+ if (typeof a === "number" && typeof b === "number") return a - b;
2936
+ if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
2937
+ return String(a).localeCompare(String(b), void 0, { numeric: true });
2938
+ }
2939
+ /** Every matching row, sorted by the active keys (stable). */
2940
+ function sortBrowserRows(rows) {
2941
+ const keys = browserSortKeys() ?? [];
2942
+ const decorated = rows.map((row, i) => ({
2943
+ row,
2944
+ i,
2945
+ v: keys.map((k) => browserSortValue(row, k.selector, k.aggregate))
2946
+ }));
2947
+ decorated.sort((x, y) => {
2948
+ for (let k = 0; k < keys.length; k++) {
2949
+ const ex = x.v[k] == null || x.v[k] === "";
2950
+ const ey = y.v[k] == null || y.v[k] === "";
2951
+ const c = compareBrowserValues(x.v[k], y.v[k]);
2952
+ if (c) return ex || ey ? c : keys[k].desc ? -c : c;
2953
+ }
2954
+ return x.i - y.i;
2955
+ });
2956
+ return decorated.map((d) => d.row);
2957
+ }
2958
+ /** Too many rows to sort in the browser: drop those keys, warn, load normally. */
2959
+ async function refuseBrowserSort(count, cap) {
2960
+ const fields = ctrl.sorts.filter((s) => isBrowserSortKey(s.field)).map((s) => s.field);
2961
+ console.warn(`[monoDataGrid] sort: "${fields.join("\", \"")}" sorts in the browser and needs every row, but ${count} match (maxRows ${cap}) — sort cleared.`);
2962
+ ctrl.sorts = ctrl.sorts.filter((s) => !isBrowserSortKey(s.field));
2963
+ syncPrimarySort();
2964
+ browserRows = null;
2965
+ applySourceSort();
2966
+ notify();
2967
+ await load();
2968
+ }
2969
+ /** Read every matching row, sort it in memory, show the first page. */
2970
+ async function loadBrowserSorted() {
2971
+ const s = bound;
2972
+ if (!s) return;
2973
+ const cap = browserSortCap();
2974
+ const known = s.totalCount?.();
2975
+ if (typeof known === "number" && known > cap) {
2976
+ await refuseBrowserSort(known, cap);
2977
+ return;
2978
+ }
2979
+ if (!browserPageSize) browserPageSize = Math.max(1, s.pageSize?.() || ctrl.pageSize || 20);
2980
+ ctrl.loading = true;
2981
+ notify();
2982
+ let rows = [];
2983
+ try {
2984
+ await runLoad(async () => {
2985
+ rows = await readAllFromSource();
2986
+ });
2987
+ } finally {
2988
+ ctrl.loading = false;
2989
+ }
2990
+ if (rows.length > cap) {
2991
+ await refuseBrowserSort(rows.length, cap);
2992
+ return;
2993
+ }
2994
+ browserRows = sortBrowserRows(rows);
2995
+ browserPageIndex = 0;
2996
+ sync();
2997
+ recomputeSummaries();
2998
+ }
2999
+ /** Map the in-memory sorted rows (current page, or all) onto `ctrl`. */
3000
+ function syncBrowser() {
3001
+ const rows = browserRows ?? [];
3002
+ const size = pageSizeAll ? Math.max(1, rows.length) : Math.max(1, browserPageSize);
3003
+ const pageCount = Math.max(1, Math.ceil(rows.length / size));
3004
+ browserPageIndex = Math.min(Math.max(0, browserPageIndex), pageCount - 1);
3005
+ const start = browserPageIndex * size;
3006
+ ctrl.grouped = false;
3007
+ ctrl.items = rows.slice(start, start + size);
3008
+ ctrl.pageIndex = browserPageIndex;
3009
+ ctrl.pageSize = size;
3010
+ ctrl.pageSizeAll = pageSizeAll;
3011
+ ctrl.totalCount = rows.length;
3012
+ ctrl.pageCount = pageCount;
3013
+ render();
3014
+ }
2858
3015
  /** Scroll modes only apply to a flat (ungrouped, non-server) grid. */
2859
3016
  function scrollActive() {
2860
3017
  return scrollMode !== "off" && !isGrouped() && !serverMode();
@@ -3787,6 +3944,41 @@ function monoDataGrid(ds = null, opts = {}) {
3787
3944
  });
3788
3945
  ctrl.sorts = marked.map((m) => m.s);
3789
3946
  }
3947
+ /**
3948
+ * The source selector a column DECLARED via `sort.selector`, if any — what the
3949
+ * DataSource / array source orders by in place of the field. Resolved like
3950
+ * `declaredSortIndex`: central `props.th` config first, then the element's own
3951
+ * `:sort.prop`. A blank string counts as not declared.
3952
+ */
3953
+ function declaredSortSelector(field) {
3954
+ const sel = declaredSortConfig(field).selector;
3955
+ return typeof sel === "string" && sel.trim() ? sel.trim() : void 0;
3956
+ }
3957
+ /**
3958
+ * A column's declared sort OBJECT — the element's `:sort.prop` with central
3959
+ * `props.th` config merged over it (props win key by key, as everywhere). `{}`
3960
+ * for `sort: true` / no config.
3961
+ */
3962
+ function declaredSortConfig(field) {
3963
+ const asObj = (sort) => typeof sort === "object" && sort ? sort : {};
3964
+ let fromEl = {};
3965
+ for (const el of columnEls) if (el.field === field && typeof el.sort === "object" && el.sort) {
3966
+ fromEl = asObj(el.sort);
3967
+ break;
3968
+ }
3969
+ return {
3970
+ ...fromEl,
3971
+ ...asObj((elementProps.th ?? []).find((c) => c?.field === field)?.sort)
3972
+ };
3973
+ }
3974
+ /**
3975
+ * Whether a sorted column is sorted IN THE BROWSER — its selector fans out over a
3976
+ * collection (`[*]`, which no source can order by) or it declares an `aggregate`.
3977
+ */
3978
+ function isBrowserSortKey(field) {
3979
+ const sel = declaredSortSelector(field);
3980
+ return !!sel && parseFieldPath(sel).hasWildcard || !!declaredSortConfig(field).aggregate;
3981
+ }
3790
3982
  /** Keep the single-value mirrors pointing at the primary key. */
3791
3983
  function syncPrimarySort() {
3792
3984
  ctrl.sortField = ctrl.sorts[0]?.field ?? null;
@@ -3975,6 +4167,11 @@ function monoDataGrid(ds = null, opts = {}) {
3975
4167
  * see the intermediate full-list state.
3976
4168
  */
3977
4169
  async function getData() {
4170
+ if (browserRows && browserSortActive()) return [...browserRows];
4171
+ return readAllFromSource();
4172
+ }
4173
+ /** `getData`'s read — every matching row from the source, in its own order. */
4174
+ async function readAllFromSource() {
3978
4175
  const s = bound;
3979
4176
  if (!s) return [];
3980
4177
  const loadOptions = {};
@@ -110,6 +110,8 @@ export interface MonoSetSortOptions {
110
110
  */
111
111
  sticky?: boolean;
112
112
  }
113
+ /** How a browser-sorted column reduces a row's `[*]` matches — see `MonoColumnSort.aggregate`. */
114
+ export type MonoSortAggregate = 'sum' | 'min' | 'max' | 'avg' | 'count';
113
115
  export interface MonoColumnSort {
114
116
  /**
115
117
  * Whether the column is sortable at all. Defaults to `true` when a `sort` object
@@ -138,6 +140,40 @@ export interface MonoColumnSort {
138
140
  index?: number;
139
141
  /** Cycle `asc ↔ desc` only (never clears). */
140
142
  noClear?: boolean;
143
+ /**
144
+ * What the SOURCE sorts by when this column is sorted — instead of the column's
145
+ * `field`. The column keeps its identity (`field`: the arrow, precedence badge,
146
+ * `sorts`, `setSort(field)` and the header menu all still name it); only the
147
+ * selector handed to the DataSource / array source changes.
148
+ *
149
+ * For a column whose `field` is not a property the server knows — a value
150
+ * derived in the browser (`_Total`), or one whose display field differs from
151
+ * the one to order by — point it at one that is: a plain property
152
+ * (`'TotalBudget'`) or a navigation path (`'Brand.Nama'` → `$orderby=Brand/Nama`
153
+ * on a remote source, resolved per row on an array source).
154
+ *
155
+ * A plain selector must be something the source can order by. A selector with a
156
+ * `[*]` WILDCARD — `'Lines.[*].Amount'` — is the one OData cannot (no `$orderby`
157
+ * over a child collection), so it is sorted IN THE BROWSER instead: while such a
158
+ * column is sorted, the grid reads every row the current filter / search matches
159
+ * (with the source's `expand`), reduces each row's matches with `aggregate`
160
+ * (default `'sum'`), sorts, and pages in memory — see `aggregate` / `maxRows`.
161
+ * Flat paged tables only (not grouped, not scroll modes).
162
+ */
163
+ selector?: string;
164
+ /**
165
+ * How a browser-sorted column (a `[*]` `selector`, or any `selector` when this
166
+ * is set) reduces a row's matches to the value it sorts by. Default `'sum'`.
167
+ * Non-numeric matches are skipped by `sum` / `min` / `max` / `avg`; `count`
168
+ * counts every match.
169
+ */
170
+ aggregate?: MonoSortAggregate;
171
+ /**
172
+ * Browser-sorted columns only: the most rows the grid will read to sort. Above
173
+ * it the sort is refused (cleared, with a console warning) instead of pulling
174
+ * the whole table. Default `5000`.
175
+ */
176
+ maxRows?: number;
141
177
  /** Sortable UI is shown but inert. */
142
178
  disabled?: boolean;
143
179
  /**