@haklex/rich-editor 0.1.1 → 0.3.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/AlertQuoteEditNode-C55sxsR3.js +267 -0
- package/dist/KaTeXRenderer-CQQT3BMw.js +215 -0
- package/dist/LinkCardRenderer-CigqFwCv.js +45 -0
- package/dist/MermaidPlugin-BrOr-wQi.js +67 -0
- package/dist/RubyRenderer-jOkydJHg.js +15 -0
- package/dist/SubmitShortcutPlugin-DhyVFzoj.js +2186 -0
- package/dist/commands-entry.mjs +54 -74
- package/dist/components/decorators/PollEditDecorator.d.ts +13 -0
- package/dist/components/decorators/PollEditDecorator.d.ts.map +1 -0
- package/dist/components/renderers/PollRenderer.d.ts +3 -0
- package/dist/components/renderers/PollRenderer.d.ts.map +1 -0
- package/dist/config-B5BuLljq.js +1633 -0
- package/dist/config-edit.d.ts.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/context/PollDataContext.d.ts +11 -0
- package/dist/context/PollDataContext.d.ts.map +1 -0
- package/dist/extractPolls-DO31LNrp.js +116 -0
- package/dist/grid.css-CJCkLTZc.js +44 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +121 -180
- package/dist/katex.css-CIOEOXyd.js +145 -0
- package/dist/node-registry-Dz5OTkh4.js +946 -0
- package/dist/nodes/PollEditNode.d.ts +14 -0
- package/dist/nodes/PollEditNode.d.ts.map +1 -0
- package/dist/nodes/PollNode.d.ts +52 -0
- package/dist/nodes/PollNode.d.ts.map +1 -0
- package/dist/nodes-entry.d.ts +3 -0
- package/dist/nodes-entry.d.ts.map +1 -1
- package/dist/nodes-entry.mjs +5 -50
- package/dist/normalizeSerializedEditorState-B-1wmGzd.js +78 -0
- package/dist/plugins-entry.mjs +3 -28
- package/dist/renderers-entry.mjs +41 -61
- package/dist/rich-editor.css +2 -1
- package/dist/static-entry.d.ts +5 -0
- package/dist/static-entry.d.ts.map +1 -1
- package/dist/static-entry.mjs +16 -66
- package/dist/styles/index.d.ts +2 -0
- package/dist/styles/index.d.ts.map +1 -1
- package/dist/styles/poll-edit.css.d.ts +35 -0
- package/dist/styles/poll-edit.css.d.ts.map +1 -0
- package/dist/styles/poll.css.d.ts +43 -0
- package/dist/styles/poll.css.d.ts.map +1 -0
- package/dist/styles-entry.mjs +3 -21
- package/dist/theme-B5B2EOWM.js +1099 -0
- package/dist/types/poll.d.ts +36 -0
- package/dist/types/poll.d.ts.map +1 -0
- package/dist/types/renderer-config.d.ts +3 -0
- package/dist/types/renderer-config.d.ts.map +1 -1
- package/dist/utils/extractPolls.d.ts +4 -0
- package/dist/utils/extractPolls.d.ts.map +1 -0
- package/package.json +30 -30
- package/dist/AlertQuoteEditNode-sPNf3_7P.js +0 -293
- package/dist/KaTeXRenderer-CQyQzNTJ.js +0 -218
- package/dist/LinkCardRenderer-QmkOlyXb.js +0 -36
- package/dist/MermaidPlugin-DKuGUcCG.js +0 -101
- package/dist/PresentDialogContext-DRroMIoK.js +0 -71
- package/dist/RubyRenderer-CJQmODir.js +0 -14
- package/dist/SubmitShortcutPlugin-D9uKYHda.js +0 -2427
- package/dist/config-Dl3ZkytB.js +0 -1362
- package/dist/grid.css-Md5-Cfx_.js +0 -11
- package/dist/katex.css-Csc-7N7u.js +0 -28
- package/dist/node-registry-CovhHUB6.js +0 -824
- package/dist/normalizeSerializedEditorState-k5G4xSi9.js +0 -85
- package/dist/theme-lEwScxEX.js +0 -1113
|
@@ -0,0 +1,2186 @@
|
|
|
1
|
+
import { d as setCodeBlockCursorIntent, l as $createCodeBlockEditNode, t as getResolvedEditNodes, u as $createBannerEditNode } from "./node-registry-Dz5OTkh4.js";
|
|
2
|
+
import { A as AlertQuoteNode, E as FootnoteNode, M as extractTextContent, T as $createFootnoteNode, _ as KaTeXInlineNode, b as KaTeXBlockNode, c as SpoilerNode, f as $createMentionNode, h as $createKaTeXInlineNode, m as MentionNode, o as $createSpoilerNode, v as $createKaTeXBlockNode, w as OPEN_IMAGE_UPLOAD_DIALOG_COMMAND, x as $createImageNode } from "./theme-B5B2EOWM.js";
|
|
3
|
+
import { a as FootnoteDefinitionsProvider, n as computeImageMeta } from "./KaTeXRenderer-CQQT3BMw.js";
|
|
4
|
+
import "./katex.css-CIOEOXyd.js";
|
|
5
|
+
import { A as $createCommentPlaceholderNode, D as $createDetailsNode, E as FootnoteSectionNode, M as CommentNode, O as DetailsNode, P as CodeBlockNode, S as $isGridContainerNode, T as $isFootnoteSectionNode, V as BannerNode, a as $createRubyNode, k as $createCommentNode, s as RubyNode, w as $createFootnoteSectionNode } from "./config-B5BuLljq.js";
|
|
6
|
+
import { t as $createAlertQuoteEditNode } from "./AlertQuoteEditNode-C55sxsR3.js";
|
|
7
|
+
import { n as getHostname, r as probeFavicon, t as normalizeSerializedEditorState } from "./normalizeSerializedEditorState-B-1wmGzd.js";
|
|
8
|
+
import { CheckListPlugin } from "@lexical/react/LexicalCheckListPlugin";
|
|
9
|
+
import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
|
|
10
|
+
import { ListPlugin } from "@lexical/react/LexicalListPlugin";
|
|
11
|
+
import { TabIndentationPlugin } from "@lexical/react/LexicalTabIndentationPlugin";
|
|
12
|
+
import { TablePlugin } from "@lexical/react/LexicalTablePlugin";
|
|
13
|
+
import { AutoLinkNode, LinkNode, createLinkMatcherWithRegExp, registerAutoLink } from "@lexical/link";
|
|
14
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
15
|
+
import { createContext, use, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
16
|
+
import { $createQuoteNode, $isQuoteNode, DRAG_DROP_PASTE, QuoteNode } from "@lexical/rich-text";
|
|
17
|
+
import { $addUpdateTag, $createLineBreakNode, $createNodeSelection, $createParagraphNode, $createRangeSelection, $createTextNode, $getNodeByKey, $getRoot, $getSelection, $getState, $insertNodes, $isDecoratorNode, $isElementNode, $isLineBreakNode, $isNodeSelection, $isParagraphNode, $isRangeSelection, $isRootNode, $isTextNode, $nodesOfType, $parseSerializedNode, $setSelection, $setState, COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_LOW, IS_CODE, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, KEY_ENTER_COMMAND, PASTE_COMMAND, SELECTION_CHANGE_COMMAND, createEditor, createState } from "lexical";
|
|
18
|
+
import { $createHorizontalRuleNode, HorizontalRuleNode, INSERT_HORIZONTAL_RULE_COMMAND } from "@lexical/extension";
|
|
19
|
+
import { CODE_BLOCK_NODE_TRANSFORMER, COMMENT_TRANSFORMER, CONTAINER_TRANSFORMER, FOOTNOTE_SECTION_TRANSFORMER, FOOTNOTE_TRANSFORMER, GIT_ALERT_TRANSFORMER, HORIZONTAL_RULE_BLOCK_TRANSFORMER, IMAGE_BLOCK_TRANSFORMER, INSERT_TRANSFORMER, KATEX_BLOCK_TRANSFORMER, KATEX_INLINE_TRANSFORMER, LINK_CARD_BLOCK_TRANSFORMER, MENTION_TRANSFORMER, MERMAID_BLOCK_TRANSFORMER, RUBY_TRANSFORMER, SPOILER_TRANSFORMER, SUBSCRIPT_TRANSFORMER, SUPERSCRIPT_TRANSFORMER, TABLE_BLOCK_TRANSFORMER, VIDEO_BLOCK_TRANSFORMER } from "@haklex/rich-headless/transformers";
|
|
20
|
+
import { $convertFromMarkdownString, CHECK_LIST, CODE, QUOTE, TRANSFORMERS } from "@lexical/markdown";
|
|
21
|
+
import { $createTableCellNode, $createTableNode, $createTableRowNode, TableCellHeaderStates } from "@lexical/table";
|
|
22
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
23
|
+
import { Check, Info, Link2, Upload } from "lucide-react";
|
|
24
|
+
import { nanoid } from "nanoid";
|
|
25
|
+
import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
|
|
26
|
+
import { ActionBar, ActionButton, Dialog, DialogPopup, DialogTitle, SegmentedControl } from "@haklex/rich-editor-ui";
|
|
27
|
+
//#region src/plugins/AutoLinkPlugin.tsx
|
|
28
|
+
var DEFAULT_MATCHERS = [createLinkMatcherWithRegExp(/https?:\/\/(?:www\.)?[\w#%+.:=@~-]{1,256}\.[A-Za-z]{2}[\w#%&()+./:=?@~-]*/), createLinkMatcherWithRegExp(/(?:[\w%+.-]+@[\d.a-z-]+\.[a-z]{2,})/i, (text) => `mailto:${text}`)];
|
|
29
|
+
function AutoLinkPlugin({ matchers }) {
|
|
30
|
+
const [editor] = useLexicalComposerContext();
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
return registerAutoLink(editor, {
|
|
33
|
+
matchers: matchers ?? DEFAULT_MATCHERS,
|
|
34
|
+
changeHandlers: [],
|
|
35
|
+
excludeParents: []
|
|
36
|
+
});
|
|
37
|
+
}, [editor, matchers]);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/plugins/BlockExitPlugin.tsx
|
|
42
|
+
function selectDecoratorNode(node, cursorPlacement = "start") {
|
|
43
|
+
if (node.getType() === "code-block") setCodeBlockCursorIntent(node.getKey(), cursorPlacement);
|
|
44
|
+
const selection = $createNodeSelection();
|
|
45
|
+
selection.add(node.getKey());
|
|
46
|
+
$setSelection(selection);
|
|
47
|
+
}
|
|
48
|
+
function isAtTopLevelBoundary(selection, direction) {
|
|
49
|
+
const point = selection.anchor;
|
|
50
|
+
const topLevel = point.getNode().getTopLevelElementOrThrow();
|
|
51
|
+
const pointNode = point.getNode();
|
|
52
|
+
if (point.type === "text") {
|
|
53
|
+
if (!$isTextNode(pointNode)) return false;
|
|
54
|
+
const expectedOffset = direction === "start" ? 0 : pointNode.getTextContentSize();
|
|
55
|
+
if (point.offset !== expectedOffset) return false;
|
|
56
|
+
} else {
|
|
57
|
+
if (!$isElementNode(pointNode)) return false;
|
|
58
|
+
const expectedOffset = direction === "start" ? 0 : pointNode.getChildrenSize();
|
|
59
|
+
if (point.offset !== expectedOffset) return false;
|
|
60
|
+
}
|
|
61
|
+
let current = pointNode;
|
|
62
|
+
while (current && current !== topLevel) {
|
|
63
|
+
if ((direction === "start" ? current.getPreviousSibling() : current.getNextSibling()) !== null) return false;
|
|
64
|
+
current = current.getParent();
|
|
65
|
+
}
|
|
66
|
+
return current === topLevel;
|
|
67
|
+
}
|
|
68
|
+
function isSingleLineParagraph(node) {
|
|
69
|
+
return $isParagraphNode(node) && !node.getTextContent().includes("\n");
|
|
70
|
+
}
|
|
71
|
+
function getOutermostNodeWithinTopLevel(node, topLevel) {
|
|
72
|
+
let current = node;
|
|
73
|
+
while (current.getParent() !== topLevel) {
|
|
74
|
+
const parent = current.getParent();
|
|
75
|
+
if (!parent) break;
|
|
76
|
+
current = parent;
|
|
77
|
+
}
|
|
78
|
+
return current;
|
|
79
|
+
}
|
|
80
|
+
function isDeeplyEmptyElement(node) {
|
|
81
|
+
if (!$isElementNode(node)) return false;
|
|
82
|
+
if (node.getChildrenSize() === 0) return true;
|
|
83
|
+
for (const child of node.getChildren()) {
|
|
84
|
+
if ($isTextNode(child)) {
|
|
85
|
+
if (child.getTextContent().trim().length > 0) return false;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!$isElementNode(child) || !isDeeplyEmptyElement(child)) return false;
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
function isSelectionInsideTopLevel(selection, topLevelKey) {
|
|
93
|
+
return selection.anchor.getNode().getTopLevelElementOrThrow().getKey() === topLevelKey && selection.focus.getNode().getTopLevelElementOrThrow().getKey() === topLevelKey;
|
|
94
|
+
}
|
|
95
|
+
function exitInlineCodeAtLineEnd(selection) {
|
|
96
|
+
if (!selection.isCollapsed() || selection.anchor.type !== "text") return false;
|
|
97
|
+
const anchorNode = selection.anchor.getNode();
|
|
98
|
+
if (!$isTextNode(anchorNode) || !anchorNode.hasFormat("code")) return false;
|
|
99
|
+
if (selection.anchor.offset !== anchorNode.getTextContentSize()) return false;
|
|
100
|
+
if (!isAtTopLevelBoundary(selection, "end")) return false;
|
|
101
|
+
const topLevel = anchorNode.getTopLevelElementOrThrow();
|
|
102
|
+
const exitOffset = getOutermostNodeWithinTopLevel(anchorNode, topLevel).getIndexWithinParent() + 1;
|
|
103
|
+
selection.anchor.set(topLevel.getKey(), exitOffset, "element");
|
|
104
|
+
selection.focus.set(topLevel.getKey(), exitOffset, "element");
|
|
105
|
+
selection.setFormat(anchorNode.getFormat() & ~IS_CODE);
|
|
106
|
+
selection.setStyle(anchorNode.getStyle());
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
function BlockExitPlugin() {
|
|
110
|
+
const [editor] = useLexicalComposerContext();
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
return registerBlockExitCommands(editor);
|
|
113
|
+
}, [editor]);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
function registerBlockExitCommands(editor) {
|
|
117
|
+
let virtualParagraphKey = null;
|
|
118
|
+
const clearVirtualParagraph = () => {
|
|
119
|
+
virtualParagraphKey = null;
|
|
120
|
+
};
|
|
121
|
+
const getVirtualParagraph = () => {
|
|
122
|
+
if (!virtualParagraphKey) return null;
|
|
123
|
+
const node = $getNodeByKey(virtualParagraphKey);
|
|
124
|
+
return $isParagraphNode(node) ? node : null;
|
|
125
|
+
};
|
|
126
|
+
const insertVirtualParagraph = (target, position, cursorPlacement = "start") => {
|
|
127
|
+
const paragraph = $createParagraphNode();
|
|
128
|
+
if (position === "before") target.insertBefore(paragraph);
|
|
129
|
+
else target.insertAfter(paragraph);
|
|
130
|
+
virtualParagraphKey = paragraph.getKey();
|
|
131
|
+
if (cursorPlacement === "end") paragraph.selectEnd();
|
|
132
|
+
else paragraph.selectStart();
|
|
133
|
+
return true;
|
|
134
|
+
};
|
|
135
|
+
const unregisterVirtualParagraphCleanup = editor.registerUpdateListener(({ editorState }) => {
|
|
136
|
+
if (!virtualParagraphKey) return;
|
|
137
|
+
let shouldKeepTracking = false;
|
|
138
|
+
let shouldRemove = false;
|
|
139
|
+
editorState.read(() => {
|
|
140
|
+
const paragraph = getVirtualParagraph();
|
|
141
|
+
if (!paragraph) return;
|
|
142
|
+
const selection = $getSelection();
|
|
143
|
+
if ($isRangeSelection(selection) && isSelectionInsideTopLevel(selection, paragraph.getKey())) {
|
|
144
|
+
shouldKeepTracking = true;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
shouldRemove = isDeeplyEmptyElement(paragraph);
|
|
148
|
+
});
|
|
149
|
+
if (shouldKeepTracking) return;
|
|
150
|
+
editor.update(() => {
|
|
151
|
+
const paragraph = getVirtualParagraph();
|
|
152
|
+
if (paragraph && shouldRemove && isDeeplyEmptyElement(paragraph)) paragraph.remove();
|
|
153
|
+
clearVirtualParagraph();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
const unregisterArrowRight = editor.registerCommand(KEY_ARROW_RIGHT_COMMAND, (event) => {
|
|
157
|
+
const selection = $getSelection();
|
|
158
|
+
if (!$isRangeSelection(selection)) return false;
|
|
159
|
+
if (!exitInlineCodeAtLineEnd(selection)) return false;
|
|
160
|
+
event?.preventDefault();
|
|
161
|
+
return true;
|
|
162
|
+
}, COMMAND_PRIORITY_CRITICAL);
|
|
163
|
+
const unregisterArrowDown = editor.registerCommand(KEY_ARROW_DOWN_COMMAND, (event) => {
|
|
164
|
+
const selection = $getSelection();
|
|
165
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed()) return false;
|
|
166
|
+
const anchorNode = selection.anchor.getNode();
|
|
167
|
+
const topLevelElement = anchorNode.getTopLevelElementOrThrow();
|
|
168
|
+
const shouldSelectNextDecorator = isAtTopLevelBoundary(selection, "end") || isSingleLineParagraph(topLevelElement);
|
|
169
|
+
if (!$isQuoteNode(topLevelElement) && shouldSelectNextDecorator) {
|
|
170
|
+
const next = topLevelElement.getNextSibling();
|
|
171
|
+
if (next && $isDecoratorNode(next) && next.isKeyboardSelectable()) {
|
|
172
|
+
event?.preventDefault();
|
|
173
|
+
selectDecoratorNode(next, "start");
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const element = $isElementNode(anchorNode) ? anchorNode : anchorNode.getParentOrThrow();
|
|
178
|
+
let quoteChild = element;
|
|
179
|
+
let quoteNode = null;
|
|
180
|
+
let current = element;
|
|
181
|
+
while (current) {
|
|
182
|
+
const parent = current.getParent();
|
|
183
|
+
if (!parent || $isRootNode(parent)) break;
|
|
184
|
+
if ($isQuoteNode(parent)) {
|
|
185
|
+
quoteNode = parent;
|
|
186
|
+
quoteChild = current;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
current = parent;
|
|
190
|
+
}
|
|
191
|
+
if (quoteNode && quoteChild.getNextSibling() === null && $isParagraphNode(quoteChild) && quoteChild.getTextContent() === "") {
|
|
192
|
+
event?.preventDefault();
|
|
193
|
+
let next = quoteNode.getNextSibling();
|
|
194
|
+
if (!next) {
|
|
195
|
+
next = $createParagraphNode();
|
|
196
|
+
quoteNode.insertAfter(next);
|
|
197
|
+
}
|
|
198
|
+
next.selectStart();
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
if (!event?.shiftKey && topLevelElement.getNextSibling() === null && isAtTopLevelBoundary(selection, "end") && topLevelElement.getKey() !== virtualParagraphKey && !isDeeplyEmptyElement(topLevelElement)) {
|
|
202
|
+
event?.preventDefault();
|
|
203
|
+
return insertVirtualParagraph(topLevelElement, "after");
|
|
204
|
+
}
|
|
205
|
+
return false;
|
|
206
|
+
}, COMMAND_PRIORITY_CRITICAL);
|
|
207
|
+
const unregisterCmdEnter = editor.registerCommand(KEY_ENTER_COMMAND, (event) => {
|
|
208
|
+
if (!event?.metaKey && !event?.ctrlKey) return false;
|
|
209
|
+
const selection = $getSelection();
|
|
210
|
+
if ($isNodeSelection(selection)) {
|
|
211
|
+
const nodes = selection.getNodes();
|
|
212
|
+
if (nodes.length !== 1) return false;
|
|
213
|
+
const node = nodes[0];
|
|
214
|
+
if (!$isDecoratorNode(node)) return false;
|
|
215
|
+
event.preventDefault();
|
|
216
|
+
let next = node.getNextSibling();
|
|
217
|
+
if (!next) {
|
|
218
|
+
next = $createParagraphNode();
|
|
219
|
+
node.insertAfter(next);
|
|
220
|
+
}
|
|
221
|
+
if ($isElementNode(next)) next.selectStart();
|
|
222
|
+
else if ($isDecoratorNode(next)) selectDecoratorNode(next, "start");
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
if (!$isRangeSelection(selection)) return false;
|
|
226
|
+
const topLevelElement = selection.anchor.getNode().getTopLevelElementOrThrow();
|
|
227
|
+
if ($isParagraphNode(topLevelElement)) return false;
|
|
228
|
+
event.preventDefault();
|
|
229
|
+
let next = topLevelElement.getNextSibling();
|
|
230
|
+
if (!next || !$isParagraphNode(next)) {
|
|
231
|
+
next = $createParagraphNode();
|
|
232
|
+
topLevelElement.insertAfter(next);
|
|
233
|
+
}
|
|
234
|
+
next.selectStart();
|
|
235
|
+
return true;
|
|
236
|
+
}, COMMAND_PRIORITY_CRITICAL);
|
|
237
|
+
function handleDeleteDecorator(event) {
|
|
238
|
+
const selection = $getSelection();
|
|
239
|
+
if (!$isNodeSelection(selection)) return false;
|
|
240
|
+
const nodes = selection.getNodes();
|
|
241
|
+
if (nodes.length !== 1) return false;
|
|
242
|
+
const node = nodes[0];
|
|
243
|
+
if (!$isDecoratorNode(node)) return false;
|
|
244
|
+
event?.preventDefault();
|
|
245
|
+
const prev = node.getPreviousSibling();
|
|
246
|
+
const next = node.getNextSibling();
|
|
247
|
+
node.remove();
|
|
248
|
+
if (prev && $isElementNode(prev)) prev.selectEnd();
|
|
249
|
+
else if (prev && $isDecoratorNode(prev)) selectDecoratorNode(prev, "end");
|
|
250
|
+
else if (next && $isElementNode(next)) next.selectStart();
|
|
251
|
+
else if (next && $isDecoratorNode(next)) selectDecoratorNode(next, "start");
|
|
252
|
+
else {
|
|
253
|
+
const root = $getRoot();
|
|
254
|
+
if (root && $isElementNode(root)) {
|
|
255
|
+
const p = $createParagraphNode();
|
|
256
|
+
root.append(p);
|
|
257
|
+
p.selectStart();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
const unregisterBackspace = editor.registerCommand(KEY_BACKSPACE_COMMAND, handleDeleteDecorator, COMMAND_PRIORITY_HIGH);
|
|
263
|
+
const unregisterDelete = editor.registerCommand(KEY_DELETE_COMMAND, handleDeleteDecorator, COMMAND_PRIORITY_HIGH);
|
|
264
|
+
const unregisterArrowUpDecorator = editor.registerCommand(KEY_ARROW_UP_COMMAND, (event) => {
|
|
265
|
+
const selection = $getSelection();
|
|
266
|
+
if ($isRangeSelection(selection) && selection.isCollapsed()) {
|
|
267
|
+
const topLevelElement = selection.anchor.getNode().getTopLevelElementOrThrow();
|
|
268
|
+
const shouldSelectPreviousDecorator = isAtTopLevelBoundary(selection, "start") || isSingleLineParagraph(topLevelElement);
|
|
269
|
+
if (!$isQuoteNode(topLevelElement) && shouldSelectPreviousDecorator) {
|
|
270
|
+
const prev = topLevelElement.getPreviousSibling();
|
|
271
|
+
if (prev && $isDecoratorNode(prev) && prev.isKeyboardSelectable()) {
|
|
272
|
+
event?.preventDefault();
|
|
273
|
+
selectDecoratorNode(prev, "end");
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (!event?.shiftKey && topLevelElement.getPreviousSibling() === null && isAtTopLevelBoundary(selection, "start") && topLevelElement.getKey() !== virtualParagraphKey && !isDeeplyEmptyElement(topLevelElement)) {
|
|
278
|
+
event?.preventDefault();
|
|
279
|
+
return insertVirtualParagraph(topLevelElement, "before");
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (!$isNodeSelection(selection)) return false;
|
|
283
|
+
const nodes = selection.getNodes();
|
|
284
|
+
if (nodes.length !== 1) return false;
|
|
285
|
+
const node = nodes[0];
|
|
286
|
+
if (!$isDecoratorNode(node)) return false;
|
|
287
|
+
const prev = node.getPreviousSibling();
|
|
288
|
+
if (prev && $isElementNode(prev)) {
|
|
289
|
+
event?.preventDefault();
|
|
290
|
+
prev.selectEnd();
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
if (prev && $isDecoratorNode(prev)) {
|
|
294
|
+
event?.preventDefault();
|
|
295
|
+
selectDecoratorNode(prev, "end");
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
if (!event?.shiftKey && node.getTopLevelElementOrThrow().getPreviousSibling() === null && node.getKey() !== virtualParagraphKey) {
|
|
299
|
+
event?.preventDefault();
|
|
300
|
+
return insertVirtualParagraph(node, "before", "end");
|
|
301
|
+
}
|
|
302
|
+
return false;
|
|
303
|
+
}, COMMAND_PRIORITY_CRITICAL);
|
|
304
|
+
const unregisterArrowDownDecorator = editor.registerCommand(KEY_ARROW_DOWN_COMMAND, (event) => {
|
|
305
|
+
const selection = $getSelection();
|
|
306
|
+
if (!$isNodeSelection(selection)) return false;
|
|
307
|
+
const nodes = selection.getNodes();
|
|
308
|
+
if (nodes.length !== 1) return false;
|
|
309
|
+
const node = nodes[0];
|
|
310
|
+
if (!$isDecoratorNode(node)) return false;
|
|
311
|
+
const next = node.getNextSibling();
|
|
312
|
+
if (next && $isElementNode(next)) {
|
|
313
|
+
event?.preventDefault();
|
|
314
|
+
next.selectStart();
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
if (next && $isDecoratorNode(next)) {
|
|
318
|
+
event?.preventDefault();
|
|
319
|
+
selectDecoratorNode(next, "start");
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
if (!event?.shiftKey && node.getTopLevelElementOrThrow().getNextSibling() === null && node.getKey() !== virtualParagraphKey) {
|
|
323
|
+
event?.preventDefault();
|
|
324
|
+
return insertVirtualParagraph(node, "after");
|
|
325
|
+
}
|
|
326
|
+
return false;
|
|
327
|
+
}, COMMAND_PRIORITY_CRITICAL);
|
|
328
|
+
return () => {
|
|
329
|
+
unregisterVirtualParagraphCleanup();
|
|
330
|
+
unregisterArrowRight();
|
|
331
|
+
unregisterArrowDown();
|
|
332
|
+
unregisterCmdEnter();
|
|
333
|
+
unregisterBackspace();
|
|
334
|
+
unregisterDelete();
|
|
335
|
+
unregisterArrowUpDecorator();
|
|
336
|
+
unregisterArrowDownDecorator();
|
|
337
|
+
clearVirtualParagraph();
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
//#endregion
|
|
341
|
+
//#region src/transformers/horizontal-rule.ts
|
|
342
|
+
var HORIZONTAL_RULE_REGEX = /^(---|\*\*\*|___)\s?$/;
|
|
343
|
+
var HORIZONTAL_RULE_BLOCK_TRANSFORMER$1 = {
|
|
344
|
+
...HORIZONTAL_RULE_BLOCK_TRANSFORMER,
|
|
345
|
+
dependencies: [HorizontalRuleNode],
|
|
346
|
+
regExp: HORIZONTAL_RULE_REGEX,
|
|
347
|
+
replace: (parentNode) => {
|
|
348
|
+
const hrNode = $createHorizontalRuleNode();
|
|
349
|
+
const paragraph = $createParagraphNode();
|
|
350
|
+
parentNode.replace(hrNode);
|
|
351
|
+
hrNode.insertAfter(paragraph);
|
|
352
|
+
paragraph.selectStart();
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
//#endregion
|
|
356
|
+
//#region src/plugins/HorizontalRulePlugin.tsx
|
|
357
|
+
function HorizontalRulePlugin() {
|
|
358
|
+
const [editor] = useLexicalComposerContext();
|
|
359
|
+
useEffect(() => {
|
|
360
|
+
const unregisterInsert = editor.registerCommand(INSERT_HORIZONTAL_RULE_COMMAND, () => {
|
|
361
|
+
const selection = $getSelection();
|
|
362
|
+
if (!$isRangeSelection(selection)) return false;
|
|
363
|
+
const topLevel = selection.focus.getNode().getTopLevelElement();
|
|
364
|
+
if (!topLevel) return false;
|
|
365
|
+
const hrNode = $createHorizontalRuleNode();
|
|
366
|
+
const paragraph = $createParagraphNode();
|
|
367
|
+
topLevel.insertAfter(hrNode);
|
|
368
|
+
hrNode.insertAfter(paragraph);
|
|
369
|
+
paragraph.selectStart();
|
|
370
|
+
return true;
|
|
371
|
+
}, COMMAND_PRIORITY_EDITOR);
|
|
372
|
+
const unregisterEnter = editor.registerCommand(KEY_ENTER_COMMAND, (event) => {
|
|
373
|
+
const selection = $getSelection();
|
|
374
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed()) return false;
|
|
375
|
+
const anchorNode = selection.anchor.getNode();
|
|
376
|
+
if (!$isTextNode(anchorNode)) return false;
|
|
377
|
+
const textContent = anchorNode.getTextContent();
|
|
378
|
+
if (!HORIZONTAL_RULE_REGEX.test(textContent)) return false;
|
|
379
|
+
const parentNode = anchorNode.getParent();
|
|
380
|
+
if (!parentNode) return false;
|
|
381
|
+
event?.preventDefault();
|
|
382
|
+
const hrNode = $createHorizontalRuleNode();
|
|
383
|
+
const paragraph = $createParagraphNode();
|
|
384
|
+
parentNode.replace(hrNode);
|
|
385
|
+
hrNode.insertAfter(paragraph);
|
|
386
|
+
paragraph.selectStart();
|
|
387
|
+
return true;
|
|
388
|
+
}, COMMAND_PRIORITY_LOW);
|
|
389
|
+
return () => {
|
|
390
|
+
unregisterInsert();
|
|
391
|
+
unregisterEnter();
|
|
392
|
+
};
|
|
393
|
+
}, [editor]);
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region src/transformers/alert.ts
|
|
398
|
+
var ALERT_TYPE_MAP = {
|
|
399
|
+
NOTE: "note",
|
|
400
|
+
TIP: "tip",
|
|
401
|
+
IMPORTANT: "important",
|
|
402
|
+
WARNING: "warning",
|
|
403
|
+
CAUTION: "caution"
|
|
404
|
+
};
|
|
405
|
+
var GIT_ALERT_TRANSFORMER$1 = {
|
|
406
|
+
...GIT_ALERT_TRANSFORMER,
|
|
407
|
+
dependencies: [AlertQuoteNode],
|
|
408
|
+
regExp: /^>\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*$/,
|
|
409
|
+
replace: (parentNode, children, match) => {
|
|
410
|
+
const alertNode = $createAlertQuoteEditNode(ALERT_TYPE_MAP[match[1]] || "note", { root: {
|
|
411
|
+
children: [{
|
|
412
|
+
type: "paragraph",
|
|
413
|
+
children: children.map((child) => child.exportJSON()),
|
|
414
|
+
direction: null,
|
|
415
|
+
format: "",
|
|
416
|
+
indent: 0,
|
|
417
|
+
textFormat: 0,
|
|
418
|
+
textStyle: "",
|
|
419
|
+
version: 1
|
|
420
|
+
}],
|
|
421
|
+
direction: null,
|
|
422
|
+
format: "",
|
|
423
|
+
indent: 0,
|
|
424
|
+
type: "root",
|
|
425
|
+
version: 1
|
|
426
|
+
} });
|
|
427
|
+
parentNode.replace(alertNode);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
//#endregion
|
|
431
|
+
//#region src/transformers/code-block.ts
|
|
432
|
+
function findCodeBlockKlass(nodes) {
|
|
433
|
+
return nodes.find((n) => n.getType?.() === "code-block") || CodeBlockNode;
|
|
434
|
+
}
|
|
435
|
+
var CODE_BLOCK_MULTILINE_TRANSFORMER = {
|
|
436
|
+
dependencies: [],
|
|
437
|
+
regExpEnd: {
|
|
438
|
+
optional: true,
|
|
439
|
+
regExp: /[\t ]*```$/
|
|
440
|
+
},
|
|
441
|
+
regExpStart: /^[\t ]*```([\w-]+)?/,
|
|
442
|
+
replace: (rootNode, _children, startMatch, _endMatch, linesInBetween, isImport) => {
|
|
443
|
+
const lang = startMatch[1] || "";
|
|
444
|
+
let code = "";
|
|
445
|
+
if (linesInBetween) {
|
|
446
|
+
const lines = [...linesInBetween];
|
|
447
|
+
while (lines.length > 0 && !lines[0].length) lines.shift();
|
|
448
|
+
while (lines.length > 0 && !lines.at(-1)?.length) lines.pop();
|
|
449
|
+
code = lines.join("\n");
|
|
450
|
+
}
|
|
451
|
+
const node = new (findCodeBlockKlass(getResolvedEditNodes()))(code, lang);
|
|
452
|
+
if (isImport || $isRootNode(rootNode)) rootNode.append(node);
|
|
453
|
+
else rootNode.replace(node);
|
|
454
|
+
const selection = $createNodeSelection();
|
|
455
|
+
selection.add(node.getKey());
|
|
456
|
+
$setSelection(selection);
|
|
457
|
+
},
|
|
458
|
+
type: "multiline-element"
|
|
459
|
+
};
|
|
460
|
+
var COMMENT_OPEN_TRANSFORMER = {
|
|
461
|
+
dependencies: [CommentNode],
|
|
462
|
+
export: () => null,
|
|
463
|
+
importRegExp: /a^/,
|
|
464
|
+
regExp: /<!--$/,
|
|
465
|
+
replace: (textNode) => {
|
|
466
|
+
const node = $createCommentPlaceholderNode();
|
|
467
|
+
textNode.replace(node);
|
|
468
|
+
node.select(0, node.getTextContentSize());
|
|
469
|
+
},
|
|
470
|
+
trigger: "-",
|
|
471
|
+
type: "text-match"
|
|
472
|
+
};
|
|
473
|
+
var COMMENT_TRANSFORMER$1 = {
|
|
474
|
+
...COMMENT_TRANSFORMER,
|
|
475
|
+
dependencies: [CommentNode],
|
|
476
|
+
replace: (textNode, match) => {
|
|
477
|
+
textNode.replace($createCommentNode(match[1] ?? ""));
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region src/transformers/container.ts
|
|
482
|
+
var BANNER_TYPE_MAP = {
|
|
483
|
+
note: "note",
|
|
484
|
+
info: "note",
|
|
485
|
+
tip: "tip",
|
|
486
|
+
success: "tip",
|
|
487
|
+
important: "important",
|
|
488
|
+
warning: "warning",
|
|
489
|
+
warn: "warning",
|
|
490
|
+
error: "caution",
|
|
491
|
+
danger: "caution",
|
|
492
|
+
caution: "caution"
|
|
493
|
+
};
|
|
494
|
+
var CONTAINER_TRANSFORMER$1 = {
|
|
495
|
+
...CONTAINER_TRANSFORMER,
|
|
496
|
+
dependencies: [BannerNode, DetailsNode],
|
|
497
|
+
regExp: /^:::\s*(\w+)(?:\{([^}]*)\})?\s*$/,
|
|
498
|
+
replace: (parentNode, children, match) => {
|
|
499
|
+
const type = match[1];
|
|
500
|
+
const params = match[2];
|
|
501
|
+
if (type in BANNER_TYPE_MAP) {
|
|
502
|
+
const bannerType = BANNER_TYPE_MAP[type];
|
|
503
|
+
const banner = $createBannerEditNode(bannerType, { root: {
|
|
504
|
+
children: [{
|
|
505
|
+
type: "paragraph",
|
|
506
|
+
children: children.map((child) => child.exportJSON()),
|
|
507
|
+
direction: null,
|
|
508
|
+
format: "",
|
|
509
|
+
indent: 0,
|
|
510
|
+
textFormat: 0,
|
|
511
|
+
textStyle: "",
|
|
512
|
+
version: 1
|
|
513
|
+
}],
|
|
514
|
+
direction: null,
|
|
515
|
+
format: "",
|
|
516
|
+
indent: 0,
|
|
517
|
+
type: "root",
|
|
518
|
+
version: 1
|
|
519
|
+
} });
|
|
520
|
+
parentNode.replace(banner);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (type === "details") {
|
|
524
|
+
const summaryMatch = params?.match(/summary="([^"]*)"/);
|
|
525
|
+
const details = $createDetailsNode(summaryMatch ? summaryMatch[1] : "Details");
|
|
526
|
+
children.forEach((child) => {
|
|
527
|
+
details.append(child);
|
|
528
|
+
});
|
|
529
|
+
parentNode.replace(details);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const paragraph = $createParagraphNode();
|
|
533
|
+
paragraph.append($createTextNode(`::: ${type}`));
|
|
534
|
+
children.forEach((child) => {
|
|
535
|
+
paragraph.append(child);
|
|
536
|
+
});
|
|
537
|
+
parentNode.replace(paragraph);
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region src/transformers/footnote.ts
|
|
542
|
+
var FOOTNOTE_TRANSFORMER$1 = {
|
|
543
|
+
...FOOTNOTE_TRANSFORMER,
|
|
544
|
+
dependencies: [FootnoteNode],
|
|
545
|
+
replace: (textNode, match) => {
|
|
546
|
+
const footnoteNode = $createFootnoteNode(match[1]);
|
|
547
|
+
textNode.replace(footnoteNode);
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
var FOOTNOTE_SECTION_TRANSFORMER$1 = {
|
|
551
|
+
...FOOTNOTE_SECTION_TRANSFORMER,
|
|
552
|
+
dependencies: [FootnoteSectionNode],
|
|
553
|
+
regExp: /^\[\^(\w+)\]:[\t ]+(\S.*)$/,
|
|
554
|
+
replace: (parentNode, _children, match) => {
|
|
555
|
+
const identifier = match[1];
|
|
556
|
+
const content = match[2];
|
|
557
|
+
const root = parentNode.getParent();
|
|
558
|
+
if (root) {
|
|
559
|
+
const children = root.getChildren();
|
|
560
|
+
for (const child of children) if ($isFootnoteSectionNode(child)) {
|
|
561
|
+
child.setDefinition(identifier, content);
|
|
562
|
+
parentNode.remove();
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const sectionNode = $createFootnoteSectionNode({ [identifier]: content });
|
|
567
|
+
parentNode.replace(sectionNode);
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
//#endregion
|
|
571
|
+
//#region src/transformers/katex.ts
|
|
572
|
+
var KATEX_INLINE_TRANSFORMER$1 = {
|
|
573
|
+
...KATEX_INLINE_TRANSFORMER,
|
|
574
|
+
dependencies: [KaTeXInlineNode],
|
|
575
|
+
replace: (textNode, match) => {
|
|
576
|
+
const katexNode = $createKaTeXInlineNode(match[1]);
|
|
577
|
+
textNode.replace(katexNode);
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
var KATEX_BLOCK_TRANSFORMER$1 = {
|
|
581
|
+
...KATEX_BLOCK_TRANSFORMER,
|
|
582
|
+
dependencies: [KaTeXBlockNode],
|
|
583
|
+
replace: (textNode, match) => {
|
|
584
|
+
const katexNode = $createKaTeXBlockNode(match[1]);
|
|
585
|
+
textNode.replace(katexNode);
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
//#endregion
|
|
589
|
+
//#region src/transformers/mention.ts
|
|
590
|
+
var MENTION_TRANSFORMER$1 = {
|
|
591
|
+
...MENTION_TRANSFORMER,
|
|
592
|
+
dependencies: [MentionNode],
|
|
593
|
+
replace: (textNode, match) => {
|
|
594
|
+
const displayName = match[1] || void 0;
|
|
595
|
+
const mentionNode = $createMentionNode(match[2], match[3], displayName);
|
|
596
|
+
textNode.replace(mentionNode);
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
//#endregion
|
|
600
|
+
//#region src/transformers/quote.ts
|
|
601
|
+
var QUOTE_TRANSFORMER = {
|
|
602
|
+
dependencies: [QuoteNode],
|
|
603
|
+
export: (node, exportChildren) => {
|
|
604
|
+
if (!$isQuoteNode(node)) return null;
|
|
605
|
+
return exportChildren(node).split("\n").map((line) => `> ${line}`).join("\n");
|
|
606
|
+
},
|
|
607
|
+
regExp: /^>\s/,
|
|
608
|
+
replace: (parentNode, children, _match, isImport) => {
|
|
609
|
+
if (isImport) {
|
|
610
|
+
const previousNode = parentNode.getPreviousSibling();
|
|
611
|
+
if ($isQuoteNode(previousNode)) {
|
|
612
|
+
previousNode.splice(previousNode.getChildrenSize(), 0, [$createLineBreakNode(), ...children]);
|
|
613
|
+
parentNode.remove();
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const node = $createQuoteNode();
|
|
618
|
+
const paragraph = $createParagraphNode();
|
|
619
|
+
paragraph.append(...children);
|
|
620
|
+
node.append(paragraph);
|
|
621
|
+
parentNode.replace(node);
|
|
622
|
+
if (!isImport) paragraph.select(0, 0);
|
|
623
|
+
},
|
|
624
|
+
type: "element"
|
|
625
|
+
};
|
|
626
|
+
//#endregion
|
|
627
|
+
//#region src/transformers/grid-container.ts
|
|
628
|
+
function quoteAttr(value) {
|
|
629
|
+
return value.replaceAll("\"", "\\\"");
|
|
630
|
+
}
|
|
631
|
+
var GRID_CONTAINER_BLOCK_TRANSFORMER = {
|
|
632
|
+
dependencies: [],
|
|
633
|
+
export: (node) => {
|
|
634
|
+
if (!$isGridContainerNode(node)) return null;
|
|
635
|
+
const body = node.getCellStates().map((state) => extractTextContent(state)).map((content) => `::: cell\n${content}\n:::`).join("\n");
|
|
636
|
+
return `::: grid{cols=${node.getCols()} gap="${quoteAttr(node.getGap())}"}\n${body}\n:::`;
|
|
637
|
+
},
|
|
638
|
+
regExp: /\b\B/,
|
|
639
|
+
replace: () => {},
|
|
640
|
+
type: "element"
|
|
641
|
+
};
|
|
642
|
+
//#endregion
|
|
643
|
+
//#region src/transformers/ruby.ts
|
|
644
|
+
var RUBY_TRANSFORMER$1 = {
|
|
645
|
+
...RUBY_TRANSFORMER,
|
|
646
|
+
dependencies: [RubyNode],
|
|
647
|
+
replace: (textNode, match) => {
|
|
648
|
+
const rubyNode = $createRubyNode(match[2] ?? "");
|
|
649
|
+
const baseText = match[1] ?? "";
|
|
650
|
+
if (baseText) rubyNode.append($createTextNode(baseText));
|
|
651
|
+
textNode.replace(rubyNode);
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
//#endregion
|
|
655
|
+
//#region src/transformers/spoiler.ts
|
|
656
|
+
var SPOILER_TRANSFORMER$1 = {
|
|
657
|
+
...SPOILER_TRANSFORMER,
|
|
658
|
+
dependencies: [SpoilerNode],
|
|
659
|
+
replace: (textNode, match) => {
|
|
660
|
+
const spoilerNode = $createSpoilerNode();
|
|
661
|
+
spoilerNode.append($createTextNode(match[1]));
|
|
662
|
+
textNode.replace(spoilerNode);
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
//#endregion
|
|
666
|
+
//#region src/transformers/table.ts
|
|
667
|
+
var TABLE_ROW_REG_EXP = /^\|(.+)\|\s*$/;
|
|
668
|
+
var TABLE_DIVIDER_REG_EXP = /^\|(?:\s*:?-+:?\s*\|)+\s*$/;
|
|
669
|
+
function parseCells(row) {
|
|
670
|
+
const match = row.match(TABLE_ROW_REG_EXP);
|
|
671
|
+
if (!match) return [];
|
|
672
|
+
return match[1].split("|").map((c) => c.trim());
|
|
673
|
+
}
|
|
674
|
+
//#endregion
|
|
675
|
+
//#region src/transformers/index.ts
|
|
676
|
+
var ALL_TRANSFORMERS = [
|
|
677
|
+
SPOILER_TRANSFORMER$1,
|
|
678
|
+
MENTION_TRANSFORMER$1,
|
|
679
|
+
FOOTNOTE_TRANSFORMER$1,
|
|
680
|
+
INSERT_TRANSFORMER,
|
|
681
|
+
SUPERSCRIPT_TRANSFORMER,
|
|
682
|
+
SUBSCRIPT_TRANSFORMER,
|
|
683
|
+
RUBY_TRANSFORMER$1,
|
|
684
|
+
COMMENT_OPEN_TRANSFORMER,
|
|
685
|
+
COMMENT_TRANSFORMER$1,
|
|
686
|
+
KATEX_INLINE_TRANSFORMER$1,
|
|
687
|
+
FOOTNOTE_SECTION_TRANSFORMER$1,
|
|
688
|
+
CONTAINER_TRANSFORMER$1,
|
|
689
|
+
GIT_ALERT_TRANSFORMER$1,
|
|
690
|
+
CHECK_LIST,
|
|
691
|
+
KATEX_BLOCK_TRANSFORMER$1,
|
|
692
|
+
IMAGE_BLOCK_TRANSFORMER,
|
|
693
|
+
VIDEO_BLOCK_TRANSFORMER,
|
|
694
|
+
CODE_BLOCK_NODE_TRANSFORMER,
|
|
695
|
+
CODE_BLOCK_MULTILINE_TRANSFORMER,
|
|
696
|
+
LINK_CARD_BLOCK_TRANSFORMER,
|
|
697
|
+
MERMAID_BLOCK_TRANSFORMER,
|
|
698
|
+
GRID_CONTAINER_BLOCK_TRANSFORMER,
|
|
699
|
+
HORIZONTAL_RULE_BLOCK_TRANSFORMER$1,
|
|
700
|
+
{
|
|
701
|
+
dependencies: [],
|
|
702
|
+
export: () => null,
|
|
703
|
+
handleImportAfterStartMatch({ lines, rootNode, startLineIndex }) {
|
|
704
|
+
if (startLineIndex + 1 >= lines.length) return null;
|
|
705
|
+
const dividerLine = lines[startLineIndex + 1];
|
|
706
|
+
if (!TABLE_DIVIDER_REG_EXP.test(dividerLine)) return null;
|
|
707
|
+
const headerCells = parseCells(lines[startLineIndex]);
|
|
708
|
+
if (headerCells.length === 0) return null;
|
|
709
|
+
let endLineIndex = startLineIndex + 1;
|
|
710
|
+
const dataRows = [];
|
|
711
|
+
for (let i = startLineIndex + 2; i < lines.length; i++) {
|
|
712
|
+
if (!TABLE_ROW_REG_EXP.test(lines[i])) break;
|
|
713
|
+
dataRows.push(parseCells(lines[i]));
|
|
714
|
+
endLineIndex = i;
|
|
715
|
+
}
|
|
716
|
+
const tableNode = $createTableNode();
|
|
717
|
+
const headerRow = $createTableRowNode();
|
|
718
|
+
for (const cell of headerCells) {
|
|
719
|
+
const cellNode = $createTableCellNode(TableCellHeaderStates.ROW);
|
|
720
|
+
const p = $createParagraphNode();
|
|
721
|
+
p.append($createTextNode(cell));
|
|
722
|
+
cellNode.append(p);
|
|
723
|
+
headerRow.append(cellNode);
|
|
724
|
+
}
|
|
725
|
+
tableNode.append(headerRow);
|
|
726
|
+
for (const row of dataRows) {
|
|
727
|
+
const rowNode = $createTableRowNode();
|
|
728
|
+
for (let i = 0; i < headerCells.length; i++) {
|
|
729
|
+
const cellNode = $createTableCellNode(TableCellHeaderStates.NO_STATUS);
|
|
730
|
+
const p = $createParagraphNode();
|
|
731
|
+
p.append($createTextNode(row[i] ?? ""));
|
|
732
|
+
cellNode.append(p);
|
|
733
|
+
rowNode.append(cellNode);
|
|
734
|
+
}
|
|
735
|
+
tableNode.append(rowNode);
|
|
736
|
+
}
|
|
737
|
+
rootNode.append(tableNode);
|
|
738
|
+
return [true, endLineIndex];
|
|
739
|
+
},
|
|
740
|
+
regExpEnd: {
|
|
741
|
+
optional: true,
|
|
742
|
+
regExp: TABLE_ROW_REG_EXP
|
|
743
|
+
},
|
|
744
|
+
regExpStart: TABLE_ROW_REG_EXP,
|
|
745
|
+
replace: () => {},
|
|
746
|
+
type: "multiline-element"
|
|
747
|
+
},
|
|
748
|
+
TABLE_BLOCK_TRANSFORMER,
|
|
749
|
+
QUOTE_TRANSFORMER,
|
|
750
|
+
...TRANSFORMERS.filter((t) => t !== QUOTE && t !== CODE)
|
|
751
|
+
];
|
|
752
|
+
//#endregion
|
|
753
|
+
//#region src/plugins/MarkdownPastePlugin.tsx
|
|
754
|
+
function getVSCodePasteData(clipboardData) {
|
|
755
|
+
const raw = clipboardData.getData("vscode-editor-data");
|
|
756
|
+
if (!raw) return null;
|
|
757
|
+
try {
|
|
758
|
+
return { language: JSON.parse(raw).mode || "text" };
|
|
759
|
+
} catch {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function hasRichHTML(clipboardData) {
|
|
764
|
+
const html = clipboardData.getData("text/html");
|
|
765
|
+
if (!html) return false;
|
|
766
|
+
if (/data-vscode|vscode-/i.test(html)) return false;
|
|
767
|
+
return new DOMParser().parseFromString(html, "text/html").body.querySelectorAll("strong,em,b,i,h1,h2,h3,h4,h5,h6,ul,ol,table,img,blockquote,pre>code,a[href]").length > 0;
|
|
768
|
+
}
|
|
769
|
+
function detectMarkdown(text) {
|
|
770
|
+
let score = 0;
|
|
771
|
+
if (/^#{1,6}\s+\S/m.test(text)) score += 5;
|
|
772
|
+
if (/^```[\w-]*$/m.test(text)) score += 5;
|
|
773
|
+
if (/\[[^\]]+\]\([^)]+\)/.test(text)) score += 4;
|
|
774
|
+
if (/!\[[^\]]*\]\([^)]+\)/.test(text)) score += 5;
|
|
775
|
+
if (/^\|.+\|$/m.test(text) && /^\|[\s:|-]+\|$/m.test(text)) score += 5;
|
|
776
|
+
if (/^>\s*\[!(?:note|tip|warning|caution|important)\]/im.test(text)) score += 5;
|
|
777
|
+
if (/^[*-]\s+\[[ x]\]/m.test(text)) score += 4;
|
|
778
|
+
if (/\*\*.+?\*\*/.test(text)) score += 2;
|
|
779
|
+
if (/(?<!\*)\*(?!\*)(?!\s).+?(?<!\s)(?<!\*)\*(?!\*)/.test(text)) score += 1;
|
|
780
|
+
if (/^[*+-]\s+\S/m.test(text)) score += 1;
|
|
781
|
+
if (/^\d+\.\s+\S/m.test(text)) score += 1;
|
|
782
|
+
if (/^>\s+\S/m.test(text)) score += 1;
|
|
783
|
+
if (/`.+?`/.test(text)) score += 1;
|
|
784
|
+
if (/^[*_-]{3,}$/m.test(text)) score += 2;
|
|
785
|
+
if (text.split(/\n{2,}/).filter(Boolean).length >= 2) score += 5;
|
|
786
|
+
if (text.length < 20) score -= 3;
|
|
787
|
+
if (!text.includes("\n")) score -= 2;
|
|
788
|
+
return score >= 5;
|
|
789
|
+
}
|
|
790
|
+
function convertAndInsert(markdown) {
|
|
791
|
+
let conversionError = null;
|
|
792
|
+
const tempEditor = createEditor({
|
|
793
|
+
namespace: "markdown-paste-temp",
|
|
794
|
+
nodes: getResolvedEditNodes(),
|
|
795
|
+
onError: (error) => {
|
|
796
|
+
conversionError = error;
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
tempEditor.update(() => {
|
|
800
|
+
$convertFromMarkdownString(markdown, ALL_TRANSFORMERS);
|
|
801
|
+
}, { discrete: true });
|
|
802
|
+
if (conversionError) {
|
|
803
|
+
console.error("MarkdownPastePlugin: convertAndInsert error", conversionError);
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
const serializedChildren = tempEditor.getEditorState().toJSON().root.children;
|
|
807
|
+
if (!serializedChildren.length) return false;
|
|
808
|
+
$insertNodes(serializedChildren.map((s) => $parseSerializedNode(s)));
|
|
809
|
+
return true;
|
|
810
|
+
}
|
|
811
|
+
function MarkdownPastePlugin() {
|
|
812
|
+
const [editor] = useLexicalComposerContext();
|
|
813
|
+
useEffect(() => {
|
|
814
|
+
return editor.registerCommand(PASTE_COMMAND, (event) => {
|
|
815
|
+
const clipboardData = "clipboardData" in event ? event.clipboardData : null;
|
|
816
|
+
if (!clipboardData) return false;
|
|
817
|
+
if (clipboardData.getData("application/x-lexical-editor")) return false;
|
|
818
|
+
if (Array.from(clipboardData.files).some((f) => f.type.startsWith("image/"))) return false;
|
|
819
|
+
const vscodeData = getVSCodePasteData(clipboardData);
|
|
820
|
+
if (vscodeData) {
|
|
821
|
+
const code = clipboardData.getData("text/plain");
|
|
822
|
+
if (code) {
|
|
823
|
+
$insertNodes(code.split(/\r?\n{3,}/).filter(Boolean).map((s) => $createCodeBlockEditNode(s.replace(/^\s*\n/, "").replace(/\n\s*$/, ""), vscodeData.language)));
|
|
824
|
+
event.preventDefault();
|
|
825
|
+
return true;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (hasRichHTML(clipboardData)) return false;
|
|
829
|
+
const text = clipboardData.getData("text/plain");
|
|
830
|
+
if (!text || !detectMarkdown(text)) return false;
|
|
831
|
+
try {
|
|
832
|
+
if (!convertAndInsert(text)) return false;
|
|
833
|
+
event.preventDefault();
|
|
834
|
+
return true;
|
|
835
|
+
} catch (error) {
|
|
836
|
+
console.error("MarkdownPastePlugin: paste error", error);
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
}, COMMAND_PRIORITY_HIGH);
|
|
840
|
+
}, [editor]);
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
//#endregion
|
|
844
|
+
//#region src/plugins/MarkdownShortcutsPlugin.tsx
|
|
845
|
+
function MarkdownShortcutsPlugin() {
|
|
846
|
+
return /* @__PURE__ */ jsx(MarkdownShortcutPlugin, { transformers: ALL_TRANSFORMERS });
|
|
847
|
+
}
|
|
848
|
+
//#endregion
|
|
849
|
+
//#region src/components/CorePlugins.tsx
|
|
850
|
+
function CorePlugins() {
|
|
851
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
852
|
+
/* @__PURE__ */ jsx(ListPlugin, {}),
|
|
853
|
+
/* @__PURE__ */ jsx(LinkPlugin, {}),
|
|
854
|
+
/* @__PURE__ */ jsx(TabIndentationPlugin, {}),
|
|
855
|
+
/* @__PURE__ */ jsx(TablePlugin, {}),
|
|
856
|
+
/* @__PURE__ */ jsx(CheckListPlugin, {}),
|
|
857
|
+
/* @__PURE__ */ jsx(MarkdownShortcutsPlugin, {}),
|
|
858
|
+
/* @__PURE__ */ jsx(MarkdownPastePlugin, {}),
|
|
859
|
+
/* @__PURE__ */ jsx(BlockExitPlugin, {}),
|
|
860
|
+
/* @__PURE__ */ jsx(HorizontalRulePlugin, {}),
|
|
861
|
+
/* @__PURE__ */ jsx(AutoLinkPlugin, {})
|
|
862
|
+
] });
|
|
863
|
+
}
|
|
864
|
+
//#endregion
|
|
865
|
+
//#region src/context/ImageUploadContext.tsx
|
|
866
|
+
var ImageUploadContext = createContext(null);
|
|
867
|
+
function ImageUploadProvider({ upload, children }) {
|
|
868
|
+
return /* @__PURE__ */ jsx(ImageUploadContext.Provider, {
|
|
869
|
+
value: upload,
|
|
870
|
+
children
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
function useImageUpload() {
|
|
874
|
+
return use(ImageUploadContext);
|
|
875
|
+
}
|
|
876
|
+
//#endregion
|
|
877
|
+
//#region src/plugins/BlockIdPlugin.tsx
|
|
878
|
+
var blockIdState = createState("blockId", { parse: (v) => typeof v === "string" ? v : "" });
|
|
879
|
+
var NORMALIZATION_TAG = "block-id-normalization";
|
|
880
|
+
function buildPreviousIdIndex(editorState) {
|
|
881
|
+
return editorState.read(() => {
|
|
882
|
+
const map = /* @__PURE__ */ new Map();
|
|
883
|
+
for (const child of $getRoot().getChildren()) {
|
|
884
|
+
const id = $getState(child, blockIdState);
|
|
885
|
+
if (!id) continue;
|
|
886
|
+
const set = map.get(id) ?? /* @__PURE__ */ new Set();
|
|
887
|
+
set.add(child.getKey());
|
|
888
|
+
map.set(id, set);
|
|
889
|
+
}
|
|
890
|
+
return map;
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
function collectRootChildren(editorState) {
|
|
894
|
+
return editorState.read(() => $getRoot().getChildren().map((child) => $getState(child, blockIdState)));
|
|
895
|
+
}
|
|
896
|
+
function hasDuplicateOrMissingId(children) {
|
|
897
|
+
const seen = /* @__PURE__ */ new Set();
|
|
898
|
+
for (const id of children) {
|
|
899
|
+
if (!id || seen.has(id)) return true;
|
|
900
|
+
seen.add(id);
|
|
901
|
+
}
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
function generateBlockId(used) {
|
|
905
|
+
while (true) {
|
|
906
|
+
const id = nanoid(8);
|
|
907
|
+
if (!used.has(id)) return id;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
function pickKeeperKey(nodes, previousKeys) {
|
|
911
|
+
if (!nodes.length) return null;
|
|
912
|
+
if (previousKeys?.size) {
|
|
913
|
+
for (const node of nodes) if (previousKeys.has(node.getKey())) return node.getKey();
|
|
914
|
+
}
|
|
915
|
+
return nodes[0]?.getKey() ?? null;
|
|
916
|
+
}
|
|
917
|
+
function normalizeRootBlockIds(editor, previousIdIndex) {
|
|
918
|
+
editor.update(() => {
|
|
919
|
+
$addUpdateTag("history-merge");
|
|
920
|
+
const children = $getRoot().getChildren();
|
|
921
|
+
const groupedById = /* @__PURE__ */ new Map();
|
|
922
|
+
for (const child of children) {
|
|
923
|
+
const id = $getState(child, blockIdState);
|
|
924
|
+
if (!id) continue;
|
|
925
|
+
const bucket = groupedById.get(id) ?? [];
|
|
926
|
+
bucket.push(child);
|
|
927
|
+
groupedById.set(id, bucket);
|
|
928
|
+
}
|
|
929
|
+
const keeperById = /* @__PURE__ */ new Map();
|
|
930
|
+
for (const [id, nodes] of groupedById) {
|
|
931
|
+
if (nodes.length <= 1) continue;
|
|
932
|
+
const keeperKey = pickKeeperKey(nodes, previousIdIndex.get(id));
|
|
933
|
+
if (keeperKey) keeperById.set(id, keeperKey);
|
|
934
|
+
}
|
|
935
|
+
const used = /* @__PURE__ */ new Set();
|
|
936
|
+
for (const child of children) {
|
|
937
|
+
let id = $getState(child, blockIdState);
|
|
938
|
+
const keeperKey = id ? keeperById.get(id) : null;
|
|
939
|
+
if (!id || used.has(id) || keeperKey !== void 0 && child.getKey() !== keeperKey) {
|
|
940
|
+
id = generateBlockId(used);
|
|
941
|
+
$setState(child, blockIdState, id);
|
|
942
|
+
}
|
|
943
|
+
used.add(id);
|
|
944
|
+
}
|
|
945
|
+
}, { tag: NORMALIZATION_TAG });
|
|
946
|
+
}
|
|
947
|
+
function syncBlockIdAttributes(editor) {
|
|
948
|
+
editor.getEditorState().read(() => {
|
|
949
|
+
for (const child of $getRoot().getChildren()) {
|
|
950
|
+
const id = $getState(child, blockIdState);
|
|
951
|
+
if (!id) continue;
|
|
952
|
+
const dom = editor.getElementByKey(child.getKey());
|
|
953
|
+
if (dom && dom.getAttribute("data-block-id") !== id) dom.setAttribute("data-block-id", id);
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
function BlockIdPlugin() {
|
|
958
|
+
const [editor] = useLexicalComposerContext();
|
|
959
|
+
useEffect(() => {
|
|
960
|
+
if (hasDuplicateOrMissingId(collectRootChildren(editor.getEditorState()))) normalizeRootBlockIds(editor, /* @__PURE__ */ new Map());
|
|
961
|
+
syncBlockIdAttributes(editor);
|
|
962
|
+
return editor.registerUpdateListener(({ tags, editorState, prevEditorState }) => {
|
|
963
|
+
if (tags.has(NORMALIZATION_TAG)) {
|
|
964
|
+
syncBlockIdAttributes(editor);
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
if (!hasDuplicateOrMissingId(collectRootChildren(editorState))) {
|
|
968
|
+
syncBlockIdAttributes(editor);
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
normalizeRootBlockIds(editor, buildPreviousIdIndex(prevEditorState));
|
|
972
|
+
});
|
|
973
|
+
}, [editor]);
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
//#endregion
|
|
977
|
+
//#region src/plugins/image-upload.css.ts
|
|
978
|
+
var draggingWrapperClass = "rich-image-upload-dragging";
|
|
979
|
+
var toastVariant = {
|
|
980
|
+
info: "_1x58dbf3",
|
|
981
|
+
success: "_1x58dbf4",
|
|
982
|
+
error: "_1x58dbf5"
|
|
983
|
+
};
|
|
984
|
+
var spinner = "_1x58dbf7";
|
|
985
|
+
var dialogPopup = "_1x58dbf8";
|
|
986
|
+
var dialogHeader = "_1x58dbf9";
|
|
987
|
+
var dialogTitle = "_1x58dbfa";
|
|
988
|
+
var dialogBody = "_1x58dbfb";
|
|
989
|
+
var tabWrap = "_1x58dbfc";
|
|
990
|
+
var uploadDropzone = "_1x58dbfd";
|
|
991
|
+
var uploadDropzoneState = {
|
|
992
|
+
idle: "_1x58dbfe",
|
|
993
|
+
active: "_1x58dbff",
|
|
994
|
+
busy: "_1x58dbfg"
|
|
995
|
+
};
|
|
996
|
+
var uploadDropIcon = "_1x58dbfh";
|
|
997
|
+
var uploadDropTitle = "_1x58dbfi";
|
|
998
|
+
var uploadDropDesc = "_1x58dbfj";
|
|
999
|
+
var hiddenInput = "_1x58dbfk";
|
|
1000
|
+
var uploadBusyWrap = "_1x58dbfl";
|
|
1001
|
+
var uploadProgress = "_1x58dbfm";
|
|
1002
|
+
var urlInputRow = "_1x58dbfn";
|
|
1003
|
+
var textInput = "_1x58dbfo";
|
|
1004
|
+
var helperText = "_1x58dbfr";
|
|
1005
|
+
var dialogFooter = "_1x58dbfs";
|
|
1006
|
+
//#endregion
|
|
1007
|
+
//#region src/plugins/ImageUploadPlugin.tsx
|
|
1008
|
+
function isImageFile(file) {
|
|
1009
|
+
return file.type.startsWith("image/");
|
|
1010
|
+
}
|
|
1011
|
+
function hasImageData(dataTransfer) {
|
|
1012
|
+
if (!dataTransfer) return false;
|
|
1013
|
+
if ([...dataTransfer.files].some(isImageFile)) return true;
|
|
1014
|
+
return [...dataTransfer.items].some((item) => item.type.startsWith("image/"));
|
|
1015
|
+
}
|
|
1016
|
+
var UNSAFE_URL_RE = /^(?:javascript\s*:|vbscript\s*:|data\s*:(?!image\/))/i;
|
|
1017
|
+
function isSafeImageUrl(url) {
|
|
1018
|
+
return !UNSAFE_URL_RE.test(url);
|
|
1019
|
+
}
|
|
1020
|
+
function loadImageByUrl(src) {
|
|
1021
|
+
return new Promise((resolve, reject) => {
|
|
1022
|
+
const image = new Image();
|
|
1023
|
+
image.onload = () => {
|
|
1024
|
+
resolve({
|
|
1025
|
+
width: image.naturalWidth || image.width,
|
|
1026
|
+
height: image.naturalHeight || image.height
|
|
1027
|
+
});
|
|
1028
|
+
};
|
|
1029
|
+
image.onerror = () => reject(/* @__PURE__ */ new Error("Failed to load image"));
|
|
1030
|
+
image.src = src;
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
function readAsDataUrl(file) {
|
|
1034
|
+
return new Promise((resolve, reject) => {
|
|
1035
|
+
const reader = new FileReader();
|
|
1036
|
+
reader.onload = () => resolve(String(reader.result));
|
|
1037
|
+
reader.onerror = () => reject(reader.error ?? /* @__PURE__ */ new Error("File read failed"));
|
|
1038
|
+
reader.readAsDataURL(file);
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
async function defaultImageUpload(file) {
|
|
1042
|
+
return {
|
|
1043
|
+
src: await readAsDataUrl(file),
|
|
1044
|
+
altText: file.name
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
function ImageUploadPlugin({ onUpload }) {
|
|
1048
|
+
const [editor] = useLexicalComposerContext();
|
|
1049
|
+
const uploadRef = useRef(onUpload);
|
|
1050
|
+
uploadRef.current = onUpload;
|
|
1051
|
+
const fileInputRef = useRef(null);
|
|
1052
|
+
const toastTimerRef = useRef(null);
|
|
1053
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
1054
|
+
const [tab, setTab] = useState("upload");
|
|
1055
|
+
const [rootDragActive, setRootDragActive] = useState(false);
|
|
1056
|
+
const [dialogDragActive, setDialogDragActive] = useState(false);
|
|
1057
|
+
const [pendingUploads, setPendingUploads] = useState(0);
|
|
1058
|
+
const [dialogUploading, setDialogUploading] = useState(false);
|
|
1059
|
+
const [toast$1, setToast] = useState(null);
|
|
1060
|
+
const [urlInput, setUrlInput] = useState("");
|
|
1061
|
+
const [urlPreview$1, setUrlPreview] = useState(null);
|
|
1062
|
+
const [urlMeta, setUrlMeta] = useState(null);
|
|
1063
|
+
const [urlLoading, setUrlLoading] = useState(false);
|
|
1064
|
+
const [urlError, setUrlError] = useState(null);
|
|
1065
|
+
const tabItems = useMemo(() => [{
|
|
1066
|
+
value: "upload",
|
|
1067
|
+
label: "Upload"
|
|
1068
|
+
}, {
|
|
1069
|
+
value: "url",
|
|
1070
|
+
label: "URL"
|
|
1071
|
+
}], []);
|
|
1072
|
+
const pushToast = useCallback((kind, message) => {
|
|
1073
|
+
setToast({
|
|
1074
|
+
kind,
|
|
1075
|
+
message
|
|
1076
|
+
});
|
|
1077
|
+
if (toastTimerRef.current) window.clearTimeout(toastTimerRef.current);
|
|
1078
|
+
toastTimerRef.current = window.setTimeout(setToast, 2200, null);
|
|
1079
|
+
}, []);
|
|
1080
|
+
const insertByUpload = useCallback(async (file, options) => {
|
|
1081
|
+
if (!isImageFile(file)) return false;
|
|
1082
|
+
const closeDialog = Boolean(options?.closeDialog);
|
|
1083
|
+
setPendingUploads((value) => value + 1);
|
|
1084
|
+
if (closeDialog) setDialogUploading(true);
|
|
1085
|
+
try {
|
|
1086
|
+
const [result, meta] = await Promise.all([uploadRef.current(file), computeImageMeta(file)]);
|
|
1087
|
+
editor.update(() => {
|
|
1088
|
+
$insertNodes([$createImageNode({
|
|
1089
|
+
src: result.src,
|
|
1090
|
+
altText: result.altText ?? file.name,
|
|
1091
|
+
width: result.width ?? meta.width,
|
|
1092
|
+
height: result.height ?? meta.height,
|
|
1093
|
+
thumbhash: result.thumbhash ?? meta.thumbhash
|
|
1094
|
+
})]);
|
|
1095
|
+
});
|
|
1096
|
+
if (closeDialog) {
|
|
1097
|
+
setDialogOpen(false);
|
|
1098
|
+
setUrlInput("");
|
|
1099
|
+
setUrlPreview(null);
|
|
1100
|
+
setUrlMeta(null);
|
|
1101
|
+
setUrlError(null);
|
|
1102
|
+
}
|
|
1103
|
+
pushToast("success", "Image uploaded");
|
|
1104
|
+
return true;
|
|
1105
|
+
} catch (err) {
|
|
1106
|
+
console.error("[ImageUploadPlugin]", err);
|
|
1107
|
+
pushToast("error", "Image upload failed");
|
|
1108
|
+
return false;
|
|
1109
|
+
} finally {
|
|
1110
|
+
setPendingUploads((value) => Math.max(value - 1, 0));
|
|
1111
|
+
setDialogUploading(false);
|
|
1112
|
+
}
|
|
1113
|
+
}, [editor, pushToast]);
|
|
1114
|
+
const handleFiles = useCallback((files) => {
|
|
1115
|
+
const images = files.filter(isImageFile);
|
|
1116
|
+
if (images.length === 0) return false;
|
|
1117
|
+
for (const file of images) insertByUpload(file);
|
|
1118
|
+
return true;
|
|
1119
|
+
}, [insertByUpload]);
|
|
1120
|
+
useEffect(() => {
|
|
1121
|
+
const unregisterDragDrop = editor.registerCommand(DRAG_DROP_PASTE, (files) => handleFiles(files), COMMAND_PRIORITY_HIGH);
|
|
1122
|
+
const unregisterPaste = editor.registerCommand(PASTE_COMMAND, (event) => {
|
|
1123
|
+
const clipboardData = "clipboardData" in event ? event.clipboardData : null;
|
|
1124
|
+
if (!clipboardData) return false;
|
|
1125
|
+
const files = [...clipboardData.files];
|
|
1126
|
+
if (files.some(isImageFile)) return handleFiles(files);
|
|
1127
|
+
return false;
|
|
1128
|
+
}, COMMAND_PRIORITY_HIGH);
|
|
1129
|
+
const unregisterOpenDialog = editor.registerCommand(OPEN_IMAGE_UPLOAD_DIALOG_COMMAND, () => {
|
|
1130
|
+
setDialogOpen(true);
|
|
1131
|
+
return true;
|
|
1132
|
+
}, COMMAND_PRIORITY_EDITOR);
|
|
1133
|
+
const rootElement = editor.getRootElement();
|
|
1134
|
+
const wrapper = rootElement?.parentElement ?? null;
|
|
1135
|
+
if (!wrapper) return () => {
|
|
1136
|
+
unregisterDragDrop();
|
|
1137
|
+
unregisterPaste();
|
|
1138
|
+
unregisterOpenDialog();
|
|
1139
|
+
};
|
|
1140
|
+
let dragCounter = 0;
|
|
1141
|
+
const setWrapperDragging = (next) => {
|
|
1142
|
+
setRootDragActive(next);
|
|
1143
|
+
wrapper.classList.toggle(draggingWrapperClass, next);
|
|
1144
|
+
};
|
|
1145
|
+
const onDragEnter = (event) => {
|
|
1146
|
+
if (!hasImageData(event.dataTransfer)) return;
|
|
1147
|
+
dragCounter += 1;
|
|
1148
|
+
setWrapperDragging(true);
|
|
1149
|
+
};
|
|
1150
|
+
const onDragOver = (event) => {
|
|
1151
|
+
if (!hasImageData(event.dataTransfer)) return;
|
|
1152
|
+
event.preventDefault();
|
|
1153
|
+
};
|
|
1154
|
+
const onDragLeave = () => {
|
|
1155
|
+
dragCounter = Math.max(dragCounter - 1, 0);
|
|
1156
|
+
if (dragCounter === 0) setWrapperDragging(false);
|
|
1157
|
+
};
|
|
1158
|
+
const onDrop = () => {
|
|
1159
|
+
dragCounter = 0;
|
|
1160
|
+
setWrapperDragging(false);
|
|
1161
|
+
};
|
|
1162
|
+
rootElement?.addEventListener("dragenter", onDragEnter);
|
|
1163
|
+
rootElement?.addEventListener("dragover", onDragOver);
|
|
1164
|
+
rootElement?.addEventListener("dragleave", onDragLeave);
|
|
1165
|
+
rootElement?.addEventListener("drop", onDrop);
|
|
1166
|
+
return () => {
|
|
1167
|
+
if (toastTimerRef.current) window.clearTimeout(toastTimerRef.current);
|
|
1168
|
+
unregisterDragDrop();
|
|
1169
|
+
unregisterPaste();
|
|
1170
|
+
unregisterOpenDialog();
|
|
1171
|
+
setWrapperDragging(false);
|
|
1172
|
+
rootElement?.removeEventListener("dragenter", onDragEnter);
|
|
1173
|
+
rootElement?.removeEventListener("dragover", onDragOver);
|
|
1174
|
+
rootElement?.removeEventListener("dragleave", onDragLeave);
|
|
1175
|
+
rootElement?.removeEventListener("drop", onDrop);
|
|
1176
|
+
};
|
|
1177
|
+
}, [editor, handleFiles]);
|
|
1178
|
+
const resetUrlState = useCallback(() => {
|
|
1179
|
+
setUrlInput("");
|
|
1180
|
+
setUrlPreview(null);
|
|
1181
|
+
setUrlMeta(null);
|
|
1182
|
+
setUrlError(null);
|
|
1183
|
+
setUrlLoading(false);
|
|
1184
|
+
}, []);
|
|
1185
|
+
const handleDialogFile = useCallback(async (file) => {
|
|
1186
|
+
if (!file) return;
|
|
1187
|
+
await insertByUpload(file, { closeDialog: true });
|
|
1188
|
+
}, [insertByUpload]);
|
|
1189
|
+
const handleUrlPreview = useCallback(async () => {
|
|
1190
|
+
const nextUrl = urlInput.trim();
|
|
1191
|
+
if (!nextUrl) return;
|
|
1192
|
+
if (!isSafeImageUrl(nextUrl)) {
|
|
1193
|
+
setUrlError("Unsupported URL scheme");
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
setUrlLoading(true);
|
|
1197
|
+
setUrlError(null);
|
|
1198
|
+
try {
|
|
1199
|
+
setUrlMeta(await loadImageByUrl(nextUrl));
|
|
1200
|
+
setUrlPreview(nextUrl);
|
|
1201
|
+
} catch {
|
|
1202
|
+
setUrlPreview(null);
|
|
1203
|
+
setUrlMeta(null);
|
|
1204
|
+
setUrlError("Could not load this image URL");
|
|
1205
|
+
} finally {
|
|
1206
|
+
setUrlLoading(false);
|
|
1207
|
+
}
|
|
1208
|
+
}, [urlInput]);
|
|
1209
|
+
const handleInsertByUrl = useCallback(() => {
|
|
1210
|
+
if (!urlPreview$1 || !isSafeImageUrl(urlPreview$1)) return;
|
|
1211
|
+
editor.update(() => {
|
|
1212
|
+
$insertNodes([$createImageNode({
|
|
1213
|
+
src: urlPreview$1,
|
|
1214
|
+
altText: "",
|
|
1215
|
+
width: urlMeta?.width,
|
|
1216
|
+
height: urlMeta?.height
|
|
1217
|
+
})]);
|
|
1218
|
+
});
|
|
1219
|
+
pushToast("success", "Image inserted");
|
|
1220
|
+
setDialogOpen(false);
|
|
1221
|
+
resetUrlState();
|
|
1222
|
+
}, [
|
|
1223
|
+
editor,
|
|
1224
|
+
pushToast,
|
|
1225
|
+
resetUrlState,
|
|
1226
|
+
urlMeta,
|
|
1227
|
+
urlPreview$1
|
|
1228
|
+
]);
|
|
1229
|
+
const helperMessage = pendingUploads > 0 ? `Uploading ${pendingUploads} image${pendingUploads > 1 ? "s" : ""}...` : rootDragActive ? "Drop image files to upload" : null;
|
|
1230
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [(helperMessage || toast$1) && /* @__PURE__ */ jsxs("div", {
|
|
1231
|
+
className: "_1x58dbf1",
|
|
1232
|
+
children: [helperMessage && /* @__PURE__ */ jsxs("div", {
|
|
1233
|
+
className: `_1x58dbf2 ${toastVariant.info}`,
|
|
1234
|
+
children: [/* @__PURE__ */ jsx("span", { className: "_1x58dbf7" }), helperMessage]
|
|
1235
|
+
}), toast$1 && /* @__PURE__ */ jsxs("div", {
|
|
1236
|
+
className: `_1x58dbf2 ${toastVariant[toast$1.kind]}`,
|
|
1237
|
+
children: [toast$1.kind === "success" ? /* @__PURE__ */ jsx(Check, { size: 12 }) : /* @__PURE__ */ jsx(Info, { size: 12 }), toast$1.message]
|
|
1238
|
+
})]
|
|
1239
|
+
}), /* @__PURE__ */ jsx(Dialog, {
|
|
1240
|
+
open: dialogOpen,
|
|
1241
|
+
onOpenChange: (nextOpen) => {
|
|
1242
|
+
if (!nextOpen && dialogUploading) return;
|
|
1243
|
+
setDialogOpen(nextOpen);
|
|
1244
|
+
if (!nextOpen) {
|
|
1245
|
+
resetUrlState();
|
|
1246
|
+
setDialogUploading(false);
|
|
1247
|
+
setDialogDragActive(false);
|
|
1248
|
+
}
|
|
1249
|
+
},
|
|
1250
|
+
children: /* @__PURE__ */ jsxs(DialogPopup, {
|
|
1251
|
+
className: dialogPopup,
|
|
1252
|
+
showCloseButton: !dialogUploading,
|
|
1253
|
+
children: [
|
|
1254
|
+
/* @__PURE__ */ jsx("div", {
|
|
1255
|
+
className: dialogHeader,
|
|
1256
|
+
children: /* @__PURE__ */ jsx(DialogTitle, {
|
|
1257
|
+
className: dialogTitle,
|
|
1258
|
+
children: "Insert image"
|
|
1259
|
+
})
|
|
1260
|
+
}),
|
|
1261
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1262
|
+
className: dialogBody,
|
|
1263
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1264
|
+
className: tabWrap,
|
|
1265
|
+
children: /* @__PURE__ */ jsx(SegmentedControl, {
|
|
1266
|
+
fullWidth: true,
|
|
1267
|
+
items: tabItems,
|
|
1268
|
+
value: tab,
|
|
1269
|
+
onChange: setTab
|
|
1270
|
+
})
|
|
1271
|
+
}), tab === "upload" ? /* @__PURE__ */ jsxs("div", {
|
|
1272
|
+
className: `${uploadDropzone} ${uploadDropzoneState[dialogUploading ? "busy" : dialogDragActive ? "active" : "idle"]}`,
|
|
1273
|
+
role: "button",
|
|
1274
|
+
tabIndex: 0,
|
|
1275
|
+
onDragLeave: () => setDialogDragActive(false),
|
|
1276
|
+
onClick: () => {
|
|
1277
|
+
if (!dialogUploading) fileInputRef.current?.click();
|
|
1278
|
+
},
|
|
1279
|
+
onDragEnter: (event) => {
|
|
1280
|
+
if (hasImageData(event.dataTransfer)) {
|
|
1281
|
+
event.preventDefault();
|
|
1282
|
+
setDialogDragActive(true);
|
|
1283
|
+
}
|
|
1284
|
+
},
|
|
1285
|
+
onDragOver: (event) => {
|
|
1286
|
+
if (hasImageData(event.dataTransfer)) event.preventDefault();
|
|
1287
|
+
},
|
|
1288
|
+
onDrop: (event) => {
|
|
1289
|
+
event.preventDefault();
|
|
1290
|
+
setDialogDragActive(false);
|
|
1291
|
+
handleDialogFile([...event.dataTransfer.files].find(isImageFile) ?? null);
|
|
1292
|
+
},
|
|
1293
|
+
onKeyDown: (event) => {
|
|
1294
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
1295
|
+
event.preventDefault();
|
|
1296
|
+
if (!dialogUploading) fileInputRef.current?.click();
|
|
1297
|
+
},
|
|
1298
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
1299
|
+
accept: "image/*",
|
|
1300
|
+
className: hiddenInput,
|
|
1301
|
+
ref: fileInputRef,
|
|
1302
|
+
type: "file",
|
|
1303
|
+
onChange: (event) => {
|
|
1304
|
+
handleDialogFile(event.currentTarget.files?.[0] ?? null);
|
|
1305
|
+
event.currentTarget.value = "";
|
|
1306
|
+
}
|
|
1307
|
+
}), dialogUploading ? /* @__PURE__ */ jsx("div", {
|
|
1308
|
+
className: uploadBusyWrap,
|
|
1309
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1310
|
+
className: uploadProgress,
|
|
1311
|
+
children: [/* @__PURE__ */ jsx("span", { className: spinner }), "Uploading image..."]
|
|
1312
|
+
})
|
|
1313
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1314
|
+
/* @__PURE__ */ jsx("span", {
|
|
1315
|
+
className: uploadDropIcon,
|
|
1316
|
+
children: /* @__PURE__ */ jsx(Upload, { size: 18 })
|
|
1317
|
+
}),
|
|
1318
|
+
/* @__PURE__ */ jsx("span", {
|
|
1319
|
+
className: uploadDropTitle,
|
|
1320
|
+
children: "Click to upload or drag and drop"
|
|
1321
|
+
}),
|
|
1322
|
+
/* @__PURE__ */ jsx("span", {
|
|
1323
|
+
className: uploadDropDesc,
|
|
1324
|
+
children: "PNG, JPG, GIF, WebP"
|
|
1325
|
+
})
|
|
1326
|
+
] })]
|
|
1327
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1328
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1329
|
+
className: urlInputRow,
|
|
1330
|
+
children: [
|
|
1331
|
+
/* @__PURE__ */ jsx("input", {
|
|
1332
|
+
className: textInput,
|
|
1333
|
+
placeholder: "https://example.com/image.jpg",
|
|
1334
|
+
type: "url",
|
|
1335
|
+
value: urlInput,
|
|
1336
|
+
onChange: (event) => {
|
|
1337
|
+
setUrlInput(event.target.value);
|
|
1338
|
+
setUrlError(null);
|
|
1339
|
+
setUrlPreview(null);
|
|
1340
|
+
setUrlMeta(null);
|
|
1341
|
+
},
|
|
1342
|
+
onKeyDown: (event) => {
|
|
1343
|
+
if (event.key === "Enter") {
|
|
1344
|
+
event.preventDefault();
|
|
1345
|
+
if (urlPreview$1) handleInsertByUrl();
|
|
1346
|
+
else handleUrlPreview();
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}),
|
|
1350
|
+
/* @__PURE__ */ jsx(ActionButton, {
|
|
1351
|
+
disabled: urlLoading || !urlInput.trim(),
|
|
1352
|
+
size: "md",
|
|
1353
|
+
variant: "outline",
|
|
1354
|
+
onClick: () => void handleUrlPreview(),
|
|
1355
|
+
children: urlLoading ? "Loading" : "Preview"
|
|
1356
|
+
}),
|
|
1357
|
+
/* @__PURE__ */ jsx(ActionButton, {
|
|
1358
|
+
disabled: !urlPreview$1,
|
|
1359
|
+
size: "md",
|
|
1360
|
+
variant: "accent",
|
|
1361
|
+
onClick: handleInsertByUrl,
|
|
1362
|
+
children: "Insert"
|
|
1363
|
+
})
|
|
1364
|
+
]
|
|
1365
|
+
}),
|
|
1366
|
+
urlError && /* @__PURE__ */ jsxs("span", {
|
|
1367
|
+
className: `_1x58dbfr ${toastVariant.error}`,
|
|
1368
|
+
children: [/* @__PURE__ */ jsx(Info, { size: 12 }), urlError]
|
|
1369
|
+
}),
|
|
1370
|
+
urlPreview$1 && /* @__PURE__ */ jsx("div", {
|
|
1371
|
+
className: "_1x58dbfp",
|
|
1372
|
+
children: /* @__PURE__ */ jsx("img", {
|
|
1373
|
+
alt: "Preview",
|
|
1374
|
+
className: "_1x58dbfq",
|
|
1375
|
+
src: urlPreview$1
|
|
1376
|
+
})
|
|
1377
|
+
})
|
|
1378
|
+
] })]
|
|
1379
|
+
}),
|
|
1380
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1381
|
+
className: dialogFooter,
|
|
1382
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
1383
|
+
className: helperText,
|
|
1384
|
+
children: [/* @__PURE__ */ jsx(Link2, { size: 12 }), "You can also paste images or drag files directly into the editor."]
|
|
1385
|
+
}), /* @__PURE__ */ jsx(ActionBar, { children: /* @__PURE__ */ jsx(ActionButton, {
|
|
1386
|
+
disabled: dialogUploading,
|
|
1387
|
+
size: "md",
|
|
1388
|
+
variant: "outline",
|
|
1389
|
+
onClick: () => setDialogOpen(false),
|
|
1390
|
+
children: "Close"
|
|
1391
|
+
}) })]
|
|
1392
|
+
})
|
|
1393
|
+
]
|
|
1394
|
+
})
|
|
1395
|
+
})] });
|
|
1396
|
+
}
|
|
1397
|
+
//#endregion
|
|
1398
|
+
//#region src/plugins/LinkFaviconPlugin.tsx
|
|
1399
|
+
function applyFavicon(dom, href) {
|
|
1400
|
+
if (dom.dataset.faviconHref === href) return;
|
|
1401
|
+
dom.dataset.faviconHref = href;
|
|
1402
|
+
const hostname = getHostname(href);
|
|
1403
|
+
if (!hostname) return;
|
|
1404
|
+
probeFavicon(hostname).then((faviconUrl) => {
|
|
1405
|
+
if (faviconUrl) {
|
|
1406
|
+
dom.style.setProperty("--rc-link-favicon", `url(${faviconUrl})`);
|
|
1407
|
+
dom.dataset.favicon = "loaded";
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
function LinkFaviconPlugin() {
|
|
1412
|
+
const [editor] = useLexicalComposerContext();
|
|
1413
|
+
useEffect(() => {
|
|
1414
|
+
const handleMutations = (mutations) => {
|
|
1415
|
+
for (const [nodeKey, mutation] of mutations) {
|
|
1416
|
+
if (mutation === "destroyed") continue;
|
|
1417
|
+
const dom = editor.getElementByKey(nodeKey);
|
|
1418
|
+
if (!dom) continue;
|
|
1419
|
+
const href = dom.getAttribute("href");
|
|
1420
|
+
if (href) applyFavicon(dom, href);
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
const unregisterLink = editor.registerMutationListener(LinkNode, handleMutations);
|
|
1424
|
+
const unregisterAutoLink = editor.registerMutationListener(AutoLinkNode, handleMutations);
|
|
1425
|
+
return () => {
|
|
1426
|
+
unregisterLink();
|
|
1427
|
+
unregisterAutoLink();
|
|
1428
|
+
};
|
|
1429
|
+
}, [editor]);
|
|
1430
|
+
return null;
|
|
1431
|
+
}
|
|
1432
|
+
//#endregion
|
|
1433
|
+
//#region src/context/TextSelectionContext.tsx
|
|
1434
|
+
function areSnapshotsEqual(a, b) {
|
|
1435
|
+
if (a === b) return true;
|
|
1436
|
+
if (!a || !b) return false;
|
|
1437
|
+
return a.text === b.text && a.anchorBlockId === b.anchorBlockId && a.anchorOffset === b.anchorOffset && a.focusBlockId === b.focusBlockId && a.focusOffset === b.focusOffset;
|
|
1438
|
+
}
|
|
1439
|
+
function createTextSelectionStore(initialState = { snapshot: null }) {
|
|
1440
|
+
let state = initialState;
|
|
1441
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1442
|
+
const emit = () => {
|
|
1443
|
+
for (const listener of listeners) listener();
|
|
1444
|
+
};
|
|
1445
|
+
return {
|
|
1446
|
+
getState: () => state,
|
|
1447
|
+
subscribe: (listener) => {
|
|
1448
|
+
listeners.add(listener);
|
|
1449
|
+
return () => {
|
|
1450
|
+
listeners.delete(listener);
|
|
1451
|
+
};
|
|
1452
|
+
},
|
|
1453
|
+
setSnapshot: (snapshot) => {
|
|
1454
|
+
if (areSnapshotsEqual(state.snapshot, snapshot)) return;
|
|
1455
|
+
state = {
|
|
1456
|
+
...state,
|
|
1457
|
+
snapshot
|
|
1458
|
+
};
|
|
1459
|
+
emit();
|
|
1460
|
+
},
|
|
1461
|
+
clearSnapshot: () => {
|
|
1462
|
+
if (state.snapshot === null) return;
|
|
1463
|
+
state = {
|
|
1464
|
+
...state,
|
|
1465
|
+
snapshot: null
|
|
1466
|
+
};
|
|
1467
|
+
emit();
|
|
1468
|
+
}
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
var TextSelectionStoreContext = createContext(null);
|
|
1472
|
+
function TextSelectionStoreProvider({ children }) {
|
|
1473
|
+
const storeRef = useRef(null);
|
|
1474
|
+
if (!storeRef.current) storeRef.current = createTextSelectionStore();
|
|
1475
|
+
return /* @__PURE__ */ jsx(TextSelectionStoreContext.Provider, {
|
|
1476
|
+
value: storeRef.current,
|
|
1477
|
+
children
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
function useTextSelectionStore() {
|
|
1481
|
+
const store = use(TextSelectionStoreContext);
|
|
1482
|
+
if (!store) throw new Error("useTextSelectionStore must be used within TextSelectionStoreProvider");
|
|
1483
|
+
return store;
|
|
1484
|
+
}
|
|
1485
|
+
function useTextSelectionSnapshot() {
|
|
1486
|
+
const store = useTextSelectionStore();
|
|
1487
|
+
return useSyncExternalStore(store.subscribe, () => store.getState().snapshot, () => store.getState().snapshot);
|
|
1488
|
+
}
|
|
1489
|
+
//#endregion
|
|
1490
|
+
//#region src/utils/comment-anchor.ts
|
|
1491
|
+
function computeBlockFingerprint(block) {
|
|
1492
|
+
const text = block.getTextContent();
|
|
1493
|
+
const input = text.slice(0, 200) + String(text.length);
|
|
1494
|
+
let hash = 5381;
|
|
1495
|
+
for (let i = 0; i < input.length; i++) hash = (hash << 5) + hash + (input.codePointAt(i) ?? 0) | 0;
|
|
1496
|
+
return (hash >>> 0).toString(16);
|
|
1497
|
+
}
|
|
1498
|
+
function $getRootBlock(node) {
|
|
1499
|
+
const root = $getRoot();
|
|
1500
|
+
let current = node;
|
|
1501
|
+
while (current) {
|
|
1502
|
+
const parent = current.getParent();
|
|
1503
|
+
if (parent === root && "getChildren" in current) return current;
|
|
1504
|
+
current = parent;
|
|
1505
|
+
}
|
|
1506
|
+
return null;
|
|
1507
|
+
}
|
|
1508
|
+
function $resolveSelectionPoint(selection, which) {
|
|
1509
|
+
const point = selection[which];
|
|
1510
|
+
const node = point.getNode();
|
|
1511
|
+
if ($isElementNode(node)) {
|
|
1512
|
+
const children = node.getChildren();
|
|
1513
|
+
if (point.offset < children.length) return {
|
|
1514
|
+
node: children[point.offset],
|
|
1515
|
+
offset: 0
|
|
1516
|
+
};
|
|
1517
|
+
const last = children.at(-1);
|
|
1518
|
+
if (last && $isTextNode(last)) return {
|
|
1519
|
+
node: last,
|
|
1520
|
+
offset: last.getTextContentSize()
|
|
1521
|
+
};
|
|
1522
|
+
return {
|
|
1523
|
+
node,
|
|
1524
|
+
offset: 0
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
return {
|
|
1528
|
+
node,
|
|
1529
|
+
offset: point.offset
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
function $getTextOffsetInBlock(block, targetNode, targetOffset) {
|
|
1533
|
+
let offset = 0;
|
|
1534
|
+
function walk(node) {
|
|
1535
|
+
if (node.is(targetNode)) {
|
|
1536
|
+
offset += targetOffset;
|
|
1537
|
+
return true;
|
|
1538
|
+
}
|
|
1539
|
+
if ($isTextNode(node)) offset += node.getTextContentSize();
|
|
1540
|
+
else if ($isLineBreakNode(node)) offset += 1;
|
|
1541
|
+
else if ($isElementNode(node)) {
|
|
1542
|
+
for (const child of node.getChildren()) if (walk(child)) return true;
|
|
1543
|
+
}
|
|
1544
|
+
return false;
|
|
1545
|
+
}
|
|
1546
|
+
for (const child of block.getChildren()) if (walk(child)) break;
|
|
1547
|
+
return offset;
|
|
1548
|
+
}
|
|
1549
|
+
function $buildBlockAnchorData(block) {
|
|
1550
|
+
const blockId = $getState(block, blockIdState);
|
|
1551
|
+
if (!blockId) return {
|
|
1552
|
+
ok: false,
|
|
1553
|
+
error: "no-block-id"
|
|
1554
|
+
};
|
|
1555
|
+
return {
|
|
1556
|
+
ok: true,
|
|
1557
|
+
anchor: {
|
|
1558
|
+
mode: "block",
|
|
1559
|
+
blockId,
|
|
1560
|
+
blockType: block.getType(),
|
|
1561
|
+
blockFingerprint: computeBlockFingerprint(block),
|
|
1562
|
+
snapshotText: block.getTextContent().slice(0, 300)
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
function buildBlockAnchor(editor, blockKey) {
|
|
1567
|
+
return editor.read(() => {
|
|
1568
|
+
if (blockKey) {
|
|
1569
|
+
const node = $getRoot().getChildren().find((c) => c.getKey() === blockKey);
|
|
1570
|
+
if (!node || !("getChildren" in node)) return {
|
|
1571
|
+
ok: false,
|
|
1572
|
+
error: "not-root-block"
|
|
1573
|
+
};
|
|
1574
|
+
return $buildBlockAnchorData(node);
|
|
1575
|
+
}
|
|
1576
|
+
const selection = $getSelection();
|
|
1577
|
+
if (!$isRangeSelection(selection)) return {
|
|
1578
|
+
ok: false,
|
|
1579
|
+
error: "no-selection"
|
|
1580
|
+
};
|
|
1581
|
+
const block = $getRootBlock(selection.anchor.getNode());
|
|
1582
|
+
if (!block) return {
|
|
1583
|
+
ok: false,
|
|
1584
|
+
error: "not-root-block"
|
|
1585
|
+
};
|
|
1586
|
+
return $buildBlockAnchorData(block);
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
function buildRangeAnchor(editor) {
|
|
1590
|
+
return editor.read(() => {
|
|
1591
|
+
const selection = $getSelection();
|
|
1592
|
+
if (!$isRangeSelection(selection)) return {
|
|
1593
|
+
ok: false,
|
|
1594
|
+
error: "no-selection"
|
|
1595
|
+
};
|
|
1596
|
+
if (selection.isCollapsed()) return {
|
|
1597
|
+
ok: false,
|
|
1598
|
+
error: "collapsed"
|
|
1599
|
+
};
|
|
1600
|
+
const anchorBlock = $getRootBlock(selection.anchor.getNode());
|
|
1601
|
+
const focusBlock = $getRootBlock(selection.focus.getNode());
|
|
1602
|
+
if (!anchorBlock || !focusBlock) return {
|
|
1603
|
+
ok: false,
|
|
1604
|
+
error: "not-root-block"
|
|
1605
|
+
};
|
|
1606
|
+
if (anchorBlock !== focusBlock) return {
|
|
1607
|
+
ok: false,
|
|
1608
|
+
error: "cross-block"
|
|
1609
|
+
};
|
|
1610
|
+
const block = anchorBlock;
|
|
1611
|
+
const blockId = $getState(block, blockIdState);
|
|
1612
|
+
if (!blockId) return {
|
|
1613
|
+
ok: false,
|
|
1614
|
+
error: "no-block-id"
|
|
1615
|
+
};
|
|
1616
|
+
const anchorPoint = $resolveSelectionPoint(selection, "anchor");
|
|
1617
|
+
const focusPoint = $resolveSelectionPoint(selection, "focus");
|
|
1618
|
+
let startOffset = $getTextOffsetInBlock(block, anchorPoint.node, anchorPoint.offset);
|
|
1619
|
+
let endOffset = $getTextOffsetInBlock(block, focusPoint.node, focusPoint.offset);
|
|
1620
|
+
if (startOffset > endOffset) [startOffset, endOffset] = [endOffset, startOffset];
|
|
1621
|
+
const text = block.getTextContent();
|
|
1622
|
+
const quote = text.slice(startOffset, endOffset);
|
|
1623
|
+
const prefix = text.slice(Math.max(0, startOffset - 50), startOffset);
|
|
1624
|
+
const suffix = text.slice(endOffset, endOffset + 50);
|
|
1625
|
+
return {
|
|
1626
|
+
ok: true,
|
|
1627
|
+
anchor: {
|
|
1628
|
+
mode: "range",
|
|
1629
|
+
blockId,
|
|
1630
|
+
blockType: block.getType(),
|
|
1631
|
+
blockFingerprint: computeBlockFingerprint(block),
|
|
1632
|
+
snapshotText: text.slice(0, 300),
|
|
1633
|
+
quote,
|
|
1634
|
+
prefix,
|
|
1635
|
+
suffix,
|
|
1636
|
+
startOffset,
|
|
1637
|
+
endOffset
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
//#endregion
|
|
1643
|
+
//#region src/utils/text-selection-constants.ts
|
|
1644
|
+
var TEXT_SELECTION_HIGHLIGHT_NAME = "rich-editor-text-selection";
|
|
1645
|
+
var TEXT_SELECTION_INACTIVE_HIGHLIGHT_NAME = "rich-editor-text-selection-inactive";
|
|
1646
|
+
//#endregion
|
|
1647
|
+
//#region src/utils/text-selection.ts
|
|
1648
|
+
function clampOffset(offset, max) {
|
|
1649
|
+
return Math.max(0, Math.min(offset, max));
|
|
1650
|
+
}
|
|
1651
|
+
function getTopLevelBlockById(blockId) {
|
|
1652
|
+
for (const child of $getRoot().getChildren()) if ($getState(child, blockIdState) === blockId && $isElementNode(child)) return child;
|
|
1653
|
+
return null;
|
|
1654
|
+
}
|
|
1655
|
+
function getTextLengthOfDomNode(node) {
|
|
1656
|
+
if (node.nodeType === Node.TEXT_NODE) return node.textContent?.length ?? 0;
|
|
1657
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return 0;
|
|
1658
|
+
if (node.tagName === "BR") return 1;
|
|
1659
|
+
let length = 0;
|
|
1660
|
+
for (const child of node.childNodes) length += getTextLengthOfDomNode(child);
|
|
1661
|
+
return length;
|
|
1662
|
+
}
|
|
1663
|
+
function compareBlockElements(a, b) {
|
|
1664
|
+
if (a === b) return 0;
|
|
1665
|
+
const position = a.compareDocumentPosition(b);
|
|
1666
|
+
if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
|
|
1667
|
+
if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1;
|
|
1668
|
+
return 0;
|
|
1669
|
+
}
|
|
1670
|
+
function buildDomRectFromClientRects(rects) {
|
|
1671
|
+
if (!rects.length) return null;
|
|
1672
|
+
let left = rects[0].left;
|
|
1673
|
+
let top = rects[0].top;
|
|
1674
|
+
let right = rects[0].right;
|
|
1675
|
+
let bottom = rects[0].bottom;
|
|
1676
|
+
for (const rect of rects.slice(1)) {
|
|
1677
|
+
left = Math.min(left, rect.left);
|
|
1678
|
+
top = Math.min(top, rect.top);
|
|
1679
|
+
right = Math.max(right, rect.right);
|
|
1680
|
+
bottom = Math.max(bottom, rect.bottom);
|
|
1681
|
+
}
|
|
1682
|
+
return new DOMRect(left, top, right - left, bottom - top);
|
|
1683
|
+
}
|
|
1684
|
+
function resolveLexicalSelectionTarget(block, requestedOffset) {
|
|
1685
|
+
let remaining = clampOffset(requestedOffset, block.getTextContent().length);
|
|
1686
|
+
let lastTarget = {
|
|
1687
|
+
key: block.getKey(),
|
|
1688
|
+
offset: block.getChildrenSize(),
|
|
1689
|
+
type: "element"
|
|
1690
|
+
};
|
|
1691
|
+
function walk(node) {
|
|
1692
|
+
if ($isTextNode(node)) {
|
|
1693
|
+
const length = node.getTextContentSize();
|
|
1694
|
+
lastTarget = {
|
|
1695
|
+
key: node.getKey(),
|
|
1696
|
+
offset: length,
|
|
1697
|
+
type: "text"
|
|
1698
|
+
};
|
|
1699
|
+
if (remaining <= length) return {
|
|
1700
|
+
key: node.getKey(),
|
|
1701
|
+
offset: remaining,
|
|
1702
|
+
type: "text"
|
|
1703
|
+
};
|
|
1704
|
+
remaining -= length;
|
|
1705
|
+
return null;
|
|
1706
|
+
}
|
|
1707
|
+
if ($isLineBreakNode(node)) {
|
|
1708
|
+
const parent = node.getParent();
|
|
1709
|
+
if (!parent) return null;
|
|
1710
|
+
const index = node.getIndexWithinParent();
|
|
1711
|
+
if (remaining === 0) return {
|
|
1712
|
+
key: parent.getKey(),
|
|
1713
|
+
offset: index,
|
|
1714
|
+
type: "element"
|
|
1715
|
+
};
|
|
1716
|
+
if (remaining === 1) return {
|
|
1717
|
+
key: parent.getKey(),
|
|
1718
|
+
offset: index + 1,
|
|
1719
|
+
type: "element"
|
|
1720
|
+
};
|
|
1721
|
+
remaining -= 1;
|
|
1722
|
+
lastTarget = {
|
|
1723
|
+
key: parent.getKey(),
|
|
1724
|
+
offset: index + 1,
|
|
1725
|
+
type: "element"
|
|
1726
|
+
};
|
|
1727
|
+
return null;
|
|
1728
|
+
}
|
|
1729
|
+
if (!$isElementNode(node)) return null;
|
|
1730
|
+
if (node.getChildrenSize() === 0) {
|
|
1731
|
+
lastTarget = {
|
|
1732
|
+
key: node.getKey(),
|
|
1733
|
+
offset: 0,
|
|
1734
|
+
type: "element"
|
|
1735
|
+
};
|
|
1736
|
+
return null;
|
|
1737
|
+
}
|
|
1738
|
+
for (const child of node.getChildren()) {
|
|
1739
|
+
const resolved = walk(child);
|
|
1740
|
+
if (resolved) return resolved;
|
|
1741
|
+
}
|
|
1742
|
+
lastTarget = {
|
|
1743
|
+
key: node.getKey(),
|
|
1744
|
+
offset: node.getChildrenSize(),
|
|
1745
|
+
type: "element"
|
|
1746
|
+
};
|
|
1747
|
+
return null;
|
|
1748
|
+
}
|
|
1749
|
+
return walk(block) ?? lastTarget;
|
|
1750
|
+
}
|
|
1751
|
+
function $captureTextSelectionFromRangeSelection(selection) {
|
|
1752
|
+
if (selection.isCollapsed()) return null;
|
|
1753
|
+
const anchorBlock = $getRootBlock(selection.anchor.getNode());
|
|
1754
|
+
const focusBlock = $getRootBlock(selection.focus.getNode());
|
|
1755
|
+
if (!anchorBlock || !focusBlock) return null;
|
|
1756
|
+
const anchorBlockId = $getState(anchorBlock, blockIdState);
|
|
1757
|
+
const focusBlockId = $getState(focusBlock, blockIdState);
|
|
1758
|
+
if (!anchorBlockId || !focusBlockId) return null;
|
|
1759
|
+
const anchorPoint = $resolveSelectionPoint(selection, "anchor");
|
|
1760
|
+
const focusPoint = $resolveSelectionPoint(selection, "focus");
|
|
1761
|
+
return {
|
|
1762
|
+
text: selection.getTextContent(),
|
|
1763
|
+
anchorBlockId,
|
|
1764
|
+
anchorOffset: $getTextOffsetInBlock(anchorBlock, anchorPoint.node, anchorPoint.offset),
|
|
1765
|
+
focusBlockId,
|
|
1766
|
+
focusOffset: $getTextOffsetInBlock(focusBlock, focusPoint.node, focusPoint.offset)
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
function $captureTextSelection() {
|
|
1770
|
+
const selection = $getSelection();
|
|
1771
|
+
if (!$isRangeSelection(selection)) return null;
|
|
1772
|
+
return $captureTextSelectionFromRangeSelection(selection);
|
|
1773
|
+
}
|
|
1774
|
+
function $restoreTextSelection(snapshot) {
|
|
1775
|
+
const anchorBlock = getTopLevelBlockById(snapshot.anchorBlockId);
|
|
1776
|
+
const focusBlock = getTopLevelBlockById(snapshot.focusBlockId);
|
|
1777
|
+
if (!anchorBlock || !focusBlock) return false;
|
|
1778
|
+
const anchor = resolveLexicalSelectionTarget(anchorBlock, snapshot.anchorOffset);
|
|
1779
|
+
const focus = resolveLexicalSelectionTarget(focusBlock, snapshot.focusOffset);
|
|
1780
|
+
const selection = $createRangeSelection();
|
|
1781
|
+
selection.anchor.set(anchor.key, anchor.offset, anchor.type);
|
|
1782
|
+
selection.focus.set(focus.key, focus.offset, focus.type);
|
|
1783
|
+
$setSelection(selection);
|
|
1784
|
+
return true;
|
|
1785
|
+
}
|
|
1786
|
+
function getTextOffsetFromDOMPoint(container, targetNode, targetOffset) {
|
|
1787
|
+
let offset = 0;
|
|
1788
|
+
function walk(node) {
|
|
1789
|
+
if (node === targetNode) {
|
|
1790
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1791
|
+
offset += Math.min(targetOffset, node.textContent?.length ?? 0);
|
|
1792
|
+
return true;
|
|
1793
|
+
}
|
|
1794
|
+
const childNodes = Array.from(node.childNodes);
|
|
1795
|
+
const boundary = Math.min(targetOffset, childNodes.length);
|
|
1796
|
+
for (let i = 0; i < boundary; i++) offset += getTextLengthOfDomNode(childNodes[i]);
|
|
1797
|
+
return true;
|
|
1798
|
+
}
|
|
1799
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1800
|
+
offset += node.textContent?.length ?? 0;
|
|
1801
|
+
return false;
|
|
1802
|
+
}
|
|
1803
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return false;
|
|
1804
|
+
if (node.tagName === "BR") {
|
|
1805
|
+
offset += 1;
|
|
1806
|
+
return false;
|
|
1807
|
+
}
|
|
1808
|
+
for (const child of node.childNodes) if (walk(child)) return true;
|
|
1809
|
+
return false;
|
|
1810
|
+
}
|
|
1811
|
+
walk(container);
|
|
1812
|
+
return offset;
|
|
1813
|
+
}
|
|
1814
|
+
function findDOMPointByTextOffset(container, requestedOffset) {
|
|
1815
|
+
const targetOffset = clampOffset(requestedOffset, getTextLengthOfDomNode(container));
|
|
1816
|
+
let consumed = 0;
|
|
1817
|
+
function walk(node) {
|
|
1818
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1819
|
+
const length = node.textContent?.length ?? 0;
|
|
1820
|
+
if (targetOffset <= consumed + length) return {
|
|
1821
|
+
node,
|
|
1822
|
+
offset: targetOffset - consumed
|
|
1823
|
+
};
|
|
1824
|
+
consumed += length;
|
|
1825
|
+
return null;
|
|
1826
|
+
}
|
|
1827
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return null;
|
|
1828
|
+
const element = node;
|
|
1829
|
+
if (element.tagName === "BR") {
|
|
1830
|
+
const parent = element.parentNode;
|
|
1831
|
+
if (!parent) return null;
|
|
1832
|
+
const index = Array.from(parent.childNodes).indexOf(element);
|
|
1833
|
+
if (targetOffset === consumed) return {
|
|
1834
|
+
node: parent,
|
|
1835
|
+
offset: index
|
|
1836
|
+
};
|
|
1837
|
+
if (targetOffset === consumed + 1) return {
|
|
1838
|
+
node: parent,
|
|
1839
|
+
offset: index + 1
|
|
1840
|
+
};
|
|
1841
|
+
consumed += 1;
|
|
1842
|
+
return null;
|
|
1843
|
+
}
|
|
1844
|
+
for (const child of node.childNodes) {
|
|
1845
|
+
const resolved = walk(child);
|
|
1846
|
+
if (resolved) return resolved;
|
|
1847
|
+
}
|
|
1848
|
+
return null;
|
|
1849
|
+
}
|
|
1850
|
+
return walk(container) ?? {
|
|
1851
|
+
node: container,
|
|
1852
|
+
offset: container.childNodes.length
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
function getBlockElementById(rootElement, blockId) {
|
|
1856
|
+
return rootElement.querySelector(`[data-block-id="${blockId}"]`);
|
|
1857
|
+
}
|
|
1858
|
+
function createDOMRangeFromTextSelection(rootElement, snapshot) {
|
|
1859
|
+
const anchorBlock = getBlockElementById(rootElement, snapshot.anchorBlockId);
|
|
1860
|
+
const focusBlock = getBlockElementById(rootElement, snapshot.focusBlockId);
|
|
1861
|
+
if (!anchorBlock || !focusBlock) return null;
|
|
1862
|
+
const anchor = findDOMPointByTextOffset(anchorBlock, snapshot.anchorOffset);
|
|
1863
|
+
const focus = findDOMPointByTextOffset(focusBlock, snapshot.focusOffset);
|
|
1864
|
+
if (!anchor || !focus) return null;
|
|
1865
|
+
const anchorComesFirst = anchorBlock === focusBlock ? snapshot.anchorOffset <= snapshot.focusOffset : compareBlockElements(anchorBlock, focusBlock) <= 0;
|
|
1866
|
+
const start = anchorComesFirst ? anchor : focus;
|
|
1867
|
+
const end = anchorComesFirst ? focus : anchor;
|
|
1868
|
+
try {
|
|
1869
|
+
const range = document.createRange();
|
|
1870
|
+
range.setStart(start.node, start.offset);
|
|
1871
|
+
range.setEnd(end.node, end.offset);
|
|
1872
|
+
return range;
|
|
1873
|
+
} catch {
|
|
1874
|
+
return null;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
function getDOMRectFromTextSelection(rootElement, snapshot) {
|
|
1878
|
+
const range = createDOMRangeFromTextSelection(rootElement, snapshot);
|
|
1879
|
+
if (!range) return null;
|
|
1880
|
+
const rect = range.getBoundingClientRect();
|
|
1881
|
+
if (rect.width > 0 || rect.height > 0) return rect;
|
|
1882
|
+
return buildDomRectFromClientRects(Array.from(range.getClientRects()));
|
|
1883
|
+
}
|
|
1884
|
+
//#endregion
|
|
1885
|
+
//#region src/plugins/text-selection.css.ts
|
|
1886
|
+
var nativeSelectionActive = "gr1ebt0";
|
|
1887
|
+
var nativeSelectionInactive = "gr1ebt1";
|
|
1888
|
+
//#endregion
|
|
1889
|
+
//#region src/plugins/TextSelectionPlugin.tsx
|
|
1890
|
+
var activeHighlightOwner = null;
|
|
1891
|
+
function getHighlightRegistry() {
|
|
1892
|
+
if (typeof CSS === "undefined" || !("highlights" in CSS) || typeof Highlight === "undefined") return null;
|
|
1893
|
+
return CSS.highlights;
|
|
1894
|
+
}
|
|
1895
|
+
function clearManagedHighlight(rootElement) {
|
|
1896
|
+
if (rootElement && rootElement !== activeHighlightOwner) {
|
|
1897
|
+
rootElement.classList.remove(nativeSelectionActive, nativeSelectionInactive);
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
const registry = getHighlightRegistry();
|
|
1901
|
+
registry?.delete(TEXT_SELECTION_HIGHLIGHT_NAME);
|
|
1902
|
+
registry?.delete(TEXT_SELECTION_INACTIVE_HIGHLIGHT_NAME);
|
|
1903
|
+
activeHighlightOwner?.classList.remove(nativeSelectionActive, nativeSelectionInactive);
|
|
1904
|
+
activeHighlightOwner = null;
|
|
1905
|
+
}
|
|
1906
|
+
function TextSelectionPlugin() {
|
|
1907
|
+
const [editor] = useLexicalComposerContext();
|
|
1908
|
+
const store = useTextSelectionStore();
|
|
1909
|
+
const snapshot = useTextSelectionSnapshot();
|
|
1910
|
+
const [isEditorFocused, setIsEditorFocused] = useState(false);
|
|
1911
|
+
const frameRef = useRef(null);
|
|
1912
|
+
const focusFrameRef = useRef(null);
|
|
1913
|
+
useEffect(() => {
|
|
1914
|
+
const syncSnapshot = () => {
|
|
1915
|
+
if (!editor.getRootElement()) {
|
|
1916
|
+
store.clearSnapshot();
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
const nextSnapshot = editor.getEditorState().read(() => $captureTextSelection());
|
|
1920
|
+
store.setSnapshot(nextSnapshot);
|
|
1921
|
+
};
|
|
1922
|
+
const syncFocusState = () => {
|
|
1923
|
+
const rootElement = editor.getRootElement();
|
|
1924
|
+
const activeElement = rootElement?.ownerDocument.activeElement;
|
|
1925
|
+
setIsEditorFocused(Boolean(rootElement && activeElement && rootElement.contains(activeElement)));
|
|
1926
|
+
};
|
|
1927
|
+
const scheduleFocusStateSync = () => {
|
|
1928
|
+
if (focusFrameRef.current !== null) cancelAnimationFrame(focusFrameRef.current);
|
|
1929
|
+
focusFrameRef.current = requestAnimationFrame(() => {
|
|
1930
|
+
focusFrameRef.current = null;
|
|
1931
|
+
syncFocusState();
|
|
1932
|
+
});
|
|
1933
|
+
};
|
|
1934
|
+
const unregisterSelectionChange = editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
|
|
1935
|
+
syncSnapshot();
|
|
1936
|
+
return false;
|
|
1937
|
+
}, COMMAND_PRIORITY_LOW);
|
|
1938
|
+
const unregisterUpdate = editor.registerUpdateListener(() => {
|
|
1939
|
+
syncSnapshot();
|
|
1940
|
+
});
|
|
1941
|
+
const rootElement = editor.getRootElement();
|
|
1942
|
+
if (rootElement) {
|
|
1943
|
+
const onFocusIn = (event) => {
|
|
1944
|
+
const target = event.target;
|
|
1945
|
+
if (!target) return;
|
|
1946
|
+
const nestedEditable = target.closest("[contenteditable=\"true\"]");
|
|
1947
|
+
if (nestedEditable && nestedEditable !== rootElement) {
|
|
1948
|
+
store.clearSnapshot();
|
|
1949
|
+
scheduleFocusStateSync();
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
scheduleFocusStateSync();
|
|
1953
|
+
syncSnapshot();
|
|
1954
|
+
};
|
|
1955
|
+
const onFocusOut = () => {
|
|
1956
|
+
scheduleFocusStateSync();
|
|
1957
|
+
syncSnapshot();
|
|
1958
|
+
};
|
|
1959
|
+
rootElement.addEventListener("focusin", onFocusIn);
|
|
1960
|
+
rootElement.addEventListener("focusout", onFocusOut);
|
|
1961
|
+
syncFocusState();
|
|
1962
|
+
syncSnapshot();
|
|
1963
|
+
return () => {
|
|
1964
|
+
unregisterSelectionChange();
|
|
1965
|
+
unregisterUpdate();
|
|
1966
|
+
if (focusFrameRef.current !== null) {
|
|
1967
|
+
cancelAnimationFrame(focusFrameRef.current);
|
|
1968
|
+
focusFrameRef.current = null;
|
|
1969
|
+
}
|
|
1970
|
+
rootElement.removeEventListener("focusin", onFocusIn);
|
|
1971
|
+
rootElement.removeEventListener("focusout", onFocusOut);
|
|
1972
|
+
store.clearSnapshot();
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
syncFocusState();
|
|
1976
|
+
syncSnapshot();
|
|
1977
|
+
return () => {
|
|
1978
|
+
unregisterSelectionChange();
|
|
1979
|
+
unregisterUpdate();
|
|
1980
|
+
if (focusFrameRef.current !== null) {
|
|
1981
|
+
cancelAnimationFrame(focusFrameRef.current);
|
|
1982
|
+
focusFrameRef.current = null;
|
|
1983
|
+
}
|
|
1984
|
+
store.clearSnapshot();
|
|
1985
|
+
};
|
|
1986
|
+
}, [editor, store]);
|
|
1987
|
+
useEffect(() => {
|
|
1988
|
+
const registry = getHighlightRegistry();
|
|
1989
|
+
const cancelScheduledSync = () => {
|
|
1990
|
+
if (frameRef.current !== null) {
|
|
1991
|
+
cancelAnimationFrame(frameRef.current);
|
|
1992
|
+
frameRef.current = null;
|
|
1993
|
+
}
|
|
1994
|
+
};
|
|
1995
|
+
const syncManagedHighlight = () => {
|
|
1996
|
+
cancelScheduledSync();
|
|
1997
|
+
frameRef.current = requestAnimationFrame(() => {
|
|
1998
|
+
frameRef.current = null;
|
|
1999
|
+
const rootElement = editor.getRootElement();
|
|
2000
|
+
if (!rootElement) {
|
|
2001
|
+
clearManagedHighlight();
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
if (!snapshot) {
|
|
2005
|
+
clearManagedHighlight(rootElement);
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
if (activeHighlightOwner && activeHighlightOwner !== rootElement) activeHighlightOwner.classList.remove(nativeSelectionActive, nativeSelectionInactive);
|
|
2009
|
+
const activeSelectionClass = isEditorFocused ? nativeSelectionActive : nativeSelectionInactive;
|
|
2010
|
+
const inactiveSelectionClass = isEditorFocused ? nativeSelectionInactive : nativeSelectionActive;
|
|
2011
|
+
const highlightName = isEditorFocused ? TEXT_SELECTION_HIGHLIGHT_NAME : TEXT_SELECTION_INACTIVE_HIGHLIGHT_NAME;
|
|
2012
|
+
const staleHighlightName = isEditorFocused ? TEXT_SELECTION_INACTIVE_HIGHLIGHT_NAME : TEXT_SELECTION_HIGHLIGHT_NAME;
|
|
2013
|
+
rootElement.classList.remove(inactiveSelectionClass);
|
|
2014
|
+
rootElement.classList.add(activeSelectionClass);
|
|
2015
|
+
activeHighlightOwner = rootElement;
|
|
2016
|
+
if (!registry) return;
|
|
2017
|
+
const range = createDOMRangeFromTextSelection(rootElement, snapshot);
|
|
2018
|
+
if (!range) {
|
|
2019
|
+
clearManagedHighlight(rootElement);
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
registry.delete(staleHighlightName);
|
|
2023
|
+
registry.set(highlightName, new Highlight(range));
|
|
2024
|
+
});
|
|
2025
|
+
};
|
|
2026
|
+
syncManagedHighlight();
|
|
2027
|
+
return () => {
|
|
2028
|
+
cancelScheduledSync();
|
|
2029
|
+
clearManagedHighlight(editor.getRootElement());
|
|
2030
|
+
};
|
|
2031
|
+
}, [
|
|
2032
|
+
editor,
|
|
2033
|
+
isEditorFocused,
|
|
2034
|
+
snapshot
|
|
2035
|
+
]);
|
|
2036
|
+
return null;
|
|
2037
|
+
}
|
|
2038
|
+
//#endregion
|
|
2039
|
+
//#region src/plugins/AutoFocusPlugin.tsx
|
|
2040
|
+
function AutoFocusPlugin() {
|
|
2041
|
+
const [editor] = useLexicalComposerContext();
|
|
2042
|
+
useEffect(() => {
|
|
2043
|
+
const root = editor.getRootElement();
|
|
2044
|
+
if (root) root.focus({ preventScroll: true });
|
|
2045
|
+
else editor.focus();
|
|
2046
|
+
}, [editor]);
|
|
2047
|
+
return null;
|
|
2048
|
+
}
|
|
2049
|
+
//#endregion
|
|
2050
|
+
//#region src/plugins/EditorRefPlugin.tsx
|
|
2051
|
+
function EditorRefPlugin({ onEditorReady }) {
|
|
2052
|
+
const [editor] = useLexicalComposerContext();
|
|
2053
|
+
const callbackRef = useRef(onEditorReady);
|
|
2054
|
+
callbackRef.current = onEditorReady;
|
|
2055
|
+
useEffect(() => {
|
|
2056
|
+
callbackRef.current?.(editor);
|
|
2057
|
+
return () => callbackRef.current?.(null);
|
|
2058
|
+
}, [editor]);
|
|
2059
|
+
return null;
|
|
2060
|
+
}
|
|
2061
|
+
//#endregion
|
|
2062
|
+
//#region src/plugins/FootnotePlugin.tsx
|
|
2063
|
+
function FootnotePlugin({ children }) {
|
|
2064
|
+
const [editor] = useLexicalComposerContext();
|
|
2065
|
+
const [definitions, setDefinitions] = useState({});
|
|
2066
|
+
const [displayNumberMap, setDisplayNumberMap] = useState({});
|
|
2067
|
+
const pendingUpdateRef = useRef(false);
|
|
2068
|
+
useEffect(() => {
|
|
2069
|
+
return editor.registerUpdateListener(({ editorState }) => {
|
|
2070
|
+
editorState.read(() => {
|
|
2071
|
+
const footnoteNodes = $nodesOfType(FootnoteNode);
|
|
2072
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2073
|
+
const numberMap = {};
|
|
2074
|
+
let counter = 1;
|
|
2075
|
+
for (const node of footnoteNodes) {
|
|
2076
|
+
const id = node.getIdentifier();
|
|
2077
|
+
if (!seen.has(id)) {
|
|
2078
|
+
seen.add(id);
|
|
2079
|
+
numberMap[id] = counter++;
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
setDisplayNumberMap(numberMap);
|
|
2083
|
+
const sectionNodes = $nodesOfType(FootnoteSectionNode);
|
|
2084
|
+
if (sectionNodes.length > 0) setDefinitions(sectionNodes[0].getDefinitions());
|
|
2085
|
+
else setDefinitions({});
|
|
2086
|
+
if (!editor.isEditable() || pendingUpdateRef.current) return;
|
|
2087
|
+
if (footnoteNodes.length === 0 && sectionNodes.length > 0) {
|
|
2088
|
+
pendingUpdateRef.current = true;
|
|
2089
|
+
queueMicrotask(() => {
|
|
2090
|
+
editor.update(() => {
|
|
2091
|
+
const sections = $nodesOfType(FootnoteSectionNode);
|
|
2092
|
+
for (const s of sections) s.remove();
|
|
2093
|
+
});
|
|
2094
|
+
pendingUpdateRef.current = false;
|
|
2095
|
+
});
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
if (footnoteNodes.length > 0 && sectionNodes.length === 0) {
|
|
2099
|
+
const seenSnapshot = [...seen];
|
|
2100
|
+
pendingUpdateRef.current = true;
|
|
2101
|
+
queueMicrotask(() => {
|
|
2102
|
+
editor.update(() => {
|
|
2103
|
+
const root = $getRoot();
|
|
2104
|
+
const defs = {};
|
|
2105
|
+
for (const id of seenSnapshot) defs[id] = "";
|
|
2106
|
+
const section = $parseSerializedNode({
|
|
2107
|
+
type: "footnote-section",
|
|
2108
|
+
definitions: defs,
|
|
2109
|
+
version: 1
|
|
2110
|
+
});
|
|
2111
|
+
root.append(section);
|
|
2112
|
+
});
|
|
2113
|
+
pendingUpdateRef.current = false;
|
|
2114
|
+
});
|
|
2115
|
+
} else if (sectionNodes.length > 0) {
|
|
2116
|
+
const existingDefs = sectionNodes[0].getDefinitions();
|
|
2117
|
+
const missingIds = [...seen].filter((id) => !(id in existingDefs));
|
|
2118
|
+
const orphanIds = Object.keys(existingDefs).filter((id) => !seen.has(id));
|
|
2119
|
+
if (missingIds.length > 0 || orphanIds.length > 0) {
|
|
2120
|
+
pendingUpdateRef.current = true;
|
|
2121
|
+
queueMicrotask(() => {
|
|
2122
|
+
editor.update(() => {
|
|
2123
|
+
const freshSections = $nodesOfType(FootnoteSectionNode);
|
|
2124
|
+
if (freshSections.length > 0) {
|
|
2125
|
+
for (const id of missingIds) freshSections[0].setDefinition(id, "");
|
|
2126
|
+
for (const id of orphanIds) freshSections[0].removeDefinition(id);
|
|
2127
|
+
}
|
|
2128
|
+
});
|
|
2129
|
+
pendingUpdateRef.current = false;
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
});
|
|
2134
|
+
});
|
|
2135
|
+
}, [editor]);
|
|
2136
|
+
return /* @__PURE__ */ jsx(FootnoteDefinitionsProvider, {
|
|
2137
|
+
definitions,
|
|
2138
|
+
displayNumberMap,
|
|
2139
|
+
children
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
//#endregion
|
|
2143
|
+
//#region src/plugins/OnChangePlugin.tsx
|
|
2144
|
+
function OnChangePlugin({ onChange, debounceMs }) {
|
|
2145
|
+
const [editor] = useLexicalComposerContext();
|
|
2146
|
+
const timerRef = useRef(void 0);
|
|
2147
|
+
const onChangeRef = useRef(onChange);
|
|
2148
|
+
onChangeRef.current = onChange;
|
|
2149
|
+
useEffect(() => {
|
|
2150
|
+
const unregister = editor.registerUpdateListener(({ editorState }) => {
|
|
2151
|
+
const fn = onChangeRef.current;
|
|
2152
|
+
if (!fn) return;
|
|
2153
|
+
const serializedState = normalizeSerializedEditorState(editorState.toJSON());
|
|
2154
|
+
if (debounceMs && debounceMs > 0) {
|
|
2155
|
+
clearTimeout(timerRef.current);
|
|
2156
|
+
timerRef.current = setTimeout(() => {
|
|
2157
|
+
fn(serializedState);
|
|
2158
|
+
}, debounceMs);
|
|
2159
|
+
} else fn(serializedState);
|
|
2160
|
+
});
|
|
2161
|
+
return () => {
|
|
2162
|
+
clearTimeout(timerRef.current);
|
|
2163
|
+
unregister();
|
|
2164
|
+
};
|
|
2165
|
+
}, [editor, debounceMs]);
|
|
2166
|
+
return null;
|
|
2167
|
+
}
|
|
2168
|
+
//#endregion
|
|
2169
|
+
//#region src/plugins/SubmitShortcutPlugin.tsx
|
|
2170
|
+
function SubmitShortcutPlugin({ onSubmit }) {
|
|
2171
|
+
const [editor] = useLexicalComposerContext();
|
|
2172
|
+
useEffect(() => {
|
|
2173
|
+
if (!onSubmit) return;
|
|
2174
|
+
return editor.registerCommand(KEY_ENTER_COMMAND, (event) => {
|
|
2175
|
+
if (event && (event.metaKey || event.ctrlKey)) {
|
|
2176
|
+
event.preventDefault();
|
|
2177
|
+
onSubmit();
|
|
2178
|
+
return true;
|
|
2179
|
+
}
|
|
2180
|
+
return false;
|
|
2181
|
+
}, COMMAND_PRIORITY_HIGH);
|
|
2182
|
+
}, [editor, onSubmit]);
|
|
2183
|
+
return null;
|
|
2184
|
+
}
|
|
2185
|
+
//#endregion
|
|
2186
|
+
export { ImageUploadProvider as A, useTextSelectionSnapshot as C, defaultImageUpload as D, ImageUploadPlugin as E, ALL_TRANSFORMERS as F, HorizontalRulePlugin as I, BlockExitPlugin as L, CorePlugins as M, MarkdownShortcutsPlugin as N, BlockIdPlugin as O, MarkdownPastePlugin as P, AutoLinkPlugin as R, createTextSelectionStore as S, LinkFaviconPlugin as T, $getTextOffsetInBlock as _, AutoFocusPlugin as a, buildRangeAnchor as b, $captureTextSelectionFromRangeSelection as c, findDOMPointByTextOffset as d, getBlockElementById as f, $getRootBlock as g, TEXT_SELECTION_HIGHLIGHT_NAME as h, EditorRefPlugin as i, useImageUpload as j, blockIdState as k, $restoreTextSelection as l, getTextOffsetFromDOMPoint as m, OnChangePlugin as n, TextSelectionPlugin as o, getDOMRectFromTextSelection as p, FootnotePlugin as r, $captureTextSelection as s, SubmitShortcutPlugin as t, createDOMRangeFromTextSelection as u, $resolveSelectionPoint as v, useTextSelectionStore as w, TextSelectionStoreProvider as x, buildBlockAnchor as y };
|