@collabchron/notiq 0.2.0

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 (188) hide show
  1. package/README.md +71 -0
  2. package/components.json +21 -0
  3. package/eslint.config.mjs +16 -0
  4. package/next.config.ts +12 -0
  5. package/package.json +108 -0
  6. package/postcss.config.mjs +5 -0
  7. package/public/file.svg +1 -0
  8. package/public/globe.svg +1 -0
  9. package/public/images/icons/plus.svg +10 -0
  10. package/public/next.svg +1 -0
  11. package/public/vercel.svg +1 -0
  12. package/public/window.svg +1 -0
  13. package/src/app/actions.ts +2 -0
  14. package/src/app/api/ai/route.ts +175 -0
  15. package/src/app/api/edgestore/[...edgestore]/route.ts +28 -0
  16. package/src/app/favicon.ico +0 -0
  17. package/src/app/globals.css +205 -0
  18. package/src/app/layout.tsx +38 -0
  19. package/src/app/page.tsx +12 -0
  20. package/src/components/editor/Core.tsx +220 -0
  21. package/src/components/editor/hooks/instructions-messages.ts +300 -0
  22. package/src/components/editor/hooks/use-mobile.ts +19 -0
  23. package/src/components/editor/hooks/useReport.ts +67 -0
  24. package/src/components/editor/hooks/useResizeObservert.ts +22 -0
  25. package/src/components/editor/index.tsx +39 -0
  26. package/src/components/editor/lexical-on-change.tsx +28 -0
  27. package/src/components/editor/nodes/CollapsibleNode/CollapsibleContainerNode.ts +92 -0
  28. package/src/components/editor/nodes/CollapsibleNode/CollapsibleContentNode.ts +65 -0
  29. package/src/components/editor/nodes/CollapsibleNode/CollapsibleTitleNode.ts +105 -0
  30. package/src/components/editor/nodes/EquationNode/EquationComponent.tsx +143 -0
  31. package/src/components/editor/nodes/EquationNode/EquationNode.tsx +170 -0
  32. package/src/components/editor/nodes/ExcalidrawNode/ExcalidrawComponent.tsx +228 -0
  33. package/src/components/editor/nodes/ExcalidrawNode/ExcalidrawImage.tsx +137 -0
  34. package/src/components/editor/nodes/ExcalidrawNode/ImageResizer.tsx +317 -0
  35. package/src/components/editor/nodes/ExcalidrawNode/index.tsx +204 -0
  36. package/src/components/editor/nodes/FigmaNode/FigmaNode.tsx +134 -0
  37. package/src/components/editor/nodes/Hint/HintComponet.tsx +221 -0
  38. package/src/components/editor/nodes/Hint/index.tsx +190 -0
  39. package/src/components/editor/nodes/ImageNode/index.tsx +328 -0
  40. package/src/components/editor/nodes/InlineImageNode/InlineImageComponent.tsx +383 -0
  41. package/src/components/editor/nodes/InlineImageNode/InlineImageNode.css +94 -0
  42. package/src/components/editor/nodes/InlineImageNode/InlineImageNode.tsx +309 -0
  43. package/src/components/editor/nodes/LayoutNode/LayoutContainerNode.ts +146 -0
  44. package/src/components/editor/nodes/LayoutNode/LayoutItemNode.ts +79 -0
  45. package/src/components/editor/nodes/PollNode/index.tsx +204 -0
  46. package/src/components/editor/nodes/Stepper/index.tsx +260 -0
  47. package/src/components/editor/nodes/TweetNode/index.tsx +214 -0
  48. package/src/components/editor/nodes/index.ts +81 -0
  49. package/src/components/editor/plugins/AutoEmbedPlugin/index.tsx +350 -0
  50. package/src/components/editor/plugins/AutoLinkPlugin/index.tsx +56 -0
  51. package/src/components/editor/plugins/CodeActionMenuPlugin/components/CopyButton.tsx +70 -0
  52. package/src/components/editor/plugins/CodeActionMenuPlugin/components/PrettierButton.tsx +192 -0
  53. package/src/components/editor/plugins/CodeActionMenuPlugin/index.tsx +217 -0
  54. package/src/components/editor/plugins/CodeActionMenuPlugin/utils.ts +26 -0
  55. package/src/components/editor/plugins/CodeHighlightPlugin/index.ts +21 -0
  56. package/src/components/editor/plugins/CollapsiblePlugin/Collapsible.css +76 -0
  57. package/src/components/editor/plugins/CollapsiblePlugin/index.ts +228 -0
  58. package/src/components/editor/plugins/DragDropPastePlugin/index.tsx +44 -0
  59. package/src/components/editor/plugins/DraggableBlockPlugin/index.tsx +52 -0
  60. package/src/components/editor/plugins/EquationsPlugin/index.tsx +85 -0
  61. package/src/components/editor/plugins/ExcalidrawPlugin/index.tsx +98 -0
  62. package/src/components/editor/plugins/FigmaPlugin/index.tsx +42 -0
  63. package/src/components/editor/plugins/FloatingLinkEditorPlugin/index.tsx +445 -0
  64. package/src/components/editor/plugins/FloatingTextFormatToolbarPlugin/index.tsx +275 -0
  65. package/src/components/editor/plugins/ImagesPlugin/index.tsx +222 -0
  66. package/src/components/editor/plugins/InlineImagePlugin/index.tsx +351 -0
  67. package/src/components/editor/plugins/LayoutPlugin/index.tsx +238 -0
  68. package/src/components/editor/plugins/LinkPlugin/index.tsx +36 -0
  69. package/src/components/editor/plugins/LinkWithMetaData/index.tsx +271 -0
  70. package/src/components/editor/plugins/MarkdownShortcutPlugin/index.tsx +11 -0
  71. package/src/components/editor/plugins/MarkdownTransformers/index.tsx +304 -0
  72. package/src/components/editor/plugins/PollPlugin/index.tsx +49 -0
  73. package/src/components/editor/plugins/ShortcutsPlugin/index.tsx +180 -0
  74. package/src/components/editor/plugins/ShortcutsPlugin/shortcuts.ts +253 -0
  75. package/src/components/editor/plugins/SlashCommand/index.tsx +621 -0
  76. package/src/components/editor/plugins/SpeechToTextPlugin/index.ts +127 -0
  77. package/src/components/editor/plugins/TabFocusPlugin/index.ts +58 -0
  78. package/src/components/editor/plugins/TableCellActionMenuPlugin/index.tsx +759 -0
  79. package/src/components/editor/plugins/TableCellResizer/index.tsx +438 -0
  80. package/src/components/editor/plugins/TableHoverActionsPlugin/index.tsx +314 -0
  81. package/src/components/editor/plugins/TablePlugin/index.tsx +99 -0
  82. package/src/components/editor/plugins/ToolbarPlugin/index.tsx +522 -0
  83. package/src/components/editor/plugins/TwitterPlugin/index.ts +35 -0
  84. package/src/components/editor/plugins/YouTubeNode/index.tsx +179 -0
  85. package/src/components/editor/plugins/YouTubePlugin/index.ts +41 -0
  86. package/src/components/editor/themes/editor-theme.ts +113 -0
  87. package/src/components/editor/themes/theme.css +377 -0
  88. package/src/components/editor/utils/ai.ts +291 -0
  89. package/src/components/editor/utils/canUseDOM.ts +12 -0
  90. package/src/components/editor/utils/editorFormatting.ts +282 -0
  91. package/src/components/editor/utils/environment.ts +50 -0
  92. package/src/components/editor/utils/extract-data.ts +166 -0
  93. package/src/components/editor/utils/getAllLexicalChildren.ts +13 -0
  94. package/src/components/editor/utils/getDOMRangeRect.ts +27 -0
  95. package/src/components/editor/utils/getSelectedNode.ts +27 -0
  96. package/src/components/editor/utils/gif.ts +29 -0
  97. package/src/components/editor/utils/invariant.ts +15 -0
  98. package/src/components/editor/utils/setFloatingElemPosition.ts +51 -0
  99. package/src/components/editor/utils/setFloatingElemPositionForLinkEditor.ts +40 -0
  100. package/src/components/editor/utils/setNodePlaceholderFromSelection/getNodePlaceholder.ts +51 -0
  101. package/src/components/editor/utils/setNodePlaceholderFromSelection/setNodePlaceholderFromSelection.ts +15 -0
  102. package/src/components/editor/utils/setNodePlaceholderFromSelection/setPlaceholderOnSelection.ts +114 -0
  103. package/src/components/editor/utils/setNodePlaceholderFromSelection/styles.css +6 -0
  104. package/src/components/editor/utils/url.ts +109 -0
  105. package/src/components/editor/utils/useLayoutEffect.ts +13 -0
  106. package/src/components/providers/QueryProvider.tsx +15 -0
  107. package/src/components/providers/SharedHistoryContext.tsx +28 -0
  108. package/src/components/providers/ToolbarContext.tsx +123 -0
  109. package/src/components/providers/theme-provider.tsx +11 -0
  110. package/src/components/theme/ModeToggle.tsx +40 -0
  111. package/src/components/ui/FileInput.tsx +40 -0
  112. package/src/components/ui/Input.css +32 -0
  113. package/src/components/ui/Select.css +42 -0
  114. package/src/components/ui/Select.tsx +36 -0
  115. package/src/components/ui/TextInput.tsx +48 -0
  116. package/src/components/ui/ai/ai-button.tsx +574 -0
  117. package/src/components/ui/ai/border.tsx +99 -0
  118. package/src/components/ui/ai/placeholder-input-vanish.tsx +282 -0
  119. package/src/components/ui/button.tsx +89 -0
  120. package/src/components/ui/card.tsx +76 -0
  121. package/src/components/ui/checkbox.tsx +30 -0
  122. package/src/components/ui/command.tsx +153 -0
  123. package/src/components/ui/dialog/Dialog.css +25 -0
  124. package/src/components/ui/dialog/Dialog.tsx +34 -0
  125. package/src/components/ui/dialog.tsx +122 -0
  126. package/src/components/ui/drop-downs/background-color.tsx +183 -0
  127. package/src/components/ui/drop-downs/block-format.tsx +159 -0
  128. package/src/components/ui/drop-downs/code.tsx +42 -0
  129. package/src/components/ui/drop-downs/color.tsx +177 -0
  130. package/src/components/ui/drop-downs/font-size.tsx +138 -0
  131. package/src/components/ui/drop-downs/font.tsx +155 -0
  132. package/src/components/ui/drop-downs/index.tsx +122 -0
  133. package/src/components/ui/drop-downs/insert-node.tsx +213 -0
  134. package/src/components/ui/drop-downs/text-align.tsx +123 -0
  135. package/src/components/ui/drop-downs/text-format.tsx +104 -0
  136. package/src/components/ui/dropdown-menu.tsx +201 -0
  137. package/src/components/ui/equation/EquationEditor.css +38 -0
  138. package/src/components/ui/equation/EquationEditor.tsx +56 -0
  139. package/src/components/ui/equation/KatexEquationAlterer.css +41 -0
  140. package/src/components/ui/equation/KatexEquationAlterer.tsx +83 -0
  141. package/src/components/ui/equation/KatexRenderer.tsx +66 -0
  142. package/src/components/ui/excalidraw/ExcalidrawModal.css +64 -0
  143. package/src/components/ui/excalidraw/ExcalidrawModal.tsx +234 -0
  144. package/src/components/ui/excalidraw/Modal.css +62 -0
  145. package/src/components/ui/excalidraw/Modal.tsx +110 -0
  146. package/src/components/ui/hover-card.tsx +29 -0
  147. package/src/components/ui/image/error-image.tsx +17 -0
  148. package/src/components/ui/image/file-upload.tsx +240 -0
  149. package/src/components/ui/image/image-resizer.tsx +297 -0
  150. package/src/components/ui/image/image-toolbar.tsx +264 -0
  151. package/src/components/ui/image/index.tsx +408 -0
  152. package/src/components/ui/image/lazy-image.tsx +68 -0
  153. package/src/components/ui/image/lazy-video.tsx +71 -0
  154. package/src/components/ui/input.tsx +22 -0
  155. package/src/components/ui/models/custom-dialog.tsx +320 -0
  156. package/src/components/ui/models/insert-gif.tsx +90 -0
  157. package/src/components/ui/models/insert-image.tsx +52 -0
  158. package/src/components/ui/models/insert-poll.tsx +29 -0
  159. package/src/components/ui/models/insert-table.tsx +62 -0
  160. package/src/components/ui/models/use-model.tsx +91 -0
  161. package/src/components/ui/poll/poll-component.tsx +304 -0
  162. package/src/components/ui/popover.tsx +33 -0
  163. package/src/components/ui/progress.tsx +28 -0
  164. package/src/components/ui/scroll-area.tsx +48 -0
  165. package/src/components/ui/separator.tsx +31 -0
  166. package/src/components/ui/skeleton.tsx +15 -0
  167. package/src/components/ui/sonner.tsx +31 -0
  168. package/src/components/ui/stepper/step.tsx +179 -0
  169. package/src/components/ui/stepper/stepper.tsx +89 -0
  170. package/src/components/ui/textarea.tsx +22 -0
  171. package/src/components/ui/toggle.tsx +71 -0
  172. package/src/components/ui/tooltip.tsx +32 -0
  173. package/src/components/ui/write/text-format-floting-toolbar.tsx +346 -0
  174. package/src/lib/edgestore.ts +9 -0
  175. package/src/lib/pinecone-client.ts +0 -0
  176. package/src/lib/utils.ts +6 -0
  177. package/src/utils/docSerialization.ts +77 -0
  178. package/src/utils/emoji-list.ts +16615 -0
  179. package/src/utils/getDOMRangeRect.ts +27 -0
  180. package/src/utils/getSelectedNode.ts +27 -0
  181. package/src/utils/getThemeSelector.ts +25 -0
  182. package/src/utils/isMobileWidth.ts +7 -0
  183. package/src/utils/joinClasses.ts +13 -0
  184. package/src/utils/setFloatingElemPosition.ts +74 -0
  185. package/src/utils/setFloatingElemPositionForLinkEditor.ts +46 -0
  186. package/src/utils/swipe.ts +127 -0
  187. package/src/utils/url.ts +38 -0
  188. package/tsconfig.json +27 -0
@@ -0,0 +1,351 @@
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 '../../nodes/InlineImageNode/InlineImageNode';
9
+ import type {JSX} from 'react';
10
+
11
+ import '../../nodes/InlineImageNode/InlineImageNode.css';
12
+
13
+ import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
14
+ import {$wrapNodeInElement, mergeRegister} from '@lexical/utils';
15
+ import {
16
+ $createParagraphNode,
17
+ $createRangeSelection,
18
+ $getSelection,
19
+ $insertNodes,
20
+ $isNodeSelection,
21
+ $isRootOrShadowRoot,
22
+ $setSelection,
23
+ COMMAND_PRIORITY_EDITOR,
24
+ COMMAND_PRIORITY_HIGH,
25
+ COMMAND_PRIORITY_LOW,
26
+ createCommand,
27
+ DRAGOVER_COMMAND,
28
+ DRAGSTART_COMMAND,
29
+ DROP_COMMAND,
30
+ getDOMSelectionFromTarget,
31
+ isHTMLElement,
32
+ LexicalCommand,
33
+ LexicalEditor,
34
+ } from 'lexical';
35
+ import * as React from 'react';
36
+ import {useEffect, useRef, useState} from 'react';
37
+
38
+ import {
39
+ $createInlineImageNode,
40
+ $isInlineImageNode,
41
+ InlineImageNode,
42
+ InlineImagePayload,
43
+ } from '../../nodes/InlineImageNode/InlineImageNode';
44
+ import { Button } from '@/components/ui/button';
45
+ import TextInput from '@/components/ui/TextInput';
46
+ import FileInput from '@/components/ui/FileInput';
47
+ import Select from '@/components/ui/Select';
48
+ import { DialogActions } from '@/components/ui/dialog/Dialog';
49
+
50
+ export type InsertInlineImagePayload = Readonly<InlineImagePayload>;
51
+
52
+ export const INSERT_INLINE_IMAGE_COMMAND: LexicalCommand<InlineImagePayload> =
53
+ createCommand('INSERT_INLINE_IMAGE_COMMAND');
54
+
55
+ export function InsertInlineImageDialog({
56
+ activeEditor,
57
+ onClose,
58
+ }: {
59
+ activeEditor: LexicalEditor;
60
+ onClose: () => void;
61
+ }): JSX.Element {
62
+ const hasModifier = useRef(false);
63
+
64
+ const [src, setSrc] = useState('');
65
+ const [altText, setAltText] = useState('');
66
+ const [showCaption, setShowCaption] = useState(false);
67
+ const [position, setPosition] = useState<Position>('left');
68
+
69
+ const isDisabled = src === '';
70
+
71
+ const handleShowCaptionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
72
+ setShowCaption(e.target.checked);
73
+ };
74
+
75
+ const handlePositionChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
76
+ setPosition(e.target.value as Position);
77
+ };
78
+
79
+ const loadImage = (files: FileList | null) => {
80
+ const reader = new FileReader();
81
+ reader.onload = function () {
82
+ if (typeof reader.result === 'string') {
83
+ setSrc(reader.result);
84
+ }
85
+ return '';
86
+ };
87
+ if (files !== null) {
88
+ reader.readAsDataURL(files[0]);
89
+ }
90
+ };
91
+
92
+ useEffect(() => {
93
+ hasModifier.current = false;
94
+ const handler = (e: KeyboardEvent) => {
95
+ hasModifier.current = e.altKey;
96
+ };
97
+ document.addEventListener('keydown', handler);
98
+ return () => {
99
+ document.removeEventListener('keydown', handler);
100
+ };
101
+ }, [activeEditor]);
102
+
103
+ const handleOnClick = () => {
104
+ const payload = {altText, position, showCaption, src};
105
+ activeEditor.dispatchCommand(INSERT_INLINE_IMAGE_COMMAND, payload);
106
+ onClose();
107
+ };
108
+
109
+ return (
110
+ <>
111
+ <div style={{marginBottom: '1em'}}>
112
+ <FileInput
113
+ label="Image Upload"
114
+ onChange={loadImage}
115
+ accept="image/*"
116
+ data-test-id="image-modal-file-upload"
117
+ />
118
+ </div>
119
+ <div style={{marginBottom: '1em'}}>
120
+ <TextInput
121
+ label="Alt Text"
122
+ placeholder="Descriptive alternative text"
123
+ onChange={setAltText}
124
+ value={altText}
125
+ data-test-id="image-modal-alt-text-input"
126
+ />
127
+ </div>
128
+
129
+ <Select
130
+ style={{marginBottom: '1em', width: '290px'}}
131
+ label="Position"
132
+ name="position"
133
+ id="position-select"
134
+ onChange={handlePositionChange}>
135
+ <option value="left">Left</option>
136
+ <option value="right">Right</option>
137
+ <option value="full">Full Width</option>
138
+ </Select>
139
+
140
+ <div className="Input__wrapper">
141
+ <input
142
+ id="caption"
143
+ className="InlineImageNode_Checkbox"
144
+ type="checkbox"
145
+ checked={showCaption}
146
+ onChange={handleShowCaptionChange}
147
+ />
148
+ <label htmlFor="caption">Show Caption</label>
149
+ </div>
150
+
151
+ <DialogActions>
152
+ <Button
153
+ data-test-id="image-modal-file-upload-btn"
154
+ disabled={isDisabled}
155
+ onClick={() => handleOnClick()}>
156
+ Confirm
157
+ </Button>
158
+ </DialogActions>
159
+ </>
160
+ );
161
+ }
162
+
163
+ export default function InlineImagePlugin(): JSX.Element | null {
164
+ const [editor] = useLexicalComposerContext();
165
+
166
+ useEffect(() => {
167
+ if (!editor.hasNodes([InlineImageNode])) {
168
+ throw new Error('ImagesPlugin: ImageNode not registered on editor');
169
+ }
170
+
171
+ return mergeRegister(
172
+ editor.registerCommand<InsertInlineImagePayload>(
173
+ INSERT_INLINE_IMAGE_COMMAND,
174
+ (payload) => {
175
+ const imageNode = $createInlineImageNode(payload);
176
+ $insertNodes([imageNode]);
177
+ if ($isRootOrShadowRoot(imageNode.getParentOrThrow())) {
178
+ $wrapNodeInElement(imageNode, $createParagraphNode).selectEnd();
179
+ }
180
+
181
+ return true;
182
+ },
183
+ COMMAND_PRIORITY_EDITOR,
184
+ ),
185
+ editor.registerCommand<DragEvent>(
186
+ DRAGSTART_COMMAND,
187
+ (event) => {
188
+ return $onDragStart(event);
189
+ },
190
+ COMMAND_PRIORITY_HIGH,
191
+ ),
192
+ editor.registerCommand<DragEvent>(
193
+ DRAGOVER_COMMAND,
194
+ (event) => {
195
+ return $onDragover(event);
196
+ },
197
+ COMMAND_PRIORITY_LOW,
198
+ ),
199
+ editor.registerCommand<DragEvent>(
200
+ DROP_COMMAND,
201
+ (event) => {
202
+ return $onDrop(event, editor);
203
+ },
204
+ COMMAND_PRIORITY_HIGH,
205
+ ),
206
+ );
207
+ }, [editor]);
208
+
209
+ return null;
210
+ }
211
+
212
+ const TRANSPARENT_IMAGE =
213
+ 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
214
+
215
+ // Create the transparent image lazily when needed
216
+ function getTransparentImage(): HTMLImageElement {
217
+ if (typeof document === 'undefined') {
218
+ // Return a mock object for SSR
219
+ return {} as HTMLImageElement;
220
+ }
221
+
222
+ const img = document.createElement('img');
223
+ img.src = TRANSPARENT_IMAGE;
224
+ return img;
225
+ }
226
+
227
+ function $onDragStart(event: DragEvent): boolean {
228
+ const node = $getImageNodeInSelection();
229
+ if (!node) {
230
+ return false;
231
+ }
232
+ const dataTransfer = event.dataTransfer;
233
+ if (!dataTransfer) {
234
+ return false;
235
+ }
236
+
237
+ dataTransfer.setData('text/plain', '_');
238
+
239
+ // Only create the image if document is available
240
+ if (typeof document !== 'undefined') {
241
+ const img = getTransparentImage();
242
+ dataTransfer.setDragImage(img, 0, 0);
243
+ }
244
+
245
+ dataTransfer.setData(
246
+ 'application/x-lexical-drag',
247
+ JSON.stringify({
248
+ data: {
249
+ altText: node.__altText,
250
+ caption: node.__caption,
251
+ height: node.__height,
252
+ key: node.getKey(),
253
+ showCaption: node.__showCaption,
254
+ src: node.__src,
255
+ width: node.__width,
256
+ },
257
+ type: 'image',
258
+ }),
259
+ );
260
+
261
+ return true;
262
+ }
263
+
264
+ function $onDragover(event: DragEvent): boolean {
265
+ const node = $getImageNodeInSelection();
266
+ if (!node) {
267
+ return false;
268
+ }
269
+ if (!canDropImage(event)) {
270
+ event.preventDefault();
271
+ }
272
+ return true;
273
+ }
274
+
275
+ function $onDrop(event: DragEvent, editor: LexicalEditor): boolean {
276
+ const node = $getImageNodeInSelection();
277
+ if (!node) {
278
+ return false;
279
+ }
280
+ const data = getDragImageData(event);
281
+ if (!data) {
282
+ return false;
283
+ }
284
+ event.preventDefault();
285
+ if (canDropImage(event)) {
286
+ const range = getDragSelection(event);
287
+ node.remove();
288
+ const rangeSelection = $createRangeSelection();
289
+ if (range !== null && range !== undefined) {
290
+ rangeSelection.applyDOMRange(range);
291
+ }
292
+ $setSelection(rangeSelection);
293
+ editor.dispatchCommand(INSERT_INLINE_IMAGE_COMMAND, data);
294
+ }
295
+ return true;
296
+ }
297
+
298
+ function $getImageNodeInSelection(): InlineImageNode | null {
299
+ const selection = $getSelection();
300
+ if (!$isNodeSelection(selection)) {
301
+ return null;
302
+ }
303
+ const nodes = selection.getNodes();
304
+ const node = nodes[0];
305
+ return $isInlineImageNode(node) ? node : null;
306
+ }
307
+
308
+ function getDragImageData(event: DragEvent): null | InsertInlineImagePayload {
309
+ const dragData = event.dataTransfer?.getData('application/x-lexical-drag');
310
+ if (!dragData) {
311
+ return null;
312
+ }
313
+ const {type, data} = JSON.parse(dragData);
314
+ if (type !== 'image') {
315
+ return null;
316
+ }
317
+
318
+ return data;
319
+ }
320
+
321
+ declare global {
322
+ interface DragEvent {
323
+ rangeOffset?: number;
324
+ rangeParent?: Node;
325
+ }
326
+ }
327
+
328
+ function canDropImage(event: DragEvent): boolean {
329
+ const target = event.target;
330
+ return !!(
331
+ isHTMLElement(target) &&
332
+ !target.closest('code, span.editor-image') &&
333
+ isHTMLElement(target.parentElement) &&
334
+ target.parentElement.closest('div.ContentEditable__root')
335
+ );
336
+ }
337
+
338
+ function getDragSelection(event: DragEvent): Range | null | undefined {
339
+ let range;
340
+ const domSelection = getDOMSelectionFromTarget(event.target);
341
+ if (document.caretRangeFromPoint) {
342
+ range = document.caretRangeFromPoint(event.clientX, event.clientY);
343
+ } else if (event.rangeParent && domSelection !== null) {
344
+ domSelection.collapse(event.rangeParent, event.rangeOffset || 0);
345
+ range = domSelection.getRangeAt(0);
346
+ } else {
347
+ throw Error('Cannot get the selection when dragging');
348
+ }
349
+
350
+ return range;
351
+ }
@@ -0,0 +1,238 @@
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 {ElementNode, LexicalCommand, LexicalNode, NodeKey} from 'lexical';
10
+
11
+ import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
12
+ import {
13
+ $findMatchingParent,
14
+ $insertNodeToNearestRoot,
15
+ mergeRegister,
16
+ } from '@lexical/utils';
17
+ import {
18
+ $createParagraphNode,
19
+ $getNodeByKey,
20
+ $getSelection,
21
+ $isRangeSelection,
22
+ COMMAND_PRIORITY_EDITOR,
23
+ COMMAND_PRIORITY_LOW,
24
+ createCommand,
25
+ KEY_ARROW_DOWN_COMMAND,
26
+ KEY_ARROW_LEFT_COMMAND,
27
+ KEY_ARROW_RIGHT_COMMAND,
28
+ KEY_ARROW_UP_COMMAND,
29
+ } from 'lexical';
30
+ import {useEffect} from 'react';
31
+
32
+ import {
33
+ $createLayoutContainerNode,
34
+ $isLayoutContainerNode,
35
+ LayoutContainerNode,
36
+ } from '../../nodes/LayoutNode/LayoutContainerNode';
37
+ import {
38
+ $createLayoutItemNode,
39
+ $isLayoutItemNode,
40
+ LayoutItemNode,
41
+ } from '../../nodes/LayoutNode/LayoutItemNode';
42
+
43
+ export const INSERT_LAYOUT_COMMAND: LexicalCommand<string> =
44
+ createCommand<string>();
45
+
46
+ export const UPDATE_LAYOUT_COMMAND: LexicalCommand<{
47
+ template: string;
48
+ nodeKey: NodeKey;
49
+ }> = createCommand<{template: string; nodeKey: NodeKey}>();
50
+
51
+ export function LayoutPlugin(): null {
52
+ const [editor] = useLexicalComposerContext();
53
+ useEffect(() => {
54
+ if (!editor.hasNodes([LayoutContainerNode, LayoutItemNode])) {
55
+ throw new Error(
56
+ 'LayoutPlugin: LayoutContainerNode, or LayoutItemNode not registered on editor',
57
+ );
58
+ }
59
+
60
+ const $onEscape = (before: boolean) => {
61
+ const selection = $getSelection();
62
+ if (
63
+ $isRangeSelection(selection) &&
64
+ selection.isCollapsed() &&
65
+ selection.anchor.offset === 0
66
+ ) {
67
+ const container = $findMatchingParent(
68
+ selection.anchor.getNode(),
69
+ $isLayoutContainerNode,
70
+ );
71
+
72
+ if ($isLayoutContainerNode(container)) {
73
+ const parent = container.getParent<ElementNode>();
74
+ const child =
75
+ parent &&
76
+ (before
77
+ ? parent.getFirstChild<LexicalNode>()
78
+ : parent?.getLastChild<LexicalNode>());
79
+ const descendant = before
80
+ ? container.getFirstDescendant<LexicalNode>()?.getKey()
81
+ : container.getLastDescendant<LexicalNode>()?.getKey();
82
+
83
+ if (
84
+ parent !== null &&
85
+ child === container &&
86
+ selection.anchor.key === descendant
87
+ ) {
88
+ if (before) {
89
+ container.insertBefore($createParagraphNode());
90
+ } else {
91
+ container.insertAfter($createParagraphNode());
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ return false;
98
+ };
99
+
100
+ const $fillLayoutItemIfEmpty = (node: LayoutItemNode) => {
101
+ if (node.isEmpty()) {
102
+ node.append($createParagraphNode());
103
+ }
104
+ };
105
+
106
+ const $removeIsolatedLayoutItem = (node: LayoutItemNode): boolean => {
107
+ const parent = node.getParent<ElementNode>();
108
+ if (!$isLayoutContainerNode(parent)) {
109
+ const children = node.getChildren<LexicalNode>();
110
+ for (const child of children) {
111
+ node.insertBefore(child);
112
+ }
113
+ node.remove();
114
+ return true;
115
+ }
116
+ return false;
117
+ };
118
+
119
+ return mergeRegister(
120
+ // When layout is the last child pressing down/right arrow will insert paragraph
121
+ // below it to allow adding more content. It's similar what $insertBlockNode
122
+ // (mainly for decorators), except it'll always be possible to continue adding
123
+ // new content even if trailing paragraph is accidentally deleted
124
+ editor.registerCommand(
125
+ KEY_ARROW_DOWN_COMMAND,
126
+ () => $onEscape(false),
127
+ COMMAND_PRIORITY_LOW,
128
+ ),
129
+ editor.registerCommand(
130
+ KEY_ARROW_RIGHT_COMMAND,
131
+ () => $onEscape(false),
132
+ COMMAND_PRIORITY_LOW,
133
+ ),
134
+ // When layout is the first child pressing up/left arrow will insert paragraph
135
+ // above it to allow adding more content. It's similar what $insertBlockNode
136
+ // (mainly for decorators), except it'll always be possible to continue adding
137
+ // new content even if leading paragraph is accidentally deleted
138
+ editor.registerCommand(
139
+ KEY_ARROW_UP_COMMAND,
140
+ () => $onEscape(true),
141
+ COMMAND_PRIORITY_LOW,
142
+ ),
143
+ editor.registerCommand(
144
+ KEY_ARROW_LEFT_COMMAND,
145
+ () => $onEscape(true),
146
+ COMMAND_PRIORITY_LOW,
147
+ ),
148
+ editor.registerCommand(
149
+ INSERT_LAYOUT_COMMAND,
150
+ (template) => {
151
+ editor.update(() => {
152
+ const container = $createLayoutContainerNode(template);
153
+ const itemsCount = getItemsCountFromTemplate(template);
154
+
155
+ for (let i = 0; i < itemsCount; i++) {
156
+ container.append(
157
+ $createLayoutItemNode().append($createParagraphNode()),
158
+ );
159
+ }
160
+
161
+ $insertNodeToNearestRoot(container);
162
+ container.selectStart();
163
+ });
164
+
165
+ return true;
166
+ },
167
+ COMMAND_PRIORITY_EDITOR,
168
+ ),
169
+ editor.registerCommand(
170
+ UPDATE_LAYOUT_COMMAND,
171
+ ({template, nodeKey}) => {
172
+ editor.update(() => {
173
+ const container = $getNodeByKey<LexicalNode>(nodeKey);
174
+
175
+ if (!$isLayoutContainerNode(container)) {
176
+ return;
177
+ }
178
+
179
+ const itemsCount = getItemsCountFromTemplate(template);
180
+ const prevItemsCount = getItemsCountFromTemplate(
181
+ container.getTemplateColumns(),
182
+ );
183
+
184
+ // Add or remove extra columns if new template does not match existing one
185
+ if (itemsCount > prevItemsCount) {
186
+ for (let i = prevItemsCount; i < itemsCount; i++) {
187
+ container.append(
188
+ $createLayoutItemNode().append($createParagraphNode()),
189
+ );
190
+ }
191
+ } else if (itemsCount < prevItemsCount) {
192
+ for (let i = prevItemsCount - 1; i >= itemsCount; i--) {
193
+ const layoutItem = container.getChildAtIndex<LexicalNode>(i);
194
+
195
+ if ($isLayoutItemNode(layoutItem)) {
196
+ layoutItem.remove();
197
+ }
198
+ }
199
+ }
200
+
201
+ container.setTemplateColumns(template);
202
+ });
203
+
204
+ return true;
205
+ },
206
+ COMMAND_PRIORITY_EDITOR,
207
+ ),
208
+
209
+ editor.registerNodeTransform(LayoutItemNode, (node) => {
210
+ // Structure enforcing transformers for each node type. In case nesting structure is not
211
+ // "Container > Item" it'll unwrap nodes and convert it back
212
+ // to regular content.
213
+ const isRemoved = $removeIsolatedLayoutItem(node);
214
+
215
+ if (!isRemoved) {
216
+ // Layout item should always have a child. this function will listen
217
+ // for any empty layout item and fill it with a paragraph node
218
+ $fillLayoutItemIfEmpty(node);
219
+ }
220
+ }),
221
+ editor.registerNodeTransform(LayoutContainerNode, (node) => {
222
+ const children = node.getChildren<LexicalNode>();
223
+ if (!children.every($isLayoutItemNode)) {
224
+ for (const child of children) {
225
+ node.insertBefore(child);
226
+ }
227
+ node.remove();
228
+ }
229
+ }),
230
+ );
231
+ }, [editor]);
232
+
233
+ return null;
234
+ }
235
+
236
+ function getItemsCountFromTemplate(template: string): number {
237
+ return template.trim().split(/\s+/).length;
238
+ }
@@ -0,0 +1,36 @@
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 {JSX} from 'react';
10
+
11
+ import {LinkPlugin as LexicalLinkPlugin} from '@lexical/react/LexicalLinkPlugin';
12
+ import * as React from 'react';
13
+
14
+ import {validateUrl} from '../../utils/url';
15
+
16
+ type Props = {
17
+ hasLinkAttributes?: boolean;
18
+ };
19
+
20
+ export default function LinkPlugin({
21
+ hasLinkAttributes = false,
22
+ }: Props): JSX.Element {
23
+ return (
24
+ <LexicalLinkPlugin
25
+ validateUrl={validateUrl}
26
+ attributes={
27
+ hasLinkAttributes
28
+ ? {
29
+ rel: 'noopener noreferrer',
30
+ target: '_blank',
31
+ }
32
+ : undefined
33
+ }
34
+ />
35
+ );
36
+ }