@tldraw/editor 5.1.1 → 5.2.0-canary.0878dbd31f0d
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/README.md +7 -1
- package/dist-cjs/index.d.ts +33 -50
- package/dist-cjs/index.js +3 -4
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +4 -1
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +3 -3
- package/dist-cjs/lib/components/default-components/DefaultLoadingScreen.js +2 -2
- package/dist-cjs/lib/components/default-components/DefaultLoadingScreen.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultShapeErrorFallback.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultShapeErrorFallback.js.map +3 -3
- package/dist-cjs/lib/components/default-components/DefaultSvgDefs.js +2 -2
- package/dist-cjs/lib/components/default-components/DefaultSvgDefs.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +57 -18
- package/dist-cjs/lib/editor/Editor.js.map +3 -3
- package/dist-cjs/lib/editor/derivations/bindingsIndex.js +2 -2
- package/dist-cjs/lib/editor/derivations/bindingsIndex.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/parentsToChildren.js +2 -2
- package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js +2 -2
- package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js.map +2 -2
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +8 -58
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +3 -3
- package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js +1 -2
- package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +15 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
- package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js +79 -0
- package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js.map +7 -0
- package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
- package/dist-cjs/lib/editor/types/event-types.js +0 -2
- package/dist-cjs/lib/editor/types/event-types.js.map +2 -2
- package/dist-cjs/lib/hooks/usePresence.js.map +2 -2
- package/dist-cjs/lib/license/LicenseProvider.js +3 -1
- package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
- package/dist-cjs/lib/primitives/utils.js +2 -2
- package/dist-cjs/lib/primitives/utils.js.map +2 -2
- package/dist-cjs/lib/utils/dom.js +5 -3
- package/dist-cjs/lib/utils/dom.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +33 -50
- package/dist-esm/index.mjs +2 -6
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +4 -1
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +3 -3
- package/dist-esm/lib/components/default-components/DefaultLoadingScreen.mjs +2 -2
- package/dist-esm/lib/components/default-components/DefaultLoadingScreen.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultShapeErrorFallback.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultShapeErrorFallback.mjs.map +3 -3
- package/dist-esm/lib/components/default-components/DefaultSvgDefs.mjs +2 -2
- package/dist-esm/lib/components/default-components/DefaultSvgDefs.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +57 -18
- package/dist-esm/lib/editor/Editor.mjs.map +3 -3
- package/dist-esm/lib/editor/derivations/bindingsIndex.mjs +2 -2
- package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +2 -2
- package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs +2 -2
- package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +8 -58
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +3 -3
- package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs +1 -2
- package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +15 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
- package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs +59 -0
- package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs.map +7 -0
- package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
- package/dist-esm/lib/editor/types/event-types.mjs +0 -2
- package/dist-esm/lib/editor/types/event-types.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePresence.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseProvider.mjs +3 -1
- package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
- package/dist-esm/lib/primitives/utils.mjs +2 -2
- package/dist-esm/lib/primitives/utils.mjs.map +2 -2
- package/dist-esm/lib/utils/dom.mjs +5 -3
- package/dist-esm/lib/utils/dom.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +2 -0
- package/package.json +8 -8
- package/src/index.ts +1 -5
- package/src/lib/components/default-components/DefaultErrorFallback.tsx +4 -1
- package/src/lib/components/default-components/DefaultLoadingScreen.tsx +1 -1
- package/src/lib/components/default-components/DefaultShapeErrorFallback.tsx +4 -3
- package/src/lib/components/default-components/DefaultSvgDefs.tsx +1 -1
- package/src/lib/editor/Editor.ts +92 -29
- package/src/lib/editor/derivations/bindingsIndex.ts +1 -1
- package/src/lib/editor/derivations/parentsToChildren.ts +1 -1
- package/src/lib/editor/derivations/shapeIdsInCurrentPage.ts +1 -1
- package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +54 -74
- package/src/lib/editor/managers/ClickManager/ClickManager.ts +15 -65
- package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.test.ts +43 -16
- package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +8 -5
- package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +4 -4
- package/src/lib/editor/managers/FocusManager/FocusManager.ts +1 -2
- package/src/lib/editor/managers/FontManager/FontManager.test.ts +13 -9
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +16 -14
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +12 -2
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +27 -2
- package/src/lib/editor/overlays/strokeShapeIndicators.ts +86 -0
- package/src/lib/editor/tools/StateNode.ts +0 -2
- package/src/lib/editor/types/event-types.ts +2 -6
- package/src/lib/hooks/usePresence.ts +2 -2
- package/src/lib/license/LicenseProvider.tsx +3 -1
- package/src/lib/primitives/utils.ts +1 -1
- package/src/lib/utils/dom.ts +5 -3
- package/src/version.ts +3 -3
- package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js +0 -161
- package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js.map +0 -7
- package/dist-esm/lib/editor/overlays/ShapeIndicatorOverlayUtil.mjs +0 -141
- package/dist-esm/lib/editor/overlays/ShapeIndicatorOverlayUtil.mjs.map +0 -7
- package/src/lib/editor/overlays/ShapeIndicatorOverlayUtil.ts +0 -216
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
PageRecordType,
|
|
3
|
+
createUserId,
|
|
4
|
+
type TLInstancePresence,
|
|
5
|
+
type TLUserId,
|
|
6
|
+
} from '@tldraw/tlschema'
|
|
2
7
|
import { vi } from 'vitest'
|
|
8
|
+
import { createTLStore } from '../../../config/createTLStore'
|
|
3
9
|
import type { Editor } from '../../Editor'
|
|
4
10
|
import { CollaboratorsManager } from './CollaboratorsManager'
|
|
5
11
|
|
|
6
12
|
const currentPageId = PageRecordType.createId('page')
|
|
7
13
|
|
|
8
|
-
function createPresence(userId:
|
|
14
|
+
function createPresence(userId: TLUserId): TLInstancePresence {
|
|
9
15
|
return {
|
|
10
16
|
typeName: 'instance_presence',
|
|
11
17
|
id: `instance_presence:${userId}` as TLInstancePresence['id'],
|
|
@@ -34,6 +40,9 @@ function createEditor(presences: TLInstancePresence[] = []) {
|
|
|
34
40
|
}))
|
|
35
41
|
const userGetId = vi.fn(() => 'current-user')
|
|
36
42
|
|
|
43
|
+
const store = createTLStore()
|
|
44
|
+
store.put(presences)
|
|
45
|
+
|
|
37
46
|
const editor = {
|
|
38
47
|
options: {
|
|
39
48
|
collaboratorCheckIntervalMs: 1000,
|
|
@@ -45,14 +54,9 @@ function createEditor(presences: TLInstancePresence[] = []) {
|
|
|
45
54
|
},
|
|
46
55
|
user: {
|
|
47
56
|
getId: userGetId,
|
|
57
|
+
getRecordId: () => createUserId(userGetId()),
|
|
48
58
|
},
|
|
49
|
-
store
|
|
50
|
-
query: {
|
|
51
|
-
records: vi.fn(() => ({
|
|
52
|
-
get: () => presences,
|
|
53
|
-
})),
|
|
54
|
-
},
|
|
55
|
-
},
|
|
59
|
+
store,
|
|
56
60
|
getInstanceState,
|
|
57
61
|
getCurrentPageId: vi.fn(() => currentPageId),
|
|
58
62
|
} as unknown as Editor
|
|
@@ -88,10 +92,19 @@ describe(CollaboratorsManager, () => {
|
|
|
88
92
|
expect(setInterval).toHaveBeenCalledTimes(1)
|
|
89
93
|
})
|
|
90
94
|
|
|
95
|
+
it("excludes the local user's own other sessions", () => {
|
|
96
|
+
const ownSession = createPresence(createUserId('current-user'))
|
|
97
|
+
const peer = createPresence(createUserId('peer'))
|
|
98
|
+
const { editor } = createEditor([ownSession, peer])
|
|
99
|
+
const manager = new CollaboratorsManager(editor)
|
|
100
|
+
|
|
101
|
+
expect(manager.getCollaborators()).toEqual([peer])
|
|
102
|
+
})
|
|
103
|
+
|
|
91
104
|
it('reads instance state once when filtering visible collaborators', () => {
|
|
92
105
|
const { editor, getInstanceState } = createEditor([
|
|
93
|
-
createPresence('user-1'),
|
|
94
|
-
createPresence('user-2'),
|
|
106
|
+
createPresence(createUserId('user-1')),
|
|
107
|
+
createPresence(createUserId('user-2')),
|
|
95
108
|
])
|
|
96
109
|
const manager = new CollaboratorsManager(editor)
|
|
97
110
|
|
|
@@ -101,9 +114,9 @@ describe(CollaboratorsManager, () => {
|
|
|
101
114
|
})
|
|
102
115
|
|
|
103
116
|
it('hides idle collaborators that are following us', () => {
|
|
104
|
-
const presence = createPresence('peer')
|
|
117
|
+
const presence = createPresence(createUserId('peer'))
|
|
105
118
|
presence.lastActivityTimestamp = Date.now() - 4000
|
|
106
|
-
presence.followingUserId = 'current-user'
|
|
119
|
+
presence.followingUserId = createUserId('current-user')
|
|
107
120
|
const { editor } = createEditor([presence])
|
|
108
121
|
const manager = new CollaboratorsManager(editor)
|
|
109
122
|
|
|
@@ -111,9 +124,9 @@ describe(CollaboratorsManager, () => {
|
|
|
111
124
|
})
|
|
112
125
|
|
|
113
126
|
it('shows idle collaborators that are following us when they have a chat message', () => {
|
|
114
|
-
const presence = createPresence('peer')
|
|
127
|
+
const presence = createPresence(createUserId('peer'))
|
|
115
128
|
presence.lastActivityTimestamp = Date.now() - 4000
|
|
116
|
-
presence.followingUserId = 'current-user'
|
|
129
|
+
presence.followingUserId = createUserId('current-user')
|
|
117
130
|
presence.chatMessage = 'hi'
|
|
118
131
|
const { editor } = createEditor([presence])
|
|
119
132
|
const manager = new CollaboratorsManager(editor)
|
|
@@ -122,11 +135,25 @@ describe(CollaboratorsManager, () => {
|
|
|
122
135
|
})
|
|
123
136
|
|
|
124
137
|
it('shows idle collaborators that are not following us', () => {
|
|
125
|
-
const presence = createPresence('peer')
|
|
138
|
+
const presence = createPresence(createUserId('peer'))
|
|
126
139
|
presence.lastActivityTimestamp = Date.now() - 4000
|
|
127
140
|
const { editor } = createEditor([presence])
|
|
128
141
|
const manager = new CollaboratorsManager(editor)
|
|
129
142
|
|
|
130
143
|
expect(manager.getVisibleCollaborators()).toHaveLength(1)
|
|
131
144
|
})
|
|
145
|
+
|
|
146
|
+
it('shows newly-joined collaborators that have not recorded any activity yet', () => {
|
|
147
|
+
// A peer who has joined but not moved their pointer broadcasts the default
|
|
148
|
+
// `lastActivityTimestamp` of 0. They should still be treated as active so
|
|
149
|
+
// they appear in the people menu / face pile. See issue #9017.
|
|
150
|
+
const zero = createPresence(createUserId('zero'))
|
|
151
|
+
zero.lastActivityTimestamp = 0
|
|
152
|
+
const nullish = createPresence(createUserId('nullish'))
|
|
153
|
+
nullish.lastActivityTimestamp = null
|
|
154
|
+
const { editor } = createEditor([zero, nullish])
|
|
155
|
+
const manager = new CollaboratorsManager(editor)
|
|
156
|
+
|
|
157
|
+
expect(manager.getVisibleCollaborators()).toHaveLength(2)
|
|
158
|
+
})
|
|
132
159
|
})
|
|
@@ -37,7 +37,7 @@ export class CollaboratorsManager {
|
|
|
37
37
|
@computed
|
|
38
38
|
private _getCollaboratorsQuery() {
|
|
39
39
|
return this.editor.store.query.records('instance_presence', () => ({
|
|
40
|
-
userId: { neq: this.editor.user.
|
|
40
|
+
userId: { neq: this.editor.user.getRecordId() },
|
|
41
41
|
}))
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -88,14 +88,17 @@ export class CollaboratorsManager {
|
|
|
88
88
|
if (!collaborators.length) return EMPTY_ARRAY
|
|
89
89
|
|
|
90
90
|
const { followingUserId, highlightedUserIds } = this.editor.getInstanceState()
|
|
91
|
-
const currentUserId = this.editor.user.
|
|
91
|
+
const currentUserId = this.editor.user.getRecordId()
|
|
92
92
|
|
|
93
93
|
return collaborators.filter((presence) => {
|
|
94
94
|
const { lastActivityTimestamp, userId, chatMessage } = presence
|
|
95
95
|
|
|
96
|
-
// Treat a missing `lastActivityTimestamp` as "active right now"
|
|
97
|
-
// so newly-joined peers aren't immediately classified as
|
|
98
|
-
|
|
96
|
+
// Treat a missing or zero `lastActivityTimestamp` as "active right now"
|
|
97
|
+
// (elapsed = 0) so newly-joined peers aren't immediately classified as
|
|
98
|
+
// idle/inactive. The broadcast default for peers who haven't moved their
|
|
99
|
+
// pointer yet is `0` (e.g. someone on a touch device who joins and just
|
|
100
|
+
// watches), so a plain `?? now` would leave them hidden. See issue #9017.
|
|
101
|
+
const elapsed = lastActivityTimestamp ? Math.max(0, now - lastActivityTimestamp) : 0
|
|
99
102
|
|
|
100
103
|
if (elapsed > collaboratorInactiveTimeoutMs) {
|
|
101
104
|
// Inactive: If they're inactive, only show if we're following them or they're highlighted
|
|
@@ -14,7 +14,7 @@ describe('FocusManager', () => {
|
|
|
14
14
|
getInstanceState: Mock
|
|
15
15
|
updateInstanceState: Mock
|
|
16
16
|
getContainer: Mock
|
|
17
|
-
|
|
17
|
+
getEditingShapeId: Mock
|
|
18
18
|
getSelectedShapeIds: Mock
|
|
19
19
|
complete: Mock
|
|
20
20
|
}
|
|
@@ -51,7 +51,7 @@ describe('FocusManager', () => {
|
|
|
51
51
|
updateInstanceState: vi.fn(),
|
|
52
52
|
getContainer: vi.fn(() => mockContainer),
|
|
53
53
|
getContainerDocument: vi.fn(() => document),
|
|
54
|
-
|
|
54
|
+
getEditingShapeId: vi.fn(() => null),
|
|
55
55
|
getSelectedShapeIds: vi.fn(() => []),
|
|
56
56
|
complete: vi.fn(),
|
|
57
57
|
} as any
|
|
@@ -243,7 +243,7 @@ describe('FocusManager', () => {
|
|
|
243
243
|
})
|
|
244
244
|
|
|
245
245
|
it('should return early when editor is in editing mode', () => {
|
|
246
|
-
editor.
|
|
246
|
+
editor.getEditingShapeId.mockReturnValue('shape:1')
|
|
247
247
|
const event = new KeyboardEvent('keydown', { key: 'Tab' })
|
|
248
248
|
|
|
249
249
|
keydownHandler(event)
|
|
@@ -407,7 +407,7 @@ describe('FocusManager', () => {
|
|
|
407
407
|
const keydownCall = addEventListenerCalls.find((call: any) => call[0] === 'keydown')
|
|
408
408
|
const keydownHandler = keydownCall![1]
|
|
409
409
|
|
|
410
|
-
editor.
|
|
410
|
+
editor.getEditingShapeId.mockReturnValue('shape:1') // Editing mode
|
|
411
411
|
|
|
412
412
|
const event = new KeyboardEvent('keydown', { key: 'Tab' })
|
|
413
413
|
keydownHandler(event)
|
|
@@ -63,8 +63,7 @@ export class FocusManager {
|
|
|
63
63
|
const activeEl = container.ownerDocument.activeElement
|
|
64
64
|
// Edit mode should remove the focus ring, however if the active element's
|
|
65
65
|
// parent is the contextual toolbar, then allow it.
|
|
66
|
-
if (this.editor.
|
|
67
|
-
return
|
|
66
|
+
if (this.editor.getEditingShapeId() && !activeEl?.closest('.tlui-contextual-toolbar')) return
|
|
68
67
|
if (activeEl === container && this.editor.getSelectedShapeIds().length > 0) return
|
|
69
68
|
if (['Tab', 'ArrowUp', 'ArrowDown'].includes(keyEvent.key)) {
|
|
70
69
|
container.classList.remove('tl-container__no-focus-ring')
|
|
@@ -15,12 +15,14 @@ import { FontManager } from './FontManager'
|
|
|
15
15
|
vi.mock('../../Editor')
|
|
16
16
|
|
|
17
17
|
// Mock globals
|
|
18
|
-
global.FontFace = vi.fn().mockImplementation((family, src, descriptors)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
global.FontFace = vi.fn().mockImplementation(function (family: any, src: any, descriptors: any) {
|
|
19
|
+
return {
|
|
20
|
+
family,
|
|
21
|
+
src,
|
|
22
|
+
...descriptors,
|
|
23
|
+
load: vi.fn(() => Promise.resolve()),
|
|
24
|
+
}
|
|
25
|
+
})
|
|
24
26
|
|
|
25
27
|
Object.defineProperty(global.document, 'fonts', {
|
|
26
28
|
value: {
|
|
@@ -200,9 +202,11 @@ describe('FontManager', () => {
|
|
|
200
202
|
const font = createMockFont()
|
|
201
203
|
const error = new Error('Font load failed')
|
|
202
204
|
|
|
203
|
-
;(global.FontFace as Mock).
|
|
204
|
-
|
|
205
|
-
|
|
205
|
+
;(global.FontFace as Mock).mockImplementationOnce(function () {
|
|
206
|
+
return {
|
|
207
|
+
family: font.family,
|
|
208
|
+
load: vi.fn(() => Promise.reject(error)),
|
|
209
|
+
}
|
|
206
210
|
})
|
|
207
211
|
|
|
208
212
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
@@ -82,20 +82,22 @@ const mockEditor = {
|
|
|
82
82
|
getContainerDocument: vi.fn(() => mockDocument),
|
|
83
83
|
} as unknown as Editor
|
|
84
84
|
|
|
85
|
-
global.Range = vi.fn(()
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
85
|
+
global.Range = vi.fn(function () {
|
|
86
|
+
return {
|
|
87
|
+
setStart: vi.fn(),
|
|
88
|
+
setEnd: vi.fn(),
|
|
89
|
+
getClientRects: vi.fn(() => [
|
|
90
|
+
{
|
|
91
|
+
width: 10,
|
|
92
|
+
height: 16,
|
|
93
|
+
left: 0,
|
|
94
|
+
top: 0,
|
|
95
|
+
right: 10,
|
|
96
|
+
bottom: 16,
|
|
97
|
+
},
|
|
98
|
+
]),
|
|
99
|
+
}
|
|
100
|
+
}) as any
|
|
99
101
|
|
|
100
102
|
describe('TextManager', () => {
|
|
101
103
|
let textManager: TextManager
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { atom } from '@tldraw/state'
|
|
2
|
+
import { createUserId } from '@tldraw/tlschema'
|
|
2
3
|
import { Mocked, vi } from 'vitest'
|
|
3
4
|
import { TLCurrentUser } from '../../../config/createTLCurrentUser'
|
|
4
5
|
import { TLUserPreferences, defaultUserPreferences } from '../../../config/TLUserPreferences'
|
|
@@ -296,8 +297,17 @@ describe('UserPreferencesManager', () => {
|
|
|
296
297
|
userPreferencesManager = new UserPreferencesManager(mockUser, 'light')
|
|
297
298
|
})
|
|
298
299
|
|
|
299
|
-
describe('
|
|
300
|
-
it('should return user id', () => {
|
|
300
|
+
describe('getExternalId / getRecordId', () => {
|
|
301
|
+
it('should return the raw external user id', () => {
|
|
302
|
+
expect(userPreferencesManager.getExternalId()).toBe(mockUserPreferences.id)
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('should return the prefixed record id', () => {
|
|
306
|
+
expect(userPreferencesManager.getRecordId()).toBe(createUserId(mockUserPreferences.id))
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('getId() still returns the external id', () => {
|
|
310
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
301
311
|
expect(userPreferencesManager.getId()).toBe(mockUserPreferences.id)
|
|
302
312
|
})
|
|
303
313
|
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { atom, computed } from '@tldraw/state'
|
|
2
|
+
import { createUserId, TLUserId } from '@tldraw/tlschema'
|
|
2
3
|
import { TLCurrentUser } from '../../../config/createTLCurrentUser'
|
|
3
4
|
import { TLUserPreferences, defaultUserPreferences } from '../../../config/TLUserPreferences'
|
|
4
5
|
import { getGlobalWindow } from '../../../utils/dom'
|
|
@@ -39,7 +40,7 @@ export class UserPreferencesManager {
|
|
|
39
40
|
}
|
|
40
41
|
@computed getUserPreferences() {
|
|
41
42
|
return {
|
|
42
|
-
id: this.
|
|
43
|
+
id: this.getExternalId(),
|
|
43
44
|
name: this.getName(),
|
|
44
45
|
locale: this.getLocale(),
|
|
45
46
|
color: this.getColor(),
|
|
@@ -89,10 +90,34 @@ export class UserPreferencesManager {
|
|
|
89
90
|
)
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
|
|
93
|
+
/**
|
|
94
|
+
* The current user's raw, app-provided id — the value set in the user's
|
|
95
|
+
* {@link @tldraw/editor#TLUserPreferences}. Use this when you need the id your application
|
|
96
|
+
* assigned to the user. To compare against or look up store records, use
|
|
97
|
+
* {@link UserPreferencesManager.getRecordId} instead.
|
|
98
|
+
*/
|
|
99
|
+
@computed getExternalId(): string {
|
|
93
100
|
return this.user.userPreferences.get().id
|
|
94
101
|
}
|
|
95
102
|
|
|
103
|
+
/**
|
|
104
|
+
* @deprecated Use {@link UserPreferencesManager.getExternalId} for the raw app-provided id, or
|
|
105
|
+
* {@link UserPreferencesManager.getRecordId} for the prefixed `TLUserId` record id.
|
|
106
|
+
*/
|
|
107
|
+
@computed getId() {
|
|
108
|
+
return this.getExternalId()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The current user's id as a tldraw {@link @tldraw/tlschema#TLUserId} record id (prefixed
|
|
113
|
+
* with `user:`). Use this when comparing against or looking up store records, such as a
|
|
114
|
+
* presence record's `userId` or `followingUserId`. For the raw, app-provided id, use
|
|
115
|
+
* {@link UserPreferencesManager.getExternalId}.
|
|
116
|
+
*/
|
|
117
|
+
@computed getRecordId(): TLUserId {
|
|
118
|
+
return createUserId(this.getExternalId())
|
|
119
|
+
}
|
|
120
|
+
|
|
96
121
|
@computed getName() {
|
|
97
122
|
return this.user.userPreferences.get().name?.trim() ?? defaultUserPreferences.name
|
|
98
123
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createComputedCache } from '@tldraw/store'
|
|
2
|
+
import { TLShape, TLShapeId } from '@tldraw/tlschema'
|
|
3
|
+
import type { Editor } from '../Editor'
|
|
4
|
+
|
|
5
|
+
const indicatorPathCache = createComputedCache(
|
|
6
|
+
'shapeIndicatorPath',
|
|
7
|
+
(editor: Editor, shape: TLShape) => {
|
|
8
|
+
const util = editor.getShapeUtil(shape)
|
|
9
|
+
return util.getIndicatorPath(shape)
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
areRecordsEqual(a, b) {
|
|
13
|
+
return a.props === b.props
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Combine every batchable shape indicator into a single page-space `Path2D` and
|
|
20
|
+
* emit one stroke call. Shapes whose indicator needs an evenodd clip (e.g.
|
|
21
|
+
* arrows with labels or complex arrowheads) can't be batched — they still
|
|
22
|
+
* stroke individually inside a save/restore with `ctx.clip` applied.
|
|
23
|
+
*
|
|
24
|
+
* Shared by any overlay util that paints shape indicators (e.g. collaborator
|
|
25
|
+
* selections).
|
|
26
|
+
*
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
29
|
+
export function strokeShapeIndicators(
|
|
30
|
+
editor: Editor,
|
|
31
|
+
ctx: CanvasRenderingContext2D,
|
|
32
|
+
shapeIds: TLShapeId[]
|
|
33
|
+
): void {
|
|
34
|
+
if (shapeIds.length === 0) return
|
|
35
|
+
|
|
36
|
+
const batched = new Path2D()
|
|
37
|
+
|
|
38
|
+
for (const shapeId of shapeIds) {
|
|
39
|
+
const shape = editor.getShape(shapeId)
|
|
40
|
+
if (!shape || shape.isLocked) continue
|
|
41
|
+
|
|
42
|
+
const pageTransform = editor.getShapePageTransform(shape)
|
|
43
|
+
if (!pageTransform) continue
|
|
44
|
+
|
|
45
|
+
const indicatorPath = indicatorPathCache.get(editor, shape.id)
|
|
46
|
+
if (!indicatorPath) continue
|
|
47
|
+
|
|
48
|
+
if (indicatorPath instanceof Path2D) {
|
|
49
|
+
batched.addPath(indicatorPath, pageTransform)
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { path, clipPath, additionalPaths } = indicatorPath
|
|
54
|
+
|
|
55
|
+
if (!clipPath) {
|
|
56
|
+
batched.addPath(path, pageTransform)
|
|
57
|
+
if (additionalPaths) {
|
|
58
|
+
for (const p of additionalPaths) batched.addPath(p, pageTransform)
|
|
59
|
+
}
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Clipped case: fall back to an individual stroke. Rare (arrows with
|
|
64
|
+
// labels / complex arrowheads), so the extra save/restore/stroke
|
|
65
|
+
// pair per such shape isn't worth batching away.
|
|
66
|
+
ctx.save()
|
|
67
|
+
ctx.transform(
|
|
68
|
+
pageTransform.a,
|
|
69
|
+
pageTransform.b,
|
|
70
|
+
pageTransform.c,
|
|
71
|
+
pageTransform.d,
|
|
72
|
+
pageTransform.e,
|
|
73
|
+
pageTransform.f
|
|
74
|
+
)
|
|
75
|
+
ctx.save()
|
|
76
|
+
ctx.clip(clipPath, 'evenodd')
|
|
77
|
+
ctx.stroke(path)
|
|
78
|
+
ctx.restore()
|
|
79
|
+
if (additionalPaths) {
|
|
80
|
+
for (const p of additionalPaths) ctx.stroke(p)
|
|
81
|
+
}
|
|
82
|
+
ctx.restore()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
ctx.stroke(batched)
|
|
86
|
+
}
|
|
@@ -268,8 +268,6 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
|
|
|
268
268
|
onLongPress?(info: TLPointerEventInfo): void
|
|
269
269
|
onPointerUp?(info: TLPointerEventInfo): void
|
|
270
270
|
onDoubleClick?(info: TLClickEventInfo): void
|
|
271
|
-
onTripleClick?(info: TLClickEventInfo): void
|
|
272
|
-
onQuadrupleClick?(info: TLClickEventInfo): void
|
|
273
271
|
onRightClick?(info: TLPointerEventInfo): void
|
|
274
272
|
onMiddleClick?(info: TLPointerEventInfo): void
|
|
275
273
|
onKeyDown?(info: TLKeyboardEventInfo): void
|
|
@@ -24,7 +24,7 @@ export type TLPointerEventName =
|
|
|
24
24
|
| 'middle_click'
|
|
25
25
|
|
|
26
26
|
/** @public */
|
|
27
|
-
export type TLCLickEventName = 'double_click'
|
|
27
|
+
export type TLCLickEventName = 'double_click'
|
|
28
28
|
|
|
29
29
|
/** @public */
|
|
30
30
|
export type TLPinchEventName = 'pinch_start' | 'pinch' | 'pinch_end'
|
|
@@ -72,7 +72,7 @@ export type TLClickEventInfo = TLBaseEventInfo & {
|
|
|
72
72
|
point: VecLike
|
|
73
73
|
pointerId: number
|
|
74
74
|
button: number
|
|
75
|
-
phase: 'down' | 'up' | 'settle'
|
|
75
|
+
phase: 'down' | 'up' | 'settle-down' | 'settle-up'
|
|
76
76
|
} & TLPointerEventTarget
|
|
77
77
|
|
|
78
78
|
/** @public */
|
|
@@ -173,8 +173,6 @@ export interface TLEventHandlers {
|
|
|
173
173
|
onLongPress: TLPointerEvent
|
|
174
174
|
onRightClick: TLPointerEvent
|
|
175
175
|
onDoubleClick: TLClickEvent
|
|
176
|
-
onTripleClick: TLClickEvent
|
|
177
|
-
onQuadrupleClick: TLClickEvent
|
|
178
176
|
onMiddleClick: TLPointerEvent
|
|
179
177
|
onPointerUp: TLPointerEvent
|
|
180
178
|
onKeyDown: TLKeyboardEvent
|
|
@@ -206,7 +204,5 @@ export const EVENT_NAME_MAP: Record<
|
|
|
206
204
|
complete: 'onComplete',
|
|
207
205
|
interrupt: 'onInterrupt',
|
|
208
206
|
double_click: 'onDoubleClick',
|
|
209
|
-
triple_click: 'onTripleClick',
|
|
210
|
-
quadruple_click: 'onQuadrupleClick',
|
|
211
207
|
tick: 'onTick',
|
|
212
208
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useValue } from '@tldraw/state-react'
|
|
2
|
-
import { TLInstancePresence } from '@tldraw/tlschema'
|
|
2
|
+
import { TLInstancePresence, TLUserId } from '@tldraw/tlschema'
|
|
3
3
|
import { useEditor } from './useEditor'
|
|
4
4
|
|
|
5
5
|
// TODO: maybe move this to a computed property on the App class?
|
|
@@ -7,7 +7,7 @@ import { useEditor } from './useEditor'
|
|
|
7
7
|
* @returns The latest presence of the user matching userId
|
|
8
8
|
* @public
|
|
9
9
|
*/
|
|
10
|
-
export function usePresence(userId:
|
|
10
|
+
export function usePresence(userId: TLUserId): TLInstancePresence | null {
|
|
11
11
|
const editor = useEditor()
|
|
12
12
|
|
|
13
13
|
const latestPresence = useValue(
|
|
@@ -6,7 +6,9 @@ import { LicenseManager } from './LicenseManager'
|
|
|
6
6
|
export const LicenseContext = createContext({} as LicenseManager)
|
|
7
7
|
|
|
8
8
|
/** @internal */
|
|
9
|
-
export
|
|
9
|
+
export function useLicenseContext() {
|
|
10
|
+
return useContext(LicenseContext)
|
|
11
|
+
}
|
|
10
12
|
|
|
11
13
|
function shouldHideEditorAfterDelay(licenseState: string): boolean {
|
|
12
14
|
return licenseState === 'expired' || licenseState === 'unlicensed-production'
|
|
@@ -363,7 +363,7 @@ export function toFixed(v: number) {
|
|
|
363
363
|
* Check if a float is safe to use. ie: Not too big or small.
|
|
364
364
|
* @public
|
|
365
365
|
*/
|
|
366
|
-
export
|
|
366
|
+
export function isSafeFloat(n: number) {
|
|
367
367
|
return Math.abs(n) < Number.MAX_SAFE_INTEGER
|
|
368
368
|
}
|
|
369
369
|
|
package/src/lib/utils/dom.ts
CHANGED
|
@@ -86,14 +86,16 @@ export function releasePointerCapture(
|
|
|
86
86
|
*
|
|
87
87
|
* @public
|
|
88
88
|
*/
|
|
89
|
-
export
|
|
89
|
+
export function stopEventPropagation(e: any) {
|
|
90
|
+
return e.stopPropagation()
|
|
91
|
+
}
|
|
90
92
|
|
|
91
93
|
/** @internal */
|
|
92
|
-
export
|
|
94
|
+
export function setStyleProperty(
|
|
93
95
|
elm: HTMLElement | null,
|
|
94
96
|
property: string,
|
|
95
97
|
value: string | number
|
|
96
|
-
)
|
|
98
|
+
) {
|
|
97
99
|
if (!elm) return
|
|
98
100
|
elm.style.setProperty(property, String(value))
|
|
99
101
|
}
|
package/src/version.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// This file is automatically generated by internal/scripts/refresh-assets.ts.
|
|
2
2
|
// Do not edit manually. Or do, I'm a comment, not a cop.
|
|
3
3
|
|
|
4
|
-
export const version = '5.
|
|
4
|
+
export const version = '5.2.0-canary.0878dbd31f0d'
|
|
5
5
|
export const publishDates = {
|
|
6
6
|
major: '2026-05-06T16:28:18.473Z',
|
|
7
|
-
minor: '2026-06-
|
|
8
|
-
patch: '2026-06-12T16:
|
|
7
|
+
minor: '2026-06-12T16:59:24.514Z',
|
|
8
|
+
patch: '2026-06-12T16:59:24.514Z',
|
|
9
9
|
}
|