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
JavaScript
'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