@toolbox-web/grid 3.9.2 → 3.10.1
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/all.js +1 -1
- package/all.js.map +1 -1
- package/index.js +1 -1
- package/index.js.map +1 -1
- package/lib/core/types.d.ts +6 -3
- package/lib/features/clipboard.js.map +1 -1
- package/lib/features/column-virtualization.js.map +1 -1
- package/lib/features/context-menu.js.map +1 -1
- package/lib/features/editing.js.map +1 -1
- package/lib/features/export.js.map +1 -1
- package/lib/features/filtering.js.map +1 -1
- package/lib/features/grouping-columns.js.map +1 -1
- package/lib/features/grouping-rows.js.map +1 -1
- package/lib/features/master-detail.js.map +1 -1
- package/lib/features/multi-sort.js.map +1 -1
- package/lib/features/pinned-columns.js.map +1 -1
- package/lib/features/pinned-rows.js.map +1 -1
- package/lib/features/pivot.js.map +1 -1
- package/lib/features/print.js.map +1 -1
- package/lib/features/registry.js +1 -1
- package/lib/features/registry.js.map +1 -1
- package/lib/features/reorder-columns.js.map +1 -1
- package/lib/features/responsive.js.map +1 -1
- package/lib/features/row-drag-drop.js.map +1 -1
- package/lib/features/selection.js.map +1 -1
- package/lib/features/server-side.js.map +1 -1
- package/lib/features/shell.js.map +1 -1
- package/lib/features/sticky-rows.js.map +1 -1
- package/lib/features/tooltip.js.map +1 -1
- package/lib/features/tree.js.map +1 -1
- package/lib/features/undo-redo.js.map +1 -1
- package/lib/features/visibility.js.map +1 -1
- package/lib/plugins/clipboard/index.js.map +1 -1
- package/lib/plugins/column-virtualization/index.js.map +1 -1
- package/lib/plugins/context-menu/index.js.map +1 -1
- package/lib/plugins/editing/editors.d.ts +15 -0
- package/lib/plugins/editing/index.js +1 -1
- package/lib/plugins/editing/index.js.map +1 -1
- package/lib/plugins/editing/internal/helpers.d.ts +23 -0
- package/lib/plugins/editing/types.d.ts +16 -1
- package/lib/plugins/export/index.js +1 -1
- package/lib/plugins/export/index.js.map +1 -1
- package/lib/plugins/filtering/filter-model.d.ts +20 -1
- package/lib/plugins/filtering/filter-panel-default.d.ts +15 -1
- package/lib/plugins/filtering/index.d.ts +1 -1
- package/lib/plugins/filtering/index.js +1 -1
- package/lib/plugins/filtering/index.js.map +1 -1
- package/lib/plugins/filtering/types.d.ts +58 -3
- package/lib/plugins/grouping-columns/index.js.map +1 -1
- package/lib/plugins/grouping-rows/index.js +1 -1
- package/lib/plugins/grouping-rows/index.js.map +1 -1
- package/lib/plugins/hierarchy/index.js.map +1 -1
- package/lib/plugins/master-detail/index.js +1 -1
- package/lib/plugins/master-detail/index.js.map +1 -1
- package/lib/plugins/master-detail/master-detail-plugin.d.ts +6 -1
- package/lib/plugins/multi-sort/index.js.map +1 -1
- package/lib/plugins/pinned-columns/index.js.map +1 -1
- package/lib/plugins/pinned-rows/index.js.map +1 -1
- package/lib/plugins/pivot/index.js +1 -1
- package/lib/plugins/pivot/index.js.map +1 -1
- package/lib/plugins/pivot/pivot-plugin.d.ts +2 -0
- package/lib/plugins/pivot/types.d.ts +2 -0
- package/lib/plugins/print/index.js.map +1 -1
- package/lib/plugins/reorder-columns/index.js.map +1 -1
- package/lib/plugins/responsive/index.js.map +1 -1
- package/lib/plugins/row-drag-drop/index.js.map +1 -1
- package/lib/plugins/selection/index.js +1 -1
- package/lib/plugins/selection/index.js.map +1 -1
- package/lib/plugins/server-side/index.js.map +1 -1
- package/lib/plugins/shared/form-field-target.d.ts +10 -0
- package/lib/plugins/shared/format-value.d.ts +12 -0
- package/lib/plugins/shared/structural-toggle.d.ts +14 -0
- package/lib/plugins/shell/index.js.map +1 -1
- package/lib/plugins/sticky-rows/index.js.map +1 -1
- package/lib/plugins/tooltip/index.js.map +1 -1
- package/lib/plugins/tree/index.js +1 -1
- package/lib/plugins/tree/index.js.map +1 -1
- package/lib/plugins/undo-redo/index.d.ts +1 -1
- package/lib/plugins/undo-redo/index.js +1 -1
- package/lib/plugins/undo-redo/index.js.map +1 -1
- package/lib/plugins/undo-redo/types.d.ts +20 -2
- package/lib/plugins/undo-redo/undo-redo-plugin.d.ts +8 -0
- package/lib/plugins/visibility/index.js.map +1 -1
- package/package.json +1 -1
- package/umd/grid.all.umd.js +1 -1
- package/umd/grid.all.umd.js.map +1 -1
- package/umd/grid.umd.js +1 -1
- package/umd/grid.umd.js.map +1 -1
- package/umd/plugins/clipboard.umd.js.map +1 -1
- package/umd/plugins/column-virtualization.umd.js.map +1 -1
- package/umd/plugins/context-menu.umd.js.map +1 -1
- package/umd/plugins/editing.umd.js +1 -1
- package/umd/plugins/editing.umd.js.map +1 -1
- package/umd/plugins/export.umd.js +1 -1
- package/umd/plugins/export.umd.js.map +1 -1
- package/umd/plugins/filtering.umd.js +1 -1
- package/umd/plugins/filtering.umd.js.map +1 -1
- package/umd/plugins/grouping-columns.umd.js.map +1 -1
- package/umd/plugins/grouping-rows.umd.js +1 -1
- package/umd/plugins/grouping-rows.umd.js.map +1 -1
- package/umd/plugins/hierarchy.umd.js.map +1 -1
- package/umd/plugins/master-detail.umd.js +1 -1
- package/umd/plugins/master-detail.umd.js.map +1 -1
- package/umd/plugins/multi-sort.umd.js.map +1 -1
- package/umd/plugins/pinned-columns.umd.js.map +1 -1
- package/umd/plugins/pinned-rows.umd.js.map +1 -1
- package/umd/plugins/pivot.umd.js +1 -1
- package/umd/plugins/pivot.umd.js.map +1 -1
- package/umd/plugins/print.umd.js.map +1 -1
- package/umd/plugins/reorder-columns.umd.js.map +1 -1
- package/umd/plugins/responsive.umd.js.map +1 -1
- package/umd/plugins/row-drag-drop.umd.js.map +1 -1
- package/umd/plugins/selection.umd.js +1 -1
- package/umd/plugins/selection.umd.js.map +1 -1
- package/umd/plugins/server-side.umd.js.map +1 -1
- package/umd/plugins/shell.umd.js.map +1 -1
- package/umd/plugins/sticky-rows.umd.js.map +1 -1
- package/umd/plugins/tooltip.umd.js.map +1 -1
- package/umd/plugins/tree.umd.js +1 -1
- package/umd/plugins/tree.umd.js.map +1 -1
- package/umd/plugins/undo-redo.umd.js +1 -1
- package/umd/plugins/undo-redo.umd.js.map +1 -1
- package/umd/plugins/visibility.umd.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"multi-sort.umd.js","names":[],"sources":["../../../../../libs/grid/src/lib/core/internal/aria.ts","../../../../../libs/grid/src/lib/plugins/multi-sort/multi-sort.ts","../../../../../libs/grid/src/lib/plugins/multi-sort/multi-sort.css?inline","../../../../../libs/grid/src/lib/plugins/multi-sort/multi-sort-plugin.ts"],"sourcesContent":["/**\n * ARIA Accessibility Helpers\n *\n * Pure functions for managing ARIA attributes on grid elements.\n * Implements caching to avoid redundant DOM writes during scroll.\n *\n * @module internal/aria\n */\n\nimport { DEFAULT_A11Y_MESSAGES } from '../defaults';\nimport type { A11yMessages, GridConfig } from '../types';\n\n// #region Types\n\n/**\n * State for caching ARIA attributes to avoid redundant DOM writes.\n */\nexport interface AriaState {\n /** Last set row count */\n rowCount: number;\n /** Last set column count */\n colCount: number;\n /** Last set aria-label */\n ariaLabel: string | undefined;\n /** Last set aria-labelledby */\n ariaLabelledBy: string | undefined;\n /** Last set aria-describedby */\n ariaDescribedBy: string | undefined;\n /** Last set aria-roledescription */\n ariaRoleDescription: string | undefined;\n /** Last source row count announced via `dataLoaded`; used to suppress duplicate announcements */\n lastAnnouncedSourceCount: number;\n}\n\n/**\n * Create initial ARIA state.\n */\nexport function createAriaState(): AriaState {\n return {\n rowCount: -1,\n colCount: -1,\n ariaLabel: undefined,\n ariaLabelledBy: undefined,\n ariaDescribedBy: undefined,\n ariaRoleDescription: undefined,\n lastAnnouncedSourceCount: -1,\n };\n}\n\n// #endregion\n\n// #region Count Updates\n\n/**\n * Update ARIA row and column counts on grid elements.\n * Uses caching to avoid redundant DOM writes on every scroll frame.\n *\n * @param state - ARIA state for caching\n * @param rowsBodyEl - Element to set aria-rowcount/aria-colcount on\n * @param bodyEl - Element to set role=\"rowgroup\" on\n * @param rowCount - Current row count\n * @param colCount - Current column count\n * @returns true if anything was updated\n */\nexport function updateAriaCounts(\n state: AriaState,\n rowsBodyEl: HTMLElement | null,\n bodyEl: HTMLElement | null,\n rowCount: number,\n colCount: number,\n): boolean {\n // Skip if nothing changed (hot path optimization for scroll)\n if (rowCount === state.rowCount && colCount === state.colCount) {\n return false;\n }\n\n const prevRowCount = state.rowCount;\n state.rowCount = rowCount;\n state.colCount = colCount;\n\n // Update ARIA counts on inner grid element\n if (rowsBodyEl) {\n rowsBodyEl.setAttribute('aria-rowcount', String(rowCount));\n rowsBodyEl.setAttribute('aria-colcount', String(colCount));\n }\n\n // Set role=\"rowgroup\" on .rows only when there are rows (ARIA compliance)\n if (rowCount !== prevRowCount && bodyEl) {\n if (rowCount > 0) {\n bodyEl.setAttribute('role', 'rowgroup');\n } else {\n bodyEl.removeAttribute('role');\n }\n }\n\n return true;\n}\n\n// #endregion\n\n// #region Label Updates\n\n/**\n * Determine the effective aria-label for the grid.\n * Priority: explicit config > shell title > nothing\n *\n * The shell title is read from `config.shell.header.title`. The shell plugin\n * folds any light-DOM `<tbw-grid-header title>` into that field via its\n * `processConfig` hook (extraction #370), so core needs no plugin state here.\n *\n * @param config - Grid configuration (the merged effective config)\n * @returns The aria-label to use, or undefined\n */\nexport function getEffectiveAriaLabel<T>(config: GridConfig<T> | undefined): string | undefined {\n const explicitLabel = config?.gridAriaLabel;\n if (explicitLabel) return explicitLabel;\n\n return config?.shell?.header?.title ?? undefined;\n}\n\n/**\n * Update ARIA label and describedby attributes on the grid container.\n * Uses caching to avoid redundant DOM writes.\n *\n * @param state - ARIA state for caching\n * @param rowsBodyEl - Element to set aria-label/aria-describedby on\n * @param config - Grid configuration (the merged effective config)\n * @returns true if anything was updated\n */\nexport function updateAriaLabels<T>(\n state: AriaState,\n rowsBodyEl: HTMLElement | null,\n config: GridConfig<T> | undefined,\n): boolean {\n if (!rowsBodyEl) return false;\n\n let updated = false;\n\n // aria-labelledby wins per WAI-ARIA accessible-name precedence — when set,\n // `aria-label` is suppressed even if explicitly configured to avoid two\n // competing names being computed by AT.\n const ariaLabelledBy = config?.gridAriaLabelledBy;\n const effectiveLabel = ariaLabelledBy ? undefined : getEffectiveAriaLabel(config);\n\n // Update aria-labelledby only if changed\n if (ariaLabelledBy !== state.ariaLabelledBy) {\n state.ariaLabelledBy = ariaLabelledBy;\n if (ariaLabelledBy) {\n rowsBodyEl.setAttribute('aria-labelledby', ariaLabelledBy);\n } else {\n rowsBodyEl.removeAttribute('aria-labelledby');\n }\n updated = true;\n }\n\n // Update aria-label only if changed\n if (effectiveLabel !== state.ariaLabel) {\n state.ariaLabel = effectiveLabel;\n if (effectiveLabel) {\n rowsBodyEl.setAttribute('aria-label', effectiveLabel);\n } else {\n rowsBodyEl.removeAttribute('aria-label');\n }\n updated = true;\n }\n\n // Update aria-describedby only if changed\n const ariaDescribedBy = config?.gridAriaDescribedBy;\n if (ariaDescribedBy !== state.ariaDescribedBy) {\n state.ariaDescribedBy = ariaDescribedBy;\n if (ariaDescribedBy) {\n rowsBodyEl.setAttribute('aria-describedby', ariaDescribedBy);\n } else {\n rowsBodyEl.removeAttribute('aria-describedby');\n }\n updated = true;\n }\n\n // Update aria-roledescription only if changed. Apps can override the\n // announced role name (e.g. localized \"Tabell\" instead of generic \"grid\").\n const ariaRoleDescription = config?.gridAriaRoleDescription;\n if (ariaRoleDescription !== state.ariaRoleDescription) {\n state.ariaRoleDescription = ariaRoleDescription;\n if (ariaRoleDescription) {\n rowsBodyEl.setAttribute('aria-roledescription', ariaRoleDescription);\n } else {\n rowsBodyEl.removeAttribute('aria-roledescription');\n }\n updated = true;\n }\n\n return updated;\n}\n\n// #endregion\n\n// #region Live Announcements\n\n/**\n * Announce a message to screen readers via the grid's aria-live region.\n * Clears then sets text to ensure repeated messages are re-announced.\n *\n * Respects `effectiveConfig.a11y.announcements` — if set to `false`,\n * the announcement is silently skipped.\n *\n * @param gridEl - The grid host element (or any ancestor containing `.tbw-sr-only`)\n * @param message - The message to announce\n */\nexport function announce(gridEl: HTMLElement, message: string): void {\n // Check if announcements are disabled via config.\n // gridEl is always a DataGridElement at runtime — use property access safely.\n if (!gridEl) return;\n const config =\n 'effectiveConfig' in gridEl\n ? (gridEl as HTMLElement & { effectiveConfig?: GridConfig }).effectiveConfig\n : undefined;\n if (config?.a11y?.announcements === false) return;\n\n const el = gridEl.querySelector?.('.tbw-sr-only');\n if (!el) return;\n // Clear first so identical consecutive messages are still announced\n el.textContent = '';\n // `requestAnimationFrame` can be undefined if a debounced announcement fires\n // after the DOM/host environment is torn down (notably in unit tests). Fall\n // back to a direct assignment so `announce` never throws out of band.\n if (typeof requestAnimationFrame === 'function') {\n requestAnimationFrame(() => {\n el.textContent = message;\n });\n } else {\n el.textContent = message;\n }\n}\n\n/**\n * Get an announcement message, preferring custom overrides from config.\n * Falls back to default English messages when no override is provided.\n *\n * @param gridEl - The grid host element\n * @param key - The message key from A11yMessages\n * @param args - Arguments to pass to the message function\n */\nexport function getA11yMessage<K extends keyof A11yMessages>(\n gridEl: HTMLElement,\n key: K,\n ...args: Parameters<Required<A11yMessages>[K]>\n): string {\n type MessageFn = (...a: Parameters<Required<A11yMessages>[K]>) => string;\n const config =\n gridEl && 'effectiveConfig' in gridEl\n ? (gridEl as HTMLElement & { effectiveConfig?: GridConfig }).effectiveConfig\n : undefined;\n const customFn = config?.a11y?.messages?.[key] as MessageFn | undefined;\n if (customFn) return customFn(...args);\n return (DEFAULT_A11Y_MESSAGES[key] as MessageFn)(...args);\n}\n\n/**\n * Announce a `dataLoaded` message — but only when the source row count has\n * actually changed since the last announcement. This prevents announcement\n * spam from internal `data-change` emits triggered by sort/filter/edit, which\n * have their own dedicated announcements.\n *\n * @param gridEl - The grid host element\n * @param state - ARIA state (tracks last announced count)\n * @param sourceRowCount - Current source row count\n */\nexport function announceDataLoaded(gridEl: HTMLElement, state: AriaState, sourceRowCount: number): void {\n if (sourceRowCount === state.lastAnnouncedSourceCount) return;\n const previousAnnouncedSourceCount = state.lastAnnouncedSourceCount;\n state.lastAnnouncedSourceCount = sourceRowCount;\n // Skip only the initial sentinel → 0 transition (empty grid mount). A later\n // transition to 0 (e.g. data cleared / reloaded to an empty result set) should\n // still announce so screen-reader users hear that the grid is now empty.\n if (sourceRowCount === 0 && previousAnnouncedSourceCount < 0) return;\n announce(gridEl, getA11yMessage(gridEl, 'dataLoaded', sourceRowCount));\n}\n\n// #endregion\n","/**\n * Multi-Sort Core Logic\n *\n * Pure functions for multi-column sorting operations.\n */\n\nimport { resolveCellValue } from '../../core/internal/value-accessor';\nimport type { ColumnConfig } from '../../core/types';\nimport type { SortModel } from './types';\n\n// Module-level cached collator. `String.prototype.localeCompare(b)` (no args)\n// lazily allocates a fresh Intl.Collator on every call in V8 — measurable on\n// large datasets where string comparisons dominate the secondary sort key.\n// A single cached `Intl.Collator` reused via `.compare(a, b)` is ~3-5x faster.\nconst stringCollator = new Intl.Collator(undefined, { sensitivity: 'variant' });\n\n/**\n * Apply multiple sort columns to a row array.\n * Sorts are applied in order - first sort has highest priority.\n *\n * **Non-mutating:** This function copies the input array before sorting and\n * returns the new array. Internal callers that already own a mutable copy\n * (e.g. inside `MultiSortPlugin.processRows`, where `plugin-manager` has already\n * cloned the row pipeline input) should call {@link sortRowsInPlace} directly\n * to avoid a redundant allocation in the hot path.\n *\n * @param rows - Array of row objects to sort\n * @param sorts - Ordered array of sort configurations\n * @param columns - Column configurations (for custom comparators)\n * @returns New sorted array (does not mutate original)\n */\nexport function applySorts<TRow = unknown>(rows: TRow[], sorts: SortModel[], columns: ColumnConfig<TRow>[]): TRow[] {\n if (!sorts.length) return [...rows];\n\n const copy = [...rows];\n sortRowsInPlace(copy, sorts, columns);\n return copy;\n}\n\n/**\n * Sort an array in-place using multiple sort columns.\n * Pre-resolves column comparators to avoid O(n·log·n·m) column lookups\n * inside the comparator.\n * @internal\n */\nexport function sortRowsInPlace<TRow = unknown>(rows: TRow[], sorts: SortModel[], columns: ColumnConfig<TRow>[]): void {\n if (!sorts.length) return;\n\n // Pre-resolve comparator chain — avoids columns.find() on every pair comparison\n const chain = sorts.map((sort) => {\n const col = columns.find((c) => c.field === sort.field);\n const comparator = col?.sortComparator ?? defaultComparator;\n // Pre-bind the value getter per link so the hot comparator path doesn't\n // re-evaluate `column?.valueAccessor` on every pair comparison. For the\n // common case (no valueAccessor) this collapses to a single property read.\n // Documented precedence: sortComparator → valueAccessor → field.\n const field = sort.field;\n const getValue: (row: TRow) => unknown = col?.valueAccessor\n ? (row: TRow) => resolveCellValue(row, col)\n : (row: TRow) => (row as Record<string, unknown>)[field];\n return {\n field,\n asc: sort.direction === 'asc',\n comparator,\n getValue,\n // Auto-pin `__loading` placeholder rows (e.g. ServerSidePlugin under `sortMode: 'local'`)\n // to the end ONLY when no custom comparator is configured. Custom comparators receive\n // the row pair as 3rd/4th args and own placeholder handling themselves.\n pinPlaceholders: !col?.sortComparator,\n };\n });\n\n // Only enable per-pair pin checks if the dataset *actually* contains loading\n // placeholders — for the common case (no `__loading` rows) we skip ~n·log·n\n // property reads + branch on the hot path. Single O(n) scan up front.\n const chainNeedsPin = chain.some((l) => l.pinPlaceholders);\n const needsPinScan = chainNeedsPin && hasLoadingRow(rows);\n\n // Schwartzian transform: extract sort keys ONCE per row up front, then sort\n // indices by the cached keys. `Array.prototype.sort` calls the comparator\n // ~n·log·n times; without caching, each call would re-extract every key\n // (2 sides × k columns = 2·k property reads / accessor invocations per\n // compare). Caching collapses ~2·k·n·log·n extractions to k·n. For 10K\n // rows × 2 keys that's 20K extractions instead of ~260K, and the comparator\n // body becomes a pure key-vs-key comparison the JIT can optimize tightly.\n const n = rows.length;\n const k = chain.length;\n const keys: unknown[] = new Array(n * k);\n for (let i = 0; i < n; i++) {\n const row = rows[i];\n const base = i * k;\n for (let j = 0; j < k; j++) {\n keys[base + j] = chain[j].getValue(row);\n }\n }\n\n // Index array — sort this and then permute `rows` in one pass at the end.\n // Typed array when it fits; falls back to plain Array for very large datasets.\n const indices: Uint32Array | number[] = n <= 0xffffffff ? new Uint32Array(n) : new Array(n);\n for (let i = 0; i < n; i++) indices[i] = i;\n\n const compareIndices = (ia: number, ib: number): number => {\n if (needsPinScan) {\n const pinned = pinLoadingRows(rows[ia], rows[ib]);\n if (pinned !== 0) return pinned;\n }\n const baseA = ia * k;\n const baseB = ib * k;\n for (let j = 0; j < k; j++) {\n const link = chain[j];\n // Custom comparators receive the row pair as 3rd/4th args; defaultComparator\n // ignores them. Keys are already cached so no extraction happens here.\n const result = link.comparator(keys[baseA + j], keys[baseB + j], rows[ia], rows[ib]);\n if (result !== 0) return link.asc ? result : -result;\n }\n return 0;\n };\n\n if (indices instanceof Uint32Array) {\n indices.sort(compareIndices);\n } else {\n (indices as number[]).sort(compareIndices);\n }\n\n // Permute rows in-place using the sorted index array. Single allocation of\n // a snapshot; then write back. Done in two passes to avoid clobbering.\n const snapshot = rows.slice();\n for (let i = 0; i < n; i++) {\n rows[i] = snapshot[indices[i]];\n }\n}\n\n/**\n * O(n) scan checking whether any row has the `__loading` placeholder marker.\n * Cheap relative to the O(n·log·n) sort, and lets us elide per-pair pin checks\n * when the dataset has no placeholders (the overwhelmingly common case).\n * @internal\n */\nfunction hasLoadingRow(rows: readonly unknown[]): boolean {\n for (let i = 0; i < rows.length; i++) {\n if ((rows[i] as { __loading?: unknown } | null)?.__loading === true) return true;\n }\n return false;\n}\n\n/**\n * Pin `__loading` placeholder rows (e.g. ServerSidePlugin under `sortMode: 'local'`)\n * to the end of the sorted array regardless of sort direction.\n *\n * Returns 0 when neither row is a placeholder, +1 when `a` is, -1 when `b` is.\n *\n * @internal\n */\nfunction pinLoadingRows(a: unknown, b: unknown): number {\n const aLoading = (a as { __loading?: unknown } | null)?.__loading === true;\n const bLoading = (b as { __loading?: unknown } | null)?.__loading === true;\n if (aLoading === bLoading) return 0;\n return aLoading ? 1 : -1;\n}\n\n/**\n * Default comparator for sorting values.\n * Handles nulls, numbers, dates, and strings.\n *\n * @param a - First value\n * @param b - Second value\n * @returns Comparison result (-1, 0, 1)\n */\nexport function defaultComparator(a: unknown, b: unknown): number {\n // Handle nulls/undefined - push to end\n if (a == null && b == null) return 0;\n if (a == null) return 1;\n if (b == null) return -1;\n\n // Type-aware comparison\n if (typeof a === 'number' && typeof b === 'number') {\n return a - b;\n }\n\n if (a instanceof Date && b instanceof Date) {\n return a.getTime() - b.getTime();\n }\n\n // Boolean comparison\n if (typeof a === 'boolean' && typeof b === 'boolean') {\n return a === b ? 0 : a ? -1 : 1;\n }\n\n // String comparison (fallback). Uses a module-level cached Intl.Collator\n // — see `stringCollator` declaration for rationale.\n return stringCollator.compare(typeof a === 'string' ? a : String(a), typeof b === 'string' ? b : String(b));\n}\n\n/**\n * Toggle sort state for a field.\n * With shift key: adds/toggles in multi-sort list\n * Without shift key: replaces entire sort with single column\n *\n * @param current - Current sort model\n * @param field - Field to toggle\n * @param shiftKey - Whether shift key is held (multi-sort mode)\n * @param maxColumns - Maximum columns allowed in sort\n * @returns New sort model\n */\nexport function toggleSort(current: SortModel[], field: string, shiftKey: boolean, maxColumns: number): SortModel[] {\n const existing = current.find((s) => s.field === field);\n\n if (shiftKey) {\n // Multi-sort: add/toggle in list\n if (existing) {\n if (existing.direction === 'asc') {\n // Flip to descending\n return current.map((s) => (s.field === field ? { ...s, direction: 'desc' as const } : s));\n } else {\n // Remove from sort\n return current.filter((s) => s.field !== field);\n }\n } else if (current.length < maxColumns) {\n // Add new sort column\n return [...current, { field, direction: 'asc' as const }];\n }\n // Max columns reached, return unchanged\n return current;\n } else {\n // Single sort: replace all\n if (existing?.direction === 'asc') {\n return [{ field, direction: 'desc' }];\n } else if (existing?.direction === 'desc') {\n return [];\n }\n return [{ field, direction: 'asc' }];\n }\n}\n\n/**\n * Get the sort index (1-based) for a field in the sort model.\n * Returns undefined if the field is not in the sort model.\n *\n * @param sortModel - Current sort model\n * @param field - Field to check\n * @returns 1-based index or undefined\n */\nexport function getSortIndex(sortModel: SortModel[], field: string): number | undefined {\n const index = sortModel.findIndex((s) => s.field === field);\n return index >= 0 ? index + 1 : undefined;\n}\n\n/**\n * Get the sort direction for a field in the sort model.\n *\n * @param sortModel - Current sort model\n * @param field - Field to check\n * @returns Sort direction or undefined if not sorted\n */\nexport function getSortDirection(sortModel: SortModel[], field: string): 'asc' | 'desc' | undefined {\n return sortModel.find((s) => s.field === field)?.direction;\n}\n","@layer tbw-plugins {\n .header-cell[data-sort='asc']::after {\n content: '↑';\n margin-left: var(--tbw-spacing-xs, 0.25em);\n opacity: 0.8;\n }\n .header-cell[data-sort='desc']::after {\n content: '↓';\n margin-left: var(--tbw-spacing-xs, 0.25em);\n opacity: 0.8;\n }\n .sort-indicator {\n margin-left: var(--tbw-spacing-xs, 0.25em);\n opacity: 0.8;\n }\n .sort-index {\n font-size: var(--tbw-font-size-2xs, 0.7em);\n background: var(--tbw-multi-sort-badge-bg, var(--tbw-color-panel-bg));\n color: var(--tbw-multi-sort-badge-color, var(--tbw-color-fg));\n border-radius: 50%;\n width: var(--tbw-multi-sort-badge-size, 1em);\n height: var(--tbw-multi-sort-badge-size, 1em);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n margin-left: var(--tbw-spacing-xs, 0.125em);\n font-weight: 600;\n }\n} /* End @layer tbw-plugins */\n","/**\n * Multi-Sort Plugin (Class-based)\n *\n * Provides multi-column sorting capabilities for tbw-grid.\n * Supports shift+click for adding secondary sort columns.\n */\n\nimport { announce, getA11yMessage } from '../../core/internal/aria';\nimport { BaseGridPlugin, HeaderClickEvent, type PluginManifest, type PluginQuery } from '../../core/plugin/base-plugin';\nimport type { ColumnState, GridHost } from '../../core/types';\nimport { getSortDirection, getSortIndex, sortRowsInPlace, toggleSort } from './multi-sort';\nimport styles from './multi-sort.css?inline';\nimport type { MultiSortConfig, SortModel } from './types';\n\n/**\n * Multi-Sort Plugin for tbw-grid\n *\n * Enables sorting by multiple columns at once—hold Shift and click additional column\n * headers to build up a sort stack. Priority badges show the sort order, so users\n * always know which column takes precedence.\n *\n * ## Installation\n *\n * ```ts\n * import { MultiSortPlugin } from '@toolbox-web/grid/plugins/multi-sort';\n * ```\n *\n * ## Keyboard Shortcuts\n *\n * | Shortcut | Action |\n * |----------|--------|\n * | `Click header` | Sort by column (clears other sorts) |\n * | `Shift + Click` | Add column to multi-sort stack |\n * | `Ctrl + Click` | Toggle sort direction |\n *\n * ## Events\n *\n * | Event | Detail | Description |\n * |-------|--------|-------------|\n * | `sort-change` | `{ sortModel: SortModel[] }` | Fired when sort changes |\n *\n * @example Basic Multi-Column Sorting\n * ```ts\n * import { queryGrid } from '@toolbox-web/grid';\n * import { MultiSortPlugin } from '@toolbox-web/grid/plugins/multi-sort';\n *\n * const grid = queryGrid('tbw-grid');\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', header: 'Name', sortable: true },\n * { field: 'department', header: 'Department', sortable: true },\n * { field: 'salary', header: 'Salary', type: 'number', sortable: true },\n * ],\n * plugins: [new MultiSortPlugin({ maxSortColumns: 3, showSortIndex: true })],\n * };\n *\n * grid.on('sort-change', ({ sortModel }) => {\n * console.log('Active sorts:', sortModel);\n * });\n * ```\n *\n * @example Initial Sort Configuration\n * ```ts\n * new MultiSortPlugin({\n * initialSort: [\n * { field: 'department', direction: 'asc' },\n * { field: 'salary', direction: 'desc' },\n * ],\n * })\n * ```\n *\n * @see {@link MultiSortConfig} for all configuration options\n * @see {@link SortModel} for the sort model structure\n *\n * @internal Extends BaseGridPlugin\n * @since 0.1.1\n */\nexport class MultiSortPlugin extends BaseGridPlugin<MultiSortConfig> {\n /**\n * Plugin manifest declaring query types this plugin responds to.\n * @internal\n */\n static override readonly manifest: PluginManifest = {\n queries: [\n { type: 'sort:get-model', description: 'Returns the current multi-sort model as SortModel[]' },\n { type: 'sort:set-model', description: 'Sets the multi-sort model from context (SortModel[])' },\n ],\n };\n\n /** @internal */\n readonly name = 'multiSort';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<MultiSortConfig> {\n return {\n maxSortColumns: 3,\n showSortIndex: true,\n };\n }\n\n // #region Internal State\n private sortModel: SortModel[] = [];\n /** Cached sort result — returned as-is while a row edit is active to prevent\n * the edited row from jumping to a new sorted position mid-edit. Row data\n * mutations are still visible because the array holds shared object refs. */\n private cachedSortResult: unknown[] | null = null;\n /** Guards deferred sort-change broadcast during applyColumnState batch. */\n #pendingStateBroadcast = false;\n /** Snapshot of sortModel taken at the start of an applyColumnState batch,\n * used by the deferred broadcast to skip when the model didn't change. */\n #preBatchSortModel: SortModel[] = [];\n\n /** Typed internal grid accessor. */\n get #internalGrid(): GridHost {\n return this.grid;\n }\n\n /**\n * Clear the core `_sortState` so that only this plugin's `processRows`\n * sorting applies. `ConfigManager.applyState()` always sets the core sort\n * state when restoring from storage, even when a plugin handles sorting.\n * Without this, the stale core state leaks into `collectState()` and\n * `reapplyCoreSort()` after the plugin clears its own model.\n */\n private clearCoreSortState(): void {\n this.#internalGrid._sortState = null;\n }\n\n /**\n * Remove sorts on fields that are owned by the grouping plugin.\n * GroupingRowsPlugin handles group header ordering independently, so\n * multi-sort should only sort by non-grouped data columns.\n */\n #filterGroupedFields(model: SortModel[]): SortModel[] {\n const results = this.grid?.query?.('grouping:get-grouped-fields', null);\n if (!Array.isArray(results) || results.length === 0) return model;\n\n const groupedFields = results[0] as string[];\n if (!Array.isArray(groupedFields) || groupedFields.length === 0) return model;\n\n const groupedSet = new Set(groupedFields);\n const filtered = model.filter((s) => !groupedSet.has(s.field));\n return filtered.length === model.length ? model : filtered;\n }\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override detach(): void {\n this.sortModel = [];\n this.cachedSortResult = null;\n }\n // #endregion\n\n // #region Query System\n\n /** @internal */\n override handleQuery(query: PluginQuery): unknown {\n switch (query.type) {\n case 'sort:get-model':\n return [...this.sortModel];\n case 'sort:set-model': {\n const model = query.context;\n if (!Array.isArray(model)) return false;\n this.sortModel = [...model] as SortModel[];\n this.clearCoreSortState();\n this.broadcast('sort-change', { sortModel: [...this.sortModel] });\n this.requestRender();\n return true;\n }\n default:\n return undefined;\n }\n }\n\n // #endregion\n\n // #region Hooks\n\n /** @internal */\n override processRows(rows: readonly unknown[]): unknown[] {\n // Nothing to sort: hand the input on untouched. A copy here would give every\n // render an O(n) slice and make ServerSide's `_rows !== managedNodes` check\n // treat each block load as a derived row model (#521).\n if (this.sortModel.length === 0) {\n this.cachedSortResult = null;\n return rows as unknown[];\n }\n\n // A ServerSide data source in `sortMode: 'server'` already sorted the rows\n // (via `getRows({ sortModel })`) with its own collation / computed keys, and\n // its loading placeholders sit at their block positions — keep that order (#521).\n if (this.grid?.query?.<'server' | 'local'>('datasource:sort-mode', null)?.[0] === 'server') {\n this.cachedSortResult = null;\n return rows as unknown[];\n }\n\n // Freeze sort order while a row is actively being edited (row mode only).\n // Re-sorting mid-edit would move the edited row to a new index while the\n // editors remain at the old position, causing data/UI mismatch.\n // In grid mode (_isGridEditMode) sorting is safe — afterCellRender\n // re-injects editors into the re-sorted cells.\n // We return the cached previous sort result (same object references, so\n // in-place value mutations are already visible) instead of unsorted input.\n const grid = this.#internalGrid;\n if (!grid._isGridEditMode && typeof grid._activeEditRows === 'number' && grid._activeEditRows !== -1) {\n if (this.cachedSortResult && this.cachedSortResult.length === rows.length) {\n return [...this.cachedSortResult];\n }\n }\n\n // Sort a copy: with no earlier copying stage the input IS the consumer's\n // `rows` array (`grid.sourceRows`) — sorting it in place reordered their data\n // and left nothing to restore when the sort is cleared. The copy is O(n)\n // next to the O(n log n) sort. A pre-resolved comparator chain avoids\n // column lookup on every pair comparison.\n // Exclude fields owned by the grouping plugin — group header order is handled\n // by GroupingRowsPlugin, so multi-sort should only affect within-group data order.\n const effectiveModel = this.#filterGroupedFields(this.sortModel);\n\n const mutableRows = [...rows];\n if (effectiveModel.length > 0) {\n sortRowsInPlace(mutableRows, effectiveModel, this.columns);\n }\n this.cachedSortResult = mutableRows;\n return mutableRows;\n }\n\n /** @internal */\n override onHeaderClick(event: HeaderClickEvent): boolean {\n const column = this.columns.find((c) => c.field === event.field);\n if (!column?.sortable) return false;\n\n const shiftKey = event.originalEvent.shiftKey;\n const maxColumns = this.config.maxSortColumns ?? 3;\n\n this.sortModel = toggleSort(this.sortModel, event.field, shiftKey, maxColumns);\n this.clearCoreSortState();\n\n this.broadcast('sort-change', { sortModel: [...this.sortModel] });\n this.requestRender();\n this.grid?.requestStateChange?.();\n\n // Announce for screen readers\n if (this.sortModel.length > 0) {\n const labels = this.sortModel.map((s) => {\n const col = this.columns.find((c) => c.field === s.field);\n return `${col?.header ?? s.field} ${s.direction === 'asc' ? 'ascending' : 'descending'}`;\n });\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortApplied', labels.join(', then '), ''));\n } else {\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortCleared'));\n }\n\n return true;\n }\n\n /** @internal */\n override afterRender(): void {\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n const showIndex = this.config.showSortIndex !== false;\n\n const headerCells = gridEl.querySelectorAll('.header-row .cell[data-field]');\n headerCells.forEach((cell) => {\n const field = cell.getAttribute('data-field');\n if (!field) return;\n\n const sortIndex = getSortIndex(this.sortModel, field);\n const sortDir = getSortDirection(this.sortModel, field);\n\n // Remove existing sort index badge (always clean up)\n cell.querySelector('.sort-index')?.remove();\n\n if (sortDir) {\n const indicator = this.updateSortIndicator(cell, sortDir);\n\n // Add sort index badge if multiple columns sorted and showSortIndex is enabled\n if (showIndex && this.sortModel.length > 1 && sortIndex !== undefined) {\n const badge = document.createElement('span');\n badge.className = 'sort-index';\n badge.textContent = String(sortIndex);\n if (indicator.nextSibling) {\n cell.insertBefore(badge, indicator.nextSibling);\n } else {\n cell.appendChild(badge);\n }\n }\n } else if (cell.classList.contains('sortable')) {\n this.updateSortIndicator(cell, null);\n }\n });\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Get the current sort model.\n * @returns Copy of the current sort model\n */\n getSortModel(): SortModel[] {\n return [...this.sortModel];\n }\n\n /**\n * Set the sort model programmatically.\n * @param model - New sort model to apply\n */\n setSortModel(model: SortModel[]): void {\n this.sortModel = [...model];\n this.clearCoreSortState();\n this.broadcast('sort-change', { sortModel: [...model] });\n this.requestRender();\n this.grid?.requestStateChange?.();\n if (model.length > 0) {\n const labels = model.map((s) => {\n const col = this.columns.find((c) => c.field === s.field);\n return `${col?.header ?? s.field} ${s.direction === 'asc' ? 'ascending' : 'descending'}`;\n });\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortApplied', labels.join(', then '), ''));\n }\n }\n\n /**\n * Clear all sorting.\n */\n clearSort(): void {\n this.sortModel = [];\n this.clearCoreSortState();\n this.broadcast('sort-change', { sortModel: [] });\n this.requestRender();\n this.grid?.requestStateChange?.();\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortCleared'));\n }\n\n /**\n * Get the sort index (1-based) for a specific field.\n * @param field - Field to check\n * @returns 1-based index or undefined if not sorted\n */\n getSortIndex(field: string): number | undefined {\n return getSortIndex(this.sortModel, field);\n }\n\n /**\n * Get the sort direction for a specific field.\n * @param field - Field to check\n * @returns Sort direction or undefined if not sorted\n */\n getSortDirection(field: string): 'asc' | 'desc' | undefined {\n return getSortDirection(this.sortModel, field);\n }\n // #endregion\n\n // #region Column State Hooks\n\n /**\n * Return sort state for a column if it's in the sort model.\n * @internal\n */\n override getColumnState(field: string): Partial<ColumnState> | undefined {\n const index = this.sortModel.findIndex((s) => s.field === field);\n if (index === -1) return undefined;\n\n const sortEntry = this.sortModel[index];\n return {\n sort: {\n direction: sortEntry.direction,\n priority: index,\n },\n };\n }\n\n /**\n * Apply sort state from column state.\n * Rebuilds the sort model from all column states.\n * @internal\n */\n override applyColumnState(field: string, state: ColumnState): void {\n // Snapshot model before mutation so the microtask can skip a no-op\n // broadcast/render when nothing actually changed.\n if (!this.#pendingStateBroadcast) {\n this.#preBatchSortModel = [...this.sortModel];\n }\n\n if (!state.sort) {\n // Remove this field from sortModel if it exists\n this.sortModel = this.sortModel.filter((s) => s.field !== field);\n } else {\n // Find existing entry or add new one\n const existingIndex = this.sortModel.findIndex((s) => s.field === field);\n const newEntry: SortModel = {\n field,\n direction: state.sort.direction,\n };\n\n if (existingIndex !== -1) {\n // Update existing entry\n this.sortModel[existingIndex] = newEntry;\n } else {\n // Add at the correct priority position\n this.sortModel.splice(state.sort.priority, 0, newEntry);\n }\n }\n\n // Clear core sort state — this plugin exclusively handles sorting via\n // processRows. The core _sortState is set by ConfigManager.applyState()\n // before plugins run; null it so reapplyCoreSort() is a no-op.\n this.clearCoreSortState();\n\n // Broadcast sort-change + force a row re-render after state restoration.\n // Both are deferred to a microtask so per-column calls coalesce into one\n // event and one render. The render handles the \"sort cleared\" case that\n // core's width-only fast path cannot detect (core only inspects the\n // *incoming* state, not what the plugin's model became). Skip entirely\n // when the model is unchanged so unrelated state restores stay cheap.\n if (!this.#pendingStateBroadcast) {\n this.#pendingStateBroadcast = true;\n queueMicrotask(() => {\n this.#pendingStateBroadcast = false;\n const prev = this.#preBatchSortModel;\n this.#preBatchSortModel = [];\n if (sortModelEquals(prev, this.sortModel)) return;\n this.broadcast('sort-change', { sortModel: [...this.sortModel] });\n this.requestRender();\n });\n }\n }\n // #endregion\n}\n\n/** Shallow equality for two sort models (order-sensitive). */\nfunction sortModelEquals(a: SortModel[], b: SortModel[]): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i].field !== b[i].field || a[i].direction !== b[i].direction) return false;\n }\n return true;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"multi-sort.umd.js","names":["DEFAULT_A11Y_MESSAGES","resolveCellValue","BaseGridPlugin","styles","global","factory","exports","module","require","define","amd","globalThis","self","TbwGridPlugin_multiSort","TbwGrid","this","_toolbox_web_grid","announce","gridEl","message","effectiveConfig","a11y","announcements","el","querySelector","textContent","requestAnimationFrame","getA11yMessage","key","args","customFn","messages","DEFAULT_A11Y_MESSAGES","Object","defineProperty","Symbol","toStringTag","value","stringCollator","Intl","Collator","sensitivity","defaultComparator","a","b","Date","getTime","compare","String","getSortIndex","sortModel","field","index","findIndex","s","getSortDirection","find","direction","MultiSortPlugin","BaseGridPlugin","static","queries","type","description","name","styles","defaultConfig","maxSortColumns","showSortIndex","cachedSortResult","pendingStateBroadcast","preBatchSortModel","internalGrid","grid","clearCoreSortState","_sortState","filterGroupedFields","model","results","query","Array","isArray","length","groupedFields","groupedSet","Set","filtered","filter","has","detach","handleQuery","context","broadcast","requestRender","processRows","rows","_isGridEditMode","_activeEditRows","effectiveModel","mutableRows","sorts","columns","chain","map","sort","col","c","comparator","sortComparator","getValue","valueAccessor","row","resolveCellValue","asc","pinPlaceholders","needsPinScan","some","l","i","__loading","hasLoadingRow","n","k","keys","base","j","indices","Uint32Array","compareIndices","ia","ib","pinned","aLoading","pinLoadingRows","baseA","baseB","link","result","snapshot","slice","sortRowsInPlace","onHeaderClick","event","sortable","shiftKey","originalEvent","maxColumns","config","current","existing","toggleSort","requestStateChange","labels","header","gridElement","join","afterRender","showIndex","querySelectorAll","forEach","cell","getAttribute","sortIndex","sortDir","remove","indicator","updateSortIndicator","badge","document","createElement","className","nextSibling","insertBefore","appendChild","classList","contains","getSortModel","setSortModel","clearSort","getColumnState","priority","applyColumnState","state","existingIndex","newEntry","splice","queueMicrotask","prev","sortModelEquals"],"sources":["../../../../../libs/grid/src/lib/core/internal/aria.ts","../../../../../libs/grid/src/lib/plugins/multi-sort/multi-sort.ts","../../../../../libs/grid/src/lib/plugins/multi-sort/multi-sort.css?inline","../../../../../libs/grid/src/lib/plugins/multi-sort/multi-sort-plugin.ts"],"sourcesContent":["/**\n * ARIA Accessibility Helpers\n *\n * Pure functions for managing ARIA attributes on grid elements.\n * Implements caching to avoid redundant DOM writes during scroll.\n *\n * @module internal/aria\n */\n\nimport { DEFAULT_A11Y_MESSAGES } from '../defaults';\nimport type { A11yMessages, GridConfig } from '../types';\n\n// #region Types\n\n/**\n * State for caching ARIA attributes to avoid redundant DOM writes.\n */\nexport interface AriaState {\n /** Last set row count */\n rowCount: number;\n /** Last set column count */\n colCount: number;\n /** Last set aria-label */\n ariaLabel: string | undefined;\n /** Last set aria-labelledby */\n ariaLabelledBy: string | undefined;\n /** Last set aria-describedby */\n ariaDescribedBy: string | undefined;\n /** Last set aria-roledescription */\n ariaRoleDescription: string | undefined;\n /** Last source row count announced via `dataLoaded`; used to suppress duplicate announcements */\n lastAnnouncedSourceCount: number;\n}\n\n/**\n * Create initial ARIA state.\n */\nexport function createAriaState(): AriaState {\n return {\n rowCount: -1,\n colCount: -1,\n ariaLabel: undefined,\n ariaLabelledBy: undefined,\n ariaDescribedBy: undefined,\n ariaRoleDescription: undefined,\n lastAnnouncedSourceCount: -1,\n };\n}\n\n// #endregion\n\n// #region Count Updates\n\n/**\n * Update ARIA row and column counts on grid elements.\n * Uses caching to avoid redundant DOM writes on every scroll frame.\n *\n * @param state - ARIA state for caching\n * @param rowsBodyEl - Element to set aria-rowcount/aria-colcount on\n * @param bodyEl - Element to set role=\"rowgroup\" on\n * @param rowCount - Current row count\n * @param colCount - Current column count\n * @returns true if anything was updated\n */\nexport function updateAriaCounts(\n state: AriaState,\n rowsBodyEl: HTMLElement | null,\n bodyEl: HTMLElement | null,\n rowCount: number,\n colCount: number,\n): boolean {\n // Skip if nothing changed (hot path optimization for scroll)\n if (rowCount === state.rowCount && colCount === state.colCount) {\n return false;\n }\n\n const prevRowCount = state.rowCount;\n state.rowCount = rowCount;\n state.colCount = colCount;\n\n // Update ARIA counts on inner grid element\n if (rowsBodyEl) {\n rowsBodyEl.setAttribute('aria-rowcount', String(rowCount));\n rowsBodyEl.setAttribute('aria-colcount', String(colCount));\n }\n\n // Set role=\"rowgroup\" on .rows only when there are rows (ARIA compliance)\n if (rowCount !== prevRowCount && bodyEl) {\n if (rowCount > 0) {\n bodyEl.setAttribute('role', 'rowgroup');\n } else {\n bodyEl.removeAttribute('role');\n }\n }\n\n return true;\n}\n\n// #endregion\n\n// #region Label Updates\n\n/**\n * Determine the effective aria-label for the grid.\n * Priority: explicit config > shell title > nothing\n *\n * The shell title is read from `config.shell.header.title`. The shell plugin\n * folds any light-DOM `<tbw-grid-header title>` into that field via its\n * `processConfig` hook (extraction #370), so core needs no plugin state here.\n *\n * @param config - Grid configuration (the merged effective config)\n * @returns The aria-label to use, or undefined\n */\nexport function getEffectiveAriaLabel<T>(config: GridConfig<T> | undefined): string | undefined {\n const explicitLabel = config?.gridAriaLabel;\n if (explicitLabel) return explicitLabel;\n\n return config?.shell?.header?.title ?? undefined;\n}\n\n/**\n * Update ARIA label and describedby attributes on the grid container.\n * Uses caching to avoid redundant DOM writes.\n *\n * @param state - ARIA state for caching\n * @param rowsBodyEl - Element to set aria-label/aria-describedby on\n * @param config - Grid configuration (the merged effective config)\n * @returns true if anything was updated\n */\nexport function updateAriaLabels<T>(\n state: AriaState,\n rowsBodyEl: HTMLElement | null,\n config: GridConfig<T> | undefined,\n): boolean {\n if (!rowsBodyEl) return false;\n\n let updated = false;\n\n // aria-labelledby wins per WAI-ARIA accessible-name precedence — when set,\n // `aria-label` is suppressed even if explicitly configured to avoid two\n // competing names being computed by AT.\n const ariaLabelledBy = config?.gridAriaLabelledBy;\n const effectiveLabel = ariaLabelledBy ? undefined : getEffectiveAriaLabel(config);\n\n // Update aria-labelledby only if changed\n if (ariaLabelledBy !== state.ariaLabelledBy) {\n state.ariaLabelledBy = ariaLabelledBy;\n if (ariaLabelledBy) {\n rowsBodyEl.setAttribute('aria-labelledby', ariaLabelledBy);\n } else {\n rowsBodyEl.removeAttribute('aria-labelledby');\n }\n updated = true;\n }\n\n // Update aria-label only if changed\n if (effectiveLabel !== state.ariaLabel) {\n state.ariaLabel = effectiveLabel;\n if (effectiveLabel) {\n rowsBodyEl.setAttribute('aria-label', effectiveLabel);\n } else {\n rowsBodyEl.removeAttribute('aria-label');\n }\n updated = true;\n }\n\n // Update aria-describedby only if changed\n const ariaDescribedBy = config?.gridAriaDescribedBy;\n if (ariaDescribedBy !== state.ariaDescribedBy) {\n state.ariaDescribedBy = ariaDescribedBy;\n if (ariaDescribedBy) {\n rowsBodyEl.setAttribute('aria-describedby', ariaDescribedBy);\n } else {\n rowsBodyEl.removeAttribute('aria-describedby');\n }\n updated = true;\n }\n\n // Update aria-roledescription only if changed. Apps can override the\n // announced role name (e.g. localized \"Tabell\" instead of generic \"grid\").\n const ariaRoleDescription = config?.gridAriaRoleDescription;\n if (ariaRoleDescription !== state.ariaRoleDescription) {\n state.ariaRoleDescription = ariaRoleDescription;\n if (ariaRoleDescription) {\n rowsBodyEl.setAttribute('aria-roledescription', ariaRoleDescription);\n } else {\n rowsBodyEl.removeAttribute('aria-roledescription');\n }\n updated = true;\n }\n\n return updated;\n}\n\n// #endregion\n\n// #region Live Announcements\n\n/**\n * Announce a message to screen readers via the grid's aria-live region.\n * Clears then sets text to ensure repeated messages are re-announced.\n *\n * Respects `effectiveConfig.a11y.announcements` — if set to `false`,\n * the announcement is silently skipped.\n *\n * @param gridEl - The grid host element (or any ancestor containing `.tbw-sr-only`)\n * @param message - The message to announce\n */\nexport function announce(gridEl: HTMLElement, message: string): void {\n // Check if announcements are disabled via config.\n // gridEl is always a DataGridElement at runtime — use property access safely.\n if (!gridEl) return;\n const config =\n 'effectiveConfig' in gridEl\n ? (gridEl as HTMLElement & { effectiveConfig?: GridConfig }).effectiveConfig\n : undefined;\n if (config?.a11y?.announcements === false) return;\n\n const el = gridEl.querySelector?.('.tbw-sr-only');\n if (!el) return;\n // Clear first so identical consecutive messages are still announced\n el.textContent = '';\n // `requestAnimationFrame` can be undefined if a debounced announcement fires\n // after the DOM/host environment is torn down (notably in unit tests). Fall\n // back to a direct assignment so `announce` never throws out of band.\n if (typeof requestAnimationFrame === 'function') {\n requestAnimationFrame(() => {\n el.textContent = message;\n });\n } else {\n el.textContent = message;\n }\n}\n\n/**\n * Get an announcement message, preferring custom overrides from config.\n * Falls back to default English messages when no override is provided.\n *\n * @param gridEl - The grid host element\n * @param key - The message key from A11yMessages\n * @param args - Arguments to pass to the message function\n */\nexport function getA11yMessage<K extends keyof A11yMessages>(\n gridEl: HTMLElement,\n key: K,\n ...args: Parameters<Required<A11yMessages>[K]>\n): string {\n type MessageFn = (...a: Parameters<Required<A11yMessages>[K]>) => string;\n const config =\n gridEl && 'effectiveConfig' in gridEl\n ? (gridEl as HTMLElement & { effectiveConfig?: GridConfig }).effectiveConfig\n : undefined;\n const customFn = config?.a11y?.messages?.[key] as MessageFn | undefined;\n if (customFn) return customFn(...args);\n return (DEFAULT_A11Y_MESSAGES[key] as MessageFn)(...args);\n}\n\n/**\n * Announce a `dataLoaded` message — but only when the source row count has\n * actually changed since the last announcement. This prevents announcement\n * spam from internal `data-change` emits triggered by sort/filter/edit, which\n * have their own dedicated announcements.\n *\n * @param gridEl - The grid host element\n * @param state - ARIA state (tracks last announced count)\n * @param sourceRowCount - Current source row count\n */\nexport function announceDataLoaded(gridEl: HTMLElement, state: AriaState, sourceRowCount: number): void {\n if (sourceRowCount === state.lastAnnouncedSourceCount) return;\n const previousAnnouncedSourceCount = state.lastAnnouncedSourceCount;\n state.lastAnnouncedSourceCount = sourceRowCount;\n // Skip only the initial sentinel → 0 transition (empty grid mount). A later\n // transition to 0 (e.g. data cleared / reloaded to an empty result set) should\n // still announce so screen-reader users hear that the grid is now empty.\n if (sourceRowCount === 0 && previousAnnouncedSourceCount < 0) return;\n announce(gridEl, getA11yMessage(gridEl, 'dataLoaded', sourceRowCount));\n}\n\n// #endregion\n","/**\n * Multi-Sort Core Logic\n *\n * Pure functions for multi-column sorting operations.\n */\n\nimport { resolveCellValue } from '../../core/internal/value-accessor';\nimport type { ColumnConfig } from '../../core/types';\nimport type { SortModel } from './types';\n\n// Module-level cached collator. `String.prototype.localeCompare(b)` (no args)\n// lazily allocates a fresh Intl.Collator on every call in V8 — measurable on\n// large datasets where string comparisons dominate the secondary sort key.\n// A single cached `Intl.Collator` reused via `.compare(a, b)` is ~3-5x faster.\nconst stringCollator = new Intl.Collator(undefined, { sensitivity: 'variant' });\n\n/**\n * Apply multiple sort columns to a row array.\n * Sorts are applied in order - first sort has highest priority.\n *\n * **Non-mutating:** This function copies the input array before sorting and\n * returns the new array. Internal callers that already own a mutable copy\n * (e.g. inside `MultiSortPlugin.processRows`, where `plugin-manager` has already\n * cloned the row pipeline input) should call {@link sortRowsInPlace} directly\n * to avoid a redundant allocation in the hot path.\n *\n * @param rows - Array of row objects to sort\n * @param sorts - Ordered array of sort configurations\n * @param columns - Column configurations (for custom comparators)\n * @returns New sorted array (does not mutate original)\n */\nexport function applySorts<TRow = unknown>(rows: TRow[], sorts: SortModel[], columns: ColumnConfig<TRow>[]): TRow[] {\n if (!sorts.length) return [...rows];\n\n const copy = [...rows];\n sortRowsInPlace(copy, sorts, columns);\n return copy;\n}\n\n/**\n * Sort an array in-place using multiple sort columns.\n * Pre-resolves column comparators to avoid O(n·log·n·m) column lookups\n * inside the comparator.\n * @internal\n */\nexport function sortRowsInPlace<TRow = unknown>(rows: TRow[], sorts: SortModel[], columns: ColumnConfig<TRow>[]): void {\n if (!sorts.length) return;\n\n // Pre-resolve comparator chain — avoids columns.find() on every pair comparison\n const chain = sorts.map((sort) => {\n const col = columns.find((c) => c.field === sort.field);\n const comparator = col?.sortComparator ?? defaultComparator;\n // Pre-bind the value getter per link so the hot comparator path doesn't\n // re-evaluate `column?.valueAccessor` on every pair comparison. For the\n // common case (no valueAccessor) this collapses to a single property read.\n // Documented precedence: sortComparator → valueAccessor → field.\n const field = sort.field;\n const getValue: (row: TRow) => unknown = col?.valueAccessor\n ? (row: TRow) => resolveCellValue(row, col)\n : (row: TRow) => (row as Record<string, unknown>)[field];\n return {\n field,\n asc: sort.direction === 'asc',\n comparator,\n getValue,\n // Auto-pin `__loading` placeholder rows (e.g. ServerSidePlugin under `sortMode: 'local'`)\n // to the end ONLY when no custom comparator is configured. Custom comparators receive\n // the row pair as 3rd/4th args and own placeholder handling themselves.\n pinPlaceholders: !col?.sortComparator,\n };\n });\n\n // Only enable per-pair pin checks if the dataset *actually* contains loading\n // placeholders — for the common case (no `__loading` rows) we skip ~n·log·n\n // property reads + branch on the hot path. Single O(n) scan up front.\n const chainNeedsPin = chain.some((l) => l.pinPlaceholders);\n const needsPinScan = chainNeedsPin && hasLoadingRow(rows);\n\n // Schwartzian transform: extract sort keys ONCE per row up front, then sort\n // indices by the cached keys. `Array.prototype.sort` calls the comparator\n // ~n·log·n times; without caching, each call would re-extract every key\n // (2 sides × k columns = 2·k property reads / accessor invocations per\n // compare). Caching collapses ~2·k·n·log·n extractions to k·n. For 10K\n // rows × 2 keys that's 20K extractions instead of ~260K, and the comparator\n // body becomes a pure key-vs-key comparison the JIT can optimize tightly.\n const n = rows.length;\n const k = chain.length;\n const keys: unknown[] = new Array(n * k);\n for (let i = 0; i < n; i++) {\n const row = rows[i];\n const base = i * k;\n for (let j = 0; j < k; j++) {\n keys[base + j] = chain[j].getValue(row);\n }\n }\n\n // Index array — sort this and then permute `rows` in one pass at the end.\n // Typed array when it fits; falls back to plain Array for very large datasets.\n const indices: Uint32Array | number[] = n <= 0xffffffff ? new Uint32Array(n) : new Array(n);\n for (let i = 0; i < n; i++) indices[i] = i;\n\n const compareIndices = (ia: number, ib: number): number => {\n if (needsPinScan) {\n const pinned = pinLoadingRows(rows[ia], rows[ib]);\n if (pinned !== 0) return pinned;\n }\n const baseA = ia * k;\n const baseB = ib * k;\n for (let j = 0; j < k; j++) {\n const link = chain[j];\n // Custom comparators receive the row pair as 3rd/4th args; defaultComparator\n // ignores them. Keys are already cached so no extraction happens here.\n const result = link.comparator(keys[baseA + j], keys[baseB + j], rows[ia], rows[ib]);\n if (result !== 0) return link.asc ? result : -result;\n }\n return 0;\n };\n\n if (indices instanceof Uint32Array) {\n indices.sort(compareIndices);\n } else {\n (indices as number[]).sort(compareIndices);\n }\n\n // Permute rows in-place using the sorted index array. Single allocation of\n // a snapshot; then write back. Done in two passes to avoid clobbering.\n const snapshot = rows.slice();\n for (let i = 0; i < n; i++) {\n rows[i] = snapshot[indices[i]];\n }\n}\n\n/**\n * O(n) scan checking whether any row has the `__loading` placeholder marker.\n * Cheap relative to the O(n·log·n) sort, and lets us elide per-pair pin checks\n * when the dataset has no placeholders (the overwhelmingly common case).\n * @internal\n */\nfunction hasLoadingRow(rows: readonly unknown[]): boolean {\n for (let i = 0; i < rows.length; i++) {\n if ((rows[i] as { __loading?: unknown } | null)?.__loading === true) return true;\n }\n return false;\n}\n\n/**\n * Pin `__loading` placeholder rows (e.g. ServerSidePlugin under `sortMode: 'local'`)\n * to the end of the sorted array regardless of sort direction.\n *\n * Returns 0 when neither row is a placeholder, +1 when `a` is, -1 when `b` is.\n *\n * @internal\n */\nfunction pinLoadingRows(a: unknown, b: unknown): number {\n const aLoading = (a as { __loading?: unknown } | null)?.__loading === true;\n const bLoading = (b as { __loading?: unknown } | null)?.__loading === true;\n if (aLoading === bLoading) return 0;\n return aLoading ? 1 : -1;\n}\n\n/**\n * Default comparator for sorting values.\n * Handles nulls, numbers, dates, and strings.\n *\n * @param a - First value\n * @param b - Second value\n * @returns Comparison result (-1, 0, 1)\n */\nexport function defaultComparator(a: unknown, b: unknown): number {\n // Handle nulls/undefined - push to end\n if (a == null && b == null) return 0;\n if (a == null) return 1;\n if (b == null) return -1;\n\n // Type-aware comparison\n if (typeof a === 'number' && typeof b === 'number') {\n return a - b;\n }\n\n if (a instanceof Date && b instanceof Date) {\n return a.getTime() - b.getTime();\n }\n\n // Boolean comparison\n if (typeof a === 'boolean' && typeof b === 'boolean') {\n return a === b ? 0 : a ? -1 : 1;\n }\n\n // String comparison (fallback). Uses a module-level cached Intl.Collator\n // — see `stringCollator` declaration for rationale.\n return stringCollator.compare(typeof a === 'string' ? a : String(a), typeof b === 'string' ? b : String(b));\n}\n\n/**\n * Toggle sort state for a field.\n * With shift key: adds/toggles in multi-sort list\n * Without shift key: replaces entire sort with single column\n *\n * @param current - Current sort model\n * @param field - Field to toggle\n * @param shiftKey - Whether shift key is held (multi-sort mode)\n * @param maxColumns - Maximum columns allowed in sort\n * @returns New sort model\n */\nexport function toggleSort(current: SortModel[], field: string, shiftKey: boolean, maxColumns: number): SortModel[] {\n const existing = current.find((s) => s.field === field);\n\n if (shiftKey) {\n // Multi-sort: add/toggle in list\n if (existing) {\n if (existing.direction === 'asc') {\n // Flip to descending\n return current.map((s) => (s.field === field ? { ...s, direction: 'desc' as const } : s));\n } else {\n // Remove from sort\n return current.filter((s) => s.field !== field);\n }\n } else if (current.length < maxColumns) {\n // Add new sort column\n return [...current, { field, direction: 'asc' as const }];\n }\n // Max columns reached, return unchanged\n return current;\n } else {\n // Single sort: replace all\n if (existing?.direction === 'asc') {\n return [{ field, direction: 'desc' }];\n } else if (existing?.direction === 'desc') {\n return [];\n }\n return [{ field, direction: 'asc' }];\n }\n}\n\n/**\n * Get the sort index (1-based) for a field in the sort model.\n * Returns undefined if the field is not in the sort model.\n *\n * @param sortModel - Current sort model\n * @param field - Field to check\n * @returns 1-based index or undefined\n */\nexport function getSortIndex(sortModel: SortModel[], field: string): number | undefined {\n const index = sortModel.findIndex((s) => s.field === field);\n return index >= 0 ? index + 1 : undefined;\n}\n\n/**\n * Get the sort direction for a field in the sort model.\n *\n * @param sortModel - Current sort model\n * @param field - Field to check\n * @returns Sort direction or undefined if not sorted\n */\nexport function getSortDirection(sortModel: SortModel[], field: string): 'asc' | 'desc' | undefined {\n return sortModel.find((s) => s.field === field)?.direction;\n}\n","@layer tbw-plugins {\n .header-cell[data-sort='asc']::after {\n content: '↑';\n margin-left: var(--tbw-spacing-xs, 0.25em);\n opacity: 0.8;\n }\n .header-cell[data-sort='desc']::after {\n content: '↓';\n margin-left: var(--tbw-spacing-xs, 0.25em);\n opacity: 0.8;\n }\n .sort-indicator {\n margin-left: var(--tbw-spacing-xs, 0.25em);\n opacity: 0.8;\n }\n .sort-index {\n font-size: var(--tbw-font-size-2xs, 0.7em);\n background: var(--tbw-multi-sort-badge-bg, var(--tbw-color-panel-bg));\n color: var(--tbw-multi-sort-badge-color, var(--tbw-color-fg));\n border-radius: 50%;\n width: var(--tbw-multi-sort-badge-size, 1em);\n height: var(--tbw-multi-sort-badge-size, 1em);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n margin-left: var(--tbw-spacing-xs, 0.125em);\n font-weight: 600;\n }\n} /* End @layer tbw-plugins */\n","/**\n * Multi-Sort Plugin (Class-based)\n *\n * Provides multi-column sorting capabilities for tbw-grid.\n * Supports shift+click for adding secondary sort columns.\n */\n\nimport { announce, getA11yMessage } from '../../core/internal/aria';\nimport { BaseGridPlugin, HeaderClickEvent, type PluginManifest, type PluginQuery } from '../../core/plugin/base-plugin';\nimport type { ColumnState, GridHost } from '../../core/types';\nimport { getSortDirection, getSortIndex, sortRowsInPlace, toggleSort } from './multi-sort';\nimport styles from './multi-sort.css?inline';\nimport type { MultiSortConfig, SortModel } from './types';\n\n/**\n * Multi-Sort Plugin for tbw-grid\n *\n * Enables sorting by multiple columns at once—hold Shift and click additional column\n * headers to build up a sort stack. Priority badges show the sort order, so users\n * always know which column takes precedence.\n *\n * ## Installation\n *\n * ```ts\n * import { MultiSortPlugin } from '@toolbox-web/grid/plugins/multi-sort';\n * ```\n *\n * ## Keyboard Shortcuts\n *\n * | Shortcut | Action |\n * |----------|--------|\n * | `Click header` | Sort by column (clears other sorts) |\n * | `Shift + Click` | Add column to multi-sort stack |\n * | `Ctrl + Click` | Toggle sort direction |\n *\n * ## Events\n *\n * | Event | Detail | Description |\n * |-------|--------|-------------|\n * | `sort-change` | `{ sortModel: SortModel[] }` | Fired when sort changes |\n *\n * @example Basic Multi-Column Sorting\n * ```ts\n * import { queryGrid } from '@toolbox-web/grid';\n * import { MultiSortPlugin } from '@toolbox-web/grid/plugins/multi-sort';\n *\n * const grid = queryGrid('tbw-grid');\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', header: 'Name', sortable: true },\n * { field: 'department', header: 'Department', sortable: true },\n * { field: 'salary', header: 'Salary', type: 'number', sortable: true },\n * ],\n * plugins: [new MultiSortPlugin({ maxSortColumns: 3, showSortIndex: true })],\n * };\n *\n * grid.on('sort-change', ({ sortModel }) => {\n * console.log('Active sorts:', sortModel);\n * });\n * ```\n *\n * @example Initial Sort Configuration\n * ```ts\n * new MultiSortPlugin({\n * initialSort: [\n * { field: 'department', direction: 'asc' },\n * { field: 'salary', direction: 'desc' },\n * ],\n * })\n * ```\n *\n * @see {@link MultiSortConfig} for all configuration options\n * @see {@link SortModel} for the sort model structure\n *\n * @internal Extends BaseGridPlugin\n * @since 0.1.1\n */\nexport class MultiSortPlugin extends BaseGridPlugin<MultiSortConfig> {\n /**\n * Plugin manifest declaring query types this plugin responds to.\n * @internal\n */\n static override readonly manifest: PluginManifest = {\n queries: [\n { type: 'sort:get-model', description: 'Returns the current multi-sort model as SortModel[]' },\n { type: 'sort:set-model', description: 'Sets the multi-sort model from context (SortModel[])' },\n ],\n };\n\n /** @internal */\n readonly name = 'multiSort';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<MultiSortConfig> {\n return {\n maxSortColumns: 3,\n showSortIndex: true,\n };\n }\n\n // #region Internal State\n private sortModel: SortModel[] = [];\n /** Cached sort result — returned as-is while a row edit is active to prevent\n * the edited row from jumping to a new sorted position mid-edit. Row data\n * mutations are still visible because the array holds shared object refs. */\n private cachedSortResult: unknown[] | null = null;\n /** Guards deferred sort-change broadcast during applyColumnState batch. */\n #pendingStateBroadcast = false;\n /** Snapshot of sortModel taken at the start of an applyColumnState batch,\n * used by the deferred broadcast to skip when the model didn't change. */\n #preBatchSortModel: SortModel[] = [];\n\n /** Typed internal grid accessor. */\n get #internalGrid(): GridHost {\n return this.grid;\n }\n\n /**\n * Clear the core `_sortState` so that only this plugin's `processRows`\n * sorting applies. `ConfigManager.applyState()` always sets the core sort\n * state when restoring from storage, even when a plugin handles sorting.\n * Without this, the stale core state leaks into `collectState()` and\n * `reapplyCoreSort()` after the plugin clears its own model.\n */\n private clearCoreSortState(): void {\n this.#internalGrid._sortState = null;\n }\n\n /**\n * Remove sorts on fields that are owned by the grouping plugin.\n * GroupingRowsPlugin handles group header ordering independently, so\n * multi-sort should only sort by non-grouped data columns.\n */\n #filterGroupedFields(model: SortModel[]): SortModel[] {\n const results = this.grid?.query?.('grouping:get-grouped-fields', null);\n if (!Array.isArray(results) || results.length === 0) return model;\n\n const groupedFields = results[0] as string[];\n if (!Array.isArray(groupedFields) || groupedFields.length === 0) return model;\n\n const groupedSet = new Set(groupedFields);\n const filtered = model.filter((s) => !groupedSet.has(s.field));\n return filtered.length === model.length ? model : filtered;\n }\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override detach(): void {\n this.sortModel = [];\n this.cachedSortResult = null;\n }\n // #endregion\n\n // #region Query System\n\n /** @internal */\n override handleQuery(query: PluginQuery): unknown {\n switch (query.type) {\n case 'sort:get-model':\n return [...this.sortModel];\n case 'sort:set-model': {\n const model = query.context;\n if (!Array.isArray(model)) return false;\n this.sortModel = [...model] as SortModel[];\n this.clearCoreSortState();\n this.broadcast('sort-change', { sortModel: [...this.sortModel] });\n this.requestRender();\n return true;\n }\n default:\n return undefined;\n }\n }\n\n // #endregion\n\n // #region Hooks\n\n /** @internal */\n override processRows(rows: readonly unknown[]): unknown[] {\n // Nothing to sort: hand the input on untouched. A copy here would give every\n // render an O(n) slice and make ServerSide's `_rows !== managedNodes` check\n // treat each block load as a derived row model (#521).\n if (this.sortModel.length === 0) {\n this.cachedSortResult = null;\n return rows as unknown[];\n }\n\n // A ServerSide data source in `sortMode: 'server'` already sorted the rows\n // (via `getRows({ sortModel })`) with its own collation / computed keys, and\n // its loading placeholders sit at their block positions — keep that order (#521).\n if (this.grid?.query?.<'server' | 'local'>('datasource:sort-mode', null)?.[0] === 'server') {\n this.cachedSortResult = null;\n return rows as unknown[];\n }\n\n // Freeze sort order while a row is actively being edited (row mode only).\n // Re-sorting mid-edit would move the edited row to a new index while the\n // editors remain at the old position, causing data/UI mismatch.\n // In grid mode (_isGridEditMode) sorting is safe — afterCellRender\n // re-injects editors into the re-sorted cells.\n // We return the cached previous sort result (same object references, so\n // in-place value mutations are already visible) instead of unsorted input.\n const grid = this.#internalGrid;\n if (!grid._isGridEditMode && typeof grid._activeEditRows === 'number' && grid._activeEditRows !== -1) {\n if (this.cachedSortResult && this.cachedSortResult.length === rows.length) {\n return [...this.cachedSortResult];\n }\n }\n\n // Sort a copy: with no earlier copying stage the input IS the consumer's\n // `rows` array (`grid.sourceRows`) — sorting it in place reordered their data\n // and left nothing to restore when the sort is cleared. The copy is O(n)\n // next to the O(n log n) sort. A pre-resolved comparator chain avoids\n // column lookup on every pair comparison.\n // Exclude fields owned by the grouping plugin — group header order is handled\n // by GroupingRowsPlugin, so multi-sort should only affect within-group data order.\n const effectiveModel = this.#filterGroupedFields(this.sortModel);\n\n const mutableRows = [...rows];\n if (effectiveModel.length > 0) {\n sortRowsInPlace(mutableRows, effectiveModel, this.columns);\n }\n this.cachedSortResult = mutableRows;\n return mutableRows;\n }\n\n /** @internal */\n override onHeaderClick(event: HeaderClickEvent): boolean {\n const column = this.columns.find((c) => c.field === event.field);\n if (!column?.sortable) return false;\n\n const shiftKey = event.originalEvent.shiftKey;\n const maxColumns = this.config.maxSortColumns ?? 3;\n\n this.sortModel = toggleSort(this.sortModel, event.field, shiftKey, maxColumns);\n this.clearCoreSortState();\n\n this.broadcast('sort-change', { sortModel: [...this.sortModel] });\n this.requestRender();\n this.grid?.requestStateChange?.();\n\n // Announce for screen readers\n if (this.sortModel.length > 0) {\n const labels = this.sortModel.map((s) => {\n const col = this.columns.find((c) => c.field === s.field);\n return `${col?.header ?? s.field} ${s.direction === 'asc' ? 'ascending' : 'descending'}`;\n });\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortApplied', labels.join(', then '), ''));\n } else {\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortCleared'));\n }\n\n return true;\n }\n\n /** @internal */\n override afterRender(): void {\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n const showIndex = this.config.showSortIndex !== false;\n\n const headerCells = gridEl.querySelectorAll('.header-row .cell[data-field]');\n headerCells.forEach((cell) => {\n const field = cell.getAttribute('data-field');\n if (!field) return;\n\n const sortIndex = getSortIndex(this.sortModel, field);\n const sortDir = getSortDirection(this.sortModel, field);\n\n // Remove existing sort index badge (always clean up)\n cell.querySelector('.sort-index')?.remove();\n\n if (sortDir) {\n const indicator = this.updateSortIndicator(cell, sortDir);\n\n // Add sort index badge if multiple columns sorted and showSortIndex is enabled\n if (showIndex && this.sortModel.length > 1 && sortIndex !== undefined) {\n const badge = document.createElement('span');\n badge.className = 'sort-index';\n badge.textContent = String(sortIndex);\n if (indicator.nextSibling) {\n cell.insertBefore(badge, indicator.nextSibling);\n } else {\n cell.appendChild(badge);\n }\n }\n } else if (cell.classList.contains('sortable')) {\n this.updateSortIndicator(cell, null);\n }\n });\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Get the current sort model.\n * @returns Copy of the current sort model\n */\n getSortModel(): SortModel[] {\n return [...this.sortModel];\n }\n\n /**\n * Set the sort model programmatically.\n * @param model - New sort model to apply\n */\n setSortModel(model: SortModel[]): void {\n this.sortModel = [...model];\n this.clearCoreSortState();\n this.broadcast('sort-change', { sortModel: [...model] });\n this.requestRender();\n this.grid?.requestStateChange?.();\n if (model.length > 0) {\n const labels = model.map((s) => {\n const col = this.columns.find((c) => c.field === s.field);\n return `${col?.header ?? s.field} ${s.direction === 'asc' ? 'ascending' : 'descending'}`;\n });\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortApplied', labels.join(', then '), ''));\n }\n }\n\n /**\n * Clear all sorting.\n */\n clearSort(): void {\n this.sortModel = [];\n this.clearCoreSortState();\n this.broadcast('sort-change', { sortModel: [] });\n this.requestRender();\n this.grid?.requestStateChange?.();\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortCleared'));\n }\n\n /**\n * Get the sort index (1-based) for a specific field.\n * @param field - Field to check\n * @returns 1-based index or undefined if not sorted\n */\n getSortIndex(field: string): number | undefined {\n return getSortIndex(this.sortModel, field);\n }\n\n /**\n * Get the sort direction for a specific field.\n * @param field - Field to check\n * @returns Sort direction or undefined if not sorted\n */\n getSortDirection(field: string): 'asc' | 'desc' | undefined {\n return getSortDirection(this.sortModel, field);\n }\n // #endregion\n\n // #region Column State Hooks\n\n /**\n * Return sort state for a column if it's in the sort model.\n * @internal\n */\n override getColumnState(field: string): Partial<ColumnState> | undefined {\n const index = this.sortModel.findIndex((s) => s.field === field);\n if (index === -1) return undefined;\n\n const sortEntry = this.sortModel[index];\n return {\n sort: {\n direction: sortEntry.direction,\n priority: index,\n },\n };\n }\n\n /**\n * Apply sort state from column state.\n * Rebuilds the sort model from all column states.\n * @internal\n */\n override applyColumnState(field: string, state: ColumnState): void {\n // Snapshot model before mutation so the microtask can skip a no-op\n // broadcast/render when nothing actually changed.\n if (!this.#pendingStateBroadcast) {\n this.#preBatchSortModel = [...this.sortModel];\n }\n\n if (!state.sort) {\n // Remove this field from sortModel if it exists\n this.sortModel = this.sortModel.filter((s) => s.field !== field);\n } else {\n // Find existing entry or add new one\n const existingIndex = this.sortModel.findIndex((s) => s.field === field);\n const newEntry: SortModel = {\n field,\n direction: state.sort.direction,\n };\n\n if (existingIndex !== -1) {\n // Update existing entry\n this.sortModel[existingIndex] = newEntry;\n } else {\n // Add at the correct priority position\n this.sortModel.splice(state.sort.priority, 0, newEntry);\n }\n }\n\n // Clear core sort state — this plugin exclusively handles sorting via\n // processRows. The core _sortState is set by ConfigManager.applyState()\n // before plugins run; null it so reapplyCoreSort() is a no-op.\n this.clearCoreSortState();\n\n // Broadcast sort-change + force a row re-render after state restoration.\n // Both are deferred to a microtask so per-column calls coalesce into one\n // event and one render. The render handles the \"sort cleared\" case that\n // core's width-only fast path cannot detect (core only inspects the\n // *incoming* state, not what the plugin's model became). Skip entirely\n // when the model is unchanged so unrelated state restores stay cheap.\n if (!this.#pendingStateBroadcast) {\n this.#pendingStateBroadcast = true;\n queueMicrotask(() => {\n this.#pendingStateBroadcast = false;\n const prev = this.#preBatchSortModel;\n this.#preBatchSortModel = [];\n if (sortModelEquals(prev, this.sortModel)) return;\n this.broadcast('sort-change', { sortModel: [...this.sortModel] });\n this.requestRender();\n });\n }\n }\n // #endregion\n}\n\n/** Shallow equality for two sort models (order-sensitive). */\nfunction sortModelEquals(a: SortModel[], b: SortModel[]): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i].field !== b[i].field || a[i].direction !== b[i].direction) return false;\n }\n return true;\n}\n"],"mappings":"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"}
|