@tldraw/editor 4.6.0-next.1f489710ee41 → 4.6.0-next.4dde09fa17ab
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 +60 -18
- package/dist-cjs/index.js +5 -4
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +4 -2
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/config/{createTLUser.js → createTLCurrentUser.js} +9 -9
- package/dist-cjs/lib/config/createTLCurrentUser.js.map +7 -0
- package/dist-cjs/lib/config/createTLStore.js +23 -0
- package/dist-cjs/lib/config/createTLStore.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +111 -4
- package/dist-cjs/lib/editor/Editor.js.map +3 -3
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +1 -1
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js +10 -0
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/types/clipboard-types.js.map +1 -1
- package/dist-cjs/lib/hooks/useGestureEvents.js +171 -127
- package/dist-cjs/lib/hooks/useGestureEvents.js.map +3 -3
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +60 -18
- package/dist-esm/index.mjs +9 -4
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +4 -2
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/config/{createTLUser.mjs → createTLCurrentUser.mjs} +6 -6
- package/dist-esm/lib/config/createTLCurrentUser.mjs.map +7 -0
- package/dist-esm/lib/config/createTLStore.mjs +27 -1
- package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +113 -4
- package/dist-esm/lib/editor/Editor.mjs.map +3 -3
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +1 -1
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +10 -0
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/hooks/useGestureEvents.mjs +171 -127
- package/dist-esm/lib/hooks/useGestureEvents.mjs.map +3 -3
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +13 -0
- package/package.json +8 -9
- package/src/index.ts +6 -1
- package/src/lib/TldrawEditor.tsx +8 -6
- package/src/lib/config/{createTLUser.ts → createTLCurrentUser.ts} +6 -6
- package/src/lib/config/createTLStore.ts +35 -1
- package/src/lib/editor/Editor.ts +140 -3
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +2 -2
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +2 -2
- package/src/lib/editor/shapes/ShapeUtil.ts +11 -0
- package/src/lib/editor/types/clipboard-types.ts +2 -1
- package/src/lib/hooks/useGestureEvents.ts +240 -168
- package/src/lib/primitives/Box.test.ts +30 -0
- package/src/lib/primitives/geometry/Geometry2d.test.ts +21 -0
- package/src/version.ts +3 -3
- package/dist-cjs/lib/config/createTLUser.js.map +0 -7
- package/dist-esm/lib/config/createTLUser.mjs.map +0 -7
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/hooks/useGestureEvents.ts"],
|
|
4
|
-
"sourcesContent": ["import type { AnyHandlerEventTypes, EventTypes, GestureKey, Handler } from '@use-gesture/core/types'\nimport { createUseGesture, pinchAction, wheelAction } from '@use-gesture/react'\nimport * as React from 'react'\nimport { TLWheelEventInfo } from '../editor/types/event-types'\nimport { Vec } from '../primitives/Vec'\nimport { preventDefault } from '../utils/dom'\nimport { isAccelKey } from '../utils/keyboard'\nimport { normalizeWheel } from '../utils/normalizeWheel'\nimport { useEditor } from './useEditor'\n\n/*\n\n# How does pinching work?\n\nThe pinching handler is fired under two circumstances: \n- when a user is on a MacBook trackpad and is ZOOMING with a two-finger pinch\n- when a user is on a touch device and is ZOOMING with a two-finger pinch\n- when a user is on a touch device and is PANNING with two fingers\n\nZooming is much more expensive than panning (because it causes shapes to render), \nso we want to be sure that we don't zoom while two-finger panning. \n\nIn order to do this, we keep track of a \"pinchState\", which is either:\n- \"zooming\"\n- \"panning\"\n- \"not sure\"\n\nIf a user is on a trackpad, the pinchState will be set to \"zooming\". \n\nIf the user is on a touch screen, then we start in the \"not sure\" state and switch back and forth\nbetween \"zooming\", \"panning\", and \"not sure\" based on what the user is doing with their fingers.\n\nIn the \"not sure\" state, we examine whether the user has moved the center of the gesture far enough\nto suggest that they're panning; or else that they've moved their fingers further apart or closer\ntogether enough to suggest that they're zooming. \n\nIn the \"panning\" state, we check whether the user's fingers have moved far enough apart to suggest\nthat they're zooming. If they have, we switch to the \"zooming\" state.\n\nIn the \"zooming\" state, we just stay zooming\u2014it's not YET possible to switch back to panning.\n\ntodo: compare velocities of change in order to determine whether the user has switched back to panning\n*/\n\ntype check<T extends AnyHandlerEventTypes, Key extends GestureKey> = undefined extends T[Key]\n\t? EventTypes[Key]\n\t: T[Key]\ntype PinchHandler = Handler<'pinch', check<EventTypes, 'pinch'>>\n\nconst useGesture = createUseGesture([wheelAction, pinchAction])\n\n/**\n * GOTCHA\n *\n * UseGesture fires a wheel event 140ms after the gesture actually ends, with a momentum-adjusted\n * delta. This creates a messed up interaction where after you stop scrolling suddenly the dang page\n * jumps a tick. why do they do this? you are asking the wrong person. it seems intentional though.\n * anyway we want to ignore that last event, but there's no way to directly detect it so we need to\n * keep track of timestamps. Yes this is awful, I am sorry.\n */\nlet lastWheelTime = undefined as undefined | number\n\nconst isWheelEndEvent = (time: number) => {\n\tif (lastWheelTime === undefined) {\n\t\tlastWheelTime = time\n\t\treturn false\n\t}\n\n\tif (time - lastWheelTime > 120 && time - lastWheelTime < 160) {\n\t\tlastWheelTime = time\n\t\treturn true\n\t}\n\n\tlastWheelTime = time\n\treturn false\n}\n\nexport function useGestureEvents(ref: React.RefObject<HTMLDivElement | null>) {\n\tconst editor = useEditor()\n\n\tconst events = React.useMemo(() => {\n\t\tlet pinchState = 'not sure' as 'not sure' | 'zooming' | 'panning'\n\n\t\tconst onWheel: Handler<'wheel', WheelEvent> = ({ event }) => {\n\t\t\tif (!editor.getInstanceState().isFocused) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tpinchState = 'not sure'\n\n\t\t\tif (isWheelEndEvent(Date.now())) {\n\t\t\t\t// ignore wheelEnd events\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Awful tht we need to put this logic here, but basically\n\t\t\t// we don't want to handle the the wheel event (or call prevent\n\t\t\t// default on the evnet) if the user is wheeling over an a shape\n\t\t\t// that is scrollable which they're currently editing.\n\n\t\t\tconst editingShapeId = editor.getEditingShapeId()\n\t\t\tif (editingShapeId) {\n\t\t\t\tconst shape = editor.getShape(editingShapeId)\n\t\t\t\tif (shape) {\n\t\t\t\t\tconst util = editor.getShapeUtil(shape)\n\t\t\t\t\tif (util.canScroll(shape)) {\n\t\t\t\t\t\tconst bounds = editor.getShapePageBounds(editingShapeId)\n\t\t\t\t\t\tif (bounds?.containsPoint(editor.inputs.getCurrentPagePoint())) {\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tpreventDefault(event)\n\t\t\tevent.stopPropagation()\n\t\t\tconst delta = normalizeWheel(event)\n\n\t\t\tif (delta.x === 0 && delta.y === 0) return\n\n\t\t\tconst info: TLWheelEventInfo = {\n\t\t\t\ttype: 'wheel',\n\t\t\t\tname: 'wheel',\n\t\t\t\tdelta,\n\t\t\t\tpoint: new Vec(event.clientX, event.clientY),\n\t\t\t\tshiftKey: event.shiftKey,\n\t\t\t\taltKey: event.altKey,\n\t\t\t\tctrlKey: event.metaKey || event.ctrlKey,\n\t\t\t\tmetaKey: event.metaKey,\n\t\t\t\taccelKey: isAccelKey(event),\n\t\t\t}\n\n\t\t\teditor.dispatch(info)\n\t\t}\n\n\t\tlet initDistanceBetweenFingers = 1 // the distance between the two fingers when the pinch starts\n\t\tlet initZoom = 1 // the browser's zoom level when the pinch starts\n\t\tlet currDistanceBetweenFingers = 0\n\t\tconst initPointBetweenFingers = new Vec()\n\t\tconst prevPointBetweenFingers = new Vec()\n\n\t\tconst onPinchStart: PinchHandler = (gesture) => {\n\t\t\tconst elm = ref.current\n\t\t\tpinchState = 'not sure'\n\n\t\t\tconst { event, origin, da } = gesture\n\n\t\t\tif (event instanceof WheelEvent) return\n\t\t\tif (!(event.target === elm || elm?.contains(event.target as Node))) return\n\n\t\t\tprevPointBetweenFingers.x = origin[0]\n\t\t\tprevPointBetweenFingers.y = origin[1]\n\t\t\tinitPointBetweenFingers.x = origin[0]\n\t\t\tinitPointBetweenFingers.y = origin[1]\n\t\t\tinitDistanceBetweenFingers = da[0]\n\t\t\tinitZoom = editor.getZoomLevel()\n\n\t\t\teditor.dispatch({\n\t\t\t\ttype: 'pinch',\n\t\t\t\tname: 'pinch_start',\n\t\t\t\tpoint: { x: origin[0], y: origin[1], z: editor.getZoomLevel() },\n\t\t\t\tdelta: { x: 0, y: 0 },\n\t\t\t\tshiftKey: event.shiftKey,\n\t\t\t\taltKey: event.altKey,\n\t\t\t\tctrlKey: event.metaKey || event.ctrlKey,\n\t\t\t\tmetaKey: event.metaKey,\n\t\t\t\taccelKey: isAccelKey(event),\n\t\t\t})\n\t\t}\n\n\t\t// let timeout: any\n\t\tconst updatePinchState = (isSafariTrackpadPinch: boolean) => {\n\t\t\tif (isSafariTrackpadPinch) {\n\t\t\t\tpinchState = 'zooming'\n\t\t\t}\n\n\t\t\tif (pinchState === 'zooming') {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Initial: [touch]-------origin-------[touch]\n\t\t\t// Current: [touch]-----------origin----------[touch]\n\t\t\t// |----| |------------|\n\t\t\t// originDistance ^ ^ touchDistance\n\n\t\t\t// How far have the two touch points moved towards or away from eachother?\n\t\t\tconst touchDistance = Math.abs(currDistanceBetweenFingers - initDistanceBetweenFingers)\n\t\t\t// How far has the point between the touches moved?\n\t\t\tconst originDistance = Vec.Dist(initPointBetweenFingers, prevPointBetweenFingers)\n\n\t\t\tswitch (pinchState) {\n\t\t\t\tcase 'not sure': {\n\t\t\t\t\tif (touchDistance > 24) {\n\t\t\t\t\t\tpinchState = 'zooming'\n\t\t\t\t\t} else if (originDistance > 16) {\n\t\t\t\t\t\tpinchState = 'panning'\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase 'panning': {\n\t\t\t\t\t// Slightly more touch distance needed to go from panning to zooming\n\t\t\t\t\tif (touchDistance > 64) {\n\t\t\t\t\t\tpinchState = 'zooming'\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst onPinch: PinchHandler = (gesture) => {\n\t\t\tconst elm = ref.current\n\t\t\tconst { event, origin, offset, da } = gesture\n\n\t\t\tif (event instanceof WheelEvent) return\n\t\t\tif (!(event.target === elm || elm?.contains(event.target as Node))) return\n\n\t\t\t// In (desktop) Safari, a two finger trackpad pinch will be a \"gesturechange\" event\n\t\t\t// and will have 0 touches; on iOS, a two-finger pinch will be a \"pointermove\" event\n\t\t\t// with two touches.\n\t\t\tconst isSafariTrackpadPinch =\n\t\t\t\tgesture.type === 'gesturechange' || gesture.type === 'gestureend'\n\n\t\t\t// The distance between the two touch points\n\t\t\tcurrDistanceBetweenFingers = da[0]\n\n\t\t\t// Only update the zoom if the pointers are far enough apart;\n\t\t\t// a very small touchDistance means that the user has probably\n\t\t\t// pinched out and their fingers are touching; this produces\n\t\t\t// very unstable zooming behavior.\n\n\t\t\tconst dx = origin[0] - prevPointBetweenFingers.x\n\t\t\tconst dy = origin[1] - prevPointBetweenFingers.y\n\n\t\t\tprevPointBetweenFingers.x = origin[0]\n\t\t\tprevPointBetweenFingers.y = origin[1]\n\n\t\t\tupdatePinchState(isSafariTrackpadPinch)\n\n\t\t\tswitch (pinchState) {\n\t\t\t\tcase 'zooming': {\n\t\t\t\t\tconst currZoom = offset[0] ** editor.getCameraOptions().zoomSpeed\n\n\t\t\t\t\teditor.dispatch({\n\t\t\t\t\t\ttype: 'pinch',\n\t\t\t\t\t\tname: 'pinch',\n\t\t\t\t\t\tpoint: { x: origin[0], y: origin[1], z: currZoom },\n\t\t\t\t\t\tdelta: { x: dx, y: dy },\n\t\t\t\t\t\tshiftKey: event.shiftKey,\n\t\t\t\t\t\taltKey: event.altKey,\n\t\t\t\t\t\tctrlKey: event.metaKey || event.ctrlKey,\n\t\t\t\t\t\tmetaKey: event.metaKey,\n\t\t\t\t\t\taccelKey: isAccelKey(event),\n\t\t\t\t\t})\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase 'panning': {\n\t\t\t\t\teditor.dispatch({\n\t\t\t\t\t\ttype: 'pinch',\n\t\t\t\t\t\tname: 'pinch',\n\t\t\t\t\t\tpoint: { x: origin[0], y: origin[1], z: initZoom },\n\t\t\t\t\t\tdelta: { x: dx, y: dy },\n\t\t\t\t\t\tshiftKey: event.shiftKey,\n\t\t\t\t\t\taltKey: event.altKey,\n\t\t\t\t\t\tctrlKey: event.metaKey || event.ctrlKey,\n\t\t\t\t\t\tmetaKey: event.metaKey,\n\t\t\t\t\t\taccelKey: isAccelKey(event),\n\t\t\t\t\t})\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst onPinchEnd: PinchHandler = (gesture) => {\n\t\t\tconst elm = ref.current\n\t\t\tconst { event, origin, offset } = gesture\n\n\t\t\tif (event instanceof WheelEvent) return\n\t\t\tif (!(event.target === elm || elm?.contains(event.target as Node))) return\n\n\t\t\tconst scale = offset[0] ** editor.getCameraOptions().zoomSpeed\n\n\t\t\tpinchState = 'not sure'\n\n\t\t\teditor.timers.requestAnimationFrame(() => {\n\t\t\t\teditor.dispatch({\n\t\t\t\t\ttype: 'pinch',\n\t\t\t\t\tname: 'pinch_end',\n\t\t\t\t\tpoint: { x: origin[0], y: origin[1], z: scale },\n\t\t\t\t\tdelta: { x: origin[0], y: origin[1] },\n\t\t\t\t\tshiftKey: event.shiftKey,\n\t\t\t\t\taltKey: event.altKey,\n\t\t\t\t\tctrlKey: event.metaKey || event.ctrlKey,\n\t\t\t\t\tmetaKey: event.metaKey,\n\t\t\t\t\taccelKey: isAccelKey(event),\n\t\t\t\t})\n\t\t\t})\n\t\t}\n\n\t\treturn {\n\t\t\tonWheel,\n\t\t\tonPinchStart,\n\t\t\tonPinchEnd,\n\t\t\tonPinch,\n\t\t}\n\t}, [editor, ref])\n\n\tuseGesture(events, {\n\t\ttarget: ref,\n\t\teventOptions: { passive: false },\n\t\tpinch: {\n\t\t\tfrom: () => {\n\t\t\t\tconst { zoomSpeed } = editor.getCameraOptions()\n\t\t\t\tconst level = editor.getZoomLevel() ** (1 / zoomSpeed)\n\t\t\t\treturn [level, 0]\n\t\t\t}, // Return the camera z to use when pinch starts\n\t\t\tscaleBounds: () => {\n\t\t\t\tconst baseZoom = editor.getBaseZoom()\n\t\t\t\tconst { zoomSteps, zoomSpeed } = editor.getCameraOptions()\n\t\t\t\tconst zoomMin = zoomSteps[0] * baseZoom\n\t\t\t\tconst zoomMax = zoomSteps[zoomSteps.length - 1] * baseZoom\n\n\t\t\t\treturn {\n\t\t\t\t\tmax: zoomMax ** (1 / zoomSpeed),\n\t\t\t\t\tmin: zoomMin ** (1 / zoomSpeed),\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t})\n}\n"],
|
|
5
|
-
"mappings": "
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["import * as React from 'react'\nimport { TLWheelEventInfo } from '../editor/types/event-types'\nimport { tlenv } from '../globals/environment'\nimport { Vec } from '../primitives/Vec'\nimport { preventDefault } from '../utils/dom'\nimport { isAccelKey } from '../utils/keyboard'\nimport { normalizeWheel } from '../utils/normalizeWheel'\nimport { useEditor } from './useEditor'\n\n/*\n\n# How does pinching work?\n\nThe pinching handler is fired under two circumstances:\n- when a user is on a MacBook trackpad and is ZOOMING with a two-finger pinch\n- when a user is on a touch device and is ZOOMING with a two-finger pinch\n- when a user is on a touch device and is PANNING with two fingers\n\nZooming is much more expensive than panning (because it causes shapes to render),\nso we want to be sure that we don't zoom while two-finger panning.\n\nIn order to do this, we keep track of a \"pinchState\", which is either:\n- \"zooming\"\n- \"panning\"\n- \"not sure\"\n\nIf a user is on a trackpad, the pinchState will be set to \"zooming\".\n\nIf the user is on a touch screen, then we start in the \"not sure\" state and switch back and forth\nbetween \"zooming\", \"panning\", and \"not sure\" based on what the user is doing with their fingers.\n\nIn the \"not sure\" state, we examine whether the user has moved the center of the gesture far enough\nto suggest that they're panning; or else that they've moved their fingers further apart or closer\ntogether enough to suggest that they're zooming.\n\nIn the \"panning\" state, we check whether the user's fingers have moved far enough apart to suggest\nthat they're zooming. If they have, we switch to the \"zooming\" state.\n\nIn the \"zooming\" state, we just stay zooming\u2014it's not YET possible to switch back to panning.\n\ntodo: compare velocities of change in order to determine whether the user has switched back to panning\n*/\n\n/** Safari's non-standard GestureEvent */\ninterface GestureEvent extends Event {\n\tscale: number\n\trotation: number\n\tclientX: number\n\tclientY: number\n\tshiftKey: boolean\n\taltKey: boolean\n\tmetaKey: boolean\n\tctrlKey: boolean\n}\n\nexport function useGestureEvents(ref: React.RefObject<HTMLDivElement | null>) {\n\tconst editor = useEditor()\n\n\tReact.useEffect(() => {\n\t\tconst elm = ref.current\n\t\tif (!elm) return\n\n\t\tlet pinchState = 'not sure' as 'not sure' | 'zooming' | 'panning'\n\n\t\t// --- Wheel handling ---\n\n\t\tfunction onWheel(event: WheelEvent) {\n\t\t\tif (!editor.getInstanceState().isFocused) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tpinchState = 'not sure'\n\n\t\t\t// Don't handle wheel events over a scrollable editing shape\n\t\t\tconst editingShapeId = editor.getEditingShapeId()\n\t\t\tif (editingShapeId) {\n\t\t\t\tconst shape = editor.getShape(editingShapeId)\n\t\t\t\tif (shape) {\n\t\t\t\t\tconst util = editor.getShapeUtil(shape)\n\t\t\t\t\tif (util.canScroll(shape)) {\n\t\t\t\t\t\tconst bounds = editor.getShapePageBounds(editingShapeId)\n\t\t\t\t\t\tif (bounds?.containsPoint(editor.inputs.getCurrentPagePoint())) {\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tpreventDefault(event)\n\t\t\tevent.stopPropagation()\n\t\t\tconst delta = normalizeWheel(event)\n\n\t\t\tif (delta.x === 0 && delta.y === 0) return\n\n\t\t\tconst info: TLWheelEventInfo = {\n\t\t\t\ttype: 'wheel',\n\t\t\t\tname: 'wheel',\n\t\t\t\tdelta,\n\t\t\t\tpoint: new Vec(event.clientX, event.clientY),\n\t\t\t\tshiftKey: event.shiftKey,\n\t\t\t\taltKey: event.altKey,\n\t\t\t\tctrlKey: event.metaKey || event.ctrlKey,\n\t\t\t\tmetaKey: event.metaKey,\n\t\t\t\taccelKey: isAccelKey(event),\n\t\t\t}\n\n\t\t\teditor.dispatch(info)\n\t\t}\n\n\t\t// --- Touch pinch handling ---\n\n\t\tlet initDistanceBetweenFingers = 1 // the distance between the two fingers when the pinch starts\n\t\tlet initZoom = 1 // the zoom level when the pinch starts\n\t\tlet currDistanceBetweenFingers = 0\n\t\tconst initPointBetweenFingers = new Vec()\n\t\tconst prevPointBetweenFingers = new Vec()\n\n\t\t// Track active touches\n\t\tlet activeTouches: Touch[] = []\n\n\t\tfunction getScaleBounds() {\n\t\t\tconst baseZoom = editor.getBaseZoom()\n\t\t\tconst { zoomSteps, zoomSpeed } = editor.getCameraOptions()\n\t\t\tconst zoomMin = zoomSteps[0] * baseZoom\n\t\t\tconst zoomMax = zoomSteps[zoomSteps.length - 1] * baseZoom\n\t\t\treturn {\n\t\t\t\tmin: zoomMin ** (1 / zoomSpeed),\n\t\t\t\tmax: zoomMax ** (1 / zoomSpeed),\n\t\t\t}\n\t\t}\n\n\t\tfunction getScaleFrom() {\n\t\t\tconst { zoomSpeed } = editor.getCameraOptions()\n\t\t\treturn editor.getZoomLevel() ** (1 / zoomSpeed)\n\t\t}\n\n\t\t// Accumulated scale offset, clamped to bounds \u2014 replaces @use-gesture's offset[0]\n\t\tlet scaleOffset = 1\n\t\tlet initScaleFrom = 1 // the scale-space zoom level when the pinch started\n\n\t\tfunction updatePinchState(isSafariTrackpadPinch: boolean) {\n\t\t\tif (isSafariTrackpadPinch) {\n\t\t\t\tpinchState = 'zooming'\n\t\t\t}\n\n\t\t\tif (pinchState === 'zooming') {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Initial: [touch]-------origin-------[touch]\n\t\t\t// Current: [touch]-----------origin----------[touch]\n\t\t\t// |----| |------------|\n\t\t\t// originDistance ^ ^ touchDistance\n\n\t\t\t// How far have the two touch points moved towards or away from each other?\n\t\t\tconst touchDistance = Math.abs(currDistanceBetweenFingers - initDistanceBetweenFingers)\n\t\t\t// How far has the point between the touches moved?\n\t\t\tconst originDistance = Vec.Dist(initPointBetweenFingers, prevPointBetweenFingers)\n\n\t\t\tswitch (pinchState) {\n\t\t\t\tcase 'not sure': {\n\t\t\t\t\tif (touchDistance > 24) {\n\t\t\t\t\t\tpinchState = 'zooming'\n\t\t\t\t\t} else if (originDistance > 16) {\n\t\t\t\t\t\tpinchState = 'panning'\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase 'panning': {\n\t\t\t\t\t// Slightly more touch distance needed to go from panning to zooming\n\t\t\t\t\tif (touchDistance > 64) {\n\t\t\t\t\t\tpinchState = 'zooming'\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfunction dispatchPinchEvent(\n\t\t\tname: 'pinch_start' | 'pinch' | 'pinch_end',\n\t\t\torigin: { x: number; y: number },\n\t\t\tdelta: { x: number; y: number },\n\t\t\tzoom: number,\n\t\t\tevent: TouchEvent | GestureEvent\n\t\t) {\n\t\t\teditor.dispatch({\n\t\t\t\ttype: 'pinch',\n\t\t\t\tname,\n\t\t\t\tpoint: { x: origin.x, y: origin.y, z: zoom },\n\t\t\t\tdelta,\n\t\t\t\tshiftKey: event.shiftKey,\n\t\t\t\taltKey: event.altKey,\n\t\t\t\tctrlKey: event.metaKey || event.ctrlKey,\n\t\t\t\tmetaKey: event.metaKey,\n\t\t\t\taccelKey: isAccelKey(event),\n\t\t\t})\n\t\t}\n\n\t\tfunction getOriginAndDistance(t0: Touch, t1: Touch) {\n\t\t\tconst origin = {\n\t\t\t\tx: (t0.clientX + t1.clientX) / 2,\n\t\t\t\ty: (t0.clientY + t1.clientY) / 2,\n\t\t\t}\n\t\t\tconst distance = Math.hypot(t1.clientX - t0.clientX, t1.clientY - t0.clientY)\n\t\t\treturn { origin, distance }\n\t\t}\n\n\t\tfunction onTouchStart(event: TouchEvent) {\n\t\t\tif (!(event.target === elm || elm?.contains(event.target as Node))) return\n\n\t\t\tactiveTouches = Array.from(event.touches)\n\n\t\t\tif (activeTouches.length === 2) {\n\t\t\t\t// Two fingers down \u2014 start pinch\n\t\t\t\tpinchState = 'not sure'\n\t\t\t\tconst { origin, distance } = getOriginAndDistance(activeTouches[0], activeTouches[1])\n\n\t\t\t\tprevPointBetweenFingers.x = origin.x\n\t\t\t\tprevPointBetweenFingers.y = origin.y\n\t\t\t\tinitPointBetweenFingers.x = origin.x\n\t\t\t\tinitPointBetweenFingers.y = origin.y\n\t\t\t\tinitDistanceBetweenFingers = Math.max(distance, 1)\n\t\t\t\tcurrDistanceBetweenFingers = distance\n\t\t\t\tinitZoom = editor.getZoomLevel()\n\t\t\t\tinitScaleFrom = getScaleFrom()\n\t\t\t\tscaleOffset = initScaleFrom\n\n\t\t\t\tdispatchPinchEvent('pinch_start', origin, { x: 0, y: 0 }, editor.getZoomLevel(), event)\n\t\t\t}\n\t\t}\n\n\t\tfunction onTouchMove(event: TouchEvent) {\n\t\t\tactiveTouches = Array.from(event.touches)\n\n\t\t\tif (activeTouches.length < 2) return\n\n\t\t\tconst { origin, distance } = getOriginAndDistance(activeTouches[0], activeTouches[1])\n\t\t\tcurrDistanceBetweenFingers = distance\n\n\t\t\tconst dx = origin.x - prevPointBetweenFingers.x\n\t\t\tconst dy = origin.y - prevPointBetweenFingers.y\n\n\t\t\tprevPointBetweenFingers.x = origin.x\n\t\t\tprevPointBetweenFingers.y = origin.y\n\n\t\t\tupdatePinchState(false)\n\n\t\t\t// Only update the zoom if the pointers are far enough apart;\n\t\t\t// a very small touchDistance means that the user has probably\n\t\t\t// pinched out and their fingers are touching; this produces\n\t\t\t// very unstable zooming behavior.\n\t\t\tconst bounds = getScaleBounds()\n\t\t\tconst rawScale = initScaleFrom * (distance / initDistanceBetweenFingers)\n\t\t\tscaleOffset = Math.min(bounds.max, Math.max(bounds.min, rawScale))\n\n\t\t\tswitch (pinchState) {\n\t\t\t\tcase 'zooming': {\n\t\t\t\t\tconst currZoom = scaleOffset ** editor.getCameraOptions().zoomSpeed\n\t\t\t\t\tdispatchPinchEvent('pinch', origin, { x: dx, y: dy }, currZoom, event)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase 'panning': {\n\t\t\t\t\tdispatchPinchEvent('pinch', origin, { x: dx, y: dy }, initZoom, event)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfunction onTouchEnd(event: TouchEvent) {\n\t\t\tconst wasPinching = activeTouches.length >= 2\n\t\t\tactiveTouches = Array.from(event.touches)\n\n\t\t\tif (wasPinching && activeTouches.length < 2) {\n\t\t\t\t// Pinch ended\n\t\t\t\tconst scale = scaleOffset ** editor.getCameraOptions().zoomSpeed\n\t\t\t\tconst origin = { ...prevPointBetweenFingers }\n\t\t\t\tpinchState = 'not sure'\n\n\t\t\t\teditor.timers.requestAnimationFrame(() => {\n\t\t\t\t\tdispatchPinchEvent('pinch_end', origin, { x: origin.x, y: origin.y }, scale, event)\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\t// --- Safari trackpad pinch (GestureEvent) ---\n\n\t\tlet safariGestureInitialScale = 1\n\n\t\tfunction onGestureStart(event: Event) {\n\t\t\tconst e = event as GestureEvent\n\t\t\tif (!(e.target === elm || elm?.contains(e.target as Node))) return\n\n\t\t\tpreventDefault(e)\n\t\t\te.stopPropagation()\n\n\t\t\tpinchState = 'not sure'\n\t\t\tsafariGestureInitialScale = getScaleFrom()\n\t\t\tscaleOffset = safariGestureInitialScale\n\t\t\tinitZoom = editor.getZoomLevel()\n\n\t\t\tprevPointBetweenFingers.x = e.clientX\n\t\t\tprevPointBetweenFingers.y = e.clientY\n\t\t\tinitPointBetweenFingers.x = e.clientX\n\t\t\tinitPointBetweenFingers.y = e.clientY\n\t\t\tinitDistanceBetweenFingers = 1\n\t\t\tcurrDistanceBetweenFingers = 1\n\n\t\t\tdispatchPinchEvent(\n\t\t\t\t'pinch_start',\n\t\t\t\t{ x: e.clientX, y: e.clientY },\n\t\t\t\t{ x: 0, y: 0 },\n\t\t\t\teditor.getZoomLevel(),\n\t\t\t\te\n\t\t\t)\n\t\t}\n\n\t\tfunction onGestureChange(event: Event) {\n\t\t\tconst e = event as GestureEvent\n\t\t\tif (!(e.target === elm || elm?.contains(e.target as Node))) return\n\n\t\t\tpreventDefault(e)\n\t\t\te.stopPropagation()\n\n\t\t\tconst dx = e.clientX - prevPointBetweenFingers.x\n\t\t\tconst dy = e.clientY - prevPointBetweenFingers.y\n\n\t\t\tprevPointBetweenFingers.x = e.clientX\n\t\t\tprevPointBetweenFingers.y = e.clientY\n\n\t\t\t// Safari GestureEvent.scale is a multiplier relative to gesture start\n\t\t\tconst bounds = getScaleBounds()\n\t\t\tconst rawScale = safariGestureInitialScale * e.scale\n\t\t\tscaleOffset = Math.min(bounds.max, Math.max(bounds.min, rawScale))\n\n\t\t\t// Update distance tracking for pinch state (treat scale change as distance change)\n\t\t\tcurrDistanceBetweenFingers = e.scale * initDistanceBetweenFingers\n\n\t\t\tupdatePinchState(true)\n\n\t\t\tconst currZoom = scaleOffset ** editor.getCameraOptions().zoomSpeed\n\n\t\t\tdispatchPinchEvent('pinch', { x: e.clientX, y: e.clientY }, { x: dx, y: dy }, currZoom, e)\n\t\t}\n\n\t\tfunction onGestureEnd(event: Event) {\n\t\t\tconst e = event as GestureEvent\n\t\t\tif (!(e.target === elm || elm?.contains(e.target as Node))) return\n\n\t\t\tpreventDefault(e)\n\t\t\te.stopPropagation()\n\n\t\t\tconst scale = scaleOffset ** editor.getCameraOptions().zoomSpeed\n\t\t\tpinchState = 'not sure'\n\n\t\t\teditor.timers.requestAnimationFrame(() => {\n\t\t\t\tdispatchPinchEvent(\n\t\t\t\t\t'pinch_end',\n\t\t\t\t\t{ x: e.clientX, y: e.clientY },\n\t\t\t\t\t{ x: e.clientX, y: e.clientY },\n\t\t\t\t\tscale,\n\t\t\t\t\te\n\t\t\t\t)\n\t\t\t})\n\t\t}\n\n\t\t// --- Attach event listeners ---\n\n\t\telm.addEventListener('wheel', onWheel, { passive: false })\n\n\t\t// On touch devices (iOS), use pointer events for pinch.\n\t\t// On non-touch Safari (macOS trackpad), use GestureEvent.\n\t\t// Never use both simultaneously \u2014 on iOS Safari, both event types fire\n\t\t// for the same pinch gesture, causing conflicting state updates.\n\t\tconst useGestureEvents = !tlenv.isIos && 'GestureEvent' in window\n\n\t\tif (useGestureEvents) {\n\t\t\telm.addEventListener('gesturestart', onGestureStart)\n\t\t\telm.addEventListener('gesturechange', onGestureChange)\n\t\t\telm.addEventListener('gestureend', onGestureEnd)\n\t\t} else {\n\t\t\telm.addEventListener('touchstart', onTouchStart)\n\t\t\telm.addEventListener('touchmove', onTouchMove)\n\t\t\telm.addEventListener('touchend', onTouchEnd)\n\t\t\telm.addEventListener('touchcancel', onTouchEnd)\n\t\t}\n\n\t\treturn () => {\n\t\t\telm.removeEventListener('wheel', onWheel)\n\t\t\tif (useGestureEvents) {\n\t\t\t\telm.removeEventListener('gesturestart', onGestureStart)\n\t\t\t\telm.removeEventListener('gesturechange', onGestureChange)\n\t\t\t\telm.removeEventListener('gestureend', onGestureEnd)\n\t\t\t} else {\n\t\t\t\telm.removeEventListener('touchstart', onTouchStart)\n\t\t\t\telm.removeEventListener('touchmove', onTouchMove)\n\t\t\t\telm.removeEventListener('touchend', onTouchEnd)\n\t\t\t\telm.removeEventListener('touchcancel', onTouchEnd)\n\t\t\t}\n\t\t}\n\t}, [editor, ref])\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["useGestureEvents"]
|
|
7
7
|
}
|
package/dist-esm/version.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
const version = "4.6.0-next.
|
|
1
|
+
const version = "4.6.0-next.4dde09fa17ab";
|
|
2
2
|
const publishDates = {
|
|
3
3
|
major: "2025-09-18T14:39:22.803Z",
|
|
4
|
-
minor: "2026-
|
|
5
|
-
patch: "2026-
|
|
4
|
+
minor: "2026-04-02T05:32:20.023Z",
|
|
5
|
+
patch: "2026-04-02T05:32:20.023Z"
|
|
6
6
|
};
|
|
7
7
|
export {
|
|
8
8
|
publishDates,
|
package/dist-esm/version.mjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/version.ts"],
|
|
4
|
-
"sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '4.6.0-next.
|
|
4
|
+
"sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '4.6.0-next.4dde09fa17ab'\nexport const publishDates = {\n\tmajor: '2025-09-18T14:39:22.803Z',\n\tminor: '2026-04-02T05:32:20.023Z',\n\tpatch: '2026-04-02T05:32:20.023Z',\n}\n"],
|
|
5
5
|
"mappings": "AAGO,MAAM,UAAU;AAChB,MAAM,eAAe;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACR;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/editor.css
CHANGED
|
@@ -1478,6 +1478,19 @@ input,
|
|
|
1478
1478
|
color: currentColor;
|
|
1479
1479
|
}
|
|
1480
1480
|
|
|
1481
|
+
.tl-note__attribution {
|
|
1482
|
+
position: absolute;
|
|
1483
|
+
bottom: calc(4px * var(--note-attribution-scale, 1));
|
|
1484
|
+
right: calc(8px * var(--note-attribution-scale, 1));
|
|
1485
|
+
font-family: var(--tl-font-sans);
|
|
1486
|
+
pointer-events: auto;
|
|
1487
|
+
white-space: nowrap;
|
|
1488
|
+
overflow: hidden;
|
|
1489
|
+
text-overflow: ellipsis;
|
|
1490
|
+
max-width: 60%;
|
|
1491
|
+
z-index: 1;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1481
1494
|
/* ------------------- Frame shape ------------------- */
|
|
1482
1495
|
|
|
1483
1496
|
.tl-frame__body {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tldraw/editor",
|
|
3
3
|
"description": "tldraw infinite canvas SDK (editor).",
|
|
4
|
-
"version": "4.6.0-next.
|
|
4
|
+
"version": "4.6.0-next.4dde09fa17ab",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -43,19 +43,18 @@
|
|
|
43
43
|
"prepack": "yarn run -T tsx ../../internal/scripts/prepack.ts",
|
|
44
44
|
"postpack": "../../internal/scripts/postpack.sh",
|
|
45
45
|
"pack-tarball": "yarn pack",
|
|
46
|
-
"lint": "
|
|
46
|
+
"lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@tiptap/core": "^3.12.1",
|
|
50
50
|
"@tiptap/pm": "^3.12.1",
|
|
51
51
|
"@tiptap/react": "^3.12.1",
|
|
52
|
-
"@tldraw/state": "4.6.0-next.
|
|
53
|
-
"@tldraw/state-react": "4.6.0-next.
|
|
54
|
-
"@tldraw/store": "4.6.0-next.
|
|
55
|
-
"@tldraw/tlschema": "4.6.0-next.
|
|
56
|
-
"@tldraw/utils": "4.6.0-next.
|
|
57
|
-
"@tldraw/validate": "4.6.0-next.
|
|
58
|
-
"@use-gesture/react": "^10.3.1",
|
|
52
|
+
"@tldraw/state": "4.6.0-next.4dde09fa17ab",
|
|
53
|
+
"@tldraw/state-react": "4.6.0-next.4dde09fa17ab",
|
|
54
|
+
"@tldraw/store": "4.6.0-next.4dde09fa17ab",
|
|
55
|
+
"@tldraw/tlschema": "4.6.0-next.4dde09fa17ab",
|
|
56
|
+
"@tldraw/utils": "4.6.0-next.4dde09fa17ab",
|
|
57
|
+
"@tldraw/validate": "4.6.0-next.4dde09fa17ab",
|
|
59
58
|
"classnames": "^2.5.1",
|
|
60
59
|
"eventemitter3": "^4.0.7",
|
|
61
60
|
"idb": "^7.1.1",
|
package/src/index.ts
CHANGED
|
@@ -80,16 +80,21 @@ export {
|
|
|
80
80
|
export { HTMLContainer, type HTMLContainerProps } from './lib/components/HTMLContainer'
|
|
81
81
|
export { MenuClickCapture } from './lib/components/MenuClickCapture'
|
|
82
82
|
export { SVGContainer, type SVGContainerProps } from './lib/components/SVGContainer'
|
|
83
|
+
export {
|
|
84
|
+
createTLCurrentUser,
|
|
85
|
+
useTldrawCurrentUser,
|
|
86
|
+
type TLCurrentUser,
|
|
87
|
+
} from './lib/config/createTLCurrentUser'
|
|
83
88
|
export {
|
|
84
89
|
createTLSchemaFromUtils,
|
|
85
90
|
createTLStore,
|
|
91
|
+
defaultUserStore,
|
|
86
92
|
inlineBase64AssetStore,
|
|
87
93
|
type TLStoreBaseOptions,
|
|
88
94
|
type TLStoreEventInfo,
|
|
89
95
|
type TLStoreOptions,
|
|
90
96
|
type TLStoreSchemaOptions,
|
|
91
97
|
} from './lib/config/createTLStore'
|
|
92
|
-
export { createTLUser, useTldrawUser, type TLUser } from './lib/config/createTLUser'
|
|
93
98
|
export { type TLAnyBindingUtilConstructor } from './lib/config/defaultBindings'
|
|
94
99
|
export { coreShapes, type TLAnyShapeUtilConstructor } from './lib/config/defaultShapes'
|
|
95
100
|
export {
|
package/src/lib/TldrawEditor.tsx
CHANGED
|
@@ -16,8 +16,8 @@ import React, {
|
|
|
16
16
|
import { version } from '../version'
|
|
17
17
|
import { DefaultErrorFallback } from './components/default-components/DefaultErrorFallback'
|
|
18
18
|
import { OptionalErrorBoundary } from './components/ErrorBoundary'
|
|
19
|
+
import { createTLCurrentUser, TLCurrentUser } from './config/createTLCurrentUser'
|
|
19
20
|
import { TLStoreBaseOptions } from './config/createTLStore'
|
|
20
|
-
import { createTLUser, TLUser } from './config/createTLUser'
|
|
21
21
|
import { TLAnyBindingUtilConstructor } from './config/defaultBindings'
|
|
22
22
|
import { TLAnyShapeUtilConstructor } from './config/defaultShapes'
|
|
23
23
|
import { TLEditorSnapshot } from './config/TLEditorSnapshot'
|
|
@@ -154,7 +154,7 @@ export interface TldrawEditorBaseProps {
|
|
|
154
154
|
/**
|
|
155
155
|
* The user interacting with the editor.
|
|
156
156
|
*/
|
|
157
|
-
user?:
|
|
157
|
+
user?: TLCurrentUser
|
|
158
158
|
|
|
159
159
|
/**
|
|
160
160
|
* Whether to infer dark mode from the user's OS. Defaults to false.
|
|
@@ -260,7 +260,7 @@ export const TldrawEditor = memo(function TldrawEditor({
|
|
|
260
260
|
...rest
|
|
261
261
|
}: TldrawEditorProps) {
|
|
262
262
|
const [container, setContainer] = useState<HTMLElement | null>(null)
|
|
263
|
-
const user = useMemo(() => _user ??
|
|
263
|
+
const user = useMemo(() => _user ?? createTLCurrentUser(), [_user])
|
|
264
264
|
|
|
265
265
|
const ErrorFallback =
|
|
266
266
|
components?.ErrorFallback === undefined ? DefaultErrorFallback : components?.ErrorFallback
|
|
@@ -331,7 +331,7 @@ export const TldrawEditor = memo(function TldrawEditor({
|
|
|
331
331
|
|
|
332
332
|
function TldrawEditorWithOwnStore(
|
|
333
333
|
props: Required<
|
|
334
|
-
TldrawEditorProps & { store: undefined; user:
|
|
334
|
+
TldrawEditorProps & { store: undefined; user: TLCurrentUser },
|
|
335
335
|
'shapeUtils' | 'bindingUtils' | 'tools'
|
|
336
336
|
>
|
|
337
337
|
) {
|
|
@@ -345,6 +345,7 @@ function TldrawEditorWithOwnStore(
|
|
|
345
345
|
sessionId,
|
|
346
346
|
user,
|
|
347
347
|
assets,
|
|
348
|
+
users,
|
|
348
349
|
migrations,
|
|
349
350
|
} = props
|
|
350
351
|
|
|
@@ -357,6 +358,7 @@ function TldrawEditorWithOwnStore(
|
|
|
357
358
|
defaultName,
|
|
358
359
|
snapshot,
|
|
359
360
|
assets,
|
|
361
|
+
users,
|
|
360
362
|
migrations,
|
|
361
363
|
})
|
|
362
364
|
|
|
@@ -368,7 +370,7 @@ const TldrawEditorWithLoadingStore = memo(function TldrawEditorBeforeLoading({
|
|
|
368
370
|
user,
|
|
369
371
|
...rest
|
|
370
372
|
}: Required<
|
|
371
|
-
TldrawEditorProps & { store: TLStoreWithStatus; user:
|
|
373
|
+
TldrawEditorProps & { store: TLStoreWithStatus; user: TLCurrentUser },
|
|
372
374
|
'shapeUtils' | 'bindingUtils' | 'tools'
|
|
373
375
|
>) {
|
|
374
376
|
const container = useContainer()
|
|
@@ -428,7 +430,7 @@ function TldrawEditorWithReadyStore({
|
|
|
428
430
|
}: Required<
|
|
429
431
|
TldrawEditorProps & {
|
|
430
432
|
store: TLStore
|
|
431
|
-
user:
|
|
433
|
+
user: TLCurrentUser
|
|
432
434
|
},
|
|
433
435
|
'shapeUtils' | 'bindingUtils' | 'tools'
|
|
434
436
|
>) {
|
|
@@ -5,7 +5,7 @@ import { useShallowObjectIdentity } from '../hooks/useIdentity'
|
|
|
5
5
|
import { TLUserPreferences, getUserPreferences, setUserPreferences } from './TLUserPreferences'
|
|
6
6
|
|
|
7
7
|
/** @public */
|
|
8
|
-
export interface
|
|
8
|
+
export interface TLCurrentUser {
|
|
9
9
|
readonly userPreferences: Signal<TLUserPreferences>
|
|
10
10
|
// eslint-disable-next-line tldraw/method-signature-style
|
|
11
11
|
readonly setUserPreferences: (userPreferences: TLUserPreferences) => void
|
|
@@ -16,13 +16,13 @@ const defaultLocalStorageUserPrefs = computed('defaultLocalStorageUserPrefs', ()
|
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
/** @public */
|
|
19
|
-
export function
|
|
19
|
+
export function createTLCurrentUser(
|
|
20
20
|
opts = {} as {
|
|
21
21
|
userPreferences?: Signal<TLUserPreferences>
|
|
22
22
|
// eslint-disable-next-line tldraw/method-signature-style
|
|
23
23
|
setUserPreferences?: (userPreferences: TLUserPreferences) => void
|
|
24
24
|
}
|
|
25
|
-
):
|
|
25
|
+
): TLCurrentUser {
|
|
26
26
|
return {
|
|
27
27
|
userPreferences: opts.userPreferences ?? defaultLocalStorageUserPrefs,
|
|
28
28
|
setUserPreferences: opts.setUserPreferences ?? setUserPreferences,
|
|
@@ -32,11 +32,11 @@ export function createTLUser(
|
|
|
32
32
|
/**
|
|
33
33
|
* @public
|
|
34
34
|
*/
|
|
35
|
-
export function
|
|
35
|
+
export function useTldrawCurrentUser(opts: {
|
|
36
36
|
userPreferences?: Signal<TLUserPreferences> | TLUserPreferences
|
|
37
37
|
// eslint-disable-next-line tldraw/method-signature-style
|
|
38
38
|
setUserPreferences?: (userPreferences: TLUserPreferences) => void
|
|
39
|
-
}):
|
|
39
|
+
}): TLCurrentUser {
|
|
40
40
|
const prefs = useShallowObjectIdentity(opts.userPreferences ?? defaultLocalStorageUserPrefs)
|
|
41
41
|
const userAtom = useAtom<TLUserPreferences | Signal<TLUserPreferences>>('userAtom', prefs)
|
|
42
42
|
useEffect(() => {
|
|
@@ -45,7 +45,7 @@ export function useTldrawUser(opts: {
|
|
|
45
45
|
|
|
46
46
|
return useMemo(
|
|
47
47
|
() =>
|
|
48
|
-
|
|
48
|
+
createTLCurrentUser({
|
|
49
49
|
userPreferences: computed('userPreferences', () => {
|
|
50
50
|
const userStuff = userAtom.get()
|
|
51
51
|
return isSignal(userStuff) ? userStuff.get() : userStuff
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Signal } from '@tldraw/state'
|
|
1
|
+
import { Signal, computed } from '@tldraw/state'
|
|
2
2
|
import { HistoryEntry, MigrationSequence, SerializedStore, Store, StoreSchema } from '@tldraw/store'
|
|
3
3
|
import {
|
|
4
4
|
CustomRecordInfo,
|
|
@@ -8,13 +8,19 @@ import {
|
|
|
8
8
|
TLStore,
|
|
9
9
|
TLStoreProps,
|
|
10
10
|
TLStoreSnapshot,
|
|
11
|
+
TLUser,
|
|
12
|
+
TLUserStore,
|
|
13
|
+
UserRecordType,
|
|
14
|
+
createCachedUserResolve,
|
|
11
15
|
createTLSchema,
|
|
16
|
+
createUserId,
|
|
12
17
|
} from '@tldraw/tlschema'
|
|
13
18
|
import { FileHelpers, assert } from '@tldraw/utils'
|
|
14
19
|
import { Editor } from '../editor/Editor'
|
|
15
20
|
import { TLAnyBindingUtilConstructor, checkBindings } from './defaultBindings'
|
|
16
21
|
import { TLAnyShapeUtilConstructor, checkShapesAndAddCore } from './defaultShapes'
|
|
17
22
|
import { TLEditorSnapshot, loadSnapshot } from './TLEditorSnapshot'
|
|
23
|
+
import { defaultUserPreferences, getUserPreferences } from './TLUserPreferences'
|
|
18
24
|
|
|
19
25
|
/** @public */
|
|
20
26
|
export interface TLStoreBaseOptions {
|
|
@@ -30,6 +36,9 @@ export interface TLStoreBaseOptions {
|
|
|
30
36
|
/** How should this store upload & resolve assets? */
|
|
31
37
|
assets?: TLAssetStore
|
|
32
38
|
|
|
39
|
+
/** How should this store resolve users for attribution? */
|
|
40
|
+
users?: TLUserStore
|
|
41
|
+
|
|
33
42
|
/** Called when the store is connected to an {@link @tldraw/editor#Editor}. */
|
|
34
43
|
onMount?(editor: Editor): void | (() => void)
|
|
35
44
|
}
|
|
@@ -61,6 +70,21 @@ export type TLStoreEventInfo = HistoryEntry<TLRecord>
|
|
|
61
70
|
|
|
62
71
|
const defaultAssetResolve: NonNullable<TLAssetStore['resolve']> = (asset) => asset.props.src
|
|
63
72
|
|
|
73
|
+
const _defaultCurrentUser: Signal<TLUser | null> = computed('defaultCurrentUser', () => {
|
|
74
|
+
const prefs = getUserPreferences()
|
|
75
|
+
if (!prefs.id) return null
|
|
76
|
+
return UserRecordType.create({
|
|
77
|
+
id: createUserId(prefs.id),
|
|
78
|
+
name: prefs.name ?? '',
|
|
79
|
+
color: prefs.color ?? defaultUserPreferences.color,
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
/** @public */
|
|
84
|
+
export const defaultUserStore: TLUserStore = {
|
|
85
|
+
currentUser: _defaultCurrentUser,
|
|
86
|
+
}
|
|
87
|
+
|
|
64
88
|
/** @public */
|
|
65
89
|
export const inlineBase64AssetStore: TLAssetStore = {
|
|
66
90
|
upload: async (_, file) => {
|
|
@@ -107,6 +131,7 @@ export function createTLStore({
|
|
|
107
131
|
defaultName = '',
|
|
108
132
|
id,
|
|
109
133
|
assets = inlineBase64AssetStore,
|
|
134
|
+
users = defaultUserStore,
|
|
110
135
|
onMount,
|
|
111
136
|
collaboration,
|
|
112
137
|
...rest
|
|
@@ -124,6 +149,15 @@ export function createTLStore({
|
|
|
124
149
|
resolve: assets.resolve ?? defaultAssetResolve,
|
|
125
150
|
remove: assets.remove ?? (() => Promise.resolve()),
|
|
126
151
|
},
|
|
152
|
+
users: {
|
|
153
|
+
currentUser: users.currentUser,
|
|
154
|
+
resolve:
|
|
155
|
+
users.resolve ??
|
|
156
|
+
createCachedUserResolve((userId) => {
|
|
157
|
+
const current = users.currentUser.get()
|
|
158
|
+
return current && current.id === createUserId(userId) ? current : null
|
|
159
|
+
}),
|
|
160
|
+
},
|
|
127
161
|
onMount: (editor) => {
|
|
128
162
|
assert(editor instanceof Editor)
|
|
129
163
|
onMount?.(editor)
|
package/src/lib/editor/Editor.ts
CHANGED
|
@@ -50,9 +50,13 @@ import {
|
|
|
50
50
|
TLShapePartial,
|
|
51
51
|
TLStore,
|
|
52
52
|
TLStoreSnapshot,
|
|
53
|
+
TLUser,
|
|
54
|
+
TLUserId,
|
|
53
55
|
TLVideoAsset,
|
|
56
|
+
UserRecordType,
|
|
54
57
|
createBindingId,
|
|
55
58
|
createShapeId,
|
|
59
|
+
createUserId,
|
|
56
60
|
getShapePropKeysByStyle,
|
|
57
61
|
isPageId,
|
|
58
62
|
isShapeId,
|
|
@@ -90,7 +94,7 @@ import {
|
|
|
90
94
|
uniqueId,
|
|
91
95
|
} from '@tldraw/utils'
|
|
92
96
|
import EventEmitter from 'eventemitter3'
|
|
93
|
-
import {
|
|
97
|
+
import { TLCurrentUser, createTLCurrentUser } from '../config/createTLCurrentUser'
|
|
94
98
|
import { TLAnyBindingUtilConstructor, checkBindings } from '../config/defaultBindings'
|
|
95
99
|
import { TLAnyShapeUtilConstructor, checkShapesAndAddCore } from '../config/defaultShapes'
|
|
96
100
|
import {
|
|
@@ -221,7 +225,7 @@ export interface TLEditorOptions {
|
|
|
221
225
|
/**
|
|
222
226
|
* A user defined externally to replace the default user.
|
|
223
227
|
*/
|
|
224
|
-
user?:
|
|
228
|
+
user?: TLCurrentUser
|
|
225
229
|
/**
|
|
226
230
|
* The editor's initial active tool (or other state node id).
|
|
227
231
|
*/
|
|
@@ -346,7 +350,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
346
350
|
|
|
347
351
|
this._textOptions = atom('text options', options?.text ?? null)
|
|
348
352
|
|
|
349
|
-
this.user = new UserPreferencesManager(user ??
|
|
353
|
+
this.user = new UserPreferencesManager(user ?? createTLCurrentUser(), inferDarkMode ?? false)
|
|
350
354
|
this.disposables.add(() => this.user.dispose())
|
|
351
355
|
|
|
352
356
|
this.getContainer = getContainer
|
|
@@ -357,6 +361,12 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
357
361
|
this.fonts = new FontManager(this, fontAssetUrls)
|
|
358
362
|
|
|
359
363
|
this._tickManager = new TickManager(this)
|
|
364
|
+
this.disposables.add(() => {
|
|
365
|
+
// Reset camera state to 'idle' so the store isn't left stuck at 'moving'
|
|
366
|
+
// when tick events stop (e.g. React strict mode disposes while camera is moving)
|
|
367
|
+
this.off('tick', this._decayCameraStateTimeout)
|
|
368
|
+
this._setCameraState('idle')
|
|
369
|
+
})
|
|
360
370
|
|
|
361
371
|
this.inputs = new InputsManager(this)
|
|
362
372
|
|
|
@@ -820,6 +830,15 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
820
830
|
})
|
|
821
831
|
)
|
|
822
832
|
}
|
|
833
|
+
|
|
834
|
+
this.disposables.add(
|
|
835
|
+
react('sync current user record', () => {
|
|
836
|
+
const user = this.store.props.users.currentUser.get()
|
|
837
|
+
if (user) {
|
|
838
|
+
this._ensureUserRecord(user)
|
|
839
|
+
}
|
|
840
|
+
})
|
|
841
|
+
)
|
|
823
842
|
}
|
|
824
843
|
|
|
825
844
|
private readonly _getShapeVisibility?: TLEditorOptions['getShapeVisibility']
|
|
@@ -3972,6 +3991,94 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3972
3991
|
return this.getCollaborators().filter((c) => c.currentPageId === currentPageId)
|
|
3973
3992
|
}
|
|
3974
3993
|
|
|
3994
|
+
// Attribution
|
|
3995
|
+
|
|
3996
|
+
/**
|
|
3997
|
+
* Get the current user's ID for attribution purposes.
|
|
3998
|
+
* Also ensures a `user:` record exists in the store for the current user.
|
|
3999
|
+
* Returns `null` when the user store has no current user.
|
|
4000
|
+
*
|
|
4001
|
+
* @public
|
|
4002
|
+
*/
|
|
4003
|
+
getAttributionUserId(): string | null {
|
|
4004
|
+
const user = this.store.props.users.currentUser.get()
|
|
4005
|
+
if (!user) return null
|
|
4006
|
+
this._ensureUserRecord(user)
|
|
4007
|
+
return UserRecordType.parseId(user.id)
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
/**
|
|
4011
|
+
* Ensure a user record exists in the store for the given user,
|
|
4012
|
+
* updating it if the data has changed.
|
|
4013
|
+
*
|
|
4014
|
+
* @internal
|
|
4015
|
+
*/
|
|
4016
|
+
_ensureUserRecord(user: TLUser): void {
|
|
4017
|
+
const existing = this.store.get(user.id)
|
|
4018
|
+
if (
|
|
4019
|
+
existing &&
|
|
4020
|
+
existing.name === user.name &&
|
|
4021
|
+
existing.color === user.color &&
|
|
4022
|
+
existing.imageUrl === user.imageUrl &&
|
|
4023
|
+
existing.meta === user.meta
|
|
4024
|
+
) {
|
|
4025
|
+
return
|
|
4026
|
+
}
|
|
4027
|
+
this.run(
|
|
4028
|
+
() => {
|
|
4029
|
+
this.store.put([user])
|
|
4030
|
+
},
|
|
4031
|
+
{ history: 'ignore' }
|
|
4032
|
+
)
|
|
4033
|
+
}
|
|
4034
|
+
|
|
4035
|
+
/**
|
|
4036
|
+
* Resolve a display name for a user ID. Asks the
|
|
4037
|
+
* {@link @tldraw/tlschema#TLUserStore} first (the app's source of truth),
|
|
4038
|
+
* falling back to the `user:` record in the store.
|
|
4039
|
+
*
|
|
4040
|
+
* @public
|
|
4041
|
+
*/
|
|
4042
|
+
getAttributionDisplayName(userId: string | null): string | null {
|
|
4043
|
+
if (!userId) return null
|
|
4044
|
+
return (
|
|
4045
|
+
this.store.props.users.resolve(userId).get()?.name ??
|
|
4046
|
+
this.store.get(createUserId(userId))?.name ??
|
|
4047
|
+
null
|
|
4048
|
+
)
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
/**
|
|
4052
|
+
* Resolve a user record by ID. Asks the
|
|
4053
|
+
* {@link @tldraw/tlschema#TLUserStore} first (the app's source of truth),
|
|
4054
|
+
* falling back to the `user:` record in the store.
|
|
4055
|
+
*
|
|
4056
|
+
* @public
|
|
4057
|
+
*/
|
|
4058
|
+
getAttributionUser(userId: string | null): TLUser | null {
|
|
4059
|
+
if (!userId) return null
|
|
4060
|
+
return (
|
|
4061
|
+
this.store.props.users.resolve(userId).get() ?? this.store.get(createUserId(userId)) ?? null
|
|
4062
|
+
)
|
|
4063
|
+
}
|
|
4064
|
+
|
|
4065
|
+
/**
|
|
4066
|
+
* Collect user IDs referenced by a set of shapes via shape-specific props
|
|
4067
|
+
* (e.g. `textFirstEditedBy` on notes).
|
|
4068
|
+
*
|
|
4069
|
+
* @internal
|
|
4070
|
+
*/
|
|
4071
|
+
_getReferencedUserIds(shapes: TLShape[]): Set<string> {
|
|
4072
|
+
const userIds = new Set<string>()
|
|
4073
|
+
for (const shape of shapes) {
|
|
4074
|
+
const util = this.getShapeUtil(shape)
|
|
4075
|
+
for (const id of util.getReferencedUserIds(shape)) {
|
|
4076
|
+
userIds.add(id)
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
return userIds
|
|
4080
|
+
}
|
|
4081
|
+
|
|
3975
4082
|
// Following
|
|
3976
4083
|
|
|
3977
4084
|
// When we are 'locked on' to a user, our camera is derived from their camera.
|
|
@@ -9157,12 +9264,23 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9157
9264
|
assets.push(asset)
|
|
9158
9265
|
}
|
|
9159
9266
|
|
|
9267
|
+
const users: TLUser[] = []
|
|
9268
|
+
const seenUserIds = new Set<TLUserId>()
|
|
9269
|
+
for (const userId of this._getReferencedUserIds(shapes)) {
|
|
9270
|
+
const recordId = createUserId(userId)
|
|
9271
|
+
if (seenUserIds.has(recordId)) continue
|
|
9272
|
+
seenUserIds.add(recordId)
|
|
9273
|
+
const user = this.store.get(recordId)
|
|
9274
|
+
if (user) users.push(user)
|
|
9275
|
+
}
|
|
9276
|
+
|
|
9160
9277
|
return {
|
|
9161
9278
|
schema: this.store.schema.serialize(),
|
|
9162
9279
|
shapes,
|
|
9163
9280
|
rootShapeIds,
|
|
9164
9281
|
bindings,
|
|
9165
9282
|
assets,
|
|
9283
|
+
users,
|
|
9166
9284
|
}
|
|
9167
9285
|
})
|
|
9168
9286
|
}
|
|
@@ -9238,6 +9356,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9238
9356
|
const assets: TLAsset[] = []
|
|
9239
9357
|
const shapes: TLShape[] = []
|
|
9240
9358
|
const bindings: TLBinding[] = []
|
|
9359
|
+
const users: TLUser[] = []
|
|
9241
9360
|
|
|
9242
9361
|
// Let's treat the content as a store, and then migrate that store.
|
|
9243
9362
|
const store: StoreSnapshot<TLRecord> = {
|
|
@@ -9247,6 +9366,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9247
9366
|
...Object.fromEntries(
|
|
9248
9367
|
content.bindings?.map((bindings) => [bindings.id, bindings] as const) ?? []
|
|
9249
9368
|
),
|
|
9369
|
+
...Object.fromEntries(content.users?.map((user) => [user.id, user] as const) ?? []),
|
|
9250
9370
|
},
|
|
9251
9371
|
schema: content.schema,
|
|
9252
9372
|
}
|
|
@@ -9268,6 +9388,23 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9268
9388
|
bindings.push(record)
|
|
9269
9389
|
break
|
|
9270
9390
|
}
|
|
9391
|
+
case 'user': {
|
|
9392
|
+
users.push(record)
|
|
9393
|
+
break
|
|
9394
|
+
}
|
|
9395
|
+
}
|
|
9396
|
+
}
|
|
9397
|
+
|
|
9398
|
+
if (users.length > 0) {
|
|
9399
|
+
const existingUserIds = new Set(
|
|
9400
|
+
this.store
|
|
9401
|
+
.allRecords()
|
|
9402
|
+
.filter((r): r is TLUser => r.typeName === 'user')
|
|
9403
|
+
.map((r) => r.id)
|
|
9404
|
+
)
|
|
9405
|
+
const usersToCreate = users.filter((u) => !existingUserIds.has(u.id))
|
|
9406
|
+
if (usersToCreate.length > 0) {
|
|
9407
|
+
this.store.put(usersToCreate)
|
|
9271
9408
|
}
|
|
9272
9409
|
}
|
|
9273
9410
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { atom } from '@tldraw/state'
|
|
2
2
|
import { Mocked, vi } from 'vitest'
|
|
3
|
-
import {
|
|
3
|
+
import { TLCurrentUser } from '../../../config/createTLCurrentUser'
|
|
4
4
|
import { TLUserPreferences, defaultUserPreferences } from '../../../config/TLUserPreferences'
|
|
5
5
|
import { UserPreferencesManager } from './UserPreferencesManager'
|
|
6
6
|
|
|
@@ -9,7 +9,7 @@ const mockMatchMedia = vi.fn()
|
|
|
9
9
|
window.matchMedia = mockMatchMedia
|
|
10
10
|
|
|
11
11
|
describe('UserPreferencesManager', () => {
|
|
12
|
-
let mockUser: Mocked<
|
|
12
|
+
let mockUser: Mocked<TLCurrentUser>
|
|
13
13
|
let mockUserPreferences: TLUserPreferences
|
|
14
14
|
let userPreferencesAtom: any
|
|
15
15
|
let userPreferencesManager: UserPreferencesManager
|