@riducms/plugin-richtext 0.2.3 → 0.4.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.
Files changed (43) hide show
  1. package/README.md +7 -1
  2. package/package.json +7 -5
  3. package/src/block/rich-text-block-card.scss +139 -0
  4. package/src/block/rich-text-block-card.svelte +42 -21
  5. package/src/block/rich-text-block-clipboard.ts +1 -1
  6. package/src/block/rich-text-block-node.ts +1 -1
  7. package/src/block/rich-text-blocks-plugin.svelte +5 -2
  8. package/src/document-validation.ts +7 -5
  9. package/src/field/reference-node-placement.ts +49 -0
  10. package/src/field/rich-text-blocks.ts +1 -5
  11. package/src/field/rich-text-config.ts +1 -4
  12. package/src/field/rich-text-document.ts +1 -4
  13. package/src/field/rich-text-editor.svelte +105 -57
  14. package/src/field/rich-text-footer.svelte +7 -12
  15. package/src/field/rich-text-markdown.ts +62 -0
  16. package/src/link/link-url.ts +30 -0
  17. package/src/link/rich-text-link-plugin.svelte +528 -0
  18. package/src/link/rich-text-link.scss +144 -0
  19. package/src/link/safe-link-transform.ts +20 -0
  20. package/src/link-url-policy.ts +14 -0
  21. package/src/menu/rich-text-block-toolbar.svelte +403 -70
  22. package/src/menu/rich-text-commands.ts +10 -1
  23. package/src/menu/rich-text-menu.scss +92 -0
  24. package/src/menu/rich-text-menu.svelte +62 -69
  25. package/src/menu/rich-text-option-filter.ts +30 -0
  26. package/src/menu/rich-text-options.ts +81 -84
  27. package/src/menu/rich-text-slash-menu.svelte +15 -11
  28. package/src/messages.ts +72 -27
  29. package/src/relationship/rich-text-relationship-node.scss +98 -0
  30. package/src/relationship/rich-text-relationship-node.svelte +38 -32
  31. package/src/relationship/rich-text-relationship-plugin.svelte +24 -40
  32. package/src/render/rich-text.svelte +10 -5
  33. package/src/render.ts +3 -8
  34. package/src/styles/integration.scss +384 -0
  35. package/src/toolbar/PAYLOAD-LICENSE.md +22 -0
  36. package/src/toolbar/rich-text-floating-toolbar.svelte +331 -169
  37. package/src/toolbar/rich-text-toolbar.scss +80 -0
  38. package/src/toolbar/toolbar-icon.svelte +200 -30
  39. package/src/upload/rich-text-upload-node.scss +308 -0
  40. package/src/upload/rich-text-upload-node.svelte +96 -125
  41. package/src/upload/rich-text-upload-node.ts +1 -0
  42. package/src/upload/rich-text-upload-plugin.svelte +10 -33
  43. 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 {label}",
104
+ "option.relationship.description": "Reference an existing document",
90
105
  "option.relationship.keywords": "relationship reference document related",
91
- "option.relationship.label": "Related {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 élément {label} existant",
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": "{label} associé",
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": "الإشارة إلى {label} موجود",
329
+ "option.relationship.description": "الإشارة إلى مستند موجود",
285
330
  "option.relationship.keywords": "علاقة مرجع مستند مرتبط",
286
- "option.relationship.label": "{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
- "group/relationship relative my-6 flex items-center gap-3 rounded-[4px] border border-control-border bg-control px-4 py-3.5 transition-colors hover:border-primary/45",
93
- isSelected() && "border-primary/50",
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
- <span
102
- class="font-mono grid size-9 shrink-0 place-items-center rounded-full bg-primary/10 text-[11px] font-semibold text-brand-primary-soft"
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
- </button>
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="flex shrink-0 items-center gap-1"
121
+ class="ridu-richtext-relationship-card__actions"
122
+ role="group"
123
123
  aria-label={i18n.t("plugin.richtext:relationship.actions")}
124
124
  >
125
- <button
126
- type="button"
127
- class="rounded-[3px] px-2 py-1.5 text-[11px] text-foreground-muted hover:bg-control-hover hover:text-foreground"
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
- {i18n.t("plugin.richtext:relationship.replace")}
131
- </button>
132
- <button
133
- type="button"
134
- class="rounded-[3px] px-2 py-1.5 text-[11px] text-destructive hover:bg-destructive/10"
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
- {i18n.t("plugin.richtext:relationship.remove")}
138
- </button>
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 { $insertNodeToNearestRoot, mergeRegister } from "@lexical/utils";
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 = relationshipCollections.find(
65
- (candidate) => candidate.slug === payload.collectionSlug
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
- const next = node.getNextSibling();
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
- const collection = targetCollection;
108
- if (id === undefined || collection === undefined) return;
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(collection.slug, id);
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: collection.slug,
111
+ relationTo: collectionSlug,
125
112
  });
126
- $insertNodeToNearestRoot(relationship);
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}<Block block={node.fields} />
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"}<br />
56
- {:else if node.type === "horizontalrule"}<hr />
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}{@render references(node)}
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"}<pre><code>{@render nodes(node.children)}</code></pre>
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.trim();
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