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,178 @@
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
+ // Core
10
+ import Button from '@client/components/button';
11
+ import Input from '@client/components/inputv3';
12
+
13
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
14
+ import {
15
+ $createTableNodeWithDimensions,
16
+ INSERT_TABLE_COMMAND,
17
+ TableNode,
18
+ } from '@lexical/table';
19
+ import {
20
+ $insertNodes,
21
+ COMMAND_PRIORITY_EDITOR,
22
+ createCommand,
23
+ EditorThemeClasses,
24
+ Klass,
25
+ LexicalCommand,
26
+ LexicalEditor,
27
+ LexicalNode,
28
+ } from 'lexical';
29
+ import { createContext, useContext, useEffect, useMemo, useState } from 'react';
30
+ import * as React from 'react';
31
+ import invariant from '../shared/invariant';
32
+
33
+ export type InsertTableCommandPayload = Readonly<{
34
+ columns: string;
35
+ rows: string;
36
+ includeHeaders?: boolean;
37
+ }>;
38
+
39
+ export type CellContextShape = {
40
+ cellEditorConfig: null | CellEditorConfig;
41
+ cellEditorPlugins: null | React.JSX.Element | Array<React.JSX.Element>;
42
+ set: (
43
+ cellEditorConfig: null | CellEditorConfig,
44
+ cellEditorPlugins: null | React.JSX.Element | Array<React.JSX.Element>,
45
+ ) => void;
46
+ };
47
+
48
+ export type CellEditorConfig = Readonly<{
49
+ namespace: string;
50
+ nodes?: ReadonlyArray<Klass<LexicalNode>>;
51
+ onError: (error: Error, editor: LexicalEditor) => void;
52
+ readOnly?: boolean;
53
+ theme?: EditorThemeClasses;
54
+ }>;
55
+
56
+ export const INSERT_NEW_TABLE_COMMAND: LexicalCommand<InsertTableCommandPayload> =
57
+ createCommand('INSERT_NEW_TABLE_COMMAND');
58
+
59
+ export const CellContext = createContext<CellContextShape>({
60
+ cellEditorConfig: null,
61
+ cellEditorPlugins: null,
62
+ set: () => {
63
+ // Empty
64
+ },
65
+ });
66
+
67
+ export function TableContext({ children }: { children: React.JSX.Element }) {
68
+ const [contextValue, setContextValue] = useState<{
69
+ cellEditorConfig: null | CellEditorConfig;
70
+ cellEditorPlugins: null | React.JSX.Element | Array<React.JSX.Element>;
71
+ }>({
72
+ cellEditorConfig: null,
73
+ cellEditorPlugins: null,
74
+ });
75
+ return (
76
+ <CellContext.Provider
77
+ value={useMemo(
78
+ () => ({
79
+ cellEditorConfig: contextValue.cellEditorConfig,
80
+ cellEditorPlugins: contextValue.cellEditorPlugins,
81
+ set: (cellEditorConfig, cellEditorPlugins) => {
82
+ setContextValue({ cellEditorConfig, cellEditorPlugins });
83
+ },
84
+ }),
85
+ [contextValue.cellEditorConfig, contextValue.cellEditorPlugins],
86
+ )}>
87
+ {children}
88
+ </CellContext.Provider>
89
+ );
90
+ }
91
+
92
+ export function InsertTableDialog({
93
+ editor,
94
+ close,
95
+ }: {
96
+ editor: LexicalEditor;
97
+ close: () => void;
98
+ }): React.JSX.Element {
99
+ const [rows, setRows] = useState('5');
100
+ const [columns, setColumns] = useState('5');
101
+ const [isDisabled, setIsDisabled] = useState(true);
102
+
103
+ useEffect(() => {
104
+ const row = Number(rows);
105
+ const column = Number(columns);
106
+ if (row && row > 0 && row <= 500 && column && column > 0 && column <= 50) {
107
+ setIsDisabled(false);
108
+ } else {
109
+ setIsDisabled(true);
110
+ }
111
+ }, [rows, columns]);
112
+
113
+ const onClick = () => {
114
+ editor.dispatchCommand(INSERT_TABLE_COMMAND, {
115
+ columns,
116
+ rows,
117
+ });
118
+
119
+ close();
120
+ };
121
+
122
+ return (
123
+ <>
124
+ <Input
125
+ placeholder={'# of rows (1-500)'}
126
+ title="Rows"
127
+ onChange={setRows}
128
+ value={rows}
129
+ type="number"
130
+ />
131
+ <Input
132
+ placeholder={'# of columns (1-50)'}
133
+ title="Columns"
134
+ onChange={setColumns}
135
+ value={columns}
136
+ type="number"
137
+ />
138
+ <Button type="primary" disabled={isDisabled} onClick={onClick}>
139
+ Confirm
140
+ </Button>
141
+ </>
142
+ );
143
+ }
144
+
145
+ export function TablePlugin({
146
+ cellEditorConfig,
147
+ children,
148
+ }: {
149
+ cellEditorConfig: CellEditorConfig;
150
+ children: React.JSX.Element | Array<React.JSX.Element>;
151
+ }): React.JSX.Element | null {
152
+ const [editor] = useLexicalComposerContext();
153
+ const cellContext = useContext(CellContext);
154
+
155
+ useEffect(() => {
156
+ if (!editor.hasNodes([TableNode])) {
157
+ invariant(false, 'TablePlugin: TableNode is not registered on editor');
158
+ }
159
+
160
+ cellContext.set(cellEditorConfig, children);
161
+
162
+ return editor.registerCommand<InsertTableCommandPayload>(
163
+ INSERT_NEW_TABLE_COMMAND,
164
+ ({ columns, rows, includeHeaders }) => {
165
+ const tableNode = $createTableNodeWithDimensions(
166
+ Number(rows),
167
+ Number(columns),
168
+ includeHeaders,
169
+ );
170
+ $insertNodes([tableNode]);
171
+ return true;
172
+ },
173
+ COMMAND_PRIORITY_EDITOR,
174
+ );
175
+ }, [cellContext, cellEditorConfig, children, editor]);
176
+
177
+ return null;
178
+ }
@@ -0,0 +1,468 @@
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 {BaseSelection, LexicalEditor} from 'lexical';
10
+
11
+ import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
12
+ import {$createParagraphNode, $createTextNode, $getRoot} from 'lexical';
13
+ import * as React from 'react';
14
+ import {useCallback, useEffect, useRef, useState} from 'react';
15
+ import {IS_APPLE} from '../../shared/environment';
16
+ import useLayoutEffect from '../../shared/useLayoutEffect';
17
+
18
+ const copy = (text: string | null) => {
19
+ const textArea = document.createElement('textarea');
20
+ textArea.value = text || '';
21
+ textArea.style.position = 'absolute';
22
+ textArea.style.opacity = '0';
23
+ document.body?.appendChild(textArea);
24
+ textArea.focus();
25
+ textArea.select();
26
+ try {
27
+ const result = document.execCommand('copy');
28
+ // eslint-disable-next-line no-console
29
+ console.log(result);
30
+ } catch (error) {
31
+ console.error(error);
32
+ }
33
+ document.body?.removeChild(textArea);
34
+ };
35
+
36
+ const download = (filename: string, text: string | null) => {
37
+ const a = document.createElement('a');
38
+ a.setAttribute(
39
+ 'href',
40
+ 'data:text/plain;charset=utf-8,' + encodeURIComponent(text || ''),
41
+ );
42
+ a.setAttribute('download', filename);
43
+ a.style.display = 'none';
44
+ document.body?.appendChild(a);
45
+ a.click();
46
+ document.body?.removeChild(a);
47
+ };
48
+
49
+ const formatStep = (step: Step) => {
50
+ const formatOneStep = (name: string, value: Step['value']) => {
51
+ switch (name) {
52
+ case 'click': {
53
+ return ` await page.mouse.click(${value.x}, ${value.y});`;
54
+ }
55
+ case 'press': {
56
+ return ` await page.keyboard.press('${value}');`;
57
+ }
58
+ case 'keydown': {
59
+ return ` await page.keyboard.keydown('${value}');`;
60
+ }
61
+ case 'keyup': {
62
+ return ` await page.keyboard.keyup('${value}');`;
63
+ }
64
+ case 'type': {
65
+ return ` await page.keyboard.type('${value}');`;
66
+ }
67
+ case 'selectAll': {
68
+ return ` await selectAll(page);`;
69
+ }
70
+ case 'snapshot': {
71
+ return ` await assertHTMLSnapshot(page);
72
+ await assertSelection(page, {
73
+ anchorPath: [${value.anchorPath.toString()}],
74
+ anchorOffset: ${value.anchorOffset},
75
+ focusPath: [${value.focusPath.toString()}],
76
+ focusOffset: ${value.focusOffset},
77
+ });
78
+ `;
79
+ }
80
+ default:
81
+ return ``;
82
+ }
83
+ };
84
+ const formattedStep = formatOneStep(step.name, step.value);
85
+ switch (step.count) {
86
+ case 1:
87
+ return formattedStep;
88
+ case 2:
89
+ return [formattedStep, formattedStep].join(`\n`);
90
+ default:
91
+ return ` await repeat(${step.count}, async () => {
92
+ ${formattedStep}
93
+ );`;
94
+ }
95
+ };
96
+
97
+ export function isSelectAll(event: KeyboardEvent): boolean {
98
+ return (
99
+ event.key.toLowerCase() === 'a' &&
100
+ (IS_APPLE ? event.metaKey : event.ctrlKey)
101
+ );
102
+ }
103
+
104
+ // stolen from LexicalSelection-test
105
+ function sanitizeSelection(selection: Selection) {
106
+ const {anchorNode, focusNode} = selection;
107
+ let {anchorOffset, focusOffset} = selection;
108
+ if (anchorOffset !== 0) {
109
+ anchorOffset--;
110
+ }
111
+ if (focusOffset !== 0) {
112
+ focusOffset--;
113
+ }
114
+ return {anchorNode, anchorOffset, focusNode, focusOffset};
115
+ }
116
+
117
+ function getPathFromNodeToEditor(node: Node, rootElement: HTMLElement | null) {
118
+ let currentNode: Node | null | undefined = node;
119
+ const path = [];
120
+ while (currentNode !== rootElement) {
121
+ if (currentNode !== null && currentNode !== undefined) {
122
+ path.unshift(
123
+ Array.from(currentNode?.parentNode?.childNodes ?? []).indexOf(
124
+ currentNode as ChildNode,
125
+ ),
126
+ );
127
+ }
128
+ currentNode = currentNode?.parentNode;
129
+ }
130
+ return path;
131
+ }
132
+
133
+ const keyPresses = new Set([
134
+ 'Enter',
135
+ 'Backspace',
136
+ 'Delete',
137
+ 'Escape',
138
+ 'ArrowLeft',
139
+ 'ArrowRight',
140
+ 'ArrowUp',
141
+ 'ArrowDown',
142
+ ]);
143
+
144
+ type Step = {
145
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
146
+ value: any;
147
+ count: number;
148
+ name: string;
149
+ };
150
+
151
+ type Steps = Step[];
152
+
153
+ function useTestRecorder(
154
+ editor: LexicalEditor,
155
+ ): [JSX.Element, JSX.Element | null] {
156
+ const [steps, setSteps] = useState<Steps>([]);
157
+ const [isRecording, setIsRecording] = useState(false);
158
+ const [, setCurrentInnerHTML] = useState('');
159
+ const [templatedTest, setTemplatedTest] = useState('');
160
+ const previousSelectionRef = useRef<BaseSelection | null>(null);
161
+ const skipNextSelectionChangeRef = useRef(false);
162
+ const preRef = useRef<HTMLPreElement>(null);
163
+
164
+ const getCurrentEditor = useCallback(() => {
165
+ return editor;
166
+ }, [editor]);
167
+
168
+ const generateTestContent = useCallback(() => {
169
+ const rootElement = editor.getRootElement();
170
+ const browserSelection = window.getSelection();
171
+
172
+ if (
173
+ rootElement == null ||
174
+ browserSelection == null ||
175
+ browserSelection.anchorNode == null ||
176
+ browserSelection.focusNode == null ||
177
+ !rootElement.contains(browserSelection.anchorNode) ||
178
+ !rootElement.contains(browserSelection.focusNode)
179
+ ) {
180
+ return null;
181
+ }
182
+
183
+ return `
184
+ /**
185
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
186
+ *
187
+ * This source code is licensed under the MIT license found in the
188
+ * LICENSE file in the root directory of this source tree.
189
+ *
190
+ */
191
+
192
+ import {
193
+ initializeE2E,
194
+ assertHTMLSnapshot,
195
+ assertSelection,
196
+ repeat,
197
+ } from '../utils';
198
+ import {selectAll} from '../keyboardShortcuts';
199
+ import { RangeSelection } from 'lexical';
200
+ import { NodeSelection } from 'lexical';
201
+
202
+ describe('Test case', () => {
203
+ initializeE2E((e2e) => {
204
+ it('Should pass this test', async () => {
205
+ const {page} = e2e;
206
+
207
+ await page.focus('div[contenteditable="true"]');
208
+ ${steps.map(formatStep).join(`\n`)}
209
+ });
210
+ });
211
+ `;
212
+ }, [editor, steps]);
213
+
214
+ // just a wrapper around inserting new actions so that we can
215
+ // coalesce some actions like insertText/moveNativeSelection
216
+ const pushStep = useCallback(
217
+ (name: string, value: Step['value']) => {
218
+ setSteps((currentSteps) => {
219
+ // trying to group steps
220
+ const currentIndex = steps.length - 1;
221
+ const lastStep = steps[currentIndex];
222
+ if (lastStep) {
223
+ if (lastStep.name === name) {
224
+ if (name === 'type') {
225
+ // for typing events we just append the text
226
+ return [
227
+ ...steps.slice(0, currentIndex),
228
+ {...lastStep, value: lastStep.value + value},
229
+ ];
230
+ } else {
231
+ // for other events we bump the counter if their values are the same
232
+ if (lastStep.value === value) {
233
+ return [
234
+ ...steps.slice(0, currentIndex),
235
+ {...lastStep, count: lastStep.count + 1},
236
+ ];
237
+ }
238
+ }
239
+ }
240
+ }
241
+ // could not group, just append a new one
242
+ return [...currentSteps, {count: 1, name, value}];
243
+ });
244
+ },
245
+ [steps, setSteps],
246
+ );
247
+
248
+ useLayoutEffect(() => {
249
+ const onKeyDown = (event: KeyboardEvent) => {
250
+ if (!isRecording) {
251
+ return;
252
+ }
253
+ const key = event.key;
254
+ if (isSelectAll(event)) {
255
+ pushStep('selectAll', '');
256
+ } else if (keyPresses.has(key)) {
257
+ pushStep('press', event.key);
258
+ } else if ([...key].length > 1) {
259
+ pushStep('keydown', event.key);
260
+ } else {
261
+ pushStep('type', event.key);
262
+ }
263
+ };
264
+
265
+ const onKeyUp = (event: KeyboardEvent) => {
266
+ if (!isRecording) {
267
+ return;
268
+ }
269
+ const key = event.key;
270
+ if (!keyPresses.has(key) && [...key].length > 1) {
271
+ pushStep('keyup', event.key);
272
+ }
273
+ };
274
+
275
+ return editor.registerRootListener(
276
+ (
277
+ rootElement: null | HTMLElement,
278
+ prevRootElement: null | HTMLElement,
279
+ ) => {
280
+ if (prevRootElement !== null) {
281
+ prevRootElement.removeEventListener('keydown', onKeyDown);
282
+ prevRootElement.removeEventListener('keyup', onKeyUp);
283
+ }
284
+ if (rootElement !== null) {
285
+ rootElement.addEventListener('keydown', onKeyDown);
286
+ rootElement.addEventListener('keyup', onKeyUp);
287
+ }
288
+ },
289
+ );
290
+ }, [editor, isRecording, pushStep]);
291
+
292
+ useLayoutEffect(() => {
293
+ if (preRef.current) {
294
+ preRef.current.scrollTo(0, preRef.current.scrollHeight);
295
+ }
296
+ }, [generateTestContent]);
297
+
298
+ useEffect(() => {
299
+ if (steps) {
300
+ const testContent = generateTestContent();
301
+ if (testContent !== null) {
302
+ setTemplatedTest(testContent);
303
+ }
304
+ if (preRef.current) {
305
+ preRef.current.scrollTo(0, preRef.current.scrollHeight);
306
+ }
307
+ }
308
+ }, [generateTestContent, steps]);
309
+
310
+ useEffect(() => {
311
+ const removeUpdateListener = editor.registerUpdateListener(
312
+ ({editorState, dirtyLeaves, dirtyElements}) => {
313
+ if (!isRecording) {
314
+ return;
315
+ }
316
+ const currentSelection = editorState._selection;
317
+ const previousSelection = previousSelectionRef.current;
318
+ const skipNextSelectionChange = skipNextSelectionChangeRef.current;
319
+ if (previousSelection !== currentSelection) {
320
+ if (
321
+ dirtyLeaves.size === 0 &&
322
+ dirtyElements.size === 0 &&
323
+ !skipNextSelectionChange
324
+ ) {
325
+ const browserSelection = window.getSelection();
326
+ if (
327
+ browserSelection &&
328
+ (browserSelection.anchorNode == null ||
329
+ browserSelection.focusNode == null)
330
+ ) {
331
+ return;
332
+ }
333
+ }
334
+ previousSelectionRef.current = currentSelection;
335
+ }
336
+ skipNextSelectionChangeRef.current = false;
337
+ const testContent = generateTestContent();
338
+ if (testContent !== null) {
339
+ setTemplatedTest(testContent);
340
+ }
341
+ },
342
+ );
343
+ return removeUpdateListener;
344
+ }, [editor, generateTestContent, isRecording, pushStep]);
345
+
346
+ // save innerHTML
347
+ useEffect(() => {
348
+ if (!isRecording) {
349
+ return;
350
+ }
351
+ const removeUpdateListener = editor.registerUpdateListener(() => {
352
+ const rootElement = editor.getRootElement();
353
+ if (rootElement !== null) {
354
+ setCurrentInnerHTML(rootElement?.innerHTML);
355
+ }
356
+ });
357
+ return removeUpdateListener;
358
+ }, [editor, isRecording]);
359
+
360
+ // clear editor and start recording
361
+ const toggleEditorSelection = useCallback(
362
+ (currentEditor: LexicalEditor) => {
363
+ if (!isRecording) {
364
+ currentEditor.update(() => {
365
+ const root = $getRoot();
366
+ root.clear();
367
+ const text = $createTextNode();
368
+ root.append($createParagraphNode().append(text));
369
+ text.select();
370
+ });
371
+ setSteps([]);
372
+ }
373
+ setIsRecording((currentIsRecording) => !currentIsRecording);
374
+ },
375
+ [isRecording],
376
+ );
377
+
378
+ const onSnapshotClick = useCallback(() => {
379
+ if (!isRecording) {
380
+ return;
381
+ }
382
+ const browserSelection = window.getSelection();
383
+ if (
384
+ browserSelection === null ||
385
+ browserSelection.anchorNode == null ||
386
+ browserSelection.focusNode == null
387
+ ) {
388
+ return;
389
+ }
390
+ const {anchorNode, anchorOffset, focusNode, focusOffset} =
391
+ sanitizeSelection(browserSelection);
392
+ const rootElement = getCurrentEditor().getRootElement();
393
+ let anchorPath;
394
+ if (anchorNode !== null) {
395
+ anchorPath = getPathFromNodeToEditor(anchorNode, rootElement);
396
+ }
397
+ let focusPath;
398
+ if (focusNode !== null) {
399
+ focusPath = getPathFromNodeToEditor(focusNode, rootElement);
400
+ }
401
+ pushStep('snapshot', {
402
+ anchorNode,
403
+ anchorOffset,
404
+ anchorPath,
405
+ focusNode,
406
+ focusOffset,
407
+ focusPath,
408
+ });
409
+ }, [pushStep, isRecording, getCurrentEditor]);
410
+
411
+ const onCopyClick = useCallback(() => {
412
+ copy(generateTestContent());
413
+ }, [generateTestContent]);
414
+
415
+ const onDownloadClick = useCallback(() => {
416
+ download('test.js', generateTestContent());
417
+ }, [generateTestContent]);
418
+
419
+ const button = (
420
+ <button
421
+ id="test-recorder-button"
422
+ className={`editor-dev-button ${isRecording ? 'active' : ''}`}
423
+ onClick={() => toggleEditorSelection(getCurrentEditor())}
424
+ title={isRecording ? 'Disable test recorder' : 'Enable test recorder'}
425
+ />
426
+ );
427
+ const output = isRecording ? (
428
+ <div className="test-recorder-output">
429
+ <div className="test-recorder-toolbar">
430
+ <button
431
+ className="test-recorder-button"
432
+ id="test-recorder-button-snapshot"
433
+ title="Insert snapshot"
434
+ onClick={onSnapshotClick}
435
+ />
436
+ <button
437
+ className="test-recorder-button"
438
+ id="test-recorder-button-copy"
439
+ title="Copy to clipboard"
440
+ onClick={onCopyClick}
441
+ />
442
+ <button
443
+ className="test-recorder-button"
444
+ id="test-recorder-button-download"
445
+ title="Download as a file"
446
+ onClick={onDownloadClick}
447
+ />
448
+ </div>
449
+ <pre id="test-recorder" ref={preRef}>
450
+ {templatedTest}
451
+ </pre>
452
+ </div>
453
+ ) : null;
454
+
455
+ return [button, output];
456
+ }
457
+
458
+ export default function TestRecorderPlugin(): JSX.Element {
459
+ const [editor] = useLexicalComposerContext();
460
+ const [testRecorderButton, testRecorderOutput] = useTestRecorder(editor);
461
+
462
+ return (
463
+ <>
464
+ {testRecorderButton}
465
+ {testRecorderOutput}
466
+ </>
467
+ );
468
+ }
@@ -0,0 +1,26 @@
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 {TreeView} from '@lexical/react/LexicalTreeView';
11
+ import * as React from 'react';
12
+
13
+ export default function TreeViewPlugin(): JSX.Element {
14
+ const [editor] = useLexicalComposerContext();
15
+ return (
16
+ <TreeView
17
+ viewClassName="tree-view-output"
18
+ treeTypeButtonClassName="debug-treetype-button"
19
+ timeTravelPanelClassName="debug-timetravel-panel"
20
+ timeTravelButtonClassName="debug-timetravel-button"
21
+ timeTravelPanelSliderClassName="debug-timetravel-panel-slider"
22
+ timeTravelPanelButtonClassName="debug-timetravel-panel-button"
23
+ editor={editor}
24
+ />
25
+ );
26
+ }
@@ -0,0 +1,41 @@
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 {$createTweetNode, TweetNode} from '../../nodes/TweetNode';
15
+
16
+ export const INSERT_TWEET_COMMAND: LexicalCommand<string> = createCommand(
17
+ 'INSERT_TWEET_COMMAND',
18
+ );
19
+
20
+ export default function TwitterPlugin(): JSX.Element | null {
21
+ const [editor] = useLexicalComposerContext();
22
+
23
+ useEffect(() => {
24
+ if (!editor.hasNodes([TweetNode])) {
25
+ throw new Error('TwitterPlugin: TweetNode not registered on editor');
26
+ }
27
+
28
+ return editor.registerCommand<string>(
29
+ INSERT_TWEET_COMMAND,
30
+ (payload) => {
31
+ const tweetNode = $createTweetNode(payload);
32
+ $insertNodeToNearestRoot(tweetNode);
33
+
34
+ return true;
35
+ },
36
+ COMMAND_PRIORITY_EDITOR,
37
+ );
38
+ }, [editor]);
39
+
40
+ return null;
41
+ }