@churchapps/apphelper 0.3.16 → 0.3.18
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/components/markdownEditor/Editor.d.ts +3 -1
- package/dist/components/markdownEditor/Editor.d.ts.map +1 -1
- package/dist/components/markdownEditor/Editor.js +25 -4
- package/dist/components/markdownEditor/Editor.js.map +1 -1
- package/dist/components/markdownEditor/MarkdownPreview.d.ts +3 -1
- package/dist/components/markdownEditor/MarkdownPreview.d.ts.map +1 -1
- package/dist/components/markdownEditor/MarkdownPreview.js +14 -2
- package/dist/components/markdownEditor/MarkdownPreview.js.map +1 -1
- package/dist/components/markdownEditor/MarkdownPreviewLight.d.ts.map +1 -1
- package/dist/components/markdownEditor/MarkdownPreviewLight.js +5 -1
- package/dist/components/markdownEditor/MarkdownPreviewLight.js.map +1 -1
- package/dist/components/markdownEditor/plugins/FloatingTextMenu/FloatingTextFormatToolbarPlugin.d.ts +13 -0
- package/dist/components/markdownEditor/plugins/FloatingTextMenu/FloatingTextFormatToolbarPlugin.d.ts.map +1 -0
- package/dist/components/markdownEditor/plugins/FloatingTextMenu/FloatingTextFormatToolbarPlugin.js +311 -0
- package/dist/components/markdownEditor/plugins/FloatingTextMenu/FloatingTextFormatToolbarPlugin.js.map +1 -0
- package/dist/components/markdownEditor/plugins/FloatingTextMenu/getDOMRangeRect.d.ts +2 -0
- package/dist/components/markdownEditor/plugins/FloatingTextMenu/getDOMRangeRect.d.ts.map +1 -0
- package/dist/components/markdownEditor/plugins/FloatingTextMenu/getDOMRangeRect.js +20 -0
- package/dist/components/markdownEditor/plugins/FloatingTextMenu/getDOMRangeRect.js.map +1 -0
- package/dist/components/markdownEditor/plugins/FloatingTextMenu/getSelectNode.d.ts +2 -0
- package/dist/components/markdownEditor/plugins/FloatingTextMenu/getSelectNode.d.ts.map +1 -0
- package/dist/components/markdownEditor/plugins/FloatingTextMenu/getSelectNode.js +22 -0
- package/dist/components/markdownEditor/plugins/FloatingTextMenu/getSelectNode.js.map +1 -0
- package/dist/components/markdownEditor/plugins/FloatingTextMenu/setFloatingElemPosition.d.ts +2 -0
- package/dist/components/markdownEditor/plugins/FloatingTextMenu/setFloatingElemPosition.d.ts.map +1 -0
- package/dist/components/markdownEditor/plugins/FloatingTextMenu/setFloatingElemPosition.js +30 -0
- package/dist/components/markdownEditor/plugins/FloatingTextMenu/setFloatingElemPosition.js.map +1 -0
- package/dist/components/markdownEditor/plugins/MarkdownTransformers.js +2 -2
- package/dist/components/markdownEditor/plugins/MarkdownTransformers.js.map +1 -1
- package/dist/components/markdownEditor/plugins/customLink/CustomLinkNode.d.ts +1 -0
- package/dist/components/markdownEditor/plugins/customLink/CustomLinkNode.d.ts.map +1 -1
- package/dist/components/markdownEditor/plugins/customLink/CustomLinkNode.js +8 -5
- package/dist/components/markdownEditor/plugins/customLink/CustomLinkNode.js.map +1 -1
- package/dist/components/markdownEditor/plugins/customLink/FloatingLinkEditor.d.ts.map +1 -1
- package/dist/components/markdownEditor/plugins/customLink/FloatingLinkEditor.js +12 -7
- package/dist/components/markdownEditor/plugins/customLink/FloatingLinkEditor.js.map +1 -1
- package/dist/components/markdownEditor/plugins/emoji/EmojisPlugin.d.ts.map +1 -1
- package/dist/components/markdownEditor/plugins/emoji/EmojisPlugin.js +4 -0
- package/dist/components/markdownEditor/plugins/emoji/EmojisPlugin.js.map +1 -1
- package/package.json +1 -1
- package/src/components/markdownEditor/Editor.tsx +33 -16
- package/src/components/markdownEditor/MarkdownPreview.tsx +5 -3
- package/src/components/markdownEditor/MarkdownPreviewLight.tsx +5 -1
- package/src/components/markdownEditor/plugins/FloatingTextMenu/FloatingTextFormatToolbarPlugin.tsx +445 -0
- package/src/components/markdownEditor/plugins/FloatingTextMenu/getDOMRangeRect.tsx +17 -0
- package/src/components/markdownEditor/plugins/FloatingTextMenu/getSelectNode.tsx +17 -0
- package/src/components/markdownEditor/plugins/FloatingTextMenu/setFloatingElemPosition.tsx +33 -0
- package/src/components/markdownEditor/plugins/MarkdownTransformers.ts +2 -2
- package/src/components/markdownEditor/plugins/customLink/CustomLinkNode.tsx +9 -1
- package/src/components/markdownEditor/plugins/customLink/FloatingLinkEditor.tsx +21 -9
- package/src/components/markdownEditor/plugins/emoji/EmojisPlugin.tsx +5 -0
package/src/components/markdownEditor/plugins/FloatingTextMenu/FloatingTextFormatToolbarPlugin.tsx
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { $isCodeHighlightNode } from "@lexical/code";
|
|
2
|
+
import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
|
|
3
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
4
|
+
import { mergeRegister } from "@lexical/utils";
|
|
5
|
+
import { $convertToMarkdownString } from "@lexical/markdown";
|
|
6
|
+
import { $createParagraphNode, $getSelection, $isRangeSelection, $isTextNode, COMMAND_PRIORITY_LOW, FORMAT_TEXT_COMMAND, SELECTION_CHANGE_COMMAND } from "lexical";
|
|
7
|
+
import { $createHeadingNode, $isHeadingNode } from "@lexical/rich-text";
|
|
8
|
+
import { $wrapNodes } from "@lexical/selection";
|
|
9
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
10
|
+
import { createPortal } from "react-dom";
|
|
11
|
+
import { Box, styled, IconButton, Icon, Select, MenuItem } from "@mui/material";
|
|
12
|
+
|
|
13
|
+
import { getDOMRangeRect } from "./getDOMRangeRect";
|
|
14
|
+
import { getSelectedNode } from "./getSelectNode";
|
|
15
|
+
import { setFloatingElemPosition } from "./setFloatingElemPosition";
|
|
16
|
+
import { PLAYGROUND_TRANSFORMERS } from "../MarkdownTransformers";
|
|
17
|
+
import { ApiHelper } from "../../../../helpers";
|
|
18
|
+
|
|
19
|
+
export const FloatingDivContainer = styled(Box)({
|
|
20
|
+
display: "flex",
|
|
21
|
+
background: "#fff",
|
|
22
|
+
padding: 4,
|
|
23
|
+
verticalAlign: "middle",
|
|
24
|
+
position: "absolute",
|
|
25
|
+
top: 0,
|
|
26
|
+
left: 0,
|
|
27
|
+
zIndex: 1400,
|
|
28
|
+
opacity: 0,
|
|
29
|
+
backgroundColor: "#fff",
|
|
30
|
+
boxShadow: "0px 5px 10px rgba(0, 0, 0, 0.3)",
|
|
31
|
+
borderRadius: 8,
|
|
32
|
+
transition: "opacity 0.5s",
|
|
33
|
+
height: 35,
|
|
34
|
+
willChange: "transform",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function TextFormatFloatingToolbar({ editor, anchorElem, isLink, isBold, isItalic, isUnderline, isCode, isStrikethrough, isSubscript, isSuperscript, blockType, setBlockType }: any) {
|
|
38
|
+
const popupCharStylesEditorRef = useRef(null);
|
|
39
|
+
|
|
40
|
+
const applyFormatting = (command: string) => {
|
|
41
|
+
editor.dispatchCommand(FORMAT_TEXT_COMMAND, command);
|
|
42
|
+
saveChanges(editor);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const insertLink = useCallback(() => {
|
|
46
|
+
if (!isLink) {
|
|
47
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, "https://");
|
|
48
|
+
} else {
|
|
49
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
50
|
+
}
|
|
51
|
+
}, [editor, isLink]);
|
|
52
|
+
|
|
53
|
+
function mouseMoveListener(e: any) {
|
|
54
|
+
if (
|
|
55
|
+
popupCharStylesEditorRef?.current &&
|
|
56
|
+
(e.buttons === 1 || e.buttons === 3)
|
|
57
|
+
) {
|
|
58
|
+
popupCharStylesEditorRef.current.style.pointerEvents = "none";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function mouseUpListener(e: any) {
|
|
62
|
+
if (popupCharStylesEditorRef?.current) {
|
|
63
|
+
popupCharStylesEditorRef.current.style.pointerEvents = "auto";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (popupCharStylesEditorRef?.current) {
|
|
69
|
+
document.addEventListener("mousemove", mouseMoveListener);
|
|
70
|
+
document.addEventListener("mouseup", mouseUpListener);
|
|
71
|
+
|
|
72
|
+
return () => {
|
|
73
|
+
document.removeEventListener("mousemove", mouseMoveListener);
|
|
74
|
+
document.removeEventListener("mouseup", mouseUpListener);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}, [popupCharStylesEditorRef]);
|
|
78
|
+
|
|
79
|
+
const updateTextFormatFloatingToolbar = useCallback(() => {
|
|
80
|
+
const selection = $getSelection();
|
|
81
|
+
|
|
82
|
+
const popupCharStylesEditorElem = popupCharStylesEditorRef.current;
|
|
83
|
+
const nativeSelection = window.getSelection();
|
|
84
|
+
|
|
85
|
+
if (popupCharStylesEditorElem === null) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const rootElement = editor.getRootElement();
|
|
90
|
+
if (
|
|
91
|
+
selection !== null &&
|
|
92
|
+
nativeSelection !== null &&
|
|
93
|
+
!nativeSelection.isCollapsed &&
|
|
94
|
+
rootElement !== null &&
|
|
95
|
+
rootElement.contains(nativeSelection.anchorNode)
|
|
96
|
+
) {
|
|
97
|
+
const rangeRect = getDOMRangeRect(nativeSelection, rootElement);
|
|
98
|
+
|
|
99
|
+
setFloatingElemPosition(rangeRect, popupCharStylesEditorElem, anchorElem);
|
|
100
|
+
}
|
|
101
|
+
}, [editor, anchorElem]);
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
const scrollerElem = anchorElem.parentElement;
|
|
105
|
+
|
|
106
|
+
const update = () => {
|
|
107
|
+
editor.getEditorState().read(() => {
|
|
108
|
+
updateTextFormatFloatingToolbar();
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
window.addEventListener("resize", update);
|
|
113
|
+
if (scrollerElem) {
|
|
114
|
+
scrollerElem.addEventListener("scroll", update);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return () => {
|
|
118
|
+
window.removeEventListener("resize", update);
|
|
119
|
+
if (scrollerElem) {
|
|
120
|
+
scrollerElem.removeEventListener("scroll", update);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}, [editor, updateTextFormatFloatingToolbar, anchorElem]);
|
|
124
|
+
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
editor.getEditorState().read(() => {
|
|
127
|
+
updateTextFormatFloatingToolbar();
|
|
128
|
+
});
|
|
129
|
+
return mergeRegister(
|
|
130
|
+
editor.registerUpdateListener(({ editorState }: any) => {
|
|
131
|
+
editorState.read(() => {
|
|
132
|
+
updateTextFormatFloatingToolbar();
|
|
133
|
+
});
|
|
134
|
+
}),
|
|
135
|
+
|
|
136
|
+
editor.registerCommand(
|
|
137
|
+
SELECTION_CHANGE_COMMAND,
|
|
138
|
+
() => {
|
|
139
|
+
updateTextFormatFloatingToolbar();
|
|
140
|
+
return false;
|
|
141
|
+
},
|
|
142
|
+
COMMAND_PRIORITY_LOW
|
|
143
|
+
)
|
|
144
|
+
);
|
|
145
|
+
}, [editor, updateTextFormatFloatingToolbar]);
|
|
146
|
+
|
|
147
|
+
const formatBlock = (type: string) => {
|
|
148
|
+
editor.update(() => {
|
|
149
|
+
const selection = $getSelection();
|
|
150
|
+
if ($isRangeSelection(selection)) {
|
|
151
|
+
$wrapNodes(selection, () =>
|
|
152
|
+
//@ts-ignore
|
|
153
|
+
type === "paragraph" ? $createParagraphNode() : $createHeadingNode(type)
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
setBlockType(type);
|
|
158
|
+
saveChanges(editor)
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<FloatingDivContainer ref={popupCharStylesEditorRef}>
|
|
163
|
+
<>
|
|
164
|
+
<Select
|
|
165
|
+
value={blockType}
|
|
166
|
+
onChange={(e) => formatBlock(e.target.value)}
|
|
167
|
+
sx={{
|
|
168
|
+
minWidth: 120,
|
|
169
|
+
backgroundColor: "#fff",
|
|
170
|
+
borderRadius: 2,
|
|
171
|
+
marginRight: 0.3,
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
<MenuItem value="paragraph">Normal</MenuItem>
|
|
175
|
+
<MenuItem value="h1">Heading 1</MenuItem>
|
|
176
|
+
<MenuItem value="h2">Heading 2</MenuItem>
|
|
177
|
+
<MenuItem value="h3">Heading 3</MenuItem>
|
|
178
|
+
<MenuItem value="h4">Heading 4</MenuItem>
|
|
179
|
+
</Select>
|
|
180
|
+
<IconButton
|
|
181
|
+
onClick={() => {
|
|
182
|
+
applyFormatting("bold");
|
|
183
|
+
}}
|
|
184
|
+
sx={{ backgroundColor: isBold ? "#e0e0e0" : undefined, borderRadius: 2, marginRight: 0.3 }}
|
|
185
|
+
>
|
|
186
|
+
<Icon>format_bold_outline</Icon>
|
|
187
|
+
</IconButton>
|
|
188
|
+
|
|
189
|
+
<IconButton
|
|
190
|
+
onClick={() => {
|
|
191
|
+
applyFormatting("italic");
|
|
192
|
+
}}
|
|
193
|
+
sx={{ backgroundColor: isItalic ? "#e0e0e0" : undefined, borderRadius: 2, marginRight: 0.3 }}
|
|
194
|
+
>
|
|
195
|
+
<Icon>format_italic_outline</Icon>
|
|
196
|
+
</IconButton>
|
|
197
|
+
|
|
198
|
+
<IconButton
|
|
199
|
+
onClick={() => {
|
|
200
|
+
applyFormatting("underline");
|
|
201
|
+
}}
|
|
202
|
+
sx={{ backgroundColor: isUnderline ? "#e0e0e0" : undefined, borderRadius: 2, marginRight: 0.3 }}
|
|
203
|
+
>
|
|
204
|
+
<Icon>format_underlined_outline</Icon>
|
|
205
|
+
</IconButton>
|
|
206
|
+
|
|
207
|
+
{/* <IconButton
|
|
208
|
+
onClick={() => {
|
|
209
|
+
applyFormatting("strikethrough");
|
|
210
|
+
}}
|
|
211
|
+
sx={{ backgroundColor: isStrikethrough ? "#e0e0e0" : undefined, borderRadius: 2, marginRight: 0.3 }}
|
|
212
|
+
>
|
|
213
|
+
<Icon>strikethrough_s_outline</Icon>
|
|
214
|
+
</IconButton> */}
|
|
215
|
+
|
|
216
|
+
<IconButton
|
|
217
|
+
onClick={() => {
|
|
218
|
+
applyFormatting("code");
|
|
219
|
+
}}
|
|
220
|
+
sx={{ backgroundColor: isCode ? "#e0e0e0" : undefined, borderRadius: 2, marginRight: 0.3 }}
|
|
221
|
+
>
|
|
222
|
+
<Icon>code</Icon>
|
|
223
|
+
</IconButton>
|
|
224
|
+
|
|
225
|
+
{/* <IconButton
|
|
226
|
+
onClick={insertLink}
|
|
227
|
+
sx={{ backgroundColor: isLink ? "#e0e0e0" : undefined, borderRadius: 2 }}
|
|
228
|
+
>
|
|
229
|
+
<Icon>insert_link_outline</Icon>
|
|
230
|
+
</IconButton> */}
|
|
231
|
+
</>
|
|
232
|
+
</FloatingDivContainer>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let lastSavedText = ""; // Track last saved text
|
|
237
|
+
let lastFormattingState = {}; // Track last formatting state
|
|
238
|
+
|
|
239
|
+
//@ts-ignore
|
|
240
|
+
const getFormattingState = (selection) => {
|
|
241
|
+
const node = getSelectedNode(selection);
|
|
242
|
+
let blockType = "paragraph";
|
|
243
|
+
if ($isHeadingNode(node)) {
|
|
244
|
+
blockType = node.getTag(); // "h1", "h2", etc.
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
isBold: selection.hasFormat("bold"),
|
|
249
|
+
isItalic: selection.hasFormat("italic"),
|
|
250
|
+
isUnderline: selection.hasFormat("underline"),
|
|
251
|
+
// isStrikethrough: selection.hasFormat("strikethrough"),
|
|
252
|
+
isCode: selection.hasFormat("code"),
|
|
253
|
+
blockType
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const saveChanges = (editor: any) => {
|
|
258
|
+
editor.update(() => {
|
|
259
|
+
const selection = $getSelection();
|
|
260
|
+
if ($isRangeSelection(selection)) {
|
|
261
|
+
const text = selection.getTextContent().trim();
|
|
262
|
+
const newFormattingState = getFormattingState(selection); // Get current formatting
|
|
263
|
+
|
|
264
|
+
// Get the parent block node (ensuring it's not just a text node)
|
|
265
|
+
const node = getSelectedNode(selection);
|
|
266
|
+
const parentNode = node.getParent(); // Get the parent block-level node
|
|
267
|
+
let blockType = "paragraph";
|
|
268
|
+
|
|
269
|
+
if ($isHeadingNode(parentNode)) {
|
|
270
|
+
blockType = parentNode.getTag(); // Get heading type
|
|
271
|
+
} else if ($isHeadingNode(node)) {
|
|
272
|
+
blockType = node.getTag();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
//@ts-ignore
|
|
276
|
+
if (JSON.stringify(newFormattingState) !== JSON.stringify(lastFormattingState) || blockType !== lastFormattingState.blockType) {
|
|
277
|
+
lastSavedText = text;
|
|
278
|
+
lastFormattingState = { ...newFormattingState, blockType };
|
|
279
|
+
|
|
280
|
+
const editorNode = editor.getRootElement();
|
|
281
|
+
const elementJSON = editorNode?.dataset?.element;
|
|
282
|
+
if (elementJSON) {
|
|
283
|
+
const markdown = $convertToMarkdownString(PLAYGROUND_TRANSFORMERS)
|
|
284
|
+
const element: any = JSON.parse(elementJSON);
|
|
285
|
+
|
|
286
|
+
element.answers.text = markdown;
|
|
287
|
+
element.answersJSON = JSON.stringify(element.answers);
|
|
288
|
+
|
|
289
|
+
ApiHelper.post("/elements", [element], "ContentApi");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const updateFormattingState = (editor: any) => {
|
|
297
|
+
editor.update(() => {
|
|
298
|
+
const selection = $getSelection();
|
|
299
|
+
if ($isRangeSelection(selection)) {
|
|
300
|
+
lastFormattingState = getFormattingState(selection);
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function useFloatingTextFormatToolbar(editor: any, anchorElem: any) {
|
|
306
|
+
const [isText, setIsText] = useState(false);
|
|
307
|
+
const [isLink, setIsLink] = useState(false);
|
|
308
|
+
const [isBold, setIsBold] = useState(false);
|
|
309
|
+
const [isItalic, setIsItalic] = useState(false);
|
|
310
|
+
const [isUnderline, setIsUnderline] = useState(false);
|
|
311
|
+
const [isStrikethrough, setIsStrikethrough] = useState(false);
|
|
312
|
+
const [isSubscript, setIsSubscript] = useState(false);
|
|
313
|
+
const [isSuperscript, setIsSuperscript] = useState(false);
|
|
314
|
+
const [isCode, setIsCode] = useState(false);
|
|
315
|
+
const [blockType, setBlockType] = useState("paragraph");
|
|
316
|
+
|
|
317
|
+
const updatePopup = useCallback(() => {
|
|
318
|
+
editor.getEditorState().read(() => {
|
|
319
|
+
// Should not to pop up the floating toolbar when using IME input
|
|
320
|
+
if (editor.isComposing()) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const selection = $getSelection();
|
|
324
|
+
const nativeSelection = window.getSelection();
|
|
325
|
+
const rootElement = editor.getRootElement();
|
|
326
|
+
|
|
327
|
+
if (
|
|
328
|
+
nativeSelection !== null &&
|
|
329
|
+
(!$isRangeSelection(selection) ||
|
|
330
|
+
rootElement === null ||
|
|
331
|
+
!rootElement.contains(nativeSelection.anchorNode))
|
|
332
|
+
) {
|
|
333
|
+
setIsText(false);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!$isRangeSelection(selection)) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const node = getSelectedNode(selection);
|
|
342
|
+
|
|
343
|
+
// Update text format
|
|
344
|
+
setIsBold(selection.hasFormat("bold"));
|
|
345
|
+
setIsItalic(selection.hasFormat("italic"));
|
|
346
|
+
setIsUnderline(selection.hasFormat("underline"));
|
|
347
|
+
setIsStrikethrough(selection.hasFormat("strikethrough"));
|
|
348
|
+
setIsSubscript(selection.hasFormat("subscript"));
|
|
349
|
+
setIsSuperscript(selection.hasFormat("superscript"));
|
|
350
|
+
setIsCode(selection.hasFormat("code"));
|
|
351
|
+
|
|
352
|
+
// Update links
|
|
353
|
+
const parent = node.getParent();
|
|
354
|
+
if ($isLinkNode(parent) || $isLinkNode(node)) {
|
|
355
|
+
setIsLink(true);
|
|
356
|
+
} else {
|
|
357
|
+
setIsLink(false);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (
|
|
361
|
+
!$isCodeHighlightNode(selection.anchor.getNode()) &&
|
|
362
|
+
selection.getTextContent() !== ""
|
|
363
|
+
) {
|
|
364
|
+
setIsText($isTextNode(node));
|
|
365
|
+
} else {
|
|
366
|
+
setIsText(false);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const rawTextContent = selection.getTextContent().replace(/\n/g, "");
|
|
370
|
+
if (!selection.isCollapsed() && rawTextContent === "") {
|
|
371
|
+
setIsText(false);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if ($isRangeSelection(selection)) {
|
|
376
|
+
const text = selection.getTextContent().trim();
|
|
377
|
+
if (text && JSON.stringify(lastFormattingState === "{}")) {
|
|
378
|
+
updateFormattingState(editor);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
let type = "paragraph";
|
|
383
|
+
if ($isHeadingNode(parent)) {
|
|
384
|
+
type = parent.getTag();
|
|
385
|
+
} else if ($isHeadingNode(node)) {
|
|
386
|
+
type = node.getTag();
|
|
387
|
+
}
|
|
388
|
+
setBlockType(type);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
}, [editor]);
|
|
392
|
+
|
|
393
|
+
useEffect(() => {
|
|
394
|
+
document.addEventListener("selectionchange", updatePopup);
|
|
395
|
+
return () => {
|
|
396
|
+
document.removeEventListener("selectionchange", updatePopup);
|
|
397
|
+
};
|
|
398
|
+
}, [updatePopup]);
|
|
399
|
+
|
|
400
|
+
useEffect(() => {
|
|
401
|
+
return mergeRegister(
|
|
402
|
+
editor.registerUpdateListener(() => {
|
|
403
|
+
updatePopup();
|
|
404
|
+
}),
|
|
405
|
+
editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
|
|
406
|
+
updatePopup();
|
|
407
|
+
return false;
|
|
408
|
+
},COMMAND_PRIORITY_LOW),
|
|
409
|
+
editor.registerRootListener(() => {
|
|
410
|
+
if (editor.getRootElement() === null) {
|
|
411
|
+
setIsText(false);
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
);
|
|
415
|
+
}, [editor, updatePopup]);
|
|
416
|
+
|
|
417
|
+
if (!isText || isLink) {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return createPortal(
|
|
422
|
+
<TextFormatFloatingToolbar
|
|
423
|
+
editor={editor}
|
|
424
|
+
anchorElem={anchorElem}
|
|
425
|
+
isLink={isLink}
|
|
426
|
+
isBold={isBold}
|
|
427
|
+
isItalic={isItalic}
|
|
428
|
+
isStrikethrough={isStrikethrough}
|
|
429
|
+
isSubscript={isSubscript}
|
|
430
|
+
isSuperscript={isSuperscript}
|
|
431
|
+
isUnderline={isUnderline}
|
|
432
|
+
isCode={isCode}
|
|
433
|
+
blockType={blockType}
|
|
434
|
+
setBlockType={setBlockType}
|
|
435
|
+
/>,
|
|
436
|
+
anchorElem
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export default function FloatingTextFormatToolbarPlugin({
|
|
441
|
+
anchorElem = document.body,
|
|
442
|
+
}) {
|
|
443
|
+
const [editor] = useLexicalComposerContext();
|
|
444
|
+
return useFloatingTextFormatToolbar(editor, anchorElem);
|
|
445
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function getDOMRangeRect(nativeSelection: any, rootElement: any) {
|
|
2
|
+
const domRange = nativeSelection.getRangeAt(0);
|
|
3
|
+
|
|
4
|
+
let rect;
|
|
5
|
+
|
|
6
|
+
if (nativeSelection.anchorNode === rootElement) {
|
|
7
|
+
let inner = rootElement;
|
|
8
|
+
while (inner.firstElementChild != null) {
|
|
9
|
+
inner = inner.firstElementChild;
|
|
10
|
+
}
|
|
11
|
+
rect = inner.getBoundingClientRect();
|
|
12
|
+
} else {
|
|
13
|
+
rect = domRange.getBoundingClientRect();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return rect;
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { $isAtNodeEnd } from "@lexical/selection";
|
|
2
|
+
|
|
3
|
+
export function getSelectedNode(selection: any) {
|
|
4
|
+
const anchor = selection.anchor;
|
|
5
|
+
const focus = selection.focus;
|
|
6
|
+
const anchorNode = selection.anchor.getNode();
|
|
7
|
+
const focusNode = selection.focus.getNode();
|
|
8
|
+
if (anchorNode === focusNode) {
|
|
9
|
+
return anchorNode;
|
|
10
|
+
}
|
|
11
|
+
const isBackward = selection.isBackward();
|
|
12
|
+
if (isBackward) {
|
|
13
|
+
return $isAtNodeEnd(focus) ? anchorNode : focusNode;
|
|
14
|
+
} else {
|
|
15
|
+
return $isAtNodeEnd(anchor) ? anchorNode : focusNode;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const VERTICAL_GAP = 10;
|
|
2
|
+
const HORIZONTAL_OFFSET = 5;
|
|
3
|
+
|
|
4
|
+
export function setFloatingElemPosition( targetRect: any, floatingElem: any, anchorElem: any, verticalGap = VERTICAL_GAP, horizontalOffset = HORIZONTAL_OFFSET ) {
|
|
5
|
+
const scrollerElem = anchorElem.parentElement;
|
|
6
|
+
|
|
7
|
+
if (targetRect === null || !scrollerElem) {
|
|
8
|
+
floatingElem.style.opacity = "0";
|
|
9
|
+
floatingElem.style.transform = "translate(-10000px, -10000px)";
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const floatingElemRect = floatingElem.getBoundingClientRect();
|
|
14
|
+
const anchorElementRect = anchorElem.getBoundingClientRect();
|
|
15
|
+
const editorScrollerRect = scrollerElem.getBoundingClientRect();
|
|
16
|
+
|
|
17
|
+
let top = targetRect.top - floatingElemRect.height - verticalGap;
|
|
18
|
+
let left = targetRect.left - horizontalOffset;
|
|
19
|
+
|
|
20
|
+
if (top < editorScrollerRect.top) {
|
|
21
|
+
top += floatingElemRect.height + targetRect.height + verticalGap * 2;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (left + floatingElemRect.width > editorScrollerRect.right) {
|
|
25
|
+
left = editorScrollerRect.right - floatingElemRect.width - horizontalOffset;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
top -= anchorElementRect.top;
|
|
29
|
+
left -= anchorElementRect.left;
|
|
30
|
+
|
|
31
|
+
floatingElem.style.opacity = "1";
|
|
32
|
+
floatingElem.style.transform = `translate(${left}px, ${top}px)`;
|
|
33
|
+
}
|
|
@@ -61,7 +61,7 @@ export type TextMatchTransformer = Readonly<{
|
|
|
61
61
|
export const UNDERLINE: TextFormatTransformer = {
|
|
62
62
|
format: ["underline"],
|
|
63
63
|
intraword: false,
|
|
64
|
-
tag: "
|
|
64
|
+
tag: "__",
|
|
65
65
|
type: "text-format"
|
|
66
66
|
};
|
|
67
67
|
/*
|
|
@@ -96,11 +96,11 @@ export const UNDERLINE: TextMatchTransformer = {
|
|
|
96
96
|
const modifiedTextTransformers = [EMOJI_NODE_MARKDOWN_TRANSFORM];
|
|
97
97
|
|
|
98
98
|
export const PLAYGROUND_TRANSFORMERS: Array<Transformer> = [
|
|
99
|
-
UNDERLINE,
|
|
100
99
|
...modifiedTextTransformers,
|
|
101
100
|
HR,
|
|
102
101
|
...ELEMENT_TRANSFORMERS,
|
|
103
102
|
|
|
104
103
|
CUSTOM_LINK_NODE_TRANSFORMER,
|
|
105
104
|
...TRANSFORMERS.splice(0, 13),
|
|
105
|
+
UNDERLINE,
|
|
106
106
|
];
|
|
@@ -42,6 +42,13 @@ export class CustomLinkNode extends LinkNode {
|
|
|
42
42
|
return $applyNodeReplacement(newLinkNode);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
addClassNamesToElement = (element: HTMLElement, classNames: string) => {
|
|
46
|
+
if (classNames) {
|
|
47
|
+
const classes = classNames.split(' ').filter(className => className.trim().length > 0);
|
|
48
|
+
element.classList.add(...classes);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
45
52
|
createDOM() {
|
|
46
53
|
const link = document.createElement("a");
|
|
47
54
|
|
|
@@ -49,7 +56,8 @@ export class CustomLinkNode extends LinkNode {
|
|
|
49
56
|
|
|
50
57
|
link.setAttribute("target", this.__target || "_blank");
|
|
51
58
|
|
|
52
|
-
utils.addClassNamesToElement(link, (this.__classNames || []).join(" "));
|
|
59
|
+
// utils.addClassNamesToElement(link, (this.__classNames || []).join(" "));
|
|
60
|
+
this.addClassNamesToElement(link, (this.__classNames || []).join(" "));
|
|
53
61
|
|
|
54
62
|
return link;
|
|
55
63
|
}
|
|
@@ -15,15 +15,27 @@ const positionEditorElement = (editor: HTMLElement, rect: DOMRect | null) => {
|
|
|
15
15
|
editor.style.left = "-1000px";
|
|
16
16
|
} else {
|
|
17
17
|
editor.style.opacity = "1";
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
18
|
+
// Add viewport height check
|
|
19
|
+
const editorHeight = editor.offsetHeight;
|
|
20
|
+
const viewportHeight = window.innerHeight;
|
|
21
|
+
let topPosition = rect.top + rect.height + 10;
|
|
22
|
+
|
|
23
|
+
// If editor would go off bottom of screen, position it above the selection instead
|
|
24
|
+
if (topPosition + editorHeight > viewportHeight) {
|
|
25
|
+
topPosition = rect.top - editorHeight - 10;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
editor.style.top = `${topPosition}px`;
|
|
29
|
+
|
|
30
|
+
// Ensure editor stays within horizontal bounds
|
|
31
|
+
const leftPosition = Math.max(
|
|
32
|
+
0,
|
|
33
|
+
Math.min(
|
|
34
|
+
rect.left + window.pageXOffset - editor.offsetWidth / 2 + rect.width / 2,
|
|
35
|
+
window.innerWidth - editor.offsetWidth
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
editor.style.left = `${leftPosition}px`;
|
|
27
39
|
}
|
|
28
40
|
};
|
|
29
41
|
|
|
@@ -23,6 +23,11 @@ function useEmojis(editor: LexicalEditor): void {
|
|
|
23
23
|
throw new Error('EmojisPlugin: EmojiNode not registered on editor');
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
if (!materialIcons || !Array.isArray(materialIcons)) {
|
|
27
|
+
return;
|
|
28
|
+
// throw new Error('EmojisPlugin: materialIcons not properly loaded');
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
return editor.registerNodeTransform(TextNode, (textNode) => {
|
|
27
32
|
if (EMOJI_NODE_MARKDOWN_REGEX.test(textNode.getTextContent()) || materialIcons.map((materialIcon : string) => ':' + materialIcon + ':').some((materialIcon : string) => textNode.getTextContent().includes(materialIcon))) {
|
|
28
33
|
|