@jackuait/blok 0.10.0-beta.15 → 0.10.0-beta.17
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-Cntb4VEa.mjs} +754 -744
- package/dist/chunks/{constants-Cr7GEExc.mjs → constants-DgIAOJXe.mjs} +1 -1
- package/dist/chunks/{tools-DBEfU2dP.mjs → tools-CfRkozOl.mjs} +1038 -888
- 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 +26 -0
- package/src/cli/commands/convert-html/block-builder.ts +98 -88
- package/src/cli/commands/convert-html/index.ts +3 -1
- 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/blockEvents/composers/keyboardNavigation.ts +18 -0
- package/src/components/modules/rectangleSelection.ts +25 -5
- package/src/components/modules/toolbar/index.ts +44 -4
- package/src/tools/callout/constants.ts +2 -1
- package/src/tools/callout/dom-builder.ts +11 -1
- package/src/tools/callout/index.ts +6 -0
- 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 +15 -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
|
|
|
@@ -344,6 +344,15 @@ export class KeyboardNavigation extends BlockEventComposer {
|
|
|
344
344
|
// Previous sibling exists in the same parent — fall through to merge/remove logic
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Don't merge across table cell boundaries.
|
|
349
|
+
* When the caret is at the start of the first input in a table cell, Backspace should be a no-op
|
|
350
|
+
* rather than merging the previous cell's last block into the current cell.
|
|
351
|
+
*/
|
|
352
|
+
if (this.isCurrentBlockInsideTableCell) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
347
356
|
/**
|
|
348
357
|
* Backspace at the start of the first Block should do nothing
|
|
349
358
|
*/
|
|
@@ -447,6 +456,15 @@ export class KeyboardNavigation extends BlockEventComposer {
|
|
|
447
456
|
return;
|
|
448
457
|
}
|
|
449
458
|
|
|
459
|
+
/**
|
|
460
|
+
* Don't merge across table cell boundaries.
|
|
461
|
+
* When the caret is at the end of the last input in a table cell, Delete should be a no-op
|
|
462
|
+
* rather than merging the next cell's first block into the current cell.
|
|
463
|
+
*/
|
|
464
|
+
if (this.isCurrentBlockInsideTableCell) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
450
468
|
/**
|
|
451
469
|
* If next Block is empty, it should be removed just like a character
|
|
452
470
|
*/
|
|
@@ -72,6 +72,14 @@ export class RectangleSelection extends Module {
|
|
|
72
72
|
*/
|
|
73
73
|
private mouseDownWithinBoundsFromContentEditable = false;
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Set when the user mousedowns in a position that should eventually close the toolbar
|
|
77
|
+
* if a drag begins, but NOT immediately (to avoid interfering with button clicks like
|
|
78
|
+
* the plus button). The toolbar is closed on the first mousemove after mousedown.
|
|
79
|
+
* Cleared on mouseup / endSelection.
|
|
80
|
+
*/
|
|
81
|
+
private pendingToolbarClose = false;
|
|
82
|
+
|
|
75
83
|
/**
|
|
76
84
|
* Is scrolling now
|
|
77
85
|
*/
|
|
@@ -187,7 +195,7 @@ export class RectangleSelection extends Module {
|
|
|
187
195
|
|
|
188
196
|
const selectorsToAvoid = [
|
|
189
197
|
createSelector(DATA_ATTR.elementContent),
|
|
190
|
-
createSelector(DATA_ATTR.
|
|
198
|
+
createSelector(DATA_ATTR.settingsToggler),
|
|
191
199
|
createSelector(DATA_ATTR.popover),
|
|
192
200
|
INLINE_TOOLBAR_INTERFACE_SELECTOR,
|
|
193
201
|
];
|
|
@@ -202,12 +210,13 @@ export class RectangleSelection extends Module {
|
|
|
202
210
|
}
|
|
203
211
|
|
|
204
212
|
/**
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
213
|
+
* Schedule toolbar close for the first mousemove (i.e. when the user actually drags).
|
|
214
|
+
* Deferring prevents a plain click (e.g. on the plus button) from accidentally closing
|
|
215
|
+
* the toolbar before its own click handler fires.
|
|
216
|
+
* Only close if starting within the horizontal bounds of the editor.
|
|
208
217
|
*/
|
|
209
218
|
if (withinEditorHorizontally) {
|
|
210
|
-
this.
|
|
219
|
+
this.pendingToolbarClose = true;
|
|
211
220
|
}
|
|
212
221
|
|
|
213
222
|
this.mousedown = true;
|
|
@@ -221,6 +230,7 @@ export class RectangleSelection extends Module {
|
|
|
221
230
|
public endSelection(): void {
|
|
222
231
|
this.mousedown = false;
|
|
223
232
|
this.mouseDownWithinBoundsFromContentEditable = false;
|
|
233
|
+
this.pendingToolbarClose = false;
|
|
224
234
|
this.startX = 0;
|
|
225
235
|
this.startY = 0;
|
|
226
236
|
this.anchorBlockIndex = null;
|
|
@@ -361,6 +371,16 @@ export class RectangleSelection extends Module {
|
|
|
361
371
|
this.Blok.Toolbar.close();
|
|
362
372
|
}
|
|
363
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Close the toolbar on the first actual drag movement after a mousedown
|
|
376
|
+
* that was scheduled to close it. Using mousemove instead of mousedown ensures
|
|
377
|
+
* plain button clicks (e.g. the plus button) don't close the toolbar prematurely.
|
|
378
|
+
*/
|
|
379
|
+
if (this.pendingToolbarClose) {
|
|
380
|
+
this.pendingToolbarClose = false;
|
|
381
|
+
this.Blok.Toolbar.close();
|
|
382
|
+
}
|
|
383
|
+
|
|
364
384
|
this.changingRectangle(mouseEvent);
|
|
365
385
|
this.scrollByZones(mouseEvent.clientY);
|
|
366
386
|
}
|
|
@@ -427,8 +427,11 @@ export class Toolbar extends Module<ToolbarNodes> {
|
|
|
427
427
|
*/
|
|
428
428
|
const focusIsInsideCell = this.isFocusInsideTableCell();
|
|
429
429
|
const isCalloutFirstChild = this.isFirstChildOfCallout(targetBlock);
|
|
430
|
+
const isCalloutBlock = targetBlock.name === 'callout';
|
|
430
431
|
|
|
431
|
-
|
|
432
|
+
// Hide plus button for callout blocks and their first children to avoid
|
|
433
|
+
// overlap with the callout emoji icon in the left padding area.
|
|
434
|
+
plusButton.style.display = (isCalloutFirstChild || isCalloutBlock) ? 'none' : '';
|
|
432
435
|
|
|
433
436
|
if (settingsToggler) {
|
|
434
437
|
settingsToggler.style.display = (focusIsInsideCell || isCalloutFirstChild) ? 'none' : '';
|
|
@@ -519,6 +522,7 @@ export class Toolbar extends Module<ToolbarNodes> {
|
|
|
519
522
|
|
|
520
523
|
if (hasLeftEdgeInteraction && this.nodes.actions) {
|
|
521
524
|
this.nodes.actions.style.pointerEvents = 'none';
|
|
525
|
+
this.restoreSettingsTogglerForLeftEdgeBlock(targetBlock);
|
|
522
526
|
}
|
|
523
527
|
|
|
524
528
|
/**
|
|
@@ -526,7 +530,9 @@ export class Toolbar extends Module<ToolbarNodes> {
|
|
|
526
530
|
* so toolbar buttons align with the block content edge, even when
|
|
527
531
|
* consumer CSS overrides the block content's margin.
|
|
528
532
|
*
|
|
529
|
-
*
|
|
533
|
+
* Uses Math.max to guarantee the actions container (positioned via right:100%)
|
|
534
|
+
* never extends beyond the left edge of the viewport, which would make the
|
|
535
|
+
* drag handle unreachable by pointer events.
|
|
530
536
|
*/
|
|
531
537
|
if (blockContentElement && this.nodes.content) {
|
|
532
538
|
const blockMarginLeft = parseFloat(getComputedStyle(blockContentElement).marginLeft) || 0;
|
|
@@ -654,7 +660,7 @@ export class Toolbar extends Module<ToolbarNodes> {
|
|
|
654
660
|
|
|
655
661
|
/**
|
|
656
662
|
* Sync toolbar content wrapper's margin with the block content element.
|
|
657
|
-
*
|
|
663
|
+
* Clamp to actionsWidth so actions never extend beyond the left viewport edge.
|
|
658
664
|
*/
|
|
659
665
|
if (blockContentElement && this.nodes.content) {
|
|
660
666
|
const blockMarginLeft = parseFloat(getComputedStyle(blockContentElement).marginLeft) || 0;
|
|
@@ -852,14 +858,48 @@ export class Toolbar extends Module<ToolbarNodes> {
|
|
|
852
858
|
|
|
853
859
|
const focusIsInsideCell = this.isFocusInsideTableCell();
|
|
854
860
|
const isCalloutFirstChild = this.hoveredBlock !== null && this.isFirstChildOfCallout(this.hoveredBlock);
|
|
861
|
+
const isCalloutBlock = this.hoveredBlock?.name === 'callout';
|
|
855
862
|
|
|
856
|
-
plusButton.style.display = isCalloutFirstChild ? 'none' : '';
|
|
863
|
+
plusButton.style.display = (isCalloutFirstChild || isCalloutBlock) ? 'none' : '';
|
|
857
864
|
|
|
858
865
|
if (settingsToggler) {
|
|
859
866
|
settingsToggler.style.display = (focusIsInsideCell || isCalloutFirstChild) ? 'none' : '';
|
|
860
867
|
}
|
|
861
868
|
}
|
|
862
869
|
|
|
870
|
+
/**
|
|
871
|
+
* Re-enables pointer-events on the settings toggler (and callout drag zone) after
|
|
872
|
+
* the actions container has been set to pointer-events: none for left-edge blocks.
|
|
873
|
+
*
|
|
874
|
+
* For callout blocks: also wires the dedicated drag zone as the drag handle and
|
|
875
|
+
* re-enables the settings toggler so the settings menu remains accessible.
|
|
876
|
+
* The emoji button (at x=32px) no longer overlaps the actions zone (x=[0,29px])
|
|
877
|
+
* because the callout uses pl-8 (32px) left padding.
|
|
878
|
+
*
|
|
879
|
+
* For all other left-edge blocks (toggle, header with arrow): simply re-enables the
|
|
880
|
+
* settings toggler so it continues to function as the drag handle.
|
|
881
|
+
*/
|
|
882
|
+
private restoreSettingsTogglerForLeftEdgeBlock(targetBlock: Block): void {
|
|
883
|
+
if (targetBlock.name === 'callout') {
|
|
884
|
+
if (this.nodes.settingsToggler) {
|
|
885
|
+
this.nodes.settingsToggler.style.pointerEvents = 'auto';
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const calloutDragZone = targetBlock.holder.querySelector<HTMLElement>('[data-callout-drag-zone]');
|
|
889
|
+
|
|
890
|
+
if (calloutDragZone) {
|
|
891
|
+
calloutDragZone.style.pointerEvents = 'auto';
|
|
892
|
+
targetBlock.setupDraggable(calloutDragZone, this.Blok.DragManager);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (this.nodes.settingsToggler) {
|
|
899
|
+
this.nodes.settingsToggler.style.pointerEvents = 'auto';
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
863
903
|
/**
|
|
864
904
|
* If the block is inside a table cell, resolve to the parent table block.
|
|
865
905
|
* This ensures the toolbar shows for the table when clicking/focusing inside cells.
|
|
@@ -25,7 +25,8 @@ export const EMOJI_CATEGORY_FLAGS_KEY = 'tools.callout.emojiCategoryFlags';
|
|
|
25
25
|
export const DEFAULT_EMOJI = '💡';
|
|
26
26
|
|
|
27
27
|
// CSS — Tailwind classes
|
|
28
|
-
export const WRAPPER_STYLES = 'rounded-xl
|
|
28
|
+
export const WRAPPER_STYLES = 'rounded-xl pl-8 pr-4 py-[5px] my-1 flex items-start gap-2 relative';
|
|
29
29
|
// h-[38px] = py-[7px]×2 + 1.5rem×1 = 14+24; explicit height prevents platform-specific emoji font metrics from inflating the button
|
|
30
30
|
export const EMOJI_BUTTON_STYLES = 'text-[1.5rem] leading-[1] cursor-pointer bg-transparent border-0 px-0 py-[7px] h-[38px] flex-shrink-0 select-none';
|
|
31
31
|
export const CHILDREN_STYLES = 'flex-1 min-w-0';
|
|
32
|
+
export const DRAG_ZONE_STYLES = 'absolute left-0 top-0 h-full cursor-grab select-none';
|
|
@@ -6,12 +6,14 @@ import {
|
|
|
6
6
|
WRAPPER_STYLES,
|
|
7
7
|
EMOJI_BUTTON_STYLES,
|
|
8
8
|
CHILDREN_STYLES,
|
|
9
|
+
DRAG_ZONE_STYLES,
|
|
9
10
|
} from './constants';
|
|
10
11
|
|
|
11
12
|
export interface CalloutDOMRefs {
|
|
12
13
|
wrapper: HTMLElement;
|
|
13
14
|
emojiButton: HTMLButtonElement;
|
|
14
15
|
childContainer: HTMLElement;
|
|
16
|
+
dragZone: HTMLElement;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export interface BuildCalloutDOMOptions {
|
|
@@ -51,5 +53,13 @@ export function buildCalloutDOM(options: BuildCalloutDOMOptions): CalloutDOMRefs
|
|
|
51
53
|
wrapper.appendChild(emojiButton);
|
|
52
54
|
wrapper.appendChild(childContainer);
|
|
53
55
|
|
|
54
|
-
|
|
56
|
+
// Drag zone — covers left padding area (x=[0,16px]) for drag handle,
|
|
57
|
+
// sits behind emoji button so emoji clicks pass through
|
|
58
|
+
const dragZone = document.createElement('span');
|
|
59
|
+
dragZone.className = DRAG_ZONE_STYLES;
|
|
60
|
+
dragZone.style.width = '32px'; // matches pl-8 left padding
|
|
61
|
+
dragZone.setAttribute('data-callout-drag-zone', '');
|
|
62
|
+
wrapper.prepend(dragZone);
|
|
63
|
+
|
|
64
|
+
return { wrapper, emojiButton, childContainer, dragZone };
|
|
55
65
|
}
|
|
@@ -66,6 +66,7 @@ export class CalloutTool implements BlockTool {
|
|
|
66
66
|
private _dom: CalloutDOMRefs | null = null;
|
|
67
67
|
private _emojiPicker: EmojiPicker | null = null;
|
|
68
68
|
private _colorPicker: ColorPickerHandle | null = null;
|
|
69
|
+
private _dragZone: HTMLElement | null = null;
|
|
69
70
|
private blockId?: string;
|
|
70
71
|
|
|
71
72
|
constructor({ data, api, readOnly, block }: BlockToolConstructorOptions<CalloutData, CalloutConfig>) {
|
|
@@ -120,6 +121,7 @@ export class CalloutTool implements BlockTool {
|
|
|
120
121
|
});
|
|
121
122
|
|
|
122
123
|
this._dom = dom;
|
|
124
|
+
this._dragZone = dom.dragZone;
|
|
123
125
|
this.applyColors();
|
|
124
126
|
|
|
125
127
|
if (!this.readOnly) {
|
|
@@ -260,6 +262,10 @@ export class CalloutTool implements BlockTool {
|
|
|
260
262
|
}
|
|
261
263
|
}
|
|
262
264
|
|
|
265
|
+
public get dragZone(): HTMLElement | null {
|
|
266
|
+
return this._dragZone;
|
|
267
|
+
}
|
|
268
|
+
|
|
263
269
|
private syncPickerActiveColors(): void {
|
|
264
270
|
if (this._colorPicker === null) {
|
|
265
271
|
return;
|
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) {
|