@tldraw/editor 5.2.0-next.e2b8d10bf10e → 5.2.0
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 +112 -53
- package/dist-cjs/index.js +7 -8
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/components/MenuClickCapture.js +8 -5
- package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
- package/dist-cjs/lib/components/Shape.js +47 -1
- package/dist-cjs/lib/components/Shape.js.map +3 -3
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +1 -16
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +97 -45
- package/dist-cjs/lib/editor/Editor.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/FontManager/FontManager.js +28 -4
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +3 -3
- package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js +24 -2
- package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +14 -3
- package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +4 -2
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +7 -3
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +0 -1
- package/dist-cjs/lib/editor/managers/TickManager/TickManager.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/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +4 -0
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
- package/dist-cjs/lib/{hooks/useTransform.js → editor/tools/cancelShapeCreationOnLongPress.js} +8 -23
- package/dist-cjs/lib/editor/tools/cancelShapeCreationOnLongPress.js.map +7 -0
- package/dist-cjs/lib/editor/types/event-types.js.map +2 -2
- package/dist-cjs/lib/globals/environment.js +6 -1
- package/dist-cjs/lib/globals/environment.js.map +2 -2
- package/dist-cjs/lib/hooks/EditorComponentsContext.js.map +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js +17 -8
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useEditorComponents.js +0 -1
- package/dist-cjs/lib/hooks/useEditorComponents.js.map +2 -2
- package/dist-cjs/lib/hooks/usePresence.js.map +2 -2
- package/dist-cjs/lib/hooks/useScreenBounds.js +1 -0
- package/dist-cjs/lib/hooks/useScreenBounds.js.map +2 -2
- package/dist-cjs/lib/license/LicenseManager.js +5 -1
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/primitives/Vec.js +3 -2
- package/dist-cjs/lib/primitives/Vec.js.map +2 -2
- package/dist-cjs/lib/utils/dom.js +12 -0
- package/dist-cjs/lib/utils/dom.js.map +2 -2
- package/dist-cjs/lib/utils/getPointerInfo.js +2 -1
- package/dist-cjs/lib/utils/getPointerInfo.js.map +2 -2
- package/dist-cjs/lib/utils/pointer.js +37 -0
- package/dist-cjs/lib/utils/pointer.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 +112 -53
- package/dist-esm/index.mjs +9 -13
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/components/MenuClickCapture.mjs +8 -5
- package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
- package/dist-esm/lib/components/Shape.mjs +48 -2
- package/dist-esm/lib/components/Shape.mjs.map +3 -3
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +1 -16
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +97 -45
- package/dist-esm/lib/editor/Editor.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/FontManager/FontManager.mjs +28 -4
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +3 -3
- package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs +24 -2
- package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +14 -3
- package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +4 -2
- package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +7 -3
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +0 -1
- package/dist-esm/lib/editor/managers/TickManager/TickManager.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/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +4 -0
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/cancelShapeCreationOnLongPress.mjs +8 -0
- package/dist-esm/lib/editor/tools/cancelShapeCreationOnLongPress.mjs.map +7 -0
- package/dist-esm/lib/editor/types/event-types.mjs.map +2 -2
- package/dist-esm/lib/globals/environment.mjs +6 -1
- package/dist-esm/lib/globals/environment.mjs.map +2 -2
- package/dist-esm/lib/hooks/EditorComponentsContext.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +17 -8
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useEditorComponents.mjs +0 -1
- package/dist-esm/lib/hooks/useEditorComponents.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePresence.mjs.map +2 -2
- package/dist-esm/lib/hooks/useScreenBounds.mjs +1 -0
- package/dist-esm/lib/hooks/useScreenBounds.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseManager.mjs +5 -1
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/primitives/Vec.mjs +3 -2
- package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
- package/dist-esm/lib/utils/dom.mjs +12 -0
- package/dist-esm/lib/utils/dom.mjs.map +2 -2
- package/dist-esm/lib/utils/getPointerInfo.mjs +2 -1
- package/dist-esm/lib/utils/getPointerInfo.mjs.map +2 -2
- package/dist-esm/lib/utils/pointer.mjs +17 -0
- package/dist-esm/lib/utils/pointer.mjs.map +7 -0
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +15 -3
- package/package.json +11 -8
- package/src/index.ts +4 -10
- package/src/lib/components/MenuClickCapture.tsx +8 -4
- package/src/lib/components/Shape.tsx +88 -1
- package/src/lib/components/default-components/DefaultCanvas.tsx +1 -17
- package/src/lib/editor/Editor.test.ts +36 -0
- package/src/lib/editor/Editor.ts +145 -55
- 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/FontManager/FontManager.test.ts +37 -10
- package/src/lib/editor/managers/FontManager/FontManager.ts +46 -1
- package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +32 -0
- package/src/lib/editor/managers/HistoryManager/HistoryManager.ts +34 -4
- package/src/lib/editor/managers/InputsManager/InputsManager.test.ts +61 -0
- package/src/lib/editor/managers/InputsManager/InputsManager.ts +16 -4
- package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +9 -2
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +16 -14
- package/src/lib/editor/managers/TextManager/TextManager.ts +17 -2
- package/src/lib/editor/managers/TickManager/TickManager.test.ts +0 -40
- package/src/lib/editor/managers/TickManager/TickManager.ts +0 -1
- 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/shapes/ShapeUtil.ts +41 -0
- package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +5 -0
- package/src/lib/editor/tools/cancelShapeCreationOnLongPress.ts +31 -0
- package/src/lib/editor/types/event-types.ts +7 -0
- package/src/lib/globals/environment.ts +7 -0
- package/src/lib/hooks/EditorComponentsContext.tsx +0 -2
- package/src/lib/hooks/useCanvasEvents.ts +24 -12
- package/src/lib/hooks/useEditorComponents.tsx +0 -1
- package/src/lib/hooks/usePresence.ts +2 -2
- package/src/lib/hooks/useScreenBounds.ts +3 -0
- package/src/lib/license/LicenseManager.test.ts +24 -16
- package/src/lib/license/LicenseManager.ts +8 -2
- package/src/lib/primitives/Vec.ts +5 -2
- package/src/lib/test/ContentElementSlot.test.tsx +201 -0
- package/src/lib/utils/dom.ts +34 -0
- package/src/lib/utils/getPointerInfo.ts +2 -1
- package/src/lib/utils/pointer.test.ts +76 -0
- package/src/lib/utils/pointer.ts +42 -0
- package/src/version.ts +3 -3
- package/dist-cjs/lib/components/default-components/DefaultSelectionBackground.js +0 -49
- package/dist-cjs/lib/components/default-components/DefaultSelectionBackground.js.map +0 -7
- package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js +0 -161
- package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js.map +0 -7
- package/dist-cjs/lib/hooks/useTransform.js.map +0 -7
- package/dist-esm/lib/components/default-components/DefaultSelectionBackground.mjs +0 -19
- package/dist-esm/lib/components/default-components/DefaultSelectionBackground.mjs.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/dist-esm/lib/hooks/useTransform.mjs +0 -23
- package/dist-esm/lib/hooks/useTransform.mjs.map +0 -7
- package/src/lib/components/default-components/DefaultSelectionBackground.tsx +0 -25
- package/src/lib/editor/overlays/ShapeIndicatorOverlayUtil.ts +0 -216
- package/src/lib/hooks/useTransform.ts +0 -30
|
@@ -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
|
|
@@ -9,18 +9,20 @@ import {
|
|
|
9
9
|
import { IndexKey } from '@tldraw/utils'
|
|
10
10
|
import { Mock, Mocked, vi } from 'vitest'
|
|
11
11
|
import { Editor } from '../../Editor'
|
|
12
|
-
import { FontManager } from './FontManager'
|
|
12
|
+
import { clearFontFaceCacheForTests, FontManager } from './FontManager'
|
|
13
13
|
|
|
14
14
|
// Mock the Editor class
|
|
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: {
|
|
@@ -71,6 +73,7 @@ describe('FontManager', () => {
|
|
|
71
73
|
|
|
72
74
|
beforeEach(() => {
|
|
73
75
|
vi.clearAllMocks()
|
|
76
|
+
clearFontFaceCacheForTests()
|
|
74
77
|
|
|
75
78
|
mockAssetUrls = {
|
|
76
79
|
'test-font.woff2': 'https://example.com/fonts/test-font.woff2',
|
|
@@ -200,9 +203,11 @@ describe('FontManager', () => {
|
|
|
200
203
|
const font = createMockFont()
|
|
201
204
|
const error = new Error('Font load failed')
|
|
202
205
|
|
|
203
|
-
;(global.FontFace as Mock).
|
|
204
|
-
|
|
205
|
-
|
|
206
|
+
;(global.FontFace as Mock).mockImplementationOnce(function () {
|
|
207
|
+
return {
|
|
208
|
+
family: font.family,
|
|
209
|
+
load: vi.fn(() => Promise.reject(error)),
|
|
210
|
+
}
|
|
206
211
|
})
|
|
207
212
|
|
|
208
213
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
@@ -310,4 +315,26 @@ describe('FontManager', () => {
|
|
|
310
315
|
await expect(fontManager.ensureFontIsLoaded(minimalFont)).resolves.toBeUndefined()
|
|
311
316
|
})
|
|
312
317
|
})
|
|
318
|
+
|
|
319
|
+
describe('findOrCreateFontFace caching', () => {
|
|
320
|
+
it('reuses a font face across manager instances instead of recreating it on remount', async () => {
|
|
321
|
+
const font = createMockFont()
|
|
322
|
+
await fontManager.ensureFontIsLoaded(font)
|
|
323
|
+
expect(global.FontFace).toHaveBeenCalledTimes(1)
|
|
324
|
+
|
|
325
|
+
// A fresh manager on the same document simulates an editor remount.
|
|
326
|
+
const remounted = new FontManager(editor, mockAssetUrls)
|
|
327
|
+
await remounted.ensureFontIsLoaded(font)
|
|
328
|
+
|
|
329
|
+
// The per-document cache lets the second manager reuse the existing face
|
|
330
|
+
// rather than re-scanning document.fonts and creating a new one.
|
|
331
|
+
expect(global.FontFace).toHaveBeenCalledTimes(1)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('creates separate font faces for fonts with different descriptors', async () => {
|
|
335
|
+
await fontManager.ensureFontIsLoaded(createMockFont({ weight: 'normal' }))
|
|
336
|
+
await fontManager.ensureFontIsLoaded(createMockFont({ weight: 'bold' }))
|
|
337
|
+
expect(global.FontFace).toHaveBeenCalledTimes(2)
|
|
338
|
+
})
|
|
339
|
+
})
|
|
313
340
|
})
|
|
@@ -145,7 +145,26 @@ export class FontManager {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
private findOrCreateFontFace(font: TLFontFace) {
|
|
148
|
-
const
|
|
148
|
+
const containerDocument = this.editor.getContainerDocument()
|
|
149
|
+
|
|
150
|
+
// `findOrCreateFontFace` runs for every font on every editor mount, and a fresh
|
|
151
|
+
// editor (e.g. switching documents) gets a fresh FontManager with no memory of the
|
|
152
|
+
// previous one. The dedup below is an O(n) scan of the document's whole FontFaceSet,
|
|
153
|
+
// so without a cache that scan re-ran on every mount (measurably expensive on mobile
|
|
154
|
+
// Safari). Cache the resolved FontFace per document so repeated lookups - and
|
|
155
|
+
// remounts - are O(1). Keyed per document for cross-window embedding.
|
|
156
|
+
let cache = fontFaceCacheByDocument.get(containerDocument)
|
|
157
|
+
if (!cache) {
|
|
158
|
+
cache = new Map()
|
|
159
|
+
fontFaceCacheByDocument.set(containerDocument, cache)
|
|
160
|
+
}
|
|
161
|
+
const key = getFontFaceCacheKey(font)
|
|
162
|
+
const cached = cache.get(key)
|
|
163
|
+
if (cached) return cached
|
|
164
|
+
|
|
165
|
+
const fonts = containerDocument.fonts
|
|
166
|
+
// On a cache miss we still scan once, so font faces added outside this manager
|
|
167
|
+
// (e.g. preloaded fonts) are reused rather than duplicated.
|
|
149
168
|
for (const existing of fonts) {
|
|
150
169
|
if (
|
|
151
170
|
existing.family === font.family &&
|
|
@@ -153,6 +172,7 @@ export class FontManager {
|
|
|
153
172
|
([key, defaultValue]) => existing[key] === (font[key] ?? defaultValue)
|
|
154
173
|
)
|
|
155
174
|
) {
|
|
175
|
+
cache.set(key, existing)
|
|
156
176
|
return existing
|
|
157
177
|
}
|
|
158
178
|
}
|
|
@@ -164,6 +184,7 @@ export class FontManager {
|
|
|
164
184
|
})
|
|
165
185
|
|
|
166
186
|
fonts.add(instance)
|
|
187
|
+
cache.set(key, instance)
|
|
167
188
|
|
|
168
189
|
return instance
|
|
169
190
|
}
|
|
@@ -205,3 +226,27 @@ const defaultFontFaceDescriptors = {
|
|
|
205
226
|
descentOverride: 'normal',
|
|
206
227
|
lineGapOverride: 'normal',
|
|
207
228
|
}
|
|
229
|
+
|
|
230
|
+
// A FontFace is fully determined by its family and descriptors, so resolved faces can be
|
|
231
|
+
// cached per document and reused across FontManager instances (e.g. editor remounts),
|
|
232
|
+
// turning the per-lookup FontFaceSet scan into an O(1) map lookup. Faces are never removed
|
|
233
|
+
// from `document.fonts` (FontManager.dispose leaves them), so the cache stays valid for the
|
|
234
|
+
// document's lifetime. Keyed per document for cross-window embedding.
|
|
235
|
+
let fontFaceCacheByDocument = new WeakMap<Document, Map<string, FontFace>>()
|
|
236
|
+
|
|
237
|
+
function getFontFaceCacheKey(font: TLFontFace): string {
|
|
238
|
+
return JSON.stringify([
|
|
239
|
+
font.family,
|
|
240
|
+
...objectMapEntries(defaultFontFaceDescriptors).map(
|
|
241
|
+
([key, defaultValue]) => font[key] ?? defaultValue
|
|
242
|
+
),
|
|
243
|
+
])
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Resets the per-document font-face cache. Only intended for tests.
|
|
248
|
+
* @internal
|
|
249
|
+
*/
|
|
250
|
+
export function clearFontFaceCacheForTests() {
|
|
251
|
+
fontFaceCacheByDocument = new WeakMap()
|
|
252
|
+
}
|
|
@@ -584,6 +584,38 @@ describe('HistoryManager getters and utilities', () => {
|
|
|
584
584
|
store.update(ids.a, (s) => ({ ...s, value: 2 }))
|
|
585
585
|
expect(manager.getNumRedos()).toBe(0)
|
|
586
586
|
})
|
|
587
|
+
|
|
588
|
+
it('should only report replaying while undoing or redoing', () => {
|
|
589
|
+
const replayStates: boolean[] = []
|
|
590
|
+
const dispose = store.addHistoryInterceptor(() => {
|
|
591
|
+
replayStates.push(manager.isReplaying())
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
store.update(ids.a, (s) => ({ ...s, value: 1 }))
|
|
595
|
+
expect(replayStates).toEqual([false])
|
|
596
|
+
|
|
597
|
+
replayStates.length = 0
|
|
598
|
+
manager.undo()
|
|
599
|
+
expect(replayStates).toEqual([true])
|
|
600
|
+
expect(manager.isReplaying()).toBe(false)
|
|
601
|
+
|
|
602
|
+
replayStates.length = 0
|
|
603
|
+
manager.redo()
|
|
604
|
+
expect(replayStates).toEqual([true])
|
|
605
|
+
expect(manager.isReplaying()).toBe(false)
|
|
606
|
+
|
|
607
|
+
replayStates.length = 0
|
|
608
|
+
manager.batch(
|
|
609
|
+
() => {
|
|
610
|
+
store.update(ids.a, (s) => ({ ...s, value: 2 }))
|
|
611
|
+
},
|
|
612
|
+
{ history: 'ignore' }
|
|
613
|
+
)
|
|
614
|
+
expect(replayStates).toEqual([false])
|
|
615
|
+
expect(manager.isReplaying()).toBe(false)
|
|
616
|
+
|
|
617
|
+
dispose()
|
|
618
|
+
})
|
|
587
619
|
})
|
|
588
620
|
|
|
589
621
|
describe('getMarkIdMatching', () => {
|
|
@@ -48,7 +48,11 @@ export class HistoryManager<R extends UnknownRecord> {
|
|
|
48
48
|
switch (this.state) {
|
|
49
49
|
case HistoryRecorderState.Recording:
|
|
50
50
|
this.pendingDiff.apply(entry.changes)
|
|
51
|
-
this
|
|
51
|
+
// this interceptor runs for every store change, so skip the update when the
|
|
52
|
+
// redo stack is already empty
|
|
53
|
+
if (this.stacks.get().redos.length > 0) {
|
|
54
|
+
this.stacks.update(({ undos }) => ({ undos, redos: stack() }))
|
|
55
|
+
}
|
|
52
56
|
break
|
|
53
57
|
case HistoryRecorderState.RecordingPreserveRedoStack:
|
|
54
58
|
this.pendingDiff.apply(entry.changes)
|
|
@@ -79,6 +83,14 @@ export class HistoryManager<R extends UnknownRecord> {
|
|
|
79
83
|
return this.stacks.get().redos.length
|
|
80
84
|
}
|
|
81
85
|
|
|
86
|
+
/** @internal */
|
|
87
|
+
private _isReplaying = false
|
|
88
|
+
|
|
89
|
+
/** @internal */
|
|
90
|
+
isReplaying() {
|
|
91
|
+
return this._isReplaying
|
|
92
|
+
}
|
|
93
|
+
|
|
82
94
|
/** @internal */
|
|
83
95
|
_isInBatch = false
|
|
84
96
|
|
|
@@ -115,7 +127,9 @@ export class HistoryManager<R extends UnknownRecord> {
|
|
|
115
127
|
// History
|
|
116
128
|
_undo({ pushToRedoStack, toMark = undefined }: { pushToRedoStack: boolean; toMark?: string }) {
|
|
117
129
|
const previousState = this.state
|
|
130
|
+
const previousIsReplaying = this._isReplaying
|
|
118
131
|
this.state = HistoryRecorderState.Paused
|
|
132
|
+
this._isReplaying = true
|
|
119
133
|
try {
|
|
120
134
|
let { undos, redos } = this.stacks.get()
|
|
121
135
|
|
|
@@ -176,11 +190,11 @@ export class HistoryManager<R extends UnknownRecord> {
|
|
|
176
190
|
this.pendingDiff.restore(pendingDiff)
|
|
177
191
|
return this
|
|
178
192
|
}
|
|
179
|
-
|
|
180
193
|
this.store.applyDiff(diffToUndo, { ignoreEphemeralKeys: true })
|
|
181
194
|
this.store.ensureStoreIsUsable()
|
|
182
195
|
this.stacks.set({ undos, redos })
|
|
183
196
|
} finally {
|
|
197
|
+
this._isReplaying = previousIsReplaying
|
|
184
198
|
this.state = previousState
|
|
185
199
|
}
|
|
186
200
|
|
|
@@ -195,7 +209,9 @@ export class HistoryManager<R extends UnknownRecord> {
|
|
|
195
209
|
|
|
196
210
|
redo() {
|
|
197
211
|
const previousState = this.state
|
|
212
|
+
const previousIsReplaying = this._isReplaying
|
|
198
213
|
this.state = HistoryRecorderState.Paused
|
|
214
|
+
this._isReplaying = true
|
|
199
215
|
try {
|
|
200
216
|
this.flushPendingDiff()
|
|
201
217
|
|
|
@@ -224,11 +240,11 @@ export class HistoryManager<R extends UnknownRecord> {
|
|
|
224
240
|
break
|
|
225
241
|
}
|
|
226
242
|
}
|
|
227
|
-
|
|
228
243
|
this.store.applyDiff(diffToRedo, { ignoreEphemeralKeys: true })
|
|
229
244
|
this.store.ensureStoreIsUsable()
|
|
230
245
|
this.stacks.set({ undos, redos })
|
|
231
246
|
} finally {
|
|
247
|
+
this._isReplaying = previousIsReplaying
|
|
232
248
|
this.state = previousState
|
|
233
249
|
}
|
|
234
250
|
|
|
@@ -349,7 +365,16 @@ class PendingDiff<R extends UnknownRecord> {
|
|
|
349
365
|
|
|
350
366
|
apply(diff: RecordsDiff<R>) {
|
|
351
367
|
squashRecordDiffsMutable(this.diff, [diff])
|
|
352
|
-
|
|
368
|
+
// Recomputing emptiness from the accumulated diff is O(N) (for-in over a
|
|
369
|
+
// dictionary-mode object pays an O(N) key-collection prologue), and this runs on
|
|
370
|
+
// every history interceptor call — e.g. every input tick while resizing many
|
|
371
|
+
// shapes. Updates can never cancel out existing entries during a squash, so the
|
|
372
|
+
// full recompute is only needed when the incoming diff adds or removes records.
|
|
373
|
+
if (hasAnyKey(diff.added) || hasAnyKey(diff.removed)) {
|
|
374
|
+
this.isEmptyAtom.set(isRecordsDiffEmpty(this.diff))
|
|
375
|
+
} else if (this.isEmptyAtom.__unsafe__getWithoutCapture()) {
|
|
376
|
+
this.isEmptyAtom.set(!hasAnyKey(diff.updated))
|
|
377
|
+
}
|
|
353
378
|
}
|
|
354
379
|
|
|
355
380
|
debug() {
|
|
@@ -357,6 +382,11 @@ class PendingDiff<R extends UnknownRecord> {
|
|
|
357
382
|
}
|
|
358
383
|
}
|
|
359
384
|
|
|
385
|
+
function hasAnyKey(obj: object) {
|
|
386
|
+
for (const _ in obj) return true
|
|
387
|
+
return false
|
|
388
|
+
}
|
|
389
|
+
|
|
360
390
|
type Stack<T> = StackItem<T> | EmptyStackItem<T>
|
|
361
391
|
|
|
362
392
|
function stack<T>(): Stack<T> {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createTLStore } from '../../../config/createTLStore'
|
|
2
|
+
import { Editor } from '../../Editor'
|
|
3
|
+
|
|
4
|
+
function createTestEditor() {
|
|
5
|
+
const store = createTLStore({})
|
|
6
|
+
store.ensureStoreIsUsable()
|
|
7
|
+
return new Editor({
|
|
8
|
+
store,
|
|
9
|
+
bindingUtils: [],
|
|
10
|
+
shapeUtils: [],
|
|
11
|
+
getContainer: () => document.createElement('div'),
|
|
12
|
+
tools: [],
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('InputsManager', () => {
|
|
17
|
+
let editor: Editor
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
editor = createTestEditor()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
editor.dispose()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('updates pointer velocity on frame events', () => {
|
|
28
|
+
const point = editor.inputs.getCurrentScreenPoint()
|
|
29
|
+
point.x = 0
|
|
30
|
+
point.y = 0
|
|
31
|
+
editor.emit('frame', 16)
|
|
32
|
+
|
|
33
|
+
point.x = 100
|
|
34
|
+
point.y = 0
|
|
35
|
+
editor.emit('frame', 16)
|
|
36
|
+
|
|
37
|
+
expect(editor.inputs.getPointerVelocity().len()).toBeGreaterThan(0)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('stops updating pointer velocity after dispose', () => {
|
|
41
|
+
const point = editor.inputs.getCurrentScreenPoint()
|
|
42
|
+
point.x = 0
|
|
43
|
+
point.y = 0
|
|
44
|
+
editor.emit('frame', 16)
|
|
45
|
+
|
|
46
|
+
point.x = 100
|
|
47
|
+
point.y = 0
|
|
48
|
+
editor.emit('frame', 16)
|
|
49
|
+
|
|
50
|
+
const velocityBeforeDispose = editor.inputs.getPointerVelocity().clone()
|
|
51
|
+
expect(velocityBeforeDispose.len()).toBeGreaterThan(0)
|
|
52
|
+
|
|
53
|
+
editor.inputs.dispose()
|
|
54
|
+
|
|
55
|
+
point.x = 200
|
|
56
|
+
point.y = 0
|
|
57
|
+
editor.emit('frame', 16)
|
|
58
|
+
|
|
59
|
+
expect(editor.inputs.getPointerVelocity()).toEqual(velocityBeforeDispose)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { atom, computed, unsafe__withoutCapture } from '@tldraw/state'
|
|
2
2
|
import { AtomSet } from '@tldraw/store'
|
|
3
3
|
import { TLINSTANCE_ID, TLPOINTER_ID } from '@tldraw/tlschema'
|
|
4
|
+
import { bind } from '@tldraw/utils'
|
|
4
5
|
import { INTERNAL_POINTER_IDS } from '../../../constants'
|
|
5
6
|
import { Vec } from '../../../primitives/Vec'
|
|
6
7
|
import { isAccelKey } from '../../../utils/keyboard'
|
|
@@ -12,7 +13,19 @@ const POINTER_VELOCITY_REFERENCE_SMOOTHING = 0.5
|
|
|
12
13
|
|
|
13
14
|
/** @public */
|
|
14
15
|
export class InputsManager {
|
|
15
|
-
constructor(private readonly editor: Editor) {
|
|
16
|
+
constructor(private readonly editor: Editor) {
|
|
17
|
+
this.editor.on('frame', this._onFrame)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** @internal */
|
|
21
|
+
dispose() {
|
|
22
|
+
this.editor.off('frame', this._onFrame)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@bind
|
|
26
|
+
private _onFrame(elapsed: number) {
|
|
27
|
+
this.updatePointerVelocity(elapsed)
|
|
28
|
+
}
|
|
16
29
|
|
|
17
30
|
private _originPagePoint = atom<Vec>('originPagePoint', new Vec())
|
|
18
31
|
/**
|
|
@@ -120,8 +133,7 @@ export class InputsManager {
|
|
|
120
133
|
}
|
|
121
134
|
|
|
122
135
|
/**
|
|
123
|
-
* Normally you shouldn't need to set the pointer velocity directly
|
|
124
|
-
* However, this is currently used in tests to fake pointer velocity.
|
|
136
|
+
* Normally you shouldn't need to set the pointer velocity directly. Used in tests to fake pointer velocity.
|
|
125
137
|
* @param pointerVelocity - The pointer velocity.
|
|
126
138
|
* @internal
|
|
127
139
|
*/
|
|
@@ -460,7 +472,7 @@ export class InputsManager {
|
|
|
460
472
|
private _velocityPrevPoint = new Vec()
|
|
461
473
|
|
|
462
474
|
/**
|
|
463
|
-
* Update the pointer velocity based on elapsed time. Called
|
|
475
|
+
* Update the pointer velocity based on elapsed time. Called each frame.
|
|
464
476
|
* @param elapsed - The time elapsed since the last tick in milliseconds.
|
|
465
477
|
* @internal
|
|
466
478
|
*/
|
|
@@ -46,6 +46,10 @@ export class SpatialIndexManager {
|
|
|
46
46
|
|
|
47
47
|
private createSpatialIndexComputed() {
|
|
48
48
|
const shapeHistory = this.editor.store.query.filterHistory('shape')
|
|
49
|
+
// Binding changes can move a shape's derived bounds (e.g. creating or
|
|
50
|
+
// deleting an arrow binding relocates the arrow's body) without
|
|
51
|
+
// touching any shape record, so they must also invalidate the index.
|
|
52
|
+
const bindingHistory = this.editor.store.query.filterHistory('binding')
|
|
49
53
|
|
|
50
54
|
return computed<number>('spatialIndex', (_prevValue, lastComputedEpoch) => {
|
|
51
55
|
if (isUninitialized(_prevValue)) {
|
|
@@ -53,8 +57,9 @@ export class SpatialIndexManager {
|
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
const shapeDiff = shapeHistory.getDiffSince(lastComputedEpoch)
|
|
60
|
+
const bindingDiff = bindingHistory.getDiffSince(lastComputedEpoch)
|
|
56
61
|
|
|
57
|
-
if (shapeDiff === RESET_VALUE) {
|
|
62
|
+
if (shapeDiff === RESET_VALUE || bindingDiff === RESET_VALUE) {
|
|
58
63
|
return this.rebuildAndBumpEpoch()
|
|
59
64
|
}
|
|
60
65
|
|
|
@@ -63,8 +68,10 @@ export class SpatialIndexManager {
|
|
|
63
68
|
return this.rebuildAndBumpEpoch()
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
if (shapeDiff.length === 0) return this._boundsEpoch
|
|
71
|
+
if (shapeDiff.length === 0 && bindingDiff.length === 0) return this._boundsEpoch
|
|
67
72
|
|
|
73
|
+
// A binding-only diff passes an empty shape diff: step 1 is a no-op
|
|
74
|
+
// and the step-2 sweep re-checks the indexed bounds of every shape.
|
|
68
75
|
if (this.processIncrementalUpdate(shapeDiff)) {
|
|
69
76
|
this._boundsEpoch++
|
|
70
77
|
}
|
|
@@ -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
|
|
@@ -2,6 +2,21 @@ import { BoxModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'
|
|
|
2
2
|
import { objectMapKeys } from '@tldraw/utils'
|
|
3
3
|
import type { Editor } from '../../Editor'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* The whole-pixel line-height for a given font size and tldraw's unitless line-height
|
|
7
|
+
* multiplier. tldraw's theme stores line-height as a multiplier (e.g. 1.35); resolving it
|
|
8
|
+
* to a whole pixel keeps line spacing identical across rendering engines, which otherwise
|
|
9
|
+
* disagree on fractional line boxes (WebKit snaps them to whole pixels, Blink keeps the
|
|
10
|
+
* fraction) and let multi-line text drift apart. Apply it everywhere line-height is used —
|
|
11
|
+
* measurement, on-canvas render, and export — so geometry and rendering agree.
|
|
12
|
+
* See https://github.com/tldraw/tldraw/issues/8970.
|
|
13
|
+
*
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
export function resolveLineHeightPx(fontSize: number, lineHeight: number): number {
|
|
17
|
+
return Math.round(fontSize * lineHeight)
|
|
18
|
+
}
|
|
19
|
+
|
|
5
20
|
const fixNewLines = /\r?\n|\r/g
|
|
6
21
|
|
|
7
22
|
function normalizeTextForDom(text: string) {
|
|
@@ -154,7 +169,7 @@ export class TextManager {
|
|
|
154
169
|
'font-style': opts.fontStyle,
|
|
155
170
|
'font-weight': opts.fontWeight,
|
|
156
171
|
'font-size': opts.fontSize + 'px',
|
|
157
|
-
'line-height': opts.lineHeight
|
|
172
|
+
'line-height': `${resolveLineHeightPx(opts.fontSize, opts.lineHeight)}px`,
|
|
158
173
|
padding: opts.padding,
|
|
159
174
|
'max-width': opts.maxWidth ? opts.maxWidth + 'px' : undefined,
|
|
160
175
|
'min-width': opts.minWidth ? opts.minWidth + 'px' : undefined,
|
|
@@ -382,7 +397,7 @@ export class TextManager {
|
|
|
382
397
|
'font-style': opts.fontStyle,
|
|
383
398
|
'font-weight': opts.fontWeight,
|
|
384
399
|
'font-size': opts.fontSize + 'px',
|
|
385
|
-
'line-height': opts.lineHeight
|
|
400
|
+
'line-height': `${resolveLineHeightPx(opts.fontSize, opts.lineHeight)}px`,
|
|
386
401
|
width: `${elementWidth}px`,
|
|
387
402
|
height: 'min-content',
|
|
388
403
|
'text-align': textAlignmentsForLtr[opts.textAlign],
|