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,350 @@
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 Button from '@client/components/button';
10
+ import Input from '@client/components/inputv3';
11
+
12
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
13
+ import { $wrapNodeInElement, mergeRegister } from '@lexical/utils';
14
+ import {
15
+ $createParagraphNode,
16
+ $createRangeSelection,
17
+ $getSelection,
18
+ $insertNodes,
19
+ $isNodeSelection,
20
+ $isRootOrShadowRoot,
21
+ $setSelection,
22
+ COMMAND_PRIORITY_EDITOR,
23
+ COMMAND_PRIORITY_HIGH,
24
+ COMMAND_PRIORITY_LOW,
25
+ createCommand,
26
+ DRAGOVER_COMMAND,
27
+ DRAGSTART_COMMAND,
28
+ DROP_COMMAND,
29
+ LexicalCommand,
30
+ LexicalEditor,
31
+ } from 'lexical';
32
+ import { useEffect, useRef, useState } from 'react';
33
+ import * as React from 'react';
34
+ import { CAN_USE_DOM } from '../../shared/canUseDOM';
35
+
36
+ import {
37
+ $createImageNode,
38
+ $isImageNode,
39
+ ImageNode,
40
+ ImagePayload,
41
+ } from '../../nodes/ImageNode';
42
+ import FileInput from '../../ui/FileInput';
43
+
44
+ export type InsertImagePayload = Readonly<ImagePayload>;
45
+
46
+ const getDOMSelection = (targetWindow: Window | null): Selection | null =>
47
+ CAN_USE_DOM ? (targetWindow || window).getSelection() : null;
48
+
49
+ export const INSERT_IMAGE_COMMAND: LexicalCommand<InsertImagePayload> =
50
+ createCommand('INSERT_IMAGE_COMMAND');
51
+
52
+ export function InsertImageUriDialogBody({
53
+ onClick,
54
+ }: {
55
+ onClick: (payload: InsertImagePayload) => void;
56
+ }) {
57
+ const [src, setSrc] = useState('');
58
+ const [altText, setAltText] = useState('');
59
+
60
+ const isDisabled = src === '';
61
+
62
+ return <div class="col w-4">
63
+ <Input
64
+ title="Image URL"
65
+ placeholder="i.e. https://source.unsplash.com/random"
66
+ onChange={setSrc}
67
+ value={src}
68
+ />
69
+ <Input
70
+ title="Alt Text"
71
+ placeholder="Random unsplash image"
72
+ onChange={setAltText}
73
+ value={altText}
74
+ />
75
+ <Button disabled={isDisabled} type="primary"
76
+ onClick={() => onClick({ altText, src })}>
77
+ Add Image
78
+ </Button>
79
+ </div>
80
+ }
81
+
82
+ export function InsertImageUploadedDialogBody({
83
+ onClick,
84
+ }: {
85
+ onClick: (payload: InsertImagePayload) => void;
86
+ }) {
87
+ const [src, setSrc] = useState('');
88
+ const [altText, setAltText] = useState('');
89
+
90
+ const isDisabled = src === '';
91
+
92
+ const loadImage = (files: FileList | null) => {
93
+ const reader = new FileReader();
94
+ reader.onload = function () {
95
+ if (typeof reader.result === 'string') {
96
+ setSrc(reader.result);
97
+ }
98
+ return '';
99
+ };
100
+ if (files !== null) {
101
+ reader.readAsDataURL(files[0]);
102
+ }
103
+ };
104
+
105
+ return (
106
+ <>
107
+ <FileInput
108
+ label="Image Upload"
109
+ onChange={loadImage}
110
+ accept="image/*"
111
+ />
112
+ <Input
113
+ title="Alt Text"
114
+ placeholder="Descriptive alternative text"
115
+ onChange={setAltText}
116
+ value={altText}
117
+ />
118
+ <Button type="primary" disabled={isDisabled} onClick={() => onClick({ altText, src })}>
119
+ Confirm
120
+ </Button>
121
+ </>
122
+ );
123
+ }
124
+
125
+ export function InsertImageDialog({
126
+ editor, close
127
+ }: {
128
+ editor: LexicalEditor;
129
+ close: () => void;
130
+ }): JSX.Element {
131
+ const [mode, setMode] = useState<null | 'url' | 'file'>(null);
132
+ const hasModifier = useRef(false);
133
+
134
+ useEffect(() => {
135
+ hasModifier.current = false;
136
+ const handler = (e: KeyboardEvent) => {
137
+ hasModifier.current = e.altKey;
138
+ };
139
+ document.addEventListener('keydown', handler);
140
+ return () => {
141
+ document.removeEventListener('keydown', handler);
142
+ };
143
+ }, [editor]);
144
+
145
+ const onClick = (payload: InsertImagePayload) => {
146
+ editor.dispatchCommand(INSERT_IMAGE_COMMAND, payload);
147
+ close();
148
+ };
149
+
150
+ return !mode ? (
151
+ <div class="row menu fill w-3">
152
+ <Button shape="tile" icon="link" onClick={() => setMode('url')}>
153
+ URL
154
+ </Button>
155
+ <Button shape='tile' icon="file" onClick={() => setMode('file')}>
156
+ File
157
+ </Button>
158
+ </div>
159
+ ) : mode === 'url' ? (
160
+ <InsertImageUriDialogBody onClick={onClick} />
161
+ ) : mode === 'file' ? (
162
+ <InsertImageUploadedDialogBody onClick={onClick} />
163
+ ) : null;
164
+ }
165
+
166
+ export default function ImagesPlugin({
167
+ captionsEnabled,
168
+ }: {
169
+ captionsEnabled?: boolean;
170
+ }): JSX.Element | null {
171
+ const [editor] = useLexicalComposerContext();
172
+
173
+ useEffect(() => {
174
+ if (!editor.hasNodes([ImageNode])) {
175
+ throw new Error('ImagesPlugin: ImageNode not registered on editor');
176
+ }
177
+
178
+ return mergeRegister(
179
+ editor.registerCommand<InsertImagePayload>(
180
+ INSERT_IMAGE_COMMAND,
181
+ (payload) => {
182
+ const imageNode = $createImageNode(payload);
183
+ $insertNodes([imageNode]);
184
+ if ($isRootOrShadowRoot(imageNode.getParentOrThrow())) {
185
+ $wrapNodeInElement(imageNode, $createParagraphNode).selectEnd();
186
+ }
187
+
188
+ return true;
189
+ },
190
+ COMMAND_PRIORITY_EDITOR,
191
+ ),
192
+ editor.registerCommand<DragEvent>(
193
+ DRAGSTART_COMMAND,
194
+ (event) => {
195
+ return $onDragStart(event);
196
+ },
197
+ COMMAND_PRIORITY_HIGH,
198
+ ),
199
+ editor.registerCommand<DragEvent>(
200
+ DRAGOVER_COMMAND,
201
+ (event) => {
202
+ return $onDragover(event);
203
+ },
204
+ COMMAND_PRIORITY_LOW,
205
+ ),
206
+ editor.registerCommand<DragEvent>(
207
+ DROP_COMMAND,
208
+ (event) => {
209
+ return $onDrop(event, editor);
210
+ },
211
+ COMMAND_PRIORITY_HIGH,
212
+ ),
213
+ );
214
+ }, [captionsEnabled, editor]);
215
+
216
+ return null;
217
+ }
218
+
219
+ const TRANSPARENT_IMAGE =
220
+ 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
221
+ const img = document.createElement('img');
222
+ img.src = TRANSPARENT_IMAGE;
223
+
224
+ function $onDragStart(event: DragEvent): boolean {
225
+ const node = $getImageNodeInSelection();
226
+ if (!node) {
227
+ return false;
228
+ }
229
+ const dataTransfer = event.dataTransfer;
230
+ if (!dataTransfer) {
231
+ return false;
232
+ }
233
+ dataTransfer.setData('text/plain', '_');
234
+ dataTransfer.setDragImage(img, 0, 0);
235
+ dataTransfer.setData(
236
+ 'application/x-lexical-drag',
237
+ JSON.stringify({
238
+ data: {
239
+ altText: node.__altText,
240
+ caption: node.__caption,
241
+ height: node.__height,
242
+ key: node.getKey(),
243
+ maxWidth: node.__maxWidth,
244
+ showCaption: node.__showCaption,
245
+ src: node.__src,
246
+ width: node.__width,
247
+ },
248
+ type: 'image',
249
+ }),
250
+ );
251
+
252
+ return true;
253
+ }
254
+
255
+ function $onDragover(event: DragEvent): boolean {
256
+ const node = $getImageNodeInSelection();
257
+ if (!node) {
258
+ return false;
259
+ }
260
+ if (!canDropImage(event)) {
261
+ event.preventDefault();
262
+ }
263
+ return true;
264
+ }
265
+
266
+ function $onDrop(event: DragEvent, editor: LexicalEditor): boolean {
267
+ const node = $getImageNodeInSelection();
268
+ if (!node) {
269
+ return false;
270
+ }
271
+ const data = getDragImageData(event);
272
+ if (!data) {
273
+ return false;
274
+ }
275
+ event.preventDefault();
276
+ if (canDropImage(event)) {
277
+ const range = getDragSelection(event);
278
+ node.remove();
279
+ const rangeSelection = $createRangeSelection();
280
+ if (range !== null && range !== undefined) {
281
+ rangeSelection.applyDOMRange(range);
282
+ }
283
+ $setSelection(rangeSelection);
284
+ editor.dispatchCommand(INSERT_IMAGE_COMMAND, data);
285
+ }
286
+ return true;
287
+ }
288
+
289
+ function $getImageNodeInSelection(): ImageNode | null {
290
+ const selection = $getSelection();
291
+ if (!$isNodeSelection(selection)) {
292
+ return null;
293
+ }
294
+ const nodes = selection.getNodes();
295
+ const node = nodes[0];
296
+ return $isImageNode(node) ? node : null;
297
+ }
298
+
299
+ function getDragImageData(event: DragEvent): null | InsertImagePayload {
300
+ const dragData = event.dataTransfer?.getData('application/x-lexical-drag');
301
+ if (!dragData) {
302
+ return null;
303
+ }
304
+ const { type, data } = JSON.parse(dragData);
305
+ if (type !== 'image') {
306
+ return null;
307
+ }
308
+
309
+ return data;
310
+ }
311
+
312
+ declare global {
313
+ interface DragEvent {
314
+ rangeOffset?: number;
315
+ rangeParent?: Node;
316
+ }
317
+ }
318
+
319
+ function canDropImage(event: DragEvent): boolean {
320
+ const target = event.target;
321
+ return !!(
322
+ target &&
323
+ target instanceof HTMLElement &&
324
+ !target.closest('code, span.editor-image') &&
325
+ target.parentElement &&
326
+ target.parentElement.closest('div.ContentEditable__root')
327
+ );
328
+ }
329
+
330
+ function getDragSelection(event: DragEvent): Range | null | undefined {
331
+ let range;
332
+ const target = event.target as null | Element | Document;
333
+ const targetWindow =
334
+ target == null
335
+ ? null
336
+ : target.nodeType === 9
337
+ ? (target as Document).defaultView
338
+ : (target as Element).ownerDocument.defaultView;
339
+ const domSelection = getDOMSelection(targetWindow);
340
+ if (document.caretRangeFromPoint) {
341
+ range = document.caretRangeFromPoint(event.clientX, event.clientY);
342
+ } else if (event.rangeParent && domSelection !== null) {
343
+ domSelection.collapse(event.rangeParent, event.rangeOffset || 0);
344
+ range = domSelection.getRangeAt(0);
345
+ } else {
346
+ throw Error(`Cannot get the selection when dragging`);
347
+ }
348
+
349
+ return range;
350
+ }
@@ -0,0 +1,336 @@
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 Select, { Choice } from '@client/components/Select';
11
+ import Button from '@client/components/button';
12
+ import Input from '@client/components/inputv3';
13
+
14
+ import type { Position } from '../../nodes/InlineImageNode/InlineImageNode';
15
+
16
+ import '../../nodes/InlineImageNode/InlineImageNode.css';
17
+
18
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
19
+ import { $wrapNodeInElement, mergeRegister } from '@lexical/utils';
20
+ import {
21
+ $createParagraphNode,
22
+ $createRangeSelection,
23
+ $getSelection,
24
+ $insertNodes,
25
+ $isNodeSelection,
26
+ $isRootOrShadowRoot,
27
+ $setSelection,
28
+ COMMAND_PRIORITY_EDITOR,
29
+ COMMAND_PRIORITY_HIGH,
30
+ COMMAND_PRIORITY_LOW,
31
+ createCommand,
32
+ DRAGOVER_COMMAND,
33
+ DRAGSTART_COMMAND,
34
+ DROP_COMMAND,
35
+ LexicalCommand,
36
+ LexicalEditor,
37
+ } from 'lexical';
38
+ import * as React from 'react';
39
+ import { useEffect, useRef, useState } from 'react';
40
+ import { CAN_USE_DOM } from '../../shared/canUseDOM';
41
+
42
+ import {
43
+ $createInlineImageNode,
44
+ $isInlineImageNode,
45
+ InlineImageNode,
46
+ InlineImagePayload,
47
+ } from '../../nodes/InlineImageNode/InlineImageNode';
48
+
49
+ import FileInput from '../../ui/FileInput';
50
+
51
+ export type InsertInlineImagePayload = Readonly<InlineImagePayload>;
52
+
53
+ const getDOMSelection = (targetWindow: Window | null): Selection | null =>
54
+ CAN_USE_DOM ? (targetWindow || window).getSelection() : null;
55
+
56
+ export const INSERT_INLINE_IMAGE_COMMAND: LexicalCommand<InlineImagePayload> =
57
+ createCommand('INSERT_INLINE_IMAGE_COMMAND');
58
+
59
+ export function InsertInlineImageDialog({
60
+ editor,
61
+ close,
62
+ }: {
63
+ editor: LexicalEditor;
64
+ close: () => void;
65
+ }): React.JSX.Element {
66
+ const hasModifier = useRef(false);
67
+
68
+ const [src, setSrc] = useState('');
69
+ const [altText, setAltText] = useState('');
70
+ const [showCaption, setShowCaption] = useState(false);
71
+ const [position, setPosition] = useState<Position>('left');
72
+
73
+ const isDisabled = src === '';
74
+
75
+ const handleShowCaptionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
76
+ setShowCaption(e.target.checked);
77
+ };
78
+
79
+ const handlePositionChange = (choice: Choice) => {
80
+ setPosition( choice.value );
81
+ };
82
+
83
+ const loadImage = (files: FileList | null) => {
84
+ const reader = new FileReader();
85
+ reader.onload = function () {
86
+ if (typeof reader.result === 'string') {
87
+ setSrc(reader.result);
88
+ }
89
+ return '';
90
+ };
91
+ if (files !== null) {
92
+ reader.readAsDataURL(files[0]);
93
+ }
94
+ };
95
+
96
+ useEffect(() => {
97
+ hasModifier.current = false;
98
+ const handler = (e: KeyboardEvent) => {
99
+ hasModifier.current = e.altKey;
100
+ };
101
+ document.addEventListener('keydown', handler);
102
+ return () => {
103
+ document.removeEventListener('keydown', handler);
104
+ };
105
+ }, [editor]);
106
+
107
+ const handleOnClick = () => {
108
+ const payload = { altText, position, showCaption, src };
109
+ editor.dispatchCommand(INSERT_INLINE_IMAGE_COMMAND, payload);
110
+ close();
111
+ };
112
+
113
+ return (
114
+ <>
115
+ <FileInput
116
+ label="Image Upload"
117
+ onChange={loadImage}
118
+ accept="image/*"
119
+ data-test-id="image-modal-file-upload"
120
+ />
121
+
122
+ <Input
123
+ title="Alt Text"
124
+ placeholder="Descriptive alternative text"
125
+ onChange={setAltText}
126
+ value={altText}
127
+ />
128
+
129
+ <Select title="Position"
130
+ style={{ marginBottom: '1em', width: '290px' }}
131
+ value={position} onChange={handlePositionChange} choices={[
132
+ { label: 'Left', value: 'left' },
133
+ { label: 'Right', value: 'right' },
134
+ { label: 'Full Width', value: 'full' },
135
+ ]} />
136
+
137
+ <div className="Input__wrapper">
138
+ <input
139
+ id="caption"
140
+ className="InlineImageNode_Checkbox"
141
+ type="checkbox"
142
+ checked={showCaption}
143
+ onChange={handleShowCaptionChange}
144
+ />
145
+ <label htmlFor="caption">Show Caption</label>
146
+ </div>
147
+
148
+ <Button type="primary"
149
+ disabled={isDisabled}
150
+ onClick={() => handleOnClick()}>
151
+ Confirm
152
+ </Button>
153
+ </>
154
+ );
155
+ }
156
+
157
+ export default function InlineImagePlugin(): JSX.Element | null {
158
+ const [editor] = useLexicalComposerContext();
159
+
160
+ useEffect(() => {
161
+ if (!editor.hasNodes([InlineImageNode])) {
162
+ throw new Error('ImagesPlugin: InlineImageNode not registered on editor');
163
+ }
164
+
165
+ return mergeRegister(
166
+ editor.registerCommand<InsertInlineImagePayload>(
167
+ INSERT_INLINE_IMAGE_COMMAND,
168
+ (payload) => {
169
+ const imageNode = $createInlineImageNode(payload);
170
+ $insertNodes([imageNode]);
171
+ if ($isRootOrShadowRoot(imageNode.getParentOrThrow())) {
172
+ $wrapNodeInElement(imageNode, $createParagraphNode).selectEnd();
173
+ }
174
+
175
+ return true;
176
+ },
177
+ COMMAND_PRIORITY_EDITOR,
178
+ ),
179
+ editor.registerCommand<DragEvent>(
180
+ DRAGSTART_COMMAND,
181
+ (event) => {
182
+ return $onDragStart(event);
183
+ },
184
+ COMMAND_PRIORITY_HIGH,
185
+ ),
186
+ editor.registerCommand<DragEvent>(
187
+ DRAGOVER_COMMAND,
188
+ (event) => {
189
+ return $onDragover(event);
190
+ },
191
+ COMMAND_PRIORITY_LOW,
192
+ ),
193
+ editor.registerCommand<DragEvent>(
194
+ DROP_COMMAND,
195
+ (event) => {
196
+ return $onDrop(event, editor);
197
+ },
198
+ COMMAND_PRIORITY_HIGH,
199
+ ),
200
+ );
201
+ }, [editor]);
202
+
203
+ return null;
204
+ }
205
+
206
+ const TRANSPARENT_IMAGE =
207
+ 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
208
+ const img = document.createElement('img');
209
+ img.src = TRANSPARENT_IMAGE;
210
+
211
+ function $onDragStart(event: DragEvent): boolean {
212
+ const node = $getImageNodeInSelection();
213
+ if (!node) {
214
+ return false;
215
+ }
216
+ const dataTransfer = event.dataTransfer;
217
+ if (!dataTransfer) {
218
+ return false;
219
+ }
220
+ dataTransfer.setData('text/plain', '_');
221
+ dataTransfer.setDragImage(img, 0, 0);
222
+ dataTransfer.setData(
223
+ 'application/x-lexical-drag',
224
+ JSON.stringify({
225
+ data: {
226
+ altText: node.__altText,
227
+ caption: node.__caption,
228
+ height: node.__height,
229
+ key: node.getKey(),
230
+ showCaption: node.__showCaption,
231
+ src: node.__src,
232
+ width: node.__width,
233
+ },
234
+ type: 'image',
235
+ }),
236
+ );
237
+
238
+ return true;
239
+ }
240
+
241
+ function $onDragover(event: DragEvent): boolean {
242
+ const node = $getImageNodeInSelection();
243
+ if (!node) {
244
+ return false;
245
+ }
246
+ if (!canDropImage(event)) {
247
+ event.preventDefault();
248
+ }
249
+ return true;
250
+ }
251
+
252
+ function $onDrop(event: DragEvent, editor: LexicalEditor): boolean {
253
+ const node = $getImageNodeInSelection();
254
+ if (!node) {
255
+ return false;
256
+ }
257
+ const data = getDragImageData(event);
258
+ if (!data) {
259
+ return false;
260
+ }
261
+ event.preventDefault();
262
+ if (canDropImage(event)) {
263
+ const range = getDragSelection(event);
264
+ node.remove();
265
+ const rangeSelection = $createRangeSelection();
266
+ if (range !== null && range !== undefined) {
267
+ rangeSelection.applyDOMRange(range);
268
+ }
269
+ $setSelection(rangeSelection);
270
+ editor.dispatchCommand(INSERT_INLINE_IMAGE_COMMAND, data);
271
+ }
272
+ return true;
273
+ }
274
+
275
+ function $getImageNodeInSelection(): InlineImageNode | null {
276
+ const selection = $getSelection();
277
+ if (!$isNodeSelection(selection)) {
278
+ return null;
279
+ }
280
+ const nodes = selection.getNodes();
281
+ const node = nodes[0];
282
+ return $isInlineImageNode(node) ? node : null;
283
+ }
284
+
285
+ function getDragImageData(event: DragEvent): null | InsertInlineImagePayload {
286
+ const dragData = event.dataTransfer?.getData('application/x-lexical-drag');
287
+ if (!dragData) {
288
+ return null;
289
+ }
290
+ const { type, data } = JSON.parse(dragData);
291
+ if (type !== 'image') {
292
+ return null;
293
+ }
294
+
295
+ return data;
296
+ }
297
+
298
+ declare global {
299
+ interface DragEvent {
300
+ rangeOffset?: number;
301
+ rangeParent?: Node;
302
+ }
303
+ }
304
+
305
+ function canDropImage(event: DragEvent): boolean {
306
+ const target = event.target;
307
+ return !!(
308
+ target &&
309
+ target instanceof HTMLElement &&
310
+ !target.closest('code, span.editor-image') &&
311
+ target.parentElement &&
312
+ target.parentElement.closest('div.ContentEditable__root')
313
+ );
314
+ }
315
+
316
+ function getDragSelection(event: DragEvent): Range | null | undefined {
317
+ let range;
318
+ const target = event.target as null | Element | Document;
319
+ const targetWindow =
320
+ target == null
321
+ ? null
322
+ : target.nodeType === 9
323
+ ? (target as Document).defaultView
324
+ : (target as Element).ownerDocument.defaultView;
325
+ const domSelection = getDOMSelection(targetWindow);
326
+ if (document.caretRangeFromPoint) {
327
+ range = document.caretRangeFromPoint(event.clientX, event.clientY);
328
+ } else if (event.rangeParent && domSelection !== null) {
329
+ domSelection.collapse(event.rangeParent, event.rangeOffset || 0);
330
+ range = domSelection.getRangeAt(0);
331
+ } else {
332
+ throw Error('Cannot get the selection when dragging');
333
+ }
334
+
335
+ return range;
336
+ }