@jackuait/blok 0.10.0-beta.15 → 0.10.0-beta.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.
- package/dist/blok.mjs +2 -2
- package/dist/chunks/{blok-1213fGsk.mjs → blok-CEVrVqlx.mjs} +5 -7
- package/dist/chunks/{constants-Cr7GEExc.mjs → constants-BURnHRy_.mjs} +1 -1
- package/dist/chunks/{tools-DBEfU2dP.mjs → tools-Dt2I14vP.mjs} +1029 -885
- package/dist/full.mjs +3 -3
- package/dist/react.mjs +2 -2
- package/dist/tools.mjs +2 -2
- package/package.json +3 -6
- package/src/cli/commands/convert-gdocs/index.ts +24 -0
- package/src/cli/commands/convert-html/block-builder.ts +98 -88
- package/src/cli/commands/convert-html/preprocessor.ts +141 -67
- package/src/cli/commands/convert-html/sanitizer.ts +34 -35
- package/src/cli/index.ts +27 -1
- package/src/components/modules/toolbar/index.ts +5 -6
- package/src/tools/table/index.ts +136 -0
- package/src/tools/table/table-add-controls.ts +31 -2
- package/src/tools/table/table-cell-blocks.ts +13 -3
- package/src/tools/table/table-corner-drag.ts +247 -0
- package/src/types-internal/jsdom.d.ts +9 -0
- package/bin/blok.mjs +0 -10
- package/bin/convert-html.mjs +0 -3
- package/dist/convert-html.mjs +0 -631
- package/src/cli/commands/convert-html/standalone.ts +0 -20
|
@@ -40,6 +40,33 @@ export function sanitize(wrapper: HTMLElement): void {
|
|
|
40
40
|
sanitizeNode(wrapper);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Unwrap a disallowed element: move its children before it, remove it,
|
|
45
|
+
* and return any element children so they can be re-queued for processing.
|
|
46
|
+
*/
|
|
47
|
+
function unwrapElement(el: HTMLElement): ChildNode[] {
|
|
48
|
+
const grandchildren = Array.from(el.childNodes);
|
|
49
|
+
|
|
50
|
+
for (const gc of grandchildren) {
|
|
51
|
+
el.before(gc);
|
|
52
|
+
}
|
|
53
|
+
el.remove();
|
|
54
|
+
|
|
55
|
+
return grandchildren.filter((gc) => gc.nodeType === gc.ELEMENT_NODE);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Strip attributes from an element that are not in the allowed set.
|
|
60
|
+
* When `allowedAttrs` is `true`, all attributes are removed.
|
|
61
|
+
*/
|
|
62
|
+
function stripAttributes(el: HTMLElement, allowedAttrs: Set<string> | true): void {
|
|
63
|
+
for (const attr of Array.from(el.attributes)) {
|
|
64
|
+
if (allowedAttrs === true || !allowedAttrs.has(attr.name)) {
|
|
65
|
+
el.removeAttribute(attr.name);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
43
70
|
function sanitizeNode(node: Node): void {
|
|
44
71
|
// Use a live-like approach: collect children, then process each.
|
|
45
72
|
// When a child is unwrapped its grandchildren are inserted in place and
|
|
@@ -52,43 +79,15 @@ function sanitizeNode(node: Node): void {
|
|
|
52
79
|
}
|
|
53
80
|
|
|
54
81
|
const el = child as HTMLElement;
|
|
55
|
-
const
|
|
56
|
-
const allowedAttrs = ALLOWED[tag];
|
|
82
|
+
const allowedAttrs = ALLOWED[el.tagName];
|
|
57
83
|
|
|
58
84
|
if (allowedAttrs === undefined) {
|
|
59
|
-
// Unwrap
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
for (const gc of grandchildren) {
|
|
63
|
-
el.before(gc);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
el.remove();
|
|
67
|
-
|
|
68
|
-
// Push the moved grandchildren onto the queue so they are evaluated
|
|
69
|
-
// for unwrapping / attribute-stripping in the same parent context.
|
|
70
|
-
for (const gc of grandchildren) {
|
|
71
|
-
if (gc.nodeType === gc.ELEMENT_NODE) {
|
|
72
|
-
queue.push(gc);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
} else {
|
|
76
|
-
// Strip disallowed attributes.
|
|
77
|
-
if (allowedAttrs !== true) {
|
|
78
|
-
for (const attr of Array.from(el.attributes)) {
|
|
79
|
-
if (!allowedAttrs.has(attr.name)) {
|
|
80
|
-
el.removeAttribute(attr.name);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
} else {
|
|
84
|
-
// true means no attributes allowed — strip all.
|
|
85
|
-
for (const attr of Array.from(el.attributes)) {
|
|
86
|
-
el.removeAttribute(attr.name);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Recurse into children.
|
|
91
|
-
sanitizeNode(el);
|
|
85
|
+
// Unwrap disallowed tag and re-queue its element children.
|
|
86
|
+
queue.push(...unwrapElement(el));
|
|
87
|
+
continue;
|
|
92
88
|
}
|
|
89
|
+
|
|
90
|
+
stripAttributes(el, allowedAttrs);
|
|
91
|
+
sanitizeNode(el);
|
|
93
92
|
}
|
|
94
93
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ const HELP_TEXT = `Usage: blok-cli [options]
|
|
|
5
5
|
|
|
6
6
|
Options:
|
|
7
7
|
--convert-html Convert legacy HTML from stdin to Blok JSON (stdout)
|
|
8
|
+
--convert-gdocs Convert Google Docs HTML from stdin to Blok JSON (stdout)
|
|
8
9
|
--migration Output the EditorJS to Blok migration guide (LLM-friendly)
|
|
9
10
|
--output <file> Write output to a file instead of stdout
|
|
10
11
|
--help Show this help message
|
|
@@ -12,6 +13,8 @@ Options:
|
|
|
12
13
|
Examples:
|
|
13
14
|
npx @jackuait/blok-cli --convert-html < article.html
|
|
14
15
|
npx @jackuait/blok-cli --convert-html < article.html --output article.json
|
|
16
|
+
npx @jackuait/blok-cli --convert-gdocs < gdocs-export.html
|
|
17
|
+
npx @jackuait/blok-cli --convert-gdocs < gdocs-export.html --output doc.json
|
|
15
18
|
npx @jackuait/blok-cli --migration
|
|
16
19
|
npx @jackuait/blok-cli --migration | pbcopy
|
|
17
20
|
npx @jackuait/blok-cli --migration --output migration-guide.md
|
|
@@ -29,6 +32,13 @@ const parseArgs = (argv: string[]): { command: string | null; output?: string }
|
|
|
29
32
|
return { command: 'convert-html', output };
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
if (argv.includes('--convert-gdocs')) {
|
|
36
|
+
const outputIndex = argv.indexOf('--output');
|
|
37
|
+
const output = outputIndex !== -1 ? argv[outputIndex + 1] : undefined;
|
|
38
|
+
|
|
39
|
+
return { command: 'convert-gdocs', output };
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
if (argv.includes('--migration')) {
|
|
33
43
|
const outputIndex = argv.indexOf('--output');
|
|
34
44
|
const output = outputIndex !== -1 ? argv[outputIndex + 1] : undefined;
|
|
@@ -44,7 +54,7 @@ export const run = async (argv: string[], version: string): Promise<void> => {
|
|
|
44
54
|
|
|
45
55
|
switch (command) {
|
|
46
56
|
case 'convert-html': {
|
|
47
|
-
const jsdom = await import('jsdom')
|
|
57
|
+
const jsdom = await import('jsdom');
|
|
48
58
|
const dom = new jsdom.JSDOM('');
|
|
49
59
|
|
|
50
60
|
globalThis.DOMParser = dom.window.DOMParser;
|
|
@@ -58,6 +68,22 @@ export const run = async (argv: string[], version: string): Promise<void> => {
|
|
|
58
68
|
writeOutput(json, output);
|
|
59
69
|
break;
|
|
60
70
|
}
|
|
71
|
+
case 'convert-gdocs': {
|
|
72
|
+
const jsdom = await import('jsdom');
|
|
73
|
+
const dom = new jsdom.JSDOM('');
|
|
74
|
+
|
|
75
|
+
globalThis.DOMParser = dom.window.DOMParser;
|
|
76
|
+
globalThis.Node = dom.window.Node;
|
|
77
|
+
(globalThis as Record<string, unknown>).document = dom.window.document;
|
|
78
|
+
|
|
79
|
+
const { convertGdocs } = await import('./commands/convert-gdocs/index');
|
|
80
|
+
const fs = await import('node:fs');
|
|
81
|
+
const html = fs.readFileSync('/dev/stdin', 'utf-8');
|
|
82
|
+
const json = convertGdocs(html);
|
|
83
|
+
|
|
84
|
+
writeOutput(json, output);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
61
87
|
case 'migration': {
|
|
62
88
|
const content = getMigrationDoc(version);
|
|
63
89
|
|
|
@@ -526,13 +526,14 @@ export class Toolbar extends Module<ToolbarNodes> {
|
|
|
526
526
|
* so toolbar buttons align with the block content edge, even when
|
|
527
527
|
* consumer CSS overrides the block content's margin.
|
|
528
528
|
*
|
|
529
|
-
*
|
|
529
|
+
* Uses the block's actual marginLeft directly — when content is left-aligned
|
|
530
|
+
* (marginLeft: 0), toolbar actions will extend into the editor's left gutter
|
|
531
|
+
* via negative positioning (right: 100%), which is the expected behavior.
|
|
530
532
|
*/
|
|
531
533
|
if (blockContentElement && this.nodes.content) {
|
|
532
534
|
const blockMarginLeft = parseFloat(getComputedStyle(blockContentElement).marginLeft) || 0;
|
|
533
|
-
const actionsWidth = this.nodes.actions?.offsetWidth ?? 0;
|
|
534
535
|
|
|
535
|
-
this.nodes.content.style.marginLeft = `${
|
|
536
|
+
this.nodes.content.style.marginLeft = `${blockMarginLeft}px`;
|
|
536
537
|
}
|
|
537
538
|
}
|
|
538
539
|
|
|
@@ -654,13 +655,11 @@ export class Toolbar extends Module<ToolbarNodes> {
|
|
|
654
655
|
|
|
655
656
|
/**
|
|
656
657
|
* Sync toolbar content wrapper's margin with the block content element.
|
|
657
|
-
* Runs after open() so the toolbar is visible and actions have correct offsetWidth.
|
|
658
658
|
*/
|
|
659
659
|
if (blockContentElement && this.nodes.content) {
|
|
660
660
|
const blockMarginLeft = parseFloat(getComputedStyle(blockContentElement).marginLeft) || 0;
|
|
661
|
-
const actionsWidth = this.nodes.actions?.offsetWidth ?? 0;
|
|
662
661
|
|
|
663
|
-
this.nodes.content.style.marginLeft = `${
|
|
662
|
+
this.nodes.content.style.marginLeft = `${blockMarginLeft}px`;
|
|
664
663
|
}
|
|
665
664
|
}
|
|
666
665
|
|
package/src/tools/table/index.ts
CHANGED
|
@@ -54,6 +54,7 @@ import type { PendingHighlight } from './table-row-col-action-handler';
|
|
|
54
54
|
import { TableRowColControls } from './table-row-col-controls';
|
|
55
55
|
import type { RowColAction } from './table-row-col-controls';
|
|
56
56
|
import { registerAdditionalRestrictedTools } from './table-restrictions';
|
|
57
|
+
import { TableCornerDrag } from './table-corner-drag';
|
|
57
58
|
import { TableScrollHaze } from './table-scroll-haze';
|
|
58
59
|
import type { CellPlacement, ClipboardBlockData, LegacyCellContent, TableCellsClipboard, TableData, TableConfig } from './types';
|
|
59
60
|
import { isCellWithBlocks } from './types';
|
|
@@ -94,6 +95,7 @@ export class Table implements BlockTool {
|
|
|
94
95
|
private rowColControls: TableRowColControls | null = null;
|
|
95
96
|
private cellBlocks: TableCellBlocks | null = null;
|
|
96
97
|
private cellSelection: TableCellSelection | null = null;
|
|
98
|
+
private cornerDrag: TableCornerDrag | null = null;
|
|
97
99
|
private scrollHaze: TableScrollHaze | null = null;
|
|
98
100
|
private element: HTMLDivElement | null = null;
|
|
99
101
|
private gridElement: HTMLElement | null = null;
|
|
@@ -196,6 +198,8 @@ export class Table implements BlockTool {
|
|
|
196
198
|
this.resize = null;
|
|
197
199
|
this.addControls?.destroy();
|
|
198
200
|
this.addControls = null;
|
|
201
|
+
this.cornerDrag?.destroy();
|
|
202
|
+
this.cornerDrag = null;
|
|
199
203
|
this.rowColControls?.destroy();
|
|
200
204
|
this.rowColControls = null;
|
|
201
205
|
this.cellSelection?.destroy();
|
|
@@ -333,6 +337,7 @@ export class Table implements BlockTool {
|
|
|
333
337
|
private initSubsystems(gridEl: HTMLElement): void {
|
|
334
338
|
this.initResize(gridEl);
|
|
335
339
|
this.initAddControls(gridEl);
|
|
340
|
+
this.initCornerDrag(gridEl);
|
|
336
341
|
this.initRowColControls(gridEl);
|
|
337
342
|
this.initCellSelection(gridEl);
|
|
338
343
|
this.initGridPasteListener(gridEl);
|
|
@@ -989,6 +994,10 @@ export class Table implements BlockTool {
|
|
|
989
994
|
wrapper: this.element,
|
|
990
995
|
grid: gridEl,
|
|
991
996
|
i18n: this.api.i18n,
|
|
997
|
+
getTableSize: () => ({
|
|
998
|
+
rows: this.model.rows,
|
|
999
|
+
cols: this.model.cols,
|
|
1000
|
+
}),
|
|
992
1001
|
getNewColumnWidth: () => {
|
|
993
1002
|
const colWidths = this.model.colWidths ?? readPixelWidths(gridEl);
|
|
994
1003
|
|
|
@@ -1129,6 +1138,131 @@ export class Table implements BlockTool {
|
|
|
1129
1138
|
}
|
|
1130
1139
|
}
|
|
1131
1140
|
|
|
1141
|
+
private initCornerDrag(gridEl: HTMLElement): void {
|
|
1142
|
+
this.cornerDrag?.destroy();
|
|
1143
|
+
|
|
1144
|
+
if (!this.element) {
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
this.cornerDrag = new TableCornerDrag({
|
|
1149
|
+
wrapper: this.element,
|
|
1150
|
+
gridEl,
|
|
1151
|
+
onAddRow: () => {
|
|
1152
|
+
this.runStructuralOp(() => {
|
|
1153
|
+
this.grid.addRow(gridEl);
|
|
1154
|
+
this.model.addRow();
|
|
1155
|
+
populateNewCells(gridEl, this.cellBlocks);
|
|
1156
|
+
updateHeadingStyles(this.gridElement, this.model.withHeadings);
|
|
1157
|
+
updateHeadingColumnStyles(this.gridElement, this.model.withHeadingColumn);
|
|
1158
|
+
});
|
|
1159
|
+
},
|
|
1160
|
+
onAddColumn: () => {
|
|
1161
|
+
this.runStructuralOp(() => {
|
|
1162
|
+
const colWidths = this.model.colWidths ?? readPixelWidths(gridEl);
|
|
1163
|
+
const halfWidth = this.model.initialColWidth !== undefined
|
|
1164
|
+
? Math.round((this.model.initialColWidth / 2) * 100) / 100
|
|
1165
|
+
: computeHalfAvgWidth(colWidths);
|
|
1166
|
+
const newWidths = [...colWidths, halfWidth];
|
|
1167
|
+
|
|
1168
|
+
this.grid.addColumn(gridEl, undefined, colWidths, halfWidth);
|
|
1169
|
+
this.model.addColumn(undefined, halfWidth);
|
|
1170
|
+
this.model.setColWidths(newWidths);
|
|
1171
|
+
applyPixelWidths(gridEl, newWidths);
|
|
1172
|
+
enableScrollOverflow(this.ensureScrollContainer());
|
|
1173
|
+
populateNewCells(gridEl, this.cellBlocks);
|
|
1174
|
+
updateHeadingColumnStyles(this.gridElement, this.model.withHeadingColumn);
|
|
1175
|
+
});
|
|
1176
|
+
},
|
|
1177
|
+
onRemoveLastRow: () => {
|
|
1178
|
+
this.runStructuralOp(() => {
|
|
1179
|
+
const rowCount = this.grid.getRowCount(gridEl);
|
|
1180
|
+
|
|
1181
|
+
if (rowCount <= 1) {
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
const { blocksToDelete } = this.model.deleteRow(rowCount - 1);
|
|
1186
|
+
|
|
1187
|
+
this.cellBlocks?.deleteBlocks(blocksToDelete);
|
|
1188
|
+
this.grid.deleteRow(gridEl, rowCount - 1);
|
|
1189
|
+
});
|
|
1190
|
+
},
|
|
1191
|
+
onRemoveLastColumn: () => {
|
|
1192
|
+
this.runStructuralOp(() => {
|
|
1193
|
+
const colCount = this.grid.getColumnCount(gridEl);
|
|
1194
|
+
|
|
1195
|
+
if (colCount <= 1) {
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
const { blocksToDelete } = this.model.deleteColumn(colCount - 1);
|
|
1200
|
+
|
|
1201
|
+
this.cellBlocks?.deleteBlocks(blocksToDelete);
|
|
1202
|
+
this.grid.deleteColumn(gridEl, colCount - 1);
|
|
1203
|
+
|
|
1204
|
+
const updatedWidths = this.model.colWidths;
|
|
1205
|
+
|
|
1206
|
+
if (updatedWidths) {
|
|
1207
|
+
applyPixelWidths(gridEl, updatedWidths);
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
},
|
|
1211
|
+
onDragStart: () => {
|
|
1212
|
+
if (this.resize) {
|
|
1213
|
+
this.resize.enabled = false;
|
|
1214
|
+
}
|
|
1215
|
+
this.rowColControls?.hideAllGrips();
|
|
1216
|
+
this.rowColControls?.setGripsDisplay(false);
|
|
1217
|
+
this.addControls?.setDisplay(false);
|
|
1218
|
+
},
|
|
1219
|
+
onDragEnd: () => {
|
|
1220
|
+
this.initResize(gridEl);
|
|
1221
|
+
this.rowColControls?.refresh();
|
|
1222
|
+
this.addControls?.setDisplay(true);
|
|
1223
|
+
this.addControls?.syncRowButtonWidth();
|
|
1224
|
+
},
|
|
1225
|
+
getTableSize: () => {
|
|
1226
|
+
return { rows: this.model.rows, cols: this.model.cols };
|
|
1227
|
+
},
|
|
1228
|
+
canRemoveLastRow: () => {
|
|
1229
|
+
return this.model.rows > 1 && isRowEmpty(gridEl, this.model.rows - 1);
|
|
1230
|
+
},
|
|
1231
|
+
canRemoveLastColumn: () => {
|
|
1232
|
+
return this.model.cols > 1 && isColumnEmpty(gridEl, this.model.cols - 1);
|
|
1233
|
+
},
|
|
1234
|
+
onClickAdd: () => {
|
|
1235
|
+
this.runTransactedStructuralOp(() => {
|
|
1236
|
+
// Add row
|
|
1237
|
+
this.grid.addRow(gridEl);
|
|
1238
|
+
this.model.addRow();
|
|
1239
|
+
populateNewCells(gridEl, this.cellBlocks);
|
|
1240
|
+
updateHeadingStyles(this.gridElement, this.model.withHeadings);
|
|
1241
|
+
updateHeadingColumnStyles(this.gridElement, this.model.withHeadingColumn);
|
|
1242
|
+
|
|
1243
|
+
// Add column
|
|
1244
|
+
const colWidths = this.model.colWidths ?? readPixelWidths(gridEl);
|
|
1245
|
+
const halfWidth = this.model.initialColWidth !== undefined
|
|
1246
|
+
? Math.round((this.model.initialColWidth / 2) * 100) / 100
|
|
1247
|
+
: computeHalfAvgWidth(colWidths);
|
|
1248
|
+
const newWidths = [...colWidths, halfWidth];
|
|
1249
|
+
|
|
1250
|
+
this.grid.addColumn(gridEl, undefined, colWidths, halfWidth);
|
|
1251
|
+
this.model.addColumn(undefined, halfWidth);
|
|
1252
|
+
this.model.setColWidths(newWidths);
|
|
1253
|
+
applyPixelWidths(gridEl, newWidths);
|
|
1254
|
+
populateNewCells(gridEl, this.cellBlocks);
|
|
1255
|
+
updateHeadingColumnStyles(this.gridElement, this.model.withHeadingColumn);
|
|
1256
|
+
|
|
1257
|
+
// Refresh subsystems
|
|
1258
|
+
this.initResize(gridEl);
|
|
1259
|
+
this.rowColControls?.refresh();
|
|
1260
|
+
this.addControls?.syncRowButtonWidth();
|
|
1261
|
+
});
|
|
1262
|
+
},
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1132
1266
|
private initRowColControls(gridEl: HTMLElement): void {
|
|
1133
1267
|
this.rowColControls?.destroy();
|
|
1134
1268
|
|
|
@@ -1152,6 +1286,7 @@ export class Table implements BlockTool {
|
|
|
1152
1286
|
}
|
|
1153
1287
|
|
|
1154
1288
|
this.addControls?.setDisplay(!isDragging);
|
|
1289
|
+
this.cornerDrag?.setDisplay(!isDragging);
|
|
1155
1290
|
|
|
1156
1291
|
if (isDragging) {
|
|
1157
1292
|
this.api.toolbar.close({ setExplicitlyClosed: false });
|
|
@@ -1531,6 +1666,7 @@ export class Table implements BlockTool {
|
|
|
1531
1666
|
}
|
|
1532
1667
|
|
|
1533
1668
|
this.addControls?.setInteractive(!hasSelection);
|
|
1669
|
+
this.cornerDrag?.setInteractive(!hasSelection);
|
|
1534
1670
|
this.rowColControls?.setGripsDisplay(!hasSelection);
|
|
1535
1671
|
},
|
|
1536
1672
|
onSelectionRangeChange: () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { I18n } from '../../../types/api';
|
|
2
2
|
import { IconPlus } from '../../components/icons';
|
|
3
3
|
import { createTooltipContent } from '../../components/modules/toolbar/tooltip';
|
|
4
|
-
import { hide as hideTooltip, onHover } from '../../components/utils/tooltip';
|
|
4
|
+
import { hide as hideTooltip, onHover, show as showTooltip } from '../../components/utils/tooltip';
|
|
5
5
|
import { twMerge } from '../../components/utils/tw';
|
|
6
6
|
|
|
7
7
|
const ADD_ROW_ATTR = 'data-blok-table-add-row';
|
|
@@ -56,6 +56,7 @@ interface TableAddControlsOptions {
|
|
|
56
56
|
onDragAddCol: () => void;
|
|
57
57
|
onDragRemoveCol: () => void;
|
|
58
58
|
onDragEnd: () => void;
|
|
59
|
+
getTableSize: () => { rows: number; cols: number };
|
|
59
60
|
/** Returns the pixel width of a newly added column, used as the drag unit size. */
|
|
60
61
|
getNewColumnWidth?: () => number;
|
|
61
62
|
}
|
|
@@ -94,6 +95,7 @@ export class TableAddControls {
|
|
|
94
95
|
private boundPointerCancel: (e: PointerEvent) => void;
|
|
95
96
|
private boundRowPointerDown: (e: PointerEvent) => void;
|
|
96
97
|
private boundColPointerDown: (e: PointerEvent) => void;
|
|
98
|
+
private getTableSize: () => { rows: number; cols: number };
|
|
97
99
|
private getNewColumnWidth: (() => number) | undefined;
|
|
98
100
|
private scrollContainer: HTMLElement | null = null;
|
|
99
101
|
private boundScrollHandler: (() => void) | null = null;
|
|
@@ -112,6 +114,7 @@ export class TableAddControls {
|
|
|
112
114
|
this.onDragAddCol = options.onDragAddCol;
|
|
113
115
|
this.onDragRemoveCol = options.onDragRemoveCol;
|
|
114
116
|
this.onDragEnd = options.onDragEnd;
|
|
117
|
+
this.getTableSize = options.getTableSize;
|
|
115
118
|
this.getNewColumnWidth = options.getNewColumnWidth;
|
|
116
119
|
this.boundMouseMove = this.handleMouseMove.bind(this);
|
|
117
120
|
this.boundDocumentMouseMove = this.handleDocumentMouseMove.bind(this);
|
|
@@ -324,6 +327,20 @@ export class TableAddControls {
|
|
|
324
327
|
this.addColBtn.remove();
|
|
325
328
|
}
|
|
326
329
|
|
|
330
|
+
private showDimensionTooltip(): void {
|
|
331
|
+
if (!this.dragState) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const size = this.getTableSize();
|
|
336
|
+
const target = this.dragState.axis === 'row' ? this.addRowBtn : this.addColBtn;
|
|
337
|
+
const opts = this.dragState.axis === 'row'
|
|
338
|
+
? { placement: 'bottom' as const, marginTop: -16 }
|
|
339
|
+
: { placement: 'bottom' as const };
|
|
340
|
+
|
|
341
|
+
showTooltip(target, `${size.cols}\u00D7${size.rows}`, opts);
|
|
342
|
+
}
|
|
343
|
+
|
|
327
344
|
private handlePointerDown(axis: 'row' | 'col', e: PointerEvent): void {
|
|
328
345
|
e.preventDefault();
|
|
329
346
|
|
|
@@ -380,8 +397,18 @@ export class TableAddControls {
|
|
|
380
397
|
if (Math.abs(delta) > DRAG_THRESHOLD && !this.dragState.didDrag) {
|
|
381
398
|
this.dragState.didDrag = true;
|
|
382
399
|
document.body.style.cursor = axis === 'row' ? 'row-resize' : 'col-resize';
|
|
383
|
-
|
|
400
|
+
this.showDimensionTooltip();
|
|
384
401
|
this.onDragStart();
|
|
402
|
+
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (this.dragState.didDrag) {
|
|
407
|
+
this.showDimensionTooltip();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (this.dragState.didDrag) {
|
|
411
|
+
this.showDimensionTooltip();
|
|
385
412
|
}
|
|
386
413
|
}
|
|
387
414
|
|
|
@@ -400,6 +427,7 @@ export class TableAddControls {
|
|
|
400
427
|
target.removeEventListener('pointercancel', this.boundPointerCancel);
|
|
401
428
|
|
|
402
429
|
document.body.style.cursor = '';
|
|
430
|
+
hideTooltip();
|
|
403
431
|
this.dragState = null;
|
|
404
432
|
|
|
405
433
|
if (!didDrag) {
|
|
@@ -431,6 +459,7 @@ export class TableAddControls {
|
|
|
431
459
|
target.removeEventListener('pointercancel', this.boundPointerCancel);
|
|
432
460
|
|
|
433
461
|
document.body.style.cursor = '';
|
|
462
|
+
hideTooltip();
|
|
434
463
|
this.dragState = null;
|
|
435
464
|
|
|
436
465
|
if (didDrag) {
|
|
@@ -1053,7 +1053,11 @@ export class TableCellBlocks {
|
|
|
1053
1053
|
}
|
|
1054
1054
|
|
|
1055
1055
|
/**
|
|
1056
|
-
* Delete blocks by their IDs (in reverse index order to avoid shifting issues)
|
|
1056
|
+
* Delete blocks by their IDs (in reverse index order to avoid shifting issues).
|
|
1057
|
+
* Preserves scroll position because api.blocks.delete() is async — its internal
|
|
1058
|
+
* `await` defers Caret.setToBlock() to microtasks that run AFTER this method returns,
|
|
1059
|
+
* causing unwanted page jumps via element.focus() and window.scrollBy().
|
|
1060
|
+
* We use Promise.all().then() to schedule the scroll restore after all those microtasks.
|
|
1057
1061
|
*/
|
|
1058
1062
|
public deleteBlocks(blockIds: string[]): void {
|
|
1059
1063
|
const blockIndices = blockIds
|
|
@@ -1061,8 +1065,14 @@ export class TableCellBlocks {
|
|
|
1061
1065
|
.filter((index): index is number => index !== undefined)
|
|
1062
1066
|
.sort((a, b) => b - a);
|
|
1063
1067
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1068
|
+
const savedScrollY = window.scrollY;
|
|
1069
|
+
|
|
1070
|
+
const deletePromises = blockIndices.map(index => this.api.blocks.delete(index));
|
|
1071
|
+
|
|
1072
|
+
void Promise.all(deletePromises).then(() => {
|
|
1073
|
+
if (window.scrollY !== savedScrollY) {
|
|
1074
|
+
window.scrollTo(0, savedScrollY);
|
|
1075
|
+
}
|
|
1066
1076
|
});
|
|
1067
1077
|
}
|
|
1068
1078
|
|