@jackuait/blok 0.10.0-beta.8 → 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-CRvF-xVm.mjs → blok-BfcBwAfE.mjs} +1211 -1159
- package/dist/chunks/{constants-BOZ5plBi.mjs → constants-QNVyXALL.mjs} +49 -48
- package/dist/chunks/{tools-CnqCfv2L.mjs → tools-DHtzbrxy.mjs} +1411 -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 +87 -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 +45 -9
- 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
package/src/tools/list/index.ts
CHANGED
|
@@ -52,6 +52,8 @@ export class ListItem implements BlockTool {
|
|
|
52
52
|
private depthValidator: ListDepthValidator;
|
|
53
53
|
private markerCalculator: ListMarkerCalculator;
|
|
54
54
|
private markerManager: OrderedMarkerManager | null;
|
|
55
|
+
private placeholderCleanup: (() => void) | null = null;
|
|
56
|
+
private boundHandleKeyDown: ((event: KeyboardEvent) => void) | null = null;
|
|
55
57
|
|
|
56
58
|
private blockId?: string;
|
|
57
59
|
|
|
@@ -120,16 +122,24 @@ export class ListItem implements BlockTool {
|
|
|
120
122
|
if (this.readOnly) {
|
|
121
123
|
return;
|
|
122
124
|
}
|
|
123
|
-
setupPlaceholder(element, this.placeholder);
|
|
125
|
+
this.placeholderCleanup = setupPlaceholder(element, this.placeholder);
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
public render(): HTMLElement {
|
|
129
|
+
if (this._element) {
|
|
130
|
+
return this._element;
|
|
131
|
+
}
|
|
132
|
+
|
|
127
133
|
const blockIndex = this.blockId
|
|
128
134
|
? this.api.blocks.getBlockIndex(this.blockId) ?? this.api.blocks.getCurrentBlockIndex()
|
|
129
135
|
: this.api.blocks.getCurrentBlockIndex();
|
|
130
136
|
const depth = this._data.depth ?? 0;
|
|
131
137
|
const markerDepth = this.markerCalculator.getVisualDepth(blockIndex, depth);
|
|
132
138
|
|
|
139
|
+
if (!this.boundHandleKeyDown) {
|
|
140
|
+
this.boundHandleKeyDown = this.handleKeyDown.bind(this);
|
|
141
|
+
}
|
|
142
|
+
|
|
133
143
|
this._element = renderListItem({
|
|
134
144
|
data: this._data,
|
|
135
145
|
readOnly: this.readOnly,
|
|
@@ -145,12 +155,54 @@ export class ListItem implements BlockTool {
|
|
|
145
155
|
content.classList.toggle('opacity-60', checked);
|
|
146
156
|
}
|
|
147
157
|
},
|
|
148
|
-
keydownHandler: this.readOnly ? undefined : this.
|
|
158
|
+
keydownHandler: this.readOnly ? undefined : this.boundHandleKeyDown,
|
|
149
159
|
});
|
|
150
160
|
|
|
151
161
|
return this._element;
|
|
152
162
|
}
|
|
153
163
|
|
|
164
|
+
public setReadOnly(state: boolean): void {
|
|
165
|
+
if (!this._element) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.readOnly = state;
|
|
170
|
+
|
|
171
|
+
const content = this.getContentElement();
|
|
172
|
+
|
|
173
|
+
// Toggle contentEditable on content container
|
|
174
|
+
if (content) {
|
|
175
|
+
content.contentEditable = state ? 'false' : 'true';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Toggle checkbox disabled state for checklists
|
|
179
|
+
const checkbox = this._element.querySelector<HTMLInputElement>('input[type="checkbox"]');
|
|
180
|
+
|
|
181
|
+
if (checkbox) {
|
|
182
|
+
checkbox.disabled = state;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Toggle keydown handler and placeholder
|
|
186
|
+
if (state) {
|
|
187
|
+
if (this.boundHandleKeyDown) {
|
|
188
|
+
this._element.removeEventListener('keydown', this.boundHandleKeyDown);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (this.placeholderCleanup) {
|
|
192
|
+
this.placeholderCleanup();
|
|
193
|
+
this.placeholderCleanup = null;
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
if (this.boundHandleKeyDown) {
|
|
197
|
+
this._element.addEventListener('keydown', this.boundHandleKeyDown);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (content) {
|
|
201
|
+
this.placeholderCleanup = setupPlaceholder(content, this.placeholder);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
154
206
|
public rendered(): void {
|
|
155
207
|
this.updateMarkersAfterPositionChange();
|
|
156
208
|
}
|
|
@@ -408,7 +460,7 @@ export class ListItem implements BlockTool {
|
|
|
408
460
|
content.classList.toggle('opacity-60', checked);
|
|
409
461
|
}
|
|
410
462
|
},
|
|
411
|
-
keydownHandler: this.readOnly ? undefined : this.
|
|
463
|
+
keydownHandler: this.readOnly ? undefined : this.boundHandleKeyDown ?? undefined,
|
|
412
464
|
});
|
|
413
465
|
|
|
414
466
|
if (newElement) {
|
|
@@ -19,8 +19,8 @@ export const getContentElement = (
|
|
|
19
19
|
if (!element) return null;
|
|
20
20
|
|
|
21
21
|
if (style === 'checklist') {
|
|
22
|
-
const
|
|
23
|
-
return
|
|
22
|
+
const checklistContent = element.querySelector('[data-blok-testid="list-checklist-content"]');
|
|
23
|
+
return checklistContent instanceof HTMLElement ? checklistContent : null;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const contentContainer = element.querySelector('[data-blok-testid="list-content-container"]');
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { DATA_ATTR } from '../components/constants/data-attributes';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mount child block holders into a container, skipping children that are
|
|
5
|
+
* already in place or claimed by another nested-blocks container.
|
|
6
|
+
*
|
|
7
|
+
* Used by toggle, header, and callout tools to reconcile child holders
|
|
8
|
+
* during the `rendered()` lifecycle hook.
|
|
9
|
+
*/
|
|
10
|
+
export const mountChildBlocks = (
|
|
11
|
+
container: HTMLElement,
|
|
12
|
+
children: { holder: HTMLElement }[],
|
|
13
|
+
): void => {
|
|
14
|
+
for (const child of children) {
|
|
15
|
+
if (child.holder.parentElement === container) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (child.holder.closest(`[${DATA_ATTR.nestedBlocks}]`)) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
container.appendChild(child.holder);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -19,7 +19,7 @@ import type {
|
|
|
19
19
|
import { DATA_ATTR } from '../../components/constants';
|
|
20
20
|
import { IconText } from '../../components/icons';
|
|
21
21
|
import { stripFakeBackgroundElements } from '../../components/utils';
|
|
22
|
-
import { PLACEHOLDER_EMPTY_EDITOR_CLASSES, PLACEHOLDER_FOCUS_ONLY_CLASSES, setupPlaceholder } from '../../components/utils/placeholder';
|
|
22
|
+
import { isContentEmpty, PLACEHOLDER_EMPTY_EDITOR_CLASSES, PLACEHOLDER_FOCUS_ONLY_CLASSES, setupPlaceholder } from '../../components/utils/placeholder';
|
|
23
23
|
import { twMerge } from '../../components/utils/tw';
|
|
24
24
|
|
|
25
25
|
/**
|
|
@@ -117,6 +117,11 @@ export class Paragraph implements BlockTool {
|
|
|
117
117
|
*/
|
|
118
118
|
private readOnly: boolean;
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Cleanup function for placeholder behavior, returned by setupPlaceholder
|
|
122
|
+
*/
|
|
123
|
+
private placeholderCleanup: (() => void) | null = null;
|
|
124
|
+
|
|
120
125
|
/**
|
|
121
126
|
* Placeholder for Paragraph Tool
|
|
122
127
|
*/
|
|
@@ -157,9 +162,7 @@ export class Paragraph implements BlockTool {
|
|
|
157
162
|
this.api = api;
|
|
158
163
|
this.readOnly = readOnly;
|
|
159
164
|
|
|
160
|
-
|
|
161
|
-
this.onKeyUp = this.onKeyUp.bind(this);
|
|
162
|
-
}
|
|
165
|
+
this.onKeyUp = this.onKeyUp.bind(this);
|
|
163
166
|
|
|
164
167
|
this._placeholder = config?.placeholder ?? Paragraph.DEFAULT_PLACEHOLDER;
|
|
165
168
|
this._data = data ?? { text: '' };
|
|
@@ -263,7 +266,7 @@ export class Paragraph implements BlockTool {
|
|
|
263
266
|
if (!this.readOnly) {
|
|
264
267
|
div.contentEditable = 'true';
|
|
265
268
|
div.addEventListener('keyup', this.onKeyUp);
|
|
266
|
-
setupPlaceholder(div, this.api.i18n.t(this._placeholder), 'data-blok-placeholder-active');
|
|
269
|
+
this.placeholderCleanup = setupPlaceholder(div, this.api.i18n.t(this._placeholder), 'data-blok-placeholder-active');
|
|
267
270
|
}
|
|
268
271
|
|
|
269
272
|
return div;
|
|
@@ -275,11 +278,49 @@ export class Paragraph implements BlockTool {
|
|
|
275
278
|
* @returns HTMLDivElement
|
|
276
279
|
*/
|
|
277
280
|
public render(): HTMLDivElement {
|
|
278
|
-
this._element
|
|
281
|
+
if (!this._element) {
|
|
282
|
+
this._element = this.drawView();
|
|
283
|
+
}
|
|
279
284
|
|
|
280
285
|
return this._element;
|
|
281
286
|
}
|
|
282
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Toggle read-only mode in-place without re-rendering the DOM element.
|
|
290
|
+
* Manages contentEditable, keyup listener, placeholder, and empty-content <br>.
|
|
291
|
+
*
|
|
292
|
+
* @param state - true to enter read-only mode, false to exit
|
|
293
|
+
*/
|
|
294
|
+
public setReadOnly(state: boolean): void {
|
|
295
|
+
if (!this._element) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
this.readOnly = state;
|
|
300
|
+
|
|
301
|
+
if (state) {
|
|
302
|
+
this._element.contentEditable = 'false';
|
|
303
|
+
this._element.removeEventListener('keyup', this.onKeyUp);
|
|
304
|
+
|
|
305
|
+
if (this.placeholderCleanup) {
|
|
306
|
+
this.placeholderCleanup();
|
|
307
|
+
this.placeholderCleanup = null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (isContentEmpty(this._element)) {
|
|
311
|
+
this._element.innerHTML = '<br>';
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
this._element.contentEditable = 'true';
|
|
315
|
+
this._element.addEventListener('keyup', this.onKeyUp);
|
|
316
|
+
this.placeholderCleanup = setupPlaceholder(this._element, this.api.i18n.t(this._placeholder), 'data-blok-placeholder-active');
|
|
317
|
+
|
|
318
|
+
if (this._element.innerHTML === '<br>') {
|
|
319
|
+
this._element.innerHTML = '';
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
283
324
|
/**
|
|
284
325
|
* Method that specified how to merge two Text blocks.
|
|
285
326
|
* Called by Editor by backspace at the beginning of the Block
|
package/src/tools/quote/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { MenuConfig } from '../../../types/tools/menu-config';
|
|
|
13
13
|
import { DATA_ATTR } from '../../components/constants';
|
|
14
14
|
import { IconQuote } from '../../components/icons';
|
|
15
15
|
import { stripFakeBackgroundElements } from '../../components/utils';
|
|
16
|
-
import { PLACEHOLDER_FOCUS_ONLY_CLASSES, setupPlaceholder } from '../../components/utils/placeholder';
|
|
16
|
+
import { isContentEmpty, PLACEHOLDER_FOCUS_ONLY_CLASSES, setupPlaceholder } from '../../components/utils/placeholder';
|
|
17
17
|
import { twMerge } from '../../components/utils/tw';
|
|
18
18
|
|
|
19
19
|
export interface QuoteData extends BlockToolData {
|
|
@@ -40,6 +40,7 @@ const LARGE_CLASS = 'text-[1.2em]';
|
|
|
40
40
|
export class Quote implements BlockTool {
|
|
41
41
|
private api: API;
|
|
42
42
|
private readOnly: boolean;
|
|
43
|
+
private placeholderCleanup: (() => void) | null = null;
|
|
43
44
|
private _data: QuoteData;
|
|
44
45
|
private _element: HTMLQuoteElement | null = null;
|
|
45
46
|
|
|
@@ -51,9 +52,7 @@ export class Quote implements BlockTool {
|
|
|
51
52
|
size: data?.size ?? 'default',
|
|
52
53
|
};
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
this.onKeyUp = this.onKeyUp.bind(this);
|
|
56
|
-
}
|
|
55
|
+
this.onKeyUp = this.onKeyUp.bind(this);
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
public onKeyUp(e: KeyboardEvent): void {
|
|
@@ -70,7 +69,7 @@ export class Quote implements BlockTool {
|
|
|
70
69
|
}
|
|
71
70
|
}
|
|
72
71
|
|
|
73
|
-
|
|
72
|
+
private drawView(): HTMLQuoteElement {
|
|
74
73
|
const el = document.createElement('blockquote');
|
|
75
74
|
|
|
76
75
|
el.className = twMerge(
|
|
@@ -91,14 +90,50 @@ export class Quote implements BlockTool {
|
|
|
91
90
|
if (!this.readOnly) {
|
|
92
91
|
el.contentEditable = 'true';
|
|
93
92
|
el.addEventListener('keyup', this.onKeyUp);
|
|
94
|
-
setupPlaceholder(el, this.api.i18n.t(DEFAULT_PLACEHOLDER), 'data-blok-placeholder-active');
|
|
93
|
+
this.placeholderCleanup = setupPlaceholder(el, this.api.i18n.t(DEFAULT_PLACEHOLDER), 'data-blok-placeholder-active');
|
|
95
94
|
}
|
|
96
95
|
|
|
97
|
-
this._element = el;
|
|
98
|
-
|
|
99
96
|
return el;
|
|
100
97
|
}
|
|
101
98
|
|
|
99
|
+
public render(): HTMLQuoteElement {
|
|
100
|
+
if (!this._element) {
|
|
101
|
+
this._element = this.drawView();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return this._element;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public setReadOnly(state: boolean): void {
|
|
108
|
+
if (!this._element) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.readOnly = state;
|
|
113
|
+
|
|
114
|
+
if (state) {
|
|
115
|
+
this._element.contentEditable = 'false';
|
|
116
|
+
this._element.removeEventListener('keyup', this.onKeyUp);
|
|
117
|
+
|
|
118
|
+
if (this.placeholderCleanup) {
|
|
119
|
+
this.placeholderCleanup();
|
|
120
|
+
this.placeholderCleanup = null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (isContentEmpty(this._element)) {
|
|
124
|
+
this._element.innerHTML = '<br>';
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
this._element.contentEditable = 'true';
|
|
128
|
+
this._element.addEventListener('keyup', this.onKeyUp);
|
|
129
|
+
this.placeholderCleanup = setupPlaceholder(this._element, this.api.i18n.t(DEFAULT_PLACEHOLDER), 'data-blok-placeholder-active');
|
|
130
|
+
|
|
131
|
+
if (this._element.innerHTML === '<br>') {
|
|
132
|
+
this._element.innerHTML = '';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
102
137
|
public save(blockContent: HTMLQuoteElement): QuoteData {
|
|
103
138
|
return {
|
|
104
139
|
text: stripFakeBackgroundElements(blockContent.innerHTML),
|
package/src/tools/stub/index.ts
CHANGED
|
@@ -73,6 +73,16 @@ export class Stub implements BlockTool {
|
|
|
73
73
|
return this.savedData;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Toggle read-only mode in-place.
|
|
78
|
+
* Stub has no interactive elements, so this is intentionally a no-op.
|
|
79
|
+
*
|
|
80
|
+
* @param _state - read-only state (unused)
|
|
81
|
+
*/
|
|
82
|
+
public setReadOnly(_state: boolean): void {
|
|
83
|
+
// no-op: stub blocks have no editable content
|
|
84
|
+
}
|
|
85
|
+
|
|
76
86
|
/**
|
|
77
87
|
* Create Tool html markup
|
|
78
88
|
* @returns {HTMLElement}
|
package/src/tools/table/index.ts
CHANGED
|
@@ -54,8 +54,10 @@ 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';
|
|
60
|
+
import { isCellWithBlocks } from './types';
|
|
59
61
|
|
|
60
62
|
const DEFAULT_ROWS = 3;
|
|
61
63
|
const DEFAULT_COLS = 3;
|
|
@@ -93,6 +95,7 @@ export class Table implements BlockTool {
|
|
|
93
95
|
private rowColControls: TableRowColControls | null = null;
|
|
94
96
|
private cellBlocks: TableCellBlocks | null = null;
|
|
95
97
|
private cellSelection: TableCellSelection | null = null;
|
|
98
|
+
private cornerDrag: TableCornerDrag | null = null;
|
|
96
99
|
private scrollHaze: TableScrollHaze | null = null;
|
|
97
100
|
private element: HTMLDivElement | null = null;
|
|
98
101
|
private gridElement: HTMLElement | null = null;
|
|
@@ -102,6 +105,8 @@ export class Table implements BlockTool {
|
|
|
102
105
|
private pendingHighlight: PendingHighlight | null = null;
|
|
103
106
|
private isNewTable = false;
|
|
104
107
|
private unregisterRestrictedTools: (() => void) | null = null;
|
|
108
|
+
private gridPasteCleanup: (() => void) | null = null;
|
|
109
|
+
private keyboardNavCleanup: (() => void) | null = null;
|
|
105
110
|
|
|
106
111
|
/**
|
|
107
112
|
* Generation counter for setData calls.
|
|
@@ -193,12 +198,18 @@ export class Table implements BlockTool {
|
|
|
193
198
|
this.resize = null;
|
|
194
199
|
this.addControls?.destroy();
|
|
195
200
|
this.addControls = null;
|
|
201
|
+
this.cornerDrag?.destroy();
|
|
202
|
+
this.cornerDrag = null;
|
|
196
203
|
this.rowColControls?.destroy();
|
|
197
204
|
this.rowColControls = null;
|
|
198
205
|
this.cellSelection?.destroy();
|
|
199
206
|
this.cellSelection = null;
|
|
200
207
|
this.scrollHaze?.destroy();
|
|
201
208
|
this.scrollHaze = null;
|
|
209
|
+
this.gridPasteCleanup?.();
|
|
210
|
+
this.gridPasteCleanup = null;
|
|
211
|
+
this.keyboardNavCleanup?.();
|
|
212
|
+
this.keyboardNavCleanup = null;
|
|
202
213
|
}
|
|
203
214
|
|
|
204
215
|
/**
|
|
@@ -256,6 +267,8 @@ export class Table implements BlockTool {
|
|
|
256
267
|
newTbody: Element,
|
|
257
268
|
blockHolders: Map<string, HTMLElement>
|
|
258
269
|
): void {
|
|
270
|
+
const mounted = new Set<string>();
|
|
271
|
+
|
|
259
272
|
content.forEach((rowData, r) => {
|
|
260
273
|
rowData.forEach((cellContent, c) => {
|
|
261
274
|
if (typeof cellContent === 'string') {
|
|
@@ -283,8 +296,9 @@ export class Table implements BlockTool {
|
|
|
283
296
|
cellContent.blocks.forEach(blockId => {
|
|
284
297
|
const holder = blockHolders.get(blockId);
|
|
285
298
|
|
|
286
|
-
if (holder) {
|
|
299
|
+
if (holder && !mounted.has(blockId)) {
|
|
287
300
|
container.appendChild(holder);
|
|
301
|
+
mounted.add(blockId);
|
|
288
302
|
}
|
|
289
303
|
});
|
|
290
304
|
});
|
|
@@ -323,6 +337,7 @@ export class Table implements BlockTool {
|
|
|
323
337
|
private initSubsystems(gridEl: HTMLElement): void {
|
|
324
338
|
this.initResize(gridEl);
|
|
325
339
|
this.initAddControls(gridEl);
|
|
340
|
+
this.initCornerDrag(gridEl);
|
|
326
341
|
this.initRowColControls(gridEl);
|
|
327
342
|
this.initCellSelection(gridEl);
|
|
328
343
|
this.initGridPasteListener(gridEl);
|
|
@@ -482,7 +497,7 @@ export class Table implements BlockTool {
|
|
|
482
497
|
|
|
483
498
|
if (!this.readOnly) {
|
|
484
499
|
this.initCellBlocks(gridEl);
|
|
485
|
-
setupKeyboardNavigation(gridEl, this.cellBlocks);
|
|
500
|
+
this.keyboardNavCleanup = setupKeyboardNavigation(gridEl, this.cellBlocks);
|
|
486
501
|
}
|
|
487
502
|
|
|
488
503
|
return wrapper;
|
|
@@ -558,6 +573,64 @@ export class Table implements BlockTool {
|
|
|
558
573
|
}
|
|
559
574
|
}
|
|
560
575
|
|
|
576
|
+
/**
|
|
577
|
+
* Toggle read-only mode in place without re-rendering.
|
|
578
|
+
* Entering readonly tears down all interactive subsystems and cell blocks;
|
|
579
|
+
* exiting readonly recreates them.
|
|
580
|
+
*/
|
|
581
|
+
public setReadOnly(state: boolean): void {
|
|
582
|
+
const wrapper = this.element;
|
|
583
|
+
const gridEl = this.gridElement;
|
|
584
|
+
|
|
585
|
+
if (!wrapper || !gridEl) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
this.readOnly = state;
|
|
590
|
+
|
|
591
|
+
if (state) {
|
|
592
|
+
// Entering readonly: tear down interactive subsystems
|
|
593
|
+
this.teardownSubsystems();
|
|
594
|
+
this.cellBlocks?.destroy();
|
|
595
|
+
this.cellBlocks = null;
|
|
596
|
+
|
|
597
|
+
// Remove grip overlay
|
|
598
|
+
if (this.gripOverlay) {
|
|
599
|
+
this.gripOverlay.remove();
|
|
600
|
+
this.gripOverlay = null;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Update wrapper classes and attributes
|
|
604
|
+
WRAPPER_EDIT_CLASSES.forEach(cls => wrapper.classList.remove(cls));
|
|
605
|
+
wrapper.setAttribute('data-blok-table-readonly', '');
|
|
606
|
+
|
|
607
|
+
// Mount cell content as non-interactive
|
|
608
|
+
const snap = this.model.snapshot();
|
|
609
|
+
|
|
610
|
+
mountCellBlocksReadOnly(gridEl, snap.content, this.api, this.blockId ?? '');
|
|
611
|
+
} else {
|
|
612
|
+
// Exiting readonly: restore interactive subsystems
|
|
613
|
+
wrapper.removeAttribute('data-blok-table-readonly');
|
|
614
|
+
WRAPPER_EDIT_CLASSES.forEach(cls => wrapper.classList.add(cls));
|
|
615
|
+
|
|
616
|
+
// Create grip overlay
|
|
617
|
+
const overlay = document.createElement('div');
|
|
618
|
+
|
|
619
|
+
overlay.setAttribute('data-blok-table-grip-overlay', '');
|
|
620
|
+
overlay.style.position = 'absolute';
|
|
621
|
+
overlay.style.inset = '0';
|
|
622
|
+
overlay.style.pointerEvents = 'none';
|
|
623
|
+
overlay.style.zIndex = '3';
|
|
624
|
+
wrapper.appendChild(overlay);
|
|
625
|
+
this.gripOverlay = overlay;
|
|
626
|
+
|
|
627
|
+
// Initialize cell blocks and subsystems
|
|
628
|
+
this.initCellBlocks(gridEl);
|
|
629
|
+
this.keyboardNavCleanup = setupKeyboardNavigation(gridEl, this.cellBlocks);
|
|
630
|
+
this.initSubsystems(gridEl);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
561
634
|
/**
|
|
562
635
|
* Remove blocks that claim this table as parent but are not referenced in any cell.
|
|
563
636
|
*
|
|
@@ -595,7 +668,28 @@ export class Table implements BlockTool {
|
|
|
595
668
|
}
|
|
596
669
|
|
|
597
670
|
public save(_blockContent: HTMLElement): TableData {
|
|
598
|
-
|
|
671
|
+
const data = this.model.snapshot();
|
|
672
|
+
|
|
673
|
+
// Filter out block IDs that don't belong to this table.
|
|
674
|
+
// Corrupted data may contain cross-table references; persisting them
|
|
675
|
+
// causes DOM node stealing and data loss on subsequent renders.
|
|
676
|
+
data.content = data.content.map(row =>
|
|
677
|
+
row.map(cell => {
|
|
678
|
+
if (!isCellWithBlocks(cell)) {
|
|
679
|
+
return cell;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const filtered = cell.blocks.filter(blockId => {
|
|
683
|
+
const block = this.api.blocks.getById?.(blockId);
|
|
684
|
+
|
|
685
|
+
return !block || block.parentId === this.blockId;
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
return { ...cell, blocks: filtered };
|
|
689
|
+
})
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
return data;
|
|
599
693
|
}
|
|
600
694
|
|
|
601
695
|
public validate(savedData: TableData): boolean {
|
|
@@ -900,6 +994,10 @@ export class Table implements BlockTool {
|
|
|
900
994
|
wrapper: this.element,
|
|
901
995
|
grid: gridEl,
|
|
902
996
|
i18n: this.api.i18n,
|
|
997
|
+
getTableSize: () => ({
|
|
998
|
+
rows: this.model.rows,
|
|
999
|
+
cols: this.model.cols,
|
|
1000
|
+
}),
|
|
903
1001
|
getNewColumnWidth: () => {
|
|
904
1002
|
const colWidths = this.model.colWidths ?? readPixelWidths(gridEl);
|
|
905
1003
|
|
|
@@ -1040,6 +1138,131 @@ export class Table implements BlockTool {
|
|
|
1040
1138
|
}
|
|
1041
1139
|
}
|
|
1042
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
|
+
|
|
1043
1266
|
private initRowColControls(gridEl: HTMLElement): void {
|
|
1044
1267
|
this.rowColControls?.destroy();
|
|
1045
1268
|
|
|
@@ -1063,6 +1286,7 @@ export class Table implements BlockTool {
|
|
|
1063
1286
|
}
|
|
1064
1287
|
|
|
1065
1288
|
this.addControls?.setDisplay(!isDragging);
|
|
1289
|
+
this.cornerDrag?.setDisplay(!isDragging);
|
|
1066
1290
|
|
|
1067
1291
|
if (isDragging) {
|
|
1068
1292
|
this.api.toolbar.close({ setExplicitlyClosed: false });
|
|
@@ -1442,6 +1666,7 @@ export class Table implements BlockTool {
|
|
|
1442
1666
|
}
|
|
1443
1667
|
|
|
1444
1668
|
this.addControls?.setInteractive(!hasSelection);
|
|
1669
|
+
this.cornerDrag?.setInteractive(!hasSelection);
|
|
1445
1670
|
this.rowColControls?.setGripsDisplay(!hasSelection);
|
|
1446
1671
|
},
|
|
1447
1672
|
onSelectionRangeChange: () => {
|
|
@@ -1531,9 +1756,14 @@ export class Table implements BlockTool {
|
|
|
1531
1756
|
}
|
|
1532
1757
|
|
|
1533
1758
|
private initGridPasteListener(gridEl: HTMLElement): void {
|
|
1534
|
-
|
|
1759
|
+
const handler = (e: ClipboardEvent): void => {
|
|
1535
1760
|
this.handleGridPaste(e, gridEl);
|
|
1536
|
-
}
|
|
1761
|
+
};
|
|
1762
|
+
|
|
1763
|
+
gridEl.addEventListener('paste', handler);
|
|
1764
|
+
this.gridPasteCleanup = () => {
|
|
1765
|
+
gridEl.removeEventListener('paste', handler);
|
|
1766
|
+
};
|
|
1537
1767
|
}
|
|
1538
1768
|
|
|
1539
1769
|
private handleGridPaste(e: ClipboardEvent, gridEl: HTMLElement): void {
|
|
@@ -1633,7 +1863,9 @@ export class Table implements BlockTool {
|
|
|
1633
1863
|
|
|
1634
1864
|
const range = selection.getRangeAt(0);
|
|
1635
1865
|
|
|
1636
|
-
range.
|
|
1866
|
+
if (!range.collapsed) {
|
|
1867
|
+
range.deleteContents();
|
|
1868
|
+
}
|
|
1637
1869
|
|
|
1638
1870
|
const fragment = document.createDocumentFragment();
|
|
1639
1871
|
const wrapper = document.createElement('div');
|