tldraw
Version:
A tiny little drawing editor.
237 lines (236 loc) • 7.25 kB
JavaScript
import { jsx, jsxs } from "react/jsx-runtime";
import {
AssetRecordType,
BaseBoxShapeUtil,
HTMLContainer,
T,
bookmarkShapeMigrations,
bookmarkShapeProps,
debounce,
getHashForString,
lerp,
stopEventPropagation,
tlenv,
toDomPrecision,
useSvgExportContext
} from "@tldraw/editor";
import classNames from "classnames";
import { useState } from "react";
import { convertCommonTitleHTMLEntities } from "../../utils/text/text.mjs";
import { HyperlinkButton } from "../shared/HyperlinkButton.mjs";
import { LINK_ICON } from "../shared/icons-editor.mjs";
import { getRotatedBoxShadow } from "../shared/rotated-box-shadow.mjs";
const BOOKMARK_WIDTH = 300;
const BOOKMARK_HEIGHT = 320;
const BOOKMARK_JUST_URL_HEIGHT = 46;
const SHORT_BOOKMARK_HEIGHT = 101;
class BookmarkShapeUtil extends BaseBoxShapeUtil {
static type = "bookmark";
static props = bookmarkShapeProps;
static migrations = bookmarkShapeMigrations;
canResize() {
return false;
}
hideSelectionBoundsFg() {
return true;
}
getText(shape) {
return shape.props.url;
}
getDefaultProps() {
return {
url: "",
w: BOOKMARK_WIDTH,
h: BOOKMARK_HEIGHT,
assetId: null
};
}
component(shape) {
return /* @__PURE__ */ jsx(BookmarkShapeComponent, { shape, util: this });
}
indicator(shape) {
return /* @__PURE__ */ jsx(
"rect",
{
width: toDomPrecision(shape.props.w),
height: toDomPrecision(shape.props.h),
rx: "6",
ry: "6"
}
);
}
onBeforeCreate(next) {
return getBookmarkSize(this.editor, next);
}
onBeforeUpdate(prev, shape) {
if (prev.props.url !== shape.props.url) {
if (!T.linkUrl.isValid(shape.props.url)) {
return { ...shape, props: { ...shape.props, url: prev.props.url } };
} else {
updateBookmarkAssetOnUrlChange(this.editor, shape);
}
}
if (prev.props.assetId !== shape.props.assetId) {
return getBookmarkSize(this.editor, shape);
}
}
getInterpolatedProps(startShape, endShape, t) {
return {
...(t > 0.5 ? endShape.props : startShape.props),
w: lerp(startShape.props.w, endShape.props.w, t),
h: lerp(startShape.props.h, endShape.props.h, t)
};
}
}
function BookmarkShapeComponent({
shape,
util
}) {
const asset = shape.props.assetId ? util.editor.getAsset(shape.props.assetId) : null;
const isSafariExport = !!useSvgExportContext() && tlenv.isSafari;
const pageRotation = util.editor.getShapePageTransform(shape).rotation();
const address = getHumanReadableAddress(shape);
const [isFaviconValid, setIsFaviconValid] = useState(true);
const onFaviconError = () => setIsFaviconValid(false);
return /* @__PURE__ */ jsx(HTMLContainer, { children: /* @__PURE__ */ jsxs(
"div",
{
className: classNames(
"tl-bookmark__container",
isSafariExport && "tl-bookmark__container--safariExport"
),
style: {
boxShadow: isSafariExport ? void 0 : getRotatedBoxShadow(pageRotation),
maxHeight: shape.props.h
},
children: [
(!asset || asset.props.image) && /* @__PURE__ */ jsxs("div", { className: "tl-bookmark__image_container", children: [
asset ? /* @__PURE__ */ jsx(
"img",
{
className: "tl-bookmark__image",
draggable: false,
referrerPolicy: "strict-origin-when-cross-origin",
src: asset?.props.image,
alt: asset?.props.title || ""
}
) : /* @__PURE__ */ jsx("div", { className: "tl-bookmark__placeholder" }),
asset?.props.image && /* @__PURE__ */ jsx(HyperlinkButton, { url: shape.props.url })
] }),
/* @__PURE__ */ jsxs("div", { className: "tl-bookmark__copy_container", children: [
asset?.props.title ? /* @__PURE__ */ jsx("h2", { className: "tl-bookmark__heading", children: convertCommonTitleHTMLEntities(asset.props.title) }) : null,
asset?.props.description && asset?.props.image ? /* @__PURE__ */ jsx("p", { className: "tl-bookmark__description", children: asset.props.description }) : null,
/* @__PURE__ */ jsxs(
"a",
{
className: "tl-bookmark__link",
href: shape.props.url || "",
target: "_blank",
rel: "noopener noreferrer",
onPointerDown: stopEventPropagation,
onPointerUp: stopEventPropagation,
onClick: stopEventPropagation,
children: [
isFaviconValid && asset?.props.favicon ? /* @__PURE__ */ jsx(
"img",
{
className: "tl-bookmark__favicon",
src: asset?.props.favicon,
referrerPolicy: "strict-origin-when-cross-origin",
onError: onFaviconError,
alt: `favicon of ${address}`
}
) : /* @__PURE__ */ jsx(
"div",
{
className: "tl-hyperlink__icon",
style: {
mask: `url("${LINK_ICON}") center 100% / 100% no-repeat`,
WebkitMask: `url("${LINK_ICON}") center 100% / 100% no-repeat`
}
}
),
/* @__PURE__ */ jsx("span", { children: address })
]
}
)
] })
]
}
) });
}
function getBookmarkSize(editor, shape) {
const asset = shape.props.assetId ? editor.getAsset(shape.props.assetId) : null;
let h = BOOKMARK_HEIGHT;
if (asset) {
if (!asset.props.image) {
if (!asset.props.title) {
h = BOOKMARK_JUST_URL_HEIGHT;
} else {
h = SHORT_BOOKMARK_HEIGHT;
}
}
}
return {
...shape,
props: {
...shape.props,
h
}
};
}
const getHumanReadableAddress = (shape) => {
try {
const url = new URL(shape.props.url);
return url.hostname.replace(/^www\./, "");
} catch {
return shape.props.url;
}
};
function updateBookmarkAssetOnUrlChange(editor, shape) {
const { url } = shape.props;
const assetId = AssetRecordType.createId(getHashForString(url));
if (editor.getAsset(assetId)) {
if (shape.props.assetId !== assetId) {
editor.updateShapes([
{
id: shape.id,
type: shape.type,
props: { assetId }
}
]);
}
} else {
editor.updateShapes([
{
id: shape.id,
type: shape.type,
props: { assetId: null }
}
]);
createBookmarkAssetOnUrlChange(editor, shape);
}
}
const createBookmarkAssetOnUrlChange = debounce(async (editor, shape) => {
if (editor.isDisposed) return;
const { url } = shape.props;
const asset = await editor.getAssetForExternalContent({ type: "url", url });
if (!asset) {
return;
}
editor.run(() => {
editor.createAssets([asset]);
editor.updateShapes([
{
id: shape.id,
type: shape.type,
props: { assetId: asset.id }
}
]);
});
}, 500);
export {
BookmarkShapeUtil,
getHumanReadableAddress
};
//# sourceMappingURL=BookmarkShapeUtil.mjs.map