tldraw 4.3.0-canary.e5f56251a468 → 4.3.0-canary.ef0248947f13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.d.ts +3 -0
- package/dist-cjs/index.js +2 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/ui/components/primitives/TldrawUiSlider.js +1 -1
- package/dist-cjs/lib/ui/components/primitives/TldrawUiSlider.js.map +2 -2
- package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js +143 -88
- package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js.map +2 -2
- package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js +1 -1
- package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js.map +2 -2
- package/dist-cjs/lib/ui/version.js +3 -3
- package/dist-cjs/lib/ui/version.js.map +1 -1
- package/dist-cjs/lib/utils/text/richText.js +7 -17
- package/dist-cjs/lib/utils/text/richText.js.map +3 -3
- package/dist-esm/index.d.mts +3 -0
- package/dist-esm/index.mjs +3 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/ui/components/primitives/TldrawUiSlider.mjs +2 -2
- package/dist-esm/lib/ui/components/primitives/TldrawUiSlider.mjs.map +2 -2
- package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs +151 -90
- package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs.map +2 -2
- package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs +2 -2
- package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs.map +2 -2
- package/dist-esm/lib/ui/version.mjs +3 -3
- package/dist-esm/lib/ui/version.mjs.map +1 -1
- package/dist-esm/lib/utils/text/richText.mjs +3 -3
- package/dist-esm/lib/utils/text/richText.mjs.map +2 -2
- package/package.json +3 -3
- package/src/index.ts +1 -0
- package/src/lib/ui/components/primitives/TldrawUiSlider.tsx +2 -2
- package/src/lib/ui/components/primitives/TldrawUiTooltip.tsx +196 -108
- package/src/lib/ui/components/primitives/menus/TldrawUiMenuItem.tsx +2 -2
- package/src/lib/ui/version.ts +3 -3
- package/src/lib/utils/text/richText.ts +3 -3
- package/src/test/TldrawEditor.test.tsx +3 -2
- package/src/test/commands/putContent.test.ts +79 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/lib/utils/text/richText.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n\tExtension,\n\tExtensions,\n\tgenerateHTML,\n\tgenerateJSON,\n\tgenerateText,\n\tJSONContent,\n} from '@tiptap/core'\nimport Code from '@tiptap/extension-code'\nimport Highlight from '@tiptap/extension-highlight'\nimport { Node } from '@tiptap/pm/model'\nimport StarterKit from '@tiptap/starter-kit'\nimport {\n\tEditor,\n\tgetOwnProperty,\n\tRichTextFontVisitorState,\n\tTLFontFace,\n\tTLRichText,\n\tWeakCache,\n} from '@tldraw/editor'\nimport { DefaultFontFaces } from '../../shapes/shared/defaultFonts'\nimport { TextDirection } from './textDirection'\n\n/** @public */\nexport const KeyboardShiftEnterTweakExtension = Extension.create({\n\tname: 'keyboardShiftEnterHandler',\n\taddKeyboardShortcuts() {\n\t\treturn {\n\t\t\t// We don't support soft breaks, so we just use the default enter command.\n\t\t\t'Shift-Enter': ({ editor }) => editor.commands.enter(),\n\t\t}\n\t},\n})\n\n// We change the default Code to override what's in the StarterKit.\n// It allows for other attributes/extensions.\n// @ts-ignore this is fine.\nCode.config.excludes = undefined\n\n// We want the highlighting to take precedence over bolding/italics/links\n// as far as rendering is concerned. Otherwise, the highlighting\n// looks broken up.\nHighlight.config.priority = 1100\n\n/**\n * Default extensions for the TipTap editor.\n *\n * @public\n */\nexport const tipTapDefaultExtensions: Extensions = [\n\tStarterKit.configure({\n\t\tblockquote: false,\n\t\tcodeBlock: false,\n\t\thorizontalRule: false,\n\t\tlink: {\n\t\t\topenOnClick: false,\n\t\t\tautolink: true,\n\t\t},\n\t}),\n\tHighlight,\n\tKeyboardShiftEnterTweakExtension,\n\tTextDirection,\n]\n\n// todo: bust this if the editor changes, too\nconst htmlCache = new WeakCache<TLRichText, string>()\n\n/**\n * Renders HTML from a rich text string.\n *\n * @param editor - The editor instance.\n * @param richText - The rich text content.\n *\n * @public\n */\nexport function renderHtmlFromRichText(editor: Editor, richText: TLRichText) {\n\treturn htmlCache.get(richText, () => {\n\t\tconst tipTapExtensions =\n\t\t\teditor.getTextOptions().tipTapConfig?.extensions ?? tipTapDefaultExtensions\n\t\tconst html = generateHTML(richText as JSONContent, tipTapExtensions)\n\t\t// We replace empty paragraphs with a single line break to prevent the browser from collapsing them.\n\t\treturn html.replaceAll('<p dir=\"auto\"></p>', '<p><br /></p>') ?? ''\n\t})\n}\n\n/**\n * Renders HTML from a rich text string for measurement.\n * @param editor - The editor instance.\n * @param richText - The rich text content.\n *\n *\n * @public\n */\nexport function renderHtmlFromRichTextForMeasurement(editor: Editor, richText: TLRichText) {\n\tconst html = renderHtmlFromRichText(editor, richText)\n\treturn `<div class=\"tl-rich-text\">${html}</div>`\n}\n\n// A weak cache used to store plaintext that's been extracted from rich text.\nconst plainTextFromRichTextCache = new WeakCache<TLRichText, string>()\n\nexport function isEmptyRichText(richText: TLRichText) {\n\tif (richText.content.length === 1) {\n\t\tif (!(richText.content[0] as any).content) return true\n\t}\n\treturn false\n}\n\n/**\n * Renders plaintext from a rich text string.\n * @param editor - The editor instance.\n * @param richText - The rich text content.\n *\n *\n * @public\n */\nexport function renderPlaintextFromRichText(editor: Editor, richText: TLRichText) {\n\tif (isEmptyRichText(richText)) return ''\n\n\treturn plainTextFromRichTextCache.get(richText, () => {\n\t\tconst tipTapExtensions =\n\t\t\teditor.getTextOptions().tipTapConfig?.extensions ?? tipTapDefaultExtensions\n\t\treturn generateText(richText as JSONContent, tipTapExtensions, {\n\t\t\tblockSeparator: '\\n',\n\t\t})\n\t})\n}\n\n/**\n * Renders JSONContent from html.\n * @param editor - The editor instance.\n * @param richText - The rich text content.\n *\n *\n * @public\n */\nexport function renderRichTextFromHTML(editor: Editor, html: string): TLRichText {\n\tconst tipTapExtensions =\n\t\teditor.getTextOptions().tipTapConfig?.extensions ?? tipTapDefaultExtensions\n\treturn generateJSON(html, tipTapExtensions) as TLRichText\n}\n\n/** @public */\nexport function defaultAddFontsFromNode(\n\tnode: Node,\n\tstate: RichTextFontVisitorState,\n\taddFont: (font: TLFontFace) => void\n) {\n\tfor (const mark of node.marks) {\n\t\tif (mark.type.name === 'bold' && state.weight !== 'bold') {\n\t\t\tstate = { ...state, weight: 'bold' }\n\t\t}\n\t\tif (mark.type.name === 'italic' && state.style !== 'italic') {\n\t\t\tstate = { ...state, style: 'italic' }\n\t\t}\n\t\tif (mark.type.name === 'code' && state.family !== 'tldraw_mono') {\n\t\t\tstate = { ...state, family: 'tldraw_mono' }\n\t\t}\n\t}\n\n\tconst fontsForFamily = getOwnProperty(DefaultFontFaces, state.family)\n\tif (!fontsForFamily) return state\n\n\tconst fontsForStyle = getOwnProperty(fontsForFamily, state.style)\n\tif (!fontsForStyle) return state\n\n\tconst fontsForWeight = getOwnProperty(fontsForStyle, state.weight)\n\tif (!fontsForWeight) return state\n\n\taddFont(fontsForWeight)\n\n\treturn state\n}\n"],
|
|
5
|
-
"mappings": "AAAA;AAAA,EACC;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,OAEM;AACP,
|
|
4
|
+
"sourcesContent": ["import {\n\tExtension,\n\tExtensions,\n\tgenerateHTML,\n\tgenerateJSON,\n\tgenerateText,\n\tJSONContent,\n} from '@tiptap/core'\nimport { Code } from '@tiptap/extension-code'\nimport { Highlight } from '@tiptap/extension-highlight'\nimport { Node } from '@tiptap/pm/model'\nimport { StarterKit } from '@tiptap/starter-kit'\nimport {\n\tEditor,\n\tgetOwnProperty,\n\tRichTextFontVisitorState,\n\tTLFontFace,\n\tTLRichText,\n\tWeakCache,\n} from '@tldraw/editor'\nimport { DefaultFontFaces } from '../../shapes/shared/defaultFonts'\nimport { TextDirection } from './textDirection'\n\n/** @public */\nexport const KeyboardShiftEnterTweakExtension = Extension.create({\n\tname: 'keyboardShiftEnterHandler',\n\taddKeyboardShortcuts() {\n\t\treturn {\n\t\t\t// We don't support soft breaks, so we just use the default enter command.\n\t\t\t'Shift-Enter': ({ editor }) => editor.commands.enter(),\n\t\t}\n\t},\n})\n\n// We change the default Code to override what's in the StarterKit.\n// It allows for other attributes/extensions.\n// @ts-ignore this is fine.\nCode.config.excludes = undefined\n\n// We want the highlighting to take precedence over bolding/italics/links\n// as far as rendering is concerned. Otherwise, the highlighting\n// looks broken up.\nHighlight.config.priority = 1100\n\n/**\n * Default extensions for the TipTap editor.\n *\n * @public\n */\nexport const tipTapDefaultExtensions: Extensions = [\n\tStarterKit.configure({\n\t\tblockquote: false,\n\t\tcodeBlock: false,\n\t\thorizontalRule: false,\n\t\tlink: {\n\t\t\topenOnClick: false,\n\t\t\tautolink: true,\n\t\t},\n\t}),\n\tHighlight,\n\tKeyboardShiftEnterTweakExtension,\n\tTextDirection,\n]\n\n// todo: bust this if the editor changes, too\nconst htmlCache = new WeakCache<TLRichText, string>()\n\n/**\n * Renders HTML from a rich text string.\n *\n * @param editor - The editor instance.\n * @param richText - The rich text content.\n *\n * @public\n */\nexport function renderHtmlFromRichText(editor: Editor, richText: TLRichText) {\n\treturn htmlCache.get(richText, () => {\n\t\tconst tipTapExtensions =\n\t\t\teditor.getTextOptions().tipTapConfig?.extensions ?? tipTapDefaultExtensions\n\t\tconst html = generateHTML(richText as JSONContent, tipTapExtensions)\n\t\t// We replace empty paragraphs with a single line break to prevent the browser from collapsing them.\n\t\treturn html.replaceAll('<p dir=\"auto\"></p>', '<p><br /></p>') ?? ''\n\t})\n}\n\n/**\n * Renders HTML from a rich text string for measurement.\n * @param editor - The editor instance.\n * @param richText - The rich text content.\n *\n *\n * @public\n */\nexport function renderHtmlFromRichTextForMeasurement(editor: Editor, richText: TLRichText) {\n\tconst html = renderHtmlFromRichText(editor, richText)\n\treturn `<div class=\"tl-rich-text\">${html}</div>`\n}\n\n// A weak cache used to store plaintext that's been extracted from rich text.\nconst plainTextFromRichTextCache = new WeakCache<TLRichText, string>()\n\nexport function isEmptyRichText(richText: TLRichText) {\n\tif (richText.content.length === 1) {\n\t\tif (!(richText.content[0] as any).content) return true\n\t}\n\treturn false\n}\n\n/**\n * Renders plaintext from a rich text string.\n * @param editor - The editor instance.\n * @param richText - The rich text content.\n *\n *\n * @public\n */\nexport function renderPlaintextFromRichText(editor: Editor, richText: TLRichText) {\n\tif (isEmptyRichText(richText)) return ''\n\n\treturn plainTextFromRichTextCache.get(richText, () => {\n\t\tconst tipTapExtensions =\n\t\t\teditor.getTextOptions().tipTapConfig?.extensions ?? tipTapDefaultExtensions\n\t\treturn generateText(richText as JSONContent, tipTapExtensions, {\n\t\t\tblockSeparator: '\\n',\n\t\t})\n\t})\n}\n\n/**\n * Renders JSONContent from html.\n * @param editor - The editor instance.\n * @param richText - The rich text content.\n *\n *\n * @public\n */\nexport function renderRichTextFromHTML(editor: Editor, html: string): TLRichText {\n\tconst tipTapExtensions =\n\t\teditor.getTextOptions().tipTapConfig?.extensions ?? tipTapDefaultExtensions\n\treturn generateJSON(html, tipTapExtensions) as TLRichText\n}\n\n/** @public */\nexport function defaultAddFontsFromNode(\n\tnode: Node,\n\tstate: RichTextFontVisitorState,\n\taddFont: (font: TLFontFace) => void\n) {\n\tfor (const mark of node.marks) {\n\t\tif (mark.type.name === 'bold' && state.weight !== 'bold') {\n\t\t\tstate = { ...state, weight: 'bold' }\n\t\t}\n\t\tif (mark.type.name === 'italic' && state.style !== 'italic') {\n\t\t\tstate = { ...state, style: 'italic' }\n\t\t}\n\t\tif (mark.type.name === 'code' && state.family !== 'tldraw_mono') {\n\t\t\tstate = { ...state, family: 'tldraw_mono' }\n\t\t}\n\t}\n\n\tconst fontsForFamily = getOwnProperty(DefaultFontFaces, state.family)\n\tif (!fontsForFamily) return state\n\n\tconst fontsForStyle = getOwnProperty(fontsForFamily, state.style)\n\tif (!fontsForStyle) return state\n\n\tconst fontsForWeight = getOwnProperty(fontsForStyle, state.weight)\n\tif (!fontsForWeight) return state\n\n\taddFont(fontsForWeight)\n\n\treturn state\n}\n"],
|
|
5
|
+
"mappings": "AAAA;AAAA,EACC;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,OAEM;AACP,SAAS,YAAY;AACrB,SAAS,iBAAiB;AAE1B,SAAS,kBAAkB;AAC3B;AAAA,EAEC;AAAA,EAIA;AAAA,OACM;AACP,SAAS,wBAAwB;AACjC,SAAS,qBAAqB;AAGvB,MAAM,mCAAmC,UAAU,OAAO;AAAA,EAChE,MAAM;AAAA,EACN,uBAAuB;AACtB,WAAO;AAAA;AAAA,MAEN,eAAe,CAAC,EAAE,OAAO,MAAM,OAAO,SAAS,MAAM;AAAA,IACtD;AAAA,EACD;AACD,CAAC;AAKD,KAAK,OAAO,WAAW;AAKvB,UAAU,OAAO,WAAW;AAOrB,MAAM,0BAAsC;AAAA,EAClD,WAAW,UAAU;AAAA,IACpB,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,gBAAgB;AAAA,IAChB,MAAM;AAAA,MACL,aAAa;AAAA,MACb,UAAU;AAAA,IACX;AAAA,EACD,CAAC;AAAA,EACD;AAAA,EACA;AAAA,EACA;AACD;AAGA,MAAM,YAAY,IAAI,UAA8B;AAU7C,SAAS,uBAAuB,QAAgB,UAAsB;AAC5E,SAAO,UAAU,IAAI,UAAU,MAAM;AACpC,UAAM,mBACL,OAAO,eAAe,EAAE,cAAc,cAAc;AACrD,UAAM,OAAO,aAAa,UAAyB,gBAAgB;AAEnE,WAAO,KAAK,WAAW,sBAAsB,eAAe,KAAK;AAAA,EAClE,CAAC;AACF;AAUO,SAAS,qCAAqC,QAAgB,UAAsB;AAC1F,QAAM,OAAO,uBAAuB,QAAQ,QAAQ;AACpD,SAAO,6BAA6B,IAAI;AACzC;AAGA,MAAM,6BAA6B,IAAI,UAA8B;AAE9D,SAAS,gBAAgB,UAAsB;AACrD,MAAI,SAAS,QAAQ,WAAW,GAAG;AAClC,QAAI,CAAE,SAAS,QAAQ,CAAC,EAAU,QAAS,QAAO;AAAA,EACnD;AACA,SAAO;AACR;AAUO,SAAS,4BAA4B,QAAgB,UAAsB;AACjF,MAAI,gBAAgB,QAAQ,EAAG,QAAO;AAEtC,SAAO,2BAA2B,IAAI,UAAU,MAAM;AACrD,UAAM,mBACL,OAAO,eAAe,EAAE,cAAc,cAAc;AACrD,WAAO,aAAa,UAAyB,kBAAkB;AAAA,MAC9D,gBAAgB;AAAA,IACjB,CAAC;AAAA,EACF,CAAC;AACF;AAUO,SAAS,uBAAuB,QAAgB,MAA0B;AAChF,QAAM,mBACL,OAAO,eAAe,EAAE,cAAc,cAAc;AACrD,SAAO,aAAa,MAAM,gBAAgB;AAC3C;AAGO,SAAS,wBACf,MACA,OACA,SACC;AACD,aAAW,QAAQ,KAAK,OAAO;AAC9B,QAAI,KAAK,KAAK,SAAS,UAAU,MAAM,WAAW,QAAQ;AACzD,cAAQ,EAAE,GAAG,OAAO,QAAQ,OAAO;AAAA,IACpC;AACA,QAAI,KAAK,KAAK,SAAS,YAAY,MAAM,UAAU,UAAU;AAC5D,cAAQ,EAAE,GAAG,OAAO,OAAO,SAAS;AAAA,IACrC;AACA,QAAI,KAAK,KAAK,SAAS,UAAU,MAAM,WAAW,eAAe;AAChE,cAAQ,EAAE,GAAG,OAAO,QAAQ,cAAc;AAAA,IAC3C;AAAA,EACD;AAEA,QAAM,iBAAiB,eAAe,kBAAkB,MAAM,MAAM;AACpE,MAAI,CAAC,eAAgB,QAAO;AAE5B,QAAM,gBAAgB,eAAe,gBAAgB,MAAM,KAAK;AAChE,MAAI,CAAC,cAAe,QAAO;AAE3B,QAAM,iBAAiB,eAAe,eAAe,MAAM,MAAM;AACjE,MAAI,CAAC,eAAgB,QAAO;AAE5B,UAAQ,cAAc;AAEtB,SAAO;AACR;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tldraw",
|
|
3
3
|
"description": "A tiny little drawing editor.",
|
|
4
|
-
"version": "4.3.0-canary.
|
|
4
|
+
"version": "4.3.0-canary.ef0248947f13",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -62,8 +62,8 @@
|
|
|
62
62
|
"@tiptap/pm": "^3.6.2",
|
|
63
63
|
"@tiptap/react": "^3.6.2",
|
|
64
64
|
"@tiptap/starter-kit": "^3.6.2",
|
|
65
|
-
"@tldraw/editor": "4.3.0-canary.
|
|
66
|
-
"@tldraw/store": "4.3.0-canary.
|
|
65
|
+
"@tldraw/editor": "4.3.0-canary.ef0248947f13",
|
|
66
|
+
"@tldraw/store": "4.3.0-canary.ef0248947f13",
|
|
67
67
|
"classnames": "^2.5.1",
|
|
68
68
|
"hotkeys-js": "^3.13.9",
|
|
69
69
|
"idb": "^7.1.1",
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Slider as _Slider } from 'radix-ui'
|
|
|
3
3
|
import React, { useCallback, useEffect, useState } from 'react'
|
|
4
4
|
import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey'
|
|
5
5
|
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
|
|
6
|
-
import {
|
|
6
|
+
import { hideAllTooltips, TldrawUiTooltip } from './TldrawUiTooltip'
|
|
7
7
|
|
|
8
8
|
/** @public */
|
|
9
9
|
export interface TLUiSliderProps {
|
|
@@ -52,7 +52,7 @@ export const TldrawUiSlider = React.forwardRef<HTMLDivElement, TLUiSliderProps>(
|
|
|
52
52
|
)
|
|
53
53
|
|
|
54
54
|
const handlePointerDown = useCallback(() => {
|
|
55
|
-
|
|
55
|
+
hideAllTooltips()
|
|
56
56
|
onHistoryMark?.('click slider')
|
|
57
57
|
}, [onHistoryMark])
|
|
58
58
|
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
assert,
|
|
3
|
+
atom,
|
|
4
|
+
Editor,
|
|
5
|
+
tlenvReactive,
|
|
6
|
+
uniqueId,
|
|
7
|
+
useMaybeEditor,
|
|
8
|
+
useValue,
|
|
9
|
+
} from '@tldraw/editor'
|
|
2
10
|
import { Tooltip as _Tooltip } from 'radix-ui'
|
|
3
11
|
import React, {
|
|
4
12
|
createContext,
|
|
@@ -6,7 +14,6 @@ import React, {
|
|
|
6
14
|
ReactNode,
|
|
7
15
|
useContext,
|
|
8
16
|
useEffect,
|
|
9
|
-
useLayoutEffect,
|
|
10
17
|
useRef,
|
|
11
18
|
useState,
|
|
12
19
|
} from 'react'
|
|
@@ -25,7 +32,7 @@ export interface TldrawUiTooltipProps {
|
|
|
25
32
|
delayDuration?: number
|
|
26
33
|
}
|
|
27
34
|
|
|
28
|
-
interface
|
|
35
|
+
interface TooltipData {
|
|
29
36
|
id: string
|
|
30
37
|
content: ReactNode
|
|
31
38
|
side: 'top' | 'right' | 'bottom' | 'left'
|
|
@@ -35,11 +42,25 @@ interface CurrentTooltip {
|
|
|
35
42
|
delayDuration: number
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
//
|
|
45
|
+
// State machine states
|
|
46
|
+
type TooltipState =
|
|
47
|
+
| { name: 'idle' }
|
|
48
|
+
| { name: 'pointer_down' }
|
|
49
|
+
| { name: 'showing'; tooltip: TooltipData }
|
|
50
|
+
| { name: 'waiting_to_hide'; tooltip: TooltipData; timeoutId: number }
|
|
51
|
+
|
|
52
|
+
// State machine events
|
|
53
|
+
type TooltipEvent =
|
|
54
|
+
| { type: 'pointer_down' }
|
|
55
|
+
| { type: 'pointer_up' }
|
|
56
|
+
| { type: 'show'; tooltip: TooltipData }
|
|
57
|
+
| { type: 'hide'; tooltipId: string; editor: Editor | null; instant: boolean }
|
|
58
|
+
| { type: 'hide_all' }
|
|
59
|
+
|
|
60
|
+
// Singleton tooltip manager using explicit state machine
|
|
39
61
|
class TooltipManager {
|
|
40
62
|
private static instance: TooltipManager | null = null
|
|
41
|
-
private
|
|
42
|
-
private destroyTimeoutId: number | null = null
|
|
63
|
+
private state = atom<TooltipState>('tooltip state', { name: 'idle' })
|
|
43
64
|
|
|
44
65
|
static getInstance(): TooltipManager {
|
|
45
66
|
if (!TooltipManager.instance) {
|
|
@@ -48,86 +69,117 @@ class TooltipManager {
|
|
|
48
69
|
return TooltipManager.instance
|
|
49
70
|
}
|
|
50
71
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
content: string | React.ReactNode,
|
|
54
|
-
targetElement: HTMLElement,
|
|
55
|
-
side: 'top' | 'right' | 'bottom' | 'left',
|
|
56
|
-
sideOffset: number,
|
|
57
|
-
showOnMobile: boolean,
|
|
58
|
-
delayDuration: number
|
|
59
|
-
) {
|
|
60
|
-
// Clear any existing destroy timeout
|
|
61
|
-
if (this.destroyTimeoutId) {
|
|
62
|
-
clearTimeout(this.destroyTimeoutId)
|
|
63
|
-
this.destroyTimeoutId = null
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Update current tooltip
|
|
67
|
-
this.currentTooltip.set({
|
|
68
|
-
id: tooltipId,
|
|
69
|
-
content,
|
|
70
|
-
side,
|
|
71
|
-
sideOffset,
|
|
72
|
-
showOnMobile,
|
|
73
|
-
targetElement,
|
|
74
|
-
delayDuration,
|
|
75
|
-
})
|
|
72
|
+
hideAllTooltips() {
|
|
73
|
+
this.handleEvent({ type: 'hide_all' })
|
|
76
74
|
}
|
|
77
75
|
|
|
78
|
-
|
|
79
|
-
this.
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
handleEvent(event: TooltipEvent) {
|
|
77
|
+
const currentState = this.state.get()
|
|
78
|
+
|
|
79
|
+
switch (event.type) {
|
|
80
|
+
case 'pointer_down': {
|
|
81
|
+
// Transition to pointer_down from any state
|
|
82
|
+
if (currentState.name === 'waiting_to_hide') {
|
|
83
|
+
clearTimeout(currentState.timeoutId)
|
|
84
|
+
}
|
|
85
|
+
this.state.set({ name: 'pointer_down' })
|
|
86
|
+
break
|
|
82
87
|
}
|
|
83
|
-
return tooltip
|
|
84
|
-
})
|
|
85
|
-
}
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
case 'pointer_up': {
|
|
90
|
+
// Only transition from pointer_down to idle
|
|
91
|
+
if (currentState.name === 'pointer_down') {
|
|
92
|
+
this.state.set({ name: 'idle' })
|
|
93
|
+
}
|
|
94
|
+
break
|
|
93
95
|
}
|
|
94
|
-
}
|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
}
|
|
97
|
+
case 'show': {
|
|
98
|
+
// Don't show tooltips while pointer is down
|
|
99
|
+
if (currentState.name === 'pointer_down') {
|
|
100
|
+
return
|
|
101
|
+
}
|
|
103
102
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
// Clear any existing timeout if transitioning from waiting_to_hide
|
|
104
|
+
if (currentState.name === 'waiting_to_hide') {
|
|
105
|
+
clearTimeout(currentState.timeoutId)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Transition to showing state
|
|
109
|
+
this.state.set({ name: 'showing', tooltip: event.tooltip })
|
|
110
|
+
break
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
case 'hide': {
|
|
114
|
+
const { tooltipId, editor, instant } = event
|
|
115
|
+
|
|
116
|
+
// Only hide if the tooltip matches
|
|
117
|
+
if (currentState.name === 'showing' && currentState.tooltip.id === tooltipId) {
|
|
118
|
+
if (editor && !instant) {
|
|
119
|
+
// Transition to waiting_to_hide state
|
|
120
|
+
const timeoutId = editor.timers.setTimeout(() => {
|
|
121
|
+
const state = this.state.get()
|
|
122
|
+
if (state.name === 'waiting_to_hide' && state.tooltip.id === tooltipId) {
|
|
123
|
+
this.state.set({ name: 'idle' })
|
|
124
|
+
}
|
|
125
|
+
}, 300)
|
|
126
|
+
this.state.set({
|
|
127
|
+
name: 'waiting_to_hide',
|
|
128
|
+
tooltip: currentState.tooltip,
|
|
129
|
+
timeoutId,
|
|
130
|
+
})
|
|
131
|
+
} else {
|
|
132
|
+
this.state.set({ name: 'idle' })
|
|
133
|
+
}
|
|
134
|
+
} else if (
|
|
135
|
+
currentState.name === 'waiting_to_hide' &&
|
|
136
|
+
currentState.tooltip.id === tooltipId
|
|
137
|
+
) {
|
|
138
|
+
// Already waiting to hide, make it instant if requested
|
|
139
|
+
if (instant) {
|
|
140
|
+
clearTimeout(currentState.timeoutId)
|
|
141
|
+
this.state.set({ name: 'idle' })
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
break
|
|
145
|
+
}
|
|
108
146
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
147
|
+
case 'hide_all': {
|
|
148
|
+
if (currentState.name === 'waiting_to_hide') {
|
|
149
|
+
clearTimeout(currentState.timeoutId)
|
|
150
|
+
}
|
|
151
|
+
// Preserve pointer_down state if that's the current state
|
|
152
|
+
if (currentState.name === 'pointer_down') {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
this.state.set({ name: 'idle' })
|
|
156
|
+
break
|
|
157
|
+
}
|
|
158
|
+
}
|
|
114
159
|
}
|
|
115
160
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
})
|
|
161
|
+
getCurrentTooltipData(): TooltipData | null {
|
|
162
|
+
const currentState = this.state.get()
|
|
163
|
+
let tooltip: TooltipData | null = null
|
|
164
|
+
|
|
165
|
+
if (currentState.name === 'showing') {
|
|
166
|
+
tooltip = currentState.tooltip
|
|
167
|
+
} else if (currentState.name === 'waiting_to_hide') {
|
|
168
|
+
tooltip = currentState.tooltip
|
|
125
169
|
}
|
|
126
|
-
|
|
170
|
+
|
|
171
|
+
if (!tooltip) return null
|
|
172
|
+
if (tlenvReactive.get().isCoarsePointer && !tooltip.showOnMobile) return null
|
|
173
|
+
return tooltip
|
|
127
174
|
}
|
|
128
175
|
}
|
|
129
176
|
|
|
130
|
-
|
|
177
|
+
const tooltipManager = TooltipManager.getInstance()
|
|
178
|
+
|
|
179
|
+
/** @public */
|
|
180
|
+
export function hideAllTooltips() {
|
|
181
|
+
tooltipManager.hideAllTooltips()
|
|
182
|
+
}
|
|
131
183
|
|
|
132
184
|
// Context for the tooltip singleton
|
|
133
185
|
const TooltipSingletonContext = createContext<boolean>(false)
|
|
@@ -167,14 +219,19 @@ function TooltipSingleton() {
|
|
|
167
219
|
// Hide tooltip when camera is moving (panning/zooming)
|
|
168
220
|
useEffect(() => {
|
|
169
221
|
if (cameraState === 'moving' && isOpen && currentTooltip) {
|
|
170
|
-
tooltipManager.
|
|
222
|
+
tooltipManager.handleEvent({
|
|
223
|
+
type: 'hide',
|
|
224
|
+
tooltipId: currentTooltip.id,
|
|
225
|
+
editor,
|
|
226
|
+
instant: true,
|
|
227
|
+
})
|
|
171
228
|
}
|
|
172
229
|
}, [cameraState, isOpen, currentTooltip, editor])
|
|
173
230
|
|
|
174
231
|
useEffect(() => {
|
|
175
232
|
function handleKeyDown(event: KeyboardEvent) {
|
|
176
233
|
if (event.key === 'Escape' && currentTooltip && isOpen) {
|
|
177
|
-
|
|
234
|
+
hideAllTooltips()
|
|
178
235
|
event.stopPropagation()
|
|
179
236
|
}
|
|
180
237
|
}
|
|
@@ -183,7 +240,29 @@ function TooltipSingleton() {
|
|
|
183
240
|
return () => {
|
|
184
241
|
document.removeEventListener('keydown', handleKeyDown, { capture: true })
|
|
185
242
|
}
|
|
186
|
-
}, [
|
|
243
|
+
}, [currentTooltip, isOpen])
|
|
244
|
+
|
|
245
|
+
// Hide tooltip and prevent new ones from opening while pointer is down
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
function handlePointerDown() {
|
|
248
|
+
tooltipManager.handleEvent({ type: 'pointer_down' })
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function handlePointerUp() {
|
|
252
|
+
tooltipManager.handleEvent({ type: 'pointer_up' })
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
document.addEventListener('pointerdown', handlePointerDown, { capture: true })
|
|
256
|
+
document.addEventListener('pointerup', handlePointerUp, { capture: true })
|
|
257
|
+
document.addEventListener('pointercancel', handlePointerUp, { capture: true })
|
|
258
|
+
return () => {
|
|
259
|
+
document.removeEventListener('pointerdown', handlePointerDown, { capture: true })
|
|
260
|
+
document.removeEventListener('pointerup', handlePointerUp, { capture: true })
|
|
261
|
+
document.removeEventListener('pointercancel', handlePointerUp, { capture: true })
|
|
262
|
+
// Reset pointer state on unmount to prevent stuck state
|
|
263
|
+
tooltipManager.handleEvent({ type: 'pointer_up' })
|
|
264
|
+
}
|
|
265
|
+
}, [])
|
|
187
266
|
|
|
188
267
|
// Update open state and trigger position
|
|
189
268
|
useEffect(() => {
|
|
@@ -280,23 +359,16 @@ export const TldrawUiTooltip = forwardRef<HTMLButtonElement, TldrawUiTooltipProp
|
|
|
280
359
|
const currentTooltipId = tooltipId.current
|
|
281
360
|
return () => {
|
|
282
361
|
if (hasProvider) {
|
|
283
|
-
tooltipManager.
|
|
362
|
+
tooltipManager.handleEvent({
|
|
363
|
+
type: 'hide',
|
|
364
|
+
tooltipId: currentTooltipId,
|
|
365
|
+
editor,
|
|
366
|
+
instant: true,
|
|
367
|
+
})
|
|
284
368
|
}
|
|
285
369
|
}
|
|
286
370
|
}, [editor, hasProvider])
|
|
287
371
|
|
|
288
|
-
useLayoutEffect(() => {
|
|
289
|
-
if (hasProvider && tooltipManager.getCurrentTooltipData()?.id === tooltipId.current) {
|
|
290
|
-
tooltipManager.updateCurrentTooltip(tooltipId.current, (tooltip) => ({
|
|
291
|
-
...tooltip,
|
|
292
|
-
content,
|
|
293
|
-
side: sideToUse,
|
|
294
|
-
sideOffset,
|
|
295
|
-
showOnMobile,
|
|
296
|
-
}))
|
|
297
|
-
}
|
|
298
|
-
}, [content, sideToUse, sideOffset, showOnMobile, hasProvider])
|
|
299
|
-
|
|
300
372
|
// Don't show tooltip if disabled, no content, or enhanced accessibility mode is disabled
|
|
301
373
|
if (disabled || !content) {
|
|
302
374
|
return <>{children}</>
|
|
@@ -340,38 +412,54 @@ export const TldrawUiTooltip = forwardRef<HTMLButtonElement, TldrawUiTooltipProp
|
|
|
340
412
|
|
|
341
413
|
const handleMouseEnter = (event: React.MouseEvent<HTMLElement>) => {
|
|
342
414
|
child.props.onMouseEnter?.(event)
|
|
343
|
-
tooltipManager.
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
415
|
+
tooltipManager.handleEvent({
|
|
416
|
+
type: 'show',
|
|
417
|
+
tooltip: {
|
|
418
|
+
id: tooltipId.current,
|
|
419
|
+
content,
|
|
420
|
+
targetElement: event.currentTarget as HTMLElement,
|
|
421
|
+
side: sideToUse,
|
|
422
|
+
sideOffset,
|
|
423
|
+
showOnMobile,
|
|
424
|
+
delayDuration: delayDurationToUse,
|
|
425
|
+
},
|
|
426
|
+
})
|
|
352
427
|
}
|
|
353
428
|
|
|
354
429
|
const handleMouseLeave = (event: React.MouseEvent<HTMLElement>) => {
|
|
355
430
|
child.props.onMouseLeave?.(event)
|
|
356
|
-
tooltipManager.
|
|
431
|
+
tooltipManager.handleEvent({
|
|
432
|
+
type: 'hide',
|
|
433
|
+
tooltipId: tooltipId.current,
|
|
434
|
+
editor,
|
|
435
|
+
instant: false,
|
|
436
|
+
})
|
|
357
437
|
}
|
|
358
438
|
|
|
359
439
|
const handleFocus = (event: React.FocusEvent<HTMLElement>) => {
|
|
360
440
|
child.props.onFocus?.(event)
|
|
361
|
-
tooltipManager.
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
441
|
+
tooltipManager.handleEvent({
|
|
442
|
+
type: 'show',
|
|
443
|
+
tooltip: {
|
|
444
|
+
id: tooltipId.current,
|
|
445
|
+
content,
|
|
446
|
+
targetElement: event.currentTarget as HTMLElement,
|
|
447
|
+
side: sideToUse,
|
|
448
|
+
sideOffset,
|
|
449
|
+
showOnMobile,
|
|
450
|
+
delayDuration: delayDurationToUse,
|
|
451
|
+
},
|
|
452
|
+
})
|
|
370
453
|
}
|
|
371
454
|
|
|
372
455
|
const handleBlur = (event: React.FocusEvent<HTMLElement>) => {
|
|
373
456
|
child.props.onBlur?.(event)
|
|
374
|
-
tooltipManager.
|
|
457
|
+
tooltipManager.handleEvent({
|
|
458
|
+
type: 'hide',
|
|
459
|
+
tooltipId: tooltipId.current,
|
|
460
|
+
editor,
|
|
461
|
+
instant: false,
|
|
462
|
+
})
|
|
375
463
|
}
|
|
376
464
|
|
|
377
465
|
const childrenWithHandlers = React.cloneElement(children as React.ReactElement, {
|
|
@@ -24,7 +24,7 @@ import { TldrawUiDropdownMenuItem } from '../TldrawUiDropdownMenu'
|
|
|
24
24
|
import { TLUiIconJsx } from '../TldrawUiIcon'
|
|
25
25
|
import { TldrawUiKbd } from '../TldrawUiKbd'
|
|
26
26
|
import { TldrawUiToolbarButton } from '../TldrawUiToolbar'
|
|
27
|
-
import {
|
|
27
|
+
import { hideAllTooltips } from '../TldrawUiTooltip'
|
|
28
28
|
import { useTldrawUiMenuContext } from './TldrawUiMenuContext'
|
|
29
29
|
|
|
30
30
|
/** @public */
|
|
@@ -350,7 +350,7 @@ function useDraggableEvents(
|
|
|
350
350
|
point: screenSpaceStart,
|
|
351
351
|
})
|
|
352
352
|
|
|
353
|
-
|
|
353
|
+
hideAllTooltips()
|
|
354
354
|
editor.getContainer().focus()
|
|
355
355
|
})
|
|
356
356
|
}
|
package/src/lib/ui/version.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// This file is automatically generated by internal/scripts/refresh-assets.ts.
|
|
2
2
|
// Do not edit manually. Or do, I'm a comment, not a cop.
|
|
3
3
|
|
|
4
|
-
export const version = '4.3.0-canary.
|
|
4
|
+
export const version = '4.3.0-canary.ef0248947f13'
|
|
5
5
|
export const publishDates = {
|
|
6
6
|
major: '2025-09-18T14:39:22.803Z',
|
|
7
|
-
minor: '2025-
|
|
8
|
-
patch: '2025-
|
|
7
|
+
minor: '2025-12-05T17:44:11.680Z',
|
|
8
|
+
patch: '2025-12-05T17:44:11.680Z',
|
|
9
9
|
}
|
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
generateText,
|
|
7
7
|
JSONContent,
|
|
8
8
|
} from '@tiptap/core'
|
|
9
|
-
import Code from '@tiptap/extension-code'
|
|
10
|
-
import Highlight from '@tiptap/extension-highlight'
|
|
9
|
+
import { Code } from '@tiptap/extension-code'
|
|
10
|
+
import { Highlight } from '@tiptap/extension-highlight'
|
|
11
11
|
import { Node } from '@tiptap/pm/model'
|
|
12
|
-
import StarterKit from '@tiptap/starter-kit'
|
|
12
|
+
import { StarterKit } from '@tiptap/starter-kit'
|
|
13
13
|
import {
|
|
14
14
|
Editor,
|
|
15
15
|
getOwnProperty,
|
|
@@ -285,8 +285,9 @@ describe('<TldrawEditor />', () => {
|
|
|
285
285
|
|
|
286
286
|
// we should only get one editor instance
|
|
287
287
|
expect(editorInstances.size).toBe(1)
|
|
288
|
-
//
|
|
289
|
-
|
|
288
|
+
// strict mode may cause onMount to be called twice, but the important
|
|
289
|
+
// thing is that we always get the same editor instance
|
|
290
|
+
expect(onMount).toHaveBeenCalled()
|
|
290
291
|
})
|
|
291
292
|
|
|
292
293
|
it('allows updating camera options without re-creating the editor', async () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TLContent, structuredClone } from '@tldraw/editor'
|
|
1
|
+
import { TLContent, createShapeId, structuredClone } from '@tldraw/editor'
|
|
2
2
|
import { TestEditor } from '../TestEditor'
|
|
3
3
|
|
|
4
4
|
let editor: TestEditor
|
|
@@ -38,3 +38,81 @@ describe('Migrations', () => {
|
|
|
38
38
|
expect(() => editor.putContentOntoCurrentPage(withInvalidShapeModel)).toThrow()
|
|
39
39
|
})
|
|
40
40
|
})
|
|
41
|
+
|
|
42
|
+
describe('Paste parent selection with explicit point', () => {
|
|
43
|
+
it('falls back to the page when the cursor is outside the original parent', () => {
|
|
44
|
+
const frameId = createShapeId('frame')
|
|
45
|
+
const childId = createShapeId('child')
|
|
46
|
+
|
|
47
|
+
editor.createShapes([
|
|
48
|
+
{
|
|
49
|
+
id: frameId,
|
|
50
|
+
type: 'frame',
|
|
51
|
+
x: 0,
|
|
52
|
+
y: 0,
|
|
53
|
+
props: { w: 200, h: 200 },
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: childId,
|
|
57
|
+
type: 'geo',
|
|
58
|
+
parentId: frameId,
|
|
59
|
+
x: 40,
|
|
60
|
+
y: 40,
|
|
61
|
+
props: { w: 60, h: 60 },
|
|
62
|
+
},
|
|
63
|
+
])
|
|
64
|
+
|
|
65
|
+
editor.select(childId)
|
|
66
|
+
editor.copy()
|
|
67
|
+
|
|
68
|
+
editor.putContentOntoCurrentPage(editor.clipboard!, {
|
|
69
|
+
point: { x: 500, y: 500 },
|
|
70
|
+
select: true,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const [pastedId] = editor.getSelectedShapeIds()
|
|
74
|
+
expect(editor.getShape(pastedId)?.parentId).toBe(editor.getCurrentPageId())
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('uses the parent under the cursor when it can accept the pasted shapes', () => {
|
|
78
|
+
const frameAId = createShapeId('frameA')
|
|
79
|
+
const frameBId = createShapeId('frameB')
|
|
80
|
+
const childId = createShapeId('child')
|
|
81
|
+
|
|
82
|
+
editor.createShapes([
|
|
83
|
+
{
|
|
84
|
+
id: frameAId,
|
|
85
|
+
type: 'frame',
|
|
86
|
+
x: 0,
|
|
87
|
+
y: 0,
|
|
88
|
+
props: { w: 200, h: 200 },
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: frameBId,
|
|
92
|
+
type: 'frame',
|
|
93
|
+
x: 400,
|
|
94
|
+
y: 0,
|
|
95
|
+
props: { w: 200, h: 200 },
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: childId,
|
|
99
|
+
type: 'geo',
|
|
100
|
+
parentId: frameAId,
|
|
101
|
+
x: 40,
|
|
102
|
+
y: 40,
|
|
103
|
+
props: { w: 60, h: 60 },
|
|
104
|
+
},
|
|
105
|
+
])
|
|
106
|
+
|
|
107
|
+
editor.select(childId)
|
|
108
|
+
editor.copy()
|
|
109
|
+
|
|
110
|
+
editor.putContentOntoCurrentPage(editor.clipboard!, {
|
|
111
|
+
point: { x: 450, y: 50 },
|
|
112
|
+
select: true,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const [pastedId] = editor.getSelectedShapeIds()
|
|
116
|
+
expect(editor.getShape(pastedId)?.parentId).toBe(frameBId)
|
|
117
|
+
})
|
|
118
|
+
})
|