@toolbox-web/grid 1.25.1 → 1.25.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/all.js +2 -2
- package/all.js.map +1 -1
- package/index.js +1 -1
- package/index.js.map +1 -1
- package/lib/core/grid.d.ts +18 -2
- package/lib/core/internal/diagnostics.d.ts +164 -0
- package/lib/core/internal/sorting.d.ts +53 -4
- package/lib/core/internal/utils.d.ts +0 -8
- package/lib/core/plugin/base-plugin.d.ts +18 -2
- package/lib/core/plugin/plugin-manager.d.ts +6 -4
- package/lib/core/types.d.ts +3 -2
- package/lib/features/registry.js +1 -1
- package/lib/features/registry.js.map +1 -1
- package/lib/plugins/clipboard/index.js +1 -1
- package/lib/plugins/clipboard/index.js.map +1 -1
- package/lib/plugins/column-virtualization/index.js +1 -1
- package/lib/plugins/column-virtualization/index.js.map +1 -1
- package/lib/plugins/context-menu/index.js +1 -1
- package/lib/plugins/context-menu/index.js.map +1 -1
- package/lib/plugins/editing/index.js +1 -1
- package/lib/plugins/editing/index.js.map +1 -1
- package/lib/plugins/editing/types.d.ts +3 -1
- package/lib/plugins/export/ExportPlugin.d.ts +2 -2
- package/lib/plugins/export/index.js +1 -1
- package/lib/plugins/export/index.js.map +1 -1
- package/lib/plugins/filtering/FilteringPlugin.d.ts +2 -2
- package/lib/plugins/filtering/index.js +1 -1
- package/lib/plugins/filtering/index.js.map +1 -1
- package/lib/plugins/grouping-columns/index.js +1 -1
- package/lib/plugins/grouping-columns/index.js.map +1 -1
- package/lib/plugins/grouping-rows/GroupingRowsPlugin.d.ts +2 -2
- package/lib/plugins/grouping-rows/index.js +2 -2
- package/lib/plugins/grouping-rows/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/multi-sort/MultiSortPlugin.d.ts +2 -2
- package/lib/plugins/multi-sort/index.js +1 -1
- package/lib/plugins/multi-sort/index.js.map +1 -1
- package/lib/plugins/pinned-columns/PinnedColumnsPlugin.d.ts +2 -2
- package/lib/plugins/pinned-columns/index.js +1 -1
- package/lib/plugins/pinned-columns/index.js.map +1 -1
- package/lib/plugins/pinned-rows/index.js +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/print/PrintPlugin.d.ts +2 -1
- package/lib/plugins/print/index.js +1 -1
- package/lib/plugins/print/index.js.map +1 -1
- package/lib/plugins/print/print-isolated.d.ts +2 -1
- package/lib/plugins/reorder-columns/ReorderPlugin.d.ts +2 -2
- package/lib/plugins/reorder-columns/index.js +1 -1
- package/lib/plugins/reorder-columns/index.js.map +1 -1
- package/lib/plugins/reorder-rows/RowReorderPlugin.d.ts +2 -2
- package/lib/plugins/reorder-rows/index.js +1 -1
- package/lib/plugins/reorder-rows/index.js.map +1 -1
- package/lib/plugins/responsive/index.js +1 -1
- package/lib/plugins/responsive/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 +1 -1
- package/lib/plugins/server-side/index.js.map +1 -1
- package/lib/plugins/tree/TreePlugin.d.ts +2 -2
- package/lib/plugins/tree/index.js +1 -1
- package/lib/plugins/tree/index.js.map +1 -1
- package/lib/plugins/undo-redo/UndoRedoPlugin.d.ts +2 -2
- package/lib/plugins/undo-redo/index.js +1 -1
- package/lib/plugins/undo-redo/index.js.map +1 -1
- package/lib/plugins/visibility/VisibilityPlugin.d.ts +2 -2
- package/lib/plugins/visibility/index.js +1 -1
- 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 +1 -1
- package/umd/plugins/clipboard.umd.js.map +1 -1
- package/umd/plugins/context-menu.umd.js +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.map +1 -1
- package/umd/plugins/filtering.umd.js.map +1 -1
- package/umd/plugins/grouping-columns.umd.js +1 -1
- package/umd/plugins/grouping-columns.umd.js.map +1 -1
- package/umd/plugins/grouping-rows.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/print.umd.js +1 -1
- package/umd/plugins/print.umd.js.map +1 -1
- package/umd/plugins/reorder-columns.umd.js.map +1 -1
- package/umd/plugins/reorder-rows.umd.js.map +1 -1
- package/umd/plugins/responsive.umd.js +1 -1
- package/umd/plugins/responsive.umd.js.map +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","sources":["../../../../../libs/grid/src/lib/plugins/multi-sort/multi-sort.ts","../../../../../libs/grid/src/lib/plugins/multi-sort/MultiSortPlugin.ts"],"sourcesContent":["/**\n * Multi-Sort Core Logic\n *\n * Pure functions for multi-column sorting operations.\n */\n\nimport type { ColumnConfig } from '../../core/types';\nimport type { SortModel } from './types';\n\n/**\n * Apply multiple sort columns to a row array.\n * Sorts are applied in order - first sort has highest priority.\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 return [...rows].sort((a, b) => {\n for (const sort of sorts) {\n const col = columns.find((c) => c.field === sort.field);\n const comparator = col?.sortComparator ?? defaultComparator;\n const aVal = (a as Record<string, unknown>)[sort.field];\n const bVal = (b as Record<string, unknown>)[sort.field];\n const result = comparator(aVal, bVal, a, b);\n if (result !== 0) {\n return sort.direction === 'asc' ? result : -result;\n }\n }\n return 0;\n });\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)\n return String(a).localeCompare(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","/**\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 } from '../../core/internal/aria';\nimport { BaseGridPlugin, HeaderClickEvent } from '../../core/plugin/base-plugin';\nimport type { ColumnState, GridHost } from '../../core/types';\nimport { applySorts, getSortDirection, getSortIndex, 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 * ## Configuration Options\n *\n * | Option | Type | Default | Description |\n * |--------|------|---------|-------------|\n * | `maxSortColumns` | `number` | `3` | Maximum columns to sort by |\n * | `showSortIndex` | `boolean` | `true` | Show sort priority badges |\n * | `initialSort` | `SortModel[]` | - | Pre-configured sort order on load |\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 * ## Programmatic API\n *\n * | Method | Signature | Description |\n * |--------|-----------|-------------|\n * | `setSort` | `(sortModel: SortModel[]) => void` | Set sort programmatically |\n * | `getSortModel` | `() => SortModel[]` | Get current sort model |\n * | `clearSort` | `() => void` | Clear all sorting |\n * | `addSort` | `(field, direction) => void` | Add a column to sort |\n * | `removeSort` | `(field) => void` | Remove a column from sort |\n *\n * @example Basic Multi-Column Sorting\n * ```ts\n * import '@toolbox-web/grid';\n * import { MultiSortPlugin } from '@toolbox-web/grid/plugins/multi-sort';\n *\n * const grid = document.querySelector('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 */\nexport class MultiSortPlugin extends BaseGridPlugin<MultiSortConfig> {\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\n /** Typed internal grid accessor. */\n get #internalGrid(): GridHost {\n return this.grid as unknown as GridHost;\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 // #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 Hooks\n\n /** @internal */\n override processRows(rows: readonly unknown[]): unknown[] {\n if (this.sortModel.length === 0) {\n this.cachedSortResult = null;\n return [...rows];\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 const sorted = applySorts([...rows], this.sortModel, [...this.columns]);\n this.cachedSortResult = sorted;\n return sorted;\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.emit('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!, `Sorted by ${labels.join(', then ')}`);\n } else {\n announce(this.gridElement!, 'Sort cleared');\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 // Update all sortable header cells with sort indicators\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 const existingBadge = cell.querySelector('.sort-index');\n existingBadge?.remove();\n\n if (sortDir) {\n // Column is sorted - remove base indicator and add our own\n const existingIndicator = cell.querySelector('[part~=\"sort-indicator\"], .sort-indicator');\n existingIndicator?.remove();\n\n cell.setAttribute('data-sort', sortDir);\n\n // Add sort arrow indicator - insert BEFORE filter button and resize handle\n // to maintain consistent order: [label, sort-indicator, sort-index, filter-btn, resize-handle]\n const indicator = document.createElement('span');\n indicator.className = 'sort-indicator';\n // Use grid-level icons (fall back to defaults)\n this.setIcon(indicator, this.resolveIcon(sortDir === 'asc' ? 'sortAsc' : 'sortDesc'));\n\n // Find insertion point: before filter button or resize handle\n const filterBtn = cell.querySelector('.tbw-filter-btn');\n const resizeHandle = cell.querySelector('.resize-handle');\n const insertBefore = filterBtn ?? resizeHandle;\n if (insertBefore) {\n cell.insertBefore(indicator, insertBefore);\n } else {\n cell.appendChild(indicator);\n }\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 // Insert badge right after the indicator\n if (indicator.nextSibling) {\n cell.insertBefore(badge, indicator.nextSibling);\n } else {\n cell.appendChild(badge);\n }\n }\n } else {\n cell.removeAttribute('data-sort');\n // Remove any stale sort indicators left by a previous afterRender cycle\n // Base indicators use part=\"sort-indicator\", plugin indicators use class=\"sort-indicator\"\n const staleIndicator = cell.querySelector('[part~=\"sort-indicator\"], .sort-indicator');\n staleIndicator?.remove();\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.emit('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!, `Sorted by ${labels.join(', then ')}`);\n }\n }\n\n /**\n * Clear all sorting.\n */\n clearSort(): void {\n this.sortModel = [];\n this.clearCoreSortState();\n this.emit('sort-change', { sortModel: [] });\n this.requestRender();\n this.grid?.requestStateChange?.();\n announce(this.gridElement!, 'Sort cleared');\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 // Only process if the column has sort state\n if (!state.sort) {\n // Remove this field from sortModel if it exists\n this.sortModel = this.sortModel.filter((s) => s.field !== field);\n return;\n }\n\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 // 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 // #endregion\n}\n"],"names":["defaultComparator","a","b","Date","getTime","String","localeCompare","getSortIndex","sortModel","field","index","findIndex","s","getSortDirection","find","direction","MultiSortPlugin","BaseGridPlugin","name","styles","defaultConfig","maxSortColumns","showSortIndex","cachedSortResult","internalGrid","this","grid","clearCoreSortState","_sortState","detach","processRows","rows","length","_isGridEditMode","_activeEditRows","sorted","sorts","columns","sort","col","c","result","sortComparator","applySorts","onHeaderClick","event","column","sortable","shiftKey","originalEvent","maxColumns","config","current","existing","map","filter","toggleSort","emit","requestRender","requestStateChange","labels","header","announce","gridElement","join","afterRender","gridEl","showIndex","querySelectorAll","forEach","cell","getAttribute","sortIndex","sortDir","existingBadge","querySelector","remove","existingIndicator","setAttribute","indicator","document","createElement","className","setIcon","resolveIcon","filterBtn","resizeHandle","insertBefore","appendChild","badge","textContent","nextSibling","removeAttribute","staleIndicator","getSortModel","setSortModel","model","clearSort","getColumnState","priority","applyColumnState","state","existingIndex","newEntry","splice"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"multi-sort.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/multi-sort/multi-sort.ts","../../../../../libs/grid/src/lib/plugins/multi-sort/MultiSortPlugin.ts"],"sourcesContent":["/**\n * Multi-Sort Core Logic\n *\n * Pure functions for multi-column sorting operations.\n */\n\nimport type { ColumnConfig } from '../../core/types';\nimport type { SortModel } from './types';\n\n/**\n * Apply multiple sort columns to a row array.\n * Sorts are applied in order - first sort has highest priority.\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 return [...rows].sort((a, b) => {\n for (const sort of sorts) {\n const col = columns.find((c) => c.field === sort.field);\n const comparator = col?.sortComparator ?? defaultComparator;\n const aVal = (a as Record<string, unknown>)[sort.field];\n const bVal = (b as Record<string, unknown>)[sort.field];\n const result = comparator(aVal, bVal, a, b);\n if (result !== 0) {\n return sort.direction === 'asc' ? result : -result;\n }\n }\n return 0;\n });\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)\n return String(a).localeCompare(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","/**\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 } from '../../core/internal/aria';\nimport { BaseGridPlugin, HeaderClickEvent } from '../../core/plugin/base-plugin';\nimport type { ColumnState, GridHost } from '../../core/types';\nimport { applySorts, getSortDirection, getSortIndex, 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 * ## Configuration Options\n *\n * | Option | Type | Default | Description |\n * |--------|------|---------|-------------|\n * | `maxSortColumns` | `number` | `3` | Maximum columns to sort by |\n * | `showSortIndex` | `boolean` | `true` | Show sort priority badges |\n * | `initialSort` | `SortModel[]` | - | Pre-configured sort order on load |\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 * ## Programmatic API\n *\n * | Method | Signature | Description |\n * |--------|-----------|-------------|\n * | `setSort` | `(sortModel: SortModel[]) => void` | Set sort programmatically |\n * | `getSortModel` | `() => SortModel[]` | Get current sort model |\n * | `clearSort` | `() => void` | Clear all sorting |\n * | `addSort` | `(field, direction) => void` | Add a column to sort |\n * | `removeSort` | `(field) => void` | Remove a column from sort |\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 */\nexport class MultiSortPlugin extends BaseGridPlugin<MultiSortConfig> {\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\n /** Typed internal grid accessor. */\n get #internalGrid(): GridHost {\n return this.grid as unknown as GridHost;\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 // #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 Hooks\n\n /** @internal */\n override processRows(rows: readonly unknown[]): unknown[] {\n if (this.sortModel.length === 0) {\n this.cachedSortResult = null;\n return [...rows];\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 const sorted = applySorts([...rows], this.sortModel, [...this.columns]);\n this.cachedSortResult = sorted;\n return sorted;\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.emit('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!, `Sorted by ${labels.join(', then ')}`);\n } else {\n announce(this.gridElement!, 'Sort cleared');\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 // Update all sortable header cells with sort indicators\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 const existingBadge = cell.querySelector('.sort-index');\n existingBadge?.remove();\n\n if (sortDir) {\n // Column is sorted - remove base indicator and add our own\n const existingIndicator = cell.querySelector('[part~=\"sort-indicator\"], .sort-indicator');\n existingIndicator?.remove();\n\n cell.setAttribute('data-sort', sortDir);\n\n // Add sort arrow indicator - insert BEFORE filter button and resize handle\n // to maintain consistent order: [label, sort-indicator, sort-index, filter-btn, resize-handle]\n const indicator = document.createElement('span');\n indicator.className = 'sort-indicator';\n // Use grid-level icons (fall back to defaults)\n this.setIcon(indicator, this.resolveIcon(sortDir === 'asc' ? 'sortAsc' : 'sortDesc'));\n\n // Find insertion point: before filter button or resize handle\n const filterBtn = cell.querySelector('.tbw-filter-btn');\n const resizeHandle = cell.querySelector('.resize-handle');\n const insertBefore = filterBtn ?? resizeHandle;\n if (insertBefore) {\n cell.insertBefore(indicator, insertBefore);\n } else {\n cell.appendChild(indicator);\n }\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 // Insert badge right after the indicator\n if (indicator.nextSibling) {\n cell.insertBefore(badge, indicator.nextSibling);\n } else {\n cell.appendChild(badge);\n }\n }\n } else {\n cell.removeAttribute('data-sort');\n // Remove any stale sort indicators left by a previous afterRender cycle\n // Base indicators use part=\"sort-indicator\", plugin indicators use class=\"sort-indicator\"\n const staleIndicator = cell.querySelector('[part~=\"sort-indicator\"], .sort-indicator');\n staleIndicator?.remove();\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.emit('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!, `Sorted by ${labels.join(', then ')}`);\n }\n }\n\n /**\n * Clear all sorting.\n */\n clearSort(): void {\n this.sortModel = [];\n this.clearCoreSortState();\n this.emit('sort-change', { sortModel: [] });\n this.requestRender();\n this.grid?.requestStateChange?.();\n announce(this.gridElement!, 'Sort cleared');\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 // Only process if the column has sort state\n if (!state.sort) {\n // Remove this field from sortModel if it exists\n this.sortModel = this.sortModel.filter((s) => s.field !== field);\n return;\n }\n\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 // 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 // #endregion\n}\n"],"names":["defaultComparator","a","b","Date","getTime","String","localeCompare","getSortIndex","sortModel","field","index","findIndex","s","getSortDirection","find","direction","MultiSortPlugin","BaseGridPlugin","name","styles","defaultConfig","maxSortColumns","showSortIndex","cachedSortResult","internalGrid","this","grid","clearCoreSortState","_sortState","detach","processRows","rows","length","_isGridEditMode","_activeEditRows","sorted","sorts","columns","sort","col","c","result","sortComparator","applySorts","onHeaderClick","event","column","sortable","shiftKey","originalEvent","maxColumns","config","current","existing","map","filter","toggleSort","emit","requestRender","requestStateChange","labels","header","announce","gridElement","join","afterRender","gridEl","showIndex","querySelectorAll","forEach","cell","getAttribute","sortIndex","sortDir","existingBadge","querySelector","remove","existingIndicator","setAttribute","indicator","document","createElement","className","setIcon","resolveIcon","filterBtn","resizeHandle","insertBefore","appendChild","badge","textContent","nextSibling","removeAttribute","staleIndicator","getSortModel","setSortModel","model","clearSort","getColumnState","priority","applyColumnState","state","existingIndex","newEntry","splice"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-columns.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/pinned-columns/pinned-columns.ts","../../../../../libs/grid/src/lib/plugins/pinned-columns/PinnedColumnsPlugin.ts"],"sourcesContent":["/**\n * Pinned Columns Core Logic\n *\n * Pure functions for applying pinned (sticky) column positioning.\n */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport { getDirection, resolveInlinePosition, type TextDirection } from '../../core/internal/utils';\nimport type { PinnedPosition, ResolvedPinnedPosition } from './types';\n\n// Keep deprecated imports working (StickyPosition = PinnedPosition)\ntype StickyPosition = PinnedPosition;\ntype ResolvedStickyPosition = ResolvedPinnedPosition;\n\n/**\n * Get the effective pinned position from a column, checking `pinned` first then `sticky` (deprecated).\n *\n * @param col - Column configuration object\n * @returns The pinned position, or undefined if not pinned\n */\nexport function getColumnPinned(col: any): PinnedPosition | undefined {\n return col.pinned ?? col.sticky ?? col.meta?.pinned ?? col.meta?.sticky;\n}\n\n/**\n * Resolve a pinned position to a physical position based on text direction.\n *\n * - `'left'` / `'right'` → unchanged (physical values)\n * - `'start'` → `'left'` in LTR, `'right'` in RTL\n * - `'end'` → `'right'` in LTR, `'left'` in RTL\n *\n * @param position - The pinned position (logical or physical)\n * @param direction - Text direction ('ltr' or 'rtl')\n * @returns Physical pinned position ('left' or 'right')\n */\nexport function resolveStickyPosition(position: StickyPosition, direction: TextDirection): ResolvedStickyPosition {\n return resolveInlinePosition(position, direction);\n}\n\n/**\n * Check if a column is pinned on the left (after resolving logical positions).\n */\nfunction isResolvedLeft(col: any, direction: TextDirection): boolean {\n const pinned = getColumnPinned(col);\n if (!pinned) return false;\n return resolveStickyPosition(pinned, direction) === 'left';\n}\n\n/**\n * Check if a column is pinned on the right (after resolving logical positions).\n */\nfunction isResolvedRight(col: any, direction: TextDirection): boolean {\n const pinned = getColumnPinned(col);\n if (!pinned) return false;\n return resolveStickyPosition(pinned, direction) === 'right';\n}\n\n/**\n * Get columns that should be sticky on the left.\n *\n * @param columns - Array of column configurations\n * @param direction - Text direction (default: 'ltr')\n * @returns Array of columns with sticky='left' or sticky='start' (in LTR)\n */\nexport function getLeftStickyColumns(columns: any[], direction: TextDirection = 'ltr'): any[] {\n return columns.filter((col) => isResolvedLeft(col, direction));\n}\n\n/**\n * Get columns that should be sticky on the right.\n *\n * @param columns - Array of column configurations\n * @param direction - Text direction (default: 'ltr')\n * @returns Array of columns with sticky='right' or sticky='end' (in LTR)\n */\nexport function getRightStickyColumns(columns: any[], direction: TextDirection = 'ltr'): any[] {\n return columns.filter((col) => isResolvedRight(col, direction));\n}\n\n/**\n * Check if any columns have sticky positioning.\n *\n * @param columns - Array of column configurations\n * @returns True if any column has sticky position\n */\nexport function hasStickyColumns(columns: any[]): boolean {\n return columns.some((col) => getColumnPinned(col) != null);\n}\n\n/**\n * Get the sticky position of a column.\n *\n * @param column - Column configuration\n * @returns The sticky position or null if not sticky\n */\nexport function getColumnStickyPosition(column: any): StickyPosition | null {\n return getColumnPinned(column) ?? null;\n}\n\n\n/**\n * Calculate left offsets for sticky-left columns.\n * Returns a map of field -> offset in pixels.\n *\n * @param columns - Array of column configurations (in order)\n * @param getColumnWidth - Function to get column width by field\n * @param direction - Text direction (default: 'ltr')\n * @returns Map of field to left offset\n */\nexport function calculateLeftStickyOffsets(\n columns: any[],\n getColumnWidth: (field: string) => number,\n direction: TextDirection = 'ltr',\n): Map<string, number> {\n const offsets = new Map<string, number>();\n let currentOffset = 0;\n\n for (const col of columns) {\n if (isResolvedLeft(col, direction)) {\n offsets.set(col.field, currentOffset);\n currentOffset += getColumnWidth(col.field);\n }\n }\n\n return offsets;\n}\n\n/**\n * Calculate right offsets for sticky-right columns.\n * Processes columns in reverse order.\n *\n * @param columns - Array of column configurations (in order)\n * @param getColumnWidth - Function to get column width by field\n * @param direction - Text direction (default: 'ltr')\n * @returns Map of field to right offset\n */\nexport function calculateRightStickyOffsets(\n columns: any[],\n getColumnWidth: (field: string) => number,\n direction: TextDirection = 'ltr',\n): Map<string, number> {\n const offsets = new Map<string, number>();\n let currentOffset = 0;\n\n // Process in reverse for right-sticky columns\n const reversed = [...columns].reverse();\n for (const col of reversed) {\n if (isResolvedRight(col, direction)) {\n offsets.set(col.field, currentOffset);\n currentOffset += getColumnWidth(col.field);\n }\n }\n\n return offsets;\n}\n\n/**\n * Apply sticky offsets to header and body cells.\n * This modifies the DOM elements in place.\n *\n * @param host - The grid host element (render root for DOM queries)\n * @param columns - Array of column configurations\n */\nexport function applyStickyOffsets(host: HTMLElement, columns: any[]): void {\n // With light DOM, query the host element directly\n const headerCells = Array.from(host.querySelectorAll('.header-row .cell')) as HTMLElement[];\n if (!headerCells.length) return;\n\n // Detect text direction from the host element\n const direction = getDirection(host);\n\n // Apply left sticky (includes 'start' in LTR, 'end' in RTL)\n let left = 0;\n for (const col of columns) {\n if (isResolvedLeft(col, direction)) {\n const cell = headerCells.find((c) => c.getAttribute('data-field') === col.field);\n if (cell) {\n cell.classList.add('sticky-left');\n cell.style.position = 'sticky';\n cell.style.left = left + 'px';\n // Body cells: use data-field for reliable matching (data-col indices may differ\n // between _columns and _visibleColumns due to hidden/utility columns)\n host.querySelectorAll(`.data-grid-row .cell[data-field=\"${col.field}\"]`).forEach((el) => {\n el.classList.add('sticky-left');\n (el as HTMLElement).style.position = 'sticky';\n (el as HTMLElement).style.left = left + 'px';\n });\n left += cell.offsetWidth;\n }\n }\n }\n\n // Apply right sticky (includes 'end' in LTR, 'start' in RTL) - process in reverse\n let right = 0;\n for (const col of [...columns].reverse()) {\n if (isResolvedRight(col, direction)) {\n const cell = headerCells.find((c) => c.getAttribute('data-field') === col.field);\n if (cell) {\n cell.classList.add('sticky-right');\n cell.style.position = 'sticky';\n cell.style.right = right + 'px';\n // Body cells: use data-field for reliable matching\n host.querySelectorAll(`.data-grid-row .cell[data-field=\"${col.field}\"]`).forEach((el) => {\n el.classList.add('sticky-right');\n (el as HTMLElement).style.position = 'sticky';\n (el as HTMLElement).style.right = right + 'px';\n });\n right += cell.offsetWidth;\n }\n }\n }\n}\n\n/**\n * Reorder columns so that pinned-left columns come first and pinned-right columns come last.\n * Maintains the relative order within each group (left-pinned, unpinned, right-pinned).\n *\n * @param columns - Array of column configurations (in their current order)\n * @param direction - Text direction ('ltr' or 'rtl'), used to resolve logical positions\n * @returns New array with pinned columns moved to the edges\n */\nexport function reorderColumnsForPinning(columns: readonly any[], direction: TextDirection = 'ltr'): any[] {\n const left: any[] = [];\n const middle: any[] = [];\n const right: any[] = [];\n\n for (const col of columns) {\n const pinned = getColumnPinned(col);\n if (pinned) {\n const resolved = resolveStickyPosition(pinned, direction);\n if (resolved === 'left') left.push(col);\n else right.push(col);\n } else {\n middle.push(col);\n }\n }\n\n return [...left, ...middle, ...right];\n}\n\n/**\n * Clear sticky positioning from all cells.\n *\n * @param host - The grid host element (render root for DOM queries)\n */\nexport function clearStickyOffsets(host: HTMLElement): void {\n // With light DOM, query the host element directly\n const cells = host.querySelectorAll('.sticky-left, .sticky-right');\n cells.forEach((cell) => {\n cell.classList.remove('sticky-left', 'sticky-right');\n (cell as HTMLElement).style.position = '';\n (cell as HTMLElement).style.left = '';\n (cell as HTMLElement).style.right = '';\n });\n}\n","/**\n * Pinned Columns Plugin (Class-based)\n *\n * Enables column pinning (sticky left/right positioning).\n */\n\nimport { getDirection } from '../../core/internal/utils';\nimport type { PluginManifest, PluginQuery } from '../../core/plugin/base-plugin';\nimport { BaseGridPlugin } from '../../core/plugin/base-plugin';\nimport type { ColumnConfig } from '../../core/types';\nimport type { ContextMenuParams, HeaderContextMenuItem } from '../context-menu/types';\nimport {\n applyStickyOffsets,\n clearStickyOffsets,\n getColumnPinned,\n getLeftStickyColumns,\n getRightStickyColumns,\n hasStickyColumns,\n reorderColumnsForPinning,\n} from './pinned-columns';\nimport type { PinnedColumnsConfig, PinnedPosition } from './types';\n\n/** Query type constant for checking if a column can be moved */\nconst QUERY_CAN_MOVE_COLUMN = 'canMoveColumn';\n\n/**\n * Pinned Columns Plugin for tbw-grid\n *\n * Freezes columns to the left or right edge of the grid—essential for keeping key\n * identifiers or action buttons visible while scrolling through wide datasets. Just set\n * `pinned: 'left'` or `pinned: 'right'` on your column definitions.\n *\n * ## Installation\n *\n * ```ts\n * import { PinnedColumnsPlugin } from '@toolbox-web/grid/plugins/pinned-columns';\n * ```\n *\n * ## Column Configuration\n *\n * | Property | Type | Description |\n * |----------|------|-------------|\n * | `pinned` | `'left' \\| 'right' \\| 'start' \\| 'end'` | Pin column to edge (logical or physical) |\n * | `meta.lockPinning` | `boolean` | `false` | Prevent user from pin/unpin via context menu |\n *\n * ### RTL Support\n *\n * Use logical values (`start`/`end`) for grids that work in both LTR and RTL layouts:\n * - `'start'` - Pins to left in LTR, right in RTL\n * - `'end'` - Pins to right in LTR, left in RTL\n *\n * ## CSS Custom Properties\n *\n * | Property | Default | Description |\n * |----------|---------|-------------|\n * | `--tbw-pinned-shadow` | `4px 0 8px rgba(0,0,0,0.1)` | Shadow on pinned column edge |\n * | `--tbw-pinned-border` | `var(--tbw-color-border)` | Border between pinned and scrollable |\n *\n * @example Pin ID Left and Actions Right\n * ```ts\n * import '@toolbox-web/grid';\n * import { PinnedColumnsPlugin } from '@toolbox-web/grid/plugins/pinned-columns';\n *\n * const grid = document.querySelector('tbw-grid');\n * grid.gridConfig = {\n * columns: [\n * { field: 'id', header: 'ID', pinned: 'left', width: 80 },\n * { field: 'name', header: 'Name' },\n * { field: 'email', header: 'Email' },\n * { field: 'department', header: 'Department' },\n * { field: 'actions', header: 'Actions', pinned: 'right', width: 120 },\n * ],\n * plugins: [new PinnedColumnsPlugin()],\n * };\n * ```\n *\n * @example RTL-Compatible Pinning\n * ```ts\n * // Same config works in LTR and RTL\n * grid.gridConfig = {\n * columns: [\n * { field: 'id', header: 'ID', pinned: 'start' }, // Left in LTR, Right in RTL\n * { field: 'name', header: 'Name' },\n * { field: 'actions', header: 'Actions', pinned: 'end' }, // Right in LTR, Left in RTL\n * ],\n * plugins: [new PinnedColumnsPlugin()],\n * };\n * ```\n *\n * @see {@link PinnedColumnsConfig} for configuration options\n *\n * @internal Extends BaseGridPlugin\n */\nexport class PinnedColumnsPlugin extends BaseGridPlugin<PinnedColumnsConfig> {\n /**\n * Plugin manifest - declares owned properties and handled queries.\n * @internal\n */\n static override readonly manifest: PluginManifest = {\n ownedProperties: [\n {\n property: 'pinned',\n level: 'column',\n description: 'the \"pinned\" column property',\n isUsed: (v) => v === 'left' || v === 'right' || v === 'start' || v === 'end',\n },\n {\n property: 'sticky',\n level: 'column',\n description: 'the \"sticky\" column property (deprecated, use \"pinned\")',\n isUsed: (v) => v === 'left' || v === 'right' || v === 'start' || v === 'end',\n },\n ],\n incompatibleWith: [\n {\n name: 'groupingColumns',\n reason:\n 'Pinning reorders columns to the grid edges, but moving a column out of its column group ' +\n 'is not supported. The group header layout cannot accommodate members at different positions.',\n },\n ],\n queries: [\n {\n type: QUERY_CAN_MOVE_COLUMN,\n description: 'Prevents pinned (sticky) columns from being moved/reordered',\n },\n {\n type: 'getStickyOffsets',\n description: 'Returns the sticky offsets for left/right pinned columns',\n },\n {\n type: 'getContextMenuItems',\n description: 'Contributes pin/unpin items to the header context menu',\n },\n ],\n };\n\n /** @internal */\n readonly name = 'pinnedColumns';\n\n /** @internal */\n protected override get defaultConfig(): Partial<PinnedColumnsConfig> {\n return {};\n }\n\n // #region Internal State\n private isApplied = false;\n private leftOffsets = new Map<string, number>();\n private rightOffsets = new Map<string, number>();\n /**\n * Snapshot of the column field order before the first context-menu pin.\n * Used to restore original positions when unpinning.\n */\n #originalColumnOrder: string[] = [];\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override detach(): void {\n this.leftOffsets.clear();\n this.rightOffsets.clear();\n this.isApplied = false;\n this.#originalColumnOrder = [];\n }\n // #endregion\n\n // #region Detection\n\n /**\n * Auto-detect sticky columns from column configuration.\n */\n static detect(rows: readonly unknown[], config: { columns?: ColumnConfig[] }): boolean {\n const columns = config?.columns;\n if (!Array.isArray(columns)) return false;\n return hasStickyColumns(columns);\n }\n // #endregion\n\n // #region Hooks\n\n /** @internal */\n override processColumns(columns: readonly ColumnConfig[]): ColumnConfig[] {\n const cols = [...columns];\n this.isApplied = hasStickyColumns(cols);\n if (!this.isApplied) return cols;\n\n const host = this.gridElement;\n const direction = host ? getDirection(host) : 'ltr';\n return reorderColumnsForPinning(cols, direction) as ColumnConfig[];\n }\n\n /** @internal */\n override afterRender(): void {\n if (!this.isApplied) {\n return;\n }\n\n const host = this.gridElement;\n const columns = [...this.columns];\n\n if (!hasStickyColumns(columns)) {\n clearStickyOffsets(host);\n this.isApplied = false;\n return;\n }\n\n // Apply sticky offsets after a microtask to ensure DOM is ready\n queueMicrotask(() => {\n applyStickyOffsets(host, columns);\n });\n }\n\n /**\n * Handle inter-plugin queries.\n * @internal\n */\n override handleQuery(query: PluginQuery): unknown {\n switch (query.type) {\n case QUERY_CAN_MOVE_COLUMN: {\n // Prevent pinned columns from being moved/reordered.\n // Pinned columns have fixed positions and should not be draggable.\n const column = query.context as ColumnConfig;\n if (getColumnPinned(column) != null) {\n return false;\n }\n return undefined; // Let other plugins or default behavior decide\n }\n case 'getStickyOffsets': {\n // Return the calculated sticky offsets for column virtualization\n return {\n left: Object.fromEntries(this.leftOffsets),\n right: Object.fromEntries(this.rightOffsets),\n };\n }\n case 'getContextMenuItems': {\n const params = query.context as ContextMenuParams;\n if (!params.isHeader) return undefined;\n\n const column = params.column as ColumnConfig;\n if (!column?.field) return undefined;\n\n // Don't offer pin/unpin for locked-pinning columns\n if (column.meta?.lockPinning) return undefined;\n\n // Don't offer pin/unpin when column grouping is active (incompatible)\n const groupingPlugin = this.grid?.getPluginByName('groupingColumns') as\n | { isGroupingActive(): boolean }\n | undefined;\n if (groupingPlugin?.isGroupingActive()) return undefined;\n\n const pinned = getColumnPinned(column);\n const isPinned = pinned != null;\n const items: HeaderContextMenuItem[] = [];\n\n if (isPinned) {\n items.push({\n id: 'pinned/unpin',\n label: 'Unpin Column',\n icon: '📌',\n order: 40,\n action: () => this.setPinPosition(column.field, undefined),\n });\n } else {\n items.push({\n id: 'pinned/pin-left',\n label: 'Pin Left',\n icon: '⬅',\n order: 40,\n action: () => this.setPinPosition(column.field, 'left'),\n });\n items.push({\n id: 'pinned/pin-right',\n label: 'Pin Right',\n icon: '➡',\n order: 41,\n action: () => this.setPinPosition(column.field, 'right'),\n });\n }\n\n return items;\n }\n default:\n return undefined;\n }\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Set the pin position for a column.\n * Updates the column's `pinned` property and triggers a full re-render.\n *\n * @param field - The field name of the column to pin/unpin\n * @param position - The pin position (`'left'`, `'right'`, `'start'`, `'end'`), or `undefined` to unpin\n */\n setPinPosition(field: string, position: PinnedPosition | undefined): void {\n // Read the currently-visible columns from the plugin accessor.\n // These are the post-processColumns result, which is the authoritative column set.\n const currentColumns = this.columns;\n if (!currentColumns?.length) return;\n\n const currentIndex = currentColumns.findIndex((col) => col.field === field);\n if (currentIndex === -1) return;\n\n const gridEl = this.gridElement as HTMLElement & { columns?: ColumnConfig[] };\n\n if (position) {\n // PINNING: snapshot original column order if this is the first context-menu pin.\n // The snapshot lets us restore columns to their original positions on unpin.\n if (this.#originalColumnOrder.length === 0) {\n this.#originalColumnOrder = currentColumns.map((c) => c.field);\n }\n\n // Set the pinned property; processColumns will reorder on next render\n const updated = currentColumns.map((col) => {\n if (col.field !== field) return col;\n const copy = { ...col };\n (copy as ColumnConfig & { pinned?: PinnedPosition }).pinned = position;\n delete (copy as ColumnConfig & { sticky?: PinnedPosition }).sticky;\n return copy;\n });\n\n gridEl.columns = updated;\n } else {\n // UNPINNING: restore column to its original position\n const col = currentColumns[currentIndex];\n const copy = { ...col };\n delete (copy as ColumnConfig & { pinned?: PinnedPosition }).pinned;\n delete (copy as ColumnConfig & { sticky?: PinnedPosition }).sticky;\n\n // Remove from current position\n const remaining = [...currentColumns];\n remaining.splice(currentIndex, 1);\n\n // Find the best insertion point using the original order snapshot\n const originalIndex = this.#originalColumnOrder.indexOf(field);\n if (originalIndex >= 0) {\n // Scan remaining non-pinned columns and find the first whose original\n // position is greater than this column's original position.\n let insertIndex = remaining.length;\n for (let i = 0; i < remaining.length; i++) {\n if (getColumnPinned(remaining[i])) continue; // skip pinned columns\n const otherOriginal = this.#originalColumnOrder.indexOf(remaining[i].field);\n if (otherOriginal > originalIndex) {\n insertIndex = i;\n break;\n }\n }\n remaining.splice(insertIndex, 0, copy);\n } else {\n // Original position unknown — keep at current index\n remaining.splice(Math.min(currentIndex, remaining.length), 0, copy);\n }\n\n // If no more pinned columns remain, clear the snapshot\n if (!remaining.some((c) => getColumnPinned(c) != null)) {\n this.#originalColumnOrder = [];\n }\n\n gridEl.columns = remaining;\n }\n }\n\n /**\n * Re-apply sticky offsets (e.g., after column resize).\n */\n refreshStickyOffsets(): void {\n const columns = [...this.columns];\n applyStickyOffsets(this.gridElement, columns);\n }\n\n /**\n * Get columns pinned to the left (after resolving logical positions for current direction).\n */\n getLeftPinnedColumns(): ColumnConfig[] {\n const columns = [...this.columns];\n const direction = getDirection(this.gridElement);\n return getLeftStickyColumns(columns, direction);\n }\n\n /**\n * Get columns pinned to the right (after resolving logical positions for current direction).\n */\n getRightPinnedColumns(): ColumnConfig[] {\n const columns = [...this.columns];\n const direction = getDirection(this.gridElement);\n return getRightStickyColumns(columns, direction);\n }\n\n /**\n * Clear all sticky positioning.\n */\n clearStickyPositions(): void {\n clearStickyOffsets(this.gridElement);\n }\n\n /**\n * Report horizontal scroll boundary offsets for pinned columns.\n * Used by keyboard navigation to ensure focused cells aren't hidden behind sticky columns.\n * @internal\n */\n override getHorizontalScrollOffsets(\n rowEl?: HTMLElement,\n focusedCell?: HTMLElement,\n ): { left: number; right: number; skipScroll?: boolean } | undefined {\n if (!this.isApplied) {\n return undefined;\n }\n\n let left = 0;\n let right = 0;\n\n if (rowEl) {\n // Calculate from rendered cells in the row\n const stickyLeftCells = rowEl.querySelectorAll('.sticky-left');\n const stickyRightCells = rowEl.querySelectorAll('.sticky-right');\n stickyLeftCells.forEach((el) => {\n left += (el as HTMLElement).offsetWidth;\n });\n stickyRightCells.forEach((el) => {\n right += (el as HTMLElement).offsetWidth;\n });\n } else {\n // Fall back to header row if no row element provided\n const host = this.gridElement;\n const headerCells = host.querySelectorAll('.header-row .cell');\n headerCells.forEach((cell) => {\n if (cell.classList.contains('sticky-left')) {\n left += (cell as HTMLElement).offsetWidth;\n } else if (cell.classList.contains('sticky-right')) {\n right += (cell as HTMLElement).offsetWidth;\n }\n });\n }\n\n // Skip horizontal scrolling if focused cell is pinned (it's always visible)\n const skipScroll =\n focusedCell?.classList.contains('sticky-left') || focusedCell?.classList.contains('sticky-right');\n\n return { left, right, skipScroll };\n }\n // #endregion\n}\n"],"names":["getColumnPinned","col","pinned","sticky","meta","resolveStickyPosition","position","direction","resolveInlinePosition","isResolvedLeft","isResolvedRight","hasStickyColumns","columns","some","applyStickyOffsets","host","headerCells","Array","from","querySelectorAll","length","getDirection","left","cell","find","c","getAttribute","field","classList","add","style","forEach","el","offsetWidth","right","reverse","clearStickyOffsets","remove","QUERY_CAN_MOVE_COLUMN","PinnedColumnsPlugin","BaseGridPlugin","static","ownedProperties","property","level","description","isUsed","v","incompatibleWith","name","reason","queries","type","defaultConfig","isApplied","leftOffsets","Map","rightOffsets","originalColumnOrder","detach","this","clear","detect","rows","config","isArray","processColumns","cols","gridElement","middle","push","reorderColumnsForPinning","afterRender","queueMicrotask","handleQuery","query","context","Object","fromEntries","params","isHeader","column","lockPinning","groupingPlugin","grid","getPluginByName","isGroupingActive","items","id","label","icon","order","action","setPinPosition","currentColumns","currentIndex","findIndex","gridEl","map","updated","copy","remaining","splice","originalIndex","indexOf","insertIndex","i","Math","min","refreshStickyOffsets","getLeftPinnedColumns","filter","getLeftStickyColumns","getRightPinnedColumns","getRightStickyColumns","clearStickyPositions","getHorizontalScrollOffsets","rowEl","focusedCell","stickyLeftCells","stickyRightCells","contains","skipScroll"],"mappings":"oaAqBO,SAASA,EAAgBC,GAC9B,OAAOA,EAAIC,QAAUD,EAAIE,QAAUF,EAAIG,MAAMF,QAAUD,EAAIG,MAAMD,MACnE,CAaO,SAASE,EAAsBC,EAA0BC,GAC9D,OAAOC,EAAAA,sBAAsBF,EAAUC,EACzC,CAKA,SAASE,EAAeR,EAAUM,GAChC,MAAML,EAASF,EAAgBC,GAC/B,QAAKC,GAC+C,SAA7CG,EAAsBH,EAAQK,EACvC,CAKA,SAASG,EAAgBT,EAAUM,GACjC,MAAML,EAASF,EAAgBC,GAC/B,QAAKC,GAC+C,UAA7CG,EAAsBH,EAAQK,EACvC,CA8BO,SAASI,EAAiBC,GAC/B,OAAOA,EAAQC,KAAMZ,GAAgC,MAAxBD,EAAgBC,GAC/C,CA4EO,SAASa,EAAmBC,EAAmBH,GAEpD,MAAMI,EAAcC,MAAMC,KAAKH,EAAKI,iBAAiB,sBACrD,IAAKH,EAAYI,OAAQ,OAGzB,MAAMb,EAAYc,EAAAA,aAAaN,GAG/B,IAAIO,EAAO,EACX,IAAA,MAAWrB,KAAOW,EAChB,GAAIH,EAAeR,EAAKM,GAAY,CAClC,MAAMgB,EAAOP,EAAYQ,KAAMC,GAAMA,EAAEC,aAAa,gBAAkBzB,EAAI0B,OACtEJ,IACFA,EAAKK,UAAUC,IAAI,eACnBN,EAAKO,MAAMxB,SAAW,SACtBiB,EAAKO,MAAMR,KAAOA,EAAO,KAGzBP,EAAKI,iBAAiB,oCAAoClB,EAAI0B,WAAWI,QAASC,IAChFA,EAAGJ,UAAUC,IAAI,eAChBG,EAAmBF,MAAMxB,SAAW,SACpC0B,EAAmBF,MAAMR,KAAOA,EAAO,OAE1CA,GAAQC,EAAKU,YAEjB,CAIF,IAAIC,EAAQ,EACZ,IAAA,MAAWjC,IAAO,IAAIW,GAASuB,UAC7B,GAAIzB,EAAgBT,EAAKM,GAAY,CACnC,MAAMgB,EAAOP,EAAYQ,KAAMC,GAAMA,EAAEC,aAAa,gBAAkBzB,EAAI0B,OACtEJ,IACFA,EAAKK,UAAUC,IAAI,gBACnBN,EAAKO,MAAMxB,SAAW,SACtBiB,EAAKO,MAAMI,MAAQA,EAAQ,KAE3BnB,EAAKI,iBAAiB,oCAAoClB,EAAI0B,WAAWI,QAASC,IAChFA,EAAGJ,UAAUC,IAAI,gBAChBG,EAAmBF,MAAMxB,SAAW,SACpC0B,EAAmBF,MAAMI,MAAQA,EAAQ,OAE5CA,GAASX,EAAKU,YAElB,CAEJ,CAkCO,SAASG,EAAmBrB,GAEnBA,EAAKI,iBAAiB,+BAC9BY,QAASR,IACbA,EAAKK,UAAUS,OAAO,cAAe,gBACpCd,EAAqBO,MAAMxB,SAAW,GACtCiB,EAAqBO,MAAMR,KAAO,GAClCC,EAAqBO,MAAMI,MAAQ,IAExC,CCxOA,MAAMI,EAAwB,gBAsEvB,MAAMC,UAA4BC,EAAAA,eAKvCC,gBAAoD,CAClDC,gBAAiB,CACf,CACEC,SAAU,SACVC,MAAO,SACPC,YAAa,+BACbC,OAASC,GAAY,SAANA,GAAsB,UAANA,GAAuB,UAANA,GAAuB,QAANA,GAEnE,CACEJ,SAAU,SACVC,MAAO,SACPC,YAAa,0DACbC,OAASC,GAAY,SAANA,GAAsB,UAANA,GAAuB,UAANA,GAAuB,QAANA,IAGrEC,iBAAkB,CAChB,CACEC,KAAM,kBACNC,OACE,yLAINC,QAAS,CACP,CACEC,KAAMd,EACNO,YAAa,+DAEf,CACEO,KAAM,mBACNP,YAAa,4DAEf,CACEO,KAAM,sBACNP,YAAa,4DAMVI,KAAO,gBAGhB,iBAAuBI,GACrB,MAAO,CAAA,CACT,CAGQC,WAAY,EACZC,gBAAkBC,IAClBC,iBAAmBD,IAK3BE,GAAiC,GAMxB,MAAAC,GACPC,KAAKL,YAAYM,QACjBD,KAAKH,aAAaI,QAClBD,KAAKN,WAAY,EACjBM,MAAKF,EAAuB,EAC9B,CAQA,aAAOI,CAAOC,EAA0BC,GACtC,MAAMpD,EAAUoD,GAAQpD,QACxB,QAAKK,MAAMgD,QAAQrD,IACZD,EAAiBC,EAC1B,CAMS,cAAAsD,CAAetD,GACtB,MAAMuD,EAAO,IAAIvD,GAEjB,GADAgD,KAAKN,UAAY3C,EAAiBwD,IAC7BP,KAAKN,UAAW,OAAOa,EAE5B,MAAMpD,EAAO6C,KAAKQ,YAElB,ODiCG,SAAkCxD,EAAyBL,EAA2B,OAC3F,MAAMe,EAAc,GACd+C,EAAgB,GAChBnC,EAAe,GAErB,IAAA,MAAWjC,KAAOW,EAAS,CACzB,MAAMV,EAASF,EAAgBC,GAC3BC,EAEe,SADAG,EAAsBH,EAAQK,GACtBe,EAAKgD,KAAKrE,GAC9BiC,EAAMoC,KAAKrE,GAEhBoE,EAAOC,KAAKrE,EAEhB,CAEA,MAAO,IAAIqB,KAAS+C,KAAWnC,EACjC,CClDWqC,CAAyBJ,EADdpD,EAAOM,eAAaN,GAAQ,MAEhD,CAGS,WAAAyD,GACP,IAAKZ,KAAKN,UACR,OAGF,MAAMvC,EAAO6C,KAAKQ,YACZxD,EAAU,IAAIgD,KAAKhD,SAEzB,IAAKD,EAAiBC,GAGpB,OAFAwB,EAAmBrB,QACnB6C,KAAKN,WAAY,GAKnBmB,eAAe,KACb3D,EAAmBC,EAAMH,IAE7B,CAMS,WAAA8D,CAAYC,GACnB,OAAQA,EAAMvB,MACZ,KAAKd,EAIH,OAA+B,MAA3BtC,EADW2E,EAAMC,eAIrB,EAEF,IAAK,mBAEH,MAAO,CACLtD,KAAMuD,OAAOC,YAAYlB,KAAKL,aAC9BrB,MAAO2C,OAAOC,YAAYlB,KAAKH,eAGnC,IAAK,sBAAuB,CAC1B,MAAMsB,EAASJ,EAAMC,QACrB,IAAKG,EAAOC,SAAU,OAEtB,MAAMC,EAASF,EAAOE,OACtB,IAAKA,GAAQtD,MAAO,OAGpB,GAAIsD,EAAO7E,MAAM8E,YAAa,OAG9B,MAAMC,EAAiBvB,KAAKwB,MAAMC,gBAAgB,mBAGlD,GAAIF,GAAgBG,mBAAoB,OAExC,MAEMC,EAAiC,GA2BvC,OA5B2B,MADZvF,EAAgBiF,GAK7BM,EAAMjB,KAAK,CACTkB,GAAI,eACJC,MAAO,eACPC,KAAM,KACNC,MAAO,GACPC,OAAQ,IAAMhC,KAAKiC,eAAeZ,EAAOtD,WAAO,MAGlD4D,EAAMjB,KAAK,CACTkB,GAAI,kBACJC,MAAO,WACPC,KAAM,IACNC,MAAO,GACPC,OAAQ,IAAMhC,KAAKiC,eAAeZ,EAAOtD,MAAO,UAElD4D,EAAMjB,KAAK,CACTkB,GAAI,mBACJC,MAAO,YACPC,KAAM,IACNC,MAAO,GACPC,OAAQ,IAAMhC,KAAKiC,eAAeZ,EAAOtD,MAAO,YAI7C4D,CACT,CACA,QACE,OAEN,CAYA,cAAAM,CAAelE,EAAerB,GAG5B,MAAMwF,EAAiBlC,KAAKhD,QAC5B,IAAKkF,GAAgB1E,OAAQ,OAE7B,MAAM2E,EAAeD,EAAeE,UAAW/F,GAAQA,EAAI0B,QAAUA,GACrE,IAAqB,IAAjBoE,EAAqB,OAEzB,MAAME,EAASrC,KAAKQ,YAEpB,GAAI9D,EAAU,CAG6B,IAArCsD,MAAKF,EAAqBtC,SAC5BwC,MAAKF,EAAuBoC,EAAeI,IAAKzE,GAAMA,EAAEE,QAI1D,MAAMwE,EAAUL,EAAeI,IAAKjG,IAClC,GAAIA,EAAI0B,QAAUA,EAAO,OAAO1B,EAChC,MAAMmG,EAAO,IAAKnG,GAGlB,OAFCmG,EAAoDlG,OAASI,SACtD8F,EAAoDjG,OACrDiG,IAGTH,EAAOrF,QAAUuF,CACnB,KAAO,CAEL,MACMC,EAAO,IADDN,EAAeC,WAEnBK,EAAoDlG,cACpDkG,EAAoDjG,OAG5D,MAAMkG,EAAY,IAAIP,GACtBO,EAAUC,OAAOP,EAAc,GAG/B,MAAMQ,EAAgB3C,MAAKF,EAAqB8C,QAAQ7E,GACxD,GAAI4E,GAAiB,EAAG,CAGtB,IAAIE,EAAcJ,EAAUjF,OAC5B,IAAA,IAASsF,EAAI,EAAGA,EAAIL,EAAUjF,OAAQsF,IAAK,CACzC,GAAI1G,EAAgBqG,EAAUK,IAAK,SAEnC,GADsB9C,MAAKF,EAAqB8C,QAAQH,EAAUK,GAAG/E,OACjD4E,EAAe,CACjCE,EAAcC,EACd,KACF,CACF,CACAL,EAAUC,OAAOG,EAAa,EAAGL,EACnC,MAEEC,EAAUC,OAAOK,KAAKC,IAAIb,EAAcM,EAAUjF,QAAS,EAAGgF,GAI3DC,EAAUxF,KAAMY,GAA4B,MAAtBzB,EAAgByB,MACzCmC,MAAKF,EAAuB,IAG9BuC,EAAOrF,QAAUyF,CACnB,CACF,CAKA,oBAAAQ,GACE,MAAMjG,EAAU,IAAIgD,KAAKhD,SACzBE,EAAmB8C,KAAKQ,YAAaxD,EACvC,CAKA,oBAAAkG,GAGE,OD1TG,SAA8BlG,EAAgBL,EAA2B,OAC9E,OAAOK,EAAQmG,OAAQ9G,GAAQQ,EAAeR,EAAKM,GACrD,CCwTWyG,CAFS,IAAIpD,KAAKhD,SACPS,EAAAA,aAAauC,KAAKQ,aAEtC,CAKA,qBAAA6C,GAGE,ODxTG,SAA+BrG,EAAgBL,EAA2B,OAC/E,OAAOK,EAAQmG,OAAQ9G,GAAQS,EAAgBT,EAAKM,GACtD,CCsTW2G,CAFS,IAAItD,KAAKhD,SACPS,EAAAA,aAAauC,KAAKQ,aAEtC,CAKA,oBAAA+C,GACE/E,EAAmBwB,KAAKQ,YAC1B,CAOS,0BAAAgD,CACPC,EACAC,GAEA,IAAK1D,KAAKN,UACR,OAGF,IAAIhC,EAAO,EACPY,EAAQ,EAEZ,GAAImF,EAAO,CAET,MAAME,EAAkBF,EAAMlG,iBAAiB,gBACzCqG,EAAmBH,EAAMlG,iBAAiB,iBAChDoG,EAAgBxF,QAASC,IACvBV,GAASU,EAAmBC,cAE9BuF,EAAiBzF,QAASC,IACxBE,GAAUF,EAAmBC,aAEjC,KAAO,CAEQ2B,KAAKQ,YACOjD,iBAAiB,qBAC9BY,QAASR,IACfA,EAAKK,UAAU6F,SAAS,eAC1BnG,GAASC,EAAqBU,YACrBV,EAAKK,UAAU6F,SAAS,kBACjCvF,GAAUX,EAAqBU,cAGrC,CAGA,MAAMyF,EACJJ,GAAa1F,UAAU6F,SAAS,gBAAkBH,GAAa1F,UAAU6F,SAAS,gBAEpF,MAAO,CAAEnG,OAAMY,QAAOwF,aACxB"}
|
|
1
|
+
{"version":3,"file":"pinned-columns.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/pinned-columns/pinned-columns.ts","../../../../../libs/grid/src/lib/plugins/pinned-columns/PinnedColumnsPlugin.ts"],"sourcesContent":["/**\n * Pinned Columns Core Logic\n *\n * Pure functions for applying pinned (sticky) column positioning.\n */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport { getDirection, resolveInlinePosition, type TextDirection } from '../../core/internal/utils';\nimport type { PinnedPosition, ResolvedPinnedPosition } from './types';\n\n// Keep deprecated imports working (StickyPosition = PinnedPosition)\ntype StickyPosition = PinnedPosition;\ntype ResolvedStickyPosition = ResolvedPinnedPosition;\n\n/**\n * Get the effective pinned position from a column, checking `pinned` first then `sticky` (deprecated).\n *\n * @param col - Column configuration object\n * @returns The pinned position, or undefined if not pinned\n */\nexport function getColumnPinned(col: any): PinnedPosition | undefined {\n return col.pinned ?? col.sticky ?? col.meta?.pinned ?? col.meta?.sticky;\n}\n\n/**\n * Resolve a pinned position to a physical position based on text direction.\n *\n * - `'left'` / `'right'` → unchanged (physical values)\n * - `'start'` → `'left'` in LTR, `'right'` in RTL\n * - `'end'` → `'right'` in LTR, `'left'` in RTL\n *\n * @param position - The pinned position (logical or physical)\n * @param direction - Text direction ('ltr' or 'rtl')\n * @returns Physical pinned position ('left' or 'right')\n */\nexport function resolveStickyPosition(position: StickyPosition, direction: TextDirection): ResolvedStickyPosition {\n return resolveInlinePosition(position, direction);\n}\n\n/**\n * Check if a column is pinned on the left (after resolving logical positions).\n */\nfunction isResolvedLeft(col: any, direction: TextDirection): boolean {\n const pinned = getColumnPinned(col);\n if (!pinned) return false;\n return resolveStickyPosition(pinned, direction) === 'left';\n}\n\n/**\n * Check if a column is pinned on the right (after resolving logical positions).\n */\nfunction isResolvedRight(col: any, direction: TextDirection): boolean {\n const pinned = getColumnPinned(col);\n if (!pinned) return false;\n return resolveStickyPosition(pinned, direction) === 'right';\n}\n\n/**\n * Get columns that should be sticky on the left.\n *\n * @param columns - Array of column configurations\n * @param direction - Text direction (default: 'ltr')\n * @returns Array of columns with sticky='left' or sticky='start' (in LTR)\n */\nexport function getLeftStickyColumns(columns: any[], direction: TextDirection = 'ltr'): any[] {\n return columns.filter((col) => isResolvedLeft(col, direction));\n}\n\n/**\n * Get columns that should be sticky on the right.\n *\n * @param columns - Array of column configurations\n * @param direction - Text direction (default: 'ltr')\n * @returns Array of columns with sticky='right' or sticky='end' (in LTR)\n */\nexport function getRightStickyColumns(columns: any[], direction: TextDirection = 'ltr'): any[] {\n return columns.filter((col) => isResolvedRight(col, direction));\n}\n\n/**\n * Check if any columns have sticky positioning.\n *\n * @param columns - Array of column configurations\n * @returns True if any column has sticky position\n */\nexport function hasStickyColumns(columns: any[]): boolean {\n return columns.some((col) => getColumnPinned(col) != null);\n}\n\n/**\n * Get the sticky position of a column.\n *\n * @param column - Column configuration\n * @returns The sticky position or null if not sticky\n */\nexport function getColumnStickyPosition(column: any): StickyPosition | null {\n return getColumnPinned(column) ?? null;\n}\n\n\n/**\n * Calculate left offsets for sticky-left columns.\n * Returns a map of field -> offset in pixels.\n *\n * @param columns - Array of column configurations (in order)\n * @param getColumnWidth - Function to get column width by field\n * @param direction - Text direction (default: 'ltr')\n * @returns Map of field to left offset\n */\nexport function calculateLeftStickyOffsets(\n columns: any[],\n getColumnWidth: (field: string) => number,\n direction: TextDirection = 'ltr',\n): Map<string, number> {\n const offsets = new Map<string, number>();\n let currentOffset = 0;\n\n for (const col of columns) {\n if (isResolvedLeft(col, direction)) {\n offsets.set(col.field, currentOffset);\n currentOffset += getColumnWidth(col.field);\n }\n }\n\n return offsets;\n}\n\n/**\n * Calculate right offsets for sticky-right columns.\n * Processes columns in reverse order.\n *\n * @param columns - Array of column configurations (in order)\n * @param getColumnWidth - Function to get column width by field\n * @param direction - Text direction (default: 'ltr')\n * @returns Map of field to right offset\n */\nexport function calculateRightStickyOffsets(\n columns: any[],\n getColumnWidth: (field: string) => number,\n direction: TextDirection = 'ltr',\n): Map<string, number> {\n const offsets = new Map<string, number>();\n let currentOffset = 0;\n\n // Process in reverse for right-sticky columns\n const reversed = [...columns].reverse();\n for (const col of reversed) {\n if (isResolvedRight(col, direction)) {\n offsets.set(col.field, currentOffset);\n currentOffset += getColumnWidth(col.field);\n }\n }\n\n return offsets;\n}\n\n/**\n * Apply sticky offsets to header and body cells.\n * This modifies the DOM elements in place.\n *\n * @param host - The grid host element (render root for DOM queries)\n * @param columns - Array of column configurations\n */\nexport function applyStickyOffsets(host: HTMLElement, columns: any[]): void {\n // With light DOM, query the host element directly\n const headerCells = Array.from(host.querySelectorAll('.header-row .cell')) as HTMLElement[];\n if (!headerCells.length) return;\n\n // Detect text direction from the host element\n const direction = getDirection(host);\n\n // Apply left sticky (includes 'start' in LTR, 'end' in RTL)\n let left = 0;\n for (const col of columns) {\n if (isResolvedLeft(col, direction)) {\n const cell = headerCells.find((c) => c.getAttribute('data-field') === col.field);\n if (cell) {\n cell.classList.add('sticky-left');\n cell.style.position = 'sticky';\n cell.style.left = left + 'px';\n // Body cells: use data-field for reliable matching (data-col indices may differ\n // between _columns and _visibleColumns due to hidden/utility columns)\n host.querySelectorAll(`.data-grid-row .cell[data-field=\"${col.field}\"]`).forEach((el) => {\n el.classList.add('sticky-left');\n (el as HTMLElement).style.position = 'sticky';\n (el as HTMLElement).style.left = left + 'px';\n });\n left += cell.offsetWidth;\n }\n }\n }\n\n // Apply right sticky (includes 'end' in LTR, 'start' in RTL) - process in reverse\n let right = 0;\n for (const col of [...columns].reverse()) {\n if (isResolvedRight(col, direction)) {\n const cell = headerCells.find((c) => c.getAttribute('data-field') === col.field);\n if (cell) {\n cell.classList.add('sticky-right');\n cell.style.position = 'sticky';\n cell.style.right = right + 'px';\n // Body cells: use data-field for reliable matching\n host.querySelectorAll(`.data-grid-row .cell[data-field=\"${col.field}\"]`).forEach((el) => {\n el.classList.add('sticky-right');\n (el as HTMLElement).style.position = 'sticky';\n (el as HTMLElement).style.right = right + 'px';\n });\n right += cell.offsetWidth;\n }\n }\n }\n}\n\n/**\n * Reorder columns so that pinned-left columns come first and pinned-right columns come last.\n * Maintains the relative order within each group (left-pinned, unpinned, right-pinned).\n *\n * @param columns - Array of column configurations (in their current order)\n * @param direction - Text direction ('ltr' or 'rtl'), used to resolve logical positions\n * @returns New array with pinned columns moved to the edges\n */\nexport function reorderColumnsForPinning(columns: readonly any[], direction: TextDirection = 'ltr'): any[] {\n const left: any[] = [];\n const middle: any[] = [];\n const right: any[] = [];\n\n for (const col of columns) {\n const pinned = getColumnPinned(col);\n if (pinned) {\n const resolved = resolveStickyPosition(pinned, direction);\n if (resolved === 'left') left.push(col);\n else right.push(col);\n } else {\n middle.push(col);\n }\n }\n\n return [...left, ...middle, ...right];\n}\n\n/**\n * Clear sticky positioning from all cells.\n *\n * @param host - The grid host element (render root for DOM queries)\n */\nexport function clearStickyOffsets(host: HTMLElement): void {\n // With light DOM, query the host element directly\n const cells = host.querySelectorAll('.sticky-left, .sticky-right');\n cells.forEach((cell) => {\n cell.classList.remove('sticky-left', 'sticky-right');\n (cell as HTMLElement).style.position = '';\n (cell as HTMLElement).style.left = '';\n (cell as HTMLElement).style.right = '';\n });\n}\n","/**\n * Pinned Columns Plugin (Class-based)\n *\n * Enables column pinning (sticky left/right positioning).\n */\n\nimport { getDirection } from '../../core/internal/utils';\nimport type { PluginManifest, PluginQuery } from '../../core/plugin/base-plugin';\nimport { BaseGridPlugin } from '../../core/plugin/base-plugin';\nimport type { ColumnConfig } from '../../core/types';\nimport type { ContextMenuParams, HeaderContextMenuItem } from '../context-menu/types';\nimport {\n applyStickyOffsets,\n clearStickyOffsets,\n getColumnPinned,\n getLeftStickyColumns,\n getRightStickyColumns,\n hasStickyColumns,\n reorderColumnsForPinning,\n} from './pinned-columns';\nimport type { PinnedColumnsConfig, PinnedPosition } from './types';\n\n/** Query type constant for checking if a column can be moved */\nconst QUERY_CAN_MOVE_COLUMN = 'canMoveColumn';\n\n/**\n * Pinned Columns Plugin for tbw-grid\n *\n * Freezes columns to the left or right edge of the grid—essential for keeping key\n * identifiers or action buttons visible while scrolling through wide datasets. Just set\n * `pinned: 'left'` or `pinned: 'right'` on your column definitions.\n *\n * ## Installation\n *\n * ```ts\n * import { PinnedColumnsPlugin } from '@toolbox-web/grid/plugins/pinned-columns';\n * ```\n *\n * ## Column Configuration\n *\n * | Property | Type | Description |\n * |----------|------|-------------|\n * | `pinned` | `'left' \\| 'right' \\| 'start' \\| 'end'` | Pin column to edge (logical or physical) |\n * | `meta.lockPinning` | `boolean` | `false` | Prevent user from pin/unpin via context menu |\n *\n * ### RTL Support\n *\n * Use logical values (`start`/`end`) for grids that work in both LTR and RTL layouts:\n * - `'start'` - Pins to left in LTR, right in RTL\n * - `'end'` - Pins to right in LTR, left in RTL\n *\n * ## CSS Custom Properties\n *\n * | Property | Default | Description |\n * |----------|---------|-------------|\n * | `--tbw-pinned-shadow` | `4px 0 8px rgba(0,0,0,0.1)` | Shadow on pinned column edge |\n * | `--tbw-pinned-border` | `var(--tbw-color-border)` | Border between pinned and scrollable |\n *\n * @example Pin ID Left and Actions Right\n * ```ts\n * import { queryGrid } from '@toolbox-web/grid';\n * import { PinnedColumnsPlugin } from '@toolbox-web/grid/plugins/pinned-columns';\n *\n * const grid = queryGrid('tbw-grid');\n * grid.gridConfig = {\n * columns: [\n * { field: 'id', header: 'ID', pinned: 'left', width: 80 },\n * { field: 'name', header: 'Name' },\n * { field: 'email', header: 'Email' },\n * { field: 'department', header: 'Department' },\n * { field: 'actions', header: 'Actions', pinned: 'right', width: 120 },\n * ],\n * plugins: [new PinnedColumnsPlugin()],\n * };\n * ```\n *\n * @example RTL-Compatible Pinning\n * ```ts\n * // Same config works in LTR and RTL\n * grid.gridConfig = {\n * columns: [\n * { field: 'id', header: 'ID', pinned: 'start' }, // Left in LTR, Right in RTL\n * { field: 'name', header: 'Name' },\n * { field: 'actions', header: 'Actions', pinned: 'end' }, // Right in LTR, Left in RTL\n * ],\n * plugins: [new PinnedColumnsPlugin()],\n * };\n * ```\n *\n * @see {@link PinnedColumnsConfig} for configuration options\n *\n * @internal Extends BaseGridPlugin\n */\nexport class PinnedColumnsPlugin extends BaseGridPlugin<PinnedColumnsConfig> {\n /**\n * Plugin manifest - declares owned properties and handled queries.\n * @internal\n */\n static override readonly manifest: PluginManifest = {\n ownedProperties: [\n {\n property: 'pinned',\n level: 'column',\n description: 'the \"pinned\" column property',\n isUsed: (v) => v === 'left' || v === 'right' || v === 'start' || v === 'end',\n },\n {\n property: 'sticky',\n level: 'column',\n description: 'the \"sticky\" column property (deprecated, use \"pinned\")',\n isUsed: (v) => v === 'left' || v === 'right' || v === 'start' || v === 'end',\n },\n ],\n incompatibleWith: [\n {\n name: 'groupingColumns',\n reason:\n 'Pinning reorders columns to the grid edges, but moving a column out of its column group ' +\n 'is not supported. The group header layout cannot accommodate members at different positions.',\n },\n ],\n queries: [\n {\n type: QUERY_CAN_MOVE_COLUMN,\n description: 'Prevents pinned (sticky) columns from being moved/reordered',\n },\n {\n type: 'getStickyOffsets',\n description: 'Returns the sticky offsets for left/right pinned columns',\n },\n {\n type: 'getContextMenuItems',\n description: 'Contributes pin/unpin items to the header context menu',\n },\n ],\n };\n\n /** @internal */\n readonly name = 'pinnedColumns';\n\n /** @internal */\n protected override get defaultConfig(): Partial<PinnedColumnsConfig> {\n return {};\n }\n\n // #region Internal State\n private isApplied = false;\n private leftOffsets = new Map<string, number>();\n private rightOffsets = new Map<string, number>();\n /**\n * Snapshot of the column field order before the first context-menu pin.\n * Used to restore original positions when unpinning.\n */\n #originalColumnOrder: string[] = [];\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override detach(): void {\n this.leftOffsets.clear();\n this.rightOffsets.clear();\n this.isApplied = false;\n this.#originalColumnOrder = [];\n }\n // #endregion\n\n // #region Detection\n\n /**\n * Auto-detect sticky columns from column configuration.\n */\n static detect(rows: readonly unknown[], config: { columns?: ColumnConfig[] }): boolean {\n const columns = config?.columns;\n if (!Array.isArray(columns)) return false;\n return hasStickyColumns(columns);\n }\n // #endregion\n\n // #region Hooks\n\n /** @internal */\n override processColumns(columns: readonly ColumnConfig[]): ColumnConfig[] {\n const cols = [...columns];\n this.isApplied = hasStickyColumns(cols);\n if (!this.isApplied) return cols;\n\n const host = this.gridElement;\n const direction = host ? getDirection(host) : 'ltr';\n return reorderColumnsForPinning(cols, direction) as ColumnConfig[];\n }\n\n /** @internal */\n override afterRender(): void {\n if (!this.isApplied) {\n return;\n }\n\n const host = this.gridElement;\n const columns = [...this.columns];\n\n if (!hasStickyColumns(columns)) {\n clearStickyOffsets(host);\n this.isApplied = false;\n return;\n }\n\n // Apply sticky offsets after a microtask to ensure DOM is ready\n queueMicrotask(() => {\n applyStickyOffsets(host, columns);\n });\n }\n\n /**\n * Handle inter-plugin queries.\n * @internal\n */\n override handleQuery(query: PluginQuery): unknown {\n switch (query.type) {\n case QUERY_CAN_MOVE_COLUMN: {\n // Prevent pinned columns from being moved/reordered.\n // Pinned columns have fixed positions and should not be draggable.\n const column = query.context as ColumnConfig;\n if (getColumnPinned(column) != null) {\n return false;\n }\n return undefined; // Let other plugins or default behavior decide\n }\n case 'getStickyOffsets': {\n // Return the calculated sticky offsets for column virtualization\n return {\n left: Object.fromEntries(this.leftOffsets),\n right: Object.fromEntries(this.rightOffsets),\n };\n }\n case 'getContextMenuItems': {\n const params = query.context as ContextMenuParams;\n if (!params.isHeader) return undefined;\n\n const column = params.column as ColumnConfig;\n if (!column?.field) return undefined;\n\n // Don't offer pin/unpin for locked-pinning columns\n if (column.meta?.lockPinning) return undefined;\n\n // Don't offer pin/unpin when column grouping is active (incompatible)\n const groupingPlugin = this.grid?.getPluginByName('groupingColumns') as\n | { isGroupingActive(): boolean }\n | undefined;\n if (groupingPlugin?.isGroupingActive()) return undefined;\n\n const pinned = getColumnPinned(column);\n const isPinned = pinned != null;\n const items: HeaderContextMenuItem[] = [];\n\n if (isPinned) {\n items.push({\n id: 'pinned/unpin',\n label: 'Unpin Column',\n icon: '📌',\n order: 40,\n action: () => this.setPinPosition(column.field, undefined),\n });\n } else {\n items.push({\n id: 'pinned/pin-left',\n label: 'Pin Left',\n icon: '⬅',\n order: 40,\n action: () => this.setPinPosition(column.field, 'left'),\n });\n items.push({\n id: 'pinned/pin-right',\n label: 'Pin Right',\n icon: '➡',\n order: 41,\n action: () => this.setPinPosition(column.field, 'right'),\n });\n }\n\n return items;\n }\n default:\n return undefined;\n }\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Set the pin position for a column.\n * Updates the column's `pinned` property and triggers a full re-render.\n *\n * @param field - The field name of the column to pin/unpin\n * @param position - The pin position (`'left'`, `'right'`, `'start'`, `'end'`), or `undefined` to unpin\n */\n setPinPosition(field: string, position: PinnedPosition | undefined): void {\n // Read the currently-visible columns from the plugin accessor.\n // These are the post-processColumns result, which is the authoritative column set.\n const currentColumns = this.columns;\n if (!currentColumns?.length) return;\n\n const currentIndex = currentColumns.findIndex((col) => col.field === field);\n if (currentIndex === -1) return;\n\n const gridEl = this.gridElement as HTMLElement & { columns?: ColumnConfig[] };\n\n if (position) {\n // PINNING: snapshot original column order if this is the first context-menu pin.\n // The snapshot lets us restore columns to their original positions on unpin.\n if (this.#originalColumnOrder.length === 0) {\n this.#originalColumnOrder = currentColumns.map((c) => c.field);\n }\n\n // Set the pinned property; processColumns will reorder on next render\n const updated = currentColumns.map((col) => {\n if (col.field !== field) return col;\n const copy = { ...col };\n (copy as ColumnConfig & { pinned?: PinnedPosition }).pinned = position;\n delete (copy as ColumnConfig & { sticky?: PinnedPosition }).sticky;\n return copy;\n });\n\n gridEl.columns = updated;\n } else {\n // UNPINNING: restore column to its original position\n const col = currentColumns[currentIndex];\n const copy = { ...col };\n delete (copy as ColumnConfig & { pinned?: PinnedPosition }).pinned;\n delete (copy as ColumnConfig & { sticky?: PinnedPosition }).sticky;\n\n // Remove from current position\n const remaining = [...currentColumns];\n remaining.splice(currentIndex, 1);\n\n // Find the best insertion point using the original order snapshot\n const originalIndex = this.#originalColumnOrder.indexOf(field);\n if (originalIndex >= 0) {\n // Scan remaining non-pinned columns and find the first whose original\n // position is greater than this column's original position.\n let insertIndex = remaining.length;\n for (let i = 0; i < remaining.length; i++) {\n if (getColumnPinned(remaining[i])) continue; // skip pinned columns\n const otherOriginal = this.#originalColumnOrder.indexOf(remaining[i].field);\n if (otherOriginal > originalIndex) {\n insertIndex = i;\n break;\n }\n }\n remaining.splice(insertIndex, 0, copy);\n } else {\n // Original position unknown — keep at current index\n remaining.splice(Math.min(currentIndex, remaining.length), 0, copy);\n }\n\n // If no more pinned columns remain, clear the snapshot\n if (!remaining.some((c) => getColumnPinned(c) != null)) {\n this.#originalColumnOrder = [];\n }\n\n gridEl.columns = remaining;\n }\n }\n\n /**\n * Re-apply sticky offsets (e.g., after column resize).\n */\n refreshStickyOffsets(): void {\n const columns = [...this.columns];\n applyStickyOffsets(this.gridElement, columns);\n }\n\n /**\n * Get columns pinned to the left (after resolving logical positions for current direction).\n */\n getLeftPinnedColumns(): ColumnConfig[] {\n const columns = [...this.columns];\n const direction = getDirection(this.gridElement);\n return getLeftStickyColumns(columns, direction);\n }\n\n /**\n * Get columns pinned to the right (after resolving logical positions for current direction).\n */\n getRightPinnedColumns(): ColumnConfig[] {\n const columns = [...this.columns];\n const direction = getDirection(this.gridElement);\n return getRightStickyColumns(columns, direction);\n }\n\n /**\n * Clear all sticky positioning.\n */\n clearStickyPositions(): void {\n clearStickyOffsets(this.gridElement);\n }\n\n /**\n * Report horizontal scroll boundary offsets for pinned columns.\n * Used by keyboard navigation to ensure focused cells aren't hidden behind sticky columns.\n * @internal\n */\n override getHorizontalScrollOffsets(\n rowEl?: HTMLElement,\n focusedCell?: HTMLElement,\n ): { left: number; right: number; skipScroll?: boolean } | undefined {\n if (!this.isApplied) {\n return undefined;\n }\n\n let left = 0;\n let right = 0;\n\n if (rowEl) {\n // Calculate from rendered cells in the row\n const stickyLeftCells = rowEl.querySelectorAll('.sticky-left');\n const stickyRightCells = rowEl.querySelectorAll('.sticky-right');\n stickyLeftCells.forEach((el) => {\n left += (el as HTMLElement).offsetWidth;\n });\n stickyRightCells.forEach((el) => {\n right += (el as HTMLElement).offsetWidth;\n });\n } else {\n // Fall back to header row if no row element provided\n const host = this.gridElement;\n const headerCells = host.querySelectorAll('.header-row .cell');\n headerCells.forEach((cell) => {\n if (cell.classList.contains('sticky-left')) {\n left += (cell as HTMLElement).offsetWidth;\n } else if (cell.classList.contains('sticky-right')) {\n right += (cell as HTMLElement).offsetWidth;\n }\n });\n }\n\n // Skip horizontal scrolling if focused cell is pinned (it's always visible)\n const skipScroll =\n focusedCell?.classList.contains('sticky-left') || focusedCell?.classList.contains('sticky-right');\n\n return { left, right, skipScroll };\n }\n // #endregion\n}\n"],"names":["getColumnPinned","col","pinned","sticky","meta","resolveStickyPosition","position","direction","resolveInlinePosition","isResolvedLeft","isResolvedRight","hasStickyColumns","columns","some","applyStickyOffsets","host","headerCells","Array","from","querySelectorAll","length","getDirection","left","cell","find","c","getAttribute","field","classList","add","style","forEach","el","offsetWidth","right","reverse","clearStickyOffsets","remove","QUERY_CAN_MOVE_COLUMN","PinnedColumnsPlugin","BaseGridPlugin","static","ownedProperties","property","level","description","isUsed","v","incompatibleWith","name","reason","queries","type","defaultConfig","isApplied","leftOffsets","Map","rightOffsets","originalColumnOrder","detach","this","clear","detect","rows","config","isArray","processColumns","cols","gridElement","middle","push","reorderColumnsForPinning","afterRender","queueMicrotask","handleQuery","query","context","Object","fromEntries","params","isHeader","column","lockPinning","groupingPlugin","grid","getPluginByName","isGroupingActive","items","id","label","icon","order","action","setPinPosition","currentColumns","currentIndex","findIndex","gridEl","map","updated","copy","remaining","splice","originalIndex","indexOf","insertIndex","i","Math","min","refreshStickyOffsets","getLeftPinnedColumns","filter","getLeftStickyColumns","getRightPinnedColumns","getRightStickyColumns","clearStickyPositions","getHorizontalScrollOffsets","rowEl","focusedCell","stickyLeftCells","stickyRightCells","contains","skipScroll"],"mappings":"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"}
|
package/umd/plugins/print.umd.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports,require("../../core/internal/utils"),require("../../core/plugin/base-plugin")):"function"==typeof define&&define.amd?define(["exports","../../core/internal/utils","../../core/plugin/base-plugin"],i):i((t="undefined"!=typeof globalThis?globalThis:t||self).TbwGridPlugin_print={},t.TbwGrid,t.TbwGrid)}(this,function(t,i,e){"use strict";const n="tbw-print-isolation-style";async function r(t,e={}){const{orientation:r="landscape"}=e,o=t.id;document.querySelectorAll(`#${CSS.escape(o)}`).length>1&&console.warn(`${i.gridPrefix(o,"print")} Multiple elements found with id="${o}". Print isolation may not work correctly. Ensure each grid has a unique ID.`),document.getElementById(n)?.remove();const a=function(t,i){const e=document.createElement("style");return e.id=n,e.textContent=`\n /* Print isolation: hide everything except the target grid */\n @media print {\n /* Hide all body children by default */\n body > *:not(#${t}) {\n display: none !important;\n }\n\n /* But show the grid and ensure it's not hidden by ancestor rules */\n #${t} {\n display: block !important;\n position: static !important;\n visibility: visible !important;\n opacity: 1 !important;\n overflow: visible !important;\n height: auto !important;\n width: 100% !important;\n max-height: none !important;\n margin: 0 !important;\n padding: 0 !important;\n transform: none !important;\n }\n\n /* If grid is nested, we need to show its ancestors too */\n #${t},\n #${t} * {\n visibility: visible !important;\n }\n\n /* Walk up the DOM and show all ancestors of the grid */\n body *:has(> #${t}),\n body *:has(#${t}) {\n display: block !important;\n visibility: visible !important;\n opacity: 1 !important;\n overflow: visible !important;\n height: auto !important;\n position: static !important;\n transform: none !important;\n background: transparent !important;\n border: none !important;\n padding: 0 !important;\n margin: 0 !important;\n }\n\n /* Hide siblings of ancestors (everything that's not in the path to the grid) */\n body *:has(#${t}) > *:not(:has(#${t})):not(#${t}) {\n display: none !important;\n }\n\n /* Page settings */\n @page {\n size: ${i};\n margin: 1cm;\n }\n\n /* Ensure proper print styling */\n body {\n margin: 0 !important;\n padding: 0 !important;\n background: white !important;\n color-scheme: light !important;\n }\n }\n\n /* Screen: also apply isolation for print preview */\n @media screen {\n /* When this stylesheet is active, we're about to print */\n /* No screen-specific rules needed - isolation only applies to print */\n }\n `,e}(o,r);return document.head.appendChild(a),new Promise(t=>{const i=()=>{window.removeEventListener("afterprint",i),document.getElementById(n)?.remove(),t()};window.addEventListener("afterprint",i),window.print(),setTimeout(()=>{window.removeEventListener("afterprint",i),document.getElementById(n)?.remove(),t()},5e3)})}const o={button:!1,orientation:"landscape",warnThreshold:500,maxRows:0,includeTitle:!0,includeTimestamp:!0,title:"",isolate:!1};class a extends e.BaseGridPlugin{name="print";version="1.0.0";styles=".tbw-print-header,.tbw-print-footer{display:none}@media print{tbw-grid{overflow:visible!important;height:auto!important;border:none!important;border-radius:0!important;color-scheme:light only;-webkit-print-color-adjust:exact;print-color-adjust:exact}tbw-grid .tbw-grid-content{overflow:visible!important;height:auto!important;max-height:none!important}tbw-grid .tbw-scroll-area{overflow:visible!important;height:auto!important;max-height:none!important}tbw-grid .rows-body{overflow:visible!important;height:auto!important;max-height:none!important}tbw-grid .rows-container,tbw-grid .rows-viewport,tbw-grid .rows{overflow:visible!important;height:auto!important;max-height:none!important;transform:none!important}tbw-grid .rows-viewport .rows{position:static!important}tbw-grid .resize-handle,tbw-grid [part=sort-indicator],tbw-grid .tbw-filter-btn,tbw-grid .tool-panel,tbw-grid .tool-panel-content,tbw-grid .tbw-shell-header,tbw-grid .shell-toolbar,tbw-grid .tool-panel-toggle,tbw-grid [data-print-hide],tbw-grid .expander-cell,tbw-grid .tree-toggle,tbw-grid .context-menu,tbw-grid .faux-vscroll{display:none!important}tbw-grid .tbw-print-header{display:flex;justify-content:space-between;align-items:baseline;padding:var(--tbw-spacing-md, .5em) 0;margin-bottom:var(--tbw-spacing-md, .5em);border-bottom:2px solid var(--tbw-print-border, var(--tbw-color-border-strong));font-family:inherit}.tbw-print-header-title{font-size:1.25em;font-weight:700}.tbw-print-header-timestamp{font-size:var(--tbw-font-size-sm, .875em);color:var(--tbw-print-muted, var(--tbw-color-fg-muted))}tbw-grid .tbw-print-footer{display:block;margin-top:var(--tbw-spacing-md, .5em);padding-top:var(--tbw-spacing-md, .5em);border-top:1px solid var(--tbw-print-border, var(--tbw-color-border));font-size:var(--tbw-font-size-xs, .75em);color:var(--tbw-print-muted, var(--tbw-color-fg-muted));text-align:end}tbw-grid .data-grid-row{break-inside:avoid;page-break-inside:avoid}tbw-grid .cell{border:1px solid var(--tbw-print-cell-border, var(--tbw-color-border))!important}tbw-grid .header-row,tbw-grid .data-grid-row{padding-inline-end:1px}tbw-grid .data-grid-row:hover,tbw-grid .cell:hover{background:inherit!important}@page{margin:1cm}@page{tbw-grid.print-landscape{size:landscape}}@page{tbw-grid.print-portrait{size:portrait}}}";#t=!1;#i=null;#e=null;#n=null;#r=null;#o=null;#a=null;get#s(){return this.grid}isPrinting(){return this.#t}async print(t){if(this.#t)return void console.warn("[PrintPlugin] Print already in progress");const i=this.gridElement;if(!i)return void console.warn("[PrintPlugin] Grid not available");const e={...o,...this.config,...t},n=this.rows.length;let r=n,a=!1;if(e.warnThreshold>0&&n>e.warnThreshold){const t=e.maxRows>0?`\n\nNote: Output will be limited to ${e.maxRows.toLocaleString()} rows.`:"";if(!confirm(`This grid has ${n.toLocaleString()} rows. Printing large datasets may cause performance issues or browser slowdowns.${t}\n\nClick OK to continue, or Cancel to abort.`))return}e.maxRows>0&&n>e.maxRows&&(r=e.maxRows,a=!0),this.#t=!0;const s=performance.now();this.emit("print-start",{rowCount:r,limitApplied:a,originalRowCount:n});try{const t=this.#s;this.#e={bypassThreshold:t._virtualization?.bypassThreshold??24},this.#d(),a&&(this.#n=this.sourceRows,this.grid.rows=this.sourceRows.slice(0,r),await new Promise(t=>setTimeout(t,50))),(e.includeTitle||e.includeTimestamp)&&this.#l(e),await this.#p(),await new Promise(t=>requestAnimationFrame(t)),await new Promise(t=>requestAnimationFrame(t)),i.classList.add(`print-${e.orientation}`),await new Promise(t=>requestAnimationFrame(t)),await new Promise(t=>requestAnimationFrame(t)),e.isolate?await this.#m(e):await this.#h(),this.emit("print-complete",{success:!0,rowCount:r,duration:Math.round(performance.now()-s)})}catch(d){console.error("[PrintPlugin] Print failed:",d),this.emit("print-complete",{success:!1,rowCount:0,duration:Math.round(performance.now()-s)})}finally{this.#c(),this.#t=!1}}#l(t){const i=this.gridElement;if(i){if(this.#r=document.createElement("div"),this.#r.className="tbw-print-header",t.includeTitle){const i=t.title||this.grid.effectiveConfig?.shell?.header?.title||"Grid Data",e=document.createElement("div");e.className="tbw-print-header-title",e.textContent=i,this.#r.appendChild(e)}if(t.includeTimestamp){const t=document.createElement("div");t.className="tbw-print-header-timestamp",t.textContent=`Printed: ${(new Date).toLocaleString()}`,this.#r.appendChild(t)}i.insertBefore(this.#r,i.firstChild),this.#o=document.createElement("div"),this.#o.className="tbw-print-footer",this.#o.textContent=`Page generated from ${window.location.hostname}`,i.appendChild(this.#o)}}async#p(){const t=this.#s;if(!t._virtualization)return;const i=this.rows.length;t._virtualization.bypassThreshold=i+100,t.refreshVirtualWindow(!0),await new Promise(t=>setTimeout(t,100))}async#h(){return new Promise(t=>{const i=()=>{window.removeEventListener("afterprint",i),t()};window.addEventListener("afterprint",i),window.print(),setTimeout(()=>{"undefined"!=typeof window&&window.removeEventListener("afterprint",i),t()},1e3)})}async#m(t){const i=this.gridElement;i&&await r(i,{orientation:t.orientation})}#d(){const t=this.columns;if(t){this.#i=new Map;for(const i of t)i.printHidden&&i.field&&(this.#i.set(i.field,!i.hidden),this.grid.setColumnVisible(i.field,!1))}}#w(){if(this.#i){for(const[t,i]of this.#i)this.grid.setColumnVisible(t,i);this.#i=null}}#c(){const t=this.gridElement;if(!t)return;this.#w(),t.classList.remove("print-portrait","print-landscape"),null!==this.#a&&(t.style.transform="",t.style.transformOrigin="",t.style.width="",this.#a=null),this.#r&&(this.#r.remove(),this.#r=null),this.#o&&(this.#o.remove(),this.#o=null);const i=this.#s;this.#e&&i._virtualization&&(i._virtualization.bypassThreshold=this.#e.bypassThreshold,i.refreshVirtualWindow(!0),this.#e=null),null!==this.#n&&(this.grid.rows=this.#n,this.#n=null)}afterRender(){this.config?.button&&!this.#g&&(this.#u(),this.#g=!0)}#g=!1;#u(){const t=this.#s;t.registerToolbarContent?.({id:"print-button",order:900,render:t=>{const i=document.createElement("button");i.className="tbw-toolbar-btn tbw-print-btn",i.title="Print grid",i.type="button";const e=this.resolveIcon("print")||"🖨️";this.setIcon(i,e),i.addEventListener("click",()=>{this.print()},{signal:this.disconnectSignal}),t.appendChild(i)}})}}t.PrintPlugin=a,t.printGridIsolated=r,Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})});
|
|
1
|
+
!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports,require("../../core/internal/diagnostics"),require("../../core/plugin/base-plugin")):"function"==typeof define&&define.amd?define(["exports","../../core/internal/diagnostics","../../core/plugin/base-plugin"],i):i((t="undefined"!=typeof globalThis?globalThis:t||self).TbwGridPlugin_print={},t.TbwGrid,t.TbwGrid)}(this,function(t,i,e){"use strict";const n="tbw-print-isolation-style";async function r(t,e={}){const{orientation:r="landscape"}=e,o=t.id;document.querySelectorAll(`#${CSS.escape(o)}`).length>1&&i.warnDiagnostic(i.PRINT_DUPLICATE_ID,`Multiple elements found with id="${o}". Print isolation may not work correctly. Ensure each grid has a unique ID.`,o,"print"),document.getElementById(n)?.remove();const a=function(t,i){const e=document.createElement("style");return e.id=n,e.textContent=`\n /* Print isolation: hide everything except the target grid */\n @media print {\n /* Hide all body children by default */\n body > *:not(#${t}) {\n display: none !important;\n }\n\n /* But show the grid and ensure it's not hidden by ancestor rules */\n #${t} {\n display: block !important;\n position: static !important;\n visibility: visible !important;\n opacity: 1 !important;\n overflow: visible !important;\n height: auto !important;\n width: 100% !important;\n max-height: none !important;\n margin: 0 !important;\n padding: 0 !important;\n transform: none !important;\n }\n\n /* If grid is nested, we need to show its ancestors too */\n #${t},\n #${t} * {\n visibility: visible !important;\n }\n\n /* Walk up the DOM and show all ancestors of the grid */\n body *:has(> #${t}),\n body *:has(#${t}) {\n display: block !important;\n visibility: visible !important;\n opacity: 1 !important;\n overflow: visible !important;\n height: auto !important;\n position: static !important;\n transform: none !important;\n background: transparent !important;\n border: none !important;\n padding: 0 !important;\n margin: 0 !important;\n }\n\n /* Hide siblings of ancestors (everything that's not in the path to the grid) */\n body *:has(#${t}) > *:not(:has(#${t})):not(#${t}) {\n display: none !important;\n }\n\n /* Page settings */\n @page {\n size: ${i};\n margin: 1cm;\n }\n\n /* Ensure proper print styling */\n body {\n margin: 0 !important;\n padding: 0 !important;\n background: white !important;\n color-scheme: light !important;\n }\n }\n\n /* Screen: also apply isolation for print preview */\n @media screen {\n /* When this stylesheet is active, we're about to print */\n /* No screen-specific rules needed - isolation only applies to print */\n }\n `,e}(o,r);return document.head.appendChild(a),new Promise(t=>{const i=()=>{window.removeEventListener("afterprint",i),document.getElementById(n)?.remove(),t()};window.addEventListener("afterprint",i),window.print(),setTimeout(()=>{window.removeEventListener("afterprint",i),document.getElementById(n)?.remove(),t()},5e3)})}const o={button:!1,orientation:"landscape",warnThreshold:500,maxRows:0,includeTitle:!0,includeTimestamp:!0,title:"",isolate:!1};class a extends e.BaseGridPlugin{name="print";version="1.0.0";styles=".tbw-print-header,.tbw-print-footer{display:none}@media print{tbw-grid{overflow:visible!important;height:auto!important;border:none!important;border-radius:0!important;color-scheme:light only;-webkit-print-color-adjust:exact;print-color-adjust:exact}tbw-grid .tbw-grid-content{overflow:visible!important;height:auto!important;max-height:none!important}tbw-grid .tbw-scroll-area{overflow:visible!important;height:auto!important;max-height:none!important}tbw-grid .rows-body{overflow:visible!important;height:auto!important;max-height:none!important}tbw-grid .rows-container,tbw-grid .rows-viewport,tbw-grid .rows{overflow:visible!important;height:auto!important;max-height:none!important;transform:none!important}tbw-grid .rows-viewport .rows{position:static!important}tbw-grid .resize-handle,tbw-grid [part=sort-indicator],tbw-grid .tbw-filter-btn,tbw-grid .tool-panel,tbw-grid .tool-panel-content,tbw-grid .tbw-shell-header,tbw-grid .shell-toolbar,tbw-grid .tool-panel-toggle,tbw-grid [data-print-hide],tbw-grid .expander-cell,tbw-grid .tree-toggle,tbw-grid .context-menu,tbw-grid .faux-vscroll{display:none!important}tbw-grid .tbw-print-header{display:flex;justify-content:space-between;align-items:baseline;padding:var(--tbw-spacing-md, .5em) 0;margin-bottom:var(--tbw-spacing-md, .5em);border-bottom:2px solid var(--tbw-print-border, var(--tbw-color-border-strong));font-family:inherit}.tbw-print-header-title{font-size:1.25em;font-weight:700}.tbw-print-header-timestamp{font-size:var(--tbw-font-size-sm, .875em);color:var(--tbw-print-muted, var(--tbw-color-fg-muted))}tbw-grid .tbw-print-footer{display:block;margin-top:var(--tbw-spacing-md, .5em);padding-top:var(--tbw-spacing-md, .5em);border-top:1px solid var(--tbw-print-border, var(--tbw-color-border));font-size:var(--tbw-font-size-xs, .75em);color:var(--tbw-print-muted, var(--tbw-color-fg-muted));text-align:end}tbw-grid .data-grid-row{break-inside:avoid;page-break-inside:avoid}tbw-grid .cell{border:1px solid var(--tbw-print-cell-border, var(--tbw-color-border))!important}tbw-grid .header-row,tbw-grid .data-grid-row{padding-inline-end:1px}tbw-grid .data-grid-row:hover,tbw-grid .cell:hover{background:inherit!important}@page{margin:1cm}@page{tbw-grid.print-landscape{size:landscape}}@page{tbw-grid.print-portrait{size:portrait}}}";#t=!1;#i=null;#e=null;#n=null;#r=null;#o=null;#a=null;get#s(){return this.grid}isPrinting(){return this.#t}async print(t){if(this.#t)return void this.warn(i.PRINT_IN_PROGRESS,"Print already in progress");const e=this.gridElement;if(!e)return void this.warn(i.PRINT_NO_GRID,"Grid not available");const n={...o,...this.config,...t},r=this.rows.length;let a=r,s=!1;if(n.warnThreshold>0&&r>n.warnThreshold){const t=n.maxRows>0?`\n\nNote: Output will be limited to ${n.maxRows.toLocaleString()} rows.`:"";if(!confirm(`This grid has ${r.toLocaleString()} rows. Printing large datasets may cause performance issues or browser slowdowns.${t}\n\nClick OK to continue, or Cancel to abort.`))return}n.maxRows>0&&r>n.maxRows&&(a=n.maxRows,s=!0),this.#t=!0;const d=performance.now();this.emit("print-start",{rowCount:a,limitApplied:s,originalRowCount:r});try{const t=this.#s;this.#e={bypassThreshold:t._virtualization?.bypassThreshold??24},this.#d(),s&&(this.#n=this.sourceRows,this.grid.rows=this.sourceRows.slice(0,a),await new Promise(t=>setTimeout(t,50))),(n.includeTitle||n.includeTimestamp)&&this.#l(n),await this.#p(),await new Promise(t=>requestAnimationFrame(t)),await new Promise(t=>requestAnimationFrame(t)),e.classList.add(`print-${n.orientation}`),await new Promise(t=>requestAnimationFrame(t)),await new Promise(t=>requestAnimationFrame(t)),n.isolate?await this.#m(n):await this.#h(),this.emit("print-complete",{success:!0,rowCount:a,duration:Math.round(performance.now()-d)})}catch(l){i.errorDiagnostic(i.PRINT_FAILED,`Print failed: ${l}`,this.gridElement?.id,this.name),this.emit("print-complete",{success:!1,rowCount:0,duration:Math.round(performance.now()-d)})}finally{this.#c(),this.#t=!1}}#l(t){const i=this.gridElement;if(i){if(this.#r=document.createElement("div"),this.#r.className="tbw-print-header",t.includeTitle){const i=t.title||this.grid.effectiveConfig?.shell?.header?.title||"Grid Data",e=document.createElement("div");e.className="tbw-print-header-title",e.textContent=i,this.#r.appendChild(e)}if(t.includeTimestamp){const t=document.createElement("div");t.className="tbw-print-header-timestamp",t.textContent=`Printed: ${(new Date).toLocaleString()}`,this.#r.appendChild(t)}i.insertBefore(this.#r,i.firstChild),this.#o=document.createElement("div"),this.#o.className="tbw-print-footer",this.#o.textContent=`Page generated from ${window.location.hostname}`,i.appendChild(this.#o)}}async#p(){const t=this.#s;if(!t._virtualization)return;const i=this.rows.length;t._virtualization.bypassThreshold=i+100,t.refreshVirtualWindow(!0),await new Promise(t=>setTimeout(t,100))}async#h(){return new Promise(t=>{const i=()=>{window.removeEventListener("afterprint",i),t()};window.addEventListener("afterprint",i),window.print(),setTimeout(()=>{"undefined"!=typeof window&&window.removeEventListener("afterprint",i),t()},1e3)})}async#m(t){const i=this.gridElement;i&&await r(i,{orientation:t.orientation})}#d(){const t=this.columns;if(t){this.#i=new Map;for(const i of t)i.printHidden&&i.field&&(this.#i.set(i.field,!i.hidden),this.grid.setColumnVisible(i.field,!1))}}#w(){if(this.#i){for(const[t,i]of this.#i)this.grid.setColumnVisible(t,i);this.#i=null}}#c(){const t=this.gridElement;if(!t)return;this.#w(),t.classList.remove("print-portrait","print-landscape"),null!==this.#a&&(t.style.transform="",t.style.transformOrigin="",t.style.width="",this.#a=null),this.#r&&(this.#r.remove(),this.#r=null),this.#o&&(this.#o.remove(),this.#o=null);const i=this.#s;this.#e&&i._virtualization&&(i._virtualization.bypassThreshold=this.#e.bypassThreshold,i.refreshVirtualWindow(!0),this.#e=null),null!==this.#n&&(this.grid.rows=this.#n,this.#n=null)}afterRender(){this.config?.button&&!this.#g&&(this.#u(),this.#g=!0)}#g=!1;#u(){const t=this.#s;t.registerToolbarContent?.({id:"print-button",order:900,render:t=>{const i=document.createElement("button");i.className="tbw-toolbar-btn tbw-print-btn",i.title="Print grid",i.type="button";const e=this.resolveIcon("print")||"🖨️";this.setIcon(i,e),i.addEventListener("click",()=>{this.print()},{signal:this.disconnectSignal}),t.appendChild(i)}})}}t.PrintPlugin=a,t.printGridIsolated=r,Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})});
|
|
2
2
|
//# sourceMappingURL=print.umd.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"print.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/print/print-isolated.ts","../../../../../libs/grid/src/lib/plugins/print/PrintPlugin.ts"],"sourcesContent":["/**\n * Utility for printing a grid in isolation by hiding all other page content.\n *\n * This approach keeps the grid in place (with virtualization disabled by PrintPlugin)\n * and uses CSS to hide everything else on the page during printing.\n */\n\nimport { gridPrefix } from '../../core/internal/utils';\nimport type { PrintOrientation } from './types';\n\nexport interface PrintIsolatedOptions {\n /** Page orientation hint */\n orientation?: PrintOrientation;\n}\n\n/** ID for the isolation stylesheet */\nconst ISOLATION_STYLE_ID = 'tbw-print-isolation-style';\n\n/**\n * Create a stylesheet that hides everything except the target grid.\n * Uses the grid's ID to target it specifically.\n */\nfunction createIsolationStylesheet(gridId: string, orientation: PrintOrientation): HTMLStyleElement {\n const style = document.createElement('style');\n style.id = ISOLATION_STYLE_ID;\n style.textContent = `\n /* Print isolation: hide everything except the target grid */\n @media print {\n /* Hide all body children by default */\n body > *:not(#${gridId}) {\n display: none !important;\n }\n\n /* But show the grid and ensure it's not hidden by ancestor rules */\n #${gridId} {\n display: block !important;\n position: static !important;\n visibility: visible !important;\n opacity: 1 !important;\n overflow: visible !important;\n height: auto !important;\n width: 100% !important;\n max-height: none !important;\n margin: 0 !important;\n padding: 0 !important;\n transform: none !important;\n }\n\n /* If grid is nested, we need to show its ancestors too */\n #${gridId},\n #${gridId} * {\n visibility: visible !important;\n }\n\n /* Walk up the DOM and show all ancestors of the grid */\n body *:has(> #${gridId}),\n body *:has(#${gridId}) {\n display: block !important;\n visibility: visible !important;\n opacity: 1 !important;\n overflow: visible !important;\n height: auto !important;\n position: static !important;\n transform: none !important;\n background: transparent !important;\n border: none !important;\n padding: 0 !important;\n margin: 0 !important;\n }\n\n /* Hide siblings of ancestors (everything that's not in the path to the grid) */\n body *:has(#${gridId}) > *:not(:has(#${gridId})):not(#${gridId}) {\n display: none !important;\n }\n\n /* Page settings */\n @page {\n size: ${orientation};\n margin: 1cm;\n }\n\n /* Ensure proper print styling */\n body {\n margin: 0 !important;\n padding: 0 !important;\n background: white !important;\n color-scheme: light !important;\n }\n }\n\n /* Screen: also apply isolation for print preview */\n @media screen {\n /* When this stylesheet is active, we're about to print */\n /* No screen-specific rules needed - isolation only applies to print */\n }\n `;\n return style;\n}\n\n/**\n * Print a grid in isolation by hiding all other page content.\n *\n * This function adds a temporary stylesheet that uses CSS to hide everything\n * on the page except the target grid during printing. The grid stays in place\n * with all its data (virtualization should be disabled separately).\n *\n * @param gridElement - The tbw-grid element to print (must have an ID)\n * @param options - Optional configuration\n * @returns Promise that resolves when the print dialog closes\n *\n * @example\n * ```typescript\n * import { printGridIsolated } from '@toolbox-web/grid/plugins/print';\n *\n * const grid = document.querySelector('tbw-grid');\n * await printGridIsolated(grid, { orientation: 'landscape' });\n * ```\n */\nexport async function printGridIsolated(gridElement: HTMLElement, options: PrintIsolatedOptions = {}): Promise<void> {\n const { orientation = 'landscape' } = options;\n\n const gridId = gridElement.id;\n\n // Warn if multiple elements share this ID (user-set IDs could collide)\n const elementsWithId = document.querySelectorAll(`#${CSS.escape(gridId)}`);\n if (elementsWithId.length > 1) {\n console.warn(\n `${gridPrefix(gridId, 'print')} Multiple elements found with id=\"${gridId}\". ` +\n `Print isolation may not work correctly. Ensure each grid has a unique ID.`,\n );\n }\n\n // Remove any existing isolation stylesheet\n document.getElementById(ISOLATION_STYLE_ID)?.remove();\n\n // Add the isolation stylesheet\n const isolationStyle = createIsolationStylesheet(gridId, orientation);\n document.head.appendChild(isolationStyle);\n\n return new Promise((resolve) => {\n // Listen for afterprint event to cleanup\n const onAfterPrint = () => {\n window.removeEventListener('afterprint', onAfterPrint);\n // Remove isolation stylesheet\n document.getElementById(ISOLATION_STYLE_ID)?.remove();\n resolve();\n };\n window.addEventListener('afterprint', onAfterPrint);\n\n // Trigger print\n window.print();\n\n // Fallback timeout in case afterprint doesn't fire (some browsers)\n setTimeout(() => {\n window.removeEventListener('afterprint', onAfterPrint);\n document.getElementById(ISOLATION_STYLE_ID)?.remove();\n resolve();\n }, 5000);\n });\n}\n","/**\n * Print Plugin (Class-based)\n *\n * Provides print layout functionality for tbw-grid.\n * Temporarily disables virtualization to render all rows and uses\n * @media print CSS for print-optimized styling.\n */\n\nimport { BaseGridPlugin } from '../../core/plugin/base-plugin';\nimport type { InternalGrid, ToolbarContentDefinition } from '../../core/types';\nimport { printGridIsolated } from './print-isolated';\nimport styles from './print.css?inline';\nimport type { PrintCompleteDetail, PrintConfig, PrintParams, PrintStartDetail } from './types';\n\n/**\n * Extended grid interface for PrintPlugin internal access.\n * Includes registerToolbarContent which is available on the grid class\n * but not exposed in the standard plugin API.\n */\ninterface PrintGridRef extends InternalGrid {\n registerToolbarContent?(content: ToolbarContentDefinition): void;\n unregisterToolbarContent?(contentId: string): void;\n}\n\n/** Default configuration */\nconst DEFAULT_CONFIG: Required<PrintConfig> = {\n button: false,\n orientation: 'landscape',\n warnThreshold: 500,\n maxRows: 0,\n includeTitle: true,\n includeTimestamp: true,\n title: '',\n isolate: false,\n};\n\n/**\n * Print Plugin for tbw-grid\n *\n * Enables printing the full grid content by temporarily disabling virtualization\n * and applying print-optimized styles. Handles large datasets gracefully with\n * configurable row limits.\n *\n * ## Installation\n *\n * ```ts\n * import { PrintPlugin } from '@toolbox-web/grid/plugins/print';\n * ```\n *\n * ## Configuration Options\n *\n * | Option | Type | Default | Description |\n * |--------|------|---------|-------------|\n * | `button` | `boolean` | `false` | Show print button in toolbar |\n * | `orientation` | `'portrait' \\| 'landscape'` | `'landscape'` | Page orientation |\n * | `warnThreshold` | `number` | `500` | Show confirmation dialog when rows exceed this (0 = no warning) |\n * | `maxRows` | `number` | `0` | Hard limit on printed rows (0 = unlimited) |\n * | `includeTitle` | `boolean` | `true` | Include grid title in print |\n * | `includeTimestamp` | `boolean` | `true` | Include timestamp in footer |\n * | `title` | `string` | `''` | Custom print title |\n *\n * ## Programmatic API\n *\n * | Method | Signature | Description |\n * |--------|-----------|-------------|\n * | `print` | `(params?) => Promise<void>` | Trigger print dialog |\n * | `isPrinting` | `() => boolean` | Check if print is in progress |\n *\n * ## Events\n *\n * | Event | Detail | Description |\n * |-------|--------|-------------|\n * | `print-start` | `PrintStartDetail` | Fired when print begins |\n * | `print-complete` | `PrintCompleteDetail` | Fired when print completes |\n *\n * @example Basic Print\n * ```ts\n * import { PrintPlugin } from '@toolbox-web/grid/plugins/print';\n *\n * const grid = document.querySelector('tbw-grid');\n * grid.gridConfig = {\n * plugins: [new PrintPlugin()],\n * };\n *\n * // Trigger print\n * const printPlugin = grid.getPluginByName('print');\n * await printPlugin.print();\n * ```\n *\n * @example With Toolbar Button\n * ```ts\n * grid.gridConfig = {\n * plugins: [new PrintPlugin({ button: true, orientation: 'landscape' })],\n * };\n * ```\n *\n * @see {@link PrintConfig} for all configuration options\n */\nexport class PrintPlugin extends BaseGridPlugin<PrintConfig> {\n /** @internal */\n readonly name = 'print';\n\n /** @internal */\n override readonly version = '1.0.0';\n\n /** CSS styles for print mode */\n override readonly styles = styles;\n\n /** Current print state */\n #printing = false;\n\n /** Saved column visibility state */\n #savedHiddenColumns: Map<string, boolean> | null = null;\n\n /** Saved virtualization state */\n #savedVirtualization: { bypassThreshold: number } | null = null;\n\n /** Saved rows when maxRows limit is applied */\n #savedRows: unknown[] | null = null;\n\n /** Print header element */\n #printHeader: HTMLElement | null = null;\n\n /** Print footer element */\n #printFooter: HTMLElement | null = null;\n\n /** Applied scale factor (legacy, used for cleanup) */\n #appliedScale: number | null = null;\n\n /**\n * Get the grid typed as PrintGridRef for internal access.\n */\n get #internalGrid(): PrintGridRef {\n return this.grid as unknown as PrintGridRef;\n }\n\n /**\n * Check if print is currently in progress\n */\n isPrinting(): boolean {\n return this.#printing;\n }\n\n /**\n * Trigger the browser print dialog\n *\n * This method:\n * 1. Validates row count against maxRows limit\n * 2. Disables virtualization to render all rows\n * 3. Applies print-specific CSS classes\n * 4. Opens the browser print dialog (or isolated window if `isolate: true`)\n * 5. Restores normal state after printing\n *\n * @param params - Optional parameters to override config for this print\n * @param params.isolate - If true, prints in an isolated window containing only the grid\n * @returns Promise that resolves when print dialog closes\n */\n async print(params?: PrintParams): Promise<void> {\n if (this.#printing) {\n console.warn('[PrintPlugin] Print already in progress');\n return;\n }\n\n const grid = this.gridElement;\n if (!grid) {\n console.warn('[PrintPlugin] Grid not available');\n return;\n }\n\n const config = { ...DEFAULT_CONFIG, ...this.config, ...params };\n const rows = this.rows;\n const originalRowCount = rows.length;\n let rowCount = originalRowCount;\n let limitApplied = false;\n\n // Check if we should warn about large datasets\n if (config.warnThreshold > 0 && originalRowCount > config.warnThreshold) {\n const limitInfo =\n config.maxRows > 0 ? `\\n\\nNote: Output will be limited to ${config.maxRows.toLocaleString()} rows.` : '';\n const proceed = confirm(\n `This grid has ${originalRowCount.toLocaleString()} rows. ` +\n `Printing large datasets may cause performance issues or browser slowdowns.${limitInfo}\\n\\n` +\n `Click OK to continue, or Cancel to abort.`,\n );\n if (!proceed) {\n return;\n }\n }\n\n // Apply hard row limit if configured\n if (config.maxRows > 0 && originalRowCount > config.maxRows) {\n rowCount = config.maxRows;\n limitApplied = true;\n }\n\n this.#printing = true;\n\n // Track timing for duration reporting\n const startTime = performance.now();\n\n // Emit print-start event\n this.emit<PrintStartDetail>('print-start', {\n rowCount,\n limitApplied,\n originalRowCount,\n });\n\n try {\n // Save current virtualization state\n const internalGrid = this.#internalGrid;\n this.#savedVirtualization = {\n bypassThreshold: internalGrid._virtualization?.bypassThreshold ?? 24,\n };\n\n // Hide columns marked with printHidden\n this.#hidePrintColumns();\n\n // Apply row limit if configured\n if (limitApplied) {\n this.#savedRows = this.sourceRows;\n // Set limited rows on the grid\n this.grid.rows = this.sourceRows.slice(0, rowCount);\n // Wait for grid to process new rows\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n\n // Add print header if configured\n if (config.includeTitle || config.includeTimestamp) {\n this.#addPrintHeader(config);\n }\n\n // Disable virtualization to render all rows\n // This forces the grid to render all rows in the DOM\n await this.#disableVirtualization();\n\n // Wait for next frame to ensure DOM is updated\n await new Promise((resolve) => requestAnimationFrame(resolve));\n await new Promise((resolve) => requestAnimationFrame(resolve));\n\n // Add orientation class for @page rules\n grid.classList.add(`print-${config.orientation}`);\n\n // Wait for next frame to ensure DOM is updated\n await new Promise((resolve) => requestAnimationFrame(resolve));\n await new Promise((resolve) => requestAnimationFrame(resolve));\n\n // Trigger browser print dialog (isolated or inline)\n if (config.isolate) {\n await this.#printInIsolatedWindow(config);\n } else {\n await this.#triggerPrint();\n }\n\n // Emit print-complete event\n this.emit<PrintCompleteDetail>('print-complete', {\n success: true,\n rowCount,\n duration: Math.round(performance.now() - startTime),\n });\n } catch (error) {\n console.error('[PrintPlugin] Print failed:', error);\n this.emit<PrintCompleteDetail>('print-complete', {\n success: false,\n rowCount: 0,\n duration: Math.round(performance.now() - startTime),\n });\n } finally {\n // Restore normal state\n this.#cleanup();\n this.#printing = false;\n }\n }\n\n /**\n * Add print header with title and timestamp\n */\n #addPrintHeader(config: Required<PrintConfig>): void {\n const grid = this.gridElement;\n if (!grid) return;\n\n // Create print header\n this.#printHeader = document.createElement('div');\n this.#printHeader.className = 'tbw-print-header';\n\n // Title\n if (config.includeTitle) {\n const title = config.title || this.grid.effectiveConfig?.shell?.header?.title || 'Grid Data';\n const titleEl = document.createElement('div');\n titleEl.className = 'tbw-print-header-title';\n titleEl.textContent = title;\n this.#printHeader.appendChild(titleEl);\n }\n\n // Timestamp\n if (config.includeTimestamp) {\n const timestampEl = document.createElement('div');\n timestampEl.className = 'tbw-print-header-timestamp';\n timestampEl.textContent = `Printed: ${new Date().toLocaleString()}`;\n this.#printHeader.appendChild(timestampEl);\n }\n\n // Insert at the beginning of the grid\n grid.insertBefore(this.#printHeader, grid.firstChild);\n\n // Create print footer\n this.#printFooter = document.createElement('div');\n this.#printFooter.className = 'tbw-print-footer';\n this.#printFooter.textContent = `Page generated from ${window.location.hostname}`;\n grid.appendChild(this.#printFooter);\n }\n\n /**\n * Disable virtualization to render all rows\n */\n async #disableVirtualization(): Promise<void> {\n const internalGrid = this.#internalGrid;\n if (!internalGrid._virtualization) return;\n\n // Set bypass threshold higher than total row count to disable virtualization\n // This makes the grid render all rows (up to maxRows) instead of just visible ones\n const totalRows = this.rows.length;\n internalGrid._virtualization.bypassThreshold = totalRows + 100;\n\n // Force a full refresh to re-render with virtualization disabled\n internalGrid.refreshVirtualWindow(true);\n\n // Wait for render to complete\n await new Promise((resolve) => setTimeout(resolve, 100));\n }\n\n /**\n * Trigger the browser print dialog\n */\n async #triggerPrint(): Promise<void> {\n return new Promise((resolve) => {\n // Listen for afterprint event\n const onAfterPrint = () => {\n window.removeEventListener('afterprint', onAfterPrint);\n resolve();\n };\n window.addEventListener('afterprint', onAfterPrint);\n\n // Trigger print\n window.print();\n\n // Fallback timeout in case afterprint doesn't fire (some browsers)\n setTimeout(() => {\n // Guard against test environment teardown where window may be undefined\n if (typeof window !== 'undefined') {\n window.removeEventListener('afterprint', onAfterPrint);\n }\n resolve();\n }, 1000);\n });\n }\n\n /**\n * Print in isolation by hiding all other page content.\n * This excludes navigation, sidebars, etc. while keeping the grid in place.\n */\n async #printInIsolatedWindow(config: Required<PrintConfig>): Promise<void> {\n const grid = this.gridElement;\n if (!grid) return;\n\n await printGridIsolated(grid, {\n orientation: config.orientation,\n });\n }\n\n /**\n * Hide columns marked with printHidden: true\n */\n #hidePrintColumns(): void {\n const columns = this.columns;\n if (!columns) return;\n\n // Save current hidden state and hide print columns\n this.#savedHiddenColumns = new Map();\n\n for (const col of columns) {\n if (col.printHidden && col.field) {\n // Save current visibility state (true = visible, false = hidden)\n this.#savedHiddenColumns.set(col.field, !col.hidden);\n // Hide the column for printing\n this.grid.setColumnVisible(col.field, false);\n }\n }\n }\n\n /**\n * Restore columns that were hidden for printing\n */\n #restorePrintColumns(): void {\n if (!this.#savedHiddenColumns) return;\n\n for (const [field, wasVisible] of this.#savedHiddenColumns) {\n // Restore original visibility\n this.grid.setColumnVisible(field, wasVisible);\n }\n\n this.#savedHiddenColumns = null;\n }\n\n /**\n * Cleanup after printing\n */\n #cleanup(): void {\n const grid = this.gridElement;\n if (!grid) return;\n\n // Restore columns that were hidden for printing\n this.#restorePrintColumns();\n\n // Remove orientation classes (both original and possibly switched)\n grid.classList.remove('print-portrait', 'print-landscape');\n\n // Remove scaling transform if applied (legacy)\n if (this.#appliedScale !== null) {\n grid.style.transform = '';\n grid.style.transformOrigin = '';\n grid.style.width = '';\n this.#appliedScale = null;\n }\n\n // Remove print header/footer\n if (this.#printHeader) {\n this.#printHeader.remove();\n this.#printHeader = null;\n }\n if (this.#printFooter) {\n this.#printFooter.remove();\n this.#printFooter = null;\n }\n\n // Restore virtualization\n const internalGrid = this.#internalGrid;\n if (this.#savedVirtualization && internalGrid._virtualization) {\n internalGrid._virtualization.bypassThreshold = this.#savedVirtualization.bypassThreshold;\n internalGrid.refreshVirtualWindow(true);\n this.#savedVirtualization = null;\n }\n\n // Restore original rows if they were limited\n if (this.#savedRows !== null) {\n this.grid.rows = this.#savedRows;\n this.#savedRows = null;\n }\n }\n\n /**\n * Register toolbar button if configured\n * @internal\n */\n override afterRender(): void {\n // Register toolbar on first render when button is enabled\n if (this.config?.button && !this.#toolbarRegistered) {\n this.#registerToolbarButton();\n this.#toolbarRegistered = true;\n }\n }\n\n /** Track if toolbar button is registered */\n #toolbarRegistered = false;\n\n /**\n * Register print button in toolbar\n */\n #registerToolbarButton(): void {\n const grid = this.#internalGrid;\n\n // Register toolbar content\n grid.registerToolbarContent?.({\n id: 'print-button',\n order: 900, // High order to appear at the end\n render: (container: HTMLElement) => {\n const button = document.createElement('button');\n button.className = 'tbw-toolbar-btn tbw-print-btn';\n button.title = 'Print grid';\n button.type = 'button';\n\n // Use print icon\n const icon = this.resolveIcon('print') || '🖨️';\n this.setIcon(button, icon);\n\n button.addEventListener(\n 'click',\n () => {\n this.print();\n },\n { signal: this.disconnectSignal },\n );\n\n container.appendChild(button);\n },\n });\n }\n}\n"],"names":["ISOLATION_STYLE_ID","async","printGridIsolated","gridElement","options","orientation","gridId","id","document","querySelectorAll","CSS","escape","length","console","warn","gridPrefix","getElementById","remove","isolationStyle","style","createElement","textContent","createIsolationStylesheet","head","appendChild","Promise","resolve","onAfterPrint","window","removeEventListener","addEventListener","print","setTimeout","DEFAULT_CONFIG","button","warnThreshold","maxRows","includeTitle","includeTimestamp","title","isolate","PrintPlugin","BaseGridPlugin","name","version","styles","printing","savedHiddenColumns","savedVirtualization","savedRows","printHeader","printFooter","appliedScale","internalGrid","this","grid","isPrinting","params","config","originalRowCount","rows","rowCount","limitApplied","limitInfo","toLocaleString","confirm","startTime","performance","now","emit","bypassThreshold","_virtualization","hidePrintColumns","sourceRows","slice","addPrintHeader","disableVirtualization","requestAnimationFrame","classList","add","printInIsolatedWindow","triggerPrint","success","duration","Math","round","error","cleanup","className","effectiveConfig","shell","header","titleEl","timestampEl","Date","insertBefore","firstChild","location","hostname","totalRows","refreshVirtualWindow","columns","Map","col","printHidden","field","set","hidden","setColumnVisible","restorePrintColumns","wasVisible","transform","transformOrigin","width","afterRender","toolbarRegistered","registerToolbarButton","registerToolbarContent","order","render","container","type","icon","resolveIcon","setIcon","signal","disconnectSignal"],"mappings":"4ZAgBA,MAAMA,EAAqB,4BAsG3BC,eAAsBC,EAAkBC,EAA0BC,EAAgC,IAChG,MAAMC,YAAEA,EAAc,aAAgBD,EAEhCE,EAASH,EAAYI,GAGJC,SAASC,iBAAiB,IAAIC,IAAIC,OAAOL,MAC7CM,OAAS,GAC1BC,QAAQC,KACN,GAAGC,EAAAA,WAAWT,EAAQ,6CAA6CA,iFAMvEE,SAASQ,eAAehB,IAAqBiB,SAG7C,MAAMC,EAlHR,SAAmCZ,EAAgBD,GACjD,MAAMc,EAAQX,SAASY,cAAc,SAyErC,OAxEAD,EAAMZ,GAAKP,EACXmB,EAAME,YAAc,+JAIAf,0IAKbA,meAeAA,cACAA,kJAKaA,0BACFA,6gBAeAA,oBAAyBA,YAAiBA,+GAM9CD,yeAmBPc,CACT,CAuCyBG,CAA0BhB,EAAQD,GAGzD,OAFAG,SAASe,KAAKC,YAAYN,GAEnB,IAAIO,QAASC,IAElB,MAAMC,EAAe,KACnBC,OAAOC,oBAAoB,aAAcF,GAEzCnB,SAASQ,eAAehB,IAAqBiB,SAC7CS,KAEFE,OAAOE,iBAAiB,aAAcH,GAGtCC,OAAOG,QAGPC,WAAW,KACTJ,OAAOC,oBAAoB,aAAcF,GACzCnB,SAASQ,eAAehB,IAAqBiB,SAC7CS,KACC,MAEP,OCtIMO,EAAwC,CAC5CC,QAAQ,EACR7B,YAAa,YACb8B,cAAe,IACfC,QAAS,EACTC,cAAc,EACdC,kBAAkB,EAClBC,MAAO,GACPC,SAAS,GAiEJ,MAAMC,UAAoBC,EAAAA,eAEtBC,KAAO,QAGEC,QAAU,QAGVC,kwEAGlBC,IAAY,EAGZC,GAAmD,KAGnDC,GAA2D,KAG3DC,GAA+B,KAG/BC,GAAmC,KAGnCC,GAAmC,KAGnCC,GAA+B,KAK/B,KAAIC,GACF,OAAOC,KAAKC,IACd,CAKA,UAAAC,GACE,OAAOF,MAAKR,CACd,CAgBA,WAAMf,CAAM0B,GACV,GAAIH,MAAKR,EAEP,YADAjC,QAAQC,KAAK,2CAIf,MAAMyC,EAAOD,KAAKnD,YAClB,IAAKoD,EAEH,YADA1C,QAAQC,KAAK,oCAIf,MAAM4C,EAAS,IAAKzB,KAAmBqB,KAAKI,UAAWD,GAEjDE,EADOL,KAAKM,KACYhD,OAC9B,IAAIiD,EAAWF,EACXG,GAAe,EAGnB,GAAIJ,EAAOvB,cAAgB,GAAKwB,EAAmBD,EAAOvB,cAAe,CACvE,MAAM4B,EACJL,EAAOtB,QAAU,EAAI,uCAAuCsB,EAAOtB,QAAQ4B,yBAA2B,GAMxG,IALgBC,QACd,iBAAiBN,EAAiBK,oGAC6CD,kDAI/E,MAEJ,CAGIL,EAAOtB,QAAU,GAAKuB,EAAmBD,EAAOtB,UAClDyB,EAAWH,EAAOtB,QAClB0B,GAAe,GAGjBR,MAAKR,GAAY,EAGjB,MAAMoB,EAAYC,YAAYC,MAG9Bd,KAAKe,KAAuB,cAAe,CACzCR,WACAC,eACAH,qBAGF,IAEE,MAAMN,EAAeC,MAAKD,EAC1BC,MAAKN,EAAuB,CAC1BsB,gBAAiBjB,EAAakB,iBAAiBD,iBAAmB,IAIpEhB,MAAKkB,IAGDV,IACFR,MAAKL,EAAaK,KAAKmB,WAEvBnB,KAAKC,KAAKK,KAAON,KAAKmB,WAAWC,MAAM,EAAGb,SAEpC,IAAIpC,QAASC,GAAYM,WAAWN,EAAS,OAIjDgC,EAAOrB,cAAgBqB,EAAOpB,mBAChCgB,MAAKqB,EAAgBjB,SAKjBJ,MAAKsB,UAGL,IAAInD,QAASC,GAAYmD,sBAAsBnD,UAC/C,IAAID,QAASC,GAAYmD,sBAAsBnD,IAGrD6B,EAAKuB,UAAUC,IAAI,SAASrB,EAAOrD,qBAG7B,IAAIoB,QAASC,GAAYmD,sBAAsBnD,UAC/C,IAAID,QAASC,GAAYmD,sBAAsBnD,IAGjDgC,EAAOlB,cACHc,MAAK0B,EAAuBtB,SAE5BJ,MAAK2B,IAIb3B,KAAKe,KAA0B,iBAAkB,CAC/Ca,SAAS,EACTrB,WACAsB,SAAUC,KAAKC,MAAMlB,YAAYC,MAAQF,IAE7C,OAASoB,GACPzE,QAAQyE,MAAM,8BAA+BA,GAC7ChC,KAAKe,KAA0B,iBAAkB,CAC/Ca,SAAS,EACTrB,SAAU,EACVsB,SAAUC,KAAKC,MAAMlB,YAAYC,MAAQF,IAE7C,CAAA,QAEEZ,MAAKiC,IACLjC,MAAKR,GAAY,CACnB,CACF,CAKA,EAAA6B,CAAgBjB,GACd,MAAMH,EAAOD,KAAKnD,YAClB,GAAKoD,EAAL,CAOA,GAJAD,MAAKJ,EAAe1C,SAASY,cAAc,OAC3CkC,MAAKJ,EAAasC,UAAY,mBAG1B9B,EAAOrB,aAAc,CACvB,MAAME,EAAQmB,EAAOnB,OAASe,KAAKC,KAAKkC,iBAAiBC,OAAOC,QAAQpD,OAAS,YAC3EqD,EAAUpF,SAASY,cAAc,OACvCwE,EAAQJ,UAAY,yBACpBI,EAAQvE,YAAckB,EACtBe,MAAKJ,EAAa1B,YAAYoE,EAChC,CAGA,GAAIlC,EAAOpB,iBAAkB,CAC3B,MAAMuD,EAAcrF,SAASY,cAAc,OAC3CyE,EAAYL,UAAY,6BACxBK,EAAYxE,YAAc,aAAA,IAAgByE,MAAO9B,mBACjDV,MAAKJ,EAAa1B,YAAYqE,EAChC,CAGAtC,EAAKwC,aAAazC,MAAKJ,EAAcK,EAAKyC,YAG1C1C,MAAKH,EAAe3C,SAASY,cAAc,OAC3CkC,MAAKH,EAAaqC,UAAY,mBAC9BlC,MAAKH,EAAa9B,YAAc,uBAAuBO,OAAOqE,SAASC,WACvE3C,EAAK/B,YAAY8B,MAAKH,EA9BX,CA+Bb,CAKA,OAAMyB,GACJ,MAAMvB,EAAeC,MAAKD,EAC1B,IAAKA,EAAakB,gBAAiB,OAInC,MAAM4B,EAAY7C,KAAKM,KAAKhD,OAC5ByC,EAAakB,gBAAgBD,gBAAkB6B,EAAY,IAG3D9C,EAAa+C,sBAAqB,SAG5B,IAAI3E,QAASC,GAAYM,WAAWN,EAAS,KACrD,CAKA,OAAMuD,GACJ,OAAO,IAAIxD,QAASC,IAElB,MAAMC,EAAe,KACnBC,OAAOC,oBAAoB,aAAcF,GACzCD,KAEFE,OAAOE,iBAAiB,aAAcH,GAGtCC,OAAOG,QAGPC,WAAW,KAEa,oBAAXJ,QACTA,OAAOC,oBAAoB,aAAcF,GAE3CD,KACC,MAEP,CAMA,OAAMsD,CAAuBtB,GAC3B,MAAMH,EAAOD,KAAKnD,YACboD,SAECrD,EAAkBqD,EAAM,CAC5BlD,YAAaqD,EAAOrD,aAExB,CAKA,EAAAmE,GACE,MAAM6B,EAAU/C,KAAK+C,QACrB,GAAKA,EAAL,CAGA/C,MAAKP,MAA0BuD,IAE/B,IAAA,MAAWC,KAAOF,EACZE,EAAIC,aAAeD,EAAIE,QAEzBnD,MAAKP,EAAoB2D,IAAIH,EAAIE,OAAQF,EAAII,QAE7CrD,KAAKC,KAAKqD,iBAAiBL,EAAIE,OAAO,GAV5B,CAahB,CAKA,EAAAI,GACE,GAAKvD,MAAKP,EAAV,CAEA,IAAA,MAAY0D,EAAOK,KAAexD,MAAKP,EAErCO,KAAKC,KAAKqD,iBAAiBH,EAAOK,GAGpCxD,MAAKP,EAAsB,IAPI,CAQjC,CAKA,EAAAwC,GACE,MAAMhC,EAAOD,KAAKnD,YAClB,IAAKoD,EAAM,OAGXD,MAAKuD,IAGLtD,EAAKuB,UAAU7D,OAAO,iBAAkB,mBAGb,OAAvBqC,MAAKF,IACPG,EAAKpC,MAAM4F,UAAY,GACvBxD,EAAKpC,MAAM6F,gBAAkB,GAC7BzD,EAAKpC,MAAM8F,MAAQ,GACnB3D,MAAKF,EAAgB,MAInBE,MAAKJ,IACPI,MAAKJ,EAAajC,SAClBqC,MAAKJ,EAAe,MAElBI,MAAKH,IACPG,MAAKH,EAAalC,SAClBqC,MAAKH,EAAe,MAItB,MAAME,EAAeC,MAAKD,EACtBC,MAAKN,GAAwBK,EAAakB,kBAC5ClB,EAAakB,gBAAgBD,gBAAkBhB,MAAKN,EAAqBsB,gBACzEjB,EAAa+C,sBAAqB,GAClC9C,MAAKN,EAAuB,MAIN,OAApBM,MAAKL,IACPK,KAAKC,KAAKK,KAAON,MAAKL,EACtBK,MAAKL,EAAa,KAEtB,CAMS,WAAAiE,GAEH5D,KAAKI,QAAQxB,SAAWoB,MAAK6D,IAC/B7D,MAAK8D,IACL9D,MAAK6D,GAAqB,EAE9B,CAGAA,IAAqB,EAKrB,EAAAC,GACE,MAAM7D,EAAOD,MAAKD,EAGlBE,EAAK8D,yBAAyB,CAC5B9G,GAAI,eACJ+G,MAAO,IACPC,OAASC,IACP,MAAMtF,EAAS1B,SAASY,cAAc,UACtCc,EAAOsD,UAAY,gCACnBtD,EAAOK,MAAQ,aACfL,EAAOuF,KAAO,SAGd,MAAMC,EAAOpE,KAAKqE,YAAY,UAAY,MAC1CrE,KAAKsE,QAAQ1F,EAAQwF,GAErBxF,EAAOJ,iBACL,QACA,KACEwB,KAAKvB,SAEP,CAAE8F,OAAQvE,KAAKwE,mBAGjBN,EAAUhG,YAAYU,KAG5B"}
|
|
1
|
+
{"version":3,"file":"print.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/print/print-isolated.ts","../../../../../libs/grid/src/lib/plugins/print/PrintPlugin.ts"],"sourcesContent":["/**\n * Utility for printing a grid in isolation by hiding all other page content.\n *\n * This approach keeps the grid in place (with virtualization disabled by PrintPlugin)\n * and uses CSS to hide everything else on the page during printing.\n */\n\nimport { PRINT_DUPLICATE_ID, warnDiagnostic } from '../../core/internal/diagnostics';\nimport type { PrintOrientation } from './types';\n\nexport interface PrintIsolatedOptions {\n /** Page orientation hint */\n orientation?: PrintOrientation;\n}\n\n/** ID for the isolation stylesheet */\nconst ISOLATION_STYLE_ID = 'tbw-print-isolation-style';\n\n/**\n * Create a stylesheet that hides everything except the target grid.\n * Uses the grid's ID to target it specifically.\n */\nfunction createIsolationStylesheet(gridId: string, orientation: PrintOrientation): HTMLStyleElement {\n const style = document.createElement('style');\n style.id = ISOLATION_STYLE_ID;\n style.textContent = `\n /* Print isolation: hide everything except the target grid */\n @media print {\n /* Hide all body children by default */\n body > *:not(#${gridId}) {\n display: none !important;\n }\n\n /* But show the grid and ensure it's not hidden by ancestor rules */\n #${gridId} {\n display: block !important;\n position: static !important;\n visibility: visible !important;\n opacity: 1 !important;\n overflow: visible !important;\n height: auto !important;\n width: 100% !important;\n max-height: none !important;\n margin: 0 !important;\n padding: 0 !important;\n transform: none !important;\n }\n\n /* If grid is nested, we need to show its ancestors too */\n #${gridId},\n #${gridId} * {\n visibility: visible !important;\n }\n\n /* Walk up the DOM and show all ancestors of the grid */\n body *:has(> #${gridId}),\n body *:has(#${gridId}) {\n display: block !important;\n visibility: visible !important;\n opacity: 1 !important;\n overflow: visible !important;\n height: auto !important;\n position: static !important;\n transform: none !important;\n background: transparent !important;\n border: none !important;\n padding: 0 !important;\n margin: 0 !important;\n }\n\n /* Hide siblings of ancestors (everything that's not in the path to the grid) */\n body *:has(#${gridId}) > *:not(:has(#${gridId})):not(#${gridId}) {\n display: none !important;\n }\n\n /* Page settings */\n @page {\n size: ${orientation};\n margin: 1cm;\n }\n\n /* Ensure proper print styling */\n body {\n margin: 0 !important;\n padding: 0 !important;\n background: white !important;\n color-scheme: light !important;\n }\n }\n\n /* Screen: also apply isolation for print preview */\n @media screen {\n /* When this stylesheet is active, we're about to print */\n /* No screen-specific rules needed - isolation only applies to print */\n }\n `;\n return style;\n}\n\n/**\n * Print a grid in isolation by hiding all other page content.\n *\n * This function adds a temporary stylesheet that uses CSS to hide everything\n * on the page except the target grid during printing. The grid stays in place\n * with all its data (virtualization should be disabled separately).\n *\n * @param gridElement - The tbw-grid element to print (must have an ID)\n * @param options - Optional configuration\n * @returns Promise that resolves when the print dialog closes\n *\n * @example\n * ```typescript\n * import { queryGrid } from '@toolbox-web/grid';\n * import { printGridIsolated } from '@toolbox-web/grid/plugins/print';\n *\n * const grid = queryGrid('tbw-grid');\n * await printGridIsolated(grid, { orientation: 'landscape' });\n * ```\n */\nexport async function printGridIsolated(gridElement: HTMLElement, options: PrintIsolatedOptions = {}): Promise<void> {\n const { orientation = 'landscape' } = options;\n\n const gridId = gridElement.id;\n\n // Warn if multiple elements share this ID (user-set IDs could collide)\n const elementsWithId = document.querySelectorAll(`#${CSS.escape(gridId)}`);\n if (elementsWithId.length > 1) {\n warnDiagnostic(\n PRINT_DUPLICATE_ID,\n `Multiple elements found with id=\"${gridId}\". ` +\n `Print isolation may not work correctly. Ensure each grid has a unique ID.`,\n gridId,\n 'print',\n );\n }\n\n // Remove any existing isolation stylesheet\n document.getElementById(ISOLATION_STYLE_ID)?.remove();\n\n // Add the isolation stylesheet\n const isolationStyle = createIsolationStylesheet(gridId, orientation);\n document.head.appendChild(isolationStyle);\n\n return new Promise((resolve) => {\n // Listen for afterprint event to cleanup\n const onAfterPrint = () => {\n window.removeEventListener('afterprint', onAfterPrint);\n // Remove isolation stylesheet\n document.getElementById(ISOLATION_STYLE_ID)?.remove();\n resolve();\n };\n window.addEventListener('afterprint', onAfterPrint);\n\n // Trigger print\n window.print();\n\n // Fallback timeout in case afterprint doesn't fire (some browsers)\n setTimeout(() => {\n window.removeEventListener('afterprint', onAfterPrint);\n document.getElementById(ISOLATION_STYLE_ID)?.remove();\n resolve();\n }, 5000);\n });\n}\n","/**\n * Print Plugin (Class-based)\n *\n * Provides print layout functionality for tbw-grid.\n * Temporarily disables virtualization to render all rows and uses\n * @media print CSS for print-optimized styling.\n */\n\nimport { PRINT_FAILED, PRINT_IN_PROGRESS, PRINT_NO_GRID, errorDiagnostic } from '../../core/internal/diagnostics';\nimport { BaseGridPlugin } from '../../core/plugin/base-plugin';\nimport type { InternalGrid, ToolbarContentDefinition } from '../../core/types';\nimport { printGridIsolated } from './print-isolated';\nimport styles from './print.css?inline';\nimport type { PrintCompleteDetail, PrintConfig, PrintParams, PrintStartDetail } from './types';\n\n/**\n * Extended grid interface for PrintPlugin internal access.\n * Includes registerToolbarContent which is available on the grid class\n * but not exposed in the standard plugin API.\n */\ninterface PrintGridRef extends InternalGrid {\n registerToolbarContent?(content: ToolbarContentDefinition): void;\n unregisterToolbarContent?(contentId: string): void;\n}\n\n/** Default configuration */\nconst DEFAULT_CONFIG: Required<PrintConfig> = {\n button: false,\n orientation: 'landscape',\n warnThreshold: 500,\n maxRows: 0,\n includeTitle: true,\n includeTimestamp: true,\n title: '',\n isolate: false,\n};\n\n/**\n * Print Plugin for tbw-grid\n *\n * Enables printing the full grid content by temporarily disabling virtualization\n * and applying print-optimized styles. Handles large datasets gracefully with\n * configurable row limits.\n *\n * ## Installation\n *\n * ```ts\n * import { PrintPlugin } from '@toolbox-web/grid/plugins/print';\n * ```\n *\n * ## Configuration Options\n *\n * | Option | Type | Default | Description |\n * |--------|------|---------|-------------|\n * | `button` | `boolean` | `false` | Show print button in toolbar |\n * | `orientation` | `'portrait' \\| 'landscape'` | `'landscape'` | Page orientation |\n * | `warnThreshold` | `number` | `500` | Show confirmation dialog when rows exceed this (0 = no warning) |\n * | `maxRows` | `number` | `0` | Hard limit on printed rows (0 = unlimited) |\n * | `includeTitle` | `boolean` | `true` | Include grid title in print |\n * | `includeTimestamp` | `boolean` | `true` | Include timestamp in footer |\n * | `title` | `string` | `''` | Custom print title |\n *\n * ## Programmatic API\n *\n * | Method | Signature | Description |\n * |--------|-----------|-------------|\n * | `print` | `(params?) => Promise<void>` | Trigger print dialog |\n * | `isPrinting` | `() => boolean` | Check if print is in progress |\n *\n * ## Events\n *\n * | Event | Detail | Description |\n * |-------|--------|-------------|\n * | `print-start` | `PrintStartDetail` | Fired when print begins |\n * | `print-complete` | `PrintCompleteDetail` | Fired when print completes |\n *\n * @example Basic Print\n * ```ts\n * import { queryGrid } from '@toolbox-web/grid';\n * import { PrintPlugin } from '@toolbox-web/grid/plugins/print';\n *\n * const grid = queryGrid('tbw-grid');\n * grid.gridConfig = {\n * plugins: [new PrintPlugin()],\n * };\n *\n * // Trigger print\n * const printPlugin = grid.getPluginByName('print');\n * await printPlugin.print();\n * ```\n *\n * @example With Toolbar Button\n * ```ts\n * grid.gridConfig = {\n * plugins: [new PrintPlugin({ button: true, orientation: 'landscape' })],\n * };\n * ```\n *\n * @see {@link PrintConfig} for all configuration options\n */\nexport class PrintPlugin extends BaseGridPlugin<PrintConfig> {\n /** @internal */\n readonly name = 'print';\n\n /** @internal */\n override readonly version = '1.0.0';\n\n /** CSS styles for print mode */\n override readonly styles = styles;\n\n /** Current print state */\n #printing = false;\n\n /** Saved column visibility state */\n #savedHiddenColumns: Map<string, boolean> | null = null;\n\n /** Saved virtualization state */\n #savedVirtualization: { bypassThreshold: number } | null = null;\n\n /** Saved rows when maxRows limit is applied */\n #savedRows: unknown[] | null = null;\n\n /** Print header element */\n #printHeader: HTMLElement | null = null;\n\n /** Print footer element */\n #printFooter: HTMLElement | null = null;\n\n /** Applied scale factor (legacy, used for cleanup) */\n #appliedScale: number | null = null;\n\n /**\n * Get the grid typed as PrintGridRef for internal access.\n */\n get #internalGrid(): PrintGridRef {\n return this.grid as unknown as PrintGridRef;\n }\n\n /**\n * Check if print is currently in progress\n */\n isPrinting(): boolean {\n return this.#printing;\n }\n\n /**\n * Trigger the browser print dialog\n *\n * This method:\n * 1. Validates row count against maxRows limit\n * 2. Disables virtualization to render all rows\n * 3. Applies print-specific CSS classes\n * 4. Opens the browser print dialog (or isolated window if `isolate: true`)\n * 5. Restores normal state after printing\n *\n * @param params - Optional parameters to override config for this print\n * @param params.isolate - If true, prints in an isolated window containing only the grid\n * @returns Promise that resolves when print dialog closes\n */\n async print(params?: PrintParams): Promise<void> {\n if (this.#printing) {\n this.warn(PRINT_IN_PROGRESS, 'Print already in progress');\n return;\n }\n\n const grid = this.gridElement;\n if (!grid) {\n this.warn(PRINT_NO_GRID, 'Grid not available');\n return;\n }\n\n const config = { ...DEFAULT_CONFIG, ...this.config, ...params };\n const rows = this.rows;\n const originalRowCount = rows.length;\n let rowCount = originalRowCount;\n let limitApplied = false;\n\n // Check if we should warn about large datasets\n if (config.warnThreshold > 0 && originalRowCount > config.warnThreshold) {\n const limitInfo =\n config.maxRows > 0 ? `\\n\\nNote: Output will be limited to ${config.maxRows.toLocaleString()} rows.` : '';\n const proceed = confirm(\n `This grid has ${originalRowCount.toLocaleString()} rows. ` +\n `Printing large datasets may cause performance issues or browser slowdowns.${limitInfo}\\n\\n` +\n `Click OK to continue, or Cancel to abort.`,\n );\n if (!proceed) {\n return;\n }\n }\n\n // Apply hard row limit if configured\n if (config.maxRows > 0 && originalRowCount > config.maxRows) {\n rowCount = config.maxRows;\n limitApplied = true;\n }\n\n this.#printing = true;\n\n // Track timing for duration reporting\n const startTime = performance.now();\n\n // Emit print-start event\n this.emit<PrintStartDetail>('print-start', {\n rowCount,\n limitApplied,\n originalRowCount,\n });\n\n try {\n // Save current virtualization state\n const internalGrid = this.#internalGrid;\n this.#savedVirtualization = {\n bypassThreshold: internalGrid._virtualization?.bypassThreshold ?? 24,\n };\n\n // Hide columns marked with printHidden\n this.#hidePrintColumns();\n\n // Apply row limit if configured\n if (limitApplied) {\n this.#savedRows = this.sourceRows;\n // Set limited rows on the grid\n this.grid.rows = this.sourceRows.slice(0, rowCount);\n // Wait for grid to process new rows\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n\n // Add print header if configured\n if (config.includeTitle || config.includeTimestamp) {\n this.#addPrintHeader(config);\n }\n\n // Disable virtualization to render all rows\n // This forces the grid to render all rows in the DOM\n await this.#disableVirtualization();\n\n // Wait for next frame to ensure DOM is updated\n await new Promise((resolve) => requestAnimationFrame(resolve));\n await new Promise((resolve) => requestAnimationFrame(resolve));\n\n // Add orientation class for @page rules\n grid.classList.add(`print-${config.orientation}`);\n\n // Wait for next frame to ensure DOM is updated\n await new Promise((resolve) => requestAnimationFrame(resolve));\n await new Promise((resolve) => requestAnimationFrame(resolve));\n\n // Trigger browser print dialog (isolated or inline)\n if (config.isolate) {\n await this.#printInIsolatedWindow(config);\n } else {\n await this.#triggerPrint();\n }\n\n // Emit print-complete event\n this.emit<PrintCompleteDetail>('print-complete', {\n success: true,\n rowCount,\n duration: Math.round(performance.now() - startTime),\n });\n } catch (error) {\n errorDiagnostic(PRINT_FAILED, `Print failed: ${error}`, this.gridElement?.id, this.name);\n this.emit<PrintCompleteDetail>('print-complete', {\n success: false,\n rowCount: 0,\n duration: Math.round(performance.now() - startTime),\n });\n } finally {\n // Restore normal state\n this.#cleanup();\n this.#printing = false;\n }\n }\n\n /**\n * Add print header with title and timestamp\n */\n #addPrintHeader(config: Required<PrintConfig>): void {\n const grid = this.gridElement;\n if (!grid) return;\n\n // Create print header\n this.#printHeader = document.createElement('div');\n this.#printHeader.className = 'tbw-print-header';\n\n // Title\n if (config.includeTitle) {\n const title = config.title || this.grid.effectiveConfig?.shell?.header?.title || 'Grid Data';\n const titleEl = document.createElement('div');\n titleEl.className = 'tbw-print-header-title';\n titleEl.textContent = title;\n this.#printHeader.appendChild(titleEl);\n }\n\n // Timestamp\n if (config.includeTimestamp) {\n const timestampEl = document.createElement('div');\n timestampEl.className = 'tbw-print-header-timestamp';\n timestampEl.textContent = `Printed: ${new Date().toLocaleString()}`;\n this.#printHeader.appendChild(timestampEl);\n }\n\n // Insert at the beginning of the grid\n grid.insertBefore(this.#printHeader, grid.firstChild);\n\n // Create print footer\n this.#printFooter = document.createElement('div');\n this.#printFooter.className = 'tbw-print-footer';\n this.#printFooter.textContent = `Page generated from ${window.location.hostname}`;\n grid.appendChild(this.#printFooter);\n }\n\n /**\n * Disable virtualization to render all rows\n */\n async #disableVirtualization(): Promise<void> {\n const internalGrid = this.#internalGrid;\n if (!internalGrid._virtualization) return;\n\n // Set bypass threshold higher than total row count to disable virtualization\n // This makes the grid render all rows (up to maxRows) instead of just visible ones\n const totalRows = this.rows.length;\n internalGrid._virtualization.bypassThreshold = totalRows + 100;\n\n // Force a full refresh to re-render with virtualization disabled\n internalGrid.refreshVirtualWindow(true);\n\n // Wait for render to complete\n await new Promise((resolve) => setTimeout(resolve, 100));\n }\n\n /**\n * Trigger the browser print dialog\n */\n async #triggerPrint(): Promise<void> {\n return new Promise((resolve) => {\n // Listen for afterprint event\n const onAfterPrint = () => {\n window.removeEventListener('afterprint', onAfterPrint);\n resolve();\n };\n window.addEventListener('afterprint', onAfterPrint);\n\n // Trigger print\n window.print();\n\n // Fallback timeout in case afterprint doesn't fire (some browsers)\n setTimeout(() => {\n // Guard against test environment teardown where window may be undefined\n if (typeof window !== 'undefined') {\n window.removeEventListener('afterprint', onAfterPrint);\n }\n resolve();\n }, 1000);\n });\n }\n\n /**\n * Print in isolation by hiding all other page content.\n * This excludes navigation, sidebars, etc. while keeping the grid in place.\n */\n async #printInIsolatedWindow(config: Required<PrintConfig>): Promise<void> {\n const grid = this.gridElement;\n if (!grid) return;\n\n await printGridIsolated(grid, {\n orientation: config.orientation,\n });\n }\n\n /**\n * Hide columns marked with printHidden: true\n */\n #hidePrintColumns(): void {\n const columns = this.columns;\n if (!columns) return;\n\n // Save current hidden state and hide print columns\n this.#savedHiddenColumns = new Map();\n\n for (const col of columns) {\n if (col.printHidden && col.field) {\n // Save current visibility state (true = visible, false = hidden)\n this.#savedHiddenColumns.set(col.field, !col.hidden);\n // Hide the column for printing\n this.grid.setColumnVisible(col.field, false);\n }\n }\n }\n\n /**\n * Restore columns that were hidden for printing\n */\n #restorePrintColumns(): void {\n if (!this.#savedHiddenColumns) return;\n\n for (const [field, wasVisible] of this.#savedHiddenColumns) {\n // Restore original visibility\n this.grid.setColumnVisible(field, wasVisible);\n }\n\n this.#savedHiddenColumns = null;\n }\n\n /**\n * Cleanup after printing\n */\n #cleanup(): void {\n const grid = this.gridElement;\n if (!grid) return;\n\n // Restore columns that were hidden for printing\n this.#restorePrintColumns();\n\n // Remove orientation classes (both original and possibly switched)\n grid.classList.remove('print-portrait', 'print-landscape');\n\n // Remove scaling transform if applied (legacy)\n if (this.#appliedScale !== null) {\n grid.style.transform = '';\n grid.style.transformOrigin = '';\n grid.style.width = '';\n this.#appliedScale = null;\n }\n\n // Remove print header/footer\n if (this.#printHeader) {\n this.#printHeader.remove();\n this.#printHeader = null;\n }\n if (this.#printFooter) {\n this.#printFooter.remove();\n this.#printFooter = null;\n }\n\n // Restore virtualization\n const internalGrid = this.#internalGrid;\n if (this.#savedVirtualization && internalGrid._virtualization) {\n internalGrid._virtualization.bypassThreshold = this.#savedVirtualization.bypassThreshold;\n internalGrid.refreshVirtualWindow(true);\n this.#savedVirtualization = null;\n }\n\n // Restore original rows if they were limited\n if (this.#savedRows !== null) {\n this.grid.rows = this.#savedRows;\n this.#savedRows = null;\n }\n }\n\n /**\n * Register toolbar button if configured\n * @internal\n */\n override afterRender(): void {\n // Register toolbar on first render when button is enabled\n if (this.config?.button && !this.#toolbarRegistered) {\n this.#registerToolbarButton();\n this.#toolbarRegistered = true;\n }\n }\n\n /** Track if toolbar button is registered */\n #toolbarRegistered = false;\n\n /**\n * Register print button in toolbar\n */\n #registerToolbarButton(): void {\n const grid = this.#internalGrid;\n\n // Register toolbar content\n grid.registerToolbarContent?.({\n id: 'print-button',\n order: 900, // High order to appear at the end\n render: (container: HTMLElement) => {\n const button = document.createElement('button');\n button.className = 'tbw-toolbar-btn tbw-print-btn';\n button.title = 'Print grid';\n button.type = 'button';\n\n // Use print icon\n const icon = this.resolveIcon('print') || '🖨️';\n this.setIcon(button, icon);\n\n button.addEventListener(\n 'click',\n () => {\n this.print();\n },\n { signal: this.disconnectSignal },\n );\n\n container.appendChild(button);\n },\n });\n }\n}\n"],"names":["ISOLATION_STYLE_ID","async","printGridIsolated","gridElement","options","orientation","gridId","id","document","querySelectorAll","CSS","escape","length","warnDiagnostic","PRINT_DUPLICATE_ID","getElementById","remove","isolationStyle","style","createElement","textContent","createIsolationStylesheet","head","appendChild","Promise","resolve","onAfterPrint","window","removeEventListener","addEventListener","print","setTimeout","DEFAULT_CONFIG","button","warnThreshold","maxRows","includeTitle","includeTimestamp","title","isolate","PrintPlugin","BaseGridPlugin","name","version","styles","printing","savedHiddenColumns","savedVirtualization","savedRows","printHeader","printFooter","appliedScale","internalGrid","this","grid","isPrinting","params","warn","PRINT_IN_PROGRESS","PRINT_NO_GRID","config","originalRowCount","rows","rowCount","limitApplied","limitInfo","toLocaleString","confirm","startTime","performance","now","emit","bypassThreshold","_virtualization","hidePrintColumns","sourceRows","slice","addPrintHeader","disableVirtualization","requestAnimationFrame","classList","add","printInIsolatedWindow","triggerPrint","success","duration","Math","round","error","errorDiagnostic","PRINT_FAILED","cleanup","className","effectiveConfig","shell","header","titleEl","timestampEl","Date","insertBefore","firstChild","location","hostname","totalRows","refreshVirtualWindow","columns","Map","col","printHidden","field","set","hidden","setColumnVisible","restorePrintColumns","wasVisible","transform","transformOrigin","width","afterRender","toolbarRegistered","registerToolbarButton","registerToolbarContent","order","render","container","type","icon","resolveIcon","setIcon","signal","disconnectSignal"],"mappings":"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"}
|