@panphora/clayjs 1.13.1 → 1.14.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/THIRD-PARTY-NOTICES.md +19 -0
- package/dist/clay.standalone.js +69667 -41708
- package/dist/server/checkpoint.cjs +70690 -0
- package/dist/server/checkpoint.mjs +8 -0
- package/dist/server/rooms.cjs +72039 -0
- package/dist/server/rooms.mjs +66 -0
- package/docs/guide.md +94 -93
- package/entries/clay.js +22 -0
- package/package.json +16 -2
- package/packed-contract.json +17 -3
- package/src/core/conflict-download.js +11 -2
- package/src/core/host-meta.js +75 -0
- package/src/core/persist.js +11 -0
- package/src/core/save-core.js +266 -31
- package/src/core/save.js +62 -32
- package/src/core/snapshot.js +39 -13
- package/src/core/style-comparisons.js +37 -0
- package/src/dom/insert-style-tag.js +11 -12
- package/src/lib/effect.js +130 -0
- package/src/lib/style-adoption.js +9 -0
- package/src/lib/style.js +44 -0
- package/src/live/fast-lane.js +347 -60
- package/src/live/local-lane.js +32 -6
- package/src/live/ops.js +77 -0
- package/src/live/provenance.js +129 -0
- package/src/live/save-history.js +188 -0
- package/src/live/server-lane.js +9 -2
- package/src/live/store.js +194 -16
- package/src/loader-logic.js +7 -3
- package/src/loader.js +6 -0
- package/src/page/address.js +20 -0
- package/src/page/adopt-boot.js +189 -0
- package/src/page/adopt.js +240 -0
- package/src/page/ai-tool.js +188 -0
- package/src/page/announce.js +21 -0
- package/src/page/blocks.js +1121 -0
- package/src/page/blocksel.js +269 -0
- package/src/page/board-drag.js +226 -0
- package/src/page/board-panel.js +353 -0
- package/src/page/board-style.js +146 -0
- package/src/page/board-view.js +447 -0
- package/src/page/board.js +41 -0
- package/src/page/chart.js +220 -0
- package/src/page/checklist.js +143 -0
- package/src/page/chrome.js +587 -0
- package/src/page/csv-import.js +169 -0
- package/src/page/drag.js +202 -0
- package/src/page/drop.js +112 -0
- package/src/page/editor.js +384 -0
- package/src/page/highlight.js +268 -0
- package/src/page/inline.js +60 -1
- package/src/page/items.js +159 -0
- package/src/page/link.js +247 -0
- package/src/page/markdown.js +71 -0
- package/src/page/media.js +570 -0
- package/src/page/merge.js +130 -0
- package/src/page/model.js +71 -9
- package/src/page/placeholder.js +23 -0
- package/src/page/projection.js +647 -0
- package/src/page/recover.js +193 -0
- package/src/page/rich/cell.js +16 -0
- package/src/page/rich/chart.js +104 -0
- package/src/page/rich/csv.js +118 -0
- package/src/page/rich/page-tool.js +86 -0
- package/src/page/rich/tool-runtime.js +195 -0
- package/src/page/save-projection.js +53 -0
- package/src/page/save-states.js +85 -0
- package/src/page/schema.js +12 -5
- package/src/page/table-kinds.js +20 -0
- package/src/page/table-paste.js +57 -0
- package/src/page/table-select.js +244 -0
- package/src/page/table-style.js +25 -0
- package/src/page/table.js +650 -0
- package/src/page/themes/content.css +46 -0
- package/src/page/themes/light-1.css +269 -0
- package/src/page/themes/light-2.css +211 -0
- package/src/page/themes/light-3.css +130 -0
- package/src/page/themes/light-4.css +299 -0
- package/src/page/tool-data.js +25 -0
- package/src/page/tool-view.js +68 -0
- package/src/page/tools.js +133 -0
- package/src/page/view.js +136 -0
- package/src/page/ybind/author.js +25 -0
- package/src/page/ybind/binding.js +92 -0
- package/src/page/ybind/board-commands.js +170 -0
- package/src/page/ybind/board.js +89 -0
- package/src/page/ybind/bridge.js +382 -0
- package/src/page/ybind/conversion.js +26 -0
- package/src/page/ybind/core.js +7 -0
- package/src/page/ybind/decode.js +208 -0
- package/src/page/ybind/doc.js +58 -0
- package/src/page/ybind/external.js +190 -0
- package/src/page/ybind/group.js +124 -0
- package/src/page/ybind/index.js +12 -0
- package/src/page/ybind/layout.js +27 -0
- package/src/page/ybind/marks.js +18 -0
- package/src/page/ybind/migrate.js +152 -0
- package/src/page/ybind/moves.js +288 -0
- package/src/page/ybind/origins.js +257 -0
- package/src/page/ybind/parts.js +44 -0
- package/src/page/ybind/projection-map.js +92 -0
- package/src/page/ybind/provider.js +285 -0
- package/src/page/ybind/recovery.js +411 -0
- package/src/page/ybind/seed.js +27 -0
- package/src/page/ybind/table.js +166 -0
- package/src/page/ybind/tokens.js +93 -0
- package/src/page/ybind/walk.js +333 -0
- package/src/page/ybind/writer.js +307 -0
- package/src/plugins/indicator.js +16 -6
- package/src/plugins/live.js +742 -65
- package/src/plugins/page.js +547 -0
- package/src/plugins/undo.js +60 -3
- package/src/server/checkpoint/index.js +218 -0
- package/src/server/rooms/index.js +59 -0
- package/src/server/rooms/live-data-ops.js +369 -0
- package/src/server/rooms/live-data-rooms.js +965 -0
- package/src/server/rooms/live-data-tags.js +49 -0
- package/src/server/rooms/live-page-history.js +41 -0
- package/src/server/rooms/live-provenance.js +142 -0
- package/src/server/rooms/page-admission.js +79 -0
- package/src/server/rooms/page-conversion.js +62 -0
- package/src/server/rooms/page-rebase.js +108 -0
- package/src/server/rooms/page-state.js +91 -0
- package/src/server/rooms/page-write-admission.js +63 -0
- package/src/server/rooms/page-write-guard.js +23 -0
- package/src/server/rooms/recent.js +18 -0
- package/src/server/rooms/spec-wire.js +17 -0
- package/src/standalone.js +22 -0
- package/src/sync/live-sync.js +13 -2
- package/src/sync/stream.js +62 -16
- package/src/ui/index.js +4 -0
- package/src/ui/toast.js +21 -5
- package/src/vendor/pierre/pierre.vendor.js +3 -0
- package/src/vendor/yjs.vendor.js +7 -0
- package/src/page/binding.js +0 -647
- package/src/page/replay.js +0 -578
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
import { state, keymap } from "../vendor/prosemirror.vendor.js";
|
|
2
|
+
import { schema } from "./schema.js";
|
|
3
|
+
import { keyBetween, keysBetween, newId, orderedIds, cellContent } from "./model.js";
|
|
4
|
+
import { placeAt } from "./ybind/board-commands.js";
|
|
5
|
+
import { parseDecimal, COLUMN_KINDS } from "./rich/csv.js";
|
|
6
|
+
import { columnKinds, KIND_ICONS, KIND_LABELS } from "./table-kinds.js";
|
|
7
|
+
import { asStep } from "./blocksel.js";
|
|
8
|
+
import { openPageMenu } from "./chrome.js";
|
|
9
|
+
import { insertChartAfterTable } from "./chart.js";
|
|
10
|
+
import { REMOTE_META } from "./ybind/layout.js";
|
|
11
|
+
import { gridFromClipboard, tableFromGrid } from "./table-paste.js";
|
|
12
|
+
import { rectTopLeft, selectLine } from "./table-select.js";
|
|
13
|
+
import toast from "../ui/toast.js";
|
|
14
|
+
|
|
15
|
+
const { Plugin, TextSelection } = state;
|
|
16
|
+
const N = schema.nodes;
|
|
17
|
+
export const TABLE_IMPORT_LIMIT = 256 * 1024;
|
|
18
|
+
|
|
19
|
+
const columns = (table) => orderedIds(table.attrs.cols);
|
|
20
|
+
const tableAt = (st) => {
|
|
21
|
+
const $pos = st.selection.$head;
|
|
22
|
+
for (let depth = $pos.depth; depth > 0; depth--) {
|
|
23
|
+
if ($pos.node(depth).type === N.table) {
|
|
24
|
+
const table = $pos.node(depth);
|
|
25
|
+
const row = depth < $pos.depth ? $pos.index(depth) : 0;
|
|
26
|
+
const cell = depth + 1 < $pos.depth ? $pos.index(depth + 1) : 0;
|
|
27
|
+
return { table, pos: $pos.before(depth), row, cell };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const cellPos = (doc, tablePos, row, cell) => {
|
|
34
|
+
const table = doc.nodeAt(tablePos);
|
|
35
|
+
if (!table || !table.childCount) return null;
|
|
36
|
+
row = Math.max(0, Math.min(row, table.childCount - 1));
|
|
37
|
+
cell = Math.max(0, Math.min(cell, table.child(row).childCount - 1));
|
|
38
|
+
let pos = tablePos + 1;
|
|
39
|
+
for (let i = 0; i < row; i++) pos += table.child(i).nodeSize;
|
|
40
|
+
pos++;
|
|
41
|
+
for (let i = 0; i < cell; i++) pos += table.child(row).child(i).nodeSize;
|
|
42
|
+
return pos + 1;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const emptyRow = (cols, pos = keyBetween()) => N.table_row.create({ id: newId("r"), pos }, cols.map((col) => N.table_cell.create({ col })));
|
|
46
|
+
const copyTable = (table, attrs, rows) => N.table.create({ ...table.attrs, ...attrs }, rows);
|
|
47
|
+
|
|
48
|
+
function changeTable(st, dispatch, change) {
|
|
49
|
+
const found = tableAt(st);
|
|
50
|
+
if (!found) return false;
|
|
51
|
+
const result = change(found.table, found);
|
|
52
|
+
if (!result) return false;
|
|
53
|
+
if (dispatch) {
|
|
54
|
+
const { table, row = found.row, cell = found.cell } = result;
|
|
55
|
+
const tr = st.tr.replaceWith(found.pos, found.pos + found.table.nodeSize, table);
|
|
56
|
+
const at = cellPos(tr.doc, found.pos, row, cell);
|
|
57
|
+
if (at !== null) tr.setSelection(TextSelection.create(tr.doc, at));
|
|
58
|
+
dispatch(asStep(tr));
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function insertTableNode(st, dispatch, table, remove = null, attachment = null) {
|
|
64
|
+
const index = st.selection.$head.index(0);
|
|
65
|
+
const current = st.doc.child(index);
|
|
66
|
+
let at = 0;
|
|
67
|
+
for (let i = 0; i < index; i++) at += st.doc.child(i).nodeSize;
|
|
68
|
+
const replace = current.isTextblock && (current.content.size === 0 || (remove?.from === at + 1 && remove?.to === at + current.nodeSize - 1));
|
|
69
|
+
const next = st.doc.maybeChild(index + 1);
|
|
70
|
+
const pos = replace ? current.attrs.pos : keyBetween(current.attrs.pos, next?.attrs.pos ?? null);
|
|
71
|
+
const node = table.type.create({ ...table.attrs, pos, parent: current.attrs.parent ?? null }, table.content);
|
|
72
|
+
if (dispatch) {
|
|
73
|
+
const tr = st.tr;
|
|
74
|
+
if (remove) tr.delete(remove.from, remove.to);
|
|
75
|
+
const mappedAt = tr.mapping.map(at);
|
|
76
|
+
const currentAfter = tr.doc.child(index);
|
|
77
|
+
const inserted = replace ? mappedAt : mappedAt + currentAfter.nodeSize;
|
|
78
|
+
tr.replaceWith(inserted, replace ? mappedAt + currentAfter.nodeSize : inserted, node);
|
|
79
|
+
if (attachment) {
|
|
80
|
+
const file = attachment.type.create({ ...attachment.attrs, pos: keyBetween(pos, next?.attrs.pos ?? null), parent: current.attrs.parent ?? null }, attachment.content);
|
|
81
|
+
tr.insert(inserted + node.nodeSize, file);
|
|
82
|
+
}
|
|
83
|
+
tr.setSelection(TextSelection.create(tr.doc, cellPos(tr.doc, inserted, 0, 0)));
|
|
84
|
+
dispatch(asStep(tr).scrollIntoView());
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function makeTable(rows = 3, cols = 3) {
|
|
90
|
+
if (!Number.isInteger(rows) || !Number.isInteger(cols) || rows < 1 || cols < 1) return null;
|
|
91
|
+
const ids = Array.from({ length: cols }, () => newId("c"));
|
|
92
|
+
const positions = keysBetween(null, null, cols);
|
|
93
|
+
const map = Object.fromEntries(ids.map((id, i) => [id, { pos: positions[i], name: `Column ${i + 1}` }]));
|
|
94
|
+
const rowPositions = keysBetween(null, null, rows);
|
|
95
|
+
const children = rowPositions.map((pos) => emptyRow(ids, pos));
|
|
96
|
+
return N.table.create({ id: newId(), cols: map }, children);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const insertTable = (rows = 3, cols = 3) => (st, dispatch) => {
|
|
100
|
+
const node = makeTable(rows, cols);
|
|
101
|
+
return node ? insertTableNode(st, dispatch, node) : false;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const moveTo = (advance) => (st, dispatch) => {
|
|
105
|
+
const found = tableAt(st);
|
|
106
|
+
if (!found || st.selection.$head.parent.type !== N.table_cell) return false;
|
|
107
|
+
const width = found.table.child(found.row).childCount;
|
|
108
|
+
let row = found.row, cell = found.cell;
|
|
109
|
+
if (advance === "down") row++;
|
|
110
|
+
else {
|
|
111
|
+
cell += advance;
|
|
112
|
+
if (cell >= width) { row++; cell = 0; }
|
|
113
|
+
if (cell < 0) { row--; cell = width - 1; }
|
|
114
|
+
}
|
|
115
|
+
if (row < 0) return true;
|
|
116
|
+
if (row >= found.table.childCount) {
|
|
117
|
+
if (advance === -1) return true;
|
|
118
|
+
return appendAndMove(st, dispatch, found, cell);
|
|
119
|
+
}
|
|
120
|
+
if (dispatch) dispatch(st.tr.setSelection(TextSelection.create(st.doc, cellPos(st.doc, found.pos, row, cell))).scrollIntoView());
|
|
121
|
+
return true;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
function appendAndMove(st, dispatch, found, cell) {
|
|
125
|
+
const rows = [...found.table.content.content];
|
|
126
|
+
rows.push(emptyRow(columns(found.table), keyBetween(rows.at(-1).attrs.pos, null)));
|
|
127
|
+
if (dispatch) {
|
|
128
|
+
const tr = st.tr.replaceWith(found.pos, found.pos + found.table.nodeSize, copyTable(found.table, { sort: null }, rows));
|
|
129
|
+
tr.setSelection(TextSelection.create(tr.doc, cellPos(tr.doc, found.pos, rows.length - 1, cell)));
|
|
130
|
+
dispatch(asStep(tr).scrollIntoView());
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const nextCell = moveTo(1);
|
|
136
|
+
export const prevCell = moveTo(-1);
|
|
137
|
+
export const cellBelow = moveTo("down");
|
|
138
|
+
|
|
139
|
+
export const addRow = (after = true) => (st, dispatch) => changeTable(st, dispatch, (table, found) => {
|
|
140
|
+
const rows = [...table.content.content];
|
|
141
|
+
const index = found.row + (after ? 1 : 0);
|
|
142
|
+
const pos = keyBetween(rows[index - 1]?.attrs.pos ?? null, rows[index]?.attrs.pos ?? null);
|
|
143
|
+
rows.splice(index, 0, emptyRow(columns(table), pos));
|
|
144
|
+
return { table: copyTable(table, { sort: null }, rows), row: index };
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
export const deleteRow = (st, dispatch) => changeTable(st, dispatch, (table, found) => {
|
|
148
|
+
const rows = [...table.content.content];
|
|
149
|
+
if (rows.length === 1) {
|
|
150
|
+
const row = rows[0];
|
|
151
|
+
rows[0] = N.table_row.create(row.attrs, columns(table).map((col) => N.table_cell.create({ col })));
|
|
152
|
+
} else rows.splice(found.row, 1);
|
|
153
|
+
return { table: copyTable(table, { sort: null }, rows), row: Math.min(found.row, rows.length - 1) };
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const placeColumn = (cols, ids, index, id, fields) => {
|
|
157
|
+
const { pos, rekey } = placeAt(ids, (col) => cols[col]?.pos ?? "", index);
|
|
158
|
+
const next = { ...cols, [id]: { ...fields, pos } };
|
|
159
|
+
for (const [col, key] of rekey) next[col] = { ...next[col], pos: key };
|
|
160
|
+
return next;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export const addColumn = (after = true) => (st, dispatch) => changeTable(st, dispatch, (table, found) => {
|
|
164
|
+
const ids = columns(table);
|
|
165
|
+
const index = found.cell + (after ? 1 : 0);
|
|
166
|
+
const id = newId("c");
|
|
167
|
+
const cols = placeColumn(table.attrs.cols, ids, index, id, { name: `Column ${ids.length + 1}` });
|
|
168
|
+
const rows = table.content.content.map((row) => {
|
|
169
|
+
const cells = [...row.content.content];
|
|
170
|
+
cells.splice(index, 0, N.table_cell.create({ col: id }));
|
|
171
|
+
return N.table_row.create(row.attrs, cells);
|
|
172
|
+
});
|
|
173
|
+
return { table: copyTable(table, { cols }, rows), cell: index };
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
export const deleteColumn = (st, dispatch) => changeTable(st, dispatch, (table, found) => {
|
|
177
|
+
const ids = columns(table);
|
|
178
|
+
if (ids.length <= 1) return null;
|
|
179
|
+
const cols = { ...table.attrs.cols };
|
|
180
|
+
delete cols[ids[found.cell]];
|
|
181
|
+
const rows = table.content.content.map((row) => N.table_row.create(row.attrs, row.content.content.filter((_, i) => i !== found.cell)));
|
|
182
|
+
return { table: copyTable(table, { cols, sort: table.attrs.sort?.col === ids[found.cell] ? null : table.attrs.sort }, rows), cell: Math.min(found.cell, ids.length - 2) };
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
export const moveRow = (delta) => (st, dispatch) => changeTable(st, dispatch, (table, found) => {
|
|
186
|
+
const to = found.row + delta;
|
|
187
|
+
if (to < 0 || to >= table.childCount) return null;
|
|
188
|
+
const rows = [...table.content.content];
|
|
189
|
+
const [row] = rows.splice(found.row, 1);
|
|
190
|
+
rows.splice(to, 0, row);
|
|
191
|
+
const pos = keyBetween(rows[to - 1]?.attrs.pos ?? null, rows[to + 1]?.attrs.pos ?? null);
|
|
192
|
+
rows[to] = N.table_row.create({ ...row.attrs, pos }, row.content);
|
|
193
|
+
return { table: copyTable(table, { sort: null }, rows), row: to };
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
export const moveColumn = (delta) => (st, dispatch) => changeTable(st, dispatch, (table, found) => {
|
|
197
|
+
const ids = columns(table);
|
|
198
|
+
const to = found.cell + delta;
|
|
199
|
+
if (to < 0 || to >= ids.length) return null;
|
|
200
|
+
const order = [...ids];
|
|
201
|
+
const [id] = order.splice(found.cell, 1);
|
|
202
|
+
order.splice(to, 0, id);
|
|
203
|
+
const cols = placeColumn(table.attrs.cols, order.filter((col) => col !== id), to, id, table.attrs.cols[id]);
|
|
204
|
+
const rows = table.content.content.map((row) => {
|
|
205
|
+
const cells = [...row.content.content];
|
|
206
|
+
const [cell] = cells.splice(found.cell, 1);
|
|
207
|
+
cells.splice(to, 0, cell);
|
|
208
|
+
return N.table_row.create(row.attrs, cells);
|
|
209
|
+
});
|
|
210
|
+
return { table: copyTable(table, { cols }, rows), cell: to };
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
export const MIN_WIDTH = 48, MAX_WIDTH = 1200, DEFAULT_WIDTH = 160;
|
|
214
|
+
const FONT = ["fontFamily", "fontSize", "fontWeight", "fontStyle", "letterSpacing"];
|
|
215
|
+
const textWidth = (el) => {
|
|
216
|
+
const doc = el.ownerDocument;
|
|
217
|
+
const style = doc.defaultView.getComputedStyle(el);
|
|
218
|
+
const probe = doc.createElement("span");
|
|
219
|
+
for (const key of FONT) probe.style[key] = style[key];
|
|
220
|
+
Object.assign(probe.style, { position: "absolute", visibility: "hidden", whiteSpace: "pre", left: "-9999px", top: "0" });
|
|
221
|
+
probe.textContent = el.textContent;
|
|
222
|
+
doc.body.append(probe);
|
|
223
|
+
const width = probe.getBoundingClientRect().width;
|
|
224
|
+
probe.remove();
|
|
225
|
+
return Math.ceil(width);
|
|
226
|
+
};
|
|
227
|
+
const widthOf = (value) => (Number.isFinite(value) && value > 0 ? Math.round(Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, value))) : null);
|
|
228
|
+
|
|
229
|
+
export const setColumnWidth = (colId, width) => (st, dispatch) => changeTable(st, dispatch, (table) => {
|
|
230
|
+
const col = table.attrs.cols[colId];
|
|
231
|
+
if (!col) return null;
|
|
232
|
+
const next = width === null ? null : widthOf(width);
|
|
233
|
+
if (width !== null && next === null) return null;
|
|
234
|
+
if ((col.width ?? null) === next) return null;
|
|
235
|
+
const { width: _old, ...rest } = col;
|
|
236
|
+
const cols = { ...table.attrs.cols, [colId]: next === null ? rest : { ...col, width: next } };
|
|
237
|
+
return { table: copyTable(table, { cols }, table.content), cell: columns(table).indexOf(colId) };
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
export const renameColumn = (colId, name) => (st, dispatch) => changeTable(st, dispatch, (table) => {
|
|
241
|
+
if (!table.attrs.cols[colId] || !String(name).trim()) return null;
|
|
242
|
+
const cols = { ...table.attrs.cols, [colId]: { ...table.attrs.cols[colId], name: String(name).trim() } };
|
|
243
|
+
return { table: copyTable(table, { cols }, table.content) };
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
export const setColumnKind = (colId, kind) => (st, dispatch) => changeTable(st, dispatch, (table) => {
|
|
247
|
+
if (!table.attrs.cols[colId] || !(kind === "auto" || COLUMN_KINDS.includes(kind))) return null;
|
|
248
|
+
const { kind: _old, ...rest } = table.attrs.cols[colId];
|
|
249
|
+
const cols = { ...table.attrs.cols, [colId]: kind === "auto" ? rest : { ...table.attrs.cols[colId], kind } };
|
|
250
|
+
return { table: copyTable(table, { cols, sort: table.attrs.sort?.col === colId ? null : table.attrs.sort }, table.content) };
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
export const sortBy = (colId, dir = "asc") => (st, dispatch) => changeTable(st, dispatch, (table) => {
|
|
254
|
+
if (!table.attrs.cols[colId] || !["asc", "desc"].includes(dir)) return null;
|
|
255
|
+
const index = columns(table).indexOf(colId);
|
|
256
|
+
const kind = columnKinds(table)[colId];
|
|
257
|
+
const values = [...table.content.content].map((row, previous) => ({ row, previous, text: row.child(index).textContent.trim() }));
|
|
258
|
+
values.sort((a, b) => {
|
|
259
|
+
if (!a.text || !b.text) return !a.text && !b.text ? a.previous - b.previous : !a.text ? 1 : -1;
|
|
260
|
+
const av = kind === "number" ? parseDecimal(a.text) : a.text;
|
|
261
|
+
const bv = kind === "number" ? parseDecimal(b.text) : b.text;
|
|
262
|
+
if (av == null || bv == null) return av == null && bv == null ? a.previous - b.previous : av == null ? 1 : -1;
|
|
263
|
+
const compare = kind === "number" ? av - bv : kind === "date" ? (av < bv ? -1 : av > bv ? 1 : 0) : av.localeCompare(bv, undefined, { numeric: true, sensitivity: "base" });
|
|
264
|
+
return compare ? (dir === "asc" ? compare : -compare) : a.previous - b.previous;
|
|
265
|
+
});
|
|
266
|
+
const positions = keysBetween(null, null, values.length);
|
|
267
|
+
const rows = values.map(({ row }, i) => N.table_row.create({ ...row.attrs, pos: positions[i] }, row.content));
|
|
268
|
+
return { table: copyTable(table, { sort: { col: colId, dir } }, rows) };
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Pasted cells overwrite the cells from `at` (the caret's cell by default)
|
|
272
|
+
// rightwards and down, adding rows below the table when the paste needs them.
|
|
273
|
+
// Cells past the last column are dropped.
|
|
274
|
+
export const pasteGrid = (grid, at = null) => (st, dispatch) => changeTable(st, dispatch, (table, found) => {
|
|
275
|
+
const ids = columns(table);
|
|
276
|
+
const row0 = at?.row ?? found.row, col0 = at?.cell ?? found.cell;
|
|
277
|
+
const rows = [...table.content.content];
|
|
278
|
+
grid.forEach((line, i) => {
|
|
279
|
+
const r = row0 + i;
|
|
280
|
+
if (r >= rows.length) rows.push(emptyRow(ids, keyBetween(rows.at(-1).attrs.pos, null)));
|
|
281
|
+
const cells = [...rows[r].content.content];
|
|
282
|
+
line.forEach((value, j) => {
|
|
283
|
+
if (col0 + j < ids.length) cells[col0 + j] = N.table_cell.create({ col: ids[col0 + j] }, cellContent(schema, value));
|
|
284
|
+
});
|
|
285
|
+
rows[r] = N.table_row.create(rows[r].attrs, cells);
|
|
286
|
+
});
|
|
287
|
+
const width = Math.max(1, ...grid.map((line) => line.length));
|
|
288
|
+
return { table: copyTable(table, { sort: null }, rows), row: row0 + grid.length - 1, cell: Math.min(col0 + width - 1, ids.length - 1) };
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const tableSize = (node) => new TextEncoder().encode(JSON.stringify(node.toJSON())).length;
|
|
292
|
+
|
|
293
|
+
export function pasteTable(view, event) {
|
|
294
|
+
const data = event.clipboardData;
|
|
295
|
+
if (!data || data.files?.length) return false;
|
|
296
|
+
const st = view.state;
|
|
297
|
+
if (st.selection.$from.parent.type.spec.code) return false;
|
|
298
|
+
const rect = rectTopLeft(st);
|
|
299
|
+
const text = data.getData("text/plain").replace(/\r?\n$/, "");
|
|
300
|
+
const found = gridFromClipboard(data.getData("text/html"), data.getData("text/plain"), view.dom.ownerDocument)
|
|
301
|
+
?? (rect && text && !/[\r\n]/.test(text) ? { grid: [[text]], width: 1, height: 1, own: false } : null);
|
|
302
|
+
if (!found) return false;
|
|
303
|
+
if (rect && rect.rows * rect.cols > 1 && found.width * found.height === 1) {
|
|
304
|
+
return pasteGrid(Array.from({ length: rect.rows }, () => Array(rect.cols).fill(found.grid[0][0])), { row: rect.row, cell: rect.cell })(st, view.dispatch);
|
|
305
|
+
}
|
|
306
|
+
if (rect) return pasteGrid(found.grid, { row: rect.row, cell: rect.cell })(st, view.dispatch);
|
|
307
|
+
if (st.selection.$head.parent.type === N.table_cell) {
|
|
308
|
+
if (found.width * found.height < 2) return false;
|
|
309
|
+
return pasteGrid(found.grid)(st, view.dispatch);
|
|
310
|
+
}
|
|
311
|
+
if (found.width * found.height < 2) return false;
|
|
312
|
+
if (found.height < 2 && !/<table[\s>]/i.test(data.getData("text/html"))) return false;
|
|
313
|
+
const table = tableFromGrid(found.grid, { header: !found.own && found.height > 1 });
|
|
314
|
+
if (!table) return false;
|
|
315
|
+
if (tableSize(table) > TABLE_IMPORT_LIMIT) {
|
|
316
|
+
toast("This paste is too large for one table.", "error");
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
const { from, to, empty, $from, $to } = st.selection;
|
|
320
|
+
if (!empty && $from.index(0) !== $to.index(0)) {
|
|
321
|
+
const tr = st.tr.deleteSelection();
|
|
322
|
+
return insertTableNode(st.apply(tr), (inner) => {
|
|
323
|
+
for (const step of inner.steps) tr.step(step);
|
|
324
|
+
tr.setSelection(TextSelection.create(tr.doc, inner.selection.from));
|
|
325
|
+
view.dispatch(asStep(tr).scrollIntoView());
|
|
326
|
+
}, table);
|
|
327
|
+
}
|
|
328
|
+
return insertTableNode(st, view.dispatch, table, empty ? null : { from, to });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export const tableKeys = keymap.keymap({ Tab: nextCell, "Shift-Tab": prevCell, Enter: cellBelow, "Mod-Enter": addRow(true) });
|
|
332
|
+
|
|
333
|
+
export function tablePlugin() {
|
|
334
|
+
return new Plugin({
|
|
335
|
+
props: {
|
|
336
|
+
handlePaste: pasteTable,
|
|
337
|
+
handleClickOn(view, _pos, node, nodePos, event, direct) {
|
|
338
|
+
if (node.type !== N.table_cell || !direct || event.button) return false;
|
|
339
|
+
const desc = view.nodeDOM(nodePos);
|
|
340
|
+
const cellView = desc && cellViews.get(desc);
|
|
341
|
+
if (cellView) cellView.openDate();
|
|
342
|
+
return false;
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
appendTransaction(transactions, oldState, newState) {
|
|
346
|
+
if (!transactions.some((tr) => tr.docChanged && !tr.getMeta(REMOTE_META))) return null;
|
|
347
|
+
const old = new Map();
|
|
348
|
+
oldState.doc.forEach((node) => { if (node.type === N.table) old.set(node.attrs.id, node); });
|
|
349
|
+
const tr = newState.tr;
|
|
350
|
+
newState.doc.forEach((table, pos) => {
|
|
351
|
+
if (table.type !== N.table || !table.attrs.sort) return;
|
|
352
|
+
const before = old.get(table.attrs.id), col = table.attrs.sort.col;
|
|
353
|
+
if (!before) return;
|
|
354
|
+
const cells = (node) => Object.fromEntries(node.content.content.map((row) => [row.attrs.id, row.content.content.find((cell) => cell.attrs.col === col)?.textContent]));
|
|
355
|
+
const previous = cells(before), current = cells(table);
|
|
356
|
+
if (Object.keys(previous).length !== Object.keys(current).length || Object.keys(previous).some((id) => previous[id] !== current[id] || !Object.hasOwn(current, id))) {
|
|
357
|
+
tr.setNodeMarkup(pos, null, { ...table.attrs, sort: null });
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
return tr.docChanged ? tr : null;
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const button = (doc, label, action) => {
|
|
366
|
+
const el = doc.createElement("button");
|
|
367
|
+
el.type = "button";
|
|
368
|
+
el.textContent = label;
|
|
369
|
+
el.setAttribute("aria-label", label);
|
|
370
|
+
el.addEventListener("click", action);
|
|
371
|
+
return el;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
export function setCellText(view, cellPos, text) {
|
|
375
|
+
const cell = view.state.doc.nodeAt(cellPos);
|
|
376
|
+
if (cell?.type !== N.table_cell) return false;
|
|
377
|
+
const tr = view.state.tr.replaceWith(cellPos + 1, cellPos + cell.nodeSize - 1, text ? schema.text(text) : []);
|
|
378
|
+
view.dispatch(tr.setSelection(TextSelection.create(tr.doc, cellPos + 1 + text.length)));
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function selectTableCell(view, tablePos, row, cell) {
|
|
383
|
+
const pos = cellPos(view.state.doc, tablePos, row, cell);
|
|
384
|
+
if (pos === null) return false;
|
|
385
|
+
view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, pos)));
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export class TableView {
|
|
390
|
+
constructor(node, view, getPos) {
|
|
391
|
+
this.node = node;
|
|
392
|
+
this.view = view;
|
|
393
|
+
this.getPos = getPos;
|
|
394
|
+
const doc = view.dom.ownerDocument;
|
|
395
|
+
this.dom = doc.createElement("div");
|
|
396
|
+
this.dom.className = "page-table";
|
|
397
|
+
this.dom.dataset.id = node.attrs.id ?? "";
|
|
398
|
+
this.dom.setAttribute("role", "group");
|
|
399
|
+
this.dom.setAttribute("aria-label", "Table");
|
|
400
|
+
const scroll = doc.createElement("div");
|
|
401
|
+
scroll.className = "page-table-scroll";
|
|
402
|
+
const table = doc.createElement("table");
|
|
403
|
+
this.table = table;
|
|
404
|
+
this.colgroup = doc.createElement("colgroup");
|
|
405
|
+
this.header = doc.createElement("thead");
|
|
406
|
+
this.header.contentEditable = "false";
|
|
407
|
+
this.contentDOM = doc.createElement("tbody");
|
|
408
|
+
table.append(this.colgroup, this.header, this.contentDOM);
|
|
409
|
+
scroll.append(table);
|
|
410
|
+
const addRowButton = button(doc, "+", () => this.append("row"));
|
|
411
|
+
addRowButton.className = "page-table-add-row";
|
|
412
|
+
addRowButton.setAttribute("aria-label", "Add a row");
|
|
413
|
+
addRowButton.title = "Add a row";
|
|
414
|
+
const addColumnButton = button(doc, "+", () => this.append("column"));
|
|
415
|
+
addColumnButton.className = "page-table-add-col";
|
|
416
|
+
addColumnButton.setAttribute("aria-label", "Add a column");
|
|
417
|
+
addColumnButton.title = "Add a column";
|
|
418
|
+
for (const control of [addRowButton, addColumnButton]) {
|
|
419
|
+
control.contentEditable = "false";
|
|
420
|
+
control.addEventListener("mousedown", (event) => event.preventDefault());
|
|
421
|
+
}
|
|
422
|
+
this.dom.append(scroll, addRowButton, addColumnButton);
|
|
423
|
+
this.render();
|
|
424
|
+
}
|
|
425
|
+
append(what) {
|
|
426
|
+
const pos = this.getPos();
|
|
427
|
+
if (pos == null) return;
|
|
428
|
+
const last = what === "row" ? [this.node.childCount - 1, 0] : [0, this.node.child(0).childCount - 1];
|
|
429
|
+
selectTableCell(this.view, pos, ...last);
|
|
430
|
+
(what === "row" ? addRow(true) : addColumn(true))(this.view.state, this.view.dispatch);
|
|
431
|
+
this.view.focus();
|
|
432
|
+
}
|
|
433
|
+
widths() {
|
|
434
|
+
const sized = columns(this.node).some((id) => this.node.attrs.cols[id].width);
|
|
435
|
+
const cols = columns(this.node).map((id) => {
|
|
436
|
+
const col = this.dom.ownerDocument.createElement("col");
|
|
437
|
+
col.dataset.col = id;
|
|
438
|
+
const width = this.node.attrs.cols[id].width;
|
|
439
|
+
if (sized) col.style.width = `${width ?? DEFAULT_WIDTH}px`;
|
|
440
|
+
return col;
|
|
441
|
+
});
|
|
442
|
+
this.colgroup.replaceChildren(...cols);
|
|
443
|
+
this.table.classList.toggle("is-sized", sized);
|
|
444
|
+
this.table.style.width = sized ? `${columns(this.node).reduce((sum, id) => sum + (this.node.attrs.cols[id].width ?? DEFAULT_WIDTH), 0)}px` : "";
|
|
445
|
+
}
|
|
446
|
+
resizer(colId, th) {
|
|
447
|
+
const doc = this.dom.ownerDocument;
|
|
448
|
+
const handle = doc.createElement("span");
|
|
449
|
+
handle.className = "page-table-resize";
|
|
450
|
+
handle.setAttribute("role", "separator");
|
|
451
|
+
handle.setAttribute("aria-orientation", "vertical");
|
|
452
|
+
handle.setAttribute("aria-label", "Resize column");
|
|
453
|
+
handle.title = "Drag to resize, double-click to fit";
|
|
454
|
+
handle.addEventListener("mousedown", (event) => event.preventDefault());
|
|
455
|
+
handle.addEventListener("pointerdown", (event) => {
|
|
456
|
+
event.preventDefault();
|
|
457
|
+
const start = event.clientX;
|
|
458
|
+
const from = this.node.attrs.cols[colId].width ?? (th.getBoundingClientRect().width || DEFAULT_WIDTH);
|
|
459
|
+
const rtl = this.dom.ownerDocument.defaultView.getComputedStyle?.(this.dom).direction === "rtl";
|
|
460
|
+
let width = from;
|
|
461
|
+
const move = (e) => {
|
|
462
|
+
width = widthOf(from + (rtl ? start - e.clientX : e.clientX - start)) ?? from;
|
|
463
|
+
const col = [...this.colgroup.children].find((el) => el.dataset.col === colId);
|
|
464
|
+
if (col) { this.table.classList.add("is-sized"); col.style.width = `${width}px`; }
|
|
465
|
+
};
|
|
466
|
+
const up = () => {
|
|
467
|
+
doc.removeEventListener("pointermove", move);
|
|
468
|
+
doc.removeEventListener("pointerup", up);
|
|
469
|
+
if (width !== from) this.command(setColumnWidth(colId, width));
|
|
470
|
+
else this.widths();
|
|
471
|
+
};
|
|
472
|
+
doc.addEventListener("pointermove", move);
|
|
473
|
+
doc.addEventListener("pointerup", up);
|
|
474
|
+
});
|
|
475
|
+
handle.addEventListener("dblclick", (event) => { event.preventDefault(); this.fit(colId); });
|
|
476
|
+
return handle;
|
|
477
|
+
}
|
|
478
|
+
command(run) {
|
|
479
|
+
const pos = this.getPos();
|
|
480
|
+
if (pos == null) return false;
|
|
481
|
+
selectTableCell(this.view, pos, 0, 0);
|
|
482
|
+
const done = run(this.view.state, this.view.dispatch);
|
|
483
|
+
this.view.focus();
|
|
484
|
+
return done;
|
|
485
|
+
}
|
|
486
|
+
fit(colId) {
|
|
487
|
+
let widest = 0;
|
|
488
|
+
for (const cell of this.contentDOM.querySelectorAll("td")) if (cell.dataset.col === colId) widest = Math.max(widest, textWidth(cell.firstElementChild ?? cell));
|
|
489
|
+
const head = [...this.header.querySelectorAll("th")].find((th) => th.dataset.col === colId)?.querySelector("button");
|
|
490
|
+
widest = Math.max(widest, head?.scrollWidth ?? 0);
|
|
491
|
+
return this.command(setColumnWidth(colId, widest + 24));
|
|
492
|
+
}
|
|
493
|
+
render() {
|
|
494
|
+
const doc = this.dom.ownerDocument;
|
|
495
|
+
const row = doc.createElement("tr");
|
|
496
|
+
const effective = columnKinds(this.node);
|
|
497
|
+
this.widths();
|
|
498
|
+
for (const [index, colId] of columns(this.node).entries()) {
|
|
499
|
+
const col = this.node.attrs.cols[colId];
|
|
500
|
+
const kind = effective[colId];
|
|
501
|
+
const th = doc.createElement("th");
|
|
502
|
+
th.dataset.col = colId;
|
|
503
|
+
th.dataset.kind = kind;
|
|
504
|
+
if (this.node.attrs.sort?.col === colId) th.setAttribute("aria-sort", this.node.attrs.sort.dir === "asc" ? "ascending" : "descending");
|
|
505
|
+
const control = button(doc, col.name || `Column ${index + 1}`, (event) => {
|
|
506
|
+
event.preventDefault();
|
|
507
|
+
const run = (command) => () => { selectTableCell(this.view, this.getPos(), 0, index); command(this.view.state, this.view.dispatch); this.view.focus(); };
|
|
508
|
+
const rename = () => {
|
|
509
|
+
const input = doc.createElement("input");
|
|
510
|
+
input.type = "text";
|
|
511
|
+
input.value = col.name;
|
|
512
|
+
input.setAttribute("aria-label", "Column name");
|
|
513
|
+
const finish = (save) => {
|
|
514
|
+
if (save && input.value.trim()) run(renameColumn(colId, input.value))();
|
|
515
|
+
else if (input.isConnected) { input.replaceWith(control); control.focus(); }
|
|
516
|
+
};
|
|
517
|
+
input.addEventListener("keydown", (e) => {
|
|
518
|
+
if (e.key === "Enter") { e.preventDefault(); finish(true); }
|
|
519
|
+
if (e.key === "Escape") { e.preventDefault(); finish(false); }
|
|
520
|
+
e.stopPropagation();
|
|
521
|
+
});
|
|
522
|
+
control.replaceWith(input);
|
|
523
|
+
input.focus();
|
|
524
|
+
input.select();
|
|
525
|
+
};
|
|
526
|
+
openPageMenu(this.view, control, [
|
|
527
|
+
{ label: "Rename", run: rename, focus: false },
|
|
528
|
+
{ label: "Kind ▸", icon: KIND_ICONS[kind], hint: col.kind ? KIND_LABELS[kind] : `Auto: ${KIND_LABELS[kind]}`, run: () => openPageMenu(this.view, control, [
|
|
529
|
+
{ label: "Auto", icon: col.kind ? "" : KIND_ICONS[kind], hint: col.kind ? "" : KIND_LABELS[kind], checked: !col.kind, run: run(setColumnKind(colId, "auto")) },
|
|
530
|
+
...COLUMN_KINDS.map((option) => ({ label: KIND_LABELS[option], icon: KIND_ICONS[option], checked: col.kind === option, run: run(setColumnKind(colId, option)) })),
|
|
531
|
+
]), focus: false },
|
|
532
|
+
{ label: "Sort ascending", icon: "↑", run: run(sortBy(colId, "asc")) },
|
|
533
|
+
{ label: "Sort descending", icon: "↓", run: run(sortBy(colId, "desc")) },
|
|
534
|
+
{ label: "Width ▸", icon: "↔", hint: col.width ? `${col.width}px` : "Auto", focus: false, run: () => openPageMenu(this.view, control, [
|
|
535
|
+
{ label: "Wider", icon: "+", run: () => this.command(setColumnWidth(colId, (col.width ?? (th.getBoundingClientRect().width || DEFAULT_WIDTH)) + 40)) },
|
|
536
|
+
{ label: "Narrower", icon: "−", run: () => this.command(setColumnWidth(colId, (col.width ?? (th.getBoundingClientRect().width || DEFAULT_WIDTH)) - 40)) },
|
|
537
|
+
{ label: "Fit to content", icon: "↔", run: () => this.fit(colId) },
|
|
538
|
+
...(col.width ? [{ label: "Reset width", icon: "×", run: () => this.command(setColumnWidth(colId, null)) }] : []),
|
|
539
|
+
]) },
|
|
540
|
+
{ label: "Chart this table", icon: "📊", run: () => insertChartAfterTable(this.view, this.getPos()) },
|
|
541
|
+
{ label: "Insert column left", icon: "+", run: run(addColumn(false)) },
|
|
542
|
+
{ label: "Insert column right", icon: "+", run: run(addColumn(true)) },
|
|
543
|
+
...(index > 0 ? [{ label: "Move left", icon: "←", run: run(moveColumn(-1)) }] : []),
|
|
544
|
+
...(index < columns(this.node).length - 1 ? [{ label: "Move right", icon: "→", run: run(moveColumn(1)) }] : []),
|
|
545
|
+
{ label: "Select column", icon: "▦", run: () => selectLine(this.view, this.getPos(), "col", index) },
|
|
546
|
+
{ label: "Delete column", icon: "×", run: run(deleteColumn) },
|
|
547
|
+
]);
|
|
548
|
+
});
|
|
549
|
+
control.dataset.kind = kind;
|
|
550
|
+
th.append(control, this.resizer(colId, th));
|
|
551
|
+
row.append(th);
|
|
552
|
+
}
|
|
553
|
+
this.header.replaceChildren(row);
|
|
554
|
+
}
|
|
555
|
+
update(node) {
|
|
556
|
+
if (node.type !== N.table) return false;
|
|
557
|
+
this.node = node;
|
|
558
|
+
this.dom.dataset.id = node.attrs.id ?? "";
|
|
559
|
+
this.render();
|
|
560
|
+
const effective = columnKinds(node);
|
|
561
|
+
for (const cell of this.contentDOM.querySelectorAll("td[data-col]")) cell.dataset.kind = effective[cell.dataset.col] ?? "text";
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
stopEvent(event) { return !!event.target.closest?.("thead"); }
|
|
565
|
+
ignoreMutation(mutation) { return !this.contentDOM.contains(mutation.target); }
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const cellViews = new WeakMap();
|
|
569
|
+
|
|
570
|
+
export class TableCellView {
|
|
571
|
+
constructor(node, view, getPos) {
|
|
572
|
+
this.node = node;
|
|
573
|
+
this.view = view;
|
|
574
|
+
this.getPos = getPos;
|
|
575
|
+
this.date = null;
|
|
576
|
+
const doc = view.dom.ownerDocument;
|
|
577
|
+
this.dom = doc.createElement("td");
|
|
578
|
+
cellViews.set(this.dom, this);
|
|
579
|
+
this.dom.dataset.col = node.attrs.col;
|
|
580
|
+
this.contentDOM = doc.createElement("span");
|
|
581
|
+
this.dom.append(this.contentDOM);
|
|
582
|
+
const menu = doc.createElement("button");
|
|
583
|
+
menu.type = "button";
|
|
584
|
+
menu.className = "page-table-row-menu";
|
|
585
|
+
menu.textContent = "Row actions";
|
|
586
|
+
menu.setAttribute("aria-label", "Row actions");
|
|
587
|
+
menu.contentEditable = "false";
|
|
588
|
+
menu.addEventListener("click", () => {
|
|
589
|
+
const $cell = this.view.state.doc.resolve(this.getPos());
|
|
590
|
+
const tablePos = $cell.before(1);
|
|
591
|
+
const row = $cell.index(1);
|
|
592
|
+
const run = (command) => () => { selectTableCell(this.view, tablePos, row, 0); command(this.view.state, this.view.dispatch); this.view.focus(); };
|
|
593
|
+
const count = $cell.node(1).childCount;
|
|
594
|
+
openPageMenu(this.view, menu, [
|
|
595
|
+
{ label: "Insert above", icon: "+", run: run(addRow(false)) },
|
|
596
|
+
{ label: "Insert below", icon: "+", run: run(addRow(true)) },
|
|
597
|
+
...(row > 0 ? [{ label: "Move up", icon: "↑", run: run(moveRow(-1)) }] : []),
|
|
598
|
+
...(row < count - 1 ? [{ label: "Move down", icon: "↓", run: run(moveRow(1)) }] : []),
|
|
599
|
+
{ label: "Select row", icon: "▦", run: () => selectLine(this.view, tablePos, "row", row) },
|
|
600
|
+
{ label: "Delete row", icon: "×", run: run(deleteRow) },
|
|
601
|
+
]);
|
|
602
|
+
});
|
|
603
|
+
this.menu = menu;
|
|
604
|
+
this.dom.append(menu);
|
|
605
|
+
this.update(node);
|
|
606
|
+
}
|
|
607
|
+
update(node) {
|
|
608
|
+
if (node.type !== N.table_cell) return false;
|
|
609
|
+
this.node = node;
|
|
610
|
+
const $cell = this.view.state.doc.resolve(this.getPos());
|
|
611
|
+
const table = $cell.node(1);
|
|
612
|
+
this.dom.dataset.kind = columnKinds(table)[node.attrs.col] ?? "text";
|
|
613
|
+
this.menu.hidden = $cell.index(2) !== 0;
|
|
614
|
+
if (this.dom.dataset.kind !== "date") this.closeDate();
|
|
615
|
+
return true;
|
|
616
|
+
}
|
|
617
|
+
openDate() {
|
|
618
|
+
if (this.date || this.dom.dataset.kind !== "date") return false;
|
|
619
|
+
const doc = this.dom.ownerDocument;
|
|
620
|
+
const input = doc.createElement("input");
|
|
621
|
+
input.type = "date";
|
|
622
|
+
input.className = "page-table-date";
|
|
623
|
+
input.contentEditable = "false";
|
|
624
|
+
input.setAttribute("aria-label", "Pick a date");
|
|
625
|
+
const current = this.node.textContent.trim();
|
|
626
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(current)) input.value = current;
|
|
627
|
+
input.addEventListener("change", () => {
|
|
628
|
+
if (input.value) setCellText(this.view, this.getPos(), input.value);
|
|
629
|
+
this.closeDate();
|
|
630
|
+
this.view.focus();
|
|
631
|
+
});
|
|
632
|
+
input.addEventListener("keydown", (event) => {
|
|
633
|
+
if (event.key === "Escape") { event.preventDefault(); this.closeDate(); this.view.focus(); }
|
|
634
|
+
event.stopPropagation();
|
|
635
|
+
});
|
|
636
|
+
input.addEventListener("blur", () => this.closeDate());
|
|
637
|
+
this.date = input;
|
|
638
|
+
this.dom.append(input);
|
|
639
|
+
try { input.showPicker?.(); } catch {}
|
|
640
|
+
return true;
|
|
641
|
+
}
|
|
642
|
+
closeDate() {
|
|
643
|
+
const input = this.date;
|
|
644
|
+
this.date = null;
|
|
645
|
+
if (input?.isConnected) input.remove();
|
|
646
|
+
}
|
|
647
|
+
destroy() { this.closeDate(); }
|
|
648
|
+
stopEvent(event) { return !!event.target.closest?.(".page-table-row-menu, .page-table-date"); }
|
|
649
|
+
ignoreMutation(mutation) { return !this.contentDOM.contains(mutation.target); }
|
|
650
|
+
}
|