@rigel-cms/rigel 0.1.4-beta.1 → 1.0.0-rc.1
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 +4 -2
- package/package.json +3 -4
- package/src/article-body.ts +9 -2
- package/src/article-render.ts +27 -1
- package/src/config.ts +8 -0
- package/src/content.ts +3 -1
- package/src/editor/App.svelte +114 -25
- package/src/editor/blocks/embed.ts +21 -0
- package/src/editor/blocks/index.ts +3 -1
- package/src/editor/blocks/video.ts +35 -0
- package/src/editor/lib/ActionDialog.svelte +2 -1
- package/src/editor/lib/BlockEditor.svelte +84 -15
- package/src/editor/lib/CollectionPanel.svelte +9 -4
- package/src/editor/lib/ImagePicker.svelte +141 -47
- package/src/editor/lib/MediaPicker.svelte +73 -0
- package/src/editor/lib/PreviewFrame.svelte +64 -22
- package/src/editor/lib/PublishDialog.svelte +65 -0
- package/src/editor/lib/SchemaPanel.svelte +6 -1
- package/src/editor/lib/Select.svelte +5 -2
- package/src/editor/lib/VisualEditor.svelte +11 -6
- package/src/editor/lib/embed-preview.ts +57 -0
- package/src/editor/lib/embeds.ts +47 -0
- package/src/editor/lib/github-backend.ts +220 -20
- package/src/editor/lib/icons.ts +13 -0
- package/src/editor/lib/images.ts +20 -8
- package/src/editor/lib/media-library.ts +11 -1
- package/src/editor/lib/settings.ts +2 -2
- package/src/editor/lib/slugify.ts +1 -15
- package/src/editor/lib/tooltip.ts +23 -4
- package/src/editor/styles.css +44 -13
- package/src/env.d.ts +1 -1
- package/src/index.ts +4 -6
- package/src/json-output.ts +5 -0
- package/src/manifest/types.ts +1 -0
- package/src/media-types.ts +14 -1
- package/src/preview.ts +14 -3
- package/src/routes/local-documents.ts +4 -4
- package/src/routes/local-media.ts +33 -28
- package/src/routes/local-menu.ts +2 -1
- package/src/routes/local-post.ts +18 -5
- package/src/routes/media-files.ts +36 -1
- package/src/routes/media-validation.ts +2 -2
- package/src/schema/paths.ts +3 -1
- package/src/schema/store.ts +24 -3
- package/src/slugs.ts +46 -0
- package/src/vite/manifest-plugin.ts +9 -2
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
import * as Links from "@lexical/link";
|
|
8
8
|
import * as Tables from "@lexical/table";
|
|
9
9
|
import { ImageNode, type ImageData } from "../blocks/image";
|
|
10
|
+
import { VideoNode, type VideoData } from "../blocks/video";
|
|
11
|
+
import { EmbedNode, type EmbedData } from "../blocks/embed";
|
|
10
12
|
import ImagePicker from "./ImagePicker.svelte";
|
|
13
|
+
import MediaPicker from "./MediaPicker.svelte";
|
|
11
14
|
import type { StockResult } from "./images";
|
|
12
15
|
import { safePreviewUrl } from "../../preview-urls";
|
|
13
16
|
import { blockTypes, blockNodes } from "../blocks";
|
|
@@ -58,7 +61,12 @@
|
|
|
58
61
|
let linkPosition = $state("");
|
|
59
62
|
let editorHost: HTMLDivElement;
|
|
60
63
|
let imageJob: { key: string; replace: boolean } | null = null;
|
|
64
|
+
let mediaJob: { key: string; replace: boolean } | null = null;
|
|
65
|
+
let mediaPicker: MediaPicker;
|
|
61
66
|
let imageData = $state<ImageData | null>(null);
|
|
67
|
+
let embedData = $state<EmbedData | null>(null);
|
|
68
|
+
let videoData = $state<VideoData | null>(null);
|
|
69
|
+
const selectedVideo = $derived(embedData ?? videoData);
|
|
62
70
|
let altOpen = $state(false);
|
|
63
71
|
let altText = $state("");
|
|
64
72
|
let altKey = "";
|
|
@@ -81,7 +89,8 @@
|
|
|
81
89
|
let tableRows = $state(0);
|
|
82
90
|
let tableColumns = $state(0);
|
|
83
91
|
let tableHeader = $state(false);
|
|
84
|
-
const
|
|
92
|
+
const mediaTypes = [{ value: "video", label: "Video" }, { value: "youtube", label: "YouTube" }, { value: "vimeo", label: "Vimeo" }] as const;
|
|
93
|
+
const choices = $derived([...blockTypes, ...mediaTypes].filter(block => block.label.toLowerCase().includes((slash ?? "").toLowerCase())));
|
|
85
94
|
const textBlocks = blockTypes.filter(block => !["image", "table", "bullet", "number"].includes(block.value));
|
|
86
95
|
const activeBlock = $derived(blockTypes.find(block => block.value === blockType));
|
|
87
96
|
|
|
@@ -106,6 +115,8 @@
|
|
|
106
115
|
}
|
|
107
116
|
if (node) {
|
|
108
117
|
imageData = node instanceof ImageNode ? { ...node.__image } : null;
|
|
118
|
+
embedData = node instanceof EmbedNode ? { ...node.__embed } : null;
|
|
119
|
+
videoData = node instanceof VideoNode ? { ...node.__video } : null;
|
|
109
120
|
activeKey = node.getKey();
|
|
110
121
|
blockType = Rich.$isHeadingNode(node) ? node.getTag() : Lists.$isListNode(node) ? node.getListType() : node.getType();
|
|
111
122
|
activeMarks = L.$isRangeSelection(selection) ? marks.filter(mark => selection.hasFormat(mark.type)).map(mark => mark.type) : [];
|
|
@@ -125,16 +136,17 @@
|
|
|
125
136
|
positionInsertion();
|
|
126
137
|
}
|
|
127
138
|
function positionFloating() {
|
|
128
|
-
if (imageJob) return;
|
|
139
|
+
if (imageJob || mediaJob) return;
|
|
129
140
|
const selection = view.getSelection();
|
|
130
141
|
if (!root.ownerDocument.hasFocus()) { floating = ""; return; }
|
|
131
142
|
if (editorHost.contains(root.ownerDocument.activeElement) && !root.contains(root.ownerDocument.activeElement)) return;
|
|
132
|
-
if (inline || imageData) {
|
|
133
|
-
if (imageData ? root.ownerDocument.activeElement !== root : !selection?.rangeCount || !root.contains(selection.anchorNode) || !root.contains(selection.focusNode)) { floating = ""; return; }
|
|
143
|
+
if (inline || imageData || selectedVideo) {
|
|
144
|
+
if (imageData || selectedVideo ? root.ownerDocument.activeElement !== root : !selection?.rangeCount || !root.contains(selection.anchorNode) || !root.contains(selection.focusNode)) { floating = ""; return; }
|
|
134
145
|
const block = editor.getElementByKey(activeKey);
|
|
135
146
|
if (!block) { floating = ""; return; }
|
|
136
|
-
const rect = imageData || selection!.isCollapsed ? block.getBoundingClientRect() : selection!.getRangeAt(0).getBoundingClientRect();
|
|
147
|
+
const rect = imageData || selectedVideo || selection!.isCollapsed ? block.getBoundingClientRect() : selection!.getRangeAt(0).getBoundingClientRect();
|
|
137
148
|
if (rect.bottom < 0 || rect.top > view.innerHeight) { floating = ""; return; }
|
|
149
|
+
if (selectedVideo) { floating = ""; return; }
|
|
138
150
|
const toolbarRect = editorHost.querySelector(imageData ? '.image-toolbar' : '.body-toolbar')?.getBoundingClientRect();
|
|
139
151
|
const toolbarHeight = toolbarRect?.height || 42, toolbarWidth = toolbarRect?.width || 330;
|
|
140
152
|
const top = rect.top >= toolbarHeight + 12 ? rect.top - toolbarHeight - 8 : rect.bottom + 8;
|
|
@@ -192,7 +204,7 @@
|
|
|
192
204
|
}, L.COMMAND_PRIORITY_CRITICAL));
|
|
193
205
|
cleanups.push(editor.registerCommand(L.DROP_COMMAND, event => { event.preventDefault(); return true; }, L.COMMAND_PRIORITY_CRITICAL));
|
|
194
206
|
cleanups.push(editor.registerCommand(L.KEY_DOWN_COMMAND, event => {
|
|
195
|
-
if ((inline || imageData) && event.altKey && event.key === "F10") { event.preventDefault(); editorHost.querySelector<HTMLButtonElement>(imageData ? '.image-toolbar button' : '.body-toolbar button')?.focus(); return true; }
|
|
207
|
+
if ((inline || imageData || selectedVideo) && event.altKey && event.key === "F10") { event.preventDefault(); (selectedVideo ? editor.getElementByKey(activeKey)?.querySelector<HTMLButtonElement>('[data-rigel-edit-video]') : editorHost.querySelector<HTMLButtonElement>(imageData ? '.image-toolbar button' : '.body-toolbar button'))?.focus(); return true; }
|
|
196
208
|
return slashKeydown(event);
|
|
197
209
|
}, L.COMMAND_PRIORITY_HIGH));
|
|
198
210
|
const updates = editor.registerUpdateListener(({ editorState, dirtyElements, dirtyLeaves }) => {
|
|
@@ -209,7 +221,7 @@
|
|
|
209
221
|
toolbarObserver.observe(root);
|
|
210
222
|
const outside = (event: PointerEvent) => {
|
|
211
223
|
if (linkOpen && !editorHost.querySelector('.markdown-link')?.contains(event.target as Node)) linkOpen = false;
|
|
212
|
-
if ((inline || imageData) && !root.contains(event.target as Node) && !editorHost.contains(event.target as Node)) { floating = ""; linkOpen = false; altOpen = false; closeMenu(); }
|
|
224
|
+
if ((inline || imageData || selectedVideo) && !root.contains(event.target as Node) && !editorHost.contains(event.target as Node)) { floating = ""; linkOpen = false; altOpen = false; closeMenu(); }
|
|
213
225
|
if (slash !== null && !slashMenu?.contains(event.target as Node) && !root.contains(event.target as Node) && !(event.target as Element).closest('.block-add')) dismissSlash();
|
|
214
226
|
};
|
|
215
227
|
const escape = (event: KeyboardEvent) => { if (event.key === "Escape") { endDrag(true); closeMenu(); dismissSlash(); if (inline) { floating = ""; editor.focus(); } } };
|
|
@@ -271,6 +283,12 @@
|
|
|
271
283
|
return node.getChildren().flatMap(child => L.$isElementNode(child) && !child.isInline() ? [...inlineNodes(child), L.$createLineBreakNode()] : [child]);
|
|
272
284
|
}
|
|
273
285
|
function changeBlock(type: string, fromSlash = false) {
|
|
286
|
+
if (type === "video" || type === "youtube" || type === "vimeo") {
|
|
287
|
+
mediaJob = { key: fromSlash ? slashKey : activeKey, replace: fromSlash && !adding };
|
|
288
|
+
dismissSlash(); closeMenu();
|
|
289
|
+
mediaPicker.pick(type);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
274
292
|
const definition = blockTypes.find(block => block.value === type);
|
|
275
293
|
if (!definition) return;
|
|
276
294
|
if (!fromSlash && type === blockType) { dismissSlash(); closeMenu(); return; }
|
|
@@ -390,6 +408,45 @@
|
|
|
390
408
|
});
|
|
391
409
|
imageJob = null; editor.focus();
|
|
392
410
|
}
|
|
411
|
+
function editMedia(key: string) {
|
|
412
|
+
editor.getEditorState().read(() => {
|
|
413
|
+
const node = L.$getNodeByKey(key);
|
|
414
|
+
if (!(node instanceof EmbedNode || node instanceof VideoNode)) return;
|
|
415
|
+
mediaJob = { key, replace: true };
|
|
416
|
+
mediaPicker.edit(node instanceof EmbedNode ? node.__embed : node.__video);
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
export function editVideoAt(index: number) {
|
|
420
|
+
const key = editor.getEditorState().read(() => L.$getRoot().getChildren()[index]?.getKey());
|
|
421
|
+
if (key) editMedia(key);
|
|
422
|
+
}
|
|
423
|
+
function insertMedia(create: () => L.LexicalNode) {
|
|
424
|
+
const job = mediaJob ?? { key: activeKey, replace: false };
|
|
425
|
+
editor.update(() => {
|
|
426
|
+
const node = create(), previous = L.$getNodeByKey(job.key), paragraph = L.$createParagraphNode();
|
|
427
|
+
if (job.replace && (previous instanceof EmbedNode || previous instanceof VideoNode)) {
|
|
428
|
+
if (previous instanceof VideoNode && node instanceof VideoNode) node.__video = { ...previous.__video, ...node.__video };
|
|
429
|
+
previous.replace(node);
|
|
430
|
+
const selection = L.$createNodeSelection(); selection.add(node.getKey()); L.$setSelection(selection); hoverKey = "";
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (previous && job.replace) previous.replace(node);
|
|
434
|
+
else if (previous && L.$isRootNode(previous.getParent())) previous.insertAfter(node);
|
|
435
|
+
else L.$getRoot().append(node);
|
|
436
|
+
node.insertAfter(paragraph);
|
|
437
|
+
paragraph.select();
|
|
438
|
+
hoverKey = "";
|
|
439
|
+
});
|
|
440
|
+
mediaJob = null;
|
|
441
|
+
refocus();
|
|
442
|
+
}
|
|
443
|
+
function pickVideo(video: VideoData) {
|
|
444
|
+
if (!safePreviewUrl(video.src, true)) return;
|
|
445
|
+
insertMedia(() => new VideoNode(video));
|
|
446
|
+
}
|
|
447
|
+
function pickEmbed(embed: EmbedData) {
|
|
448
|
+
insertMedia(() => new EmbedNode({ ...embed, privacy: embed.privacy !== false }));
|
|
449
|
+
}
|
|
393
450
|
function closeMenu() { for (const popup of [menu, tableMenu, formatMenu]) if (popup?.matches(":popover-open")) popup.hidePopover(); }
|
|
394
451
|
function openMenu(event?: MouseEvent, popup = menu) {
|
|
395
452
|
if (suppressClick) { suppressClick = false; return; }
|
|
@@ -497,6 +554,11 @@
|
|
|
497
554
|
}
|
|
498
555
|
function contentClick(event: MouseEvent) {
|
|
499
556
|
const element = (event.target as Element).closest<HTMLElement>('figure[data-block-key],hr[data-block-key]');
|
|
557
|
+
if (element && (event.target as Element).closest('[data-rigel-edit-video]')) {
|
|
558
|
+
event.preventDefault();
|
|
559
|
+
if (event.type === "click") editMedia(element.dataset.blockKey!);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
500
562
|
if (event.type === "pointerdown" && !element) {
|
|
501
563
|
editor.update(() => { if (L.$isNodeSelection(L.$getSelection())) L.$setSelection(null); }, { discrete: true });
|
|
502
564
|
return;
|
|
@@ -520,23 +582,28 @@
|
|
|
520
582
|
<button type="button" disabled={!tableCell || tableColumns >= MAX_TABLE_COLUMNS} onpointerdown={event => event.preventDefault()} onclick={() => tableAction("column")}><Icon name="plus" />Add column right</button>
|
|
521
583
|
<button type="button" disabled={!tableCell || tableRows <= 1} onpointerdown={event => event.preventDefault()} onclick={() => tableAction("delete-row")}><Icon name="minus" />Remove row</button>
|
|
522
584
|
<button type="button" disabled={!tableCell || tableColumns <= 1} onpointerdown={event => event.preventDefault()} onclick={() => tableAction("delete-column")}><Icon name="minus" />Remove column</button>
|
|
523
|
-
<label class="inline"><
|
|
585
|
+
<label class="inline"><input type="checkbox" role="switch" class="switch" checked={tableHeader} disabled={!tableCell} onchange={event => tableAction("header", event.currentTarget.checked)} /><Icon name="table" /><span>Header row</span></label>
|
|
524
586
|
</div>
|
|
525
587
|
{/snippet}
|
|
526
588
|
{#snippet toolbar(image: boolean)}
|
|
527
|
-
<div class="body-toolbar formatting-toolbar" class:image-toolbar={image} role="group" aria-label={image ? "Image editing" : "Body formatting"} style={inline || image ? floating : undefined} hidden={markdownView || ((inline || image) && !floating) || (image && !imageData)}>
|
|
589
|
+
<div class="body-toolbar formatting-toolbar" class:image-toolbar={image} role="group" aria-label={image ? "Image editing" : "Body formatting"} style={inline || image ? floating : undefined} hidden={markdownView || ((inline || image) && !floating) || (image && !imageData) || (inline && !!selectedVideo)}>
|
|
528
590
|
{#if image}
|
|
529
591
|
<button type="button" aria-label="Edit alt text" data-tooltip="Edit alt text" onpointerdown={event => event.preventDefault()} onclick={openAlt}><Icon name="textAa" size={18} /></button>
|
|
530
592
|
<button type="button" aria-label={imageData?.url ? "Edit image link" : "Add image link"} data-tooltip={imageData?.url ? "Edit image link" : "Add image link"} onpointerdown={event => event.preventDefault()} onclick={openLink}><Icon name="link" size={18} /></button>
|
|
531
593
|
{:else}
|
|
532
|
-
<Select ariaLabel="Block type" data-tooltip={activeBlock?.label ?? "Block type"} icon={activeBlock?.icon ?? "paragraph"} value={imageData ? "" : blockType} options={textBlocks} disabled={!!imageData} onchange={next => { hoverKey = ""; changeBlock(String(next)); }} />
|
|
533
|
-
{#if !inline || !["image", "divider"].includes(blockType)}<span class="toolbar-separator" aria-hidden="true"></span>{@render formatButtons()}<span class="toolbar-separator" aria-hidden="true"></span>{/if}
|
|
594
|
+
<Select ariaLabel="Block type" data-tooltip={activeBlock?.label ?? "Block type"} icon={activeBlock?.icon ?? "paragraph"} value={imageData || embedData ? "" : blockType} options={textBlocks} disabled={!!imageData} onchange={next => { hoverKey = ""; changeBlock(String(next)); }} />
|
|
595
|
+
{#if !inline || !["image", "video", "embed", "divider"].includes(blockType)}<span class="toolbar-separator" aria-hidden="true"></span>{@render formatButtons()}<span class="toolbar-separator" aria-hidden="true"></span>{/if}
|
|
596
|
+
{/if}
|
|
597
|
+
{#if image}
|
|
598
|
+
<ImagePicker toolbar value={imageData?.src ?? ""} altText={imageData?.alt ?? ""} triggerLabel="Replace image" onopen={() => { if (!imageJob) imageJob = { key: activeKey, replace: true }; linkOpen = false; altOpen = false; }} onpick={pickImage} onclose={() => { imageJob = null; refocus(); }} />
|
|
599
|
+
{:else}
|
|
600
|
+
<button type="button" data-media-trigger aria-label="Insert media" data-tooltip="Insert media" aria-haspopup="menu" onpointerdown={event => event.preventDefault()} onclick={event => mediaPicker.toggleMenu(event.currentTarget)}><Icon name="image" size={18} /></button>
|
|
601
|
+
<ImagePicker toolbar hiddenTrigger value="" onopen={() => { if (!imageJob) imageJob = { key: activeKey, replace: false }; linkOpen = false; altOpen = false; }} onpick={pickImage} onclose={() => { imageJob = null; refocus(); }} />
|
|
534
602
|
{/if}
|
|
535
|
-
<ImagePicker toolbar value={image ? imageData?.src ?? "" : ""} altText={image ? imageData?.alt ?? "" : ""} triggerLabel={image ? "Replace image" : "Insert image"} onopen={() => { if (!imageJob) imageJob = { key: activeKey, replace: image }; linkOpen = false; altOpen = false; }} onpick={pickImage} onclose={() => { imageJob = null; refocus(); }} />
|
|
536
603
|
{#if !image}
|
|
537
604
|
<button type="button" aria-label="Table" data-tooltip="Table" aria-pressed={blockType === "table"} popovertarget={`${id}-table`} onpointerdown={event => event.preventDefault()} onclick={event => { event.preventDefault(); tableMenu.matches(':popover-open') ? closeMenu() : openMenu(event, tableMenu); }}><Icon name="table" size={18} /></button>
|
|
538
|
-
<button type="button" aria-label="Bulleted list" data-tooltip="Bulleted list" aria-pressed={listType === "bullet"} disabled={["image", "divider"].includes(blockType)} onpointerdown={event => event.preventDefault()} onclick={() => toggleList("bullet")}><Icon name="listBullets" size={18} /></button>
|
|
539
|
-
<button type="button" aria-label="Numbered list" data-tooltip="Numbered list" aria-pressed={listType === "number"} disabled={["image", "divider"].includes(blockType)} onpointerdown={event => event.preventDefault()} onclick={() => toggleList("number")}><Icon name="listNumbers" size={18} /></button>
|
|
605
|
+
<button type="button" aria-label="Bulleted list" data-tooltip="Bulleted list" aria-pressed={listType === "bullet"} disabled={["image", "video", "embed", "divider"].includes(blockType)} onpointerdown={event => event.preventDefault()} onclick={() => toggleList("bullet")}><Icon name="listBullets" size={18} /></button>
|
|
606
|
+
<button type="button" aria-label="Numbered list" data-tooltip="Numbered list" aria-pressed={listType === "number"} disabled={["image", "video", "embed", "divider"].includes(blockType)} onpointerdown={event => event.preventDefault()} onclick={() => toggleList("number")}><Icon name="listNumbers" size={18} /></button>
|
|
540
607
|
{#if !inline}
|
|
541
608
|
<button type="button" class="block-history" disabled={!canUndo} onclick={() => editor.dispatchCommand(L.UNDO_COMMAND, undefined)}>Undo</button>
|
|
542
609
|
<button type="button" class="block-history" disabled={!canRedo} onclick={() => editor.dispatchCommand(L.REDO_COMMAND, undefined)}>Redo</button>
|
|
@@ -547,9 +614,11 @@
|
|
|
547
614
|
</div>
|
|
548
615
|
{/snippet}
|
|
549
616
|
<div class="block-editor" bind:this={editorHost}>
|
|
617
|
+
<MediaPicker bind:this={mediaPicker} hiddenTrigger onopen={() => editor.blur()} onimage={() => { if (!imageJob) imageJob = { key: activeKey, replace: false }; editorHost.querySelector<HTMLButtonElement>('.body-toolbar:not(.image-toolbar) [data-image-trigger]')?.click(); }} onvideo={pickVideo} onembed={pickEmbed} onclose={() => { mediaJob = null; refocus(); }} />
|
|
550
618
|
{#if !inline}<div class="markdown-heading"><div class="field-label">Body</div><div class="markdown-modes"><button type="button" aria-pressed={markdownView} onclick={() => { markdownView = !markdownView; linkOpen = false; closeMenu(); dismissSlash(); }}>View as Markdown</button></div></div>{/if}
|
|
551
619
|
{@render toolbar(inline && !!imageData)}
|
|
552
620
|
{#if !inline}{@render toolbar(true)}{/if}
|
|
621
|
+
|
|
553
622
|
{#if error}<p role="alert">{error}</p>{/if}
|
|
554
623
|
{#if linkOpen}
|
|
555
624
|
<div class="markdown-link" class:image-form={!!imageData} style={linkPosition} id={`${id}-link`} role="group" aria-label="Edit link" onkeydown={event => { if (event.key === "Escape") { event.stopPropagation(); linkOpen = false; refocus(); } if (event.key === "Enter") { event.preventDefault(); applyLink(); } }}>
|
|
@@ -573,7 +642,7 @@
|
|
|
573
642
|
</div>
|
|
574
643
|
</div>
|
|
575
644
|
{/if}
|
|
576
|
-
{#if !inline && !imageData && floating && !markdownView}<div class="block-float" style={floating} role="group" aria-label="Selected text formatting">{@render formatButtons()}</div>{/if}
|
|
645
|
+
{#if !inline && !imageData && !selectedVideo && floating && !markdownView}<div class="block-float" style={floating} role="group" aria-label="Selected text formatting">{@render formatButtons()}</div>{/if}
|
|
577
646
|
{#if markdownView}
|
|
578
647
|
<div class="block-markdown-view">{#if inline}<button type="button" class="outlined" onclick={async () => { markdownView = false; await tick(); editor.focus(); }}>Close Markdown view</button>{/if}<pre class="block-markdown" aria-label="Body as Markdown" tabindex="0">{articleMarkdown(value)}</pre></div>
|
|
579
648
|
{/if}
|
|
@@ -13,12 +13,15 @@
|
|
|
13
13
|
import { referenceOptions } from "../../manifest/references";
|
|
14
14
|
import { isSeoField } from "../../manifest/seo";
|
|
15
15
|
import type { ContentDocument } from "../../schema/types";
|
|
16
|
+
import { slugify } from "../../slugs";
|
|
16
17
|
|
|
17
|
-
let { fields, values, body, selected, focusToken, devMode, documents = [], bodyInline = false, oneditbody = () => {}, onchange, onbody }: {
|
|
18
|
+
let { fields, values, storageSlug, body, selected, focusToken, devMode, documents = [], bodyInline = false, bodyLoading = false, oneditbody = () => {}, onchange, onbody }: {
|
|
18
19
|
fields: CollectionField[];
|
|
19
20
|
values: Record<string, unknown>;
|
|
21
|
+
storageSlug: string;
|
|
20
22
|
body: ArticleBody;
|
|
21
23
|
bodyInline?: boolean;
|
|
24
|
+
bodyLoading?: boolean;
|
|
22
25
|
oneditbody?: () => void;
|
|
23
26
|
selected: SelectedField | null;
|
|
24
27
|
focusToken: number;
|
|
@@ -49,7 +52,9 @@
|
|
|
49
52
|
<div id="properties-content" role="tabpanel" aria-labelledby="properties-tab-content" hidden={tab !== "content"}>
|
|
50
53
|
<section>
|
|
51
54
|
<h3>Article details</h3>
|
|
52
|
-
{
|
|
55
|
+
<label><span>URL slug</span><input type="text" value={String(values.slug ?? storageSlug)} oninput={(event) => onchange("slug", event.currentTarget.value)} onblur={() => { if (Object.hasOwn(values, "slug")) onchange("slug", slugify(String(values.slug))); }} /></label>
|
|
56
|
+
<p class="hint">Changes the URL, not the content filename. The website route must use the slug metadata.</p>
|
|
57
|
+
{#each fields.filter(field => field.key !== "slug" && !isSeoField(field.key)) as field (field.key)}
|
|
53
58
|
<div data-collection-field={field.key}>
|
|
54
59
|
{#if field.type === "reference"}
|
|
55
60
|
<ReferenceSelect label={field.label} value={values[field.key]} options={referenceOptions(field, documents)} multiple={field.multiple} onchange={(value) => onchange(field.key, value)} />
|
|
@@ -79,8 +84,8 @@
|
|
|
79
84
|
</section>
|
|
80
85
|
<section data-collection-field="$body">
|
|
81
86
|
{#if bodyInline}
|
|
82
|
-
<button class="outlined" onclick={oneditbody}>Edit article body in preview</button>
|
|
83
|
-
<p class="hint">Click the article body in the preview to edit. Content changes require Save
|
|
87
|
+
<button class="outlined" disabled={bodyLoading} onclick={oneditbody}>Edit article body in preview</button>
|
|
88
|
+
<p class="hint">{bodyLoading ? "Loading preview…" : "Click the article body in the preview to edit. Content changes require Save."}</p>
|
|
84
89
|
{:else}<BlockEditor value={body} oninput={onbody} />{/if}
|
|
85
90
|
</section>
|
|
86
91
|
</div>
|