@inkbrick/react 0.0.0-stage → 0.1.0-alpha.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/FormulaPalette-WFRPRHS5.js +320 -0
- package/dist/FormulaPalette-WFRPRHS5.js.map +1 -0
- package/dist/chunk-TPUX4C45.js +2309 -0
- package/dist/chunk-TPUX4C45.js.map +1 -0
- package/dist/index.d.ts +1431 -0
- package/dist/index.js +4880 -0
- package/dist/index.js.map +1 -0
- package/dist/mathCss-FLPE6QBM.js +361 -0
- package/dist/mathCss-FLPE6QBM.js.map +1 -0
- package/package.json +46 -3
- package/src/block-handle/BlockHandleController.ts +93 -0
- package/src/block-handle/BlockHandleHost.tsx +108 -0
- package/src/block-handle/DefaultBlockHandle.tsx +71 -0
- package/src/block-handle/DefaultBlockMenu.tsx +114 -0
- package/src/block-handle/DropIndicatorHost.tsx +58 -0
- package/src/block-handle/actions.tsx +146 -0
- package/src/block-handle/blockDrag.ts +215 -0
- package/src/block-handle/geometry.ts +64 -0
- package/src/block-handle/gripGesture.ts +108 -0
- package/src/block-handle/types.ts +65 -0
- package/src/block-handle/useBlockDrag.ts +21 -0
- package/src/block-handle/useBlockHandles.ts +125 -0
- package/src/blocks/BlockChildren.tsx +17 -0
- package/src/blocks/BlockProps.ts +5 -0
- package/src/blocks/CalloutBlock.tsx +61 -0
- package/src/blocks/CodeBlock.tsx +96 -0
- package/src/blocks/DividerBlock.tsx +15 -0
- package/src/blocks/Editable.tsx +84 -0
- package/src/blocks/FormulaBlock.tsx +173 -0
- package/src/blocks/HeadingBlock.tsx +22 -0
- package/src/blocks/ImageBlock.tsx +362 -0
- package/src/blocks/ListBlock.tsx +86 -0
- package/src/blocks/NoteBlock.tsx +10 -0
- package/src/blocks/PageBlock.tsx +10 -0
- package/src/blocks/ParagraphBlock.tsx +20 -0
- package/src/blocks/QuoteBlock.tsx +18 -0
- package/src/blocks/UnknownBlockView.tsx +27 -0
- package/src/blocks/defineBlock.ts +49 -0
- package/src/clipboard/html/blocks.ts +379 -0
- package/src/clipboard/html/inline.ts +92 -0
- package/src/clipboard/html/style.ts +92 -0
- package/src/clipboard/html/tags.ts +77 -0
- package/src/clipboard/htmlToFragment.ts +41 -0
- package/src/clipboard/useClipboard.ts +296 -0
- package/src/code/highlight.ts +56 -0
- package/src/code/languages.ts +236 -0
- package/src/code/scan/css.ts +164 -0
- package/src/code/scan/generic.ts +328 -0
- package/src/code/scan/html.ts +129 -0
- package/src/code/scan/markdown.ts +58 -0
- package/src/code/scan/shared.ts +81 -0
- package/src/code/scan/yaml.ts +34 -0
- package/src/code/types.ts +34 -0
- package/src/context/EditorContext.ts +9 -0
- package/src/context/EditorProvider.tsx +172 -0
- package/src/controllers/adapters.tsx +63 -0
- package/src/editing/BlockSelectionController.ts +27 -0
- package/src/editing/CaretController.ts +27 -0
- package/src/editing/EditorView.ts +108 -0
- package/src/editing/ViewportController.ts +63 -0
- package/src/editing/beforeInput.ts +276 -0
- package/src/editing/deleteAtBoundary.ts +16 -0
- package/src/editing/inputActions.ts +109 -0
- package/src/editing/renderCode.ts +69 -0
- package/src/editing/renderInline.ts +144 -0
- package/src/editing/selection.ts +262 -0
- package/src/editing/useEditorInput.ts +223 -0
- package/src/editing/useSelectionState.ts +46 -0
- package/src/formula/FormulaController.ts +51 -0
- package/src/formula/FormulaPalette.tsx +79 -0
- package/src/formula/FormulaPopover.tsx +181 -0
- package/src/formula/LazyFormulaPalette.tsx +42 -0
- package/src/formula/actions.ts +138 -0
- package/src/formula/snippet.ts +70 -0
- package/src/formula/symbols.ts +264 -0
- package/src/hooks/useBlock.ts +21 -0
- package/src/hooks/useBlockController.ts +30 -0
- package/src/hooks/useChildren.ts +16 -0
- package/src/hooks/useDocument.ts +16 -0
- package/src/hooks/useEditor.ts +13 -0
- package/src/hooks/useEditorView.ts +13 -0
- package/src/hooks/useHistory.ts +46 -0
- package/src/hooks/useReadOnly.ts +21 -0
- package/src/hooks/useT.ts +10 -0
- package/src/i18n/en.ts +182 -0
- package/src/i18n/ru.ts +174 -0
- package/src/i18n/translate.ts +77 -0
- package/src/icons/ICONS_LICENSE.md +50 -0
- package/src/icons/Icon.tsx +41 -0
- package/src/icons/lucide.ts +234 -0
- package/src/images/ImageError.ts +39 -0
- package/src/images/UploadTracker.ts +49 -0
- package/src/images/insertImages.ts +142 -0
- package/src/images/types.ts +22 -0
- package/src/images/upload.ts +62 -0
- package/src/index.ts +102 -0
- package/src/keymap/defaultKeymap.ts +189 -0
- package/src/keymap/keymap.ts +42 -0
- package/src/keymap/shortcut.ts +105 -0
- package/src/keymap/types.ts +40 -0
- package/src/math/FormulaView.tsx +62 -0
- package/src/math/mathCss.ts +4 -0
- package/src/math/renderer.ts +198 -0
- package/src/registry/defaultBlockComponents.ts +26 -0
- package/src/registry/resolveBlockComponent.ts +14 -0
- package/src/renderer/BlockRenderer.tsx +63 -0
- package/src/renderer/DocumentRenderer.tsx +33 -0
- package/src/renderer/DocumentView.tsx +94 -0
- package/src/renderer/virtual/VirtualChildList.tsx +355 -0
- package/src/renderer/virtual/estimate.ts +62 -0
- package/src/renderer/virtual/virtualRenderer.tsx +35 -0
- package/src/slash/DefaultSlashMenu.tsx +119 -0
- package/src/slash/SlashMenuController.ts +66 -0
- package/src/slash/SlashMenuHost.tsx +48 -0
- package/src/slash/applySlashItem.ts +57 -0
- package/src/slash/defaultSlashItems.tsx +186 -0
- package/src/slash/filter.ts +29 -0
- package/src/slash/items.ts +30 -0
- package/src/slash/syncSlashMenu.ts +67 -0
- package/src/slash/types.ts +21 -0
- package/src/toolbar/DefaultToolbar.tsx +281 -0
- package/src/toolbar/ToolbarController.ts +100 -0
- package/src/toolbar/ToolbarHost.tsx +81 -0
- package/src/toolbar/formatting.ts +75 -0
- package/src/toolbar/rich.ts +58 -0
- package/src/toolbar/syncToolbar.ts +39 -0
- package/src/toolbar/types.ts +21 -0
- package/src/ui/EditorUIContext.ts +26 -0
- package/src/ui/presentation.ts +24 -0
- package/src/ui/types.ts +70 -0
- package/README.md +0 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,4880 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EditorUIContext,
|
|
3
|
+
HIGHLIGHT_LIMIT,
|
|
4
|
+
ICON_SHAPES,
|
|
5
|
+
Icon,
|
|
6
|
+
applyInlineFormula,
|
|
7
|
+
applySlashItem,
|
|
8
|
+
availableSlashItems,
|
|
9
|
+
createTranslator,
|
|
10
|
+
defaultKeymap,
|
|
11
|
+
defaultSlashItems,
|
|
12
|
+
deleteForwardAtEnd,
|
|
13
|
+
en,
|
|
14
|
+
ensureMathStyles,
|
|
15
|
+
filterSlashItems,
|
|
16
|
+
getLocales,
|
|
17
|
+
highlightCode,
|
|
18
|
+
matchInlineFormula,
|
|
19
|
+
mountFormula,
|
|
20
|
+
normalizeLanguage,
|
|
21
|
+
openFormulaAt,
|
|
22
|
+
openInlineFormula,
|
|
23
|
+
readDocSelection,
|
|
24
|
+
registerLocale,
|
|
25
|
+
resolveMessages,
|
|
26
|
+
resolveSlashItems,
|
|
27
|
+
ru,
|
|
28
|
+
setAlignInSelection,
|
|
29
|
+
setColorInSelection,
|
|
30
|
+
setDomSelection,
|
|
31
|
+
setLinkInSelection,
|
|
32
|
+
syncToolbar,
|
|
33
|
+
temmlRenderer,
|
|
34
|
+
toggleFormatInSelection,
|
|
35
|
+
useEditorUI,
|
|
36
|
+
useLocale,
|
|
37
|
+
useSnippetInsert,
|
|
38
|
+
useT,
|
|
39
|
+
visibleSlashItems
|
|
40
|
+
} from "./chunk-TPUX4C45.js";
|
|
41
|
+
|
|
42
|
+
// src/context/EditorContext.ts
|
|
43
|
+
import { createContext } from "react";
|
|
44
|
+
var EditorContext = createContext(null);
|
|
45
|
+
var EditorViewContext = createContext(null);
|
|
46
|
+
|
|
47
|
+
// src/context/EditorProvider.tsx
|
|
48
|
+
import { useLayoutEffect, useMemo } from "react";
|
|
49
|
+
|
|
50
|
+
// src/block-handle/BlockHandleController.ts
|
|
51
|
+
var IDLE = Object.freeze({ hover: null, menu: null, drag: null });
|
|
52
|
+
var sameAnchor = (a, b) => a === b || !!a && !!b && a.blockId === b.blockId && a.x === b.x && a.y === b.y;
|
|
53
|
+
var BlockHandleController = class {
|
|
54
|
+
state = IDLE;
|
|
55
|
+
listeners = /* @__PURE__ */ new Set();
|
|
56
|
+
getState = () => this.state;
|
|
57
|
+
subscribe = (listener) => {
|
|
58
|
+
this.listeners.add(listener);
|
|
59
|
+
return () => {
|
|
60
|
+
this.listeners.delete(listener);
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
setHover(hover) {
|
|
64
|
+
if (!sameAnchor(this.state.hover, hover)) this.set({ ...this.state, hover });
|
|
65
|
+
}
|
|
66
|
+
openMenu(menu) {
|
|
67
|
+
this.set({ ...this.state, menu });
|
|
68
|
+
}
|
|
69
|
+
closeMenu() {
|
|
70
|
+
if (this.state.menu) this.set({ ...this.state, menu: null });
|
|
71
|
+
}
|
|
72
|
+
startDrag(blockId) {
|
|
73
|
+
this.set({
|
|
74
|
+
...this.state,
|
|
75
|
+
menu: null,
|
|
76
|
+
drag: { blockId, targetId: null, position: null, indicator: null }
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
updateDrag(patch) {
|
|
80
|
+
const drag = this.state.drag;
|
|
81
|
+
if (!drag) return;
|
|
82
|
+
const next = { ...drag, ...patch };
|
|
83
|
+
const same = next.targetId === drag.targetId && next.position === drag.position && next.indicator?.left === drag.indicator?.left && next.indicator?.top === drag.indicator?.top && next.indicator?.width === drag.indicator?.width;
|
|
84
|
+
if (!same) this.set({ ...this.state, drag: next });
|
|
85
|
+
}
|
|
86
|
+
endDrag() {
|
|
87
|
+
if (this.state.drag) this.set({ ...this.state, drag: null });
|
|
88
|
+
}
|
|
89
|
+
set(state) {
|
|
90
|
+
this.state = state;
|
|
91
|
+
for (const listener of [...this.listeners]) listener();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// src/images/UploadTracker.ts
|
|
96
|
+
var UploadTracker = class {
|
|
97
|
+
active = /* @__PURE__ */ new Set();
|
|
98
|
+
errors = /* @__PURE__ */ new Map();
|
|
99
|
+
listeners = /* @__PURE__ */ new Set();
|
|
100
|
+
has = (blockId) => this.active.has(blockId);
|
|
101
|
+
errorOf = (blockId) => this.errors.get(blockId)?.message;
|
|
102
|
+
errorDetailsOf = (blockId) => this.errors.get(blockId);
|
|
103
|
+
subscribe = (listener) => {
|
|
104
|
+
this.listeners.add(listener);
|
|
105
|
+
return () => {
|
|
106
|
+
this.listeners.delete(listener);
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
start(blockId) {
|
|
110
|
+
this.errors.delete(blockId);
|
|
111
|
+
this.active.add(blockId);
|
|
112
|
+
this.notify();
|
|
113
|
+
}
|
|
114
|
+
finish(blockId, error) {
|
|
115
|
+
const changed = this.active.delete(blockId);
|
|
116
|
+
if (error) this.errors.set(blockId, typeof error === "string" ? { message: error } : error);
|
|
117
|
+
if (changed || error) this.notify();
|
|
118
|
+
}
|
|
119
|
+
clearError(blockId) {
|
|
120
|
+
if (this.errors.delete(blockId)) this.notify();
|
|
121
|
+
}
|
|
122
|
+
notify() {
|
|
123
|
+
for (const listener of [...this.listeners]) listener();
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// src/editing/BlockSelectionController.ts
|
|
128
|
+
var BlockSelectionController = class {
|
|
129
|
+
selected = /* @__PURE__ */ new Set();
|
|
130
|
+
listeners = /* @__PURE__ */ new Set();
|
|
131
|
+
isSelected = (blockId) => this.selected.has(blockId);
|
|
132
|
+
subscribe = (listener) => {
|
|
133
|
+
this.listeners.add(listener);
|
|
134
|
+
return () => {
|
|
135
|
+
this.listeners.delete(listener);
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
set(ids) {
|
|
139
|
+
if (ids.length === this.selected.size && ids.every((id) => this.selected.has(id))) return;
|
|
140
|
+
this.selected = new Set(ids);
|
|
141
|
+
for (const listener of [...this.listeners]) listener();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// src/formula/FormulaController.ts
|
|
146
|
+
var CLOSED = Object.freeze({
|
|
147
|
+
open: false,
|
|
148
|
+
mode: "insert",
|
|
149
|
+
blockId: "",
|
|
150
|
+
from: 0,
|
|
151
|
+
to: 0,
|
|
152
|
+
latex: "",
|
|
153
|
+
position: { x: 0, y: 0, top: 0 }
|
|
154
|
+
});
|
|
155
|
+
var FormulaController = class {
|
|
156
|
+
state = CLOSED;
|
|
157
|
+
listeners = /* @__PURE__ */ new Set();
|
|
158
|
+
getState = () => this.state;
|
|
159
|
+
subscribe = (listener) => {
|
|
160
|
+
this.listeners.add(listener);
|
|
161
|
+
return () => {
|
|
162
|
+
this.listeners.delete(listener);
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
open(state) {
|
|
166
|
+
this.set({ ...state, open: true });
|
|
167
|
+
}
|
|
168
|
+
close() {
|
|
169
|
+
if (this.state.open) this.set(CLOSED);
|
|
170
|
+
}
|
|
171
|
+
set(state) {
|
|
172
|
+
this.state = state;
|
|
173
|
+
for (const listener of [...this.listeners]) listener();
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// src/editing/CaretController.ts
|
|
178
|
+
var CaretController = class {
|
|
179
|
+
blockId = null;
|
|
180
|
+
listeners = /* @__PURE__ */ new Set();
|
|
181
|
+
has = (blockId) => this.blockId === blockId;
|
|
182
|
+
subscribe = (listener) => {
|
|
183
|
+
this.listeners.add(listener);
|
|
184
|
+
return () => {
|
|
185
|
+
this.listeners.delete(listener);
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
set(blockId) {
|
|
189
|
+
if (this.blockId === blockId) return;
|
|
190
|
+
this.blockId = blockId;
|
|
191
|
+
for (const listener of [...this.listeners]) listener();
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// src/slash/SlashMenuController.ts
|
|
196
|
+
var CLOSED2 = Object.freeze({
|
|
197
|
+
open: false,
|
|
198
|
+
blockId: null,
|
|
199
|
+
slashOffset: 0,
|
|
200
|
+
query: "",
|
|
201
|
+
activeIndex: 0,
|
|
202
|
+
position: null
|
|
203
|
+
});
|
|
204
|
+
var SlashMenuController = class {
|
|
205
|
+
state = CLOSED2;
|
|
206
|
+
listeners = /* @__PURE__ */ new Set();
|
|
207
|
+
getState = () => this.state;
|
|
208
|
+
subscribe = (listener) => {
|
|
209
|
+
this.listeners.add(listener);
|
|
210
|
+
return () => {
|
|
211
|
+
this.listeners.delete(listener);
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
open(blockId, slashOffset) {
|
|
215
|
+
this.set({ ...CLOSED2, open: true, blockId, slashOffset });
|
|
216
|
+
}
|
|
217
|
+
close() {
|
|
218
|
+
if (this.state.open) this.set(CLOSED2);
|
|
219
|
+
}
|
|
220
|
+
update(patch) {
|
|
221
|
+
if (!this.state.open) return;
|
|
222
|
+
const next = { ...this.state, ...patch };
|
|
223
|
+
const same = next.query === this.state.query && next.activeIndex === this.state.activeIndex && next.position?.x === this.state.position?.x && next.position?.y === this.state.position?.y;
|
|
224
|
+
if (!same) this.set(next);
|
|
225
|
+
}
|
|
226
|
+
move(delta, count) {
|
|
227
|
+
if (!this.state.open || count === 0) return;
|
|
228
|
+
this.update({ activeIndex: (this.state.activeIndex + delta + count) % count });
|
|
229
|
+
}
|
|
230
|
+
set(state) {
|
|
231
|
+
this.state = state;
|
|
232
|
+
for (const listener of [...this.listeners]) listener();
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// src/toolbar/ToolbarController.ts
|
|
237
|
+
var HIDDEN = Object.freeze({
|
|
238
|
+
visible: false,
|
|
239
|
+
position: null,
|
|
240
|
+
formats: null,
|
|
241
|
+
range: null,
|
|
242
|
+
linkOpen: false,
|
|
243
|
+
panel: null
|
|
244
|
+
});
|
|
245
|
+
function sameFormats(a, b) {
|
|
246
|
+
return a === b || !!a && !!b && a.bold === b.bold && a.italic === b.italic && a.underline === b.underline && a.strike === b.strike && a.code === b.code && a.link === b.link && a.color === b.color && a.backgroundColor === b.backgroundColor && a.align === b.align;
|
|
247
|
+
}
|
|
248
|
+
var ToolbarController = class {
|
|
249
|
+
state = HIDDEN;
|
|
250
|
+
listeners = /* @__PURE__ */ new Set();
|
|
251
|
+
getState = () => this.state;
|
|
252
|
+
subscribe = (listener) => {
|
|
253
|
+
this.listeners.add(listener);
|
|
254
|
+
return () => {
|
|
255
|
+
this.listeners.delete(listener);
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
show(range, formats, position) {
|
|
259
|
+
const prev = this.state;
|
|
260
|
+
const same = prev.visible && sameFormats(prev.formats, formats) && prev.position?.x === position.x && prev.position?.y === position.y && prev.range?.from.blockId === range.from.blockId && prev.range?.from.offset === range.from.offset && prev.range?.to.blockId === range.to.blockId && prev.range?.to.offset === range.to.offset;
|
|
261
|
+
if (!same) this.set({ ...prev, visible: true, position, formats, range });
|
|
262
|
+
}
|
|
263
|
+
hide() {
|
|
264
|
+
if (this.state.visible) this.set(HIDDEN);
|
|
265
|
+
}
|
|
266
|
+
openLink() {
|
|
267
|
+
if (this.state.visible && !this.state.linkOpen) {
|
|
268
|
+
this.set({ ...this.state, linkOpen: true, panel: null });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
togglePanel(panel) {
|
|
272
|
+
if (!this.state.visible || this.state.linkOpen) return;
|
|
273
|
+
this.set({ ...this.state, panel: this.state.panel === panel ? null : panel });
|
|
274
|
+
}
|
|
275
|
+
closePanel() {
|
|
276
|
+
if (this.state.panel) this.set({ ...this.state, panel: null });
|
|
277
|
+
}
|
|
278
|
+
closeLink() {
|
|
279
|
+
if (this.state.linkOpen) this.set({ ...this.state, linkOpen: false });
|
|
280
|
+
}
|
|
281
|
+
set(state) {
|
|
282
|
+
this.state = state;
|
|
283
|
+
for (const listener of [...this.listeners]) listener();
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// src/editing/ViewportController.ts
|
|
288
|
+
var ViewportController = class {
|
|
289
|
+
edges = [];
|
|
290
|
+
revealed = [];
|
|
291
|
+
lists = 0;
|
|
292
|
+
version = 0;
|
|
293
|
+
listeners = /* @__PURE__ */ new Set();
|
|
294
|
+
getVersion = () => this.version;
|
|
295
|
+
subscribe = (listener) => {
|
|
296
|
+
this.listeners.add(listener);
|
|
297
|
+
return () => {
|
|
298
|
+
this.listeners.delete(listener);
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
getPinned() {
|
|
302
|
+
return this.revealed.length === 0 ? this.edges : [...this.edges, ...this.revealed];
|
|
303
|
+
}
|
|
304
|
+
get virtual() {
|
|
305
|
+
return this.lists > 0;
|
|
306
|
+
}
|
|
307
|
+
attach() {
|
|
308
|
+
this.lists += 1;
|
|
309
|
+
return () => {
|
|
310
|
+
this.lists -= 1;
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
setEdges(ids) {
|
|
314
|
+
if (ids.length === this.edges.length && ids.every((id, i) => this.edges[i] === id)) return;
|
|
315
|
+
this.edges = ids;
|
|
316
|
+
this.notify();
|
|
317
|
+
}
|
|
318
|
+
reveal(ids) {
|
|
319
|
+
if (!this.virtual) return false;
|
|
320
|
+
this.revealed = ids;
|
|
321
|
+
this.notify();
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
notify() {
|
|
325
|
+
this.version += 1;
|
|
326
|
+
for (const listener of [...this.listeners]) listener();
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// src/editing/EditorView.ts
|
|
331
|
+
var EditorView = class {
|
|
332
|
+
elements = /* @__PURE__ */ new Map();
|
|
333
|
+
root = null;
|
|
334
|
+
slash = new SlashMenuController();
|
|
335
|
+
uploads = new UploadTracker();
|
|
336
|
+
selection = new BlockSelectionController();
|
|
337
|
+
caret = new CaretController();
|
|
338
|
+
blocks = new BlockHandleController();
|
|
339
|
+
formula = new FormulaController();
|
|
340
|
+
toolbar = new ToolbarController();
|
|
341
|
+
viewport = new ViewportController();
|
|
342
|
+
pointerDown = false;
|
|
343
|
+
pending = null;
|
|
344
|
+
setRoot(element) {
|
|
345
|
+
this.root = element;
|
|
346
|
+
return () => {
|
|
347
|
+
if (this.root === element) this.root = null;
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
getRoot() {
|
|
351
|
+
return this.root;
|
|
352
|
+
}
|
|
353
|
+
register(id, element) {
|
|
354
|
+
this.elements.set(id, element);
|
|
355
|
+
const pending = this.pending;
|
|
356
|
+
if (pending && this.elements.has(pending.from.blockId) && this.elements.has(pending.to.blockId)) {
|
|
357
|
+
queueMicrotask(() => {
|
|
358
|
+
if (this.pending === pending) this.placeSelection(pending.from, pending.to);
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return () => {
|
|
362
|
+
if (this.elements.get(id) === element) {
|
|
363
|
+
this.elements.delete(id);
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
getElement(id) {
|
|
368
|
+
return this.elements.get(id);
|
|
369
|
+
}
|
|
370
|
+
placeCaret(id, offset) {
|
|
371
|
+
this.placeSelection({ blockId: id, offset }, { blockId: id, offset });
|
|
372
|
+
}
|
|
373
|
+
placeSelection(from, to) {
|
|
374
|
+
const start = this.elements.get(from.blockId);
|
|
375
|
+
const end = this.elements.get(to.blockId);
|
|
376
|
+
if (!start || !end) {
|
|
377
|
+
this.pending = this.viewport.reveal([from.blockId, to.blockId]) ? { from, to } : null;
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
this.pending = null;
|
|
381
|
+
if (this.root && globalThis.document.activeElement !== this.root) {
|
|
382
|
+
this.root.focus({ preventScroll: true });
|
|
383
|
+
}
|
|
384
|
+
setDomSelection(start, from.offset, end, to.offset);
|
|
385
|
+
end.scrollIntoView?.({ block: "nearest" });
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// src/keymap/shortcut.ts
|
|
390
|
+
var MODIFIER = /^(mod|ctrl|control|meta|cmd|alt|option|shift)-/i;
|
|
391
|
+
var KEY_ALIASES = {
|
|
392
|
+
esc: "escape",
|
|
393
|
+
space: " ",
|
|
394
|
+
up: "arrowup",
|
|
395
|
+
down: "arrowdown",
|
|
396
|
+
left: "arrowleft",
|
|
397
|
+
right: "arrowright",
|
|
398
|
+
del: "delete",
|
|
399
|
+
return: "enter"
|
|
400
|
+
};
|
|
401
|
+
var cache = /* @__PURE__ */ new Map();
|
|
402
|
+
function parse(spec) {
|
|
403
|
+
const cached = cache.get(spec);
|
|
404
|
+
if (cached) return cached;
|
|
405
|
+
const shortcut = {
|
|
406
|
+
mod: false,
|
|
407
|
+
ctrl: false,
|
|
408
|
+
meta: false,
|
|
409
|
+
alt: false,
|
|
410
|
+
shift: false,
|
|
411
|
+
key: ""
|
|
412
|
+
};
|
|
413
|
+
let rest = spec.trim();
|
|
414
|
+
for (let match = MODIFIER.exec(rest); match; match = MODIFIER.exec(rest)) {
|
|
415
|
+
switch (match[1].toLowerCase()) {
|
|
416
|
+
case "mod":
|
|
417
|
+
shortcut.mod = true;
|
|
418
|
+
break;
|
|
419
|
+
case "ctrl":
|
|
420
|
+
case "control":
|
|
421
|
+
shortcut.ctrl = true;
|
|
422
|
+
break;
|
|
423
|
+
case "meta":
|
|
424
|
+
case "cmd":
|
|
425
|
+
shortcut.meta = true;
|
|
426
|
+
break;
|
|
427
|
+
case "alt":
|
|
428
|
+
case "option":
|
|
429
|
+
shortcut.alt = true;
|
|
430
|
+
break;
|
|
431
|
+
default:
|
|
432
|
+
shortcut.shift = true;
|
|
433
|
+
}
|
|
434
|
+
rest = rest.slice(match[0].length);
|
|
435
|
+
}
|
|
436
|
+
const key = rest.toLowerCase();
|
|
437
|
+
if (key === "") throw new Error(`Invalid shortcut "${spec}": no key`);
|
|
438
|
+
shortcut.key = KEY_ALIASES[key] ?? key;
|
|
439
|
+
cache.set(spec, shortcut);
|
|
440
|
+
return shortcut;
|
|
441
|
+
}
|
|
442
|
+
function matchesKey(event, key) {
|
|
443
|
+
const pressed = event.key.toLowerCase();
|
|
444
|
+
if (pressed === key) return true;
|
|
445
|
+
return /^[a-z]$/.test(key) && !/^[a-z]$/.test(pressed) && event.code === `Key${key.toUpperCase()}`;
|
|
446
|
+
}
|
|
447
|
+
function matchesShortcut(event, spec) {
|
|
448
|
+
const shortcut = parse(spec);
|
|
449
|
+
if (event.shiftKey !== shortcut.shift || event.altKey !== shortcut.alt) return false;
|
|
450
|
+
if (shortcut.mod) {
|
|
451
|
+
if (!(event.ctrlKey || event.metaKey)) return false;
|
|
452
|
+
} else if (event.ctrlKey !== shortcut.ctrl || event.metaKey !== shortcut.meta) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
return matchesKey(event, shortcut.key);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/keymap/keymap.ts
|
|
459
|
+
function resolveKeymap(custom = []) {
|
|
460
|
+
if (custom.length === 0) return defaultKeymap;
|
|
461
|
+
const overrides = new Map(custom.map((binding) => [binding.id, binding]));
|
|
462
|
+
const defaultIds = new Set(defaultKeymap.map((binding) => binding.id));
|
|
463
|
+
return [
|
|
464
|
+
...custom.filter((binding) => !defaultIds.has(binding.id)),
|
|
465
|
+
...defaultKeymap.map((binding) => overrides.get(binding.id) ?? binding)
|
|
466
|
+
];
|
|
467
|
+
}
|
|
468
|
+
function unbind(id) {
|
|
469
|
+
return { id, keys: [], run: () => false };
|
|
470
|
+
}
|
|
471
|
+
function handleKeyDown(bindings, context) {
|
|
472
|
+
const { event } = context;
|
|
473
|
+
for (const binding of bindings) {
|
|
474
|
+
const keys = typeof binding.keys === "string" ? [binding.keys] : binding.keys;
|
|
475
|
+
if (!keys.some((spec) => matchesShortcut(event, spec))) continue;
|
|
476
|
+
if (binding.when && !binding.when(context)) continue;
|
|
477
|
+
if (binding.run(context) === false) continue;
|
|
478
|
+
event.preventDefault();
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/blocks/defineBlock.ts
|
|
485
|
+
function defineBlock(definition) {
|
|
486
|
+
return definition;
|
|
487
|
+
}
|
|
488
|
+
function blockComponentsOf(blocks) {
|
|
489
|
+
const registry = {};
|
|
490
|
+
for (const block2 of blocks ?? []) {
|
|
491
|
+
registry[block2.schema.flavour] = block2.component;
|
|
492
|
+
}
|
|
493
|
+
return registry;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/controllers/adapters.tsx
|
|
497
|
+
import { jsx } from "react/jsx-runtime";
|
|
498
|
+
function iconByName(name) {
|
|
499
|
+
return name && Object.hasOwn(ICON_SHAPES, name) ? /* @__PURE__ */ jsx(Icon, { name }) : void 0;
|
|
500
|
+
}
|
|
501
|
+
function controllerSlashItems(editor) {
|
|
502
|
+
return editor.controllers.entries().flatMap(
|
|
503
|
+
([flavour, controller]) => (controller.slashItems ?? []).map((item) => ({
|
|
504
|
+
id: item.id,
|
|
505
|
+
title: item.title,
|
|
506
|
+
description: item.description,
|
|
507
|
+
keywords: item.keywords,
|
|
508
|
+
icon: iconByName(item.icon),
|
|
509
|
+
flavour,
|
|
510
|
+
props: item.props
|
|
511
|
+
}))
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
function controlledBlockAt(context, flavour) {
|
|
515
|
+
const selection = context.selection();
|
|
516
|
+
const block2 = selection && context.editor.store.getBlock(selection.from.blockId);
|
|
517
|
+
return block2?.flavour === flavour ? block2 : void 0;
|
|
518
|
+
}
|
|
519
|
+
function controllerKeymap(editor) {
|
|
520
|
+
return editor.controllers.entries().flatMap(
|
|
521
|
+
([flavour, controller]) => (controller.keymap ?? []).map((binding) => ({
|
|
522
|
+
id: binding.id,
|
|
523
|
+
keys: binding.keys,
|
|
524
|
+
when: (context) => controlledBlockAt(context, flavour) !== void 0,
|
|
525
|
+
run: (context) => {
|
|
526
|
+
const block2 = controlledBlockAt(context, flavour);
|
|
527
|
+
return block2 ? binding.run(context.editor.controllers.contextOf(block2)) : false;
|
|
528
|
+
}
|
|
529
|
+
}))
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
function mergeById(base, preferred, order = "base-first") {
|
|
533
|
+
const ids = new Set(preferred.map((item) => item.id));
|
|
534
|
+
const rest = base.filter((item) => !ids.has(item.id));
|
|
535
|
+
return order === "base-first" ? [...rest, ...preferred] : [...preferred, ...rest];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/context/EditorProvider.tsx
|
|
539
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
540
|
+
function EditorProvider({
|
|
541
|
+
editor,
|
|
542
|
+
children,
|
|
543
|
+
presentation,
|
|
544
|
+
blocks,
|
|
545
|
+
components,
|
|
546
|
+
slots,
|
|
547
|
+
slashItems,
|
|
548
|
+
keymap,
|
|
549
|
+
highlighter,
|
|
550
|
+
formulaRenderer,
|
|
551
|
+
locale,
|
|
552
|
+
locales,
|
|
553
|
+
messages,
|
|
554
|
+
readOnly,
|
|
555
|
+
images
|
|
556
|
+
}) {
|
|
557
|
+
const view = useMemo(() => new EditorView(), [editor]);
|
|
558
|
+
useLayoutEffect(() => {
|
|
559
|
+
if (readOnly === void 0) return;
|
|
560
|
+
const previous = editor.readOnly;
|
|
561
|
+
editor.setReadOnly(readOnly);
|
|
562
|
+
return () => editor.setReadOnly(previous);
|
|
563
|
+
}, [editor, readOnly]);
|
|
564
|
+
const t = useMemo(
|
|
565
|
+
() => createTranslator(resolveMessages({ locale, locales, messages })),
|
|
566
|
+
[locale, locales, messages]
|
|
567
|
+
);
|
|
568
|
+
const ui = useMemo(() => {
|
|
569
|
+
const visual = presentation ?? {};
|
|
570
|
+
const code = highlighter ?? visual.highlighter;
|
|
571
|
+
const math = formulaRenderer ?? visual.formulaRenderer;
|
|
572
|
+
const customSlash = slashItems ?? visual.slashItems;
|
|
573
|
+
const customKeys = keymap ?? [];
|
|
574
|
+
return {
|
|
575
|
+
components: { ...blockComponentsOf(blocks), ...visual.components, ...components },
|
|
576
|
+
slots: { ...visual.slots, ...slots },
|
|
577
|
+
// Пункты контроллеров идут после стандартных; пункт приложения с тем же id важнее.
|
|
578
|
+
slashItems: customSlash === false ? [] : resolveSlashItems(mergeById(controllerSlashItems(editor), customSlash ?? []), t),
|
|
579
|
+
highlighter: code === false ? null : code ?? highlightCode,
|
|
580
|
+
formulaRenderer: math === false ? null : math ?? temmlRenderer,
|
|
581
|
+
locale: locale ?? "en",
|
|
582
|
+
t,
|
|
583
|
+
keymap: resolveKeymap(mergeById(controllerKeymap(editor), customKeys, "preferred-first")),
|
|
584
|
+
images: images ?? {},
|
|
585
|
+
readOnly
|
|
586
|
+
};
|
|
587
|
+
}, [
|
|
588
|
+
editor,
|
|
589
|
+
presentation,
|
|
590
|
+
blocks,
|
|
591
|
+
components,
|
|
592
|
+
slots,
|
|
593
|
+
slashItems,
|
|
594
|
+
keymap,
|
|
595
|
+
highlighter,
|
|
596
|
+
formulaRenderer,
|
|
597
|
+
locale,
|
|
598
|
+
t,
|
|
599
|
+
readOnly,
|
|
600
|
+
images
|
|
601
|
+
]);
|
|
602
|
+
return /* @__PURE__ */ jsx2(EditorContext.Provider, { value: editor, children: /* @__PURE__ */ jsx2(EditorViewContext.Provider, { value: view, children: /* @__PURE__ */ jsx2(EditorUIContext.Provider, { value: ui, children }) }) });
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// src/ui/presentation.ts
|
|
606
|
+
function definePresentation(presentation) {
|
|
607
|
+
return presentation;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/hooks/useEditor.ts
|
|
611
|
+
import { useContext } from "react";
|
|
612
|
+
function useEditor() {
|
|
613
|
+
const editor = useContext(EditorContext);
|
|
614
|
+
if (!editor) {
|
|
615
|
+
throw new Error("useEditor must be used inside <EditorProvider>");
|
|
616
|
+
}
|
|
617
|
+
return editor;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// src/hooks/useEditorView.ts
|
|
621
|
+
import { useContext as useContext2 } from "react";
|
|
622
|
+
function useEditorView() {
|
|
623
|
+
const view = useContext2(EditorViewContext);
|
|
624
|
+
if (!view) {
|
|
625
|
+
throw new Error("useEditorView must be used inside <EditorProvider>");
|
|
626
|
+
}
|
|
627
|
+
return view;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/hooks/useDocument.ts
|
|
631
|
+
import { useSyncExternalStore } from "react";
|
|
632
|
+
function useDocument() {
|
|
633
|
+
const editor = useEditor();
|
|
634
|
+
const { store } = editor;
|
|
635
|
+
useSyncExternalStore(
|
|
636
|
+
(listener) => store.subscribe(listener),
|
|
637
|
+
() => store.getVersion(),
|
|
638
|
+
() => store.getVersion()
|
|
639
|
+
);
|
|
640
|
+
return editor.document;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/hooks/useBlock.ts
|
|
644
|
+
import { useCallback, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
645
|
+
function useBlock(id) {
|
|
646
|
+
const { store } = useEditor();
|
|
647
|
+
const subscribe = useCallback(
|
|
648
|
+
(listener) => store.subscribeBlock(id, listener),
|
|
649
|
+
[store, id]
|
|
650
|
+
);
|
|
651
|
+
const getSnapshot = useCallback(() => store.getBlock(id), [store, id]);
|
|
652
|
+
return useSyncExternalStore2(subscribe, getSnapshot, getSnapshot);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/hooks/useChildren.ts
|
|
656
|
+
var NO_CHILDREN = [];
|
|
657
|
+
function useChildren(parentId) {
|
|
658
|
+
const parent = useBlock(parentId);
|
|
659
|
+
return parent?.children ?? NO_CHILDREN;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// src/hooks/useHistory.ts
|
|
663
|
+
import { useCallback as useCallback2, useSyncExternalStore as useSyncExternalStore3 } from "react";
|
|
664
|
+
import { flushSync } from "react-dom";
|
|
665
|
+
function useHistory() {
|
|
666
|
+
const editor = useEditor();
|
|
667
|
+
const view = useEditorView();
|
|
668
|
+
const { store } = editor;
|
|
669
|
+
useSyncExternalStore3(
|
|
670
|
+
(listener) => store.subscribe(listener),
|
|
671
|
+
() => store.getHistoryVersion(),
|
|
672
|
+
() => store.getHistoryVersion()
|
|
673
|
+
);
|
|
674
|
+
const apply = useCallback2(
|
|
675
|
+
(action) => {
|
|
676
|
+
let result;
|
|
677
|
+
flushSync(() => {
|
|
678
|
+
result = action();
|
|
679
|
+
});
|
|
680
|
+
if (result?.caret) view.placeCaret(result.caret.blockId, result.caret.offset);
|
|
681
|
+
return result !== void 0;
|
|
682
|
+
},
|
|
683
|
+
[view]
|
|
684
|
+
);
|
|
685
|
+
const undo = useCallback2(() => apply(() => editor.undo()), [apply, editor]);
|
|
686
|
+
const redo = useCallback2(() => apply(() => editor.redo()), [apply, editor]);
|
|
687
|
+
return { canUndo: editor.canUndo, canRedo: editor.canRedo, undo, redo };
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// src/hooks/useReadOnly.ts
|
|
691
|
+
import { useSyncExternalStore as useSyncExternalStore4 } from "react";
|
|
692
|
+
function useReadOnly() {
|
|
693
|
+
const editor = useEditor();
|
|
694
|
+
const { readOnly } = useEditorUI();
|
|
695
|
+
const state = useSyncExternalStore4(
|
|
696
|
+
(listener) => editor.subscribeReadOnly(listener),
|
|
697
|
+
() => editor.readOnly,
|
|
698
|
+
() => editor.readOnly
|
|
699
|
+
);
|
|
700
|
+
return readOnly ?? state;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/hooks/useBlockController.ts
|
|
704
|
+
import { useMemo as useMemo2 } from "react";
|
|
705
|
+
function useBlockController(block2) {
|
|
706
|
+
const editor = useEditor();
|
|
707
|
+
return useMemo2(() => {
|
|
708
|
+
const context = editor.controllers.contextOf(block2);
|
|
709
|
+
return Object.assign(Object.create(context), {
|
|
710
|
+
controller: editor.controllers.get(block2.flavour)
|
|
711
|
+
});
|
|
712
|
+
}, [editor, block2]);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/clipboard/htmlToFragment.ts
|
|
716
|
+
import { sanitizeFragment } from "@inkbrick/engine";
|
|
717
|
+
|
|
718
|
+
// src/clipboard/html/blocks.ts
|
|
719
|
+
import { isSafeImageUrl, normalizeInlineNodes as normalizeInlineNodes2 } from "@inkbrick/document";
|
|
720
|
+
|
|
721
|
+
// src/clipboard/html/inline.ts
|
|
722
|
+
import { normalizeInlineNodes } from "@inkbrick/document";
|
|
723
|
+
|
|
724
|
+
// src/clipboard/html/style.ts
|
|
725
|
+
import { isSafeUrl, normalizeLinkUrl } from "@inkbrick/document";
|
|
726
|
+
|
|
727
|
+
// src/clipboard/html/tags.ts
|
|
728
|
+
var MAX_DEPTH = 60;
|
|
729
|
+
var SKIP_TAGS = /* @__PURE__ */ new Set([
|
|
730
|
+
"MATH",
|
|
731
|
+
"SCRIPT",
|
|
732
|
+
"STYLE",
|
|
733
|
+
"HEAD",
|
|
734
|
+
"TEMPLATE",
|
|
735
|
+
"NOSCRIPT",
|
|
736
|
+
"IFRAME",
|
|
737
|
+
"OBJECT",
|
|
738
|
+
"EMBED",
|
|
739
|
+
"SVG",
|
|
740
|
+
"CANVAS",
|
|
741
|
+
"IMG",
|
|
742
|
+
"VIDEO",
|
|
743
|
+
"AUDIO",
|
|
744
|
+
"TITLE",
|
|
745
|
+
"META",
|
|
746
|
+
"LINK",
|
|
747
|
+
"INPUT",
|
|
748
|
+
"BUTTON",
|
|
749
|
+
"SELECT",
|
|
750
|
+
"TEXTAREA",
|
|
751
|
+
"FORM"
|
|
752
|
+
]);
|
|
753
|
+
var BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
754
|
+
"P",
|
|
755
|
+
"DIV",
|
|
756
|
+
"H1",
|
|
757
|
+
"H2",
|
|
758
|
+
"H3",
|
|
759
|
+
"H4",
|
|
760
|
+
"H5",
|
|
761
|
+
"H6",
|
|
762
|
+
"UL",
|
|
763
|
+
"OL",
|
|
764
|
+
"LI",
|
|
765
|
+
"BLOCKQUOTE",
|
|
766
|
+
"PRE",
|
|
767
|
+
"HR",
|
|
768
|
+
"TABLE",
|
|
769
|
+
"TR",
|
|
770
|
+
"SECTION",
|
|
771
|
+
"ARTICLE",
|
|
772
|
+
"HEADER",
|
|
773
|
+
"FOOTER",
|
|
774
|
+
"MAIN",
|
|
775
|
+
"ASIDE",
|
|
776
|
+
"NAV",
|
|
777
|
+
"FIGURE",
|
|
778
|
+
"FIGCAPTION",
|
|
779
|
+
"DL",
|
|
780
|
+
"DT",
|
|
781
|
+
"DD",
|
|
782
|
+
"ADDRESS",
|
|
783
|
+
"FIELDSET",
|
|
784
|
+
"BODY",
|
|
785
|
+
"HTML",
|
|
786
|
+
"TBODY",
|
|
787
|
+
"THEAD",
|
|
788
|
+
"TFOOT",
|
|
789
|
+
"CAPTION"
|
|
790
|
+
]);
|
|
791
|
+
var tagOf = (element) => element.localName.toUpperCase();
|
|
792
|
+
|
|
793
|
+
// src/clipboard/html/style.ts
|
|
794
|
+
function weight(value) {
|
|
795
|
+
const v = value.trim().toLowerCase();
|
|
796
|
+
if (v === "") return void 0;
|
|
797
|
+
if (v === "bold" || v === "bolder") return true;
|
|
798
|
+
if (v === "normal" || v === "lighter") return false;
|
|
799
|
+
const n = Number(v);
|
|
800
|
+
return Number.isNaN(n) ? void 0 : n >= 600;
|
|
801
|
+
}
|
|
802
|
+
function styleOf(element, base) {
|
|
803
|
+
const style = { ...base };
|
|
804
|
+
switch (tagOf(element)) {
|
|
805
|
+
case "B":
|
|
806
|
+
case "STRONG":
|
|
807
|
+
style.bold = true;
|
|
808
|
+
break;
|
|
809
|
+
case "I":
|
|
810
|
+
case "EM":
|
|
811
|
+
case "CITE":
|
|
812
|
+
case "DFN":
|
|
813
|
+
style.italic = true;
|
|
814
|
+
break;
|
|
815
|
+
case "U":
|
|
816
|
+
case "INS":
|
|
817
|
+
style.underline = true;
|
|
818
|
+
break;
|
|
819
|
+
case "S":
|
|
820
|
+
case "DEL":
|
|
821
|
+
case "STRIKE":
|
|
822
|
+
style.strike = true;
|
|
823
|
+
break;
|
|
824
|
+
case "CODE":
|
|
825
|
+
case "KBD":
|
|
826
|
+
case "SAMP":
|
|
827
|
+
case "TT":
|
|
828
|
+
style.code = true;
|
|
829
|
+
break;
|
|
830
|
+
case "A": {
|
|
831
|
+
const href = element.getAttribute("href");
|
|
832
|
+
const link = href ? normalizeLinkUrl(href) : null;
|
|
833
|
+
if (link) style.link = link;
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
const css = element.style;
|
|
838
|
+
if (css) {
|
|
839
|
+
const bold = weight(css.fontWeight ?? "");
|
|
840
|
+
if (bold !== void 0) style.bold = bold;
|
|
841
|
+
const italic = (css.fontStyle ?? "").toLowerCase();
|
|
842
|
+
if (italic === "italic" || italic === "oblique") style.italic = true;
|
|
843
|
+
else if (italic === "normal") style.italic = false;
|
|
844
|
+
const decoration = `${css.textDecorationLine ?? ""} ${css.textDecoration ?? ""}`.toLowerCase();
|
|
845
|
+
if (decoration.includes("underline")) style.underline = true;
|
|
846
|
+
if (decoration.includes("line-through")) style.strike = true;
|
|
847
|
+
}
|
|
848
|
+
return style;
|
|
849
|
+
}
|
|
850
|
+
function attributesOf(style) {
|
|
851
|
+
const attributes = {};
|
|
852
|
+
if (style.bold) attributes.bold = true;
|
|
853
|
+
if (style.italic) attributes.italic = true;
|
|
854
|
+
if (style.underline) attributes.underline = true;
|
|
855
|
+
if (style.strike) attributes.strike = true;
|
|
856
|
+
if (style.code) attributes.code = true;
|
|
857
|
+
if (style.link && isSafeUrl(style.link)) attributes.link = style.link;
|
|
858
|
+
return Object.keys(attributes).length > 0 ? attributes : void 0;
|
|
859
|
+
}
|
|
860
|
+
function isHidden(element) {
|
|
861
|
+
return element.hasAttribute("hidden") || element.style?.display === "none" || element.style?.visibility === "hidden";
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// src/clipboard/html/inline.ts
|
|
865
|
+
function cleanInline(nodes, preserve = false) {
|
|
866
|
+
if (preserve) return normalizeInlineNodes(nodes.filter((n) => n.insert.length > 0));
|
|
867
|
+
const cleaned = [];
|
|
868
|
+
let previousSpace = true;
|
|
869
|
+
for (const node of nodes) {
|
|
870
|
+
let text = "";
|
|
871
|
+
for (const ch of node.insert.replace(/\u00a0/g, " ")) {
|
|
872
|
+
if (ch === "\n") {
|
|
873
|
+
text = text.replace(/ $/, "") + ch;
|
|
874
|
+
previousSpace = true;
|
|
875
|
+
} else if (/\s/.test(ch)) {
|
|
876
|
+
if (!previousSpace) text += " ";
|
|
877
|
+
previousSpace = true;
|
|
878
|
+
} else {
|
|
879
|
+
text += ch;
|
|
880
|
+
previousSpace = false;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
if (text)
|
|
884
|
+
cleaned.push({ insert: text, ...node.attributes ? { attributes: node.attributes } : {} });
|
|
885
|
+
}
|
|
886
|
+
while (cleaned.length > 0) {
|
|
887
|
+
const last = cleaned[cleaned.length - 1];
|
|
888
|
+
const trimmed = last.insert.replace(/[ \n]+$/, "");
|
|
889
|
+
if (trimmed === "") cleaned.pop();
|
|
890
|
+
else {
|
|
891
|
+
last.insert = trimmed;
|
|
892
|
+
break;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
while (cleaned.length > 0 && /^\n+/.test(cleaned[0].insert)) {
|
|
896
|
+
cleaned[0].insert = cleaned[0].insert.replace(/^\n+/, "");
|
|
897
|
+
if (cleaned[0].insert === "") cleaned.shift();
|
|
898
|
+
}
|
|
899
|
+
return normalizeInlineNodes(cleaned);
|
|
900
|
+
}
|
|
901
|
+
function inlineOf(node, style, out, depth, preserve = false) {
|
|
902
|
+
if (depth > MAX_DEPTH) return;
|
|
903
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
904
|
+
const raw = node.textContent ?? "";
|
|
905
|
+
const text = preserve ? raw : raw.replace(/[\t\n\r\f ]+/g, " ");
|
|
906
|
+
const attributes = attributesOf(style);
|
|
907
|
+
if (text) out.push({ insert: text, ...attributes ? { attributes } : {} });
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
|
911
|
+
const element = node;
|
|
912
|
+
if (SKIP_TAGS.has(tagOf(element)) || isHidden(element)) return;
|
|
913
|
+
if (/mso-list:\s*ignore/i.test(element.getAttribute("style") ?? "")) return;
|
|
914
|
+
if (tagOf(element) === "BR") {
|
|
915
|
+
out.push({ insert: "\n" });
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
const next = styleOf(element, style);
|
|
919
|
+
const isBlock = BLOCK_TAGS.has(tagOf(element));
|
|
920
|
+
if (isBlock && out.length > 0) out.push({ insert: "\n" });
|
|
921
|
+
element.childNodes.forEach((child) => inlineOf(child, next, out, depth + 1, preserve));
|
|
922
|
+
if (isBlock) out.push({ insert: "\n" });
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// src/clipboard/html/blocks.ts
|
|
926
|
+
function pastedImageSrc(src) {
|
|
927
|
+
const value = (src ?? "").trim();
|
|
928
|
+
return value !== "" && !value.startsWith("/") && isSafeImageUrl(value) ? value : null;
|
|
929
|
+
}
|
|
930
|
+
function imageBlock(img, caption = "") {
|
|
931
|
+
const src = pastedImageSrc(img.getAttribute("src"));
|
|
932
|
+
if (!src) return null;
|
|
933
|
+
return block("block:image", [], {
|
|
934
|
+
src,
|
|
935
|
+
alt: (img.getAttribute("alt") ?? "").slice(0, 1e3),
|
|
936
|
+
caption: caption.slice(0, 2e3)
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
var block = (flavour, content, props = {}) => ({
|
|
940
|
+
flavour,
|
|
941
|
+
props,
|
|
942
|
+
content,
|
|
943
|
+
children: []
|
|
944
|
+
});
|
|
945
|
+
function hasBlockDescendant(element) {
|
|
946
|
+
const stack = [];
|
|
947
|
+
for (let child = element.lastElementChild; child; child = child.previousElementSibling)
|
|
948
|
+
stack.push(child);
|
|
949
|
+
while (stack.length > 0) {
|
|
950
|
+
const current = stack.pop();
|
|
951
|
+
const tag = tagOf(current);
|
|
952
|
+
if (BLOCK_TAGS.has(tag) && !SKIP_TAGS.has(tag)) return true;
|
|
953
|
+
for (let child = current.lastElementChild; child; child = child.previousElementSibling)
|
|
954
|
+
stack.push(child);
|
|
955
|
+
}
|
|
956
|
+
return false;
|
|
957
|
+
}
|
|
958
|
+
function wordListLevel(element) {
|
|
959
|
+
const style = element.getAttribute("style") ?? "";
|
|
960
|
+
const match = /mso-list:[^;]*level(\d+)/i.exec(style);
|
|
961
|
+
if (match) return Number(match[1]);
|
|
962
|
+
return /^MsoListParagraph/i.test(element.className) ? 1 : null;
|
|
963
|
+
}
|
|
964
|
+
function wordListMarker(element) {
|
|
965
|
+
for (const span of element.querySelectorAll("span")) {
|
|
966
|
+
if (/mso-list:\s*ignore/i.test(span.getAttribute("style") ?? "")) return span.textContent ?? "";
|
|
967
|
+
}
|
|
968
|
+
return "";
|
|
969
|
+
}
|
|
970
|
+
function listItems(list, ordered, style, depth) {
|
|
971
|
+
const result = [];
|
|
972
|
+
for (const child of Array.from(list.children)) {
|
|
973
|
+
if (SKIP_TAGS.has(tagOf(child)) || isHidden(child)) continue;
|
|
974
|
+
if (tagOf(child) === "UL" || tagOf(child) === "OL") {
|
|
975
|
+
const nested = listItems(child, tagOf(child) === "OL", style, depth + 1);
|
|
976
|
+
const parent = result[result.length - 1];
|
|
977
|
+
if (parent) parent.children.push(...nested);
|
|
978
|
+
else result.push(...nested);
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
if (tagOf(child) !== "LI") continue;
|
|
982
|
+
const content = [];
|
|
983
|
+
const children = [];
|
|
984
|
+
const itemStyle = styleOf(child, style);
|
|
985
|
+
const checkbox = child.querySelector(
|
|
986
|
+
':scope > input[type="checkbox"], :scope > p > input[type="checkbox"]'
|
|
987
|
+
);
|
|
988
|
+
const todo = !!checkbox || /task-list-item/.test(child.className);
|
|
989
|
+
child.childNodes.forEach((node) => {
|
|
990
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
991
|
+
const element = node;
|
|
992
|
+
if (tagOf(element) === "UL" || tagOf(element) === "OL") {
|
|
993
|
+
children.push(...listItems(element, tagOf(element) === "OL", itemStyle, depth + 1));
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
inlineOf(node, itemStyle, content, depth + 1);
|
|
998
|
+
});
|
|
999
|
+
const props = { type: todo ? "todo" : ordered ? "ordered" : "bullet" };
|
|
1000
|
+
if (todo) props.checked = !!checkbox?.checked || checkbox?.hasAttribute("checked") === true;
|
|
1001
|
+
const item = block("block:list", cleanInline(content), props);
|
|
1002
|
+
item.children = children;
|
|
1003
|
+
result.push(item);
|
|
1004
|
+
}
|
|
1005
|
+
return result;
|
|
1006
|
+
}
|
|
1007
|
+
function pushWordItem(out, stack, item, level) {
|
|
1008
|
+
while (stack.length > 0 && stack[stack.length - 1].level >= level) stack.pop();
|
|
1009
|
+
const parent = stack[stack.length - 1];
|
|
1010
|
+
if (parent) parent.block.children.push(item);
|
|
1011
|
+
else out.push(item);
|
|
1012
|
+
stack.push({ block: item, level });
|
|
1013
|
+
}
|
|
1014
|
+
function collect(container, style, out, depth) {
|
|
1015
|
+
if (depth > MAX_DEPTH) return;
|
|
1016
|
+
let run = [];
|
|
1017
|
+
let wordStack = [];
|
|
1018
|
+
const flush = () => {
|
|
1019
|
+
const content = cleanInline(run);
|
|
1020
|
+
if (content.length > 0) out.push(block("block:paragraph", content));
|
|
1021
|
+
run = [];
|
|
1022
|
+
};
|
|
1023
|
+
const endWordList = () => {
|
|
1024
|
+
wordStack = [];
|
|
1025
|
+
};
|
|
1026
|
+
container.childNodes.forEach((node) => {
|
|
1027
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1028
|
+
inlineOf(node, style, run, depth + 1);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
|
1032
|
+
const element = node;
|
|
1033
|
+
if (SKIP_TAGS.has(tagOf(element)) || isHidden(element)) return;
|
|
1034
|
+
const tag = tagOf(element);
|
|
1035
|
+
const next = styleOf(element, style);
|
|
1036
|
+
const level = tag === "P" ? wordListLevel(element) : null;
|
|
1037
|
+
if (level !== null) {
|
|
1038
|
+
flush();
|
|
1039
|
+
const marker = wordListMarker(element).trim();
|
|
1040
|
+
const content = [];
|
|
1041
|
+
element.childNodes.forEach((child) => inlineOf(child, next, content, depth + 1));
|
|
1042
|
+
pushWordItem(
|
|
1043
|
+
out,
|
|
1044
|
+
wordStack,
|
|
1045
|
+
block("block:list", cleanInline(content), {
|
|
1046
|
+
type: /^(\d+|[a-z]|[ivxlc]+)[.)]/i.test(marker) ? "ordered" : "bullet"
|
|
1047
|
+
}),
|
|
1048
|
+
level
|
|
1049
|
+
);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
endWordList();
|
|
1053
|
+
if (/^H[1-6]$/.test(tag)) {
|
|
1054
|
+
flush();
|
|
1055
|
+
const content = [];
|
|
1056
|
+
element.childNodes.forEach((child) => inlineOf(child, next, content, depth + 1));
|
|
1057
|
+
const cleaned = cleanInline(content);
|
|
1058
|
+
if (cleaned.length > 0) out.push(block("block:heading", cleaned, { level: Number(tag[1]) }));
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
if (tag === "IMG") {
|
|
1062
|
+
flush();
|
|
1063
|
+
const image = imageBlock(element);
|
|
1064
|
+
if (image) out.push(image);
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
switch (tag) {
|
|
1068
|
+
case "FIGURE": {
|
|
1069
|
+
const img = element.querySelector("img");
|
|
1070
|
+
if (img) {
|
|
1071
|
+
flush();
|
|
1072
|
+
const caption = (element.querySelector("figcaption")?.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
1073
|
+
const image = imageBlock(img, caption);
|
|
1074
|
+
if (image) out.push(image);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
break;
|
|
1078
|
+
}
|
|
1079
|
+
case "ASIDE": {
|
|
1080
|
+
const tone = element.getAttribute("data-callout");
|
|
1081
|
+
if (tone !== null) {
|
|
1082
|
+
flush();
|
|
1083
|
+
const content = [];
|
|
1084
|
+
element.childNodes.forEach((child) => inlineOf(child, next, content, depth + 1));
|
|
1085
|
+
const cleaned = cleanInline(content);
|
|
1086
|
+
if (cleaned.length > 0) {
|
|
1087
|
+
out.push(
|
|
1088
|
+
block(
|
|
1089
|
+
"block:callout",
|
|
1090
|
+
cleaned,
|
|
1091
|
+
/^(info|warning|success|danger)$/.test(tone) ? { tone } : {}
|
|
1092
|
+
)
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
break;
|
|
1098
|
+
}
|
|
1099
|
+
case "UL":
|
|
1100
|
+
case "OL":
|
|
1101
|
+
flush();
|
|
1102
|
+
out.push(...listItems(element, tag === "OL", next, depth + 1));
|
|
1103
|
+
return;
|
|
1104
|
+
case "LI":
|
|
1105
|
+
flush();
|
|
1106
|
+
out.push(
|
|
1107
|
+
...listItems(element.parentElement ?? element, false, next, depth + 1).slice(0, 1)
|
|
1108
|
+
);
|
|
1109
|
+
return;
|
|
1110
|
+
case "PRE": {
|
|
1111
|
+
flush();
|
|
1112
|
+
const code = element.querySelector("code");
|
|
1113
|
+
const language = /language-([\w+#-]+)/.exec(code?.className ?? element.className)?.[1] ?? element.getAttribute("data-language") ?? "plain";
|
|
1114
|
+
const text = (element.textContent ?? "").replace(/\n$/, "");
|
|
1115
|
+
out.push(block("block:code", text ? [{ insert: text }] : [], { language }));
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
case "HR":
|
|
1119
|
+
flush();
|
|
1120
|
+
out.push(block("block:divider", []));
|
|
1121
|
+
return;
|
|
1122
|
+
case "BLOCKQUOTE": {
|
|
1123
|
+
flush();
|
|
1124
|
+
if (hasBlockDescendant(element)) {
|
|
1125
|
+
const inner = [];
|
|
1126
|
+
collect(element, next, inner, depth + 1);
|
|
1127
|
+
out.push(
|
|
1128
|
+
...inner.map(
|
|
1129
|
+
(b) => b.flavour === "block:paragraph" ? { ...b, flavour: "block:quote" } : b
|
|
1130
|
+
)
|
|
1131
|
+
);
|
|
1132
|
+
} else {
|
|
1133
|
+
const content = [];
|
|
1134
|
+
element.childNodes.forEach((child) => inlineOf(child, next, content, depth + 1));
|
|
1135
|
+
const cleaned = cleanInline(content);
|
|
1136
|
+
if (cleaned.length > 0) out.push(block("block:quote", cleaned));
|
|
1137
|
+
}
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
case "TABLE": {
|
|
1141
|
+
flush();
|
|
1142
|
+
element.querySelectorAll("tr").forEach((row) => {
|
|
1143
|
+
const cells = [];
|
|
1144
|
+
row.querySelectorAll(":scope > td, :scope > th").forEach((cell, index) => {
|
|
1145
|
+
const content = [];
|
|
1146
|
+
if (index > 0) cells.push({ insert: " " });
|
|
1147
|
+
cell.childNodes.forEach((child) => inlineOf(child, next, content, depth + 1));
|
|
1148
|
+
cells.push(...cleanInline(content));
|
|
1149
|
+
});
|
|
1150
|
+
const cleaned = normalizeInlineNodes2(cells);
|
|
1151
|
+
if (cleaned.length > 0) out.push(block("block:paragraph", cleaned));
|
|
1152
|
+
});
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
if (hasBlockDescendant(element)) {
|
|
1157
|
+
flush();
|
|
1158
|
+
collect(element, next, out, depth + 1);
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
if (BLOCK_TAGS.has(tag)) {
|
|
1162
|
+
flush();
|
|
1163
|
+
const content = [];
|
|
1164
|
+
element.childNodes.forEach((child) => inlineOf(child, next, content, depth + 1));
|
|
1165
|
+
const cleaned = cleanInline(content);
|
|
1166
|
+
if (cleaned.length > 0) out.push(block("block:paragraph", cleaned));
|
|
1167
|
+
element.querySelectorAll("img").forEach((img) => {
|
|
1168
|
+
const image = imageBlock(img);
|
|
1169
|
+
if (image) out.push(image);
|
|
1170
|
+
});
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
inlineOf(element, style, run, depth + 1);
|
|
1174
|
+
});
|
|
1175
|
+
flush();
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// src/clipboard/htmlToFragment.ts
|
|
1179
|
+
var MAX_HTML = 2e6;
|
|
1180
|
+
function embeddedFragment(html) {
|
|
1181
|
+
if (!html.includes("data-inkbrick-fragment")) return null;
|
|
1182
|
+
const doc = new DOMParser().parseFromString(html.slice(0, MAX_HTML), "text/html");
|
|
1183
|
+
const raw = doc.querySelector("[data-inkbrick-fragment]")?.getAttribute("data-inkbrick-fragment");
|
|
1184
|
+
if (!raw) return null;
|
|
1185
|
+
try {
|
|
1186
|
+
return sanitizeFragment(JSON.parse(decodeURIComponent(raw)));
|
|
1187
|
+
} catch {
|
|
1188
|
+
return null;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
function htmlToFragment(html) {
|
|
1192
|
+
if (!html || html.length > MAX_HTML) return null;
|
|
1193
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
1194
|
+
const blocks = [];
|
|
1195
|
+
collect(doc.body, {}, blocks, 0);
|
|
1196
|
+
const sanitized = sanitizeFragment({ blocks: blocks.map((b) => ({ ...b, complete: true })) });
|
|
1197
|
+
return sanitized;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// src/images/upload.ts
|
|
1201
|
+
import { isSafeImageUrl as isSafeImageUrl2 } from "@inkbrick/document";
|
|
1202
|
+
|
|
1203
|
+
// src/images/ImageError.ts
|
|
1204
|
+
var ImageError = class extends Error {
|
|
1205
|
+
constructor(code, message, params) {
|
|
1206
|
+
super(message);
|
|
1207
|
+
this.code = code;
|
|
1208
|
+
this.params = params;
|
|
1209
|
+
this.name = "ImageError";
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
function toImageErrorInfo(error) {
|
|
1213
|
+
if (error instanceof ImageError) {
|
|
1214
|
+
return { message: error.message, code: error.code, params: error.params };
|
|
1215
|
+
}
|
|
1216
|
+
return { message: error instanceof Error ? error.message : "" };
|
|
1217
|
+
}
|
|
1218
|
+
function formatImageError(info, t) {
|
|
1219
|
+
if (info.code) return t(`image.error.${info.code}`, info.params);
|
|
1220
|
+
return info.message || t("image.error.generic");
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// src/images/types.ts
|
|
1224
|
+
var DEFAULT_IMAGE_TYPES = [
|
|
1225
|
+
"image/png",
|
|
1226
|
+
"image/jpeg",
|
|
1227
|
+
"image/gif",
|
|
1228
|
+
"image/webp",
|
|
1229
|
+
"image/avif"
|
|
1230
|
+
];
|
|
1231
|
+
var DEFAULT_MAX_INLINE_BYTES = 15e5;
|
|
1232
|
+
|
|
1233
|
+
// src/images/upload.ts
|
|
1234
|
+
function isAcceptedImage(file, options) {
|
|
1235
|
+
return (options.acceptedTypes ?? DEFAULT_IMAGE_TYPES).includes(file.type.toLowerCase());
|
|
1236
|
+
}
|
|
1237
|
+
function readAsDataUrl(file) {
|
|
1238
|
+
return new Promise((resolve, reject) => {
|
|
1239
|
+
const reader = new FileReader();
|
|
1240
|
+
reader.onload = () => resolve(String(reader.result));
|
|
1241
|
+
reader.onerror = () => reject(new ImageError("read", "Could not read the file"));
|
|
1242
|
+
reader.readAsDataURL(file);
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
async function uploadImage(file, options) {
|
|
1246
|
+
if (!isAcceptedImage(file, options)) {
|
|
1247
|
+
const type = file.type || "unknown";
|
|
1248
|
+
throw new ImageError("type", `Unsupported image type "${type}"`, { type });
|
|
1249
|
+
}
|
|
1250
|
+
let url;
|
|
1251
|
+
if (options.upload) {
|
|
1252
|
+
url = await options.upload(file);
|
|
1253
|
+
} else {
|
|
1254
|
+
const limit = options.maxInlineBytes ?? DEFAULT_MAX_INLINE_BYTES;
|
|
1255
|
+
if (file.size > limit) {
|
|
1256
|
+
const size = Math.round(limit / 1024);
|
|
1257
|
+
throw new ImageError(
|
|
1258
|
+
"size",
|
|
1259
|
+
`The image is larger than ${size} KB. Provide "images.upload" to store larger images.`,
|
|
1260
|
+
{ size }
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
url = await readAsDataUrl(file);
|
|
1264
|
+
}
|
|
1265
|
+
if (!isSafeImageUrl2(url))
|
|
1266
|
+
throw new ImageError("uploadAddress", "The upload returned an unsupported image address");
|
|
1267
|
+
return url;
|
|
1268
|
+
}
|
|
1269
|
+
function normalizeImageUrl(input) {
|
|
1270
|
+
const value = input.trim();
|
|
1271
|
+
if (value === "") return null;
|
|
1272
|
+
const candidate = /^[a-z][a-z0-9+.-]*:/i.test(value) || value.startsWith("/") ? value : `https://${value}`;
|
|
1273
|
+
return isSafeImageUrl2(candidate) ? candidate : null;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// src/blocks/BlockChildren.tsx
|
|
1277
|
+
import { useContext as useContext3 } from "react";
|
|
1278
|
+
|
|
1279
|
+
// src/renderer/DocumentRenderer.tsx
|
|
1280
|
+
import { createContext as createContext2 } from "react";
|
|
1281
|
+
|
|
1282
|
+
// src/renderer/BlockRenderer.tsx
|
|
1283
|
+
import { memo, useSyncExternalStore as useSyncExternalStore7 } from "react";
|
|
1284
|
+
|
|
1285
|
+
// src/blocks/UnknownBlockView.tsx
|
|
1286
|
+
import { jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
1287
|
+
function UnknownBlockView({ block: block2, children }) {
|
|
1288
|
+
const t = useT();
|
|
1289
|
+
return /* @__PURE__ */ jsxs(
|
|
1290
|
+
"div",
|
|
1291
|
+
{
|
|
1292
|
+
className: "block block-unknown",
|
|
1293
|
+
"data-inkbrick-block": "unknown",
|
|
1294
|
+
role: "note",
|
|
1295
|
+
contentEditable: false,
|
|
1296
|
+
"data-block-id": block2.id,
|
|
1297
|
+
"data-unknown-flavour": block2.flavour,
|
|
1298
|
+
children: [
|
|
1299
|
+
/* @__PURE__ */ jsxs("div", { className: "block-unknown__message", "data-inkbrick-part": "message", children: [
|
|
1300
|
+
t("block.unknown.title"),
|
|
1301
|
+
" ",
|
|
1302
|
+
/* @__PURE__ */ jsx3("code", { children: block2.flavour }),
|
|
1303
|
+
". ",
|
|
1304
|
+
t("block.unknown.hint"),
|
|
1305
|
+
" ",
|
|
1306
|
+
/* @__PURE__ */ jsx3("code", { children: "components" }),
|
|
1307
|
+
"."
|
|
1308
|
+
] }),
|
|
1309
|
+
children
|
|
1310
|
+
]
|
|
1311
|
+
}
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// src/blocks/PageBlock.tsx
|
|
1316
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1317
|
+
function PageBlock({ block: block2 }) {
|
|
1318
|
+
return /* @__PURE__ */ jsx4("div", { className: "page", "data-inkbrick-block": "page", "data-block-id": block2.id, children: /* @__PURE__ */ jsx4(BlockChildren, { id: block2.id }) });
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// src/blocks/NoteBlock.tsx
|
|
1322
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
1323
|
+
function NoteBlock({ block: block2 }) {
|
|
1324
|
+
return /* @__PURE__ */ jsx5("section", { className: "page-content", "data-inkbrick-block": "note", "data-block-id": block2.id, children: /* @__PURE__ */ jsx5(BlockChildren, { id: block2.id }) });
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// src/blocks/ParagraphBlock.tsx
|
|
1328
|
+
import { getBlockAlign } from "@inkbrick/document";
|
|
1329
|
+
|
|
1330
|
+
// src/blocks/Editable.tsx
|
|
1331
|
+
import { useLayoutEffect as useLayoutEffect2, useRef, useSyncExternalStore as useSyncExternalStore5 } from "react";
|
|
1332
|
+
|
|
1333
|
+
// src/editing/renderCode.ts
|
|
1334
|
+
var TOKEN_TYPE = /^[a-z][a-z0-9-]{0,31}$/;
|
|
1335
|
+
function normalizeTokens(tokens, length) {
|
|
1336
|
+
const valid = tokens.filter(
|
|
1337
|
+
(token) => Number.isInteger(token.from) && Number.isInteger(token.to) && token.from >= 0 && token.to <= length && token.from < token.to && TOKEN_TYPE.test(token.type)
|
|
1338
|
+
).sort((a, b) => a.from - b.from || b.to - a.to);
|
|
1339
|
+
const result = [];
|
|
1340
|
+
let end = 0;
|
|
1341
|
+
for (const token of valid) {
|
|
1342
|
+
if (token.from >= end) {
|
|
1343
|
+
result.push(token);
|
|
1344
|
+
end = token.to;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
return result;
|
|
1348
|
+
}
|
|
1349
|
+
function renderCode(root, text, tokens) {
|
|
1350
|
+
const doc = globalThis.document;
|
|
1351
|
+
const fragment = doc.createDocumentFragment();
|
|
1352
|
+
let position = 0;
|
|
1353
|
+
for (const token of normalizeTokens(tokens, text.length)) {
|
|
1354
|
+
if (token.from > position)
|
|
1355
|
+
fragment.appendChild(doc.createTextNode(text.slice(position, token.from)));
|
|
1356
|
+
const span = doc.createElement("span");
|
|
1357
|
+
span.setAttribute("data-inkbrick-token", token.type);
|
|
1358
|
+
span.textContent = text.slice(token.from, token.to);
|
|
1359
|
+
fragment.appendChild(span);
|
|
1360
|
+
position = token.to;
|
|
1361
|
+
}
|
|
1362
|
+
if (position < text.length) fragment.appendChild(doc.createTextNode(text.slice(position)));
|
|
1363
|
+
if (text === "" || text.endsWith("\n")) fragment.appendChild(doc.createElement("br"));
|
|
1364
|
+
root.replaceChildren(fragment);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// src/editing/renderInline.ts
|
|
1368
|
+
import {
|
|
1369
|
+
FORMULA_CHAR,
|
|
1370
|
+
isColorName,
|
|
1371
|
+
isSafeUrl as isSafeUrl2,
|
|
1372
|
+
safeCssColor
|
|
1373
|
+
} from "@inkbrick/document";
|
|
1374
|
+
function wrap(node, tag) {
|
|
1375
|
+
const element = globalThis.document.createElement(tag);
|
|
1376
|
+
element.appendChild(node);
|
|
1377
|
+
return element;
|
|
1378
|
+
}
|
|
1379
|
+
function renderFormulaAtom(latex, options) {
|
|
1380
|
+
const doc = globalThis.document;
|
|
1381
|
+
const host = doc.createElement("span");
|
|
1382
|
+
host.setAttribute("data-inkbrick-formula", "");
|
|
1383
|
+
host.setAttribute("data-inkbrick-part", "formula-inline");
|
|
1384
|
+
host.setAttribute("data-latex", latex);
|
|
1385
|
+
host.setAttribute("contenteditable", "false");
|
|
1386
|
+
host.textContent = FORMULA_CHAR;
|
|
1387
|
+
const view = doc.createElement("span");
|
|
1388
|
+
view.setAttribute("part", "math");
|
|
1389
|
+
try {
|
|
1390
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
1391
|
+
ensureMathStyles(shadow);
|
|
1392
|
+
shadow.appendChild(view);
|
|
1393
|
+
} catch {
|
|
1394
|
+
host.setAttribute("data-state", "fallback");
|
|
1395
|
+
return host;
|
|
1396
|
+
}
|
|
1397
|
+
mountFormula(view, latex, {
|
|
1398
|
+
display: false,
|
|
1399
|
+
renderer: options.formulaRenderer ?? null,
|
|
1400
|
+
onSettled: (state) => host.setAttribute("data-state", state)
|
|
1401
|
+
});
|
|
1402
|
+
host.setAttribute("data-state", view.getAttribute("data-state") ?? "loading");
|
|
1403
|
+
return host;
|
|
1404
|
+
}
|
|
1405
|
+
function renderNode(insert, attributes, options) {
|
|
1406
|
+
let node;
|
|
1407
|
+
if (typeof attributes?.formula === "string") {
|
|
1408
|
+
const fragment = globalThis.document.createDocumentFragment();
|
|
1409
|
+
for (let i = 0; i < insert.length; i++)
|
|
1410
|
+
fragment.appendChild(renderFormulaAtom(attributes.formula, options));
|
|
1411
|
+
node = fragment;
|
|
1412
|
+
} else {
|
|
1413
|
+
node = globalThis.document.createTextNode(insert);
|
|
1414
|
+
}
|
|
1415
|
+
if (!attributes) return node;
|
|
1416
|
+
if (attributes.code) node = wrap(node, "code");
|
|
1417
|
+
if (attributes.bold) node = wrap(node, "strong");
|
|
1418
|
+
if (attributes.italic) node = wrap(node, "em");
|
|
1419
|
+
if (attributes.underline) node = wrap(node, "u");
|
|
1420
|
+
if (attributes.strike) node = wrap(node, "s");
|
|
1421
|
+
if (typeof attributes.link === "string" && isSafeUrl2(attributes.link)) {
|
|
1422
|
+
const anchor = wrap(node, "a");
|
|
1423
|
+
anchor.href = attributes.link.trim();
|
|
1424
|
+
anchor.rel = "noopener noreferrer";
|
|
1425
|
+
anchor.target = "_blank";
|
|
1426
|
+
node = anchor;
|
|
1427
|
+
}
|
|
1428
|
+
const color = attributes.color;
|
|
1429
|
+
const background = attributes.backgroundColor;
|
|
1430
|
+
const safeColor = isColorName(color) ? null : safeCssColor(color);
|
|
1431
|
+
const safeBackground = isColorName(background) ? null : safeCssColor(background);
|
|
1432
|
+
if (isColorName(color) || isColorName(background) || safeColor || safeBackground) {
|
|
1433
|
+
const span = wrap(node, "span");
|
|
1434
|
+
if (isColorName(color)) span.setAttribute("data-inkbrick-color", color);
|
|
1435
|
+
else if (safeColor) span.style.color = safeColor;
|
|
1436
|
+
if (isColorName(background)) span.setAttribute("data-inkbrick-bg", background);
|
|
1437
|
+
else if (safeBackground) span.style.backgroundColor = safeBackground;
|
|
1438
|
+
node = span;
|
|
1439
|
+
}
|
|
1440
|
+
return node;
|
|
1441
|
+
}
|
|
1442
|
+
function renderInline(root, nodes, options = {}) {
|
|
1443
|
+
const fragment = globalThis.document.createDocumentFragment();
|
|
1444
|
+
for (const { insert, attributes } of nodes) {
|
|
1445
|
+
if (insert.length > 0) fragment.appendChild(renderNode(insert, attributes, options));
|
|
1446
|
+
}
|
|
1447
|
+
const text = nodes.map((node) => node.insert).join("");
|
|
1448
|
+
if (text === "" || text.endsWith("\n")) {
|
|
1449
|
+
fragment.appendChild(globalThis.document.createElement("br"));
|
|
1450
|
+
}
|
|
1451
|
+
root.replaceChildren(fragment);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// src/blocks/Editable.tsx
|
|
1455
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
1456
|
+
function Editable({ block: block2, placeholder }) {
|
|
1457
|
+
const ref = useRef(null);
|
|
1458
|
+
const view = useEditorView();
|
|
1459
|
+
const t = useT();
|
|
1460
|
+
const readOnly = useReadOnly();
|
|
1461
|
+
const { slashItems, highlighter, formulaRenderer } = useEditorUI();
|
|
1462
|
+
const focused = useSyncExternalStore5(
|
|
1463
|
+
view.caret.subscribe,
|
|
1464
|
+
() => view.caret.has(block2.id),
|
|
1465
|
+
() => false
|
|
1466
|
+
);
|
|
1467
|
+
useLayoutEffect2(() => view.register(block2.id, ref.current), [view, block2.id]);
|
|
1468
|
+
const isCode = block2.flavour === "block:code";
|
|
1469
|
+
const language = typeof block2.props.language === "string" ? block2.props.language : "plain";
|
|
1470
|
+
useLayoutEffect2(() => {
|
|
1471
|
+
const plain = block2.content.every((node) => !node.attributes);
|
|
1472
|
+
if (isCode && highlighter && plain) {
|
|
1473
|
+
const text = block2.content.map((node) => node.insert).join("");
|
|
1474
|
+
let tokens = [];
|
|
1475
|
+
try {
|
|
1476
|
+
tokens = highlighter(text, language);
|
|
1477
|
+
} catch (error) {
|
|
1478
|
+
console.error("[@inkbrick/react] The code highlighter failed:", error);
|
|
1479
|
+
}
|
|
1480
|
+
renderCode(ref.current, text, tokens);
|
|
1481
|
+
} else {
|
|
1482
|
+
renderInline(ref.current, block2.content, { formulaRenderer });
|
|
1483
|
+
}
|
|
1484
|
+
}, [block2.content, isCode, language, highlighter, formulaRenderer]);
|
|
1485
|
+
const isEmpty = block2.content.every((node) => node.insert.length === 0);
|
|
1486
|
+
const hinted = !readOnly && slashItems.length > 0 && block2.flavour !== "block:code";
|
|
1487
|
+
return /* @__PURE__ */ jsx6(
|
|
1488
|
+
"div",
|
|
1489
|
+
{
|
|
1490
|
+
ref,
|
|
1491
|
+
className: "editable",
|
|
1492
|
+
"data-inkbrick-part": "content",
|
|
1493
|
+
"data-id": block2.id,
|
|
1494
|
+
"data-empty": isEmpty,
|
|
1495
|
+
"data-focused": focused || void 0,
|
|
1496
|
+
"data-hint": hinted ? t("editor.hint") : void 0,
|
|
1497
|
+
"data-placeholder": placeholder
|
|
1498
|
+
}
|
|
1499
|
+
);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
// src/blocks/ParagraphBlock.tsx
|
|
1503
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
1504
|
+
function ParagraphBlock({ block: block2, selected }) {
|
|
1505
|
+
const align = getBlockAlign(block2) ?? "left";
|
|
1506
|
+
return /* @__PURE__ */ jsx7(
|
|
1507
|
+
"div",
|
|
1508
|
+
{
|
|
1509
|
+
className: "block block-paragraph",
|
|
1510
|
+
"data-inkbrick-block": "paragraph",
|
|
1511
|
+
"data-block-id": block2.id,
|
|
1512
|
+
"data-selected": selected || void 0,
|
|
1513
|
+
"data-align": align,
|
|
1514
|
+
children: /* @__PURE__ */ jsx7(Editable, { block: block2 })
|
|
1515
|
+
}
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// src/blocks/HeadingBlock.tsx
|
|
1520
|
+
import { getBlockAlign as getBlockAlign2 } from "@inkbrick/document";
|
|
1521
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
1522
|
+
function HeadingBlock({ block: block2, selected }) {
|
|
1523
|
+
const level = typeof block2.props.level === "number" ? block2.props.level : 1;
|
|
1524
|
+
const align = getBlockAlign2(block2) ?? "left";
|
|
1525
|
+
return /* @__PURE__ */ jsx8(
|
|
1526
|
+
"div",
|
|
1527
|
+
{
|
|
1528
|
+
className: `block block-heading level-${level}`,
|
|
1529
|
+
"data-inkbrick-block": "heading",
|
|
1530
|
+
"data-level": level,
|
|
1531
|
+
"data-block-id": block2.id,
|
|
1532
|
+
"data-selected": selected || void 0,
|
|
1533
|
+
"data-align": align,
|
|
1534
|
+
children: /* @__PURE__ */ jsx8(Editable, { block: block2 })
|
|
1535
|
+
}
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// src/blocks/ListBlock.tsx
|
|
1540
|
+
import { getBlockAlign as getBlockAlign3 } from "@inkbrick/document";
|
|
1541
|
+
import { jsx as jsx9, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1542
|
+
function useOrderedNumber(block2) {
|
|
1543
|
+
const { store } = useEditor();
|
|
1544
|
+
const parent = useBlock(block2.parentId ?? "");
|
|
1545
|
+
if (!parent) return 1;
|
|
1546
|
+
let number = 1;
|
|
1547
|
+
for (let i = parent.children.indexOf(block2.id) - 1; i >= 0; i--) {
|
|
1548
|
+
const sibling = store.getBlock(parent.children[i]);
|
|
1549
|
+
if (sibling?.flavour !== "block:list" || sibling.props.type !== "ordered") break;
|
|
1550
|
+
number++;
|
|
1551
|
+
}
|
|
1552
|
+
return number;
|
|
1553
|
+
}
|
|
1554
|
+
function ListBlock({ block: block2, selected, readOnly }) {
|
|
1555
|
+
const editor = useEditor();
|
|
1556
|
+
const t = useT();
|
|
1557
|
+
const children = useChildren(block2.id);
|
|
1558
|
+
const orderedNumber = useOrderedNumber(block2);
|
|
1559
|
+
const type = typeof block2.props.type === "string" ? block2.props.type : "bullet";
|
|
1560
|
+
return /* @__PURE__ */ jsxs2(
|
|
1561
|
+
"div",
|
|
1562
|
+
{
|
|
1563
|
+
className: "block-list-item",
|
|
1564
|
+
"data-inkbrick-block": "list",
|
|
1565
|
+
"data-align": getBlockAlign3(block2) ?? "left",
|
|
1566
|
+
"data-block-id": block2.id,
|
|
1567
|
+
"data-list-type": type,
|
|
1568
|
+
"data-checked": type === "todo" ? block2.props.checked === true : void 0,
|
|
1569
|
+
"data-selected": selected || void 0,
|
|
1570
|
+
children: [
|
|
1571
|
+
/* @__PURE__ */ jsxs2("div", { className: "block block-list", "data-inkbrick-part": "row", "data-list-type": type, children: [
|
|
1572
|
+
type === "todo" ? /* @__PURE__ */ jsx9(
|
|
1573
|
+
"input",
|
|
1574
|
+
{
|
|
1575
|
+
className: "list-marker list-checkbox",
|
|
1576
|
+
"data-inkbrick-part": "checkbox",
|
|
1577
|
+
type: "checkbox",
|
|
1578
|
+
"aria-label": t("block.list.done"),
|
|
1579
|
+
contentEditable: false,
|
|
1580
|
+
disabled: readOnly,
|
|
1581
|
+
checked: block2.props.checked === true,
|
|
1582
|
+
onChange: (event) => editor.dispatch({
|
|
1583
|
+
type: "setBlockProps",
|
|
1584
|
+
payload: { blockId: block2.id, props: { checked: event.target.checked } }
|
|
1585
|
+
})
|
|
1586
|
+
}
|
|
1587
|
+
) : /* @__PURE__ */ jsx9(
|
|
1588
|
+
"span",
|
|
1589
|
+
{
|
|
1590
|
+
className: "list-marker",
|
|
1591
|
+
"data-inkbrick-part": "marker",
|
|
1592
|
+
contentEditable: false,
|
|
1593
|
+
"aria-hidden": "true",
|
|
1594
|
+
children: type === "ordered" ? `${orderedNumber}.` : "\u2022"
|
|
1595
|
+
}
|
|
1596
|
+
),
|
|
1597
|
+
/* @__PURE__ */ jsx9(Editable, { block: block2 })
|
|
1598
|
+
] }),
|
|
1599
|
+
children.length > 0 && /* @__PURE__ */ jsx9("div", { className: "block-nested-children", "data-inkbrick-part": "children", children: /* @__PURE__ */ jsx9(BlockChildren, { id: block2.id }) })
|
|
1600
|
+
]
|
|
1601
|
+
}
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// src/blocks/QuoteBlock.tsx
|
|
1606
|
+
import { getBlockAlign as getBlockAlign4 } from "@inkbrick/document";
|
|
1607
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
1608
|
+
function QuoteBlock({ block: block2, selected }) {
|
|
1609
|
+
return /* @__PURE__ */ jsx10(
|
|
1610
|
+
"blockquote",
|
|
1611
|
+
{
|
|
1612
|
+
className: "block block-quote",
|
|
1613
|
+
"data-inkbrick-block": "quote",
|
|
1614
|
+
"data-align": getBlockAlign4(block2) ?? "left",
|
|
1615
|
+
"data-block-id": block2.id,
|
|
1616
|
+
"data-selected": selected || void 0,
|
|
1617
|
+
children: /* @__PURE__ */ jsx10(Editable, { block: block2 })
|
|
1618
|
+
}
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// src/blocks/CodeBlock.tsx
|
|
1623
|
+
import { useEffect, useRef as useRef2, useState } from "react";
|
|
1624
|
+
import { jsx as jsx11, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1625
|
+
var CODE_LANGUAGES = [
|
|
1626
|
+
"plain",
|
|
1627
|
+
"typescript",
|
|
1628
|
+
"javascript",
|
|
1629
|
+
"json",
|
|
1630
|
+
"html",
|
|
1631
|
+
"css",
|
|
1632
|
+
"python",
|
|
1633
|
+
"bash",
|
|
1634
|
+
"sql",
|
|
1635
|
+
"go",
|
|
1636
|
+
"rust",
|
|
1637
|
+
"java",
|
|
1638
|
+
"c",
|
|
1639
|
+
"cpp",
|
|
1640
|
+
"markdown",
|
|
1641
|
+
"yaml"
|
|
1642
|
+
];
|
|
1643
|
+
function CodeBlock({ block: block2, editor, selected, readOnly }) {
|
|
1644
|
+
const t = useT();
|
|
1645
|
+
const language = typeof block2.props.language === "string" ? block2.props.language : "plain";
|
|
1646
|
+
const languages = CODE_LANGUAGES.includes(language) ? CODE_LANGUAGES : [...CODE_LANGUAGES, language];
|
|
1647
|
+
const [copied, setCopied] = useState(false);
|
|
1648
|
+
const timer = useRef2(void 0);
|
|
1649
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
1650
|
+
const copy = () => {
|
|
1651
|
+
const text = block2.content.map((node) => node.insert).join("");
|
|
1652
|
+
void navigator.clipboard?.writeText(text).then(() => {
|
|
1653
|
+
setCopied(true);
|
|
1654
|
+
clearTimeout(timer.current);
|
|
1655
|
+
timer.current = setTimeout(() => setCopied(false), 1500);
|
|
1656
|
+
});
|
|
1657
|
+
};
|
|
1658
|
+
return /* @__PURE__ */ jsxs3(
|
|
1659
|
+
"div",
|
|
1660
|
+
{
|
|
1661
|
+
className: "block block-code",
|
|
1662
|
+
"data-inkbrick-block": "code",
|
|
1663
|
+
"data-block-id": block2.id,
|
|
1664
|
+
"data-language": language,
|
|
1665
|
+
"data-selected": selected || void 0,
|
|
1666
|
+
children: [
|
|
1667
|
+
/* @__PURE__ */ jsxs3("div", { className: "code-toolbar", "data-inkbrick-part": "toolbar", contentEditable: false, children: [
|
|
1668
|
+
/* @__PURE__ */ jsx11(
|
|
1669
|
+
"select",
|
|
1670
|
+
{
|
|
1671
|
+
className: "code-language",
|
|
1672
|
+
"data-inkbrick-part": "language",
|
|
1673
|
+
"aria-label": t("block.code.language"),
|
|
1674
|
+
disabled: readOnly,
|
|
1675
|
+
value: language,
|
|
1676
|
+
onChange: (event) => editor.dispatch({
|
|
1677
|
+
type: "setBlockProps",
|
|
1678
|
+
payload: { blockId: block2.id, props: { language: event.target.value } }
|
|
1679
|
+
}),
|
|
1680
|
+
children: languages.map((name) => /* @__PURE__ */ jsx11("option", { value: name, children: name }, name))
|
|
1681
|
+
}
|
|
1682
|
+
),
|
|
1683
|
+
/* @__PURE__ */ jsx11(
|
|
1684
|
+
"button",
|
|
1685
|
+
{
|
|
1686
|
+
type: "button",
|
|
1687
|
+
className: "code-copy",
|
|
1688
|
+
"data-inkbrick-part": "copy",
|
|
1689
|
+
"data-state": copied ? "copied" : void 0,
|
|
1690
|
+
"aria-label": copied ? t("block.code.copied") : t("block.code.copy"),
|
|
1691
|
+
title: copied ? t("block.code.copied") : t("block.code.copy"),
|
|
1692
|
+
onClick: copy,
|
|
1693
|
+
children: /* @__PURE__ */ jsx11(Icon, { name: copied ? "check" : "copy" })
|
|
1694
|
+
}
|
|
1695
|
+
)
|
|
1696
|
+
] }),
|
|
1697
|
+
/* @__PURE__ */ jsx11(Editable, { block: block2 })
|
|
1698
|
+
]
|
|
1699
|
+
}
|
|
1700
|
+
);
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
// src/blocks/DividerBlock.tsx
|
|
1704
|
+
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
1705
|
+
function DividerBlock({ block: block2, selected }) {
|
|
1706
|
+
return /* @__PURE__ */ jsx12(
|
|
1707
|
+
"div",
|
|
1708
|
+
{
|
|
1709
|
+
className: "block block-divider",
|
|
1710
|
+
"data-inkbrick-block": "divider",
|
|
1711
|
+
"data-block-id": block2.id,
|
|
1712
|
+
"data-selected": selected || void 0,
|
|
1713
|
+
contentEditable: false,
|
|
1714
|
+
children: /* @__PURE__ */ jsx12("hr", { "data-inkbrick-part": "rule" })
|
|
1715
|
+
}
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// src/blocks/CalloutBlock.tsx
|
|
1720
|
+
import { CALLOUT_TONES, getBlockAlign as getBlockAlign5 } from "@inkbrick/document";
|
|
1721
|
+
import { jsx as jsx13, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1722
|
+
var ICONS = {
|
|
1723
|
+
info: "info",
|
|
1724
|
+
warning: "warning",
|
|
1725
|
+
success: "success",
|
|
1726
|
+
danger: "danger"
|
|
1727
|
+
};
|
|
1728
|
+
var isTone = (value) => CALLOUT_TONES.includes(value);
|
|
1729
|
+
function CalloutBlock({ block: block2, editor, selected, readOnly }) {
|
|
1730
|
+
const t = useT();
|
|
1731
|
+
const tone = isTone(block2.props.tone) ? block2.props.tone : "info";
|
|
1732
|
+
const icon = typeof block2.props.icon === "string" && block2.props.icon !== "" ? block2.props.icon : /* @__PURE__ */ jsx13(Icon, { name: ICONS[tone] });
|
|
1733
|
+
const nextTone = CALLOUT_TONES[(CALLOUT_TONES.indexOf(tone) + 1) % CALLOUT_TONES.length];
|
|
1734
|
+
return /* @__PURE__ */ jsxs4(
|
|
1735
|
+
"div",
|
|
1736
|
+
{
|
|
1737
|
+
className: "block block-callout",
|
|
1738
|
+
"data-inkbrick-block": "callout",
|
|
1739
|
+
"data-align": getBlockAlign5(block2) ?? "left",
|
|
1740
|
+
"data-tone": tone,
|
|
1741
|
+
"data-block-id": block2.id,
|
|
1742
|
+
"data-selected": selected || void 0,
|
|
1743
|
+
children: [
|
|
1744
|
+
/* @__PURE__ */ jsx13(
|
|
1745
|
+
"button",
|
|
1746
|
+
{
|
|
1747
|
+
type: "button",
|
|
1748
|
+
className: "callout-icon",
|
|
1749
|
+
"data-inkbrick-part": "icon",
|
|
1750
|
+
contentEditable: false,
|
|
1751
|
+
disabled: readOnly,
|
|
1752
|
+
"aria-label": t("block.callout.label", { tone: t(`block.callout.tone.${tone}`) }),
|
|
1753
|
+
title: t("block.callout.change"),
|
|
1754
|
+
onClick: () => editor.dispatch({
|
|
1755
|
+
type: "setBlockProps",
|
|
1756
|
+
payload: { blockId: block2.id, props: { tone: nextTone } }
|
|
1757
|
+
}),
|
|
1758
|
+
children: icon
|
|
1759
|
+
}
|
|
1760
|
+
),
|
|
1761
|
+
/* @__PURE__ */ jsx13(Editable, { block: block2 })
|
|
1762
|
+
]
|
|
1763
|
+
}
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// src/blocks/ImageBlock.tsx
|
|
1768
|
+
import { useEffect as useEffect2, useRef as useRef3, useState as useState2, useSyncExternalStore as useSyncExternalStore6 } from "react";
|
|
1769
|
+
import { isSafeImageUrl as isSafeImageUrl3 } from "@inkbrick/document";
|
|
1770
|
+
|
|
1771
|
+
// src/images/insertImages.ts
|
|
1772
|
+
import { flushSync as flushSync2 } from "react-dom";
|
|
1773
|
+
import { createId } from "@inkbrick/document";
|
|
1774
|
+
function topLevelBlockId(editor, blockId) {
|
|
1775
|
+
let current = editor.store.getBlock(blockId);
|
|
1776
|
+
while (current?.parentId) {
|
|
1777
|
+
const parent = editor.store.getBlock(current.parentId);
|
|
1778
|
+
if (!parent || parent.flavour === "block:note" || !parent.parentId) break;
|
|
1779
|
+
current = parent;
|
|
1780
|
+
}
|
|
1781
|
+
return current?.id ?? blockId;
|
|
1782
|
+
}
|
|
1783
|
+
var altFromName = (file) => file.name.replace(/\.[^.]+$/, "").slice(0, 200);
|
|
1784
|
+
function report(options, error, file) {
|
|
1785
|
+
if (options.onError) options.onError(error, file);
|
|
1786
|
+
else console.error("[@inkbrick/react] Could not add the image:", error);
|
|
1787
|
+
}
|
|
1788
|
+
async function fillImageBlock(editor, view, options, blockId, file, mergeKey) {
|
|
1789
|
+
view.uploads.start(blockId);
|
|
1790
|
+
try {
|
|
1791
|
+
const src = await uploadImage(file, options);
|
|
1792
|
+
flushSync2(() => {
|
|
1793
|
+
editor.dispatch({
|
|
1794
|
+
type: "setBlockProps",
|
|
1795
|
+
payload: { blockId, props: { src, alt: altFromName(file) }, mergeKey }
|
|
1796
|
+
});
|
|
1797
|
+
});
|
|
1798
|
+
view.uploads.finish(blockId);
|
|
1799
|
+
return true;
|
|
1800
|
+
} catch (error) {
|
|
1801
|
+
view.uploads.finish(blockId, toImageErrorInfo(error));
|
|
1802
|
+
report(options, error, file);
|
|
1803
|
+
return false;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
function insertImageFiles(editor, view, options, files, afterId) {
|
|
1807
|
+
const images = files.filter((file) => isAcceptedImage(file, options));
|
|
1808
|
+
if (images.length === 0 || editor.readOnly) return false;
|
|
1809
|
+
let anchor = topLevelBlockId(editor, afterId);
|
|
1810
|
+
let caret;
|
|
1811
|
+
const mergeKey = `images:${createId()}`;
|
|
1812
|
+
const created = [];
|
|
1813
|
+
flushSync2(() => {
|
|
1814
|
+
editor.batch(
|
|
1815
|
+
() => {
|
|
1816
|
+
for (const file of images) {
|
|
1817
|
+
const target = editor.store.getBlock(anchor);
|
|
1818
|
+
const alt = altFromName(file);
|
|
1819
|
+
const emptyParagraph = target?.flavour === "block:paragraph" && target.content.length === 0 && target.children.length === 0;
|
|
1820
|
+
if (emptyParagraph) {
|
|
1821
|
+
caret = editor.dispatch({
|
|
1822
|
+
type: "convertBlock",
|
|
1823
|
+
payload: { blockId: anchor, flavour: "block:image", props: { alt } }
|
|
1824
|
+
});
|
|
1825
|
+
created.push({ id: anchor, file });
|
|
1826
|
+
} else {
|
|
1827
|
+
const inserted = editor.dispatch({
|
|
1828
|
+
type: "insertBlock",
|
|
1829
|
+
payload: { flavour: "block:image", afterId: anchor, props: { alt } }
|
|
1830
|
+
});
|
|
1831
|
+
if (!inserted) continue;
|
|
1832
|
+
created.push({ id: inserted.blockId, file });
|
|
1833
|
+
anchor = inserted.blockId;
|
|
1834
|
+
if (!editor.document.getNextSibling(anchor)) {
|
|
1835
|
+
caret = editor.dispatch({
|
|
1836
|
+
type: "insertBlock",
|
|
1837
|
+
payload: { flavour: "block:paragraph", afterId: anchor }
|
|
1838
|
+
});
|
|
1839
|
+
} else {
|
|
1840
|
+
caret = { blockId: editor.document.getNextSibling(anchor).id, offset: 0 };
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
},
|
|
1845
|
+
{ mergeKey }
|
|
1846
|
+
);
|
|
1847
|
+
});
|
|
1848
|
+
if (caret) view.placeCaret(caret.blockId, caret.offset);
|
|
1849
|
+
for (const { id, file } of created) {
|
|
1850
|
+
void fillImageBlock(editor, view, options, id, file, mergeKey).then((ok) => {
|
|
1851
|
+
if (!ok && (editor.store.getBlock(id)?.props.src ?? "") === "") {
|
|
1852
|
+
flushSync2(() => {
|
|
1853
|
+
editor.execute("delete-block", { id });
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
return true;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
// src/blocks/ImageBlock.tsx
|
|
1862
|
+
import { jsx as jsx14, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1863
|
+
var WIDTHS = [25, 50, 75, 100];
|
|
1864
|
+
var ALIGNS = [
|
|
1865
|
+
{ value: "left", label: "image.align.left", icon: "align-left" },
|
|
1866
|
+
{ value: "center", label: "image.align.center", icon: "align-center" },
|
|
1867
|
+
{ value: "right", label: "image.align.right", icon: "align-right" }
|
|
1868
|
+
];
|
|
1869
|
+
var setProps = (editor, blockId, props) => editor.dispatch({ type: "setBlockProps", payload: { blockId, props } });
|
|
1870
|
+
function CommitInput({
|
|
1871
|
+
value,
|
|
1872
|
+
label,
|
|
1873
|
+
placeholder,
|
|
1874
|
+
className,
|
|
1875
|
+
part,
|
|
1876
|
+
onCommit
|
|
1877
|
+
}) {
|
|
1878
|
+
const [draft, setDraft] = useState2(value);
|
|
1879
|
+
const saved = useRef3(value);
|
|
1880
|
+
const cancelled = useRef3(false);
|
|
1881
|
+
useEffect2(() => {
|
|
1882
|
+
saved.current = value;
|
|
1883
|
+
setDraft(value);
|
|
1884
|
+
}, [value]);
|
|
1885
|
+
const commit = () => {
|
|
1886
|
+
if (cancelled.current) {
|
|
1887
|
+
cancelled.current = false;
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
if (draft !== saved.current) {
|
|
1891
|
+
saved.current = draft;
|
|
1892
|
+
onCommit(draft);
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1895
|
+
return /* @__PURE__ */ jsx14(
|
|
1896
|
+
"input",
|
|
1897
|
+
{
|
|
1898
|
+
className,
|
|
1899
|
+
"data-inkbrick-part": part,
|
|
1900
|
+
type: "text",
|
|
1901
|
+
"aria-label": label,
|
|
1902
|
+
placeholder,
|
|
1903
|
+
value: draft,
|
|
1904
|
+
onChange: (event) => setDraft(event.target.value),
|
|
1905
|
+
onBlur: commit,
|
|
1906
|
+
onKeyDown: (event) => {
|
|
1907
|
+
if (event.key === "Enter") {
|
|
1908
|
+
event.preventDefault();
|
|
1909
|
+
commit();
|
|
1910
|
+
event.target.blur();
|
|
1911
|
+
} else if (event.key === "Escape") {
|
|
1912
|
+
cancelled.current = true;
|
|
1913
|
+
setDraft(saved.current);
|
|
1914
|
+
event.target.blur();
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1920
|
+
function ImagePicker({ blockId, editor }) {
|
|
1921
|
+
const view = useEditorView();
|
|
1922
|
+
const t = useT();
|
|
1923
|
+
const { images } = useEditorUI();
|
|
1924
|
+
const [url, setUrl] = useState2("");
|
|
1925
|
+
const [linkError, setLinkError] = useState2(null);
|
|
1926
|
+
const uploadError = useSyncExternalStore6(
|
|
1927
|
+
view.uploads.subscribe,
|
|
1928
|
+
() => view.uploads.errorDetailsOf(blockId),
|
|
1929
|
+
() => void 0
|
|
1930
|
+
);
|
|
1931
|
+
const error = linkError ? t(linkError) : uploadError ? formatImageError(uploadError, t) : null;
|
|
1932
|
+
const file = useRef3(null);
|
|
1933
|
+
const embed = () => {
|
|
1934
|
+
const normalized = normalizeImageUrl(url);
|
|
1935
|
+
if (!normalized) {
|
|
1936
|
+
setLinkError("image.error.invalidLink");
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
if (editor.dispatch({
|
|
1940
|
+
type: "setBlockProps",
|
|
1941
|
+
payload: { blockId, props: { src: normalized } }
|
|
1942
|
+
}) === void 0) {
|
|
1943
|
+
setLinkError("image.error.unsupportedAddress");
|
|
1944
|
+
}
|
|
1945
|
+
};
|
|
1946
|
+
const upload = async (selected) => {
|
|
1947
|
+
if (!selected) return;
|
|
1948
|
+
setLinkError(null);
|
|
1949
|
+
await fillImageBlock(editor, view, images, blockId, selected);
|
|
1950
|
+
};
|
|
1951
|
+
return /* @__PURE__ */ jsxs5("div", { className: "image-picker", "data-inkbrick-part": "picker", "data-testid": "image-picker", children: [
|
|
1952
|
+
/* @__PURE__ */ jsxs5("div", { className: "image-picker__row", "data-inkbrick-part": "picker-row", children: [
|
|
1953
|
+
/* @__PURE__ */ jsx14(
|
|
1954
|
+
"input",
|
|
1955
|
+
{
|
|
1956
|
+
className: "image-picker__input",
|
|
1957
|
+
"data-inkbrick-part": "picker-input",
|
|
1958
|
+
type: "text",
|
|
1959
|
+
"aria-label": t("image.url.label"),
|
|
1960
|
+
"aria-invalid": linkError !== null,
|
|
1961
|
+
placeholder: t("image.url.placeholder"),
|
|
1962
|
+
value: url,
|
|
1963
|
+
onChange: (event) => {
|
|
1964
|
+
setUrl(event.target.value);
|
|
1965
|
+
setLinkError(null);
|
|
1966
|
+
view.uploads.clearError(blockId);
|
|
1967
|
+
},
|
|
1968
|
+
onKeyDown: (event) => {
|
|
1969
|
+
if (event.key === "Enter") {
|
|
1970
|
+
event.preventDefault();
|
|
1971
|
+
embed();
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
),
|
|
1976
|
+
/* @__PURE__ */ jsx14(
|
|
1977
|
+
"button",
|
|
1978
|
+
{
|
|
1979
|
+
type: "button",
|
|
1980
|
+
className: "image-picker__button",
|
|
1981
|
+
"data-inkbrick-part": "picker-button",
|
|
1982
|
+
onClick: embed,
|
|
1983
|
+
children: t("image.embed")
|
|
1984
|
+
}
|
|
1985
|
+
),
|
|
1986
|
+
/* @__PURE__ */ jsx14(
|
|
1987
|
+
"button",
|
|
1988
|
+
{
|
|
1989
|
+
type: "button",
|
|
1990
|
+
className: "image-picker__button",
|
|
1991
|
+
onClick: () => file.current?.click(),
|
|
1992
|
+
children: t("image.upload")
|
|
1993
|
+
}
|
|
1994
|
+
),
|
|
1995
|
+
/* @__PURE__ */ jsx14(
|
|
1996
|
+
"input",
|
|
1997
|
+
{
|
|
1998
|
+
ref: file,
|
|
1999
|
+
type: "file",
|
|
2000
|
+
hidden: true,
|
|
2001
|
+
"data-testid": "image-file",
|
|
2002
|
+
accept: (images.acceptedTypes ?? DEFAULT_IMAGE_TYPES).join(","),
|
|
2003
|
+
onChange: (event) => {
|
|
2004
|
+
void upload(event.target.files?.[0]);
|
|
2005
|
+
event.target.value = "";
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
)
|
|
2009
|
+
] }),
|
|
2010
|
+
error && /* @__PURE__ */ jsx14("div", { className: "image-picker__error", "data-inkbrick-part": "picker-error", role: "alert", children: error })
|
|
2011
|
+
] });
|
|
2012
|
+
}
|
|
2013
|
+
function ImageBlock({ block: block2, editor, selected, readOnly }) {
|
|
2014
|
+
const view = useEditorView();
|
|
2015
|
+
const t = useT();
|
|
2016
|
+
const uploading = useSyncExternalStore6(
|
|
2017
|
+
view.uploads.subscribe,
|
|
2018
|
+
() => view.uploads.has(block2.id),
|
|
2019
|
+
() => false
|
|
2020
|
+
);
|
|
2021
|
+
const { src, alt, caption } = block2.props;
|
|
2022
|
+
const width = typeof block2.props.width === "number" ? block2.props.width : 100;
|
|
2023
|
+
const align = typeof block2.props.align === "string" ? block2.props.align : "center";
|
|
2024
|
+
const address = typeof src === "string" ? src : "";
|
|
2025
|
+
const [failed, setFailed] = useState2(false);
|
|
2026
|
+
useEffect2(() => setFailed(false), [address]);
|
|
2027
|
+
let content;
|
|
2028
|
+
if (uploading) {
|
|
2029
|
+
content = /* @__PURE__ */ jsx14("div", { className: "image-status", "data-inkbrick-part": "status", "data-state": "loading", role: "status", children: t("image.status.uploading") });
|
|
2030
|
+
} else if (address === "") {
|
|
2031
|
+
content = readOnly ? /* @__PURE__ */ jsx14("div", { className: "image-status", "data-inkbrick-part": "status", "data-state": "empty", children: t("image.status.none") }) : /* @__PURE__ */ jsx14(ImagePicker, { blockId: block2.id, editor });
|
|
2032
|
+
} else if (!isSafeImageUrl3(address)) {
|
|
2033
|
+
content = /* @__PURE__ */ jsx14(
|
|
2034
|
+
"div",
|
|
2035
|
+
{
|
|
2036
|
+
className: "image-status image-status--error",
|
|
2037
|
+
"data-inkbrick-part": "status",
|
|
2038
|
+
"data-state": "error",
|
|
2039
|
+
children: t("image.status.unsupported")
|
|
2040
|
+
}
|
|
2041
|
+
);
|
|
2042
|
+
} else if (failed) {
|
|
2043
|
+
content = /* @__PURE__ */ jsxs5(
|
|
2044
|
+
"div",
|
|
2045
|
+
{
|
|
2046
|
+
className: "image-status image-status--error",
|
|
2047
|
+
"data-inkbrick-part": "status",
|
|
2048
|
+
"data-state": "error",
|
|
2049
|
+
role: "alert",
|
|
2050
|
+
children: [
|
|
2051
|
+
t("image.status.failed"),
|
|
2052
|
+
!readOnly && /* @__PURE__ */ jsx14(
|
|
2053
|
+
"button",
|
|
2054
|
+
{
|
|
2055
|
+
type: "button",
|
|
2056
|
+
className: "image-picker__button",
|
|
2057
|
+
"data-inkbrick-part": "picker-button",
|
|
2058
|
+
onClick: () => setProps(editor, block2.id, { src: "" }),
|
|
2059
|
+
children: t("image.status.retry")
|
|
2060
|
+
}
|
|
2061
|
+
)
|
|
2062
|
+
]
|
|
2063
|
+
}
|
|
2064
|
+
);
|
|
2065
|
+
} else {
|
|
2066
|
+
content = /* @__PURE__ */ jsxs5(
|
|
2067
|
+
"figure",
|
|
2068
|
+
{
|
|
2069
|
+
className: "image-figure",
|
|
2070
|
+
"data-inkbrick-part": "figure",
|
|
2071
|
+
"data-align": align,
|
|
2072
|
+
style: { width: `${width}%` },
|
|
2073
|
+
children: [
|
|
2074
|
+
/* @__PURE__ */ jsx14(
|
|
2075
|
+
"img",
|
|
2076
|
+
{
|
|
2077
|
+
src: address,
|
|
2078
|
+
alt: alt ?? "",
|
|
2079
|
+
loading: "lazy",
|
|
2080
|
+
decoding: "async",
|
|
2081
|
+
draggable: false,
|
|
2082
|
+
"data-inkbrick-part": "image",
|
|
2083
|
+
referrerPolicy: "no-referrer",
|
|
2084
|
+
onError: () => setFailed(true)
|
|
2085
|
+
}
|
|
2086
|
+
),
|
|
2087
|
+
!readOnly && /* @__PURE__ */ jsxs5(
|
|
2088
|
+
"div",
|
|
2089
|
+
{
|
|
2090
|
+
className: "image-tools",
|
|
2091
|
+
"data-inkbrick-part": "tools",
|
|
2092
|
+
role: "toolbar",
|
|
2093
|
+
"aria-label": t("image.options"),
|
|
2094
|
+
children: [
|
|
2095
|
+
ALIGNS.map(({ value, label, icon }) => /* @__PURE__ */ jsx14(
|
|
2096
|
+
"button",
|
|
2097
|
+
{
|
|
2098
|
+
type: "button",
|
|
2099
|
+
className: "image-tools__button",
|
|
2100
|
+
"data-inkbrick-part": "tool",
|
|
2101
|
+
"data-pressed": align === value || void 0,
|
|
2102
|
+
"aria-label": t(label),
|
|
2103
|
+
"aria-pressed": align === value,
|
|
2104
|
+
title: t(label),
|
|
2105
|
+
onClick: () => setProps(editor, block2.id, { align: value }),
|
|
2106
|
+
children: /* @__PURE__ */ jsx14(Icon, { name: icon })
|
|
2107
|
+
},
|
|
2108
|
+
value
|
|
2109
|
+
)),
|
|
2110
|
+
/* @__PURE__ */ jsxs5(
|
|
2111
|
+
"select",
|
|
2112
|
+
{
|
|
2113
|
+
className: "image-tools__select",
|
|
2114
|
+
"data-inkbrick-part": "width",
|
|
2115
|
+
"aria-label": t("image.width"),
|
|
2116
|
+
value: WIDTHS.includes(width) ? width : "",
|
|
2117
|
+
onChange: (event) => setProps(editor, block2.id, { width: Number(event.target.value) }),
|
|
2118
|
+
children: [
|
|
2119
|
+
!WIDTHS.includes(width) && /* @__PURE__ */ jsxs5("option", { value: "", children: [
|
|
2120
|
+
width,
|
|
2121
|
+
"%"
|
|
2122
|
+
] }),
|
|
2123
|
+
WIDTHS.map((w) => /* @__PURE__ */ jsxs5("option", { value: w, children: [
|
|
2124
|
+
w,
|
|
2125
|
+
"%"
|
|
2126
|
+
] }, w))
|
|
2127
|
+
]
|
|
2128
|
+
}
|
|
2129
|
+
),
|
|
2130
|
+
/* @__PURE__ */ jsx14(
|
|
2131
|
+
CommitInput,
|
|
2132
|
+
{
|
|
2133
|
+
className: "image-tools__alt",
|
|
2134
|
+
part: "alt",
|
|
2135
|
+
label: t("image.alt"),
|
|
2136
|
+
placeholder: t("image.alt"),
|
|
2137
|
+
value: alt ?? "",
|
|
2138
|
+
onCommit: (value) => setProps(editor, block2.id, { alt: value })
|
|
2139
|
+
}
|
|
2140
|
+
),
|
|
2141
|
+
/* @__PURE__ */ jsx14(
|
|
2142
|
+
"button",
|
|
2143
|
+
{
|
|
2144
|
+
type: "button",
|
|
2145
|
+
className: "image-tools__button",
|
|
2146
|
+
"data-inkbrick-part": "tool",
|
|
2147
|
+
"aria-label": t("image.replace"),
|
|
2148
|
+
title: t("image.replace"),
|
|
2149
|
+
onClick: () => setProps(editor, block2.id, { src: "" }),
|
|
2150
|
+
children: /* @__PURE__ */ jsx14(Icon, { name: "replace" })
|
|
2151
|
+
}
|
|
2152
|
+
)
|
|
2153
|
+
]
|
|
2154
|
+
}
|
|
2155
|
+
),
|
|
2156
|
+
readOnly ? caption ? /* @__PURE__ */ jsx14("figcaption", { className: "image-caption", "data-inkbrick-part": "caption", children: caption }) : null : /* @__PURE__ */ jsx14(
|
|
2157
|
+
CommitInput,
|
|
2158
|
+
{
|
|
2159
|
+
className: "image-caption image-caption--input",
|
|
2160
|
+
part: "caption",
|
|
2161
|
+
label: t("image.caption.label"),
|
|
2162
|
+
placeholder: t("image.caption.placeholder"),
|
|
2163
|
+
value: caption ?? "",
|
|
2164
|
+
onCommit: (value) => setProps(editor, block2.id, { caption: value })
|
|
2165
|
+
}
|
|
2166
|
+
)
|
|
2167
|
+
]
|
|
2168
|
+
}
|
|
2169
|
+
);
|
|
2170
|
+
}
|
|
2171
|
+
return /* @__PURE__ */ jsx14(
|
|
2172
|
+
"div",
|
|
2173
|
+
{
|
|
2174
|
+
className: "block block-image",
|
|
2175
|
+
"data-inkbrick-block": "image",
|
|
2176
|
+
"data-block-id": block2.id,
|
|
2177
|
+
"data-selected": selected || void 0,
|
|
2178
|
+
contentEditable: false,
|
|
2179
|
+
children: content
|
|
2180
|
+
}
|
|
2181
|
+
);
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// src/blocks/FormulaBlock.tsx
|
|
2185
|
+
import { useCallback as useCallback3, useEffect as useEffect5, useRef as useRef5, useState as useState5 } from "react";
|
|
2186
|
+
import { MAX_FORMULA_LENGTH } from "@inkbrick/document";
|
|
2187
|
+
|
|
2188
|
+
// src/formula/LazyFormulaPalette.tsx
|
|
2189
|
+
import { useEffect as useEffect3, useState as useState3 } from "react";
|
|
2190
|
+
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
2191
|
+
var loaded;
|
|
2192
|
+
var loading;
|
|
2193
|
+
function preloadFormulaPalette() {
|
|
2194
|
+
return loading ??= import("./FormulaPalette-WFRPRHS5.js").then(
|
|
2195
|
+
(module) => loaded = module.FormulaPalette
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2198
|
+
function LazyFormulaPalette(props) {
|
|
2199
|
+
const [Palette, setPalette] = useState3(() => loaded);
|
|
2200
|
+
useEffect3(() => {
|
|
2201
|
+
if (Palette) return;
|
|
2202
|
+
let active = true;
|
|
2203
|
+
void preloadFormulaPalette().then((component) => {
|
|
2204
|
+
if (active) setPalette(() => component);
|
|
2205
|
+
});
|
|
2206
|
+
return () => {
|
|
2207
|
+
active = false;
|
|
2208
|
+
};
|
|
2209
|
+
}, [Palette]);
|
|
2210
|
+
return Palette ? /* @__PURE__ */ jsx15(Palette, { ...props }) : null;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
// src/math/FormulaView.tsx
|
|
2214
|
+
import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
|
|
2215
|
+
import { Fragment, jsx as jsx16, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2216
|
+
function FormulaView({
|
|
2217
|
+
latex,
|
|
2218
|
+
display = false,
|
|
2219
|
+
showError = false,
|
|
2220
|
+
onSettled
|
|
2221
|
+
}) {
|
|
2222
|
+
const { formulaRenderer } = useEditorUI();
|
|
2223
|
+
const ref = useRef4(null);
|
|
2224
|
+
const [error, setError] = useState4(null);
|
|
2225
|
+
const Tag = display ? "div" : "span";
|
|
2226
|
+
useEffect4(() => {
|
|
2227
|
+
const target = ref.current;
|
|
2228
|
+
if (!target) return;
|
|
2229
|
+
ensureMathStyles(globalThis.document);
|
|
2230
|
+
setError(null);
|
|
2231
|
+
return mountFormula(target, latex, {
|
|
2232
|
+
display,
|
|
2233
|
+
renderer: formulaRenderer,
|
|
2234
|
+
onSettled: (state, message) => {
|
|
2235
|
+
setError(state === "error" ? message ?? "" : null);
|
|
2236
|
+
onSettled?.(state, message);
|
|
2237
|
+
}
|
|
2238
|
+
});
|
|
2239
|
+
}, [latex, display, formulaRenderer, onSettled]);
|
|
2240
|
+
return /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
2241
|
+
/* @__PURE__ */ jsx16(
|
|
2242
|
+
Tag,
|
|
2243
|
+
{
|
|
2244
|
+
ref: (node) => {
|
|
2245
|
+
ref.current = node;
|
|
2246
|
+
},
|
|
2247
|
+
"data-inkbrick-part": "formula-render",
|
|
2248
|
+
"data-display": display || void 0
|
|
2249
|
+
}
|
|
2250
|
+
),
|
|
2251
|
+
showError && error !== null && /* @__PURE__ */ jsx16("span", { "data-inkbrick-part": "formula-error", role: "alert", children: error })
|
|
2252
|
+
] });
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
// src/blocks/FormulaBlock.tsx
|
|
2256
|
+
import { jsx as jsx17, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2257
|
+
function FormulaBlock({ block: block2, editor, selected, readOnly }) {
|
|
2258
|
+
const t = useT();
|
|
2259
|
+
const latex = typeof block2.props.latex === "string" ? block2.props.latex : "";
|
|
2260
|
+
const [editing, setEditing] = useState5(latex === "" && !readOnly);
|
|
2261
|
+
const [draft, setDraft] = useState5(latex);
|
|
2262
|
+
const [error, setError] = useState5(null);
|
|
2263
|
+
const field = useRef5(null);
|
|
2264
|
+
const container = useRef5(null);
|
|
2265
|
+
const insertSnippet = useSnippetInsert(field, draft, setDraft);
|
|
2266
|
+
const cancelled = useRef5(false);
|
|
2267
|
+
const born = useRef5(Date.now());
|
|
2268
|
+
useEffect5(() => {
|
|
2269
|
+
if (!editing) setDraft(latex);
|
|
2270
|
+
}, [latex, editing]);
|
|
2271
|
+
useEffect5(() => {
|
|
2272
|
+
if (!editing) return;
|
|
2273
|
+
cancelled.current = false;
|
|
2274
|
+
born.current = Date.now();
|
|
2275
|
+
field.current?.focus();
|
|
2276
|
+
const timer = setTimeout(() => {
|
|
2277
|
+
if (Date.now() - born.current < 300) field.current?.focus();
|
|
2278
|
+
}, 0);
|
|
2279
|
+
return () => clearTimeout(timer);
|
|
2280
|
+
}, [editing]);
|
|
2281
|
+
useEffect5(() => {
|
|
2282
|
+
if (readOnly) setEditing(false);
|
|
2283
|
+
}, [readOnly]);
|
|
2284
|
+
const settle = useCallback3((state, message) => {
|
|
2285
|
+
setError(state === "error" ? message ?? "" : null);
|
|
2286
|
+
}, []);
|
|
2287
|
+
const commit = () => {
|
|
2288
|
+
if (cancelled.current) {
|
|
2289
|
+
cancelled.current = false;
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
if (latex === "" && draft === "" && Date.now() - born.current < 300) {
|
|
2293
|
+
setTimeout(() => field.current?.focus(), 0);
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
if (draft !== latex) {
|
|
2297
|
+
editor.dispatch({
|
|
2298
|
+
type: "setBlockProps",
|
|
2299
|
+
payload: { blockId: block2.id, props: { latex: draft } }
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
setEditing(false);
|
|
2303
|
+
};
|
|
2304
|
+
const cancel = () => {
|
|
2305
|
+
cancelled.current = true;
|
|
2306
|
+
setDraft(latex);
|
|
2307
|
+
setEditing(false);
|
|
2308
|
+
};
|
|
2309
|
+
return /* @__PURE__ */ jsx17(
|
|
2310
|
+
"div",
|
|
2311
|
+
{
|
|
2312
|
+
className: "block block-formula",
|
|
2313
|
+
"data-inkbrick-block": "formula",
|
|
2314
|
+
"data-block-id": block2.id,
|
|
2315
|
+
"data-selected": selected || void 0,
|
|
2316
|
+
contentEditable: false,
|
|
2317
|
+
children: editing ? /* @__PURE__ */ jsxs7(
|
|
2318
|
+
"div",
|
|
2319
|
+
{
|
|
2320
|
+
ref: container,
|
|
2321
|
+
className: "formula-editor",
|
|
2322
|
+
"data-inkbrick-part": "formula-editor",
|
|
2323
|
+
onBlur: (event) => {
|
|
2324
|
+
if (event.relatedTarget instanceof Node && container.current?.contains(event.relatedTarget))
|
|
2325
|
+
return;
|
|
2326
|
+
commit();
|
|
2327
|
+
},
|
|
2328
|
+
children: [
|
|
2329
|
+
/* @__PURE__ */ jsx17(
|
|
2330
|
+
"textarea",
|
|
2331
|
+
{
|
|
2332
|
+
ref: field,
|
|
2333
|
+
className: "formula-source",
|
|
2334
|
+
"data-inkbrick-part": "formula-source",
|
|
2335
|
+
"aria-label": t("formula.source"),
|
|
2336
|
+
"aria-invalid": error !== null || void 0,
|
|
2337
|
+
placeholder: t("formula.placeholder"),
|
|
2338
|
+
spellCheck: false,
|
|
2339
|
+
rows: Math.min(8, Math.max(2, draft.split("\n").length)),
|
|
2340
|
+
maxLength: MAX_FORMULA_LENGTH,
|
|
2341
|
+
value: draft,
|
|
2342
|
+
onChange: (event) => setDraft(event.target.value),
|
|
2343
|
+
onKeyDown: (event) => {
|
|
2344
|
+
if (event.key === "Escape") {
|
|
2345
|
+
event.preventDefault();
|
|
2346
|
+
cancel();
|
|
2347
|
+
event.target.blur();
|
|
2348
|
+
} else if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
|
2349
|
+
event.preventDefault();
|
|
2350
|
+
event.target.blur();
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
),
|
|
2355
|
+
/* @__PURE__ */ jsx17("div", { className: "formula-preview", "data-inkbrick-part": "formula-preview", children: /* @__PURE__ */ jsx17(FormulaView, { latex: draft, display: true, showError: true, onSettled: settle }) }),
|
|
2356
|
+
/* @__PURE__ */ jsx17(LazyFormulaPalette, { onInsert: insertSnippet }),
|
|
2357
|
+
/* @__PURE__ */ jsx17("div", { className: "formula-hint", "data-inkbrick-part": "formula-hint", children: t("formula.hint") })
|
|
2358
|
+
]
|
|
2359
|
+
}
|
|
2360
|
+
) : latex === "" ? /* @__PURE__ */ jsx17(
|
|
2361
|
+
"button",
|
|
2362
|
+
{
|
|
2363
|
+
type: "button",
|
|
2364
|
+
className: "formula-empty",
|
|
2365
|
+
"data-inkbrick-part": "formula-empty",
|
|
2366
|
+
disabled: readOnly,
|
|
2367
|
+
onClick: () => setEditing(true),
|
|
2368
|
+
children: t("formula.empty")
|
|
2369
|
+
}
|
|
2370
|
+
) : /* @__PURE__ */ jsx17(
|
|
2371
|
+
"div",
|
|
2372
|
+
{
|
|
2373
|
+
className: "formula-display",
|
|
2374
|
+
"data-inkbrick-part": "formula-display",
|
|
2375
|
+
role: readOnly ? void 0 : "button",
|
|
2376
|
+
tabIndex: readOnly ? void 0 : 0,
|
|
2377
|
+
"aria-label": readOnly ? void 0 : t("formula.edit"),
|
|
2378
|
+
title: readOnly ? void 0 : t("formula.edit"),
|
|
2379
|
+
onClick: () => !readOnly && setEditing(true),
|
|
2380
|
+
onKeyDown: (event) => {
|
|
2381
|
+
if (!readOnly && (event.key === "Enter" || event.key === " ")) {
|
|
2382
|
+
event.preventDefault();
|
|
2383
|
+
setEditing(true);
|
|
2384
|
+
}
|
|
2385
|
+
},
|
|
2386
|
+
children: /* @__PURE__ */ jsx17(FormulaView, { latex, display: true, showError: true })
|
|
2387
|
+
}
|
|
2388
|
+
)
|
|
2389
|
+
}
|
|
2390
|
+
);
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
// src/registry/defaultBlockComponents.ts
|
|
2394
|
+
var defaultBlockComponents = Object.freeze({
|
|
2395
|
+
"block:page": PageBlock,
|
|
2396
|
+
"block:note": NoteBlock,
|
|
2397
|
+
"block:paragraph": ParagraphBlock,
|
|
2398
|
+
"block:heading": HeadingBlock,
|
|
2399
|
+
"block:list": ListBlock,
|
|
2400
|
+
"block:quote": QuoteBlock,
|
|
2401
|
+
"block:code": CodeBlock,
|
|
2402
|
+
"block:divider": DividerBlock,
|
|
2403
|
+
"block:callout": CalloutBlock,
|
|
2404
|
+
"block:image": ImageBlock,
|
|
2405
|
+
"block:formula": FormulaBlock
|
|
2406
|
+
});
|
|
2407
|
+
|
|
2408
|
+
// src/registry/resolveBlockComponent.ts
|
|
2409
|
+
function lookup(registry, flavour) {
|
|
2410
|
+
return Object.hasOwn(registry, flavour) ? registry[flavour] : void 0;
|
|
2411
|
+
}
|
|
2412
|
+
function resolveBlockComponent(flavour, custom) {
|
|
2413
|
+
return lookup(custom, flavour) ?? lookup(defaultBlockComponents, flavour);
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
// src/renderer/BlockRenderer.tsx
|
|
2417
|
+
import { jsx as jsx18 } from "react/jsx-runtime";
|
|
2418
|
+
var warned = /* @__PURE__ */ new Set();
|
|
2419
|
+
var BlockRenderer = memo(function BlockRenderer2({ id }) {
|
|
2420
|
+
const editor = useEditor();
|
|
2421
|
+
const view = useEditorView();
|
|
2422
|
+
const readOnly = useReadOnly();
|
|
2423
|
+
const { components, slots } = useEditorUI();
|
|
2424
|
+
const selected = useSyncExternalStore7(
|
|
2425
|
+
view.selection.subscribe,
|
|
2426
|
+
() => view.selection.isSelected(id),
|
|
2427
|
+
() => false
|
|
2428
|
+
);
|
|
2429
|
+
const block2 = useBlock(id);
|
|
2430
|
+
if (!block2) {
|
|
2431
|
+
return null;
|
|
2432
|
+
}
|
|
2433
|
+
const Component = resolveBlockComponent(block2.flavour, components);
|
|
2434
|
+
if (Component) {
|
|
2435
|
+
return /* @__PURE__ */ jsx18(
|
|
2436
|
+
Component,
|
|
2437
|
+
{
|
|
2438
|
+
block: block2,
|
|
2439
|
+
editor,
|
|
2440
|
+
selected,
|
|
2441
|
+
readOnly,
|
|
2442
|
+
controller: editor.controllers.get(block2.flavour)
|
|
2443
|
+
}
|
|
2444
|
+
);
|
|
2445
|
+
}
|
|
2446
|
+
if (!warned.has(block2.flavour)) {
|
|
2447
|
+
warned.add(block2.flavour);
|
|
2448
|
+
console.warn(`[@inkbrick/react] No component registered for block "${block2.flavour}"`);
|
|
2449
|
+
}
|
|
2450
|
+
const Fallback = slots.UnknownBlock ?? UnknownBlockView;
|
|
2451
|
+
return /* @__PURE__ */ jsx18(Fallback, { block: block2, editor, selected, readOnly, children: /* @__PURE__ */ jsx18(BlockChildren, { id: block2.id }) });
|
|
2452
|
+
});
|
|
2453
|
+
|
|
2454
|
+
// src/renderer/DocumentRenderer.tsx
|
|
2455
|
+
import { Fragment as Fragment2, jsx as jsx19 } from "react/jsx-runtime";
|
|
2456
|
+
function FullChildList({ ids }) {
|
|
2457
|
+
return /* @__PURE__ */ jsx19(Fragment2, { children: ids.map((id) => /* @__PURE__ */ jsx19(BlockRenderer, { id }, id)) });
|
|
2458
|
+
}
|
|
2459
|
+
var fullRenderer = { ChildList: FullChildList };
|
|
2460
|
+
var DocumentRendererContext = createContext2(fullRenderer);
|
|
2461
|
+
|
|
2462
|
+
// src/blocks/BlockChildren.tsx
|
|
2463
|
+
import { jsx as jsx20 } from "react/jsx-runtime";
|
|
2464
|
+
function BlockChildren({ id }) {
|
|
2465
|
+
const children = useChildren(id);
|
|
2466
|
+
const { ChildList } = useContext3(DocumentRendererContext);
|
|
2467
|
+
return /* @__PURE__ */ jsx20(ChildList, { parentId: id, ids: children });
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
// src/renderer/DocumentView.tsx
|
|
2471
|
+
import { useEffect as useEffect16, useLayoutEffect as useLayoutEffect8, useRef as useRef12 } from "react";
|
|
2472
|
+
|
|
2473
|
+
// src/block-handle/BlockHandleHost.tsx
|
|
2474
|
+
import { useEffect as useEffect7, useRef as useRef7, useSyncExternalStore as useSyncExternalStore9 } from "react";
|
|
2475
|
+
import { createPortal } from "react-dom";
|
|
2476
|
+
|
|
2477
|
+
// src/block-handle/actions.tsx
|
|
2478
|
+
import { flushSync as flushSync3 } from "react-dom";
|
|
2479
|
+
|
|
2480
|
+
// src/slash/syncSlashMenu.ts
|
|
2481
|
+
function caretPosition(view, blockId) {
|
|
2482
|
+
const selection = globalThis.getSelection();
|
|
2483
|
+
const range = selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
|
|
2484
|
+
const rect = range?.getClientRects()[0];
|
|
2485
|
+
if (rect && (rect.width > 0 || rect.height > 0)) return { x: rect.left, y: rect.bottom };
|
|
2486
|
+
const element = view.getElement(blockId);
|
|
2487
|
+
if (!element) return null;
|
|
2488
|
+
const box = element.getBoundingClientRect();
|
|
2489
|
+
return { x: box.left, y: box.bottom };
|
|
2490
|
+
}
|
|
2491
|
+
function syncSlashMenu(editor, view, items) {
|
|
2492
|
+
const state = view.slash.getState();
|
|
2493
|
+
if (!state.open || !state.blockId) return;
|
|
2494
|
+
const root = view.getRoot();
|
|
2495
|
+
const selection = root ? readDocSelection(root) : null;
|
|
2496
|
+
const block2 = editor.store.getBlock(state.blockId);
|
|
2497
|
+
const text = block2 ? editor.engine.inline.toPlainText(block2.content) : "";
|
|
2498
|
+
if (!selection || !selection.collapsed || selection.from.blockId !== state.blockId || selection.from.offset <= state.slashOffset || text[state.slashOffset] !== "/") {
|
|
2499
|
+
view.slash.close();
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
const query = text.slice(state.slashOffset + 1, selection.from.offset);
|
|
2503
|
+
const visible = visibleSlashItems(editor, items, state.blockId, query);
|
|
2504
|
+
if (visible.length === 0 && /\s$/.test(query)) {
|
|
2505
|
+
view.slash.close();
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
view.slash.update({
|
|
2509
|
+
query,
|
|
2510
|
+
activeIndex: query !== state.query ? 0 : Math.min(state.activeIndex, Math.max(visible.length - 1, 0)),
|
|
2511
|
+
position: caretPosition(view, state.blockId)
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
// src/block-handle/actions.tsx
|
|
2516
|
+
import { jsx as jsx21 } from "react/jsx-runtime";
|
|
2517
|
+
var isCaret = (value) => typeof value === "object" && value !== null && "blockId" in value && "offset" in value;
|
|
2518
|
+
function performBlockAction(view, action) {
|
|
2519
|
+
let result;
|
|
2520
|
+
flushSync3(() => {
|
|
2521
|
+
result = action();
|
|
2522
|
+
});
|
|
2523
|
+
view.blocks.closeMenu();
|
|
2524
|
+
if (isCaret(result) && view.getElement(result.blockId)) {
|
|
2525
|
+
view.placeCaret(result.blockId, result.offset);
|
|
2526
|
+
} else {
|
|
2527
|
+
view.getRoot()?.focus({ preventScroll: true });
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
function buildBlockActions(editor, view, blockId, slashItems, t) {
|
|
2531
|
+
const previous = editor.document.getPreviousSibling(blockId);
|
|
2532
|
+
const next = editor.document.getNextSibling(blockId);
|
|
2533
|
+
const perform = (fn) => () => performBlockAction(view, fn);
|
|
2534
|
+
const moveUp = previous && { blockId, targetId: previous.id, position: "before" };
|
|
2535
|
+
const moveDown = next && { blockId, targetId: next.id, position: "after" };
|
|
2536
|
+
const block2 = editor.store.getBlock(blockId);
|
|
2537
|
+
const controller = block2 && editor.controllers.get(block2.flavour);
|
|
2538
|
+
const own = (controller?.menuItems ?? []).map((item) => {
|
|
2539
|
+
const context = editor.controllers.contextOf(block2);
|
|
2540
|
+
return {
|
|
2541
|
+
id: item.id,
|
|
2542
|
+
title: item.title,
|
|
2543
|
+
icon: iconByName(item.icon),
|
|
2544
|
+
danger: item.danger,
|
|
2545
|
+
disabled: item.isDisabled ? item.isDisabled(context) : context.readOnly,
|
|
2546
|
+
run: perform(() => item.run(context))
|
|
2547
|
+
};
|
|
2548
|
+
});
|
|
2549
|
+
const actions = [
|
|
2550
|
+
...own,
|
|
2551
|
+
{
|
|
2552
|
+
id: "duplicate",
|
|
2553
|
+
title: t("menu.duplicate"),
|
|
2554
|
+
icon: /* @__PURE__ */ jsx21(Icon, { name: "copy" }),
|
|
2555
|
+
disabled: !editor.can("duplicateBlock", { blockId }),
|
|
2556
|
+
run: perform(() => editor.dispatch({ type: "duplicateBlock", payload: { blockId } }))
|
|
2557
|
+
},
|
|
2558
|
+
{
|
|
2559
|
+
id: "move-up",
|
|
2560
|
+
title: t("menu.moveUp"),
|
|
2561
|
+
icon: /* @__PURE__ */ jsx21(Icon, { name: "arrow-up" }),
|
|
2562
|
+
disabled: !moveUp || !editor.can("moveBlock", moveUp),
|
|
2563
|
+
run: perform(() => moveUp && editor.dispatch({ type: "moveBlock", payload: moveUp }))
|
|
2564
|
+
},
|
|
2565
|
+
{
|
|
2566
|
+
id: "move-down",
|
|
2567
|
+
title: t("menu.moveDown"),
|
|
2568
|
+
icon: /* @__PURE__ */ jsx21(Icon, { name: "arrow-down" }),
|
|
2569
|
+
disabled: !moveDown || !editor.can("moveBlock", moveDown),
|
|
2570
|
+
run: perform(() => moveDown && editor.dispatch({ type: "moveBlock", payload: moveDown }))
|
|
2571
|
+
},
|
|
2572
|
+
{
|
|
2573
|
+
id: "delete",
|
|
2574
|
+
title: t("menu.delete"),
|
|
2575
|
+
icon: /* @__PURE__ */ jsx21(Icon, { name: "trash" }),
|
|
2576
|
+
danger: true,
|
|
2577
|
+
run: perform(() => editor.execute("delete-block", { id: blockId }))
|
|
2578
|
+
}
|
|
2579
|
+
];
|
|
2580
|
+
const turnInto = availableSlashItems(editor, slashItems, blockId).filter(
|
|
2581
|
+
(item) => editor.can("convertBlock", { blockId, flavour: item.flavour, props: item.props })
|
|
2582
|
+
).map((item) => ({
|
|
2583
|
+
id: item.id,
|
|
2584
|
+
title: item.title,
|
|
2585
|
+
icon: item.icon,
|
|
2586
|
+
run: perform(
|
|
2587
|
+
() => editor.dispatch({
|
|
2588
|
+
type: "convertBlock",
|
|
2589
|
+
payload: { blockId, flavour: item.flavour, props: item.props }
|
|
2590
|
+
})
|
|
2591
|
+
)
|
|
2592
|
+
}));
|
|
2593
|
+
return { actions, turnInto };
|
|
2594
|
+
}
|
|
2595
|
+
function addBlockBelow(editor, view, blockId, slashItems) {
|
|
2596
|
+
const block2 = editor.store.getBlock(blockId);
|
|
2597
|
+
const parent = block2?.parentId ? editor.store.getBlock(block2.parentId) : void 0;
|
|
2598
|
+
if (!block2 || !parent) return;
|
|
2599
|
+
const nested = parent.flavour !== "block:note";
|
|
2600
|
+
const payload = {
|
|
2601
|
+
flavour: nested ? block2.flavour : "block:paragraph",
|
|
2602
|
+
afterId: blockId,
|
|
2603
|
+
props: nested ? block2.props : void 0,
|
|
2604
|
+
content: [{ insert: "/" }]
|
|
2605
|
+
};
|
|
2606
|
+
let caret;
|
|
2607
|
+
flushSync3(() => {
|
|
2608
|
+
caret = editor.dispatch({ type: "insertBlock", payload });
|
|
2609
|
+
});
|
|
2610
|
+
view.blocks.setHover(null);
|
|
2611
|
+
if (!caret) return;
|
|
2612
|
+
view.placeCaret(caret.blockId, 1);
|
|
2613
|
+
view.slash.open(caret.blockId, 0);
|
|
2614
|
+
syncSlashMenu(editor, view, slashItems);
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
// src/block-handle/DefaultBlockHandle.tsx
|
|
2618
|
+
import { jsx as jsx22, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2619
|
+
var HANDLE_WIDTH = 52;
|
|
2620
|
+
function DefaultBlockHandle({
|
|
2621
|
+
blockId,
|
|
2622
|
+
position,
|
|
2623
|
+
canAdd,
|
|
2624
|
+
dragging,
|
|
2625
|
+
menuOpen,
|
|
2626
|
+
onAdd,
|
|
2627
|
+
onOpenMenu,
|
|
2628
|
+
gripProps
|
|
2629
|
+
}) {
|
|
2630
|
+
const t = useT();
|
|
2631
|
+
const left = position.x - HANDLE_WIDTH;
|
|
2632
|
+
return /* @__PURE__ */ jsxs8(
|
|
2633
|
+
"div",
|
|
2634
|
+
{
|
|
2635
|
+
className: "inkbrick-block-handle",
|
|
2636
|
+
"data-inkbrick-ui": "block-handle",
|
|
2637
|
+
"data-testid": "block-handle",
|
|
2638
|
+
"data-block": blockId,
|
|
2639
|
+
"data-inkbrick-block-ui": true,
|
|
2640
|
+
"data-compact": left < 2 || void 0,
|
|
2641
|
+
style: {
|
|
2642
|
+
left: Math.max(2, left),
|
|
2643
|
+
top: position.y,
|
|
2644
|
+
pointerEvents: dragging ? "none" : void 0
|
|
2645
|
+
},
|
|
2646
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
2647
|
+
children: [
|
|
2648
|
+
/* @__PURE__ */ jsx22(
|
|
2649
|
+
"button",
|
|
2650
|
+
{
|
|
2651
|
+
type: "button",
|
|
2652
|
+
className: "inkbrick-block-handle__button",
|
|
2653
|
+
"data-inkbrick-part": "button",
|
|
2654
|
+
"aria-label": t("handle.add"),
|
|
2655
|
+
title: t("handle.add"),
|
|
2656
|
+
"data-action": "add",
|
|
2657
|
+
disabled: !canAdd,
|
|
2658
|
+
onClick: onAdd,
|
|
2659
|
+
children: /* @__PURE__ */ jsx22(Icon, { name: "plus" })
|
|
2660
|
+
}
|
|
2661
|
+
),
|
|
2662
|
+
/* @__PURE__ */ jsx22(
|
|
2663
|
+
"button",
|
|
2664
|
+
{
|
|
2665
|
+
type: "button",
|
|
2666
|
+
className: "inkbrick-block-handle__button inkbrick-block-handle__grip",
|
|
2667
|
+
"data-inkbrick-part": "grip",
|
|
2668
|
+
"data-active": menuOpen || void 0,
|
|
2669
|
+
"aria-label": t("handle.grip"),
|
|
2670
|
+
title: t("handle.grip"),
|
|
2671
|
+
"aria-haspopup": "menu",
|
|
2672
|
+
"aria-expanded": menuOpen,
|
|
2673
|
+
"data-action": "grip",
|
|
2674
|
+
...gripProps,
|
|
2675
|
+
onKeyDown: (event) => {
|
|
2676
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
2677
|
+
event.preventDefault();
|
|
2678
|
+
onOpenMenu();
|
|
2679
|
+
}
|
|
2680
|
+
},
|
|
2681
|
+
children: /* @__PURE__ */ jsx22(Icon, { name: "grip" })
|
|
2682
|
+
}
|
|
2683
|
+
)
|
|
2684
|
+
]
|
|
2685
|
+
}
|
|
2686
|
+
);
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
// src/block-handle/DefaultBlockMenu.tsx
|
|
2690
|
+
import { useEffect as useEffect6, useLayoutEffect as useLayoutEffect3, useRef as useRef6, useState as useState6 } from "react";
|
|
2691
|
+
import { Fragment as Fragment3, jsx as jsx23, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2692
|
+
function DefaultBlockMenu({ position, actions, turnInto, onClose }) {
|
|
2693
|
+
const t = useT();
|
|
2694
|
+
const ref = useRef6(null);
|
|
2695
|
+
const [flip, setFlip] = useState6(false);
|
|
2696
|
+
const entries = [...actions, ...turnInto].filter((entry) => !entry.disabled);
|
|
2697
|
+
useEffect6(() => {
|
|
2698
|
+
ref.current?.querySelector('[role="menuitem"]:not([aria-disabled="true"])')?.focus();
|
|
2699
|
+
}, []);
|
|
2700
|
+
useLayoutEffect3(() => {
|
|
2701
|
+
const menu = ref.current;
|
|
2702
|
+
if (menu)
|
|
2703
|
+
setFlip(
|
|
2704
|
+
position.y + menu.offsetHeight > window.innerHeight && position.y - menu.offsetHeight > 0
|
|
2705
|
+
);
|
|
2706
|
+
}, [position.y]);
|
|
2707
|
+
const move = (delta) => {
|
|
2708
|
+
const items = [
|
|
2709
|
+
...ref.current?.querySelectorAll(
|
|
2710
|
+
'[role="menuitem"]:not([aria-disabled="true"])'
|
|
2711
|
+
) ?? []
|
|
2712
|
+
];
|
|
2713
|
+
const index = items.indexOf(document.activeElement);
|
|
2714
|
+
items[(index + delta + items.length) % items.length]?.focus();
|
|
2715
|
+
};
|
|
2716
|
+
const renderItem = (action) => /* @__PURE__ */ jsxs9(
|
|
2717
|
+
"div",
|
|
2718
|
+
{
|
|
2719
|
+
role: "menuitem",
|
|
2720
|
+
tabIndex: action.disabled ? -1 : 0,
|
|
2721
|
+
"aria-disabled": action.disabled || void 0,
|
|
2722
|
+
"data-action": action.id,
|
|
2723
|
+
"data-inkbrick-part": "item",
|
|
2724
|
+
"data-disabled": action.disabled || void 0,
|
|
2725
|
+
"data-danger": action.danger || void 0,
|
|
2726
|
+
className: `inkbrick-block-menu__item${action.danger ? " inkbrick-block-menu__item--danger" : ""}`,
|
|
2727
|
+
onClick: () => !action.disabled && action.run(),
|
|
2728
|
+
onKeyDown: (event) => {
|
|
2729
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
2730
|
+
event.preventDefault();
|
|
2731
|
+
if (!action.disabled) action.run();
|
|
2732
|
+
}
|
|
2733
|
+
},
|
|
2734
|
+
children: [
|
|
2735
|
+
action.icon !== void 0 && /* @__PURE__ */ jsx23("span", { className: "inkbrick-block-menu__icon", "data-inkbrick-part": "icon", children: action.icon }),
|
|
2736
|
+
action.title
|
|
2737
|
+
]
|
|
2738
|
+
},
|
|
2739
|
+
action.id
|
|
2740
|
+
);
|
|
2741
|
+
return /* @__PURE__ */ jsxs9(
|
|
2742
|
+
"div",
|
|
2743
|
+
{
|
|
2744
|
+
ref,
|
|
2745
|
+
className: "inkbrick-block-menu",
|
|
2746
|
+
"data-inkbrick-ui": "block-menu",
|
|
2747
|
+
role: "menu",
|
|
2748
|
+
"aria-label": t("menu.label"),
|
|
2749
|
+
"data-testid": "block-menu",
|
|
2750
|
+
"data-inkbrick-block-ui": true,
|
|
2751
|
+
style: {
|
|
2752
|
+
left: Math.max(
|
|
2753
|
+
8,
|
|
2754
|
+
Math.min(position.x, window.innerWidth - (ref.current?.offsetWidth ?? 232) - 8)
|
|
2755
|
+
),
|
|
2756
|
+
top: flip ? Math.max(8, position.y - (ref.current?.offsetHeight ?? 0) - 32) : position.y
|
|
2757
|
+
},
|
|
2758
|
+
onKeyDown: (event) => {
|
|
2759
|
+
if (event.key === "ArrowDown") {
|
|
2760
|
+
event.preventDefault();
|
|
2761
|
+
move(1);
|
|
2762
|
+
} else if (event.key === "ArrowUp") {
|
|
2763
|
+
event.preventDefault();
|
|
2764
|
+
move(-1);
|
|
2765
|
+
} else if (event.key === "Escape") {
|
|
2766
|
+
event.preventDefault();
|
|
2767
|
+
onClose();
|
|
2768
|
+
}
|
|
2769
|
+
},
|
|
2770
|
+
children: [
|
|
2771
|
+
actions.map(renderItem),
|
|
2772
|
+
turnInto.length > 0 && /* @__PURE__ */ jsxs9(Fragment3, { children: [
|
|
2773
|
+
/* @__PURE__ */ jsx23("div", { className: "inkbrick-block-menu__label", "data-inkbrick-part": "label", role: "separator", children: t("menu.turnInto") }),
|
|
2774
|
+
turnInto.map(renderItem)
|
|
2775
|
+
] }),
|
|
2776
|
+
entries.length === 0 && /* @__PURE__ */ jsx23("div", { className: "inkbrick-block-menu__label", "data-inkbrick-part": "label", children: t("menu.empty") })
|
|
2777
|
+
]
|
|
2778
|
+
}
|
|
2779
|
+
);
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
// src/block-handle/gripGesture.ts
|
|
2783
|
+
var DRAG_THRESHOLD = 5;
|
|
2784
|
+
var LONG_PRESS_MS = 350;
|
|
2785
|
+
var TOUCH_SLOP = 10;
|
|
2786
|
+
function startGripGesture(drag, blockId, event, { onTap }) {
|
|
2787
|
+
const touch = event.pointerType === "touch" || event.pointerType === "pen";
|
|
2788
|
+
const pointerId = event.pointerId;
|
|
2789
|
+
const startX = event.clientX;
|
|
2790
|
+
const startY = event.clientY;
|
|
2791
|
+
let dragging = false;
|
|
2792
|
+
let aborted = false;
|
|
2793
|
+
let timer;
|
|
2794
|
+
const begin = () => {
|
|
2795
|
+
if (dragging || aborted) return;
|
|
2796
|
+
dragging = true;
|
|
2797
|
+
drag.start(blockId);
|
|
2798
|
+
if (touch) navigator.vibrate?.(10);
|
|
2799
|
+
};
|
|
2800
|
+
const finish = () => {
|
|
2801
|
+
clearTimeout(timer);
|
|
2802
|
+
document.removeEventListener("pointermove", onMove);
|
|
2803
|
+
document.removeEventListener("pointerup", onUp);
|
|
2804
|
+
document.removeEventListener("pointercancel", onCancel);
|
|
2805
|
+
document.removeEventListener("keydown", onKey);
|
|
2806
|
+
};
|
|
2807
|
+
const onMove = (move) => {
|
|
2808
|
+
if (move.pointerId !== pointerId) return;
|
|
2809
|
+
const distance = Math.hypot(move.clientX - startX, move.clientY - startY);
|
|
2810
|
+
if (!dragging) {
|
|
2811
|
+
if (touch) {
|
|
2812
|
+
if (distance > TOUCH_SLOP) {
|
|
2813
|
+
aborted = true;
|
|
2814
|
+
finish();
|
|
2815
|
+
}
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2818
|
+
if (distance > DRAG_THRESHOLD) begin();
|
|
2819
|
+
}
|
|
2820
|
+
if (dragging) {
|
|
2821
|
+
move.preventDefault();
|
|
2822
|
+
drag.update(move.clientX, move.clientY);
|
|
2823
|
+
}
|
|
2824
|
+
};
|
|
2825
|
+
const onUp = (up) => {
|
|
2826
|
+
if (up.pointerId !== pointerId) return;
|
|
2827
|
+
finish();
|
|
2828
|
+
if (dragging) drag.drop();
|
|
2829
|
+
else if (!aborted) onTap();
|
|
2830
|
+
};
|
|
2831
|
+
const onCancel = (cancel) => {
|
|
2832
|
+
if (cancel.pointerId !== pointerId) return;
|
|
2833
|
+
abort();
|
|
2834
|
+
};
|
|
2835
|
+
const onKey = (key) => {
|
|
2836
|
+
if (key.key === "Escape") abort();
|
|
2837
|
+
};
|
|
2838
|
+
const abort = () => {
|
|
2839
|
+
aborted = true;
|
|
2840
|
+
finish();
|
|
2841
|
+
if (dragging) drag.cancel();
|
|
2842
|
+
};
|
|
2843
|
+
if (touch) timer = setTimeout(begin, LONG_PRESS_MS);
|
|
2844
|
+
document.addEventListener("pointermove", onMove, { passive: false });
|
|
2845
|
+
document.addEventListener("pointerup", onUp);
|
|
2846
|
+
document.addEventListener("pointercancel", onCancel);
|
|
2847
|
+
document.addEventListener("keydown", onKey);
|
|
2848
|
+
return abort;
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
// src/block-handle/useBlockDrag.ts
|
|
2852
|
+
import { useSyncExternalStore as useSyncExternalStore8 } from "react";
|
|
2853
|
+
|
|
2854
|
+
// src/block-handle/blockDrag.ts
|
|
2855
|
+
import { flushSync as flushSync4 } from "react-dom";
|
|
2856
|
+
|
|
2857
|
+
// src/block-handle/geometry.ts
|
|
2858
|
+
var GUTTER = 72;
|
|
2859
|
+
function hasHandle(editor, blockId) {
|
|
2860
|
+
const block2 = editor.store.getBlock(blockId);
|
|
2861
|
+
const parent = block2?.parentId ? editor.store.getBlock(block2.parentId) : void 0;
|
|
2862
|
+
return !!parent?.parentId;
|
|
2863
|
+
}
|
|
2864
|
+
function blockElement(view, blockId) {
|
|
2865
|
+
return view.getRoot()?.querySelector(`[data-block-id="${CSS.escape(blockId)}"]`) ?? null;
|
|
2866
|
+
}
|
|
2867
|
+
function blockAtPoint(editor, view, x, y) {
|
|
2868
|
+
const root = view.getRoot();
|
|
2869
|
+
const hit = root ? document.elementFromPoint(x, y) : null;
|
|
2870
|
+
if (!root || !hit || !root.contains(hit)) return null;
|
|
2871
|
+
let node = hit.closest("[data-block-id]");
|
|
2872
|
+
while (node && root.contains(node)) {
|
|
2873
|
+
const id = node.getAttribute("data-block-id");
|
|
2874
|
+
if (hasHandle(editor, id)) return id;
|
|
2875
|
+
node = node.parentElement?.closest("[data-block-id]") ?? null;
|
|
2876
|
+
}
|
|
2877
|
+
return null;
|
|
2878
|
+
}
|
|
2879
|
+
function probePoint(view, x, y) {
|
|
2880
|
+
const root = view.getRoot();
|
|
2881
|
+
if (!root) return null;
|
|
2882
|
+
const rect = root.getBoundingClientRect();
|
|
2883
|
+
if (x < rect.left - GUTTER || x > rect.right || y < rect.top || y > rect.bottom) return null;
|
|
2884
|
+
return { x: Math.min(Math.max(x, rect.left + 12), rect.right - 12), y };
|
|
2885
|
+
}
|
|
2886
|
+
function rowRect(view, blockId) {
|
|
2887
|
+
return (view.getElement(blockId) ?? blockElement(view, blockId))?.getBoundingClientRect() ?? null;
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
// src/block-handle/blockDrag.ts
|
|
2891
|
+
var EDGE = 56;
|
|
2892
|
+
var cache2 = /* @__PURE__ */ new WeakMap();
|
|
2893
|
+
function getBlockDrag(editor, view) {
|
|
2894
|
+
let api = cache2.get(view);
|
|
2895
|
+
if (!api) {
|
|
2896
|
+
api = createBlockDrag(editor, view);
|
|
2897
|
+
cache2.set(view, api);
|
|
2898
|
+
}
|
|
2899
|
+
return api;
|
|
2900
|
+
}
|
|
2901
|
+
function createBlockDrag(editor, view) {
|
|
2902
|
+
const controller = view.blocks;
|
|
2903
|
+
const markBody = (on) => {
|
|
2904
|
+
if (typeof document === "undefined") return;
|
|
2905
|
+
document.body.classList.toggle("inkbrick-dragging", on);
|
|
2906
|
+
if (on) document.body.setAttribute("data-inkbrick-dragging", "");
|
|
2907
|
+
else document.body.removeAttribute("data-inkbrick-dragging");
|
|
2908
|
+
};
|
|
2909
|
+
const canMove = (blockId, targetId, position) => blockId !== targetId && editor.can("moveBlock", { blockId, targetId, position });
|
|
2910
|
+
const blockRects = (blockId) => {
|
|
2911
|
+
const wrapper = blockElement(view, blockId);
|
|
2912
|
+
const row = rowRect(view, blockId);
|
|
2913
|
+
return wrapper && row ? { row, box: wrapper.getBoundingClientRect() } : null;
|
|
2914
|
+
};
|
|
2915
|
+
const api = {
|
|
2916
|
+
blockAtPoint(x, y) {
|
|
2917
|
+
const probe = probePoint(view, x, y);
|
|
2918
|
+
return probe ? blockAtPoint(editor, view, probe.x, probe.y) : null;
|
|
2919
|
+
},
|
|
2920
|
+
blockRects,
|
|
2921
|
+
canMove,
|
|
2922
|
+
dropTargetAt(blockId, x, y) {
|
|
2923
|
+
const targetId = api.blockAtPoint(x, y);
|
|
2924
|
+
const rects = targetId ? blockRects(targetId) : null;
|
|
2925
|
+
if (!targetId || !rects) return null;
|
|
2926
|
+
const { row, box } = rects;
|
|
2927
|
+
const position = y < row.top + row.height / 2 ? "before" : "after";
|
|
2928
|
+
if (!canMove(blockId, targetId, position)) return null;
|
|
2929
|
+
return {
|
|
2930
|
+
targetId,
|
|
2931
|
+
position,
|
|
2932
|
+
// «После» — после всего поддерева цели (вместе с вложенными блоками).
|
|
2933
|
+
indicator: {
|
|
2934
|
+
left: box.left,
|
|
2935
|
+
width: box.width,
|
|
2936
|
+
top: position === "before" ? row.top - 1 : box.bottom + 1
|
|
2937
|
+
}
|
|
2938
|
+
};
|
|
2939
|
+
},
|
|
2940
|
+
start(blockId) {
|
|
2941
|
+
controller.startDrag(blockId);
|
|
2942
|
+
markBody(true);
|
|
2943
|
+
},
|
|
2944
|
+
setTarget(target) {
|
|
2945
|
+
const drag = controller.getState().drag;
|
|
2946
|
+
if (!drag) return;
|
|
2947
|
+
if (!target || !canMove(drag.blockId, target.targetId, target.position)) {
|
|
2948
|
+
controller.updateDrag({ targetId: null, position: null, indicator: null });
|
|
2949
|
+
return;
|
|
2950
|
+
}
|
|
2951
|
+
controller.updateDrag({
|
|
2952
|
+
targetId: target.targetId,
|
|
2953
|
+
position: target.position,
|
|
2954
|
+
indicator: target.indicator
|
|
2955
|
+
});
|
|
2956
|
+
},
|
|
2957
|
+
update(x, y) {
|
|
2958
|
+
const drag = controller.getState().drag;
|
|
2959
|
+
if (!drag) return null;
|
|
2960
|
+
api.autoScroll(y);
|
|
2961
|
+
const target = api.dropTargetAt(drag.blockId, x, y);
|
|
2962
|
+
api.setTarget(target);
|
|
2963
|
+
return target;
|
|
2964
|
+
},
|
|
2965
|
+
drop() {
|
|
2966
|
+
const drag = controller.getState().drag;
|
|
2967
|
+
controller.endDrag();
|
|
2968
|
+
markBody(false);
|
|
2969
|
+
if (!drag?.targetId || !drag.position) return false;
|
|
2970
|
+
return api.move(drag.blockId, drag.targetId, drag.position);
|
|
2971
|
+
},
|
|
2972
|
+
cancel() {
|
|
2973
|
+
controller.endDrag();
|
|
2974
|
+
markBody(false);
|
|
2975
|
+
},
|
|
2976
|
+
move(blockId, targetId, position) {
|
|
2977
|
+
if (!canMove(blockId, targetId, position)) return false;
|
|
2978
|
+
const payload = { blockId, targetId, position };
|
|
2979
|
+
flushSync4(() => {
|
|
2980
|
+
editor.dispatch({ type: "moveBlock", payload });
|
|
2981
|
+
});
|
|
2982
|
+
controller.setHover(null);
|
|
2983
|
+
return true;
|
|
2984
|
+
},
|
|
2985
|
+
autoScroll(clientY) {
|
|
2986
|
+
if (typeof window === "undefined") return;
|
|
2987
|
+
if (clientY < EDGE) window.scrollBy(0, -14);
|
|
2988
|
+
else if (clientY > window.innerHeight - EDGE) window.scrollBy(0, 14);
|
|
2989
|
+
},
|
|
2990
|
+
getState: () => controller.getState().drag,
|
|
2991
|
+
subscribe: (listener) => {
|
|
2992
|
+
let last = controller.getState().drag;
|
|
2993
|
+
return controller.subscribe(() => {
|
|
2994
|
+
const next = controller.getState().drag;
|
|
2995
|
+
if (next !== last) {
|
|
2996
|
+
last = next;
|
|
2997
|
+
listener();
|
|
2998
|
+
}
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
3001
|
+
};
|
|
3002
|
+
return api;
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
// src/block-handle/useBlockDrag.ts
|
|
3006
|
+
function useBlockDrag() {
|
|
3007
|
+
return getBlockDrag(useEditor(), useEditorView());
|
|
3008
|
+
}
|
|
3009
|
+
function useBlockDragState() {
|
|
3010
|
+
const drag = useBlockDrag();
|
|
3011
|
+
return useSyncExternalStore8(drag.subscribe, drag.getState, drag.getState);
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
// src/block-handle/BlockHandleHost.tsx
|
|
3015
|
+
import { Fragment as Fragment4, jsx as jsx24, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
3016
|
+
var GRIP_STYLE = Object.freeze({ touchAction: "none" });
|
|
3017
|
+
function BlockHandleHost() {
|
|
3018
|
+
const editor = useEditor();
|
|
3019
|
+
const view = useEditorView();
|
|
3020
|
+
const drag = useBlockDrag();
|
|
3021
|
+
const { slots, slashItems, t } = useEditorUI();
|
|
3022
|
+
const readOnly = useReadOnly();
|
|
3023
|
+
const state = useSyncExternalStore9(
|
|
3024
|
+
view.blocks.subscribe,
|
|
3025
|
+
view.blocks.getState,
|
|
3026
|
+
view.blocks.getState
|
|
3027
|
+
);
|
|
3028
|
+
const cleanup = useRef7(null);
|
|
3029
|
+
useEffect7(() => () => cleanup.current?.(), []);
|
|
3030
|
+
if (slots.BlockHandle === null || readOnly || typeof document === "undefined") return null;
|
|
3031
|
+
const { hover, menu } = state;
|
|
3032
|
+
const dragging = state.drag;
|
|
3033
|
+
const openMenuFor = (blockId) => {
|
|
3034
|
+
const anchor = view.blocks.getState().hover ?? hover;
|
|
3035
|
+
if (anchor) view.blocks.openMenu({ blockId, x: anchor.x, y: anchor.y + 30 });
|
|
3036
|
+
};
|
|
3037
|
+
const gripPropsFor = (blockId) => ({
|
|
3038
|
+
onPointerDown: (event) => {
|
|
3039
|
+
if (event.button !== 0) return;
|
|
3040
|
+
event.preventDefault();
|
|
3041
|
+
cleanup.current?.();
|
|
3042
|
+
const anchor = hover;
|
|
3043
|
+
const cancel = startGripGesture(drag, blockId, event.nativeEvent, {
|
|
3044
|
+
onTap: () => {
|
|
3045
|
+
cleanup.current = null;
|
|
3046
|
+
if (anchor) view.blocks.openMenu({ blockId, x: anchor.x, y: anchor.y + 30 });
|
|
3047
|
+
}
|
|
3048
|
+
});
|
|
3049
|
+
cleanup.current = cancel;
|
|
3050
|
+
},
|
|
3051
|
+
onContextMenu: (event) => event.preventDefault(),
|
|
3052
|
+
style: GRIP_STYLE,
|
|
3053
|
+
"data-inkbrick-grip": ""
|
|
3054
|
+
});
|
|
3055
|
+
const Handle = slots.BlockHandle ?? DefaultBlockHandle;
|
|
3056
|
+
const Menu = slots.BlockMenu ?? DefaultBlockMenu;
|
|
3057
|
+
const activeId = dragging?.blockId ?? hover?.blockId;
|
|
3058
|
+
const menuBlock = menu ? editor.store.getBlock(menu.blockId) : void 0;
|
|
3059
|
+
const menuContent = menu && menuBlock ? buildBlockActions(editor, view, menu.blockId, slashItems, t) : null;
|
|
3060
|
+
return createPortal(
|
|
3061
|
+
/* @__PURE__ */ jsxs10(Fragment4, { children: [
|
|
3062
|
+
hover && activeId && /* @__PURE__ */ jsx24(
|
|
3063
|
+
Handle,
|
|
3064
|
+
{
|
|
3065
|
+
blockId: activeId,
|
|
3066
|
+
position: { x: hover.x, y: hover.y },
|
|
3067
|
+
canAdd: editor.can("insertBlock", { flavour: "block:paragraph", afterId: activeId }),
|
|
3068
|
+
dragging: !!dragging,
|
|
3069
|
+
menuOpen: !!menu,
|
|
3070
|
+
onAdd: () => addBlockBelow(editor, view, activeId, slashItems),
|
|
3071
|
+
onOpenMenu: () => openMenuFor(activeId),
|
|
3072
|
+
gripProps: gripPropsFor(activeId)
|
|
3073
|
+
}
|
|
3074
|
+
),
|
|
3075
|
+
menu && menuBlock && menuContent && /* @__PURE__ */ jsx24(
|
|
3076
|
+
Menu,
|
|
3077
|
+
{
|
|
3078
|
+
block: menuBlock,
|
|
3079
|
+
position: { x: menu.x, y: menu.y },
|
|
3080
|
+
actions: menuContent.actions,
|
|
3081
|
+
turnInto: menuContent.turnInto,
|
|
3082
|
+
onClose: () => {
|
|
3083
|
+
view.blocks.closeMenu();
|
|
3084
|
+
view.getRoot()?.focus({ preventScroll: true });
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
)
|
|
3088
|
+
] }),
|
|
3089
|
+
document.body
|
|
3090
|
+
);
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
// src/block-handle/DropIndicatorHost.tsx
|
|
3094
|
+
import { useEffect as useEffect8 } from "react";
|
|
3095
|
+
import { createPortal as createPortal2 } from "react-dom";
|
|
3096
|
+
import { jsx as jsx25 } from "react/jsx-runtime";
|
|
3097
|
+
function DefaultDropIndicator({ indicator, position }) {
|
|
3098
|
+
return /* @__PURE__ */ jsx25(
|
|
3099
|
+
"div",
|
|
3100
|
+
{
|
|
3101
|
+
className: "inkbrick-drop-indicator",
|
|
3102
|
+
"data-inkbrick-ui": "drop-indicator",
|
|
3103
|
+
"data-testid": "drop-indicator",
|
|
3104
|
+
"data-position": position,
|
|
3105
|
+
style: { left: indicator.left, top: indicator.top, width: indicator.width }
|
|
3106
|
+
}
|
|
3107
|
+
);
|
|
3108
|
+
}
|
|
3109
|
+
function DropIndicatorHost() {
|
|
3110
|
+
const view = useEditorView();
|
|
3111
|
+
const { slots } = useEditorUI();
|
|
3112
|
+
const readOnly = useReadOnly();
|
|
3113
|
+
const drag = useBlockDragState();
|
|
3114
|
+
const draggedId = drag?.blockId;
|
|
3115
|
+
useEffect8(() => {
|
|
3116
|
+
if (!draggedId) return;
|
|
3117
|
+
const element = blockElement(view, draggedId);
|
|
3118
|
+
element?.setAttribute("data-inkbrick-dragging", "true");
|
|
3119
|
+
return () => element?.removeAttribute("data-inkbrick-dragging");
|
|
3120
|
+
}, [draggedId, view]);
|
|
3121
|
+
if (readOnly || slots.DropIndicator === null || typeof document === "undefined") return null;
|
|
3122
|
+
if (!drag?.indicator || !drag.targetId || !drag.position) return null;
|
|
3123
|
+
const Indicator = slots.DropIndicator ?? DefaultDropIndicator;
|
|
3124
|
+
return createPortal2(
|
|
3125
|
+
/* @__PURE__ */ jsx25(
|
|
3126
|
+
Indicator,
|
|
3127
|
+
{
|
|
3128
|
+
blockId: drag.blockId,
|
|
3129
|
+
targetId: drag.targetId,
|
|
3130
|
+
position: drag.position,
|
|
3131
|
+
indicator: drag.indicator
|
|
3132
|
+
}
|
|
3133
|
+
),
|
|
3134
|
+
document.body
|
|
3135
|
+
);
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
// src/clipboard/useClipboard.ts
|
|
3139
|
+
import { useEffect as useEffect9 } from "react";
|
|
3140
|
+
import { normalizeLinkUrl as normalizeLinkUrl2 } from "@inkbrick/document";
|
|
3141
|
+
import {
|
|
3142
|
+
FRAGMENT_MIME,
|
|
3143
|
+
fragmentToHtml,
|
|
3144
|
+
fragmentToPlainText,
|
|
3145
|
+
sanitizeFragment as sanitizeFragment2
|
|
3146
|
+
} from "@inkbrick/engine";
|
|
3147
|
+
|
|
3148
|
+
// src/editing/inputActions.ts
|
|
3149
|
+
import { flushSync as flushSync5 } from "react-dom";
|
|
3150
|
+
function createInputActions(editor, view, root, slashItems) {
|
|
3151
|
+
const exec = (intent) => editor.dispatch(intent);
|
|
3152
|
+
const run = (action) => {
|
|
3153
|
+
let caret;
|
|
3154
|
+
flushSync5(() => {
|
|
3155
|
+
caret = action();
|
|
3156
|
+
});
|
|
3157
|
+
if (caret) view.placeCaret(caret.blockId, caret.offset);
|
|
3158
|
+
syncSlash();
|
|
3159
|
+
};
|
|
3160
|
+
const syncSlash = () => syncSlashMenu(editor, view, slashItems);
|
|
3161
|
+
const blockText = (blockId) => {
|
|
3162
|
+
const block2 = editor.store.getBlock(blockId);
|
|
3163
|
+
return block2 ? editor.engine.inline.toPlainText(block2.content) : "";
|
|
3164
|
+
};
|
|
3165
|
+
const isSingleBlock = (selection) => selection.from.blockId === selection.to.blockId;
|
|
3166
|
+
const deleteSelection = (selection) => isSingleBlock(selection) ? exec({
|
|
3167
|
+
type: "deleteText",
|
|
3168
|
+
payload: {
|
|
3169
|
+
blockId: selection.from.blockId,
|
|
3170
|
+
from: selection.from.offset,
|
|
3171
|
+
to: selection.to.offset
|
|
3172
|
+
}
|
|
3173
|
+
}) : exec({
|
|
3174
|
+
type: "deleteRange",
|
|
3175
|
+
payload: { from: selection.from, to: selection.to }
|
|
3176
|
+
});
|
|
3177
|
+
const insertAt = (blockId, offset, text) => exec({
|
|
3178
|
+
type: "replaceText",
|
|
3179
|
+
payload: { blockId, from: offset, to: offset, text }
|
|
3180
|
+
});
|
|
3181
|
+
const replaceSelection = (selection, text) => {
|
|
3182
|
+
if (selection.collapsed) {
|
|
3183
|
+
return insertAt(selection.from.blockId, selection.from.offset, text);
|
|
3184
|
+
}
|
|
3185
|
+
return editor.batch(() => {
|
|
3186
|
+
const caret = deleteSelection(selection);
|
|
3187
|
+
return caret ? insertAt(caret.blockId, caret.offset, text) : void 0;
|
|
3188
|
+
});
|
|
3189
|
+
};
|
|
3190
|
+
const toggleFormat = (format) => {
|
|
3191
|
+
const selection = readDocSelection(root);
|
|
3192
|
+
if (selection) toggleFormatInSelection(editor, view, selection, format);
|
|
3193
|
+
};
|
|
3194
|
+
const undo = () => run(() => editor.undo()?.caret);
|
|
3195
|
+
const redo = () => run(() => editor.redo()?.caret);
|
|
3196
|
+
return {
|
|
3197
|
+
exec,
|
|
3198
|
+
run,
|
|
3199
|
+
syncSlash,
|
|
3200
|
+
blockText,
|
|
3201
|
+
deleteSelection,
|
|
3202
|
+
insertAt,
|
|
3203
|
+
replaceSelection,
|
|
3204
|
+
toggleFormat,
|
|
3205
|
+
undo,
|
|
3206
|
+
redo
|
|
3207
|
+
};
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
// src/clipboard/useClipboard.ts
|
|
3211
|
+
var URL_ONLY = /^(https?:\/\/|mailto:)\S+$/i;
|
|
3212
|
+
function isForeignControl(_target) {
|
|
3213
|
+
const active = document.activeElement;
|
|
3214
|
+
return active instanceof Element && active.closest("input, textarea, select, [data-inkbrick-ignore-input]") !== null;
|
|
3215
|
+
}
|
|
3216
|
+
function parseJson(text) {
|
|
3217
|
+
try {
|
|
3218
|
+
return JSON.parse(text);
|
|
3219
|
+
} catch {
|
|
3220
|
+
return null;
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
function useClipboard(ref, readOnly) {
|
|
3224
|
+
const editor = useEditor();
|
|
3225
|
+
const view = useEditorView();
|
|
3226
|
+
const { images, slashItems } = useEditorUI();
|
|
3227
|
+
useEffect9(() => {
|
|
3228
|
+
const root = ref.current;
|
|
3229
|
+
if (!root) return;
|
|
3230
|
+
let plainNext = false;
|
|
3231
|
+
let plainTimer;
|
|
3232
|
+
const { exec, run, deleteSelection, insertAt } = createInputActions(
|
|
3233
|
+
editor,
|
|
3234
|
+
view,
|
|
3235
|
+
root,
|
|
3236
|
+
slashItems
|
|
3237
|
+
);
|
|
3238
|
+
const writeSelection = (event, cut) => {
|
|
3239
|
+
if (isForeignControl(event.target)) return;
|
|
3240
|
+
const selection = readDocSelection(root);
|
|
3241
|
+
if (!selection || selection.collapsed || !event.clipboardData) return;
|
|
3242
|
+
const fragment = editor.copyFragment(selection.from, selection.to);
|
|
3243
|
+
if (fragment.blocks.length === 0) return;
|
|
3244
|
+
const json = JSON.stringify(fragment);
|
|
3245
|
+
event.clipboardData.setData("text/plain", fragmentToPlainText(fragment));
|
|
3246
|
+
event.clipboardData.setData("text/html", fragmentToHtml(fragment, encodeURIComponent(json)));
|
|
3247
|
+
event.clipboardData.setData(FRAGMENT_MIME, json);
|
|
3248
|
+
event.preventDefault();
|
|
3249
|
+
if (cut && !readOnly) run(() => editor.batch(() => deleteSelection(selection)));
|
|
3250
|
+
};
|
|
3251
|
+
const pastePlain = (text, selection) => {
|
|
3252
|
+
const trimmed = text.trim();
|
|
3253
|
+
if (!selection.collapsed && selection.from.blockId === selection.to.blockId && URL_ONLY.test(trimmed) && normalizeLinkUrl2(trimmed) && editor.store.getBlock(selection.from.blockId)?.flavour !== "block:code" && setLinkInSelection(editor, view, selection, trimmed)) {
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
3256
|
+
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
3257
|
+
run(
|
|
3258
|
+
() => editor.batch(() => {
|
|
3259
|
+
let caret = selection.collapsed ? { blockId: selection.from.blockId, offset: selection.from.offset } : deleteSelection(selection);
|
|
3260
|
+
if (!caret) return void 0;
|
|
3261
|
+
const flavour = editor.store.getBlock(caret.blockId)?.flavour ?? "";
|
|
3262
|
+
if (editor.schema.getBlockSchema(flavour)?.enter?.mode === "newline") {
|
|
3263
|
+
return insertAt(caret.blockId, caret.offset, lines.join("\n"));
|
|
3264
|
+
}
|
|
3265
|
+
caret = insertAt(caret.blockId, caret.offset, lines[0]);
|
|
3266
|
+
for (const line of lines.slice(1)) {
|
|
3267
|
+
if (!caret) return void 0;
|
|
3268
|
+
const next = exec({
|
|
3269
|
+
type: "splitBlock",
|
|
3270
|
+
payload: { blockId: caret.blockId, offset: caret.offset }
|
|
3271
|
+
});
|
|
3272
|
+
if (!next) return caret;
|
|
3273
|
+
caret = insertAt(next.blockId, next.offset, line);
|
|
3274
|
+
}
|
|
3275
|
+
return caret;
|
|
3276
|
+
})
|
|
3277
|
+
);
|
|
3278
|
+
};
|
|
3279
|
+
const pasteFragment = (fragment, selection) => {
|
|
3280
|
+
if (!editor.can("pasteFragment", { at: selection.from, fragment })) return false;
|
|
3281
|
+
run(
|
|
3282
|
+
() => editor.batch(() => {
|
|
3283
|
+
const caret = selection.collapsed ? { blockId: selection.from.blockId, offset: selection.from.offset } : deleteSelection(selection);
|
|
3284
|
+
return caret ? exec({ type: "pasteFragment", payload: { at: caret, fragment } }) : void 0;
|
|
3285
|
+
})
|
|
3286
|
+
);
|
|
3287
|
+
return true;
|
|
3288
|
+
};
|
|
3289
|
+
const onPaste = (event) => {
|
|
3290
|
+
if (isForeignControl(event.target)) return;
|
|
3291
|
+
event.preventDefault();
|
|
3292
|
+
const data = event.clipboardData;
|
|
3293
|
+
const selection = readDocSelection(root);
|
|
3294
|
+
if (readOnly || !data || !selection) return;
|
|
3295
|
+
const text = data.getData("text/plain");
|
|
3296
|
+
const files = [...data.files].filter((file) => isAcceptedImage(file, images));
|
|
3297
|
+
if (files.length > 0 && text.trim() === "") {
|
|
3298
|
+
plainNext = false;
|
|
3299
|
+
insertImageFiles(editor, view, images, files, selection.to.blockId);
|
|
3300
|
+
return;
|
|
3301
|
+
}
|
|
3302
|
+
const plainOnly = plainNext;
|
|
3303
|
+
plainNext = false;
|
|
3304
|
+
const inCode = editor.store.getBlock(selection.from.blockId)?.flavour === "block:code";
|
|
3305
|
+
if (!plainOnly && !inCode) {
|
|
3306
|
+
const json = data.getData(FRAGMENT_MIME);
|
|
3307
|
+
const html = data.getData("text/html");
|
|
3308
|
+
const fragment = (json ? sanitizeFragment2(parseJson(json)) : null) ?? (html ? embeddedFragment(html) ?? htmlToFragment(html) : null);
|
|
3309
|
+
if (fragment && pasteFragment(fragment, selection)) return;
|
|
3310
|
+
}
|
|
3311
|
+
if (text) pastePlain(text, selection);
|
|
3312
|
+
};
|
|
3313
|
+
const onKeyDown = (event) => {
|
|
3314
|
+
if (readOnly && event.key === "Backspace" && !isForeignControl(event.target)) {
|
|
3315
|
+
event.preventDefault();
|
|
3316
|
+
return;
|
|
3317
|
+
}
|
|
3318
|
+
if (readOnly && (event.ctrlKey || event.metaKey) && !event.shiftKey && event.key.toLowerCase() === "a") {
|
|
3319
|
+
const selection = globalThis.getSelection();
|
|
3320
|
+
event.preventDefault();
|
|
3321
|
+
selection?.removeAllRanges();
|
|
3322
|
+
const range = document.createRange();
|
|
3323
|
+
range.selectNodeContents(root);
|
|
3324
|
+
selection?.addRange(range);
|
|
3325
|
+
return;
|
|
3326
|
+
}
|
|
3327
|
+
if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key.toLowerCase() === "v") {
|
|
3328
|
+
plainNext = true;
|
|
3329
|
+
clearTimeout(plainTimer);
|
|
3330
|
+
plainTimer = setTimeout(() => {
|
|
3331
|
+
plainNext = false;
|
|
3332
|
+
}, 1e3);
|
|
3333
|
+
}
|
|
3334
|
+
};
|
|
3335
|
+
const hasFiles = (event) => !!event.dataTransfer && [...event.dataTransfer.types].includes("Files");
|
|
3336
|
+
const onDragOver = (event) => {
|
|
3337
|
+
if (hasFiles(event)) event.preventDefault();
|
|
3338
|
+
};
|
|
3339
|
+
const onDrop = (event) => {
|
|
3340
|
+
if (!hasFiles(event)) return;
|
|
3341
|
+
event.preventDefault();
|
|
3342
|
+
const files = [...event.dataTransfer?.files ?? []].filter(
|
|
3343
|
+
(file) => isAcceptedImage(file, images)
|
|
3344
|
+
);
|
|
3345
|
+
if (readOnly || files.length === 0) return;
|
|
3346
|
+
const probe = probePoint(view, event.clientX, event.clientY);
|
|
3347
|
+
const target = (probe ? blockAtPoint(editor, view, probe.x, probe.y) : null) ?? readDocSelection(root)?.to.blockId;
|
|
3348
|
+
if (target) insertImageFiles(editor, view, images, files, target);
|
|
3349
|
+
};
|
|
3350
|
+
const onCopy = (event) => writeSelection(event, false);
|
|
3351
|
+
const onCut = (event) => writeSelection(event, true);
|
|
3352
|
+
root.addEventListener("copy", onCopy);
|
|
3353
|
+
root.addEventListener("cut", onCut);
|
|
3354
|
+
root.addEventListener("paste", onPaste);
|
|
3355
|
+
root.addEventListener("keydown", onKeyDown);
|
|
3356
|
+
root.addEventListener("dragover", onDragOver);
|
|
3357
|
+
root.addEventListener("drop", onDrop);
|
|
3358
|
+
return () => {
|
|
3359
|
+
clearTimeout(plainTimer);
|
|
3360
|
+
root.removeEventListener("copy", onCopy);
|
|
3361
|
+
root.removeEventListener("cut", onCut);
|
|
3362
|
+
root.removeEventListener("paste", onPaste);
|
|
3363
|
+
root.removeEventListener("keydown", onKeyDown);
|
|
3364
|
+
root.removeEventListener("dragover", onDragOver);
|
|
3365
|
+
root.removeEventListener("drop", onDrop);
|
|
3366
|
+
};
|
|
3367
|
+
}, [editor, view, ref, readOnly, images, slashItems]);
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
// src/block-handle/useBlockHandles.ts
|
|
3371
|
+
import { useEffect as useEffect10 } from "react";
|
|
3372
|
+
function useBlockHandles(ref, enabled) {
|
|
3373
|
+
const editor = useEditor();
|
|
3374
|
+
const view = useEditorView();
|
|
3375
|
+
useEffect10(() => {
|
|
3376
|
+
const root = ref.current;
|
|
3377
|
+
if (!root || !enabled) return;
|
|
3378
|
+
const showAt = (event) => {
|
|
3379
|
+
const probe = probePoint(view, event.clientX, event.clientY);
|
|
3380
|
+
if (!probe) {
|
|
3381
|
+
view.blocks.setHover(null);
|
|
3382
|
+
return;
|
|
3383
|
+
}
|
|
3384
|
+
const blockId = blockAtPoint(editor, view, probe.x, probe.y);
|
|
3385
|
+
if (!blockId) return;
|
|
3386
|
+
const wrapper = blockElement(view, blockId);
|
|
3387
|
+
const row = rowRect(view, blockId);
|
|
3388
|
+
if (wrapper && row) {
|
|
3389
|
+
view.blocks.setHover({
|
|
3390
|
+
blockId,
|
|
3391
|
+
x: Math.round(wrapper.getBoundingClientRect().left),
|
|
3392
|
+
y: Math.round(row.top)
|
|
3393
|
+
});
|
|
3394
|
+
}
|
|
3395
|
+
};
|
|
3396
|
+
const onUiTarget = (event) => event.target instanceof Element && !!event.target.closest("[data-inkbrick-block-ui]");
|
|
3397
|
+
const onMove = (event) => {
|
|
3398
|
+
const { menu, drag } = view.blocks.getState();
|
|
3399
|
+
if (event.pointerType === "touch") return;
|
|
3400
|
+
if (drag || menu || view.pointerDown) return;
|
|
3401
|
+
if (onUiTarget(event)) return;
|
|
3402
|
+
showAt(event);
|
|
3403
|
+
};
|
|
3404
|
+
const onPointerUp = (event) => {
|
|
3405
|
+
if (event.pointerType === "mouse" || onUiTarget(event)) return;
|
|
3406
|
+
const { menu, drag } = view.blocks.getState();
|
|
3407
|
+
if (!drag && !menu) showAt(event);
|
|
3408
|
+
};
|
|
3409
|
+
const onPointerDown = (event) => {
|
|
3410
|
+
if (onUiTarget(event)) return;
|
|
3411
|
+
view.blocks.closeMenu();
|
|
3412
|
+
};
|
|
3413
|
+
const hide = (event) => {
|
|
3414
|
+
if (onUiTarget(event)) return;
|
|
3415
|
+
if (view.blocks.getState().drag) return;
|
|
3416
|
+
view.blocks.setHover(null);
|
|
3417
|
+
view.blocks.closeMenu();
|
|
3418
|
+
};
|
|
3419
|
+
const onKeyDown = (event) => {
|
|
3420
|
+
if (event.key === "Escape") view.blocks.closeMenu();
|
|
3421
|
+
if (view.blocks.getState().hover && !event.metaKey && !event.ctrlKey)
|
|
3422
|
+
view.blocks.setHover(null);
|
|
3423
|
+
};
|
|
3424
|
+
const onLeave = (event) => {
|
|
3425
|
+
if (event.pointerType === "touch") return;
|
|
3426
|
+
if (!event.relatedTarget) view.blocks.setHover(null);
|
|
3427
|
+
};
|
|
3428
|
+
const unsubscribe = editor.store.subscribe(() => {
|
|
3429
|
+
const { hover, menu } = view.blocks.getState();
|
|
3430
|
+
if (hover && !hasHandle(editor, hover.blockId)) view.blocks.setHover(null);
|
|
3431
|
+
if (menu && !hasHandle(editor, menu.blockId)) view.blocks.closeMenu();
|
|
3432
|
+
});
|
|
3433
|
+
document.addEventListener("pointermove", onMove);
|
|
3434
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
3435
|
+
document.addEventListener("pointerup", onPointerUp);
|
|
3436
|
+
document.addEventListener("pointerout", onLeave);
|
|
3437
|
+
document.addEventListener("keydown", onKeyDown);
|
|
3438
|
+
window.addEventListener("scroll", hide, true);
|
|
3439
|
+
return () => {
|
|
3440
|
+
unsubscribe();
|
|
3441
|
+
document.removeEventListener("pointermove", onMove);
|
|
3442
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
3443
|
+
document.removeEventListener("pointerup", onPointerUp);
|
|
3444
|
+
document.removeEventListener("pointerout", onLeave);
|
|
3445
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
3446
|
+
window.removeEventListener("scroll", hide, true);
|
|
3447
|
+
view.blocks.setHover(null);
|
|
3448
|
+
view.blocks.closeMenu();
|
|
3449
|
+
};
|
|
3450
|
+
}, [editor, view, ref, enabled]);
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3453
|
+
// src/editing/useEditorInput.ts
|
|
3454
|
+
import { useEffect as useEffect11 } from "react";
|
|
3455
|
+
|
|
3456
|
+
// src/editing/beforeInput.ts
|
|
3457
|
+
var FORMAT_BY_INPUT_TYPE = {
|
|
3458
|
+
formatBold: "bold",
|
|
3459
|
+
formatItalic: "italic",
|
|
3460
|
+
formatUnderline: "underline",
|
|
3461
|
+
formatStrikeThrough: "strike"
|
|
3462
|
+
};
|
|
3463
|
+
function matchShortcut(prefix) {
|
|
3464
|
+
if (/^#{1,6}$/.test(prefix)) {
|
|
3465
|
+
return { flavour: "block:heading", props: { level: prefix.length } };
|
|
3466
|
+
}
|
|
3467
|
+
if (prefix === ">") return { flavour: "block:quote", props: {} };
|
|
3468
|
+
if (prefix === "```") return { flavour: "block:code", props: {} };
|
|
3469
|
+
if (prefix === "---") return { flavour: "block:divider", props: {} };
|
|
3470
|
+
if (prefix === "$$") return { flavour: "block:formula", props: {} };
|
|
3471
|
+
if (/^[-*+]$/.test(prefix)) return { flavour: "block:list", props: { type: "bullet" } };
|
|
3472
|
+
if (/^\d+\.$/.test(prefix)) return { flavour: "block:list", props: { type: "ordered" } };
|
|
3473
|
+
if (prefix === "[]") return { flavour: "block:list", props: { type: "todo", checked: false } };
|
|
3474
|
+
return null;
|
|
3475
|
+
}
|
|
3476
|
+
function applyBeforeInput(event, root, editor, view, slashItems, actions) {
|
|
3477
|
+
const {
|
|
3478
|
+
exec,
|
|
3479
|
+
run,
|
|
3480
|
+
syncSlash,
|
|
3481
|
+
blockText,
|
|
3482
|
+
deleteSelection,
|
|
3483
|
+
replaceSelection,
|
|
3484
|
+
toggleFormat,
|
|
3485
|
+
undo,
|
|
3486
|
+
redo
|
|
3487
|
+
} = actions;
|
|
3488
|
+
const selection = readDocSelection(root);
|
|
3489
|
+
if (!selection) return;
|
|
3490
|
+
const { from, collapsed } = selection;
|
|
3491
|
+
const type = event.inputType;
|
|
3492
|
+
const block2 = editor.store.getBlock(from.blockId);
|
|
3493
|
+
const text = blockText(from.blockId);
|
|
3494
|
+
switch (type) {
|
|
3495
|
+
case "insertText":
|
|
3496
|
+
case "insertReplacementText": {
|
|
3497
|
+
const data = event.data ?? event.dataTransfer?.getData("text/plain") ?? "";
|
|
3498
|
+
if (!data) return;
|
|
3499
|
+
if (data === "$" && collapsed && block2 && block2.flavour !== "block:code") {
|
|
3500
|
+
const formula = matchInlineFormula(text.slice(0, from.offset));
|
|
3501
|
+
if (formula && editor.can("insertFormula", {
|
|
3502
|
+
blockId: from.blockId,
|
|
3503
|
+
from: formula.from,
|
|
3504
|
+
to: from.offset,
|
|
3505
|
+
latex: formula.latex
|
|
3506
|
+
})) {
|
|
3507
|
+
run(
|
|
3508
|
+
() => exec({
|
|
3509
|
+
type: "insertFormula",
|
|
3510
|
+
payload: {
|
|
3511
|
+
blockId: from.blockId,
|
|
3512
|
+
from: formula.from,
|
|
3513
|
+
to: from.offset,
|
|
3514
|
+
latex: formula.latex
|
|
3515
|
+
}
|
|
3516
|
+
})
|
|
3517
|
+
);
|
|
3518
|
+
return;
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
if (data === " " && collapsed && block2?.flavour === "block:paragraph") {
|
|
3522
|
+
const shortcut = matchShortcut(text.slice(0, from.offset));
|
|
3523
|
+
if (shortcut) {
|
|
3524
|
+
run(
|
|
3525
|
+
() => editor.batch(() => {
|
|
3526
|
+
exec({
|
|
3527
|
+
type: "deleteText",
|
|
3528
|
+
payload: { blockId: from.blockId, from: 0, to: from.offset }
|
|
3529
|
+
});
|
|
3530
|
+
return exec({
|
|
3531
|
+
type: "convertBlock",
|
|
3532
|
+
payload: { blockId: from.blockId, ...shortcut, offset: 0 }
|
|
3533
|
+
});
|
|
3534
|
+
})
|
|
3535
|
+
);
|
|
3536
|
+
return;
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
const opensSlash = data === "/" && collapsed && !!block2 && block2.flavour !== "block:code" && editor.schema.getBlockSchema(block2.flavour)?.inline !== false && (from.offset === 0 || /\s/.test(text[from.offset - 1])) && availableSlashItems(editor, slashItems, from.blockId).length > 0;
|
|
3540
|
+
run(() => replaceSelection(selection, data));
|
|
3541
|
+
if (opensSlash) {
|
|
3542
|
+
view.slash.open(from.blockId, from.offset);
|
|
3543
|
+
syncSlash();
|
|
3544
|
+
}
|
|
3545
|
+
return;
|
|
3546
|
+
}
|
|
3547
|
+
case "insertParagraph":
|
|
3548
|
+
run(
|
|
3549
|
+
() => editor.batch(() => {
|
|
3550
|
+
const caret = collapsed ? { blockId: from.blockId, offset: from.offset } : deleteSelection(selection);
|
|
3551
|
+
return caret ? exec({
|
|
3552
|
+
type: "splitBlock",
|
|
3553
|
+
payload: { blockId: caret.blockId, offset: caret.offset }
|
|
3554
|
+
}) : void 0;
|
|
3555
|
+
})
|
|
3556
|
+
);
|
|
3557
|
+
return;
|
|
3558
|
+
case "insertLineBreak":
|
|
3559
|
+
run(() => replaceSelection(selection, "\n"));
|
|
3560
|
+
return;
|
|
3561
|
+
case "deleteContentBackward":
|
|
3562
|
+
run(
|
|
3563
|
+
() => collapsed ? exec({
|
|
3564
|
+
type: "deleteBackward",
|
|
3565
|
+
payload: { blockId: from.blockId, offset: from.offset }
|
|
3566
|
+
}) : deleteSelection(selection)
|
|
3567
|
+
);
|
|
3568
|
+
return;
|
|
3569
|
+
case "deleteContentForward":
|
|
3570
|
+
run(() => {
|
|
3571
|
+
if (!collapsed) return deleteSelection(selection);
|
|
3572
|
+
if (from.offset < text.length) {
|
|
3573
|
+
return exec({
|
|
3574
|
+
type: "deleteText",
|
|
3575
|
+
payload: { blockId: from.blockId, from: from.offset, to: from.offset + 1 }
|
|
3576
|
+
});
|
|
3577
|
+
}
|
|
3578
|
+
return deleteForwardAtEnd(editor, from.blockId);
|
|
3579
|
+
});
|
|
3580
|
+
return;
|
|
3581
|
+
case "deleteWordBackward":
|
|
3582
|
+
run(() => {
|
|
3583
|
+
if (!collapsed) return deleteSelection(selection);
|
|
3584
|
+
const start = text.slice(0, from.offset).replace(/(\S+\s*|\s+)$/, "").length;
|
|
3585
|
+
return start < from.offset ? exec({
|
|
3586
|
+
type: "deleteText",
|
|
3587
|
+
payload: { blockId: from.blockId, from: start, to: from.offset }
|
|
3588
|
+
}) : exec({
|
|
3589
|
+
type: "deleteBackward",
|
|
3590
|
+
payload: { blockId: from.blockId, offset: from.offset }
|
|
3591
|
+
});
|
|
3592
|
+
});
|
|
3593
|
+
return;
|
|
3594
|
+
case "deleteWordForward":
|
|
3595
|
+
run(() => {
|
|
3596
|
+
if (!collapsed) return deleteSelection(selection);
|
|
3597
|
+
const rest = text.slice(from.offset);
|
|
3598
|
+
const length = rest.length - rest.replace(/^(\s*[^\s]+|\s+)/, "").length;
|
|
3599
|
+
return length > 0 ? exec({
|
|
3600
|
+
type: "deleteText",
|
|
3601
|
+
payload: { blockId: from.blockId, from: from.offset, to: from.offset + length }
|
|
3602
|
+
}) : void 0;
|
|
3603
|
+
});
|
|
3604
|
+
return;
|
|
3605
|
+
case "deleteSoftLineBackward":
|
|
3606
|
+
case "deleteHardLineBackward":
|
|
3607
|
+
run(() => {
|
|
3608
|
+
if (!collapsed) return deleteSelection(selection);
|
|
3609
|
+
return from.offset > 0 ? exec({
|
|
3610
|
+
type: "deleteText",
|
|
3611
|
+
payload: { blockId: from.blockId, from: 0, to: from.offset }
|
|
3612
|
+
}) : void 0;
|
|
3613
|
+
});
|
|
3614
|
+
return;
|
|
3615
|
+
case "deleteSoftLineForward":
|
|
3616
|
+
case "deleteHardLineForward":
|
|
3617
|
+
run(() => {
|
|
3618
|
+
if (!collapsed) return deleteSelection(selection);
|
|
3619
|
+
return from.offset < text.length ? exec({
|
|
3620
|
+
type: "deleteText",
|
|
3621
|
+
payload: { blockId: from.blockId, from: from.offset, to: text.length }
|
|
3622
|
+
}) : void 0;
|
|
3623
|
+
});
|
|
3624
|
+
return;
|
|
3625
|
+
case "deleteByCut":
|
|
3626
|
+
case "deleteByDrag":
|
|
3627
|
+
case "deleteContent":
|
|
3628
|
+
if (!collapsed) run(() => deleteSelection(selection));
|
|
3629
|
+
return;
|
|
3630
|
+
case "historyUndo":
|
|
3631
|
+
undo();
|
|
3632
|
+
return;
|
|
3633
|
+
case "historyRedo":
|
|
3634
|
+
redo();
|
|
3635
|
+
return;
|
|
3636
|
+
case "formatBold":
|
|
3637
|
+
case "formatItalic":
|
|
3638
|
+
case "formatUnderline":
|
|
3639
|
+
case "formatStrikeThrough":
|
|
3640
|
+
toggleFormat(FORMAT_BY_INPUT_TYPE[type]);
|
|
3641
|
+
return;
|
|
3642
|
+
default:
|
|
3643
|
+
return;
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
// src/editing/useEditorInput.ts
|
|
3648
|
+
var NAVIGATION_KEYS = /* @__PURE__ */ new Set([
|
|
3649
|
+
"ArrowLeft",
|
|
3650
|
+
"ArrowRight",
|
|
3651
|
+
"ArrowUp",
|
|
3652
|
+
"ArrowDown",
|
|
3653
|
+
"Home",
|
|
3654
|
+
"End",
|
|
3655
|
+
"PageUp",
|
|
3656
|
+
"PageDown"
|
|
3657
|
+
]);
|
|
3658
|
+
function diffText(before, after) {
|
|
3659
|
+
let start = 0;
|
|
3660
|
+
while (start < before.length && start < after.length && before[start] === after[start]) start++;
|
|
3661
|
+
let endBefore = before.length;
|
|
3662
|
+
let endAfter = after.length;
|
|
3663
|
+
while (endBefore > start && endAfter > start && before[endBefore - 1] === after[endAfter - 1]) {
|
|
3664
|
+
endBefore--;
|
|
3665
|
+
endAfter--;
|
|
3666
|
+
}
|
|
3667
|
+
return { from: start, to: endBefore, text: after.slice(start, endAfter) };
|
|
3668
|
+
}
|
|
3669
|
+
function isForeignControl2(target) {
|
|
3670
|
+
return target instanceof Element && target.closest("input, textarea, select, button, [data-inkbrick-ignore-input]") !== null;
|
|
3671
|
+
}
|
|
3672
|
+
function useEditorInput(ref, enabled = true) {
|
|
3673
|
+
const editor = useEditor();
|
|
3674
|
+
const view = useEditorView();
|
|
3675
|
+
const { slashItems, keymap } = useEditorUI();
|
|
3676
|
+
useEffect11(() => {
|
|
3677
|
+
const root = ref.current;
|
|
3678
|
+
if (!root || !enabled) return;
|
|
3679
|
+
let composing = false;
|
|
3680
|
+
const actions = createInputActions(editor, view, root, slashItems);
|
|
3681
|
+
const { exec, run, syncSlash, blockText, replaceSelection, toggleFormat, undo, redo } = actions;
|
|
3682
|
+
const onBeforeInput = (event) => {
|
|
3683
|
+
if (isForeignControl2(event.target)) return;
|
|
3684
|
+
if (composing || event.isComposing) return;
|
|
3685
|
+
event.preventDefault();
|
|
3686
|
+
applyBeforeInput(event, root, editor, view, slashItems, actions);
|
|
3687
|
+
};
|
|
3688
|
+
const onKeyDown = (event) => {
|
|
3689
|
+
if (isForeignControl2(event.target)) return;
|
|
3690
|
+
const handled = handleKeyDown(keymap, {
|
|
3691
|
+
editor,
|
|
3692
|
+
view,
|
|
3693
|
+
root,
|
|
3694
|
+
event,
|
|
3695
|
+
slashItems,
|
|
3696
|
+
selection: () => readDocSelection(root),
|
|
3697
|
+
exec,
|
|
3698
|
+
run,
|
|
3699
|
+
replaceSelection,
|
|
3700
|
+
undo,
|
|
3701
|
+
redo,
|
|
3702
|
+
toggleFormat
|
|
3703
|
+
});
|
|
3704
|
+
if (!handled && NAVIGATION_KEYS.has(event.key)) editor.sealHistory();
|
|
3705
|
+
};
|
|
3706
|
+
const editableAtCaret = () => {
|
|
3707
|
+
const node = document.getSelection()?.anchorNode ?? null;
|
|
3708
|
+
const element = node instanceof Element ? node : node?.parentElement;
|
|
3709
|
+
const editable = element?.closest(".editable[data-id]");
|
|
3710
|
+
return editable && root.contains(editable) ? editable : null;
|
|
3711
|
+
};
|
|
3712
|
+
let compositionBlock = null;
|
|
3713
|
+
const onCompositionStart = () => {
|
|
3714
|
+
composing = true;
|
|
3715
|
+
compositionBlock = editableAtCaret();
|
|
3716
|
+
compositionBlock?.setAttribute("data-composing", "");
|
|
3717
|
+
};
|
|
3718
|
+
const onCompositionEnd = () => {
|
|
3719
|
+
composing = false;
|
|
3720
|
+
const started = compositionBlock?.isConnected ? compositionBlock : null;
|
|
3721
|
+
const element = started ?? editableAtCaret();
|
|
3722
|
+
const blockId = element?.dataset.id;
|
|
3723
|
+
compositionBlock?.removeAttribute("data-composing");
|
|
3724
|
+
compositionBlock = null;
|
|
3725
|
+
if (!element || !blockId) return;
|
|
3726
|
+
const { from, to, text } = diffText(blockText(blockId), element.textContent ?? "");
|
|
3727
|
+
if (from === to && text === "") return;
|
|
3728
|
+
run(() => exec({ type: "replaceText", payload: { blockId, from, to, text } }));
|
|
3729
|
+
};
|
|
3730
|
+
const prevent = (event) => event.preventDefault();
|
|
3731
|
+
const sealHistory = () => editor.sealHistory();
|
|
3732
|
+
const closeSlash = () => view.slash.close();
|
|
3733
|
+
const onPointerDown = () => {
|
|
3734
|
+
view.pointerDown = true;
|
|
3735
|
+
view.toolbar.closeLink();
|
|
3736
|
+
view.toolbar.hide();
|
|
3737
|
+
};
|
|
3738
|
+
const onPointerUp = () => {
|
|
3739
|
+
if (!view.pointerDown) return;
|
|
3740
|
+
view.pointerDown = false;
|
|
3741
|
+
setTimeout(() => syncToolbar(editor, view), 0);
|
|
3742
|
+
};
|
|
3743
|
+
const onClick = (event) => {
|
|
3744
|
+
const formula = event.target instanceof Element ? event.target.closest("[data-inkbrick-formula]") : null;
|
|
3745
|
+
if (formula && root.contains(formula)) {
|
|
3746
|
+
event.preventDefault();
|
|
3747
|
+
openFormulaAt(view, formula);
|
|
3748
|
+
return;
|
|
3749
|
+
}
|
|
3750
|
+
if (!(event.ctrlKey || event.metaKey) || !(event.target instanceof Element)) return;
|
|
3751
|
+
const anchor = event.target.closest("a[href]");
|
|
3752
|
+
if (anchor && root.contains(anchor)) {
|
|
3753
|
+
event.preventDefault();
|
|
3754
|
+
window.open(anchor.href, "_blank", "noopener,noreferrer");
|
|
3755
|
+
}
|
|
3756
|
+
};
|
|
3757
|
+
const onSelectionChange = () => {
|
|
3758
|
+
if (view.slash.getState().open) syncSlash();
|
|
3759
|
+
syncToolbar(editor, view);
|
|
3760
|
+
};
|
|
3761
|
+
const syncIfOpen = () => {
|
|
3762
|
+
if (view.slash.getState().open) syncSlash();
|
|
3763
|
+
if (view.toolbar.getState().visible) syncToolbar(editor, view);
|
|
3764
|
+
};
|
|
3765
|
+
root.addEventListener("beforeinput", onBeforeInput);
|
|
3766
|
+
root.addEventListener("keydown", onKeyDown);
|
|
3767
|
+
root.addEventListener("compositionstart", onCompositionStart);
|
|
3768
|
+
root.addEventListener("compositionend", onCompositionEnd);
|
|
3769
|
+
root.addEventListener("drop", prevent);
|
|
3770
|
+
root.addEventListener("pointerdown", sealHistory);
|
|
3771
|
+
root.addEventListener("focusout", sealHistory);
|
|
3772
|
+
root.addEventListener("pointerdown", closeSlash);
|
|
3773
|
+
root.addEventListener("focusout", closeSlash);
|
|
3774
|
+
root.addEventListener("pointerdown", onPointerDown);
|
|
3775
|
+
root.addEventListener("click", onClick);
|
|
3776
|
+
document.addEventListener("pointerup", onPointerUp);
|
|
3777
|
+
document.addEventListener("pointercancel", onPointerUp);
|
|
3778
|
+
document.addEventListener("selectionchange", onSelectionChange);
|
|
3779
|
+
window.addEventListener("scroll", syncIfOpen, true);
|
|
3780
|
+
return () => {
|
|
3781
|
+
root.removeEventListener("beforeinput", onBeforeInput);
|
|
3782
|
+
root.removeEventListener("keydown", onKeyDown);
|
|
3783
|
+
root.removeEventListener("compositionstart", onCompositionStart);
|
|
3784
|
+
root.removeEventListener("compositionend", onCompositionEnd);
|
|
3785
|
+
root.removeEventListener("drop", prevent);
|
|
3786
|
+
root.removeEventListener("pointerdown", sealHistory);
|
|
3787
|
+
root.removeEventListener("focusout", sealHistory);
|
|
3788
|
+
root.removeEventListener("pointerdown", closeSlash);
|
|
3789
|
+
root.removeEventListener("focusout", closeSlash);
|
|
3790
|
+
root.removeEventListener("pointerdown", onPointerDown);
|
|
3791
|
+
root.removeEventListener("click", onClick);
|
|
3792
|
+
document.removeEventListener("pointerup", onPointerUp);
|
|
3793
|
+
document.removeEventListener("pointercancel", onPointerUp);
|
|
3794
|
+
document.removeEventListener("selectionchange", onSelectionChange);
|
|
3795
|
+
window.removeEventListener("scroll", syncIfOpen, true);
|
|
3796
|
+
compositionBlock?.removeAttribute("data-composing");
|
|
3797
|
+
view.slash.close();
|
|
3798
|
+
view.toolbar.hide();
|
|
3799
|
+
};
|
|
3800
|
+
}, [editor, view, ref, slashItems, keymap, enabled]);
|
|
3801
|
+
}
|
|
3802
|
+
|
|
3803
|
+
// src/editing/useSelectionState.ts
|
|
3804
|
+
import { useEffect as useEffect12 } from "react";
|
|
3805
|
+
function useSelectionState(ref) {
|
|
3806
|
+
const editor = useEditor();
|
|
3807
|
+
const view = useEditorView();
|
|
3808
|
+
useEffect12(() => {
|
|
3809
|
+
const root = ref.current;
|
|
3810
|
+
if (!root) return;
|
|
3811
|
+
const update = () => {
|
|
3812
|
+
const selection = readDocSelection(root);
|
|
3813
|
+
view.selection.set(
|
|
3814
|
+
selection && !selection.collapsed ? editor.blocksBetween(selection.from, selection.to) : []
|
|
3815
|
+
);
|
|
3816
|
+
const caret = selection?.collapsed ? selection.from.blockId : null;
|
|
3817
|
+
view.caret.set(caret);
|
|
3818
|
+
editor.controllers.setFocusedBlock(caret);
|
|
3819
|
+
view.viewport.setEdges(
|
|
3820
|
+
!selection ? [] : selection.collapsed ? [selection.from.blockId] : [selection.from.blockId, selection.to.blockId]
|
|
3821
|
+
);
|
|
3822
|
+
};
|
|
3823
|
+
document.addEventListener("selectionchange", update);
|
|
3824
|
+
update();
|
|
3825
|
+
return () => {
|
|
3826
|
+
document.removeEventListener("selectionchange", update);
|
|
3827
|
+
view.selection.set([]);
|
|
3828
|
+
view.caret.set(null);
|
|
3829
|
+
view.viewport.setEdges([]);
|
|
3830
|
+
editor.controllers.setFocusedBlock(null);
|
|
3831
|
+
};
|
|
3832
|
+
}, [editor, view, ref]);
|
|
3833
|
+
}
|
|
3834
|
+
|
|
3835
|
+
// src/formula/FormulaPopover.tsx
|
|
3836
|
+
import {
|
|
3837
|
+
useCallback as useCallback4,
|
|
3838
|
+
useEffect as useEffect13,
|
|
3839
|
+
useLayoutEffect as useLayoutEffect4,
|
|
3840
|
+
useRef as useRef8,
|
|
3841
|
+
useState as useState7,
|
|
3842
|
+
useSyncExternalStore as useSyncExternalStore10
|
|
3843
|
+
} from "react";
|
|
3844
|
+
import { createPortal as createPortal3 } from "react-dom";
|
|
3845
|
+
import { MAX_FORMULA_LENGTH as MAX_FORMULA_LENGTH2 } from "@inkbrick/document";
|
|
3846
|
+
import { jsx as jsx26, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
3847
|
+
var WIDTH = 340;
|
|
3848
|
+
function close(view, state) {
|
|
3849
|
+
view.formula.close();
|
|
3850
|
+
view.placeSelection(
|
|
3851
|
+
{ blockId: state.blockId, offset: state.mode === "edit" ? state.to : state.from },
|
|
3852
|
+
{ blockId: state.blockId, offset: state.to }
|
|
3853
|
+
);
|
|
3854
|
+
}
|
|
3855
|
+
function Body({ state }) {
|
|
3856
|
+
const editor = useEditor();
|
|
3857
|
+
const view = useEditorView();
|
|
3858
|
+
const t = useT();
|
|
3859
|
+
const [draft, setDraft] = useState7(state.latex);
|
|
3860
|
+
const [error, setError] = useState7(false);
|
|
3861
|
+
const ref = useRef8(null);
|
|
3862
|
+
const field = useRef8(null);
|
|
3863
|
+
const [top, setTop] = useState7(null);
|
|
3864
|
+
const insertSnippet = useSnippetInsert(field, draft, setDraft);
|
|
3865
|
+
const settle = useCallback4((result) => setError(result === "error"), []);
|
|
3866
|
+
const width = Math.min(WIDTH, window.innerWidth - 16);
|
|
3867
|
+
useEffect13(() => {
|
|
3868
|
+
field.current?.focus();
|
|
3869
|
+
field.current?.select();
|
|
3870
|
+
}, []);
|
|
3871
|
+
useLayoutEffect4(() => {
|
|
3872
|
+
const height = ref.current?.offsetHeight ?? 0;
|
|
3873
|
+
const below = state.position.y + 8;
|
|
3874
|
+
setTop(
|
|
3875
|
+
below + height > window.innerHeight && state.position.top - height - 8 > 0 ? state.position.top - height - 8 : below
|
|
3876
|
+
);
|
|
3877
|
+
}, [state.position.y, state.position.top, draft]);
|
|
3878
|
+
useEffect13(() => {
|
|
3879
|
+
const onPointerDown = (event) => {
|
|
3880
|
+
if (event.target instanceof Node && ref.current?.contains(event.target)) return;
|
|
3881
|
+
close(view, state);
|
|
3882
|
+
};
|
|
3883
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
3884
|
+
return () => document.removeEventListener("pointerdown", onPointerDown, true);
|
|
3885
|
+
}, [view, state]);
|
|
3886
|
+
const apply = () => {
|
|
3887
|
+
if (applyInlineFormula(editor, view, state, draft)) view.formula.close();
|
|
3888
|
+
};
|
|
3889
|
+
const remove = () => {
|
|
3890
|
+
if (applyInlineFormula(editor, view, state, "")) view.formula.close();
|
|
3891
|
+
};
|
|
3892
|
+
return /* @__PURE__ */ jsxs11(
|
|
3893
|
+
"div",
|
|
3894
|
+
{
|
|
3895
|
+
ref,
|
|
3896
|
+
className: "formula-popover",
|
|
3897
|
+
"data-inkbrick-ui": "formula-popover",
|
|
3898
|
+
"data-testid": "formula-popover",
|
|
3899
|
+
role: "dialog",
|
|
3900
|
+
"aria-label": t("formula.inline.title"),
|
|
3901
|
+
style: {
|
|
3902
|
+
left: Math.max(8, Math.min(state.position.x - width / 2, window.innerWidth - width - 8)),
|
|
3903
|
+
top: top ?? state.position.y + 8,
|
|
3904
|
+
width
|
|
3905
|
+
},
|
|
3906
|
+
onMouseDown: (event) => {
|
|
3907
|
+
if (!(event.target instanceof HTMLTextAreaElement)) event.preventDefault();
|
|
3908
|
+
},
|
|
3909
|
+
children: [
|
|
3910
|
+
/* @__PURE__ */ jsx26(
|
|
3911
|
+
"textarea",
|
|
3912
|
+
{
|
|
3913
|
+
ref: field,
|
|
3914
|
+
className: "formula-popover__source",
|
|
3915
|
+
"data-inkbrick-part": "formula-source",
|
|
3916
|
+
"aria-label": t("formula.source"),
|
|
3917
|
+
"aria-invalid": error || void 0,
|
|
3918
|
+
placeholder: t("formula.placeholder"),
|
|
3919
|
+
spellCheck: false,
|
|
3920
|
+
rows: 2,
|
|
3921
|
+
maxLength: MAX_FORMULA_LENGTH2,
|
|
3922
|
+
value: draft,
|
|
3923
|
+
onChange: (event) => setDraft(event.target.value),
|
|
3924
|
+
onKeyDown: (event) => {
|
|
3925
|
+
if (event.key === "Escape") {
|
|
3926
|
+
event.preventDefault();
|
|
3927
|
+
close(view, state);
|
|
3928
|
+
} else if (event.key === "Enter" && !event.shiftKey) {
|
|
3929
|
+
event.preventDefault();
|
|
3930
|
+
apply();
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
),
|
|
3935
|
+
/* @__PURE__ */ jsx26("div", { className: "formula-popover__preview", "data-inkbrick-part": "formula-preview", children: /* @__PURE__ */ jsx26(FormulaView, { latex: draft, showError: true, onSettled: settle }) }),
|
|
3936
|
+
/* @__PURE__ */ jsx26(LazyFormulaPalette, { onInsert: insertSnippet }),
|
|
3937
|
+
/* @__PURE__ */ jsxs11("div", { className: "formula-popover__actions", "data-inkbrick-part": "formula-actions", children: [
|
|
3938
|
+
state.mode === "edit" && /* @__PURE__ */ jsx26(
|
|
3939
|
+
"button",
|
|
3940
|
+
{
|
|
3941
|
+
type: "button",
|
|
3942
|
+
"data-inkbrick-part": "button",
|
|
3943
|
+
"data-danger": "",
|
|
3944
|
+
"data-action": "remove",
|
|
3945
|
+
"aria-label": t("formula.remove"),
|
|
3946
|
+
title: t("formula.remove"),
|
|
3947
|
+
onClick: remove,
|
|
3948
|
+
children: /* @__PURE__ */ jsx26(Icon, { name: "trash" })
|
|
3949
|
+
}
|
|
3950
|
+
),
|
|
3951
|
+
/* @__PURE__ */ jsx26("span", { className: "formula-popover__spacer" }),
|
|
3952
|
+
/* @__PURE__ */ jsx26(
|
|
3953
|
+
"button",
|
|
3954
|
+
{
|
|
3955
|
+
type: "button",
|
|
3956
|
+
"data-inkbrick-part": "button",
|
|
3957
|
+
"data-action": "cancel",
|
|
3958
|
+
onClick: () => close(view, state),
|
|
3959
|
+
children: t("formula.cancel")
|
|
3960
|
+
}
|
|
3961
|
+
),
|
|
3962
|
+
/* @__PURE__ */ jsx26(
|
|
3963
|
+
"button",
|
|
3964
|
+
{
|
|
3965
|
+
type: "button",
|
|
3966
|
+
"data-inkbrick-part": "button",
|
|
3967
|
+
"data-primary": "",
|
|
3968
|
+
"data-action": "apply",
|
|
3969
|
+
disabled: state.mode === "insert" && draft.trim() === "",
|
|
3970
|
+
onClick: apply,
|
|
3971
|
+
children: t("formula.apply")
|
|
3972
|
+
}
|
|
3973
|
+
)
|
|
3974
|
+
] })
|
|
3975
|
+
]
|
|
3976
|
+
}
|
|
3977
|
+
);
|
|
3978
|
+
}
|
|
3979
|
+
function FormulaPopover() {
|
|
3980
|
+
const view = useEditorView();
|
|
3981
|
+
const readOnly = useReadOnly();
|
|
3982
|
+
const state = useSyncExternalStore10(
|
|
3983
|
+
view.formula.subscribe,
|
|
3984
|
+
view.formula.getState,
|
|
3985
|
+
view.formula.getState
|
|
3986
|
+
);
|
|
3987
|
+
if (!state.open || readOnly || typeof document === "undefined") return null;
|
|
3988
|
+
return createPortal3(
|
|
3989
|
+
/* @__PURE__ */ jsx26(Body, { state }, `${state.mode}:${state.blockId}:${state.from}:${state.to}`),
|
|
3990
|
+
document.body
|
|
3991
|
+
);
|
|
3992
|
+
}
|
|
3993
|
+
|
|
3994
|
+
// src/slash/SlashMenuHost.tsx
|
|
3995
|
+
import { createPortal as createPortal4 } from "react-dom";
|
|
3996
|
+
import { useSyncExternalStore as useSyncExternalStore11 } from "react";
|
|
3997
|
+
|
|
3998
|
+
// src/slash/DefaultSlashMenu.tsx
|
|
3999
|
+
import { useId, useLayoutEffect as useLayoutEffect5, useRef as useRef9, useState as useState8 } from "react";
|
|
4000
|
+
import { jsx as jsx27, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
4001
|
+
var ESTIMATED_LINE = 28;
|
|
4002
|
+
function DefaultSlashMenu({
|
|
4003
|
+
items,
|
|
4004
|
+
activeIndex,
|
|
4005
|
+
position,
|
|
4006
|
+
onSelect,
|
|
4007
|
+
onHover
|
|
4008
|
+
}) {
|
|
4009
|
+
const t = useT();
|
|
4010
|
+
const view = useEditorView();
|
|
4011
|
+
const ref = useRef9(null);
|
|
4012
|
+
const [flip, setFlip] = useState8(false);
|
|
4013
|
+
const menuId = useId();
|
|
4014
|
+
const optionId = (index) => `${menuId}-option-${index}`;
|
|
4015
|
+
useLayoutEffect5(() => {
|
|
4016
|
+
const menu = ref.current;
|
|
4017
|
+
if (!menu) return;
|
|
4018
|
+
setFlip(
|
|
4019
|
+
position.y + menu.offsetHeight > window.innerHeight && position.y - menu.offsetHeight - ESTIMATED_LINE > 0
|
|
4020
|
+
);
|
|
4021
|
+
}, [position.x, position.y, items.length]);
|
|
4022
|
+
useLayoutEffect5(() => {
|
|
4023
|
+
ref.current?.querySelector('[aria-selected="true"]')?.scrollIntoView?.({ block: "nearest" });
|
|
4024
|
+
}, [activeIndex, items]);
|
|
4025
|
+
useLayoutEffect5(() => {
|
|
4026
|
+
const root = view.getRoot();
|
|
4027
|
+
if (!root) return;
|
|
4028
|
+
root.setAttribute("aria-controls", menuId);
|
|
4029
|
+
if (items.length > 0) root.setAttribute("aria-activedescendant", optionId(activeIndex));
|
|
4030
|
+
else root.removeAttribute("aria-activedescendant");
|
|
4031
|
+
return () => {
|
|
4032
|
+
root.removeAttribute("aria-controls");
|
|
4033
|
+
root.removeAttribute("aria-activedescendant");
|
|
4034
|
+
};
|
|
4035
|
+
}, [view, menuId, activeIndex, items.length]);
|
|
4036
|
+
return /* @__PURE__ */ jsxs12(
|
|
4037
|
+
"div",
|
|
4038
|
+
{
|
|
4039
|
+
ref,
|
|
4040
|
+
id: menuId,
|
|
4041
|
+
className: "inkbrick-slash-menu",
|
|
4042
|
+
"data-inkbrick-ui": "slash-menu",
|
|
4043
|
+
tabIndex: 0,
|
|
4044
|
+
role: "listbox",
|
|
4045
|
+
"aria-label": t("slash.label"),
|
|
4046
|
+
"data-testid": "slash-menu",
|
|
4047
|
+
style: {
|
|
4048
|
+
left: Math.max(
|
|
4049
|
+
8,
|
|
4050
|
+
Math.min(position.x, window.innerWidth - (ref.current?.offsetWidth ?? 320) - 8)
|
|
4051
|
+
),
|
|
4052
|
+
top: flip ? position.y - (ref.current?.offsetHeight ?? 0) - ESTIMATED_LINE : position.y + 4
|
|
4053
|
+
},
|
|
4054
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
4055
|
+
children: [
|
|
4056
|
+
items.length === 0 && /* @__PURE__ */ jsx27("div", { className: "inkbrick-slash-menu__description", "data-inkbrick-part": "empty", children: t("slash.empty") }),
|
|
4057
|
+
items.map((item, index) => /* @__PURE__ */ jsxs12(
|
|
4058
|
+
"div",
|
|
4059
|
+
{
|
|
4060
|
+
id: optionId(index),
|
|
4061
|
+
role: "option",
|
|
4062
|
+
"aria-selected": index === activeIndex,
|
|
4063
|
+
"data-slash-item": item.id,
|
|
4064
|
+
"data-inkbrick-part": "item",
|
|
4065
|
+
"data-active": index === activeIndex || void 0,
|
|
4066
|
+
className: `inkbrick-slash-menu__item${index === activeIndex ? " inkbrick-slash-menu__item--selected" : ""}`,
|
|
4067
|
+
onMouseEnter: () => onHover(index),
|
|
4068
|
+
onClick: () => onSelect(item),
|
|
4069
|
+
children: [
|
|
4070
|
+
item.icon !== void 0 && /* @__PURE__ */ jsx27("span", { className: "inkbrick-slash-menu__icon", "data-inkbrick-part": "icon", children: item.icon }),
|
|
4071
|
+
/* @__PURE__ */ jsxs12("div", { className: "inkbrick-slash-menu__body", "data-inkbrick-part": "body", children: [
|
|
4072
|
+
/* @__PURE__ */ jsx27("div", { className: "inkbrick-slash-menu__header", "data-inkbrick-part": "title", children: item.title }),
|
|
4073
|
+
item.description && /* @__PURE__ */ jsx27("div", { className: "inkbrick-slash-menu__description", "data-inkbrick-part": "description", children: item.description })
|
|
4074
|
+
] })
|
|
4075
|
+
]
|
|
4076
|
+
},
|
|
4077
|
+
item.id
|
|
4078
|
+
))
|
|
4079
|
+
]
|
|
4080
|
+
}
|
|
4081
|
+
);
|
|
4082
|
+
}
|
|
4083
|
+
|
|
4084
|
+
// src/slash/SlashMenuHost.tsx
|
|
4085
|
+
import { jsx as jsx28 } from "react/jsx-runtime";
|
|
4086
|
+
function SlashMenuHost() {
|
|
4087
|
+
const editor = useEditor();
|
|
4088
|
+
const view = useEditorView();
|
|
4089
|
+
const { slots, slashItems } = useEditorUI();
|
|
4090
|
+
const readOnly = useReadOnly();
|
|
4091
|
+
const state = useSyncExternalStore11(
|
|
4092
|
+
view.slash.subscribe,
|
|
4093
|
+
view.slash.getState,
|
|
4094
|
+
view.slash.getState
|
|
4095
|
+
);
|
|
4096
|
+
if (readOnly || !state.open || !state.blockId || !state.position || typeof document === "undefined") {
|
|
4097
|
+
return null;
|
|
4098
|
+
}
|
|
4099
|
+
const items = visibleSlashItems(editor, slashItems, state.blockId, state.query);
|
|
4100
|
+
const Menu = slots.SlashMenu ?? DefaultSlashMenu;
|
|
4101
|
+
return createPortal4(
|
|
4102
|
+
/* @__PURE__ */ jsx28(
|
|
4103
|
+
Menu,
|
|
4104
|
+
{
|
|
4105
|
+
items,
|
|
4106
|
+
query: state.query,
|
|
4107
|
+
activeIndex: Math.min(state.activeIndex, Math.max(items.length - 1, 0)),
|
|
4108
|
+
position: state.position,
|
|
4109
|
+
onSelect: (item) => applySlashItem(editor, view, item),
|
|
4110
|
+
onHover: (index) => view.slash.update({ activeIndex: index }),
|
|
4111
|
+
onClose: () => view.slash.close()
|
|
4112
|
+
}
|
|
4113
|
+
),
|
|
4114
|
+
document.body
|
|
4115
|
+
);
|
|
4116
|
+
}
|
|
4117
|
+
|
|
4118
|
+
// src/toolbar/ToolbarHost.tsx
|
|
4119
|
+
import { createPortal as createPortal5 } from "react-dom";
|
|
4120
|
+
import { useSyncExternalStore as useSyncExternalStore12 } from "react";
|
|
4121
|
+
|
|
4122
|
+
// src/toolbar/DefaultToolbar.tsx
|
|
4123
|
+
import { useEffect as useEffect14, useLayoutEffect as useLayoutEffect6, useRef as useRef10, useState as useState9 } from "react";
|
|
4124
|
+
import { ALIGNMENTS, COLOR_NAMES, isColorName as isColorName2 } from "@inkbrick/document";
|
|
4125
|
+
import { Fragment as Fragment5, jsx as jsx29, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
4126
|
+
var BUTTONS = [
|
|
4127
|
+
{ format: "bold", icon: "bold" },
|
|
4128
|
+
{ format: "italic", icon: "italic" },
|
|
4129
|
+
{ format: "underline", icon: "underline" },
|
|
4130
|
+
{ format: "strike", icon: "strike" },
|
|
4131
|
+
{ format: "code", icon: "code" }
|
|
4132
|
+
];
|
|
4133
|
+
var ALIGN_ICONS = {
|
|
4134
|
+
left: "align-left",
|
|
4135
|
+
center: "align-center",
|
|
4136
|
+
right: "align-right",
|
|
4137
|
+
justify: "align-justify"
|
|
4138
|
+
};
|
|
4139
|
+
var PANELS = [
|
|
4140
|
+
{ panel: "color", icon: "baseline" },
|
|
4141
|
+
{ panel: "highlight", icon: "highlighter" }
|
|
4142
|
+
];
|
|
4143
|
+
function DefaultToolbar({
|
|
4144
|
+
formats,
|
|
4145
|
+
position,
|
|
4146
|
+
linkOpen,
|
|
4147
|
+
panel,
|
|
4148
|
+
onToggle,
|
|
4149
|
+
onOpenLink,
|
|
4150
|
+
onApplyLink,
|
|
4151
|
+
onCancelLink,
|
|
4152
|
+
onTogglePanel,
|
|
4153
|
+
onApplyColor,
|
|
4154
|
+
onAlign,
|
|
4155
|
+
onInsertFormula
|
|
4156
|
+
}) {
|
|
4157
|
+
const t = useT();
|
|
4158
|
+
const input = useRef10(null);
|
|
4159
|
+
const ref = useRef10(null);
|
|
4160
|
+
const [value, setValue] = useState9("");
|
|
4161
|
+
const [invalid, setInvalid] = useState9(false);
|
|
4162
|
+
const [below, setBelow] = useState9(false);
|
|
4163
|
+
const [width, setWidth] = useState9(0);
|
|
4164
|
+
useEffect14(() => {
|
|
4165
|
+
if (!linkOpen) return;
|
|
4166
|
+
setValue(formats.link ?? "");
|
|
4167
|
+
setInvalid(false);
|
|
4168
|
+
input.current?.focus();
|
|
4169
|
+
input.current?.select();
|
|
4170
|
+
}, [linkOpen]);
|
|
4171
|
+
useLayoutEffect6(() => {
|
|
4172
|
+
const coarse = window.matchMedia?.("(pointer: coarse)").matches ?? false;
|
|
4173
|
+
setBelow(coarse || position.y - (ref.current?.offsetHeight ?? 40) - 12 < 0);
|
|
4174
|
+
setWidth(ref.current?.offsetWidth ?? 0);
|
|
4175
|
+
}, [position.x, position.y, linkOpen, panel]);
|
|
4176
|
+
const left = Math.max(8, Math.min(position.x - width / 2, window.innerWidth - width - 8));
|
|
4177
|
+
const submit = (event) => {
|
|
4178
|
+
event.preventDefault();
|
|
4179
|
+
if (!onApplyLink(value)) setInvalid(true);
|
|
4180
|
+
};
|
|
4181
|
+
const colorKind = panel === "highlight" ? "backgroundColor" : "color";
|
|
4182
|
+
const currentColor = panel === "highlight" ? formats.backgroundColor : formats.color;
|
|
4183
|
+
return /* @__PURE__ */ jsx29(
|
|
4184
|
+
"div",
|
|
4185
|
+
{
|
|
4186
|
+
ref,
|
|
4187
|
+
className: "inkbrick-toolbar",
|
|
4188
|
+
role: "toolbar",
|
|
4189
|
+
"aria-label": t("toolbar.label"),
|
|
4190
|
+
"data-inkbrick-ui": "toolbar",
|
|
4191
|
+
"data-placement": below ? "below" : "above",
|
|
4192
|
+
"data-testid": "toolbar",
|
|
4193
|
+
style: {
|
|
4194
|
+
left,
|
|
4195
|
+
top: position.y,
|
|
4196
|
+
transform: below ? "translateY(28px)" : "translateY(calc(-100% - 8px))"
|
|
4197
|
+
},
|
|
4198
|
+
onMouseDown: (event) => {
|
|
4199
|
+
if (event.target !== input.current) event.preventDefault();
|
|
4200
|
+
},
|
|
4201
|
+
children: linkOpen ? /* @__PURE__ */ jsxs13("form", { className: "inkbrick-toolbar__link", "data-inkbrick-part": "link-form", onSubmit: submit, children: [
|
|
4202
|
+
/* @__PURE__ */ jsx29(
|
|
4203
|
+
"input",
|
|
4204
|
+
{
|
|
4205
|
+
ref: input,
|
|
4206
|
+
"data-inkbrick-part": "input",
|
|
4207
|
+
"data-invalid": invalid || void 0,
|
|
4208
|
+
type: "text",
|
|
4209
|
+
"aria-label": t("toolbar.link.url"),
|
|
4210
|
+
"aria-invalid": invalid,
|
|
4211
|
+
placeholder: t("toolbar.link.placeholder"),
|
|
4212
|
+
value,
|
|
4213
|
+
onChange: (event) => {
|
|
4214
|
+
setValue(event.target.value);
|
|
4215
|
+
setInvalid(false);
|
|
4216
|
+
},
|
|
4217
|
+
onKeyDown: (event) => {
|
|
4218
|
+
if (event.key === "Escape") {
|
|
4219
|
+
event.preventDefault();
|
|
4220
|
+
onCancelLink();
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
4224
|
+
),
|
|
4225
|
+
/* @__PURE__ */ jsx29(
|
|
4226
|
+
"button",
|
|
4227
|
+
{
|
|
4228
|
+
type: "submit",
|
|
4229
|
+
className: "inkbrick-toolbar__button",
|
|
4230
|
+
"data-inkbrick-part": "button",
|
|
4231
|
+
"aria-label": t("toolbar.link.apply"),
|
|
4232
|
+
title: t("toolbar.link.apply"),
|
|
4233
|
+
children: /* @__PURE__ */ jsx29(Icon, { name: "check" })
|
|
4234
|
+
}
|
|
4235
|
+
),
|
|
4236
|
+
formats.link && /* @__PURE__ */ jsx29(
|
|
4237
|
+
"button",
|
|
4238
|
+
{
|
|
4239
|
+
type: "button",
|
|
4240
|
+
className: "inkbrick-toolbar__button",
|
|
4241
|
+
"data-inkbrick-part": "button",
|
|
4242
|
+
"aria-label": t("toolbar.link.remove"),
|
|
4243
|
+
title: t("toolbar.link.remove"),
|
|
4244
|
+
onClick: () => onApplyLink(null),
|
|
4245
|
+
children: /* @__PURE__ */ jsx29(Icon, { name: "unlink" })
|
|
4246
|
+
}
|
|
4247
|
+
)
|
|
4248
|
+
] }) : /* @__PURE__ */ jsxs13(Fragment5, { children: [
|
|
4249
|
+
BUTTONS.map(({ format, icon }) => /* @__PURE__ */ jsx29(
|
|
4250
|
+
"button",
|
|
4251
|
+
{
|
|
4252
|
+
type: "button",
|
|
4253
|
+
className: "inkbrick-toolbar__button",
|
|
4254
|
+
"data-inkbrick-part": "button",
|
|
4255
|
+
"data-format": format,
|
|
4256
|
+
"data-pressed": formats[format] || void 0,
|
|
4257
|
+
"aria-label": t(`toolbar.${format}.label`),
|
|
4258
|
+
"aria-pressed": formats[format],
|
|
4259
|
+
title: t(`toolbar.${format}.title`),
|
|
4260
|
+
onClick: () => onToggle(format),
|
|
4261
|
+
children: /* @__PURE__ */ jsx29(Icon, { name: icon })
|
|
4262
|
+
},
|
|
4263
|
+
format
|
|
4264
|
+
)),
|
|
4265
|
+
/* @__PURE__ */ jsx29(
|
|
4266
|
+
"span",
|
|
4267
|
+
{
|
|
4268
|
+
className: "inkbrick-toolbar__separator",
|
|
4269
|
+
"data-inkbrick-part": "separator",
|
|
4270
|
+
role: "separator"
|
|
4271
|
+
}
|
|
4272
|
+
),
|
|
4273
|
+
/* @__PURE__ */ jsx29(
|
|
4274
|
+
"button",
|
|
4275
|
+
{
|
|
4276
|
+
type: "button",
|
|
4277
|
+
className: "inkbrick-toolbar__button",
|
|
4278
|
+
"data-inkbrick-part": "button",
|
|
4279
|
+
"data-format": "link",
|
|
4280
|
+
"data-pressed": formats.link !== void 0 || void 0,
|
|
4281
|
+
"aria-label": t("toolbar.link.label"),
|
|
4282
|
+
"aria-pressed": formats.link !== void 0,
|
|
4283
|
+
title: t("toolbar.link.title"),
|
|
4284
|
+
onClick: onOpenLink,
|
|
4285
|
+
children: /* @__PURE__ */ jsx29(Icon, { name: "link" })
|
|
4286
|
+
}
|
|
4287
|
+
),
|
|
4288
|
+
/* @__PURE__ */ jsx29(
|
|
4289
|
+
"button",
|
|
4290
|
+
{
|
|
4291
|
+
type: "button",
|
|
4292
|
+
className: "inkbrick-toolbar__button",
|
|
4293
|
+
"data-inkbrick-part": "button",
|
|
4294
|
+
"data-format": "formula",
|
|
4295
|
+
"aria-label": t("toolbar.formula.label"),
|
|
4296
|
+
title: t("toolbar.formula.title"),
|
|
4297
|
+
onClick: onInsertFormula,
|
|
4298
|
+
children: /* @__PURE__ */ jsx29(Icon, { name: "sigma" })
|
|
4299
|
+
}
|
|
4300
|
+
),
|
|
4301
|
+
/* @__PURE__ */ jsx29(
|
|
4302
|
+
"span",
|
|
4303
|
+
{
|
|
4304
|
+
className: "inkbrick-toolbar__separator",
|
|
4305
|
+
"data-inkbrick-part": "separator",
|
|
4306
|
+
role: "separator"
|
|
4307
|
+
}
|
|
4308
|
+
),
|
|
4309
|
+
PANELS.map(({ panel: name, icon }) => {
|
|
4310
|
+
const current = name === "color" ? formats.color : formats.backgroundColor;
|
|
4311
|
+
return /* @__PURE__ */ jsxs13(
|
|
4312
|
+
"button",
|
|
4313
|
+
{
|
|
4314
|
+
type: "button",
|
|
4315
|
+
className: "inkbrick-toolbar__button",
|
|
4316
|
+
"data-inkbrick-part": "button",
|
|
4317
|
+
"data-format": name,
|
|
4318
|
+
"data-pressed": panel === name || void 0,
|
|
4319
|
+
"data-current-color": isColorName2(current) ? current : void 0,
|
|
4320
|
+
"aria-label": t(`toolbar.${name}.label`),
|
|
4321
|
+
"aria-haspopup": "true",
|
|
4322
|
+
"aria-expanded": panel === name,
|
|
4323
|
+
title: t(`toolbar.${name}.title`),
|
|
4324
|
+
onClick: () => onTogglePanel(name),
|
|
4325
|
+
children: [
|
|
4326
|
+
/* @__PURE__ */ jsx29(Icon, { name: icon }),
|
|
4327
|
+
/* @__PURE__ */ jsx29("span", { className: "inkbrick-toolbar__indicator", "data-inkbrick-part": "indicator" })
|
|
4328
|
+
]
|
|
4329
|
+
},
|
|
4330
|
+
name
|
|
4331
|
+
);
|
|
4332
|
+
}),
|
|
4333
|
+
/* @__PURE__ */ jsx29(
|
|
4334
|
+
"span",
|
|
4335
|
+
{
|
|
4336
|
+
className: "inkbrick-toolbar__separator",
|
|
4337
|
+
"data-inkbrick-part": "separator",
|
|
4338
|
+
role: "separator"
|
|
4339
|
+
}
|
|
4340
|
+
),
|
|
4341
|
+
ALIGNMENTS.map((align) => /* @__PURE__ */ jsx29(
|
|
4342
|
+
"button",
|
|
4343
|
+
{
|
|
4344
|
+
type: "button",
|
|
4345
|
+
className: "inkbrick-toolbar__button",
|
|
4346
|
+
"data-inkbrick-part": "button",
|
|
4347
|
+
"data-format": `align-${align}`,
|
|
4348
|
+
"data-pressed": formats.align === align || void 0,
|
|
4349
|
+
"aria-label": t(`toolbar.align.${align}.label`),
|
|
4350
|
+
"aria-pressed": formats.align === align,
|
|
4351
|
+
title: t(`toolbar.align.${align}.title`),
|
|
4352
|
+
onClick: () => onAlign(align),
|
|
4353
|
+
children: /* @__PURE__ */ jsx29(Icon, { name: ALIGN_ICONS[align] })
|
|
4354
|
+
},
|
|
4355
|
+
align
|
|
4356
|
+
)),
|
|
4357
|
+
panel && /* @__PURE__ */ jsxs13(
|
|
4358
|
+
"div",
|
|
4359
|
+
{
|
|
4360
|
+
className: "inkbrick-toolbar__panel",
|
|
4361
|
+
"data-inkbrick-part": "panel",
|
|
4362
|
+
"data-kind": panel,
|
|
4363
|
+
role: "group",
|
|
4364
|
+
"aria-label": t(`toolbar.${panel}.label`),
|
|
4365
|
+
children: [
|
|
4366
|
+
/* @__PURE__ */ jsx29(
|
|
4367
|
+
"button",
|
|
4368
|
+
{
|
|
4369
|
+
type: "button",
|
|
4370
|
+
"data-inkbrick-part": "swatch",
|
|
4371
|
+
"data-color": "",
|
|
4372
|
+
"data-pressed": currentColor === void 0 || void 0,
|
|
4373
|
+
"aria-label": t(
|
|
4374
|
+
panel === "color" ? "toolbar.color.default" : "toolbar.highlight.none"
|
|
4375
|
+
),
|
|
4376
|
+
title: t(panel === "color" ? "toolbar.color.default" : "toolbar.highlight.none"),
|
|
4377
|
+
onClick: () => onApplyColor(colorKind, null),
|
|
4378
|
+
children: /* @__PURE__ */ jsx29(Icon, { name: "x" })
|
|
4379
|
+
}
|
|
4380
|
+
),
|
|
4381
|
+
COLOR_NAMES.map((name) => /* @__PURE__ */ jsx29(
|
|
4382
|
+
"button",
|
|
4383
|
+
{
|
|
4384
|
+
type: "button",
|
|
4385
|
+
"data-inkbrick-part": "swatch",
|
|
4386
|
+
"data-color": name,
|
|
4387
|
+
"data-pressed": currentColor === name || void 0,
|
|
4388
|
+
"aria-label": t(`color.${name}`),
|
|
4389
|
+
title: t(`color.${name}`),
|
|
4390
|
+
onClick: () => onApplyColor(colorKind, name),
|
|
4391
|
+
children: panel === "color" ? "A" : null
|
|
4392
|
+
},
|
|
4393
|
+
name
|
|
4394
|
+
))
|
|
4395
|
+
]
|
|
4396
|
+
}
|
|
4397
|
+
)
|
|
4398
|
+
] })
|
|
4399
|
+
}
|
|
4400
|
+
);
|
|
4401
|
+
}
|
|
4402
|
+
|
|
4403
|
+
// src/toolbar/ToolbarHost.tsx
|
|
4404
|
+
import { jsx as jsx30 } from "react/jsx-runtime";
|
|
4405
|
+
function ToolbarHost() {
|
|
4406
|
+
const editor = useEditor();
|
|
4407
|
+
const view = useEditorView();
|
|
4408
|
+
const { slots } = useEditorUI();
|
|
4409
|
+
const readOnly = useReadOnly();
|
|
4410
|
+
const state = useSyncExternalStore12(
|
|
4411
|
+
view.toolbar.subscribe,
|
|
4412
|
+
view.toolbar.getState,
|
|
4413
|
+
view.toolbar.getState
|
|
4414
|
+
);
|
|
4415
|
+
if (slots.Toolbar === null || readOnly) return null;
|
|
4416
|
+
if (!state.visible || !state.position || !state.formats || !state.range || typeof document === "undefined") {
|
|
4417
|
+
return null;
|
|
4418
|
+
}
|
|
4419
|
+
const Toolbar = slots.Toolbar ?? DefaultToolbar;
|
|
4420
|
+
const range = state.range;
|
|
4421
|
+
return createPortal5(
|
|
4422
|
+
/* @__PURE__ */ jsx30(
|
|
4423
|
+
Toolbar,
|
|
4424
|
+
{
|
|
4425
|
+
formats: state.formats,
|
|
4426
|
+
position: state.position,
|
|
4427
|
+
linkOpen: state.linkOpen,
|
|
4428
|
+
panel: state.panel,
|
|
4429
|
+
onToggle: (format) => {
|
|
4430
|
+
toggleFormatInSelection(editor, view, range, format);
|
|
4431
|
+
syncToolbar(editor, view);
|
|
4432
|
+
},
|
|
4433
|
+
onOpenLink: () => view.toolbar.openLink(),
|
|
4434
|
+
onApplyLink: (url) => {
|
|
4435
|
+
if (!setLinkInSelection(editor, view, range, url)) return false;
|
|
4436
|
+
view.toolbar.closeLink();
|
|
4437
|
+
syncToolbar(editor, view);
|
|
4438
|
+
return true;
|
|
4439
|
+
},
|
|
4440
|
+
onTogglePanel: (panel) => view.toolbar.togglePanel(panel),
|
|
4441
|
+
onApplyColor: (kind, value) => {
|
|
4442
|
+
setColorInSelection(editor, view, range, kind, value);
|
|
4443
|
+
view.toolbar.closePanel();
|
|
4444
|
+
syncToolbar(editor, view);
|
|
4445
|
+
},
|
|
4446
|
+
onAlign: (align) => {
|
|
4447
|
+
setAlignInSelection(editor, view, range, align);
|
|
4448
|
+
syncToolbar(editor, view);
|
|
4449
|
+
},
|
|
4450
|
+
onInsertFormula: () => {
|
|
4451
|
+
view.placeSelection(range.from, range.to);
|
|
4452
|
+
openInlineFormula(editor, view);
|
|
4453
|
+
},
|
|
4454
|
+
onCancelLink: () => {
|
|
4455
|
+
view.toolbar.closeLink();
|
|
4456
|
+
view.placeSelection(range.from, range.to);
|
|
4457
|
+
syncToolbar(editor, view);
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
),
|
|
4461
|
+
document.body
|
|
4462
|
+
);
|
|
4463
|
+
}
|
|
4464
|
+
|
|
4465
|
+
// src/renderer/virtual/estimate.ts
|
|
4466
|
+
var CHARS_PER_LINE = 80;
|
|
4467
|
+
function textLength(block2) {
|
|
4468
|
+
let length = 0;
|
|
4469
|
+
for (const node of block2.content) length += node.insert.length;
|
|
4470
|
+
return length;
|
|
4471
|
+
}
|
|
4472
|
+
var estimateBlockHeight = (block2) => {
|
|
4473
|
+
const lines = Math.max(1, Math.ceil(textLength(block2) / CHARS_PER_LINE));
|
|
4474
|
+
switch (block2.flavour) {
|
|
4475
|
+
case "block:heading":
|
|
4476
|
+
return 20 + 34 * lines;
|
|
4477
|
+
case "block:code": {
|
|
4478
|
+
let breaks = 0;
|
|
4479
|
+
for (const node of block2.content)
|
|
4480
|
+
for (const char of node.insert) if (char === "\n") breaks += 1;
|
|
4481
|
+
return 40 + 21 * (breaks + 1);
|
|
4482
|
+
}
|
|
4483
|
+
case "block:image":
|
|
4484
|
+
return 260;
|
|
4485
|
+
case "block:divider":
|
|
4486
|
+
return 25;
|
|
4487
|
+
case "block:formula":
|
|
4488
|
+
return 64;
|
|
4489
|
+
case "block:callout":
|
|
4490
|
+
return 24 + 26 * lines;
|
|
4491
|
+
default:
|
|
4492
|
+
return 8 + 26 * lines;
|
|
4493
|
+
}
|
|
4494
|
+
};
|
|
4495
|
+
function estimateSubtree(getBlock, id, estimate) {
|
|
4496
|
+
let total = 0;
|
|
4497
|
+
const queue = [id];
|
|
4498
|
+
for (let i = 0; i < queue.length; i++) {
|
|
4499
|
+
const block2 = getBlock(queue[i]);
|
|
4500
|
+
if (!block2) continue;
|
|
4501
|
+
total += estimate(block2);
|
|
4502
|
+
for (const child of block2.children) queue.push(child);
|
|
4503
|
+
}
|
|
4504
|
+
return total;
|
|
4505
|
+
}
|
|
4506
|
+
|
|
4507
|
+
// src/renderer/virtual/VirtualChildList.tsx
|
|
4508
|
+
import {
|
|
4509
|
+
useEffect as useEffect15,
|
|
4510
|
+
useLayoutEffect as useLayoutEffect7,
|
|
4511
|
+
useMemo as useMemo3,
|
|
4512
|
+
useReducer,
|
|
4513
|
+
useRef as useRef11,
|
|
4514
|
+
useState as useState10,
|
|
4515
|
+
useSyncExternalStore as useSyncExternalStore13
|
|
4516
|
+
} from "react";
|
|
4517
|
+
import { Fragment as Fragment6, jsx as jsx31, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
4518
|
+
var SPACER = "data-inkbrick-spacer";
|
|
4519
|
+
function upperBound(offsets, value) {
|
|
4520
|
+
let lo = 0;
|
|
4521
|
+
let hi = offsets.length;
|
|
4522
|
+
while (lo < hi) {
|
|
4523
|
+
const mid = lo + hi >>> 1;
|
|
4524
|
+
if (offsets[mid] <= value) lo = mid + 1;
|
|
4525
|
+
else hi = mid;
|
|
4526
|
+
}
|
|
4527
|
+
return lo;
|
|
4528
|
+
}
|
|
4529
|
+
function scrollParents(element) {
|
|
4530
|
+
const result = [];
|
|
4531
|
+
for (let node = element.parentElement; node && node !== document.body && node !== document.documentElement; node = node.parentElement) {
|
|
4532
|
+
if (/(auto|scroll|hidden|clip)/.test(getComputedStyle(node).overflowY)) result.push(node);
|
|
4533
|
+
}
|
|
4534
|
+
return result;
|
|
4535
|
+
}
|
|
4536
|
+
function visibleRange(marker, parents) {
|
|
4537
|
+
let top = 0;
|
|
4538
|
+
let bottom = window.innerHeight;
|
|
4539
|
+
for (const parent of parents) {
|
|
4540
|
+
const rect = parent.getBoundingClientRect();
|
|
4541
|
+
top = Math.max(top, rect.top);
|
|
4542
|
+
bottom = Math.min(bottom, rect.bottom);
|
|
4543
|
+
}
|
|
4544
|
+
const origin = marker.getBoundingClientRect().top;
|
|
4545
|
+
return { top: top - origin, bottom: Math.max(top, bottom) - origin };
|
|
4546
|
+
}
|
|
4547
|
+
function indexOfItem(editor, parentId, ids, id) {
|
|
4548
|
+
let block2 = editor.store.getBlock(id);
|
|
4549
|
+
while (block2 && block2.parentId !== parentId) {
|
|
4550
|
+
block2 = block2.parentId ? editor.store.getBlock(block2.parentId) : void 0;
|
|
4551
|
+
}
|
|
4552
|
+
return block2 ? ids.indexOf(block2.id) : -1;
|
|
4553
|
+
}
|
|
4554
|
+
function Spacer({ height }) {
|
|
4555
|
+
return /* @__PURE__ */ jsx31(
|
|
4556
|
+
"div",
|
|
4557
|
+
{
|
|
4558
|
+
className: "inkbrick-spacer",
|
|
4559
|
+
"data-inkbrick-spacer": "",
|
|
4560
|
+
"aria-hidden": "true",
|
|
4561
|
+
contentEditable: false,
|
|
4562
|
+
style: { height }
|
|
4563
|
+
}
|
|
4564
|
+
);
|
|
4565
|
+
}
|
|
4566
|
+
function VirtualChildList({
|
|
4567
|
+
parentId,
|
|
4568
|
+
ids,
|
|
4569
|
+
options
|
|
4570
|
+
}) {
|
|
4571
|
+
const editor = useEditor();
|
|
4572
|
+
const view = useEditorView();
|
|
4573
|
+
const windowed = ids.length >= options.threshold;
|
|
4574
|
+
const marker = useRef11(null);
|
|
4575
|
+
const parents = useRef11([]);
|
|
4576
|
+
const [heights] = useState10(() => /* @__PURE__ */ new Map());
|
|
4577
|
+
const [estimates] = useState10(() => /* @__PURE__ */ new Map());
|
|
4578
|
+
const [measured, remeasure] = useReducer((n) => n + 1, 0);
|
|
4579
|
+
const [range, setRange] = useState10(() => ({
|
|
4580
|
+
top: 0,
|
|
4581
|
+
bottom: typeof window === "undefined" ? 1e3 : window.innerHeight
|
|
4582
|
+
}));
|
|
4583
|
+
const pins = useSyncExternalStore13(
|
|
4584
|
+
view.viewport.subscribe,
|
|
4585
|
+
view.viewport.getVersion,
|
|
4586
|
+
view.viewport.getVersion
|
|
4587
|
+
);
|
|
4588
|
+
const dragId = useSyncExternalStore13(
|
|
4589
|
+
view.blocks.subscribe,
|
|
4590
|
+
() => view.blocks.getState().drag?.blockId ?? null,
|
|
4591
|
+
() => null
|
|
4592
|
+
);
|
|
4593
|
+
const dropId = useSyncExternalStore13(
|
|
4594
|
+
view.blocks.subscribe,
|
|
4595
|
+
() => view.blocks.getState().drag?.targetId ?? null,
|
|
4596
|
+
() => null
|
|
4597
|
+
);
|
|
4598
|
+
useEffect15(() => windowed ? view.viewport.attach() : void 0, [view, windowed]);
|
|
4599
|
+
const offsets = useMemo3(() => {
|
|
4600
|
+
if (!windowed) return null;
|
|
4601
|
+
const result = new Float64Array(ids.length + 1);
|
|
4602
|
+
const getBlock = (id) => editor.store.getBlock(id);
|
|
4603
|
+
for (let i = 0; i < ids.length; i++) {
|
|
4604
|
+
const id = ids[i];
|
|
4605
|
+
let height = heights.get(id) ?? estimates.get(id);
|
|
4606
|
+
if (height === void 0) {
|
|
4607
|
+
height = estimateSubtree(getBlock, id, options.estimateHeight);
|
|
4608
|
+
estimates.set(id, height);
|
|
4609
|
+
}
|
|
4610
|
+
result[i + 1] = result[i] + height;
|
|
4611
|
+
}
|
|
4612
|
+
return result;
|
|
4613
|
+
}, [ids, windowed, measured, heights, estimates, editor, options.estimateHeight]);
|
|
4614
|
+
const rendered = useMemo3(() => {
|
|
4615
|
+
if (!offsets) return null;
|
|
4616
|
+
const last = ids.length - 1;
|
|
4617
|
+
const from = Math.max(0, upperBound(offsets, range.top - options.overscan) - 1);
|
|
4618
|
+
const to = Math.min(
|
|
4619
|
+
last,
|
|
4620
|
+
upperBound(offsets, range.bottom + options.overscan) - 1,
|
|
4621
|
+
from + options.maxRendered - 1
|
|
4622
|
+
);
|
|
4623
|
+
const pinned = /* @__PURE__ */ new Set([0, last]);
|
|
4624
|
+
for (const id of [...view.viewport.getPinned(), dragId, dropId]) {
|
|
4625
|
+
if (id) pinned.add(indexOfItem(editor, parentId, ids, id));
|
|
4626
|
+
}
|
|
4627
|
+
pinned.delete(-1);
|
|
4628
|
+
const result = [];
|
|
4629
|
+
for (const i of [...pinned].sort((a, b) => a - b)) if (i < from) result.push(i);
|
|
4630
|
+
for (let i = from; i <= to; i++) result.push(i);
|
|
4631
|
+
for (const i of [...pinned].sort((a, b) => a - b)) if (i > to) result.push(i);
|
|
4632
|
+
return result;
|
|
4633
|
+
}, [offsets, ids, range, options, view, editor, parentId, dragId, dropId, pins]);
|
|
4634
|
+
const observer = useRef11(null);
|
|
4635
|
+
const observed = useRef11(/* @__PURE__ */ new Map());
|
|
4636
|
+
useLayoutEffect7(() => {
|
|
4637
|
+
if (!windowed || !rendered || !marker.current) return;
|
|
4638
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4639
|
+
const expected = new Set(rendered.map((i) => ids[i]));
|
|
4640
|
+
let changed = false;
|
|
4641
|
+
let previous = null;
|
|
4642
|
+
let remaining = rendered.length;
|
|
4643
|
+
const settle = (height) => {
|
|
4644
|
+
if (!previous) return;
|
|
4645
|
+
if (height > 0 && Math.abs((heights.get(previous.id) ?? -1) - height) > 0.5) {
|
|
4646
|
+
heights.set(previous.id, height);
|
|
4647
|
+
changed = true;
|
|
4648
|
+
}
|
|
4649
|
+
previous = null;
|
|
4650
|
+
};
|
|
4651
|
+
for (let element = marker.current.nextElementSibling; element && remaining > 0; element = element.nextElementSibling) {
|
|
4652
|
+
const rect = element.getBoundingClientRect();
|
|
4653
|
+
if (previous) settle(rect.top - previous.top);
|
|
4654
|
+
if (element.hasAttribute(SPACER)) continue;
|
|
4655
|
+
remaining -= 1;
|
|
4656
|
+
const id = element.getAttribute("data-block-id");
|
|
4657
|
+
if (id && expected.has(id)) {
|
|
4658
|
+
previous = { id, top: rect.top, height: rect.height };
|
|
4659
|
+
seen.add(element);
|
|
4660
|
+
}
|
|
4661
|
+
}
|
|
4662
|
+
if (previous) settle(previous.height);
|
|
4663
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
4664
|
+
const sizes = observed.current;
|
|
4665
|
+
observer.current ??= new ResizeObserver((entries) => {
|
|
4666
|
+
let resized = false;
|
|
4667
|
+
for (const entry of entries) {
|
|
4668
|
+
const size = entry.contentRect.height;
|
|
4669
|
+
const known = sizes.get(entry.target);
|
|
4670
|
+
if (known !== void 0 && Math.abs(known - size) > 0.5) resized = true;
|
|
4671
|
+
sizes.set(entry.target, size);
|
|
4672
|
+
}
|
|
4673
|
+
if (resized) remeasure();
|
|
4674
|
+
});
|
|
4675
|
+
for (const element of [...sizes.keys()]) {
|
|
4676
|
+
if (!seen.has(element)) {
|
|
4677
|
+
observer.current.unobserve(element);
|
|
4678
|
+
sizes.delete(element);
|
|
4679
|
+
}
|
|
4680
|
+
}
|
|
4681
|
+
for (const element of seen) {
|
|
4682
|
+
if (!sizes.has(element)) observer.current.observe(element);
|
|
4683
|
+
}
|
|
4684
|
+
}
|
|
4685
|
+
const next2 = visibleRange(marker.current, parents.current);
|
|
4686
|
+
const step = options.overscan / 4;
|
|
4687
|
+
setRange(
|
|
4688
|
+
(current) => Math.abs(current.top - next2.top) < step && Math.abs(current.bottom - next2.bottom) < step ? current : next2
|
|
4689
|
+
);
|
|
4690
|
+
if (changed) remeasure();
|
|
4691
|
+
});
|
|
4692
|
+
useEffect15(() => () => observer.current?.disconnect(), []);
|
|
4693
|
+
useEffect15(() => {
|
|
4694
|
+
if (!windowed || !marker.current) return;
|
|
4695
|
+
parents.current = scrollParents(marker.current);
|
|
4696
|
+
let frame = 0;
|
|
4697
|
+
const step = options.overscan / 4;
|
|
4698
|
+
const onScroll = () => {
|
|
4699
|
+
if (frame) return;
|
|
4700
|
+
frame = requestAnimationFrame(() => {
|
|
4701
|
+
frame = 0;
|
|
4702
|
+
if (!marker.current) return;
|
|
4703
|
+
const next2 = visibleRange(marker.current, parents.current);
|
|
4704
|
+
setRange(
|
|
4705
|
+
(current) => Math.abs(current.top - next2.top) < step && Math.abs(current.bottom - next2.bottom) < step ? current : next2
|
|
4706
|
+
);
|
|
4707
|
+
});
|
|
4708
|
+
};
|
|
4709
|
+
window.addEventListener("scroll", onScroll, { capture: true, passive: true });
|
|
4710
|
+
window.addEventListener("resize", onScroll);
|
|
4711
|
+
return () => {
|
|
4712
|
+
cancelAnimationFrame(frame);
|
|
4713
|
+
window.removeEventListener("scroll", onScroll, { capture: true });
|
|
4714
|
+
window.removeEventListener("resize", onScroll);
|
|
4715
|
+
};
|
|
4716
|
+
}, [windowed, options.overscan]);
|
|
4717
|
+
if (!rendered || !offsets) {
|
|
4718
|
+
return /* @__PURE__ */ jsxs14(Fragment6, { children: [
|
|
4719
|
+
null,
|
|
4720
|
+
ids.map((id) => /* @__PURE__ */ jsx31(BlockRenderer, { id }, id))
|
|
4721
|
+
] });
|
|
4722
|
+
}
|
|
4723
|
+
const items = [];
|
|
4724
|
+
let next = 0;
|
|
4725
|
+
for (const i of rendered) {
|
|
4726
|
+
if (i > next) items.push(/* @__PURE__ */ jsx31(Spacer, { height: offsets[i] - offsets[next] }, `gap:${next}`));
|
|
4727
|
+
items.push(/* @__PURE__ */ jsx31(BlockRenderer, { id: ids[i] }, ids[i]));
|
|
4728
|
+
next = i + 1;
|
|
4729
|
+
}
|
|
4730
|
+
return /* @__PURE__ */ jsxs14(Fragment6, { children: [
|
|
4731
|
+
/* @__PURE__ */ jsx31(
|
|
4732
|
+
"div",
|
|
4733
|
+
{
|
|
4734
|
+
ref: marker,
|
|
4735
|
+
className: "inkbrick-spacer",
|
|
4736
|
+
"data-inkbrick-spacer": "start",
|
|
4737
|
+
"aria-hidden": "true",
|
|
4738
|
+
contentEditable: false
|
|
4739
|
+
}
|
|
4740
|
+
),
|
|
4741
|
+
items
|
|
4742
|
+
] });
|
|
4743
|
+
}
|
|
4744
|
+
|
|
4745
|
+
// src/renderer/virtual/virtualRenderer.tsx
|
|
4746
|
+
import { jsx as jsx32 } from "react/jsx-runtime";
|
|
4747
|
+
function virtualRenderer(options = {}) {
|
|
4748
|
+
const resolved = {
|
|
4749
|
+
threshold: Math.max(1, options.threshold ?? 100),
|
|
4750
|
+
overscan: Math.max(0, options.overscan ?? 1200),
|
|
4751
|
+
maxRendered: Math.max(1, options.maxRendered ?? 400),
|
|
4752
|
+
estimateHeight: options.estimateHeight ?? estimateBlockHeight
|
|
4753
|
+
};
|
|
4754
|
+
function VirtualChildren(props) {
|
|
4755
|
+
return /* @__PURE__ */ jsx32(VirtualChildList, { ...props, options: resolved });
|
|
4756
|
+
}
|
|
4757
|
+
return { ChildList: VirtualChildren };
|
|
4758
|
+
}
|
|
4759
|
+
var defaultVirtualRenderer = virtualRenderer();
|
|
4760
|
+
|
|
4761
|
+
// src/renderer/DocumentView.tsx
|
|
4762
|
+
import { Fragment as Fragment7, jsx as jsx33, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
4763
|
+
function resolveRenderer(renderer) {
|
|
4764
|
+
if (renderer === "virtual") return defaultVirtualRenderer;
|
|
4765
|
+
if (!renderer || renderer === "full") return fullRenderer;
|
|
4766
|
+
return renderer;
|
|
4767
|
+
}
|
|
4768
|
+
function DocumentView({ label, renderer } = {}) {
|
|
4769
|
+
const editor = useEditor();
|
|
4770
|
+
const view = useEditorView();
|
|
4771
|
+
const ref = useRef12(null);
|
|
4772
|
+
const { slots } = useEditorUI();
|
|
4773
|
+
const readOnly = useReadOnly();
|
|
4774
|
+
const t = useT();
|
|
4775
|
+
useEditorInput(ref, !readOnly);
|
|
4776
|
+
useSelectionState(ref);
|
|
4777
|
+
useClipboard(ref, readOnly);
|
|
4778
|
+
useBlockHandles(ref, slots.BlockHandle !== null && !readOnly);
|
|
4779
|
+
useLayoutEffect8(() => view.setRoot(ref.current), [view]);
|
|
4780
|
+
useEffect16(() => {
|
|
4781
|
+
if (!readOnly) void preloadFormulaPalette();
|
|
4782
|
+
}, [readOnly]);
|
|
4783
|
+
return /* @__PURE__ */ jsxs15(Fragment7, { children: [
|
|
4784
|
+
/* @__PURE__ */ jsx33(
|
|
4785
|
+
"div",
|
|
4786
|
+
{
|
|
4787
|
+
ref,
|
|
4788
|
+
className: "inkbrick-editor",
|
|
4789
|
+
"data-inkbrick-root": "",
|
|
4790
|
+
contentEditable: !readOnly,
|
|
4791
|
+
suppressContentEditableWarning: true,
|
|
4792
|
+
spellCheck: false,
|
|
4793
|
+
role: "textbox",
|
|
4794
|
+
"aria-multiline": "true",
|
|
4795
|
+
"aria-label": label ?? t("editor.label"),
|
|
4796
|
+
tabIndex: readOnly ? 0 : void 0,
|
|
4797
|
+
"aria-readonly": readOnly || void 0,
|
|
4798
|
+
"data-readonly": readOnly || void 0,
|
|
4799
|
+
children: /* @__PURE__ */ jsx33(DocumentRendererContext.Provider, { value: resolveRenderer(renderer), children: /* @__PURE__ */ jsx33(BlockRenderer, { id: editor.document.rootId }) })
|
|
4800
|
+
}
|
|
4801
|
+
),
|
|
4802
|
+
/* @__PURE__ */ jsx33(SlashMenuHost, {}),
|
|
4803
|
+
/* @__PURE__ */ jsx33(ToolbarHost, {}),
|
|
4804
|
+
/* @__PURE__ */ jsx33(FormulaPopover, {}),
|
|
4805
|
+
/* @__PURE__ */ jsx33(BlockHandleHost, {}),
|
|
4806
|
+
/* @__PURE__ */ jsx33(DropIndicatorHost, {})
|
|
4807
|
+
] });
|
|
4808
|
+
}
|
|
4809
|
+
export {
|
|
4810
|
+
BlockChildren,
|
|
4811
|
+
BlockRenderer,
|
|
4812
|
+
CODE_LANGUAGES,
|
|
4813
|
+
CalloutBlock,
|
|
4814
|
+
CodeBlock,
|
|
4815
|
+
DefaultBlockHandle,
|
|
4816
|
+
DefaultBlockMenu,
|
|
4817
|
+
DefaultDropIndicator,
|
|
4818
|
+
DefaultSlashMenu,
|
|
4819
|
+
DefaultToolbar,
|
|
4820
|
+
DividerBlock,
|
|
4821
|
+
DocumentView,
|
|
4822
|
+
Editable,
|
|
4823
|
+
EditorContext,
|
|
4824
|
+
EditorProvider,
|
|
4825
|
+
EditorView,
|
|
4826
|
+
EditorViewContext,
|
|
4827
|
+
FormulaView,
|
|
4828
|
+
HIGHLIGHT_LIMIT,
|
|
4829
|
+
HeadingBlock,
|
|
4830
|
+
Icon,
|
|
4831
|
+
ImageBlock,
|
|
4832
|
+
ListBlock,
|
|
4833
|
+
NoteBlock,
|
|
4834
|
+
PageBlock,
|
|
4835
|
+
ParagraphBlock,
|
|
4836
|
+
QuoteBlock,
|
|
4837
|
+
UnknownBlockView,
|
|
4838
|
+
createTranslator,
|
|
4839
|
+
defaultBlockComponents,
|
|
4840
|
+
defaultKeymap,
|
|
4841
|
+
defaultSlashItems,
|
|
4842
|
+
defineBlock,
|
|
4843
|
+
definePresentation,
|
|
4844
|
+
embeddedFragment,
|
|
4845
|
+
en,
|
|
4846
|
+
ensureMathStyles,
|
|
4847
|
+
estimateBlockHeight,
|
|
4848
|
+
filterSlashItems,
|
|
4849
|
+
fullRenderer,
|
|
4850
|
+
getLocales,
|
|
4851
|
+
highlightCode,
|
|
4852
|
+
htmlToFragment,
|
|
4853
|
+
matchesShortcut,
|
|
4854
|
+
mountFormula,
|
|
4855
|
+
normalizeImageUrl,
|
|
4856
|
+
normalizeLanguage,
|
|
4857
|
+
registerLocale,
|
|
4858
|
+
resolveBlockComponent,
|
|
4859
|
+
resolveKeymap,
|
|
4860
|
+
resolveMessages,
|
|
4861
|
+
ru,
|
|
4862
|
+
temmlRenderer,
|
|
4863
|
+
unbind,
|
|
4864
|
+
uploadImage,
|
|
4865
|
+
useBlock,
|
|
4866
|
+
useBlockController,
|
|
4867
|
+
useBlockDrag,
|
|
4868
|
+
useBlockDragState,
|
|
4869
|
+
useChildren,
|
|
4870
|
+
useDocument,
|
|
4871
|
+
useEditor,
|
|
4872
|
+
useEditorUI,
|
|
4873
|
+
useEditorView,
|
|
4874
|
+
useHistory,
|
|
4875
|
+
useLocale,
|
|
4876
|
+
useReadOnly,
|
|
4877
|
+
useT,
|
|
4878
|
+
virtualRenderer
|
|
4879
|
+
};
|
|
4880
|
+
//# sourceMappingURL=index.js.map
|