tldraw
Version:
A tiny little drawing editor.
295 lines (294 loc) • 10.3 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
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 __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var TextShapeUtil_exports = {};
__export(TextShapeUtil_exports, {
TextShapeUtil: () => TextShapeUtil
});
module.exports = __toCommonJS(TextShapeUtil_exports);
var import_jsx_runtime = require("react/jsx-runtime");
var import_editor = require("@tldraw/editor");
var import_react = require("react");
var import_richText = require("../../utils/text/richText");
var import_RichTextLabel = require("../shared/RichTextLabel");
var import_default_shape_constants = require("../shared/default-shape-constants");
var import_useDefaultColorTheme = require("../shared/useDefaultColorTheme");
const sizeCache = (0, import_editor.createComputedCache)(
"text size",
(editor, shape) => {
editor.fonts.trackFontsForShape(shape);
return getTextSize(editor, shape.props);
},
{ areRecordsEqual: (a, b) => a.props === b.props }
);
class TextShapeUtil extends import_editor.ShapeUtil {
static type = "text";
static props = import_editor.textShapeProps;
static migrations = import_editor.textShapeMigrations;
options = {
extraArrowHorizontalPadding: 10,
showTextOutline: true
};
getDefaultProps() {
return {
color: "black",
size: "m",
w: 8,
font: "draw",
textAlign: "start",
autoSize: true,
scale: 1,
richText: (0, import_editor.toRichText)("")
};
}
getMinDimensions(shape) {
return sizeCache.get(this.editor, shape.id);
}
getGeometry(shape, opts) {
const { scale } = shape.props;
const { width, height } = this.getMinDimensions(shape);
const context = opts?.context ?? "none";
return new import_editor.Rectangle2d({
x: (context === "@tldraw/arrow-without-arrowhead" ? -this.options.extraArrowHorizontalPadding : 0) * scale,
width: (width + (context === "@tldraw/arrow-without-arrowhead" ? this.options.extraArrowHorizontalPadding * 2 : 0)) * scale,
height: height * scale,
isFilled: true,
isLabel: true
});
}
getFontFaces(shape) {
return (0, import_editor.getFontsFromRichText)(this.editor, shape.props.richText, {
family: `tldraw_${shape.props.font}`,
weight: "normal",
style: "normal"
});
}
getText(shape) {
return (0, import_richText.renderPlaintextFromRichText)(this.editor, shape.props.richText);
}
canEdit() {
return true;
}
isAspectRatioLocked() {
return true;
}
// WAIT NO THIS IS HARD CODED IN THE RESIZE HANDLER
component(shape) {
const {
id,
props: { font, size, richText, color, scale, textAlign }
} = shape;
const { width, height } = this.getMinDimensions(shape);
const isSelected = shape.id === this.editor.getOnlySelectedShapeId();
const theme = (0, import_useDefaultColorTheme.useDefaultColorTheme)();
const handleKeyDown = useTextShapeKeydownHandler(id);
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_RichTextLabel.RichTextLabel,
{
shapeId: id,
classNamePrefix: "tl-text-shape",
type: "text",
font,
fontSize: import_default_shape_constants.FONT_SIZES[size],
lineHeight: import_default_shape_constants.TEXT_PROPS.lineHeight,
align: textAlign,
verticalAlign: "middle",
richText,
labelColor: (0, import_editor.getColorValue)(theme, color, "solid"),
isSelected,
textWidth: width,
textHeight: height,
showTextOutline: this.options.showTextOutline,
style: {
transform: `scale(${scale})`,
transformOrigin: "top left"
},
wrap: true,
onKeyDown: handleKeyDown
}
);
}
indicator(shape) {
const bounds = this.editor.getShapeGeometry(shape).bounds;
const editor = (0, import_editor.useEditor)();
if (shape.props.autoSize && editor.getEditingShapeId() === shape.id) return null;
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { width: (0, import_editor.toDomPrecision)(bounds.width), height: (0, import_editor.toDomPrecision)(bounds.height) });
}
useLegacyIndicator() {
return false;
}
getIndicatorPath(shape) {
if (shape.props.autoSize && this.editor.getEditingShapeId() === shape.id) return void 0;
const bounds = this.editor.getShapeGeometry(shape).bounds;
const path = new Path2D();
path.rect(0, 0, bounds.width, bounds.height);
return path;
}
toSvg(shape, ctx) {
const bounds = this.editor.getShapeGeometry(shape).bounds;
const width = bounds.width / (shape.props.scale ?? 1);
const height = bounds.height / (shape.props.scale ?? 1);
const theme = (0, import_editor.getDefaultColorTheme)(ctx);
const exportBounds = new import_editor.Box(0, 0, width, height);
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_RichTextLabel.RichTextSVG,
{
fontSize: import_default_shape_constants.FONT_SIZES[shape.props.size],
font: shape.props.font,
align: shape.props.textAlign,
verticalAlign: "middle",
richText: shape.props.richText,
labelColor: (0, import_editor.getColorValue)(theme, shape.props.color, "solid"),
bounds: exportBounds,
padding: 0,
showTextOutline: this.options.showTextOutline
}
);
}
onResize(shape, info) {
const { newPoint, initialBounds, initialShape, scaleX, handle } = info;
if (info.mode === "scale_shape" || handle !== "right" && handle !== "left") {
return {
id: shape.id,
type: shape.type,
...(0, import_editor.resizeScaled)(shape, info)
};
} else {
const nextWidth = Math.max(1, Math.abs(initialBounds.width * scaleX));
const { x, y } = scaleX < 0 ? import_editor.Vec.Sub(newPoint, import_editor.Vec.FromAngle(shape.rotation).mul(nextWidth)) : newPoint;
return {
id: shape.id,
type: shape.type,
x,
y,
props: {
w: nextWidth / initialShape.props.scale,
autoSize: false
}
};
}
}
onEditEnd(shape) {
const trimmedText = (0, import_richText.renderPlaintextFromRichText)(this.editor, shape.props.richText).trimEnd();
if (trimmedText.length === 0) {
this.editor.deleteShapes([shape.id]);
}
}
onBeforeUpdate(prev, next) {
if (!next.props.autoSize) return;
const styleDidChange = prev.props.size !== next.props.size || prev.props.textAlign !== next.props.textAlign || prev.props.font !== next.props.font || prev.props.scale !== 1 && next.props.scale === 1;
const textDidChange = !(0, import_editor.isEqual)(prev.props.richText, next.props.richText);
if (!styleDidChange && !textDidChange) return;
const boundsA = this.getMinDimensions(prev);
const boundsB = getTextSize(this.editor, next.props);
const wA = boundsA.width * prev.props.scale;
const hA = boundsA.height * prev.props.scale;
const wB = boundsB.width * next.props.scale;
const hB = boundsB.height * next.props.scale;
let delta;
switch (next.props.textAlign) {
case "middle": {
delta = new import_editor.Vec((wB - wA) / 2, textDidChange ? 0 : (hB - hA) / 2);
break;
}
case "end": {
delta = new import_editor.Vec(wB - wA, textDidChange ? 0 : (hB - hA) / 2);
break;
}
default: {
if (textDidChange) break;
delta = new import_editor.Vec(0, (hB - hA) / 2);
break;
}
}
if (delta) {
delta.rot(next.rotation);
const { x, y } = next;
return {
...next,
x: x - delta.x,
y: y - delta.y,
props: { ...next.props, w: wB }
};
} else {
return {
...next,
props: { ...next.props, w: wB }
};
}
}
// todo: The edge doubleclicking feels like a mistake more often than
// not, especially on multiline text. Removed June 16 2024
// override onDoubleClickEdge = (shape: TLTextShape) => {
// // If the shape has a fixed width, set it to autoSize.
// if (!shape.props.autoSize) {
// return {
// id: shape.id,
// type: shape.type,
// props: {
// autoSize: true,
// },
// }
// }
// // If the shape is scaled, reset the scale to 1.
// if (shape.props.scale !== 1) {
// return {
// id: shape.id,
// type: shape.type,
// props: {
// scale: 1,
// },
// }
// }
// }
}
function getTextSize(editor, props) {
const { font, richText, size, w } = props;
const minWidth = 16;
const fontSize = import_default_shape_constants.FONT_SIZES[size];
const maybeFixedWidth = props.autoSize ? null : Math.max(minWidth, Math.floor(w));
const html = (0, import_richText.renderHtmlFromRichTextForMeasurement)(editor, richText);
const result = editor.textMeasure.measureHtml(html, {
...import_default_shape_constants.TEXT_PROPS,
fontFamily: import_default_shape_constants.FONT_FAMILIES[font],
fontSize,
maxWidth: maybeFixedWidth
});
return {
width: maybeFixedWidth ?? Math.max(minWidth, result.w + 1),
height: Math.max(fontSize, result.h)
};
}
function useTextShapeKeydownHandler(id) {
const editor = (0, import_editor.useEditor)();
return (0, import_react.useCallback)(
(e) => {
if (editor.getEditingShapeId() !== id) return;
switch (e.key) {
case "Enter": {
if (e.ctrlKey || e.metaKey) {
editor.complete();
}
break;
}
}
},
[editor, id]
);
}
//# sourceMappingURL=TextShapeUtil.js.map