@tldraw/editor 5.1.0 → 5.2.0-canary.4a316fdfb2bb

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 (79) hide show
  1. package/dist-cjs/index.d.ts +8 -10
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +4 -1
  4. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +3 -3
  5. package/dist-cjs/lib/components/default-components/DefaultLoadingScreen.js +2 -2
  6. package/dist-cjs/lib/components/default-components/DefaultLoadingScreen.js.map +2 -2
  7. package/dist-cjs/lib/components/default-components/DefaultShapeErrorFallback.js +1 -1
  8. package/dist-cjs/lib/components/default-components/DefaultShapeErrorFallback.js.map +3 -3
  9. package/dist-cjs/lib/components/default-components/DefaultSvgDefs.js +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultSvgDefs.js.map +2 -2
  11. package/dist-cjs/lib/editor/Editor.js +23 -2
  12. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  13. package/dist-cjs/lib/editor/derivations/bindingsIndex.js +2 -2
  14. package/dist-cjs/lib/editor/derivations/bindingsIndex.js.map +2 -2
  15. package/dist-cjs/lib/editor/derivations/parentsToChildren.js +2 -2
  16. package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
  17. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js +2 -2
  18. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js.map +2 -2
  19. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +8 -58
  20. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
  21. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  22. package/dist-cjs/lib/editor/types/event-types.js +0 -2
  23. package/dist-cjs/lib/editor/types/event-types.js.map +2 -2
  24. package/dist-cjs/lib/license/LicenseProvider.js +3 -1
  25. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  26. package/dist-cjs/lib/primitives/utils.js +2 -2
  27. package/dist-cjs/lib/primitives/utils.js.map +2 -2
  28. package/dist-cjs/lib/utils/dom.js +5 -3
  29. package/dist-cjs/lib/utils/dom.js.map +2 -2
  30. package/dist-cjs/version.js +3 -3
  31. package/dist-cjs/version.js.map +1 -1
  32. package/dist-esm/index.d.mts +8 -10
  33. package/dist-esm/index.mjs +1 -1
  34. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +4 -1
  35. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +3 -3
  36. package/dist-esm/lib/components/default-components/DefaultLoadingScreen.mjs +2 -2
  37. package/dist-esm/lib/components/default-components/DefaultLoadingScreen.mjs.map +2 -2
  38. package/dist-esm/lib/components/default-components/DefaultShapeErrorFallback.mjs +1 -1
  39. package/dist-esm/lib/components/default-components/DefaultShapeErrorFallback.mjs.map +3 -3
  40. package/dist-esm/lib/components/default-components/DefaultSvgDefs.mjs +2 -2
  41. package/dist-esm/lib/components/default-components/DefaultSvgDefs.mjs.map +2 -2
  42. package/dist-esm/lib/editor/Editor.mjs +23 -2
  43. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  44. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs +2 -2
  45. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
  46. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +2 -2
  47. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
  48. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs +2 -2
  49. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs.map +2 -2
  50. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +8 -58
  51. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
  52. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  53. package/dist-esm/lib/editor/types/event-types.mjs +0 -2
  54. package/dist-esm/lib/editor/types/event-types.mjs.map +2 -2
  55. package/dist-esm/lib/license/LicenseProvider.mjs +3 -1
  56. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  57. package/dist-esm/lib/primitives/utils.mjs +2 -2
  58. package/dist-esm/lib/primitives/utils.mjs.map +2 -2
  59. package/dist-esm/lib/utils/dom.mjs +5 -3
  60. package/dist-esm/lib/utils/dom.mjs.map +2 -2
  61. package/dist-esm/version.mjs +3 -3
  62. package/dist-esm/version.mjs.map +1 -1
  63. package/package.json +7 -7
  64. package/src/lib/components/default-components/DefaultErrorFallback.tsx +4 -1
  65. package/src/lib/components/default-components/DefaultLoadingScreen.tsx +1 -1
  66. package/src/lib/components/default-components/DefaultShapeErrorFallback.tsx +4 -3
  67. package/src/lib/components/default-components/DefaultSvgDefs.tsx +1 -1
  68. package/src/lib/editor/Editor.ts +39 -6
  69. package/src/lib/editor/derivations/bindingsIndex.ts +1 -1
  70. package/src/lib/editor/derivations/parentsToChildren.ts +1 -1
  71. package/src/lib/editor/derivations/shapeIdsInCurrentPage.ts +1 -1
  72. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +54 -74
  73. package/src/lib/editor/managers/ClickManager/ClickManager.ts +15 -65
  74. package/src/lib/editor/tools/StateNode.ts +0 -2
  75. package/src/lib/editor/types/event-types.ts +2 -6
  76. package/src/lib/license/LicenseProvider.tsx +3 -1
  77. package/src/lib/primitives/utils.ts +1 -1
  78. package/src/lib/utils/dom.ts +5 -3
  79. package/src/version.ts +3 -3
@@ -36,11 +36,13 @@ function releasePointerCapture(element, event) {
36
36
  console.warn("releasePointerCapture called on element:", element, event);
37
37
  }
38
38
  }
39
- const stopEventPropagation = (e) => e.stopPropagation();
40
- const setStyleProperty = (elm, property, value) => {
39
+ function stopEventPropagation(e) {
40
+ return e.stopPropagation();
41
+ }
42
+ function setStyleProperty(elm, property, value) {
41
43
  if (!elm) return;
42
44
  elm.style.setProperty(property, String(value));
43
- };
45
+ }
44
46
  function elementShouldCaptureKeys(el, includeButtonsAndMenus = true) {
45
47
  if (!el) return false;
46
48
  const tagName = el.tagName.toLowerCase();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/utils/dom.ts"],
4
- "sourcesContent": ["/*\nThis is used to facilitate double clicking and pointer capture on elements.\n\nThe events in this file are possibly set on individual SVG elements, \nsuch as handles or corner handles, rather than on HTML elements or \nSVGSVGElements. Raw SVG elements do not support pointerCapture in\nmost cases, meaning that in order for pointer capture to work, we \nneed to crawl up the DOM tree to find the nearest HTML element. Then,\nin order for that element to also call the `onPointerUp` event from\nthis file, we need to manually set that event on that element and\nlater remove it when the pointerup occurs. This is a potential leak\nif the user clicks on a handle but the pointerup does not fire for\nwhatever reason.\n*/\n\nimport { debugFlags, pointerCaptureTrackingObject } from './debug-flags'\n\n/** @public */\nexport function loopToHtmlElement(elm: Element): HTMLElement {\n\tif (elm.nodeType === Node.ELEMENT_NODE) return elm as HTMLElement\n\tif (elm.parentElement) return loopToHtmlElement(elm.parentElement)\n\telse throw Error('Could not find a parent element of an HTML type!')\n}\n\n/**\n * This function calls `event.preventDefault()` for you. Why is that useful?\n *\n * Because if you enable `window.preventDefaultLogging = true` it'll log out a message when it\n * happens. Because we use console.warn rather than (log) you'll get a stack trace in the inspector\n * telling you exactly where it happened. This is important because `e.preventDefault()` is the\n * source of many bugs, but unfortunately it can't be avoided because it also stops a lot of default\n * behaviour which doesn't make sense in our UI\n *\n * @param event - To prevent default on\n * @public\n */\nexport function preventDefault(event: React.BaseSyntheticEvent | Event) {\n\tif ('cancelable' in event && !event.cancelable) return\n\tevent.preventDefault()\n\tif (debugFlags.logPreventDefaults.get()) {\n\t\tconsole.warn('preventDefault called on event:', event)\n\t}\n}\n\n/** @public */\nexport function setPointerCapture(\n\telement: Element,\n\tevent: React.PointerEvent<Element> | PointerEvent\n) {\n\telement.setPointerCapture(event.pointerId)\n\tif (debugFlags.logPointerCaptures.get()) {\n\t\tconst trackingObj = pointerCaptureTrackingObject.get()\n\t\ttrackingObj.set(element, (trackingObj.get(element) ?? 0) + 1)\n\t\tconsole.warn('setPointerCapture called on element:', element, event)\n\t}\n}\n\n/** @public */\nexport function releasePointerCapture(\n\telement: Element,\n\tevent: React.PointerEvent<Element> | PointerEvent\n) {\n\tif (!element.hasPointerCapture(event.pointerId)) {\n\t\treturn\n\t}\n\n\telement.releasePointerCapture(event.pointerId)\n\tif (debugFlags.logPointerCaptures.get()) {\n\t\tconst trackingObj = pointerCaptureTrackingObject.get()\n\t\tif (trackingObj.get(element) === 1) {\n\t\t\ttrackingObj.delete(element)\n\t\t} else if (trackingObj.has(element)) {\n\t\t\ttrackingObj.set(element, trackingObj.get(element)! - 1)\n\t\t} else {\n\t\t\tconsole.warn('Release without capture')\n\t\t}\n\t\tconsole.warn('releasePointerCapture called on element:', element, event)\n\t}\n}\n\n/**\n * Calls `event.stopPropagation()`.\n *\n * @deprecated Use {@link Editor.markEventAsHandled} instead, or manually call `event.stopPropagation()` if\n * that's what you really want.\n *\n * @public\n */\nexport const stopEventPropagation = (e: any) => e.stopPropagation()\n\n/** @internal */\nexport const setStyleProperty = (\n\telm: HTMLElement | null,\n\tproperty: string,\n\tvalue: string | number\n) => {\n\tif (!elm) return\n\telm.style.setProperty(property, String(value))\n}\n\n/** @internal */\nexport function elementShouldCaptureKeys(el: Element | null, includeButtonsAndMenus = true) {\n\tif (!el) return false\n\n\tconst tagName = el.tagName.toLowerCase()\n\treturn (\n\t\t(el as HTMLElement).isContentEditable ||\n\t\ttagName === 'input' ||\n\t\ttagName === 'textarea' ||\n\t\t(includeButtonsAndMenus && tagName === 'select') ||\n\t\t(includeButtonsAndMenus && tagName === 'button') ||\n\t\tel.classList.contains('tlui-slider__thumb')\n\t)\n}\n\n/**\n * Returns the global `document`. Use this instead of bare `document` to satisfy lint rules.\n *\n * When you have a DOM node or editor instance, prefer the scoped versions instead:\n * - `getOwnerDocument(node)` \u2013 the document that owns a specific DOM node\n * - `editor.getContainerDocument()` \u2013 the document where the editor is mounted\n *\n * @internal\n */\nexport function getGlobalDocument(): Document {\n\t// eslint-disable-next-line no-restricted-globals\n\tif (typeof document !== 'undefined') return document\n\treturn globalThis.document\n}\n\n/**\n * Returns the global `window`. Use this instead of bare `window` to satisfy lint rules.\n *\n * When you have a DOM node or editor instance, prefer the scoped versions instead:\n * - `getOwnerWindow(node)` \u2013 the window that owns a specific DOM node\n * - `editor.getContainerWindow()` \u2013 the window where the editor is mounted\n *\n * @internal\n */\nexport function getGlobalWindow(): Window & typeof globalThis {\n\tif (typeof window !== 'undefined') return window as Window & typeof globalThis\n\treturn globalThis as Window & typeof globalThis\n}\n\n/** @internal */\nexport function activeElementShouldCaptureKeys(includeButtonsAndMenus = true, doc?: Document) {\n\treturn elementShouldCaptureKeys(\n\t\t(doc ?? getGlobalDocument()).activeElement,\n\t\tincludeButtonsAndMenus\n\t)\n}\n"],
5
- "mappings": "AAeA,SAAS,YAAY,oCAAoC;AAGlD,SAAS,kBAAkB,KAA2B;AAC5D,MAAI,IAAI,aAAa,KAAK,aAAc,QAAO;AAC/C,MAAI,IAAI,cAAe,QAAO,kBAAkB,IAAI,aAAa;AAAA,MAC5D,OAAM,MAAM,kDAAkD;AACpE;AAcO,SAAS,eAAe,OAAyC;AACvE,MAAI,gBAAgB,SAAS,CAAC,MAAM,WAAY;AAChD,QAAM,eAAe;AACrB,MAAI,WAAW,mBAAmB,IAAI,GAAG;AACxC,YAAQ,KAAK,mCAAmC,KAAK;AAAA,EACtD;AACD;AAGO,SAAS,kBACf,SACA,OACC;AACD,UAAQ,kBAAkB,MAAM,SAAS;AACzC,MAAI,WAAW,mBAAmB,IAAI,GAAG;AACxC,UAAM,cAAc,6BAA6B,IAAI;AACrD,gBAAY,IAAI,UAAU,YAAY,IAAI,OAAO,KAAK,KAAK,CAAC;AAC5D,YAAQ,KAAK,wCAAwC,SAAS,KAAK;AAAA,EACpE;AACD;AAGO,SAAS,sBACf,SACA,OACC;AACD,MAAI,CAAC,QAAQ,kBAAkB,MAAM,SAAS,GAAG;AAChD;AAAA,EACD;AAEA,UAAQ,sBAAsB,MAAM,SAAS;AAC7C,MAAI,WAAW,mBAAmB,IAAI,GAAG;AACxC,UAAM,cAAc,6BAA6B,IAAI;AACrD,QAAI,YAAY,IAAI,OAAO,MAAM,GAAG;AACnC,kBAAY,OAAO,OAAO;AAAA,IAC3B,WAAW,YAAY,IAAI,OAAO,GAAG;AACpC,kBAAY,IAAI,SAAS,YAAY,IAAI,OAAO,IAAK,CAAC;AAAA,IACvD,OAAO;AACN,cAAQ,KAAK,yBAAyB;AAAA,IACvC;AACA,YAAQ,KAAK,4CAA4C,SAAS,KAAK;AAAA,EACxE;AACD;AAUO,MAAM,uBAAuB,CAAC,MAAW,EAAE,gBAAgB;AAG3D,MAAM,mBAAmB,CAC/B,KACA,UACA,UACI;AACJ,MAAI,CAAC,IAAK;AACV,MAAI,MAAM,YAAY,UAAU,OAAO,KAAK,CAAC;AAC9C;AAGO,SAAS,yBAAyB,IAAoB,yBAAyB,MAAM;AAC3F,MAAI,CAAC,GAAI,QAAO;AAEhB,QAAM,UAAU,GAAG,QAAQ,YAAY;AACvC,SACE,GAAmB,qBACpB,YAAY,WACZ,YAAY,cACX,0BAA0B,YAAY,YACtC,0BAA0B,YAAY,YACvC,GAAG,UAAU,SAAS,oBAAoB;AAE5C;AAWO,SAAS,oBAA8B;AAE7C,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,SAAO,WAAW;AACnB;AAWO,SAAS,kBAA8C;AAC7D,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,SAAO;AACR;AAGO,SAAS,+BAA+B,yBAAyB,MAAM,KAAgB;AAC7F,SAAO;AAAA,KACL,OAAO,kBAAkB,GAAG;AAAA,IAC7B;AAAA,EACD;AACD;",
4
+ "sourcesContent": ["/*\nThis is used to facilitate double clicking and pointer capture on elements.\n\nThe events in this file are possibly set on individual SVG elements, \nsuch as handles or corner handles, rather than on HTML elements or \nSVGSVGElements. Raw SVG elements do not support pointerCapture in\nmost cases, meaning that in order for pointer capture to work, we \nneed to crawl up the DOM tree to find the nearest HTML element. Then,\nin order for that element to also call the `onPointerUp` event from\nthis file, we need to manually set that event on that element and\nlater remove it when the pointerup occurs. This is a potential leak\nif the user clicks on a handle but the pointerup does not fire for\nwhatever reason.\n*/\n\nimport { debugFlags, pointerCaptureTrackingObject } from './debug-flags'\n\n/** @public */\nexport function loopToHtmlElement(elm: Element): HTMLElement {\n\tif (elm.nodeType === Node.ELEMENT_NODE) return elm as HTMLElement\n\tif (elm.parentElement) return loopToHtmlElement(elm.parentElement)\n\telse throw Error('Could not find a parent element of an HTML type!')\n}\n\n/**\n * This function calls `event.preventDefault()` for you. Why is that useful?\n *\n * Because if you enable `window.preventDefaultLogging = true` it'll log out a message when it\n * happens. Because we use console.warn rather than (log) you'll get a stack trace in the inspector\n * telling you exactly where it happened. This is important because `e.preventDefault()` is the\n * source of many bugs, but unfortunately it can't be avoided because it also stops a lot of default\n * behaviour which doesn't make sense in our UI\n *\n * @param event - To prevent default on\n * @public\n */\nexport function preventDefault(event: React.BaseSyntheticEvent | Event) {\n\tif ('cancelable' in event && !event.cancelable) return\n\tevent.preventDefault()\n\tif (debugFlags.logPreventDefaults.get()) {\n\t\tconsole.warn('preventDefault called on event:', event)\n\t}\n}\n\n/** @public */\nexport function setPointerCapture(\n\telement: Element,\n\tevent: React.PointerEvent<Element> | PointerEvent\n) {\n\telement.setPointerCapture(event.pointerId)\n\tif (debugFlags.logPointerCaptures.get()) {\n\t\tconst trackingObj = pointerCaptureTrackingObject.get()\n\t\ttrackingObj.set(element, (trackingObj.get(element) ?? 0) + 1)\n\t\tconsole.warn('setPointerCapture called on element:', element, event)\n\t}\n}\n\n/** @public */\nexport function releasePointerCapture(\n\telement: Element,\n\tevent: React.PointerEvent<Element> | PointerEvent\n) {\n\tif (!element.hasPointerCapture(event.pointerId)) {\n\t\treturn\n\t}\n\n\telement.releasePointerCapture(event.pointerId)\n\tif (debugFlags.logPointerCaptures.get()) {\n\t\tconst trackingObj = pointerCaptureTrackingObject.get()\n\t\tif (trackingObj.get(element) === 1) {\n\t\t\ttrackingObj.delete(element)\n\t\t} else if (trackingObj.has(element)) {\n\t\t\ttrackingObj.set(element, trackingObj.get(element)! - 1)\n\t\t} else {\n\t\t\tconsole.warn('Release without capture')\n\t\t}\n\t\tconsole.warn('releasePointerCapture called on element:', element, event)\n\t}\n}\n\n/**\n * Calls `event.stopPropagation()`.\n *\n * @deprecated Use {@link Editor.markEventAsHandled} instead, or manually call `event.stopPropagation()` if\n * that's what you really want.\n *\n * @public\n */\nexport function stopEventPropagation(e: any) {\n\treturn e.stopPropagation()\n}\n\n/** @internal */\nexport function setStyleProperty(\n\telm: HTMLElement | null,\n\tproperty: string,\n\tvalue: string | number\n) {\n\tif (!elm) return\n\telm.style.setProperty(property, String(value))\n}\n\n/** @internal */\nexport function elementShouldCaptureKeys(el: Element | null, includeButtonsAndMenus = true) {\n\tif (!el) return false\n\n\tconst tagName = el.tagName.toLowerCase()\n\treturn (\n\t\t(el as HTMLElement).isContentEditable ||\n\t\ttagName === 'input' ||\n\t\ttagName === 'textarea' ||\n\t\t(includeButtonsAndMenus && tagName === 'select') ||\n\t\t(includeButtonsAndMenus && tagName === 'button') ||\n\t\tel.classList.contains('tlui-slider__thumb')\n\t)\n}\n\n/**\n * Returns the global `document`. Use this instead of bare `document` to satisfy lint rules.\n *\n * When you have a DOM node or editor instance, prefer the scoped versions instead:\n * - `getOwnerDocument(node)` \u2013 the document that owns a specific DOM node\n * - `editor.getContainerDocument()` \u2013 the document where the editor is mounted\n *\n * @internal\n */\nexport function getGlobalDocument(): Document {\n\t// eslint-disable-next-line no-restricted-globals\n\tif (typeof document !== 'undefined') return document\n\treturn globalThis.document\n}\n\n/**\n * Returns the global `window`. Use this instead of bare `window` to satisfy lint rules.\n *\n * When you have a DOM node or editor instance, prefer the scoped versions instead:\n * - `getOwnerWindow(node)` \u2013 the window that owns a specific DOM node\n * - `editor.getContainerWindow()` \u2013 the window where the editor is mounted\n *\n * @internal\n */\nexport function getGlobalWindow(): Window & typeof globalThis {\n\tif (typeof window !== 'undefined') return window as Window & typeof globalThis\n\treturn globalThis as Window & typeof globalThis\n}\n\n/** @internal */\nexport function activeElementShouldCaptureKeys(includeButtonsAndMenus = true, doc?: Document) {\n\treturn elementShouldCaptureKeys(\n\t\t(doc ?? getGlobalDocument()).activeElement,\n\t\tincludeButtonsAndMenus\n\t)\n}\n"],
5
+ "mappings": "AAeA,SAAS,YAAY,oCAAoC;AAGlD,SAAS,kBAAkB,KAA2B;AAC5D,MAAI,IAAI,aAAa,KAAK,aAAc,QAAO;AAC/C,MAAI,IAAI,cAAe,QAAO,kBAAkB,IAAI,aAAa;AAAA,MAC5D,OAAM,MAAM,kDAAkD;AACpE;AAcO,SAAS,eAAe,OAAyC;AACvE,MAAI,gBAAgB,SAAS,CAAC,MAAM,WAAY;AAChD,QAAM,eAAe;AACrB,MAAI,WAAW,mBAAmB,IAAI,GAAG;AACxC,YAAQ,KAAK,mCAAmC,KAAK;AAAA,EACtD;AACD;AAGO,SAAS,kBACf,SACA,OACC;AACD,UAAQ,kBAAkB,MAAM,SAAS;AACzC,MAAI,WAAW,mBAAmB,IAAI,GAAG;AACxC,UAAM,cAAc,6BAA6B,IAAI;AACrD,gBAAY,IAAI,UAAU,YAAY,IAAI,OAAO,KAAK,KAAK,CAAC;AAC5D,YAAQ,KAAK,wCAAwC,SAAS,KAAK;AAAA,EACpE;AACD;AAGO,SAAS,sBACf,SACA,OACC;AACD,MAAI,CAAC,QAAQ,kBAAkB,MAAM,SAAS,GAAG;AAChD;AAAA,EACD;AAEA,UAAQ,sBAAsB,MAAM,SAAS;AAC7C,MAAI,WAAW,mBAAmB,IAAI,GAAG;AACxC,UAAM,cAAc,6BAA6B,IAAI;AACrD,QAAI,YAAY,IAAI,OAAO,MAAM,GAAG;AACnC,kBAAY,OAAO,OAAO;AAAA,IAC3B,WAAW,YAAY,IAAI,OAAO,GAAG;AACpC,kBAAY,IAAI,SAAS,YAAY,IAAI,OAAO,IAAK,CAAC;AAAA,IACvD,OAAO;AACN,cAAQ,KAAK,yBAAyB;AAAA,IACvC;AACA,YAAQ,KAAK,4CAA4C,SAAS,KAAK;AAAA,EACxE;AACD;AAUO,SAAS,qBAAqB,GAAQ;AAC5C,SAAO,EAAE,gBAAgB;AAC1B;AAGO,SAAS,iBACf,KACA,UACA,OACC;AACD,MAAI,CAAC,IAAK;AACV,MAAI,MAAM,YAAY,UAAU,OAAO,KAAK,CAAC;AAC9C;AAGO,SAAS,yBAAyB,IAAoB,yBAAyB,MAAM;AAC3F,MAAI,CAAC,GAAI,QAAO;AAEhB,QAAM,UAAU,GAAG,QAAQ,YAAY;AACvC,SACE,GAAmB,qBACpB,YAAY,WACZ,YAAY,cACX,0BAA0B,YAAY,YACtC,0BAA0B,YAAY,YACvC,GAAG,UAAU,SAAS,oBAAoB;AAE5C;AAWO,SAAS,oBAA8B;AAE7C,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,SAAO,WAAW;AACnB;AAWO,SAAS,kBAA8C;AAC7D,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,SAAO;AACR;AAGO,SAAS,+BAA+B,yBAAyB,MAAM,KAAgB;AAC7F,SAAO;AAAA,KACL,OAAO,kBAAkB,GAAG;AAAA,IAC7B;AAAA,EACD;AACD;",
6
6
  "names": []
7
7
  }
@@ -1,8 +1,8 @@
1
- const version = "5.1.0";
1
+ const version = "5.2.0-canary.4a316fdfb2bb";
2
2
  const publishDates = {
3
3
  major: "2026-05-06T16:28:18.473Z",
4
- minor: "2026-06-03T10:26:13.606Z",
5
- patch: "2026-06-03T10:26:13.606Z"
4
+ minor: "2026-06-05T11:56:57.032Z",
5
+ patch: "2026-06-05T11:56:57.032Z"
6
6
  };
7
7
  export {
8
8
  publishDates,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/version.ts"],
4
- "sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.1.0'\nexport const publishDates = {\n\tmajor: '2026-05-06T16:28:18.473Z',\n\tminor: '2026-06-03T10:26:13.606Z',\n\tpatch: '2026-06-03T10:26:13.606Z',\n}\n"],
4
+ "sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.2.0-canary.4a316fdfb2bb'\nexport const publishDates = {\n\tmajor: '2026-05-06T16:28:18.473Z',\n\tminor: '2026-06-05T11:56:57.032Z',\n\tpatch: '2026-06-05T11:56:57.032Z',\n}\n"],
5
5
  "mappings": "AAGO,MAAM,UAAU;AAChB,MAAM,eAAe;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACR;",
6
6
  "names": []
7
7
  }
package/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.1.0",
4
+ "version": "5.2.0-canary.4a316fdfb2bb",
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.1.0",
53
- "@tldraw/state-react": "5.1.0",
54
- "@tldraw/store": "5.1.0",
55
- "@tldraw/tlschema": "5.1.0",
56
- "@tldraw/utils": "5.1.0",
57
- "@tldraw/validate": "5.1.0",
52
+ "@tldraw/state": "5.2.0-canary.4a316fdfb2bb",
53
+ "@tldraw/state-react": "5.2.0-canary.4a316fdfb2bb",
54
+ "@tldraw/store": "5.2.0-canary.4a316fdfb2bb",
55
+ "@tldraw/tlschema": "5.2.0-canary.4a316fdfb2bb",
56
+ "@tldraw/utils": "5.2.0-canary.4a316fdfb2bb",
57
+ "@tldraw/validate": "5.2.0-canary.4a316fdfb2bb",
58
58
  "classnames": "^2.5.1",
59
59
  "eventemitter3": "^4.0.7",
60
60
  "idb": "^7.1.1",
@@ -16,7 +16,10 @@ const BASE_ERROR_URL = 'https://github.com/tldraw/tldraw/issues/new'
16
16
  export type TLErrorFallbackComponent = ComponentType<{ error: unknown; editor?: Editor }>
17
17
 
18
18
  /** @public @react */
19
- export const DefaultErrorFallback: TLErrorFallbackComponent = ({ error, editor }) => {
19
+ export const DefaultErrorFallback: TLErrorFallbackComponent = function DefaultErrorFallback({
20
+ error,
21
+ editor,
22
+ }) {
20
23
  const containerRef = useRef<HTMLDivElement>(null)
21
24
  const [shouldShowError, setShouldShowError] = useState(process.env.NODE_ENV === 'development')
22
25
  const [didCopy, setDidCopy] = useState(false)
@@ -1,7 +1,7 @@
1
1
  import { useEditorComponents } from '../../hooks/EditorComponentsContext'
2
2
 
3
3
  /** @public @react */
4
- export const DefaultLoadingScreen = () => {
4
+ export function DefaultLoadingScreen() {
5
5
  const { Spinner } = useEditorComponents()
6
6
  return (
7
7
  <div className="tl-loading" aria-busy="true" tabIndex={0}>
@@ -4,6 +4,7 @@ import { ComponentType } from 'react'
4
4
  export type TLShapeErrorFallbackComponent = ComponentType<{ error: any }>
5
5
 
6
6
  /** @internal */
7
- export const DefaultShapeErrorFallback: TLShapeErrorFallbackComponent = () => {
8
- return <div className="tl-shape-error-boundary" />
9
- }
7
+ export const DefaultShapeErrorFallback: TLShapeErrorFallbackComponent =
8
+ function DefaultShapeErrorFallback() {
9
+ return <div className="tl-shape-error-boundary" />
10
+ }
@@ -1,4 +1,4 @@
1
1
  /** @public @react */
2
- export const DefaultSvgDefs = () => {
2
+ export function DefaultSvgDefs() {
3
3
  return null
4
4
  }
@@ -10768,6 +10768,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10768
10768
  /** @internal */
10769
10769
  private _selectedShapeIdsAtPointerDown: TLShapeId[] = []
10770
10770
 
10771
+ /**
10772
+ * Whether `_selectedShapeIdsAtPointerDown` holds a pre-gesture selection
10773
+ * captured by a `pointer_down` (the touch path) that a following pinch
10774
+ * should restore. False when no pointer_down preceded the pinch (the
10775
+ * Safari trackpad path uses gesture events), in which case `pinch_start`
10776
+ * captures the live selection instead.
10777
+ * @internal
10778
+ */
10779
+ private _didCaptureSelectionAtPointerDown = false
10780
+
10771
10781
  /** @internal */
10772
10782
  private _longPressTimeout = -1 as any
10773
10783
 
@@ -10933,16 +10943,28 @@ export class Editor extends EventEmitter<TLEventMap> {
10933
10943
  if (inputs.getIsPinching()) return
10934
10944
 
10935
10945
  if (!inputs.getIsEditing()) {
10936
- // Always capture the current selection when pinch starts.
10937
- // This ensures Safari (which uses gesture events instead of wheel)
10938
- // doesn't restore a stale selection from an earlier pointer_down.
10939
- this._selectedShapeIdsAtPointerDown = [...pageState.selectedShapeIds]
10946
+ // If a pointer_down already captured the pre-gesture selection,
10947
+ // keep it: on touch, the first finger's pointer_down can change
10948
+ // the selection before the second finger starts the pinch, and we
10949
+ // want to restore the selection from before that change. When no
10950
+ // pointer_down preceded the pinch (Safari delivers trackpad pinches
10951
+ // as gesture events), capture the live selection now.
10952
+ if (!this._didCaptureSelectionAtPointerDown) {
10953
+ this._selectedShapeIdsAtPointerDown = [...pageState.selectedShapeIds]
10954
+ }
10940
10955
 
10941
10956
  this._didPinch = true
10942
10957
 
10943
10958
  inputs.setIsPinching(true)
10944
10959
 
10945
10960
  this.interrupt()
10961
+
10962
+ // If the first finger changed the selection, roll it back now rather
10963
+ // than waiting for the pinch to end, so the pre-gesture selection is
10964
+ // what's shown during the pinch.
10965
+ if (this._didCaptureSelectionAtPointerDown) {
10966
+ this.setSelectedShapes(this._selectedShapeIdsAtPointerDown)
10967
+ }
10946
10968
  }
10947
10969
 
10948
10970
  this.emit('event', info)
@@ -10994,6 +11016,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10994
11016
  const { _selectedShapeIdsAtPointerDown: shapesToReselect } = this
10995
11017
  this.setSelectedShapes(this._selectedShapeIdsAtPointerDown)
10996
11018
  this._selectedShapeIdsAtPointerDown = []
11019
+ this._didCaptureSelectionAtPointerDown = false
10997
11020
 
10998
11021
  if (this._didPinch) {
10999
11022
  this._didPinch = false
@@ -11122,8 +11145,15 @@ export class Editor extends EventEmitter<TLEventMap> {
11122
11145
  }, this.options.longPressDurationMs)
11123
11146
  }
11124
11147
 
11125
- // Save the selected ids at pointer down
11126
- this._selectedShapeIdsAtPointerDown = this.getSelectedShapeIds()
11148
+ // Save the selected ids at the start of an interaction so a pinch can
11149
+ // restore the pre-gesture selection. Only capture on the first pointer:
11150
+ // on touch, the second finger's pointer_down arrives after the first
11151
+ // has already changed the selection, and we want the earlier snapshot.
11152
+ // Cleared on pointer_up / pinch_end.
11153
+ if (!this._didCaptureSelectionAtPointerDown) {
11154
+ this._selectedShapeIdsAtPointerDown = this.getSelectedShapeIds()
11155
+ this._didCaptureSelectionAtPointerDown = true
11156
+ }
11127
11157
 
11128
11158
  // Firefox bug fix...
11129
11159
  // If it's a left-mouse-click, we store the pointer id for later user
@@ -11243,6 +11273,7 @@ export class Editor extends EventEmitter<TLEventMap> {
11243
11273
  if (this.inputs.getIsRightPointing() && !this.inputs.getIsPanning()) {
11244
11274
  this.inputs.setIsRightPointing(false)
11245
11275
  this._selectedShapeIdsAtPointerDown = []
11276
+ this._didCaptureSelectionAtPointerDown = false
11246
11277
  break // fall through to state chart dispatch as right_click
11247
11278
  }
11248
11279
 
@@ -11289,6 +11320,7 @@ export class Editor extends EventEmitter<TLEventMap> {
11289
11320
  this.slideCamera({ speed: slideSpeed, direction: slideDirection })
11290
11321
  }
11291
11322
  this._selectedShapeIdsAtPointerDown = []
11323
+ this._didCaptureSelectionAtPointerDown = false
11292
11324
  return this
11293
11325
  }
11294
11326
  }
@@ -11307,6 +11339,7 @@ export class Editor extends EventEmitter<TLEventMap> {
11307
11339
  // Clear the stashed selection so the next pinch captures fresh state.
11308
11340
  // This fixes Safari pinch zoom restoring outdated selections.
11309
11341
  this._selectedShapeIdsAtPointerDown = []
11342
+ this._didCaptureSelectionAtPointerDown = false
11310
11343
 
11311
11344
  break
11312
11345
  }
@@ -29,7 +29,7 @@ function fromScratch(bindingsQuery: Computed<TLBinding[], unknown>) {
29
29
  return shapesToBindings
30
30
  }
31
31
 
32
- export const bindingsIndex = (editor: Editor): Computed<TLBindingsIndex> => {
32
+ export function bindingsIndex(editor: Editor): Computed<TLBindingsIndex> {
33
33
  const { store } = editor
34
34
  const bindingsHistory = store.query.filterHistory('binding')
35
35
  const bindingsQuery = store.query.records('binding')
@@ -22,7 +22,7 @@ function fromScratch(
22
22
  return result
23
23
  }
24
24
 
25
- export const parentsToChildren = (store: TLStore) => {
25
+ export function parentsToChildren(store: TLStore) {
26
26
  const shapeIdsQuery = store.query.ids<'shape'>('shape')
27
27
  const shapeHistory = store.query.filterHistory('shape')
28
28
 
@@ -33,7 +33,7 @@ const isShapeInPage = (store: TLStore, pageId: TLPageId, shape: TLShape): boolea
33
33
  * @param store - The tldraw store.
34
34
  * @param getCurrentPageId - A function that returns the current page id.
35
35
  */
36
- export const deriveShapeIdsInCurrentPage = (store: TLStore, getCurrentPageId: () => TLPageId) => {
36
+ export function deriveShapeIdsInCurrentPage(store: TLStore, getCurrentPageId: () => TLPageId) {
37
37
  const shapesIndex = store.query.ids('shape')
38
38
  let lastPageId: null | TLPageId = null
39
39
  function fromScratch() {
@@ -122,7 +122,7 @@ describe('ClickManager', () => {
122
122
  expect(result.type).toBe('click')
123
123
  expect(result.name).toBe('double_click')
124
124
  expect(result.phase).toBe('down')
125
- expect(clickManager.clickState).toBe('pendingTriple')
125
+ expect(clickManager.clickState).toBe('pendingOverflow')
126
126
  })
127
127
 
128
128
  it('should generate double_click up event on pointer_up after double_click down', () => {
@@ -139,12 +139,34 @@ describe('ClickManager', () => {
139
139
  expect(result.phase).toBe('up')
140
140
  })
141
141
 
142
- it('should dispatch double_click settle event after timeout in pendingTriple', () => {
142
+ it('should dispatch double_click settle-down event after timeout in pendingOverflow (pointer held)', () => {
143
143
  const firstDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
144
144
  const secondDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
145
145
 
146
146
  clickManager.handlePointerEvent(firstDown)
147
147
  clickManager.handlePointerEvent(secondDown)
148
+ // no pointer_up between or after — pointer is still down at settle time
149
+
150
+ vi.advanceTimersByTime(350)
151
+
152
+ expect(editor.dispatch).toHaveBeenCalledWith(
153
+ expect.objectContaining({
154
+ type: 'click',
155
+ name: 'double_click',
156
+ phase: 'settle-down',
157
+ })
158
+ )
159
+ expect(clickManager.clickState).toBe('idle')
160
+ })
161
+
162
+ it('should dispatch double_click settle-up event after timeout in pendingOverflow (pointer released)', () => {
163
+ const down = createPointerEvent('pointer_down', { x: 100, y: 100 })
164
+ const up = createPointerEvent('pointer_up', { x: 100, y: 100 })
165
+
166
+ clickManager.handlePointerEvent(down)
167
+ clickManager.handlePointerEvent(up)
168
+ clickManager.handlePointerEvent(down)
169
+ clickManager.handlePointerEvent(up)
148
170
 
149
171
  vi.advanceTimersByTime(350)
150
172
 
@@ -152,124 +174,84 @@ describe('ClickManager', () => {
152
174
  expect.objectContaining({
153
175
  type: 'click',
154
176
  name: 'double_click',
155
- phase: 'settle',
177
+ phase: 'settle-up',
156
178
  })
157
179
  )
158
180
  expect(clickManager.clickState).toBe('idle')
159
181
  })
160
182
  })
161
183
 
162
- describe('triple and quadruple click detection', () => {
163
- it('should detect triple click on third pointer_down', () => {
184
+ describe('overflow click handling', () => {
185
+ it('should enter overflow on the third pointer_down without emitting another click', () => {
164
186
  const firstDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
165
187
  const secondDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
166
188
  const thirdDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
167
189
 
168
190
  clickManager.handlePointerEvent(firstDown)
169
191
  clickManager.handlePointerEvent(secondDown)
170
- const result = clickManager.handlePointerEvent(thirdDown) as TLClickEventInfo
192
+ const result = clickManager.handlePointerEvent(thirdDown)
171
193
 
172
- expect(result.type).toBe('click')
173
- expect(result.name).toBe('triple_click')
174
- expect(result.phase).toBe('down')
175
- expect(clickManager.clickState).toBe('pendingQuadruple')
194
+ expect(result).toBe(thirdDown)
195
+ expect(clickManager.clickState).toBe('overflow')
176
196
  })
177
197
 
178
- it('should detect quadruple click on fourth pointer_down', () => {
198
+ it('should keep overflow active on further clicks', () => {
179
199
  const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
180
200
 
181
201
  clickManager.handlePointerEvent(pointerDown) // first
182
202
  clickManager.handlePointerEvent(pointerDown) // second (double_click)
183
- clickManager.handlePointerEvent(pointerDown) // third (triple_click)
184
- const result = clickManager.handlePointerEvent(pointerDown) as TLClickEventInfo // fourth
185
-
186
- expect(result.type).toBe('click')
187
- expect(result.name).toBe('quadruple_click')
188
- expect(result.phase).toBe('down')
189
- expect(clickManager.clickState).toBe('pendingOverflow')
190
- })
191
-
192
- it('should handle overflow state after quadruple click', () => {
193
- const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
194
-
195
- clickManager.handlePointerEvent(pointerDown) // first
196
- clickManager.handlePointerEvent(pointerDown) // second
197
- clickManager.handlePointerEvent(pointerDown) // third
198
- clickManager.handlePointerEvent(pointerDown) // fourth
199
- const result = clickManager.handlePointerEvent(pointerDown) // fifth
203
+ clickManager.handlePointerEvent(pointerDown) // third (overflow)
204
+ const result = clickManager.handlePointerEvent(pointerDown) // fourth
200
205
 
201
206
  expect(result).toBe(pointerDown)
202
207
  expect(clickManager.clickState).toBe('overflow')
203
208
  })
204
209
 
205
- it('should generate triple_click up event on pointer_up after triple_click down', () => {
206
- const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
207
- const pointerUp = createPointerEvent('pointer_up', { x: 100, y: 100 })
208
-
209
- clickManager.handlePointerEvent(pointerDown) // first
210
- clickManager.handlePointerEvent(pointerDown) // second
211
- clickManager.handlePointerEvent(pointerDown) // third
212
- const result = clickManager.handlePointerEvent(pointerUp) as TLClickEventInfo
213
-
214
- expect(result.type).toBe('click')
215
- expect(result.name).toBe('triple_click')
216
- expect(result.phase).toBe('up')
217
- })
218
-
219
- it('should generate quadruple_click up event on pointer_up after quadruple_click down', () => {
210
+ it('should not emit double_click up events while in overflow', () => {
220
211
  const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
221
212
  const pointerUp = createPointerEvent('pointer_up', { x: 100, y: 100 })
222
213
 
223
214
  clickManager.handlePointerEvent(pointerDown) // first
224
215
  clickManager.handlePointerEvent(pointerDown) // second
225
216
  clickManager.handlePointerEvent(pointerDown) // third
226
- clickManager.handlePointerEvent(pointerDown) // fourth
227
- const result = clickManager.handlePointerEvent(pointerUp) as TLClickEventInfo
217
+ const result = clickManager.handlePointerEvent(pointerUp)
228
218
 
229
- expect(result.type).toBe('click')
230
- expect(result.name).toBe('quadruple_click')
231
- expect(result.phase).toBe('up')
219
+ expect(result).toBe(pointerUp)
220
+ expect(clickManager.clickState).toBe('overflow')
232
221
  })
233
- })
234
222
 
235
- describe('timeout behavior and settle events', () => {
236
- it('should dispatch triple_click settle event after timeout in pendingQuadruple', () => {
223
+ it('should return to idle after overflow timeout without dispatching a settle event', () => {
237
224
  const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
238
225
 
239
226
  clickManager.handlePointerEvent(pointerDown) // first
240
227
  clickManager.handlePointerEvent(pointerDown) // second
241
- clickManager.handlePointerEvent(pointerDown) // third
228
+ clickManager.handlePointerEvent(pointerDown) // third -> overflow
242
229
 
243
230
  vi.advanceTimersByTime(350)
244
231
 
245
- expect(editor.dispatch).toHaveBeenCalledWith(
246
- expect.objectContaining({
247
- type: 'click',
248
- name: 'triple_click',
249
- phase: 'settle',
250
- })
251
- )
232
+ expect(editor.dispatch).not.toHaveBeenCalled()
252
233
  expect(clickManager.clickState).toBe('idle')
253
234
  })
235
+ })
254
236
 
255
- it('should dispatch quadruple_click settle event after timeout in pendingOverflow', () => {
256
- const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
237
+ describe('timeout behavior and settle events', () => {
238
+ it('should track press/release state across the pending window (settle-down then release → settle-up)', () => {
239
+ const down = createPointerEvent('pointer_down', { x: 100, y: 100 })
240
+ const up = createPointerEvent('pointer_up', { x: 100, y: 100 })
257
241
 
258
- clickManager.handlePointerEvent(pointerDown) // first
259
- clickManager.handlePointerEvent(pointerDown) // second
260
- clickManager.handlePointerEvent(pointerDown) // third
261
- clickManager.handlePointerEvent(pointerDown) // fourth
242
+ clickManager.handlePointerEvent(down)
243
+ clickManager.handlePointerEvent(up)
244
+ clickManager.handlePointerEvent(down) // second press — pointer is down...
245
+ clickManager.handlePointerEvent(up) // ...but released before timeout
262
246
 
263
247
  vi.advanceTimersByTime(350)
264
248
 
265
249
  expect(editor.dispatch).toHaveBeenCalledWith(
266
250
  expect.objectContaining({
267
- type: 'click',
268
- name: 'quadruple_click',
269
- phase: 'settle',
251
+ name: 'double_click',
252
+ phase: 'settle-up',
270
253
  })
271
254
  )
272
- expect(clickManager.clickState).toBe('idle')
273
255
  })
274
256
 
275
257
  it('should use different timeout durations for different states', () => {
@@ -316,7 +298,7 @@ describe('ClickManager', () => {
316
298
 
317
299
  expect(result.type).toBe('click')
318
300
  expect(result.name).toBe('double_click')
319
- expect(clickManager.clickState).toBe('pendingTriple')
301
+ expect(clickManager.clickState).toBe('pendingOverflow')
320
302
  })
321
303
  })
322
304
 
@@ -396,7 +378,7 @@ describe('ClickManager', () => {
396
378
 
397
379
  clickManager.handlePointerEvent(pointerDown)
398
380
  clickManager.handlePointerEvent(pointerDown) // double click
399
- expect(clickManager.clickState).toBe('pendingTriple')
381
+ expect(clickManager.clickState).toBe('pendingOverflow')
400
382
 
401
383
  clickManager.cancelDoubleClickTimeout()
402
384
 
@@ -416,9 +398,7 @@ describe('ClickManager', () => {
416
398
  // Get to overflow state
417
399
  clickManager.handlePointerEvent(pointerDown) // 1
418
400
  clickManager.handlePointerEvent(pointerDown) // 2
419
- clickManager.handlePointerEvent(pointerDown) // 3
420
- clickManager.handlePointerEvent(pointerDown) // 4
421
- clickManager.handlePointerEvent(pointerDown) // 5 -> overflow
401
+ clickManager.handlePointerEvent(pointerDown) // 3 -> overflow
422
402
 
423
403
  expect(clickManager.clickState).toBe('overflow')
424
404
 
@@ -4,13 +4,7 @@ import type { Editor } from '../../Editor'
4
4
  import { TLClickEventInfo, TLPointerEventInfo } from '../../types/event-types'
5
5
 
6
6
  /** @public */
7
- export type TLClickState =
8
- | 'idle'
9
- | 'pendingDouble'
10
- | 'pendingTriple'
11
- | 'pendingQuadruple'
12
- | 'pendingOverflow'
13
- | 'overflow'
7
+ export type TLClickState = 'idle' | 'pendingDouble' | 'pendingOverflow' | 'overflow'
14
8
 
15
9
  const MAX_CLICK_DISTANCE = 40
16
10
 
@@ -26,6 +20,8 @@ export class ClickManager {
26
20
 
27
21
  private _previousScreenPoint?: Vec
28
22
 
23
+ private _isPressingWhilePending = false
24
+
29
25
  @bind
30
26
  _getClickTimeout(state: TLClickState, id = uniqueId()) {
31
27
  this._clickId = id
@@ -34,30 +30,12 @@ export class ClickManager {
34
30
  () => {
35
31
  if (this._clickState === state && this._clickId === id) {
36
32
  switch (this._clickState) {
37
- case 'pendingTriple': {
38
- this.editor.dispatch({
39
- ...this.lastPointerInfo,
40
- type: 'click',
41
- name: 'double_click',
42
- phase: 'settle',
43
- })
44
- break
45
- }
46
- case 'pendingQuadruple': {
47
- this.editor.dispatch({
48
- ...this.lastPointerInfo,
49
- type: 'click',
50
- name: 'triple_click',
51
- phase: 'settle',
52
- })
53
- break
54
- }
55
33
  case 'pendingOverflow': {
56
34
  this.editor.dispatch({
57
35
  ...this.lastPointerInfo,
58
36
  type: 'click',
59
- name: 'quadruple_click',
60
- phase: 'settle',
37
+ name: 'double_click',
38
+ phase: this._isPressingWhilePending ? 'settle-down' : 'settle-up',
61
39
  })
62
40
  break
63
41
  }
@@ -100,6 +78,8 @@ export class ClickManager {
100
78
  if (!this._clickState) return info
101
79
  this._clickScreenPoint = Vec.From(info.point)
102
80
 
81
+ this._isPressingWhilePending = true
82
+
103
83
  if (
104
84
  this._previousScreenPoint &&
105
85
  Vec.Dist2(this._previousScreenPoint, this._clickScreenPoint) > MAX_CLICK_DISTANCE ** 2
@@ -113,32 +93,12 @@ export class ClickManager {
113
93
 
114
94
  switch (this._clickState) {
115
95
  case 'pendingDouble': {
116
- this._clickState = 'pendingTriple'
117
- this._clickTimeout = this._getClickTimeout(this._clickState)
118
- return {
119
- ...info,
120
- type: 'click',
121
- name: 'double_click',
122
- phase: 'down',
123
- }
124
- }
125
- case 'pendingTriple': {
126
- this._clickState = 'pendingQuadruple'
127
- this._clickTimeout = this._getClickTimeout(this._clickState)
128
- return {
129
- ...info,
130
- type: 'click',
131
- name: 'triple_click',
132
- phase: 'down',
133
- }
134
- }
135
- case 'pendingQuadruple': {
136
96
  this._clickState = 'pendingOverflow'
137
97
  this._clickTimeout = this._getClickTimeout(this._clickState)
138
98
  return {
139
99
  ...info,
140
100
  type: 'click',
141
- name: 'quadruple_click',
101
+ name: 'double_click',
142
102
  phase: 'down',
143
103
  }
144
104
  }
@@ -159,30 +119,17 @@ export class ClickManager {
159
119
  }
160
120
  case 'pointer_up': {
161
121
  if (!this._clickState) return info
122
+
162
123
  this._clickScreenPoint = Vec.From(info.point)
163
124
 
125
+ this._isPressingWhilePending = false
126
+
164
127
  switch (this._clickState) {
165
- case 'pendingTriple': {
166
- return {
167
- ...this.lastPointerInfo,
168
- type: 'click',
169
- name: 'double_click',
170
- phase: 'up',
171
- }
172
- }
173
- case 'pendingQuadruple': {
174
- return {
175
- ...this.lastPointerInfo,
176
- type: 'click',
177
- name: 'triple_click',
178
- phase: 'up',
179
- }
180
- }
181
128
  case 'pendingOverflow': {
182
129
  return {
183
130
  ...this.lastPointerInfo,
184
131
  type: 'click',
185
- name: 'quadruple_click',
132
+ name: 'double_click',
186
133
  phase: 'up',
187
134
  }
188
135
  }
@@ -219,5 +166,8 @@ export class ClickManager {
219
166
  cancelDoubleClickTimeout() {
220
167
  this._clickTimeout = clearTimeout(this._clickTimeout)
221
168
  this._clickState = 'idle'
169
+ // when a double click is cancelled, we are no longer pending any further
170
+ // clicks, so we set this to false even if the user is still pressing
171
+ this._isPressingWhilePending = false
222
172
  }
223
173
  }