@jackuait/blok 0.10.0-beta.9 → 0.10.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/dist/blok.mjs +2 -2
- package/dist/chunks/{blok-DDu252IK.mjs → blok-BfcBwAfE.mjs} +1211 -1159
- package/dist/chunks/{constants-DMW9a31I.mjs → constants-QNVyXALL.mjs} +49 -48
- package/dist/chunks/{tools-XmzH2rgQ.mjs → tools-DHtzbrxy.mjs} +1403 -1220
- package/dist/full.mjs +3 -3
- package/dist/react.mjs +2 -2
- package/dist/tools.mjs +2 -2
- package/package.json +3 -5
- package/src/cli/commands/convert-gdocs/index.ts +26 -0
- package/src/cli/commands/convert-html/block-builder.ts +392 -0
- package/src/cli/commands/convert-html/id-generator.ts +11 -0
- package/src/cli/commands/convert-html/index.ts +23 -0
- package/src/cli/commands/convert-html/preprocessor.ts +422 -0
- package/src/cli/commands/convert-html/sanitizer.ts +93 -0
- package/src/cli/commands/convert-html/types.ts +15 -0
- package/src/cli/index.ts +56 -5
- package/src/components/block/index.ts +44 -10
- package/src/components/constants/data-attributes.ts +10 -0
- package/src/components/icons/index.ts +16 -0
- package/src/components/modules/blockEvents/composers/keyboardNavigation.ts +18 -0
- package/src/components/modules/blockManager/hierarchy.ts +4 -1
- package/src/components/modules/readonly.ts +46 -0
- package/src/components/modules/rectangleSelection.ts +25 -5
- package/src/components/modules/toolbar/index.ts +96 -19
- package/src/components/modules/toolbar/styles.ts +0 -2
- package/src/components/modules/uiControllers/controllers/blockHover.ts +44 -1
- package/src/components/tools/block.ts +10 -0
- package/src/components/utils/placeholder.ts +9 -2
- package/src/styles/main.css +16 -0
- package/src/tools/callout/constants.ts +2 -1
- package/src/tools/callout/dom-builder.ts +13 -1
- package/src/tools/callout/index.ts +21 -7
- package/src/tools/code/constants.ts +9 -1
- package/src/tools/code/dom-builder.ts +90 -54
- package/src/tools/code/index.ts +73 -31
- package/src/tools/divider/index.ts +5 -0
- package/src/tools/header/index.ts +47 -1
- package/src/tools/list/dom-builder.ts +3 -1
- package/src/tools/list/index.ts +55 -3
- package/src/tools/list/list-helpers.ts +2 -2
- package/src/tools/nested-blocks.ts +25 -0
- package/src/tools/paragraph/index.ts +47 -6
- package/src/tools/quote/index.ts +43 -8
- package/src/tools/stub/index.ts +10 -0
- package/src/tools/table/index.ts +238 -6
- package/src/tools/table/table-add-controls.ts +37 -5
- package/src/tools/table/table-cell-blocks.ts +57 -18
- package/src/tools/table/table-core.ts +2 -0
- package/src/tools/table/table-corner-drag.ts +247 -0
- package/src/tools/table/table-operations.ts +41 -14
- package/src/tools/toggle/dom-builder.ts +1 -0
- package/src/tools/toggle/index.ts +25 -0
- package/src/tools/toggle/toggle-lifecycle.ts +5 -4
- package/src/types-internal/jsdom.d.ts +9 -0
- package/types/tools/adapters/block-tool-adapter.d.ts +6 -0
- package/types/tools/block-tool.d.ts +10 -0
- package/bin/blok.mjs +0 -10
- package/dist/cli.mjs +0 -37
- package/src/tools/code/language-picker.ts +0 -241
|
@@ -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';
|
|
@@ -29,7 +29,7 @@ const VISUAL_CLASSES = [
|
|
|
29
29
|
'justify-center',
|
|
30
30
|
'border',
|
|
31
31
|
'border-gray-300',
|
|
32
|
-
'rounded-
|
|
32
|
+
'rounded-sm',
|
|
33
33
|
'group-hover/add:bg-gray-50',
|
|
34
34
|
];
|
|
35
35
|
|
|
@@ -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) {
|
|
@@ -495,8 +524,8 @@ export class TableAddControls {
|
|
|
495
524
|
* Document-level mousemove handler.
|
|
496
525
|
* Catches mouse movements outside the wrapper (e.g. in the ::after
|
|
497
526
|
* pseudo-element zone below the grid, which has pointer-events-none).
|
|
498
|
-
*
|
|
499
|
-
*
|
|
527
|
+
* Delegates to handleMouseMove when the cursor is within the proximity
|
|
528
|
+
* zone around the grid; schedules hiding when the cursor is far away.
|
|
500
529
|
*/
|
|
501
530
|
private handleDocumentMouseMove(e: MouseEvent): void {
|
|
502
531
|
if (this.wrapper.contains(e.target as Node)) {
|
|
@@ -513,6 +542,9 @@ export class TableAddControls {
|
|
|
513
542
|
|
|
514
543
|
if (nearGrid) {
|
|
515
544
|
this.handleMouseMove(e);
|
|
545
|
+
} else {
|
|
546
|
+
this.scheduleHideRow();
|
|
547
|
+
this.scheduleHideCol();
|
|
516
548
|
}
|
|
517
549
|
}
|
|
518
550
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { API } from '../../../types';
|
|
2
|
+
import { DATA_ATTR } from '../../components/constants/data-attributes';
|
|
2
3
|
|
|
3
4
|
import { CELL_ATTR, ROW_ATTR, CELL_COL_ATTR } from './table-core';
|
|
4
5
|
import type { TableModel } from './table-model';
|
|
@@ -7,15 +8,6 @@ import { isCellWithBlocks } from './types';
|
|
|
7
8
|
|
|
8
9
|
export const CELL_BLOCKS_ATTR = 'data-blok-table-cell-blocks';
|
|
9
10
|
|
|
10
|
-
/**
|
|
11
|
-
* Check if an element is inside a block-based table cell
|
|
12
|
-
*/
|
|
13
|
-
export const isInCellBlock = (element: HTMLElement): boolean => {
|
|
14
|
-
const cellBlocksContainer = element.closest(`[${CELL_BLOCKS_ATTR}]`);
|
|
15
|
-
|
|
16
|
-
return cellBlocksContainer !== null;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
11
|
/**
|
|
20
12
|
* Get the cell element that contains the given element
|
|
21
13
|
*/
|
|
@@ -390,9 +382,9 @@ export class TableCellBlocks {
|
|
|
390
382
|
const referencedBlockIds = isCellWithBlocks(cellContent) && cellContent.blocks.length > 0
|
|
391
383
|
? [...cellContent.blocks]
|
|
392
384
|
: null;
|
|
393
|
-
const mountedIds = referencedBlockIds
|
|
385
|
+
const { mountedIds, replacements } = referencedBlockIds
|
|
394
386
|
? this.mountBlocksInCell(container, referencedBlockIds)
|
|
395
|
-
: [];
|
|
387
|
+
: { mountedIds: [] as string[], replacements: new Map<string, string>() };
|
|
396
388
|
|
|
397
389
|
const cellColorProps: Pick<CellContent, 'color' | 'textColor'> = {};
|
|
398
390
|
|
|
@@ -406,7 +398,12 @@ export class TableCellBlocks {
|
|
|
406
398
|
}
|
|
407
399
|
|
|
408
400
|
if (mountedIds.length > 0) {
|
|
409
|
-
|
|
401
|
+
const baseIds = referencedBlockIds ?? mountedIds;
|
|
402
|
+
const blockIds = replacements.size > 0
|
|
403
|
+
? baseIds.map(id => replacements.get(id) ?? id)
|
|
404
|
+
: baseIds;
|
|
405
|
+
|
|
406
|
+
normalizedRow.push({ blocks: blockIds, ...cellColorProps });
|
|
410
407
|
} else {
|
|
411
408
|
const text = typeof cellContent === 'string'
|
|
412
409
|
? cellContent
|
|
@@ -455,10 +452,15 @@ export class TableCellBlocks {
|
|
|
455
452
|
|
|
456
453
|
/**
|
|
457
454
|
* Mount existing blocks into a cell container by their IDs.
|
|
458
|
-
* Returns the IDs of blocks that were successfully mounted
|
|
455
|
+
* Returns the IDs of blocks that were successfully mounted and a map of
|
|
456
|
+
* original→duplicate IDs for blocks that were already in another cell.
|
|
459
457
|
*/
|
|
460
|
-
private mountBlocksInCell(
|
|
458
|
+
private mountBlocksInCell(
|
|
459
|
+
container: HTMLElement,
|
|
460
|
+
blockIds: string[]
|
|
461
|
+
): { mountedIds: string[]; replacements: Map<string, string> } {
|
|
461
462
|
const mountedIds: string[] = [];
|
|
463
|
+
const replacements = new Map<string, string>();
|
|
462
464
|
|
|
463
465
|
for (const blockId of blockIds) {
|
|
464
466
|
const index = this.api.blocks.getBlockIndex(blockId);
|
|
@@ -473,11 +475,30 @@ export class TableCellBlocks {
|
|
|
473
475
|
continue;
|
|
474
476
|
}
|
|
475
477
|
|
|
478
|
+
// Guard: if the block is already mounted in another nested container
|
|
479
|
+
// (table cell, toggle, callout, header), create a duplicate with the
|
|
480
|
+
// same tool name and data rather than stealing the DOM node.
|
|
481
|
+
if (block.holder.closest(`[${DATA_ATTR.nestedBlocks}]`)) {
|
|
482
|
+
const duplicate = this.api.blocks.insert(
|
|
483
|
+
block.name,
|
|
484
|
+
block.preservedData,
|
|
485
|
+
{},
|
|
486
|
+
this.api.blocks.getBlocksCount(),
|
|
487
|
+
false
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
container.appendChild(duplicate.holder);
|
|
491
|
+
this.api.blocks.setBlockParent(duplicate.id, this.tableBlockId);
|
|
492
|
+
mountedIds.push(duplicate.id);
|
|
493
|
+
replacements.set(blockId, duplicate.id);
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
|
|
476
497
|
container.appendChild(block.holder);
|
|
477
498
|
this.api.blocks.setBlockParent(blockId, this.tableBlockId);
|
|
478
499
|
mountedIds.push(blockId);
|
|
479
500
|
}
|
|
480
|
-
return mountedIds;
|
|
501
|
+
return { mountedIds, replacements };
|
|
481
502
|
}
|
|
482
503
|
|
|
483
504
|
/**
|
|
@@ -508,6 +529,12 @@ export class TableCellBlocks {
|
|
|
508
529
|
return;
|
|
509
530
|
}
|
|
510
531
|
|
|
532
|
+
// Guard: skip blocks already mounted in another nested container.
|
|
533
|
+
// Without this, insertBefore would steal the DOM node from the other container.
|
|
534
|
+
if (block.holder.closest(`[${DATA_ATTR.nestedBlocks}]`)) {
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
511
538
|
// Insert at the correct DOM position based on the flat array order,
|
|
512
539
|
// so that pressing Enter on a non-last paragraph inserts the new block
|
|
513
540
|
// right after the current one instead of always at the end of the cell.
|
|
@@ -1026,7 +1053,11 @@ export class TableCellBlocks {
|
|
|
1026
1053
|
}
|
|
1027
1054
|
|
|
1028
1055
|
/**
|
|
1029
|
-
* 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.
|
|
1030
1061
|
*/
|
|
1031
1062
|
public deleteBlocks(blockIds: string[]): void {
|
|
1032
1063
|
const blockIndices = blockIds
|
|
@@ -1034,8 +1065,16 @@ export class TableCellBlocks {
|
|
|
1034
1065
|
.filter((index): index is number => index !== undefined)
|
|
1035
1066
|
.sort((a, b) => b - a);
|
|
1036
1067
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1068
|
+
const savedScrollY = window.scrollY;
|
|
1069
|
+
|
|
1070
|
+
const deletePromises = blockIndices.map(index => {
|
|
1071
|
+
return this.api.blocks.delete(index);
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
void Promise.all(deletePromises).then(() => {
|
|
1075
|
+
if (window.scrollY !== savedScrollY) {
|
|
1076
|
+
window.scrollTo(0, savedScrollY);
|
|
1077
|
+
}
|
|
1039
1078
|
});
|
|
1040
1079
|
}
|
|
1041
1080
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { twMerge } from '../../components/utils/tw';
|
|
2
|
+
import { DATA_ATTR } from '../../components/constants/data-attributes';
|
|
2
3
|
|
|
3
4
|
import { CELL_BLOCKS_ATTR } from './table-cell-blocks';
|
|
4
5
|
import type { TableModel } from './table-model';
|
|
@@ -631,6 +632,7 @@ export class TableGrid {
|
|
|
631
632
|
const blocksContainer = document.createElement('div');
|
|
632
633
|
|
|
633
634
|
blocksContainer.setAttribute(CELL_BLOCKS_ATTR, '');
|
|
635
|
+
blocksContainer.setAttribute(DATA_ATTR.nestedBlocks, '');
|
|
634
636
|
blocksContainer.style.display = 'flex';
|
|
635
637
|
blocksContainer.style.flexDirection = 'column';
|
|
636
638
|
blocksContainer.style.minHeight = '100%';
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { show as showTooltip, hide as hideTooltip } from '../../components/utils/tooltip';
|
|
2
|
+
|
|
3
|
+
const CORNER_DRAG_ATTR = 'data-blok-table-corner-drag';
|
|
4
|
+
|
|
5
|
+
export interface TableCornerDragOptions {
|
|
6
|
+
wrapper: HTMLElement;
|
|
7
|
+
gridEl: HTMLElement;
|
|
8
|
+
onAddRow: () => void;
|
|
9
|
+
onAddColumn: () => void;
|
|
10
|
+
onRemoveLastRow: () => void;
|
|
11
|
+
onRemoveLastColumn: () => void;
|
|
12
|
+
onDragStart: () => void;
|
|
13
|
+
onDragEnd: () => void;
|
|
14
|
+
getTableSize: () => { rows: number; cols: number };
|
|
15
|
+
canRemoveLastRow: () => boolean;
|
|
16
|
+
canRemoveLastColumn: () => boolean;
|
|
17
|
+
onClickAdd?: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DRAG_THRESHOLD = 5;
|
|
21
|
+
|
|
22
|
+
interface DragState {
|
|
23
|
+
startX: number;
|
|
24
|
+
startY: number;
|
|
25
|
+
unitWidth: number;
|
|
26
|
+
unitHeight: number;
|
|
27
|
+
addedRows: number;
|
|
28
|
+
addedCols: number;
|
|
29
|
+
pointerId: number;
|
|
30
|
+
didDrag: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class TableCornerDrag {
|
|
34
|
+
private wrapper: HTMLElement;
|
|
35
|
+
private gridEl: HTMLElement;
|
|
36
|
+
private hitZone: HTMLElement;
|
|
37
|
+
private getTableSize: () => { rows: number; cols: number };
|
|
38
|
+
private onAddRow: () => void;
|
|
39
|
+
private onAddColumn: () => void;
|
|
40
|
+
private onRemoveLastRow: () => void;
|
|
41
|
+
private onRemoveLastColumn: () => void;
|
|
42
|
+
private onDragStart: () => void;
|
|
43
|
+
private onDragEnd: () => void;
|
|
44
|
+
private canRemoveLastRow: () => boolean;
|
|
45
|
+
private canRemoveLastColumn: () => boolean;
|
|
46
|
+
private onClickAdd: (() => void) | null;
|
|
47
|
+
private dragState: DragState | null = null;
|
|
48
|
+
private readonly boundMouseEnter: () => void;
|
|
49
|
+
private readonly boundMouseLeave: () => void;
|
|
50
|
+
private readonly boundPointerDown: (e: PointerEvent) => void;
|
|
51
|
+
private readonly boundPointerMove: (e: PointerEvent) => void;
|
|
52
|
+
private readonly boundPointerUp: (e: PointerEvent) => void;
|
|
53
|
+
|
|
54
|
+
constructor(options: TableCornerDragOptions) {
|
|
55
|
+
this.wrapper = options.wrapper;
|
|
56
|
+
this.gridEl = options.gridEl;
|
|
57
|
+
this.getTableSize = options.getTableSize;
|
|
58
|
+
this.onAddRow = options.onAddRow;
|
|
59
|
+
this.onAddColumn = options.onAddColumn;
|
|
60
|
+
this.onRemoveLastRow = options.onRemoveLastRow;
|
|
61
|
+
this.onRemoveLastColumn = options.onRemoveLastColumn;
|
|
62
|
+
this.onDragStart = options.onDragStart;
|
|
63
|
+
this.onDragEnd = options.onDragEnd;
|
|
64
|
+
this.canRemoveLastRow = options.canRemoveLastRow;
|
|
65
|
+
this.canRemoveLastColumn = options.canRemoveLastColumn;
|
|
66
|
+
this.onClickAdd = options.onClickAdd ?? null;
|
|
67
|
+
|
|
68
|
+
this.hitZone = document.createElement('div');
|
|
69
|
+
this.hitZone.setAttribute(CORNER_DRAG_ATTR, '');
|
|
70
|
+
this.hitZone.setAttribute('contenteditable', 'false');
|
|
71
|
+
this.hitZone.style.position = 'absolute';
|
|
72
|
+
this.hitZone.style.width = '36px';
|
|
73
|
+
this.hitZone.style.height = '36px';
|
|
74
|
+
this.hitZone.style.cursor = 'nwse-resize';
|
|
75
|
+
this.hitZone.style.zIndex = '2';
|
|
76
|
+
this.hitZone.style.pointerEvents = 'auto';
|
|
77
|
+
this.hitZone.style.bottom = '-36px';
|
|
78
|
+
this.hitZone.style.right = '-16px';
|
|
79
|
+
|
|
80
|
+
this.boundMouseEnter = this.handleMouseEnter.bind(this);
|
|
81
|
+
this.boundMouseLeave = this.handleMouseLeave.bind(this);
|
|
82
|
+
this.boundPointerDown = this.handlePointerDown.bind(this);
|
|
83
|
+
this.boundPointerMove = this.handlePointerMove.bind(this);
|
|
84
|
+
this.boundPointerUp = this.handlePointerUp.bind(this);
|
|
85
|
+
|
|
86
|
+
this.hitZone.addEventListener('mouseenter', this.boundMouseEnter);
|
|
87
|
+
this.hitZone.addEventListener('mouseleave', this.boundMouseLeave);
|
|
88
|
+
this.hitZone.addEventListener('pointerdown', this.boundPointerDown);
|
|
89
|
+
|
|
90
|
+
this.wrapper.appendChild(this.hitZone);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private updateTooltip(): void {
|
|
94
|
+
const size = this.getTableSize();
|
|
95
|
+
|
|
96
|
+
showTooltip(this.hitZone, `${size.cols}\u00D7${size.rows}`, { placement: 'bottom' });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private handleMouseEnter(): void {
|
|
100
|
+
this.updateTooltip();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private handleMouseLeave(): void {
|
|
104
|
+
if (this.dragState !== null) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
hideTooltip();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private measureUnitHeight(): number {
|
|
111
|
+
const rows = this.gridEl.querySelectorAll('[data-blok-table-row]');
|
|
112
|
+
const lastRow = rows[rows.length - 1] as HTMLElement | undefined;
|
|
113
|
+
|
|
114
|
+
return lastRow?.offsetHeight || 30;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private measureUnitWidth(): number {
|
|
118
|
+
const firstRow = this.gridEl.querySelector('[data-blok-table-row]');
|
|
119
|
+
|
|
120
|
+
if (!firstRow) {
|
|
121
|
+
return 100;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const cells = firstRow.querySelectorAll('[data-blok-table-cell]');
|
|
125
|
+
const lastCell = cells[cells.length - 1] as HTMLElement | undefined;
|
|
126
|
+
|
|
127
|
+
return lastCell?.offsetWidth || 100;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private handlePointerDown(e: PointerEvent): void {
|
|
131
|
+
this.dragState = {
|
|
132
|
+
startX: e.clientX,
|
|
133
|
+
startY: e.clientY,
|
|
134
|
+
unitWidth: this.measureUnitWidth(),
|
|
135
|
+
unitHeight: this.measureUnitHeight(),
|
|
136
|
+
addedRows: 0,
|
|
137
|
+
addedCols: 0,
|
|
138
|
+
pointerId: e.pointerId,
|
|
139
|
+
didDrag: false,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
this.updateTooltip();
|
|
143
|
+
|
|
144
|
+
this.hitZone.setPointerCapture(e.pointerId);
|
|
145
|
+
this.hitZone.addEventListener('pointermove', this.boundPointerMove);
|
|
146
|
+
this.hitZone.addEventListener('pointerup', this.boundPointerUp);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private handlePointerMove(e: PointerEvent): void {
|
|
150
|
+
if (this.dragState === null) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const dx = e.clientX - this.dragState.startX;
|
|
155
|
+
const dy = e.clientY - this.dragState.startY;
|
|
156
|
+
|
|
157
|
+
if (!this.dragState.didDrag) {
|
|
158
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
159
|
+
|
|
160
|
+
if (distance < DRAG_THRESHOLD) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.dragState.didDrag = true;
|
|
165
|
+
document.body.style.cursor = 'nwse-resize';
|
|
166
|
+
document.body.style.userSelect = 'none';
|
|
167
|
+
this.onDragStart();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const { unitHeight, unitWidth } = this.dragState;
|
|
171
|
+
|
|
172
|
+
const targetRows = Math.trunc(dy / unitHeight);
|
|
173
|
+
const targetCols = Math.trunc(dx / unitWidth);
|
|
174
|
+
|
|
175
|
+
while (this.dragState.addedRows < targetRows) {
|
|
176
|
+
this.onAddRow();
|
|
177
|
+
this.dragState.addedRows++;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
while (this.dragState.addedRows > targetRows && this.canRemoveLastRow()) {
|
|
181
|
+
this.onRemoveLastRow();
|
|
182
|
+
this.dragState.addedRows--;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
while (this.dragState.addedCols < targetCols) {
|
|
186
|
+
this.onAddColumn();
|
|
187
|
+
this.dragState.addedCols++;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
while (this.dragState.addedCols > targetCols && this.canRemoveLastColumn()) {
|
|
191
|
+
this.onRemoveLastColumn();
|
|
192
|
+
this.dragState.addedCols--;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.updateTooltip();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private handlePointerUp(_e: PointerEvent): void {
|
|
199
|
+
if (this.dragState === null) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const { didDrag, pointerId } = this.dragState;
|
|
204
|
+
|
|
205
|
+
this.dragState = null;
|
|
206
|
+
hideTooltip();
|
|
207
|
+
this.hitZone.releasePointerCapture(pointerId);
|
|
208
|
+
this.hitZone.removeEventListener('pointermove', this.boundPointerMove);
|
|
209
|
+
this.hitZone.removeEventListener('pointerup', this.boundPointerUp);
|
|
210
|
+
|
|
211
|
+
if (!didDrag) {
|
|
212
|
+
if (this.onClickAdd) {
|
|
213
|
+
this.onClickAdd();
|
|
214
|
+
} else {
|
|
215
|
+
this.onAddRow();
|
|
216
|
+
this.onAddColumn();
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
document.body.style.cursor = '';
|
|
220
|
+
document.body.style.userSelect = '';
|
|
221
|
+
this.onDragEnd();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
public setDisplay(visible: boolean): void {
|
|
226
|
+
this.hitZone.style.display = visible ? '' : 'none';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
public setInteractive(interactive: boolean): void {
|
|
230
|
+
this.hitZone.style.pointerEvents = interactive ? 'auto' : 'none';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
public destroy(): void {
|
|
234
|
+
this.hitZone.removeEventListener('mouseenter', this.boundMouseEnter);
|
|
235
|
+
this.hitZone.removeEventListener('mouseleave', this.boundMouseLeave);
|
|
236
|
+
this.hitZone.removeEventListener('pointerdown', this.boundPointerDown);
|
|
237
|
+
this.hitZone.removeEventListener('pointermove', this.boundPointerMove);
|
|
238
|
+
this.hitZone.removeEventListener('pointerup', this.boundPointerUp);
|
|
239
|
+
if (this.dragState?.didDrag) {
|
|
240
|
+
document.body.style.cursor = '';
|
|
241
|
+
document.body.style.userSelect = '';
|
|
242
|
+
}
|
|
243
|
+
this.dragState = null;
|
|
244
|
+
hideTooltip();
|
|
245
|
+
this.hitZone.remove();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { API } from '../../../types';
|
|
2
|
+
import { DATA_ATTR } from '../../components/constants/data-attributes';
|
|
2
3
|
|
|
3
4
|
import type { TableCellBlocks } from './table-cell-blocks';
|
|
4
5
|
import { CELL_BLOCKS_ATTR } from './table-cell-blocks';
|
|
@@ -288,16 +289,28 @@ export const mountCellBlocksReadOnly = (
|
|
|
288
289
|
|
|
289
290
|
if (!isCellWithBlocks(cellContent)) {
|
|
290
291
|
// Read-only render path must not mutate block state.
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
|
|
292
|
+
// Wrap in a div with leading-[1.5] so the line-height matches paragraph
|
|
293
|
+
// blocks used in edit mode (where legacy strings are converted to real
|
|
294
|
+
// paragraph blocks with that line-height). Without this wrapper, the
|
|
295
|
+
// text inherits the cell's leading-none, producing shorter cells.
|
|
296
|
+
const wrapper = document.createElement('div');
|
|
297
|
+
|
|
298
|
+
wrapper.className = 'leading-[1.5]';
|
|
299
|
+
wrapper.innerHTML = cellContent;
|
|
300
|
+
container.replaceChildren(wrapper);
|
|
294
301
|
|
|
295
302
|
return;
|
|
296
303
|
}
|
|
297
304
|
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
|
|
305
|
+
// Clear the container before (re-)mounting block holders.
|
|
306
|
+
// This covers two cases:
|
|
307
|
+
// 1. Legacy text was previously rendered and needs to be replaced with blocks.
|
|
308
|
+
// 2. Block holders are already mounted (e.g. from edit mode before a
|
|
309
|
+
// setReadOnly toggle) — without clearing, the clone-guard below would
|
|
310
|
+
// duplicate every holder because it detects them inside a
|
|
311
|
+
// [data-blok-nested-blocks] container and appends a cloneNode(true).
|
|
312
|
+
if (hasExistingBlocks || (container.textContent ?? '').length > 0) {
|
|
313
|
+
container.replaceChildren();
|
|
301
314
|
}
|
|
302
315
|
|
|
303
316
|
for (const blockId of cellContent.blocks) {
|
|
@@ -313,12 +326,20 @@ export const mountCellBlocksReadOnly = (
|
|
|
313
326
|
continue;
|
|
314
327
|
}
|
|
315
328
|
|
|
329
|
+
// Skip blocks that don't belong to this table.
|
|
330
|
+
// Corrupted data may contain cross-table references; mounting them
|
|
331
|
+
// would steal (or clone) DOM nodes from the other table.
|
|
332
|
+
if (block.parentId !== _tableBlockId) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
316
336
|
// Guard: if the block holder is already inside another table cell's
|
|
317
|
-
// blocks container,
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
if (block.holder.closest(`[${
|
|
337
|
+
// blocks container, clone its visual content instead of moving (stealing)
|
|
338
|
+
// the DOM node. This can happen when corrupted data references the same
|
|
339
|
+
// block in multiple tables. In read-only mode a deep clone is safe
|
|
340
|
+
// because the content is non-interactive.
|
|
341
|
+
if (block.holder.closest(`[${DATA_ATTR.nestedBlocks}]`)) {
|
|
342
|
+
container.appendChild(block.holder.cloneNode(true));
|
|
322
343
|
continue;
|
|
323
344
|
}
|
|
324
345
|
|
|
@@ -374,8 +395,8 @@ export const normalizeTableData = (
|
|
|
374
395
|
export const setupKeyboardNavigation = (
|
|
375
396
|
gridEl: HTMLElement,
|
|
376
397
|
cellBlocks: TableCellBlocks | null,
|
|
377
|
-
): void => {
|
|
378
|
-
|
|
398
|
+
): (() => void) => {
|
|
399
|
+
const handler = (event: KeyboardEvent): void => {
|
|
379
400
|
const target = event.target as HTMLElement;
|
|
380
401
|
const cell = target.closest<HTMLElement>(`[${CELL_ATTR}]`);
|
|
381
402
|
|
|
@@ -388,7 +409,13 @@ export const setupKeyboardNavigation = (
|
|
|
388
409
|
if (position) {
|
|
389
410
|
cellBlocks?.handleKeyDown(event, position);
|
|
390
411
|
}
|
|
391
|
-
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
gridEl.addEventListener('keydown', handler);
|
|
415
|
+
|
|
416
|
+
return () => {
|
|
417
|
+
gridEl.removeEventListener('keydown', handler);
|
|
418
|
+
};
|
|
392
419
|
};
|
|
393
420
|
|
|
394
421
|
export const SCROLL_OVERFLOW_CLASSES = ['overflow-x-auto', 'overflow-y-hidden'];
|
|
@@ -88,6 +88,7 @@ export const buildToggleItem = (context: ToggleDOMBuilderContext): ToggleBuildRe
|
|
|
88
88
|
const childContainerElement = document.createElement('div');
|
|
89
89
|
childContainerElement.className = TOGGLE_CHILDREN_STYLES;
|
|
90
90
|
childContainerElement.setAttribute(TOGGLE_ATTR.toggleChildren, '');
|
|
91
|
+
childContainerElement.setAttribute(DATA_ATTR.nestedBlocks, '');
|
|
91
92
|
// Block DOM mutations inside the children container from triggering the toggle tool's
|
|
92
93
|
// didMutated → syncBlockDataToYjs path. Child block insertions/removals are tracked
|
|
93
94
|
// via the block hierarchy (parentId / contentIds) and must not create spurious Yjs
|
|
@@ -229,6 +229,31 @@ export class ToggleItem implements BlockTool {
|
|
|
229
229
|
this.setOpenState(false);
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
public setReadOnly(state: boolean): void {
|
|
233
|
+
if (!this._element) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const wasReadOnly = this.readOnly;
|
|
238
|
+
|
|
239
|
+
this.readOnly = state;
|
|
240
|
+
|
|
241
|
+
// Toggle contentEditable on the content element
|
|
242
|
+
if (this._contentElement) {
|
|
243
|
+
this._contentElement.contentEditable = state ? 'false' : 'true';
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Manage block changed event subscription
|
|
247
|
+
if (state && !wasReadOnly) {
|
|
248
|
+
this.api.events.off('block changed', this.handleBlockChanged);
|
|
249
|
+
} else if (!state && wasReadOnly) {
|
|
250
|
+
this.api.events.on('block changed', this.handleBlockChanged);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Update body placeholder visibility (hidden in read-only mode)
|
|
254
|
+
this.updateBodyPlaceholderVisibility();
|
|
255
|
+
}
|
|
256
|
+
|
|
232
257
|
public removed(): void {
|
|
233
258
|
this.api.events.off('block changed', this.handleBlockChanged);
|
|
234
259
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { API } from '../../../types';
|
|
8
8
|
|
|
9
|
+
import { mountChildBlocks } from '../nested-blocks';
|
|
9
10
|
import { setupPlaceholder } from '../../components/utils/placeholder';
|
|
10
11
|
|
|
11
12
|
import { TOGGLE_ATTR } from './constants';
|
|
@@ -94,11 +95,11 @@ export const updateChildrenVisibility = (
|
|
|
94
95
|
arrowElement.focus();
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
98
|
+
if (childContainer) {
|
|
99
|
+
mountChildBlocks(childContainer, children);
|
|
100
|
+
}
|
|
101
101
|
|
|
102
|
+
for (const child of children) {
|
|
102
103
|
if (isOpen) {
|
|
103
104
|
child.holder.classList.remove('hidden');
|
|
104
105
|
} else {
|