@goodfellow-cms/admin 0.7.0 → 0.9.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 +2 -0
- package/dist/admin-text-sources-BTZJPlVK.js +273 -0
- package/dist/custom-font-faces-DJ2AF2oN.js +526 -0
- package/dist/editor.js +858 -87
- package/dist/entry-values-FcVgwQkJ.js +5005 -0
- package/dist/es-ChxY0KVH.js +2071 -0
- package/dist/es-labels-DBu66eln.js +520 -0
- package/dist/index.d.ts +3 -1467
- package/dist/index.js +9571 -4516
- package/dist/plugin.js +43 -3
- package/dist/strings-CWaRN_0v.d.ts +1999 -0
- package/dist/strings-D94noZrv.js +2018 -0
- package/dist/text.d.ts +52 -0
- package/dist/text.js +22 -0
- package/package.json +16 -6
- package/styles.css +786 -25
- package/dist/entry-values-DO01xZ9L.js +0 -5414
- package/dist/plugin-protocol-DkvBDqsF.js +0 -119
package/dist/editor.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { $ as formattedHtml, K as storedData, Q as editedMarkdown, _ as mediaUrl, a as showMediaInPreview, q as storedEntryFields, r as createMediaPreviews, s as createChannel, t as installCustomFonts } from "./custom-font-faces-DJ2AF2oN.js";
|
|
2
|
+
import { C as MediaChoiceContext, D as MediaThumb, G as isForwardedShortcut, H as READY_MESSAGE, J as shortcutFor, K as isTyping, X as SETTINGS_SECTIONS, c as AdminContext, f as useSiteContent, g as useDarkMode, i as usePreviewStyles, lt as Button, m as setThemeChoice, ot as EditorTextContext, rt as syncedBlockEditorHref, s as withBlocksInPlace, st as useEditorText, t as SCHEDULE_PROP, u as useAdmin, w as MediaChooser, y as ScheduleFields, yt as findPage } from "./entry-values-FcVgwQkJ.js";
|
|
3
|
+
import { i as useStrings, t as StringsProvider } from "./strings-D94noZrv.js";
|
|
4
|
+
import { COLOR_SCHEME_ATTRIBUTE, DEFAULT_THEME_COLORS, DRAWN_SLOT_ELEMENT, FONT_SIZES, MEDIA_DIR, MINIMUM_CONTRAST, RADII, SCREEN_SIZES, SIDES, SPACING_STEPS, SYNCED_BLOCK, SYNCED_BLOCK_PROP, TEXT_ALIGNS, THEME_COLORS, WEEKDAYS, allPages, canFormatMarkdown, cleanEventValue, colorSlug, contentInLanguage, contrastRatio, customFonts, customLength, datePart, describedIn, entryTitle, escapeCellText, escapeStyleText, expandSyncedBlocks, findEntry, findSyncedBlock, focusPosition, formatDate, formatOccurrence, getPageHead, googleFontsUrl, hasLanguages, isAllDay, isCustomColor, isEventLike, isOwnersOnlyField, isSafeUrl, languageHome, languageLinks, mainLanguage, neverIndexed, pageLinks, pageView, pathLanguage, readBlockStyle, renderedProblems, shownOccurrence, sitePages, syncedBlockIn, tableCellHtml, themeColorOf, themeToCss, todayIn, wallClock, weekdayName, weekdayOf, withBlockStyle } from "@goodfellow-cms/core";
|
|
5
|
+
import { Box } from "lucide-react";
|
|
6
|
+
import { createContext, createElement, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
5
7
|
import { createRoot } from "react-dom/client";
|
|
6
8
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
-
import { PageBody, PageContent, SiteProvider, adminSite, applyEntry, blockIcon, blockKeywords, createPuckConfig, createPuckConfigs, cx, isStyleField, isTableField, mediaFieldKind, preparePage, siteMetadata, tableCells } from "@goodfellow-cms/react";
|
|
8
|
-
import { Box } from "lucide-react";
|
|
9
|
+
import { PageBody, PageContent, SiteProvider, adminSite, applyEntry, blockIcon, blockKeywords, createPuckConfig, createPuckConfigs, cx, isStyleField, isTableField, mediaFieldKind, pageHeadings, preparePage, siteMetadata, tableCells } from "@goodfellow-cms/react";
|
|
9
10
|
import { AutoField, Drawer, FieldLabel, Puck, Render, RichTextMenu, createUsePuck, legacySideBarPlugin, migrate, registerOverlayPortal, resolveAllData, useGetPuck } from "@puckeditor/core";
|
|
10
11
|
import { createPortal, flushSync } from "react-dom";
|
|
11
12
|
const CANVAS = `.gf-canvas`;
|
|
@@ -190,14 +191,16 @@ function installSiteCss(css) {
|
|
|
190
191
|
}
|
|
191
192
|
/**
|
|
192
193
|
* The theme, fonts, custom CSS and Tailwind for the canvas, as the site
|
|
193
|
-
* shows them, light or dark as `scheme` says.
|
|
194
|
+
* shows them, light or dark as `scheme` says. The site's own fonts come from
|
|
195
|
+
* their bytes, through `media`, since the frame can't load the site's files.
|
|
194
196
|
*/
|
|
195
|
-
function useCanvasStyles(preview, styles) {
|
|
197
|
+
function useCanvasStyles(preview, styles, media) {
|
|
196
198
|
const { theme, customCss, scheme } = styles;
|
|
197
199
|
useEffect(() => scopeTailwindOutput(), []);
|
|
198
200
|
useEffect(() => {
|
|
199
201
|
document.documentElement.setAttribute(COLOR_SCHEME_ATTRIBUTE, theme.darkMode ? scheme : "light");
|
|
200
|
-
styleElement("theme").textContent = escapeStyleText(scopeCss(themeToCss(theme)));
|
|
202
|
+
styleElement("theme").textContent = escapeStyleText(scopeCss(themeToCss(theme, void 0, { fontFaces: false })));
|
|
203
|
+
installCustomFonts(document, customFonts(theme), (url) => media.bytes(url));
|
|
201
204
|
const fonts = googleFontsUrl(theme);
|
|
202
205
|
const link = document.head.querySelector("link[data-gf=\"fonts\"]");
|
|
203
206
|
if (fonts) {
|
|
@@ -207,7 +210,11 @@ function useCanvasStyles(preview, styles) {
|
|
|
207
210
|
element.href = fonts;
|
|
208
211
|
if (!link) document.head.append(element);
|
|
209
212
|
} else link?.remove();
|
|
210
|
-
}, [
|
|
213
|
+
}, [
|
|
214
|
+
theme,
|
|
215
|
+
scheme,
|
|
216
|
+
media
|
|
217
|
+
]);
|
|
211
218
|
useEffect(() => {
|
|
212
219
|
const text = escapeStyleText(`${preview.themeCss}\n${customCss}`);
|
|
213
220
|
const current = document.head.querySelector("style[data-gf=\"tailwind\"]");
|
|
@@ -234,7 +241,8 @@ const usePuck$2 = createUsePuck();
|
|
|
234
241
|
/**
|
|
235
242
|
* How the page looks when a link to it is shared, such as in a message or on
|
|
236
243
|
* social media: its title, description and image as link previews show them,
|
|
237
|
-
* from the page's settings or else Site Settings', updated as they're edited
|
|
244
|
+
* from the page's settings or else Site Settings', updated as they're edited,
|
|
245
|
+
* with the picture large over the title or small beside it, as its settings say.
|
|
238
246
|
*/
|
|
239
247
|
function SharePreview({ path }) {
|
|
240
248
|
const t = useStrings();
|
|
@@ -265,7 +273,7 @@ function SharePreview({ path }) {
|
|
|
265
273
|
children: t("sharePreview.title")
|
|
266
274
|
}),
|
|
267
275
|
/* @__PURE__ */ jsxs("div", {
|
|
268
|
-
className: "gfa-share-card",
|
|
276
|
+
className: head.card === "summary" ? "gfa-share-card gfa-share-card-small" : "gfa-share-card",
|
|
269
277
|
children: [image ? /* @__PURE__ */ jsx("img", {
|
|
270
278
|
src: image,
|
|
271
279
|
alt: "",
|
|
@@ -709,6 +717,135 @@ function EventField({ id, label, hint, value, onChange, language, timeZone }) {
|
|
|
709
717
|
});
|
|
710
718
|
}
|
|
711
719
|
//#endregion
|
|
720
|
+
//#region src/item-fields.tsx
|
|
721
|
+
/**
|
|
722
|
+
* The item editor's controls for links to items and for several images: a
|
|
723
|
+
* searchable list of another collection's items, and a list of images to add
|
|
724
|
+
* to, order and take out.
|
|
725
|
+
*/
|
|
726
|
+
/** How many matches the item picker lists at once; searching narrows the rest. */
|
|
727
|
+
const SHOWN_MATCHES = 50;
|
|
728
|
+
/** The items a link can name: the collection's own, never translations, which links reach through the item they translate. */
|
|
729
|
+
function linkableItems(collection) {
|
|
730
|
+
return (collection?.entries ?? []).filter((entry) => entry.content.translationOf === void 0).map((entry) => ({
|
|
731
|
+
slug: entry.slug,
|
|
732
|
+
title: entryTitle(entry)
|
|
733
|
+
})).sort((a, b) => a.title.localeCompare(b.title));
|
|
734
|
+
}
|
|
735
|
+
/** Chooses one item, or several, of a collection, by searching their titles. */
|
|
736
|
+
function ItemPicker({ id, collection, multiple, value, onChange }) {
|
|
737
|
+
const t = useStrings();
|
|
738
|
+
const listId = useId();
|
|
739
|
+
const [search, setSearch] = useState("");
|
|
740
|
+
const items = linkableItems(collection);
|
|
741
|
+
const chosen = typeof value === "string" ? value ? [value] : [] : Array.isArray(value) ? value.map(String) : [];
|
|
742
|
+
const name = collection?.settings.name ?? "";
|
|
743
|
+
const titleOf = (slug) => items.find((item) => item.slug === slug)?.title ?? slug;
|
|
744
|
+
const words = search.trim().toLowerCase();
|
|
745
|
+
const matches = items.filter((item) => !chosen.includes(item.slug) && (!words || item.title.toLowerCase().includes(words)));
|
|
746
|
+
const set = (next) => onChange(multiple ? next.length > 0 ? next : void 0 : next[0]);
|
|
747
|
+
if (!collection) return /* @__PURE__ */ jsx("p", {
|
|
748
|
+
className: "gfa-puck-hint",
|
|
749
|
+
children: t("editEntry.items.missing")
|
|
750
|
+
});
|
|
751
|
+
if (items.length === 0) return /* @__PURE__ */ jsx("p", {
|
|
752
|
+
className: "gfa-puck-hint",
|
|
753
|
+
children: t("editEntry.items.empty", { name })
|
|
754
|
+
});
|
|
755
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
756
|
+
className: "gfa-item-picker",
|
|
757
|
+
children: [chosen.length > 0 && /* @__PURE__ */ jsx("ul", {
|
|
758
|
+
className: "gfa-item-picker-chosen",
|
|
759
|
+
children: chosen.map((slug) => /* @__PURE__ */ jsxs("li", {
|
|
760
|
+
className: "gfa-chosen-row",
|
|
761
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
762
|
+
className: "gfa-chosen-row-name",
|
|
763
|
+
children: titleOf(slug)
|
|
764
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
765
|
+
variant: "ghost",
|
|
766
|
+
"aria-label": t("editEntry.items.remove", { title: titleOf(slug) }),
|
|
767
|
+
onClick: () => set(chosen.filter((other) => other !== slug)),
|
|
768
|
+
children: "×"
|
|
769
|
+
})]
|
|
770
|
+
}, slug))
|
|
771
|
+
}), (multiple || chosen.length === 0) && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("input", {
|
|
772
|
+
id,
|
|
773
|
+
type: "search",
|
|
774
|
+
className: "gfa-puck-input",
|
|
775
|
+
placeholder: t("editEntry.items.search", { name }),
|
|
776
|
+
"aria-controls": listId,
|
|
777
|
+
value: search,
|
|
778
|
+
onChange: (event) => setSearch(event.target.value)
|
|
779
|
+
}), /* @__PURE__ */ jsxs("ul", {
|
|
780
|
+
id: listId,
|
|
781
|
+
className: "gfa-item-picker-matches",
|
|
782
|
+
children: [matches.slice(0, SHOWN_MATCHES).map((item) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
|
|
783
|
+
type: "button",
|
|
784
|
+
onClick: () => {
|
|
785
|
+
set([...chosen, item.slug]);
|
|
786
|
+
setSearch("");
|
|
787
|
+
},
|
|
788
|
+
children: item.title
|
|
789
|
+
}) }, item.slug)), matches.length === 0 && /* @__PURE__ */ jsx("li", {
|
|
790
|
+
className: "gfa-puck-hint",
|
|
791
|
+
children: t("editEntry.items.none", { name })
|
|
792
|
+
})]
|
|
793
|
+
})] })]
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
/** Several images, in order: each can move up or down or be taken out, and the media chooser adds more. */
|
|
797
|
+
function ImagesField({ value, onChange }) {
|
|
798
|
+
const t = useStrings();
|
|
799
|
+
const images = Array.isArray(value) ? value.filter((item) => typeof item === "string" && !!item) : [];
|
|
800
|
+
const set = (next) => onChange(next.length > 0 ? next : void 0);
|
|
801
|
+
const move = (index, by) => {
|
|
802
|
+
const next = [...images];
|
|
803
|
+
const [moved] = next.splice(index, 1);
|
|
804
|
+
next.splice(index + by, 0, moved);
|
|
805
|
+
set(next);
|
|
806
|
+
};
|
|
807
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
808
|
+
className: "gfa-images-field",
|
|
809
|
+
children: [images.length > 0 && /* @__PURE__ */ jsx("ul", { children: images.map((src, index) => /* @__PURE__ */ jsxs("li", {
|
|
810
|
+
className: "gfa-chosen-row",
|
|
811
|
+
children: [
|
|
812
|
+
/* @__PURE__ */ jsx(MediaThumb, {
|
|
813
|
+
url: src,
|
|
814
|
+
className: "gfa-images-field-thumb"
|
|
815
|
+
}),
|
|
816
|
+
/* @__PURE__ */ jsx("span", {
|
|
817
|
+
className: "gfa-chosen-row-name",
|
|
818
|
+
children: src.slice(src.lastIndexOf("/") + 1)
|
|
819
|
+
}),
|
|
820
|
+
/* @__PURE__ */ jsx(Button, {
|
|
821
|
+
variant: "ghost",
|
|
822
|
+
disabled: index === 0,
|
|
823
|
+
"aria-label": t("editEntry.images.up"),
|
|
824
|
+
onClick: () => move(index, -1),
|
|
825
|
+
children: "↑"
|
|
826
|
+
}),
|
|
827
|
+
/* @__PURE__ */ jsx(Button, {
|
|
828
|
+
variant: "ghost",
|
|
829
|
+
disabled: index === images.length - 1,
|
|
830
|
+
"aria-label": t("editEntry.images.down"),
|
|
831
|
+
onClick: () => move(index, 1),
|
|
832
|
+
children: "↓"
|
|
833
|
+
}),
|
|
834
|
+
/* @__PURE__ */ jsx(Button, {
|
|
835
|
+
variant: "ghost",
|
|
836
|
+
"aria-label": t("editEntry.images.remove"),
|
|
837
|
+
onClick: () => set(images.filter((_, other) => other !== index)),
|
|
838
|
+
children: "×"
|
|
839
|
+
})
|
|
840
|
+
]
|
|
841
|
+
}, src)) }), /* @__PURE__ */ jsx(MediaChooser, {
|
|
842
|
+
value: "",
|
|
843
|
+
kind: "image",
|
|
844
|
+
onChange: (src) => src && !images.includes(src) && set([...images, src])
|
|
845
|
+
})]
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
//#endregion
|
|
712
849
|
//#region src/markdown-field.tsx
|
|
713
850
|
/**
|
|
714
851
|
* Puck's formatted editor, without what Markdown can't store: underlining and
|
|
@@ -839,7 +976,7 @@ function Hint({ text }) {
|
|
|
839
976
|
}) : null;
|
|
840
977
|
}
|
|
841
978
|
/** What the AI assistant is told a field holds, since it can't tell from a custom field. */
|
|
842
|
-
function aiHint(field, markdown) {
|
|
979
|
+
function aiHint(field, markdown, collections) {
|
|
843
980
|
const description = field.hint;
|
|
844
981
|
if (markdown) return {
|
|
845
982
|
type: "string",
|
|
@@ -864,6 +1001,25 @@ function aiHint(field, markdown) {
|
|
|
864
1001
|
options: field.options ?? [],
|
|
865
1002
|
description
|
|
866
1003
|
};
|
|
1004
|
+
case "yesno": return {
|
|
1005
|
+
type: "boolean",
|
|
1006
|
+
description
|
|
1007
|
+
};
|
|
1008
|
+
case "item": {
|
|
1009
|
+
const options = linkableItems(collections.find((candidate) => candidate.id === field.collection)).slice(0, MAX_AI_ITEMS).map((item) => ({
|
|
1010
|
+
value: item.slug,
|
|
1011
|
+
label: item.title
|
|
1012
|
+
}));
|
|
1013
|
+
if (options.length === 0) return void 0;
|
|
1014
|
+
return {
|
|
1015
|
+
type: field.multiple ? "choices" : "choice",
|
|
1016
|
+
options,
|
|
1017
|
+
description
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
case "images":
|
|
1021
|
+
case "file":
|
|
1022
|
+
case "group": return;
|
|
867
1023
|
case "link": return {
|
|
868
1024
|
type: "string",
|
|
869
1025
|
description: [description, "A web address or a page address such as /about"].filter(Boolean).join(". ")
|
|
@@ -878,16 +1034,102 @@ function aiHint(field, markdown) {
|
|
|
878
1034
|
};
|
|
879
1035
|
}
|
|
880
1036
|
}
|
|
1037
|
+
/** The most items the assistant is offered to link to, so a large collection doesn't fill its request. */
|
|
1038
|
+
const MAX_AI_ITEMS = 200;
|
|
881
1039
|
/** The Puck field for one of a collection's fields, shown in the editor's sidebar. */
|
|
882
|
-
function puckField(field, collection,
|
|
1040
|
+
function puckField(field, collection, content) {
|
|
883
1041
|
const markdown = collection.settings.markdown?.body === field.name;
|
|
884
|
-
const control = fieldControl(field, markdown,
|
|
885
|
-
|
|
1042
|
+
const control = fieldControl(field, markdown, content);
|
|
1043
|
+
const ai = field.type === "event" ? void 0 : aiHint(field, markdown, content.collections);
|
|
1044
|
+
return ai ? {
|
|
886
1045
|
...control,
|
|
887
|
-
metadata: { ai
|
|
888
|
-
};
|
|
1046
|
+
metadata: { ai }
|
|
1047
|
+
} : control;
|
|
1048
|
+
}
|
|
1049
|
+
/** A text field for an address, with the media chooser below it. */
|
|
1050
|
+
function addressControl(label, hint, kind) {
|
|
1051
|
+
return { render: ({ value, onChange, id }) => /* @__PURE__ */ jsxs(FieldLabel, {
|
|
1052
|
+
label,
|
|
1053
|
+
children: [
|
|
1054
|
+
/* @__PURE__ */ jsx(AutoField, {
|
|
1055
|
+
id,
|
|
1056
|
+
field: { type: "text" },
|
|
1057
|
+
value: typeof value === "string" ? value : "",
|
|
1058
|
+
onChange
|
|
1059
|
+
}),
|
|
1060
|
+
/* @__PURE__ */ jsx(MediaChooser, {
|
|
1061
|
+
value: typeof value === "string" ? value : "",
|
|
1062
|
+
kind,
|
|
1063
|
+
onChange
|
|
1064
|
+
}),
|
|
1065
|
+
/* @__PURE__ */ jsx(Hint, { text: hint })
|
|
1066
|
+
]
|
|
1067
|
+
}) };
|
|
889
1068
|
}
|
|
890
|
-
|
|
1069
|
+
/** A checkbox for a yes or no field, which stores nothing when it's off. */
|
|
1070
|
+
function YesNo({ id, label, hint, value, onChange }) {
|
|
1071
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1072
|
+
className: "gfa-puck-field",
|
|
1073
|
+
children: [/* @__PURE__ */ jsxs("label", {
|
|
1074
|
+
className: "gfa-checkbox",
|
|
1075
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
1076
|
+
id,
|
|
1077
|
+
type: "checkbox",
|
|
1078
|
+
checked: value === true,
|
|
1079
|
+
onChange: (event) => onChange(event.target.checked ? true : void 0)
|
|
1080
|
+
}), label]
|
|
1081
|
+
}), /* @__PURE__ */ jsx(Hint, { text: hint })]
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
/** One of a repeating group's fields, as the group's rows show it. */
|
|
1085
|
+
function groupFieldControl(field) {
|
|
1086
|
+
switch (field.type) {
|
|
1087
|
+
case "textarea": return {
|
|
1088
|
+
type: "textarea",
|
|
1089
|
+
label: field.label
|
|
1090
|
+
};
|
|
1091
|
+
case "number": return {
|
|
1092
|
+
type: "number",
|
|
1093
|
+
label: field.label
|
|
1094
|
+
};
|
|
1095
|
+
case "date": return {
|
|
1096
|
+
type: "custom",
|
|
1097
|
+
label: field.label,
|
|
1098
|
+
render: ({ value, onChange, id }) => /* @__PURE__ */ jsx(FieldLabel, {
|
|
1099
|
+
label: field.label,
|
|
1100
|
+
children: /* @__PURE__ */ jsx("input", {
|
|
1101
|
+
id,
|
|
1102
|
+
type: "date",
|
|
1103
|
+
className: "gfa-puck-input",
|
|
1104
|
+
value: typeof value === "string" ? value : "",
|
|
1105
|
+
onChange: (event) => onChange(event.target.value)
|
|
1106
|
+
})
|
|
1107
|
+
})
|
|
1108
|
+
};
|
|
1109
|
+
case "yesno": return {
|
|
1110
|
+
type: "custom",
|
|
1111
|
+
label: field.label,
|
|
1112
|
+
render: ({ value, onChange, id }) => /* @__PURE__ */ jsx(YesNo, {
|
|
1113
|
+
id,
|
|
1114
|
+
label: field.label,
|
|
1115
|
+
value,
|
|
1116
|
+
onChange
|
|
1117
|
+
})
|
|
1118
|
+
};
|
|
1119
|
+
case "image":
|
|
1120
|
+
case "link": return {
|
|
1121
|
+
type: "custom",
|
|
1122
|
+
label: field.label,
|
|
1123
|
+
...addressControl(field.label, void 0, field.type === "image" ? "image" : "file")
|
|
1124
|
+
};
|
|
1125
|
+
default: return {
|
|
1126
|
+
type: "text",
|
|
1127
|
+
label: field.label
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
function fieldControl(field, markdown, content) {
|
|
1132
|
+
const { settings } = content;
|
|
891
1133
|
const label = field.required ? `${field.label} *` : field.label;
|
|
892
1134
|
if (markdown) return {
|
|
893
1135
|
type: "custom",
|
|
@@ -965,6 +1207,59 @@ function fieldControl(field, markdown, settings) {
|
|
|
965
1207
|
});
|
|
966
1208
|
}
|
|
967
1209
|
};
|
|
1210
|
+
case "yesno": return {
|
|
1211
|
+
type: "custom",
|
|
1212
|
+
label,
|
|
1213
|
+
render: ({ value, onChange, id }) => /* @__PURE__ */ jsx(YesNo, {
|
|
1214
|
+
id,
|
|
1215
|
+
label,
|
|
1216
|
+
hint: field.hint,
|
|
1217
|
+
value,
|
|
1218
|
+
onChange
|
|
1219
|
+
})
|
|
1220
|
+
};
|
|
1221
|
+
case "item": return {
|
|
1222
|
+
type: "custom",
|
|
1223
|
+
label,
|
|
1224
|
+
render: ({ value, onChange, id }) => /* @__PURE__ */ jsxs(FieldLabel, {
|
|
1225
|
+
label,
|
|
1226
|
+
el: "div",
|
|
1227
|
+
children: [/* @__PURE__ */ jsx(ItemPicker, {
|
|
1228
|
+
id,
|
|
1229
|
+
collection: content.collections.find((candidate) => candidate.id === field.collection),
|
|
1230
|
+
multiple: field.multiple === true,
|
|
1231
|
+
value,
|
|
1232
|
+
onChange
|
|
1233
|
+
}), /* @__PURE__ */ jsx(Hint, { text: field.hint })]
|
|
1234
|
+
})
|
|
1235
|
+
};
|
|
1236
|
+
case "images": return {
|
|
1237
|
+
type: "custom",
|
|
1238
|
+
label,
|
|
1239
|
+
render: ({ value, onChange }) => /* @__PURE__ */ jsxs(FieldLabel, {
|
|
1240
|
+
label,
|
|
1241
|
+
el: "div",
|
|
1242
|
+
children: [/* @__PURE__ */ jsx(ImagesField, {
|
|
1243
|
+
value,
|
|
1244
|
+
onChange
|
|
1245
|
+
}), /* @__PURE__ */ jsx(Hint, { text: field.hint })]
|
|
1246
|
+
})
|
|
1247
|
+
};
|
|
1248
|
+
case "file": return {
|
|
1249
|
+
type: "custom",
|
|
1250
|
+
label,
|
|
1251
|
+
...addressControl(label, field.hint, "file")
|
|
1252
|
+
};
|
|
1253
|
+
case "group": {
|
|
1254
|
+
const subFields = field.fields ?? [];
|
|
1255
|
+
const first = subFields[0]?.name;
|
|
1256
|
+
return {
|
|
1257
|
+
type: "array",
|
|
1258
|
+
label,
|
|
1259
|
+
arrayFields: Object.fromEntries(subFields.map((sub) => [sub.name, groupFieldControl(sub)])),
|
|
1260
|
+
getItemSummary: (row, index) => first && typeof row?.[first] === "string" && row[first] || `${(index ?? 0) + 1}`
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
968
1263
|
case "event": return {
|
|
969
1264
|
type: "custom",
|
|
970
1265
|
label,
|
|
@@ -1041,12 +1336,11 @@ function EntryPreview({ collection, entry, content, templateConfig, layoutConfig
|
|
|
1041
1336
|
current
|
|
1042
1337
|
]);
|
|
1043
1338
|
const metadata = useMemo(() => siteMetadata(site), [site]);
|
|
1044
|
-
const applied = useMemo(() => applyEntry(migrate(collection.settings.template), templateConfig, collection, current, content.settings.language, content.settings.timeZone), [
|
|
1339
|
+
const applied = useMemo(() => applyEntry(expandSyncedBlocks(migrate(collection.settings.template), content, content.settings.language), templateConfig, collection, current, content.settings.language, content.settings.timeZone, content.collections), [
|
|
1045
1340
|
collection,
|
|
1046
1341
|
templateConfig,
|
|
1047
1342
|
current,
|
|
1048
|
-
content
|
|
1049
|
-
content.settings.timeZone
|
|
1343
|
+
content
|
|
1050
1344
|
]);
|
|
1051
1345
|
const [resolved, setResolved] = useState(applied);
|
|
1052
1346
|
useEffect(() => {
|
|
@@ -1076,8 +1370,8 @@ function EntryPreview({ collection, entry, content, templateConfig, layoutConfig
|
|
|
1076
1370
|
value: site,
|
|
1077
1371
|
children: /* @__PURE__ */ jsx(SiteFrame, {
|
|
1078
1372
|
layoutConfig,
|
|
1079
|
-
header: content.header.data,
|
|
1080
|
-
footer: content.footer.data,
|
|
1373
|
+
header: expandSyncedBlocks(content.header.data, content, content.settings.language),
|
|
1374
|
+
footer: expandSyncedBlocks(content.footer.data, content, content.settings.language),
|
|
1081
1375
|
site,
|
|
1082
1376
|
className: typeof className === "string" ? className : void 0,
|
|
1083
1377
|
children: /* @__PURE__ */ jsx(Render, {
|
|
@@ -1104,7 +1398,7 @@ function entryConfig(collection, entry, content, configs) {
|
|
|
1104
1398
|
return {
|
|
1105
1399
|
components: {},
|
|
1106
1400
|
root: {
|
|
1107
|
-
fields: Object.fromEntries([...collection.settings.fields.map((field) => [field.name, puckField(field, collection, content
|
|
1401
|
+
fields: Object.fromEntries([...collection.settings.fields.map((field) => [field.name, puckField(field, collection, content)]), [SCHEDULE_PROP, scheduleField(content)]]),
|
|
1108
1402
|
render: () => /* @__PURE__ */ jsx("div", {
|
|
1109
1403
|
className: "gf-canvas",
|
|
1110
1404
|
children: /* @__PURE__ */ jsx(EntryPreview, {
|
|
@@ -1149,25 +1443,56 @@ function withSlotPlaces(block, config) {
|
|
|
1149
1443
|
}
|
|
1150
1444
|
};
|
|
1151
1445
|
}
|
|
1152
|
-
/**
|
|
1153
|
-
|
|
1446
|
+
/** The config a target's block is in, and the page it's on, with that block alone. */
|
|
1447
|
+
function targetPage(configs, content, { kind, path, block }) {
|
|
1154
1448
|
const layout = kind === "header" || kind === "footer";
|
|
1155
1449
|
const config = layout ? configs.layout : kind === "template" ? configs.template : configs.page;
|
|
1156
1450
|
if (!config.components[block.type]) throw new Error(`The site has no block called ${block.type} here.`);
|
|
1157
1451
|
const found = allPages(content).find((page) => page.path === path);
|
|
1158
1452
|
const root = layout ? (kind === "header" ? content.header : content.footer).data.root : found?.content.data.root;
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1453
|
+
return {
|
|
1454
|
+
config,
|
|
1455
|
+
page: {
|
|
1456
|
+
path,
|
|
1457
|
+
file: found?.file ?? "",
|
|
1458
|
+
content: {
|
|
1459
|
+
...found?.content,
|
|
1460
|
+
version: found?.content.version ?? 1,
|
|
1461
|
+
data: {
|
|
1462
|
+
root: root ?? { props: {} },
|
|
1463
|
+
content: [withSlotPlaces(block, config.components[block.type])]
|
|
1464
|
+
}
|
|
1168
1465
|
}
|
|
1169
1466
|
}
|
|
1170
1467
|
};
|
|
1468
|
+
}
|
|
1469
|
+
/** A copy of fields as JSON, as the site's description has them: what they say, without the code in them. */
|
|
1470
|
+
function fieldsAsJson(fields) {
|
|
1471
|
+
return JSON.parse(JSON.stringify(fields ?? {}));
|
|
1472
|
+
}
|
|
1473
|
+
/** A block's fields as its props make them, as `blockFields()` describes. */
|
|
1474
|
+
async function blockFields(configs, content, target) {
|
|
1475
|
+
const { config, page } = targetPage(configs, content, target);
|
|
1476
|
+
const component = config.components[target.block.type];
|
|
1477
|
+
const fields = component.fields ?? {};
|
|
1478
|
+
if (!component.resolveFields) return fieldsAsJson(fields);
|
|
1479
|
+
const prepared = await preparePage({
|
|
1480
|
+
...configs,
|
|
1481
|
+
page: config
|
|
1482
|
+
}, content, page);
|
|
1483
|
+
return fieldsAsJson(await component.resolveFields(target.block, {
|
|
1484
|
+
changed: {},
|
|
1485
|
+
fields,
|
|
1486
|
+
lastFields: fields,
|
|
1487
|
+
lastData: null,
|
|
1488
|
+
metadata: siteMetadata(prepared.site),
|
|
1489
|
+
appState: {},
|
|
1490
|
+
parent: null
|
|
1491
|
+
}));
|
|
1492
|
+
}
|
|
1493
|
+
/** One block drawn as the site draws it, in the page at its address, as `drawBlock()` describes. */
|
|
1494
|
+
async function drawBlock(configs, content, target) {
|
|
1495
|
+
const { config, page } = targetPage(configs, content, target);
|
|
1171
1496
|
const prepared = await preparePage({
|
|
1172
1497
|
...configs,
|
|
1173
1498
|
page: config
|
|
@@ -1242,6 +1567,9 @@ function localSiteCode(config) {
|
|
|
1242
1567
|
}
|
|
1243
1568
|
});
|
|
1244
1569
|
return drawBlock(drawing, content, target);
|
|
1570
|
+
},
|
|
1571
|
+
async blockFields(content, target) {
|
|
1572
|
+
return blockFields(configs, content, target);
|
|
1245
1573
|
}
|
|
1246
1574
|
};
|
|
1247
1575
|
}
|
|
@@ -1289,6 +1617,7 @@ function FrameProviders({ config, client, init, site, children }) {
|
|
|
1289
1617
|
pageConfig: createPuckConfig(config, "page"),
|
|
1290
1618
|
layoutConfig: createPuckConfig(config, "layout"),
|
|
1291
1619
|
templateConfig: createPuckConfig(config, "template"),
|
|
1620
|
+
syncedConfig: createPuckConfig(config, "synced"),
|
|
1292
1621
|
code: localSiteCode(config)
|
|
1293
1622
|
}), [config]);
|
|
1294
1623
|
const described = useMemo(() => adminSite(config), [config]);
|
|
@@ -1354,17 +1683,24 @@ function FrameProviders({ config, client, init, site, children }) {
|
|
|
1354
1683
|
kind,
|
|
1355
1684
|
value
|
|
1356
1685
|
}), [client]);
|
|
1686
|
+
const text = useMemo(() => ({
|
|
1687
|
+
puck: init.puck ?? {},
|
|
1688
|
+
labels: init.labels ?? {}
|
|
1689
|
+
}), [init.puck, init.labels]);
|
|
1357
1690
|
return /* @__PURE__ */ jsx(StringsProvider, {
|
|
1358
1691
|
strings: init.strings,
|
|
1359
|
-
children: /* @__PURE__ */ jsx(
|
|
1360
|
-
value:
|
|
1361
|
-
children: /* @__PURE__ */ jsx(
|
|
1362
|
-
value:
|
|
1363
|
-
children: /* @__PURE__ */ jsx(
|
|
1364
|
-
value:
|
|
1365
|
-
children: /* @__PURE__ */ jsx(
|
|
1366
|
-
value:
|
|
1367
|
-
children
|
|
1692
|
+
children: /* @__PURE__ */ jsx(EditorTextContext.Provider, {
|
|
1693
|
+
value: text,
|
|
1694
|
+
children: /* @__PURE__ */ jsx(AdminClientContext.Provider, {
|
|
1695
|
+
value: client,
|
|
1696
|
+
children: /* @__PURE__ */ jsx(FrameCodeContext.Provider, {
|
|
1697
|
+
value: code,
|
|
1698
|
+
children: /* @__PURE__ */ jsx(AdminContext.Provider, {
|
|
1699
|
+
value: admin,
|
|
1700
|
+
children: /* @__PURE__ */ jsx(MediaChoiceContext.Provider, {
|
|
1701
|
+
value: chooseMedia,
|
|
1702
|
+
children
|
|
1703
|
+
})
|
|
1368
1704
|
})
|
|
1369
1705
|
})
|
|
1370
1706
|
})
|
|
@@ -1372,21 +1708,149 @@ function FrameProviders({ config, client, init, site, children }) {
|
|
|
1372
1708
|
});
|
|
1373
1709
|
}
|
|
1374
1710
|
//#endregion
|
|
1711
|
+
//#region src/frame/labels.ts
|
|
1712
|
+
/** A label in the admin panel's language, or as it is where the table has none. */
|
|
1713
|
+
function translated(labels, text) {
|
|
1714
|
+
return text === void 0 ? text : labels[text] ?? text;
|
|
1715
|
+
}
|
|
1716
|
+
function withFieldLabels(field, labels) {
|
|
1717
|
+
return {
|
|
1718
|
+
...field,
|
|
1719
|
+
...field.label !== void 0 && { label: translated(labels, field.label) },
|
|
1720
|
+
...typeof field.placeholder === "string" && { placeholder: translated(labels, field.placeholder) },
|
|
1721
|
+
...Array.isArray(field.options) && { options: field.options.map((option) => ({
|
|
1722
|
+
...option,
|
|
1723
|
+
label: translated(labels, option.label) ?? option.label
|
|
1724
|
+
})) },
|
|
1725
|
+
...field.arrayFields && { arrayFields: withFieldsLabels(field.arrayFields, labels) },
|
|
1726
|
+
...field.objectFields && { objectFields: withFieldsLabels(field.objectFields, labels) }
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
function withFieldsLabels(fields, labels) {
|
|
1730
|
+
return Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, withFieldLabels(field, labels)]));
|
|
1731
|
+
}
|
|
1732
|
+
function withComponentLabels(component, labels) {
|
|
1733
|
+
if (!component) return component;
|
|
1734
|
+
const { resolveFields, label } = component;
|
|
1735
|
+
return {
|
|
1736
|
+
...component,
|
|
1737
|
+
...label !== void 0 && { label: translated(labels, label) },
|
|
1738
|
+
...component.fields && { fields: withFieldsLabels(component.fields, labels) },
|
|
1739
|
+
...resolveFields && { resolveFields: async (...args) => withFieldsLabels(await resolveFields(...args), labels) }
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* The config with blocks' names, groups and settings' labels, choices and
|
|
1744
|
+
* placeholders in the admin panel's language, looked up by their English
|
|
1745
|
+
* wording, so a site's own blocks that word theirs the same way are
|
|
1746
|
+
* translated too. Only what the editor shows changes: props, choices' values
|
|
1747
|
+
* and what blocks render stay as they are.
|
|
1748
|
+
*/
|
|
1749
|
+
function withLabels(config, labels) {
|
|
1750
|
+
if (Object.keys(labels).length === 0) return config;
|
|
1751
|
+
const components = Object.fromEntries(Object.entries(config.components).map(([name, component]) => [name, withComponentLabels(component, labels)]));
|
|
1752
|
+
const categories = config.categories && Object.fromEntries(Object.entries(config.categories).map(([name, category]) => [name, {
|
|
1753
|
+
...category,
|
|
1754
|
+
...category.title !== void 0 && { title: translated(labels, category.title) }
|
|
1755
|
+
}]));
|
|
1756
|
+
return {
|
|
1757
|
+
...config,
|
|
1758
|
+
components,
|
|
1759
|
+
...categories && { categories },
|
|
1760
|
+
...config.root && { root: withComponentLabels(config.root, labels) }
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
//#endregion
|
|
1764
|
+
//#region src/frame/owners-only.tsx
|
|
1765
|
+
/**
|
|
1766
|
+
* Block settings only owners may change (`metadata: { owners: true }`, such as
|
|
1767
|
+
* Custom HTML used exactly as written), shown to editors on full-stack sites
|
|
1768
|
+
* as they are, without a way to change them. Their server refuses such a
|
|
1769
|
+
* change anyway (`ownersOnlyChanges()`); this only says so up front.
|
|
1770
|
+
*/
|
|
1771
|
+
function valueLabel(field, value) {
|
|
1772
|
+
const option = ("options" in field && Array.isArray(field.options) ? field.options : []).find((choice) => JSON.stringify(choice.value) === JSON.stringify(value));
|
|
1773
|
+
if (option) return String(option.label);
|
|
1774
|
+
return value === void 0 || value === null ? "" : String(value);
|
|
1775
|
+
}
|
|
1776
|
+
function LockedField({ field, value }) {
|
|
1777
|
+
const t = useStrings();
|
|
1778
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1779
|
+
className: "gfa-locked-field",
|
|
1780
|
+
children: [
|
|
1781
|
+
field.label && /* @__PURE__ */ jsx("div", {
|
|
1782
|
+
className: "gfa-locked-field-label",
|
|
1783
|
+
children: field.label
|
|
1784
|
+
}),
|
|
1785
|
+
/* @__PURE__ */ jsx("div", {
|
|
1786
|
+
className: "gfa-locked-field-value",
|
|
1787
|
+
children: valueLabel(field, value)
|
|
1788
|
+
}),
|
|
1789
|
+
/* @__PURE__ */ jsx("p", {
|
|
1790
|
+
className: "gfa-locked-field-note",
|
|
1791
|
+
children: t("owners.only")
|
|
1792
|
+
})
|
|
1793
|
+
]
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
function locked(fields) {
|
|
1797
|
+
return Object.fromEntries(Object.entries(fields).map(([name, field]) => {
|
|
1798
|
+
if (!isOwnersOnlyField(field)) return [name, field];
|
|
1799
|
+
return [name, {
|
|
1800
|
+
type: "custom",
|
|
1801
|
+
...field.label !== void 0 && { label: field.label },
|
|
1802
|
+
...field.metadata !== void 0 && { metadata: field.metadata },
|
|
1803
|
+
render: ({ value }) => /* @__PURE__ */ jsx(LockedField, {
|
|
1804
|
+
field,
|
|
1805
|
+
value
|
|
1806
|
+
})
|
|
1807
|
+
}];
|
|
1808
|
+
}));
|
|
1809
|
+
}
|
|
1810
|
+
/** The config with owners-only settings shown but not changeable, for editors who aren't owners. */
|
|
1811
|
+
function withOwnersOnlyLocked(config) {
|
|
1812
|
+
const components = Object.fromEntries(Object.entries(config.components).map(([name, component]) => {
|
|
1813
|
+
const fields = component.fields ?? {};
|
|
1814
|
+
if (!Object.values(fields).some(isOwnersOnlyField)) return [name, component];
|
|
1815
|
+
return [name, {
|
|
1816
|
+
...component,
|
|
1817
|
+
fields: locked(fields)
|
|
1818
|
+
}];
|
|
1819
|
+
}));
|
|
1820
|
+
return {
|
|
1821
|
+
...config,
|
|
1822
|
+
components
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
//#endregion
|
|
1375
1826
|
//#region src/frame/page-settings.ts
|
|
1827
|
+
/** What each setting is where a page leaves it out, which the editor shows as chosen. */
|
|
1828
|
+
const SHOWN_DEFAULTS = {
|
|
1829
|
+
noIndex: false,
|
|
1830
|
+
noFollow: false,
|
|
1831
|
+
noSnippet: false,
|
|
1832
|
+
card: "auto"
|
|
1833
|
+
};
|
|
1376
1834
|
/** For pages and templates, whose settings include `noIndex`; `path` is the page's, for pages. */
|
|
1377
1835
|
function shownSettings(original, path) {
|
|
1378
1836
|
const fixed = path !== void 0 && neverIndexed(path);
|
|
1379
1837
|
const originalRoot = original.root;
|
|
1380
|
-
const
|
|
1838
|
+
const originalProps = originalRoot.props ?? {};
|
|
1381
1839
|
return {
|
|
1382
1840
|
show(data) {
|
|
1383
1841
|
const root = data.root;
|
|
1842
|
+
const missing = Object.entries(SHOWN_DEFAULTS).filter(([name]) => root.props?.[name] === void 0);
|
|
1843
|
+
if (missing.length === 0 && !fixed) return data;
|
|
1844
|
+
const props = {
|
|
1845
|
+
...root.props,
|
|
1846
|
+
...Object.fromEntries(missing)
|
|
1847
|
+
};
|
|
1384
1848
|
if (fixed) return {
|
|
1385
1849
|
...data,
|
|
1386
1850
|
root: {
|
|
1387
1851
|
...root,
|
|
1388
1852
|
props: {
|
|
1389
|
-
...
|
|
1853
|
+
...props,
|
|
1390
1854
|
noIndex: true
|
|
1391
1855
|
},
|
|
1392
1856
|
readOnly: {
|
|
@@ -1395,32 +1859,29 @@ function shownSettings(original, path) {
|
|
|
1395
1859
|
}
|
|
1396
1860
|
}
|
|
1397
1861
|
};
|
|
1398
|
-
if (root.props?.noIndex !== void 0) return data;
|
|
1399
1862
|
return {
|
|
1400
1863
|
...data,
|
|
1401
1864
|
root: {
|
|
1402
1865
|
...root,
|
|
1403
|
-
props
|
|
1404
|
-
...root.props,
|
|
1405
|
-
noIndex: false
|
|
1406
|
-
}
|
|
1866
|
+
props
|
|
1407
1867
|
}
|
|
1408
1868
|
};
|
|
1409
1869
|
},
|
|
1410
1870
|
keep(data) {
|
|
1411
1871
|
const root = data.root;
|
|
1412
|
-
const
|
|
1413
|
-
const
|
|
1872
|
+
const props = { ...root.props };
|
|
1873
|
+
for (const [name, value] of Object.entries(SHOWN_DEFAULTS)) {
|
|
1874
|
+
const kept = fixed && name === "noIndex" || originalProps[name] === void 0 && props[name] === value ? originalProps[name] : props[name];
|
|
1875
|
+
if (kept === void 0) delete props[name];
|
|
1876
|
+
else props[name] = kept;
|
|
1877
|
+
}
|
|
1414
1878
|
const { readOnly: _shown, ...rest } = root;
|
|
1415
1879
|
const readOnly = fixed ? originalRoot.readOnly : root.readOnly;
|
|
1416
1880
|
return {
|
|
1417
1881
|
...data,
|
|
1418
1882
|
root: {
|
|
1419
1883
|
...rest,
|
|
1420
|
-
props
|
|
1421
|
-
...props,
|
|
1422
|
-
noIndex: kept
|
|
1423
|
-
},
|
|
1884
|
+
props,
|
|
1424
1885
|
...readOnly && { readOnly }
|
|
1425
1886
|
}
|
|
1426
1887
|
};
|
|
@@ -1432,8 +1893,8 @@ function shownSettings(original, path) {
|
|
|
1432
1893
|
/**
|
|
1433
1894
|
* A block's Style field (`classNameField` in `@goodfellow-cms/react`), which
|
|
1434
1895
|
* Puck's editor shows as controls for its look in place of a box of classes:
|
|
1435
|
-
* text alignment, size and color, background, space inside and around it
|
|
1436
|
-
* rounded corners, written into its `className` as Tailwind classes
|
|
1896
|
+
* text alignment, size and color, background, space inside and around it,
|
|
1897
|
+
* rounded corners and the sizes of screen it's hidden on, written into its `className` as Tailwind classes
|
|
1437
1898
|
* (`withBlockStyle()` in `@goodfellow-cms/core`), with any other classes kept.
|
|
1438
1899
|
*/
|
|
1439
1900
|
/** A color's value to show and compare: a theme color's, or the editor's own. */
|
|
@@ -1627,6 +2088,36 @@ function ColorChoice({ label, value, colors, readOnly, onChange }) {
|
|
|
1627
2088
|
})]
|
|
1628
2089
|
});
|
|
1629
2090
|
}
|
|
2091
|
+
/** The sizes of screen a block is hidden on, a box for each. */
|
|
2092
|
+
function HiddenOnChoice({ hiddenOn, readOnly, onChange }) {
|
|
2093
|
+
const t = useStrings();
|
|
2094
|
+
const hidden = new Set(hiddenOn);
|
|
2095
|
+
return /* @__PURE__ */ jsxs("fieldset", {
|
|
2096
|
+
className: "gfa-style-group",
|
|
2097
|
+
children: [
|
|
2098
|
+
/* @__PURE__ */ jsx("legend", {
|
|
2099
|
+
className: "gfa-style-label",
|
|
2100
|
+
children: t("style.hiddenOn")
|
|
2101
|
+
}),
|
|
2102
|
+
SCREEN_SIZES.map((size) => /* @__PURE__ */ jsxs("label", {
|
|
2103
|
+
className: "gfa-style-check",
|
|
2104
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
2105
|
+
type: "checkbox",
|
|
2106
|
+
checked: hidden.has(size),
|
|
2107
|
+
disabled: readOnly,
|
|
2108
|
+
onChange: (event) => {
|
|
2109
|
+
const next = SCREEN_SIZES.filter((candidate) => candidate === size ? event.target.checked : hidden.has(candidate));
|
|
2110
|
+
onChange(next.length > 0 ? next : void 0);
|
|
2111
|
+
}
|
|
2112
|
+
}), t(`style.hiddenOn.${size}`)]
|
|
2113
|
+
}, size)),
|
|
2114
|
+
hidden.size > 0 && /* @__PURE__ */ jsx("p", {
|
|
2115
|
+
className: "gfa-hint",
|
|
2116
|
+
children: t(hidden.size === SCREEN_SIZES.length ? "style.hiddenOnAll" : "style.hiddenOnHint")
|
|
2117
|
+
})
|
|
2118
|
+
]
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
1630
2121
|
/** Other classes, kept as typed while the box has focus, so a class isn't taken into the controls halfway. */
|
|
1631
2122
|
function OtherClasses({ other, readOnly, onChange }) {
|
|
1632
2123
|
const t = useStrings();
|
|
@@ -1665,7 +2156,7 @@ function OtherClasses({ other, readOnly, onChange }) {
|
|
|
1665
2156
|
]
|
|
1666
2157
|
});
|
|
1667
2158
|
}
|
|
1668
|
-
function StyleControls({ value, colors, readOnly, label, ownAlign, onChange }) {
|
|
2159
|
+
function StyleControls({ value, colors, readOnly, label, ownAlign, block, onChange }) {
|
|
1669
2160
|
const t = useStrings();
|
|
1670
2161
|
const { style, other } = readBlockStyle(value);
|
|
1671
2162
|
const set = (next) => onChange(withBlockStyle(other, next));
|
|
@@ -1750,6 +2241,11 @@ function StyleControls({ value, colors, readOnly, label, ownAlign, onChange }) {
|
|
|
1750
2241
|
readOnly,
|
|
1751
2242
|
onChange: (radius) => change("radius", radius)
|
|
1752
2243
|
}),
|
|
2244
|
+
(block || style.hiddenOn) && /* @__PURE__ */ jsx(HiddenOnChoice, {
|
|
2245
|
+
hiddenOn: style.hiddenOn,
|
|
2246
|
+
readOnly,
|
|
2247
|
+
onChange: (hiddenOn) => change("hiddenOn", hiddenOn)
|
|
2248
|
+
}),
|
|
1753
2249
|
/* @__PURE__ */ jsx(OtherClasses, {
|
|
1754
2250
|
other,
|
|
1755
2251
|
readOnly,
|
|
@@ -1765,7 +2261,7 @@ function StyleControls({ value, colors, readOnly, label, ownAlign, onChange }) {
|
|
|
1765
2261
|
]
|
|
1766
2262
|
});
|
|
1767
2263
|
}
|
|
1768
|
-
function withStyleField(field, colors, ownAlign) {
|
|
2264
|
+
function withStyleField(field, colors, ownAlign, block) {
|
|
1769
2265
|
if (field.type !== "text" || !isStyleField(field)) return field;
|
|
1770
2266
|
const render = ({ value, onChange, readOnly }) => /* @__PURE__ */ jsx(StyleControls, {
|
|
1771
2267
|
value,
|
|
@@ -1773,6 +2269,7 @@ function withStyleField(field, colors, ownAlign) {
|
|
|
1773
2269
|
readOnly,
|
|
1774
2270
|
label: field.label,
|
|
1775
2271
|
ownAlign,
|
|
2272
|
+
block,
|
|
1776
2273
|
onChange
|
|
1777
2274
|
});
|
|
1778
2275
|
return {
|
|
@@ -1782,27 +2279,96 @@ function withStyleField(field, colors, ownAlign) {
|
|
|
1782
2279
|
render
|
|
1783
2280
|
};
|
|
1784
2281
|
}
|
|
1785
|
-
function withStyleFieldsIn(fields, colors) {
|
|
2282
|
+
function withStyleFieldsIn(fields, colors, block) {
|
|
1786
2283
|
if (!fields) return fields;
|
|
1787
2284
|
const ownAlign = "align" in fields;
|
|
1788
|
-
return Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, withStyleField(field, colors, ownAlign)]));
|
|
2285
|
+
return Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, withStyleField(field, colors, ownAlign, block)]));
|
|
1789
2286
|
}
|
|
1790
|
-
function withStyleComponent(component, colors) {
|
|
2287
|
+
function withStyleComponent(component, colors, block) {
|
|
1791
2288
|
if (!component) return component;
|
|
1792
2289
|
const { resolveFields } = component;
|
|
1793
2290
|
return {
|
|
1794
2291
|
...component,
|
|
1795
|
-
...component.fields && { fields: withStyleFieldsIn(component.fields, colors) },
|
|
1796
|
-
...resolveFields && { resolveFields: async (...args) => withStyleFieldsIn(await resolveFields(...args), colors) }
|
|
2292
|
+
...component.fields && { fields: withStyleFieldsIn(component.fields, colors, block) },
|
|
2293
|
+
...resolveFields && { resolveFields: async (...args) => withStyleFieldsIn(await resolveFields(...args), colors, block) }
|
|
1797
2294
|
};
|
|
1798
2295
|
}
|
|
1799
2296
|
/** The config with every block's Style field, and the page's, drawn as a style's controls. */
|
|
1800
2297
|
function withStyleFields(config, colors) {
|
|
1801
|
-
const components = Object.fromEntries(Object.entries(config.components).map(([name, component]) => [name, withStyleComponent(component, colors)]));
|
|
2298
|
+
const components = Object.fromEntries(Object.entries(config.components).map(([name, component]) => [name, withStyleComponent(component, colors, true)]));
|
|
1802
2299
|
return {
|
|
1803
2300
|
...config,
|
|
1804
2301
|
components,
|
|
1805
|
-
...config.root && { root: withStyleComponent(config.root, colors) }
|
|
2302
|
+
...config.root && { root: withStyleComponent(config.root, colors, false) }
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
//#endregion
|
|
2306
|
+
//#region src/frame/synced-block.tsx
|
|
2307
|
+
/**
|
|
2308
|
+
* Synced blocks on the editor's canvas: a placed Synced block draws the
|
|
2309
|
+
* synced block's own blocks in its place, read-only, with a link to edit it,
|
|
2310
|
+
* since they're kept once for every page that places them. Builds put them
|
|
2311
|
+
* in place before rendering (`expandSyncedBlocks()`); the editor keeps the
|
|
2312
|
+
* placement, which is what the page saves.
|
|
2313
|
+
*/
|
|
2314
|
+
function SyncedBlockDrawing({ name, config, drawing }) {
|
|
2315
|
+
const t = useStrings();
|
|
2316
|
+
const block = name ? findSyncedBlock(drawing.content, name) : void 0;
|
|
2317
|
+
if (!block) return /* @__PURE__ */ jsx("div", {
|
|
2318
|
+
className: "gfa-synced-block gfa-synced-block-missing",
|
|
2319
|
+
children: /* @__PURE__ */ jsx("p", { children: name ? t("synced.missing", { name }) : t("synced.choose") })
|
|
2320
|
+
});
|
|
2321
|
+
const shown = syncedBlockIn(block, drawing.language);
|
|
2322
|
+
const href = syncedBlockEditorHref(block.name, drawing.main ? void 0 : drawing.language);
|
|
2323
|
+
const data = {
|
|
2324
|
+
root: { props: {} },
|
|
2325
|
+
content: shown.data.content
|
|
2326
|
+
};
|
|
2327
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2328
|
+
className: "gfa-synced-block",
|
|
2329
|
+
"data-gf-synced-block": block.name,
|
|
2330
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2331
|
+
className: "gfa-synced-block-bar",
|
|
2332
|
+
children: [/* @__PURE__ */ jsx("span", { children: t("synced.label", { title: shown.title }) }), /* @__PURE__ */ jsx("a", {
|
|
2333
|
+
href,
|
|
2334
|
+
ref: (element) => registerOverlayPortal(element, { disableDrag: true }),
|
|
2335
|
+
onClick: (event) => {
|
|
2336
|
+
event.preventDefault();
|
|
2337
|
+
drawing.navigate(href);
|
|
2338
|
+
},
|
|
2339
|
+
children: t("synced.edit")
|
|
2340
|
+
})]
|
|
2341
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2342
|
+
inert: true,
|
|
2343
|
+
children: /* @__PURE__ */ jsx(Render, {
|
|
2344
|
+
config,
|
|
2345
|
+
data,
|
|
2346
|
+
metadata: drawing.metadata
|
|
2347
|
+
})
|
|
2348
|
+
})]
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
/**
|
|
2352
|
+
* The config with its Synced block drawing the synced block it places, from
|
|
2353
|
+
* the blocks of the same config, whose own Synced block draws nothing, since
|
|
2354
|
+
* a synced block can't place another.
|
|
2355
|
+
*/
|
|
2356
|
+
function withSyncedBlockDrawing(config, drawing) {
|
|
2357
|
+
const component = config.components[SYNCED_BLOCK];
|
|
2358
|
+
if (!component) return config;
|
|
2359
|
+
return {
|
|
2360
|
+
...config,
|
|
2361
|
+
components: {
|
|
2362
|
+
...config.components,
|
|
2363
|
+
[SYNCED_BLOCK]: {
|
|
2364
|
+
...component,
|
|
2365
|
+
render: (props) => /* @__PURE__ */ jsx(SyncedBlockDrawing, {
|
|
2366
|
+
name: typeof props[SYNCED_BLOCK_PROP] === "string" ? props[SYNCED_BLOCK_PROP] : "",
|
|
2367
|
+
config,
|
|
2368
|
+
drawing
|
|
2369
|
+
})
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
1806
2372
|
};
|
|
1807
2373
|
}
|
|
1808
2374
|
//#endregion
|
|
@@ -1810,12 +2376,157 @@ function withStyleFields(config, colors) {
|
|
|
1810
2376
|
/**
|
|
1811
2377
|
* The grid of cells a table's field is edited in (`tableField()` in
|
|
1812
2378
|
* `@goodfellow-cms/react`), which Puck's editor shows in place of the field's
|
|
1813
|
-
* own render, since blocks can't bring their own editing code.
|
|
2379
|
+
* own render, since blocks can't bring their own editing code. Each cell is
|
|
2380
|
+
* a small box of formatted text: bold, italics, links and line breaks, with
|
|
2381
|
+
* what's pasted kept to those (`tableCellHtml()`).
|
|
1814
2382
|
*/
|
|
1815
2383
|
const MAX_ROWS = 100;
|
|
1816
2384
|
const MAX_COLUMNS = 12;
|
|
2385
|
+
/** One cell, as formatted text. What it holds comes from the cell's value, except while it's being typed in. */
|
|
2386
|
+
function RichCell({ value, label, readOnly, onChange, onFocus }) {
|
|
2387
|
+
const ref = useRef(null);
|
|
2388
|
+
useLayoutEffect(() => {
|
|
2389
|
+
const cell = ref.current;
|
|
2390
|
+
if (!cell || document.activeElement === cell) return;
|
|
2391
|
+
const html = tableCellHtml(value);
|
|
2392
|
+
if (cell.innerHTML !== html) cell.innerHTML = html;
|
|
2393
|
+
}, [value]);
|
|
2394
|
+
const changed = () => {
|
|
2395
|
+
if (ref.current) onChange(tableCellHtml(ref.current.innerHTML));
|
|
2396
|
+
};
|
|
2397
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2398
|
+
ref,
|
|
2399
|
+
role: "textbox",
|
|
2400
|
+
tabIndex: 0,
|
|
2401
|
+
"aria-label": label,
|
|
2402
|
+
"aria-multiline": "true",
|
|
2403
|
+
"aria-readonly": readOnly || void 0,
|
|
2404
|
+
contentEditable: !readOnly,
|
|
2405
|
+
suppressContentEditableWarning: true,
|
|
2406
|
+
className: "gfa-input gfa-table-field-cell gfa-table-field-rich",
|
|
2407
|
+
onInput: changed,
|
|
2408
|
+
onFocus: () => ref.current && onFocus(ref.current),
|
|
2409
|
+
onKeyDown: (event) => {
|
|
2410
|
+
if (event.key === "Enter") {
|
|
2411
|
+
event.preventDefault();
|
|
2412
|
+
document.execCommand("insertLineBreak");
|
|
2413
|
+
} else if ((event.ctrlKey || event.metaKey) && (event.key === "b" || event.key === "i")) {
|
|
2414
|
+
event.preventDefault();
|
|
2415
|
+
document.execCommand(event.key === "b" ? "bold" : "italic");
|
|
2416
|
+
}
|
|
2417
|
+
},
|
|
2418
|
+
onPaste: (event) => {
|
|
2419
|
+
event.preventDefault();
|
|
2420
|
+
const html = event.clipboardData.getData("text/html");
|
|
2421
|
+
const text = event.clipboardData.getData("text/plain");
|
|
2422
|
+
document.execCommand("insertHTML", false, html ? tableCellHtml(html) : escapeCellText(text));
|
|
2423
|
+
}
|
|
2424
|
+
});
|
|
2425
|
+
}
|
|
2426
|
+
/** Bold, italics and links for the cell last typed in. */
|
|
2427
|
+
function CellToolbar({ cell, readOnly }) {
|
|
2428
|
+
const t = useStrings();
|
|
2429
|
+
const [linking, setLinking] = useState();
|
|
2430
|
+
const disabled = readOnly || !cell;
|
|
2431
|
+
const command = (name, argument) => {
|
|
2432
|
+
cell?.focus();
|
|
2433
|
+
document.execCommand(name, false, argument);
|
|
2434
|
+
};
|
|
2435
|
+
const startLink = () => {
|
|
2436
|
+
const selection = document.getSelection();
|
|
2437
|
+
const range = selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : void 0;
|
|
2438
|
+
setLinking({
|
|
2439
|
+
range: range && cell?.contains(range.commonAncestorContainer) ? range : void 0,
|
|
2440
|
+
address: ""
|
|
2441
|
+
});
|
|
2442
|
+
};
|
|
2443
|
+
const applyLink = (event) => {
|
|
2444
|
+
event.preventDefault();
|
|
2445
|
+
const address = linking?.address.trim() ?? "";
|
|
2446
|
+
if (cell && linking?.range && address && isSafeUrl(address)) {
|
|
2447
|
+
cell.focus();
|
|
2448
|
+
const selection = document.getSelection();
|
|
2449
|
+
selection?.removeAllRanges();
|
|
2450
|
+
selection?.addRange(linking.range);
|
|
2451
|
+
document.execCommand("createLink", false, address);
|
|
2452
|
+
}
|
|
2453
|
+
setLinking(void 0);
|
|
2454
|
+
};
|
|
2455
|
+
const keep = (event) => event.preventDefault();
|
|
2456
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2457
|
+
className: "gfa-table-field-toolbar",
|
|
2458
|
+
children: [
|
|
2459
|
+
/* @__PURE__ */ jsx("button", {
|
|
2460
|
+
type: "button",
|
|
2461
|
+
className: "gfa-table-field-tool",
|
|
2462
|
+
disabled,
|
|
2463
|
+
onMouseDown: keep,
|
|
2464
|
+
onClick: () => command("bold"),
|
|
2465
|
+
children: /* @__PURE__ */ jsx("strong", { children: t("table.bold") })
|
|
2466
|
+
}),
|
|
2467
|
+
/* @__PURE__ */ jsx("button", {
|
|
2468
|
+
type: "button",
|
|
2469
|
+
className: "gfa-table-field-tool",
|
|
2470
|
+
disabled,
|
|
2471
|
+
onMouseDown: keep,
|
|
2472
|
+
onClick: () => command("italic"),
|
|
2473
|
+
children: /* @__PURE__ */ jsx("em", { children: t("table.italic") })
|
|
2474
|
+
}),
|
|
2475
|
+
/* @__PURE__ */ jsx("button", {
|
|
2476
|
+
type: "button",
|
|
2477
|
+
className: "gfa-table-field-tool",
|
|
2478
|
+
disabled,
|
|
2479
|
+
onMouseDown: keep,
|
|
2480
|
+
onClick: startLink,
|
|
2481
|
+
children: t("table.link")
|
|
2482
|
+
}),
|
|
2483
|
+
/* @__PURE__ */ jsx("button", {
|
|
2484
|
+
type: "button",
|
|
2485
|
+
className: "gfa-table-field-tool",
|
|
2486
|
+
disabled,
|
|
2487
|
+
onMouseDown: keep,
|
|
2488
|
+
onClick: () => command("unlink"),
|
|
2489
|
+
children: t("table.unlink")
|
|
2490
|
+
}),
|
|
2491
|
+
linking && /* @__PURE__ */ jsxs("form", {
|
|
2492
|
+
className: "gfa-table-field-link",
|
|
2493
|
+
onSubmit: applyLink,
|
|
2494
|
+
children: [
|
|
2495
|
+
/* @__PURE__ */ jsx("input", {
|
|
2496
|
+
className: "gfa-input",
|
|
2497
|
+
"aria-label": t("table.linkAddress"),
|
|
2498
|
+
placeholder: "/about",
|
|
2499
|
+
value: linking.address,
|
|
2500
|
+
autoFocus: true,
|
|
2501
|
+
onChange: (event) => setLinking({
|
|
2502
|
+
...linking,
|
|
2503
|
+
address: event.target.value
|
|
2504
|
+
})
|
|
2505
|
+
}),
|
|
2506
|
+
/* @__PURE__ */ jsx("button", {
|
|
2507
|
+
type: "submit",
|
|
2508
|
+
className: "gfa-button gfa-button-secondary",
|
|
2509
|
+
disabled: !linking.range,
|
|
2510
|
+
children: t("table.linkApply")
|
|
2511
|
+
}),
|
|
2512
|
+
/* @__PURE__ */ jsx("button", {
|
|
2513
|
+
type: "button",
|
|
2514
|
+
className: "gfa-button gfa-button-secondary",
|
|
2515
|
+
onClick: () => setLinking(void 0),
|
|
2516
|
+
children: t("action.cancel")
|
|
2517
|
+
}),
|
|
2518
|
+
!linking.range && /* @__PURE__ */ jsx("p", {
|
|
2519
|
+
className: "gfa-hint",
|
|
2520
|
+
children: t("table.linkSelect")
|
|
2521
|
+
})
|
|
2522
|
+
]
|
|
2523
|
+
})
|
|
2524
|
+
]
|
|
2525
|
+
});
|
|
2526
|
+
}
|
|
1817
2527
|
function TableCellsField({ value, onChange, readOnly, label }) {
|
|
1818
2528
|
const t = useStrings();
|
|
2529
|
+
const [active, setActive] = useState();
|
|
1819
2530
|
const rows = tableCells(value);
|
|
1820
2531
|
const width = rows[0]?.length ?? 1;
|
|
1821
2532
|
const set = (row, column, text) => onChange(rows.map((cells, r) => r === row ? cells.map((cell, c) => c === column ? text : cell) : cells));
|
|
@@ -1826,20 +2537,24 @@ function TableCellsField({ value, onChange, readOnly, label }) {
|
|
|
1826
2537
|
className: "gfa-table-field-label",
|
|
1827
2538
|
children: label
|
|
1828
2539
|
}),
|
|
2540
|
+
/* @__PURE__ */ jsx(CellToolbar, {
|
|
2541
|
+
cell: active?.isConnected ? active : void 0,
|
|
2542
|
+
readOnly
|
|
2543
|
+
}),
|
|
1829
2544
|
/* @__PURE__ */ jsxs("div", {
|
|
1830
2545
|
className: "gfa-table-field-grid",
|
|
1831
2546
|
style: { gridTemplateColumns: `repeat(${width}, minmax(6rem, 1fr)) auto` },
|
|
1832
2547
|
children: [rows.map((cells, row) => /* @__PURE__ */ jsxs("div", {
|
|
1833
2548
|
className: "gfa-table-field-row",
|
|
1834
|
-
children: [cells.map((cell, column) => /* @__PURE__ */ jsx(
|
|
1835
|
-
className: "gfa-input gfa-table-field-cell",
|
|
2549
|
+
children: [cells.map((cell, column) => /* @__PURE__ */ jsx(RichCell, {
|
|
1836
2550
|
value: cell,
|
|
1837
2551
|
readOnly,
|
|
1838
|
-
|
|
2552
|
+
label: t("table.cell", {
|
|
1839
2553
|
row: String(row + 1),
|
|
1840
2554
|
column: String(column + 1)
|
|
1841
2555
|
}),
|
|
1842
|
-
onChange: (
|
|
2556
|
+
onChange: (html) => set(row, column, html),
|
|
2557
|
+
onFocus: setActive
|
|
1843
2558
|
}, column)), /* @__PURE__ */ jsx("button", {
|
|
1844
2559
|
type: "button",
|
|
1845
2560
|
className: "gfa-table-field-remove",
|
|
@@ -1932,7 +2647,8 @@ const ADMIN_PLACES = {
|
|
|
1932
2647
|
"contact-settings": `#/settings/general?section=${SETTINGS_SECTIONS.contact}`,
|
|
1933
2648
|
"forms-settings": `#/settings/general?section=${SETTINGS_SECTIONS.forms}`,
|
|
1934
2649
|
"theme-settings": "#/settings/theme",
|
|
1935
|
-
"language-settings": "#/settings/languages"
|
|
2650
|
+
"language-settings": "#/settings/languages",
|
|
2651
|
+
"comments-settings": "#/settings/comments"
|
|
1936
2652
|
};
|
|
1937
2653
|
/** Puck's text field, with the media library's chooser below it for fields that hold a media address. */
|
|
1938
2654
|
function TextFieldWithMedia({ field, value, onChange, readOnly, children }) {
|
|
@@ -1969,9 +2685,12 @@ function canvasConfig(kind, config, layoutConfig, header, footer, site) {
|
|
|
1969
2685
|
children: kind === "header" ? /* @__PURE__ */ jsx("header", {
|
|
1970
2686
|
className: "gf-header",
|
|
1971
2687
|
children
|
|
1972
|
-
}) : /* @__PURE__ */ jsx("footer", {
|
|
2688
|
+
}) : kind === "footer" ? /* @__PURE__ */ jsx("footer", {
|
|
1973
2689
|
className: "gf-footer",
|
|
1974
2690
|
children
|
|
2691
|
+
}) : /* @__PURE__ */ jsx("main", {
|
|
2692
|
+
className: "gf-main",
|
|
2693
|
+
children
|
|
1975
2694
|
})
|
|
1976
2695
|
});
|
|
1977
2696
|
return {
|
|
@@ -2091,6 +2810,13 @@ function Bridge({ channel, settings }) {
|
|
|
2091
2810
|
content: [...previous.content, ...change.blocks]
|
|
2092
2811
|
})
|
|
2093
2812
|
});
|
|
2813
|
+
else if (change.type === "detach") {
|
|
2814
|
+
if (!getSelectorForId(change.id)) throw new Error("The block being changed is no longer on the page.");
|
|
2815
|
+
dispatch({
|
|
2816
|
+
type: "setData",
|
|
2817
|
+
data: (previous) => withBlocksInPlace(previous, change.id, change.blocks)
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2094
2820
|
})
|
|
2095
2821
|
];
|
|
2096
2822
|
return () => {
|
|
@@ -2120,7 +2846,7 @@ const READ_ONLY = {
|
|
|
2120
2846
|
};
|
|
2121
2847
|
function FrameEditor({ open, onOpened }) {
|
|
2122
2848
|
const client = useAdminClient();
|
|
2123
|
-
const { config: siteConfig, pageConfig, layoutConfig, templateConfig } = useFrameCode();
|
|
2849
|
+
const { config: siteConfig, pageConfig, layoutConfig, templateConfig, syncedConfig } = useFrameCode();
|
|
2124
2850
|
const { preview, mediaPreviews } = useAdmin();
|
|
2125
2851
|
const { content: loaded } = useSiteContent();
|
|
2126
2852
|
const { kind, path, title, data } = open;
|
|
@@ -2147,7 +2873,7 @@ function FrameEditor({ open, onOpened }) {
|
|
|
2147
2873
|
loaded.settings.theme,
|
|
2148
2874
|
loaded.customCss,
|
|
2149
2875
|
dark
|
|
2150
|
-
]));
|
|
2876
|
+
]), mediaPreviews);
|
|
2151
2877
|
useEffect(() => showMediaInPreview(document, mediaPreviews), [mediaPreviews]);
|
|
2152
2878
|
useEffect(() => {
|
|
2153
2879
|
const onClick = (event) => {
|
|
@@ -2180,9 +2906,13 @@ function FrameEditor({ open, onOpened }) {
|
|
|
2180
2906
|
siteConfig.blocks
|
|
2181
2907
|
]);
|
|
2182
2908
|
const [view, setView] = useState(() => viewOf(initialData));
|
|
2909
|
+
const headingsOf = useCallback((next) => kind === "entry" || kind === "header" || kind === "footer" ? [] : pageHeadings(next, { components: siteConfig.blocks }), [kind, siteConfig.blocks]);
|
|
2910
|
+
const [headings, setHeadings] = useState(() => headingsOf(initialData));
|
|
2183
2911
|
const updateView = (next) => {
|
|
2184
2912
|
const found = viewOf(next);
|
|
2185
2913
|
setView((current) => current?.block === found?.block ? current : found);
|
|
2914
|
+
const listed = headingsOf(next);
|
|
2915
|
+
setHeadings((current) => JSON.stringify(current) === JSON.stringify(listed) ? current : listed);
|
|
2186
2916
|
};
|
|
2187
2917
|
const adminLink = useCallback((place) => {
|
|
2188
2918
|
const href = ADMIN_PLACES[place];
|
|
@@ -2195,6 +2925,16 @@ function FrameEditor({ open, onOpened }) {
|
|
|
2195
2925
|
}
|
|
2196
2926
|
};
|
|
2197
2927
|
}, [client]);
|
|
2928
|
+
useEffect(() => {
|
|
2929
|
+
const onKeyDown = (event) => {
|
|
2930
|
+
const shortcut = shortcutFor(event, isTyping(event.target));
|
|
2931
|
+
if (!isForwardedShortcut(shortcut)) return;
|
|
2932
|
+
event.preventDefault();
|
|
2933
|
+
client.call("shortcut", shortcut).catch(() => {});
|
|
2934
|
+
};
|
|
2935
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
2936
|
+
return () => document.removeEventListener("keydown", onKeyDown, true);
|
|
2937
|
+
}, [client]);
|
|
2198
2938
|
const callPlugin = useCallback((plugin, method, ...args) => client.call("plugin.call", {
|
|
2199
2939
|
plugin,
|
|
2200
2940
|
method,
|
|
@@ -2208,10 +2948,14 @@ function FrameEditor({ open, onOpened }) {
|
|
|
2208
2948
|
pages: pageLinks(content),
|
|
2209
2949
|
collection,
|
|
2210
2950
|
...view && { view },
|
|
2951
|
+
...headings.length > 0 && { headings },
|
|
2211
2952
|
...languages && {
|
|
2212
2953
|
languages,
|
|
2213
2954
|
home: languageHome(loaded, language)
|
|
2214
2955
|
},
|
|
2956
|
+
...content.descriptions && { descriptions: Object.fromEntries(Object.entries(content.descriptions).map(([url, description]) => [url, describedIn(description, content.settings.language)])) },
|
|
2957
|
+
...content.focus && { focus: Object.fromEntries(Object.entries(content.focus).map(([url, point]) => [url, focusPosition(point)])) },
|
|
2958
|
+
...loaded.syncedBlocks && { syncedBlocks: loaded.syncedBlocks },
|
|
2215
2959
|
adminLink,
|
|
2216
2960
|
callPlugin
|
|
2217
2961
|
}), [
|
|
@@ -2221,35 +2965,59 @@ function FrameEditor({ open, onOpened }) {
|
|
|
2221
2965
|
path,
|
|
2222
2966
|
collection,
|
|
2223
2967
|
view,
|
|
2968
|
+
headings,
|
|
2224
2969
|
languages,
|
|
2225
2970
|
adminLink,
|
|
2226
2971
|
callPlugin
|
|
2227
2972
|
]);
|
|
2228
2973
|
const entry = kind === "entry" && collection && open.entry ? findEntry(loaded, collection.id, open.entry) : void 0;
|
|
2974
|
+
const { puck: dictionary, labels } = useEditorText();
|
|
2975
|
+
const metadata = useMemo(() => siteMetadata(site), [site]);
|
|
2976
|
+
const drawn = useCallback((config) => withSyncedBlockDrawing(config, {
|
|
2977
|
+
content: loaded,
|
|
2978
|
+
language,
|
|
2979
|
+
main: language === mainLanguage(loaded.settings),
|
|
2980
|
+
metadata,
|
|
2981
|
+
navigate: (href) => void client.call("navigate", href)
|
|
2982
|
+
}), [
|
|
2983
|
+
loaded,
|
|
2984
|
+
language,
|
|
2985
|
+
metadata,
|
|
2986
|
+
client
|
|
2987
|
+
]);
|
|
2229
2988
|
const config = useMemo(() => {
|
|
2230
2989
|
if (kind === "entry") {
|
|
2231
2990
|
if (!collection || !entry) throw new Error("The item isn't in the site.");
|
|
2232
|
-
return entryConfig(collection, entry.entry, content, {
|
|
2991
|
+
return withLabels(entryConfig(collection, entry.entry, content, {
|
|
2233
2992
|
templateConfig,
|
|
2234
2993
|
layoutConfig
|
|
2235
|
-
});
|
|
2994
|
+
}), labels);
|
|
2236
2995
|
}
|
|
2237
|
-
return withTableFields(canvasConfig(kind, kind === "page" ? pageConfig : kind === "template" ? templateConfig : layoutConfig, layoutConfig, content.header.data, content.footer.data, site));
|
|
2996
|
+
return withLabels(withTableFields(canvasConfig(kind, drawn(kind === "page" ? pageConfig : kind === "template" ? templateConfig : kind === "synced" ? syncedConfig : layoutConfig), layoutConfig, expandSyncedBlocks(content.header.data, loaded, language), expandSyncedBlocks(content.footer.data, loaded, language), site)), labels);
|
|
2238
2997
|
}, [
|
|
2239
2998
|
kind,
|
|
2240
2999
|
collection,
|
|
2241
3000
|
entry,
|
|
2242
3001
|
pageConfig,
|
|
2243
3002
|
templateConfig,
|
|
3003
|
+
syncedConfig,
|
|
2244
3004
|
layoutConfig,
|
|
2245
3005
|
content,
|
|
2246
|
-
|
|
3006
|
+
loaded,
|
|
3007
|
+
language,
|
|
3008
|
+
site,
|
|
3009
|
+
labels,
|
|
3010
|
+
drawn
|
|
2247
3011
|
]);
|
|
2248
3012
|
const described = useMemo(() => describeConfig(config), [config]);
|
|
2249
|
-
const puckConfig = useMemo(() =>
|
|
3013
|
+
const puckConfig = useMemo(() => {
|
|
3014
|
+
const styled = kind === "entry" ? config : withStyleFields(config, content.settings.theme.colors);
|
|
3015
|
+
return open.ownersOnlyLocked ? withOwnersOnlyLocked(styled) : styled;
|
|
3016
|
+
}, [
|
|
2250
3017
|
kind,
|
|
2251
3018
|
config,
|
|
2252
|
-
content.settings.theme.colors
|
|
3019
|
+
content.settings.theme.colors,
|
|
3020
|
+
open.ownersOnlyLocked
|
|
2253
3021
|
]);
|
|
2254
3022
|
const told = useRef(void 0);
|
|
2255
3023
|
useEffect(() => {
|
|
@@ -2305,8 +3073,9 @@ function FrameEditor({ open, onOpened }) {
|
|
|
2305
3073
|
children: /* @__PURE__ */ jsx(Puck, {
|
|
2306
3074
|
config: puckConfig,
|
|
2307
3075
|
data: initialData,
|
|
2308
|
-
metadata
|
|
3076
|
+
metadata,
|
|
2309
3077
|
headerTitle: title,
|
|
3078
|
+
dictionary,
|
|
2310
3079
|
height: "100%",
|
|
2311
3080
|
iframe: { enabled: false },
|
|
2312
3081
|
ui,
|
|
@@ -2414,7 +3183,8 @@ function CodeService({ channel }) {
|
|
|
2414
3183
|
channel.handle("code.sitePages", ({ content, today }) => code.sitePages(content, today)),
|
|
2415
3184
|
channel.handle("code.pageView", async ({ page, content, today }) => await code.pageView(page, content, today) ?? null),
|
|
2416
3185
|
channel.handle("code.renderedProblems", ({ content, targets, today }) => code.renderedProblems(content, targets, today)),
|
|
2417
|
-
channel.handle("code.drawBlock", ({ content, target }) => code.drawBlock(content, target))
|
|
3186
|
+
channel.handle("code.drawBlock", ({ content, target }) => code.drawBlock(content, target)),
|
|
3187
|
+
channel.handle("code.blockFields", ({ content, target }) => code.blockFields(content, target))
|
|
2418
3188
|
];
|
|
2419
3189
|
return () => {
|
|
2420
3190
|
for (const stop of stops) stop();
|
|
@@ -2429,7 +3199,8 @@ function editorKey(open) {
|
|
|
2429
3199
|
open.path,
|
|
2430
3200
|
open.collection,
|
|
2431
3201
|
open.entry,
|
|
2432
|
-
open.language
|
|
3202
|
+
open.language,
|
|
3203
|
+
open.reopened
|
|
2433
3204
|
]);
|
|
2434
3205
|
}
|
|
2435
3206
|
/** Opens an editor when the admin panel's page asks, and tells it Puck's config once it's open. */
|