UNPKG

tldraw

Version:

A tiny little drawing editor.

435 lines (434 loc) • 16.7 kB
"use strict"; 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