@tldraw/editor 5.2.1 → 5.3.0-canary.263f9dbe88bb

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.
Files changed (60) hide show
  1. package/dist-cjs/index.d.ts +45 -8
  2. package/dist-cjs/index.js +3 -3
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/editor/Editor.js +2 -2
  5. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  6. package/dist-cjs/lib/editor/managers/EditorManager.js +52 -0
  7. package/dist-cjs/lib/editor/managers/EditorManager.js.map +7 -0
  8. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js +9 -11
  9. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +2 -2
  10. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +5 -9
  11. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +2 -2
  12. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +24 -19
  13. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  14. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +4 -7
  15. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
  16. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +1 -1
  17. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +1 -2
  18. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
  19. package/dist-cjs/version.js +3 -3
  20. package/dist-cjs/version.js.map +1 -1
  21. package/dist-esm/index.d.mts +45 -8
  22. package/dist-esm/index.mjs +3 -3
  23. package/dist-esm/index.mjs.map +2 -2
  24. package/dist-esm/lib/editor/Editor.mjs +2 -2
  25. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  26. package/dist-esm/lib/editor/managers/EditorManager.mjs +32 -0
  27. package/dist-esm/lib/editor/managers/EditorManager.mjs.map +7 -0
  28. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs +9 -11
  29. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +2 -2
  30. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +5 -9
  31. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +2 -2
  32. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +24 -19
  33. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  34. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +4 -7
  35. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
  36. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +1 -1
  37. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +1 -2
  38. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
  39. package/dist-esm/version.mjs +3 -3
  40. package/dist-esm/version.mjs.map +1 -1
  41. package/package.json +7 -7
  42. package/src/index.ts +1 -1
  43. package/src/lib/editor/Editor.ts +4 -2
  44. package/src/lib/editor/managers/EditorManager.test.ts +51 -0
  45. package/src/lib/editor/managers/EditorManager.ts +60 -0
  46. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +2 -1
  47. package/src/lib/editor/managers/FocusManager/FocusManager.ts +10 -14
  48. package/src/lib/editor/managers/InputsManager/InputsManager.ts +6 -9
  49. package/src/lib/editor/managers/TextManager/TextManager.test.ts +61 -1
  50. package/src/lib/editor/managers/TextManager/TextManager.ts +33 -19
  51. package/src/lib/editor/managers/TickManager/TickManager.test.ts +0 -10
  52. package/src/lib/editor/managers/TickManager/TickManager.ts +5 -6
  53. package/src/lib/editor/shapes/ShapeUtil.ts +1 -0
  54. package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +2 -2
  55. package/src/version.ts +3 -3
  56. package/dist-cjs/lib/editor/tools/cancelShapeCreationOnLongPress.js +0 -28
  57. package/dist-cjs/lib/editor/tools/cancelShapeCreationOnLongPress.js.map +0 -7
  58. package/dist-esm/lib/editor/tools/cancelShapeCreationOnLongPress.mjs +0 -8
  59. package/dist-esm/lib/editor/tools/cancelShapeCreationOnLongPress.mjs.map +0 -7
  60. package/src/lib/editor/tools/cancelShapeCreationOnLongPress.ts +0 -31
@@ -1,6 +1,7 @@
1
1
  import { BoxModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'
2
2
  import { objectMapKeys } from '@tldraw/utils'
3
3
  import type { Editor } from '../../Editor'
4
+ import { EditorManager } from '../EditorManager'
4
5
 
5
6
  /**
6
7
  * The whole-pixel line-height for a given font size and tldraw's unitless line-height
@@ -93,6 +94,10 @@ export interface TLMeasureTextSpanOpts {
93
94
 
94
95
  const spaceCharacterRegex = /\s/
95
96
 
97
+ // Iterate by grapheme cluster (e.g. emoji sequences, flags, accented letters)
98
+ // rather than by code point, so a single glyph is measured as one unit.
99
+ const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' })
100
+
96
101
  const initialDefaultStyles = Object.freeze({
97
102
  'overflow-wrap': 'break-word',
98
103
  'word-break': 'auto',
@@ -103,13 +108,21 @@ const initialDefaultStyles = Object.freeze({
103
108
  })
104
109
 
105
110
  /** @public */
106
- export class TextManager {
111
+ export class TextManager extends EditorManager {
107
112
  private elm: HTMLDivElement
108
113
  private poolElms: PoolItem[] = []
109
114
 
110
- constructor(public editor: Editor) {
115
+ constructor(editor: Editor) {
116
+ super(editor)
111
117
  this.elm = this.createMeasurementEl()
112
118
  this.editor.getContainer().appendChild(this.elm)
119
+ this.register(() => {
120
+ this.elm.remove()
121
+ for (const { el } of this.poolElms) {
122
+ el.remove()
123
+ }
124
+ this.poolElms.length = 0
125
+ })
113
126
  }
114
127
 
115
128
  private createMeasurementEl(): HTMLDivElement {
@@ -178,14 +191,6 @@ export class TextManager {
178
191
  }
179
192
  }
180
193
 
181
- dispose() {
182
- this.elm.remove()
183
- for (const { el } of this.poolElms) {
184
- el.remove()
185
- }
186
- this.poolElms.length = 0
187
- }
188
-
189
194
  private ensurePoolSize(size: number) {
190
195
  if (this.poolElms.length >= size) return
191
196
 
@@ -301,15 +306,24 @@ export class TextManager {
301
306
  for (const childNode of element.childNodes) {
302
307
  if (childNode.nodeType !== Node.TEXT_NODE) continue
303
308
 
304
- for (const char of childNode.textContent ?? '') {
305
- // place the range around the characters we're interested in
309
+ for (const { segment } of graphemeSegmenter.segment(childNode.textContent ?? '')) {
310
+ // place the range around the grapheme we're interested in
306
311
  range.setStart(textNode, idx)
307
- range.setEnd(textNode, idx + char.length)
312
+ range.setEnd(textNode, idx + segment.length)
308
313
  // measure the range. some browsers return multiple rects for the
309
314
  // first char in a new line - one for the line break, and one for
310
315
  // the character itself. we're only interested in the character.
311
316
  const rects = range.getClientRects()
312
- const rect = rects[rects.length - 1]!
317
+ const rect = rects[rects.length - 1]
318
+
319
+ // some graphemes produce no layout rectangle (e.g. zero-width
320
+ // spaces or newlines). skip them rather than crashing, but keep
321
+ // advancing the index so later graphemes stay aligned.
322
+ // See https://github.com/tldraw/tldraw/issues/9112.
323
+ if (!rect) {
324
+ idx += segment.length
325
+ continue
326
+ }
313
327
 
314
328
  // calculate the position of the character relative to the element
315
329
  const top = rect.top + offsetY
@@ -317,7 +331,7 @@ export class TextManager {
317
331
  const right = rect.right + offsetX
318
332
  const isRTL = left < prevCharLeftForRTLTest
319
333
 
320
- const isSpaceCharacter = spaceCharacterRegex.test(char)
334
+ const isSpaceCharacter = spaceCharacterRegex.test(segment)
321
335
  if (
322
336
  // If we're at a word boundary...
323
337
  isSpaceCharacter !== prevCharWasSpaceCharacter ||
@@ -341,7 +355,7 @@ export class TextManager {
341
355
  // start a new span
342
356
  currentSpan = {
343
357
  box: { x: left, y: top, w: rect.width, h: rect.height },
344
- text: char,
358
+ text: segment,
345
359
  }
346
360
  prevCharLeftForRTLTest = left
347
361
  } else {
@@ -352,16 +366,16 @@ export class TextManager {
352
366
 
353
367
  // otherwise we just need to extend the current span with the next character
354
368
  currentSpan.box.w = isRTL ? currentSpan.box.w + rect.width : right - currentSpan.box.x
355
- currentSpan.text += char
369
+ currentSpan.text += segment
356
370
  }
357
371
 
358
- if (char === '\n') {
372
+ if (segment === '\n') {
359
373
  prevCharLeftForRTLTest = 0
360
374
  }
361
375
 
362
376
  prevCharWasSpaceCharacter = isSpaceCharacter
363
377
  prevCharTop = top
364
- idx += char.length
378
+ idx += segment.length
365
379
  }
366
380
  }
367
381
 
@@ -19,7 +19,6 @@ describe('TickManager', () => {
19
19
  let editor: Mocked<Editor>
20
20
  let tickManager: TickManager
21
21
  let mockEmit: Mock
22
- let mockDisposablesAdd: Mock
23
22
 
24
23
  beforeEach(() => {
25
24
  vi.clearAllMocks()
@@ -38,13 +37,9 @@ describe('TickManager', () => {
38
37
  mockCancelAnimationFrame.mockImplementation(() => {})
39
38
 
40
39
  mockEmit = vi.fn()
41
- mockDisposablesAdd = vi.fn()
42
40
 
43
41
  editor = {
44
42
  emit: mockEmit,
45
- disposables: {
46
- add: mockDisposablesAdd,
47
- },
48
43
  } as unknown as Mocked<Editor>
49
44
 
50
45
  tickManager = new TickManager(editor)
@@ -58,15 +53,10 @@ describe('TickManager', () => {
58
53
 
59
54
  describe('constructor and initialization', () => {
60
55
  it('should initialize with correct default values', () => {
61
- expect(tickManager.editor).toBe(editor)
62
56
  expect(tickManager.isPaused).toBe(false)
63
57
  expect(tickManager.now).toBe(1000)
64
58
  })
65
59
 
66
- it('should add dispose method to editor disposables', () => {
67
- expect(mockDisposablesAdd).toHaveBeenCalledWith(tickManager.dispose)
68
- })
69
-
70
60
  it('should start the tick loop on construction', () => {
71
61
  expect(mockRequestAnimationFrame).toHaveBeenCalled()
72
62
  })
@@ -1,5 +1,6 @@
1
1
  import { throttleToNextFrame as _throttleToNextFrame, bind } from '@tldraw/utils'
2
2
  import type { Editor } from '../../Editor'
3
+ import { EditorManager } from '../EditorManager'
3
4
 
4
5
  const throttleToNextFrame =
5
6
  typeof process !== 'undefined' && process.env.NODE_ENV === 'test'
@@ -13,9 +14,9 @@ const throttleToNextFrame =
13
14
  : _throttleToNextFrame
14
15
 
15
16
  /** @internal */
16
- export class TickManager {
17
- constructor(public editor: Editor) {
18
- this.editor.disposables.add(this.dispose)
17
+ export class TickManager extends EditorManager {
18
+ constructor(editor: Editor) {
19
+ super(editor)
19
20
  this.start()
20
21
  }
21
22
 
@@ -45,11 +46,9 @@ export class TickManager {
45
46
  this.cancelRaf = throttleToNextFrame(this.tick)
46
47
  }
47
48
 
48
- // Clear the listener
49
- @bind
50
49
  dispose() {
51
50
  this.isPaused = true
52
-
53
51
  this.cancelRaf?.()
52
+ super.dispose()
54
53
  }
55
54
  }
@@ -1039,6 +1039,7 @@ export interface TLCropInfo<T extends TLShape> {
1039
1039
  uncroppedSize: { w: number; h: number }
1040
1040
  initialShape: T
1041
1041
  aspectRatioLocked?: boolean
1042
+ isResizingFromCenter?: boolean
1042
1043
  }
1043
1044
 
1044
1045
  /** @public */
@@ -4,7 +4,6 @@ import { Vec } from '../../../../primitives/Vec'
4
4
  import type { Editor } from '../../../Editor'
5
5
  import { TLBaseBoxShape } from '../../../shapes/BaseBoxShapeUtil'
6
6
  import { TLPointerEventInfo } from '../../../types/event-types'
7
- import { cancelShapeCreationOnLongPress } from '../../cancelShapeCreationOnLongPress'
8
7
  import { StateNode } from '../../StateNode'
9
8
  import type { BaseBoxShapeTool } from '../BaseBoxShapeTool'
10
9
 
@@ -67,7 +66,8 @@ export class Pointing extends StateNode {
67
66
  }
68
67
 
69
68
  override onLongPress() {
70
- cancelShapeCreationOnLongPress(this.editor, () => this.cancel())
69
+ // On a touch (coarse pointer) long-press, cancel the pending shape so it leaves nothing behind.
70
+ if (this.editor.getInstanceState().isCoarsePointer) this.cancel()
71
71
  }
72
72
 
73
73
  override onCancel() {
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.2.1'
4
+ export const version = '5.3.0-canary.263f9dbe88bb'
5
5
  export const publishDates = {
6
6
  major: '2026-05-06T16:28:18.473Z',
7
- minor: '2026-07-01T09:20:01.735Z',
8
- patch: '2026-07-01T10:28:54.350Z',
7
+ minor: '2026-07-01T09:30:40.438Z',
8
+ patch: '2026-07-01T09:30:40.438Z',
9
9
  }
@@ -1,28 +0,0 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
- var cancelShapeCreationOnLongPress_exports = {};
20
- __export(cancelShapeCreationOnLongPress_exports, {
21
- cancelShapeCreationOnLongPress: () => cancelShapeCreationOnLongPress
22
- });
23
- module.exports = __toCommonJS(cancelShapeCreationOnLongPress_exports);
24
- function cancelShapeCreationOnLongPress(editor, cancelPendingCreation) {
25
- if (!editor.getInstanceState().isCoarsePointer) return;
26
- cancelPendingCreation();
27
- }
28
- //# sourceMappingURL=cancelShapeCreationOnLongPress.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../../src/lib/editor/tools/cancelShapeCreationOnLongPress.ts"],
4
- "sourcesContent": ["import type { Editor } from '../Editor'\n\n/**\n * Shared handler for a long-press in a discrete shape-creation tool's pointing\n * state (geo, note, line, text, arrow, frame). On a coarse pointer this cancels\n * any pending creation so the long-press leaves no shape behind (issue #8277).\n * It does not open a context menu \u2014 that is a select-tool surface, reached on\n * touch only from the select tool, and on desktop from a right-click (which the\n * editor routes through the select tool).\n *\n * Gated on `isCoarsePointer`: on a fine pointer (mouse) the long-press timer\n * powers the deliberate \"pause before drag to start a precise arrow\" gesture,\n * which we must not disturb. The bug only manifests on coarse pointers, because\n * that is also where the browser fires `contextmenu` at the long-press mark.\n *\n * The `cancelPendingCreation` callback is the tool's own cleanup \u2014 it knows\n * whether it needs to `bailToMark` a shape it created early or simply transition\n * back to idle. Continuous-gesture tools (draw, highlight, eraser, laser) are\n * excluded: their press is their own action, so they handle (or ignore) the\n * long-press themselves.\n *\n * @internal\n */\nexport function cancelShapeCreationOnLongPress(\n\teditor: Editor,\n\tcancelPendingCreation: () => void\n): void {\n\tif (!editor.getInstanceState().isCoarsePointer) return\n\n\tcancelPendingCreation()\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAuBO,SAAS,+BACf,QACA,uBACO;AACP,MAAI,CAAC,OAAO,iBAAiB,EAAE,gBAAiB;AAEhD,wBAAsB;AACvB;",
6
- "names": []
7
- }
@@ -1,8 +0,0 @@
1
- function cancelShapeCreationOnLongPress(editor, cancelPendingCreation) {
2
- if (!editor.getInstanceState().isCoarsePointer) return;
3
- cancelPendingCreation();
4
- }
5
- export {
6
- cancelShapeCreationOnLongPress
7
- };
8
- //# sourceMappingURL=cancelShapeCreationOnLongPress.mjs.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../../src/lib/editor/tools/cancelShapeCreationOnLongPress.ts"],
4
- "sourcesContent": ["import type { Editor } from '../Editor'\n\n/**\n * Shared handler for a long-press in a discrete shape-creation tool's pointing\n * state (geo, note, line, text, arrow, frame). On a coarse pointer this cancels\n * any pending creation so the long-press leaves no shape behind (issue #8277).\n * It does not open a context menu \u2014 that is a select-tool surface, reached on\n * touch only from the select tool, and on desktop from a right-click (which the\n * editor routes through the select tool).\n *\n * Gated on `isCoarsePointer`: on a fine pointer (mouse) the long-press timer\n * powers the deliberate \"pause before drag to start a precise arrow\" gesture,\n * which we must not disturb. The bug only manifests on coarse pointers, because\n * that is also where the browser fires `contextmenu` at the long-press mark.\n *\n * The `cancelPendingCreation` callback is the tool's own cleanup \u2014 it knows\n * whether it needs to `bailToMark` a shape it created early or simply transition\n * back to idle. Continuous-gesture tools (draw, highlight, eraser, laser) are\n * excluded: their press is their own action, so they handle (or ignore) the\n * long-press themselves.\n *\n * @internal\n */\nexport function cancelShapeCreationOnLongPress(\n\teditor: Editor,\n\tcancelPendingCreation: () => void\n): void {\n\tif (!editor.getInstanceState().isCoarsePointer) return\n\n\tcancelPendingCreation()\n}\n"],
5
- "mappings": "AAuBO,SAAS,+BACf,QACA,uBACO;AACP,MAAI,CAAC,OAAO,iBAAiB,EAAE,gBAAiB;AAEhD,wBAAsB;AACvB;",
6
- "names": []
7
- }
@@ -1,31 +0,0 @@
1
- import type { Editor } from '../Editor'
2
-
3
- /**
4
- * Shared handler for a long-press in a discrete shape-creation tool's pointing
5
- * state (geo, note, line, text, arrow, frame). On a coarse pointer this cancels
6
- * any pending creation so the long-press leaves no shape behind (issue #8277).
7
- * It does not open a context menu — that is a select-tool surface, reached on
8
- * touch only from the select tool, and on desktop from a right-click (which the
9
- * editor routes through the select tool).
10
- *
11
- * Gated on `isCoarsePointer`: on a fine pointer (mouse) the long-press timer
12
- * powers the deliberate "pause before drag to start a precise arrow" gesture,
13
- * which we must not disturb. The bug only manifests on coarse pointers, because
14
- * that is also where the browser fires `contextmenu` at the long-press mark.
15
- *
16
- * The `cancelPendingCreation` callback is the tool's own cleanup — it knows
17
- * whether it needs to `bailToMark` a shape it created early or simply transition
18
- * back to idle. Continuous-gesture tools (draw, highlight, eraser, laser) are
19
- * excluded: their press is their own action, so they handle (or ignore) the
20
- * long-press themselves.
21
- *
22
- * @internal
23
- */
24
- export function cancelShapeCreationOnLongPress(
25
- editor: Editor,
26
- cancelPendingCreation: () => void
27
- ): void {
28
- if (!editor.getInstanceState().isCoarsePointer) return
29
-
30
- cancelPendingCreation()
31
- }