tldraw
Version:
A tiny little drawing editor.
435 lines (434 loc) • 16.7 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 NoteShapeUtil_exports = {};
__export(NoteShapeUtil_exports, {
NoteShapeUtil: () => NoteShapeUtil
});
module.exports = __toCommonJS(NoteShapeUtil_exports);
var import_jsx_runtime = require("react/jsx-runtime");
var import_editor = require("@tldraw/editor");
var import_react = require("react");
var import_useTranslation = require("../../ui/hooks/useTranslation/useTranslation");
var import_text = require("../../utils/text/text");
var import_HyperlinkButton = require("../shared/HyperlinkButton");
var import_RichTextLabel = require("../shared/RichTextLabel");
var import_default_shape_constants = require("../shared/default-shape-constants");
var import_selectHelpers = require("../../tools/SelectTool/selectHelpers");
var import_lodash = __toESM(require("lodash.isequal"));
var import_richText = require("../../utils/text/richText");
var import_useDefaultColorTheme = require("../shared/useDefaultColorTheme");
var import_useEditablePlainText = require("../shared/useEditablePlainText");
var import_noteHelpers = require("./noteHelpers");
class NoteShapeUtil extends import_editor.ShapeUtil {
static type = "note";
static props = import_editor.noteShapeProps;
static migrations = import_editor.noteShapeMigrations;
options = {
resizeMode: "none"
};
canEdit() {
return true;
}
hideResizeHandles() {
const { resizeMode } = this.options;
switch (resizeMode) {
case "none": {
return true;
}
case "scale": {
return false;
}
default: {
throw (0, import_editor.exhaustiveSwitchError)(resizeMode);
}
}
}
isAspectRatioLocked() {
return this.options.resizeMode === "scale";
}
hideSelectionBoundsFg() {
return false;
}
getDefaultProps() {
return {
color: "black",
richText: (0, import_editor.toRichText)(""),
size: "m",
font: "draw",
align: "middle",
verticalAlign: "middle",
labelColor: "black",
growY: 0,
fontSizeAdjustment: 0,
url: "",
scale: 1
};
}
getGeometry(shape) {
const { labelHeight, labelWidth } = getLabelSize(this.editor, shape);
const { scale } = shape.props;
const lh = labelHeight * scale;
const lw = labelWidth * scale;
const nw = import_noteHelpers.NOTE_SIZE * scale;
const nh = getNoteHeight(shape);
return new import_editor.Group2d({
children: [
new import_editor.Rectangle2d({ width: nw, height: nh, isFilled: true }),
new import_editor.Rectangle2d({
x: shape.props.align === "start" ? 0 : shape.props.align === "end" ? nw - lw : (nw - lw) / 2,
y: shape.props.verticalAlign === "start" ? 0 : shape.props.verticalAlign === "end" ? nh - lh : (nh - lh) / 2,
width: lw,
height: lh,
isFilled: true,
isLabel: true
})
]
});
}
getHandles(shape) {
const { scale } = shape.props;
const isCoarsePointer = this.editor.getInstanceState().isCoarsePointer;
if (isCoarsePointer) return [];
const zoom = this.editor.getZoomLevel();
if (zoom * scale < 0.25) return [];
const nh = getNoteHeight(shape);
const nw = import_noteHelpers.NOTE_SIZE * scale;
const offset = import_noteHelpers.CLONE_HANDLE_MARGIN / zoom * scale;
if (zoom * scale < 0.5) {
return [
{
id: "bottom",
index: "a3",
type: "clone",
x: nw / 2,
y: nh + offset
}
];
}
return [
{
id: "top",
index: "a1",
type: "clone",
x: nw / 2,
y: -offset
},
{
id: "right",
index: "a2",
type: "clone",
x: nw + offset,
y: nh / 2
},
{
id: "bottom",
index: "a3",
type: "clone",
x: nw / 2,
y: nh + offset
},
{
id: "left",
index: "a4",
type: "clone",
x: -offset,
y: nh / 2
}
];
}
onResize(shape, info) {
const { resizeMode } = this.options;
switch (resizeMode) {
case "none": {
return void 0;
}
case "scale": {
return (0, import_editor.resizeScaled)(shape, info);
}
default: {
throw (0, import_editor.exhaustiveSwitchError)(resizeMode);
}
}
}
getText(shape) {
return (0, import_richText.renderPlaintextFromRichText)(this.editor, shape.props.richText);
}
getFontFaces(shape) {
return (0, import_editor.getFontsFromRichText)(this.editor, shape.props.richText, {
family: `tldraw_${shape.props.font}`,
weight: "normal",
style: "normal"
});
}
component(shape) {
const {
id,
type,
props: {
labelColor,
scale,
color,
font,
size,
align,
richText,
verticalAlign,
fontSizeAdjustment
}
} = shape;
const handleKeyDown = useNoteKeydownHandler(id);
const theme = (0, import_useDefaultColorTheme.useDefaultColorTheme)();
const nw = import_noteHelpers.NOTE_SIZE * scale;
const nh = getNoteHeight(shape);
const rotation = (0, import_editor.useValue)(
"shape rotation",
() => this.editor.getShapePageTransform(id)?.rotation() ?? 0,
[this.editor]
);
const hideShadows = (0, import_editor.useValue)("zoom", () => this.editor.getZoomLevel() < 0.35 / scale, [
scale,
this.editor
]);
const isDarkMode = (0, import_editor.useValue)("dark mode", () => this.editor.user.getIsDarkMode(), [this.editor]);
const isSelected = shape.id === this.editor.getOnlySelectedShapeId();
const isReadyForEditing = (0, import_useEditablePlainText.useIsReadyForEditing)(this.editor, shape.id);
const isEmpty = (0, import_richText.isEmptyRichText)(richText);
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"div",
{
id,
className: "tl-note__container",
style: {
width: nw,
height: nh,
backgroundColor: theme[color].note.fill,
borderBottom: hideShadows ? isDarkMode ? `${2 * scale}px solid rgb(20, 20, 20)` : `${2 * scale}px solid rgb(144, 144, 144)` : "none",
boxShadow: hideShadows ? "none" : getNoteShadow(shape.id, rotation, scale)
},
children: (isSelected || isReadyForEditing || !isEmpty) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_RichTextLabel.RichTextLabel,
{
shapeId: id,
type,
font,
fontSize: (fontSizeAdjustment || import_default_shape_constants.LABEL_FONT_SIZES[size]) * scale,
lineHeight: import_default_shape_constants.TEXT_PROPS.lineHeight,
align,
verticalAlign,
richText,
isSelected,
labelColor: labelColor === "black" ? theme[color].note.text : theme[labelColor].fill,
wrap: true,
padding: import_default_shape_constants.LABEL_PADDING * scale,
hasCustomTabBehavior: true,
onKeyDown: handleKeyDown
}
)
}
),
"url" in shape.props && shape.props.url && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_HyperlinkButton.HyperlinkButton, { url: shape.props.url })
] });
}
indicator(shape) {
const { scale } = shape.props;
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"rect",
{
rx: scale,
width: (0, import_editor.toDomPrecision)(import_noteHelpers.NOTE_SIZE * scale),
height: (0, import_editor.toDomPrecision)(getNoteHeight(shape))
}
);
}
toSvg(shape, ctx) {
const theme = (0, import_editor.getDefaultColorTheme)({ isDarkMode: ctx.isDarkMode });
const bounds = getBoundsForSVG(shape);
const textLabel = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_RichTextLabel.RichTextSVG,
{
fontSize: shape.props.fontSizeAdjustment || import_default_shape_constants.LABEL_FONT_SIZES[shape.props.size],
font: shape.props.font,
align: shape.props.align,
verticalAlign: shape.props.verticalAlign,
richText: shape.props.richText,
labelColor: theme[shape.props.color].note.text,
bounds,
padding: import_default_shape_constants.LABEL_PADDING * shape.props.scale
}
);
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: 5, y: 5, rx: 1, width: import_noteHelpers.NOTE_SIZE - 10, height: bounds.h, fill: "rgba(0,0,0,.1)" }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"rect",
{
rx: 1,
width: import_noteHelpers.NOTE_SIZE,
height: bounds.h,
fill: theme[shape.props.color].note.fill
}
),
textLabel
] });
}
onBeforeCreate(next) {
return getNoteSizeAdjustments(this.editor, next);
}
onBeforeUpdate(prev, next) {
if ((0, import_lodash.default)(prev.props.richText, next.props.richText) && prev.props.font === next.props.font && prev.props.size === next.props.size) {
return;
}
return getNoteSizeAdjustments(this.editor, next);
}
getInterpolatedProps(startShape, endShape, t) {
return {
...t > 0.5 ? endShape.props : startShape.props,
scale: (0, import_editor.lerp)(startShape.props.scale, endShape.props.scale, t)
};
}
}
function getNoteSizeAdjustments(editor, shape) {
const { labelHeight, fontSizeAdjustment } = getLabelSize(editor, shape);
const growY = Math.max(0, labelHeight - import_noteHelpers.NOTE_SIZE);
if (growY !== shape.props.growY || fontSizeAdjustment !== shape.props.fontSizeAdjustment) {
return {
...shape,
props: {
...shape.props,
growY,
fontSizeAdjustment
}
};
}
}
function getNoteLabelSize(editor, shape) {
const { richText } = shape.props;
if ((0, import_richText.isEmptyRichText)(richText)) {
const minHeight = import_default_shape_constants.LABEL_FONT_SIZES[shape.props.size] * import_default_shape_constants.TEXT_PROPS.lineHeight + import_default_shape_constants.LABEL_PADDING * 2;
return { labelHeight: minHeight, labelWidth: 100, fontSizeAdjustment: 0 };
}
const unadjustedFontSize = import_default_shape_constants.LABEL_FONT_SIZES[shape.props.size];
let fontSizeAdjustment = 0;
let iterations = 0;
let labelHeight = import_noteHelpers.NOTE_SIZE;
let labelWidth = import_noteHelpers.NOTE_SIZE;
const FUZZ = 1;
do {
fontSizeAdjustment = Math.min(unadjustedFontSize, unadjustedFontSize - iterations);
const html = (0, import_richText.renderHtmlFromRichTextForMeasurement)(editor, richText);
const nextTextSize = editor.textMeasure.measureHtml(html, {
...import_default_shape_constants.TEXT_PROPS,
fontFamily: import_default_shape_constants.FONT_FAMILIES[shape.props.font],
fontSize: fontSizeAdjustment,
maxWidth: import_noteHelpers.NOTE_SIZE - import_default_shape_constants.LABEL_PADDING * 2 - FUZZ,
disableOverflowWrapBreaking: true
});
labelHeight = nextTextSize.h + import_default_shape_constants.LABEL_PADDING * 2;
labelWidth = nextTextSize.w + import_default_shape_constants.LABEL_PADDING * 2;
if (fontSizeAdjustment <= 14) {
const html2 = (0, import_richText.renderHtmlFromRichTextForMeasurement)(editor, richText);
const nextTextSizeWithOverflowBreak = editor.textMeasure.measureHtml(html2, {
...import_default_shape_constants.TEXT_PROPS,
fontFamily: import_default_shape_constants.FONT_FAMILIES[shape.props.font],
fontSize: fontSizeAdjustment,
maxWidth: import_noteHelpers.NOTE_SIZE - import_default_shape_constants.LABEL_PADDING * 2 - FUZZ
});
labelHeight = nextTextSizeWithOverflowBreak.h + import_default_shape_constants.LABEL_PADDING * 2;
labelWidth = nextTextSizeWithOverflowBreak.w + import_default_shape_constants.LABEL_PADDING * 2;
break;
}
if (nextTextSize.scrollWidth.toFixed(0) === nextTextSize.w.toFixed(0)) {
break;
}
} while (iterations++ < 50);
return {
labelHeight,
labelWidth,
fontSizeAdjustment
};
}
const labelSizesForNote = new import_editor.WeakCache();
function getLabelSize(editor, shape) {
return labelSizesForNote.get(shape, () => getNoteLabelSize(editor, shape));
}
function useNoteKeydownHandler(id) {
const editor = (0, import_editor.useEditor)();
const translation = (0, import_useTranslation.useCurrentTranslation)();
return (0, import_react.useCallback)(
(e) => {
const shape = editor.getShape(id);
if (!shape) return;
const isTab = e.key === "Tab";
const isCmdEnter = (e.metaKey || e.ctrlKey) && e.key === "Enter";
if (isTab || isCmdEnter) {
e.preventDefault();
const pageTransform = editor.getShapePageTransform(id);
const pageRotation = pageTransform.rotation();
const isRTL = !!(translation.dir === "rtl" || // todo: can we check a partial of the text, so that we don't have to render the whole thing?
(0, import_text.isRightToLeftLanguage)((0, import_richText.renderPlaintextFromRichText)(editor, shape.props.richText)));
const offsetLength = (import_noteHelpers.NOTE_SIZE + editor.options.adjacentShapeMargin + // If we're growing down, we need to account for the current shape's growY
(isCmdEnter && !e.shiftKey ? shape.props.growY : 0)) * shape.props.scale;
const adjacentCenter = new import_editor.Vec(
isTab ? e.shiftKey != isRTL ? -1 : 1 : 0,
isCmdEnter ? e.shiftKey ? -1 : 1 : 0
).mul(offsetLength).add(import_noteHelpers.NOTE_CENTER_OFFSET.clone().mul(shape.props.scale)).rot(pageRotation).add(pageTransform.point());
const newNote = (0, import_noteHelpers.getNoteShapeForAdjacentPosition)(editor, shape, adjacentCenter, pageRotation);
if (newNote) {
editor.markHistoryStoppingPoint("editing adjacent shape");
(0, import_selectHelpers.startEditingShapeWithLabel)(
editor,
newNote,
true
/* selectAll */
);
}
}
},
[id, editor, translation.dir]
);
}
function getNoteHeight(shape) {
return (import_noteHelpers.NOTE_SIZE + shape.props.growY) * shape.props.scale;
}
function getNoteShadow(id, rotation, scale) {
const random = (0, import_editor.rng)(id);
const lift = Math.abs(random()) + 0.5;
const oy = Math.cos(rotation);
const a = 5 * scale;
const b = 4 * scale;
const c = 6 * scale;
const d = 7 * scale;
return `0px ${a - lift}px ${a}px -${a}px rgba(15, 23, 31, .6),
0px ${(b + lift * d) * Math.max(0, oy)}px ${c + lift * d}px -${b + lift * c}px rgba(15, 23, 31, ${(0.3 + lift * 0.1).toFixed(2)}),
0px ${48 * scale}px ${10 * scale}px -${10 * scale}px inset rgba(15, 23, 44, ${((0.022 + random() * 5e-3) * ((1 + oy) / 2)).toFixed(2)})`;
}
function getBoundsForSVG(shape) {
return new import_editor.Box(0, 0, import_noteHelpers.NOTE_SIZE, import_noteHelpers.NOTE_SIZE + shape.props.growY);
}
//# sourceMappingURL=NoteShapeUtil.js.map