@sovann72-dev/lynqify-ui 1.0.0 → 1.0.2
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/NoteEditor/BatchImageGalleryNodeView.d.ts +19 -0
- package/dist/components/RichTextEditor/Extension/Image/ImageResize/constants/index.d.ts +22 -0
- package/dist/components/RichTextEditor/Extension/Image/ImageResize/controllers/image-node-view.d.ts +38 -0
- package/dist/components/RichTextEditor/Extension/Image/ImageResize/controllers/position-controller.d.ts +12 -0
- package/dist/components/RichTextEditor/Extension/Image/ImageResize/controllers/resize-controller.d.ts +13 -0
- package/dist/components/RichTextEditor/Extension/Image/ImageResize/image-resize.d.ts +22 -0
- package/dist/components/RichTextEditor/Extension/Image/ImageResize/index.d.ts +4 -0
- package/dist/components/RichTextEditor/Extension/Image/ImageResize/types/index.d.ts +20 -0
- package/dist/components/RichTextEditor/Extension/Image/ImageResize/utils/attribute-parser.d.ts +4 -0
- package/dist/components/RichTextEditor/Extension/Image/ImageResize/utils/clamp-width.d.ts +6 -0
- package/dist/components/RichTextEditor/Extension/Image/ImageResize/utils/index.d.ts +7 -0
- package/dist/components/RichTextEditor/Extension/Image/ImageResize/utils/resize-handler.d.ts +12 -0
- package/dist/components/RichTextEditor/Extension/Image/ImageResize/utils/style-manager.d.ts +6 -0
- package/dist/components/RichTextEditor/Extension/Indent/backspace.indent.handlers.d.ts +18 -0
- package/dist/components/RichTextEditor/Extension/Indent/indent.extension.d.ts +26 -0
- package/dist/components/RichTextEditor/Extension/Indent/indent.handlers.d.ts +16 -0
- package/{src/components/RichTextEditor/Extension/Indent/indent.types.ts → dist/components/RichTextEditor/Extension/Indent/indent.types.d.ts} +10 -35
- package/dist/components/RichTextEditor/Extension/Indent/indent.utils.d.ts +8 -0
- package/dist/components/RichTextEditor/Extension/Indent/outdent.handlers.d.ts +6 -0
- package/dist/components/RichTextEditor/Extension/Indent/shifttab.indent.handlers.d.ts +18 -0
- package/dist/components/RichTextEditor/Extension/Indent/tab.indent.handlers.d.ts +21 -0
- package/dist/components/RichTextEditor/Extension/List/custom-list-item.extension.d.ts +17 -0
- package/dist/components/RichTextEditor/Extension/List/dynamic-bullet-styling.extension.d.ts +2 -0
- package/dist/components/RichTextEditor/Extension/batch-segment-images.extension.d.ts +19 -0
- package/dist/components/RichTextEditor/Extension/batch-segment-images.types.d.ts +33 -0
- package/dist/components/RichTextEditor/Extension/custom-image.extension.d.ts +4 -0
- package/dist/components/RichTextEditor/Extension/custom-link.extension.d.ts +3 -0
- package/dist/components/RichTextEditor/Extension/custom-mention.extension.d.ts +3 -0
- package/dist/components/RichTextEditor/Extension/custom-paragraph.extension.d.ts +6 -0
- package/dist/components/RichTextEditor/Extension/extensions.d.ts +4 -0
- package/dist/components/RichTextEditor/Extension/file-filtering.extension.d.ts +1 -0
- package/dist/components/RichTextEditor/Extension/list-indent-integration.extension.d.ts +2 -0
- package/dist/components/RichTextEditor/Extension/mentionstorage.extension.d.ts +9 -0
- package/dist/components/RichTextEditor/Extension/tiptap-extension-fontsize.d.ts +21 -0
- package/dist/components/RichTextEditor/Extension/tiptap-extension-lineheight.d.ts +21 -0
- package/{src/index.ts → dist/index.d.ts} +9 -17
- package/dist/lynqify-ui.js +3806 -0
- package/dist/lynqify-ui.umd.cjs +53 -0
- package/package.json +60 -31
- package/src/components/RichTextEditor/Extension/Indent/backspace.indent.handlers.ts +0 -77
- package/src/components/RichTextEditor/Extension/Indent/indent.extension.ts +0 -285
- package/src/components/RichTextEditor/Extension/Indent/indent.handlers.ts +0 -121
- package/src/components/RichTextEditor/Extension/Indent/indent.utils.ts +0 -8
- package/src/components/RichTextEditor/Extension/Indent/outdent.handlers.ts +0 -71
- package/src/components/RichTextEditor/Extension/Indent/shifttab.indent.handlers.ts +0 -133
- package/src/components/RichTextEditor/Extension/Indent/tab.indent.handlers.ts +0 -103
- package/src/components/RichTextEditor/Extension/List/custom-list-item.extension.ts +0 -107
- package/src/components/RichTextEditor/Extension/List/dynamic-bullet-styling.extension.ts +0 -40
- package/src/components/RichTextEditor/Extension/batch-segment-images.extension.ts +0 -486
- package/src/components/RichTextEditor/Extension/batch-segment-images.types.ts +0 -35
- package/src/components/RichTextEditor/Extension/custom-image.extension.ts +0 -18
- package/src/components/RichTextEditor/Extension/custom-link.extension.ts +0 -58
- package/src/components/RichTextEditor/Extension/custom-mention.extension.ts +0 -29
- package/src/components/RichTextEditor/Extension/custom-paragraph.extension.ts +0 -46
- package/src/components/RichTextEditor/Extension/extensions.ts +0 -118
- package/src/components/RichTextEditor/Extension/file-filtering.extension.ts +0 -0
- package/src/components/RichTextEditor/Extension/list-indent-integration.extension.ts +0 -125
- package/src/components/RichTextEditor/Extension/mentionstorage.extension.ts +0 -10
- package/src/components/RichTextEditor/Extension/tiptap-extension-fontsize.ts +0 -73
- package/src/components/RichTextEditor/Extension/tiptap-extension-lineheight.ts +0 -73
|
@@ -1,486 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
import { CommandProps, Node } from '@tiptap/core';
|
|
3
|
-
import { NodeSelection, Plugin, TextSelection } from '@tiptap/pm/state';
|
|
4
|
-
import { createRoot } from 'react-dom/client';
|
|
5
|
-
import BatchImageGalleryNodeView from 'src/components/NoteEditor/BatchImageGalleryNodeView';
|
|
6
|
-
import { BatchImage, BatchSegmentImagesOptions } from './batch-segment-images.types';
|
|
7
|
-
|
|
8
|
-
export type { BatchImage, BatchSegmentImagesOptions };
|
|
9
|
-
|
|
10
|
-
declare module '@tiptap/core' {
|
|
11
|
-
interface Commands<ReturnType> {
|
|
12
|
-
batchSegmentImages: {
|
|
13
|
-
insertBatchImages: ({
|
|
14
|
-
batchId,
|
|
15
|
-
images,
|
|
16
|
-
}: {
|
|
17
|
-
batchId: string;
|
|
18
|
-
images: BatchImage[];
|
|
19
|
-
}) => ReturnType;
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Tracks the last arrow key pressed — consumed by selectNode() to determine entry side.
|
|
25
|
-
// Module-level is safe: key events are synchronous and selectNode fires in the same tick.
|
|
26
|
-
let _pendingEntryDirection: 'left' | 'right' | null = null;
|
|
27
|
-
|
|
28
|
-
export const BatchSegmentImagesExtension = Node.create<BatchSegmentImagesOptions>({
|
|
29
|
-
name: 'batchSegmentImages',
|
|
30
|
-
|
|
31
|
-
group: 'block',
|
|
32
|
-
inline: false,
|
|
33
|
-
atom: true,
|
|
34
|
-
|
|
35
|
-
addOptions() {
|
|
36
|
-
return {
|
|
37
|
-
maxImageAmount: undefined,
|
|
38
|
-
height: undefined,
|
|
39
|
-
onAdd: undefined,
|
|
40
|
-
onRemove: undefined,
|
|
41
|
-
onImageClick: undefined,
|
|
42
|
-
initialImageRegistry: undefined as unknown as Map<string, BatchImage[]> | undefined,
|
|
43
|
-
};
|
|
44
|
-
},
|
|
45
|
-
|
|
46
|
-
// Create a storage registry for this extension storage
|
|
47
|
-
addStorage() {
|
|
48
|
-
return {
|
|
49
|
-
imageRegistry: new Map<string, BatchImage[]>(this.options.initialImageRegistry),
|
|
50
|
-
};
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
addAttributes() {
|
|
54
|
-
return {
|
|
55
|
-
batchId: {
|
|
56
|
-
default: null,
|
|
57
|
-
parseHTML: (element: HTMLElement) => element.getAttribute('data-batch-id'),
|
|
58
|
-
renderHTML: (attrs) => ({ 'data-batch-id': attrs.batchId }),
|
|
59
|
-
},
|
|
60
|
-
id: {
|
|
61
|
-
default: null,
|
|
62
|
-
parseHTML: (element: HTMLElement) => element.getAttribute('data-id'),
|
|
63
|
-
renderHTML: (attrs) => ({
|
|
64
|
-
'data-id': attrs.id,
|
|
65
|
-
}),
|
|
66
|
-
},
|
|
67
|
-
images: {
|
|
68
|
-
default: [],
|
|
69
|
-
parseHTML: (element: HTMLElement) => {
|
|
70
|
-
const imgs = element.querySelectorAll('img');
|
|
71
|
-
const images: BatchImage[] = [];
|
|
72
|
-
imgs.forEach((img) => {
|
|
73
|
-
const id = img.getAttribute('data-id');
|
|
74
|
-
const src = img.getAttribute('src');
|
|
75
|
-
const alt = img.getAttribute('alt');
|
|
76
|
-
const title = img.getAttribute('title');
|
|
77
|
-
if (id && src) {
|
|
78
|
-
images.push({
|
|
79
|
-
id,
|
|
80
|
-
src,
|
|
81
|
-
alt: alt ?? undefined,
|
|
82
|
-
title: title ?? undefined,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
return images;
|
|
87
|
-
},
|
|
88
|
-
renderHTML: () => ({}),
|
|
89
|
-
},
|
|
90
|
-
imageVersion: {
|
|
91
|
-
default: 0,
|
|
92
|
-
parseHTML: () => 0,
|
|
93
|
-
renderHTML: () => ({}),
|
|
94
|
-
},
|
|
95
|
-
// Ephemeral UI state — not persisted in HTML
|
|
96
|
-
focusedImageIndex: {
|
|
97
|
-
default: null,
|
|
98
|
-
parseHTML: () => null,
|
|
99
|
-
renderHTML: () => ({}),
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
},
|
|
103
|
-
|
|
104
|
-
addProseMirrorPlugins() {
|
|
105
|
-
return [
|
|
106
|
-
new Plugin({
|
|
107
|
-
props: {
|
|
108
|
-
handleKeyDown(_view, event) {
|
|
109
|
-
if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
|
|
110
|
-
_pendingEntryDirection = 'left';
|
|
111
|
-
} else if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
|
|
112
|
-
_pendingEntryDirection = 'right';
|
|
113
|
-
} else {
|
|
114
|
-
_pendingEntryDirection = null;
|
|
115
|
-
}
|
|
116
|
-
return false;
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
}),
|
|
120
|
-
];
|
|
121
|
-
},
|
|
122
|
-
|
|
123
|
-
addKeyboardShortcuts() {
|
|
124
|
-
const exitLeft = (pos: number, node: ReturnType<typeof this.editor.state.doc.nodeAt>) => {
|
|
125
|
-
const tr = this.editor.state.tr
|
|
126
|
-
.setNodeMarkup(pos, undefined, { ...node!.attrs, focusedImageIndex: null })
|
|
127
|
-
.setMeta('addToHistory', false);
|
|
128
|
-
tr.setSelection(TextSelection.near(tr.doc.resolve(pos)));
|
|
129
|
-
this.editor.view.dispatch(tr);
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const exitRight = (pos: number, node: ReturnType<typeof this.editor.state.doc.nodeAt>) => {
|
|
133
|
-
const afterPos = pos + node!.nodeSize;
|
|
134
|
-
const tr = this.editor.state.tr
|
|
135
|
-
.setNodeMarkup(pos, undefined, { ...node!.attrs, focusedImageIndex: null })
|
|
136
|
-
.setMeta('addToHistory', false);
|
|
137
|
-
tr.setSelection(TextSelection.near(tr.doc.resolve(afterPos)));
|
|
138
|
-
this.editor.view.dispatch(tr);
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const navigate = (direction: 'left' | 'right'): boolean => {
|
|
142
|
-
const { selection } = this.editor.state;
|
|
143
|
-
if (!(selection instanceof NodeSelection)) return false;
|
|
144
|
-
|
|
145
|
-
const node = selection.node;
|
|
146
|
-
if (node.type.name !== 'batchSegmentImages') return false;
|
|
147
|
-
|
|
148
|
-
const images =
|
|
149
|
-
(
|
|
150
|
-
this.editor.storage['batchSegmentImages'] as
|
|
151
|
-
| { imageRegistry: Map<string, BatchImage[]> }
|
|
152
|
-
| undefined
|
|
153
|
-
)?.imageRegistry?.get(node.attrs.batchId) ?? (node.attrs.images as BatchImage[]);
|
|
154
|
-
if (!images.length) return false;
|
|
155
|
-
|
|
156
|
-
const pos = selection.from;
|
|
157
|
-
const currentIndex = node.attrs.focusedImageIndex as number | null;
|
|
158
|
-
const hasAdd = Boolean(this.options.onAdd);
|
|
159
|
-
|
|
160
|
-
let newIndex: number | null;
|
|
161
|
-
|
|
162
|
-
if (direction === 'right') {
|
|
163
|
-
if (currentIndex === null) {
|
|
164
|
-
// From add button, exit right
|
|
165
|
-
exitRight(pos, node);
|
|
166
|
-
return true;
|
|
167
|
-
} else if (currentIndex < images.length - 1) {
|
|
168
|
-
newIndex = currentIndex + 1;
|
|
169
|
-
} else if (currentIndex === images.length - 1 && hasAdd) {
|
|
170
|
-
// From last image to add button
|
|
171
|
-
newIndex = null;
|
|
172
|
-
} else {
|
|
173
|
-
exitRight(pos, node);
|
|
174
|
-
return true;
|
|
175
|
-
}
|
|
176
|
-
} else {
|
|
177
|
-
if (currentIndex === null) {
|
|
178
|
-
// From add button to last image
|
|
179
|
-
newIndex = images.length - 1;
|
|
180
|
-
} else if (currentIndex > 0) {
|
|
181
|
-
newIndex = currentIndex - 1;
|
|
182
|
-
} else {
|
|
183
|
-
exitLeft(pos, node);
|
|
184
|
-
return true;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
this.editor.view.dispatch(
|
|
189
|
-
this.editor.state.tr
|
|
190
|
-
.setNodeMarkup(pos, undefined, { ...node.attrs, focusedImageIndex: newIndex })
|
|
191
|
-
.setMeta('addToHistory', false),
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
// Auto-scroll to focused image
|
|
195
|
-
if (newIndex !== null && newIndex >= 0 && newIndex < images.length) {
|
|
196
|
-
const domNode = this.editor.view.nodeDOM(pos);
|
|
197
|
-
if (domNode instanceof HTMLElement) {
|
|
198
|
-
requestAnimationFrame(() => {
|
|
199
|
-
const galleryImgs = domNode.querySelectorAll('img');
|
|
200
|
-
galleryImgs[newIndex]?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return true;
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
return {
|
|
209
|
-
ArrowLeft: () => navigate('left'),
|
|
210
|
-
ArrowRight: () => navigate('right'),
|
|
211
|
-
Space: () => {
|
|
212
|
-
const { selection } = this.editor.state;
|
|
213
|
-
if (!(selection instanceof NodeSelection)) return false;
|
|
214
|
-
|
|
215
|
-
const node = selection.node;
|
|
216
|
-
if (node.type.name !== 'batchSegmentImages') return false;
|
|
217
|
-
|
|
218
|
-
const images =
|
|
219
|
-
(
|
|
220
|
-
this.editor.storage['batchSegmentImages'] as
|
|
221
|
-
| { imageRegistry: Map<string, BatchImage[]> }
|
|
222
|
-
| undefined
|
|
223
|
-
)?.imageRegistry?.get(node.attrs.batchId) ?? (node.attrs.images as BatchImage[]);
|
|
224
|
-
const focusedIndex = node.attrs.focusedImageIndex as number | null;
|
|
225
|
-
const pos = selection.from;
|
|
226
|
-
|
|
227
|
-
if (focusedIndex === null) {
|
|
228
|
-
this.options.onAdd?.({
|
|
229
|
-
position: 'last',
|
|
230
|
-
batchId: node.attrs.batchId,
|
|
231
|
-
currentImages: images,
|
|
232
|
-
getPos: () => pos,
|
|
233
|
-
editor: this.editor,
|
|
234
|
-
});
|
|
235
|
-
return true;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (focusedIndex >= 0 && focusedIndex < images.length) {
|
|
239
|
-
const image = images[focusedIndex];
|
|
240
|
-
this.options.onImageClick?.({
|
|
241
|
-
imageId: image.id,
|
|
242
|
-
batchId: node.attrs.batchId,
|
|
243
|
-
src: image.src,
|
|
244
|
-
getPos: () => pos,
|
|
245
|
-
editor: this.editor,
|
|
246
|
-
});
|
|
247
|
-
return true;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return false;
|
|
251
|
-
},
|
|
252
|
-
};
|
|
253
|
-
},
|
|
254
|
-
|
|
255
|
-
renderHTML({ node }) {
|
|
256
|
-
const images = (node.attrs.images as BatchImage[]) ?? [];
|
|
257
|
-
|
|
258
|
-
return [
|
|
259
|
-
'div',
|
|
260
|
-
{
|
|
261
|
-
'data-batch-id': node.attrs.batchId,
|
|
262
|
-
style: 'display: flex; overflow-x: auto; gap: 10px; white-space: nowrap; padding: 10px 0;',
|
|
263
|
-
},
|
|
264
|
-
...images.map(
|
|
265
|
-
(image) =>
|
|
266
|
-
[
|
|
267
|
-
'img',
|
|
268
|
-
{
|
|
269
|
-
'data-id': image.id,
|
|
270
|
-
src: image.src,
|
|
271
|
-
alt: image.alt ?? '',
|
|
272
|
-
title: image.title ?? '',
|
|
273
|
-
style: 'width: 80px; height: 80px; object-fit: cover; flex-shrink: 0;',
|
|
274
|
-
},
|
|
275
|
-
] as const,
|
|
276
|
-
),
|
|
277
|
-
];
|
|
278
|
-
},
|
|
279
|
-
|
|
280
|
-
addNodeView() {
|
|
281
|
-
return ({ node, getPos, editor }) => {
|
|
282
|
-
const options = this.options;
|
|
283
|
-
const storage = this.storage as { imageRegistry: Map<string, BatchImage[]> };
|
|
284
|
-
const container = document.createElement('div');
|
|
285
|
-
container.style.width = '100%';
|
|
286
|
-
container.classList.add('batch-segment-gallery');
|
|
287
|
-
|
|
288
|
-
if (!storage.imageRegistry.has(node.attrs.batchId)) {
|
|
289
|
-
storage.imageRegistry.set(node.attrs.batchId, (node.attrs.images as BatchImage[]) ?? []);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const root = createRoot(container);
|
|
293
|
-
|
|
294
|
-
type GalleryState = {
|
|
295
|
-
images: BatchImage[];
|
|
296
|
-
focusedImageIndex: number | null;
|
|
297
|
-
batchId: string;
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
// nodeRef always points to latest node — stable callbacks read from it without being recreated
|
|
301
|
-
const nodeRef = { current: node };
|
|
302
|
-
let triggerUpdate: ((state: GalleryState) => void) | null = null;
|
|
303
|
-
|
|
304
|
-
// Stable callbacks — created once, never recreated across TipTap transactions
|
|
305
|
-
const stableOnAdd = () => {
|
|
306
|
-
options.onAdd?.({
|
|
307
|
-
position: 'last',
|
|
308
|
-
batchId: nodeRef.current.attrs.batchId,
|
|
309
|
-
currentImages: storage.imageRegistry.get(nodeRef.current.attrs.batchId) ?? [],
|
|
310
|
-
getPos,
|
|
311
|
-
editor,
|
|
312
|
-
});
|
|
313
|
-
};
|
|
314
|
-
const stableOnDelete = (index: number) => {
|
|
315
|
-
options.onRemove?.({
|
|
316
|
-
index,
|
|
317
|
-
imageId: (storage.imageRegistry.get(nodeRef.current.attrs.batchId) ?? [])[index]?.id,
|
|
318
|
-
batchId: nodeRef.current.attrs.batchId,
|
|
319
|
-
getPos,
|
|
320
|
-
editor,
|
|
321
|
-
});
|
|
322
|
-
};
|
|
323
|
-
const stableOnImageClick = (image: BatchImage) => {
|
|
324
|
-
const pos = getPos();
|
|
325
|
-
if (pos !== undefined) {
|
|
326
|
-
const imageIndex = (
|
|
327
|
-
storage.imageRegistry.get(nodeRef.current.attrs.batchId) ?? []
|
|
328
|
-
).findIndex((img) => img.id === image.id);
|
|
329
|
-
editor.view.dispatch(
|
|
330
|
-
editor.state.tr
|
|
331
|
-
.setNodeMarkup(pos, undefined, {
|
|
332
|
-
...nodeRef.current.attrs,
|
|
333
|
-
focusedImageIndex: imageIndex,
|
|
334
|
-
})
|
|
335
|
-
.setMeta('addToHistory', false),
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
|
-
options.onImageClick?.({
|
|
339
|
-
imageId: image.id,
|
|
340
|
-
batchId: nodeRef.current.attrs.batchId,
|
|
341
|
-
src: image.src,
|
|
342
|
-
getPos,
|
|
343
|
-
editor,
|
|
344
|
-
});
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
// GalleryWrapper renders ONCE — exposes setState via triggerUpdate assignment
|
|
348
|
-
const GalleryWrapper = () => {
|
|
349
|
-
const [state, setState] = React.useState<GalleryState>({
|
|
350
|
-
images: storage.imageRegistry.get(node.attrs.batchId) ?? [],
|
|
351
|
-
focusedImageIndex: node.attrs.focusedImageIndex as number | null,
|
|
352
|
-
batchId: node.attrs.batchId,
|
|
353
|
-
});
|
|
354
|
-
// assigned during render so update() always gets the current setter
|
|
355
|
-
triggerUpdate = setState;
|
|
356
|
-
return React.createElement(BatchImageGalleryNodeView, {
|
|
357
|
-
batchId: state.batchId,
|
|
358
|
-
images: state.images,
|
|
359
|
-
maxImageAmount: options.maxImageAmount,
|
|
360
|
-
height: options.height,
|
|
361
|
-
focusedImageIndex: state.focusedImageIndex,
|
|
362
|
-
onAdd: stableOnAdd,
|
|
363
|
-
onDelete: stableOnDelete,
|
|
364
|
-
onImageClick: stableOnImageClick,
|
|
365
|
-
});
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
// Render ONCE — subsequent updates go through triggerUpdate (React state, not root.render)
|
|
369
|
-
root.render(React.createElement(GalleryWrapper));
|
|
370
|
-
|
|
371
|
-
let lastRenderedNode = node;
|
|
372
|
-
|
|
373
|
-
// Single transaction: NodeSelection + focusedImageIndex = null together (no double render)
|
|
374
|
-
container.addEventListener('click', () => {
|
|
375
|
-
const pos = getPos();
|
|
376
|
-
if (pos !== undefined) {
|
|
377
|
-
const { state, view } = editor;
|
|
378
|
-
const currentNode = state.doc.nodeAt(pos);
|
|
379
|
-
if (!currentNode) return;
|
|
380
|
-
const tr = state.tr
|
|
381
|
-
.setNodeMarkup(pos, undefined, { ...currentNode.attrs, focusedImageIndex: null })
|
|
382
|
-
.setMeta('addToHistory', false);
|
|
383
|
-
view.dispatch(tr.setSelection(NodeSelection.create(tr.doc, pos)));
|
|
384
|
-
}
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
return {
|
|
388
|
-
dom: container,
|
|
389
|
-
update: (updatedNode) => {
|
|
390
|
-
if (updatedNode.type !== node.type) return false;
|
|
391
|
-
// ProseMirror structural sharing: same reference = node unchanged, skip update
|
|
392
|
-
if (updatedNode === lastRenderedNode) return true;
|
|
393
|
-
lastRenderedNode = updatedNode;
|
|
394
|
-
nodeRef.current = updatedNode;
|
|
395
|
-
triggerUpdate?.({
|
|
396
|
-
images: storage.imageRegistry.get(updatedNode.attrs.batchId) ?? [],
|
|
397
|
-
focusedImageIndex: updatedNode.attrs.focusedImageIndex as number | null,
|
|
398
|
-
batchId: updatedNode.attrs.batchId,
|
|
399
|
-
});
|
|
400
|
-
return true;
|
|
401
|
-
},
|
|
402
|
-
// selectNode fires when node becomes selected (ArrowUp/Down/Left/Right from outside).
|
|
403
|
-
// Deferred via queueMicrotask — safe since selectNode is called during view.dispatch.
|
|
404
|
-
selectNode: () => {
|
|
405
|
-
const dir = _pendingEntryDirection;
|
|
406
|
-
_pendingEntryDirection = null;
|
|
407
|
-
|
|
408
|
-
const pos = getPos();
|
|
409
|
-
if (pos === undefined) return;
|
|
410
|
-
|
|
411
|
-
const currentNode = editor.state.doc.nodeAt(pos);
|
|
412
|
-
// Already focused (e.g. via click listener) — nothing to do
|
|
413
|
-
if (!currentNode || currentNode.attrs.focusedImageIndex !== null) return;
|
|
414
|
-
|
|
415
|
-
// Enter from right → start at add button (null), enter from left → start at first image (0)
|
|
416
|
-
const focusIdx = dir === 'left' ? null : 0;
|
|
417
|
-
|
|
418
|
-
queueMicrotask(() => {
|
|
419
|
-
const pos2 = getPos();
|
|
420
|
-
if (pos2 === undefined) return;
|
|
421
|
-
const node2 = editor.state.doc.nodeAt(pos2);
|
|
422
|
-
if (!node2 || node2.attrs.focusedImageIndex !== null) return;
|
|
423
|
-
if (!(editor.state.selection instanceof NodeSelection)) return;
|
|
424
|
-
editor.view.dispatch(
|
|
425
|
-
editor.state.tr
|
|
426
|
-
.setNodeMarkup(pos2, undefined, { ...node2.attrs, focusedImageIndex: focusIdx })
|
|
427
|
-
.setMeta('addToHistory', false),
|
|
428
|
-
);
|
|
429
|
-
});
|
|
430
|
-
},
|
|
431
|
-
deselectNode: () => {
|
|
432
|
-
// Reset focus ring when node loses selection
|
|
433
|
-
const pos = getPos();
|
|
434
|
-
if (pos === undefined) return;
|
|
435
|
-
const currentNode = editor.state.doc.nodeAt(pos);
|
|
436
|
-
if (!currentNode || currentNode.attrs.focusedImageIndex === null) return;
|
|
437
|
-
editor.view.dispatch(
|
|
438
|
-
editor.state.tr
|
|
439
|
-
.setNodeMarkup(pos, undefined, { ...currentNode.attrs, focusedImageIndex: null })
|
|
440
|
-
.setMeta('addToHistory', false),
|
|
441
|
-
);
|
|
442
|
-
},
|
|
443
|
-
destroy: () => {
|
|
444
|
-
storage.imageRegistry.delete(node.attrs.batchId);
|
|
445
|
-
queueMicrotask(() => root.unmount());
|
|
446
|
-
},
|
|
447
|
-
};
|
|
448
|
-
};
|
|
449
|
-
},
|
|
450
|
-
|
|
451
|
-
addCommands() {
|
|
452
|
-
return {
|
|
453
|
-
insertBatchImages:
|
|
454
|
-
({ batchId, images }: { batchId: string; images: BatchImage[] }) =>
|
|
455
|
-
(commands: CommandProps) => {
|
|
456
|
-
const editor = commands.editor;
|
|
457
|
-
// 1. Seed registry immediately — before NodeView even mounts
|
|
458
|
-
const storage = editor.storage.batchSegmentImages as {
|
|
459
|
-
imageRegistry: Map<string, BatchImage[]>;
|
|
460
|
-
};
|
|
461
|
-
storage.imageRegistry.set(batchId, images);
|
|
462
|
-
|
|
463
|
-
// 2. Insert the node (attrs.images is just the serialization seed)
|
|
464
|
-
return commands
|
|
465
|
-
.chain()
|
|
466
|
-
.insertContent({
|
|
467
|
-
type: 'batchSegmentImages',
|
|
468
|
-
attrs: {
|
|
469
|
-
batchId,
|
|
470
|
-
images,
|
|
471
|
-
id: crypto.randomUUID(),
|
|
472
|
-
},
|
|
473
|
-
})
|
|
474
|
-
.run();
|
|
475
|
-
},
|
|
476
|
-
};
|
|
477
|
-
},
|
|
478
|
-
|
|
479
|
-
parseHTML() {
|
|
480
|
-
return [
|
|
481
|
-
{
|
|
482
|
-
tag: 'div[data-batch-id]',
|
|
483
|
-
},
|
|
484
|
-
];
|
|
485
|
-
},
|
|
486
|
-
});
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { Editor } from '@tiptap/react';
|
|
2
|
-
|
|
3
|
-
export interface BatchImage {
|
|
4
|
-
id: string;
|
|
5
|
-
src: string;
|
|
6
|
-
alt?: string;
|
|
7
|
-
title?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface BatchSegmentImagesOptions {
|
|
11
|
-
initialImageRegistry?: Map<string, BatchImage[]>;
|
|
12
|
-
maxImageAmount?: number;
|
|
13
|
-
height?: number;
|
|
14
|
-
onAdd?: (params: {
|
|
15
|
-
position: 'last';
|
|
16
|
-
batchId: string;
|
|
17
|
-
currentImages: BatchImage[];
|
|
18
|
-
getPos: () => number | undefined;
|
|
19
|
-
editor: Editor;
|
|
20
|
-
}) => void;
|
|
21
|
-
onRemove?: (params: {
|
|
22
|
-
index: number;
|
|
23
|
-
imageId: string;
|
|
24
|
-
batchId: string;
|
|
25
|
-
getPos: () => number | undefined;
|
|
26
|
-
editor: Editor;
|
|
27
|
-
}) => void;
|
|
28
|
-
onImageClick?: (params: {
|
|
29
|
-
imageId: string;
|
|
30
|
-
batchId: string;
|
|
31
|
-
src: string;
|
|
32
|
-
getPos: () => number | undefined;
|
|
33
|
-
editor: Editor;
|
|
34
|
-
}) => void;
|
|
35
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import Image from '@tiptap/extension-image';
|
|
2
|
-
|
|
3
|
-
/** Image extension that persist `id` prop. */
|
|
4
|
-
export const CustomImage = Image.extend({
|
|
5
|
-
addAttributes() {
|
|
6
|
-
return {
|
|
7
|
-
...(this.parent?.() || {}),
|
|
8
|
-
id: {
|
|
9
|
-
default: null,
|
|
10
|
-
parseHTML: (element: HTMLElement) => element.getAttribute('data-id'),
|
|
11
|
-
renderHTML: (attributes: Record<string, any>) => {
|
|
12
|
-
if (!attributes.id) return {};
|
|
13
|
-
return { 'data-id': attributes.id };
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
};
|
|
17
|
-
},
|
|
18
|
-
});
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import Link from '@tiptap/extension-link';
|
|
2
|
-
import { Plugin } from '@tiptap/pm/state';
|
|
3
|
-
|
|
4
|
-
export const CustomLink = Link.extend({
|
|
5
|
-
addAttributes() {
|
|
6
|
-
return {
|
|
7
|
-
...this.parent?.(),
|
|
8
|
-
'data-note-file-id': {
|
|
9
|
-
default: null,
|
|
10
|
-
parseHTML: (element) => element.getAttribute('data-note-file-id'),
|
|
11
|
-
renderHTML: (attributes) => {
|
|
12
|
-
if (!attributes['data-note-file-id']) return {};
|
|
13
|
-
return { 'data-note-file-id': attributes['data-note-file-id'] };
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
};
|
|
17
|
-
},
|
|
18
|
-
|
|
19
|
-
addProseMirrorPlugins() {
|
|
20
|
-
return [
|
|
21
|
-
new Plugin({
|
|
22
|
-
appendTransaction(transactions, oldState, newState) {
|
|
23
|
-
if (!transactions.some((tr) => tr.docChanged)) return null;
|
|
24
|
-
|
|
25
|
-
const { selection } = newState;
|
|
26
|
-
if (!selection.empty) return null;
|
|
27
|
-
|
|
28
|
-
const { $from } = selection;
|
|
29
|
-
if ($from.parent.type.name !== 'paragraph') return null;
|
|
30
|
-
if ($from.parent.content.size !== 0) return null;
|
|
31
|
-
|
|
32
|
-
const pos = $from.before($from.depth);
|
|
33
|
-
const oldFrom = oldState.selection.$from;
|
|
34
|
-
if (oldFrom.depth < 1) return null;
|
|
35
|
-
const oldPos = oldFrom.before(oldFrom.depth);
|
|
36
|
-
if (pos !== oldPos) return null;
|
|
37
|
-
|
|
38
|
-
const oldNode = oldState.doc.nodeAt(pos);
|
|
39
|
-
if (!oldNode || oldNode.type.name !== 'paragraph') return null;
|
|
40
|
-
|
|
41
|
-
let hadLink = false;
|
|
42
|
-
oldNode.forEach((child) => {
|
|
43
|
-
if (child.marks.some((m) => m.type.name === 'link')) hadLink = true;
|
|
44
|
-
});
|
|
45
|
-
if (!hadLink) return null;
|
|
46
|
-
|
|
47
|
-
const tr = newState.tr;
|
|
48
|
-
try {
|
|
49
|
-
tr.join(pos);
|
|
50
|
-
} catch {
|
|
51
|
-
// no previous node to join with — still clear stored marks below
|
|
52
|
-
}
|
|
53
|
-
return tr.setStoredMarks([]);
|
|
54
|
-
},
|
|
55
|
-
}),
|
|
56
|
-
];
|
|
57
|
-
},
|
|
58
|
-
});
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { Mention } from '@tiptap/extension-mention';
|
|
2
|
-
export const CustomMention = Mention.extend({
|
|
3
|
-
addAttributes() {
|
|
4
|
-
return {
|
|
5
|
-
...this.parent?.(),
|
|
6
|
-
value: {
|
|
7
|
-
default: null,
|
|
8
|
-
parseHTML: (element) => element.getAttribute('data-value'),
|
|
9
|
-
renderHTML: (attributes) => {
|
|
10
|
-
if (!attributes.value) return {};
|
|
11
|
-
return { 'data-value': attributes.value };
|
|
12
|
-
},
|
|
13
|
-
},
|
|
14
|
-
};
|
|
15
|
-
},
|
|
16
|
-
addCommands() {
|
|
17
|
-
return {
|
|
18
|
-
...this.parent?.(),
|
|
19
|
-
createMention:
|
|
20
|
-
({ id, label, value }: any) =>
|
|
21
|
-
({ commands }: any) => {
|
|
22
|
-
return commands.insertContent({
|
|
23
|
-
type: this.name,
|
|
24
|
-
attrs: { id, label, value },
|
|
25
|
-
});
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
},
|
|
29
|
-
});
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Custom Paragraph Extension: Selectable Indent Spacing
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import Paragraph from '@tiptap/extension-paragraph';
|
|
6
|
-
|
|
7
|
-
export const CustomParagraph = Paragraph.extend({
|
|
8
|
-
// Keep name as 'paragraph' for schema compatibility
|
|
9
|
-
|
|
10
|
-
addNodeView() {
|
|
11
|
-
return ({ node }) => {
|
|
12
|
-
const indentLevel = node.attrs.indent || 0;
|
|
13
|
-
const dom = document.createElement('p');
|
|
14
|
-
dom.setAttribute('data-indent', String(indentLevel));
|
|
15
|
-
|
|
16
|
-
// Apply padding based on indent level (20px per level)
|
|
17
|
-
if (indentLevel > 0) {
|
|
18
|
-
dom.style.paddingLeft = `${indentLevel * 20}px`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Content goes directly in the paragraph (no spacing span)
|
|
22
|
-
const contentDOM = dom;
|
|
23
|
-
|
|
24
|
-
return { dom, contentDOM };
|
|
25
|
-
};
|
|
26
|
-
},
|
|
27
|
-
|
|
28
|
-
// Placeholder positioning based on indent level
|
|
29
|
-
addAttributes() {
|
|
30
|
-
return {
|
|
31
|
-
...this.parent?.(),
|
|
32
|
-
indent: {
|
|
33
|
-
default: 0,
|
|
34
|
-
parseHTML: (element) => parseInt(element.getAttribute('data-indent') || '0'),
|
|
35
|
-
renderHTML: (attributes) => {
|
|
36
|
-
if (!attributes.indent) return {};
|
|
37
|
-
return {
|
|
38
|
-
'data-indent': attributes.indent,
|
|
39
|
-
// CSS custom property for dynamic placeholder offset
|
|
40
|
-
style: `--indent-level: ${attributes.indent};`,
|
|
41
|
-
};
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
},
|
|
46
|
-
});
|