@jackuait/blok 0.10.2 → 0.10.3
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-D-T1XZ92.mjs → blok-3wc3aInM.mjs} +480 -463
- package/dist/chunks/{constants-CaB-mlB5.mjs → constants-Bp622jic.mjs} +109 -103
- package/dist/chunks/{tools-BFK2MvVI.mjs → tools-BC1jRfoS.mjs} +695 -644
- package/dist/full.mjs +3 -3
- package/dist/react.mjs +2 -2
- package/dist/tools.mjs +2 -2
- package/package.json +1 -1
- package/src/components/modules/toolbar/index.ts +95 -3
- package/src/components/modules/toolbar/plus-button.ts +37 -0
- package/src/components/modules/toolbar/settings-toggler.ts +6 -0
- package/src/components/modules/uiControllers/controllers/keyboard.ts +56 -22
- package/src/components/selection/cursor.ts +12 -2
- package/src/components/ui/toolbox.ts +31 -4
- package/src/components/utils/popover/popover-position.ts +8 -3
- package/src/tools/table/index.ts +10 -19
- package/src/tools/table/table-cell-selection.ts +126 -7
- package/src/tools/table/table-core.ts +59 -5
- package/src/tools/table/table-model.ts +8 -0
- package/src/tools/table/table-row-col-controls.ts +40 -18
package/dist/full.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { n as e, t } from "./chunks/blok-
|
|
2
|
-
import { nr as n } from "./chunks/constants-
|
|
1
|
+
import { n as e, t } from "./chunks/blok-3wc3aInM.mjs";
|
|
2
|
+
import { nr as n } from "./chunks/constants-Bp622jic.mjs";
|
|
3
3
|
import { t as r } from "./chunks/objectSpread2-CWwMYL_U.mjs";
|
|
4
|
-
import { _ as i, a, c as o, g as s, i as c, l, m as u, n as d, o as f, s as p, t as m, v as h } from "./chunks/tools-
|
|
4
|
+
import { _ as i, a, c as o, g as s, i as c, l, m as u, n as d, o as f, s as p, t as m, v as h } from "./chunks/tools-BC1jRfoS.mjs";
|
|
5
5
|
//#region src/full.ts
|
|
6
6
|
var g = {
|
|
7
7
|
paragraph: {
|
package/dist/react.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as e } from "./chunks/blok-
|
|
2
|
-
import "./chunks/constants-
|
|
1
|
+
import { t as e } from "./chunks/blok-3wc3aInM.mjs";
|
|
2
|
+
import "./chunks/constants-Bp622jic.mjs";
|
|
3
3
|
import { t } from "./chunks/objectSpread2-CWwMYL_U.mjs";
|
|
4
4
|
import { t as n } from "./chunks/objectWithoutProperties-D0XxKB4n.mjs";
|
|
5
5
|
import { forwardRef as r, useEffect as i, useMemo as a, useRef as o, useState as s } from "react";
|
package/dist/tools.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { m as e } from "./chunks/constants-
|
|
2
|
-
import { _ as t, a as n, c as r, d as i, f as a, g as o, h as s, i as c, l, m as u, n as d, o as f, p, r as m, s as h, t as g, u as _, v } from "./chunks/tools-
|
|
1
|
+
import { m as e } from "./chunks/constants-Bp622jic.mjs";
|
|
2
|
+
import { _ as t, a as n, c as r, d as i, f as a, g as o, h as s, i as c, l, m as u, n as d, o as f, p, r as m, s as h, t as g, u as _, v } from "./chunks/tools-BC1jRfoS.mjs";
|
|
3
3
|
export { l as Bold, p as Callout, _ as Code, e as Convert, a as Divider, t as Header, m as InlineCode, r as Italic, h as Link, o as List, f as Marker, v as Paragraph, i as Quote, c as Strikethrough, s as Table, u as Toggle, n as Underline, g as defaultBlockTools, d as defaultInlineTools };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jackuait/blok",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "Blok — headless, highly extensible rich text editor built for developers who need to implement a block-based editing experience (similar to Notion) without building it from scratch",
|
|
5
5
|
"module": "dist/blok.mjs",
|
|
6
6
|
"types": "./types/index.d.ts",
|
|
@@ -118,6 +118,22 @@ export class Toolbar extends Module<ToolbarNodes> {
|
|
|
118
118
|
*/
|
|
119
119
|
private settingsTogglerHandler: SettingsTogglerHandler;
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* The block that had focus immediately before the plus button opened the toolbox.
|
|
123
|
+
* Captured via the onFocusBlockCaptured callback in PlusButtonHandler.handleClick(),
|
|
124
|
+
* before any block manipulation occurs.
|
|
125
|
+
* Used to restore focus if the user dismisses the toolbox without selecting a tool.
|
|
126
|
+
* Cleared when a tool is selected (ToolboxEvent.BlockAdded) or when focus is restored.
|
|
127
|
+
*/
|
|
128
|
+
private preToolboxBlock: Block | null = null;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A newly-inserted empty block created by the plus button click (not a reused block).
|
|
132
|
+
* If the user dismisses the toolbox without selecting a tool, this block is removed.
|
|
133
|
+
* Cleared when a tool is selected or when the block is removed on cancel.
|
|
134
|
+
*/
|
|
135
|
+
private plusInsertedBlock: Block | null = null;
|
|
136
|
+
|
|
121
137
|
/**
|
|
122
138
|
* @class
|
|
123
139
|
* @param moduleConfiguration - Module Configuration
|
|
@@ -143,6 +159,10 @@ export class Toolbar extends Module<ToolbarNodes> {
|
|
|
143
159
|
openToolboxWithoutSlash: () => this.toolbox.openWithoutSlash(),
|
|
144
160
|
closeToolbox: () => this.toolbox.close(),
|
|
145
161
|
moveAndOpenToolbar: (block, target) => this.moveAndOpen(block, target),
|
|
162
|
+
onFocusBlockCaptured: (block, insertedBlock) => {
|
|
163
|
+
this.preToolboxBlock = block;
|
|
164
|
+
this.plusInsertedBlock = insertedBlock;
|
|
165
|
+
},
|
|
146
166
|
}
|
|
147
167
|
);
|
|
148
168
|
|
|
@@ -537,14 +557,22 @@ export class Toolbar extends Module<ToolbarNodes> {
|
|
|
537
557
|
* Uses Math.max to guarantee the actions container (positioned via right:100%)
|
|
538
558
|
* never extends beyond the left edge of the viewport, which would make the
|
|
539
559
|
* drag handle unreachable by pointer events.
|
|
560
|
+
*
|
|
561
|
+
* For nested blocks (e.g. children inside a callout), the holder is already
|
|
562
|
+
* offset from the viewport left by the parent's indentation. In that case we
|
|
563
|
+
* only need to ensure the actions don't extend beyond the viewport left edge
|
|
564
|
+
* (holderLeft px are available to the left), so the minimum margin is
|
|
565
|
+
* max(0, actionsWidth - holderLeft) rather than a flat actionsWidth clamp.
|
|
540
566
|
*/
|
|
541
567
|
if (blockContentElement && this.nodes.content) {
|
|
542
568
|
const holderRect = this.nodes.wrapper?.getBoundingClientRect();
|
|
543
569
|
const contentRect = blockContentElement.getBoundingClientRect();
|
|
544
570
|
const visualOffset = holderRect ? Math.max(0, contentRect.left - holderRect.left) : 0;
|
|
545
571
|
const actionsWidth = this.nodes.actions?.offsetWidth ?? 0;
|
|
572
|
+
const holderLeft = holderRect ? Math.max(0, holderRect.left) : 0;
|
|
573
|
+
const minMarginLeft = Math.max(0, actionsWidth - holderLeft);
|
|
546
574
|
|
|
547
|
-
this.nodes.content.style.marginLeft = `${Math.max(visualOffset,
|
|
575
|
+
this.nodes.content.style.marginLeft = `${Math.max(visualOffset, minMarginLeft)}px`;
|
|
548
576
|
this.nodes.content.style.maxWidth = `${contentRect.width}px`;
|
|
549
577
|
}
|
|
550
578
|
}
|
|
@@ -668,15 +696,19 @@ export class Toolbar extends Module<ToolbarNodes> {
|
|
|
668
696
|
/**
|
|
669
697
|
* Sync toolbar content wrapper's position and width with the block content element.
|
|
670
698
|
* Uses getBoundingClientRect so wide-mode content (max-width: none) is handled correctly.
|
|
671
|
-
* Clamp to actionsWidth so actions never extend beyond the left
|
|
699
|
+
* Clamp to max(0, actionsWidth - holderLeft) so actions never extend beyond the left
|
|
700
|
+
* viewport edge. For nested blocks already offset from the left, a smaller clamp is
|
|
701
|
+
* used so buttons are not pushed into the text content.
|
|
672
702
|
*/
|
|
673
703
|
if (blockContentElement && this.nodes.content) {
|
|
674
704
|
const holderRect = this.nodes.wrapper?.getBoundingClientRect();
|
|
675
705
|
const contentRect = blockContentElement.getBoundingClientRect();
|
|
676
706
|
const visualOffset = holderRect ? Math.max(0, contentRect.left - holderRect.left) : 0;
|
|
677
707
|
const actionsWidth = this.nodes.actions?.offsetWidth ?? 0;
|
|
708
|
+
const holderLeft = holderRect ? Math.max(0, holderRect.left) : 0;
|
|
709
|
+
const minMarginLeft = Math.max(0, actionsWidth - holderLeft);
|
|
678
710
|
|
|
679
|
-
this.nodes.content.style.marginLeft = `${Math.max(visualOffset,
|
|
711
|
+
this.nodes.content.style.marginLeft = `${Math.max(visualOffset, minMarginLeft)}px`;
|
|
680
712
|
this.nodes.content.style.maxWidth = `${contentRect.width}px`;
|
|
681
713
|
}
|
|
682
714
|
}
|
|
@@ -1089,9 +1121,63 @@ export class Toolbar extends Module<ToolbarNodes> {
|
|
|
1089
1121
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
1090
1122
|
this.Blok.UI.nodes.wrapper.classList.remove(this.CSS.openedToolboxHolderModifier);
|
|
1091
1123
|
this.Blok.UI.nodes.wrapper.removeAttribute(DATA_ATTR.toolboxOpened);
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* If the toolbox was opened via the plus button and the user dismissed
|
|
1127
|
+
* it without selecting a tool (Escape / click outside), restore focus to
|
|
1128
|
+
* the block that was focused BEFORE the plus button was clicked and
|
|
1129
|
+
* remove the orphan empty block that was inserted.
|
|
1130
|
+
*
|
|
1131
|
+
* When a tool IS selected, ToolboxEvent.BlockAdded fires first and clears
|
|
1132
|
+
* preToolboxBlock, so this branch is skipped for that case.
|
|
1133
|
+
*/
|
|
1134
|
+
if (this.preToolboxBlock !== null) {
|
|
1135
|
+
const blockToRestore = this.preToolboxBlock;
|
|
1136
|
+
|
|
1137
|
+
this.preToolboxBlock = null;
|
|
1138
|
+
|
|
1139
|
+
// Remove the orphan block that was inserted by the plus button click,
|
|
1140
|
+
// then restore focus. removeBlock() is Promise-based but resolves
|
|
1141
|
+
// synchronously; chaining ensures setToBlock runs after removal.
|
|
1142
|
+
if (this.plusInsertedBlock !== null) {
|
|
1143
|
+
const orphan = this.plusInsertedBlock;
|
|
1144
|
+
|
|
1145
|
+
this.plusInsertedBlock = null;
|
|
1146
|
+
void this.Blok.BlockManager.removeBlock(orphan, false).then(() => {
|
|
1147
|
+
if (blockToRestore.inputs.length > 0) {
|
|
1148
|
+
this.Blok.Caret.setToBlock(blockToRestore, this.Blok.Caret.positions.END);
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
} else if (blockToRestore.inputs.length > 0) {
|
|
1152
|
+
// Reused an existing block (emptyBlockToReuse path) — just restore focus
|
|
1153
|
+
this.Blok.Caret.setToBlock(blockToRestore, this.Blok.Caret.positions.END);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* Restore focus to the current block when the toolbox closes via any
|
|
1161
|
+
* non-plus-button path (e.g. slash-search dismissed via Escape).
|
|
1162
|
+
* Without this, focus falls to document.body after non-keyboard close
|
|
1163
|
+
* paths, causing subsequent keystrokes to be lost.
|
|
1164
|
+
*/
|
|
1165
|
+
const currentBlock = this.Blok.BlockManager.currentBlock;
|
|
1166
|
+
|
|
1167
|
+
if (currentBlock && currentBlock.inputs.length > 0) {
|
|
1168
|
+
this.Blok.Caret.setToBlock(currentBlock, this.Blok.Caret.positions.END);
|
|
1169
|
+
}
|
|
1092
1170
|
});
|
|
1093
1171
|
|
|
1094
1172
|
this.toolboxInstance.on(ToolboxEvent.BlockAdded, ({ block }) => {
|
|
1173
|
+
/**
|
|
1174
|
+
* A tool was selected and a block was added — clear the cancel context so
|
|
1175
|
+
* ToolboxEvent.Closed (which fires after this) does not try to undo the
|
|
1176
|
+
* insertion and restore focus to the pre-plus block.
|
|
1177
|
+
*/
|
|
1178
|
+
this.preToolboxBlock = null;
|
|
1179
|
+
this.plusInsertedBlock = null;
|
|
1180
|
+
|
|
1095
1181
|
const { BlockManager, Caret } = this.Blok;
|
|
1096
1182
|
const newBlock = BlockManager.getBlockById(block.id);
|
|
1097
1183
|
|
|
@@ -1142,6 +1228,12 @@ export class Toolbar extends Module<ToolbarNodes> {
|
|
|
1142
1228
|
|
|
1143
1229
|
if (plusButton) {
|
|
1144
1230
|
this.readOnlyMutableListeners.on(plusButton, 'mousedown', (e) => {
|
|
1231
|
+
/**
|
|
1232
|
+
* Prevent focus from moving away from the currently-active contenteditable block.
|
|
1233
|
+
* Without this, clicking the plus button steals DOM focus, causing subsequent
|
|
1234
|
+
* keystrokes to land in the wrong block (text-jumping bug).
|
|
1235
|
+
*/
|
|
1236
|
+
(e as MouseEvent).preventDefault();
|
|
1145
1237
|
hide();
|
|
1146
1238
|
|
|
1147
1239
|
this.clickDragHandler.setup(
|
|
@@ -46,6 +46,13 @@ export class PlusButtonHandler {
|
|
|
46
46
|
*/
|
|
47
47
|
private moveAndOpenToolbar: (block?: Block | null, target?: Element | null) => void;
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Optional callback invoked at the very start of handleClick(), before any
|
|
51
|
+
* block manipulation, with the block that currently has focus.
|
|
52
|
+
* Used by Toolbar to capture the pre-toolbox block for focus restoration on cancel.
|
|
53
|
+
*/
|
|
54
|
+
private onFocusBlockCaptured: ((block: Block | null, insertedBlock: Block | null) => void) | undefined;
|
|
55
|
+
|
|
49
56
|
/**
|
|
50
57
|
* @param getBlok - Function to get Blok modules reference
|
|
51
58
|
* @param callbacks - Object containing callback functions
|
|
@@ -58,6 +65,7 @@ export class PlusButtonHandler {
|
|
|
58
65
|
openToolboxWithoutSlash: () => void;
|
|
59
66
|
closeToolbox: () => void;
|
|
60
67
|
moveAndOpenToolbar: (block?: Block | null, target?: Element | null) => void;
|
|
68
|
+
onFocusBlockCaptured?: (block: Block | null, insertedBlock: Block | null) => void;
|
|
61
69
|
}
|
|
62
70
|
) {
|
|
63
71
|
this.getBlok = getBlok;
|
|
@@ -66,6 +74,7 @@ export class PlusButtonHandler {
|
|
|
66
74
|
this.openToolboxWithoutSlash = callbacks.openToolboxWithoutSlash;
|
|
67
75
|
this.closeToolbox = callbacks.closeToolbox;
|
|
68
76
|
this.moveAndOpenToolbar = callbacks.moveAndOpenToolbar;
|
|
77
|
+
this.onFocusBlockCaptured = callbacks.onFocusBlockCaptured;
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
/**
|
|
@@ -175,6 +184,23 @@ export class PlusButtonHandler {
|
|
|
175
184
|
// If hoveredBlock is not empty (e.g. a table), check if the focused block
|
|
176
185
|
// is empty and nested inside it (e.g. an empty paragraph in a table cell).
|
|
177
186
|
const currentBlock = BlockManager.currentBlock ?? null;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Capture the block that CURRENTLY HAS DOM FOCUS before any manipulation,
|
|
190
|
+
* so that focus can be restored to it if the user cancels (Escape) without
|
|
191
|
+
* selecting a tool.
|
|
192
|
+
*
|
|
193
|
+
* We cannot rely on BlockManager.currentBlock here: the mousedown event on
|
|
194
|
+
* the plus button (which lives inside the hovered block's DOM) triggers the
|
|
195
|
+
* redactorTouchHandler in capture phase, which calls setCurrentBlockByChildNode
|
|
196
|
+
* and overwrites currentBlock to the hovered block BEFORE our preventDefault
|
|
197
|
+
* or handleClick() runs. Instead we look at the actual DOM-focused element
|
|
198
|
+
* and find which block owns it.
|
|
199
|
+
*/
|
|
200
|
+
const activeEl = document.activeElement;
|
|
201
|
+
const focusedBlockBeforeOpen = activeEl !== null && activeEl !== document.body
|
|
202
|
+
? (BlockManager.getBlockByChildNode(activeEl) ?? null)
|
|
203
|
+
: null;
|
|
178
204
|
const hoveredIsEmpty = hoveredBlock !== null && hoveredBlock.isEmpty;
|
|
179
205
|
const nestedCurrentBlockIsEmpty = !hoveredIsEmpty && currentBlock !== null
|
|
180
206
|
&& currentBlock !== hoveredBlock && currentBlock.isEmpty
|
|
@@ -215,6 +241,17 @@ export class PlusButtonHandler {
|
|
|
215
241
|
hoveredBlock?.holder.after(targetBlock.holder);
|
|
216
242
|
}
|
|
217
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Notify Toolbar of the pre-open focus context.
|
|
246
|
+
* insertedBlock is non-null only when we created a brand-new empty block
|
|
247
|
+
* (not when we're reusing an existing empty block or operating in slash mode).
|
|
248
|
+
* On cancel (Escape), Toolbar will remove the inserted block and restore focus
|
|
249
|
+
* to focusedBlockBeforeOpen.
|
|
250
|
+
*/
|
|
251
|
+
const insertedBlock = (!startsWithSlash && emptyBlockToReuse === null) ? targetBlock : null;
|
|
252
|
+
|
|
253
|
+
this.onFocusBlockCaptured?.(focusedBlockBeforeOpen, insertedBlock);
|
|
254
|
+
|
|
218
255
|
// Position caret and open toolbox
|
|
219
256
|
if (startsWithSlash) {
|
|
220
257
|
// Block already has "/" - keep slash-search mode, position after the slash
|
|
@@ -179,6 +179,12 @@ export class SettingsTogglerHandler {
|
|
|
179
179
|
*/
|
|
180
180
|
public createMousedownHandler(): (e: Event) => void {
|
|
181
181
|
return (e: Event) => {
|
|
182
|
+
/**
|
|
183
|
+
* Prevent focus from moving away from the currently-active contenteditable block.
|
|
184
|
+
* Without this, clicking the settings toggler steals DOM focus, causing subsequent
|
|
185
|
+
* keystrokes to land in the wrong block (text-jumping bug).
|
|
186
|
+
*/
|
|
187
|
+
(e as MouseEvent).preventDefault();
|
|
182
188
|
hide();
|
|
183
189
|
|
|
184
190
|
this.clickDragHandler.setup(
|
|
@@ -29,6 +29,33 @@ export class KeyboardController extends Controller {
|
|
|
29
29
|
*/
|
|
30
30
|
private redactorElement: HTMLElement | null = null;
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Stable handler references for deduplication via Listeners.findOne.
|
|
34
|
+
* Storing as class properties ensures the same function reference is passed
|
|
35
|
+
* to addEventListener on every enable() call, so the Listeners utility can
|
|
36
|
+
* detect and skip duplicate registrations (e.g. when toggleReadOnly calls
|
|
37
|
+
* enable() more than once via requestIdleCallback).
|
|
38
|
+
*/
|
|
39
|
+
private readonly documentKeydownHandler = (event: Event): void => {
|
|
40
|
+
if (event instanceof KeyboardEvent) {
|
|
41
|
+
this.handleKeydown(event);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
private readonly redactorBeforeinputHandler = (): void => {
|
|
46
|
+
this.Blok.YjsManager.markCaretBeforeChange();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
private readonly redactorKeydownHandler = (event: Event): void => {
|
|
50
|
+
if (!(event instanceof KeyboardEvent)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (KEYS_REQUIRING_CARET_CAPTURE.has(event.key)) {
|
|
55
|
+
this.Blok.YjsManager.markCaretBeforeChange();
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
32
59
|
constructor(options: {
|
|
33
60
|
config: Controller['config'];
|
|
34
61
|
eventsDispatcher: Controller['eventsDispatcher'];
|
|
@@ -54,34 +81,20 @@ export class KeyboardController extends Controller {
|
|
|
54
81
|
}
|
|
55
82
|
|
|
56
83
|
// Document-level keydown handler
|
|
57
|
-
this.readOnlyMutableListeners.on(document, 'keydown',
|
|
58
|
-
if (event instanceof KeyboardEvent) {
|
|
59
|
-
this.handleKeydown(event);
|
|
60
|
-
}
|
|
61
|
-
}, true);
|
|
84
|
+
this.readOnlyMutableListeners.on(document, 'keydown', this.documentKeydownHandler, true);
|
|
62
85
|
|
|
63
86
|
/**
|
|
64
87
|
* Capture caret position before any input changes the DOM.
|
|
65
88
|
* This ensures undo/redo restores the caret to the correct position.
|
|
66
89
|
*/
|
|
67
|
-
this.readOnlyMutableListeners.on(this.redactorElement, 'beforeinput',
|
|
68
|
-
this.Blok.YjsManager.markCaretBeforeChange();
|
|
69
|
-
}, true);
|
|
90
|
+
this.readOnlyMutableListeners.on(this.redactorElement, 'beforeinput', this.redactorBeforeinputHandler, true);
|
|
70
91
|
|
|
71
92
|
/**
|
|
72
93
|
* Capture caret position on keydown for keys that tools commonly intercept.
|
|
73
94
|
* Uses capture phase to run before tool handlers.
|
|
74
95
|
* markCaretBeforeChange() is idempotent - if beforeinput also fires, the second call is ignored.
|
|
75
96
|
*/
|
|
76
|
-
this.readOnlyMutableListeners.on(this.redactorElement, 'keydown',
|
|
77
|
-
if (!(event instanceof KeyboardEvent)) {
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (KEYS_REQUIRING_CARET_CAPTURE.has(event.key)) {
|
|
82
|
-
this.Blok.YjsManager.markCaretBeforeChange();
|
|
83
|
-
}
|
|
84
|
-
}, true);
|
|
97
|
+
this.readOnlyMutableListeners.on(this.redactorElement, 'keydown', this.redactorKeydownHandler, true);
|
|
85
98
|
}
|
|
86
99
|
|
|
87
100
|
/**
|
|
@@ -260,12 +273,18 @@ export class KeyboardController extends Controller {
|
|
|
260
273
|
|
|
261
274
|
/**
|
|
262
275
|
* Toolbox needs specific Escape handling for caret restoration,
|
|
263
|
-
* so check it before the registry
|
|
276
|
+
* so check it before the registry.
|
|
277
|
+
*
|
|
278
|
+
* stopPropagation() is required here: this handler runs in the capture phase
|
|
279
|
+
* on document, BEFORE the block-level keydown handler. If we let the event
|
|
280
|
+
* continue bubbling after closing the toolbox, the block's keydown handler
|
|
281
|
+
* (navigationMode.handleEscape) will see `toolbox.opened === false` and
|
|
282
|
+
* incorrectly enable navigation mode, which calls `activeElement.blur()`
|
|
283
|
+
* and drops focus to body.
|
|
264
284
|
*/
|
|
265
285
|
if (this.Blok.Toolbar.toolbox.opened) {
|
|
286
|
+
event.stopPropagation();
|
|
266
287
|
this.Blok.Toolbar.toolbox.close();
|
|
267
|
-
this.Blok.BlockManager.currentBlock &&
|
|
268
|
-
this.Blok.Caret.setToBlock(this.Blok.BlockManager.currentBlock, this.Blok.Caret.positions.END);
|
|
269
288
|
|
|
270
289
|
return;
|
|
271
290
|
}
|
|
@@ -313,15 +332,30 @@ export class KeyboardController extends Controller {
|
|
|
313
332
|
|
|
314
333
|
/**
|
|
315
334
|
* If focus is inside editor content and no toolbars are open,
|
|
316
|
-
* enable navigation mode for keyboard-based block navigation
|
|
335
|
+
* enable navigation mode for keyboard-based block navigation.
|
|
336
|
+
*
|
|
337
|
+
* Skip navigation mode when a drag operation is in progress:
|
|
338
|
+
* the drag's own keydown handler (DragController.onKeyDown) must receive
|
|
339
|
+
* this Escape event to announce the cancellation and clean up drag state.
|
|
340
|
+
* Enabling navigation mode here would call blur() on the active element,
|
|
341
|
+
* then the block holder's bubbling keydown handler would see navigation
|
|
342
|
+
* mode enabled and call event.stopPropagation(), preventing DragController
|
|
343
|
+
* from ever receiving the event.
|
|
317
344
|
*/
|
|
318
345
|
const target = event.target;
|
|
319
346
|
const isTargetElement = target instanceof HTMLElement;
|
|
320
347
|
const isInsideRedactor = this.redactorElement && isTargetElement && this.redactorElement.contains(target);
|
|
321
348
|
const hasCurrentBlock = this.Blok.BlockManager.currentBlock !== undefined;
|
|
322
349
|
|
|
323
|
-
if (isInsideRedactor && hasCurrentBlock) {
|
|
350
|
+
if (isInsideRedactor && hasCurrentBlock && !this.Blok.DragManager.isDragging) {
|
|
324
351
|
event.preventDefault();
|
|
352
|
+
/**
|
|
353
|
+
* Stop propagation so the block holder's bubble keydown handler (blockEvents.keydown)
|
|
354
|
+
* does not see this same Escape event. Without this, the block-level NavigationMode
|
|
355
|
+
* composer's handleKey() would receive the event AFTER navigation mode is enabled,
|
|
356
|
+
* see navigationModeEnabled=true + key='Escape', and immediately disable it.
|
|
357
|
+
*/
|
|
358
|
+
event.stopPropagation();
|
|
325
359
|
this.Blok.Toolbar.close();
|
|
326
360
|
this.Blok.BlockSelection.enableNavigationMode();
|
|
327
361
|
|
|
@@ -46,8 +46,18 @@ export class SelectionCursor {
|
|
|
46
46
|
// Focus contenteditable elements explicitly after setting the selection range.
|
|
47
47
|
// Placed after addRange() so the selection is preserved when focus transfers —
|
|
48
48
|
// calling focus() before addRange() can reset the caret during arrow navigation.
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
//
|
|
50
|
+
// When `element` is a text node or a non-focusable inline element (e.g. <b>, <span>),
|
|
51
|
+
// `isContentEditable` will be false. In that case we walk up to the nearest
|
|
52
|
+
// contenteditable ancestor so that DOM focus is transferred there. Without this,
|
|
53
|
+
// focus stays on whatever had it before (e.g. the toolbox search input), causing
|
|
54
|
+
// subsequent keystrokes to land in the wrong place.
|
|
55
|
+
const focusTarget = $.isContentEditable(element)
|
|
56
|
+
? element
|
|
57
|
+
: (element.parentElement?.closest('[contenteditable="true"]') as HTMLElement | null) ?? null;
|
|
58
|
+
|
|
59
|
+
if (focusTarget !== null && document.activeElement !== focusTarget) {
|
|
60
|
+
focusTarget.focus();
|
|
51
61
|
}
|
|
52
62
|
|
|
53
63
|
return range.getBoundingClientRect();
|
|
@@ -341,13 +341,17 @@ export class Toolbox extends EventsDispatcher<ToolboxEventMap> {
|
|
|
341
341
|
this.popover?.show();
|
|
342
342
|
|
|
343
343
|
/**
|
|
344
|
-
* When opening toolbox inside a table cell
|
|
345
|
-
* instead of at the trigger element (which is outside
|
|
344
|
+
* When opening toolbox inside a table cell or a nested block (toggle, callout),
|
|
345
|
+
* position it at the caret instead of at the trigger element (which is outside
|
|
346
|
+
* the nested container).
|
|
346
347
|
* Must be called after show() so the popover is in the DOM.
|
|
347
348
|
*/
|
|
348
|
-
const
|
|
349
|
+
const triggerRect = this.triggerElement?.getBoundingClientRect();
|
|
350
|
+
const triggerHidden = triggerRect?.height === 0;
|
|
351
|
+
const triggerOffScreen = triggerRect !== undefined && triggerRect.bottom < 0;
|
|
352
|
+
const isInsideNestedBlock = currentBlock !== undefined && currentBlock.parentId !== null;
|
|
349
353
|
|
|
350
|
-
if ((this.isInsideTableCell || triggerHidden) && this.popover instanceof PopoverDesktop) {
|
|
354
|
+
if ((this.isInsideTableCell || triggerHidden || triggerOffScreen || isInsideNestedBlock) && this.popover instanceof PopoverDesktop) {
|
|
351
355
|
const caretRect = SelectionUtils.rect;
|
|
352
356
|
|
|
353
357
|
this.popover.updatePosition(caretRect);
|
|
@@ -381,6 +385,18 @@ export class Toolbox extends EventsDispatcher<ToolboxEventMap> {
|
|
|
381
385
|
|
|
382
386
|
this.stopListeningToBlockInput();
|
|
383
387
|
this.popover?.hide();
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Only emit Closed event when the toolbox was actually open.
|
|
391
|
+
* This prevents spurious Closed events (and their side-effects such as
|
|
392
|
+
* caret restoration) when close() is called as routine cleanup (e.g.
|
|
393
|
+
* during cross-block selection, block deletion, or toolbar dismissal)
|
|
394
|
+
* even though the toolbox was never shown.
|
|
395
|
+
*/
|
|
396
|
+
if (!this.opened) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
384
400
|
this.opened = false;
|
|
385
401
|
this.emit(ToolboxEvent.Closed);
|
|
386
402
|
}
|
|
@@ -447,6 +463,17 @@ export class Toolbox extends EventsDispatcher<ToolboxEventMap> {
|
|
|
447
463
|
* Handles popover close event
|
|
448
464
|
*/
|
|
449
465
|
private onPopoverClose = (): void => {
|
|
466
|
+
/**
|
|
467
|
+
* Only handle the Closed event when the toolbox was actually open.
|
|
468
|
+
* The popover can fire Closed during routine cleanup (e.g. when Toolbar.close()
|
|
469
|
+
* is called unconditionally as part of CBS, block deletion, etc.), even though
|
|
470
|
+
* the toolbox was never shown. Emitting ToolboxEvent.Closed in those cases
|
|
471
|
+
* triggers side-effects (like caret restoration) that break cross-block selection.
|
|
472
|
+
*/
|
|
473
|
+
if (!this.opened) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
450
477
|
if (this.isInsideTableCell) {
|
|
451
478
|
this.toggleRestrictedToolsHidden(false);
|
|
452
479
|
this.isInsideTableCell = false;
|
|
@@ -75,9 +75,14 @@ export function resolvePosition(input: PositionInput): ResolvedPosition {
|
|
|
75
75
|
? anchor.top - offset - popoverSize.height + scrollOffset.y
|
|
76
76
|
: anchor.bottom + offset + scrollOffset.y;
|
|
77
77
|
|
|
78
|
-
// Clamp: ensure popover doesn't overflow above top boundary
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
// Clamp: ensure popover doesn't overflow above top boundary.
|
|
79
|
+
// Use the scope's top in document coords (scopeBounds.top + scrollOffset.y) rather
|
|
80
|
+
// than the viewport-clamped boundaryTop, so the clamp is correct when the page is
|
|
81
|
+
// scrolled (boundaryTop is viewport-relative; adding scrollOffset converts it to
|
|
82
|
+
// document coords but discards any negative scope top that was clamped to 0).
|
|
83
|
+
const scopeTopInDocCoords = scopeBounds.top + scrollOffset.y;
|
|
84
|
+
const top = rawTop < scopeTopInDocCoords
|
|
85
|
+
? scopeTopInDocCoords
|
|
81
86
|
: rawTop;
|
|
82
87
|
|
|
83
88
|
// --- Horizontal ---
|
package/src/tools/table/index.ts
CHANGED
|
@@ -1575,18 +1575,9 @@ export class Table implements BlockTool {
|
|
|
1575
1575
|
return [];
|
|
1576
1576
|
}
|
|
1577
1577
|
|
|
1578
|
-
const allRows = Array.from(gridEl.querySelectorAll(`[${ROW_ATTR}]`));
|
|
1579
|
-
|
|
1580
1578
|
return cells.map(cell => {
|
|
1581
|
-
const
|
|
1582
|
-
|
|
1583
|
-
if (!row) {
|
|
1584
|
-
return null;
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
const rowIndex = allRows.indexOf(row);
|
|
1588
|
-
const cellsInRow = Array.from(row.querySelectorAll(`[${CELL_ATTR}]`));
|
|
1589
|
-
const colIndex = cellsInRow.indexOf(cell);
|
|
1579
|
+
const rowIndex = parseInt(cell.getAttribute(CELL_ROW_ATTR) ?? '0', 10);
|
|
1580
|
+
const colIndex = parseInt(cell.getAttribute(CELL_COL_ATTR) ?? '0', 10);
|
|
1590
1581
|
|
|
1591
1582
|
const container = cell.querySelector(`[${CELL_BLOCKS_ATTR}]`);
|
|
1592
1583
|
const blocks: ClipboardBlockData[] = [];
|
|
@@ -1643,7 +1634,7 @@ export class Table implements BlockTool {
|
|
|
1643
1634
|
...(color !== undefined ? { color } : {}),
|
|
1644
1635
|
...(textColor !== undefined ? { textColor } : {}),
|
|
1645
1636
|
};
|
|
1646
|
-
})
|
|
1637
|
+
});
|
|
1647
1638
|
}
|
|
1648
1639
|
|
|
1649
1640
|
private initCellSelection(gridEl: HTMLElement): void {
|
|
@@ -1741,6 +1732,9 @@ export class Table implements BlockTool {
|
|
|
1741
1732
|
this.rebuildTableBody();
|
|
1742
1733
|
});
|
|
1743
1734
|
},
|
|
1735
|
+
getCellSpan: (row, col) => {
|
|
1736
|
+
return this.model.getCellSpan(row, col);
|
|
1737
|
+
},
|
|
1744
1738
|
});
|
|
1745
1739
|
}
|
|
1746
1740
|
|
|
@@ -1804,9 +1798,7 @@ export class Table implements BlockTool {
|
|
|
1804
1798
|
return;
|
|
1805
1799
|
}
|
|
1806
1800
|
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
if (!targetRow) {
|
|
1801
|
+
if (!targetCell.closest(`[${ROW_ATTR}]`)) {
|
|
1810
1802
|
return;
|
|
1811
1803
|
}
|
|
1812
1804
|
|
|
@@ -1827,10 +1819,9 @@ export class Table implements BlockTool {
|
|
|
1827
1819
|
e.preventDefault();
|
|
1828
1820
|
e.stopPropagation();
|
|
1829
1821
|
|
|
1830
|
-
|
|
1831
|
-
const targetRowIndex =
|
|
1832
|
-
const
|
|
1833
|
-
const targetColIndex = cellsInRow.indexOf(targetCell);
|
|
1822
|
+
// Read true model coordinates from stamped data attributes
|
|
1823
|
+
const targetRowIndex = parseInt(targetCell.getAttribute(CELL_ROW_ATTR) ?? '0', 10);
|
|
1824
|
+
const targetColIndex = parseInt(targetCell.getAttribute(CELL_COL_ATTR) ?? '0', 10);
|
|
1834
1825
|
|
|
1835
1826
|
this.pastePayloadIntoCells(gridEl, payload, targetRowIndex, targetColIndex);
|
|
1836
1827
|
}
|