tldraw
Version:
A tiny little drawing editor.
228 lines (227 loc) • 6.28 kB
JavaScript
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
import {
BaseBoxShapeUtil,
Group2d,
Rectangle2d,
SVGContainer,
frameShapeMigrations,
frameShapeProps,
getDefaultColorTheme,
lerp,
resizeBox,
toDomPrecision,
useValue
} from "@tldraw/editor";
import classNames from "classnames";
import {
createTextJsxFromSpans
} from "../shared/createTextJsxFromSpans.mjs";
import { useDefaultColorTheme } from "../shared/useDefaultColorTheme.mjs";
import { FrameHeading } from "./components/FrameHeading.mjs";
import {
getFrameHeadingInfo,
getFrameHeadingOpts,
getFrameHeadingSide,
getFrameHeadingTranslation
} from "./frameHelpers.mjs";
function defaultEmptyAs(str, dflt) {
if (str.match(/^\s*$/)) {
return dflt;
}
return str;
}
class FrameShapeUtil extends BaseBoxShapeUtil {
static type = "frame";
static props = frameShapeProps;
static migrations = frameShapeMigrations;
canEdit() {
return true;
}
getDefaultProps() {
return { w: 160 * 2, h: 90 * 2, name: "" };
}
getGeometry(shape) {
const { editor } = this;
const z = editor.getZoomLevel();
const opts = getFrameHeadingOpts(shape, "black");
const headingInfo = getFrameHeadingInfo(editor, shape, opts);
const labelSide = getFrameHeadingSide(editor, shape);
let x, y, w, h;
const { w: hw, h: hh } = headingInfo.box;
const scaledW = Math.min(hw, shape.props.w * z);
const scaledH = Math.min(hh, shape.props.h * z);
switch (labelSide) {
case 0: {
x = -8 / z;
y = (-hh - 4) / z;
w = (scaledW + 16) / z;
h = hh / z;
break;
}
case 1: {
x = (-hh - 4) / z;
h = (scaledH + 16) / z;
y = shape.props.h - h + 8 / z;
w = hh / z;
break;
}
case 2: {
x = shape.props.w - (scaledW + 8) / z;
y = shape.props.h + 4 / z;
w = (scaledH + 16) / z;
h = hh / z;
break;
}
case 3: {
x = shape.props.w + 4 / z;
h = (scaledH + 16) / z;
y = -8 / z;
w = hh / z;
break;
}
}
return new Group2d({
children: [
new Rectangle2d({
width: shape.props.w,
height: shape.props.h,
isFilled: false
}),
new Rectangle2d({
x,
y,
width: w,
height: h,
isFilled: true,
isLabel: true
})
]
});
}
getText(shape) {
return shape.props.name;
}
component(shape) {
const bounds = this.editor.getShapeGeometry(shape).bounds;
const theme = useDefaultColorTheme();
const isCreating = useValue(
"is creating this shape",
() => {
const resizingState = this.editor.getStateDescendant("select.resizing");
if (!resizingState) return false;
if (!resizingState.getIsActive()) return false;
const info = resizingState?.info;
if (!info) return false;
return info.isCreating && this.editor.getOnlySelectedShapeId() === shape.id;
},
[shape.id]
);
return /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(SVGContainer, { children: /* @__PURE__ */ jsx(
"rect",
{
className: classNames("tl-frame__body", { "tl-frame__creating": isCreating }),
width: bounds.width,
height: bounds.height,
fill: theme.solid,
stroke: theme.text
}
) }),
isCreating ? null : /* @__PURE__ */ jsx(
FrameHeading,
{
id: shape.id,
name: shape.props.name,
width: bounds.width,
height: bounds.height
}
)
] });
}
toSvg(shape, ctx) {
const theme = getDefaultColorTheme({ isDarkMode: ctx.isDarkMode });
const labelSide = getFrameHeadingSide(this.editor, shape);
const labelTranslate = getFrameHeadingTranslation(shape, labelSide, true);
const opts = getFrameHeadingOpts(shape, theme.text);
const { box: labelBounds, spans } = getFrameHeadingInfo(this.editor, shape, opts);
const text = createTextJsxFromSpans(this.editor, spans, opts);
return /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(
"rect",
{
width: shape.props.w,
height: shape.props.h,
fill: theme.solid,
stroke: theme.black.solid,
strokeWidth: 1,
rx: 1,
ry: 1
}
),
/* @__PURE__ */ jsxs("g", { transform: labelTranslate, children: [
/* @__PURE__ */ jsx(
"rect",
{
x: labelBounds.x - 8,
y: labelBounds.y - 4,
width: labelBounds.width + 20,
height: labelBounds.height,
fill: theme.background,
rx: 4,
ry: 4
}
),
text
] })
] });
}
indicator(shape) {
const bounds = this.editor.getShapeGeometry(shape).bounds;
return /* @__PURE__ */ jsx(
"rect",
{
width: toDomPrecision(bounds.width),
height: toDomPrecision(bounds.height),
className: `tl-frame-indicator`
}
);
}
canReceiveNewChildrenOfType(shape, _type) {
return !shape.isLocked;
}
providesBackgroundForChildren() {
return true;
}
canDropShapes(shape, _shapes) {
return !shape.isLocked;
}
onDragShapesOver(frame, shapes) {
if (!shapes.every((child) => child.parentId === frame.id)) {
this.editor.reparentShapes(shapes, frame.id);
}
}
onDragShapesOut(_shape, shapes) {
const parent = this.editor.getShape(_shape.parentId);
const isInGroup = parent && this.editor.isShapeOfType(parent, "group");
if (isInGroup) {
this.editor.reparentShapes(shapes, parent.id);
} else {
this.editor.reparentShapes(shapes, this.editor.getCurrentPageId());
}
}
onResize(shape, info) {
return resizeBox(shape, info);
}
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)
};
}
}
export {
FrameShapeUtil,
defaultEmptyAs
};
//# sourceMappingURL=FrameShapeUtil.mjs.map