@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.
- package/README.md +71 -0
- package/components.json +21 -0
- package/eslint.config.mjs +16 -0
- package/next.config.ts +12 -0
- package/package.json +108 -0
- package/postcss.config.mjs +5 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/images/icons/plus.svg +10 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/actions.ts +2 -0
- package/src/app/api/ai/route.ts +175 -0
- package/src/app/api/edgestore/[...edgestore]/route.ts +28 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +205 -0
- package/src/app/layout.tsx +38 -0
- package/src/app/page.tsx +12 -0
- package/src/components/editor/Core.tsx +220 -0
- package/src/components/editor/hooks/instructions-messages.ts +300 -0
- package/src/components/editor/hooks/use-mobile.ts +19 -0
- package/src/components/editor/hooks/useReport.ts +67 -0
- package/src/components/editor/hooks/useResizeObservert.ts +22 -0
- package/src/components/editor/index.tsx +39 -0
- package/src/components/editor/lexical-on-change.tsx +28 -0
- package/src/components/editor/nodes/CollapsibleNode/CollapsibleContainerNode.ts +92 -0
- package/src/components/editor/nodes/CollapsibleNode/CollapsibleContentNode.ts +65 -0
- package/src/components/editor/nodes/CollapsibleNode/CollapsibleTitleNode.ts +105 -0
- package/src/components/editor/nodes/EquationNode/EquationComponent.tsx +143 -0
- package/src/components/editor/nodes/EquationNode/EquationNode.tsx +170 -0
- package/src/components/editor/nodes/ExcalidrawNode/ExcalidrawComponent.tsx +228 -0
- package/src/components/editor/nodes/ExcalidrawNode/ExcalidrawImage.tsx +137 -0
- package/src/components/editor/nodes/ExcalidrawNode/ImageResizer.tsx +317 -0
- package/src/components/editor/nodes/ExcalidrawNode/index.tsx +204 -0
- package/src/components/editor/nodes/FigmaNode/FigmaNode.tsx +134 -0
- package/src/components/editor/nodes/Hint/HintComponet.tsx +221 -0
- package/src/components/editor/nodes/Hint/index.tsx +190 -0
- package/src/components/editor/nodes/ImageNode/index.tsx +328 -0
- package/src/components/editor/nodes/InlineImageNode/InlineImageComponent.tsx +383 -0
- package/src/components/editor/nodes/InlineImageNode/InlineImageNode.css +94 -0
- package/src/components/editor/nodes/InlineImageNode/InlineImageNode.tsx +309 -0
- package/src/components/editor/nodes/LayoutNode/LayoutContainerNode.ts +146 -0
- package/src/components/editor/nodes/LayoutNode/LayoutItemNode.ts +79 -0
- package/src/components/editor/nodes/PollNode/index.tsx +204 -0
- package/src/components/editor/nodes/Stepper/index.tsx +260 -0
- package/src/components/editor/nodes/TweetNode/index.tsx +214 -0
- package/src/components/editor/nodes/index.ts +81 -0
- package/src/components/editor/plugins/AutoEmbedPlugin/index.tsx +350 -0
- package/src/components/editor/plugins/AutoLinkPlugin/index.tsx +56 -0
- package/src/components/editor/plugins/CodeActionMenuPlugin/components/CopyButton.tsx +70 -0
- package/src/components/editor/plugins/CodeActionMenuPlugin/components/PrettierButton.tsx +192 -0
- package/src/components/editor/plugins/CodeActionMenuPlugin/index.tsx +217 -0
- package/src/components/editor/plugins/CodeActionMenuPlugin/utils.ts +26 -0
- package/src/components/editor/plugins/CodeHighlightPlugin/index.ts +21 -0
- package/src/components/editor/plugins/CollapsiblePlugin/Collapsible.css +76 -0
- package/src/components/editor/plugins/CollapsiblePlugin/index.ts +228 -0
- package/src/components/editor/plugins/DragDropPastePlugin/index.tsx +44 -0
- package/src/components/editor/plugins/DraggableBlockPlugin/index.tsx +52 -0
- package/src/components/editor/plugins/EquationsPlugin/index.tsx +85 -0
- package/src/components/editor/plugins/ExcalidrawPlugin/index.tsx +98 -0
- package/src/components/editor/plugins/FigmaPlugin/index.tsx +42 -0
- package/src/components/editor/plugins/FloatingLinkEditorPlugin/index.tsx +445 -0
- package/src/components/editor/plugins/FloatingTextFormatToolbarPlugin/index.tsx +275 -0
- package/src/components/editor/plugins/ImagesPlugin/index.tsx +222 -0
- package/src/components/editor/plugins/InlineImagePlugin/index.tsx +351 -0
- package/src/components/editor/plugins/LayoutPlugin/index.tsx +238 -0
- package/src/components/editor/plugins/LinkPlugin/index.tsx +36 -0
- package/src/components/editor/plugins/LinkWithMetaData/index.tsx +271 -0
- package/src/components/editor/plugins/MarkdownShortcutPlugin/index.tsx +11 -0
- package/src/components/editor/plugins/MarkdownTransformers/index.tsx +304 -0
- package/src/components/editor/plugins/PollPlugin/index.tsx +49 -0
- package/src/components/editor/plugins/ShortcutsPlugin/index.tsx +180 -0
- package/src/components/editor/plugins/ShortcutsPlugin/shortcuts.ts +253 -0
- package/src/components/editor/plugins/SlashCommand/index.tsx +621 -0
- package/src/components/editor/plugins/SpeechToTextPlugin/index.ts +127 -0
- package/src/components/editor/plugins/TabFocusPlugin/index.ts +58 -0
- package/src/components/editor/plugins/TableCellActionMenuPlugin/index.tsx +759 -0
- package/src/components/editor/plugins/TableCellResizer/index.tsx +438 -0
- package/src/components/editor/plugins/TableHoverActionsPlugin/index.tsx +314 -0
- package/src/components/editor/plugins/TablePlugin/index.tsx +99 -0
- package/src/components/editor/plugins/ToolbarPlugin/index.tsx +522 -0
- package/src/components/editor/plugins/TwitterPlugin/index.ts +35 -0
- package/src/components/editor/plugins/YouTubeNode/index.tsx +179 -0
- package/src/components/editor/plugins/YouTubePlugin/index.ts +41 -0
- package/src/components/editor/themes/editor-theme.ts +113 -0
- package/src/components/editor/themes/theme.css +377 -0
- package/src/components/editor/utils/ai.ts +291 -0
- package/src/components/editor/utils/canUseDOM.ts +12 -0
- package/src/components/editor/utils/editorFormatting.ts +282 -0
- package/src/components/editor/utils/environment.ts +50 -0
- package/src/components/editor/utils/extract-data.ts +166 -0
- package/src/components/editor/utils/getAllLexicalChildren.ts +13 -0
- package/src/components/editor/utils/getDOMRangeRect.ts +27 -0
- package/src/components/editor/utils/getSelectedNode.ts +27 -0
- package/src/components/editor/utils/gif.ts +29 -0
- package/src/components/editor/utils/invariant.ts +15 -0
- package/src/components/editor/utils/setFloatingElemPosition.ts +51 -0
- package/src/components/editor/utils/setFloatingElemPositionForLinkEditor.ts +40 -0
- package/src/components/editor/utils/setNodePlaceholderFromSelection/getNodePlaceholder.ts +51 -0
- package/src/components/editor/utils/setNodePlaceholderFromSelection/setNodePlaceholderFromSelection.ts +15 -0
- package/src/components/editor/utils/setNodePlaceholderFromSelection/setPlaceholderOnSelection.ts +114 -0
- package/src/components/editor/utils/setNodePlaceholderFromSelection/styles.css +6 -0
- package/src/components/editor/utils/url.ts +109 -0
- package/src/components/editor/utils/useLayoutEffect.ts +13 -0
- package/src/components/providers/QueryProvider.tsx +15 -0
- package/src/components/providers/SharedHistoryContext.tsx +28 -0
- package/src/components/providers/ToolbarContext.tsx +123 -0
- package/src/components/providers/theme-provider.tsx +11 -0
- package/src/components/theme/ModeToggle.tsx +40 -0
- package/src/components/ui/FileInput.tsx +40 -0
- package/src/components/ui/Input.css +32 -0
- package/src/components/ui/Select.css +42 -0
- package/src/components/ui/Select.tsx +36 -0
- package/src/components/ui/TextInput.tsx +48 -0
- package/src/components/ui/ai/ai-button.tsx +574 -0
- package/src/components/ui/ai/border.tsx +99 -0
- package/src/components/ui/ai/placeholder-input-vanish.tsx +282 -0
- package/src/components/ui/button.tsx +89 -0
- package/src/components/ui/card.tsx +76 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/command.tsx +153 -0
- package/src/components/ui/dialog/Dialog.css +25 -0
- package/src/components/ui/dialog/Dialog.tsx +34 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/drop-downs/background-color.tsx +183 -0
- package/src/components/ui/drop-downs/block-format.tsx +159 -0
- package/src/components/ui/drop-downs/code.tsx +42 -0
- package/src/components/ui/drop-downs/color.tsx +177 -0
- package/src/components/ui/drop-downs/font-size.tsx +138 -0
- package/src/components/ui/drop-downs/font.tsx +155 -0
- package/src/components/ui/drop-downs/index.tsx +122 -0
- package/src/components/ui/drop-downs/insert-node.tsx +213 -0
- package/src/components/ui/drop-downs/text-align.tsx +123 -0
- package/src/components/ui/drop-downs/text-format.tsx +104 -0
- package/src/components/ui/dropdown-menu.tsx +201 -0
- package/src/components/ui/equation/EquationEditor.css +38 -0
- package/src/components/ui/equation/EquationEditor.tsx +56 -0
- package/src/components/ui/equation/KatexEquationAlterer.css +41 -0
- package/src/components/ui/equation/KatexEquationAlterer.tsx +83 -0
- package/src/components/ui/equation/KatexRenderer.tsx +66 -0
- package/src/components/ui/excalidraw/ExcalidrawModal.css +64 -0
- package/src/components/ui/excalidraw/ExcalidrawModal.tsx +234 -0
- package/src/components/ui/excalidraw/Modal.css +62 -0
- package/src/components/ui/excalidraw/Modal.tsx +110 -0
- package/src/components/ui/hover-card.tsx +29 -0
- package/src/components/ui/image/error-image.tsx +17 -0
- package/src/components/ui/image/file-upload.tsx +240 -0
- package/src/components/ui/image/image-resizer.tsx +297 -0
- package/src/components/ui/image/image-toolbar.tsx +264 -0
- package/src/components/ui/image/index.tsx +408 -0
- package/src/components/ui/image/lazy-image.tsx +68 -0
- package/src/components/ui/image/lazy-video.tsx +71 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/models/custom-dialog.tsx +320 -0
- package/src/components/ui/models/insert-gif.tsx +90 -0
- package/src/components/ui/models/insert-image.tsx +52 -0
- package/src/components/ui/models/insert-poll.tsx +29 -0
- package/src/components/ui/models/insert-table.tsx +62 -0
- package/src/components/ui/models/use-model.tsx +91 -0
- package/src/components/ui/poll/poll-component.tsx +304 -0
- package/src/components/ui/popover.tsx +33 -0
- package/src/components/ui/progress.tsx +28 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/sonner.tsx +31 -0
- package/src/components/ui/stepper/step.tsx +179 -0
- package/src/components/ui/stepper/stepper.tsx +89 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/toggle.tsx +71 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/components/ui/write/text-format-floting-toolbar.tsx +346 -0
- package/src/lib/edgestore.ts +9 -0
- package/src/lib/pinecone-client.ts +0 -0
- package/src/lib/utils.ts +6 -0
- package/src/utils/docSerialization.ts +77 -0
- package/src/utils/emoji-list.ts +16615 -0
- package/src/utils/getDOMRangeRect.ts +27 -0
- package/src/utils/getSelectedNode.ts +27 -0
- package/src/utils/getThemeSelector.ts +25 -0
- package/src/utils/isMobileWidth.ts +7 -0
- package/src/utils/joinClasses.ts +13 -0
- package/src/utils/setFloatingElemPosition.ts +74 -0
- package/src/utils/setFloatingElemPositionForLinkEditor.ts +46 -0
- package/src/utils/swipe.ts +127 -0
- package/src/utils/url.ts +38 -0
- 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
|
+
}
|