@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.
Files changed (60) hide show
  1. package/dist/components/NoteEditor/BatchImageGalleryNodeView.d.ts +19 -0
  2. package/dist/components/RichTextEditor/Extension/Image/ImageResize/constants/index.d.ts +22 -0
  3. package/dist/components/RichTextEditor/Extension/Image/ImageResize/controllers/image-node-view.d.ts +38 -0
  4. package/dist/components/RichTextEditor/Extension/Image/ImageResize/controllers/position-controller.d.ts +12 -0
  5. package/dist/components/RichTextEditor/Extension/Image/ImageResize/controllers/resize-controller.d.ts +13 -0
  6. package/dist/components/RichTextEditor/Extension/Image/ImageResize/image-resize.d.ts +22 -0
  7. package/dist/components/RichTextEditor/Extension/Image/ImageResize/index.d.ts +4 -0
  8. package/dist/components/RichTextEditor/Extension/Image/ImageResize/types/index.d.ts +20 -0
  9. package/dist/components/RichTextEditor/Extension/Image/ImageResize/utils/attribute-parser.d.ts +4 -0
  10. package/dist/components/RichTextEditor/Extension/Image/ImageResize/utils/clamp-width.d.ts +6 -0
  11. package/dist/components/RichTextEditor/Extension/Image/ImageResize/utils/index.d.ts +7 -0
  12. package/dist/components/RichTextEditor/Extension/Image/ImageResize/utils/resize-handler.d.ts +12 -0
  13. package/dist/components/RichTextEditor/Extension/Image/ImageResize/utils/style-manager.d.ts +6 -0
  14. package/dist/components/RichTextEditor/Extension/Indent/backspace.indent.handlers.d.ts +18 -0
  15. package/dist/components/RichTextEditor/Extension/Indent/indent.extension.d.ts +26 -0
  16. package/dist/components/RichTextEditor/Extension/Indent/indent.handlers.d.ts +16 -0
  17. package/{src/components/RichTextEditor/Extension/Indent/indent.types.ts → dist/components/RichTextEditor/Extension/Indent/indent.types.d.ts} +10 -35
  18. package/dist/components/RichTextEditor/Extension/Indent/indent.utils.d.ts +8 -0
  19. package/dist/components/RichTextEditor/Extension/Indent/outdent.handlers.d.ts +6 -0
  20. package/dist/components/RichTextEditor/Extension/Indent/shifttab.indent.handlers.d.ts +18 -0
  21. package/dist/components/RichTextEditor/Extension/Indent/tab.indent.handlers.d.ts +21 -0
  22. package/dist/components/RichTextEditor/Extension/List/custom-list-item.extension.d.ts +17 -0
  23. package/dist/components/RichTextEditor/Extension/List/dynamic-bullet-styling.extension.d.ts +2 -0
  24. package/dist/components/RichTextEditor/Extension/batch-segment-images.extension.d.ts +19 -0
  25. package/dist/components/RichTextEditor/Extension/batch-segment-images.types.d.ts +33 -0
  26. package/dist/components/RichTextEditor/Extension/custom-image.extension.d.ts +4 -0
  27. package/dist/components/RichTextEditor/Extension/custom-link.extension.d.ts +3 -0
  28. package/dist/components/RichTextEditor/Extension/custom-mention.extension.d.ts +3 -0
  29. package/dist/components/RichTextEditor/Extension/custom-paragraph.extension.d.ts +6 -0
  30. package/dist/components/RichTextEditor/Extension/extensions.d.ts +4 -0
  31. package/dist/components/RichTextEditor/Extension/file-filtering.extension.d.ts +1 -0
  32. package/dist/components/RichTextEditor/Extension/list-indent-integration.extension.d.ts +2 -0
  33. package/dist/components/RichTextEditor/Extension/mentionstorage.extension.d.ts +9 -0
  34. package/dist/components/RichTextEditor/Extension/tiptap-extension-fontsize.d.ts +21 -0
  35. package/dist/components/RichTextEditor/Extension/tiptap-extension-lineheight.d.ts +21 -0
  36. package/{src/index.ts → dist/index.d.ts} +9 -17
  37. package/dist/lynqify-ui.js +3806 -0
  38. package/dist/lynqify-ui.umd.cjs +53 -0
  39. package/package.json +60 -31
  40. package/src/components/RichTextEditor/Extension/Indent/backspace.indent.handlers.ts +0 -77
  41. package/src/components/RichTextEditor/Extension/Indent/indent.extension.ts +0 -285
  42. package/src/components/RichTextEditor/Extension/Indent/indent.handlers.ts +0 -121
  43. package/src/components/RichTextEditor/Extension/Indent/indent.utils.ts +0 -8
  44. package/src/components/RichTextEditor/Extension/Indent/outdent.handlers.ts +0 -71
  45. package/src/components/RichTextEditor/Extension/Indent/shifttab.indent.handlers.ts +0 -133
  46. package/src/components/RichTextEditor/Extension/Indent/tab.indent.handlers.ts +0 -103
  47. package/src/components/RichTextEditor/Extension/List/custom-list-item.extension.ts +0 -107
  48. package/src/components/RichTextEditor/Extension/List/dynamic-bullet-styling.extension.ts +0 -40
  49. package/src/components/RichTextEditor/Extension/batch-segment-images.extension.ts +0 -486
  50. package/src/components/RichTextEditor/Extension/batch-segment-images.types.ts +0 -35
  51. package/src/components/RichTextEditor/Extension/custom-image.extension.ts +0 -18
  52. package/src/components/RichTextEditor/Extension/custom-link.extension.ts +0 -58
  53. package/src/components/RichTextEditor/Extension/custom-mention.extension.ts +0 -29
  54. package/src/components/RichTextEditor/Extension/custom-paragraph.extension.ts +0 -46
  55. package/src/components/RichTextEditor/Extension/extensions.ts +0 -118
  56. package/src/components/RichTextEditor/Extension/file-filtering.extension.ts +0 -0
  57. package/src/components/RichTextEditor/Extension/list-indent-integration.extension.ts +0 -125
  58. package/src/components/RichTextEditor/Extension/mentionstorage.extension.ts +0 -10
  59. package/src/components/RichTextEditor/Extension/tiptap-extension-fontsize.ts +0 -73
  60. 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
- });