@tldraw/editor 5.3.0-next.75e2874c4b3f → 5.3.0-next.7654e7ac2a02
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 +115 -3
- package/dist-cjs/index.js +13 -3
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +24 -19
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/components/EditorPortal.js +44 -0
- package/dist-cjs/lib/components/EditorPortal.js.map +7 -0
- package/dist-cjs/lib/components/LiveCollaborators.js +102 -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/managers/CollaboratorsManager/CollaboratorsManager.js +4 -4
- package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +2 -2
- 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 +51 -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 +115 -3
- package/dist-esm/index.mjs +19 -4
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +23 -18
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/components/EditorPortal.mjs +24 -0
- package/dist-esm/lib/components/EditorPortal.mjs.map +7 -0
- package/dist-esm/lib/components/LiveCollaborators.mjs +82 -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/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +5 -5
- package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
- 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 +52 -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 +43 -4
- package/package.json +7 -7
- package/src/index.ts +14 -2
- package/src/lib/TldrawEditor.tsx +21 -13
- package/src/lib/components/EditorPortal.tsx +77 -0
- package/src/lib/components/LiveCollaborators.tsx +133 -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/managers/CollaboratorsManager/CollaboratorsManager.test.ts +46 -1
- package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +10 -5
- 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 +146 -0
- package/src/lib/license/LicenseManager.ts +98 -2
- package/src/lib/license/useLicenseManagerState.ts +17 -1
- package/src/lib/test/EditorPortal.test.tsx +48 -0
- 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-cjs/lib/hooks/usePassThroughMouseOverEvents.js +0 -51
- package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +0 -7
- package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs +0 -31
- package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +0 -7
- package/src/lib/hooks/usePassThroughMouseOverEvents.ts +0 -32
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,
|
|
@@ -286,6 +295,12 @@
|
|
|
286
295
|
outline: 1px solid var(--tl-color-low);
|
|
287
296
|
}
|
|
288
297
|
|
|
298
|
+
/* The host for <EditorPortal>, last among the container's children. It's a position in the DOM
|
|
299
|
+
rather than a box: `display: contents` keeps what it holds positioned against the container. */
|
|
300
|
+
.tl-portal-host {
|
|
301
|
+
display: contents;
|
|
302
|
+
}
|
|
303
|
+
|
|
289
304
|
input,
|
|
290
305
|
*[contenteditable],
|
|
291
306
|
*[contenteditable] * {
|
|
@@ -372,6 +387,30 @@ input,
|
|
|
372
387
|
contain: layout style size;
|
|
373
388
|
}
|
|
374
389
|
|
|
390
|
+
/* The collaborator cursor layer: a sibling of .tl-canvas, above all canvas content, below the
|
|
391
|
+
in-front layer (250) and the UI panels (300). Display-only — pointer events pass through. */
|
|
392
|
+
.tl-collaborators {
|
|
393
|
+
position: absolute;
|
|
394
|
+
inset: 0px;
|
|
395
|
+
z-index: var(--tl-layer-collaborators);
|
|
396
|
+
pointer-events: none;
|
|
397
|
+
overflow: clip;
|
|
398
|
+
contain: layout style size;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.tl-collaborators__item {
|
|
402
|
+
position: absolute;
|
|
403
|
+
top: 0px;
|
|
404
|
+
left: 0px;
|
|
405
|
+
overflow: visible;
|
|
406
|
+
pointer-events: none;
|
|
407
|
+
transform-origin: top left;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.tl-cursor {
|
|
411
|
+
overflow: visible;
|
|
412
|
+
}
|
|
413
|
+
|
|
375
414
|
/* --------------- Overlay Stack --------------- */
|
|
376
415
|
|
|
377
416
|
/* -------------- 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.7654e7ac2a02",
|
|
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.7654e7ac2a02",
|
|
53
|
+
"@tldraw/state-react": "5.3.0-next.7654e7ac2a02",
|
|
54
|
+
"@tldraw/store": "5.3.0-next.7654e7ac2a02",
|
|
55
|
+
"@tldraw/tlschema": "5.3.0-next.7654e7ac2a02",
|
|
56
|
+
"@tldraw/utils": "5.3.0-next.7654e7ac2a02",
|
|
57
|
+
"@tldraw/validate": "5.3.0-next.7654e7ac2a02",
|
|
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,
|
|
@@ -30,6 +34,11 @@ export {
|
|
|
30
34
|
} from './lib/components/default-components/DefaultShapeWrapper'
|
|
31
35
|
export { DefaultSpinner } from './lib/components/default-components/DefaultSpinner'
|
|
32
36
|
export { DefaultSvgDefs } from './lib/components/default-components/DefaultSvgDefs'
|
|
37
|
+
export {
|
|
38
|
+
EditorPortal,
|
|
39
|
+
useEditorPortalHost,
|
|
40
|
+
type EditorPortalProps,
|
|
41
|
+
} from './lib/components/EditorPortal'
|
|
33
42
|
export {
|
|
34
43
|
ErrorBoundary,
|
|
35
44
|
OptionalErrorBoundary,
|
|
@@ -306,7 +315,6 @@ export { useShallowArrayIdentity, useShallowObjectIdentity } from './lib/hooks/u
|
|
|
306
315
|
export { useIsCropping } from './lib/hooks/useIsCropping'
|
|
307
316
|
export { useIsEditing } from './lib/hooks/useIsEditing'
|
|
308
317
|
export { useLocalStore } from './lib/hooks/useLocalStore'
|
|
309
|
-
export { usePassThroughMouseOverEvents } from './lib/hooks/usePassThroughMouseOverEvents'
|
|
310
318
|
export { usePassThroughWheelEvents } from './lib/hooks/usePassThroughWheelEvents'
|
|
311
319
|
export { usePeerIds } from './lib/hooks/usePeerIds'
|
|
312
320
|
export { usePresence } from './lib/hooks/usePresence'
|
|
@@ -318,18 +326,22 @@ export {
|
|
|
318
326
|
useUniqueSafeId,
|
|
319
327
|
type SafeId,
|
|
320
328
|
} from './lib/hooks/useSafeId'
|
|
329
|
+
export { useTransform } from './lib/hooks/useTransform'
|
|
330
|
+
export { isCursorInViewport } from './lib/utils/collaborators'
|
|
321
331
|
export { useTLSchemaFromUtils, useTLStore } from './lib/hooks/useTLStore'
|
|
322
332
|
export { useViewportHeight } from './lib/hooks/useViewportHeight'
|
|
323
333
|
export {
|
|
324
334
|
LicenseManager,
|
|
325
335
|
type InvalidLicenseKeyResult,
|
|
326
336
|
type InvalidLicenseReason,
|
|
337
|
+
type LicenseFeatureName,
|
|
327
338
|
type LicenseFromKeyResult,
|
|
328
339
|
type LicenseInfo,
|
|
329
340
|
type LicenseState,
|
|
330
341
|
type ValidLicenseKeyResult,
|
|
331
342
|
} from './lib/license/LicenseManager'
|
|
332
|
-
export { LICENSE_TIMEOUT } from './lib/license/LicenseProvider'
|
|
343
|
+
export { LICENSE_TIMEOUT, useLicenseContext } from './lib/license/LicenseProvider'
|
|
344
|
+
export { useLicenseFeatureFlag } from './lib/license/useLicenseManagerState'
|
|
333
345
|
export {
|
|
334
346
|
defaultTldrawOptions,
|
|
335
347
|
type TLClipboardPasteRawInfo,
|
package/src/lib/TldrawEditor.tsx
CHANGED
|
@@ -24,6 +24,7 @@ import React, {
|
|
|
24
24
|
} from 'react'
|
|
25
25
|
import { version } from '../version'
|
|
26
26
|
import { DefaultErrorFallback } from './components/default-components/DefaultErrorFallback'
|
|
27
|
+
import { EditorPortalProvider } from './components/EditorPortal'
|
|
27
28
|
import { OptionalErrorBoundary } from './components/ErrorBoundary'
|
|
28
29
|
import { createTLCurrentUser, TLCurrentUser } from './config/createTLCurrentUser'
|
|
29
30
|
import { TLStoreBaseOptions } from './config/createTLStore'
|
|
@@ -304,6 +305,7 @@ export const TldrawEditor = memo(function TldrawEditor({
|
|
|
304
305
|
registerFontsFromThemes(resolvedThemes)
|
|
305
306
|
|
|
306
307
|
const [container, setContainer] = useState<HTMLElement | null>(null)
|
|
308
|
+
const [portalHost, setPortalHost] = useState<HTMLElement | null>(null)
|
|
307
309
|
const user = useMemo(() => _user ?? createTLCurrentUser(), [_user])
|
|
308
310
|
|
|
309
311
|
const ErrorFallback =
|
|
@@ -352,24 +354,30 @@ export const TldrawEditor = memo(function TldrawEditor({
|
|
|
352
354
|
{container && (
|
|
353
355
|
<LicenseProvider licenseKey={rest.licenseKey}>
|
|
354
356
|
<ContainerProvider container={container}>
|
|
355
|
-
<
|
|
356
|
-
{
|
|
357
|
-
store
|
|
358
|
-
|
|
359
|
-
|
|
357
|
+
<EditorPortalProvider host={portalHost}>
|
|
358
|
+
<EditorComponentsProvider overrides={components}>
|
|
359
|
+
{store ? (
|
|
360
|
+
store instanceof Store ? (
|
|
361
|
+
// Store is ready to go, whether externally synced or not
|
|
362
|
+
<TldrawEditorWithReadyStore {...withDefaults} store={store} user={user} />
|
|
363
|
+
) : (
|
|
364
|
+
// Store is a synced store, so handle syncing stages internally
|
|
365
|
+
<TldrawEditorWithLoadingStore {...withDefaults} store={store} user={user} />
|
|
366
|
+
)
|
|
360
367
|
) : (
|
|
361
|
-
//
|
|
362
|
-
<
|
|
363
|
-
)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
<TldrawEditorWithOwnStore {...withDefaults} store={store} user={user} />
|
|
367
|
-
)}
|
|
368
|
-
</EditorComponentsProvider>
|
|
368
|
+
// We have no store (it's undefined) so create one and possibly sync it
|
|
369
|
+
<TldrawEditorWithOwnStore {...withDefaults} store={store} user={user} />
|
|
370
|
+
)}
|
|
371
|
+
</EditorComponentsProvider>
|
|
372
|
+
</EditorPortalProvider>
|
|
369
373
|
</ContainerProvider>
|
|
370
374
|
</LicenseProvider>
|
|
371
375
|
)}
|
|
372
376
|
</OptionalErrorBoundary>
|
|
377
|
+
{/* The host for <EditorPortal>, last among the container's children so that anything
|
|
378
|
+
portaled through it lands after the canvas and the UI — behind the UI's "skip to main
|
|
379
|
+
content" link in the tab order, which only works while nothing precedes it. */}
|
|
380
|
+
<div className="tl-portal-host" ref={setPortalHost} />
|
|
373
381
|
</div>
|
|
374
382
|
)
|
|
375
383
|
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { ReactNode, createContext, useContext } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
|
|
4
|
+
const EditorPortalContext = createContext<HTMLElement | null>(null)
|
|
5
|
+
|
|
6
|
+
/** @internal */
|
|
7
|
+
export function EditorPortalProvider({
|
|
8
|
+
host,
|
|
9
|
+
children,
|
|
10
|
+
}: {
|
|
11
|
+
host: HTMLElement | null
|
|
12
|
+
children: ReactNode
|
|
13
|
+
}) {
|
|
14
|
+
return <EditorPortalContext.Provider value={host}>{children}</EditorPortalContext.Provider>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The DOM node that {@link EditorPortal} renders into: an empty `display: contents` element the
|
|
19
|
+
* editor renders last among its container's children, after the canvas and after the UI.
|
|
20
|
+
*
|
|
21
|
+
* Null until the container has mounted, so prefer {@link EditorPortal}, which handles that for you.
|
|
22
|
+
*
|
|
23
|
+
* @public
|
|
24
|
+
*/
|
|
25
|
+
export function useEditorPortalHost(): HTMLElement | null {
|
|
26
|
+
return useContext(EditorPortalContext)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** @public */
|
|
30
|
+
export interface EditorPortalProps {
|
|
31
|
+
children: ReactNode
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Renders its children into the end of the editor's container, escaping the canvas layer.
|
|
36
|
+
*
|
|
37
|
+
* Use this for anything anchored to canvas coordinates that also has to draw over the UI — a
|
|
38
|
+
* popover on a shape, a comment thread, an annotation panel. The `OnTheCanvas` and
|
|
39
|
+
* `InFrontOfTheCanvas` slots are fixed layers, each its own stacking context, so content mounted
|
|
40
|
+
* there can't paint above whatever outranks its layer whatever z-index it asks for. Mount in
|
|
41
|
+
* whichever slot suits your data flow, wrap the part that needs its own layer in this, and give
|
|
42
|
+
* that part a z-index.
|
|
43
|
+
*
|
|
44
|
+
* This package's own layers stop at 250 (`--tl-layer-canvas-in-front`); above that belongs to
|
|
45
|
+
* whatever renders the UI. In `tldraw` that's `--tl-layer-panels` (300) and `--tl-layer-menus`
|
|
46
|
+
* (400), defined by `tldraw`'s stylesheet rather than this one.
|
|
47
|
+
*
|
|
48
|
+
* Positioning still resolves against the editor's container, so a `position: absolute` child uses
|
|
49
|
+
* ordinary container coordinates.
|
|
50
|
+
*
|
|
51
|
+
* Prefer this over `createPortal(children, useContainer())`, which picks its DOM position from the
|
|
52
|
+
* commit that mounts it — landing the node ahead of the UI and of the "skip to main content" link
|
|
53
|
+
* that only works while nothing precedes it. This host is a real element placed last, so ordering
|
|
54
|
+
* is fixed rather than dependent on mount timing.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```tsx
|
|
58
|
+
* // 400 clears tldraw's menus layer; the bare editor's own layers stop at 250.
|
|
59
|
+
* function ShapeAnnotation({ point }: { point: VecLike }) {
|
|
60
|
+
* return (
|
|
61
|
+
* <EditorPortal>
|
|
62
|
+
* <div style={{ position: 'absolute', left: point.x, top: point.y, zIndex: 400 }}>
|
|
63
|
+
* Above the UI
|
|
64
|
+
* </div>
|
|
65
|
+
* </EditorPortal>
|
|
66
|
+
* )
|
|
67
|
+
* }
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* @public
|
|
71
|
+
* @react
|
|
72
|
+
*/
|
|
73
|
+
export function EditorPortal({ children }: EditorPortalProps) {
|
|
74
|
+
const host = useEditorPortalHost()
|
|
75
|
+
if (!host) return null
|
|
76
|
+
return createPortal(children, host)
|
|
77
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { track, useQuickReactor, useValue } 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. Re-render traffic is kept narrow: per-cursor positioning writes `transform` directly
|
|
19
|
+
* (see `useTransform`), so a pointer move re-renders only the moved cursor; the camera transform
|
|
20
|
+
* is written imperatively below, so a pure pan re-renders only cursors whose viewport visibility
|
|
21
|
+
* flips (an equality-gated boolean per cursor); a zoom change re-renders every visible cursor,
|
|
22
|
+
* because each one rescales by `1/zoom`.
|
|
23
|
+
*
|
|
24
|
+
* @public @react
|
|
25
|
+
*/
|
|
26
|
+
export const LiveCollaborators = track(function LiveCollaborators() {
|
|
27
|
+
const editor = useEditor()
|
|
28
|
+
const { CollaboratorCursor } = useEditorComponents()
|
|
29
|
+
|
|
30
|
+
// The inner layer carries the camera transform, so the cursor components position in page
|
|
31
|
+
// space exactly as canvas content does. Unlike the canvas's html layers this one unmounts
|
|
32
|
+
// while no collaborators are visible, so the transform is written at two moments: on every
|
|
33
|
+
// camera change (the reactor), and after every render (the layout effect, like useTransform
|
|
34
|
+
// does per cursor) — the reactor's last run hit a null ref while the layer was unmounted,
|
|
35
|
+
// and without the render-time write a reappearing layer would keep an identity transform
|
|
36
|
+
// until the next camera move.
|
|
37
|
+
const rHtmlLayer = useRef<HTMLDivElement | null>(null)
|
|
38
|
+
useLayoutEffect(() => {
|
|
39
|
+
const elm = rHtmlLayer.current
|
|
40
|
+
if (!elm) return
|
|
41
|
+
setStyleProperty(elm, 'transform', getHtmlLayerTransform(editor))
|
|
42
|
+
})
|
|
43
|
+
useQuickReactor(
|
|
44
|
+
'position collaborators layer',
|
|
45
|
+
function positionCollaboratorsWhenCameraMoves() {
|
|
46
|
+
// Reads the camera even while the layer is unmounted, keeping the subscription alive.
|
|
47
|
+
const transform = getHtmlLayerTransform(editor)
|
|
48
|
+
setStyleProperty(rHtmlLayer.current, 'transform', transform)
|
|
49
|
+
},
|
|
50
|
+
[editor]
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
// Visibility (activity state, following, highlighting) is handled by the editor.
|
|
54
|
+
const collaborators = editor.getVisibleCollaboratorsOnCurrentPage()
|
|
55
|
+
if (collaborators.length === 0) return null
|
|
56
|
+
if (!CollaboratorCursor) return null
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="tl-collaborators">
|
|
60
|
+
<svg className="tl-svg-context" aria-hidden="true">
|
|
61
|
+
<defs>
|
|
62
|
+
<CursorDef />
|
|
63
|
+
</defs>
|
|
64
|
+
</svg>
|
|
65
|
+
<div ref={rHtmlLayer} className="tl-html-layer">
|
|
66
|
+
{collaborators.map((presence) => (
|
|
67
|
+
<Collaborator key={presence.userId} latestPresence={presence} />
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const Collaborator = track(function Collaborator({
|
|
75
|
+
latestPresence,
|
|
76
|
+
}: {
|
|
77
|
+
latestPresence: TLInstancePresence
|
|
78
|
+
}) {
|
|
79
|
+
const editor = useEditor()
|
|
80
|
+
const { CollaboratorCursor } = useEditorComponents()
|
|
81
|
+
|
|
82
|
+
const { userId, chatMessage, userName, cursor, color } = latestPresence
|
|
83
|
+
|
|
84
|
+
// The viewport read lives inside this equality-gated computed rather than the tracked render:
|
|
85
|
+
// pans move the viewport every frame, but this component only needs to know when the cursor
|
|
86
|
+
// crosses the edge, so it subscribes to the boolean and re-renders only when it flips.
|
|
87
|
+
const cursorInViewport = useValue(
|
|
88
|
+
'cursor in viewport',
|
|
89
|
+
() =>
|
|
90
|
+
!!cursor && isCursorInViewport(cursor, editor.getViewportPageBounds(), editor.getZoomLevel()),
|
|
91
|
+
[editor, cursor]
|
|
92
|
+
)
|
|
93
|
+
// The zoom read is tracked directly: the cursor scales by 1/zoom, so zoom changes must
|
|
94
|
+
// re-render it. Pure pans leave the zoom value unchanged, so they never invalidate this.
|
|
95
|
+
const zoomLevel = editor.getZoomLevel()
|
|
96
|
+
|
|
97
|
+
if (!cursor) return null
|
|
98
|
+
|
|
99
|
+
// Off-viewport collaborators show as the canvas-drawn hint arrows
|
|
100
|
+
// (CollaboratorHintOverlayUtil), which shares this predicate — only the cursor itself is DOM.
|
|
101
|
+
if (!cursorInViewport) return null
|
|
102
|
+
if (!CollaboratorCursor) return null
|
|
103
|
+
return (
|
|
104
|
+
<CollaboratorCursor
|
|
105
|
+
className="tl-collaborator__cursor"
|
|
106
|
+
userId={userId}
|
|
107
|
+
point={cursor}
|
|
108
|
+
color={color}
|
|
109
|
+
zoom={zoomLevel}
|
|
110
|
+
name={userName !== 'New User' ? userName : null}
|
|
111
|
+
chatMessage={chatMessage ?? ''}
|
|
112
|
+
/>
|
|
113
|
+
)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
function CursorDef() {
|
|
117
|
+
return (
|
|
118
|
+
<g id={useSharedSafeId('cursor')}>
|
|
119
|
+
<g fill="rgba(0,0,0,.2)" transform="translate(-11,-11)">
|
|
120
|
+
<path d="m12 24.4219v-16.015l11.591 11.619h-6.781l-.411.124z" />
|
|
121
|
+
<path d="m21.0845 25.0962-3.605 1.535-4.682-11.089 3.686-1.553z" />
|
|
122
|
+
</g>
|
|
123
|
+
<g fill="white" transform="translate(-12,-12)">
|
|
124
|
+
<path d="m12 24.4219v-16.015l11.591 11.619h-6.781l-.411.124z" />
|
|
125
|
+
<path d="m21.0845 25.0962-3.605 1.535-4.682-11.089 3.686-1.553z" />
|
|
126
|
+
</g>
|
|
127
|
+
<g fill="currentColor" transform="translate(-12,-12)">
|
|
128
|
+
<path d="m19.751 24.4155-1.844.774-3.1-7.374 1.841-.775z" />
|
|
129
|
+
<path d="m13 10.814v11.188l2.969-2.866.428-.139h4.768z" />
|
|
130
|
+
</g>
|
|
131
|
+
</g>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
@@ -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
|
+
})
|
|
@@ -33,7 +33,7 @@ function createPresence(userId: TLUserId): TLInstancePresence {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function createEditor(presences: TLInstancePresence[] = []) {
|
|
36
|
-
const setInterval = vi.fn(() => 123)
|
|
36
|
+
const setInterval = vi.fn((_fn: () => void, _ms: number) => 123)
|
|
37
37
|
const getInstanceState = vi.fn(() => ({
|
|
38
38
|
followingUserId: null,
|
|
39
39
|
highlightedUserIds: [],
|
|
@@ -67,6 +67,7 @@ function createEditor(presences: TLInstancePresence[] = []) {
|
|
|
67
67
|
describe(CollaboratorsManager, () => {
|
|
68
68
|
afterEach(() => {
|
|
69
69
|
vi.clearAllMocks()
|
|
70
|
+
vi.useRealTimers()
|
|
70
71
|
})
|
|
71
72
|
|
|
72
73
|
it('starts the visibility clock on the first visible collaborators read', () => {
|
|
@@ -143,6 +144,50 @@ describe(CollaboratorsManager, () => {
|
|
|
143
144
|
expect(manager.getVisibleCollaborators()).toHaveLength(1)
|
|
144
145
|
})
|
|
145
146
|
|
|
147
|
+
it('keeps array identity when a presence update leaves the derived lists unchanged', () => {
|
|
148
|
+
const peerHere = createPresence(createUserId('peer-here'))
|
|
149
|
+
const peerElsewhere = createPresence(createUserId('peer-elsewhere'))
|
|
150
|
+
peerElsewhere.currentPageId = PageRecordType.createId('other-page')
|
|
151
|
+
const { editor } = createEditor([peerHere, peerElsewhere])
|
|
152
|
+
const manager = new CollaboratorsManager(editor)
|
|
153
|
+
|
|
154
|
+
const onPage = manager.getCollaboratorsOnCurrentPage()
|
|
155
|
+
const visibleOnPage = manager.getVisibleCollaboratorsOnCurrentPage()
|
|
156
|
+
expect(onPage).toEqual([peerHere])
|
|
157
|
+
|
|
158
|
+
// The off-page peer sends a presence update: the current-page lists are unaffected, so
|
|
159
|
+
// they keep the exact same array identity and don't invalidate downstream subscribers.
|
|
160
|
+
editor.store.put([{ ...peerElsewhere, chatMessage: 'hello from another page' }])
|
|
161
|
+
expect(manager.getCollaboratorsOnCurrentPage()).toBe(onPage)
|
|
162
|
+
expect(manager.getVisibleCollaboratorsOnCurrentPage()).toBe(visibleOnPage)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('keeps array identity across a visibility-clock tick that changes nothing', () => {
|
|
166
|
+
vi.useFakeTimers()
|
|
167
|
+
const peer = createPresence(createUserId('peer'))
|
|
168
|
+
const { editor, setInterval, getInstanceState } = createEditor([peer])
|
|
169
|
+
const manager = new CollaboratorsManager(editor)
|
|
170
|
+
|
|
171
|
+
const visible = manager.getVisibleCollaborators()
|
|
172
|
+
const visibleOnPage = manager.getVisibleCollaboratorsOnCurrentPage()
|
|
173
|
+
expect(visible).toEqual([peer])
|
|
174
|
+
expect(getInstanceState).toHaveBeenCalledTimes(1)
|
|
175
|
+
|
|
176
|
+
// The clock ticks on a fixed interval whether or not anything changed. Advance far enough
|
|
177
|
+
// to move the clock atom, but not far enough to change anyone's activity state.
|
|
178
|
+
const tick = setInterval.mock.calls[0][0]
|
|
179
|
+
vi.advanceTimersByTime(1000)
|
|
180
|
+
tick()
|
|
181
|
+
|
|
182
|
+
// The tick really did invalidate the query — it re-evaluates and re-reads instance state —
|
|
183
|
+
// but the result is element-wise identical, so it keeps the previous array's identity. An
|
|
184
|
+
// idle room therefore doesn't re-render the cursor layer once per interval.
|
|
185
|
+
const revisited = manager.getVisibleCollaborators()
|
|
186
|
+
expect(getInstanceState).toHaveBeenCalledTimes(2)
|
|
187
|
+
expect(revisited).toBe(visible)
|
|
188
|
+
expect(manager.getVisibleCollaboratorsOnCurrentPage()).toBe(visibleOnPage)
|
|
189
|
+
})
|
|
190
|
+
|
|
146
191
|
it('shows newly-joined collaborators that have not recorded any activity yet', () => {
|
|
147
192
|
// A peer who has joined but not moved their pointer broadcasts the default
|
|
148
193
|
// `lastActivityTimestamp` of 0. They should still be treated as active so
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EMPTY_ARRAY, atom, computed } from '@tldraw/state'
|
|
2
2
|
import type { TLInstancePresence } from '@tldraw/tlschema'
|
|
3
|
-
import { maxBy } from '@tldraw/utils'
|
|
3
|
+
import { areArraysShallowEqual, maxBy } from '@tldraw/utils'
|
|
4
4
|
import type { Editor } from '../../Editor'
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -41,11 +41,16 @@ export class CollaboratorsManager {
|
|
|
41
41
|
}))
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// These queries all derive fresh arrays with map/filter, so they compare results with
|
|
45
|
+
// shallow (element-identity) equality: a presence update that leaves a derived list's
|
|
46
|
+
// elements unchanged — e.g. a peer moving on another page — keeps the previous array's
|
|
47
|
+
// identity and doesn't invalidate downstream subscribers such as the cursor layer.
|
|
48
|
+
|
|
44
49
|
/**
|
|
45
50
|
* Returns a list of presence records for all peer collaborators.
|
|
46
51
|
* This will return the latest presence record for each connected user.
|
|
47
52
|
*/
|
|
48
|
-
@computed
|
|
53
|
+
@computed({ isEqual: areArraysShallowEqual })
|
|
49
54
|
getCollaborators(): TLInstancePresence[] {
|
|
50
55
|
const allPresenceRecords = this._getCollaboratorsQuery().get()
|
|
51
56
|
if (!allPresenceRecords.length) return EMPTY_ARRAY
|
|
@@ -63,7 +68,7 @@ export class CollaboratorsManager {
|
|
|
63
68
|
* Returns a list of presence records for all peer collaborators on the current page.
|
|
64
69
|
* This will return the latest presence record for each connected user.
|
|
65
70
|
*/
|
|
66
|
-
@computed
|
|
71
|
+
@computed({ isEqual: areArraysShallowEqual })
|
|
67
72
|
getCollaboratorsOnCurrentPage(): TLInstancePresence[] {
|
|
68
73
|
const currentPageId = this.editor.getCurrentPageId()
|
|
69
74
|
return this.getCollaborators().filter((c) => c.currentPageId === currentPageId)
|
|
@@ -76,7 +81,7 @@ export class CollaboratorsManager {
|
|
|
76
81
|
* highlighted users. Re-evaluates on the visibility clock, so callers don't need to
|
|
77
82
|
* drive their own activity timer.
|
|
78
83
|
*/
|
|
79
|
-
@computed
|
|
84
|
+
@computed({ isEqual: areArraysShallowEqual })
|
|
80
85
|
getVisibleCollaborators(): TLInstancePresence[] {
|
|
81
86
|
const { editor } = this
|
|
82
87
|
const { collaboratorInactiveTimeoutMs, collaboratorIdleTimeoutMs } = editor.options
|
|
@@ -121,7 +126,7 @@ export class CollaboratorsManager {
|
|
|
121
126
|
* Returns a list of presence records for peer collaborators who should currently be
|
|
122
127
|
* shown in the UI, filtered to those on the current page.
|
|
123
128
|
*/
|
|
124
|
-
@computed
|
|
129
|
+
@computed({ isEqual: areArraysShallowEqual })
|
|
125
130
|
getVisibleCollaboratorsOnCurrentPage(): TLInstancePresence[] {
|
|
126
131
|
const currentPageId = this.editor.getCurrentPageId()
|
|
127
132
|
return this.getVisibleCollaborators().filter((c) => c.currentPageId === currentPageId)
|