@tldraw/editor 5.3.0-next.c9d39e46c83b → 5.3.0-next.d7d8ced023d5
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 +58 -0
- package/dist-cjs/index.js +10 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/components/LiveCollaborators.js +98 -0
- package/dist-cjs/lib/components/LiveCollaborators.js.map +7 -0
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +4 -10
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +3 -3
- package/dist-cjs/lib/components/default-components/DefaultCursor.js +59 -0
- package/dist-cjs/lib/components/default-components/DefaultCursor.js.map +7 -0
- package/dist-cjs/lib/editor/types/external-content.js.map +1 -1
- package/dist-cjs/lib/hooks/EditorComponentsContext.js.map +2 -2
- package/dist-cjs/lib/hooks/useCursor.js +1 -0
- package/dist-cjs/lib/hooks/useCursor.js.map +2 -2
- package/dist-cjs/lib/hooks/useEditorComponents.js +2 -0
- package/dist-cjs/lib/hooks/useEditorComponents.js.map +2 -2
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +25 -1
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useTransform.js +45 -0
- package/dist-cjs/lib/hooks/useTransform.js.map +7 -0
- package/dist-cjs/lib/license/LicenseManager.js +48 -2
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/license/useLicenseManagerState.js +7 -0
- package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
- package/dist-cjs/lib/utils/collaborators.js +27 -0
- package/dist-cjs/lib/utils/collaborators.js.map +7 -0
- package/dist-cjs/lib/utils/getHtmlLayerTransform.js +33 -0
- package/dist-cjs/lib/utils/getHtmlLayerTransform.js.map +7 -0
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +58 -0
- package/dist-esm/index.mjs +13 -2
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/components/LiveCollaborators.mjs +78 -0
- package/dist-esm/lib/components/LiveCollaborators.mjs.map +7 -0
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +5 -11
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCursor.mjs +29 -0
- package/dist-esm/lib/components/default-components/DefaultCursor.mjs.map +7 -0
- package/dist-esm/lib/hooks/EditorComponentsContext.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCursor.mjs +1 -0
- package/dist-esm/lib/hooks/useCursor.mjs.map +2 -2
- package/dist-esm/lib/hooks/useEditorComponents.mjs +2 -0
- package/dist-esm/lib/hooks/useEditorComponents.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +25 -1
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useTransform.mjs +25 -0
- package/dist-esm/lib/hooks/useTransform.mjs.map +7 -0
- package/dist-esm/lib/license/LicenseManager.mjs +49 -3
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/license/useLicenseManagerState.mjs +7 -0
- package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
- package/dist-esm/lib/utils/collaborators.mjs +7 -0
- package/dist-esm/lib/utils/collaborators.mjs.map +7 -0
- package/dist-esm/lib/utils/getHtmlLayerTransform.mjs +13 -0
- package/dist-esm/lib/utils/getHtmlLayerTransform.mjs.map +7 -0
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +37 -4
- package/package.json +7 -7
- package/src/index.ts +9 -1
- package/src/lib/components/LiveCollaborators.tsx +119 -0
- package/src/lib/components/default-components/DefaultCanvas.tsx +5 -17
- package/src/lib/components/default-components/DefaultCursor.tsx +65 -0
- package/src/lib/editor/types/external-content.ts +5 -0
- package/src/lib/hooks/EditorComponentsContext.tsx +2 -0
- package/src/lib/hooks/useCursor.ts +1 -0
- package/src/lib/hooks/useEditorComponents.tsx +2 -0
- package/src/lib/hooks/usePassThroughWheelEvents.ts +44 -2
- package/src/lib/hooks/useTransform.ts +38 -0
- package/src/lib/license/LicenseManager.test.ts +121 -0
- package/src/lib/license/LicenseManager.ts +91 -2
- package/src/lib/license/useLicenseManagerState.ts +17 -1
- package/src/lib/utils/collaborators.test.ts +34 -0
- package/src/lib/utils/collaborators.ts +25 -0
- package/src/lib/utils/getHtmlLayerTransform.ts +22 -0
- package/src/version.ts +3 -3
package/dist-esm/version.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
const version = "5.3.0-next.
|
|
1
|
+
const version = "5.3.0-next.d7d8ced023d5";
|
|
2
2
|
const publishDates = {
|
|
3
3
|
major: "2026-05-06T16:28:18.473Z",
|
|
4
|
-
minor: "2026-
|
|
5
|
-
patch: "2026-
|
|
4
|
+
minor: "2026-08-03T09:27:05.718Z",
|
|
5
|
+
patch: "2026-08-03T09:27:05.718Z"
|
|
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 = '5.3.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 = '5.3.0-next.d7d8ced023d5'\nexport const publishDates = {\n\tmajor: '2026-05-06T16:28:18.473Z',\n\tminor: '2026-08-03T09:27:05.718Z',\n\tpatch: '2026-08-03T09:27:05.718Z',\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
|
@@ -29,15 +29,20 @@
|
|
|
29
29
|
* background, grid, shapes, overlays, and blocker are all children of .tl-canvas,
|
|
30
30
|
* so their z-indices are relative to the canvas, not the container.
|
|
31
31
|
*
|
|
32
|
-
* watermark and canvas-in-front are siblings of .tl-canvas
|
|
33
|
-
* so they compete with the UI layer (.tlui-layout at z-index 300).
|
|
32
|
+
* collaborators, watermark, and canvas-in-front are siblings of .tl-canvas
|
|
33
|
+
* (outside it), so they compete with the UI layer (.tlui-layout at z-index 300).
|
|
34
34
|
* canvas-in-front must be below the UI layer (300) to sit between
|
|
35
|
-
* the canvas and the UI
|
|
35
|
+
* the canvas and the UI; collaborator cursors sit above canvas content
|
|
36
|
+
* (and layers that anchor to it, like comment pins) but below the
|
|
37
|
+
* in-front content and the UI panels. The watermark (a licensing element)
|
|
38
|
+
* sits above that content so it can't be occluded, but still below the
|
|
39
|
+
* in-front content and the UI.
|
|
36
40
|
*/
|
|
37
41
|
--tl-layer-canvas-hidden: -999999;
|
|
38
42
|
--tl-layer-canvas-background: 100;
|
|
39
43
|
--tl-layer-canvas-grid: 150;
|
|
40
|
-
--tl-layer-
|
|
44
|
+
--tl-layer-collaborators: 245;
|
|
45
|
+
--tl-layer-watermark: 248;
|
|
41
46
|
--tl-layer-canvas-in-front: 250;
|
|
42
47
|
--tl-layer-canvas-shapes: 300;
|
|
43
48
|
--tl-layer-canvas-overlays: 500;
|
|
@@ -86,6 +91,10 @@
|
|
|
86
91
|
url("data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: black;'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='1' dy='1' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(0 16 16)' filter='url(%23shadow)'><path d='m25 16h-6.01v-6h-2.98v6h-6.01v3h6.01v6h2.98v-6h6.01z' fill='white'/><path d='m23.9902 17.0103h-6v-6.01h-.98v6.01h-6v.98h6v6.01h.98v-6.01h6z' fill='%23231f1f'/></g></svg>")
|
|
87
92
|
16 16,
|
|
88
93
|
crosshair;
|
|
94
|
+
--tl-cursor-comment:
|
|
95
|
+
url("data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='1' dy='1' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g filter='url(%23shadow)'><path d='M5 19.5 L5 11 A8 8 0 0 1 13 3 A8 8 0 0 1 21 11 A8 8 0 0 1 13 19 Z' fill='white' stroke='black' stroke-width='1' stroke-linejoin='round'/></g></svg>")
|
|
96
|
+
5 19,
|
|
97
|
+
crosshair;
|
|
89
98
|
--tl-cursor-move:
|
|
90
99
|
url("data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: black;'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='1' dy='1' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(0 16 16)' filter='url(%23shadow)'><path d='m19 14h1v1h-1zm1 6h-1v-1h1zm-5-5h-1v-1h1zm0 5h-1v-1h1zm2-10.987-7.985 7.988 5.222 5.221 2.763 2.763 7.984-7.985z' fill='white'/><g fill='black'><path d='m23.5664 16.9971-2.557-2.809v1.829h-4.009-4.001v-1.829l-2.571 2.809 2.572 2.808-.001-1.808h4.001 4.009l-.001 1.808z'/><path d='m17.9873 17h.013v-4.001l1.807.001-2.807-2.571-2.809 2.57h1.809v4.001h.008v4.002l-1.828-.001 2.807 2.577 2.805-2.576h-1.805z'/></g></g></svg>")
|
|
91
100
|
16 16,
|
|
@@ -372,6 +381,30 @@ input,
|
|
|
372
381
|
contain: layout style size;
|
|
373
382
|
}
|
|
374
383
|
|
|
384
|
+
/* The collaborator cursor layer: a sibling of .tl-canvas, above all canvas content, below the
|
|
385
|
+
in-front layer (250) and the UI panels (300). Display-only — pointer events pass through. */
|
|
386
|
+
.tl-collaborators {
|
|
387
|
+
position: absolute;
|
|
388
|
+
inset: 0px;
|
|
389
|
+
z-index: var(--tl-layer-collaborators);
|
|
390
|
+
pointer-events: none;
|
|
391
|
+
overflow: clip;
|
|
392
|
+
contain: layout style size;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.tl-collaborators__item {
|
|
396
|
+
position: absolute;
|
|
397
|
+
top: 0px;
|
|
398
|
+
left: 0px;
|
|
399
|
+
overflow: visible;
|
|
400
|
+
pointer-events: none;
|
|
401
|
+
transform-origin: top left;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.tl-cursor {
|
|
405
|
+
overflow: visible;
|
|
406
|
+
}
|
|
407
|
+
|
|
375
408
|
/* --------------- Overlay Stack --------------- */
|
|
376
409
|
|
|
377
410
|
/* -------------- Selection foreground -------------- */
|
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": "5.3.0-next.
|
|
4
|
+
"version": "5.3.0-next.d7d8ced023d5",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -49,12 +49,12 @@
|
|
|
49
49
|
"@tiptap/core": "^3.12.1",
|
|
50
50
|
"@tiptap/pm": "^3.12.1",
|
|
51
51
|
"@tiptap/react": "^3.12.1",
|
|
52
|
-
"@tldraw/state": "5.3.0-next.
|
|
53
|
-
"@tldraw/state-react": "5.3.0-next.
|
|
54
|
-
"@tldraw/store": "5.3.0-next.
|
|
55
|
-
"@tldraw/tlschema": "5.3.0-next.
|
|
56
|
-
"@tldraw/utils": "5.3.0-next.
|
|
57
|
-
"@tldraw/validate": "5.3.0-next.
|
|
52
|
+
"@tldraw/state": "5.3.0-next.d7d8ced023d5",
|
|
53
|
+
"@tldraw/state-react": "5.3.0-next.d7d8ced023d5",
|
|
54
|
+
"@tldraw/store": "5.3.0-next.d7d8ced023d5",
|
|
55
|
+
"@tldraw/tlschema": "5.3.0-next.d7d8ced023d5",
|
|
56
|
+
"@tldraw/utils": "5.3.0-next.d7d8ced023d5",
|
|
57
|
+
"@tldraw/validate": "5.3.0-next.d7d8ced023d5",
|
|
58
58
|
"classnames": "^2.5.1",
|
|
59
59
|
"eventemitter3": "^4.0.7",
|
|
60
60
|
"idb": "^7.1.1",
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,10 @@ export {
|
|
|
18
18
|
DefaultCanvas,
|
|
19
19
|
type TLCanvasComponentProps,
|
|
20
20
|
} from './lib/components/default-components/DefaultCanvas'
|
|
21
|
+
export {
|
|
22
|
+
DefaultCursor,
|
|
23
|
+
type TLCursorProps,
|
|
24
|
+
} from './lib/components/default-components/DefaultCursor'
|
|
21
25
|
export {
|
|
22
26
|
DefaultErrorFallback,
|
|
23
27
|
type TLErrorFallbackComponent,
|
|
@@ -318,18 +322,22 @@ export {
|
|
|
318
322
|
useUniqueSafeId,
|
|
319
323
|
type SafeId,
|
|
320
324
|
} from './lib/hooks/useSafeId'
|
|
325
|
+
export { useTransform } from './lib/hooks/useTransform'
|
|
326
|
+
export { isCursorInViewport } from './lib/utils/collaborators'
|
|
321
327
|
export { useTLSchemaFromUtils, useTLStore } from './lib/hooks/useTLStore'
|
|
322
328
|
export { useViewportHeight } from './lib/hooks/useViewportHeight'
|
|
323
329
|
export {
|
|
324
330
|
LicenseManager,
|
|
325
331
|
type InvalidLicenseKeyResult,
|
|
326
332
|
type InvalidLicenseReason,
|
|
333
|
+
type LicenseFeatureName,
|
|
327
334
|
type LicenseFromKeyResult,
|
|
328
335
|
type LicenseInfo,
|
|
329
336
|
type LicenseState,
|
|
330
337
|
type ValidLicenseKeyResult,
|
|
331
338
|
} from './lib/license/LicenseManager'
|
|
332
|
-
export { LICENSE_TIMEOUT } from './lib/license/LicenseProvider'
|
|
339
|
+
export { LICENSE_TIMEOUT, useLicenseContext } from './lib/license/LicenseProvider'
|
|
340
|
+
export { useLicenseFeatureFlag } from './lib/license/useLicenseManagerState'
|
|
333
341
|
export {
|
|
334
342
|
defaultTldrawOptions,
|
|
335
343
|
type TLClipboardPasteRawInfo,
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { track, useQuickReactor } from '@tldraw/state-react'
|
|
2
|
+
import { TLInstancePresence } from '@tldraw/tlschema'
|
|
3
|
+
import { useLayoutEffect, useRef } from 'react'
|
|
4
|
+
import { useEditorComponents } from '../hooks/EditorComponentsContext'
|
|
5
|
+
import { useEditor } from '../hooks/useEditor'
|
|
6
|
+
import { useSharedSafeId } from '../hooks/useSafeId'
|
|
7
|
+
import { isCursorInViewport } from '../utils/collaborators'
|
|
8
|
+
import { setStyleProperty } from '../utils/dom'
|
|
9
|
+
import { getHtmlLayerTransform } from '../utils/getHtmlLayerTransform'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The collaborator cursor layer: a DOM layer stacked as a sibling of the canvas — above all canvas
|
|
13
|
+
* content, below the in-front layer and the UI panels — hosting each visible collaborator's cursor
|
|
14
|
+
* (arrow, name tag, chat message). Off-viewport collaborators are the canvas-drawn hint arrows' job
|
|
15
|
+
* (CollaboratorHintOverlayUtil), not this layer's.
|
|
16
|
+
*
|
|
17
|
+
* Cursors are DOM rather than canvas-drawn so their chrome styles and composes like the rest of
|
|
18
|
+
* the UI; per-cursor positioning writes `transform` directly (see `useTransform`), so pointer- and
|
|
19
|
+
* camera-frequency updates never re-render more than the moved cursor.
|
|
20
|
+
*
|
|
21
|
+
* @public @react
|
|
22
|
+
*/
|
|
23
|
+
export const LiveCollaborators = track(function LiveCollaborators() {
|
|
24
|
+
const editor = useEditor()
|
|
25
|
+
const { CollaboratorCursor } = useEditorComponents()
|
|
26
|
+
|
|
27
|
+
// The inner layer carries the camera transform, so the cursor components position in page
|
|
28
|
+
// space exactly as canvas content does. Unlike the canvas's html layers this one unmounts
|
|
29
|
+
// while no collaborators are visible, so the transform is written at two moments: on every
|
|
30
|
+
// camera change (the reactor), and after every render (the layout effect, like useTransform
|
|
31
|
+
// does per cursor) — the reactor's last run hit a null ref while the layer was unmounted,
|
|
32
|
+
// and without the render-time write a reappearing layer would keep an identity transform
|
|
33
|
+
// until the next camera move.
|
|
34
|
+
const rHtmlLayer = useRef<HTMLDivElement | null>(null)
|
|
35
|
+
useLayoutEffect(() => {
|
|
36
|
+
const elm = rHtmlLayer.current
|
|
37
|
+
if (!elm) return
|
|
38
|
+
setStyleProperty(elm, 'transform', getHtmlLayerTransform(editor))
|
|
39
|
+
})
|
|
40
|
+
useQuickReactor(
|
|
41
|
+
'position collaborators layer',
|
|
42
|
+
function positionCollaboratorsWhenCameraMoves() {
|
|
43
|
+
// Reads the camera even while the layer is unmounted, keeping the subscription alive.
|
|
44
|
+
const transform = getHtmlLayerTransform(editor)
|
|
45
|
+
setStyleProperty(rHtmlLayer.current, 'transform', transform)
|
|
46
|
+
},
|
|
47
|
+
[editor]
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
// Visibility (activity state, following, highlighting) is handled by the editor.
|
|
51
|
+
const collaborators = editor.getVisibleCollaboratorsOnCurrentPage()
|
|
52
|
+
if (collaborators.length === 0) return null
|
|
53
|
+
if (!CollaboratorCursor) return null
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="tl-collaborators">
|
|
57
|
+
<svg className="tl-svg-context" aria-hidden="true">
|
|
58
|
+
<defs>
|
|
59
|
+
<CursorDef />
|
|
60
|
+
</defs>
|
|
61
|
+
</svg>
|
|
62
|
+
<div ref={rHtmlLayer} className="tl-html-layer">
|
|
63
|
+
{collaborators.map((presence) => (
|
|
64
|
+
<Collaborator key={presence.userId} latestPresence={presence} />
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const Collaborator = track(function Collaborator({
|
|
72
|
+
latestPresence,
|
|
73
|
+
}: {
|
|
74
|
+
latestPresence: TLInstancePresence
|
|
75
|
+
}) {
|
|
76
|
+
const editor = useEditor()
|
|
77
|
+
const { CollaboratorCursor } = useEditorComponents()
|
|
78
|
+
|
|
79
|
+
const zoomLevel = editor.getZoomLevel()
|
|
80
|
+
const viewportPageBounds = editor.getViewportPageBounds()
|
|
81
|
+
const { userId, chatMessage, userName, cursor, color } = latestPresence
|
|
82
|
+
|
|
83
|
+
if (!cursor) return null
|
|
84
|
+
|
|
85
|
+
// Off-viewport collaborators show as the canvas-drawn hint arrows
|
|
86
|
+
// (CollaboratorHintOverlayUtil), which shares this predicate — only the cursor itself is DOM.
|
|
87
|
+
if (!isCursorInViewport(cursor, viewportPageBounds, zoomLevel)) return null
|
|
88
|
+
if (!CollaboratorCursor) return null
|
|
89
|
+
return (
|
|
90
|
+
<CollaboratorCursor
|
|
91
|
+
className="tl-collaborator__cursor"
|
|
92
|
+
userId={userId}
|
|
93
|
+
point={cursor}
|
|
94
|
+
color={color}
|
|
95
|
+
zoom={zoomLevel}
|
|
96
|
+
name={userName !== 'New User' ? userName : null}
|
|
97
|
+
chatMessage={chatMessage ?? ''}
|
|
98
|
+
/>
|
|
99
|
+
)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
function CursorDef() {
|
|
103
|
+
return (
|
|
104
|
+
<g id={useSharedSafeId('cursor')}>
|
|
105
|
+
<g fill="rgba(0,0,0,.2)" transform="translate(-11,-11)">
|
|
106
|
+
<path d="m12 24.4219v-16.015l11.591 11.619h-6.781l-.411.124z" />
|
|
107
|
+
<path d="m21.0845 25.0962-3.605 1.535-4.682-11.089 3.686-1.553z" />
|
|
108
|
+
</g>
|
|
109
|
+
<g fill="white" transform="translate(-12,-12)">
|
|
110
|
+
<path d="m12 24.4219v-16.015l11.591 11.619h-6.781l-.411.124z" />
|
|
111
|
+
<path d="m21.0845 25.0962-3.605 1.535-4.682-11.089 3.686-1.553z" />
|
|
112
|
+
</g>
|
|
113
|
+
<g fill="currentColor" transform="translate(-12,-12)">
|
|
114
|
+
<path d="m19.751 24.4155-1.844.774-3.1-7.374 1.841-.775z" />
|
|
115
|
+
<path d="m13 10.814v11.188l2.969-2.866.428-.139h4.768z" />
|
|
116
|
+
</g>
|
|
117
|
+
</g>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { react } from '@tldraw/state'
|
|
2
2
|
import { useQuickReactor, useValue } from '@tldraw/state-react'
|
|
3
3
|
import { TLShapeId } from '@tldraw/tlschema'
|
|
4
|
-
import {
|
|
4
|
+
import { objectMapValues } from '@tldraw/utils'
|
|
5
5
|
import classNames from 'classnames'
|
|
6
6
|
import { Fragment, JSX, useEffect, useRef, useState } from 'react'
|
|
7
7
|
import { tlenv } from '../../globals/environment'
|
|
@@ -16,9 +16,10 @@ import { useGestureEvents } from '../../hooks/useGestureEvents'
|
|
|
16
16
|
import { useScreenBounds } from '../../hooks/useScreenBounds'
|
|
17
17
|
import { ShapeCullingProvider, useShapeCulling } from '../../hooks/useShapeCulling'
|
|
18
18
|
import { Box } from '../../primitives/Box'
|
|
19
|
-
import { toDomPrecision } from '../../primitives/utils'
|
|
20
19
|
import { debugFlags } from '../../utils/debug-flags'
|
|
21
20
|
import { setStyleProperty } from '../../utils/dom'
|
|
21
|
+
import { getHtmlLayerTransform } from '../../utils/getHtmlLayerTransform'
|
|
22
|
+
import { LiveCollaborators } from '../LiveCollaborators'
|
|
22
23
|
import { MenuClickCapture } from '../MenuClickCapture'
|
|
23
24
|
import { Shape } from '../Shape'
|
|
24
25
|
import { CanvasOverlays } from './CanvasOverlays'
|
|
@@ -94,21 +95,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
|
|
|
94
95
|
useQuickReactor(
|
|
95
96
|
'position layers',
|
|
96
97
|
function positionLayersWhenCameraMoves() {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// Because the html container has a width/height of 1px, we
|
|
100
|
-
// need to create a small offset when zoomed to ensure that
|
|
101
|
-
// the html container and svg container are lined up exactly.
|
|
102
|
-
const offset =
|
|
103
|
-
z >= 1 ? modulate(z, [1, 8], [0.125, 0.5], true) : modulate(z, [0.1, 1], [-2, 0.125], true)
|
|
104
|
-
|
|
105
|
-
setStyleProperty(
|
|
106
|
-
rHtmlLayer.current,
|
|
107
|
-
'transform',
|
|
108
|
-
`scale(${toDomPrecision(z)}) translate(${toDomPrecision(
|
|
109
|
-
x + offset
|
|
110
|
-
)}px,${toDomPrecision(y + offset)}px)`
|
|
111
|
-
)
|
|
98
|
+
setStyleProperty(rHtmlLayer.current, 'transform', getHtmlLayerTransform(editor))
|
|
112
99
|
},
|
|
113
100
|
[editor, container]
|
|
114
101
|
)
|
|
@@ -166,6 +153,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
|
|
|
166
153
|
<MovingCameraHitTestBlocker />
|
|
167
154
|
</div>
|
|
168
155
|
<InFrontOfTheCanvasWrapper />
|
|
156
|
+
<LiveCollaborators />
|
|
169
157
|
<MenuClickCapture />
|
|
170
158
|
</>
|
|
171
159
|
)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { VecModel } from '@tldraw/tlschema'
|
|
2
|
+
import classNames from 'classnames'
|
|
3
|
+
import { memo, useRef } from 'react'
|
|
4
|
+
import { useSharedSafeId } from '../../hooks/useSafeId'
|
|
5
|
+
import { useTransform } from '../../hooks/useTransform'
|
|
6
|
+
|
|
7
|
+
/** @public */
|
|
8
|
+
export interface TLCursorProps {
|
|
9
|
+
userId: string
|
|
10
|
+
className?: string
|
|
11
|
+
point: VecModel | null
|
|
12
|
+
zoom: number
|
|
13
|
+
color?: string
|
|
14
|
+
name: string | null
|
|
15
|
+
chatMessage: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The default collaborator cursor: the arrow glyph plus a name tag or chat bubble. The arrow is a
|
|
20
|
+
* `<use>` of a shared `<defs>` symbol that `LiveCollaborators` renders, so a `DefaultCursor`
|
|
21
|
+
* used outside that layer draws no arrow.
|
|
22
|
+
*
|
|
23
|
+
* @public @react
|
|
24
|
+
*/
|
|
25
|
+
export const DefaultCursor = memo(function DefaultCursor({
|
|
26
|
+
className,
|
|
27
|
+
zoom,
|
|
28
|
+
point,
|
|
29
|
+
color,
|
|
30
|
+
name,
|
|
31
|
+
chatMessage,
|
|
32
|
+
}: TLCursorProps) {
|
|
33
|
+
const rCursor = useRef<HTMLDivElement>(null)
|
|
34
|
+
useTransform(rCursor, point?.x, point?.y, 1 / zoom)
|
|
35
|
+
|
|
36
|
+
const cursorId = useSharedSafeId('cursor')
|
|
37
|
+
|
|
38
|
+
if (!point) return null
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div ref={rCursor} className={classNames('tl-collaborators__item', className)}>
|
|
42
|
+
<svg className="tl-cursor" aria-hidden="true">
|
|
43
|
+
<use href={`#${cursorId}`} color={color} />
|
|
44
|
+
</svg>
|
|
45
|
+
{chatMessage ? (
|
|
46
|
+
<>
|
|
47
|
+
{name && (
|
|
48
|
+
<div className="tl-nametag-title" style={{ color }}>
|
|
49
|
+
{name}
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
<div className="tl-nametag-chat" style={{ backgroundColor: color }}>
|
|
53
|
+
{chatMessage}
|
|
54
|
+
</div>
|
|
55
|
+
</>
|
|
56
|
+
) : (
|
|
57
|
+
name && (
|
|
58
|
+
<div className="tl-nametag" style={{ backgroundColor: color }}>
|
|
59
|
+
{name}
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
})
|
|
@@ -37,6 +37,11 @@ export type TLExternalContentSource =
|
|
|
37
37
|
|
|
38
38
|
/** @public */
|
|
39
39
|
export interface TLBaseExternalContent {
|
|
40
|
+
/**
|
|
41
|
+
* The other content sources found on the clipboard alongside this content. For example, when
|
|
42
|
+
* pasting an image copied together with HTML and plain text, the `files` content will have
|
|
43
|
+
* text sources for the HTML and plain text parts.
|
|
44
|
+
*/
|
|
40
45
|
sources?: TLExternalContentSource[]
|
|
41
46
|
point?: VecLike
|
|
42
47
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ComponentType, RefAttributes, createContext, useContext } from 'react'
|
|
2
2
|
import type { TLCanvasComponentProps } from '../components/default-components/DefaultCanvas'
|
|
3
|
+
import type { TLCursorProps } from '../components/default-components/DefaultCursor'
|
|
3
4
|
import type { TLErrorFallbackComponent } from '../components/default-components/DefaultErrorFallback'
|
|
4
5
|
import type { TLGridProps } from '../components/default-components/DefaultGrid'
|
|
5
6
|
import type { TLShapeErrorFallbackComponent } from '../components/default-components/DefaultShapeErrorFallback'
|
|
@@ -9,6 +10,7 @@ import type { TLShapeWrapperProps } from '../components/default-components/Defau
|
|
|
9
10
|
export interface TLEditorComponents {
|
|
10
11
|
Background?: ComponentType | null
|
|
11
12
|
Canvas?: ComponentType<TLCanvasComponentProps> | null
|
|
13
|
+
CollaboratorCursor?: ComponentType<TLCursorProps> | null
|
|
12
14
|
Grid?: ComponentType<TLGridProps> | null
|
|
13
15
|
InFrontOfTheCanvas?: ComponentType | null
|
|
14
16
|
LoadingScreen?: ComponentType | null
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ReactNode, useMemo } from 'react'
|
|
2
2
|
import { DefaultBackground } from '../components/default-components/DefaultBackground'
|
|
3
3
|
import { DefaultCanvas } from '../components/default-components/DefaultCanvas'
|
|
4
|
+
import { DefaultCursor } from '../components/default-components/DefaultCursor'
|
|
4
5
|
import { DefaultErrorFallback } from '../components/default-components/DefaultErrorFallback'
|
|
5
6
|
import { DefaultGrid } from '../components/default-components/DefaultGrid'
|
|
6
7
|
import { DefaultLoadingScreen } from '../components/default-components/DefaultLoadingScreen'
|
|
@@ -29,6 +30,7 @@ export function EditorComponentsProvider({
|
|
|
29
30
|
(): Required<TLEditorComponents> => ({
|
|
30
31
|
Background: DefaultBackground,
|
|
31
32
|
Canvas: DefaultCanvas,
|
|
33
|
+
CollaboratorCursor: DefaultCursor,
|
|
32
34
|
Grid: DefaultGrid,
|
|
33
35
|
InFrontOfTheCanvas: null,
|
|
34
36
|
LoadingScreen: DefaultLoadingScreen,
|
|
@@ -3,6 +3,48 @@ import { preventDefault } from '../utils/dom'
|
|
|
3
3
|
import { useContainer } from './useContainer'
|
|
4
4
|
import { useMaybeEditor } from './useEditor'
|
|
5
5
|
|
|
6
|
+
function isScrollableOverflow(overflow: string) {
|
|
7
|
+
return overflow === 'auto' || overflow === 'scroll' || overflow === 'overlay'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// An element only consumes a wheel event when it's a real scroll container: its content overflows
|
|
11
|
+
// AND its computed overflow is scrollable. Checking scrollHeight/scrollWidth alone is not enough —
|
|
12
|
+
// an `overflow: visible` element whose content merely overflows (e.g. a comment pin whose hover
|
|
13
|
+
// `transform: scale()` inflates scrollHeight past clientHeight) never scrolls, so the wheel must
|
|
14
|
+
// still pass through to the canvas.
|
|
15
|
+
function isScrollable(elm: HTMLElement) {
|
|
16
|
+
// Cheap first: if nothing overflows, it can't scroll — skip the getComputedStyle read. This runs
|
|
17
|
+
// per ancestor on every wheel event, so avoid resolving styles for the common (non-overflowing) case.
|
|
18
|
+
const overflowsY = elm.scrollHeight > elm.clientHeight
|
|
19
|
+
const overflowsX = elm.scrollWidth > elm.clientWidth
|
|
20
|
+
if (!overflowsY && !overflowsX) return false
|
|
21
|
+
|
|
22
|
+
const style = getComputedStyle(elm)
|
|
23
|
+
return (
|
|
24
|
+
(overflowsY && isScrollableOverflow(style.overflowY)) ||
|
|
25
|
+
(overflowsX && isScrollableOverflow(style.overflowX))
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Walk from the wheeled element up to (and including) the pass-through root, looking for a scroll
|
|
30
|
+
// container. If one is found, the wheel belongs to it — don't redispatch it to the canvas.
|
|
31
|
+
function hasScrollableElement(target: EventTarget | null, root: HTMLElement) {
|
|
32
|
+
if (!(target instanceof Node)) {
|
|
33
|
+
return isScrollable(root)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let elm: Element | null = target instanceof Element ? target : target.parentElement
|
|
37
|
+
while (elm) {
|
|
38
|
+
if (elm instanceof HTMLElement && isScrollable(elm)) {
|
|
39
|
+
return true
|
|
40
|
+
}
|
|
41
|
+
if (elm === root) return false
|
|
42
|
+
elm = elm.parentElement
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
|
|
6
48
|
/** @public */
|
|
7
49
|
export function usePassThroughWheelEvents(ref: RefObject<HTMLElement | null>) {
|
|
8
50
|
if (!ref) throw Error('usePassThroughWheelEvents must be passed a ref')
|
|
@@ -16,9 +58,9 @@ export function usePassThroughWheelEvents(ref: RefObject<HTMLElement | null>) {
|
|
|
16
58
|
|
|
17
59
|
if ((e as any).isSpecialRedispatchedEvent) return
|
|
18
60
|
|
|
19
|
-
//
|
|
61
|
+
// If the wheel is over a scrollable element, let it scroll instead of redispatching.
|
|
20
62
|
const elm = ref.current
|
|
21
|
-
if (elm &&
|
|
63
|
+
if (elm && hasScrollableElement(e.target, elm)) {
|
|
22
64
|
return
|
|
23
65
|
}
|
|
24
66
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useLayoutEffect } from 'react'
|
|
2
|
+
import { VecLike } from '../primitives/Vec'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Position an element by writing its `transform` directly, outside React's render output — the
|
|
6
|
+
* cheap path for elements that move at pointer/presence frequency.
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
export function useTransform(
|
|
10
|
+
ref: React.RefObject<HTMLElement | SVGElement | null>,
|
|
11
|
+
x?: number,
|
|
12
|
+
y?: number,
|
|
13
|
+
scale?: number,
|
|
14
|
+
rotate?: number,
|
|
15
|
+
additionalOffset?: VecLike
|
|
16
|
+
) {
|
|
17
|
+
// Read the offset into primitives so the effect can depend on them, not the object identity.
|
|
18
|
+
const additionalOffsetX = additionalOffset?.x
|
|
19
|
+
const additionalOffsetY = additionalOffset?.y
|
|
20
|
+
|
|
21
|
+
useLayoutEffect(() => {
|
|
22
|
+
const elm = ref.current
|
|
23
|
+
if (!elm) return
|
|
24
|
+
if (x === undefined) return
|
|
25
|
+
|
|
26
|
+
let trans = `translate(${x}px, ${y}px)`
|
|
27
|
+
if (scale !== undefined) {
|
|
28
|
+
trans += ` scale(${scale})`
|
|
29
|
+
}
|
|
30
|
+
if (rotate !== undefined) {
|
|
31
|
+
trans += ` rotate(${rotate}rad)`
|
|
32
|
+
}
|
|
33
|
+
if (additionalOffsetX !== undefined && additionalOffsetY !== undefined) {
|
|
34
|
+
trans += ` translate(${additionalOffsetX}px, ${additionalOffsetY}px)`
|
|
35
|
+
}
|
|
36
|
+
elm.style.transform = trans
|
|
37
|
+
}, [additionalOffsetX, additionalOffsetY, ref, rotate, scale, x, y])
|
|
38
|
+
}
|