@alaarab/ogrid-core 2.0.14 → 2.0.16

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.
@@ -0,0 +1,56 @@
1
+ import { getCellValue } from './cellValue';
2
+ import { normalizeSelectionRange } from '../types';
3
+ /**
4
+ * Format a single cell value for inclusion in a TSV clipboard string.
5
+ * Strips tabs and newlines so they don't corrupt the TSV structure.
6
+ *
7
+ * @param raw Raw cell value (from getCellValue).
8
+ * @param formatted Formatted value (from valueFormatter, if present).
9
+ * @returns TSV-safe string representation of the cell.
10
+ */
11
+ export function formatCellValueForTsv(raw, formatted) {
12
+ const val = formatted != null && formatted !== '' ? formatted : raw;
13
+ if (val == null || val === '')
14
+ return '';
15
+ return String(val).replace(/\t/g, ' ').replace(/\n/g, ' ');
16
+ }
17
+ /**
18
+ * Serialize a rectangular cell range to a TSV (tab-separated values) string
19
+ * suitable for writing to the clipboard.
20
+ *
21
+ * @param items Flat array of all row data objects.
22
+ * @param visibleCols Visible column definitions.
23
+ * @param range The selection range to serialize (will be normalized).
24
+ * @returns TSV string with rows separated by \\r\\n and columns by \\t.
25
+ */
26
+ export function formatSelectionAsTsv(items, visibleCols, range) {
27
+ const norm = normalizeSelectionRange(range);
28
+ const rows = [];
29
+ for (let r = norm.startRow; r <= norm.endRow; r++) {
30
+ const cells = [];
31
+ for (let c = norm.startCol; c <= norm.endCol; c++) {
32
+ if (r >= items.length || c >= visibleCols.length)
33
+ break;
34
+ const item = items[r];
35
+ const col = visibleCols[c];
36
+ const raw = getCellValue(item, col);
37
+ const formatted = col.valueFormatter ? col.valueFormatter(raw, item) : raw;
38
+ cells.push(formatCellValueForTsv(raw, formatted));
39
+ }
40
+ rows.push(cells.join('\t'));
41
+ }
42
+ return rows.join('\r\n');
43
+ }
44
+ /**
45
+ * Parse a TSV clipboard string into a 2D array of cell strings.
46
+ * Handles both \\r\\n and \\n line endings. Ignores trailing empty lines.
47
+ *
48
+ * @param text Raw clipboard text (TSV format).
49
+ * @returns 2D array: rows of cells. Empty if text is blank.
50
+ */
51
+ export function parseTsvClipboard(text) {
52
+ if (!text.trim())
53
+ return [];
54
+ const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
55
+ return lines.map((line) => line.split('\t'));
56
+ }
@@ -17,3 +17,7 @@ export { debounce } from './debounce';
17
17
  export { measureRange, injectGlobalStyles } from './dom';
18
18
  export { computeNextSortState } from './sortHelpers';
19
19
  export { measureColumnContentWidth, AUTOSIZE_EXTRA_PX, AUTOSIZE_MAX_PX } from './columnAutosize';
20
+ export { findCtrlArrowTarget, computeTabNavigation } from './keyboardNavigation';
21
+ export { rangesEqual, clampSelectionToBounds, computeAutoScrollSpeed } from './selectionHelpers';
22
+ export { formatCellValueForTsv, formatSelectionAsTsv, parseTsvClipboard, } from './clipboardHelpers';
23
+ export { UndoRedoStack } from './undoRedoStack';
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Pure keyboard navigation helpers shared across React, Vue, Angular, and JS.
3
+ * No framework dependencies — takes plain values, returns plain values.
4
+ */
5
+ /**
6
+ * Excel-style Ctrl+Arrow: find the target position along a 1D axis.
7
+ * - Non-empty current + non-empty next → scan through non-empties, stop at last before empty/edge.
8
+ * - Otherwise → skip empties, land on next non-empty or edge.
9
+ *
10
+ * @param pos Current position (row or column index).
11
+ * @param edge The boundary position (0 for backward, max for forward).
12
+ * @param step Direction: +1 (forward) or -1 (backward).
13
+ * @param isEmpty Predicate: returns true if the cell at this index is empty.
14
+ * @returns The target position after the jump.
15
+ */
16
+ export function findCtrlArrowTarget(pos, edge, step, isEmpty) {
17
+ if (pos === edge)
18
+ return pos;
19
+ const next = pos + step;
20
+ if (!isEmpty(pos) && !isEmpty(next)) {
21
+ // Scan forward through non-empties; stop at the last before an empty or edge
22
+ let p = next;
23
+ while (p !== edge) {
24
+ if (isEmpty(p + step))
25
+ return p;
26
+ p += step;
27
+ }
28
+ return edge;
29
+ }
30
+ // Skip empties; land on first non-empty or edge
31
+ let p = next;
32
+ while (p !== edge) {
33
+ if (!isEmpty(p))
34
+ return p;
35
+ p += step;
36
+ }
37
+ return edge;
38
+ }
39
+ /**
40
+ * Compute the new Tab navigation position given the current position and direction.
41
+ *
42
+ * @param rowIndex Current row index.
43
+ * @param columnIndex Current absolute column index (includes checkbox offset).
44
+ * @param maxRowIndex Maximum row index (items.length - 1).
45
+ * @param maxColIndex Maximum absolute column index.
46
+ * @param colOffset Number of non-data leading columns (checkbox column offset).
47
+ * @param shiftKey True if Shift is held (backward tab).
48
+ * @returns New { rowIndex, columnIndex } after tab.
49
+ */
50
+ export function computeTabNavigation(rowIndex, columnIndex, maxRowIndex, maxColIndex, colOffset, shiftKey) {
51
+ let newRow = rowIndex;
52
+ let newCol = columnIndex;
53
+ if (shiftKey) {
54
+ if (columnIndex > colOffset) {
55
+ newCol = columnIndex - 1;
56
+ }
57
+ else if (rowIndex > 0) {
58
+ newRow = rowIndex - 1;
59
+ newCol = maxColIndex;
60
+ }
61
+ }
62
+ else {
63
+ if (columnIndex < maxColIndex) {
64
+ newCol = columnIndex + 1;
65
+ }
66
+ else if (rowIndex < maxRowIndex) {
67
+ newRow = rowIndex + 1;
68
+ newCol = colOffset;
69
+ }
70
+ }
71
+ return { rowIndex: newRow, columnIndex: newCol };
72
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Compare two selection ranges by value (deep equality).
3
+ * Returns true if both ranges are equal, including when both are null.
4
+ */
5
+ export function rangesEqual(a, b) {
6
+ if (a === b)
7
+ return true;
8
+ if (!a || !b)
9
+ return false;
10
+ return (a.startRow === b.startRow &&
11
+ a.endRow === b.endRow &&
12
+ a.startCol === b.startCol &&
13
+ a.endCol === b.endCol);
14
+ }
15
+ /**
16
+ * Clamp a selection range to the grid bounds (0-based, inclusive).
17
+ *
18
+ * @param range The selection range to clamp.
19
+ * @param maxRow Maximum valid row index (items.length - 1).
20
+ * @param maxCol Maximum valid column index (visibleCols.length - 1).
21
+ * @returns The clamped range, or null if the grid is empty.
22
+ */
23
+ export function clampSelectionToBounds(range, maxRow, maxCol) {
24
+ if (maxRow < 0 || maxCol < 0)
25
+ return null;
26
+ return {
27
+ startRow: Math.max(0, Math.min(range.startRow, maxRow)),
28
+ endRow: Math.max(0, Math.min(range.endRow, maxRow)),
29
+ startCol: Math.max(0, Math.min(range.startCol, maxCol)),
30
+ endCol: Math.max(0, Math.min(range.endCol, maxCol)),
31
+ };
32
+ }
33
+ /**
34
+ * Auto-scroll speed: proportional to how far past the scroll edge the pointer is.
35
+ * Used by drag-selection auto-scroll in both React and Vue.
36
+ *
37
+ * @param distance Distance past the edge threshold (pixels).
38
+ * @param edgePx Scroll edge threshold in pixels (default: 40).
39
+ * @param minSpeed Minimum scroll speed (default: 2).
40
+ * @param maxSpeed Maximum scroll speed (default: 20).
41
+ * @returns Scroll speed in pixels per interval tick.
42
+ */
43
+ export function computeAutoScrollSpeed(distance, edgePx = 40, minSpeed = 2, maxSpeed = 20) {
44
+ const t = Math.min(distance / edgePx, 1);
45
+ return minSpeed + t * (maxSpeed - minSpeed);
46
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Pure undo/redo stack data structure shared across React, Vue, Angular, and JS.
3
+ * No framework dependencies — all state is plain arrays.
4
+ *
5
+ * Usage:
6
+ * const stack = new UndoRedoStack<MyEvent>(100);
7
+ * stack.push([event]); // single event
8
+ * stack.beginBatch();
9
+ * stack.push([event1]);
10
+ * stack.push([event2]);
11
+ * stack.endBatch(); // event1 + event2 are one undo step
12
+ * const batch = stack.undo(); // returns the batch to reverse
13
+ * const batch = stack.redo(); // returns the batch to re-apply
14
+ */
15
+ export class UndoRedoStack {
16
+ constructor(maxDepth = 100) {
17
+ this.history = [];
18
+ this.redoStack = [];
19
+ this.batch = null;
20
+ this.maxDepth = maxDepth;
21
+ }
22
+ /** Whether there are undo steps available. */
23
+ get canUndo() {
24
+ return this.history.length > 0;
25
+ }
26
+ /** Whether there are redo steps available. */
27
+ get canRedo() {
28
+ return this.redoStack.length > 0;
29
+ }
30
+ /** Number of history entries. */
31
+ get historyLength() {
32
+ return this.history.length;
33
+ }
34
+ /** Number of redo entries. */
35
+ get redoLength() {
36
+ return this.redoStack.length;
37
+ }
38
+ /** Whether a batch is currently open. */
39
+ get isBatching() {
40
+ return this.batch !== null;
41
+ }
42
+ /**
43
+ * Record a group of events as a single undoable step.
44
+ * If a batch is open, accumulates into the batch instead.
45
+ * Clears the redo stack on any new entry.
46
+ */
47
+ push(events) {
48
+ if (events.length === 0)
49
+ return;
50
+ if (this.batch !== null) {
51
+ this.batch.push(...events);
52
+ }
53
+ else {
54
+ this.history = [...this.history, events].slice(-this.maxDepth);
55
+ this.redoStack = [];
56
+ }
57
+ }
58
+ /**
59
+ * Record a single event as a step (shorthand for push([event])).
60
+ * If a batch is open, accumulates into the batch instead.
61
+ */
62
+ record(event) {
63
+ this.push([event]);
64
+ }
65
+ /**
66
+ * Start a batch — subsequent record/push calls accumulate into one undo step.
67
+ * Has no effect if a batch is already open.
68
+ */
69
+ beginBatch() {
70
+ if (this.batch === null) {
71
+ this.batch = [];
72
+ }
73
+ }
74
+ /**
75
+ * End a batch — commits all accumulated events as one undo step.
76
+ * Has no effect if no batch is open or if the batch is empty.
77
+ */
78
+ endBatch() {
79
+ const b = this.batch;
80
+ this.batch = null;
81
+ if (!b || b.length === 0)
82
+ return;
83
+ this.history = [...this.history, b].slice(-this.maxDepth);
84
+ this.redoStack = [];
85
+ }
86
+ /**
87
+ * Pop the most recent history entry for undo.
88
+ * Returns the batch of events (in original order) to be reversed by the caller,
89
+ * or null if there is nothing to undo.
90
+ *
91
+ * The caller is responsible for applying the events in reverse order.
92
+ */
93
+ undo() {
94
+ if (this.history.length === 0)
95
+ return null;
96
+ const lastBatch = this.history[this.history.length - 1];
97
+ this.history = this.history.slice(0, -1);
98
+ this.redoStack = [...this.redoStack, lastBatch];
99
+ return lastBatch;
100
+ }
101
+ /**
102
+ * Pop the most recent redo entry.
103
+ * Returns the batch of events (in original order) to be re-applied by the caller,
104
+ * or null if there is nothing to redo.
105
+ */
106
+ redo() {
107
+ if (this.redoStack.length === 0)
108
+ return null;
109
+ const nextBatch = this.redoStack[this.redoStack.length - 1];
110
+ this.redoStack = this.redoStack.slice(0, -1);
111
+ this.history = [...this.history, nextBatch];
112
+ return nextBatch;
113
+ }
114
+ /**
115
+ * Clear all history and redo state.
116
+ * Does not affect any open batch — call endBatch() first if needed.
117
+ */
118
+ clear() {
119
+ this.history = [];
120
+ this.redoStack = [];
121
+ // Intentionally leaves this.batch untouched. If a batch is open,
122
+ // subsequent records will still accumulate until endBatch() is called.
123
+ // Callers that want to abort an open batch should call endBatch() first.
124
+ }
125
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Pure clipboard helpers shared across React, Vue, Angular, and JS.
3
+ * No framework dependencies — operates on plain values and produces strings.
4
+ */
5
+ import type { IColumnDef } from '../types/columnTypes';
6
+ import type { ISelectionRange } from '../types/dataGridTypes';
7
+ /**
8
+ * Format a single cell value for inclusion in a TSV clipboard string.
9
+ * Strips tabs and newlines so they don't corrupt the TSV structure.
10
+ *
11
+ * @param raw Raw cell value (from getCellValue).
12
+ * @param formatted Formatted value (from valueFormatter, if present).
13
+ * @returns TSV-safe string representation of the cell.
14
+ */
15
+ export declare function formatCellValueForTsv(raw: unknown, formatted: unknown): string;
16
+ /**
17
+ * Serialize a rectangular cell range to a TSV (tab-separated values) string
18
+ * suitable for writing to the clipboard.
19
+ *
20
+ * @param items Flat array of all row data objects.
21
+ * @param visibleCols Visible column definitions.
22
+ * @param range The selection range to serialize (will be normalized).
23
+ * @returns TSV string with rows separated by \\r\\n and columns by \\t.
24
+ */
25
+ export declare function formatSelectionAsTsv<T>(items: T[], visibleCols: IColumnDef<T>[], range: ISelectionRange): string;
26
+ /**
27
+ * Parse a TSV clipboard string into a 2D array of cell strings.
28
+ * Handles both \\r\\n and \\n line endings. Ignores trailing empty lines.
29
+ *
30
+ * @param text Raw clipboard text (TSV format).
31
+ * @returns 2D array: rows of cells. Empty if text is blank.
32
+ */
33
+ export declare function parseTsvClipboard(text: string): string[][];
@@ -29,3 +29,7 @@ export type { OverlayRect } from './dom';
29
29
  export { computeNextSortState } from './sortHelpers';
30
30
  export type { ISortState } from './sortHelpers';
31
31
  export { measureColumnContentWidth, AUTOSIZE_EXTRA_PX, AUTOSIZE_MAX_PX } from './columnAutosize';
32
+ export { findCtrlArrowTarget, computeTabNavigation } from './keyboardNavigation';
33
+ export { rangesEqual, clampSelectionToBounds, computeAutoScrollSpeed } from './selectionHelpers';
34
+ export { formatCellValueForTsv, formatSelectionAsTsv, parseTsvClipboard, } from './clipboardHelpers';
35
+ export { UndoRedoStack } from './undoRedoStack';
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Pure keyboard navigation helpers shared across React, Vue, Angular, and JS.
3
+ * No framework dependencies — takes plain values, returns plain values.
4
+ */
5
+ /**
6
+ * Excel-style Ctrl+Arrow: find the target position along a 1D axis.
7
+ * - Non-empty current + non-empty next → scan through non-empties, stop at last before empty/edge.
8
+ * - Otherwise → skip empties, land on next non-empty or edge.
9
+ *
10
+ * @param pos Current position (row or column index).
11
+ * @param edge The boundary position (0 for backward, max for forward).
12
+ * @param step Direction: +1 (forward) or -1 (backward).
13
+ * @param isEmpty Predicate: returns true if the cell at this index is empty.
14
+ * @returns The target position after the jump.
15
+ */
16
+ export declare function findCtrlArrowTarget(pos: number, edge: number, step: number, isEmpty: (i: number) => boolean): number;
17
+ /**
18
+ * Compute the new Tab navigation position given the current position and direction.
19
+ *
20
+ * @param rowIndex Current row index.
21
+ * @param columnIndex Current absolute column index (includes checkbox offset).
22
+ * @param maxRowIndex Maximum row index (items.length - 1).
23
+ * @param maxColIndex Maximum absolute column index.
24
+ * @param colOffset Number of non-data leading columns (checkbox column offset).
25
+ * @param shiftKey True if Shift is held (backward tab).
26
+ * @returns New { rowIndex, columnIndex } after tab.
27
+ */
28
+ export declare function computeTabNavigation(rowIndex: number, columnIndex: number, maxRowIndex: number, maxColIndex: number, colOffset: number, shiftKey: boolean): {
29
+ rowIndex: number;
30
+ columnIndex: number;
31
+ };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Pure selection helpers shared across React, Vue, Angular, and JS.
3
+ * No framework dependencies — operates only on plain ISelectionRange values.
4
+ */
5
+ import type { ISelectionRange } from '../types/dataGridTypes';
6
+ /**
7
+ * Compare two selection ranges by value (deep equality).
8
+ * Returns true if both ranges are equal, including when both are null.
9
+ */
10
+ export declare function rangesEqual(a: ISelectionRange | null, b: ISelectionRange | null): boolean;
11
+ /**
12
+ * Clamp a selection range to the grid bounds (0-based, inclusive).
13
+ *
14
+ * @param range The selection range to clamp.
15
+ * @param maxRow Maximum valid row index (items.length - 1).
16
+ * @param maxCol Maximum valid column index (visibleCols.length - 1).
17
+ * @returns The clamped range, or null if the grid is empty.
18
+ */
19
+ export declare function clampSelectionToBounds(range: ISelectionRange, maxRow: number, maxCol: number): ISelectionRange | null;
20
+ /**
21
+ * Auto-scroll speed: proportional to how far past the scroll edge the pointer is.
22
+ * Used by drag-selection auto-scroll in both React and Vue.
23
+ *
24
+ * @param distance Distance past the edge threshold (pixels).
25
+ * @param edgePx Scroll edge threshold in pixels (default: 40).
26
+ * @param minSpeed Minimum scroll speed (default: 2).
27
+ * @param maxSpeed Maximum scroll speed (default: 20).
28
+ * @returns Scroll speed in pixels per interval tick.
29
+ */
30
+ export declare function computeAutoScrollSpeed(distance: number, edgePx?: number, minSpeed?: number, maxSpeed?: number): number;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Pure undo/redo stack data structure shared across React, Vue, Angular, and JS.
3
+ * No framework dependencies — all state is plain arrays.
4
+ *
5
+ * Usage:
6
+ * const stack = new UndoRedoStack<MyEvent>(100);
7
+ * stack.push([event]); // single event
8
+ * stack.beginBatch();
9
+ * stack.push([event1]);
10
+ * stack.push([event2]);
11
+ * stack.endBatch(); // event1 + event2 are one undo step
12
+ * const batch = stack.undo(); // returns the batch to reverse
13
+ * const batch = stack.redo(); // returns the batch to re-apply
14
+ */
15
+ export declare class UndoRedoStack<T> {
16
+ private history;
17
+ private redoStack;
18
+ private batch;
19
+ readonly maxDepth: number;
20
+ constructor(maxDepth?: number);
21
+ /** Whether there are undo steps available. */
22
+ get canUndo(): boolean;
23
+ /** Whether there are redo steps available. */
24
+ get canRedo(): boolean;
25
+ /** Number of history entries. */
26
+ get historyLength(): number;
27
+ /** Number of redo entries. */
28
+ get redoLength(): number;
29
+ /** Whether a batch is currently open. */
30
+ get isBatching(): boolean;
31
+ /**
32
+ * Record a group of events as a single undoable step.
33
+ * If a batch is open, accumulates into the batch instead.
34
+ * Clears the redo stack on any new entry.
35
+ */
36
+ push(events: T[]): void;
37
+ /**
38
+ * Record a single event as a step (shorthand for push([event])).
39
+ * If a batch is open, accumulates into the batch instead.
40
+ */
41
+ record(event: T): void;
42
+ /**
43
+ * Start a batch — subsequent record/push calls accumulate into one undo step.
44
+ * Has no effect if a batch is already open.
45
+ */
46
+ beginBatch(): void;
47
+ /**
48
+ * End a batch — commits all accumulated events as one undo step.
49
+ * Has no effect if no batch is open or if the batch is empty.
50
+ */
51
+ endBatch(): void;
52
+ /**
53
+ * Pop the most recent history entry for undo.
54
+ * Returns the batch of events (in original order) to be reversed by the caller,
55
+ * or null if there is nothing to undo.
56
+ *
57
+ * The caller is responsible for applying the events in reverse order.
58
+ */
59
+ undo(): T[] | null;
60
+ /**
61
+ * Pop the most recent redo entry.
62
+ * Returns the batch of events (in original order) to be re-applied by the caller,
63
+ * or null if there is nothing to redo.
64
+ */
65
+ redo(): T[] | null;
66
+ /**
67
+ * Clear all history and redo state.
68
+ * Does not affect any open batch — call endBatch() first if needed.
69
+ */
70
+ clear(): void;
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-core",
3
- "version": "2.0.14",
3
+ "version": "2.0.16",
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",