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,266 @@
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 {
10
+ DOMConversionMap,
11
+ DOMConversionOutput,
12
+ DOMExportOutput,
13
+ EditorConfig,
14
+ LexicalEditor,
15
+ LexicalNode,
16
+ NodeKey,
17
+ SerializedEditor,
18
+ SerializedLexicalNode,
19
+ Spread,
20
+ } from 'lexical';
21
+
22
+ import {$applyNodeReplacement, createEditor, DecoratorNode} from 'lexical';
23
+ import * as React from 'react';
24
+ import {Suspense} from 'react';
25
+
26
+ const ImageComponent = React.lazy(() => import('./ImageComponent'));
27
+
28
+ export interface ImagePayload {
29
+ altText: string;
30
+ caption?: LexicalEditor;
31
+ height?: number;
32
+ key?: NodeKey;
33
+ maxWidth?: number;
34
+ showCaption?: boolean;
35
+ src: string;
36
+ width?: number;
37
+ captionsEnabled?: boolean;
38
+ }
39
+
40
+ function isGoogleDocCheckboxImg(img: HTMLImageElement): boolean {
41
+ return (
42
+ img.parentElement != null &&
43
+ img.parentElement.tagName === 'LI' &&
44
+ img.previousSibling === null &&
45
+ img.getAttribute('aria-roledescription') === 'checkbox'
46
+ );
47
+ }
48
+
49
+ function $convertImageElement(domNode: Node): null | DOMConversionOutput {
50
+ const img = domNode as HTMLImageElement;
51
+ if (img.src.startsWith('file:///') || isGoogleDocCheckboxImg(img)) {
52
+ return null;
53
+ }
54
+ const {alt: altText, src, width, height} = img;
55
+ const node = $createImageNode({altText, height, src, width});
56
+ return {node};
57
+ }
58
+
59
+ export type SerializedImageNode = Spread<
60
+ {
61
+ altText: string;
62
+ caption: SerializedEditor;
63
+ height?: number;
64
+ maxWidth: number;
65
+ showCaption: boolean;
66
+ src: string;
67
+ width?: number;
68
+ },
69
+ SerializedLexicalNode
70
+ >;
71
+
72
+ export class ImageNode extends DecoratorNode<JSX.Element> {
73
+ __src: string;
74
+ __altText: string;
75
+ __width: 'inherit' | number;
76
+ __height: 'inherit' | number;
77
+ __maxWidth: number;
78
+ __showCaption: boolean;
79
+ __caption: LexicalEditor;
80
+ // Captions cannot yet be used within editor cells
81
+ __captionsEnabled: boolean;
82
+
83
+ static getType(): string {
84
+ return 'image';
85
+ }
86
+
87
+ static clone(node: ImageNode): ImageNode {
88
+ return new ImageNode(
89
+ node.__src,
90
+ node.__altText,
91
+ node.__maxWidth,
92
+ node.__width,
93
+ node.__height,
94
+ node.__showCaption,
95
+ node.__caption,
96
+ node.__captionsEnabled,
97
+ node.__key,
98
+ );
99
+ }
100
+
101
+ static importJSON(serializedNode: SerializedImageNode): ImageNode {
102
+ const {altText, height, width, maxWidth, caption, src, showCaption} =
103
+ serializedNode;
104
+ const node = $createImageNode({
105
+ altText,
106
+ height,
107
+ maxWidth,
108
+ showCaption,
109
+ src,
110
+ width,
111
+ });
112
+ const nestedEditor = node.__caption;
113
+ const editorState = nestedEditor.parseEditorState(caption.editorState);
114
+ if (!editorState.isEmpty()) {
115
+ nestedEditor.setEditorState(editorState);
116
+ }
117
+ return node;
118
+ }
119
+
120
+ exportDOM(): DOMExportOutput {
121
+ const element = document.createElement('img');
122
+ element.setAttribute('src', this.__src);
123
+ element.setAttribute('alt', this.__altText);
124
+ element.setAttribute('width', this.__width.toString());
125
+ element.setAttribute('height', this.__height.toString());
126
+ return {element};
127
+ }
128
+
129
+ static importDOM(): DOMConversionMap | null {
130
+ return {
131
+ img: (node: Node) => ({
132
+ conversion: $convertImageElement,
133
+ priority: 0,
134
+ }),
135
+ };
136
+ }
137
+
138
+ constructor(
139
+ src: string,
140
+ altText: string,
141
+ maxWidth: number,
142
+ width?: 'inherit' | number,
143
+ height?: 'inherit' | number,
144
+ showCaption?: boolean,
145
+ caption?: LexicalEditor,
146
+ captionsEnabled?: boolean,
147
+ key?: NodeKey,
148
+ ) {
149
+ super(key);
150
+ this.__src = src;
151
+ this.__altText = altText;
152
+ this.__maxWidth = maxWidth;
153
+ this.__width = width || 'inherit';
154
+ this.__height = height || 'inherit';
155
+ this.__showCaption = showCaption || false;
156
+ this.__caption =
157
+ caption ||
158
+ createEditor({
159
+ nodes: [],
160
+ });
161
+ this.__captionsEnabled = captionsEnabled || captionsEnabled === undefined;
162
+ }
163
+
164
+ exportJSON(): SerializedImageNode {
165
+ return {
166
+ altText: this.getAltText(),
167
+ caption: this.__caption.toJSON(),
168
+ height: this.__height === 'inherit' ? 0 : this.__height,
169
+ maxWidth: this.__maxWidth,
170
+ showCaption: this.__showCaption,
171
+ src: this.getSrc(),
172
+ type: 'image',
173
+ version: 1,
174
+ width: this.__width === 'inherit' ? 0 : this.__width,
175
+ };
176
+ }
177
+
178
+ setWidthAndHeight(
179
+ width: 'inherit' | number,
180
+ height: 'inherit' | number,
181
+ ): void {
182
+ const writable = this.getWritable();
183
+ writable.__width = width;
184
+ writable.__height = height;
185
+ }
186
+
187
+ setShowCaption(showCaption: boolean): void {
188
+ const writable = this.getWritable();
189
+ writable.__showCaption = showCaption;
190
+ }
191
+
192
+ // View
193
+
194
+ createDOM(config: EditorConfig): HTMLElement {
195
+ const span = document.createElement('span');
196
+ const theme = config.theme;
197
+ const className = theme.image;
198
+ if (className !== undefined) {
199
+ span.className = className;
200
+ }
201
+ return span;
202
+ }
203
+
204
+ updateDOM(): false {
205
+ return false;
206
+ }
207
+
208
+ getSrc(): string {
209
+ return this.__src;
210
+ }
211
+
212
+ getAltText(): string {
213
+ return this.__altText;
214
+ }
215
+
216
+ decorate(): JSX.Element {
217
+ return (
218
+ <Suspense fallback={null}>
219
+ <ImageComponent
220
+ src={this.__src}
221
+ altText={this.__altText}
222
+ width={this.__width}
223
+ height={this.__height}
224
+ maxWidth={this.__maxWidth}
225
+ nodeKey={this.getKey()}
226
+ showCaption={this.__showCaption}
227
+ caption={this.__caption}
228
+ captionsEnabled={this.__captionsEnabled}
229
+ resizable={true}
230
+ />
231
+ </Suspense>
232
+ );
233
+ }
234
+ }
235
+
236
+ export function $createImageNode({
237
+ altText,
238
+ height,
239
+ maxWidth = 500,
240
+ captionsEnabled,
241
+ src,
242
+ width,
243
+ showCaption,
244
+ caption,
245
+ key,
246
+ }: ImagePayload): ImageNode {
247
+ return $applyNodeReplacement(
248
+ new ImageNode(
249
+ src,
250
+ altText,
251
+ maxWidth,
252
+ width,
253
+ height,
254
+ showCaption,
255
+ caption,
256
+ captionsEnabled,
257
+ key,
258
+ ),
259
+ );
260
+ }
261
+
262
+ export function $isImageNode(
263
+ node: LexicalNode | null | undefined,
264
+ ): node is ImageNode {
265
+ return node instanceof ImageNode;
266
+ }
@@ -0,0 +1,402 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+ import type { Position } from './InlineImageNode';
9
+ import type { BaseSelection, LexicalEditor, NodeKey } from 'lexical';
10
+
11
+ import './InlineImageNode.css';
12
+
13
+ import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin';
14
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
15
+ import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
16
+ import { LexicalNestedComposer } from '@lexical/react/LexicalNestedComposer';
17
+ import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
18
+ import { useLexicalEditable } from '@lexical/react/useLexicalEditable';
19
+ import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection';
20
+ import { mergeRegister } from '@lexical/utils';
21
+ import {
22
+ $getNodeByKey,
23
+ $getSelection,
24
+ $isNodeSelection,
25
+ $setSelection,
26
+ CLICK_COMMAND,
27
+ COMMAND_PRIORITY_LOW,
28
+ DRAGSTART_COMMAND,
29
+ KEY_BACKSPACE_COMMAND,
30
+ KEY_DELETE_COMMAND,
31
+ KEY_ENTER_COMMAND,
32
+ KEY_ESCAPE_COMMAND,
33
+ SELECTION_CHANGE_COMMAND,
34
+ } from 'lexical';
35
+ import * as React from 'react';
36
+ import { Suspense, useCallback, useEffect, useRef, useState } from 'react';
37
+
38
+ // Core
39
+ import useContext from '@/client/context';
40
+ import Select, { Choice } from '@client/components/Select';
41
+ import Button from '@client/components/button';
42
+ import Input from '@client/components/inputv3';
43
+
44
+ import LinkPlugin from '../../plugins/LinkPlugin';
45
+ import ContentEditable from '../../ui/ContentEditable';
46
+ import { $isInlineImageNode, InlineImageNode } from './InlineImageNode';
47
+
48
+ const imageCache = new Set();
49
+
50
+ function useSuspenseImage(src: string) {
51
+ if (!imageCache.has(src)) {
52
+ throw new Promise((resolve) => {
53
+ const img = new Image();
54
+ img.src = src;
55
+ img.onload = () => {
56
+ imageCache.add(src);
57
+ resolve(null);
58
+ };
59
+ });
60
+ }
61
+ }
62
+
63
+ function LazyImage({
64
+ altText,
65
+ className,
66
+ imageRef,
67
+ src,
68
+ width,
69
+ height,
70
+ position,
71
+ }: {
72
+ altText: string;
73
+ className: string | null;
74
+ height: 'inherit' | number;
75
+ imageRef: { current: null | HTMLImageElement };
76
+ src: string;
77
+ width: 'inherit' | number;
78
+ position: Position;
79
+ }): React.JSX.Element {
80
+ useSuspenseImage(src);
81
+ return (
82
+ <img
83
+ className={className || undefined}
84
+ src={src}
85
+ alt={altText}
86
+ ref={imageRef}
87
+ data-position={position}
88
+ style={{
89
+ display: 'block',
90
+ height,
91
+ width,
92
+ }}
93
+ draggable={false}
94
+ />
95
+ );
96
+ }
97
+
98
+ export function UpdateInlineImageDialog({
99
+ activeEditor,
100
+ nodeKey,
101
+ close,
102
+ }: {
103
+ activeEditor: LexicalEditor;
104
+ nodeKey: NodeKey;
105
+ close: () => void;
106
+ }): React.JSX.Element {
107
+ const editorState = activeEditor.getEditorState();
108
+ const node = editorState.read(
109
+ () => $getNodeByKey(nodeKey) as InlineImageNode,
110
+ );
111
+ const [altText, setAltText] = useState(node.getAltText());
112
+ const [showCaption, setShowCaption] = useState(node.getShowCaption());
113
+ const [position, setPosition] = useState<Position>(node.getPosition());
114
+
115
+ const handleShowCaptionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
116
+ setShowCaption(e.target.checked);
117
+ };
118
+
119
+ const handlePositionChange = (choice: Choice) => {
120
+ setPosition( choice.value as Position );
121
+ };
122
+
123
+ const handleOnConfirm = () => {
124
+ const payload = { altText, position, showCaption };
125
+ if (node) {
126
+ activeEditor.update(() => {
127
+ node.update(payload);
128
+ });
129
+ }
130
+ close();
131
+ };
132
+
133
+ return (
134
+ <>
135
+ <Input
136
+ title="Alt Text"
137
+ placeholder="Descriptive alternative text"
138
+ onChange={setAltText}
139
+ value={altText}
140
+ />
141
+
142
+ <Select
143
+ style={{ marginBottom: '1em', width: '208px' }}
144
+ value={position}
145
+ title="Position"
146
+ onChange={handlePositionChange} choices={[
147
+ { value: 'left', label: 'Left' },
148
+ { value: 'right', label: 'Right' },
149
+ { value: 'full', label: 'Full Width' },
150
+ ]} />
151
+
152
+ <div className="Input__wrapper">
153
+ <input
154
+ id="caption"
155
+ type="checkbox"
156
+ checked={showCaption}
157
+ onChange={handleShowCaptionChange}
158
+ />
159
+ <label htmlFor="caption">Show Caption</label>
160
+ </div>
161
+
162
+ <Button type='primary'
163
+ onClick={() => handleOnConfirm()}>
164
+ Confirm
165
+ </Button>
166
+ </>
167
+ );
168
+ }
169
+
170
+ export default function InlineImageComponent({
171
+ src,
172
+ altText,
173
+ nodeKey,
174
+ width,
175
+ height,
176
+ showCaption,
177
+ caption,
178
+ position,
179
+ }: {
180
+ altText: string;
181
+ caption: LexicalEditor;
182
+ height: 'inherit' | number;
183
+ nodeKey: NodeKey;
184
+ showCaption: boolean;
185
+ src: string;
186
+ width: 'inherit' | number;
187
+ position: Position;
188
+ }): React.JSX.Element {
189
+
190
+ const { modal } = useContext();
191
+
192
+ const imageRef = useRef<null | HTMLImageElement>(null);
193
+ const buttonRef = useRef<HTMLButtonElement | null>(null);
194
+ const [isSelected, setSelected, clearSelection] =
195
+ useLexicalNodeSelection(nodeKey);
196
+ const [editor] = useLexicalComposerContext();
197
+ const [selection, setSelection] = useState<BaseSelection | null>(null);
198
+ const activeEditorRef = useRef<LexicalEditor | null>(null);
199
+ const isEditable = useLexicalEditable();
200
+
201
+ const $onDelete = useCallback(
202
+ (payload: KeyboardEvent) => {
203
+ const deleteSelection = $getSelection();
204
+ if (isSelected && $isNodeSelection(deleteSelection)) {
205
+ const event: KeyboardEvent = payload;
206
+ event.preventDefault();
207
+ if (isSelected && $isNodeSelection(deleteSelection)) {
208
+ editor.update(() => {
209
+ deleteSelection.getNodes().forEach((node) => {
210
+ if ($isInlineImageNode(node)) {
211
+ node.remove();
212
+ }
213
+ });
214
+ });
215
+ }
216
+ }
217
+ return false;
218
+ },
219
+ [editor, isSelected],
220
+ );
221
+
222
+ const $onEnter = useCallback(
223
+ (event: KeyboardEvent) => {
224
+ const latestSelection = $getSelection();
225
+ const buttonElem = buttonRef.current;
226
+ if (
227
+ isSelected &&
228
+ $isNodeSelection(latestSelection) &&
229
+ latestSelection.getNodes().length === 1
230
+ ) {
231
+ if (showCaption) {
232
+ // Move focus into nested editor
233
+ $setSelection(null);
234
+ event.preventDefault();
235
+ caption.focus();
236
+ return true;
237
+ } else if (
238
+ buttonElem !== null &&
239
+ buttonElem !== document.activeElement
240
+ ) {
241
+ event.preventDefault();
242
+ buttonElem.focus();
243
+ return true;
244
+ }
245
+ }
246
+ return false;
247
+ },
248
+ [caption, isSelected, showCaption],
249
+ );
250
+
251
+ const $onEscape = useCallback(
252
+ (event: KeyboardEvent) => {
253
+ if (
254
+ activeEditorRef.current === caption ||
255
+ buttonRef.current === event.target
256
+ ) {
257
+ $setSelection(null);
258
+ editor.update(() => {
259
+ setSelected(true);
260
+ const parentRootElement = editor.getRootElement();
261
+ if (parentRootElement !== null) {
262
+ parentRootElement.focus();
263
+ }
264
+ });
265
+ return true;
266
+ }
267
+ return false;
268
+ },
269
+ [caption, editor, setSelected],
270
+ );
271
+
272
+ useEffect(() => {
273
+ let isMounted = true;
274
+ const unregister = mergeRegister(
275
+ editor.registerUpdateListener(({ editorState }) => {
276
+ if (isMounted) {
277
+ setSelection(editorState.read(() => $getSelection()));
278
+ }
279
+ }),
280
+ editor.registerCommand(
281
+ SELECTION_CHANGE_COMMAND,
282
+ (_, activeEditor) => {
283
+ activeEditorRef.current = activeEditor;
284
+ return false;
285
+ },
286
+ COMMAND_PRIORITY_LOW,
287
+ ),
288
+ editor.registerCommand<MouseEvent>(
289
+ CLICK_COMMAND,
290
+ (payload) => {
291
+ const event = payload;
292
+ if (event.target === imageRef.current) {
293
+ if (event.shiftKey) {
294
+ setSelected(!isSelected);
295
+ } else {
296
+ clearSelection();
297
+ setSelected(true);
298
+ }
299
+ return true;
300
+ }
301
+
302
+ return false;
303
+ },
304
+ COMMAND_PRIORITY_LOW,
305
+ ),
306
+ editor.registerCommand(
307
+ DRAGSTART_COMMAND,
308
+ (event) => {
309
+ if (event.target === imageRef.current) {
310
+ // TODO This is just a temporary workaround for FF to behave like other browsers.
311
+ // Ideally, this handles drag & drop too (and all browsers).
312
+ event.preventDefault();
313
+ return true;
314
+ }
315
+ return false;
316
+ },
317
+ COMMAND_PRIORITY_LOW,
318
+ ),
319
+ editor.registerCommand(
320
+ KEY_DELETE_COMMAND,
321
+ $onDelete,
322
+ COMMAND_PRIORITY_LOW,
323
+ ),
324
+ editor.registerCommand(
325
+ KEY_BACKSPACE_COMMAND,
326
+ $onDelete,
327
+ COMMAND_PRIORITY_LOW,
328
+ ),
329
+ editor.registerCommand(KEY_ENTER_COMMAND, $onEnter, COMMAND_PRIORITY_LOW),
330
+ editor.registerCommand(
331
+ KEY_ESCAPE_COMMAND,
332
+ $onEscape,
333
+ COMMAND_PRIORITY_LOW,
334
+ ),
335
+ );
336
+ return () => {
337
+ isMounted = false;
338
+ unregister();
339
+ };
340
+ }, [
341
+ clearSelection,
342
+ editor,
343
+ isSelected,
344
+ nodeKey,
345
+ $onDelete,
346
+ $onEnter,
347
+ $onEscape,
348
+ setSelected,
349
+ ]);
350
+
351
+ const draggable = isSelected && $isNodeSelection(selection);
352
+ const isFocused = isSelected && isEditable;
353
+ return (
354
+ <Suspense fallback={null}>
355
+ <>
356
+ <span draggable={draggable}>
357
+ {isEditable && (
358
+ <button
359
+ className="image-edit-button"
360
+ ref={buttonRef}
361
+ onClick={() => {
362
+ modal.show('Update Inline Image', UpdateInlineImageDialog, { editor, nodeKey });
363
+ }}>
364
+ Edit
365
+ </button>
366
+ )}
367
+ <LazyImage
368
+ className={
369
+ isFocused
370
+ ? `focused ${$isNodeSelection(selection) ? 'draggable' : ''}`
371
+ : null
372
+ }
373
+ src={src}
374
+ altText={altText}
375
+ imageRef={imageRef}
376
+ width={width}
377
+ height={height}
378
+ position={position}
379
+ />
380
+ </span>
381
+ {showCaption && (
382
+ <span className="image-caption-container">
383
+ <LexicalNestedComposer initialEditor={caption}>
384
+ <AutoFocusPlugin />
385
+ <LinkPlugin />
386
+ <RichTextPlugin
387
+ contentEditable={
388
+ <ContentEditable
389
+ placeholder="Enter a caption..."
390
+ placeholderClassName="InlineImageNode__placeholder"
391
+ className="InlineImageNode__contentEditable"
392
+ />
393
+ }
394
+ ErrorBoundary={LexicalErrorBoundary}
395
+ />
396
+ </LexicalNestedComposer>
397
+ </span>
398
+ )}
399
+ </>
400
+ </Suspense>
401
+ );
402
+ }