UNPKG

sanity-plugin-asset-source-ogimage

Version:

Allow editors to generate social sharing images on the fly inside of Sanity.

436 lines (434 loc) 14.7 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var jsxRuntime = require('react/jsx-runtime'); var sanity = require('sanity'); var ui = require('@sanity/ui'); var isHotkey = require('is-hotkey'); var React = require('react'); var reactDom = require('react-dom'); var icons = require('@sanity/icons'); var mutator = require('@sanity/mutator'); var download = require('downloadjs'); var htmlToImage = require('html-to-image'); var imageUrlBuilder = require('@sanity/image-url'); function _interopDefaultCompat(e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; } var isHotkey__default = /*#__PURE__*/_interopDefaultCompat(isHotkey); var React__default = /*#__PURE__*/_interopDefaultCompat(React); var download__default = /*#__PURE__*/_interopDefaultCompat(download); var imageUrlBuilder__default = /*#__PURE__*/_interopDefaultCompat(imageUrlBuilder); function useEditorLogic(props) { var _a, _b, _c; const captureRef = React__default.default.useRef(null); const schema = sanity.useSchema(); const [status, setStatus] = React__default.default.useState("idle"); const disabled = status === "loading"; const validLayouts = ((_a = props.layouts) == null ? void 0 : _a.filter(layout => layout.component)) || []; const [activeLayout, setActiveLayout] = React__default.default.useState(validLayouts[0]); const [formData, setFormData] = React__default.default.useState( // Only asset sources (which include onSelect) should use the prepare function activeLayout.prepareFormData && props.onSelect ? activeLayout.prepareFormData(props.document) : // Studio tools should start with empty data {}); React__default.default.useEffect(() => { if (!(activeLayout == null ? void 0 : activeLayout.prepareFormData)) return; setFormData(activeLayout.prepareFormData(props.document)); }, [activeLayout, setFormData, props.document]); const schemaType = sanity.createSchema({ name: "default", types: [ // Make sure to include users' custom types in case they're referencing any ...(((_b = schema._original) == null ? void 0 : _b.types) || []).filter( // Exclude native Sanity types s => !s.name.startsWith("sanity.") && !["slug", "geopoint"].includes(s.name)), sanity.defineType({ name: "media-editor.internal", type: "object", fields: activeLayout.fields || [{ name: "title", type: "string" }] })] }).get("media-editor.internal"); const formBuilderOnChange = React.useCallback(changeEvent => { const transitiveId = "fake-id"; const mutation = new mutator.Mutation({ mutations: sanity.toMutationPatches(changeEvent.patches).map(m => ({ patch: { id: transitiveId, ...m } })) }); const newData = mutation.apply({ ...formData, _id: transitiveId }); if (!newData) return; delete newData._id; setFormData(newData); }, [formData, setFormData]); const formState = sanity.useFormState(schemaType, { value: formData, presence: [], validation: [], focusPath: [], openPath: [], comparisonValue: formData }); const patchChannel = React.useMemo(() => sanity.createPatchChannel(), []); const formBuilderProps = !!formState && !!((_c = activeLayout == null ? void 0 : activeLayout.fields) == null ? void 0 : _c.length) && { ...formState, focused: true, changed: false, validation: [], // eslint-disable-next-line __internal_patchChannel: patchChannel, collapsedFieldSets: void 0, collapsedPaths: void 0, onChange: formBuilderOnChange, onFieldGroupSelect: noop, onPathBlur: noop, onPathFocus: noop, onPathOpen: noop, onSetFieldSetCollapsed: noop, onSetPathCollapsed: noop }; async function generateImage(e) { e.preventDefault(); if (!(captureRef == null ? void 0 : captureRef.current)) { return; } try { setStatus("loading"); const imgBase64 = await htmlToImage.toPng(captureRef.current, { quality: 1, pixelRatio: 1, includeQueryParams: true }); setStatus("success"); if (props.onSelect) { props.onSelect([{ kind: "base64", value: imgBase64, assetDocumentProps: { originalFilename: "OG Image - ".concat(activeLayout.title || activeLayout.name, " - ").concat(new Date(Date.now()).toISOString()), source: { name: "asset-source-ogimage", id: "asset-source-ogimage" } } }]); } else { download__default.default(imgBase64, "generated.png"); } } catch (error) { setStatus("error"); console.error(error); } } return { activeLayout, setActiveLayout, disabled, generateImage, captureRef, formData, formBuilderProps }; } function noop() {} const LayoutsPicker = props => { var _a; const { setActiveLayout } = props; const handleClick = React.useCallback(layout => () => setActiveLayout == null ? void 0 : setActiveLayout(layout), [setActiveLayout]); if (!((_a = props.layouts) == null ? void 0 : _a.length) || props.layouts.length < 2 || !props.activeLayout || !setActiveLayout) { return null; } return /* @__PURE__ */jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [/* @__PURE__ */jsxRuntime.jsx(ui.Box, { children: /* @__PURE__ */jsxRuntime.jsx(ui.Text, { children: "Choose layout" }) }), /* @__PURE__ */jsxRuntime.jsx(ui.Inline, { space: 3, children: props.layouts.map((layout, i) => { var _a2, _b; return /* @__PURE__ */jsxRuntime.jsx(ui.Button, { mode: ((_a2 = props.activeLayout) == null ? void 0 : _a2.name) === layout.name ? "default" : "ghost", tone: ((_b = props.activeLayout) == null ? void 0 : _b.name) === layout.name ? "positive" : "default", text: layout.title || layout.name, onClick: handleClick(layout), disabled: props.disabled }, layout.name || layout.title || "".concat(i, "-layout")); }) })] }); }; const DEFAULT_DIMENSIONS = { width: 1200, height: 630 }; const Editor = props => { var _a, _b, _c, _d, _e, _f; const containerRef = React.useRef(null); const [containerRect, setContainerRect] = React.useState((_a = containerRef == null ? void 0 : containerRef.current) == null ? void 0 : _a.getBoundingClientRect()); const { activeLayout, setActiveLayout, generateImage, disabled, captureRef, formData, formBuilderProps } = useEditorLogic(props); React.useEffect(() => { const observer = new ResizeObserver(entries => { var _a2; for (const entry of entries) { setContainerRect((_a2 = entry.target) == null ? void 0 : _a2.getBoundingClientRect()); } }); if (containerRef == null ? void 0 : containerRef.current) observer.observe(containerRef == null ? void 0 : containerRef.current); return () => { observer.disconnect(); }; }, [containerRef]); const LayoutComponent = activeLayout.component; const width = ((_b = activeLayout.dimensions) == null ? void 0 : _b.width) || DEFAULT_DIMENSIONS.width; const height = ((_c = activeLayout.dimensions) == null ? void 0 : _c.height) || DEFAULT_DIMENSIONS.height; return /* @__PURE__ */jsxRuntime.jsxs(ui.Card, { scheme: "light", height: "fill", sizing: "border", display: "flex", style: { flexDirection: "column" }, children: [/* @__PURE__ */jsxRuntime.jsx(ui.Card, { tone: "default", padding: 4, marginBottom: [4, 0], borderBottom: true, style: { textAlign: "right" }, children: /* @__PURE__ */jsxRuntime.jsxs(ui.Flex, { justify: "space-between", align: "center", children: [/* @__PURE__ */jsxRuntime.jsxs(ui.Inline, { space: 3, children: [/* @__PURE__ */jsxRuntime.jsx(ui.Text, { size: 3, weight: "semibold", children: ((_d = props.dialog) == null ? void 0 : _d.title) || "Create image" }), /* @__PURE__ */jsxRuntime.jsx(ui.Button, { icon: disabled ? ui.Spinner : icons.GenerateIcon, tone: "positive", text: ((_e = props.dialog) == null ? void 0 : _e.finishCta) || "Generate", onClick: generateImage, disabled })] }), props.onClose && /* @__PURE__ */jsxRuntime.jsx(ui.Button, { onClick: props.onClose, icon: icons.CloseIcon, mode: "bleed", tone: "critical", title: ((_f = props.dialog) == null ? void 0 : _f.ariaClose) || "close" })] }) }), /* @__PURE__ */jsxRuntime.jsxs(ui.Flex, { justify: "flex-start", align: "stretch", height: "fill", width: "100%", sizing: "border", padding: 3, children: [formBuilderProps && /* @__PURE__ */jsxRuntime.jsx(ui.Card, { paddingLeft: 2, paddingTop: 2, paddingRight: 4, paddingBottom: 4, marginRight: 3, style: { maxWidth: "370px", flex: "1 0 200px" }, sizing: "border", overflow: "auto", height: "fill", children: /* @__PURE__ */jsxRuntime.jsx(ui.Stack, { space: 4, children: /* @__PURE__ */jsxRuntime.jsx(ui.Layer, { zOffset: 1e4, children: /* @__PURE__ */jsxRuntime.jsx(sanity.FormBuilder, { ...formBuilderProps }) }) }) }), /* @__PURE__ */jsxRuntime.jsx(ui.Card, { height: "fill", overflow: "hidden", style: { padding: "20px 10px", maxWidth: "".concat(width + 10 * 2, "px"), flex: 2 }, sizing: "border", children: /* @__PURE__ */jsxRuntime.jsxs(ui.Flex, { gap: 3, direction: "column", children: [/* @__PURE__ */jsxRuntime.jsx(LayoutsPicker, { layouts: props.layouts, activeLayout, disabled, setActiveLayout }), /* @__PURE__ */jsxRuntime.jsx("div", { style: { flex: 1, overflow: "hidden" }, ref: containerRef, children: /* @__PURE__ */jsxRuntime.jsx(ui.Card, { style: { width: "".concat(width, "px"), height: "".concat(height, "px"), transformOrigin: "top left", transform: getScaleFitTransform(containerRect, width, height) }, border: true, sizing: "border", overflow: "hidden", children: /* @__PURE__ */jsxRuntime.jsx("div", { ref: captureRef, style: { width: "".concat(width, "px"), height: "".concat(height, "px"), boxSizing: "border-box", overflow: "hidden", // Hack to display the full border of the parent card transform: "scale(0.99)" }, children: LayoutComponent && /* @__PURE__ */jsxRuntime.jsx(LayoutComponent, { document: props.document, formData }) }) }) })] }) })] })] }); }; function getScaleFitTransform(containerRect, width, height) { if (!containerRect) return void 0; const scaleX = containerRect.width / width; const scaleY = containerRect.height / height; const scale = Math.min(scaleX, scaleY); if (scale > 1) return void 0; return "scale(".concat(scale, ")"); } const EditorInDialog = props => { var _a; const { onClose } = props; const handleGlobalKeyDown = React__default.default.useCallback(event => { if (isHotkey__default.default("esc", event) && onClose) { onClose(); } }, [onClose]); ui.useGlobalKeyDown(handleGlobalKeyDown); const layouts = ((_a = props.layouts) == null ? void 0 : _a.filter(layout => typeof layout.component === "function")) || []; if (!(layouts == null ? void 0 : layouts.length)) { if (props.onClose) { props.onClose(); } return null; } return /* @__PURE__ */jsxRuntime.jsx(ui.ThemeProvider, { theme: ui.studioTheme, children: props.context === "tool" ? /* @__PURE__ */jsxRuntime.jsx(ui.Box, { style: { height: "100%", position: "relative" }, children: /* @__PURE__ */jsxRuntime.jsx(Editor, { ...props }) }) : // Use a portal to render outside the document pane reactDom.createPortal( /* @__PURE__ */jsxRuntime.jsx(ui.Box, { style: { position: "fixed", bottom: 0, left: 0, top: 0, width: "100%", height: "100%", zIndex: 1e5 }, children: /* @__PURE__ */jsxRuntime.jsx(Editor, { ...props }) }), document.body) }); }; function BuildEditorAssetSource(config) { return function EditorAssetSource(props) { var _a; const document = sanity.useFormValue([]) || { _id: "loading_doc", _type: "loading_type", _createdAt: "loading_createdAt", _updatedAt: "loading_updatedAt", _rev: "loading_rev" }; if (!((_a = config.layouts) == null ? void 0 : _a.some(layout => typeof layout.component === "function"))) { throw new Error("[media-editor] No valid layouts configured"); } if (props.assetType !== "image") { throw new Error("[media-editor] This asset source only supports images"); } if (props.selectionType !== "single") { throw new Error("[media-editor] This asset source only supports single selection"); } return /* @__PURE__ */jsxRuntime.jsx(EditorInDialog, { ...props, ...config, context: "asset-source", document }); }; } const SanityImage = props => { var _a, _b; const projectId = sanity.useProjectId(); const dataset = sanity.useDataset(); const builder = imageUrlBuilder__default.default({ projectId, dataset }); if (!((_b = (_a = props.image) == null ? void 0 : _a.asset) == null ? void 0 : _b._ref)) { return null; } const src = builder.image(props.image).width(props.width || 500).format("png").url(); if (!src) { return null; } return /* @__PURE__ */jsxRuntime.jsx("img", { src, alt: "" }); }; exports.BuildEditorAssetSource = BuildEditorAssetSource; exports.EditorInDialog = EditorInDialog; exports.SanityImage = SanityImage; //# sourceMappingURL=index.js.map