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,334 @@
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 type { LexicalEditor } from 'lexical';
10
+
11
+ import { $createCodeNode, $isCodeNode } from '@lexical/code';
12
+ import {
13
+ editorStateFromSerializedDocument,
14
+ exportFile,
15
+ importFile,
16
+ SerializedDocument,
17
+ serializedDocumentFromEditorState,
18
+ } from '@lexical/file';
19
+ import {
20
+ $convertFromMarkdownString,
21
+ $convertToMarkdownString,
22
+ } from '@lexical/markdown';
23
+ import { useCollaborationContext } from '@lexical/react/LexicalCollaborationContext';
24
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
25
+ import { mergeRegister } from '@lexical/utils';
26
+ import { CONNECTED_COMMAND, TOGGLE_CONNECT_COMMAND } from '@lexical/yjs';
27
+ import {
28
+ $createTextNode,
29
+ $getRoot,
30
+ $isParagraphNode,
31
+ CLEAR_EDITOR_COMMAND,
32
+ CLEAR_HISTORY_COMMAND,
33
+ COMMAND_PRIORITY_EDITOR,
34
+ } from 'lexical';
35
+ import { useCallback, useEffect, useState, JSX } from 'react';
36
+
37
+ // Core
38
+ import useContext from '@/client/context';
39
+ import Button from '@client/components/button';
40
+
41
+ import { INITIAL_SETTINGS } from '../../appSettings';
42
+ import useFlashMessage from '../../hooks/useFlashMessage';
43
+ import { docFromHash, docToHash } from '../../utils/docSerialization';
44
+ import { PLAYGROUND_TRANSFORMERS } from '../MarkdownTransformers';
45
+ import {
46
+ SPEECH_TO_TEXT_COMMAND,
47
+ SUPPORT_SPEECH_RECOGNITION,
48
+ } from '../SpeechToTextPlugin';
49
+
50
+ async function sendEditorState(editor: LexicalEditor): Promise<void> {
51
+ const stringifiedEditorState = JSON.stringify(editor.getEditorState());
52
+ try {
53
+ await fetch('http://localhost:1235/setEditorState', {
54
+ body: stringifiedEditorState,
55
+ headers: {
56
+ Accept: 'application/json',
57
+ 'Content-type': 'application/json',
58
+ },
59
+ method: 'POST',
60
+ });
61
+ } catch {
62
+ // NO-OP
63
+ }
64
+ }
65
+
66
+ async function validateEditorState(editor: LexicalEditor): Promise<void> {
67
+ const stringifiedEditorState = JSON.stringify(editor.getEditorState());
68
+ let response = null;
69
+ try {
70
+ response = await fetch('http://localhost:1235/validateEditorState', {
71
+ body: stringifiedEditorState,
72
+ headers: {
73
+ Accept: 'application/json',
74
+ 'Content-type': 'application/json',
75
+ },
76
+ method: 'POST',
77
+ });
78
+ } catch {
79
+ // NO-OP
80
+ }
81
+ if (response !== null && response.status === 403) {
82
+ throw new Error(
83
+ 'Editor state validation failed! Server did not accept changes.',
84
+ );
85
+ }
86
+ }
87
+
88
+ async function shareDoc(doc: SerializedDocument): Promise<void> {
89
+ const url = new URL(window.location.toString());
90
+ url.hash = await docToHash(doc);
91
+ const newUrl = url.toString();
92
+ window.history.replaceState({}, '', newUrl);
93
+ await window.navigator.clipboard.writeText(newUrl);
94
+ }
95
+
96
+ export default function ActionsPlugin({
97
+ isRichText,
98
+ shouldPreserveNewLinesInMarkdown,
99
+ }: {
100
+ isRichText: boolean;
101
+ shouldPreserveNewLinesInMarkdown: boolean;
102
+ }): JSX.Element {
103
+
104
+ const [editor] = useLexicalComposerContext();
105
+ const [isEditable, setIsEditable] = useState(() => editor.isEditable());
106
+ const [isSpeechToText, setIsSpeechToText] = useState(false);
107
+ const [connected, setConnected] = useState(false);
108
+ const [isEditorEmpty, setIsEditorEmpty] = useState(true);
109
+ const showFlashMessage = useFlashMessage();
110
+ const { isCollabActive } = useCollaborationContext();
111
+
112
+ const { modal } = useContext();
113
+
114
+ useEffect(() => {
115
+ if (INITIAL_SETTINGS.isCollab) {
116
+ return;
117
+ }
118
+ docFromHash(window.location.hash).then((doc) => {
119
+ if (doc && doc.source === 'Playground') {
120
+ editor.setEditorState(editorStateFromSerializedDocument(editor, doc));
121
+ editor.dispatchCommand(CLEAR_HISTORY_COMMAND, undefined);
122
+ }
123
+ });
124
+ }, [editor]);
125
+
126
+ useEffect(() => {
127
+ return mergeRegister(
128
+ editor.registerEditableListener((editable) => {
129
+ setIsEditable(editable);
130
+ }),
131
+ editor.registerCommand<boolean>(
132
+ CONNECTED_COMMAND,
133
+ (payload) => {
134
+ const isConnected = payload;
135
+ setConnected(isConnected);
136
+ return false;
137
+ },
138
+ COMMAND_PRIORITY_EDITOR,
139
+ ),
140
+ );
141
+ }, [editor]);
142
+
143
+ useEffect(() => {
144
+ return editor.registerUpdateListener(
145
+ ({ dirtyElements, prevEditorState, tags }) => {
146
+ // If we are in read only mode, send the editor state
147
+ // to server and ask for validation if possible.
148
+ if (
149
+ !isEditable &&
150
+ dirtyElements.size > 0 &&
151
+ !tags.has('historic') &&
152
+ !tags.has('collaboration')
153
+ ) {
154
+ validateEditorState(editor);
155
+ }
156
+ editor.getEditorState().read(() => {
157
+ const root = $getRoot();
158
+ const children = root.getChildren();
159
+
160
+ if (children.length > 1) {
161
+ setIsEditorEmpty(false);
162
+ } else {
163
+ if ($isParagraphNode(children[0])) {
164
+ const paragraphChildren = children[0].getChildren();
165
+ setIsEditorEmpty(paragraphChildren.length === 0);
166
+ } else {
167
+ setIsEditorEmpty(false);
168
+ }
169
+ }
170
+ });
171
+ },
172
+ );
173
+ }, [editor, isEditable]);
174
+
175
+ const handleMarkdownToggle = useCallback(() => {
176
+ editor.update(() => {
177
+ const root = $getRoot();
178
+ const firstChild = root.getFirstChild();
179
+ if ($isCodeNode(firstChild) && firstChild.getLanguage() === 'markdown') {
180
+ $convertFromMarkdownString(
181
+ firstChild.getTextContent(),
182
+ PLAYGROUND_TRANSFORMERS,
183
+ undefined, // node
184
+ shouldPreserveNewLinesInMarkdown,
185
+ );
186
+ } else {
187
+ const markdown = $convertToMarkdownString(
188
+ PLAYGROUND_TRANSFORMERS,
189
+ undefined, //node
190
+ shouldPreserveNewLinesInMarkdown,
191
+ );
192
+ const codeNode = $createCodeNode('markdown');
193
+ codeNode.append($createTextNode(markdown));
194
+ root.clear().append(codeNode);
195
+ if (markdown.length === 0) {
196
+ codeNode.select();
197
+ }
198
+ }
199
+ });
200
+ }, [editor, shouldPreserveNewLinesInMarkdown]);
201
+
202
+ return (
203
+ <div className="actions">
204
+ {SUPPORT_SPEECH_RECOGNITION && (
205
+ <button
206
+ onClick={() => {
207
+ editor.dispatchCommand(SPEECH_TO_TEXT_COMMAND, !isSpeechToText);
208
+ setIsSpeechToText(!isSpeechToText);
209
+ }}
210
+ className={
211
+ 'action-button action-button-mic ' +
212
+ (isSpeechToText ? 'active' : '')
213
+ }
214
+ title="Speech To Text"
215
+ aria-label={`${isSpeechToText ? 'Enable' : 'Disable'
216
+ } speech to text`}>
217
+ <i className="mic" />
218
+ </button>
219
+ )}
220
+ <button
221
+ className="action-button import"
222
+ onClick={() => importFile(editor)}
223
+ title="Import"
224
+ aria-label="Import editor state from JSON">
225
+ <i className="import" />
226
+ </button>
227
+
228
+ <button
229
+ className="action-button export"
230
+ onClick={() =>
231
+ exportFile(editor, {
232
+ fileName: `Playground ${new Date().toISOString()}`,
233
+ source: 'Playground',
234
+ })
235
+ }
236
+ title="Export"
237
+ aria-label="Export editor state to JSON">
238
+ <i className="export" />
239
+ </button>
240
+ <button
241
+ className="action-button share"
242
+ disabled={isCollabActive || INITIAL_SETTINGS.isCollab}
243
+ onClick={() =>
244
+ shareDoc(
245
+ serializedDocumentFromEditorState(editor.getEditorState(), {
246
+ source: 'Playground',
247
+ }),
248
+ ).then(
249
+ () => showFlashMessage('URL copied to clipboard'),
250
+ () => showFlashMessage('URL could not be copied to clipboard'),
251
+ )
252
+ }
253
+ title="Share"
254
+ aria-label="Share Playground link to current editor state">
255
+ <i className="share" />
256
+ </button>
257
+ <button
258
+ className="action-button clear"
259
+ disabled={isEditorEmpty}
260
+ onClick={() => {
261
+ modal.show('Clear editor', ShowClearDialog, { editor });
262
+ }}
263
+ title="Clear"
264
+ aria-label="Clear editor contents">
265
+ <i className="clear" />
266
+ </button>
267
+ <button
268
+ className={`action-button ${!isEditable ? 'unlock' : 'lock'}`}
269
+ onClick={() => {
270
+ // Send latest editor state to commenting validation server
271
+ if (isEditable) {
272
+ sendEditorState(editor);
273
+ }
274
+ editor.setEditable(!editor.isEditable());
275
+ }}
276
+ title="Read-Only Mode"
277
+ aria-label={`${!isEditable ? 'Unlock' : 'Lock'} read-only mode`}>
278
+ <i className={!isEditable ? 'unlock' : 'lock'} />
279
+ </button>
280
+ <button
281
+ className="action-button"
282
+ onClick={handleMarkdownToggle}
283
+ title="Convert From Markdown"
284
+ aria-label="Convert from markdown">
285
+ <i className="markdown" />
286
+ </button>
287
+ {isCollabActive && (
288
+ <button
289
+ className="action-button connect"
290
+ onClick={() => {
291
+ editor.dispatchCommand(TOGGLE_CONNECT_COMMAND, !connected);
292
+ }}
293
+ title={`${connected ? 'Disconnect' : 'Connect'
294
+ } Collaborative Editing`}
295
+ aria-label={`${connected ? 'Disconnect from' : 'Connect to'
296
+ } a collaborative editing server`}>
297
+ <i className={connected ? 'disconnect' : 'connect'} />
298
+ </button>
299
+ )}
300
+ </div>
301
+ );
302
+ }
303
+
304
+ function ShowClearDialog({
305
+ editor,
306
+ close,
307
+ }: {
308
+ editor: LexicalEditor;
309
+ close: () => void;
310
+ }): JSX.Element {
311
+ return (
312
+ <>
313
+ Are you sure you want to clear the editor?
314
+
315
+ <div className="row fill">
316
+ <Button type="primary"
317
+ onClick={() => {
318
+ editor.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined);
319
+ editor.focus();
320
+ close();
321
+ }}>
322
+ Clear
323
+ </Button>{' '}
324
+ <Button
325
+ onClick={() => {
326
+ editor.focus();
327
+ close();
328
+ }}>
329
+ Cancel
330
+ </Button>
331
+ </div>
332
+ </>
333
+ );
334
+ }
@@ -0,0 +1,352 @@
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 type { LexicalEditor } from 'lexical';
10
+
11
+ import {
12
+ AutoEmbedOption,
13
+ EmbedConfig,
14
+ EmbedMatchResult,
15
+ LexicalAutoEmbedPlugin,
16
+ URL_MATCHER,
17
+ } from '@lexical/react/LexicalAutoEmbedPlugin';
18
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
19
+ import { useMemo, useState, JSX } from 'react';
20
+ import * as React from 'react';
21
+ import * as ReactDOM from 'react-dom';
22
+
23
+ // Core
24
+ import useContext from '@/client/context';
25
+ import Button from '@client/components/button';
26
+ import Input from '@client/components/inputv3';
27
+
28
+ import { INSERT_FIGMA_COMMAND } from '../FigmaPlugin';
29
+ import { INSERT_TWEET_COMMAND } from '../TwitterPlugin';
30
+ import { INSERT_YOUTUBE_COMMAND } from '../YouTubePlugin';
31
+
32
+ interface PlaygroundEmbedConfig extends EmbedConfig {
33
+ // Human readable name of the embeded content e.g. Tweet or Google Map.
34
+ contentName: string;
35
+
36
+ // Icon for display.
37
+ icon?: JSX.Element;
38
+
39
+ // An example of a matching url https://twitter.com/jack/status/20
40
+ exampleUrl: string;
41
+
42
+ // For extra searching.
43
+ keywords: Array<string>;
44
+
45
+ // Embed a Figma Project.
46
+ description?: string;
47
+ }
48
+
49
+ export const YoutubeEmbedConfig: PlaygroundEmbedConfig = {
50
+ contentName: 'Youtube Video',
51
+
52
+ exampleUrl: 'https://www.youtube.com/watch?v=jNQXAC9IVRw',
53
+
54
+ // Icon for display.
55
+ icon: <i className="icon youtube" />,
56
+
57
+ insertNode: (editor: LexicalEditor, result: EmbedMatchResult) => {
58
+ editor.dispatchCommand(INSERT_YOUTUBE_COMMAND, result.id);
59
+ },
60
+
61
+ keywords: ['youtube', 'video'],
62
+
63
+ // Determine if a given URL is a match and return url data.
64
+ parseUrl: async (url: string) => {
65
+ const match =
66
+ /^.*(youtu\.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/.exec(url);
67
+
68
+ const id = match ? (match?.[2].length === 11 ? match[2] : null) : null;
69
+
70
+ if (id != null) {
71
+ return {
72
+ id,
73
+ url,
74
+ };
75
+ }
76
+
77
+ return null;
78
+ },
79
+
80
+ type: 'youtube-video',
81
+ };
82
+
83
+ export const TwitterEmbedConfig: PlaygroundEmbedConfig = {
84
+ // e.g. Tweet or Google Map.
85
+ contentName: 'Tweet',
86
+
87
+ exampleUrl: 'https://twitter.com/jack/status/20',
88
+
89
+ // Icon for display.
90
+ icon: <i className="icon tweet" />,
91
+
92
+ // Create the Lexical embed node from the url data.
93
+ insertNode: (editor: LexicalEditor, result: EmbedMatchResult) => {
94
+ editor.dispatchCommand(INSERT_TWEET_COMMAND, result.id);
95
+ },
96
+
97
+ // For extra searching.
98
+ keywords: ['tweet', 'twitter'],
99
+
100
+ // Determine if a given URL is a match and return url data.
101
+ parseUrl: (text: string) => {
102
+ const match =
103
+ /^https:\/\/(twitter|x)\.com\/(#!\/)?(\w+)\/status(es)*\/(\d+)/.exec(
104
+ text,
105
+ );
106
+
107
+ if (match != null) {
108
+ return {
109
+ id: match[5],
110
+ url: match[1],
111
+ };
112
+ }
113
+
114
+ return null;
115
+ },
116
+
117
+ type: 'tweet',
118
+ };
119
+
120
+ export const FigmaEmbedConfig: PlaygroundEmbedConfig = {
121
+ contentName: 'Figma Document',
122
+
123
+ exampleUrl: 'https://www.figma.com/file/LKQ4FJ4bTnCSjedbRpk931/Sample-File',
124
+
125
+ icon: <i className="icon figma" />,
126
+
127
+ insertNode: (editor: LexicalEditor, result: EmbedMatchResult) => {
128
+ editor.dispatchCommand(INSERT_FIGMA_COMMAND, result.id);
129
+ },
130
+
131
+ keywords: ['figma', 'figma.com', 'mock-up'],
132
+
133
+ // Determine if a given URL is a match and return url data.
134
+ parseUrl: (text: string) => {
135
+ const match =
136
+ /https:\/\/([\w.-]+\.)?figma.com\/(file|proto)\/([0-9a-zA-Z]{22,128})(?:\/.*)?$/.exec(
137
+ text,
138
+ );
139
+
140
+ if (match != null) {
141
+ return {
142
+ id: match[3],
143
+ url: match[0],
144
+ };
145
+ }
146
+
147
+ return null;
148
+ },
149
+
150
+ type: 'figma',
151
+ };
152
+
153
+ export const EmbedConfigs = [
154
+ TwitterEmbedConfig,
155
+ YoutubeEmbedConfig,
156
+ FigmaEmbedConfig,
157
+ ];
158
+
159
+ function AutoEmbedMenuItem({
160
+ index,
161
+ isSelected,
162
+ onClick,
163
+ onMouseEnter,
164
+ option,
165
+ }: {
166
+ index: number;
167
+ isSelected: boolean;
168
+ onClick: () => void;
169
+ onMouseEnter: () => void;
170
+ option: AutoEmbedOption;
171
+ }) {
172
+ let className = 'item';
173
+ if (isSelected) {
174
+ className += ' selected';
175
+ }
176
+ return (
177
+ <li
178
+ key={option.key}
179
+ tabIndex={-1}
180
+ className={className}
181
+ ref={option.setRefElement}
182
+ role="option"
183
+ aria-selected={isSelected}
184
+ id={'typeahead-item-' + index}
185
+ onMouseEnter={onMouseEnter}
186
+ onClick={onClick}>
187
+ <span className="text">{option.title}</span>
188
+ </li>
189
+ );
190
+ }
191
+
192
+ function AutoEmbedMenu({
193
+ options,
194
+ selectedItemIndex,
195
+ onOptionClick,
196
+ onOptionMouseEnter,
197
+ }: {
198
+ selectedItemIndex: number | null;
199
+ onOptionClick: (option: AutoEmbedOption, index: number) => void;
200
+ onOptionMouseEnter: (index: number) => void;
201
+ options: Array<AutoEmbedOption>;
202
+ }) {
203
+ return (
204
+ <div className="typeahead-popover">
205
+ <ul>
206
+ {options.map((option: AutoEmbedOption, i: number) => (
207
+ <AutoEmbedMenuItem
208
+ index={i}
209
+ isSelected={selectedItemIndex === i}
210
+ onClick={() => onOptionClick(option, i)}
211
+ onMouseEnter={() => onOptionMouseEnter(i)}
212
+ key={option.key}
213
+ option={option}
214
+ />
215
+ ))}
216
+ </ul>
217
+ </div>
218
+ );
219
+ }
220
+
221
+ const debounce = (callback: (text: string) => void, delay: number) => {
222
+ let timeoutId: number;
223
+ return (text: string) => {
224
+ window.clearTimeout(timeoutId);
225
+ timeoutId = window.setTimeout(() => {
226
+ callback(text);
227
+ }, delay);
228
+ };
229
+ };
230
+
231
+ export function AutoEmbedDialog({
232
+ embedConfig,
233
+ close,
234
+ }: {
235
+ embedConfig: PlaygroundEmbedConfig;
236
+ close: () => void;
237
+ }): JSX.Element {
238
+
239
+ const [text, setText] = useState('');
240
+ const [editor] = useLexicalComposerContext();
241
+ const [embedResult, setEmbedResult] = useState<EmbedMatchResult | null>(null);
242
+
243
+ const validateText = useMemo(
244
+ () =>
245
+ debounce((inputText: string) => {
246
+ const urlMatch = URL_MATCHER.exec(inputText);
247
+ if (embedConfig != null && inputText != null && urlMatch != null) {
248
+ Promise.resolve(embedConfig.parseUrl(inputText)).then(
249
+ (parseResult) => {
250
+ setEmbedResult(parseResult);
251
+ },
252
+ );
253
+ } else if (embedResult != null) {
254
+ setEmbedResult(null);
255
+ }
256
+ }, 200),
257
+ [embedConfig, embedResult],
258
+ );
259
+
260
+ const onClick = () => {
261
+ if (embedResult != null) {
262
+ embedConfig.insertNode(editor, embedResult);
263
+ close();
264
+ }
265
+ };
266
+
267
+ return (
268
+ <div style={{ width: '600px' }}>
269
+ <div className="Input__wrapper">
270
+ <Input
271
+ title="URL"
272
+ placeholder={embedConfig.exampleUrl}
273
+ value={text}
274
+ onChange={(value) => {
275
+ setText(value);
276
+ validateText(value);
277
+ }}
278
+ />
279
+ </div>
280
+ <Button type="primary"
281
+ disabled={!embedResult}
282
+ onClick={onClick}>
283
+ Embed
284
+ </Button>
285
+ </div>
286
+ );
287
+ }
288
+
289
+ export default function AutoEmbedPlugin(): JSX.Element {
290
+
291
+ const { modal } = useContext();
292
+
293
+ const openEmbedModal = (embedConfig: PlaygroundEmbedConfig) => {
294
+ modal.show(`Embed ${embedConfig.contentName}`, AutoEmbedDialog, { embedConfig });
295
+ };
296
+
297
+ const getMenuOptions = (
298
+ activeEmbedConfig: PlaygroundEmbedConfig,
299
+ embedFn: () => void,
300
+ dismissFn: () => void,
301
+ ) => {
302
+ return [
303
+ new AutoEmbedOption('Dismiss', {
304
+ onSelect: dismissFn,
305
+ }),
306
+ new AutoEmbedOption(`Embed ${activeEmbedConfig.contentName}`, {
307
+ onSelect: embedFn,
308
+ }),
309
+ ];
310
+ };
311
+
312
+ return (
313
+ <>
314
+ <LexicalAutoEmbedPlugin<PlaygroundEmbedConfig>
315
+ embedConfigs={EmbedConfigs}
316
+ onOpenEmbedModalForConfig={openEmbedModal}
317
+ getMenuOptions={getMenuOptions}
318
+ menuRenderFn={(
319
+ anchorElementRef,
320
+ { selectedIndex, options, selectOptionAndCleanUp, setHighlightedIndex },
321
+ ) =>
322
+ anchorElementRef.current
323
+ ? ReactDOM.createPortal(
324
+ <div
325
+ className="typeahead-popover auto-embed-menu"
326
+ style={{
327
+ marginLeft: `${Math.max(
328
+ parseFloat(anchorElementRef.current.style.width) - 200,
329
+ 0,
330
+ )}px`,
331
+ width: 200,
332
+ }}>
333
+ <AutoEmbedMenu
334
+ options={options}
335
+ selectedItemIndex={selectedIndex}
336
+ onOptionClick={(option: AutoEmbedOption, index: number) => {
337
+ setHighlightedIndex(index);
338
+ selectOptionAndCleanUp(option);
339
+ }}
340
+ onOptionMouseEnter={(index: number) => {
341
+ setHighlightedIndex(index);
342
+ }}
343
+ />
344
+ </div>,
345
+ anchorElementRef.current,
346
+ )
347
+ : null
348
+ }
349
+ />
350
+ </>
351
+ );
352
+ }