@goodfellow-cms/admin 0.8.0 → 0.10.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-B1CoPEu6.js +273 -0
- package/dist/{plugin-protocol-BgYL9WSm.js → custom-font-faces-DCan2jgW.js} +75 -7
- package/dist/editor.js +664 -60
- package/dist/entry-values-CAPB0igb.js +5005 -0
- package/dist/{es-Dxyy-khR.js → es-Z-QY8sdc.js} +572 -480
- package/dist/es-labels-DBu66eln.js +520 -0
- package/dist/index.d.ts +2 -1579
- package/dist/index.js +10013 -5113
- package/dist/plugin.js +7 -2
- package/dist/strings-Bd9r6qQn.d.ts +2142 -0
- package/dist/strings-CgC6pd6k.js +2161 -0
- package/dist/text.d.ts +52 -0
- package/dist/text.js +22 -0
- package/package.json +16 -7
- package/styles.css +556 -0
- package/dist/entry-values-Bu8ljnoX.js +0 -5438
package/dist/editor.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
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-DCan2jgW.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-CAPB0igb.js";
|
|
3
|
+
import { i as useStrings, t as StringsProvider } from "./strings-CgC6pd6k.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";
|
|
4
5
|
import { Box } from "lucide-react";
|
|
5
|
-
import { createContext, createElement, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
6
|
+
import { createContext, createElement, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
6
7
|
import { createRoot } from "react-dom/client";
|
|
7
8
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
-
import { PageBody, PageContent, SiteProvider, adminSite, applyEntry, blockIcon, blockKeywords, createPuckConfig, createPuckConfigs, cx, isStyleField, isTableField, mediaFieldKind, preparePage, siteMetadata, tableCells } from "@goodfellow-cms/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
|
+
}) };
|
|
1068
|
+
}
|
|
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
|
+
}
|
|
889
1130
|
}
|
|
890
|
-
function fieldControl(field, markdown,
|
|
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]);
|
|
@@ -1495,20 +1824,33 @@ function withOwnersOnlyLocked(config) {
|
|
|
1495
1824
|
}
|
|
1496
1825
|
//#endregion
|
|
1497
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
|
+
};
|
|
1498
1834
|
/** For pages and templates, whose settings include `noIndex`; `path` is the page's, for pages. */
|
|
1499
1835
|
function shownSettings(original, path) {
|
|
1500
1836
|
const fixed = path !== void 0 && neverIndexed(path);
|
|
1501
1837
|
const originalRoot = original.root;
|
|
1502
|
-
const
|
|
1838
|
+
const originalProps = originalRoot.props ?? {};
|
|
1503
1839
|
return {
|
|
1504
1840
|
show(data) {
|
|
1505
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
|
+
};
|
|
1506
1848
|
if (fixed) return {
|
|
1507
1849
|
...data,
|
|
1508
1850
|
root: {
|
|
1509
1851
|
...root,
|
|
1510
1852
|
props: {
|
|
1511
|
-
...
|
|
1853
|
+
...props,
|
|
1512
1854
|
noIndex: true
|
|
1513
1855
|
},
|
|
1514
1856
|
readOnly: {
|
|
@@ -1517,32 +1859,29 @@ function shownSettings(original, path) {
|
|
|
1517
1859
|
}
|
|
1518
1860
|
}
|
|
1519
1861
|
};
|
|
1520
|
-
if (root.props?.noIndex !== void 0) return data;
|
|
1521
1862
|
return {
|
|
1522
1863
|
...data,
|
|
1523
1864
|
root: {
|
|
1524
1865
|
...root,
|
|
1525
|
-
props
|
|
1526
|
-
...root.props,
|
|
1527
|
-
noIndex: false
|
|
1528
|
-
}
|
|
1866
|
+
props
|
|
1529
1867
|
}
|
|
1530
1868
|
};
|
|
1531
1869
|
},
|
|
1532
1870
|
keep(data) {
|
|
1533
1871
|
const root = data.root;
|
|
1534
|
-
const
|
|
1535
|
-
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
|
+
}
|
|
1536
1878
|
const { readOnly: _shown, ...rest } = root;
|
|
1537
1879
|
const readOnly = fixed ? originalRoot.readOnly : root.readOnly;
|
|
1538
1880
|
return {
|
|
1539
1881
|
...data,
|
|
1540
1882
|
root: {
|
|
1541
1883
|
...rest,
|
|
1542
|
-
props
|
|
1543
|
-
...props,
|
|
1544
|
-
noIndex: kept
|
|
1545
|
-
},
|
|
1884
|
+
props,
|
|
1546
1885
|
...readOnly && { readOnly }
|
|
1547
1886
|
}
|
|
1548
1887
|
};
|
|
@@ -1964,16 +2303,230 @@ function withStyleFields(config, colors) {
|
|
|
1964
2303
|
};
|
|
1965
2304
|
}
|
|
1966
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
|
+
}
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
//#endregion
|
|
1967
2375
|
//#region src/frame/table-field.tsx
|
|
1968
2376
|
/**
|
|
1969
2377
|
* The grid of cells a table's field is edited in (`tableField()` in
|
|
1970
2378
|
* `@goodfellow-cms/react`), which Puck's editor shows in place of the field's
|
|
1971
|
-
* 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()`).
|
|
1972
2382
|
*/
|
|
1973
2383
|
const MAX_ROWS = 100;
|
|
1974
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
|
+
}
|
|
1975
2527
|
function TableCellsField({ value, onChange, readOnly, label }) {
|
|
1976
2528
|
const t = useStrings();
|
|
2529
|
+
const [active, setActive] = useState();
|
|
1977
2530
|
const rows = tableCells(value);
|
|
1978
2531
|
const width = rows[0]?.length ?? 1;
|
|
1979
2532
|
const set = (row, column, text) => onChange(rows.map((cells, r) => r === row ? cells.map((cell, c) => c === column ? text : cell) : cells));
|
|
@@ -1984,20 +2537,24 @@ function TableCellsField({ value, onChange, readOnly, label }) {
|
|
|
1984
2537
|
className: "gfa-table-field-label",
|
|
1985
2538
|
children: label
|
|
1986
2539
|
}),
|
|
2540
|
+
/* @__PURE__ */ jsx(CellToolbar, {
|
|
2541
|
+
cell: active?.isConnected ? active : void 0,
|
|
2542
|
+
readOnly
|
|
2543
|
+
}),
|
|
1987
2544
|
/* @__PURE__ */ jsxs("div", {
|
|
1988
2545
|
className: "gfa-table-field-grid",
|
|
1989
2546
|
style: { gridTemplateColumns: `repeat(${width}, minmax(6rem, 1fr)) auto` },
|
|
1990
2547
|
children: [rows.map((cells, row) => /* @__PURE__ */ jsxs("div", {
|
|
1991
2548
|
className: "gfa-table-field-row",
|
|
1992
|
-
children: [cells.map((cell, column) => /* @__PURE__ */ jsx(
|
|
1993
|
-
className: "gfa-input gfa-table-field-cell",
|
|
2549
|
+
children: [cells.map((cell, column) => /* @__PURE__ */ jsx(RichCell, {
|
|
1994
2550
|
value: cell,
|
|
1995
2551
|
readOnly,
|
|
1996
|
-
|
|
2552
|
+
label: t("table.cell", {
|
|
1997
2553
|
row: String(row + 1),
|
|
1998
2554
|
column: String(column + 1)
|
|
1999
2555
|
}),
|
|
2000
|
-
onChange: (
|
|
2556
|
+
onChange: (html) => set(row, column, html),
|
|
2557
|
+
onFocus: setActive
|
|
2001
2558
|
}, column)), /* @__PURE__ */ jsx("button", {
|
|
2002
2559
|
type: "button",
|
|
2003
2560
|
className: "gfa-table-field-remove",
|
|
@@ -2128,9 +2685,12 @@ function canvasConfig(kind, config, layoutConfig, header, footer, site) {
|
|
|
2128
2685
|
children: kind === "header" ? /* @__PURE__ */ jsx("header", {
|
|
2129
2686
|
className: "gf-header",
|
|
2130
2687
|
children
|
|
2131
|
-
}) : /* @__PURE__ */ jsx("footer", {
|
|
2688
|
+
}) : kind === "footer" ? /* @__PURE__ */ jsx("footer", {
|
|
2132
2689
|
className: "gf-footer",
|
|
2133
2690
|
children
|
|
2691
|
+
}) : /* @__PURE__ */ jsx("main", {
|
|
2692
|
+
className: "gf-main",
|
|
2693
|
+
children
|
|
2134
2694
|
})
|
|
2135
2695
|
});
|
|
2136
2696
|
return {
|
|
@@ -2250,6 +2810,13 @@ function Bridge({ channel, settings }) {
|
|
|
2250
2810
|
content: [...previous.content, ...change.blocks]
|
|
2251
2811
|
})
|
|
2252
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
|
+
}
|
|
2253
2820
|
})
|
|
2254
2821
|
];
|
|
2255
2822
|
return () => {
|
|
@@ -2279,7 +2846,7 @@ const READ_ONLY = {
|
|
|
2279
2846
|
};
|
|
2280
2847
|
function FrameEditor({ open, onOpened }) {
|
|
2281
2848
|
const client = useAdminClient();
|
|
2282
|
-
const { config: siteConfig, pageConfig, layoutConfig, templateConfig } = useFrameCode();
|
|
2849
|
+
const { config: siteConfig, pageConfig, layoutConfig, templateConfig, syncedConfig } = useFrameCode();
|
|
2283
2850
|
const { preview, mediaPreviews } = useAdmin();
|
|
2284
2851
|
const { content: loaded } = useSiteContent();
|
|
2285
2852
|
const { kind, path, title, data } = open;
|
|
@@ -2306,7 +2873,7 @@ function FrameEditor({ open, onOpened }) {
|
|
|
2306
2873
|
loaded.settings.theme,
|
|
2307
2874
|
loaded.customCss,
|
|
2308
2875
|
dark
|
|
2309
|
-
]));
|
|
2876
|
+
]), mediaPreviews);
|
|
2310
2877
|
useEffect(() => showMediaInPreview(document, mediaPreviews), [mediaPreviews]);
|
|
2311
2878
|
useEffect(() => {
|
|
2312
2879
|
const onClick = (event) => {
|
|
@@ -2339,9 +2906,13 @@ function FrameEditor({ open, onOpened }) {
|
|
|
2339
2906
|
siteConfig.blocks
|
|
2340
2907
|
]);
|
|
2341
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));
|
|
2342
2911
|
const updateView = (next) => {
|
|
2343
2912
|
const found = viewOf(next);
|
|
2344
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);
|
|
2345
2916
|
};
|
|
2346
2917
|
const adminLink = useCallback((place) => {
|
|
2347
2918
|
const href = ADMIN_PLACES[place];
|
|
@@ -2354,6 +2925,16 @@ function FrameEditor({ open, onOpened }) {
|
|
|
2354
2925
|
}
|
|
2355
2926
|
};
|
|
2356
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]);
|
|
2357
2938
|
const callPlugin = useCallback((plugin, method, ...args) => client.call("plugin.call", {
|
|
2358
2939
|
plugin,
|
|
2359
2940
|
method,
|
|
@@ -2367,10 +2948,14 @@ function FrameEditor({ open, onOpened }) {
|
|
|
2367
2948
|
pages: pageLinks(content),
|
|
2368
2949
|
collection,
|
|
2369
2950
|
...view && { view },
|
|
2951
|
+
...headings.length > 0 && { headings },
|
|
2370
2952
|
...languages && {
|
|
2371
2953
|
languages,
|
|
2372
2954
|
home: languageHome(loaded, language)
|
|
2373
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 },
|
|
2374
2959
|
adminLink,
|
|
2375
2960
|
callPlugin
|
|
2376
2961
|
}), [
|
|
@@ -2380,12 +2965,26 @@ function FrameEditor({ open, onOpened }) {
|
|
|
2380
2965
|
path,
|
|
2381
2966
|
collection,
|
|
2382
2967
|
view,
|
|
2968
|
+
headings,
|
|
2383
2969
|
languages,
|
|
2384
2970
|
adminLink,
|
|
2385
2971
|
callPlugin
|
|
2386
2972
|
]);
|
|
2387
2973
|
const entry = kind === "entry" && collection && open.entry ? findEntry(loaded, collection.id, open.entry) : void 0;
|
|
2388
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
|
+
]);
|
|
2389
2988
|
const config = useMemo(() => {
|
|
2390
2989
|
if (kind === "entry") {
|
|
2391
2990
|
if (!collection || !entry) throw new Error("The item isn't in the site.");
|
|
@@ -2394,17 +2993,21 @@ function FrameEditor({ open, onOpened }) {
|
|
|
2394
2993
|
layoutConfig
|
|
2395
2994
|
}), labels);
|
|
2396
2995
|
}
|
|
2397
|
-
return withLabels(withTableFields(canvasConfig(kind, kind === "page" ? pageConfig : kind === "template" ? templateConfig : layoutConfig, layoutConfig, content.header.data, content.footer.data, site)), labels);
|
|
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);
|
|
2398
2997
|
}, [
|
|
2399
2998
|
kind,
|
|
2400
2999
|
collection,
|
|
2401
3000
|
entry,
|
|
2402
3001
|
pageConfig,
|
|
2403
3002
|
templateConfig,
|
|
3003
|
+
syncedConfig,
|
|
2404
3004
|
layoutConfig,
|
|
2405
3005
|
content,
|
|
3006
|
+
loaded,
|
|
3007
|
+
language,
|
|
2406
3008
|
site,
|
|
2407
|
-
labels
|
|
3009
|
+
labels,
|
|
3010
|
+
drawn
|
|
2408
3011
|
]);
|
|
2409
3012
|
const described = useMemo(() => describeConfig(config), [config]);
|
|
2410
3013
|
const puckConfig = useMemo(() => {
|
|
@@ -2470,7 +3073,7 @@ function FrameEditor({ open, onOpened }) {
|
|
|
2470
3073
|
children: /* @__PURE__ */ jsx(Puck, {
|
|
2471
3074
|
config: puckConfig,
|
|
2472
3075
|
data: initialData,
|
|
2473
|
-
metadata
|
|
3076
|
+
metadata,
|
|
2474
3077
|
headerTitle: title,
|
|
2475
3078
|
dictionary,
|
|
2476
3079
|
height: "100%",
|
|
@@ -2580,7 +3183,8 @@ function CodeService({ channel }) {
|
|
|
2580
3183
|
channel.handle("code.sitePages", ({ content, today }) => code.sitePages(content, today)),
|
|
2581
3184
|
channel.handle("code.pageView", async ({ page, content, today }) => await code.pageView(page, content, today) ?? null),
|
|
2582
3185
|
channel.handle("code.renderedProblems", ({ content, targets, today }) => code.renderedProblems(content, targets, today)),
|
|
2583
|
-
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))
|
|
2584
3188
|
];
|
|
2585
3189
|
return () => {
|
|
2586
3190
|
for (const stop of stops) stop();
|