@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.
Files changed (51) hide show
  1. package/dist/components/markdownEditor/Editor.d.ts +3 -1
  2. package/dist/components/markdownEditor/Editor.d.ts.map +1 -1
  3. package/dist/components/markdownEditor/Editor.js +25 -4
  4. package/dist/components/markdownEditor/Editor.js.map +1 -1
  5. package/dist/components/markdownEditor/MarkdownPreview.d.ts +3 -1
  6. package/dist/components/markdownEditor/MarkdownPreview.d.ts.map +1 -1
  7. package/dist/components/markdownEditor/MarkdownPreview.js +14 -2
  8. package/dist/components/markdownEditor/MarkdownPreview.js.map +1 -1
  9. package/dist/components/markdownEditor/MarkdownPreviewLight.d.ts.map +1 -1
  10. package/dist/components/markdownEditor/MarkdownPreviewLight.js +5 -1
  11. package/dist/components/markdownEditor/MarkdownPreviewLight.js.map +1 -1
  12. package/dist/components/markdownEditor/plugins/FloatingTextMenu/FloatingTextFormatToolbarPlugin.d.ts +13 -0
  13. package/dist/components/markdownEditor/plugins/FloatingTextMenu/FloatingTextFormatToolbarPlugin.d.ts.map +1 -0
  14. package/dist/components/markdownEditor/plugins/FloatingTextMenu/FloatingTextFormatToolbarPlugin.js +311 -0
  15. package/dist/components/markdownEditor/plugins/FloatingTextMenu/FloatingTextFormatToolbarPlugin.js.map +1 -0
  16. package/dist/components/markdownEditor/plugins/FloatingTextMenu/getDOMRangeRect.d.ts +2 -0
  17. package/dist/components/markdownEditor/plugins/FloatingTextMenu/getDOMRangeRect.d.ts.map +1 -0
  18. package/dist/components/markdownEditor/plugins/FloatingTextMenu/getDOMRangeRect.js +20 -0
  19. package/dist/components/markdownEditor/plugins/FloatingTextMenu/getDOMRangeRect.js.map +1 -0
  20. package/dist/components/markdownEditor/plugins/FloatingTextMenu/getSelectNode.d.ts +2 -0
  21. package/dist/components/markdownEditor/plugins/FloatingTextMenu/getSelectNode.d.ts.map +1 -0
  22. package/dist/components/markdownEditor/plugins/FloatingTextMenu/getSelectNode.js +22 -0
  23. package/dist/components/markdownEditor/plugins/FloatingTextMenu/getSelectNode.js.map +1 -0
  24. package/dist/components/markdownEditor/plugins/FloatingTextMenu/setFloatingElemPosition.d.ts +2 -0
  25. package/dist/components/markdownEditor/plugins/FloatingTextMenu/setFloatingElemPosition.d.ts.map +1 -0
  26. package/dist/components/markdownEditor/plugins/FloatingTextMenu/setFloatingElemPosition.js +30 -0
  27. package/dist/components/markdownEditor/plugins/FloatingTextMenu/setFloatingElemPosition.js.map +1 -0
  28. package/dist/components/markdownEditor/plugins/MarkdownTransformers.js +2 -2
  29. package/dist/components/markdownEditor/plugins/MarkdownTransformers.js.map +1 -1
  30. package/dist/components/markdownEditor/plugins/customLink/CustomLinkNode.d.ts +1 -0
  31. package/dist/components/markdownEditor/plugins/customLink/CustomLinkNode.d.ts.map +1 -1
  32. package/dist/components/markdownEditor/plugins/customLink/CustomLinkNode.js +8 -5
  33. package/dist/components/markdownEditor/plugins/customLink/CustomLinkNode.js.map +1 -1
  34. package/dist/components/markdownEditor/plugins/customLink/FloatingLinkEditor.d.ts.map +1 -1
  35. package/dist/components/markdownEditor/plugins/customLink/FloatingLinkEditor.js +12 -7
  36. package/dist/components/markdownEditor/plugins/customLink/FloatingLinkEditor.js.map +1 -1
  37. package/dist/components/markdownEditor/plugins/emoji/EmojisPlugin.d.ts.map +1 -1
  38. package/dist/components/markdownEditor/plugins/emoji/EmojisPlugin.js +4 -0
  39. package/dist/components/markdownEditor/plugins/emoji/EmojisPlugin.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/components/markdownEditor/Editor.tsx +33 -16
  42. package/src/components/markdownEditor/MarkdownPreview.tsx +5 -3
  43. package/src/components/markdownEditor/MarkdownPreviewLight.tsx +5 -1
  44. package/src/components/markdownEditor/plugins/FloatingTextMenu/FloatingTextFormatToolbarPlugin.tsx +445 -0
  45. package/src/components/markdownEditor/plugins/FloatingTextMenu/getDOMRangeRect.tsx +17 -0
  46. package/src/components/markdownEditor/plugins/FloatingTextMenu/getSelectNode.tsx +17 -0
  47. package/src/components/markdownEditor/plugins/FloatingTextMenu/setFloatingElemPosition.tsx +33 -0
  48. package/src/components/markdownEditor/plugins/MarkdownTransformers.ts +2 -2
  49. package/src/components/markdownEditor/plugins/customLink/CustomLinkNode.tsx +9 -1
  50. package/src/components/markdownEditor/plugins/customLink/FloatingLinkEditor.tsx +21 -9
  51. package/src/components/markdownEditor/plugins/emoji/EmojisPlugin.tsx +5 -0
@@ -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
- editor.style.top = `${rect.top + rect.height + 10}px`;
19
- editor.style.left = `${
20
- rect.left + window.pageXOffset - editor.offsetWidth / 2 + rect.width / 2 < 0
21
- ? 0
22
- : rect.left
23
- + window.pageXOffset
24
- - editor.offsetWidth / 2
25
- + rect.width / 2
26
- }px`;
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