UNPKG

tldraw

Version:

A tiny little drawing editor.

377 lines (376 loc) • 13.6 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 TldrawUiTooltip_exports = {}; __export(TldrawUiTooltip_exports, { TldrawUiTooltip: () => TldrawUiTooltip, TldrawUiTooltipProvider: () => TldrawUiTooltipProvider, hideAllTooltips: () => hideAllTooltips }); module.exports = __toCommonJS(TldrawUiTooltip_exports); var import_jsx_runtime = require("react/jsx-runtime"); var import_editor = require("@tldraw/editor"); var import_radix_ui = require("radix-ui"); var import_react = __toESM(require("react"), 1); var import_layout = require("./layout"); const DEFAULT_TOOLTIP_DELAY_MS = 700; class TooltipManager { static instance = null; state = (0, import_editor.atom)("tooltip state", { name: "idle" }); static getInstance() { if (!TooltipManager.instance) { TooltipManager.instance = new TooltipManager(); } return TooltipManager.instance; } hideAllTooltips() { this.handleEvent({ type: "hide_all" }); } handleEvent(event) { const currentState = this.state.get(); switch (event.type) { case "pointer_down": { if (currentState.name === "waiting_to_hide") { clearTimeout(currentState.timeoutId); } this.state.set({ name: "pointer_down" }); break; } case "pointer_up": { if (currentState.name === "pointer_down") { this.state.set({ name: "idle" }); } break; } case "show": { if (currentState.name === "pointer_down") { return; } if (currentState.name === "waiting_to_hide") { clearTimeout(currentState.timeoutId); } this.state.set({ name: "showing", tooltip: event.tooltip }); break; } case "hide": { const { tooltipId, editor, instant } = event; if (currentState.name === "showing" && currentState.tooltip.id === tooltipId) { if (editor && !instant) { const timeoutId = editor.timers.setTimeout(() => { const state = this.state.get(); if (state.name === "waiting_to_hide" && state.tooltip.id === tooltipId) { this.state.set({ name: "idle" }); } }, 300); this.state.set({ name: "waiting_to_hide", tooltip: currentState.tooltip, timeoutId }); } else { this.state.set({ name: "idle" }); } } else if (currentState.name === "waiting_to_hide" && currentState.tooltip.id === tooltipId) { if (instant) { clearTimeout(currentState.timeoutId); this.state.set({ name: "idle" }); } } break; } case "hide_all": { if (currentState.name === "waiting_to_hide") { clearTimeout(currentState.timeoutId); } if (currentState.name === "pointer_down") { return; } this.state.set({ name: "idle" }); break; } } } getCurrentTooltipData() { const currentState = this.state.get(); let tooltip = null; if (currentState.name === "showing") { tooltip = currentState.tooltip; } else if (currentState.name === "waiting_to_hide") { tooltip = currentState.tooltip; } if (!tooltip) return null; if (import_editor.tlenvReactive.get().isCoarsePointer && !tooltip.showOnMobile) return null; return tooltip; } } const tooltipManager = TooltipManager.getInstance(); function hideAllTooltips() { tooltipManager.hideAllTooltips(); } const TooltipSingletonContext = (0, import_react.createContext)(false); function TldrawUiTooltipProvider({ children }) { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_radix_ui.Tooltip.Provider, { skipDelayDuration: 700, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TooltipSingletonContext.Provider, { value: true, children: [ children, /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TooltipSingleton, {}) ] }) }); } function TooltipSingleton() { const [isOpen, setIsOpen] = (0, import_react.useState)(false); const triggerRef = (0, import_react.useRef)(null); const isFirstShowRef = (0, import_react.useRef)(true); const editor = (0, import_editor.useMaybeEditor)(); const currentTooltip = (0, import_editor.useValue)( "current tooltip", () => tooltipManager.getCurrentTooltipData(), [] ); const cameraState = (0, import_editor.useValue)("camera state", () => editor?.getCameraState(), [editor]); (0, import_react.useEffect)(() => { if (cameraState === "moving" && isOpen && currentTooltip) { tooltipManager.handleEvent({ type: "hide", tooltipId: currentTooltip.id, editor, instant: true }); } }, [cameraState, isOpen, currentTooltip, editor]); (0, import_react.useEffect)(() => { function handleKeyDown(event) { if (event.key === "Escape" && currentTooltip && isOpen) { hideAllTooltips(); event.stopPropagation(); } } document.addEventListener("keydown", handleKeyDown, { capture: true }); return () => { document.removeEventListener("keydown", handleKeyDown, { capture: true }); }; }, [currentTooltip, isOpen]); (0, import_react.useEffect)(() => { function handlePointerDown() { tooltipManager.handleEvent({ type: "pointer_down" }); } function handlePointerUp() { tooltipManager.handleEvent({ type: "pointer_up" }); } document.addEventListener("pointerdown", handlePointerDown, { capture: true }); document.addEventListener("pointerup", handlePointerUp, { capture: true }); document.addEventListener("pointercancel", handlePointerUp, { capture: true }); return () => { document.removeEventListener("pointerdown", handlePointerDown, { capture: true }); document.removeEventListener("pointerup", handlePointerUp, { capture: true }); document.removeEventListener("pointercancel", handlePointerUp, { capture: true }); tooltipManager.handleEvent({ type: "pointer_up" }); }; }, []); (0, import_react.useEffect)(() => { let timer = null; if (currentTooltip && triggerRef.current) { const activeRect = currentTooltip.targetElement.getBoundingClientRect(); const trigger = triggerRef.current; trigger.style.position = "fixed"; trigger.style.left = "0px"; trigger.style.top = "0px"; const cbOffset = trigger.getBoundingClientRect(); trigger.style.left = `${activeRect.left - cbOffset.left}px`; trigger.style.top = `${activeRect.top - cbOffset.top}px`; trigger.style.width = `${activeRect.width}px`; trigger.style.height = `${activeRect.height}px`; trigger.style.pointerEvents = "none"; trigger.style.zIndex = "9999"; if (isFirstShowRef.current) { timer = setTimeout(() => { setIsOpen(true); isFirstShowRef.current = false; }, currentTooltip.delayDuration); } else { setIsOpen(true); } } else { setIsOpen(false); isFirstShowRef.current = true; } return () => { if (timer !== null) { clearTimeout(timer); } }; }, [currentTooltip]); if (!currentTooltip) { return null; } return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_radix_ui.Tooltip.Root, { open: isOpen, delayDuration: 0, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_radix_ui.Tooltip.Trigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref: triggerRef }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( import_radix_ui.Tooltip.Content, { className: "tlui-tooltip", side: currentTooltip.side, sideOffset: currentTooltip.sideOffset, avoidCollisions: true, collisionPadding: 8, dir: "ltr", children: [ currentTooltip.content, /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_radix_ui.Tooltip.Arrow, { className: "tlui-tooltip__arrow" }) ] } ) ] }); } const TldrawUiTooltip = (0, import_react.forwardRef)( ({ children, content, side, sideOffset = 5, disabled = false, showOnMobile = false, delayDuration }, ref) => { const editor = (0, import_editor.useMaybeEditor)(); const tooltipId = (0, import_react.useRef)((0, import_editor.uniqueId)()); const hasProvider = (0, import_react.useContext)(TooltipSingletonContext); const enhancedA11yMode = (0, import_editor.useValue)( "enhancedA11yMode", () => editor?.user.getEnhancedA11yMode(), [editor] ); const orientationCtx = (0, import_layout.useTldrawUiOrientation)(); const sideToUse = side ?? orientationCtx.tooltipSide; (0, import_react.useEffect)(() => { const currentTooltipId = tooltipId.current; return () => { if (hasProvider) { tooltipManager.handleEvent({ type: "hide", tooltipId: currentTooltipId, editor, instant: true }); } }; }, [editor, hasProvider]); if (disabled || !content) { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children }); } let delayDurationToUse; if (enhancedA11yMode) { delayDurationToUse = 0; } else { delayDurationToUse = delayDuration ?? (editor?.options.tooltipDelayMs || DEFAULT_TOOLTIP_DELAY_MS); } if (!hasProvider || enhancedA11yMode) { return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( import_radix_ui.Tooltip.Root, { delayDuration: delayDurationToUse, disableHoverableContent: !enhancedA11yMode, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_radix_ui.Tooltip.Trigger, { asChild: true, ref, children }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( import_radix_ui.Tooltip.Content, { className: "tlui-tooltip", side: sideToUse, sideOffset, avoidCollisions: true, collisionPadding: 8, dir: "ltr", children: [ content, /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_radix_ui.Tooltip.Arrow, { className: "tlui-tooltip__arrow" }) ] } ) ] } ); } const child = import_react.default.Children.only(children); (0, import_editor.assert)(import_react.default.isValidElement(child), "TldrawUiTooltip children must be a single element"); const childElement = child; const handleMouseEnter = (event) => { childElement.props.onMouseEnter?.(event); tooltipManager.handleEvent({ type: "show", tooltip: { id: tooltipId.current, content, targetElement: event.currentTarget, side: sideToUse, sideOffset, showOnMobile, delayDuration: delayDurationToUse } }); }; const handleMouseLeave = (event) => { childElement.props.onMouseLeave?.(event); tooltipManager.handleEvent({ type: "hide", tooltipId: tooltipId.current, editor, instant: false }); }; const handleFocus = (event) => { childElement.props.onFocus?.(event); tooltipManager.handleEvent({ type: "show", tooltip: { id: tooltipId.current, content, targetElement: event.currentTarget, side: sideToUse, sideOffset, showOnMobile, delayDuration: delayDurationToUse } }); }; const handleBlur = (event) => { childElement.props.onBlur?.(event); tooltipManager.handleEvent({ type: "hide", tooltipId: tooltipId.current, editor, instant: false }); }; const childrenWithHandlers = import_react.default.cloneElement(childElement, { onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onFocus: handleFocus, onBlur: handleBlur }); return childrenWithHandlers; } ); //# sourceMappingURL=TldrawUiTooltip.js.map