5htp-core 0.4.8-3 → 0.4.9-1

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 (186) hide show
  1. package/package.json +5 -1
  2. package/src/client/components/Form.ts +2 -6
  3. package/src/client/components/button.tsx +2 -1
  4. package/src/client/components/containers/Popover/index.tsx +2 -2
  5. package/src/client/components/dropdown/index.tsx +16 -6
  6. package/src/client/components/input/Slider/index.tsx +0 -2
  7. package/src/client/components/inputv3/Rte/Editor.tsx +271 -0
  8. package/src/client/components/inputv3/Rte/ToolbarPlugin/BlockFormat.tsx +220 -0
  9. package/src/client/components/inputv3/Rte/ToolbarPlugin/ElementFormat.tsx +107 -0
  10. package/src/client/components/inputv3/Rte/ToolbarPlugin/index.tsx +768 -0
  11. package/src/client/components/inputv3/Rte/appSettings.ts +36 -0
  12. package/src/client/components/inputv3/Rte/context/FlashMessageContext.tsx +68 -0
  13. package/src/client/components/inputv3/Rte/context/SettingsContext.tsx +71 -0
  14. package/src/client/components/inputv3/Rte/context/SharedAutocompleteContext.tsx +71 -0
  15. package/src/client/components/inputv3/Rte/context/SharedHistoryContext.tsx +35 -0
  16. package/src/client/components/inputv3/Rte/currentEditor.ts +3 -1
  17. package/src/client/components/inputv3/Rte/hooks/useFlashMessage.tsx +16 -0
  18. package/src/client/components/inputv3/Rte/hooks/useReport.ts +67 -0
  19. package/src/client/components/inputv3/Rte/images/emoji/1F600.png +0 -0
  20. package/src/client/components/inputv3/Rte/images/emoji/1F641.png +0 -0
  21. package/src/client/components/inputv3/Rte/images/emoji/1F642.png +0 -0
  22. package/src/client/components/inputv3/Rte/images/emoji/2764.png +0 -0
  23. package/src/client/components/inputv3/Rte/images/emoji/LICENSE.md +5 -0
  24. package/src/client/components/inputv3/Rte/images/icons/draggable-block-menu.svg +1 -0
  25. package/src/client/components/inputv3/Rte/images/icons/prettier-error.svg +1 -0
  26. package/src/client/components/inputv3/Rte/images/icons/prettier.svg +1 -0
  27. package/src/client/components/inputv3/Rte/images/image/LICENSE.md +5 -0
  28. package/src/client/components/inputv3/Rte/images/image-broken.svg +4 -0
  29. package/src/client/components/inputv3/Rte/images/logo.svg +1 -0
  30. package/src/client/components/inputv3/Rte/index.tsx +25 -94
  31. package/src/client/components/inputv3/Rte/nodes/AutocompleteNode.tsx +119 -0
  32. package/src/client/components/inputv3/Rte/nodes/EmojiNode.tsx +102 -0
  33. package/src/client/components/inputv3/Rte/nodes/EquationComponent.tsx +141 -0
  34. package/src/client/components/inputv3/Rte/nodes/EquationNode.tsx +174 -0
  35. package/src/client/components/inputv3/Rte/nodes/FigmaNode.tsx +135 -0
  36. package/src/client/components/inputv3/Rte/nodes/ImageComponent.tsx +468 -0
  37. package/src/client/components/inputv3/Rte/nodes/ImageNode.css +43 -0
  38. package/src/client/components/inputv3/Rte/nodes/ImageNode.tsx +266 -0
  39. package/src/client/components/inputv3/Rte/nodes/InlineImageNode/InlineImageComponent.tsx +402 -0
  40. package/src/client/components/inputv3/Rte/nodes/InlineImageNode/InlineImageNode.css +94 -0
  41. package/src/client/components/inputv3/Rte/nodes/InlineImageNode/InlineImageNode.tsx +294 -0
  42. package/src/client/components/inputv3/Rte/nodes/KeywordNode.ts +67 -0
  43. package/src/client/components/inputv3/Rte/nodes/LayoutContainerNode.ts +137 -0
  44. package/src/client/components/inputv3/Rte/nodes/LayoutItemNode.ts +71 -0
  45. package/src/client/components/inputv3/Rte/nodes/MentionNode.ts +130 -0
  46. package/src/client/components/inputv3/Rte/nodes/PageBreakNode/index.css +62 -0
  47. package/src/client/components/inputv3/Rte/nodes/PageBreakNode/index.tsx +170 -0
  48. package/src/client/components/inputv3/Rte/nodes/PlaygroundNodes.ts +76 -0
  49. package/src/client/components/inputv3/Rte/nodes/PollComponent.tsx +249 -0
  50. package/src/client/components/inputv3/Rte/nodes/PollNode.css +187 -0
  51. package/src/client/components/inputv3/Rte/nodes/PollNode.tsx +209 -0
  52. package/src/client/components/inputv3/Rte/nodes/StickyComponent.tsx +261 -0
  53. package/src/client/components/inputv3/Rte/nodes/StickyNode.css +37 -0
  54. package/src/client/components/inputv3/Rte/nodes/StickyNode.tsx +150 -0
  55. package/src/client/components/inputv3/Rte/nodes/TweetNode.tsx +223 -0
  56. package/src/client/components/inputv3/Rte/nodes/YouTubeNode.tsx +184 -0
  57. package/src/client/components/inputv3/Rte/plugins/ActionsPlugin/index.tsx +334 -0
  58. package/src/client/components/inputv3/Rte/plugins/AutoEmbedPlugin/index.tsx +352 -0
  59. package/src/client/components/inputv3/Rte/plugins/AutoLinkPlugin/index.tsx +32 -0
  60. package/src/client/components/inputv3/Rte/plugins/AutocompletePlugin/index.tsx +2529 -0
  61. package/src/client/components/inputv3/Rte/plugins/CodeActionMenuPlugin/components/CopyButton/index.tsx +70 -0
  62. package/src/client/components/inputv3/Rte/plugins/CodeActionMenuPlugin/components/PrettierButton/index.css +14 -0
  63. package/src/client/components/inputv3/Rte/plugins/CodeActionMenuPlugin/components/PrettierButton/index.tsx +156 -0
  64. package/src/client/components/inputv3/Rte/plugins/CodeActionMenuPlugin/index.css +54 -0
  65. package/src/client/components/inputv3/Rte/plugins/CodeActionMenuPlugin/index.tsx +190 -0
  66. package/src/client/components/inputv3/Rte/plugins/CodeActionMenuPlugin/utils.ts +33 -0
  67. package/src/client/components/inputv3/Rte/plugins/CodeHighlightPlugin/index.ts +21 -0
  68. package/src/client/components/inputv3/Rte/plugins/CollapsiblePlugin/Collapsible.css +57 -0
  69. package/src/client/components/inputv3/Rte/plugins/CollapsiblePlugin/CollapsibleContainerNode.ts +168 -0
  70. package/src/client/components/inputv3/Rte/plugins/CollapsiblePlugin/CollapsibleContentNode.ts +127 -0
  71. package/src/client/components/inputv3/Rte/plugins/CollapsiblePlugin/CollapsibleTitleNode.ts +152 -0
  72. package/src/client/components/inputv3/Rte/plugins/CollapsiblePlugin/CollapsibleUtils.ts +17 -0
  73. package/src/client/components/inputv3/Rte/plugins/CollapsiblePlugin/index.ts +284 -0
  74. package/src/client/components/inputv3/Rte/plugins/ComponentPickerPlugin/index.tsx +370 -0
  75. package/src/client/components/inputv3/Rte/plugins/ContextMenuPlugin/index.tsx +270 -0
  76. package/src/client/components/inputv3/Rte/plugins/DocsPlugin/index.tsx +20 -0
  77. package/src/client/components/inputv3/Rte/plugins/DragDropPastePlugin/index.ts +51 -0
  78. package/src/client/components/inputv3/Rte/plugins/DraggableBlockPlugin/index.css +36 -0
  79. package/src/client/components/inputv3/Rte/plugins/DraggableBlockPlugin/index.tsx +43 -0
  80. package/src/client/components/inputv3/Rte/plugins/EmojiPickerPlugin/index.tsx +198 -0
  81. package/src/client/components/inputv3/Rte/plugins/EmojisPlugin/index.ts +75 -0
  82. package/src/client/components/inputv3/Rte/plugins/EquationsPlugin/index.tsx +82 -0
  83. package/src/client/components/inputv3/Rte/plugins/FigmaPlugin/index.tsx +40 -0
  84. package/src/client/components/inputv3/Rte/plugins/FloatingLinkEditorPlugin/index.css +41 -0
  85. package/src/client/components/inputv3/Rte/plugins/FloatingLinkEditorPlugin/index.tsx +393 -0
  86. package/src/client/components/inputv3/Rte/plugins/FloatingTextFormatToolbarPlugin/index.css +141 -0
  87. package/src/client/components/inputv3/Rte/plugins/FloatingTextFormatToolbarPlugin/index.tsx +388 -0
  88. package/src/client/components/inputv3/Rte/plugins/ImagesPlugin/index.tsx +350 -0
  89. package/src/client/components/inputv3/Rte/plugins/InlineImagePlugin/index.tsx +336 -0
  90. package/src/client/components/inputv3/Rte/plugins/KeywordsPlugin/index.ts +56 -0
  91. package/src/client/components/inputv3/Rte/plugins/LayoutPlugin/InsertLayoutDialog.tsx +58 -0
  92. package/src/client/components/inputv3/Rte/plugins/LayoutPlugin/LayoutPlugin.tsx +219 -0
  93. package/src/client/components/inputv3/Rte/plugins/LinkPlugin/index.tsx +34 -0
  94. package/src/client/components/inputv3/Rte/plugins/ListMaxIndentLevelPlugin/index.ts +85 -0
  95. package/src/client/components/inputv3/Rte/plugins/MarkdownShortcutPlugin/index.tsx +16 -0
  96. package/src/client/components/inputv3/Rte/plugins/MarkdownTransformers/index.ts +324 -0
  97. package/src/client/components/inputv3/Rte/plugins/MaxLengthPlugin/index.tsx +53 -0
  98. package/src/client/components/inputv3/Rte/plugins/MentionsPlugin/index.tsx +696 -0
  99. package/src/client/components/inputv3/Rte/plugins/PageBreakPlugin/index.tsx +57 -0
  100. package/src/client/components/inputv3/Rte/plugins/PasteLogPlugin/index.tsx +54 -0
  101. package/src/client/components/inputv3/Rte/plugins/PollPlugin/index.tsx +86 -0
  102. package/src/client/components/inputv3/Rte/plugins/SpeechToTextPlugin/index.ts +125 -0
  103. package/src/client/components/inputv3/Rte/plugins/StickyPlugin/index.ts +22 -0
  104. package/src/client/components/inputv3/Rte/plugins/TabFocusPlugin/index.tsx +65 -0
  105. package/src/client/components/inputv3/Rte/plugins/TableActionMenuPlugin/index.tsx +773 -0
  106. package/src/client/components/inputv3/Rte/plugins/TableCellResizer/index.css +12 -0
  107. package/src/client/components/inputv3/Rte/plugins/TableCellResizer/index.tsx +436 -0
  108. package/src/client/components/inputv3/Rte/plugins/TableHoverActionsPlugin/index.tsx +287 -0
  109. package/src/client/components/inputv3/Rte/plugins/TableOfContentsPlugin/index.css +95 -0
  110. package/src/client/components/inputv3/Rte/plugins/TableOfContentsPlugin/index.tsx +197 -0
  111. package/src/client/components/inputv3/Rte/plugins/TablePlugin.tsx +178 -0
  112. package/src/client/components/inputv3/Rte/plugins/TestRecorderPlugin/index.tsx +468 -0
  113. package/src/client/components/inputv3/Rte/plugins/TreeViewPlugin/index.tsx +26 -0
  114. package/src/client/components/inputv3/Rte/plugins/TwitterPlugin/index.ts +41 -0
  115. package/src/client/components/inputv3/Rte/plugins/TypingPerfPlugin/index.ts +117 -0
  116. package/src/client/components/inputv3/Rte/plugins/YouTubePlugin/index.ts +41 -0
  117. package/src/client/components/inputv3/Rte/shared/canUseDOM.ts +4 -0
  118. package/src/client/components/inputv3/Rte/shared/caretFromPoint.ts +40 -0
  119. package/src/client/components/inputv3/Rte/shared/environment.ts +56 -0
  120. package/src/client/components/inputv3/Rte/shared/invariant.ts +26 -0
  121. package/src/client/components/inputv3/Rte/shared/normalizeClassNames.ts +21 -0
  122. package/src/client/components/inputv3/Rte/shared/react-test-utils.ts +18 -0
  123. package/src/client/components/inputv3/Rte/shared/reactPatches.ts +22 -0
  124. package/src/client/components/inputv3/Rte/shared/simpleDiffWithCursor.ts +49 -0
  125. package/src/client/components/inputv3/Rte/shared/useLayoutEffect.ts +19 -0
  126. package/src/client/components/inputv3/Rte/shared/warnOnlyOnce.ts +20 -0
  127. package/src/client/components/inputv3/Rte/style.less +30 -60
  128. package/src/client/components/inputv3/Rte/themes/CommentEditorTheme.css +13 -0
  129. package/src/client/components/inputv3/Rte/themes/CommentEditorTheme.ts +20 -0
  130. package/src/client/components/inputv3/Rte/themes/PlaygroundEditorTheme.css +447 -0
  131. package/src/client/components/inputv3/Rte/themes/PlaygroundEditorTheme.ts +120 -0
  132. package/src/client/components/inputv3/Rte/themes/StickyEditorTheme.css +13 -0
  133. package/src/client/components/inputv3/Rte/themes/StickyEditorTheme.ts +20 -0
  134. package/src/client/components/inputv3/Rte/ui/ColorPicker.css +88 -0
  135. package/src/client/components/inputv3/Rte/ui/ColorPicker.tsx +365 -0
  136. package/src/client/components/inputv3/Rte/ui/ContentEditable.css +44 -0
  137. package/src/client/components/inputv3/Rte/ui/ContentEditable.tsx +36 -0
  138. package/src/client/components/inputv3/Rte/ui/DropDown.tsx +259 -0
  139. package/src/client/components/inputv3/Rte/ui/DropdownColorPicker.tsx +41 -0
  140. package/src/client/components/inputv3/Rte/ui/EquationEditor.css +38 -0
  141. package/src/client/components/inputv3/Rte/ui/EquationEditor.tsx +56 -0
  142. package/src/client/components/inputv3/Rte/ui/FileInput.tsx +38 -0
  143. package/src/client/components/inputv3/Rte/ui/FlashMessage.css +28 -0
  144. package/src/client/components/inputv3/Rte/ui/FlashMessage.tsx +29 -0
  145. package/src/client/components/inputv3/Rte/ui/ImageResizer.tsx +316 -0
  146. package/src/client/components/inputv3/Rte/ui/Input.css +32 -0
  147. package/src/client/components/inputv3/Rte/ui/KatexRenderer.tsx +54 -0
  148. package/src/client/components/inputv3/Rte/ui/Switch.tsx +36 -0
  149. package/src/client/components/inputv3/Rte/utils/docSerialization.ts +77 -0
  150. package/src/client/components/inputv3/Rte/utils/emoji-list.ts +16615 -0
  151. package/src/client/components/inputv3/Rte/utils/getDOMRangeRect.ts +27 -0
  152. package/src/client/components/inputv3/Rte/utils/getSelectedNode.ts +27 -0
  153. package/src/client/components/inputv3/Rte/utils/guard.ts +10 -0
  154. package/src/client/components/inputv3/Rte/utils/isMobileWidth.ts +7 -0
  155. package/src/client/components/inputv3/Rte/utils/joinClasses.ts +13 -0
  156. package/src/client/components/inputv3/Rte/utils/setFloatingElemPosition.ts +51 -0
  157. package/src/client/components/inputv3/Rte/utils/setFloatingElemPositionForLinkEditor.ts +46 -0
  158. package/src/client/components/inputv3/Rte/utils/swipe.ts +127 -0
  159. package/src/client/components/inputv3/Rte/utils/url.ts +38 -0
  160. package/src/client/components/inputv3/base.tsx +8 -5
  161. package/src/client/components/inputv3/index.tsx +1 -1
  162. package/src/common/data/rte/nodes.ts +60 -9
  163. package/src/common/validation/index.ts +21 -2
  164. package/src/common/validation/schema.ts +27 -10
  165. package/src/common/validation/validator.ts +12 -4
  166. package/src/common/validation/validators.ts +108 -66
  167. package/src/server/services/router/http/multipart.ts +0 -1
  168. package/src/server/services/schema/index.ts +26 -4
  169. package/src/server/services/schema/request.ts +3 -2
  170. package/src/server/services/schema/rte.ts +110 -0
  171. package/src/server/utils/rte.ts +7 -4
  172. package/src/client/components/inputv3/Rte/ExampleTheme.tsx +0 -42
  173. package/src/client/components/inputv3/Rte/ToolbarPlugin.tsx +0 -167
  174. package/src/client/components/inputv3/Rte/icons/LICENSE.md +0 -5
  175. package/src/client/components/inputv3/Rte/icons/arrow-clockwise.svg +0 -4
  176. package/src/client/components/inputv3/Rte/icons/arrow-counterclockwise.svg +0 -4
  177. package/src/client/components/inputv3/Rte/icons/journal-text.svg +0 -5
  178. package/src/client/components/inputv3/Rte/icons/justify.svg +0 -3
  179. package/src/client/components/inputv3/Rte/icons/text-center.svg +0 -3
  180. package/src/client/components/inputv3/Rte/icons/text-left.svg +0 -3
  181. package/src/client/components/inputv3/Rte/icons/text-paragraph.svg +0 -3
  182. package/src/client/components/inputv3/Rte/icons/text-right.svg +0 -3
  183. package/src/client/components/inputv3/Rte/icons/type-bold.svg +0 -3
  184. package/src/client/components/inputv3/Rte/icons/type-italic.svg +0 -3
  185. package/src/client/components/inputv3/Rte/icons/type-strikethrough.svg +0 -3
  186. 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
+ }