@verdant-web/tiptap 4.0.0 → 4.1.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/esm/extensions/Verdant.d.ts +0 -1
- package/dist/esm/extensions/Verdant.js +1 -1
- package/dist/esm/extensions/Verdant.js.map +1 -1
- package/dist/esm/extensions/VerdantMedia.d.ts +11 -1
- package/dist/esm/extensions/VerdantMedia.js +193 -18
- package/dist/esm/extensions/VerdantMedia.js.map +1 -1
- package/dist/esm/extensions/attributes.d.ts +5 -0
- package/dist/esm/extensions/attributes.js +6 -0
- package/dist/esm/extensions/attributes.js.map +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/server/index.d.ts +6 -0
- package/dist/esm/server/index.js +27 -0
- package/dist/esm/server/index.js.map +1 -0
- package/package.json +25 -13
- package/src/extensions/Verdant.ts +1 -1
- package/src/extensions/VerdantMedia.ts +206 -19
- package/src/extensions/attributes.ts +6 -0
- package/src/index.ts +1 -0
- package/src/server/index.ts +38 -0
- package/dist/esm/__browserTests__/react.test.d.ts +0 -1
- package/dist/esm/__browserTests__/react.test.js +0 -501
- package/dist/esm/__browserTests__/react.test.js.map +0 -1
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { mergeAttributes, Node } from '@tiptap/core';
|
|
2
|
-
import {
|
|
2
|
+
import type {
|
|
3
3
|
EntityFile,
|
|
4
4
|
EntityFileSnapshot,
|
|
5
|
-
id,
|
|
6
5
|
ObjectEntity,
|
|
7
6
|
} from '@verdant-web/store';
|
|
7
|
+
import {
|
|
8
|
+
fileIdAttribute,
|
|
9
|
+
fileKeyAttribute,
|
|
10
|
+
fileNameAttribute,
|
|
11
|
+
fileTypeAttribute,
|
|
12
|
+
} from './attributes.js';
|
|
8
13
|
|
|
9
14
|
export type VerdantMediaFileMap = ObjectEntity<
|
|
10
15
|
Record<string, File>,
|
|
@@ -31,7 +36,6 @@ declare module '@tiptap/core' {
|
|
|
31
36
|
}
|
|
32
37
|
}
|
|
33
38
|
|
|
34
|
-
const fileIdAttribute = 'data-verdant-file';
|
|
35
39
|
export const VerdantMediaExtension = Node.create<VerdantMediaExtensionOptions>({
|
|
36
40
|
name: 'verdant-media',
|
|
37
41
|
group: 'block',
|
|
@@ -43,21 +47,59 @@ export const VerdantMediaExtension = Node.create<VerdantMediaExtensionOptions>({
|
|
|
43
47
|
},
|
|
44
48
|
addAttributes() {
|
|
45
49
|
return {
|
|
50
|
+
[fileKeyAttribute]: {
|
|
51
|
+
default: null,
|
|
52
|
+
keepOnSplit: false,
|
|
53
|
+
parseHTML: (element) => element.getAttribute(fileKeyAttribute),
|
|
54
|
+
rendered: true,
|
|
55
|
+
},
|
|
46
56
|
[fileIdAttribute]: {
|
|
47
57
|
default: null,
|
|
58
|
+
keepOnSplit: false,
|
|
59
|
+
parseHTML: (element) => element.getAttribute(fileIdAttribute),
|
|
60
|
+
rendered: true,
|
|
48
61
|
},
|
|
49
62
|
alt: {
|
|
50
63
|
default: null,
|
|
64
|
+
keepOnSplit: false,
|
|
65
|
+
parseHTML: (element) => element.getAttribute('alt'),
|
|
66
|
+
},
|
|
67
|
+
[fileTypeAttribute]: {
|
|
68
|
+
default: null,
|
|
69
|
+
keepOnSplit: false,
|
|
70
|
+
parseHTML: (element) => element.getAttribute(fileTypeAttribute),
|
|
71
|
+
rendered: true,
|
|
72
|
+
},
|
|
73
|
+
[fileNameAttribute]: {
|
|
74
|
+
default: null,
|
|
75
|
+
keepOnSplit: false,
|
|
76
|
+
parseHTML: (element) => element.getAttribute(fileNameAttribute),
|
|
77
|
+
rendered: true,
|
|
51
78
|
},
|
|
52
79
|
};
|
|
53
80
|
},
|
|
54
81
|
parseHTML() {
|
|
55
82
|
return [
|
|
56
83
|
{
|
|
57
|
-
tag: `[${
|
|
84
|
+
tag: `[${fileKeyAttribute}]`,
|
|
58
85
|
getAttrs: (element) => {
|
|
86
|
+
let fileKey = element.getAttribute(fileKeyAttribute);
|
|
87
|
+
|
|
88
|
+
// back compat
|
|
89
|
+
if (!fileKey && element.getAttribute('data-verdant-file')) {
|
|
90
|
+
fileKey = element.getAttribute('data-verdant-file');
|
|
91
|
+
}
|
|
92
|
+
|
|
59
93
|
const fileId = element.getAttribute(fileIdAttribute);
|
|
94
|
+
const alt = element.getAttribute('alt');
|
|
95
|
+
const type = element.getAttribute(fileTypeAttribute);
|
|
96
|
+
const name = element.getAttribute(fileNameAttribute);
|
|
97
|
+
|
|
60
98
|
return {
|
|
99
|
+
[fileKeyAttribute]: fileKey,
|
|
100
|
+
alt,
|
|
101
|
+
[fileTypeAttribute]: type,
|
|
102
|
+
[fileNameAttribute]: name,
|
|
61
103
|
[fileIdAttribute]: fileId,
|
|
62
104
|
};
|
|
63
105
|
},
|
|
@@ -65,38 +107,52 @@ export const VerdantMediaExtension = Node.create<VerdantMediaExtensionOptions>({
|
|
|
65
107
|
];
|
|
66
108
|
},
|
|
67
109
|
renderHTML(props) {
|
|
68
|
-
const
|
|
69
|
-
if (!
|
|
110
|
+
const fileKey = props.node.attrs[fileKeyAttribute];
|
|
111
|
+
if (!fileKey) {
|
|
70
112
|
return ['div', props.HTMLAttributes, 'Missing file'];
|
|
71
113
|
}
|
|
72
|
-
const file = this.options.fileMap.get(
|
|
114
|
+
const file = this.options.fileMap.get(fileKey);
|
|
73
115
|
if (!file) {
|
|
74
116
|
return ['div', props.HTMLAttributes, 'Missing file'];
|
|
75
117
|
}
|
|
118
|
+
const fileId = file.id;
|
|
119
|
+
const baseAttrs = {
|
|
120
|
+
[fileKeyAttribute]: fileKey,
|
|
121
|
+
[fileIdAttribute]: fileId,
|
|
122
|
+
[fileTypeAttribute]: file.type,
|
|
123
|
+
[fileNameAttribute]: file.name,
|
|
124
|
+
};
|
|
76
125
|
if (file.loading) {
|
|
77
126
|
// this means the user didn't preload files before rendering...
|
|
78
127
|
return [
|
|
79
128
|
'div',
|
|
80
|
-
props.HTMLAttributes,
|
|
129
|
+
mergeAttributes(baseAttrs, props.HTMLAttributes),
|
|
81
130
|
'Loading file. This file was not preloaded before rendering the document.',
|
|
82
131
|
];
|
|
83
132
|
}
|
|
84
133
|
const type = file.type;
|
|
85
134
|
if (type?.startsWith('image/')) {
|
|
86
|
-
return [
|
|
135
|
+
return [
|
|
136
|
+
'img',
|
|
137
|
+
mergeAttributes({ src: file.url, ...baseAttrs }, props.HTMLAttributes),
|
|
138
|
+
];
|
|
87
139
|
} else if (type?.startsWith('video/')) {
|
|
88
140
|
return [
|
|
89
141
|
'video',
|
|
90
|
-
mergeAttributes({ src: file.url }, props.HTMLAttributes),
|
|
142
|
+
mergeAttributes({ src: file.url, ...baseAttrs }, props.HTMLAttributes),
|
|
91
143
|
];
|
|
92
144
|
} else if (type?.startsWith('audio/')) {
|
|
93
145
|
return [
|
|
94
146
|
'audio',
|
|
95
|
-
mergeAttributes({ src: file.url }, props.HTMLAttributes),
|
|
147
|
+
mergeAttributes({ src: file.url, ...baseAttrs }, props.HTMLAttributes),
|
|
96
148
|
];
|
|
97
149
|
} else {
|
|
98
150
|
// TODO: render file download
|
|
99
|
-
return [
|
|
151
|
+
return [
|
|
152
|
+
'div',
|
|
153
|
+
mergeAttributes(baseAttrs, props.HTMLAttributes),
|
|
154
|
+
'Unsupported file type',
|
|
155
|
+
];
|
|
100
156
|
}
|
|
101
157
|
},
|
|
102
158
|
onBeforeCreate() {
|
|
@@ -123,12 +179,12 @@ export const VerdantMediaExtension = Node.create<VerdantMediaExtensionOptions>({
|
|
|
123
179
|
this.editor.on('update', ({ transaction }) => {
|
|
124
180
|
const fileIds = new Set<string>();
|
|
125
181
|
transaction.doc.forEach((node) => {
|
|
126
|
-
if (node.attrs[
|
|
127
|
-
fileIds.add(node.attrs[
|
|
182
|
+
if (node.attrs[fileKeyAttribute]) {
|
|
183
|
+
fileIds.add(node.attrs[fileKeyAttribute]);
|
|
128
184
|
}
|
|
129
185
|
});
|
|
130
186
|
transaction.before.forEach((node) => {
|
|
131
|
-
const fileId = node.attrs[
|
|
187
|
+
const fileId = node.attrs[fileKeyAttribute];
|
|
132
188
|
if (fileId && !fileIds.has(fileId)) {
|
|
133
189
|
// the file was removed from the document
|
|
134
190
|
this.options.fileMap.delete(fileId);
|
|
@@ -141,13 +197,20 @@ export const VerdantMediaExtension = Node.create<VerdantMediaExtensionOptions>({
|
|
|
141
197
|
insertMedia:
|
|
142
198
|
(file: File) =>
|
|
143
199
|
({ commands }) => {
|
|
144
|
-
const
|
|
145
|
-
this.options.fileMap.set(
|
|
200
|
+
const fileKey = crypto.randomUUID();
|
|
201
|
+
this.options.fileMap.set(fileKey, file);
|
|
202
|
+
const storedFile = this.options.fileMap.get(fileKey);
|
|
203
|
+
if (!storedFile) {
|
|
204
|
+
throw new Error('Failed to store file in file map');
|
|
205
|
+
}
|
|
146
206
|
return commands.insertContent(
|
|
147
207
|
{
|
|
148
208
|
type: this.name,
|
|
149
209
|
attrs: {
|
|
150
|
-
[
|
|
210
|
+
[fileKeyAttribute]: fileKey,
|
|
211
|
+
[fileTypeAttribute]: file.type,
|
|
212
|
+
[fileNameAttribute]: file.name,
|
|
213
|
+
[fileIdAttribute]: storedFile.id,
|
|
151
214
|
},
|
|
152
215
|
} as any,
|
|
153
216
|
{
|
|
@@ -171,7 +234,7 @@ export const VerdantMediaExtension = Node.create<VerdantMediaExtensionOptions>({
|
|
|
171
234
|
});
|
|
172
235
|
|
|
173
236
|
// get the file ID from attrs
|
|
174
|
-
const fileId = node.attrs[
|
|
237
|
+
const fileId = node.attrs[fileKeyAttribute];
|
|
175
238
|
if (!fileId) {
|
|
176
239
|
root.textContent = 'Missing file';
|
|
177
240
|
return {
|
|
@@ -297,3 +360,127 @@ export async function preloadMedia(files: VerdantMediaFileMap) {
|
|
|
297
360
|
}),
|
|
298
361
|
);
|
|
299
362
|
}
|
|
363
|
+
|
|
364
|
+
export interface VerdantMediaRendererExtensionOptions {}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* An extension for rendering TipTap documents with Verdant media nodes.
|
|
368
|
+
* - ONLY works if the files being rendered are already synced to a Verdant server.
|
|
369
|
+
* - ONLY works if you process the document JSON with `@verdant-web/tiptap/server`'s `attachFileUrls` function on your server.
|
|
370
|
+
* It's good for static sites or server-side rendering of documents you authored
|
|
371
|
+
* with Verdant.
|
|
372
|
+
*/
|
|
373
|
+
export const VerdantMediaRendererExtension =
|
|
374
|
+
Node.create<VerdantMediaRendererExtensionOptions>({
|
|
375
|
+
name: 'verdant-media',
|
|
376
|
+
group: 'block',
|
|
377
|
+
addAttributes() {
|
|
378
|
+
return {
|
|
379
|
+
[fileKeyAttribute]: {
|
|
380
|
+
default: null,
|
|
381
|
+
keepOnSplit: false,
|
|
382
|
+
parseHTML: (element) => element.getAttribute(fileKeyAttribute),
|
|
383
|
+
rendered: true,
|
|
384
|
+
},
|
|
385
|
+
[fileIdAttribute]: {
|
|
386
|
+
default: null,
|
|
387
|
+
keepOnSplit: false,
|
|
388
|
+
parseHTML: (element) => element.getAttribute(fileIdAttribute),
|
|
389
|
+
rendered: true,
|
|
390
|
+
},
|
|
391
|
+
alt: {
|
|
392
|
+
default: null,
|
|
393
|
+
keepOnSplit: false,
|
|
394
|
+
parseHTML: (element) => element.getAttribute('alt'),
|
|
395
|
+
},
|
|
396
|
+
[fileTypeAttribute]: {
|
|
397
|
+
default: null,
|
|
398
|
+
keepOnSplit: false,
|
|
399
|
+
parseHTML: (element) => element.getAttribute(fileTypeAttribute),
|
|
400
|
+
rendered: true,
|
|
401
|
+
},
|
|
402
|
+
[fileNameAttribute]: {
|
|
403
|
+
default: null,
|
|
404
|
+
keepOnSplit: false,
|
|
405
|
+
parseHTML: (element) => element.getAttribute(fileNameAttribute),
|
|
406
|
+
rendered: true,
|
|
407
|
+
},
|
|
408
|
+
src: {
|
|
409
|
+
default: null,
|
|
410
|
+
keepOnSplit: false,
|
|
411
|
+
parseHTML: (element) => element.getAttribute('src'),
|
|
412
|
+
rendered: true,
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
},
|
|
416
|
+
parseHTML() {
|
|
417
|
+
return [
|
|
418
|
+
{
|
|
419
|
+
tag: `[${fileKeyAttribute}]`,
|
|
420
|
+
getAttrs: (element) => {
|
|
421
|
+
const fileKey = element.getAttribute(fileKeyAttribute);
|
|
422
|
+
const fileId = element.getAttribute(fileIdAttribute);
|
|
423
|
+
const alt = element.getAttribute('alt');
|
|
424
|
+
const type = element.getAttribute(fileTypeAttribute);
|
|
425
|
+
const name = element.getAttribute(fileNameAttribute);
|
|
426
|
+
const src = element.getAttribute('src');
|
|
427
|
+
return {
|
|
428
|
+
[fileKeyAttribute]: fileKey,
|
|
429
|
+
[fileIdAttribute]: fileId,
|
|
430
|
+
alt,
|
|
431
|
+
src,
|
|
432
|
+
[fileTypeAttribute]: type,
|
|
433
|
+
[fileNameAttribute]: name,
|
|
434
|
+
};
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
];
|
|
438
|
+
},
|
|
439
|
+
renderHTML(props) {
|
|
440
|
+
const src = props.node.attrs.src;
|
|
441
|
+
const fileKey = props.node.attrs[fileKeyAttribute];
|
|
442
|
+
const fileId = props.node.attrs[fileIdAttribute];
|
|
443
|
+
const alt = props.node.attrs.alt;
|
|
444
|
+
const name = props.node.attrs[fileNameAttribute];
|
|
445
|
+
const type = props.node.attrs[fileTypeAttribute];
|
|
446
|
+
|
|
447
|
+
const baseAttrs = {
|
|
448
|
+
[fileKeyAttribute]: fileKey,
|
|
449
|
+
[fileIdAttribute]: fileId,
|
|
450
|
+
alt,
|
|
451
|
+
[fileTypeAttribute]: type,
|
|
452
|
+
[fileNameAttribute]: name,
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
if (!src) {
|
|
456
|
+
return [
|
|
457
|
+
'div',
|
|
458
|
+
mergeAttributes(baseAttrs, props.HTMLAttributes),
|
|
459
|
+
'Missing file',
|
|
460
|
+
];
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (type?.startsWith('image/')) {
|
|
464
|
+
return [
|
|
465
|
+
'img',
|
|
466
|
+
mergeAttributes({ src, ...baseAttrs }, props.HTMLAttributes),
|
|
467
|
+
];
|
|
468
|
+
} else if (type?.startsWith('video/')) {
|
|
469
|
+
return [
|
|
470
|
+
'video',
|
|
471
|
+
mergeAttributes({ src, ...baseAttrs }, props.HTMLAttributes),
|
|
472
|
+
];
|
|
473
|
+
} else if (type?.startsWith('audio/')) {
|
|
474
|
+
return [
|
|
475
|
+
'audio',
|
|
476
|
+
mergeAttributes({ src, ...baseAttrs }, props.HTMLAttributes),
|
|
477
|
+
];
|
|
478
|
+
} else {
|
|
479
|
+
return [
|
|
480
|
+
'a',
|
|
481
|
+
mergeAttributes({ href: src, ...baseAttrs }, props.HTMLAttributes),
|
|
482
|
+
'Download',
|
|
483
|
+
];
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const verdantIdAttribute = 'data-verdant-oid';
|
|
2
|
+
|
|
3
|
+
export const fileKeyAttribute = 'data-verdant-file-key';
|
|
4
|
+
export const fileTypeAttribute = 'data-verdant-file-type';
|
|
5
|
+
export const fileNameAttribute = 'data-verdant-file-name';
|
|
6
|
+
export const fileIdAttribute = 'data-verdant-file-id';
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Server } from '@verdant-web/server';
|
|
2
|
+
import {
|
|
3
|
+
fileIdAttribute,
|
|
4
|
+
fileNameAttribute,
|
|
5
|
+
fileTypeAttribute,
|
|
6
|
+
} from '../extensions/attributes.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Attaches file URLs to all Verdant media nodes in the document, according
|
|
10
|
+
* to the file service attached to your Verdant server.
|
|
11
|
+
*/
|
|
12
|
+
export async function attachFileUrls(
|
|
13
|
+
document: any,
|
|
14
|
+
libraryId: string,
|
|
15
|
+
server: Server,
|
|
16
|
+
) {
|
|
17
|
+
await visitNode(document, libraryId, server);
|
|
18
|
+
return document;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function visitNode(node: any, libraryId: string, server: Server) {
|
|
22
|
+
if (node.type === 'verdant-media') {
|
|
23
|
+
const fileId = node.attrs[fileIdAttribute];
|
|
24
|
+
if (fileId) {
|
|
25
|
+
const file = await server.getFileData(libraryId, fileId);
|
|
26
|
+
if (file) {
|
|
27
|
+
node.attrs.src = file.url;
|
|
28
|
+
node.attrs[fileTypeAttribute] = file.type;
|
|
29
|
+
node.attrs[fileNameAttribute] = file.name;
|
|
30
|
+
node.attrs[fileIdAttribute] = file.id;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} else if (node.content) {
|
|
34
|
+
await Promise.all(
|
|
35
|
+
node.content.map((child: any) => visitNode(child, libraryId, server)),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|