@thebes/cadmea 1.5.0 → 1.6.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.
|
@@ -168,6 +168,20 @@ interface CollectionEditDraftOptions {
|
|
|
168
168
|
saveDraftLabel?: string;
|
|
169
169
|
publishLabel?: string;
|
|
170
170
|
previewLabel?: string;
|
|
171
|
+
/** Enable debounced autosave of drafts (see CollectionEdit's autosave). */
|
|
172
|
+
autosave?: boolean;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Side-by-side live preview: renders a {@link VisualEditingPane} next to the
|
|
176
|
+
* form (stacked on mobile, two-up on `lg`) and streams the form's in-progress
|
|
177
|
+
* values into it as the user types. The preview page must call
|
|
178
|
+
* `mountPreviewSync` to receive them.
|
|
179
|
+
*/
|
|
180
|
+
interface CollectionEditPreviewOptions {
|
|
181
|
+
/** The `?edit=1` preview URL to embed. Reactive — re-read as the id/draft changes. */
|
|
182
|
+
url: () => string | undefined;
|
|
183
|
+
/** Restrict postMessage to this origin (defaults to the url's origin). */
|
|
184
|
+
allowedOrigin?: () => string | undefined;
|
|
171
185
|
}
|
|
172
186
|
interface CollectionEditPageOptions {
|
|
173
187
|
collection: CollectionConfig;
|
|
@@ -211,6 +225,8 @@ interface CollectionEditPageOptions {
|
|
|
211
225
|
* `CollectionEdit` to gate Save via `canUpdate`. See issue #26.
|
|
212
226
|
*/
|
|
213
227
|
capabilities?: () => CollectionCapabilities | undefined;
|
|
228
|
+
/** Side-by-side as-you-type live preview (issue #15/#28). */
|
|
229
|
+
preview?: CollectionEditPreviewOptions;
|
|
214
230
|
}
|
|
215
231
|
/**
|
|
216
232
|
* Builds an edit-page component for a collection — fetch, update, and
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { className, createComponent, delegateEvents, effect, insert, memo, setAttribute, template } from "solid-js/web";
|
|
1
|
+
import { className, createComponent, delegateEvents, effect, insert, memo, setAttribute, template, use } from "solid-js/web";
|
|
2
2
|
import { createMutation, createQuery, useQueryClient } from "@tanstack/solid-query";
|
|
3
|
-
import { For, Index, Show, Suspense, createEffect, createSignal, lazy, onCleanup } from "solid-js";
|
|
3
|
+
import { For, Index, Show, Suspense, createEffect, createSignal, lazy, onCleanup, onMount } from "solid-js";
|
|
4
4
|
import { createForm } from "@tanstack/solid-form";
|
|
5
|
-
import { validateDocument } from "@thebes/cadmus/cms";
|
|
5
|
+
import { PREVIEW_VALUES_MESSAGE, VISUAL_EDIT_MESSAGE, validateDocument } from "@thebes/cadmus/cms";
|
|
6
6
|
import { Link, useBlocker } from "@tanstack/solid-router";
|
|
7
7
|
import { createSolidTable, flexRender, getCoreRowModel } from "@tanstack/solid-table";
|
|
8
8
|
//#region src/CollectionEdit.tsx
|
|
9
|
-
var _tmpl$$
|
|
9
|
+
var _tmpl$$5 = /*#__PURE__*/ template(`<p class="text-sm text-error"role=alert>`), _tmpl$2$3 = /*#__PURE__*/ template(`<span class="loading loading-spinner loading-sm">`), _tmpl$3$2 = /*#__PURE__*/ template(`<button type=button class="btn flex-1">`), _tmpl$4$2 = /*#__PURE__*/ template(`<button type=button class="btn btn-primary flex-1">`), _tmpl$5$1 = /*#__PURE__*/ template(`<button type=button class="btn btn-outline flex-1">`), _tmpl$6$1 = /*#__PURE__*/ template(`<span class="text-base-content/60 self-center px-1 text-xs"aria-live=polite>`), _tmpl$7$1 = /*#__PURE__*/ template(`<form class="flex flex-col gap-4"><div class="bg-base-100 sticky bottom-0 flex gap-2 border-t py-3">`), _tmpl$8$1 = /*#__PURE__*/ template(`<fieldset class="border-base-300 rounded-box border p-4"><legend class="px-2 text-sm font-semibold">`), _tmpl$9$1 = /*#__PURE__*/ template(`<div class="grid grid-cols-1 gap-4 md:grid-cols-2">`), _tmpl$0$1 = /*#__PURE__*/ template(`<span class=text-error> *`), _tmpl$1$1 = /*#__PURE__*/ template(`<p class="text-base-content/60 mb-1 text-xs">`), _tmpl$10$1 = /*#__PURE__*/ template(`<p class="text-error mt-1 text-sm"role=alert>`), _tmpl$11$1 = /*#__PURE__*/ template(`<div><label class=label>`), _tmpl$12$1 = /*#__PURE__*/ template(`<input class=input type=text>`), _tmpl$13$1 = /*#__PURE__*/ template(`<select class=select>`), _tmpl$14 = /*#__PURE__*/ template(`<option>`), _tmpl$15 = /*#__PURE__*/ template(`<input class=input type=number>`), _tmpl$16 = /*#__PURE__*/ template(`<input class=input type=text readonly>`), _tmpl$17 = /*#__PURE__*/ template(`<input class=checkbox type=checkbox>`), _tmpl$18 = /*#__PURE__*/ template(`<p class="text-sm opacity-70 break-all">`), _tmpl$19 = /*#__PURE__*/ template(`<p class="text-sm text-error">`), _tmpl$20 = /*#__PURE__*/ template(`<div class="flex flex-col gap-2"><input class=file-input type=file>`), _tmpl$21 = /*#__PURE__*/ template(`<div class="mb-1 flex flex-wrap gap-1">`), _tmpl$22 = /*#__PURE__*/ template(`<button type=button aria-label=Clear class="absolute top-2 right-2 cursor-pointer opacity-60">×`), _tmpl$23 = /*#__PURE__*/ template(`<div role=listbox class="bg-base-100 border-base-300 rounded-box absolute z-10 mt-1 flex max-h-56 w-full flex-col overflow-auto border p-1 shadow">`), _tmpl$24 = /*#__PURE__*/ template(`<div class=relative><input type=text role=combobox autocomplete=off class=input>`), _tmpl$25 = /*#__PURE__*/ template(`<span class="badge badge-primary gap-1"><button type=button class=cursor-pointer>×`), _tmpl$26 = /*#__PURE__*/ template(`<button type=button role=option class="rounded px-3 py-2 text-left">`), _tmpl$27 = /*#__PURE__*/ template(`<div role=menu class="bg-base-100 border-base-300 rounded-box absolute z-10 mt-1 flex flex-col border p-1 shadow">`), _tmpl$28 = /*#__PURE__*/ template(`<div class="relative self-start"><button type=button class="btn btn-outline btn-sm"aria-haspopup=menu>Add block`), _tmpl$29 = /*#__PURE__*/ template(`<div class="form-control md:col-span-2"><div class="label font-medium"></div><div class="flex flex-col gap-3">`), _tmpl$30 = /*#__PURE__*/ template(`<span class="text-base-content/60 truncate text-sm">`), _tmpl$31 = /*#__PURE__*/ template(`<div class="flex flex-col gap-2">`), _tmpl$32 = /*#__PURE__*/ template(`<div class="card bg-base-200 flex flex-col gap-2 p-3"><div class="flex items-center gap-2"><button type=button class="btn btn-ghost btn-sm gap-2"><span aria-hidden=true></span><span class=font-semibold></span></button><div class="ml-auto flex gap-1"><button type=button class="btn btn-ghost btn-xs"aria-label="Move up">↑</button><button type=button class="btn btn-ghost btn-xs"aria-label="Move down">↓</button><button type=button class="btn btn-ghost btn-xs"aria-label=Duplicate>⧉</button><button type=button class="btn btn-ghost btn-xs text-error"aria-label=Remove>Remove`), _tmpl$33 = /*#__PURE__*/ template(`<button type=button class="btn btn-outline btn-sm self-start">Add `), _tmpl$34 = /*#__PURE__*/ template(`<i aria-hidden=true>`), _tmpl$35 = /*#__PURE__*/ template(`<button type=button role=menuitem class="flex items-center gap-2 rounded px-3 py-2 text-left">`);
|
|
10
10
|
const RichTextEditor = lazy(() => import("../RichTextEditor-ComcBFfl.js").then((mod) => ({ default: mod.RichTextEditor })));
|
|
11
11
|
function editableFields(config) {
|
|
12
12
|
return Object.entries(config.fields).filter(([key]) => key !== "id");
|
|
@@ -104,7 +104,7 @@ function CollectionEdit(props) {
|
|
|
104
104
|
return props.error;
|
|
105
105
|
},
|
|
106
106
|
get children() {
|
|
107
|
-
var _el$2 = _tmpl$$
|
|
107
|
+
var _el$2 = _tmpl$$5();
|
|
108
108
|
insert(_el$2, () => props.error);
|
|
109
109
|
return _el$2;
|
|
110
110
|
}
|
|
@@ -150,7 +150,7 @@ function CollectionEdit(props) {
|
|
|
150
150
|
return props.capabilities?.canUpdate !== false;
|
|
151
151
|
},
|
|
152
152
|
get children() {
|
|
153
|
-
var _el$11 = _tmpl$4$
|
|
153
|
+
var _el$11 = _tmpl$4$2();
|
|
154
154
|
_el$11.$$click = () => void form.handleSubmit();
|
|
155
155
|
insert(_el$11, createComponent(Show, {
|
|
156
156
|
get when() {
|
|
@@ -171,7 +171,7 @@ function CollectionEdit(props) {
|
|
|
171
171
|
get children() {
|
|
172
172
|
return [
|
|
173
173
|
(() => {
|
|
174
|
-
var _el$4 = _tmpl$3$
|
|
174
|
+
var _el$4 = _tmpl$3$2();
|
|
175
175
|
_el$4.$$click = () => void props.draftActions?.onSaveDraft(editablePayload(formValues()));
|
|
176
176
|
insert(_el$4, createComponent(Show, {
|
|
177
177
|
get when() {
|
|
@@ -188,7 +188,7 @@ function CollectionEdit(props) {
|
|
|
188
188
|
return _el$4;
|
|
189
189
|
})(),
|
|
190
190
|
(() => {
|
|
191
|
-
var _el$6 = _tmpl$4$
|
|
191
|
+
var _el$6 = _tmpl$4$2();
|
|
192
192
|
_el$6.$$click = () => void props.draftActions?.onPublish?.();
|
|
193
193
|
insert(_el$6, createComponent(Show, {
|
|
194
194
|
get when() {
|
|
@@ -850,7 +850,7 @@ delegateEvents([
|
|
|
850
850
|
]);
|
|
851
851
|
//#endregion
|
|
852
852
|
//#region src/tanstack-start/create.tsx
|
|
853
|
-
var _tmpl$$
|
|
853
|
+
var _tmpl$$4 = /*#__PURE__*/ template(`<div class="flex flex-col gap-4"><h1 class="text-xl font-semibold">`);
|
|
854
854
|
/**
|
|
855
855
|
* Builds a create-page component for a collection. See
|
|
856
856
|
* `createCollectionListPage`'s doc comment for the same rationale on
|
|
@@ -869,7 +869,7 @@ function createCollectionCreatePage(options) {
|
|
|
869
869
|
onError: (e) => setError(e.message)
|
|
870
870
|
}));
|
|
871
871
|
return (() => {
|
|
872
|
-
var _el$ = _tmpl$$
|
|
872
|
+
var _el$ = _tmpl$$4(), _el$2 = _el$.firstChild;
|
|
873
873
|
insert(_el$2, () => options.label ?? `New ${options.collection.slug}`);
|
|
874
874
|
insert(_el$, createComponent(CollectionEdit, {
|
|
875
875
|
get config() {
|
|
@@ -894,8 +894,63 @@ function createCollectionCreatePage(options) {
|
|
|
894
894
|
};
|
|
895
895
|
}
|
|
896
896
|
//#endregion
|
|
897
|
+
//#region src/VisualEditingPane.tsx
|
|
898
|
+
var _tmpl$$3 = /*#__PURE__*/ template(`<iframe>`);
|
|
899
|
+
function originOf(url) {
|
|
900
|
+
try {
|
|
901
|
+
return new URL(url).origin;
|
|
902
|
+
} catch {
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
function VisualEditingPane(props) {
|
|
907
|
+
let iframe;
|
|
908
|
+
const targetOrigin = () => props.allowedOrigin ?? originOf(props.src) ?? "*";
|
|
909
|
+
onMount(() => {
|
|
910
|
+
const expected = props.allowedOrigin ?? originOf(props.src);
|
|
911
|
+
const handler = (event) => {
|
|
912
|
+
if (expected && event.origin !== expected) return;
|
|
913
|
+
const data = event.data;
|
|
914
|
+
if (data?.type === VISUAL_EDIT_MESSAGE && data.ref) props.onEdit?.(data.ref);
|
|
915
|
+
};
|
|
916
|
+
window.addEventListener("message", handler);
|
|
917
|
+
onCleanup(() => window.removeEventListener("message", handler));
|
|
918
|
+
});
|
|
919
|
+
createEffect(() => {
|
|
920
|
+
const values = props.previewValues;
|
|
921
|
+
const target = props.previewTarget;
|
|
922
|
+
const win = iframe?.contentWindow;
|
|
923
|
+
if (!values || !target || !win) return;
|
|
924
|
+
const message = {
|
|
925
|
+
type: PREVIEW_VALUES_MESSAGE,
|
|
926
|
+
collection: target.collection,
|
|
927
|
+
id: target.id,
|
|
928
|
+
values
|
|
929
|
+
};
|
|
930
|
+
win.postMessage(message, targetOrigin());
|
|
931
|
+
});
|
|
932
|
+
return (() => {
|
|
933
|
+
var _el$ = _tmpl$$3();
|
|
934
|
+
use((el) => {
|
|
935
|
+
iframe = el;
|
|
936
|
+
}, _el$);
|
|
937
|
+
effect((_p$) => {
|
|
938
|
+
var _v$ = props.src, _v$2 = props.title ?? "Preview", _v$3 = props.class ?? "h-full w-full border-0";
|
|
939
|
+
_v$ !== _p$.e && setAttribute(_el$, "src", _p$.e = _v$);
|
|
940
|
+
_v$2 !== _p$.t && setAttribute(_el$, "title", _p$.t = _v$2);
|
|
941
|
+
_v$3 !== _p$.a && className(_el$, _p$.a = _v$3);
|
|
942
|
+
return _p$;
|
|
943
|
+
}, {
|
|
944
|
+
e: void 0,
|
|
945
|
+
t: void 0,
|
|
946
|
+
a: void 0
|
|
947
|
+
});
|
|
948
|
+
return _el$;
|
|
949
|
+
})();
|
|
950
|
+
}
|
|
951
|
+
//#endregion
|
|
897
952
|
//#region src/tanstack-start/edit.tsx
|
|
898
|
-
var _tmpl$$2 = /*#__PURE__*/ template(`<button type=button class="btn btn-error btn-outline btn-sm self-start">`), _tmpl$2$2 = /*#__PURE__*/ template(`<div class="flex flex-col gap-4"><h1 class="text-xl font-semibold">`);
|
|
953
|
+
var _tmpl$$2 = /*#__PURE__*/ template(`<button type=button class="btn btn-error btn-outline btn-sm self-start">`), _tmpl$2$2 = /*#__PURE__*/ template(`<div class="flex flex-col gap-4"><h1 class="text-xl font-semibold">`), _tmpl$3$1 = /*#__PURE__*/ template(`<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">`), _tmpl$4$1 = /*#__PURE__*/ template(`<div class="lg:sticky lg:top-4 lg:h-[calc(100vh-2rem)]">`);
|
|
899
954
|
/**
|
|
900
955
|
* Builds an edit-page component for a collection — fetch, update, and
|
|
901
956
|
* delete, all wired together, plus a router-level unsaved-changes guard
|
|
@@ -909,6 +964,7 @@ function createCollectionEditPage(options) {
|
|
|
909
964
|
const [error, setError] = createSignal();
|
|
910
965
|
const [dirty, setDirty] = createSignal(false);
|
|
911
966
|
const [latestDraftId, setLatestDraftId] = createSignal();
|
|
967
|
+
const [previewValues, setPreviewValues] = createSignal({});
|
|
912
968
|
useBlocker({
|
|
913
969
|
shouldBlockFn: () => dirty(),
|
|
914
970
|
enableBeforeUnload: () => dirty()
|
|
@@ -965,7 +1021,7 @@ function createCollectionEditPage(options) {
|
|
|
965
1021
|
},
|
|
966
1022
|
onError: (e) => setError(e.message)
|
|
967
1023
|
}));
|
|
968
|
-
|
|
1024
|
+
const EditorPane = () => (() => {
|
|
969
1025
|
var _el$ = _tmpl$2$2(), _el$2 = _el$.firstChild;
|
|
970
1026
|
insert(_el$2, () => options.label ?? `Edit ${options.collection.slug}`);
|
|
971
1027
|
insert(_el$, createComponent(Show, {
|
|
@@ -997,6 +1053,7 @@ function createCollectionEditPage(options) {
|
|
|
997
1053
|
return options.fieldWidgets;
|
|
998
1054
|
},
|
|
999
1055
|
onDirtyChange: setDirty,
|
|
1056
|
+
onValuesChange: setPreviewValues,
|
|
1000
1057
|
get capabilities() {
|
|
1001
1058
|
return options.capabilities?.();
|
|
1002
1059
|
},
|
|
@@ -1012,7 +1069,8 @@ function createCollectionEditPage(options) {
|
|
|
1012
1069
|
canPreview: latestDraftId() !== void 0,
|
|
1013
1070
|
saveDraftLabel: options.draftActions.saveDraftLabel,
|
|
1014
1071
|
publishLabel: options.draftActions.publishLabel,
|
|
1015
|
-
previewLabel: options.draftActions.previewLabel
|
|
1072
|
+
previewLabel: options.draftActions.previewLabel,
|
|
1073
|
+
autosave: options.draftActions.autosave
|
|
1016
1074
|
};
|
|
1017
1075
|
}
|
|
1018
1076
|
});
|
|
@@ -1031,6 +1089,47 @@ function createCollectionEditPage(options) {
|
|
|
1031
1089
|
}), null);
|
|
1032
1090
|
return _el$;
|
|
1033
1091
|
})();
|
|
1092
|
+
return createComponent(Show, {
|
|
1093
|
+
get when() {
|
|
1094
|
+
return options.preview;
|
|
1095
|
+
},
|
|
1096
|
+
get fallback() {
|
|
1097
|
+
return createComponent(EditorPane, {});
|
|
1098
|
+
},
|
|
1099
|
+
get children() {
|
|
1100
|
+
var _el$4 = _tmpl$3$1();
|
|
1101
|
+
insert(_el$4, createComponent(EditorPane, {}), null);
|
|
1102
|
+
insert(_el$4, createComponent(Show, {
|
|
1103
|
+
get when() {
|
|
1104
|
+
return options.preview?.url();
|
|
1105
|
+
},
|
|
1106
|
+
children: (url) => (() => {
|
|
1107
|
+
var _el$5 = _tmpl$4$1();
|
|
1108
|
+
insert(_el$5, createComponent(VisualEditingPane, {
|
|
1109
|
+
get src() {
|
|
1110
|
+
return url();
|
|
1111
|
+
},
|
|
1112
|
+
get allowedOrigin() {
|
|
1113
|
+
return options.preview?.allowedOrigin?.();
|
|
1114
|
+
},
|
|
1115
|
+
get previewValues() {
|
|
1116
|
+
return previewValues();
|
|
1117
|
+
},
|
|
1118
|
+
get previewTarget() {
|
|
1119
|
+
return {
|
|
1120
|
+
collection: options.collection.slug,
|
|
1121
|
+
id: Number(row.data?.id)
|
|
1122
|
+
};
|
|
1123
|
+
},
|
|
1124
|
+
"class": "border-base-300 rounded-box h-full w-full border",
|
|
1125
|
+
title: "Live preview"
|
|
1126
|
+
}));
|
|
1127
|
+
return _el$5;
|
|
1128
|
+
})()
|
|
1129
|
+
}), null);
|
|
1130
|
+
return _el$4;
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1034
1133
|
};
|
|
1035
1134
|
}
|
|
1036
1135
|
delegateEvents(["click"]);
|