@qwanyx/ai-editor 1.0.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/dist/components/AIEditor.d.ts +15 -0
- package/dist/components/AIEditor.d.ts.map +1 -0
- package/dist/components/AIEditor.js +154 -0
- package/dist/components/AIToolbar.d.ts +22 -0
- package/dist/components/AIToolbar.d.ts.map +1 -0
- package/dist/components/AIToolbar.js +10 -0
- package/dist/components/EditorToolbar.d.ts +8 -0
- package/dist/components/EditorToolbar.d.ts.map +1 -0
- package/dist/components/EditorToolbar.js +241 -0
- package/dist/components/MarkdownPreview.d.ts +7 -0
- package/dist/components/MarkdownPreview.d.ts.map +1 -0
- package/dist/components/MarkdownPreview.js +40 -0
- package/dist/components/PromptModal.d.ts +9 -0
- package/dist/components/PromptModal.d.ts.map +1 -0
- package/dist/components/PromptModal.js +38 -0
- package/dist/components/RichTextEditor.d.ts +15 -0
- package/dist/components/RichTextEditor.d.ts.map +1 -0
- package/dist/components/RichTextEditor.js +253 -0
- package/dist/hooks/useAIEditor.d.ts +15 -0
- package/dist/hooks/useAIEditor.d.ts.map +1 -0
- package/dist/hooks/useAIEditor.js +46 -0
- package/dist/hooks/useSelection.d.ts +12 -0
- package/dist/hooks/useSelection.d.ts.map +1 -0
- package/dist/hooks/useSelection.js +45 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/nodes/ImageLinkNode.d.ts +48 -0
- package/dist/nodes/ImageLinkNode.d.ts.map +1 -0
- package/dist/nodes/ImageLinkNode.js +157 -0
- package/dist/nodes/ImageNode.d.ts +62 -0
- package/dist/nodes/ImageNode.d.ts.map +1 -0
- package/dist/nodes/ImageNode.js +487 -0
- package/dist/nodes/LinkNode.d.ts +33 -0
- package/dist/nodes/LinkNode.d.ts.map +1 -0
- package/dist/nodes/LinkNode.js +108 -0
- package/dist/plugins/ImageLinkPlugin.d.ts +2 -0
- package/dist/plugins/ImageLinkPlugin.d.ts.map +1 -0
- package/dist/plugins/ImageLinkPlugin.js +112 -0
- package/dist/plugins/InsertObjectPlugin.d.ts +4 -0
- package/dist/plugins/InsertObjectPlugin.d.ts.map +1 -0
- package/dist/plugins/InsertObjectPlugin.js +464 -0
- package/dist/plugins/LinkPlugin.d.ts +2 -0
- package/dist/plugins/LinkPlugin.d.ts.map +1 -0
- package/dist/plugins/LinkPlugin.js +45 -0
- package/package.json +45 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface RichTextEditorProps {
|
|
2
|
+
initialContent?: string;
|
|
3
|
+
onChange?: (html: string, json: string) => void;
|
|
4
|
+
onAIRequest?: (params: {
|
|
5
|
+
fullText: string;
|
|
6
|
+
selectedText?: string;
|
|
7
|
+
action: 'rewrite' | 'proofread' | 'custom';
|
|
8
|
+
customPrompt?: string;
|
|
9
|
+
}) => Promise<string>;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
minHeight?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function RichTextEditor({ initialContent, onChange, onAIRequest, placeholder, className, minHeight }: RichTextEditorProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
//# sourceMappingURL=RichTextEditor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RichTextEditor.d.ts","sourceRoot":"","sources":["../../src/components/RichTextEditor.tsx"],"names":[],"mappings":"AAiCA,MAAM,WAAW,mBAAmB;IAClC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/C,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE;QACrB,QAAQ,EAAE,MAAM,CAAA;QAChB,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAA;QAC1C,YAAY,CAAC,EAAE,MAAM,CAAA;KACtB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAqKD,wBAAgB,cAAc,CAAC,EAC7B,cAAmB,EACnB,QAAQ,EACR,WAAW,EACX,WAAqC,EACrC,SAAc,EACd,SAAmB,EACpB,EAAE,mBAAmB,2CAyIrB"}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import React, { useCallback, useEffect } from 'react';
|
|
4
|
+
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
|
5
|
+
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
|
6
|
+
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
|
7
|
+
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
|
|
8
|
+
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
|
|
9
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
10
|
+
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
|
11
|
+
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
|
|
12
|
+
import { ListNode, ListItemNode } from '@lexical/list';
|
|
13
|
+
import { LinkNode } from '@lexical/link';
|
|
14
|
+
import { ImageNode } from '../nodes/ImageNode';
|
|
15
|
+
import { ImageLinkNode } from '../nodes/ImageLinkNode';
|
|
16
|
+
import { SimpleLinkNode } from '../nodes/LinkNode';
|
|
17
|
+
import { InsertObjectPlugin } from '../plugins/InsertObjectPlugin';
|
|
18
|
+
import { ImageLinkPlugin } from '../plugins/ImageLinkPlugin';
|
|
19
|
+
import { SimpleLinkPlugin } from '../plugins/LinkPlugin';
|
|
20
|
+
import { ListPlugin } from '@lexical/react/LexicalListPlugin';
|
|
21
|
+
import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin';
|
|
22
|
+
import { $getRoot, $getSelection, $isRangeSelection, $createParagraphNode, $createTextNode } from 'lexical';
|
|
23
|
+
import { EditorToolbar } from './EditorToolbar';
|
|
24
|
+
// Theme for Lexical editor
|
|
25
|
+
const editorTheme = {
|
|
26
|
+
paragraph: 'editor-paragraph',
|
|
27
|
+
heading: {
|
|
28
|
+
h1: 'editor-heading-h1',
|
|
29
|
+
h2: 'editor-heading-h2',
|
|
30
|
+
h3: 'editor-heading-h3',
|
|
31
|
+
},
|
|
32
|
+
text: {
|
|
33
|
+
bold: 'editor-text-bold',
|
|
34
|
+
italic: 'editor-text-italic',
|
|
35
|
+
underline: 'editor-text-underline',
|
|
36
|
+
strikethrough: 'editor-text-strikethrough',
|
|
37
|
+
},
|
|
38
|
+
list: {
|
|
39
|
+
ul: 'editor-list-ul',
|
|
40
|
+
ol: 'editor-list-ol',
|
|
41
|
+
listitem: 'editor-listitem',
|
|
42
|
+
nested: {
|
|
43
|
+
listitem: 'editor-nested-listitem',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
quote: 'editor-quote',
|
|
47
|
+
link: 'editor-link',
|
|
48
|
+
};
|
|
49
|
+
// CSS styles for the editor (injected as style tag)
|
|
50
|
+
const editorStyles = `
|
|
51
|
+
.editor-paragraph {
|
|
52
|
+
margin-bottom: 8px;
|
|
53
|
+
}
|
|
54
|
+
.editor-heading-h1 {
|
|
55
|
+
font-size: 1.875rem;
|
|
56
|
+
font-weight: bold;
|
|
57
|
+
margin-bottom: 16px;
|
|
58
|
+
margin-top: 24px;
|
|
59
|
+
}
|
|
60
|
+
.editor-heading-h2 {
|
|
61
|
+
font-size: 1.5rem;
|
|
62
|
+
font-weight: bold;
|
|
63
|
+
margin-bottom: 12px;
|
|
64
|
+
margin-top: 20px;
|
|
65
|
+
}
|
|
66
|
+
.editor-heading-h3 {
|
|
67
|
+
font-size: 1.25rem;
|
|
68
|
+
font-weight: 600;
|
|
69
|
+
margin-bottom: 8px;
|
|
70
|
+
margin-top: 16px;
|
|
71
|
+
}
|
|
72
|
+
.editor-text-bold {
|
|
73
|
+
font-weight: bold;
|
|
74
|
+
}
|
|
75
|
+
.editor-text-italic {
|
|
76
|
+
font-style: italic;
|
|
77
|
+
}
|
|
78
|
+
.editor-text-underline {
|
|
79
|
+
text-decoration: underline;
|
|
80
|
+
}
|
|
81
|
+
.editor-text-strikethrough {
|
|
82
|
+
text-decoration: line-through;
|
|
83
|
+
}
|
|
84
|
+
.editor-list-ul {
|
|
85
|
+
list-style-type: disc;
|
|
86
|
+
margin-left: 24px;
|
|
87
|
+
margin-bottom: 8px;
|
|
88
|
+
padding-left: 0;
|
|
89
|
+
}
|
|
90
|
+
.editor-list-ol {
|
|
91
|
+
list-style-type: decimal;
|
|
92
|
+
margin-left: 24px;
|
|
93
|
+
margin-bottom: 8px;
|
|
94
|
+
padding-left: 0;
|
|
95
|
+
}
|
|
96
|
+
.editor-listitem {
|
|
97
|
+
margin-bottom: 4px;
|
|
98
|
+
}
|
|
99
|
+
.editor-nested-listitem {
|
|
100
|
+
list-style-type: none;
|
|
101
|
+
}
|
|
102
|
+
.editor-quote {
|
|
103
|
+
border-left: 4px solid #d1d5db;
|
|
104
|
+
padding-left: 16px;
|
|
105
|
+
font-style: italic;
|
|
106
|
+
color: #6b7280;
|
|
107
|
+
margin: 16px 0;
|
|
108
|
+
}
|
|
109
|
+
.editor-link {
|
|
110
|
+
color: #2563eb;
|
|
111
|
+
text-decoration: underline;
|
|
112
|
+
}
|
|
113
|
+
.editor-link:hover {
|
|
114
|
+
color: #1e40af;
|
|
115
|
+
}
|
|
116
|
+
`;
|
|
117
|
+
// Initial editor config
|
|
118
|
+
function getEditorConfig(initialContent) {
|
|
119
|
+
return {
|
|
120
|
+
namespace: 'RichTextEditor',
|
|
121
|
+
theme: editorTheme,
|
|
122
|
+
onError: (error) => {
|
|
123
|
+
console.error('Lexical error:', error);
|
|
124
|
+
},
|
|
125
|
+
nodes: [
|
|
126
|
+
HeadingNode,
|
|
127
|
+
QuoteNode,
|
|
128
|
+
ListNode,
|
|
129
|
+
ListItemNode,
|
|
130
|
+
LinkNode,
|
|
131
|
+
ImageNode,
|
|
132
|
+
ImageLinkNode,
|
|
133
|
+
SimpleLinkNode,
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// Plugin to set initial content (supports both JSON and plain text)
|
|
138
|
+
function InitialContentPlugin({ content }) {
|
|
139
|
+
const [editor] = useLexicalComposerContext();
|
|
140
|
+
const isFirstRender = React.useRef(true);
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (isFirstRender.current && content) {
|
|
143
|
+
isFirstRender.current = false;
|
|
144
|
+
// Try to parse as JSON (Lexical state)
|
|
145
|
+
try {
|
|
146
|
+
const parsed = JSON.parse(content);
|
|
147
|
+
if (parsed.root) {
|
|
148
|
+
// It's a Lexical state JSON
|
|
149
|
+
const editorState = editor.parseEditorState(parsed);
|
|
150
|
+
editor.setEditorState(editorState);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Not JSON, treat as plain text
|
|
156
|
+
}
|
|
157
|
+
// Fallback: plain text
|
|
158
|
+
editor.update(() => {
|
|
159
|
+
const root = $getRoot();
|
|
160
|
+
root.clear();
|
|
161
|
+
const paragraph = $createParagraphNode();
|
|
162
|
+
paragraph.append($createTextNode(content));
|
|
163
|
+
root.append(paragraph);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}, [editor, content]);
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
// Plugin to get editor reference
|
|
170
|
+
function EditorRefPlugin({ editorRef }) {
|
|
171
|
+
const [editor] = useLexicalComposerContext();
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
editorRef.current = editor;
|
|
174
|
+
}, [editor, editorRef]);
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
export function RichTextEditor({ initialContent = '', onChange, onAIRequest, placeholder = 'Commencez à écrire...', className = '', minHeight = '300px' }) {
|
|
178
|
+
const editorRef = React.useRef(null);
|
|
179
|
+
const [isLoading, setIsLoading] = React.useState(false);
|
|
180
|
+
const [isFullscreen, setIsFullscreen] = React.useState(false);
|
|
181
|
+
const toggleFullscreen = useCallback(() => {
|
|
182
|
+
setIsFullscreen(prev => !prev);
|
|
183
|
+
}, []);
|
|
184
|
+
// Handle editor changes
|
|
185
|
+
const handleChange = useCallback((editorState, editor) => {
|
|
186
|
+
editorState.read(() => {
|
|
187
|
+
// Get HTML
|
|
188
|
+
const root = $getRoot();
|
|
189
|
+
const textContent = root.getTextContent();
|
|
190
|
+
// Get JSON
|
|
191
|
+
const json = JSON.stringify(editorState.toJSON());
|
|
192
|
+
// For now, pass text content as "html" - we can enhance this later
|
|
193
|
+
onChange?.(textContent, json);
|
|
194
|
+
});
|
|
195
|
+
}, [onChange]);
|
|
196
|
+
// Get selected text for AI operations
|
|
197
|
+
const getSelectedText = useCallback(() => {
|
|
198
|
+
if (!editorRef.current)
|
|
199
|
+
return undefined;
|
|
200
|
+
let selectedText;
|
|
201
|
+
editorRef.current.getEditorState().read(() => {
|
|
202
|
+
const selection = $getSelection();
|
|
203
|
+
if ($isRangeSelection(selection)) {
|
|
204
|
+
selectedText = selection.getTextContent();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
return selectedText || undefined;
|
|
208
|
+
}, []);
|
|
209
|
+
// Get full text for AI operations
|
|
210
|
+
const getFullText = useCallback(() => {
|
|
211
|
+
if (!editorRef.current)
|
|
212
|
+
return '';
|
|
213
|
+
let fullText = '';
|
|
214
|
+
editorRef.current.getEditorState().read(() => {
|
|
215
|
+
fullText = $getRoot().getTextContent();
|
|
216
|
+
});
|
|
217
|
+
return fullText;
|
|
218
|
+
}, []);
|
|
219
|
+
// Handle AI actions
|
|
220
|
+
const handleAIAction = useCallback(async (action, customPrompt) => {
|
|
221
|
+
if (!onAIRequest || !editorRef.current)
|
|
222
|
+
return;
|
|
223
|
+
setIsLoading(true);
|
|
224
|
+
try {
|
|
225
|
+
const selectedText = getSelectedText();
|
|
226
|
+
const fullText = getFullText();
|
|
227
|
+
const result = await onAIRequest({
|
|
228
|
+
fullText,
|
|
229
|
+
selectedText,
|
|
230
|
+
action,
|
|
231
|
+
customPrompt
|
|
232
|
+
});
|
|
233
|
+
// Replace content with AI result
|
|
234
|
+
editorRef.current.update(() => {
|
|
235
|
+
const root = $getRoot();
|
|
236
|
+
root.clear();
|
|
237
|
+
const paragraph = $createParagraphNode();
|
|
238
|
+
paragraph.append($createTextNode(result));
|
|
239
|
+
root.append(paragraph);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
console.error('AI request failed:', error);
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
setIsLoading(false);
|
|
247
|
+
}
|
|
248
|
+
}, [onAIRequest, getSelectedText, getFullText]);
|
|
249
|
+
const fullscreenClasses = isFullscreen
|
|
250
|
+
? 'fixed inset-0 z-50'
|
|
251
|
+
: '';
|
|
252
|
+
return (_jsxs("div", { className: `border border-gray-200 rounded-lg bg-white flex flex-col ${fullscreenClasses} ${className}`, children: [_jsx("style", { dangerouslySetInnerHTML: { __html: editorStyles } }), _jsxs(LexicalComposer, { initialConfig: getEditorConfig(initialContent), children: [_jsx(EditorToolbar, { onAIAction: handleAIAction, isLoading: isLoading, isFullscreen: isFullscreen, onToggleFullscreen: toggleFullscreen }), _jsxs("div", { className: "relative flex-1 overflow-auto", style: minHeight && !isFullscreen ? { minHeight } : {}, children: [_jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: "outline-none p-4 text-gray-900 h-full min-h-full" }), placeholder: _jsx("div", { className: "absolute top-4 left-4 text-gray-400 pointer-events-none", children: placeholder }), ErrorBoundary: LexicalErrorBoundary }), _jsx(HistoryPlugin, {}), _jsx(ListPlugin, {}), _jsx(LinkPlugin, {}), _jsx(OnChangePlugin, { onChange: handleChange }), _jsx(InitialContentPlugin, { content: initialContent }), _jsx(EditorRefPlugin, { editorRef: editorRef }), _jsx(InsertObjectPlugin, {}), _jsx(ImageLinkPlugin, {}), _jsx(SimpleLinkPlugin, {}), isLoading && (_jsx("div", { className: "absolute inset-0 bg-white/80 flex items-center justify-center z-10", children: _jsxs("div", { className: "flex items-center gap-2 text-blue-600", children: [_jsx("span", { className: "material-icons animate-spin", children: "sync" }), _jsx("span", { children: "L'IA travaille..." })] }) }))] })] })] }));
|
|
253
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface AIEditorState {
|
|
2
|
+
content: string;
|
|
3
|
+
isLoading: boolean;
|
|
4
|
+
error: string | null;
|
|
5
|
+
}
|
|
6
|
+
export interface AIEditorActions {
|
|
7
|
+
setContent: (content: string) => void;
|
|
8
|
+
undo: () => void;
|
|
9
|
+
redo: () => void;
|
|
10
|
+
canUndo: boolean;
|
|
11
|
+
canRedo: boolean;
|
|
12
|
+
applyAIEdit: (newContent: string) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare function useAIEditor(initialContent?: string): [AIEditorState, AIEditorActions];
|
|
15
|
+
//# sourceMappingURL=useAIEditor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAIEditor.d.ts","sourceRoot":"","sources":["../../src/hooks/useAIEditor.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,WAAW,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAA;CAC1C;AAED,wBAAgB,WAAW,CAAC,cAAc,GAAE,MAAW,GAAG,CAAC,aAAa,EAAE,eAAe,CAAC,CAqDzF"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useState, useCallback, useRef } from 'react';
|
|
2
|
+
export function useAIEditor(initialContent = '') {
|
|
3
|
+
const [content, setContentState] = useState(initialContent);
|
|
4
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
5
|
+
const [error, setError] = useState(null);
|
|
6
|
+
// History for undo/redo
|
|
7
|
+
const historyRef = useRef([{ content: initialContent, timestamp: Date.now() }]);
|
|
8
|
+
const historyIndexRef = useRef(0);
|
|
9
|
+
const [canUndo, setCanUndo] = useState(false);
|
|
10
|
+
const [canRedo, setCanRedo] = useState(false);
|
|
11
|
+
const updateUndoRedoState = useCallback(() => {
|
|
12
|
+
setCanUndo(historyIndexRef.current > 0);
|
|
13
|
+
setCanRedo(historyIndexRef.current < historyRef.current.length - 1);
|
|
14
|
+
}, []);
|
|
15
|
+
const setContent = useCallback((newContent) => {
|
|
16
|
+
// Add to history, removing any future states
|
|
17
|
+
historyRef.current = historyRef.current.slice(0, historyIndexRef.current + 1);
|
|
18
|
+
historyRef.current.push({ content: newContent, timestamp: Date.now() });
|
|
19
|
+
historyIndexRef.current = historyRef.current.length - 1;
|
|
20
|
+
setContentState(newContent);
|
|
21
|
+
updateUndoRedoState();
|
|
22
|
+
}, [updateUndoRedoState]);
|
|
23
|
+
const undo = useCallback(() => {
|
|
24
|
+
if (historyIndexRef.current > 0) {
|
|
25
|
+
historyIndexRef.current--;
|
|
26
|
+
setContentState(historyRef.current[historyIndexRef.current].content);
|
|
27
|
+
updateUndoRedoState();
|
|
28
|
+
}
|
|
29
|
+
}, [updateUndoRedoState]);
|
|
30
|
+
const redo = useCallback(() => {
|
|
31
|
+
if (historyIndexRef.current < historyRef.current.length - 1) {
|
|
32
|
+
historyIndexRef.current++;
|
|
33
|
+
setContentState(historyRef.current[historyIndexRef.current].content);
|
|
34
|
+
updateUndoRedoState();
|
|
35
|
+
}
|
|
36
|
+
}, [updateUndoRedoState]);
|
|
37
|
+
const applyAIEdit = useCallback((newContent) => {
|
|
38
|
+
setContent(newContent);
|
|
39
|
+
setIsLoading(false);
|
|
40
|
+
setError(null);
|
|
41
|
+
}, [setContent]);
|
|
42
|
+
return [
|
|
43
|
+
{ content, isLoading, error },
|
|
44
|
+
{ setContent, undo, redo, canUndo, canRedo, applyAIEdit }
|
|
45
|
+
];
|
|
46
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface SelectionInfo {
|
|
2
|
+
text: string;
|
|
3
|
+
startOffset: number;
|
|
4
|
+
endOffset: number;
|
|
5
|
+
originalStart: number;
|
|
6
|
+
originalEnd: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function useSelection(containerRef: React.RefObject<HTMLElement>): {
|
|
9
|
+
selection: SelectionInfo | null;
|
|
10
|
+
clearSelection: () => void;
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=useSelection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSelection.d.ts","sourceRoot":"","sources":["../../src/hooks/useSelection.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IAEjB,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,wBAAgB,YAAY,CAAC,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC;;;EAmDtE"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
2
|
+
export function useSelection(containerRef) {
|
|
3
|
+
const [selection, setSelection] = useState(null);
|
|
4
|
+
const handleSelectionChange = useCallback(() => {
|
|
5
|
+
const sel = window.getSelection();
|
|
6
|
+
if (!sel || sel.isCollapsed || !containerRef.current) {
|
|
7
|
+
setSelection(null);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
// Check if selection is within our container
|
|
11
|
+
const range = sel.getRangeAt(0);
|
|
12
|
+
if (!containerRef.current.contains(range.commonAncestorContainer)) {
|
|
13
|
+
setSelection(null);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const text = sel.toString();
|
|
17
|
+
if (!text.trim()) {
|
|
18
|
+
setSelection(null);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
// Get the text content of the container up to the selection
|
|
22
|
+
const preSelectionRange = document.createRange();
|
|
23
|
+
preSelectionRange.selectNodeContents(containerRef.current);
|
|
24
|
+
preSelectionRange.setEnd(range.startContainer, range.startOffset);
|
|
25
|
+
const startOffset = preSelectionRange.toString().length;
|
|
26
|
+
setSelection({
|
|
27
|
+
text,
|
|
28
|
+
startOffset,
|
|
29
|
+
endOffset: startOffset + text.length,
|
|
30
|
+
originalStart: startOffset,
|
|
31
|
+
originalEnd: startOffset + text.length
|
|
32
|
+
});
|
|
33
|
+
}, [containerRef]);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
document.addEventListener('selectionchange', handleSelectionChange);
|
|
36
|
+
return () => {
|
|
37
|
+
document.removeEventListener('selectionchange', handleSelectionChange);
|
|
38
|
+
};
|
|
39
|
+
}, [handleSelectionChange]);
|
|
40
|
+
const clearSelection = useCallback(() => {
|
|
41
|
+
setSelection(null);
|
|
42
|
+
window.getSelection()?.removeAllRanges();
|
|
43
|
+
}, []);
|
|
44
|
+
return { selection, clearSelection };
|
|
45
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { RichTextEditor } from './components/RichTextEditor';
|
|
2
|
+
export type { RichTextEditorProps } from './components/RichTextEditor';
|
|
3
|
+
export { EditorToolbar } from './components/EditorToolbar';
|
|
4
|
+
export type { EditorToolbarProps } from './components/EditorToolbar';
|
|
5
|
+
export { ImageNode, $createImageNode, $isImageNode } from './nodes/ImageNode';
|
|
6
|
+
export type { ImagePayload, ImageFloatType } from './nodes/ImageNode';
|
|
7
|
+
export { ImageLinkNode, $createImageLinkNode, $isImageLinkNode, $wrapSelectionInImageLink } from './nodes/ImageLinkNode';
|
|
8
|
+
export type { ImageLinkPayload } from './nodes/ImageLinkNode';
|
|
9
|
+
export { SimpleLinkNode, $createSimpleLinkNode, $isSimpleLinkNode, $wrapSelectionInSimpleLink } from './nodes/LinkNode';
|
|
10
|
+
export type { SimpleLinkPayload } from './nodes/LinkNode';
|
|
11
|
+
export { InsertObjectPlugin, INSERT_OBJECT_COMMAND } from './plugins/InsertObjectPlugin';
|
|
12
|
+
export { ImageLinkPlugin } from './plugins/ImageLinkPlugin';
|
|
13
|
+
export { SimpleLinkPlugin } from './plugins/LinkPlugin';
|
|
14
|
+
export { RichTextEditor as AIEditor } from './components/RichTextEditor';
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC5D,YAAY,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AAGtE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAA;AAC1D,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAGpE,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAC7E,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAErE,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAA;AACxH,YAAY,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAE7D,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAA;AACvH,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAGzD,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAA;AACxF,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAGvD,OAAO,EAAE,cAAc,IAAI,QAAQ,EAAE,MAAM,6BAA6B,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Main WYSIWYG editor component
|
|
2
|
+
export { RichTextEditor } from './components/RichTextEditor';
|
|
3
|
+
// Toolbar component (for custom implementations)
|
|
4
|
+
export { EditorToolbar } from './components/EditorToolbar';
|
|
5
|
+
// Custom nodes
|
|
6
|
+
export { ImageNode, $createImageNode, $isImageNode } from './nodes/ImageNode';
|
|
7
|
+
export { ImageLinkNode, $createImageLinkNode, $isImageLinkNode, $wrapSelectionInImageLink } from './nodes/ImageLinkNode';
|
|
8
|
+
export { SimpleLinkNode, $createSimpleLinkNode, $isSimpleLinkNode, $wrapSelectionInSimpleLink } from './nodes/LinkNode';
|
|
9
|
+
// Plugins
|
|
10
|
+
export { InsertObjectPlugin, INSERT_OBJECT_COMMAND } from './plugins/InsertObjectPlugin';
|
|
11
|
+
export { ImageLinkPlugin } from './plugins/ImageLinkPlugin';
|
|
12
|
+
export { SimpleLinkPlugin } from './plugins/LinkPlugin';
|
|
13
|
+
// Alias for backwards compatibility
|
|
14
|
+
export { RichTextEditor as AIEditor } from './components/RichTextEditor';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ElementNode, LexicalNode, NodeKey, RangeSelection, SerializedElementNode, Spread } from 'lexical';
|
|
2
|
+
export interface ImageLinkPayload {
|
|
3
|
+
url: string;
|
|
4
|
+
altText?: string;
|
|
5
|
+
copyright?: string;
|
|
6
|
+
photographer?: string;
|
|
7
|
+
comment?: string;
|
|
8
|
+
}
|
|
9
|
+
export type SerializedImageLinkNode = Spread<{
|
|
10
|
+
url: string;
|
|
11
|
+
altText?: string;
|
|
12
|
+
copyright?: string;
|
|
13
|
+
photographer?: string;
|
|
14
|
+
comment?: string;
|
|
15
|
+
}, SerializedElementNode>;
|
|
16
|
+
export declare class ImageLinkNode extends ElementNode {
|
|
17
|
+
__url: string;
|
|
18
|
+
__altText?: string;
|
|
19
|
+
__copyright?: string;
|
|
20
|
+
__photographer?: string;
|
|
21
|
+
__comment?: string;
|
|
22
|
+
static getType(): string;
|
|
23
|
+
static clone(node: ImageLinkNode): ImageLinkNode;
|
|
24
|
+
constructor(url: string, altText?: string, copyright?: string, photographer?: string, comment?: string, key?: NodeKey);
|
|
25
|
+
createDOM(): HTMLElement;
|
|
26
|
+
updateDOM(): boolean;
|
|
27
|
+
getUrl(): string;
|
|
28
|
+
setUrl(url: string): void;
|
|
29
|
+
getAltText(): string | undefined;
|
|
30
|
+
setAltText(altText: string | undefined): void;
|
|
31
|
+
getCopyright(): string | undefined;
|
|
32
|
+
setCopyright(copyright: string | undefined): void;
|
|
33
|
+
getPhotographer(): string | undefined;
|
|
34
|
+
setPhotographer(photographer: string | undefined): void;
|
|
35
|
+
getComment(): string | undefined;
|
|
36
|
+
setComment(comment: string | undefined): void;
|
|
37
|
+
static importJSON(serializedNode: SerializedImageLinkNode): ImageLinkNode;
|
|
38
|
+
exportJSON(): SerializedImageLinkNode;
|
|
39
|
+
canInsertTextBefore(): boolean;
|
|
40
|
+
canInsertTextAfter(): boolean;
|
|
41
|
+
canBeEmpty(): boolean;
|
|
42
|
+
isInline(): boolean;
|
|
43
|
+
extractWithChild(): boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare function $createImageLinkNode(payload: ImageLinkPayload): ImageLinkNode;
|
|
46
|
+
export declare function $isImageLinkNode(node: LexicalNode | null | undefined): node is ImageLinkNode;
|
|
47
|
+
export declare function $wrapSelectionInImageLink(selection: RangeSelection, url: string, altText?: string, copyright?: string, photographer?: string, comment?: string): void;
|
|
48
|
+
//# sourceMappingURL=ImageLinkNode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageLinkNode.d.ts","sourceRoot":"","sources":["../../src/nodes/ImageLinkNode.tsx"],"names":[],"mappings":"AAEA,OAAO,EAGL,WAAW,EACX,WAAW,EACX,OAAO,EACP,cAAc,EACd,qBAAqB,EACrB,MAAM,EACP,MAAM,SAAS,CAAA;AAGhB,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,MAAM,uBAAuB,GAAG,MAAM,CAC1C;IACE,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,EACD,qBAAqB,CACtB,CAAA;AAoED,qBAAa,aAAc,SAAQ,WAAW;IAC5C,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB,MAAM,CAAC,OAAO,IAAI,MAAM;IAIxB,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,aAAa,GAAG,aAAa;gBAY9C,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,MAAM,EAChB,GAAG,CAAC,EAAE,OAAO;IAUf,SAAS,IAAI,WAAW;IAUxB,SAAS,IAAI,OAAO;IAIpB,MAAM,IAAI,MAAM;IAIhB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAKzB,UAAU,IAAI,MAAM,GAAG,SAAS;IAIhC,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAK7C,YAAY,IAAI,MAAM,GAAG,SAAS;IAIlC,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAKjD,eAAe,IAAI,MAAM,GAAG,SAAS;IAIrC,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAKvD,UAAU,IAAI,MAAM,GAAG,SAAS;IAIhC,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAK7C,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,uBAAuB,GAAG,aAAa;IAczE,UAAU,IAAI,uBAAuB;IAarC,mBAAmB,IAAI,OAAO;IAI9B,kBAAkB,IAAI,OAAO;IAI7B,UAAU,IAAI,OAAO;IAIrB,QAAQ,IAAI,OAAO;IAInB,gBAAgB,IAAI,OAAO;CAG5B;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,gBAAgB,GAAG,aAAa,CAU7E;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,IAAI,aAAa,CAE5F;AAGD,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,cAAc,EACzB,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,MAAM,GACf,IAAI,CAYN"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { $applyNodeReplacement, ElementNode, } from 'lexical';
|
|
4
|
+
// Fullscreen modal component for the image link
|
|
5
|
+
function ImageLinkFullscreen({ url, altText, onClose, }) {
|
|
6
|
+
return (_jsxs("div", { style: {
|
|
7
|
+
position: 'fixed',
|
|
8
|
+
inset: 0,
|
|
9
|
+
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
|
10
|
+
display: 'flex',
|
|
11
|
+
alignItems: 'center',
|
|
12
|
+
justifyContent: 'center',
|
|
13
|
+
zIndex: 100000,
|
|
14
|
+
cursor: 'pointer',
|
|
15
|
+
}, onClick: onClose, children: [_jsx("button", { onClick: onClose, style: {
|
|
16
|
+
position: 'absolute',
|
|
17
|
+
top: '20px',
|
|
18
|
+
right: '20px',
|
|
19
|
+
width: '40px',
|
|
20
|
+
height: '40px',
|
|
21
|
+
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
22
|
+
border: 'none',
|
|
23
|
+
borderRadius: '50%',
|
|
24
|
+
cursor: 'pointer',
|
|
25
|
+
display: 'flex',
|
|
26
|
+
alignItems: 'center',
|
|
27
|
+
justifyContent: 'center',
|
|
28
|
+
transition: 'background-color 0.2s',
|
|
29
|
+
}, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = 'rgba(255, 255, 255, 0.2)', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'rgba(255, 255, 255, 0.1)', title: "Fermer", children: _jsx("span", { className: "material-icons", style: { fontSize: '24px', color: 'white' }, children: "close" }) }), _jsx("img", { src: url, alt: altText || '', style: {
|
|
30
|
+
maxWidth: '95vw',
|
|
31
|
+
maxHeight: '95vh',
|
|
32
|
+
objectFit: 'contain',
|
|
33
|
+
borderRadius: '4px',
|
|
34
|
+
boxShadow: '0 25px 50px rgba(0,0,0,0.5)',
|
|
35
|
+
}, onClick: (e) => e.stopPropagation() })] }));
|
|
36
|
+
}
|
|
37
|
+
export class ImageLinkNode extends ElementNode {
|
|
38
|
+
static getType() {
|
|
39
|
+
return 'image-link';
|
|
40
|
+
}
|
|
41
|
+
static clone(node) {
|
|
42
|
+
return new ImageLinkNode(node.__url, node.__altText, node.__copyright, node.__photographer, node.__comment, node.__key);
|
|
43
|
+
}
|
|
44
|
+
constructor(url, altText, copyright, photographer, comment, key) {
|
|
45
|
+
super(key);
|
|
46
|
+
this.__url = url;
|
|
47
|
+
this.__altText = altText;
|
|
48
|
+
this.__copyright = copyright;
|
|
49
|
+
this.__photographer = photographer;
|
|
50
|
+
this.__comment = comment;
|
|
51
|
+
}
|
|
52
|
+
createDOM() {
|
|
53
|
+
const span = document.createElement('span');
|
|
54
|
+
span.style.color = '#8b5cf6';
|
|
55
|
+
span.style.textDecoration = 'underline';
|
|
56
|
+
span.style.textDecorationStyle = 'dotted';
|
|
57
|
+
span.style.cursor = 'pointer';
|
|
58
|
+
span.title = 'Lien image - Ctrl+Clic pour voir';
|
|
59
|
+
return span;
|
|
60
|
+
}
|
|
61
|
+
updateDOM() {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
getUrl() {
|
|
65
|
+
return this.__url;
|
|
66
|
+
}
|
|
67
|
+
setUrl(url) {
|
|
68
|
+
const writable = this.getWritable();
|
|
69
|
+
writable.__url = url;
|
|
70
|
+
}
|
|
71
|
+
getAltText() {
|
|
72
|
+
return this.__altText;
|
|
73
|
+
}
|
|
74
|
+
setAltText(altText) {
|
|
75
|
+
const writable = this.getWritable();
|
|
76
|
+
writable.__altText = altText;
|
|
77
|
+
}
|
|
78
|
+
getCopyright() {
|
|
79
|
+
return this.__copyright;
|
|
80
|
+
}
|
|
81
|
+
setCopyright(copyright) {
|
|
82
|
+
const writable = this.getWritable();
|
|
83
|
+
writable.__copyright = copyright;
|
|
84
|
+
}
|
|
85
|
+
getPhotographer() {
|
|
86
|
+
return this.__photographer;
|
|
87
|
+
}
|
|
88
|
+
setPhotographer(photographer) {
|
|
89
|
+
const writable = this.getWritable();
|
|
90
|
+
writable.__photographer = photographer;
|
|
91
|
+
}
|
|
92
|
+
getComment() {
|
|
93
|
+
return this.__comment;
|
|
94
|
+
}
|
|
95
|
+
setComment(comment) {
|
|
96
|
+
const writable = this.getWritable();
|
|
97
|
+
writable.__comment = comment;
|
|
98
|
+
}
|
|
99
|
+
static importJSON(serializedNode) {
|
|
100
|
+
const node = $createImageLinkNode({
|
|
101
|
+
url: serializedNode.url,
|
|
102
|
+
altText: serializedNode.altText,
|
|
103
|
+
copyright: serializedNode.copyright,
|
|
104
|
+
photographer: serializedNode.photographer,
|
|
105
|
+
comment: serializedNode.comment,
|
|
106
|
+
});
|
|
107
|
+
node.setFormat(serializedNode.format);
|
|
108
|
+
node.setIndent(serializedNode.indent);
|
|
109
|
+
node.setDirection(serializedNode.direction);
|
|
110
|
+
return node;
|
|
111
|
+
}
|
|
112
|
+
exportJSON() {
|
|
113
|
+
return {
|
|
114
|
+
...super.exportJSON(),
|
|
115
|
+
type: 'image-link',
|
|
116
|
+
url: this.__url,
|
|
117
|
+
altText: this.__altText,
|
|
118
|
+
copyright: this.__copyright,
|
|
119
|
+
photographer: this.__photographer,
|
|
120
|
+
comment: this.__comment,
|
|
121
|
+
version: 1,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
canInsertTextBefore() {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
canInsertTextAfter() {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
canBeEmpty() {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
isInline() {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
extractWithChild() {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export function $createImageLinkNode(payload) {
|
|
141
|
+
return $applyNodeReplacement(new ImageLinkNode(payload.url, payload.altText, payload.copyright, payload.photographer, payload.comment));
|
|
142
|
+
}
|
|
143
|
+
export function $isImageLinkNode(node) {
|
|
144
|
+
return node instanceof ImageLinkNode;
|
|
145
|
+
}
|
|
146
|
+
// Helper to wrap selection in an image link
|
|
147
|
+
export function $wrapSelectionInImageLink(selection, url, altText, copyright, photographer, comment) {
|
|
148
|
+
const nodes = selection.extract();
|
|
149
|
+
const imageLinkNode = $createImageLinkNode({ url, altText, copyright, photographer, comment });
|
|
150
|
+
if (nodes.length > 0) {
|
|
151
|
+
const firstNode = nodes[0];
|
|
152
|
+
firstNode.insertBefore(imageLinkNode);
|
|
153
|
+
for (const node of nodes) {
|
|
154
|
+
imageLinkNode.append(node);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|