5htp-core 0.4.8 → 0.4.9

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 (187) hide show
  1. package/package.json +5 -1
  2. package/src/client/assets/css/components/table.less +2 -0
  3. package/src/client/components/Form.ts +1 -1
  4. package/src/client/components/button.tsx +2 -1
  5. package/src/client/components/containers/Popover/index.tsx +2 -2
  6. package/src/client/components/dropdown/index.tsx +16 -6
  7. package/src/client/components/input/Slider/index.tsx +0 -2
  8. package/src/client/components/inputv3/Rte/Editor.tsx +271 -0
  9. package/src/client/components/inputv3/Rte/ToolbarPlugin/BlockFormat.tsx +220 -0
  10. package/src/client/components/inputv3/Rte/ToolbarPlugin/ElementFormat.tsx +107 -0
  11. package/src/client/components/inputv3/Rte/ToolbarPlugin/index.tsx +768 -0
  12. package/src/client/components/inputv3/Rte/appSettings.ts +36 -0
  13. package/src/client/components/inputv3/Rte/context/FlashMessageContext.tsx +68 -0
  14. package/src/client/components/inputv3/Rte/context/SettingsContext.tsx +71 -0
  15. package/src/client/components/inputv3/Rte/context/SharedAutocompleteContext.tsx +71 -0
  16. package/src/client/components/inputv3/Rte/context/SharedHistoryContext.tsx +35 -0
  17. package/src/client/components/inputv3/Rte/currentEditor.ts +42 -0
  18. package/src/client/components/inputv3/Rte/hooks/useFlashMessage.tsx +16 -0
  19. package/src/client/components/inputv3/Rte/hooks/useReport.ts +67 -0
  20. package/src/client/components/inputv3/Rte/images/emoji/1F600.png +0 -0
  21. package/src/client/components/inputv3/Rte/images/emoji/1F641.png +0 -0
  22. package/src/client/components/inputv3/Rte/images/emoji/1F642.png +0 -0
  23. package/src/client/components/inputv3/Rte/images/emoji/2764.png +0 -0
  24. package/src/client/components/inputv3/Rte/images/emoji/LICENSE.md +5 -0
  25. package/src/client/components/inputv3/Rte/images/icons/draggable-block-menu.svg +1 -0
  26. package/src/client/components/inputv3/Rte/images/icons/prettier-error.svg +1 -0
  27. package/src/client/components/inputv3/Rte/images/icons/prettier.svg +1 -0
  28. package/src/client/components/inputv3/Rte/images/image/LICENSE.md +5 -0
  29. package/src/client/components/inputv3/Rte/images/image-broken.svg +4 -0
  30. package/src/client/components/inputv3/Rte/images/logo.svg +1 -0
  31. package/src/client/components/inputv3/Rte/index.tsx +63 -79
  32. package/src/client/components/inputv3/Rte/nodes/AutocompleteNode.tsx +119 -0
  33. package/src/client/components/inputv3/Rte/nodes/EmojiNode.tsx +102 -0
  34. package/src/client/components/inputv3/Rte/nodes/EquationComponent.tsx +141 -0
  35. package/src/client/components/inputv3/Rte/nodes/EquationNode.tsx +174 -0
  36. package/src/client/components/inputv3/Rte/nodes/FigmaNode.tsx +135 -0
  37. package/src/client/components/inputv3/Rte/nodes/ImageComponent.tsx +468 -0
  38. package/src/client/components/inputv3/Rte/nodes/ImageNode.css +43 -0
  39. package/src/client/components/inputv3/Rte/nodes/ImageNode.tsx +266 -0
  40. package/src/client/components/inputv3/Rte/nodes/InlineImageNode/InlineImageComponent.tsx +402 -0
  41. package/src/client/components/inputv3/Rte/nodes/InlineImageNode/InlineImageNode.css +94 -0
  42. package/src/client/components/inputv3/Rte/nodes/InlineImageNode/InlineImageNode.tsx +294 -0
  43. package/src/client/components/inputv3/Rte/nodes/KeywordNode.ts +67 -0
  44. package/src/client/components/inputv3/Rte/nodes/LayoutContainerNode.ts +137 -0
  45. package/src/client/components/inputv3/Rte/nodes/LayoutItemNode.ts +71 -0
  46. package/src/client/components/inputv3/Rte/nodes/MentionNode.ts +130 -0
  47. package/src/client/components/inputv3/Rte/nodes/PageBreakNode/index.css +62 -0
  48. package/src/client/components/inputv3/Rte/nodes/PageBreakNode/index.tsx +170 -0
  49. package/src/client/components/inputv3/Rte/nodes/PlaygroundNodes.ts +76 -0
  50. package/src/client/components/inputv3/Rte/nodes/PollComponent.tsx +249 -0
  51. package/src/client/components/inputv3/Rte/nodes/PollNode.css +187 -0
  52. package/src/client/components/inputv3/Rte/nodes/PollNode.tsx +209 -0
  53. package/src/client/components/inputv3/Rte/nodes/StickyComponent.tsx +261 -0
  54. package/src/client/components/inputv3/Rte/nodes/StickyNode.css +37 -0
  55. package/src/client/components/inputv3/Rte/nodes/StickyNode.tsx +150 -0
  56. package/src/client/components/inputv3/Rte/nodes/TweetNode.tsx +223 -0
  57. package/src/client/components/inputv3/Rte/nodes/YouTubeNode.tsx +184 -0
  58. package/src/client/components/inputv3/Rte/plugins/ActionsPlugin/index.tsx +334 -0
  59. package/src/client/components/inputv3/Rte/plugins/AutoEmbedPlugin/index.tsx +352 -0
  60. package/src/client/components/inputv3/Rte/plugins/AutoLinkPlugin/index.tsx +32 -0
  61. package/src/client/components/inputv3/Rte/plugins/AutocompletePlugin/index.tsx +2529 -0
  62. package/src/client/components/inputv3/Rte/plugins/CodeActionMenuPlugin/components/CopyButton/index.tsx +70 -0
  63. package/src/client/components/inputv3/Rte/plugins/CodeActionMenuPlugin/components/PrettierButton/index.css +14 -0
  64. package/src/client/components/inputv3/Rte/plugins/CodeActionMenuPlugin/components/PrettierButton/index.tsx +156 -0
  65. package/src/client/components/inputv3/Rte/plugins/CodeActionMenuPlugin/index.css +54 -0
  66. package/src/client/components/inputv3/Rte/plugins/CodeActionMenuPlugin/index.tsx +190 -0
  67. package/src/client/components/inputv3/Rte/plugins/CodeActionMenuPlugin/utils.ts +33 -0
  68. package/src/client/components/inputv3/Rte/plugins/CodeHighlightPlugin/index.ts +21 -0
  69. package/src/client/components/inputv3/Rte/plugins/CollapsiblePlugin/Collapsible.css +57 -0
  70. package/src/client/components/inputv3/Rte/plugins/CollapsiblePlugin/CollapsibleContainerNode.ts +168 -0
  71. package/src/client/components/inputv3/Rte/plugins/CollapsiblePlugin/CollapsibleContentNode.ts +127 -0
  72. package/src/client/components/inputv3/Rte/plugins/CollapsiblePlugin/CollapsibleTitleNode.ts +152 -0
  73. package/src/client/components/inputv3/Rte/plugins/CollapsiblePlugin/CollapsibleUtils.ts +17 -0
  74. package/src/client/components/inputv3/Rte/plugins/CollapsiblePlugin/index.ts +284 -0
  75. package/src/client/components/inputv3/Rte/plugins/ComponentPickerPlugin/index.tsx +370 -0
  76. package/src/client/components/inputv3/Rte/plugins/ContextMenuPlugin/index.tsx +270 -0
  77. package/src/client/components/inputv3/Rte/plugins/DocsPlugin/index.tsx +20 -0
  78. package/src/client/components/inputv3/Rte/plugins/DragDropPastePlugin/index.ts +51 -0
  79. package/src/client/components/inputv3/Rte/plugins/DraggableBlockPlugin/index.css +36 -0
  80. package/src/client/components/inputv3/Rte/plugins/DraggableBlockPlugin/index.tsx +43 -0
  81. package/src/client/components/inputv3/Rte/plugins/EmojiPickerPlugin/index.tsx +198 -0
  82. package/src/client/components/inputv3/Rte/plugins/EmojisPlugin/index.ts +75 -0
  83. package/src/client/components/inputv3/Rte/plugins/EquationsPlugin/index.tsx +82 -0
  84. package/src/client/components/inputv3/Rte/plugins/FigmaPlugin/index.tsx +40 -0
  85. package/src/client/components/inputv3/Rte/plugins/FloatingLinkEditorPlugin/index.css +41 -0
  86. package/src/client/components/inputv3/Rte/plugins/FloatingLinkEditorPlugin/index.tsx +393 -0
  87. package/src/client/components/inputv3/Rte/plugins/FloatingTextFormatToolbarPlugin/index.css +141 -0
  88. package/src/client/components/inputv3/Rte/plugins/FloatingTextFormatToolbarPlugin/index.tsx +388 -0
  89. package/src/client/components/inputv3/Rte/plugins/ImagesPlugin/index.tsx +350 -0
  90. package/src/client/components/inputv3/Rte/plugins/InlineImagePlugin/index.tsx +336 -0
  91. package/src/client/components/inputv3/Rte/plugins/KeywordsPlugin/index.ts +56 -0
  92. package/src/client/components/inputv3/Rte/plugins/LayoutPlugin/InsertLayoutDialog.tsx +58 -0
  93. package/src/client/components/inputv3/Rte/plugins/LayoutPlugin/LayoutPlugin.tsx +219 -0
  94. package/src/client/components/inputv3/Rte/plugins/LinkPlugin/index.tsx +34 -0
  95. package/src/client/components/inputv3/Rte/plugins/ListMaxIndentLevelPlugin/index.ts +85 -0
  96. package/src/client/components/inputv3/Rte/plugins/MarkdownShortcutPlugin/index.tsx +16 -0
  97. package/src/client/components/inputv3/Rte/plugins/MarkdownTransformers/index.ts +324 -0
  98. package/src/client/components/inputv3/Rte/plugins/MaxLengthPlugin/index.tsx +53 -0
  99. package/src/client/components/inputv3/Rte/plugins/MentionsPlugin/index.tsx +696 -0
  100. package/src/client/components/inputv3/Rte/plugins/PageBreakPlugin/index.tsx +57 -0
  101. package/src/client/components/inputv3/Rte/plugins/PasteLogPlugin/index.tsx +54 -0
  102. package/src/client/components/inputv3/Rte/plugins/PollPlugin/index.tsx +86 -0
  103. package/src/client/components/inputv3/Rte/plugins/SpeechToTextPlugin/index.ts +125 -0
  104. package/src/client/components/inputv3/Rte/plugins/StickyPlugin/index.ts +22 -0
  105. package/src/client/components/inputv3/Rte/plugins/TabFocusPlugin/index.tsx +65 -0
  106. package/src/client/components/inputv3/Rte/plugins/TableActionMenuPlugin/index.tsx +773 -0
  107. package/src/client/components/inputv3/Rte/plugins/TableCellResizer/index.css +12 -0
  108. package/src/client/components/inputv3/Rte/plugins/TableCellResizer/index.tsx +436 -0
  109. package/src/client/components/inputv3/Rte/plugins/TableHoverActionsPlugin/index.tsx +287 -0
  110. package/src/client/components/inputv3/Rte/plugins/TableOfContentsPlugin/index.css +95 -0
  111. package/src/client/components/inputv3/Rte/plugins/TableOfContentsPlugin/index.tsx +197 -0
  112. package/src/client/components/inputv3/Rte/plugins/TablePlugin.tsx +178 -0
  113. package/src/client/components/inputv3/Rte/plugins/TestRecorderPlugin/index.tsx +468 -0
  114. package/src/client/components/inputv3/Rte/plugins/TreeViewPlugin/index.tsx +26 -0
  115. package/src/client/components/inputv3/Rte/plugins/TwitterPlugin/index.ts +41 -0
  116. package/src/client/components/inputv3/Rte/plugins/TypingPerfPlugin/index.ts +117 -0
  117. package/src/client/components/inputv3/Rte/plugins/YouTubePlugin/index.ts +41 -0
  118. package/src/client/components/inputv3/Rte/shared/canUseDOM.ts +4 -0
  119. package/src/client/components/inputv3/Rte/shared/caretFromPoint.ts +40 -0
  120. package/src/client/components/inputv3/Rte/shared/environment.ts +56 -0
  121. package/src/client/components/inputv3/Rte/shared/invariant.ts +26 -0
  122. package/src/client/components/inputv3/Rte/shared/normalizeClassNames.ts +21 -0
  123. package/src/client/components/inputv3/Rte/shared/react-test-utils.ts +18 -0
  124. package/src/client/components/inputv3/Rte/shared/reactPatches.ts +22 -0
  125. package/src/client/components/inputv3/Rte/shared/simpleDiffWithCursor.ts +49 -0
  126. package/src/client/components/inputv3/Rte/shared/useLayoutEffect.ts +19 -0
  127. package/src/client/components/inputv3/Rte/shared/warnOnlyOnce.ts +20 -0
  128. package/src/client/components/inputv3/Rte/style.less +30 -60
  129. package/src/client/components/inputv3/Rte/themes/CommentEditorTheme.css +13 -0
  130. package/src/client/components/inputv3/Rte/themes/CommentEditorTheme.ts +20 -0
  131. package/src/client/components/inputv3/Rte/themes/PlaygroundEditorTheme.css +447 -0
  132. package/src/client/components/inputv3/Rte/themes/PlaygroundEditorTheme.ts +120 -0
  133. package/src/client/components/inputv3/Rte/themes/StickyEditorTheme.css +13 -0
  134. package/src/client/components/inputv3/Rte/themes/StickyEditorTheme.ts +20 -0
  135. package/src/client/components/inputv3/Rte/ui/ColorPicker.css +88 -0
  136. package/src/client/components/inputv3/Rte/ui/ColorPicker.tsx +365 -0
  137. package/src/client/components/inputv3/Rte/ui/ContentEditable.css +44 -0
  138. package/src/client/components/inputv3/Rte/ui/ContentEditable.tsx +36 -0
  139. package/src/client/components/inputv3/Rte/ui/DropDown.tsx +259 -0
  140. package/src/client/components/inputv3/Rte/ui/DropdownColorPicker.tsx +41 -0
  141. package/src/client/components/inputv3/Rte/ui/EquationEditor.css +38 -0
  142. package/src/client/components/inputv3/Rte/ui/EquationEditor.tsx +56 -0
  143. package/src/client/components/inputv3/Rte/ui/FileInput.tsx +38 -0
  144. package/src/client/components/inputv3/Rte/ui/FlashMessage.css +28 -0
  145. package/src/client/components/inputv3/Rte/ui/FlashMessage.tsx +29 -0
  146. package/src/client/components/inputv3/Rte/ui/ImageResizer.tsx +316 -0
  147. package/src/client/components/inputv3/Rte/ui/Input.css +32 -0
  148. package/src/client/components/inputv3/Rte/ui/KatexRenderer.tsx +54 -0
  149. package/src/client/components/inputv3/Rte/ui/Switch.tsx +36 -0
  150. package/src/client/components/inputv3/Rte/utils/docSerialization.ts +77 -0
  151. package/src/client/components/inputv3/Rte/utils/emoji-list.ts +16615 -0
  152. package/src/client/components/inputv3/Rte/utils/getDOMRangeRect.ts +27 -0
  153. package/src/client/components/inputv3/Rte/utils/getSelectedNode.ts +27 -0
  154. package/src/client/components/inputv3/Rte/utils/guard.ts +10 -0
  155. package/src/client/components/inputv3/Rte/utils/isMobileWidth.ts +7 -0
  156. package/src/client/components/inputv3/Rte/utils/joinClasses.ts +13 -0
  157. package/src/client/components/inputv3/Rte/utils/setFloatingElemPosition.ts +51 -0
  158. package/src/client/components/inputv3/Rte/utils/setFloatingElemPositionForLinkEditor.ts +46 -0
  159. package/src/client/components/inputv3/Rte/utils/swipe.ts +127 -0
  160. package/src/client/components/inputv3/Rte/utils/url.ts +38 -0
  161. package/src/client/components/inputv3/base.tsx +8 -5
  162. package/src/client/components/inputv3/file/index.tsx +11 -5
  163. package/src/common/data/rte/nodes.ts +60 -9
  164. package/src/common/validation/index.ts +21 -2
  165. package/src/common/validation/schema.ts +42 -10
  166. package/src/common/validation/validator.ts +12 -4
  167. package/src/common/validation/validators.ts +82 -53
  168. package/src/server/services/router/http/multipart.ts +0 -1
  169. package/src/server/services/schema/index.ts +24 -2
  170. package/src/server/services/schema/request.ts +3 -2
  171. package/src/server/services/schema/rte.ts +110 -0
  172. package/src/{common/data/rte/index.ts → server/utils/rte.ts} +27 -16
  173. package/src/client/components/inputv3/Rte/ExampleTheme.tsx +0 -42
  174. package/src/client/components/inputv3/Rte/ToolbarPlugin.tsx +0 -167
  175. package/src/client/components/inputv3/Rte/icons/LICENSE.md +0 -5
  176. package/src/client/components/inputv3/Rte/icons/arrow-clockwise.svg +0 -4
  177. package/src/client/components/inputv3/Rte/icons/arrow-counterclockwise.svg +0 -4
  178. package/src/client/components/inputv3/Rte/icons/journal-text.svg +0 -5
  179. package/src/client/components/inputv3/Rte/icons/justify.svg +0 -3
  180. package/src/client/components/inputv3/Rte/icons/text-center.svg +0 -3
  181. package/src/client/components/inputv3/Rte/icons/text-left.svg +0 -3
  182. package/src/client/components/inputv3/Rte/icons/text-paragraph.svg +0 -3
  183. package/src/client/components/inputv3/Rte/icons/text-right.svg +0 -3
  184. package/src/client/components/inputv3/Rte/icons/type-bold.svg +0 -3
  185. package/src/client/components/inputv3/Rte/icons/type-italic.svg +0 -3
  186. package/src/client/components/inputv3/Rte/icons/type-strikethrough.svg +0 -3
  187. package/src/client/components/inputv3/Rte/icons/type-underline.svg +0 -3
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import 'katex/dist/katex.css';
10
+
11
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
12
+ import { $wrapNodeInElement } from '@lexical/utils';
13
+ import {
14
+ $createParagraphNode,
15
+ $insertNodes,
16
+ $isRootOrShadowRoot,
17
+ COMMAND_PRIORITY_EDITOR,
18
+ createCommand,
19
+ LexicalCommand,
20
+ LexicalEditor,
21
+ } from 'lexical';
22
+ import { useCallback, useEffect } from 'react';
23
+ import * as React from 'react';
24
+
25
+ import { $createEquationNode, EquationNode } from '../../nodes/EquationNode';
26
+ import KatexEquationAlterer from '../../ui/KatexEquationAlterer';
27
+
28
+ type CommandPayload = {
29
+ equation: string;
30
+ inline: boolean;
31
+ };
32
+
33
+ export const INSERT_EQUATION_COMMAND: LexicalCommand<CommandPayload> =
34
+ createCommand('INSERT_EQUATION_COMMAND');
35
+
36
+ export function InsertEquationDialog({
37
+ editor,
38
+ close,
39
+ }: {
40
+ editor: LexicalEditor;
41
+ close: () => void;
42
+ }): JSX.Element {
43
+ const onEquationConfirm = useCallback(
44
+ (equation: string, inline: boolean) => {
45
+ editor.dispatchCommand(INSERT_EQUATION_COMMAND, { equation, inline });
46
+ close();
47
+ },
48
+ [editor, close],
49
+ );
50
+
51
+ return <KatexEquationAlterer onConfirm={onEquationConfirm} />;
52
+ }
53
+
54
+ export default function EquationsPlugin(): JSX.Element | null {
55
+ const [editor] = useLexicalComposerContext();
56
+
57
+ useEffect(() => {
58
+ if (!editor.hasNodes([EquationNode])) {
59
+ throw new Error(
60
+ 'EquationsPlugins: EquationsNode not registered on editor',
61
+ );
62
+ }
63
+
64
+ return editor.registerCommand<CommandPayload>(
65
+ INSERT_EQUATION_COMMAND,
66
+ (payload) => {
67
+ const { equation, inline } = payload;
68
+ const equationNode = $createEquationNode(equation, inline);
69
+
70
+ $insertNodes([equationNode]);
71
+ if ($isRootOrShadowRoot(equationNode.getParentOrThrow())) {
72
+ $wrapNodeInElement(equationNode, $createParagraphNode).selectEnd();
73
+ }
74
+
75
+ return true;
76
+ },
77
+ COMMAND_PRIORITY_EDITOR,
78
+ );
79
+ }, [editor]);
80
+
81
+ return null;
82
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
10
+ import {$insertNodeToNearestRoot} from '@lexical/utils';
11
+ import {COMMAND_PRIORITY_EDITOR, createCommand, LexicalCommand} from 'lexical';
12
+ import {useEffect} from 'react';
13
+
14
+ import {$createFigmaNode, FigmaNode} from '../../nodes/FigmaNode';
15
+
16
+ export const INSERT_FIGMA_COMMAND: LexicalCommand<string> = createCommand(
17
+ 'INSERT_FIGMA_COMMAND',
18
+ );
19
+
20
+ export default function FigmaPlugin(): JSX.Element | null {
21
+ const [editor] = useLexicalComposerContext();
22
+
23
+ useEffect(() => {
24
+ if (!editor.hasNodes([FigmaNode])) {
25
+ throw new Error('FigmaPlugin: FigmaNode not registered on editor');
26
+ }
27
+
28
+ return editor.registerCommand<string>(
29
+ INSERT_FIGMA_COMMAND,
30
+ (payload) => {
31
+ const figmaNode = $createFigmaNode(payload);
32
+ $insertNodeToNearestRoot(figmaNode);
33
+ return true;
34
+ },
35
+ COMMAND_PRIORITY_EDITOR,
36
+ );
37
+ }, [editor]);
38
+
39
+ return null;
40
+ }
@@ -0,0 +1,41 @@
1
+ .link-editor {
2
+ display: flex;
3
+ position: absolute;
4
+ top: 0;
5
+ left: 0;
6
+ z-index: 10;
7
+ max-width: 400px;
8
+ width: 100%;
9
+ opacity: 0;
10
+ background-color: #fff;
11
+ box-shadow: 0 5px 10px rgba(0, 0, 0, 0.3);
12
+ border-radius: 0 0 8px 8px;
13
+ transition: opacity 0.5s;
14
+ will-change: transform;
15
+ }
16
+
17
+ .link-editor .button {
18
+ width: 20px;
19
+ height: 20px;
20
+ display: inline-block;
21
+ padding: 6px;
22
+ border-radius: 8px;
23
+ cursor: pointer;
24
+ margin: 0 2px;
25
+ }
26
+
27
+ .link-editor .button.hovered {
28
+ width: 20px;
29
+ height: 20px;
30
+ display: inline-block;
31
+ background-color: #eee;
32
+ }
33
+
34
+ .link-editor .button i,
35
+ .actions i {
36
+ background-size: contain;
37
+ display: inline-block;
38
+ height: 20px;
39
+ width: 20px;
40
+ vertical-align: -0.25em;
41
+ }
@@ -0,0 +1,393 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+ import './index.css';
9
+
10
+ import {
11
+ $createLinkNode,
12
+ $isAutoLinkNode,
13
+ $isLinkNode,
14
+ TOGGLE_LINK_COMMAND,
15
+ } from '@lexical/link';
16
+ import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
17
+ import {$findMatchingParent, mergeRegister} from '@lexical/utils';
18
+ import {
19
+ $getSelection,
20
+ $isLineBreakNode,
21
+ $isRangeSelection,
22
+ BaseSelection,
23
+ CLICK_COMMAND,
24
+ COMMAND_PRIORITY_CRITICAL,
25
+ COMMAND_PRIORITY_HIGH,
26
+ COMMAND_PRIORITY_LOW,
27
+ KEY_ESCAPE_COMMAND,
28
+ LexicalEditor,
29
+ SELECTION_CHANGE_COMMAND,
30
+ } from 'lexical';
31
+ import {Dispatch, useCallback, useEffect, useRef, useState} from 'react';
32
+ import * as React from 'react';
33
+ import {createPortal} from 'react-dom';
34
+
35
+ import {getSelectedNode} from '../../utils/getSelectedNode';
36
+ import {setFloatingElemPositionForLinkEditor} from '../../utils/setFloatingElemPositionForLinkEditor';
37
+ import {sanitizeUrl} from '../../utils/url';
38
+
39
+ function FloatingLinkEditor({
40
+ editor,
41
+ isLink,
42
+ setIsLink,
43
+ anchorElem,
44
+ isLinkEditMode,
45
+ setIsLinkEditMode,
46
+ }: {
47
+ editor: LexicalEditor;
48
+ isLink: boolean;
49
+ setIsLink: Dispatch<boolean>;
50
+ anchorElem: HTMLElement;
51
+ isLinkEditMode: boolean;
52
+ setIsLinkEditMode: Dispatch<boolean>;
53
+ }): JSX.Element {
54
+ const editorRef = useRef<HTMLDivElement | null>(null);
55
+ const inputRef = useRef<HTMLInputElement>(null);
56
+ const [linkUrl, setLinkUrl] = useState('');
57
+ const [editedLinkUrl, setEditedLinkUrl] = useState('https://');
58
+ const [lastSelection, setLastSelection] = useState<BaseSelection | null>(
59
+ null,
60
+ );
61
+
62
+ const $updateLinkEditor = useCallback(() => {
63
+ const selection = $getSelection();
64
+ if ($isRangeSelection(selection)) {
65
+ const node = getSelectedNode(selection);
66
+ const linkParent = $findMatchingParent(node, $isLinkNode);
67
+
68
+ if (linkParent) {
69
+ setLinkUrl(linkParent.getURL());
70
+ } else if ($isLinkNode(node)) {
71
+ setLinkUrl(node.getURL());
72
+ } else {
73
+ setLinkUrl('');
74
+ }
75
+ if (isLinkEditMode) {
76
+ setEditedLinkUrl(linkUrl);
77
+ }
78
+ }
79
+ const editorElem = editorRef.current;
80
+ const nativeSelection = window.getSelection();
81
+ const activeElement = document.activeElement;
82
+
83
+ if (editorElem === null) {
84
+ return;
85
+ }
86
+
87
+ const rootElement = editor.getRootElement();
88
+
89
+ if (
90
+ selection !== null &&
91
+ nativeSelection !== null &&
92
+ rootElement !== null &&
93
+ rootElement.contains(nativeSelection.anchorNode) &&
94
+ editor.isEditable()
95
+ ) {
96
+ const domRect: DOMRect | undefined =
97
+ nativeSelection.focusNode?.parentElement?.getBoundingClientRect();
98
+ if (domRect) {
99
+ domRect.y += 40;
100
+ setFloatingElemPositionForLinkEditor(domRect, editorElem, anchorElem);
101
+ }
102
+ setLastSelection(selection);
103
+ } else if (!activeElement || activeElement.className !== 'link-input') {
104
+ if (rootElement !== null) {
105
+ setFloatingElemPositionForLinkEditor(null, editorElem, anchorElem);
106
+ }
107
+ setLastSelection(null);
108
+ setIsLinkEditMode(false);
109
+ setLinkUrl('');
110
+ }
111
+
112
+ return true;
113
+ }, [anchorElem, editor, setIsLinkEditMode, isLinkEditMode, linkUrl]);
114
+
115
+ useEffect(() => {
116
+ const scrollerElem = anchorElem.parentElement;
117
+
118
+ const update = () => {
119
+ editor.getEditorState().read(() => {
120
+ $updateLinkEditor();
121
+ });
122
+ };
123
+
124
+ window.addEventListener('resize', update);
125
+
126
+ if (scrollerElem) {
127
+ scrollerElem.addEventListener('scroll', update);
128
+ }
129
+
130
+ return () => {
131
+ window.removeEventListener('resize', update);
132
+
133
+ if (scrollerElem) {
134
+ scrollerElem.removeEventListener('scroll', update);
135
+ }
136
+ };
137
+ }, [anchorElem.parentElement, editor, $updateLinkEditor]);
138
+
139
+ useEffect(() => {
140
+ return mergeRegister(
141
+ editor.registerUpdateListener(({editorState}) => {
142
+ editorState.read(() => {
143
+ $updateLinkEditor();
144
+ });
145
+ }),
146
+
147
+ editor.registerCommand(
148
+ SELECTION_CHANGE_COMMAND,
149
+ () => {
150
+ $updateLinkEditor();
151
+ return true;
152
+ },
153
+ COMMAND_PRIORITY_LOW,
154
+ ),
155
+ editor.registerCommand(
156
+ KEY_ESCAPE_COMMAND,
157
+ () => {
158
+ if (isLink) {
159
+ setIsLink(false);
160
+ return true;
161
+ }
162
+ return false;
163
+ },
164
+ COMMAND_PRIORITY_HIGH,
165
+ ),
166
+ );
167
+ }, [editor, $updateLinkEditor, setIsLink, isLink]);
168
+
169
+ useEffect(() => {
170
+ editor.getEditorState().read(() => {
171
+ $updateLinkEditor();
172
+ });
173
+ }, [editor, $updateLinkEditor]);
174
+
175
+ useEffect(() => {
176
+ if (isLinkEditMode && inputRef.current) {
177
+ inputRef.current.focus();
178
+ }
179
+ }, [isLinkEditMode, isLink]);
180
+
181
+ const monitorInputInteraction = (
182
+ event: React.KeyboardEvent<HTMLInputElement>,
183
+ ) => {
184
+ if (event.key === 'Enter') {
185
+ event.preventDefault();
186
+ handleLinkSubmission();
187
+ } else if (event.key === 'Escape') {
188
+ event.preventDefault();
189
+ setIsLinkEditMode(false);
190
+ }
191
+ };
192
+
193
+ const handleLinkSubmission = () => {
194
+ if (lastSelection !== null) {
195
+ if (linkUrl !== '') {
196
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl(editedLinkUrl));
197
+ editor.update(() => {
198
+ const selection = $getSelection();
199
+ if ($isRangeSelection(selection)) {
200
+ const parent = getSelectedNode(selection).getParent();
201
+ if ($isAutoLinkNode(parent)) {
202
+ const linkNode = $createLinkNode(parent.getURL(), {
203
+ rel: parent.__rel,
204
+ target: parent.__target,
205
+ title: parent.__title,
206
+ });
207
+ parent.replace(linkNode, true);
208
+ }
209
+ }
210
+ });
211
+ }
212
+ setEditedLinkUrl('https://');
213
+ setIsLinkEditMode(false);
214
+ }
215
+ };
216
+
217
+ return (
218
+ <div ref={editorRef} className="link-editor">
219
+ {!isLink ? null : isLinkEditMode ? (
220
+ <>
221
+ <input
222
+ ref={inputRef}
223
+ className="link-input"
224
+ value={editedLinkUrl}
225
+ onChange={(event) => {
226
+ setEditedLinkUrl(event.target.value);
227
+ }}
228
+ onKeyDown={(event) => {
229
+ monitorInputInteraction(event);
230
+ }}
231
+ />
232
+ <div>
233
+ <div
234
+ className="link-cancel"
235
+ role="button"
236
+ tabIndex={0}
237
+ onMouseDown={(event) => event.preventDefault()}
238
+ onClick={() => {
239
+ setIsLinkEditMode(false);
240
+ }}
241
+ />
242
+
243
+ <div
244
+ className="link-confirm"
245
+ role="button"
246
+ tabIndex={0}
247
+ onMouseDown={(event) => event.preventDefault()}
248
+ onClick={handleLinkSubmission}
249
+ />
250
+ </div>
251
+ </>
252
+ ) : (
253
+ <div className="link-view">
254
+ <a
255
+ href={sanitizeUrl(linkUrl)}
256
+ target="_blank"
257
+ rel="noopener noreferrer">
258
+ {linkUrl}
259
+ </a>
260
+ <div
261
+ className="link-edit"
262
+ role="button"
263
+ tabIndex={0}
264
+ onMouseDown={(event) => event.preventDefault()}
265
+ onClick={() => {
266
+ setEditedLinkUrl(linkUrl);
267
+ setIsLinkEditMode(true);
268
+ }}
269
+ />
270
+ <div
271
+ className="link-trash"
272
+ role="button"
273
+ tabIndex={0}
274
+ onMouseDown={(event) => event.preventDefault()}
275
+ onClick={() => {
276
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
277
+ }}
278
+ />
279
+ </div>
280
+ )}
281
+ </div>
282
+ );
283
+ }
284
+
285
+ function useFloatingLinkEditorToolbar(
286
+ editor: LexicalEditor,
287
+ anchorElem: HTMLElement,
288
+ isLinkEditMode: boolean,
289
+ setIsLinkEditMode: Dispatch<boolean>,
290
+ ): JSX.Element | null {
291
+ const [activeEditor, setActiveEditor] = useState(editor);
292
+ const [isLink, setIsLink] = useState(false);
293
+
294
+ useEffect(() => {
295
+ function $updateToolbar() {
296
+ const selection = $getSelection();
297
+ if ($isRangeSelection(selection)) {
298
+ const focusNode = getSelectedNode(selection);
299
+ const focusLinkNode = $findMatchingParent(focusNode, $isLinkNode);
300
+ const focusAutoLinkNode = $findMatchingParent(
301
+ focusNode,
302
+ $isAutoLinkNode,
303
+ );
304
+ if (!(focusLinkNode || focusAutoLinkNode)) {
305
+ setIsLink(false);
306
+ return;
307
+ }
308
+ const badNode = selection
309
+ .getNodes()
310
+ .filter((node) => !$isLineBreakNode(node))
311
+ .find((node) => {
312
+ const linkNode = $findMatchingParent(node, $isLinkNode);
313
+ const autoLinkNode = $findMatchingParent(node, $isAutoLinkNode);
314
+ return (
315
+ (focusLinkNode && !focusLinkNode.is(linkNode)) ||
316
+ (linkNode && !linkNode.is(focusLinkNode)) ||
317
+ (focusAutoLinkNode && !focusAutoLinkNode.is(autoLinkNode)) ||
318
+ (autoLinkNode &&
319
+ (!autoLinkNode.is(focusAutoLinkNode) ||
320
+ autoLinkNode.getIsUnlinked()))
321
+ );
322
+ });
323
+ if (!badNode) {
324
+ setIsLink(true);
325
+ } else {
326
+ setIsLink(false);
327
+ }
328
+ }
329
+ }
330
+ return mergeRegister(
331
+ editor.registerUpdateListener(({editorState}) => {
332
+ editorState.read(() => {
333
+ $updateToolbar();
334
+ });
335
+ }),
336
+ editor.registerCommand(
337
+ SELECTION_CHANGE_COMMAND,
338
+ (_payload, newEditor) => {
339
+ $updateToolbar();
340
+ setActiveEditor(newEditor);
341
+ return false;
342
+ },
343
+ COMMAND_PRIORITY_CRITICAL,
344
+ ),
345
+ editor.registerCommand(
346
+ CLICK_COMMAND,
347
+ (payload) => {
348
+ const selection = $getSelection();
349
+ if ($isRangeSelection(selection)) {
350
+ const node = getSelectedNode(selection);
351
+ const linkNode = $findMatchingParent(node, $isLinkNode);
352
+ if ($isLinkNode(linkNode) && (payload.metaKey || payload.ctrlKey)) {
353
+ window.open(linkNode.getURL(), '_blank');
354
+ return true;
355
+ }
356
+ }
357
+ return false;
358
+ },
359
+ COMMAND_PRIORITY_LOW,
360
+ ),
361
+ );
362
+ }, [editor]);
363
+
364
+ return createPortal(
365
+ <FloatingLinkEditor
366
+ editor={activeEditor}
367
+ isLink={isLink}
368
+ anchorElem={anchorElem}
369
+ setIsLink={setIsLink}
370
+ isLinkEditMode={isLinkEditMode}
371
+ setIsLinkEditMode={setIsLinkEditMode}
372
+ />,
373
+ anchorElem,
374
+ );
375
+ }
376
+
377
+ export default function FloatingLinkEditorPlugin({
378
+ anchorElem = document.body,
379
+ isLinkEditMode,
380
+ setIsLinkEditMode,
381
+ }: {
382
+ anchorElem?: HTMLElement;
383
+ isLinkEditMode: boolean;
384
+ setIsLinkEditMode: Dispatch<boolean>;
385
+ }): JSX.Element | null {
386
+ const [editor] = useLexicalComposerContext();
387
+ return useFloatingLinkEditorToolbar(
388
+ editor,
389
+ anchorElem,
390
+ isLinkEditMode,
391
+ setIsLinkEditMode,
392
+ );
393
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ .floating-text-format-popup {
10
+ display: flex;
11
+ background: #fff;
12
+ padding: 4px;
13
+ vertical-align: middle;
14
+ position: absolute;
15
+ top: 0;
16
+ left: 0;
17
+ z-index: 10;
18
+ opacity: 0;
19
+ box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.3);
20
+ border-radius: 8px;
21
+ transition: opacity 0.5s;
22
+ height: 35px;
23
+ will-change: transform;
24
+ }
25
+
26
+ .floating-text-format-popup button.popup-item {
27
+ border: 0;
28
+ display: flex;
29
+ background: none;
30
+ border-radius: 10px;
31
+ padding: 8px;
32
+ cursor: pointer;
33
+ vertical-align: middle;
34
+ }
35
+
36
+ .floating-text-format-popup button.popup-item:disabled {
37
+ cursor: not-allowed;
38
+ }
39
+
40
+ .floating-text-format-popup button.popup-item.spaced {
41
+ margin-right: 2px;
42
+ }
43
+
44
+ .floating-text-format-popup button.popup-item i.format {
45
+ background-size: contain;
46
+ height: 18px;
47
+ width: 18px;
48
+ margin-top: 2px;
49
+ vertical-align: -0.25em;
50
+ display: flex;
51
+ opacity: 0.6;
52
+ }
53
+
54
+ .floating-text-format-popup button.popup-item:disabled i.format {
55
+ opacity: 0.2;
56
+ }
57
+
58
+ .floating-text-format-popup button.popup-item.active {
59
+ background-color: rgba(223, 232, 250, 0.3);
60
+ }
61
+
62
+ .floating-text-format-popup button.popup-item.active i {
63
+ opacity: 1;
64
+ }
65
+
66
+ .floating-text-format-popup .popup-item:hover:not([disabled]) {
67
+ background-color: #eee;
68
+ }
69
+
70
+ .floating-text-format-popup select.popup-item {
71
+ border: 0;
72
+ display: flex;
73
+ background: none;
74
+ border-radius: 10px;
75
+ padding: 8px;
76
+ vertical-align: middle;
77
+ -webkit-appearance: none;
78
+ -moz-appearance: none;
79
+ width: 70px;
80
+ font-size: 14px;
81
+ color: #777;
82
+ text-overflow: ellipsis;
83
+ }
84
+
85
+ .floating-text-format-popup select.code-language {
86
+ text-transform: capitalize;
87
+ width: 130px;
88
+ }
89
+
90
+ .floating-text-format-popup .popup-item .text {
91
+ display: flex;
92
+ line-height: 20px;
93
+ vertical-align: middle;
94
+ font-size: 14px;
95
+ color: #777;
96
+ text-overflow: ellipsis;
97
+ width: 70px;
98
+ overflow: hidden;
99
+ height: 20px;
100
+ text-align: left;
101
+ }
102
+
103
+ .floating-text-format-popup .popup-item .icon {
104
+ display: flex;
105
+ width: 20px;
106
+ height: 20px;
107
+ user-select: none;
108
+ margin-right: 8px;
109
+ line-height: 16px;
110
+ background-size: contain;
111
+ }
112
+
113
+ .floating-text-format-popup i.chevron-down {
114
+ margin-top: 3px;
115
+ width: 16px;
116
+ height: 16px;
117
+ display: flex;
118
+ user-select: none;
119
+ }
120
+
121
+ .floating-text-format-popup i.chevron-down.inside {
122
+ width: 16px;
123
+ height: 16px;
124
+ display: flex;
125
+ margin-left: -25px;
126
+ margin-top: 11px;
127
+ margin-right: 10px;
128
+ pointer-events: none;
129
+ }
130
+
131
+ .floating-text-format-popup .divider {
132
+ width: 1px;
133
+ background-color: #eee;
134
+ margin: 0 4px;
135
+ }
136
+
137
+ @media (max-width: 1024px) {
138
+ .floating-text-format-popup button.insert-comment {
139
+ display: none;
140
+ }
141
+ }