UNPKG

tldraw

Version:

A tiny little drawing editor.

279 lines (278 loc) • 11.5 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 TldrawUiContextualToolbar_exports = {}; __export(TldrawUiContextualToolbar_exports, { TldrawUiContextualToolbar: () => TldrawUiContextualToolbar, getToolbarScreenPosition: () => getToolbarScreenPosition, rectToBox: () => rectToBox, useToolbarVisibilityStateMachine: () => useToolbarVisibilityStateMachine }); module.exports = __toCommonJS(TldrawUiContextualToolbar_exports); var import_jsx_runtime = require("react/jsx-runtime"); var import_editor = require("@tldraw/editor"); var import_classnames = __toESM(require("classnames"), 1); var import_react = require("react"); var import_react_dom = require("react-dom"); var import_TldrawUiToolbar = require("./TldrawUiToolbar"); const MOVE_TIMEOUT = 150; const HIDE_VISIBILITY_TIMEOUT = 16; const SHOW_VISIBILITY_TIMEOUT = 16; const MIN_DISTANCE_TO_REPOSITION_SQUARED = 16 ** 2; const TOOLBAR_GAP = 8; const SCREEN_MARGIN = 16; const HIDE_TOOLBAR_WHEN_CAMERA_IS_MOVING = false; const LEFT_ALIGN_TOOLBAR = false; const TldrawUiContextualToolbar = ({ children, className, isMousingDown, getSelectionBounds, changeOnlyWhenYChanges = false, label }) => { const editor = (0, import_editor.useEditor)(); const toolbarRef = (0, import_react.useRef)(null); (0, import_editor.usePassThroughWheelEvents)(toolbarRef); (0, import_editor.usePassThroughMouseOverEvents)(toolbarRef); const { isVisible, isInteractive, hide, show, position, move } = useToolbarVisibilityStateMachine(changeOnlyWhenYChanges); const rCouldShowToolbar = (0, import_react.useRef)(false); const [hasValidToolbarPosition, setHasValidToolbarPosition] = (0, import_react.useState)(false); const contentSizeUpdateCounter = (0, import_editor.useAtom)("content size update counter", 0); (0, import_react.useEffect)(() => { (0, import_editor.assert)(toolbarRef.current); const observer = new ResizeObserver(() => { contentSizeUpdateCounter.update((n) => n + 1); }); observer.observe(toolbarRef.current); return () => observer.disconnect(); }, [contentSizeUpdateCounter]); (0, import_react.useEffect)(() => { let lastContentSizeUpdateCounter = contentSizeUpdateCounter.get(); return (0, import_editor.react)("toolbar position", function updateToolbarPositionAndDisplay() { const toolbarElm = toolbarRef.current; if (!toolbarElm) return; const nextContentSizeUpdateCounter = contentSizeUpdateCounter.get(); editor.getCamera(); contentSizeUpdateCounter.get(); const position2 = getToolbarScreenPosition(editor, toolbarElm, getSelectionBounds); if (!position2) { if (rCouldShowToolbar.current) { rCouldShowToolbar.current = false; setHasValidToolbarPosition(false); } } else { const cameraState2 = editor.getCameraState(); if (cameraState2 === "moving") { const elm = toolbarRef.current; if (elm) { elm.style.setProperty("transform", `translate(${position2.x}px, ${position2.y}px)`); } } else { const moveImmediately = lastContentSizeUpdateCounter !== nextContentSizeUpdateCounter; move(position2.x, position2.y, moveImmediately); } if (!rCouldShowToolbar.current) { rCouldShowToolbar.current = true; setHasValidToolbarPosition(true); } } lastContentSizeUpdateCounter = nextContentSizeUpdateCounter; }); }, [editor, getSelectionBounds, contentSizeUpdateCounter, move]); const cameraState = (0, import_editor.useValue)("camera state", () => editor.getCameraState(), [editor]); (0, import_react.useEffect)(() => { if (cameraState === "moving" && HIDE_TOOLBAR_WHEN_CAMERA_IS_MOVING) { hide(true); return; } if (isMousingDown || !hasValidToolbarPosition) { hide(); return; } show(); }, [hasValidToolbarPosition, cameraState, isMousingDown, show, hide]); (0, import_react.useLayoutEffect)(() => { const elm = toolbarRef.current; if (!elm) return; elm.dataset.visible = `${isVisible}`; }, [isVisible, position]); (0, import_react.useLayoutEffect)(() => { const elm = toolbarRef.current; if (!elm) return; elm.style.setProperty("transform", `translate(${position.x}px, ${position.y}px)`); }, [position]); (0, import_react.useLayoutEffect)(() => { const elm = toolbarRef.current; if (!elm) return; elm.dataset.interactive = `${isInteractive}`; }, [isInteractive]); return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "div", { ref: toolbarRef, "data-interactive": false, "data-visible": false, "data-testid": "contextual-toolbar", className: (0, import_classnames.default)("tlui-contextual-toolbar", className), onPointerDown: editor.markEventAsHandled, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_TldrawUiToolbar.TldrawUiToolbar, { orientation: "horizontal", className: "tlui-menu", label, tooltipSide: "top", children } ) } ); }; function rectToBox(rect) { return new import_editor.Box(rect.x, rect.y, rect.width, rect.height); } function getToolbarScreenPosition(editor, toolbarElm, getSelectionBounds) { const selectionBounds = getSelectionBounds()?.clone(); if (!selectionBounds) return; const vsb = editor.getViewportScreenBounds(); selectionBounds.x -= vsb.x; selectionBounds.y -= vsb.y; if (selectionBounds.midY < SCREEN_MARGIN || selectionBounds.midY > vsb.h - SCREEN_MARGIN || selectionBounds.midX < SCREEN_MARGIN || selectionBounds.midX > vsb.w - SCREEN_MARGIN) { return; } const toolbarBounds = rectToBox(toolbarElm.getBoundingClientRect()); if (!toolbarBounds.width || !toolbarBounds.height) return; const { scrollLeft, scrollTop } = editor.getContainer(); let x = LEFT_ALIGN_TOOLBAR ? selectionBounds.x : selectionBounds.midX - toolbarBounds.w / 2; let y = selectionBounds.y - toolbarBounds.h - TOOLBAR_GAP; x = (0, import_editor.clamp)(x, SCREEN_MARGIN, vsb.w - toolbarBounds.w - SCREEN_MARGIN); y = (0, import_editor.clamp)(y, SCREEN_MARGIN, vsb.h - toolbarBounds.h - SCREEN_MARGIN); x += scrollLeft; y += scrollTop; x = Math.round(x); y = Math.round(y); return { x, y }; } function sufficientlyDistant(curr, next, changeOnlyWhenYChanges) { if (changeOnlyWhenYChanges) { return import_editor.Vec.Sub(next, curr).y ** 2 >= MIN_DISTANCE_TO_REPOSITION_SQUARED; } return import_editor.Vec.Len2(import_editor.Vec.Sub(next, curr)) >= MIN_DISTANCE_TO_REPOSITION_SQUARED; } function useToolbarVisibilityStateMachine(changeOnlyWhenYChanges) { const editor = (0, import_editor.useEditor)(); const rState = (0, import_react.useRef)({ name: "hidden" }); const [isInteractive, setIsInteractive] = (0, import_react.useState)(false); const [isVisible, setIsVisible] = (0, import_react.useState)(false); const [position, setPosition] = (0, import_react.useState)({ x: -1e3, y: -1e3 }); const rCurrPosition = (0, import_react.useRef)(new import_editor.Vec(-1e3, -1e3)); const rNextPosition = (0, import_react.useRef)(new import_editor.Vec(-1e3, -1e3)); const rStableVisibilityTimeout = (0, import_react.useRef)(-1); const rStablePositionTimeout = (0, import_react.useRef)(-1); const move = (0, import_react.useCallback)( (x, y, immediate = false) => { rNextPosition.current.x = x; rNextPosition.current.y = y; if (rState.current.name === "hidden" || rState.current.name === "showing") return; clearTimeout(rStablePositionTimeout.current); const flushMove = () => { if (rState.current.name === "shown" && sufficientlyDistant(rNextPosition.current, rCurrPosition.current, changeOnlyWhenYChanges)) { const { x: x2, y: y2 } = rNextPosition.current; rCurrPosition.current = new import_editor.Vec(x2, y2); if (immediate) { (0, import_react_dom.flushSync)(() => setPosition({ x: x2, y: y2 })); } else { setPosition({ x: x2, y: y2 }); } } }; if (immediate) { flushMove(); } else { rStablePositionTimeout.current = editor.timers.setTimeout(flushMove, MOVE_TIMEOUT); } }, [editor, changeOnlyWhenYChanges] ); const hide = (0, import_react.useCallback)( (immediate = false) => { switch (rState.current.name) { case "showing": { clearTimeout(rStableVisibilityTimeout.current); rState.current = { name: "hidden" }; break; } case "shown": { rState.current = { name: "hiding" }; setIsInteractive(false); if (immediate) { rState.current = { name: "hidden" }; setIsVisible(false); } else { rStableVisibilityTimeout.current = editor.timers.setTimeout(() => { rState.current = { name: "hidden" }; setIsVisible(false); }, HIDE_VISIBILITY_TIMEOUT); } break; } default: { } } }, [editor] ); const show = (0, import_react.useCallback)(() => { switch (rState.current.name) { case "hidden": { rState.current = { name: "showing" }; rStableVisibilityTimeout.current = editor.timers.setTimeout(() => { const { x, y } = rNextPosition.current; rCurrPosition.current = new import_editor.Vec(x, y); setPosition({ x, y }); rState.current = { name: "shown" }; setIsVisible(true); setIsInteractive(true); }, SHOW_VISIBILITY_TIMEOUT); break; } case "hiding": { clearTimeout(rStableVisibilityTimeout.current); rState.current = { name: "shown" }; setIsInteractive(true); move(rNextPosition.current.x, rNextPosition.current.y); break; } default: { } } }, [editor, move]); return { isVisible, isInteractive, show, hide, move, position }; } //# sourceMappingURL=TldrawUiContextualToolbar.js.map