@riducms/plugin-richtext 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/package.json +7 -5
- package/src/block/rich-text-block-card.scss +139 -0
- package/src/block/rich-text-block-card.svelte +42 -21
- package/src/block/rich-text-block-clipboard.ts +1 -1
- package/src/block/rich-text-block-node.ts +1 -1
- package/src/block/rich-text-blocks-plugin.svelte +5 -2
- package/src/document-validation.ts +7 -5
- package/src/field/reference-node-placement.ts +49 -0
- package/src/field/rich-text-blocks.ts +1 -5
- package/src/field/rich-text-config.ts +1 -4
- package/src/field/rich-text-document.ts +1 -4
- package/src/field/rich-text-editor.svelte +105 -57
- package/src/field/rich-text-footer.svelte +7 -12
- package/src/field/rich-text-markdown.ts +62 -0
- package/src/link/link-url.ts +30 -0
- package/src/link/rich-text-link-plugin.svelte +528 -0
- package/src/link/rich-text-link.scss +144 -0
- package/src/link/safe-link-transform.ts +20 -0
- package/src/link-url-policy.ts +14 -0
- package/src/menu/rich-text-block-toolbar.svelte +403 -70
- package/src/menu/rich-text-commands.ts +10 -1
- package/src/menu/rich-text-menu.scss +92 -0
- package/src/menu/rich-text-menu.svelte +62 -69
- package/src/menu/rich-text-option-filter.ts +30 -0
- package/src/menu/rich-text-options.ts +81 -84
- package/src/menu/rich-text-slash-menu.svelte +15 -11
- package/src/messages.ts +72 -27
- package/src/relationship/rich-text-relationship-node.scss +98 -0
- package/src/relationship/rich-text-relationship-node.svelte +38 -32
- package/src/relationship/rich-text-relationship-plugin.svelte +24 -40
- package/src/render/rich-text.svelte +10 -5
- package/src/render.ts +3 -8
- package/src/styles/integration.scss +384 -0
- package/src/toolbar/PAYLOAD-LICENSE.md +22 -0
- package/src/toolbar/rich-text-floating-toolbar.svelte +331 -169
- package/src/toolbar/rich-text-toolbar.scss +80 -0
- package/src/toolbar/toolbar-icon.svelte +200 -30
- package/src/upload/rich-text-upload-node.scss +308 -0
- package/src/upload/rich-text-upload-node.svelte +96 -125
- package/src/upload/rich-text-upload-node.ts +1 -0
- package/src/upload/rich-text-upload-plugin.svelte +10 -33
- package/src/styles/integration.css +0 -83
package/src/messages.ts
CHANGED
|
@@ -2,6 +2,27 @@ import { defineAdminMessages } from "@riducms/plugin";
|
|
|
2
2
|
|
|
3
3
|
export const richTextMessages = defineAdminMessages({
|
|
4
4
|
fallback: {
|
|
5
|
+
"menu.lists": "Lists",
|
|
6
|
+
"menu.basic": "Basic",
|
|
7
|
+
"menu.blocks": "Blocks",
|
|
8
|
+
"editor.insertParagraph": "Insert paragraph",
|
|
9
|
+
"link.textLabel": "Text to display",
|
|
10
|
+
"link.textRequired": "Text to display is required.",
|
|
11
|
+
"link.urlRequired": "URL is required.",
|
|
12
|
+
"link.urlInvalid": "Enter a valid web, email, or telephone URL.",
|
|
13
|
+
"link.newTab": "Open in a new tab",
|
|
14
|
+
"link.saveChanges": "Save changes",
|
|
15
|
+
"link.preview": "Link options",
|
|
16
|
+
"editor.textStyle": "Text style",
|
|
17
|
+
"editor.alignment": "Alignment",
|
|
18
|
+
"editor.heading": "Heading {level}",
|
|
19
|
+
"editor.align.left": "Align left",
|
|
20
|
+
"editor.align.center": "Align center",
|
|
21
|
+
"editor.align.right": "Align right",
|
|
22
|
+
"editor.align.justify": "Justify",
|
|
23
|
+
"editor.indent": "Increase indent",
|
|
24
|
+
"editor.outdent": "Decrease indent",
|
|
25
|
+
|
|
5
26
|
"block.label": "{label} block",
|
|
6
27
|
"block.select": "Select {label} block",
|
|
7
28
|
"block.edit": "Edit",
|
|
@@ -28,7 +49,6 @@ export const richTextMessages = defineAdminMessages({
|
|
|
28
49
|
"asset.unavailable": "Unavailable",
|
|
29
50
|
"editor.addBlock": "Add block",
|
|
30
51
|
"editor.addLink": "Add link",
|
|
31
|
-
"editor.apply": "Apply",
|
|
32
52
|
"editor.blockActions": "Block actions",
|
|
33
53
|
"editor.blockAlreadyFirst": "This block is already first.",
|
|
34
54
|
"editor.blockAlreadyLast": "This block is already last.",
|
|
@@ -53,6 +73,7 @@ export const richTextMessages = defineAdminMessages({
|
|
|
53
73
|
"editor.noMatchingBlocks": "No matching blocks",
|
|
54
74
|
"editor.placeholder": "Write body copy, or type / for commands…",
|
|
55
75
|
"editor.remove": "Remove",
|
|
76
|
+
"editor.removeLink": "Remove link",
|
|
56
77
|
"editor.removeNamed": "Remove {name}",
|
|
57
78
|
"editor.replaceNamed": "Replace {name}",
|
|
58
79
|
"editor.strikethrough": "Strikethrough",
|
|
@@ -71,12 +92,6 @@ export const richTextMessages = defineAdminMessages({
|
|
|
71
92
|
"option.divider.description": "Horizontal rule",
|
|
72
93
|
"option.divider.keywords": "divider horizontal rule hr",
|
|
73
94
|
"option.divider.label": "Divider",
|
|
74
|
-
"option.heading2.description": "Section heading",
|
|
75
|
-
"option.heading2.keywords": "heading title section h2",
|
|
76
|
-
"option.heading2.label": "Heading 2",
|
|
77
|
-
"option.heading3.description": "Subsection heading",
|
|
78
|
-
"option.heading3.keywords": "heading subtitle subsection h3",
|
|
79
|
-
"option.heading3.label": "Heading 3",
|
|
80
95
|
"option.numberedList.description": "Ordered list",
|
|
81
96
|
"option.numberedList.keywords": "numbered ordered list ol",
|
|
82
97
|
"option.numberedList.label": "Numbered list",
|
|
@@ -86,9 +101,9 @@ export const richTextMessages = defineAdminMessages({
|
|
|
86
101
|
"option.quote.description": "Quoted passage",
|
|
87
102
|
"option.quote.keywords": "quote blockquote citation",
|
|
88
103
|
"option.quote.label": "Quote",
|
|
89
|
-
"option.relationship.description": "Reference an existing
|
|
104
|
+
"option.relationship.description": "Reference an existing document",
|
|
90
105
|
"option.relationship.keywords": "relationship reference document related",
|
|
91
|
-
"option.relationship.label": "
|
|
106
|
+
"option.relationship.label": "Relationship",
|
|
92
107
|
"option.upload.description": "Select or upload {label}",
|
|
93
108
|
"option.upload.keywords": "upload image photo media file asset",
|
|
94
109
|
"relationship.actions": "Relationship actions",
|
|
@@ -99,6 +114,27 @@ export const richTextMessages = defineAdminMessages({
|
|
|
99
114
|
},
|
|
100
115
|
translations: {
|
|
101
116
|
fr: {
|
|
117
|
+
"menu.lists": "Listes",
|
|
118
|
+
"menu.basic": "Basique",
|
|
119
|
+
"menu.blocks": "Blocs",
|
|
120
|
+
"editor.insertParagraph": "Insérer un paragraphe",
|
|
121
|
+
"link.textLabel": "Texte à afficher",
|
|
122
|
+
"link.textRequired": "Le texte à afficher est obligatoire.",
|
|
123
|
+
"link.urlRequired": "L’URL est obligatoire.",
|
|
124
|
+
"link.urlInvalid": "Saisissez une URL web, e-mail ou téléphone valide.",
|
|
125
|
+
"link.newTab": "Ouvrir dans un nouvel onglet",
|
|
126
|
+
"link.saveChanges": "Enregistrer les modifications",
|
|
127
|
+
"link.preview": "Options du lien",
|
|
128
|
+
"editor.textStyle": "Style de texte",
|
|
129
|
+
"editor.alignment": "Alignement",
|
|
130
|
+
"editor.heading": "Titre {level}",
|
|
131
|
+
"editor.align.left": "Aligner à gauche",
|
|
132
|
+
"editor.align.center": "Centrer",
|
|
133
|
+
"editor.align.right": "Aligner à droite",
|
|
134
|
+
"editor.align.justify": "Justifier",
|
|
135
|
+
"editor.indent": "Augmenter le retrait",
|
|
136
|
+
"editor.outdent": "Diminuer le retrait",
|
|
137
|
+
|
|
102
138
|
"block.label": "Bloc {label}",
|
|
103
139
|
"block.select": "Sélectionner le bloc {label}",
|
|
104
140
|
"block.edit": "Modifier",
|
|
@@ -127,7 +163,6 @@ export const richTextMessages = defineAdminMessages({
|
|
|
127
163
|
"asset.unavailable": "Indisponible",
|
|
128
164
|
"editor.addBlock": "Ajouter un bloc",
|
|
129
165
|
"editor.addLink": "Ajouter un lien",
|
|
130
|
-
"editor.apply": "Appliquer",
|
|
131
166
|
"editor.blockActions": "Actions du bloc",
|
|
132
167
|
"editor.blockAlreadyFirst": "Ce bloc est déjà le premier.",
|
|
133
168
|
"editor.blockAlreadyLast": "Ce bloc est déjà le dernier.",
|
|
@@ -152,6 +187,7 @@ export const richTextMessages = defineAdminMessages({
|
|
|
152
187
|
"editor.noMatchingBlocks": "Aucun bloc correspondant",
|
|
153
188
|
"editor.placeholder": "Rédigez le contenu ou saisissez / pour les commandes…",
|
|
154
189
|
"editor.remove": "Supprimer",
|
|
190
|
+
"editor.removeLink": "Supprimer le lien",
|
|
155
191
|
"editor.removeNamed": "Supprimer {name}",
|
|
156
192
|
"editor.replaceNamed": "Remplacer {name}",
|
|
157
193
|
"editor.strikethrough": "Barré",
|
|
@@ -170,12 +206,6 @@ export const richTextMessages = defineAdminMessages({
|
|
|
170
206
|
"option.divider.description": "Ligne horizontale",
|
|
171
207
|
"option.divider.keywords": "séparateur ligne horizontale hr",
|
|
172
208
|
"option.divider.label": "Séparateur",
|
|
173
|
-
"option.heading2.description": "Titre de section",
|
|
174
|
-
"option.heading2.keywords": "titre section en-tête h2",
|
|
175
|
-
"option.heading2.label": "Titre 2",
|
|
176
|
-
"option.heading3.description": "Titre de sous-section",
|
|
177
|
-
"option.heading3.keywords": "titre sous-section sous-titre h3",
|
|
178
|
-
"option.heading3.label": "Titre 3",
|
|
179
209
|
"option.numberedList.description": "Liste ordonnée",
|
|
180
210
|
"option.numberedList.keywords": "liste numérotée ordonnée ol",
|
|
181
211
|
"option.numberedList.label": "Liste numérotée",
|
|
@@ -185,9 +215,9 @@ export const richTextMessages = defineAdminMessages({
|
|
|
185
215
|
"option.quote.description": "Passage cité",
|
|
186
216
|
"option.quote.keywords": "citation extrait bloc",
|
|
187
217
|
"option.quote.label": "Citation",
|
|
188
|
-
"option.relationship.description": "Référencer un
|
|
218
|
+
"option.relationship.description": "Référencer un document existant",
|
|
189
219
|
"option.relationship.keywords": "relation référence document associé",
|
|
190
|
-
"option.relationship.label": "
|
|
220
|
+
"option.relationship.label": "Relation",
|
|
191
221
|
"option.upload.description": "Sélectionner ou téléverser {label}",
|
|
192
222
|
"option.upload.keywords": "téléverser image photo média fichier ressource",
|
|
193
223
|
"relationship.actions": "Actions sur la relation",
|
|
@@ -197,6 +227,27 @@ export const richTextMessages = defineAdminMessages({
|
|
|
197
227
|
"relationship.unavailable": "Référence indisponible",
|
|
198
228
|
},
|
|
199
229
|
ar: {
|
|
230
|
+
"menu.lists": "القوائم",
|
|
231
|
+
"menu.basic": "أساسي",
|
|
232
|
+
"menu.blocks": "الكتل",
|
|
233
|
+
"editor.insertParagraph": "إدراج فقرة",
|
|
234
|
+
"link.textLabel": "النص المعروض",
|
|
235
|
+
"link.textRequired": "النص المعروض مطلوب.",
|
|
236
|
+
"link.urlRequired": "الرابط مطلوب.",
|
|
237
|
+
"link.urlInvalid": "أدخل رابط ويب أو بريد إلكتروني أو هاتف صالحًا.",
|
|
238
|
+
"link.newTab": "فتح في علامة تبويب جديدة",
|
|
239
|
+
"link.saveChanges": "حفظ التغييرات",
|
|
240
|
+
"link.preview": "خيارات الرابط",
|
|
241
|
+
"editor.textStyle": "نمط النص",
|
|
242
|
+
"editor.alignment": "المحاذاة",
|
|
243
|
+
"editor.heading": "عنوان {level}",
|
|
244
|
+
"editor.align.left": "محاذاة لليسار",
|
|
245
|
+
"editor.align.center": "توسيط",
|
|
246
|
+
"editor.align.right": "محاذاة لليمين",
|
|
247
|
+
"editor.align.justify": "ضبط",
|
|
248
|
+
"editor.indent": "زيادة المسافة البادئة",
|
|
249
|
+
"editor.outdent": "تقليل المسافة البادئة",
|
|
250
|
+
|
|
200
251
|
"block.label": "كتلة {label}",
|
|
201
252
|
"block.select": "تحديد كتلة {label}",
|
|
202
253
|
"block.edit": "تعديل",
|
|
@@ -223,7 +274,6 @@ export const richTextMessages = defineAdminMessages({
|
|
|
223
274
|
"asset.unavailable": "غير متاح",
|
|
224
275
|
"editor.addBlock": "إضافة كتلة",
|
|
225
276
|
"editor.addLink": "إضافة رابط",
|
|
226
|
-
"editor.apply": "تطبيق",
|
|
227
277
|
"editor.blockActions": "إجراءات الكتلة",
|
|
228
278
|
"editor.blockAlreadyFirst": "هذه الكتلة هي الأولى بالفعل.",
|
|
229
279
|
"editor.blockAlreadyLast": "هذه الكتلة هي الأخيرة بالفعل.",
|
|
@@ -248,6 +298,7 @@ export const richTextMessages = defineAdminMessages({
|
|
|
248
298
|
"editor.noMatchingBlocks": "لا توجد كتل مطابقة",
|
|
249
299
|
"editor.placeholder": "اكتب المحتوى أو اكتب / لعرض الأوامر…",
|
|
250
300
|
"editor.remove": "إزالة",
|
|
301
|
+
"editor.removeLink": "إزالة الرابط",
|
|
251
302
|
"editor.removeNamed": "إزالة {name}",
|
|
252
303
|
"editor.replaceNamed": "استبدال {name}",
|
|
253
304
|
"editor.strikethrough": "يتوسطه خط",
|
|
@@ -266,12 +317,6 @@ export const richTextMessages = defineAdminMessages({
|
|
|
266
317
|
"option.divider.description": "خط أفقي",
|
|
267
318
|
"option.divider.keywords": "فاصل خط أفقي hr",
|
|
268
319
|
"option.divider.label": "فاصل",
|
|
269
|
-
"option.heading2.description": "عنوان قسم",
|
|
270
|
-
"option.heading2.keywords": "عنوان قسم رئيسي h2",
|
|
271
|
-
"option.heading2.label": "عنوان 2",
|
|
272
|
-
"option.heading3.description": "عنوان قسم فرعي",
|
|
273
|
-
"option.heading3.keywords": "عنوان قسم فرعي h3",
|
|
274
|
-
"option.heading3.label": "عنوان 3",
|
|
275
320
|
"option.numberedList.description": "قائمة مرتبة",
|
|
276
321
|
"option.numberedList.keywords": "قائمة مرقمة مرتبة ol",
|
|
277
322
|
"option.numberedList.label": "قائمة مرقمة",
|
|
@@ -281,9 +326,9 @@ export const richTextMessages = defineAdminMessages({
|
|
|
281
326
|
"option.quote.description": "نص مقتبس",
|
|
282
327
|
"option.quote.keywords": "اقتباس نص مقتبس",
|
|
283
328
|
"option.quote.label": "اقتباس",
|
|
284
|
-
"option.relationship.description": "الإشارة إلى
|
|
329
|
+
"option.relationship.description": "الإشارة إلى مستند موجود",
|
|
285
330
|
"option.relationship.keywords": "علاقة مرجع مستند مرتبط",
|
|
286
|
-
"option.relationship.label": "
|
|
331
|
+
"option.relationship.label": "علاقة",
|
|
287
332
|
"option.upload.description": "اختيار أو رفع {label}",
|
|
288
333
|
"option.upload.keywords": "رفع صورة وسائط ملف أصل",
|
|
289
334
|
"relationship.actions": "إجراءات العلاقة",
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
@use "@riducms/ui/styles.scss" as ui;
|
|
2
|
+
|
|
3
|
+
@layer ridu.components {
|
|
4
|
+
.ridu-richtext-relationship-card {
|
|
5
|
+
display: flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
gap: 12px;
|
|
8
|
+
inline-size: min(100%, 360px);
|
|
9
|
+
margin-block: 12px;
|
|
10
|
+
padding: 14px 15px;
|
|
11
|
+
border: 1px solid var(--control-border);
|
|
12
|
+
border-radius: var(--control-radius);
|
|
13
|
+
background: var(--control-surface);
|
|
14
|
+
box-shadow: var(--control-shadow);
|
|
15
|
+
color: var(--foreground);
|
|
16
|
+
font-family: var(--font-sans);
|
|
17
|
+
line-height: var(--line-height-body);
|
|
18
|
+
transition:
|
|
19
|
+
border-color 100ms,
|
|
20
|
+
box-shadow 100ms;
|
|
21
|
+
|
|
22
|
+
&:hover {
|
|
23
|
+
border-color: var(--control-border-hover);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&.is-selected {
|
|
27
|
+
border-color: var(--foreground-muted);
|
|
28
|
+
box-shadow:
|
|
29
|
+
var(--control-shadow),
|
|
30
|
+
0 0 0 1px var(--foreground-muted);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.ridu-richtext-relationship-card__content {
|
|
35
|
+
display: grid;
|
|
36
|
+
flex: 1;
|
|
37
|
+
gap: 5px;
|
|
38
|
+
min-width: 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.ridu-richtext-relationship-card__label,
|
|
42
|
+
.ridu-richtext-relationship-card__title {
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
line-height: 16px;
|
|
45
|
+
text-overflow: ellipsis;
|
|
46
|
+
white-space: nowrap;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.ridu-richtext-relationship-card__label {
|
|
50
|
+
color: var(--foreground-muted);
|
|
51
|
+
font-size: 12px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.ridu-richtext-relationship-card__title {
|
|
55
|
+
display: block;
|
|
56
|
+
min-width: 0;
|
|
57
|
+
max-width: 100%;
|
|
58
|
+
border: 0;
|
|
59
|
+
padding: 0;
|
|
60
|
+
background: transparent;
|
|
61
|
+
color: var(--foreground);
|
|
62
|
+
font: inherit;
|
|
63
|
+
font-weight: 600;
|
|
64
|
+
text-align: start;
|
|
65
|
+
text-decoration: underline;
|
|
66
|
+
text-decoration-thickness: 1px;
|
|
67
|
+
text-underline-offset: 2px;
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
|
|
70
|
+
&:focus-visible {
|
|
71
|
+
@include ui.focus-outline;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
&:disabled {
|
|
75
|
+
color: var(--control-disabled-foreground);
|
|
76
|
+
text-decoration: none;
|
|
77
|
+
cursor: default;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.ridu-richtext-relationship-card__actions {
|
|
82
|
+
display: flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
flex-shrink: 0;
|
|
85
|
+
gap: 3px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@media (max-width: 30rem) {
|
|
89
|
+
.ridu-richtext-relationship-card {
|
|
90
|
+
align-items: flex-start;
|
|
91
|
+
flex-wrap: wrap;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.ridu-richtext-relationship-card__actions {
|
|
95
|
+
margin-inline-start: auto;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -5,13 +5,17 @@
|
|
|
5
5
|
useLexicalNodeSelection,
|
|
6
6
|
} from "@hvniel/lexical-svelte";
|
|
7
7
|
import { getAdminI18n } from "@riducms/plugin";
|
|
8
|
+
import { Button } from "@riducms/ui";
|
|
8
9
|
import type { NodeKey } from "lexical";
|
|
10
|
+
import ReplaceIcon from "~icons/lucide/refresh-cw";
|
|
11
|
+
import RemoveIcon from "~icons/lucide/x";
|
|
9
12
|
|
|
10
13
|
import { getRichTextAuthoringHost } from "@plugin-richtext/field/rich-text-context.svelte";
|
|
11
14
|
import {
|
|
12
15
|
OPEN_RELATIONSHIP_BROWSER_COMMAND,
|
|
13
16
|
REMOVE_RELATIONSHIP_COMMAND,
|
|
14
17
|
} from "@plugin-richtext/menu/rich-text-commands";
|
|
18
|
+
import "@plugin-richtext/relationship/rich-text-relationship-node.scss";
|
|
15
19
|
|
|
16
20
|
let {
|
|
17
21
|
documentID,
|
|
@@ -44,6 +48,8 @@
|
|
|
44
48
|
if (authoring === undefined) return;
|
|
45
49
|
void authoring.documentRevision;
|
|
46
50
|
const request = new AbortController();
|
|
51
|
+
document = undefined;
|
|
52
|
+
loadError = false;
|
|
47
53
|
authoring
|
|
48
54
|
.findDocument(relationTo, documentID, request.signal)
|
|
49
55
|
.then((value) => {
|
|
@@ -88,54 +94,54 @@
|
|
|
88
94
|
</script>
|
|
89
95
|
|
|
90
96
|
<article
|
|
91
|
-
class={[
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
]}
|
|
97
|
+
class={["ridu-richtext-relationship-card", isSelected() && "is-selected"]}
|
|
98
|
+
data-selected={isSelected() || undefined}
|
|
99
|
+
aria-busy={authoring !== undefined && document === undefined && !loadError}
|
|
95
100
|
role="group"
|
|
96
101
|
aria-label={i18n.t("plugin.richtext:relationship.label", {
|
|
97
102
|
collection: collection?.labels.singular ?? relationTo,
|
|
98
103
|
title,
|
|
99
104
|
})}
|
|
100
105
|
>
|
|
101
|
-
<
|
|
102
|
-
class="
|
|
103
|
-
aria-hidden="true"
|
|
104
|
-
>
|
|
105
|
-
{(collection?.labels.singular ?? relationTo).slice(0, 2).toLocaleUpperCase(i18n.language)}
|
|
106
|
-
</span>
|
|
107
|
-
<button
|
|
108
|
-
type="button"
|
|
109
|
-
class="min-w-0 flex-1 cursor-pointer border-0 bg-transparent p-0 text-start outline-none"
|
|
110
|
-
disabled={!isEditable()}
|
|
111
|
-
onclick={openDocument}
|
|
112
|
-
>
|
|
113
|
-
<span class="block truncate text-[13.5px] font-medium text-foreground-strong">
|
|
114
|
-
{loadError ? i18n.t("plugin.richtext:relationship.unavailable") : title}
|
|
115
|
-
</span>
|
|
116
|
-
<span class="font-mono mt-0.5 block truncate text-[10px] text-foreground-faint">
|
|
106
|
+
<div class="ridu-richtext-relationship-card__content" aria-live="polite">
|
|
107
|
+
<span class="ridu-richtext-relationship-card__label">
|
|
117
108
|
{collection?.labels.singular ?? relationTo} · {documentID}
|
|
118
109
|
</span>
|
|
119
|
-
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
class="ridu-richtext-relationship-card__title"
|
|
113
|
+
disabled={!isEditable()}
|
|
114
|
+
onclick={openDocument}
|
|
115
|
+
>
|
|
116
|
+
{loadError ? i18n.t("plugin.richtext:relationship.unavailable") : title}
|
|
117
|
+
</button>
|
|
118
|
+
</div>
|
|
120
119
|
{#if isEditable()}
|
|
121
120
|
<div
|
|
122
|
-
class="
|
|
121
|
+
class="ridu-richtext-relationship-card__actions"
|
|
122
|
+
role="group"
|
|
123
123
|
aria-label={i18n.t("plugin.richtext:relationship.actions")}
|
|
124
124
|
>
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
<Button
|
|
126
|
+
class="ridu-richtext-relationship-card__action"
|
|
127
|
+
variant="ghost"
|
|
128
|
+
size="icon-sm"
|
|
128
129
|
onclick={replaceDocument}
|
|
130
|
+
aria-label={i18n.t("plugin.richtext:relationship.replace")}
|
|
131
|
+
tooltip={i18n.t("plugin.richtext:relationship.replace")}
|
|
129
132
|
>
|
|
130
|
-
|
|
131
|
-
</
|
|
132
|
-
<
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
<ReplaceIcon />
|
|
134
|
+
</Button>
|
|
135
|
+
<Button
|
|
136
|
+
class="ridu-richtext-relationship-card__action"
|
|
137
|
+
variant="ghost"
|
|
138
|
+
size="icon-sm"
|
|
135
139
|
onclick={removeRelationship}
|
|
140
|
+
aria-label={i18n.t("plugin.richtext:relationship.remove")}
|
|
141
|
+
tooltip={i18n.t("plugin.richtext:relationship.remove")}
|
|
136
142
|
>
|
|
137
|
-
|
|
138
|
-
</
|
|
143
|
+
<RemoveIcon />
|
|
144
|
+
</Button>
|
|
139
145
|
</div>
|
|
140
146
|
{/if}
|
|
141
147
|
</article>
|
|
@@ -1,19 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { FieldAuthoringHost } from "@riducms/plugin";
|
|
3
3
|
import type { SchemaCollection, SchemaField } from "@riducms/protocol";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
$createParagraphNode,
|
|
7
|
-
$getNodeByKey,
|
|
8
|
-
$getPreviousSelection,
|
|
9
|
-
$getRoot,
|
|
10
|
-
$getSelection,
|
|
11
|
-
$isElementNode,
|
|
12
|
-
$isParagraphNode,
|
|
13
|
-
$isRangeSelection,
|
|
14
|
-
COMMAND_PRIORITY_EDITOR,
|
|
15
|
-
type NodeKey,
|
|
16
|
-
} from "lexical";
|
|
4
|
+
import { mergeRegister } from "@lexical/utils";
|
|
5
|
+
import { $getNodeByKey, COMMAND_PRIORITY_EDITOR, type NodeKey } from "lexical";
|
|
17
6
|
import { useLexicalComposerContext } from "@hvniel/lexical-svelte";
|
|
18
7
|
|
|
19
8
|
import {
|
|
@@ -21,6 +10,10 @@
|
|
|
21
10
|
REMOVE_RELATIONSHIP_COMMAND,
|
|
22
11
|
} from "@plugin-richtext/menu/rich-text-commands";
|
|
23
12
|
import type { RichTextConfig } from "@plugin-richtext/field/rich-text-config";
|
|
13
|
+
import {
|
|
14
|
+
insertReferenceNode,
|
|
15
|
+
removeReferenceNode,
|
|
16
|
+
} from "@plugin-richtext/field/reference-node-placement";
|
|
24
17
|
import {
|
|
25
18
|
createRelationshipNode,
|
|
26
19
|
isRelationshipNode,
|
|
@@ -38,7 +31,7 @@
|
|
|
38
31
|
} = $props();
|
|
39
32
|
const editor = useLexicalComposerContext()[0];
|
|
40
33
|
let browserOpen = $state(false);
|
|
41
|
-
let targetCollection = $state<SchemaCollection>();
|
|
34
|
+
let targetCollection = $state.raw<SchemaCollection>();
|
|
42
35
|
let documentID = $state<string>();
|
|
43
36
|
let browserMode = $state<"edit" | "insert" | "replace">("insert");
|
|
44
37
|
let replaceNodeKey: NodeKey | undefined;
|
|
@@ -61,9 +54,12 @@
|
|
|
61
54
|
editor.registerCommand(
|
|
62
55
|
OPEN_RELATIONSHIP_BROWSER_COMMAND,
|
|
63
56
|
(payload) => {
|
|
64
|
-
const collection =
|
|
65
|
-
|
|
66
|
-
|
|
57
|
+
const collection =
|
|
58
|
+
payload.collectionSlug === undefined
|
|
59
|
+
? relationshipCollections[0]
|
|
60
|
+
: relationshipCollections.find(
|
|
61
|
+
(candidate) => candidate.slug === payload.collectionSlug
|
|
62
|
+
);
|
|
67
63
|
if (collection === undefined) return false;
|
|
68
64
|
browserMode =
|
|
69
65
|
payload.mode ??
|
|
@@ -85,16 +81,7 @@
|
|
|
85
81
|
(nodeKey) => {
|
|
86
82
|
const node = $getNodeByKey(nodeKey);
|
|
87
83
|
if (!isRelationshipNode(node)) return false;
|
|
88
|
-
|
|
89
|
-
const previous = node.getPreviousSibling();
|
|
90
|
-
node.remove();
|
|
91
|
-
if ($isElementNode(next)) next.selectStart();
|
|
92
|
-
else if ($isElementNode(previous)) previous.selectEnd();
|
|
93
|
-
else {
|
|
94
|
-
const paragraph = $createParagraphNode();
|
|
95
|
-
$getRoot().append(paragraph);
|
|
96
|
-
paragraph.select();
|
|
97
|
-
}
|
|
84
|
+
removeReferenceNode(node);
|
|
98
85
|
return true;
|
|
99
86
|
},
|
|
100
87
|
COMMAND_PRIORITY_EDITOR
|
|
@@ -102,10 +89,13 @@
|
|
|
102
89
|
);
|
|
103
90
|
});
|
|
104
91
|
|
|
105
|
-
function commit(ids: string[]) {
|
|
92
|
+
function commit(ids: string[], collectionSlug: string) {
|
|
106
93
|
const id = ids[0];
|
|
107
|
-
|
|
108
|
-
|
|
94
|
+
if (
|
|
95
|
+
id === undefined ||
|
|
96
|
+
!relationshipCollections.some((collection) => collection.slug === collectionSlug)
|
|
97
|
+
)
|
|
98
|
+
return;
|
|
109
99
|
if (browserMode === "edit") {
|
|
110
100
|
closeBrowser();
|
|
111
101
|
return;
|
|
@@ -113,21 +103,14 @@
|
|
|
113
103
|
editor.update(() => {
|
|
114
104
|
if (replaceNodeKey !== undefined) {
|
|
115
105
|
const current = $getNodeByKey(replaceNodeKey);
|
|
116
|
-
if (isRelationshipNode(current)) current.setReference(
|
|
106
|
+
if (isRelationshipNode(current)) current.setReference(collectionSlug, id);
|
|
117
107
|
return;
|
|
118
108
|
}
|
|
119
|
-
const selection = $getSelection() ?? $getPreviousSelection();
|
|
120
|
-
if (!$isRangeSelection(selection)) $getRoot().selectEnd();
|
|
121
|
-
const focusNode = $isRangeSelection(selection) ? selection.focus.getNode() : undefined;
|
|
122
109
|
const relationship = createRelationshipNode({
|
|
123
110
|
documentID: id,
|
|
124
|
-
relationTo:
|
|
111
|
+
relationTo: collectionSlug,
|
|
125
112
|
});
|
|
126
|
-
|
|
127
|
-
if ($isParagraphNode(focusNode) && focusNode.getChildrenSize() === 0) focusNode.remove();
|
|
128
|
-
const paragraph = $createParagraphNode();
|
|
129
|
-
relationship.insertAfter(paragraph);
|
|
130
|
-
paragraph.select();
|
|
113
|
+
insertReferenceNode(relationship);
|
|
131
114
|
});
|
|
132
115
|
closeBrowser();
|
|
133
116
|
}
|
|
@@ -165,6 +148,7 @@
|
|
|
165
148
|
open
|
|
166
149
|
field={browserField}
|
|
167
150
|
collection={targetCollection}
|
|
151
|
+
{...browserMode === "edit" ? {} : { collections: relationshipCollections }}
|
|
168
152
|
hasMany={false}
|
|
169
153
|
selectedIDs={documentID === undefined ? [] : [documentID]}
|
|
170
154
|
{...browserMode === "edit" && documentID !== undefined ? { initialDocumentID: documentID } : {}}
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
{#each values as node}
|
|
41
41
|
{#if node.type === "block"}
|
|
42
42
|
{const Block = $derived(component(node.fields))}
|
|
43
|
-
{#if Block !== undefined}
|
|
43
|
+
{#if Block !== undefined}
|
|
44
|
+
<Block block={node.fields} />
|
|
44
45
|
{:else}
|
|
45
46
|
{const failure = $derived(
|
|
46
47
|
unsupported(
|
|
@@ -52,10 +53,13 @@
|
|
|
52
53
|
{/if}
|
|
53
54
|
{:else if node.type === "text"}
|
|
54
55
|
{@html renderRichTextText(node)}
|
|
55
|
-
{:else if node.type === "linebreak"}
|
|
56
|
-
|
|
56
|
+
{:else if node.type === "linebreak"}
|
|
57
|
+
<br />
|
|
58
|
+
{:else if node.type === "horizontalrule"}
|
|
59
|
+
<hr />
|
|
57
60
|
{:else if node.type === "upload" || node.type === "relationship"}
|
|
58
|
-
{#if references !== undefined}
|
|
61
|
+
{#if references !== undefined}
|
|
62
|
+
{@render references(node)}
|
|
59
63
|
{:else}
|
|
60
64
|
{const failure = $derived(
|
|
61
65
|
unsupported(node, `No renderer registered for rich-text node ${node.type}`)
|
|
@@ -63,7 +67,8 @@
|
|
|
63
67
|
{@render failure.render(failure.node, failure.error)}
|
|
64
68
|
{/if}
|
|
65
69
|
{:else if node.type === "paragraph" || node.type === "heading" || node.type === "quote" || node.type === "link" || node.type === "list" || node.type === "listitem" || node.type === "code"}
|
|
66
|
-
{#if node.type === "code"}
|
|
70
|
+
{#if node.type === "code"}
|
|
71
|
+
<pre><code>{@render nodes(node.children)}</code></pre>
|
|
67
72
|
{:else}
|
|
68
73
|
<svelte:element
|
|
69
74
|
this={richTextElementTag(node)}
|
package/src/render.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
RichTextTextNode,
|
|
7
7
|
} from "#richtext/document";
|
|
8
8
|
import { documentRecoveryIssue } from "#richtext/document-validation";
|
|
9
|
+
import { renderableRichTextURL } from "#richtext/link-url-policy";
|
|
9
10
|
|
|
10
11
|
export type { RichTextBlockRenderers } from "#richtext/document";
|
|
11
12
|
export { documentRecoveryIssue } from "#richtext/document-validation";
|
|
@@ -33,14 +34,8 @@ export function escapeRichText(value: string): string {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export function safeRichTextURL(value: string): string {
|
|
36
|
-
const url = value
|
|
37
|
-
if (
|
|
38
|
-
/[\u0000-\u001f\u007f]/u.test(value) ||
|
|
39
|
-
url.startsWith("//") ||
|
|
40
|
-
(/^[a-z][a-z\d+.-]*:/iu.test(url) && !/^(https?|mailto|tel):/iu.test(url))
|
|
41
|
-
) {
|
|
42
|
-
throw new Error("Unsafe rich-text link URL");
|
|
43
|
-
}
|
|
37
|
+
const url = renderableRichTextURL(value);
|
|
38
|
+
if (url === undefined) throw new Error("Unsafe rich-text link URL");
|
|
44
39
|
return url;
|
|
45
40
|
}
|
|
46
41
|
|