tldraw
Version:
A tiny little drawing editor.
356 lines (355 loc) • 14.5 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var FrameShapeUtil_exports = {};
__export(FrameShapeUtil_exports, {
FrameShapeUtil: () => FrameShapeUtil
});
module.exports = __toCommonJS(FrameShapeUtil_exports);
var import_jsx_runtime = require("react/jsx-runtime");
var import_editor = require("@tldraw/editor");
var import_classnames = __toESM(require("classnames"), 1);
var import_frames = require("../../utils/frames/frames");
var import_createTextJsxFromSpans = require("../shared/createTextJsxFromSpans");
var import_useDefaultColorTheme = require("../shared/useDefaultColorTheme");
var import_FrameHeading = require("./components/FrameHeading");
var import_frameHelpers = require("./frameHelpers");
const FRAME_HEADING_EXTRA_WIDTH = 12;
const FRAME_HEADING_MIN_WIDTH = 32;
const FRAME_HEADING_NOCOLORS_OFFSET_X = -7;
const FRAME_HEADING_OFFSET_Y = 4;
class FrameShapeUtil extends import_editor.BaseBoxShapeUtil {
static type = "frame";
static props = import_editor.frameShapeProps;
static migrations = import_editor.frameShapeMigrations;
options = {
showColors: false,
resizeChildren: false
};
// evil crimes :)
// By default, showColors is off. Because they use style props, which are picked up
// automatically, we don't have DefaultColorStyle in the props in the schema by default.
// Instead, when someone calls .configure to turn the option on, we manually add in the color
// style here so it plays nicely with the other editor APIs.
static configure(options) {
const withOptions = super.configure.call(this, options);
if (options.showColors) {
;
withOptions.props = { ...withOptions.props, color: import_editor.DefaultColorStyle };
}
return withOptions;
}
canEdit(shape, info) {
return info.type === "click-header" || info.type === "unknown";
}
canResize() {
return true;
}
canResizeChildren() {
return this.options.resizeChildren;
}
isExportBoundsContainer() {
return true;
}
getDefaultProps() {
return { w: 160 * 2, h: 90 * 2, name: "", color: "black" };
}
getAriaDescriptor(shape) {
return shape.props.name;
}
getGeometry(shape) {
const { editor } = this;
const z = editor.getEfficientZoomLevel();
const labelSide = (0, import_frameHelpers.getFrameHeadingSide)(editor, shape);
const isVertical = labelSide % 2 === 1;
const rotatedTopEdgeWidth = isVertical ? shape.props.h : shape.props.w;
const opts = (0, import_frameHelpers.getFrameHeadingOpts)(rotatedTopEdgeWidth, false);
const headingSize = (0, import_frameHelpers.getFrameHeadingSize)(editor, shape, opts);
const isShowingFrameColors = this.options.showColors;
const extraWidth = FRAME_HEADING_EXTRA_WIDTH / z;
const minWidth = FRAME_HEADING_MIN_WIDTH / z;
const maxWidth = rotatedTopEdgeWidth + (isShowingFrameColors ? 1 : extraWidth);
const labelWidth = headingSize.w / z;
const labelHeight = headingSize.h / z;
const clampedLabelWidth = (0, import_editor.clamp)(labelWidth + extraWidth, minWidth, maxWidth);
const offsetX = (isShowingFrameColors ? -1 : FRAME_HEADING_NOCOLORS_OFFSET_X) / z;
const offsetY = FRAME_HEADING_OFFSET_Y / z;
const width = isVertical ? labelHeight : clampedLabelWidth;
const height = isVertical ? clampedLabelWidth : labelHeight;
let x, y;
switch (labelSide) {
case 0: {
x = offsetX;
y = -(labelHeight + offsetY);
break;
}
case 1: {
x = -(labelHeight + offsetY);
y = shape.props.h - (offsetX + clampedLabelWidth);
break;
}
case 2: {
x = shape.props.w - (offsetX + clampedLabelWidth);
y = shape.props.h + offsetY;
break;
}
case 3: {
x = shape.props.w + offsetY;
y = offsetX;
break;
}
}
return new import_editor.Group2d({
children: [
new import_editor.Rectangle2d({
width: shape.props.w,
height: shape.props.h,
isFilled: false
}),
new import_editor.Rectangle2d({
x,
y,
width,
height,
isFilled: true,
isLabel: true,
excludeFromShapeBounds: true
})
]
});
}
getText(shape) {
return shape.props.name;
}
component(shape) {
const theme = (0, import_useDefaultColorTheme.useDefaultColorTheme)();
const isCreating = (0, import_editor.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]
);
const showFrameColors = this.options.showColors;
const colorToUse = showFrameColors ? shape.props.color : "black";
const frameFill = (0, import_editor.getColorValue)(theme, colorToUse, "frameFill");
const frameStroke = (0, import_editor.getColorValue)(theme, colorToUse, "frameStroke");
const frameHeadingStroke = showFrameColors ? (0, import_editor.getColorValue)(theme, colorToUse, "frameHeadingStroke") : theme.background;
const frameHeadingFill = showFrameColors ? (0, import_editor.getColorValue)(theme, colorToUse, "frameHeadingFill") : theme.background;
const frameHeadingText = (0, import_editor.getColorValue)(theme, colorToUse, "frameText");
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_editor.SVGContainer, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"rect",
{
className: (0, import_classnames.default)("tl-frame__body", { "tl-frame__creating": isCreating }),
fill: frameFill,
stroke: frameStroke,
style: {
width: `calc(${shape.props.w}px + 1px / var(--tl-zoom))`,
height: `calc(${shape.props.h}px + 1px / var(--tl-zoom))`,
transform: `translate(calc(-0.5px / var(--tl-zoom)), calc(-0.5px / var(--tl-zoom)))`
}
}
) }),
isCreating ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_FrameHeading.FrameHeading,
{
id: shape.id,
name: shape.props.name,
fill: frameHeadingFill,
stroke: frameHeadingStroke,
color: frameHeadingText,
width: shape.props.w,
height: shape.props.h,
offsetX: showFrameColors ? -1 : -7,
showColors: this.options.showColors
}
)
] });
}
toSvg(shape, ctx) {
const theme = (0, import_editor.getDefaultColorTheme)({ isDarkMode: ctx.isDarkMode });
const labelSide = (0, import_frameHelpers.getFrameHeadingSide)(this.editor, shape);
const isVertical = labelSide % 2 === 1;
const rotatedTopEdgeWidth = isVertical ? shape.props.h : shape.props.w;
const labelTranslate = (0, import_frameHelpers.getFrameHeadingTranslation)(shape, labelSide, true);
const opts = (0, import_frameHelpers.getFrameHeadingOpts)(rotatedTopEdgeWidth - 12, true);
const frameTitle = (0, import_frameHelpers.defaultEmptyAs)(shape.props.name, "Frame") + String.fromCharCode(8203);
const labelBounds = (0, import_frameHelpers.getFrameHeadingSize)(this.editor, shape, opts);
const spans = this.editor.textMeasure.measureTextSpans(frameTitle, opts);
const text = (0, import_createTextJsxFromSpans.createTextJsxFromSpans)(this.editor, spans, opts);
const showFrameColors = this.options.showColors;
const colorToUse = showFrameColors ? shape.props.color : "black";
const frameFill = (0, import_editor.getColorValue)(theme, colorToUse, "frameFill");
const frameStroke = (0, import_editor.getColorValue)(theme, colorToUse, "frameStroke");
const frameHeadingStroke = showFrameColors ? (0, import_editor.getColorValue)(theme, colorToUse, "frameHeadingStroke") : theme.background;
const frameHeadingFill = showFrameColors ? (0, import_editor.getColorValue)(theme, colorToUse, "frameHeadingFill") : theme.background;
const frameHeadingText = (0, import_editor.getColorValue)(theme, colorToUse, "frameText");
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"rect",
{
width: shape.props.w,
height: shape.props.h,
fill: frameFill,
stroke: frameStroke,
strokeWidth: 1,
x: 0,
rx: 0,
ry: 0
}
),
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("g", { fill: frameHeadingText, transform: labelTranslate, children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"rect",
{
x: labelBounds.x - (showFrameColors ? 0 : 6),
y: labelBounds.y - 6,
width: Math.min(rotatedTopEdgeWidth, labelBounds.width + 12),
height: labelBounds.height,
fill: frameHeadingFill,
stroke: frameHeadingStroke,
rx: 4,
ry: 4
}
),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("g", { transform: `translate(${showFrameColors ? 8 : 0}, 4)`, children: text })
] })
] });
}
indicator(shape) {
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { width: (0, import_editor.toDomPrecision)(shape.props.w), height: (0, import_editor.toDomPrecision)(shape.props.h) });
}
useLegacyIndicator() {
return false;
}
getIndicatorPath(shape) {
const path = new Path2D();
path.rect(0, 0, shape.props.w, shape.props.h);
return path;
}
providesBackgroundForChildren() {
return true;
}
getClipPath(shape) {
return this.editor.getShapeGeometry(shape.id).vertices;
}
canReceiveNewChildrenOfType(shape) {
return !shape.isLocked;
}
onResize(shape, info) {
return (0, import_editor.resizeBox)(shape, info);
}
getInterpolatedProps(startShape, endShape, t) {
return {
...t > 0.5 ? endShape.props : startShape.props,
w: (0, import_editor.lerp)(startShape.props.w, endShape.props.w, t),
h: (0, import_editor.lerp)(startShape.props.h, endShape.props.h, t)
};
}
onDoubleClickEdge(shape, info) {
if (info.target !== "selection") return;
const { handle } = info;
if (!handle) return;
const isHorizontalEdge = handle === "left" || handle === "right";
const isVerticalEdge = handle === "top" || handle === "bottom";
const childIds = this.editor.getSortedChildIdsForParent(shape.id);
const children = (0, import_editor.compact)(childIds.map((id) => this.editor.getShape(id)));
if (!children.length) return;
const { dx, dy, w, h } = (0, import_frames.getFrameChildrenBounds)(children, this.editor, { padding: 10 });
this.editor.run(() => {
const changes = childIds.map((childId) => {
const childShape = this.editor.getShape(childId);
return {
id: childShape.id,
type: childShape.type,
x: isHorizontalEdge ? childShape.x + dx : childShape.x,
y: isVerticalEdge ? childShape.y + dy : childShape.y
};
});
this.editor.updateShapes(changes);
});
return {
id: shape.id,
type: shape.type,
props: {
w: isHorizontalEdge ? w : shape.props.w,
h: isVerticalEdge ? h : shape.props.h
}
};
}
onDoubleClickCorner(shape) {
(0, import_frames.fitFrameToContent)(this.editor, shape.id, { padding: 10 });
return {
id: shape.id,
type: shape.type
};
}
onDragShapesIn(shape, draggingShapes, { initialParentIds, initialIndices }) {
const { editor } = this;
if (draggingShapes.every((s) => s.parentId === shape.id)) return;
let canRestoreOriginalIndices = false;
const previousChildren = draggingShapes.filter((s) => shape.id === initialParentIds.get(s.id));
if (previousChildren.length > 0) {
const currentChildren = (0, import_editor.compact)(
editor.getSortedChildIdsForParent(shape).map((id) => editor.getShape(id))
);
if (previousChildren.every((s) => !currentChildren.find((c) => c.index === s.index))) {
canRestoreOriginalIndices = true;
}
}
if (draggingShapes.some((s) => editor.hasAncestor(shape, s.id))) return;
editor.reparentShapes(draggingShapes, shape.id);
if (canRestoreOriginalIndices) {
for (const shape2 of previousChildren) {
editor.updateShape({
id: shape2.id,
type: shape2.type,
index: initialIndices.get(shape2.id)
});
}
}
}
onDragShapesOut(shape, draggingShapes, info) {
const { editor } = this;
if (!info.nextDraggingOverShapeId) {
editor.reparentShapes(
draggingShapes.filter(
(s) => s.parentId === shape.id && this.canReceiveNewChildrenOfType(s)
),
editor.getCurrentPageId()
);
}
}
}
//# sourceMappingURL=FrameShapeUtil.js.map