@extable/core 0.1.0
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/README.md +77 -0
- package/dist/address.d.ts +9 -0
- package/dist/cellValueCodec.d.ts +4 -0
- package/dist/commandQueue.d.ts +20 -0
- package/dist/dataModel.d.ts +127 -0
- package/dist/dateUtils.d.ts +5 -0
- package/dist/fillHandle.d.ts +17 -0
- package/dist/findReplace.d.ts +53 -0
- package/dist/geometry.d.ts +5 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +177 -0
- package/dist/index.js +4445 -0
- package/dist/index.js.map +1 -0
- package/dist/lockManager.d.ts +15 -0
- package/dist/renderers.d.ts +129 -0
- package/dist/selectionManager.d.ts +135 -0
- package/dist/styleResolver.d.ts +10 -0
- package/dist/types.d.ts +300 -0
- package/dist/utils.d.ts +3 -0
- package/dist/validation.d.ts +2 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @extable/core
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Excel-like HTML table component with a fixed column schema and built-in multi-user editing support.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Excel Familiarity** - Direct cell editing, fill handle, copy & paste, built-in filtering & sorting
|
|
10
|
+
- **Performance** - HTML5 Canvas rendering for thousands of rows, with HTML table mode for testing
|
|
11
|
+
- **Schema-Driven** - Centralized column definitions with validation, formatting, and formulas
|
|
12
|
+
- **Edit Modes** - View-only, direct edit, or commit-based workflows
|
|
13
|
+
- **Collaboration-Ready** - Row-level locking and command-stream architecture for multi-user support
|
|
14
|
+
- **Framework-Agnostic** - Core library works with Vanilla JS, React, Vue, and other frameworks
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @extable/core
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Basic Usage
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { ExtableCore } from "@extable/core";
|
|
28
|
+
import "@extable/core/style.css";
|
|
29
|
+
|
|
30
|
+
const schema = {
|
|
31
|
+
columns: [
|
|
32
|
+
{ key: "id", header: "ID", type: "string", readonly: true },
|
|
33
|
+
{ key: "name", header: "Name", type: "string" },
|
|
34
|
+
{ key: "active", header: "Active", type: "boolean" },
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const data = [
|
|
39
|
+
{ id: "1", name: "Alice", active: true },
|
|
40
|
+
{ id: "2", name: "Bob", active: false },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const table = new ExtableCore({
|
|
44
|
+
root: document.getElementById("table"),
|
|
45
|
+
schema,
|
|
46
|
+
defaultData: data,
|
|
47
|
+
defaultView: {},
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Documentation
|
|
52
|
+
|
|
53
|
+
For comprehensive guides and examples, visit the [Extable Documentation](https://shibukawa.github.io/extable/).
|
|
54
|
+
|
|
55
|
+
### Key Topics
|
|
56
|
+
|
|
57
|
+
- [Concepts](https://shibukawa.github.io/extable/concepts/) - Design philosophy and architecture
|
|
58
|
+
- [Integration Guide](https://shibukawa.github.io/extable/guides/integration) - Setup and configuration
|
|
59
|
+
- [Data Format](https://shibukawa.github.io/extable/guides/data-format) - Supported column types and validation
|
|
60
|
+
- [Edit Modes](https://shibukawa.github.io/extable/guides/editmode) - View-only, direct, and commit modes
|
|
61
|
+
- [Core API Reference](https://shibukawa.github.io/extable/reference/core) - Complete API documentation
|
|
62
|
+
- [Demos](https://shibukawa.github.io/extable/demos/) - Interactive examples
|
|
63
|
+
|
|
64
|
+
## Framework Wrappers
|
|
65
|
+
|
|
66
|
+
Use Extable with your favorite framework:
|
|
67
|
+
|
|
68
|
+
- **React**: [@extable/react](https://www.npmjs.com/package/@extable/react)
|
|
69
|
+
- **Vue**: [@extable/vue](https://www.npmjs.com/package/@extable/vue)
|
|
70
|
+
|
|
71
|
+
## Repository
|
|
72
|
+
|
|
73
|
+
GitHub: https://github.com/shibukawa/extable
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
Apache License 2.0 - See LICENSE for details.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DataModel } from "./dataModel";
|
|
2
|
+
import type { CellAddress } from "./types";
|
|
3
|
+
export type ResolvedCellTarget = {
|
|
4
|
+
rowId: string;
|
|
5
|
+
colKey: string;
|
|
6
|
+
rowIndex: number;
|
|
7
|
+
colIndex: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function resolveCellAddress(dataModel: DataModel, address: CellAddress): ResolvedCellTarget | null;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ColumnSchema } from "./types";
|
|
2
|
+
export declare function safeParseDate(value: string): Date | null;
|
|
3
|
+
export declare function safeParseTime(value: string): Date | null;
|
|
4
|
+
export declare function toRawValue(raw: unknown, value: unknown, col: ColumnSchema): string | null;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Command } from "./types";
|
|
2
|
+
export type CommandGroupSnapshot = {
|
|
3
|
+
batchId: string | null;
|
|
4
|
+
commands: Command[];
|
|
5
|
+
};
|
|
6
|
+
export declare class CommandQueue {
|
|
7
|
+
private applied;
|
|
8
|
+
private undone;
|
|
9
|
+
private cap;
|
|
10
|
+
constructor(cap?: number);
|
|
11
|
+
enqueue(command: Command): void;
|
|
12
|
+
canUndo(): boolean;
|
|
13
|
+
canRedo(): boolean;
|
|
14
|
+
listApplied(): Command[];
|
|
15
|
+
listUndoGroups(): CommandGroupSnapshot[];
|
|
16
|
+
listRedoGroups(): CommandGroupSnapshot[];
|
|
17
|
+
undo(): Command[] | null;
|
|
18
|
+
redo(): Command[] | null;
|
|
19
|
+
clear(): void;
|
|
20
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { CellDiagnostic, ColumnSchema, ConditionalStyleFn, InternalRow, RowObject, Schema, StyleDelta, View } from "./types";
|
|
2
|
+
export declare class DataModel {
|
|
3
|
+
private schema;
|
|
4
|
+
private view;
|
|
5
|
+
private rows;
|
|
6
|
+
private baseIndexById;
|
|
7
|
+
private dataVersion;
|
|
8
|
+
private visibleRowsCache;
|
|
9
|
+
private distinctValueCache;
|
|
10
|
+
private pending;
|
|
11
|
+
private rowVersion;
|
|
12
|
+
private listeners;
|
|
13
|
+
private cellStyles;
|
|
14
|
+
private cellConditionalStyles;
|
|
15
|
+
private computedCache;
|
|
16
|
+
private conditionalCache;
|
|
17
|
+
private rowConditionalCache;
|
|
18
|
+
private formulaDiagnostics;
|
|
19
|
+
private conditionalDiagnostics;
|
|
20
|
+
private baseValidationErrors;
|
|
21
|
+
private uniqueValidationErrors;
|
|
22
|
+
private notifySuspended;
|
|
23
|
+
private notifyDirty;
|
|
24
|
+
constructor(dataset: RowObject[] | undefined, schema: Schema<any>, view: View);
|
|
25
|
+
subscribe(listener: () => void): () => boolean;
|
|
26
|
+
private notify;
|
|
27
|
+
batchUpdate(run: () => void): void;
|
|
28
|
+
setData(dataset: RowObject[] | undefined): void;
|
|
29
|
+
setSchema(schema: Schema<any>): void;
|
|
30
|
+
setView(view: View): void;
|
|
31
|
+
getSchema(): Schema<any>;
|
|
32
|
+
getColumns(): ColumnSchema<any>[];
|
|
33
|
+
getRowConditionalStyleFn(): ConditionalStyleFn | null;
|
|
34
|
+
getView(): View;
|
|
35
|
+
getDataVersion(): number;
|
|
36
|
+
getFullSchema(): Schema<any>;
|
|
37
|
+
listRows(): InternalRow[];
|
|
38
|
+
listAllRows(): InternalRow[];
|
|
39
|
+
getAllRowCount(): number;
|
|
40
|
+
private findRow;
|
|
41
|
+
getRowHeight(rowId: string): number | undefined;
|
|
42
|
+
setRowHeight(rowId: string, height: number): void;
|
|
43
|
+
setRowHeightsBulk(next: Record<string, number>): void;
|
|
44
|
+
getCell(rowId: string, key: string): unknown;
|
|
45
|
+
getRawCell(rowId: string, key: string): unknown;
|
|
46
|
+
isRowReadonly(rowId: string): boolean;
|
|
47
|
+
isColumnReadonly(colKey: string): boolean;
|
|
48
|
+
isReadonly(rowId: string, colKey: string): boolean;
|
|
49
|
+
setCell(rowId: string, key: string, value: unknown, committed: boolean): void;
|
|
50
|
+
applyPending(rowId: string): void;
|
|
51
|
+
clearPending(rowId: string): void;
|
|
52
|
+
getPending(): Map<string, Record<string, unknown>>;
|
|
53
|
+
hasPending(rowId: string, key: string): boolean;
|
|
54
|
+
insertRow(rowData: InternalRow["raw"]): string;
|
|
55
|
+
insertRowAt(rowData: InternalRow["raw"], index: number, forcedId?: string): string;
|
|
56
|
+
removeRow(rowId: string): {
|
|
57
|
+
row: InternalRow;
|
|
58
|
+
index: number;
|
|
59
|
+
} | null;
|
|
60
|
+
getDisplayIndex(rowId: string): number | null;
|
|
61
|
+
getRowIndex(rowId: string): number;
|
|
62
|
+
getBaseRowIndex(rowId: string): number;
|
|
63
|
+
getColumnIndex(colKey: string): number;
|
|
64
|
+
getColumnByIndex(colIndex: number): ColumnSchema<any, any, import("./types").ColumnType, string>;
|
|
65
|
+
getRowByIndex(rowIndex: number): InternalRow;
|
|
66
|
+
private rebuildBaseIndex;
|
|
67
|
+
private getFilterSortKey;
|
|
68
|
+
private stableValueKey;
|
|
69
|
+
private isBlankValue;
|
|
70
|
+
private resolveFilterColumnKey;
|
|
71
|
+
private getFilterCellValue;
|
|
72
|
+
private valuesFilterMatches;
|
|
73
|
+
private rowPassesColumnDiagnostics;
|
|
74
|
+
private computeRowsAfterFilter;
|
|
75
|
+
private computeVisibleRows;
|
|
76
|
+
getDistinctValuesForColumn(colKey: string): {
|
|
77
|
+
version: number;
|
|
78
|
+
values: Array<{
|
|
79
|
+
value: unknown;
|
|
80
|
+
label: string;
|
|
81
|
+
}>;
|
|
82
|
+
hasBlanks: boolean;
|
|
83
|
+
total: number;
|
|
84
|
+
};
|
|
85
|
+
private cellStyleKey;
|
|
86
|
+
private clearDiagnosticsForCell;
|
|
87
|
+
getCellDiagnostic(rowId: string, colKey: string): CellDiagnostic | null;
|
|
88
|
+
getDiagnostics(): {
|
|
89
|
+
rowId: string;
|
|
90
|
+
colKey: string;
|
|
91
|
+
diag: CellDiagnostic | null;
|
|
92
|
+
}[];
|
|
93
|
+
private getRowObjectEffective;
|
|
94
|
+
resolveCellValue(rowId: string, col: ColumnSchema): {
|
|
95
|
+
value: unknown;
|
|
96
|
+
textOverride?: string;
|
|
97
|
+
diagnostic: CellDiagnostic | null;
|
|
98
|
+
};
|
|
99
|
+
setCellConditionalStyle(rowId: string, colKey: string, fn: ConditionalStyleFn | null): void;
|
|
100
|
+
private evalConditionalStyleFn;
|
|
101
|
+
private resolveRowConditionalStyle;
|
|
102
|
+
resolveConditionalStyle(rowId: string, col: ColumnSchema): {
|
|
103
|
+
delta: StyleDelta | null;
|
|
104
|
+
diagnostic: CellDiagnostic | null;
|
|
105
|
+
forceErrorText: boolean;
|
|
106
|
+
};
|
|
107
|
+
getValidationErrors(): {
|
|
108
|
+
rowId: string;
|
|
109
|
+
colKey: string;
|
|
110
|
+
message: string;
|
|
111
|
+
}[];
|
|
112
|
+
getCellValidationMessage(rowId: string, colKey: string): string | null;
|
|
113
|
+
getCellMarker(rowId: string, colKey: string): {
|
|
114
|
+
level: "warning" | "error";
|
|
115
|
+
message: string;
|
|
116
|
+
} | null;
|
|
117
|
+
private updateValidationForCell;
|
|
118
|
+
private recomputeValidationErrors;
|
|
119
|
+
private normalizeUniqueValue;
|
|
120
|
+
private recomputeUniqueValidationForColumn;
|
|
121
|
+
getCellStyle(rowId: string, colKey: string): StyleDelta | null;
|
|
122
|
+
setCellStyle(rowId: string, colKey: string, style: StyleDelta | null): void;
|
|
123
|
+
updateColumnStyle(colKey: string, updater: ColumnSchema["style"] | ((oldValue: ColumnSchema["style"] | undefined) => ColumnSchema["style"] | undefined)): void;
|
|
124
|
+
private isEqual;
|
|
125
|
+
getRowVersion(rowId: string): number;
|
|
126
|
+
private reindexRows;
|
|
127
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
type DatePatternKind = "date" | "time" | "datetime";
|
|
2
|
+
export declare function parseIsoDate(value: string): Date | null;
|
|
3
|
+
export declare function formatDateLite(date: Date, pattern: string): string;
|
|
4
|
+
export declare function coerceDatePattern(pattern: string | undefined, kind: DatePatternKind): string;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DataModel } from "./dataModel";
|
|
2
|
+
import type { EditMode, SelectionRange } from "./types";
|
|
3
|
+
export type FillHandleMode = 'copy' | 'sequence';
|
|
4
|
+
export interface FillHandleSource {
|
|
5
|
+
colKey: string;
|
|
6
|
+
colIndex: number;
|
|
7
|
+
startRowIndex: number;
|
|
8
|
+
endRowIndex: number;
|
|
9
|
+
mode: FillHandleMode;
|
|
10
|
+
}
|
|
11
|
+
export declare function getFillHandleSource(dataModel: DataModel, ranges: SelectionRange[]): FillHandleSource | null;
|
|
12
|
+
export declare function makeFillValueGetter(dataModel: DataModel, source: FillHandleSource): ((offsetFromEnd: number) => unknown) | null;
|
|
13
|
+
export declare const FILL_HANDLE_VISUAL_SIZE_PX = 12;
|
|
14
|
+
export declare const FILL_HANDLE_HIT_SIZE_PX = 14;
|
|
15
|
+
export declare function getFillHandleRect(cellRect: DOMRect, size?: number): DOMRect;
|
|
16
|
+
export declare function isPointInRect(x: number, y: number, rect: DOMRect): boolean;
|
|
17
|
+
export declare function shouldShowFillHandle(dataModel: DataModel, ranges: SelectionRange[], activeRowId: string | null, activeColKey: string | null, editMode: EditMode): boolean;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { DataModel } from "./dataModel";
|
|
2
|
+
export type FindReplaceMode = "find" | "replace";
|
|
3
|
+
export interface FindReplaceOptions {
|
|
4
|
+
caseInsensitive?: boolean;
|
|
5
|
+
regex?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface FindReplaceMatch {
|
|
8
|
+
rowId: string;
|
|
9
|
+
colKey: string;
|
|
10
|
+
rowIndex: number;
|
|
11
|
+
colIndex: number;
|
|
12
|
+
start: number;
|
|
13
|
+
end: number;
|
|
14
|
+
text: string;
|
|
15
|
+
}
|
|
16
|
+
export interface FindReplaceState {
|
|
17
|
+
query: string;
|
|
18
|
+
replace: string;
|
|
19
|
+
mode: FindReplaceMode;
|
|
20
|
+
options: Required<FindReplaceOptions>;
|
|
21
|
+
matches: FindReplaceMatch[];
|
|
22
|
+
activeIndex: number;
|
|
23
|
+
error: string | null;
|
|
24
|
+
}
|
|
25
|
+
export type FindReplaceListener = (state: FindReplaceState) => void;
|
|
26
|
+
export declare class FindReplaceController {
|
|
27
|
+
private dataModel;
|
|
28
|
+
private navigateToCell;
|
|
29
|
+
private applyEdit;
|
|
30
|
+
private canEdit;
|
|
31
|
+
private state;
|
|
32
|
+
private listeners;
|
|
33
|
+
private debounceId;
|
|
34
|
+
private unsubscribeData;
|
|
35
|
+
constructor(dataModel: DataModel, navigateToCell: (rowId: string, colKey: string) => void, applyEdit: (rowId: string, colKey: string, next: unknown) => void, canEdit: (rowId: string, colKey: string) => boolean);
|
|
36
|
+
destroy(): void;
|
|
37
|
+
getState(): FindReplaceState;
|
|
38
|
+
subscribe(listener: FindReplaceListener): () => boolean;
|
|
39
|
+
setMode(mode: FindReplaceMode): void;
|
|
40
|
+
setQuery(query: string): void;
|
|
41
|
+
setReplace(replace: string): void;
|
|
42
|
+
setOptions(next: FindReplaceOptions): void;
|
|
43
|
+
next(): void;
|
|
44
|
+
prev(): void;
|
|
45
|
+
activateIndex(index: number): void;
|
|
46
|
+
replaceCurrent(): void;
|
|
47
|
+
replaceAll(): void;
|
|
48
|
+
private applyReplacement;
|
|
49
|
+
private applyReplacementAllInCell;
|
|
50
|
+
private scheduleRecompute;
|
|
51
|
+
recompute(): void;
|
|
52
|
+
private setState;
|
|
53
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Schema, View } from "./types";
|
|
2
|
+
export declare const HEADER_HEIGHT_PX = 24;
|
|
3
|
+
export declare const ROW_HEADER_WIDTH_PX = 48;
|
|
4
|
+
export declare const DEFAULT_ROW_HEIGHT_PX = 24;
|
|
5
|
+
export declare function getColumnWidths(schema: Schema, view: View, fallbackWidth?: number): number[];
|
package/dist/index.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.extable-root{--extable-border: 1px solid #dadce0;--extable-accent: #3b82f6;--extable-invalid: #ef4444;--extable-header-bg: #e5e7eb;border:1px solid #d0d7de;box-sizing:border-box}.extable-root .extable-shell{display:flex;width:100%;height:100%;min-width:0;min-height:0;position:relative;font-family:Inter,Segoe UI,system-ui,-apple-system,Helvetica Neue,sans-serif;overflow:visible}.extable-root .extable-viewport{flex:1;flex-basis:0;min-width:0;min-height:0;overflow:auto;position:relative;overscroll-behavior:contain;background:#f8fafc;border:none;box-sizing:border-box}.extable-root .extable-overlay-layer{position:sticky;top:0;left:0;width:0;height:0;pointer-events:none;z-index:20}.extable-root.extable-loading .extable-shell:after{content:"";position:absolute;inset:0;background:#ffffffa6;-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px);z-index:20}.extable-root.extable-loading .extable-shell:before{content:"";position:absolute;left:50%;top:50%;width:28px;height:28px;margin-left:-14px;margin-top:-14px;border-radius:9999px;border:3px solid rgba(148,163,184,.5);border-top-color:#3b82f6e6;animation:extable-spin .9s linear infinite;z-index:21}.extable-root table[data-extable-renderer=html],.extable-root table[data-extable-renderer=canvas-html-overlay]{border-collapse:collapse;width:100%;background:transparent;table-layout:fixed;font-size:14px;line-height:16px}:is(.extable-root table[data-extable-renderer=html],.extable-root table[data-extable-renderer=canvas-html-overlay]) thead th{position:sticky;top:0;z-index:5}:is(.extable-root table[data-extable-renderer=html],.extable-root table[data-extable-renderer=canvas-html-overlay]) th{position:relative;background:var(--extable-header-bg);border:1px solid #d0d7de;padding:4px 8px;font-weight:700;text-align:left}:is(.extable-root table[data-extable-renderer=html],.extable-root table[data-extable-renderer=canvas-html-overlay]) thead th:not(.extable-row-header):after{content:"";position:absolute;top:0;right:-3px;width:6px;height:100%;cursor:col-resize}:is(.extable-root table[data-extable-renderer=html],.extable-root table[data-extable-renderer=canvas-html-overlay]) td{border:1px solid #d0d7de;padding:6px 8px;min-width:80px;cursor:cell;font-weight:400}:is(.extable-root table[data-extable-renderer=html],.extable-root table[data-extable-renderer=canvas-html-overlay]) td:focus-within{outline:2px solid #2b7fff;outline-offset:-1px}:is(.extable-root table[data-extable-renderer=html],.extable-root table[data-extable-renderer=canvas-html-overlay]) td.pending{color:#b91c1c}:is(.extable-root table[data-extable-renderer=html],.extable-root table[data-extable-renderer=canvas-html-overlay]) input{border:none;padding:2px 4px;width:100%;box-sizing:border-box;background:#fff;font:inherit}:is(.extable-root table[data-extable-renderer=html],.extable-root table[data-extable-renderer=canvas-html-overlay]) input:focus{outline:none}.extable-root table[data-extable-renderer=canvas-html-overlay]{opacity:0}.extable-root .extable-cell{border:var(--extable-border);padding:4px 8px;position:relative}.extable-root .extable-cell.invalid{outline:1px solid var(--extable-invalid)}.extable-root .extable-diag-warning:before,.extable-root .extable-diag-error:before{content:"";position:absolute;top:0;right:0;width:10px;height:10px;background:linear-gradient(225deg,#f59e0b 50%,transparent 50%)}.extable-root .extable-diag-error:before{background:linear-gradient(225deg,#ef4444 50%,transparent 50%)}.extable-root .extable-row-header{position:sticky;left:0;background:var(--extable-header-bg);text-align:center;vertical-align:middle;line-height:24px;padding:0;border:var(--extable-border);font-weight:600;color:#334155;z-index:4;cursor:default}.extable-root .extable-corner{position:sticky;top:0;left:0;z-index:6;background:var(--extable-header-bg);padding:0}.extable-root .extable-corner:after{content:"";position:absolute;top:4px;left:4px;width:12px;height:12px;background:linear-gradient(135deg,#9ca3af 50%,transparent 50%)}.extable-root .extable-active-row-header,.extable-root .extable-active-col-header{background:#3b82f629}.extable-root .extable-selected{background:#3b82f61f!important}.extable-root td.extable-selected{cursor:cell}.extable-root td.extable-active-cell.extable-editable{cursor:text}.extable-root td.extable-active-cell.extable-readonly,.extable-root td.extable-active-cell.extable-boolean{cursor:default}.extable-root .extable-all-selected td,.extable-root .extable-all-selected th{background:#3b82f614!important}.extable-root .extable-active-cell{outline:2px solid var(--extable-accent);outline-offset:-2px;position:relative}.extable-root .extable-active-cell:after{content:"";position:absolute;width:12px;height:12px;right:1px;bottom:1px;background:var(--extable-accent);border:1px solid #ffffff;cursor:crosshair;opacity:0}.extable-root[data-extable-fill-handle="1"] .extable-active-cell:after{opacity:1}.extable-root .extable-readonly-muted{background:#f3f4f6!important;color:#94a3b8!important}.extable-root td.extable-selected.extable-readonly-muted{background:#3b82f61f!important}.extable-root .extable-all-selected td.extable-readonly-muted,.extable-root .extable-all-selected th.extable-readonly-muted{background:#3b82f614!important}.extable-root.extable-readonly-all .extable-readonly-muted{background:inherit!important;color:inherit!important}.extable-root.extable-readonly-all table[data-extable-renderer=html] tbody td,.extable-root.extable-readonly-all table[data-extable-renderer=canvas-html-overlay] tbody td{cursor:cell}.extable-root.extable-readonly-all td.extable-active-cell{cursor:cell!important}.extable-root.extable-readonly-all .extable-active-cell:after{cursor:cell}.extable-root .extable-col-header{display:flex;align-items:center;justify-content:space-between;gap:8px;min-width:0;cursor:default}.extable-root .extable-col-header-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.extable-root .extable-filter-sort-trigger{flex:0 0 auto;width:22px;height:22px;padding:0;border:1px solid rgba(148,163,184,.55);border-radius:6px;background:#ffffffb3;cursor:pointer;opacity:0;display:inline-flex;align-items:center;justify-content:center;line-height:1;color:#0f172abf}.extable-root .extable-filter-sort-trigger:hover,.extable-root .extable-filter-sort-trigger:focus-visible{opacity:1;outline:none;background:#ffffffeb;border-color:#3b82f6b3;color:#0f172aeb}.extable-root th:hover .extable-filter-sort-trigger{opacity:.55}.extable-root th[data-extable-fs-active="1"] .extable-filter-sort-trigger,.extable-root th[data-extable-sort-dir] .extable-filter-sort-trigger{opacity:.75}.extable-root th:hover[data-extable-fs-active="1"] .extable-filter-sort-trigger,.extable-root th:hover[data-extable-sort-dir] .extable-filter-sort-trigger{opacity:1}.extable-root .cell-nowrap{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.extable-root .cell-wrap{white-space:pre-wrap;text-overflow:clip;overflow-wrap:anywhere}.extable-root .align-left{text-align:left}.extable-root .align-right{text-align:right}.extable-root td[data-extable-diag-message]:hover:after{content:attr(data-extable-diag-message);position:absolute;top:6px;left:calc(100% + 10px);max-width:360px;padding:8px 10px;border-radius:10px;background:#111827;color:#f8fafc;font-size:12px;line-height:1.25;box-shadow:0 12px 28px #00000047;border:1px solid rgba(148,163,184,.35);white-space:pre-wrap;z-index:20}.extable-root td[data-extable-diag-message]:hover:before{content:"";position:absolute;top:14px;left:calc(100% + 2px);width:0;height:0;border:7px solid transparent;border-right-color:#111827;z-index:21}.extable-root .extable-context-menu{border:none;padding:6px 0;border-radius:8px;background:#fffffffa;box-shadow:0 18px 38px #00000040,0 8px 12px #0000002e;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);min-width:200px;position:fixed;margin:0;inset:auto;z-index:1000;pointer-events:auto}.extable-root .extable-context-menu::backdrop{background:transparent}.extable-root .extable-context-menu hr.extable-context-sep{border:0;border-top:1px solid #e5e7eb;margin:6px 0}.extable-root .extable-context-menu button{display:flex;gap:8px;align-items:center;width:100%;padding:8px 14px;background:transparent;border:none;font:inherit;text-align:left;cursor:pointer}.extable-root .extable-context-menu button:hover,.extable-root .extable-context-menu button:focus-visible{background:#3b82f61f;outline:none}.extable-root .extable-search-sidebar,.extable-root .extable-filter-sort-sidebar{flex:0 0 0px;width:0px;min-width:0;border-left:0px solid #e5e7eb;background:#fffffffa;border:1px solid rgba(148,163,184,.45);backdrop-filter:none;-webkit-backdrop-filter:none;box-shadow:none;display:flex;flex-direction:column;gap:10px;padding:0;transform:translate(12px);opacity:0;pointer-events:none;transition:flex-basis .18s ease-out,width .18s ease-out,padding .18s ease-out,border-left-width .18s ease-out,opacity .18s ease-out,transform .18s ease-out}.extable-root.extable-search-open .extable-search-sidebar,.extable-root.extable-filter-sort-open .extable-filter-sort-sidebar{flex-basis:440px;width:440px;padding:12px;border-left-width:1px;opacity:1;transform:translate(0);pointer-events:auto}.extable-root .extable-search-sidebar input,.extable-root .extable-search-sidebar button{border-radius:8px;border:1px solid rgba(148,163,184,.7);background:#fff;font-family:inherit;font-size:.85rem}.extable-root .extable-search-sidebar input[type=checkbox],.extable-root .extable-filter-sort-sidebar input[type=checkbox]{width:16px;height:16px;accent-color:#0f172a;vertical-align:middle}.extable-root .extable-search-sidebar input{padding:6px 8px}.extable-root .extable-search-sidebar button{padding:6px 10px;cursor:pointer}@keyframes extable-spin{to{transform:rotate(360deg)}}.extable-root .extable-filter-sort-header{display:flex;flex-direction:column;gap:8px}.extable-root .extable-filter-sort-row{display:flex;align-items:center;gap:10px;justify-content:space-between;flex-wrap:wrap}.extable-root .extable-filter-sort-title{font-weight:700;color:#0f172a}.extable-root .extable-filter-sort-close{width:28px;height:28px;border-radius:8px;border:1px solid rgba(148,163,184,.5);background:#fffc;cursor:pointer}.extable-root .extable-filter-sort-body{display:flex;flex-direction:column;gap:12px;min-height:0;flex:1;overflow:hidden;border:1px solid rgba(148,163,184,.35);border-radius:0;padding:10px;background:#f8fafcb3}.extable-root .extable-filter-sort-section{display:flex;flex-direction:column;gap:8px;border:1px solid rgba(148,163,184,.25);border-radius:10px;padding:10px;background:#fff}.extable-root .extable-filter-sort-section-filter{flex:1;min-height:0}.extable-root .extable-filter-sort-section-sort{flex:0 0 auto}.extable-root .extable-filter-sort-section-title{font-weight:700;color:#0f172a}.extable-root .extable-filter-sort-values{min-height:0;flex:1;overflow:auto;border:1px solid rgba(148,163,184,.35);border-radius:10px;padding:8px;background:#ffffffe6}.extable-root .extable-filter-sort-values label{display:grid;grid-template-columns:26px 1fr;gap:8px;align-items:center;padding:6px;border-radius:8px}.extable-root .extable-filter-sort-values label:hover{background:#3b82f614}.extable-root .extable-filter-sort-actions{display:flex;gap:8px;flex-wrap:nowrap}.extable-root .extable-filter-sort-actions label{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}.extable-root .extable-filter-sort-actions[data-align=split]{justify-content:space-between}.extable-root .extable-filter-sort-actions[data-align=split] button[data-extable-fs=select-none]{margin-right:auto}.extable-root .extable-filter-sort-actions[data-align=right]{justify-content:flex-end}.extable-root .extable-filter-sort-actions button{padding:6px 10px;border-radius:10px;border:1px solid rgba(148,163,184,.55);background:#ffffffe6;cursor:pointer}.extable-root .extable-filter-sort-actions button:hover,.extable-root .extable-filter-sort-actions button:focus-visible{outline:none;border-color:#3b82f6b3}.extable-root .extable-filter-sort-actions button[data-extable-fs=apply-filter],.extable-root .extable-filter-sort-actions button[data-active="1"]{background:#0f172a;color:#f8fafc;border-color:#0f172a}.extable-tooltip{position:absolute;pointer-events:none;z-index:9999;display:none;max-width:360px;padding:8px 10px;border-radius:10px;background:#111827;color:#f8fafc;font-size:12px;line-height:1.25;box-shadow:0 12px 28px #00000047;border:1px solid rgba(148,163,184,.35);white-space:pre-wrap}.extable-tooltip[data-visible="1"]{display:block}.extable-tooltip:after{content:"";position:absolute;top:10px;width:0;height:0;border:7px solid transparent}.extable-tooltip[data-side=right]:after{left:-14px;border-right-color:#111827}.extable-tooltip[data-side=left]:after{right:-14px;border-left-color:#111827}.extable-root .extable-search-header{display:flex;flex-direction:column;gap:8px}.extable-root .extable-search-row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.extable-root .extable-search-row label{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}.extable-root .extable-search-label{flex:1;display:flex;gap:8px;align-items:center}.extable-root .extable-search-title{font-weight:700;color:#0f172a}.extable-root .extable-search-close{border:none;background:transparent;font-size:18px;line-height:1;padding:6px 8px;cursor:pointer}.extable-root .extable-search-status{justify-content:space-between;font-size:12px;color:#475569}.extable-root .extable-search-error{color:#b91c1c}.extable-root .extable-search-body{display:flex;flex-direction:column;gap:10px;min-height:0}.extable-root .extable-search-body label{display:inline-flex;align-items:center;gap:4px;font-size:.85rem}.extable-root .extable-search-actions{display:flex;gap:8px;flex-wrap:wrap}.extable-root .extable-search-actions button{padding:6px 10px;border-radius:8px;border:1px solid #d0d7de;background:#fff;cursor:pointer}.extable-root .extable-search-actions button:disabled{opacity:.5;cursor:default}.extable-root .extable-search-results{flex:1;min-height:0;overflow:auto;border:1px solid #e5e7eb;border-radius:10px;background:#fff;font-size:13px}.extable-root .extable-search-table{width:100%;border-collapse:collapse;font-size:13px;table-layout:fixed}.extable-root .extable-search-table th,.extable-root .extable-search-table td{border-bottom:1px solid #eef2f7;padding:6px 8px;vertical-align:middle;line-height:1.4}.extable-root .extable-search-table td{color:#1f2937}.extable-root .extable-search-table th{position:sticky;top:0;background:#fff;z-index:1;text-align:left;color:#334155;font-weight:700}.extable-root .extable-search-table th:first-child,.extable-root .extable-search-table td:first-child{width:72px;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.extable-root .extable-search-table th:nth-child(2),.extable-root .extable-search-table td:nth-child(2){width:auto;min-width:0}.extable-root .extable-search-table td:nth-child(2){white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.extable-root .extable-search-result-row{cursor:pointer}.extable-root .extable-search-result-row:hover,.extable-root .extable-search-result-row:focus-visible{outline:none;background:#3b82f614}.extable-root .extable-search-result-row[data-active="1"]{background:#3b82f61f}.extable-root .extable-search-sidebar[data-extable-fr-readonly="1"] [data-extable-fr=replace-row],.extable-root .extable-search-sidebar[data-extable-fr-readonly="1"] [data-extable-fr=replace-toggle-row],.extable-root .extable-search-sidebar[data-extable-fr-readonly="1"] [data-extable-fr-only=replace]{display:none!important}.extable-root .extable-toast{border-radius:8px;padding:10px 14px;background:#19191ceb;color:#f8fafc;box-shadow:0 12px 30px #00000047,0 6px 12px #0000002e;font-size:13px;max-width:320px;line-height:1.4}.extable-root .extable-toast[data-variant=error]{background:#dc2626eb}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import "./styles.css";
|
|
2
|
+
import { type FindReplaceMode } from "./findReplace";
|
|
3
|
+
import type { CoreOptions, InternalRow, NullableData, Schema, SelectionListener, SelectionSnapshot, TableConfig, TableState, TableStateListener, UndoRedoHistory, Updater, View, RowStateSnapshot, RowStateListener } from "./types";
|
|
4
|
+
export * from "./types";
|
|
5
|
+
export type { FindReplaceMatch, FindReplaceMode, FindReplaceOptions, FindReplaceState, } from "./findReplace";
|
|
6
|
+
export { FindReplaceController } from "./findReplace";
|
|
7
|
+
export interface CoreInit<T extends object = Record<string, unknown>> {
|
|
8
|
+
root: HTMLElement;
|
|
9
|
+
defaultData: NullableData<T>;
|
|
10
|
+
defaultView: View;
|
|
11
|
+
schema: Schema<any>;
|
|
12
|
+
options?: CoreOptions;
|
|
13
|
+
}
|
|
14
|
+
export declare class ExtableCore<T extends object = Record<string, unknown>, R extends object = T> {
|
|
15
|
+
private root;
|
|
16
|
+
private shell;
|
|
17
|
+
private viewportEl;
|
|
18
|
+
private viewportResizeObserver;
|
|
19
|
+
private dataModel;
|
|
20
|
+
private dataLoaded;
|
|
21
|
+
private loadingEnabled;
|
|
22
|
+
private commandQueue;
|
|
23
|
+
private lockManager;
|
|
24
|
+
private renderer;
|
|
25
|
+
private selectionManager;
|
|
26
|
+
private renderMode;
|
|
27
|
+
private editMode;
|
|
28
|
+
private lockMode;
|
|
29
|
+
private server?;
|
|
30
|
+
private user?;
|
|
31
|
+
private unsubscribe?;
|
|
32
|
+
private resizeHandler;
|
|
33
|
+
private scrollHandler;
|
|
34
|
+
private viewportState;
|
|
35
|
+
private rafId;
|
|
36
|
+
private contextMenu;
|
|
37
|
+
private contextMenuRowId;
|
|
38
|
+
private handleGlobalPointer;
|
|
39
|
+
private toast;
|
|
40
|
+
private toastTimer;
|
|
41
|
+
private findReplace;
|
|
42
|
+
private findReplaceSidebar;
|
|
43
|
+
private findReplaceSidebarUnsub;
|
|
44
|
+
private findReplaceEnabled;
|
|
45
|
+
private findReplaceUiEnabled;
|
|
46
|
+
private findReplaceEnableSearch;
|
|
47
|
+
private mounted;
|
|
48
|
+
private isCellReadonly;
|
|
49
|
+
private resolveRowId;
|
|
50
|
+
private filterSortSidebar;
|
|
51
|
+
private filterSortSidebarUnsub;
|
|
52
|
+
private filterSortKeydown;
|
|
53
|
+
private filterSortClickCapture;
|
|
54
|
+
private filterSortOpenEvent;
|
|
55
|
+
private filterSortActiveColumnKey;
|
|
56
|
+
private filterSortDraft;
|
|
57
|
+
private tableStateListeners;
|
|
58
|
+
private selectionListeners;
|
|
59
|
+
private lastTableState;
|
|
60
|
+
private lastSelectionSnapshot;
|
|
61
|
+
private selectionRanges;
|
|
62
|
+
private activeCell;
|
|
63
|
+
private activeErrors;
|
|
64
|
+
private rowStateListeners;
|
|
65
|
+
private lastRowStates;
|
|
66
|
+
private isSearchPanelVisible;
|
|
67
|
+
private isFilterSortPanelVisible;
|
|
68
|
+
private safeRender;
|
|
69
|
+
constructor(init: CoreInit<T>);
|
|
70
|
+
private loadInitial;
|
|
71
|
+
private applyRootDecor;
|
|
72
|
+
private applyReadonlyClass;
|
|
73
|
+
private chooseRenderer;
|
|
74
|
+
private ensureShell;
|
|
75
|
+
private getScrollHost;
|
|
76
|
+
private mount;
|
|
77
|
+
destroy(): void;
|
|
78
|
+
setRootClass(classNames: string | string[]): void;
|
|
79
|
+
setRootStyle(style: Partial<CSSStyleDeclaration>): void;
|
|
80
|
+
setData(data: NullableData<T>): void;
|
|
81
|
+
setView(view: View): void;
|
|
82
|
+
getSchema(): Schema;
|
|
83
|
+
getView(): View;
|
|
84
|
+
/**
|
|
85
|
+
* Returns the current table data (includes pending edits in commit mode and formula results).
|
|
86
|
+
* Note: This is the full dataset (not affected by view filters/sorts). Use `listRows()` for the visible rows.
|
|
87
|
+
*/
|
|
88
|
+
getData(): R[];
|
|
89
|
+
/**
|
|
90
|
+
* Returns the raw dataset without pending edits and without formula results.
|
|
91
|
+
* Note: This is the full dataset (not affected by view filters/sorts). Use `listRows()` for the visible rows.
|
|
92
|
+
*/
|
|
93
|
+
getRawData(): T[];
|
|
94
|
+
getCell<K extends keyof R & string>(rowId: string, colKey: K): R[K] | undefined;
|
|
95
|
+
getRowIndex(rowId: string): number;
|
|
96
|
+
getColumnIndex(colKey: string): number;
|
|
97
|
+
findRowById(rowId: string): InternalRow | null;
|
|
98
|
+
listRows(): InternalRow[];
|
|
99
|
+
getAllRows(): InternalRow[];
|
|
100
|
+
/**
|
|
101
|
+
* Returns a single row as an object (includes pending edits in commit mode and formula results).
|
|
102
|
+
* When a number is passed, it is interpreted as the base row index (not affected by view filters/sorts).
|
|
103
|
+
*/
|
|
104
|
+
getRow(rowIdOrIndex: string | number): R | null;
|
|
105
|
+
private buildRow;
|
|
106
|
+
getTableData(): R[];
|
|
107
|
+
getColumnData<K extends keyof R & string>(colKey: K): R[K][];
|
|
108
|
+
getPending(): Map<string, Record<string, unknown>>;
|
|
109
|
+
getPendingRowIds(): string[];
|
|
110
|
+
hasPendingChanges(): boolean;
|
|
111
|
+
getPendingCellCount(): number;
|
|
112
|
+
getCellPending<K extends keyof R & string>(row: string | number, colKey: K): boolean;
|
|
113
|
+
getDisplayValue<K extends keyof R & string>(row: string | number, colKey: K): string;
|
|
114
|
+
insertRow(rowData: T, position?: number | string): string | null;
|
|
115
|
+
deleteRow(row: string | number): boolean;
|
|
116
|
+
private handleEdit;
|
|
117
|
+
undo(): void;
|
|
118
|
+
redo(): void;
|
|
119
|
+
getUndoRedoHistory(): UndoRedoHistory;
|
|
120
|
+
private applyInverse;
|
|
121
|
+
private applyForward;
|
|
122
|
+
commit(): Promise<RowStateSnapshot<T, R>[]>;
|
|
123
|
+
private sendCommit;
|
|
124
|
+
private handleServerEvent;
|
|
125
|
+
private applyCommand;
|
|
126
|
+
private ensureContextMenu;
|
|
127
|
+
private showContextMenu;
|
|
128
|
+
private handleContextAction;
|
|
129
|
+
private createBlankRow;
|
|
130
|
+
private closeContextMenu;
|
|
131
|
+
private ensureToast;
|
|
132
|
+
private showToast;
|
|
133
|
+
private bindViewport;
|
|
134
|
+
private unbindViewport;
|
|
135
|
+
remount(target: HTMLElement): void;
|
|
136
|
+
showSearchPanel(mode?: FindReplaceMode): void;
|
|
137
|
+
hideSearchPanel(): void;
|
|
138
|
+
toggleSearchPanel(mode?: FindReplaceMode): void;
|
|
139
|
+
private updateFindReplaceReadonlyUI;
|
|
140
|
+
getTableState(): TableState;
|
|
141
|
+
subscribeTableState(listener: TableStateListener): () => boolean;
|
|
142
|
+
private emitTableState;
|
|
143
|
+
getSelectionSnapshot(): SelectionSnapshot;
|
|
144
|
+
subscribeSelection(listener: SelectionListener): () => boolean;
|
|
145
|
+
private emitSelection;
|
|
146
|
+
private getRowStateSnapshot;
|
|
147
|
+
subscribeRowState(listener: RowStateListener<T, R>): () => boolean;
|
|
148
|
+
private emitRowState;
|
|
149
|
+
setCellValue<K extends keyof T & string>(row: number | string, colKey: K, next: Updater<T[K]>): void;
|
|
150
|
+
setValueToSelection(next: Updater<unknown>): void;
|
|
151
|
+
openFindReplaceDialog(mode?: FindReplaceMode): void;
|
|
152
|
+
closeFindReplaceDialog(): void;
|
|
153
|
+
private ensureFindReplace;
|
|
154
|
+
private ensureFindReplaceSidebar;
|
|
155
|
+
private teardownFindReplace;
|
|
156
|
+
private stableValueKey;
|
|
157
|
+
private ensureFilterSort;
|
|
158
|
+
private ensureFilterSortEscape;
|
|
159
|
+
private ensureFilterSortHeaderIntegration;
|
|
160
|
+
private ensureFilterSortSidebar;
|
|
161
|
+
showFilterSortPanel(colKey: string): void;
|
|
162
|
+
hideFilterSortPanel(): void;
|
|
163
|
+
toggleFilterSortPanel(colKey: string): void;
|
|
164
|
+
private buildFilterSortDraft;
|
|
165
|
+
private renderFilterSortSidebar;
|
|
166
|
+
private renderFilterSortValues;
|
|
167
|
+
private applyFilterSortDraft;
|
|
168
|
+
private clearFilterSortForActiveColumn;
|
|
169
|
+
private setSortForActiveColumn;
|
|
170
|
+
private clearSort;
|
|
171
|
+
private teardownFilterSort;
|
|
172
|
+
private initViewportState;
|
|
173
|
+
private updateViewportFromRoot;
|
|
174
|
+
private flushRender;
|
|
175
|
+
}
|
|
176
|
+
export declare function createTablePlaceholder<T extends object = Record<string, unknown>>(config: TableConfig<T>, options?: CoreOptions): ExtableCore<T, T>;
|
|
177
|
+
export declare function mountTable(target: HTMLElement, core: ExtableCore): ExtableCore<Record<string, unknown>, Record<string, unknown>>;
|