@alaarab/ogrid-core 2.1.2 → 2.1.4

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.
Files changed (35) hide show
  1. package/dist/esm/index.js +1426 -54
  2. package/package.json +3 -3
  3. package/dist/esm/constants/index.js +0 -3
  4. package/dist/esm/constants/layout.js +0 -13
  5. package/dist/esm/constants/timing.js +0 -10
  6. package/dist/esm/constants/zIndex.js +0 -16
  7. package/dist/esm/types/columnTypes.js +0 -1
  8. package/dist/esm/types/dataGridTypes.js +0 -27
  9. package/dist/esm/types/index.js +0 -2
  10. package/dist/esm/utils/aggregationUtils.js +0 -48
  11. package/dist/esm/utils/cellValue.js +0 -14
  12. package/dist/esm/utils/clientSideData.js +0 -155
  13. package/dist/esm/utils/clipboardHelpers.js +0 -142
  14. package/dist/esm/utils/columnAutosize.js +0 -38
  15. package/dist/esm/utils/columnReorder.js +0 -99
  16. package/dist/esm/utils/columnUtils.js +0 -122
  17. package/dist/esm/utils/dataGridStatusBar.js +0 -15
  18. package/dist/esm/utils/dataGridViewModel.js +0 -206
  19. package/dist/esm/utils/debounce.js +0 -40
  20. package/dist/esm/utils/dom.js +0 -53
  21. package/dist/esm/utils/exportToCsv.js +0 -50
  22. package/dist/esm/utils/fillHelpers.js +0 -47
  23. package/dist/esm/utils/gridContextMenuHelpers.js +0 -80
  24. package/dist/esm/utils/gridRowComparator.js +0 -78
  25. package/dist/esm/utils/index.js +0 -25
  26. package/dist/esm/utils/keyboardNavigation.js +0 -181
  27. package/dist/esm/utils/ogridHelpers.js +0 -67
  28. package/dist/esm/utils/paginationHelpers.js +0 -58
  29. package/dist/esm/utils/selectionHelpers.js +0 -94
  30. package/dist/esm/utils/sortHelpers.js +0 -28
  31. package/dist/esm/utils/statusBarHelpers.js +0 -27
  32. package/dist/esm/utils/undoRedoStack.js +0 -130
  33. package/dist/esm/utils/validation.js +0 -43
  34. package/dist/esm/utils/valueParsers.js +0 -121
  35. package/dist/esm/utils/virtualScroll.js +0 -46
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-core",
3
- "version": "2.1.2",
3
+ "version": "2.1.4",
4
4
  "description": "OGrid core – framework-agnostic types, algorithms, and utilities for OGrid data grids.",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -9,11 +9,11 @@
9
9
  ".": {
10
10
  "types": "./dist/types/index.d.ts",
11
11
  "import": "./dist/esm/index.js",
12
- "require": "./dist/esm/index.js"
12
+ "default": "./dist/esm/index.js"
13
13
  }
14
14
  },
15
15
  "scripts": {
16
- "build": "rimraf dist && tsc -p tsconfig.build.json",
16
+ "build": "rimraf dist && tsup && tsc -p tsconfig.build.json",
17
17
  "test": "jest"
18
18
  },
19
19
  "keywords": [
@@ -1,3 +0,0 @@
1
- export { CHECKBOX_COLUMN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, GRID_BORDER_RADIUS, } from './layout';
2
- export { DEFAULT_DEBOUNCE_MS, PEOPLE_SEARCH_DEBOUNCE_MS, SIDEBAR_TRANSITION_MS, } from './timing';
3
- export { Z_INDEX } from './zIndex';
@@ -1,13 +0,0 @@
1
- /**
2
- * Layout and sizing constants for OGrid components.
3
- */
4
- /** Width of the row selection checkbox column in pixels. */
5
- export const CHECKBOX_COLUMN_WIDTH = 48;
6
- /** Width of the row numbers column in pixels. */
7
- export const ROW_NUMBER_COLUMN_WIDTH = 50;
8
- /** Default minimum width for resizable columns in pixels. */
9
- export const DEFAULT_MIN_COLUMN_WIDTH = 80;
10
- /** Horizontal padding inside cells, used for width calculations. */
11
- export const CELL_PADDING = 16;
12
- /** Border radius for the grid container in pixels. */
13
- export const GRID_BORDER_RADIUS = 6;
@@ -1,10 +0,0 @@
1
- /**
2
- * Timing constants used across OGrid.
3
- * Centralizes magic numbers for consistency and easier tuning.
4
- */
5
- /** Default debounce delay for generic inputs (milliseconds) */
6
- export const DEFAULT_DEBOUNCE_MS = 300;
7
- /** Debounce delay for people/user search inputs (milliseconds) */
8
- export const PEOPLE_SEARCH_DEBOUNCE_MS = DEFAULT_DEBOUNCE_MS;
9
- /** Sidebar panel transition duration (milliseconds) */
10
- export const SIDEBAR_TRANSITION_MS = 300;
@@ -1,16 +0,0 @@
1
- /**
2
- * Z-index constants for layering OGrid UI elements.
3
- * Ensures consistent stacking order across all packages.
4
- */
5
- export const Z_INDEX = {
6
- /** Selection range overlay (marching ants) */
7
- SELECTION_OVERLAY: 4,
8
- /** Clipboard overlay (copy/cut animation) */
9
- CLIPBOARD_OVERLAY: 5,
10
- /** Dropdown menus (column chooser, pagination size select) */
11
- DROPDOWN: 1000,
12
- /** Modal dialogs */
13
- MODAL: 2000,
14
- /** Context menus (right-click grid menu) */
15
- CONTEXT_MENU: 9999,
16
- };
@@ -1 +0,0 @@
1
- export {};
@@ -1,27 +0,0 @@
1
- export function toUserLike(u) {
2
- if (!u)
3
- return undefined;
4
- return {
5
- id: u.id,
6
- displayName: u.displayName,
7
- email: 'email' in u && u.email ? u.email : (u.mail || u.userPrincipalName || ''),
8
- photo: u.photo
9
- };
10
- }
11
- /** Returns true if (row, col) is inside the range (inclusive). */
12
- export function isInSelectionRange(range, row, col) {
13
- const minR = Math.min(range.startRow, range.endRow);
14
- const maxR = Math.max(range.startRow, range.endRow);
15
- const minC = Math.min(range.startCol, range.endCol);
16
- const maxC = Math.max(range.startCol, range.endCol);
17
- return row >= minR && row <= maxR && col >= minC && col <= maxC;
18
- }
19
- /** Normalize range so start ≤ end for both dimensions. */
20
- export function normalizeSelectionRange(range) {
21
- return {
22
- startRow: Math.min(range.startRow, range.endRow),
23
- endRow: Math.max(range.startRow, range.endRow),
24
- startCol: Math.min(range.startCol, range.endCol),
25
- endCol: Math.max(range.startCol, range.endCol),
26
- };
27
- }
@@ -1,2 +0,0 @@
1
- // Utility functions
2
- export { toUserLike, isInSelectionRange, normalizeSelectionRange, } from './dataGridTypes';
@@ -1,48 +0,0 @@
1
- import { getCellValue } from './cellValue';
2
- import { normalizeSelectionRange } from '../types/dataGridTypes';
3
- /**
4
- * Computes numeric aggregations (sum, avg, min, max, count) for selected cells.
5
- * Only numeric values are included in sum/avg/min/max. Count includes only numeric cells.
6
- * Returns null when selection is absent, has fewer than 2 cells, or contains no numeric values.
7
- */
8
- export function computeAggregations(items, visibleCols, selectionRange) {
9
- if (!selectionRange)
10
- return null;
11
- const norm = normalizeSelectionRange(selectionRange);
12
- let totalCells = 0;
13
- let sum = 0;
14
- let min = Infinity;
15
- let max = -Infinity;
16
- let count = 0;
17
- for (let r = norm.startRow; r <= norm.endRow; r++) {
18
- for (let c = norm.startCol; c <= norm.endCol; c++) {
19
- if (r >= items.length || c >= visibleCols.length)
20
- continue;
21
- totalCells++;
22
- const item = items[r];
23
- const col = visibleCols[c];
24
- const raw = getCellValue(item, col);
25
- // Use Number() instead of parseFloat() so date strings like "2020-08-22"
26
- // return NaN instead of partially parsing to 2020
27
- const num = typeof raw === 'number' ? raw : Number(raw);
28
- if (!isNaN(num) && isFinite(num)) {
29
- sum += num;
30
- if (num < min)
31
- min = num;
32
- if (num > max)
33
- max = num;
34
- count++;
35
- }
36
- }
37
- }
38
- // Need at least 2 cells selected and at least 1 numeric value to show aggregation
39
- if (totalCells < 2 || count === 0)
40
- return null;
41
- return {
42
- sum,
43
- avg: sum / count,
44
- min,
45
- max,
46
- count,
47
- };
48
- }
@@ -1,14 +0,0 @@
1
- /**
2
- * Get the cell value for a row/column, using valueGetter when defined otherwise item[columnId].
3
- *
4
- * @param item - The row data object.
5
- * @param col - Column definition. If `valueGetter` is defined it takes priority;
6
- * otherwise the value is read via `item[col.columnId]`.
7
- * Assumes `columnId` is a valid key on the item when no `valueGetter` is provided.
8
- * @returns The raw cell value (`unknown`). May be `undefined` if the key does not exist on the item.
9
- */
10
- export function getCellValue(item, col) {
11
- if (col.valueGetter)
12
- return col.valueGetter(item);
13
- return item[col.columnId];
14
- }
@@ -1,155 +0,0 @@
1
- import { getCellValue } from './cellValue';
2
- import { getFilterField } from './ogridHelpers';
3
- /**
4
- * Cached column map to avoid rebuilding on every call.
5
- * WeakMap keyed by columns array reference.
6
- */
7
- const columnMapCache = new WeakMap();
8
- /**
9
- * Apply client-side filtering and sorting to data.
10
- * Extracted from useOGrid for testability and reuse.
11
- *
12
- * @param data - The full dataset to process
13
- * @param columns - Column definitions (used for filtering and sorting)
14
- * @param filters - Current filter state (discriminated FilterValue union)
15
- * @param sortBy - Column ID to sort by (optional)
16
- * @param sortDirection - Sort direction (optional)
17
- * @returns Filtered and sorted array
18
- */
19
- export function processClientSideData(data, columns, filters, sortBy, sortDirection) {
20
- // Get or build column lookup map (cached via WeakMap)
21
- let columnMap = columnMapCache.get(columns);
22
- if (!columnMap) {
23
- columnMap = new Map();
24
- for (let i = 0; i < columns.length; i++) {
25
- columnMap.set(columns[i].columnId, columns[i]);
26
- }
27
- columnMapCache.set(columns, columnMap);
28
- }
29
- // --- Filtering (single-pass: build predicates, then one .filter()) ---
30
- const predicates = [];
31
- for (let i = 0; i < columns.length; i++) {
32
- const col = columns[i];
33
- const filterKey = getFilterField(col);
34
- const val = filters[filterKey];
35
- if (!val)
36
- continue;
37
- switch (val.type) {
38
- case 'multiSelect':
39
- // NOTE: Cell values are coerced to string via String() for set membership checks.
40
- // Object-typed column values will produce "[object Object]" — use valueGetter or
41
- // valueFormatter on the column def to ensure meaningful string representation.
42
- if (val.value.length > 0) {
43
- const allowedSet = new Set(val.value);
44
- predicates.push((r) => allowedSet.has(String(getCellValue(r, col))));
45
- }
46
- break;
47
- case 'text': {
48
- const trimmed = val.value.trim();
49
- if (trimmed) {
50
- const lower = trimmed.toLowerCase();
51
- predicates.push((r) => String(getCellValue(r, col) ?? '').toLowerCase().includes(lower));
52
- }
53
- break;
54
- }
55
- case 'people': {
56
- const email = val.value.email.toLowerCase();
57
- predicates.push((r) => String(getCellValue(r, col) ?? '').toLowerCase() === email);
58
- break;
59
- }
60
- case 'date': {
61
- const dv = val.value;
62
- // Pre-compute filter boundary timestamps to avoid repeated Date parsing in the filter loop
63
- const fromTs = dv.from ? new Date(dv.from + 'T00:00:00').getTime() : NaN;
64
- const toTs = dv.to ? new Date(dv.to + 'T23:59:59.999').getTime() : NaN;
65
- predicates.push((r) => {
66
- const cellVal = getCellValue(r, col);
67
- if (cellVal == null)
68
- return false;
69
- const cellTs = new Date(String(cellVal)).getTime();
70
- if (Number.isNaN(cellTs))
71
- return false;
72
- if (!Number.isNaN(fromTs) && cellTs < fromTs)
73
- return false;
74
- if (!Number.isNaN(toTs) && cellTs > toTs)
75
- return false;
76
- return true;
77
- });
78
- break;
79
- }
80
- }
81
- }
82
- const filtered = predicates.length > 0;
83
- const rows = filtered
84
- ? data.filter((row) => {
85
- for (let i = 0; i < predicates.length; i++) {
86
- if (!predicates[i](row))
87
- return false;
88
- }
89
- return true;
90
- })
91
- : data;
92
- // --- Sorting ---
93
- if (sortBy) {
94
- // Copy before sorting if we didn't filter (filter already creates a new array).
95
- // This avoids mutating the caller's original data array.
96
- const sortable = filtered ? rows : rows.slice();
97
- const sortCol = columnMap.get(sortBy);
98
- const compare = sortCol?.compare;
99
- const dir = sortDirection === 'asc' ? 1 : -1;
100
- const isDateSort = sortCol?.type === 'date';
101
- // For date columns, pre-compute timestamps to avoid repeated new Date() in O(n log n) comparisons.
102
- // NOTE: The timestamp cache is scoped to this single sort invocation. It is rebuilt on every call,
103
- // so mutating row objects between calls is safe — stale timestamps cannot persist across invocations.
104
- if (isDateSort && !compare) {
105
- const timestampCache = new Map();
106
- for (let i = 0; i < sortable.length; i++) {
107
- const row = sortable[i];
108
- const val = sortCol ? getCellValue(row, sortCol) : row[sortBy];
109
- if (val == null) {
110
- timestampCache.set(row, NaN);
111
- }
112
- else {
113
- const t = new Date(String(val)).getTime();
114
- timestampCache.set(row, Number.isNaN(t) ? 0 : t);
115
- }
116
- }
117
- sortable.sort((a, b) => {
118
- const at = timestampCache.get(a) ?? NaN;
119
- const bt = timestampCache.get(b) ?? NaN;
120
- if (Number.isNaN(at) && Number.isNaN(bt))
121
- return 0;
122
- if (Number.isNaN(at))
123
- return -1 * dir;
124
- if (Number.isNaN(bt))
125
- return 1 * dir;
126
- return at === bt ? 0 : at > bt ? dir : -dir;
127
- });
128
- }
129
- else {
130
- sortable.sort((a, b) => {
131
- if (compare)
132
- return compare(a, b) * dir;
133
- const av = sortCol
134
- ? getCellValue(a, sortCol)
135
- : a[sortBy];
136
- const bv = sortCol
137
- ? getCellValue(b, sortCol)
138
- : b[sortBy];
139
- if (av == null && bv == null)
140
- return 0;
141
- if (av == null)
142
- return -1 * dir;
143
- if (bv == null)
144
- return 1 * dir;
145
- if (typeof av === 'number' && typeof bv === 'number')
146
- return av === bv ? 0 : av > bv ? dir : -dir;
147
- const as = String(av).toLowerCase();
148
- const bs = String(bv).toLowerCase();
149
- return as === bs ? 0 : as > bs ? dir : -dir;
150
- });
151
- }
152
- return sortable;
153
- }
154
- return rows;
155
- }
@@ -1,142 +0,0 @@
1
- import { getCellValue } from './cellValue';
2
- import { parseValue } from './valueParsers';
3
- import { normalizeSelectionRange } from '../types';
4
- /**
5
- * Format a single cell value for inclusion in a TSV clipboard string.
6
- * Strips tabs and newlines so they don't corrupt the TSV structure.
7
- *
8
- * @param raw Raw cell value (from getCellValue).
9
- * @param formatted Formatted value (from valueFormatter, if present).
10
- * @returns TSV-safe string representation of the cell.
11
- */
12
- export function formatCellValueForTsv(raw, formatted) {
13
- const val = formatted != null && formatted !== '' ? formatted : raw;
14
- if (val == null || val === '')
15
- return '';
16
- try {
17
- return String(val).replace(/[\t\n]/g, ' ');
18
- }
19
- catch {
20
- return '[Object]';
21
- }
22
- }
23
- /**
24
- * Serialize a rectangular cell range to a TSV (tab-separated values) string
25
- * suitable for writing to the clipboard.
26
- *
27
- * @param items Flat array of all row data objects.
28
- * @param visibleCols Visible column definitions.
29
- * @param range The selection range to serialize (will be normalized).
30
- * @returns TSV string with rows separated by \\r\\n and columns by \\t.
31
- */
32
- export function formatSelectionAsTsv(items, visibleCols, range) {
33
- const norm = normalizeSelectionRange(range);
34
- const rows = [];
35
- for (let r = norm.startRow; r <= norm.endRow; r++) {
36
- const cells = [];
37
- for (let c = norm.startCol; c <= norm.endCol; c++) {
38
- if (r >= items.length || c >= visibleCols.length)
39
- break;
40
- const item = items[r];
41
- const col = visibleCols[c];
42
- const raw = getCellValue(item, col);
43
- const clipboard = col.clipboardFormatter ? col.clipboardFormatter(raw, item) : null;
44
- const formatted = clipboard ?? (col.valueFormatter ? col.valueFormatter(raw, item) : raw);
45
- cells.push(formatCellValueForTsv(raw, formatted));
46
- }
47
- rows.push(cells.join('\t'));
48
- }
49
- return rows.join('\r\n');
50
- }
51
- /**
52
- * Parse a TSV clipboard string into a 2D array of cell strings.
53
- * Handles both \\r\\n and \\n line endings. Ignores trailing empty lines.
54
- *
55
- * @param text Raw clipboard text (TSV format).
56
- * @returns 2D array: rows of cells. Empty if text is blank.
57
- */
58
- export function parseTsvClipboard(text) {
59
- if (!text.trim())
60
- return [];
61
- const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
62
- return lines.map((line) => line.split('\t'));
63
- }
64
- /**
65
- * Apply parsed clipboard rows to the grid starting at anchor position.
66
- * For each cell in the parsed rows, validates editability, parses the value,
67
- * and produces a cell value changed event.
68
- *
69
- * @param parsedRows 2D array of string values (from parseTsvClipboard).
70
- * @param anchorRow Target starting row index.
71
- * @param anchorCol Target starting column index (data column, not absolute).
72
- * @param items Array of all row data objects.
73
- * @param visibleCols Visible column definitions.
74
- * @returns Array of cell value changed events to apply.
75
- */
76
- export function applyPastedValues(parsedRows, anchorRow, anchorCol, items, visibleCols) {
77
- const events = [];
78
- for (let r = 0; r < parsedRows.length; r++) {
79
- const cells = parsedRows[r];
80
- for (let c = 0; c < cells.length; c++) {
81
- const targetRow = anchorRow + r;
82
- const targetCol = anchorCol + c;
83
- if (targetRow >= items.length || targetCol >= visibleCols.length)
84
- continue;
85
- const item = items[targetRow];
86
- const col = visibleCols[targetCol];
87
- const colEditable = col.editable === true ||
88
- (typeof col.editable === 'function' && col.editable(item));
89
- if (!colEditable)
90
- continue;
91
- const rawValue = cells[c] ?? '';
92
- const oldValue = getCellValue(item, col);
93
- const result = parseValue(rawValue, oldValue, item, col);
94
- if (!result.valid)
95
- continue;
96
- events.push({
97
- item,
98
- columnId: col.columnId,
99
- oldValue,
100
- newValue: result.value,
101
- rowIndex: targetRow,
102
- });
103
- }
104
- }
105
- return events;
106
- }
107
- /**
108
- * Clear cells in a cut range by setting each editable cell to an empty-string-parsed value.
109
- * Used after pasting cut content to clear the original cells.
110
- *
111
- * @param cutRange The normalized range of cells to clear.
112
- * @param items Array of all row data objects.
113
- * @param visibleCols Visible column definitions.
114
- * @returns Array of cell value changed events to apply.
115
- */
116
- export function applyCutClear(cutRange, items, visibleCols) {
117
- const events = [];
118
- for (let r = cutRange.startRow; r <= cutRange.endRow; r++) {
119
- for (let c = cutRange.startCol; c <= cutRange.endCol; c++) {
120
- if (r >= items.length || c >= visibleCols.length)
121
- continue;
122
- const item = items[r];
123
- const col = visibleCols[c];
124
- const colEditable = col.editable === true ||
125
- (typeof col.editable === 'function' && col.editable(item));
126
- if (!colEditable)
127
- continue;
128
- const oldValue = getCellValue(item, col);
129
- const result = parseValue('', oldValue, item, col);
130
- if (!result.valid)
131
- continue;
132
- events.push({
133
- item,
134
- columnId: col.columnId,
135
- oldValue,
136
- newValue: result.value,
137
- rowIndex: r,
138
- });
139
- }
140
- }
141
- return events;
142
- }
@@ -1,38 +0,0 @@
1
- /**
2
- * Column autosize DOM measurement utilities shared across all frameworks.
3
- */
4
- import { DEFAULT_MIN_COLUMN_WIDTH } from '../constants/layout';
5
- /** Extra pixels added to header label width to account for filter icon + padding. */
6
- export const AUTOSIZE_EXTRA_PX = 28;
7
- /** Maximum column width from autosize. */
8
- export const AUTOSIZE_MAX_PX = 520;
9
- /**
10
- * Measure the ideal width for a column by scanning all DOM cells with
11
- * `[data-column-id="<columnId>"]` and computing the maximum scrollWidth.
12
- *
13
- * Header cells with a `[data-header-label]` child get extra padding for icons.
14
- *
15
- * @param columnId - Column to measure
16
- * @param minWidth - Minimum width (defaults to DEFAULT_MIN_COLUMN_WIDTH)
17
- * @param container - Optional container element to scope the query (defaults to `document`)
18
- * @returns The ideal column width in pixels, clamped between minWidth and AUTOSIZE_MAX_PX
19
- */
20
- export function measureColumnContentWidth(columnId, minWidth, container) {
21
- const minW = minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
22
- const root = container ?? document;
23
- const cells = root.querySelectorAll(`[data-column-id="${columnId}"]`);
24
- if (cells.length === 0)
25
- return minW;
26
- let maxWidth = minW;
27
- cells.forEach((cell) => {
28
- const el = cell;
29
- const label = el.querySelector?.('[data-header-label]');
30
- if (label) {
31
- maxWidth = Math.max(maxWidth, label.scrollWidth + AUTOSIZE_EXTRA_PX);
32
- }
33
- else {
34
- maxWidth = Math.max(maxWidth, el.scrollWidth);
35
- }
36
- });
37
- return Math.min(AUTOSIZE_MAX_PX, Math.max(minW, Math.ceil(maxWidth)));
38
- }
@@ -1,99 +0,0 @@
1
- /**
2
- * Determine which pin zone a column belongs to.
3
- */
4
- export function getPinStateForColumn(columnId, pinnedColumns) {
5
- if (!pinnedColumns)
6
- return 'unpinned';
7
- if (pinnedColumns.left?.includes(columnId))
8
- return 'left';
9
- if (pinnedColumns.right?.includes(columnId))
10
- return 'right';
11
- return 'unpinned';
12
- }
13
- /**
14
- * Remove `columnId` from `order` and insert it at `targetIndex`.
15
- * Returns a new array (does not mutate the input).
16
- */
17
- export function reorderColumnArray(order, columnId, targetIndex) {
18
- const filtered = order.filter(id => id !== columnId);
19
- const clampedIndex = Math.max(0, Math.min(targetIndex, filtered.length));
20
- const result = [...filtered];
21
- result.splice(clampedIndex, 0, columnId);
22
- return result;
23
- }
24
- /**
25
- * Calculate the drop target for a dragged column based on mouse position.
26
- *
27
- * Iterates visible column header elements (queried via `[data-column-id]`),
28
- * finds the midpoint of each header cell, and determines insertion side.
29
- * Respects pinning zones: a left-pinned column can only drop among left-pinned, etc.
30
- *
31
- * @param params - Options object containing mouseX, columnOrder, draggedColumnId,
32
- * draggedPinState, tableElement, and optional pinnedColumns.
33
- * @returns Drop target with insertion index and indicator X, or null if no valid target.
34
- */
35
- export function calculateDropTarget(params) {
36
- const { mouseX, columnOrder, draggedColumnId, draggedPinState, tableElement, pinnedColumns } = params;
37
- const headerCells = tableElement.querySelectorAll('[data-column-id]');
38
- if (headerCells.length === 0)
39
- return null;
40
- // Build ordered list of header rects for columns in the same pin zone
41
- const targets = [];
42
- headerCells.forEach(cell => {
43
- const colId = cell.getAttribute('data-column-id');
44
- if (!colId)
45
- return;
46
- const pinState = getPinStateForColumn(colId, pinnedColumns);
47
- if (pinState !== draggedPinState)
48
- return;
49
- const rect = cell.getBoundingClientRect();
50
- const orderIndex = columnOrder.indexOf(colId);
51
- if (orderIndex === -1)
52
- return;
53
- targets.push({
54
- columnId: colId,
55
- left: rect.left,
56
- right: rect.right,
57
- midX: rect.left + rect.width / 2,
58
- orderIndex,
59
- });
60
- });
61
- if (targets.length === 0)
62
- return null;
63
- // Sort by visual position (left edge)
64
- targets.sort((a, b) => a.left - b.left);
65
- // Find where mouse falls relative to column midpoints
66
- let targetIndex;
67
- let indicatorX;
68
- if (mouseX <= targets[0].midX) {
69
- // Before the first target
70
- targetIndex = targets[0].orderIndex;
71
- indicatorX = targets[0].left;
72
- }
73
- else if (mouseX >= targets[targets.length - 1].midX) {
74
- // After the last target
75
- const last = targets[targets.length - 1];
76
- targetIndex = last.orderIndex + 1;
77
- indicatorX = last.right;
78
- }
79
- else {
80
- // Between two targets — find the boundary
81
- let matchIndex = -1;
82
- for (let i = 0; i < targets.length - 1; i++) {
83
- if (mouseX >= targets[i].midX && mouseX < targets[i + 1].midX) {
84
- matchIndex = i;
85
- break;
86
- }
87
- }
88
- if (matchIndex === -1)
89
- return null;
90
- targetIndex = targets[matchIndex].orderIndex + 1;
91
- indicatorX = targets[matchIndex].right;
92
- }
93
- // Check if this is a no-op (dropping at same position)
94
- const currentIndex = columnOrder.indexOf(draggedColumnId);
95
- if (currentIndex === targetIndex || currentIndex + 1 === targetIndex) {
96
- return { targetIndex, indicatorX: null };
97
- }
98
- return { targetIndex, indicatorX };
99
- }