@tldraw/editor 3.14.0-canary.2f3caa391d5d → 3.14.0-canary.306affbc4326

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 (40) hide show
  1. package/dist-cjs/index.d.ts +27 -34
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/editor/Editor.js +1 -0
  5. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  6. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +1 -2
  7. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
  8. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +72 -42
  9. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  10. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +1 -1
  11. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +1 -1
  12. package/dist-cjs/lib/utils/richText.js +7 -2
  13. package/dist-cjs/lib/utils/richText.js.map +2 -2
  14. package/dist-cjs/version.js +3 -3
  15. package/dist-cjs/version.js.map +1 -1
  16. package/dist-esm/index.d.mts +27 -34
  17. package/dist-esm/index.mjs +1 -1
  18. package/dist-esm/index.mjs.map +2 -2
  19. package/dist-esm/lib/editor/Editor.mjs +1 -0
  20. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  21. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +1 -2
  22. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
  23. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +72 -42
  24. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  25. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +1 -1
  26. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +1 -1
  27. package/dist-esm/lib/utils/richText.mjs +8 -3
  28. package/dist-esm/lib/utils/richText.mjs.map +2 -2
  29. package/dist-esm/version.mjs +3 -3
  30. package/dist-esm/version.mjs.map +1 -1
  31. package/editor.css +458 -523
  32. package/package.json +7 -7
  33. package/src/index.ts +1 -0
  34. package/src/lib/editor/Editor.ts +2 -0
  35. package/src/lib/editor/managers/FontManager/FontManager.ts +1 -2
  36. package/src/lib/editor/managers/TextManager/TextManager.test.ts +1 -5
  37. package/src/lib/editor/managers/TextManager/TextManager.ts +116 -86
  38. package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +1 -1
  39. package/src/lib/utils/richText.ts +9 -3
  40. package/src/version.ts +3 -3
@@ -36,8 +36,7 @@ class FontManager {
36
36
  },
37
37
  {
38
38
  areResultsEqual: import_utils.areArraysShallowEqual,
39
- // @ts-expect-error
40
- areRecordsEqual: (a, b) => a.props.richText === b.props.richText
39
+ areRecordsEqual: (a, b) => a.props === b.props && a.meta === b.meta
41
40
  }
42
41
  );
43
42
  this.shapeFontLoadStateCache = editor.store.createCache(
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/lib/editor/managers/FontManager/FontManager.ts"],
4
- "sourcesContent": ["import { computed, EMPTY_ARRAY, transact } from '@tldraw/state'\nimport { AtomMap } from '@tldraw/store'\nimport { TLShape, TLShapeId } from '@tldraw/tlschema'\nimport {\n\tareArraysShallowEqual,\n\tcompact,\n\tFileHelpers,\n\tmapObjectMapValues,\n\tobjectMapEntries,\n} from '@tldraw/utils'\nimport { Editor } from '../../Editor'\n\n/**\n * Represents the `src` property of a {@link TLFontFace}.\n * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/src | `src`} for details of the properties here.\n * @public\n */\nexport interface TLFontFaceSource {\n\t/**\n\t * A URL from which to load the font. If the value here is a key in\n\t * {@link tldraw#TLEditorAssetUrls.fonts}, the value from there will be used instead.\n\t */\n\turl: string\n\tformat?: string\n\ttech?: string\n}\n\n/**\n * A font face that can be used in the editor. The properties of this are largely the same as the\n * ones in the\n * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face | css `@font-face` rule}.\n * @public\n */\nexport interface TLFontFace {\n\t/**\n\t * How this font can be referred to in CSS.\n\t * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-family | `font-family`}.\n\t */\n\treadonly family: string\n\t/**\n\t * The source of the font. This\n\t * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/src | `src`}.\n\t */\n\treadonly src: TLFontFaceSource\n\t/**\n\t * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/ascent-override | `ascent-override`}.\n\t */\n\treadonly ascentOverride?: string\n\t/**\n\t * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/descent-override | `descent-override`}.\n\t */\n\treadonly descentOverride?: string\n\t/**\n\t * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-stretch | `font-stretch`}.\n\t */\n\treadonly stretch?: string\n\t/**\n\t * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-style | `font-style`}.\n\t */\n\treadonly style?: string\n\t/**\n\t * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-weight | `font-weight`}.\n\t */\n\treadonly weight?: string\n\t/**\n\t * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-feature-settings | `font-feature-settings`}.\n\t */\n\treadonly featureSettings?: string\n\t/**\n\t * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/line-gap-override | `line-gap-override`}.\n\t */\n\treadonly lineGapOverride?: string\n\t/**\n\t * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range | `unicode-range`}.\n\t */\n\treadonly unicodeRange?: string\n}\n\ninterface FontState {\n\treadonly state: 'loading' | 'ready' | 'error'\n\treadonly instance: FontFace\n\treadonly loadingPromise: Promise<void>\n}\n\n/** @public */\nexport class FontManager {\n\tconstructor(\n\t\tprivate readonly editor: Editor,\n\t\tprivate readonly assetUrls?: { [key: string]: string | undefined }\n\t) {\n\t\tthis.shapeFontFacesCache = editor.store.createComputedCache(\n\t\t\t'shape font faces',\n\t\t\t(shape: TLShape) => {\n\t\t\t\tconst shapeUtil = this.editor.getShapeUtil(shape)\n\t\t\t\treturn shapeUtil.getFontFaces(shape)\n\t\t\t},\n\t\t\t{\n\t\t\t\tareResultsEqual: areArraysShallowEqual,\n\t\t\t\t// @ts-expect-error\n\t\t\t\tareRecordsEqual: (a, b) => a.props.richText === b.props.richText,\n\t\t\t}\n\t\t)\n\n\t\tthis.shapeFontLoadStateCache = editor.store.createCache<(FontState | null)[], TLShape>(\n\t\t\t(id: TLShapeId) => {\n\t\t\t\tconst fontFacesComputed = computed('font faces', () => this.getShapeFontFaces(id))\n\t\t\t\treturn computed(\n\t\t\t\t\t'font load state',\n\t\t\t\t\t() => {\n\t\t\t\t\t\tconst states = fontFacesComputed.get().map((face) => this.getFontState(face))\n\t\t\t\t\t\treturn states\n\t\t\t\t\t},\n\t\t\t\t\t{ isEqual: areArraysShallowEqual }\n\t\t\t\t)\n\t\t\t}\n\t\t)\n\t}\n\n\tprivate readonly shapeFontFacesCache\n\tprivate readonly shapeFontLoadStateCache\n\n\tgetShapeFontFaces(shape: TLShape | TLShapeId): TLFontFace[] {\n\t\tconst shapeId = typeof shape === 'string' ? shape : shape.id\n\t\treturn this.shapeFontFacesCache.get(shapeId) ?? EMPTY_ARRAY\n\t}\n\n\ttrackFontsForShape(shape: TLShape | TLShapeId) {\n\t\tconst shapeId = typeof shape === 'string' ? shape : shape.id\n\t\tthis.shapeFontLoadStateCache.get(shapeId)\n\t}\n\n\tasync loadRequiredFontsForCurrentPage(limit = Infinity) {\n\t\tconst neededFonts = new Set<TLFontFace>()\n\t\tfor (const shapeId of this.editor.getCurrentPageShapeIds()) {\n\t\t\tfor (const font of this.getShapeFontFaces(this.editor.getShape(shapeId)!)) {\n\t\t\t\tneededFonts.add(font)\n\t\t\t}\n\t\t}\n\n\t\tif (neededFonts.size > limit) {\n\t\t\treturn\n\t\t}\n\n\t\tconst promises = Array.from(neededFonts, (font) => this.ensureFontIsLoaded(font))\n\t\tawait Promise.all(promises)\n\t}\n\n\tprivate readonly fontStates = new AtomMap<TLFontFace, FontState>('font states')\n\tprivate getFontState(font: TLFontFace): FontState | null {\n\t\treturn this.fontStates.get(font) ?? null\n\t}\n\n\tensureFontIsLoaded(font: TLFontFace): Promise<void> {\n\t\tconst existingState = this.getFontState(font)\n\t\tif (existingState) return existingState.loadingPromise\n\n\t\tconst instance = this.findOrCreateFontFace(font)\n\t\tconst state: FontState = {\n\t\t\tstate: 'loading',\n\t\t\tinstance,\n\t\t\tloadingPromise: instance\n\t\t\t\t.load()\n\t\t\t\t.then(() => {\n\t\t\t\t\tdocument.fonts.add(instance)\n\t\t\t\t\tthis.fontStates.update(font, (s) => ({ ...s, state: 'ready' }))\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tconsole.error(err)\n\t\t\t\t\tthis.fontStates.update(font, (s) => ({ ...s, state: 'error' }))\n\t\t\t\t}),\n\t\t}\n\n\t\tthis.fontStates.set(font, state)\n\t\treturn state.loadingPromise\n\t}\n\n\tprivate fontsToLoad = new Set<TLFontFace>()\n\trequestFonts(fonts: TLFontFace[]) {\n\t\tif (!this.fontsToLoad.size) {\n\t\t\tqueueMicrotask(() => {\n\t\t\t\tif (this.editor.isDisposed) return\n\t\t\t\tconst toLoad = this.fontsToLoad\n\t\t\t\tthis.fontsToLoad = new Set()\n\t\t\t\ttransact(() => {\n\t\t\t\t\tfor (const font of toLoad) {\n\t\t\t\t\t\tthis.ensureFontIsLoaded(font)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t})\n\t\t}\n\t\tfor (const font of fonts) {\n\t\t\tthis.fontsToLoad.add(font)\n\t\t}\n\t}\n\n\tprivate findOrCreateFontFace(font: TLFontFace) {\n\t\tfor (const existing of document.fonts) {\n\t\t\tif (\n\t\t\t\texisting.family === font.family &&\n\t\t\t\tobjectMapEntries(defaultFontFaceDescriptors).every(\n\t\t\t\t\t([key, defaultValue]) => existing[key] === (font[key] ?? defaultValue)\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\treturn existing\n\t\t\t}\n\t\t}\n\n\t\tconst url = this.assetUrls?.[font.src.url] ?? font.src.url\n\t\tconst instance = new FontFace(font.family, `url(${JSON.stringify(url)})`, {\n\t\t\t...mapObjectMapValues(defaultFontFaceDescriptors, (key) => font[key]),\n\t\t\tdisplay: 'swap',\n\t\t})\n\n\t\tdocument.fonts.add(instance)\n\n\t\treturn instance\n\t}\n\n\tasync toEmbeddedCssDeclaration(font: TLFontFace) {\n\t\tconst url = this.assetUrls?.[font.src.url] ?? font.src.url\n\t\tconst dataUrl = await FileHelpers.urlToDataUrl(url)\n\n\t\tconst src = compact([\n\t\t\t`url(\"${dataUrl}\")`,\n\t\t\tfont.src.format ? `format(${font.src.format})` : null,\n\t\t\tfont.src.tech ? `tech(${font.src.tech})` : null,\n\t\t]).join(' ')\n\t\treturn compact([\n\t\t\t`@font-face {`,\n\t\t\t` font-family: \"${font.family}\";`,\n\t\t\tfont.ascentOverride ? ` ascent-override: ${font.ascentOverride};` : null,\n\t\t\tfont.descentOverride ? ` descent-override: ${font.descentOverride};` : null,\n\t\t\tfont.stretch ? ` font-stretch: ${font.stretch};` : null,\n\t\t\tfont.style ? ` font-style: ${font.style};` : null,\n\t\t\tfont.weight ? ` font-weight: ${font.weight};` : null,\n\t\t\tfont.featureSettings ? ` font-feature-settings: ${font.featureSettings};` : null,\n\t\t\tfont.lineGapOverride ? ` line-gap-override: ${font.lineGapOverride};` : null,\n\t\t\tfont.unicodeRange ? ` unicode-range: ${font.unicodeRange};` : null,\n\t\t\t` src: ${src};`,\n\t\t\t`}`,\n\t\t]).join('\\n')\n\t}\n}\n\n// From https://drafts.csswg.org/css-font-loading/#fontface-interface\nconst defaultFontFaceDescriptors = {\n\tstyle: 'normal',\n\tweight: 'normal',\n\tstretch: 'normal',\n\tunicodeRange: 'U+0-10FFFF',\n\tfeatureSettings: 'normal',\n\tascentOverride: 'normal',\n\tdescentOverride: 'normal',\n\tlineGapOverride: 'normal',\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { computed, EMPTY_ARRAY, transact } from '@tldraw/state'\nimport { AtomMap } from '@tldraw/store'\nimport { TLShape, TLShapeId } from '@tldraw/tlschema'\nimport {\n\tareArraysShallowEqual,\n\tcompact,\n\tFileHelpers,\n\tmapObjectMapValues,\n\tobjectMapEntries,\n} from '@tldraw/utils'\nimport { Editor } from '../../Editor'\n\n/**\n * Represents the `src` property of a {@link TLFontFace}.\n * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/src | `src`} for details of the properties here.\n * @public\n */\nexport interface TLFontFaceSource {\n\t/**\n\t * A URL from which to load the font. If the value here is a key in\n\t * {@link tldraw#TLEditorAssetUrls.fonts}, the value from there will be used instead.\n\t */\n\turl: string\n\tformat?: string\n\ttech?: string\n}\n\n/**\n * A font face that can be used in the editor. The properties of this are largely the same as the\n * ones in the\n * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face | css `@font-face` rule}.\n * @public\n */\nexport interface TLFontFace {\n\t/**\n\t * How this font can be referred to in CSS.\n\t * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-family | `font-family`}.\n\t */\n\treadonly family: string\n\t/**\n\t * The source of the font. This\n\t * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/src | `src`}.\n\t */\n\treadonly src: TLFontFaceSource\n\t/**\n\t * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/ascent-override | `ascent-override`}.\n\t */\n\treadonly ascentOverride?: string\n\t/**\n\t * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/descent-override | `descent-override`}.\n\t */\n\treadonly descentOverride?: string\n\t/**\n\t * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-stretch | `font-stretch`}.\n\t */\n\treadonly stretch?: string\n\t/**\n\t * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-style | `font-style`}.\n\t */\n\treadonly style?: string\n\t/**\n\t * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-weight | `font-weight`}.\n\t */\n\treadonly weight?: string\n\t/**\n\t * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-feature-settings | `font-feature-settings`}.\n\t */\n\treadonly featureSettings?: string\n\t/**\n\t * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/line-gap-override | `line-gap-override`}.\n\t */\n\treadonly lineGapOverride?: string\n\t/**\n\t * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range | `unicode-range`}.\n\t */\n\treadonly unicodeRange?: string\n}\n\ninterface FontState {\n\treadonly state: 'loading' | 'ready' | 'error'\n\treadonly instance: FontFace\n\treadonly loadingPromise: Promise<void>\n}\n\n/** @public */\nexport class FontManager {\n\tconstructor(\n\t\tprivate readonly editor: Editor,\n\t\tprivate readonly assetUrls?: { [key: string]: string | undefined }\n\t) {\n\t\tthis.shapeFontFacesCache = editor.store.createComputedCache(\n\t\t\t'shape font faces',\n\t\t\t(shape: TLShape) => {\n\t\t\t\tconst shapeUtil = this.editor.getShapeUtil(shape)\n\t\t\t\treturn shapeUtil.getFontFaces(shape)\n\t\t\t},\n\t\t\t{\n\t\t\t\tareResultsEqual: areArraysShallowEqual,\n\t\t\t\tareRecordsEqual: (a, b) => a.props === b.props && a.meta === b.meta,\n\t\t\t}\n\t\t)\n\n\t\tthis.shapeFontLoadStateCache = editor.store.createCache<(FontState | null)[], TLShape>(\n\t\t\t(id: TLShapeId) => {\n\t\t\t\tconst fontFacesComputed = computed('font faces', () => this.getShapeFontFaces(id))\n\t\t\t\treturn computed(\n\t\t\t\t\t'font load state',\n\t\t\t\t\t() => {\n\t\t\t\t\t\tconst states = fontFacesComputed.get().map((face) => this.getFontState(face))\n\t\t\t\t\t\treturn states\n\t\t\t\t\t},\n\t\t\t\t\t{ isEqual: areArraysShallowEqual }\n\t\t\t\t)\n\t\t\t}\n\t\t)\n\t}\n\n\tprivate readonly shapeFontFacesCache\n\tprivate readonly shapeFontLoadStateCache\n\n\tgetShapeFontFaces(shape: TLShape | TLShapeId): TLFontFace[] {\n\t\tconst shapeId = typeof shape === 'string' ? shape : shape.id\n\t\treturn this.shapeFontFacesCache.get(shapeId) ?? EMPTY_ARRAY\n\t}\n\n\ttrackFontsForShape(shape: TLShape | TLShapeId) {\n\t\tconst shapeId = typeof shape === 'string' ? shape : shape.id\n\t\tthis.shapeFontLoadStateCache.get(shapeId)\n\t}\n\n\tasync loadRequiredFontsForCurrentPage(limit = Infinity) {\n\t\tconst neededFonts = new Set<TLFontFace>()\n\t\tfor (const shapeId of this.editor.getCurrentPageShapeIds()) {\n\t\t\tfor (const font of this.getShapeFontFaces(this.editor.getShape(shapeId)!)) {\n\t\t\t\tneededFonts.add(font)\n\t\t\t}\n\t\t}\n\n\t\tif (neededFonts.size > limit) {\n\t\t\treturn\n\t\t}\n\n\t\tconst promises = Array.from(neededFonts, (font) => this.ensureFontIsLoaded(font))\n\t\tawait Promise.all(promises)\n\t}\n\n\tprivate readonly fontStates = new AtomMap<TLFontFace, FontState>('font states')\n\tprivate getFontState(font: TLFontFace): FontState | null {\n\t\treturn this.fontStates.get(font) ?? null\n\t}\n\n\tensureFontIsLoaded(font: TLFontFace): Promise<void> {\n\t\tconst existingState = this.getFontState(font)\n\t\tif (existingState) return existingState.loadingPromise\n\n\t\tconst instance = this.findOrCreateFontFace(font)\n\t\tconst state: FontState = {\n\t\t\tstate: 'loading',\n\t\t\tinstance,\n\t\t\tloadingPromise: instance\n\t\t\t\t.load()\n\t\t\t\t.then(() => {\n\t\t\t\t\tdocument.fonts.add(instance)\n\t\t\t\t\tthis.fontStates.update(font, (s) => ({ ...s, state: 'ready' }))\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tconsole.error(err)\n\t\t\t\t\tthis.fontStates.update(font, (s) => ({ ...s, state: 'error' }))\n\t\t\t\t}),\n\t\t}\n\n\t\tthis.fontStates.set(font, state)\n\t\treturn state.loadingPromise\n\t}\n\n\tprivate fontsToLoad = new Set<TLFontFace>()\n\trequestFonts(fonts: TLFontFace[]) {\n\t\tif (!this.fontsToLoad.size) {\n\t\t\tqueueMicrotask(() => {\n\t\t\t\tif (this.editor.isDisposed) return\n\t\t\t\tconst toLoad = this.fontsToLoad\n\t\t\t\tthis.fontsToLoad = new Set()\n\t\t\t\ttransact(() => {\n\t\t\t\t\tfor (const font of toLoad) {\n\t\t\t\t\t\tthis.ensureFontIsLoaded(font)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t})\n\t\t}\n\t\tfor (const font of fonts) {\n\t\t\tthis.fontsToLoad.add(font)\n\t\t}\n\t}\n\n\tprivate findOrCreateFontFace(font: TLFontFace) {\n\t\tfor (const existing of document.fonts) {\n\t\t\tif (\n\t\t\t\texisting.family === font.family &&\n\t\t\t\tobjectMapEntries(defaultFontFaceDescriptors).every(\n\t\t\t\t\t([key, defaultValue]) => existing[key] === (font[key] ?? defaultValue)\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\treturn existing\n\t\t\t}\n\t\t}\n\n\t\tconst url = this.assetUrls?.[font.src.url] ?? font.src.url\n\t\tconst instance = new FontFace(font.family, `url(${JSON.stringify(url)})`, {\n\t\t\t...mapObjectMapValues(defaultFontFaceDescriptors, (key) => font[key]),\n\t\t\tdisplay: 'swap',\n\t\t})\n\n\t\tdocument.fonts.add(instance)\n\n\t\treturn instance\n\t}\n\n\tasync toEmbeddedCssDeclaration(font: TLFontFace) {\n\t\tconst url = this.assetUrls?.[font.src.url] ?? font.src.url\n\t\tconst dataUrl = await FileHelpers.urlToDataUrl(url)\n\n\t\tconst src = compact([\n\t\t\t`url(\"${dataUrl}\")`,\n\t\t\tfont.src.format ? `format(${font.src.format})` : null,\n\t\t\tfont.src.tech ? `tech(${font.src.tech})` : null,\n\t\t]).join(' ')\n\t\treturn compact([\n\t\t\t`@font-face {`,\n\t\t\t` font-family: \"${font.family}\";`,\n\t\t\tfont.ascentOverride ? ` ascent-override: ${font.ascentOverride};` : null,\n\t\t\tfont.descentOverride ? ` descent-override: ${font.descentOverride};` : null,\n\t\t\tfont.stretch ? ` font-stretch: ${font.stretch};` : null,\n\t\t\tfont.style ? ` font-style: ${font.style};` : null,\n\t\t\tfont.weight ? ` font-weight: ${font.weight};` : null,\n\t\t\tfont.featureSettings ? ` font-feature-settings: ${font.featureSettings};` : null,\n\t\t\tfont.lineGapOverride ? ` line-gap-override: ${font.lineGapOverride};` : null,\n\t\t\tfont.unicodeRange ? ` unicode-range: ${font.unicodeRange};` : null,\n\t\t\t` src: ${src};`,\n\t\t\t`}`,\n\t\t]).join('\\n')\n\t}\n}\n\n// From https://drafts.csswg.org/css-font-loading/#fontface-interface\nconst defaultFontFaceDescriptors = {\n\tstyle: 'normal',\n\tweight: 'normal',\n\tstretch: 'normal',\n\tunicodeRange: 'U+0-10FFFF',\n\tfeatureSettings: 'normal',\n\tascentOverride: 'normal',\n\tdescentOverride: 'normal',\n\tlineGapOverride: 'normal',\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -37,44 +37,70 @@ const spaceCharacterRegex = /\s/;
37
37
  class TextManager {
38
38
  constructor(editor) {
39
39
  this.editor = editor;
40
- this.baseElem = document.createElement("div");
41
- this.baseElem.classList.add("tl-text");
42
- this.baseElem.classList.add("tl-text-measure");
43
- this.baseElem.tabIndex = -1;
40
+ const elm = document.createElement("div");
41
+ elm.classList.add("tl-text");
42
+ elm.classList.add("tl-text-measure");
43
+ elm.setAttribute("dir", "auto");
44
+ elm.tabIndex = -1;
45
+ this.editor.getContainer().appendChild(elm);
46
+ this.defaultStyles = {
47
+ "word-break": "auto",
48
+ width: null,
49
+ height: null,
50
+ "max-width": null,
51
+ "min-width": null
52
+ };
53
+ this.elm = elm;
54
+ }
55
+ elm;
56
+ defaultStyles;
57
+ dispose() {
58
+ return this.elm.remove();
59
+ }
60
+ resetElmStyles() {
61
+ const { elm, defaultStyles } = this;
62
+ for (const key in defaultStyles) {
63
+ elm.style.setProperty(key, defaultStyles[key]);
64
+ }
44
65
  }
45
- baseElem;
46
66
  measureText(textToMeasure, opts) {
47
67
  const div = document.createElement("div");
48
68
  div.textContent = normalizeTextForDom(textToMeasure);
49
69
  return this.measureHtml(div.innerHTML, opts);
50
70
  }
51
71
  measureHtml(html, opts) {
52
- const wrapperElm = this.baseElem.cloneNode();
53
- this.editor.getContainer().appendChild(wrapperElm);
54
- wrapperElm.innerHTML = html;
55
- this.baseElem.insertAdjacentElement("afterend", wrapperElm);
56
- wrapperElm.setAttribute("dir", "auto");
57
- wrapperElm.style.setProperty("unicode-bidi", "plaintext");
58
- wrapperElm.style.setProperty("font-family", opts.fontFamily);
59
- wrapperElm.style.setProperty("font-style", opts.fontStyle);
60
- wrapperElm.style.setProperty("font-weight", opts.fontWeight);
61
- wrapperElm.style.setProperty("font-size", opts.fontSize + "px");
62
- wrapperElm.style.setProperty("line-height", opts.lineHeight * opts.fontSize + "px");
63
- wrapperElm.style.setProperty("max-width", opts.maxWidth === null ? null : opts.maxWidth + "px");
64
- wrapperElm.style.setProperty("min-width", opts.minWidth === null ? null : opts.minWidth + "px");
65
- wrapperElm.style.setProperty("padding", opts.padding);
66
- wrapperElm.style.setProperty(
67
- "overflow-wrap",
68
- opts.disableOverflowWrapBreaking ? "normal" : "break-word"
69
- );
72
+ const { elm } = this;
73
+ if (opts.otherStyles) {
74
+ for (const key in opts.otherStyles) {
75
+ if (!this.defaultStyles[key]) {
76
+ this.defaultStyles[key] = elm.style.getPropertyValue(key);
77
+ }
78
+ }
79
+ }
80
+ elm.innerHTML = html;
81
+ this.resetElmStyles();
82
+ elm.style.setProperty("font-family", opts.fontFamily);
83
+ elm.style.setProperty("font-style", opts.fontStyle);
84
+ elm.style.setProperty("font-weight", opts.fontWeight);
85
+ elm.style.setProperty("font-size", opts.fontSize + "px");
86
+ elm.style.setProperty("line-height", opts.lineHeight * opts.fontSize + "px");
87
+ elm.style.setProperty("padding", opts.padding);
88
+ if (opts.maxWidth) {
89
+ elm.style.setProperty("max-width", opts.maxWidth + "px");
90
+ }
91
+ if (opts.minWidth) {
92
+ elm.style.setProperty("min-width", opts.minWidth + "px");
93
+ }
94
+ if (opts.disableOverflowWrapBreaking) {
95
+ elm.style.setProperty("overflow-wrap", "normal");
96
+ }
70
97
  if (opts.otherStyles) {
71
98
  for (const [key, value] of Object.entries(opts.otherStyles)) {
72
- wrapperElm.style.setProperty(key, value);
99
+ elm.style.setProperty(key, value);
73
100
  }
74
101
  }
75
- const scrollWidth = wrapperElm.scrollWidth;
76
- const rect = wrapperElm.getBoundingClientRect();
77
- wrapperElm.remove();
102
+ const scrollWidth = opts.measureScrollWidth ? elm.scrollWidth : 0;
103
+ const rect = elm.getBoundingClientRect();
78
104
  return {
79
105
  x: 0,
80
106
  y: 0,
@@ -160,29 +186,34 @@ class TextManager {
160
186
  */
161
187
  measureTextSpans(textToMeasure, opts) {
162
188
  if (textToMeasure === "") return [];
163
- const elm = this.baseElem.cloneNode();
164
- this.editor.getContainer().appendChild(elm);
189
+ const { elm } = this;
190
+ if (opts.otherStyles) {
191
+ for (const key in opts.otherStyles) {
192
+ if (!this.defaultStyles[key]) {
193
+ this.defaultStyles[key] = elm.style.getPropertyValue(key);
194
+ }
195
+ }
196
+ }
197
+ this.resetElmStyles();
198
+ elm.style.setProperty("font-family", opts.fontFamily);
199
+ elm.style.setProperty("font-style", opts.fontStyle);
200
+ elm.style.setProperty("font-weight", opts.fontWeight);
201
+ elm.style.setProperty("font-size", opts.fontSize + "px");
202
+ elm.style.setProperty("line-height", opts.lineHeight * opts.fontSize + "px");
165
203
  const elementWidth = Math.ceil(opts.width - opts.padding * 2);
166
- elm.setAttribute("dir", "auto");
167
- elm.style.setProperty("unicode-bidi", "plaintext");
168
204
  elm.style.setProperty("width", `${elementWidth}px`);
169
205
  elm.style.setProperty("height", "min-content");
170
- elm.style.setProperty("font-size", `${opts.fontSize}px`);
171
- elm.style.setProperty("font-family", opts.fontFamily);
172
- elm.style.setProperty("font-weight", opts.fontWeight);
173
- elm.style.setProperty("line-height", `${opts.lineHeight * opts.fontSize}px`);
174
206
  elm.style.setProperty("text-align", textAlignmentsForLtr[opts.textAlign]);
175
- elm.style.setProperty("font-style", opts.fontStyle);
176
- if (opts.otherStyles) {
177
- for (const [key, value] of Object.entries(opts.otherStyles)) {
178
- elm.style.setProperty(key, value);
179
- }
180
- }
181
207
  const shouldTruncateToFirstLine = opts.overflow === "truncate-ellipsis" || opts.overflow === "truncate-clip";
182
208
  if (shouldTruncateToFirstLine) {
183
209
  elm.style.setProperty("overflow-wrap", "anywhere");
184
210
  elm.style.setProperty("word-break", "break-all");
185
211
  }
212
+ if (opts.otherStyles) {
213
+ for (const [key, value] of Object.entries(opts.otherStyles)) {
214
+ elm.style.setProperty(key, value);
215
+ }
216
+ }
186
217
  const normalizedText = normalizeTextForDom(textToMeasure);
187
218
  elm.textContent = normalizedText;
188
219
  const { spans, didTruncate } = this.measureElementTextNodeSpans(elm, {
@@ -208,7 +239,6 @@ class TextManager {
208
239
  });
209
240
  return truncatedSpans;
210
241
  }
211
- elm.remove();
212
242
  return spans;
213
243
  }
214
244
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/lib/editor/managers/TextManager/TextManager.ts"],
4
- "sourcesContent": ["import { BoxModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'\nimport { Editor } from '../../Editor'\n\nconst fixNewLines = /\\r?\\n|\\r/g\n\nfunction normalizeTextForDom(text: string) {\n\treturn text\n\t\t.replace(fixNewLines, '\\n')\n\t\t.split('\\n')\n\t\t.map((x) => x || ' ')\n\t\t.join('\\n')\n}\n\nconst textAlignmentsForLtr = {\n\tstart: 'left',\n\t'start-legacy': 'left',\n\tmiddle: 'center',\n\t'middle-legacy': 'center',\n\tend: 'right',\n\t'end-legacy': 'right',\n}\n\n/** @public */\nexport interface TLMeasureTextSpanOpts {\n\toverflow: 'wrap' | 'truncate-ellipsis' | 'truncate-clip'\n\twidth: number\n\theight: number\n\tpadding: number\n\tfontSize: number\n\tfontWeight: string\n\tfontFamily: string\n\tfontStyle: string\n\tlineHeight: number\n\ttextAlign: TLDefaultHorizontalAlignStyle\n\totherStyles?: Record<string, string>\n}\n\nconst spaceCharacterRegex = /\\s/\n\n/** @public */\nexport class TextManager {\n\tprivate baseElem: HTMLDivElement\n\n\tconstructor(public editor: Editor) {\n\t\tthis.baseElem = document.createElement('div')\n\t\tthis.baseElem.classList.add('tl-text')\n\t\tthis.baseElem.classList.add('tl-text-measure')\n\t\tthis.baseElem.tabIndex = -1\n\t}\n\n\tmeasureText(\n\t\ttextToMeasure: string,\n\t\topts: {\n\t\t\tfontStyle: string\n\t\t\tfontWeight: string\n\t\t\tfontFamily: string\n\t\t\tfontSize: number\n\t\t\tlineHeight: number\n\t\t\t/**\n\t\t\t * When maxWidth is a number, the text will be wrapped to that maxWidth. When maxWidth\n\t\t\t * is null, the text will be measured without wrapping, but explicit line breaks and\n\t\t\t * space are preserved.\n\t\t\t */\n\t\t\tmaxWidth: null | number\n\t\t\tminWidth?: null | number\n\t\t\tpadding: string\n\t\t\tdisableOverflowWrapBreaking?: boolean\n\t\t}\n\t): BoxModel & { scrollWidth: number } {\n\t\tconst div = document.createElement('div')\n\t\tdiv.textContent = normalizeTextForDom(textToMeasure)\n\t\treturn this.measureHtml(div.innerHTML, opts)\n\t}\n\n\tmeasureHtml(\n\t\thtml: string,\n\t\topts: {\n\t\t\tfontStyle: string\n\t\t\tfontWeight: string\n\t\t\tfontFamily: string\n\t\t\tfontSize: number\n\t\t\tlineHeight: number\n\t\t\t/**\n\t\t\t * When maxWidth is a number, the text will be wrapped to that maxWidth. When maxWidth\n\t\t\t * is null, the text will be measured without wrapping, but explicit line breaks and\n\t\t\t * space are preserved.\n\t\t\t */\n\t\t\tmaxWidth: null | number\n\t\t\tminWidth?: null | number\n\t\t\totherStyles?: Record<string, string>\n\t\t\tpadding: string\n\t\t\tdisableOverflowWrapBreaking?: boolean\n\t\t}\n\t): BoxModel & { scrollWidth: number } {\n\t\t// Duplicate our base element; we don't need to clone deep\n\t\tconst wrapperElm = this.baseElem.cloneNode() as HTMLDivElement\n\t\tthis.editor.getContainer().appendChild(wrapperElm)\n\t\twrapperElm.innerHTML = html\n\t\tthis.baseElem.insertAdjacentElement('afterend', wrapperElm)\n\n\t\twrapperElm.setAttribute('dir', 'auto')\n\t\t// N.B. This property, while discouraged (\"intended for Document Type Definition (DTD) designers\")\n\t\t// is necessary for ensuring correct mixed RTL/LTR behavior when exporting SVGs.\n\t\twrapperElm.style.setProperty('unicode-bidi', 'plaintext')\n\t\twrapperElm.style.setProperty('font-family', opts.fontFamily)\n\t\twrapperElm.style.setProperty('font-style', opts.fontStyle)\n\t\twrapperElm.style.setProperty('font-weight', opts.fontWeight)\n\t\twrapperElm.style.setProperty('font-size', opts.fontSize + 'px')\n\t\twrapperElm.style.setProperty('line-height', opts.lineHeight * opts.fontSize + 'px')\n\t\twrapperElm.style.setProperty('max-width', opts.maxWidth === null ? null : opts.maxWidth + 'px')\n\t\twrapperElm.style.setProperty('min-width', opts.minWidth === null ? null : opts.minWidth + 'px')\n\t\twrapperElm.style.setProperty('padding', opts.padding)\n\t\twrapperElm.style.setProperty(\n\t\t\t'overflow-wrap',\n\t\t\topts.disableOverflowWrapBreaking ? 'normal' : 'break-word'\n\t\t)\n\t\tif (opts.otherStyles) {\n\t\t\tfor (const [key, value] of Object.entries(opts.otherStyles)) {\n\t\t\t\twrapperElm.style.setProperty(key, value)\n\t\t\t}\n\t\t}\n\n\t\tconst scrollWidth = wrapperElm.scrollWidth\n\t\tconst rect = wrapperElm.getBoundingClientRect()\n\t\twrapperElm.remove()\n\n\t\treturn {\n\t\t\tx: 0,\n\t\t\ty: 0,\n\t\t\tw: rect.width,\n\t\t\th: rect.height,\n\t\t\tscrollWidth,\n\t\t}\n\t}\n\n\t/**\n\t * Given an html element, measure the position of each span of unbroken\n\t * word/white-space characters within any text nodes it contains.\n\t */\n\tmeasureElementTextNodeSpans(\n\t\telement: HTMLElement,\n\t\t{ shouldTruncateToFirstLine = false }: { shouldTruncateToFirstLine?: boolean } = {}\n\t): { spans: { box: BoxModel; text: string }[]; didTruncate: boolean } {\n\t\tconst spans = []\n\n\t\t// Measurements of individual spans are relative to the containing element\n\t\tconst elmBounds = element.getBoundingClientRect()\n\t\tconst offsetX = -elmBounds.left\n\t\tconst offsetY = -elmBounds.top\n\n\t\t// we measure by creating a range that spans each character in the elements text node\n\t\tconst range = new Range()\n\t\tconst textNode = element.childNodes[0]\n\t\tlet idx = 0\n\n\t\tlet currentSpan = null\n\t\tlet prevCharWasSpaceCharacter = null\n\t\tlet prevCharTop = 0\n\t\tlet prevCharLeftForRTLTest = 0\n\t\tlet didTruncate = false\n\t\tfor (const childNode of element.childNodes) {\n\t\t\tif (childNode.nodeType !== Node.TEXT_NODE) continue\n\n\t\t\tfor (const char of childNode.textContent ?? '') {\n\t\t\t\t// place the range around the characters we're interested in\n\t\t\t\trange.setStart(textNode, idx)\n\t\t\t\trange.setEnd(textNode, idx + char.length)\n\t\t\t\t// measure the range. some browsers return multiple rects for the\n\t\t\t\t// first char in a new line - one for the line break, and one for\n\t\t\t\t// the character itself. we're only interested in the character.\n\t\t\t\tconst rects = range.getClientRects()\n\t\t\t\tconst rect = rects[rects.length - 1]!\n\n\t\t\t\t// calculate the position of the character relative to the element\n\t\t\t\tconst top = rect.top + offsetY\n\t\t\t\tconst left = rect.left + offsetX\n\t\t\t\tconst right = rect.right + offsetX\n\t\t\t\tconst isRTL = left < prevCharLeftForRTLTest\n\n\t\t\t\tconst isSpaceCharacter = spaceCharacterRegex.test(char)\n\t\t\t\tif (\n\t\t\t\t\t// If we're at a word boundary...\n\t\t\t\t\tisSpaceCharacter !== prevCharWasSpaceCharacter ||\n\t\t\t\t\t// ...or we're on a different line...\n\t\t\t\t\ttop !== prevCharTop ||\n\t\t\t\t\t// ...or we're at the start of the text and haven't created a span yet...\n\t\t\t\t\t!currentSpan\n\t\t\t\t) {\n\t\t\t\t\t// ...then we're at a span boundary!\n\n\t\t\t\t\tif (currentSpan) {\n\t\t\t\t\t\t// if we're truncating to a single line & we just finished the first line, stop there\n\t\t\t\t\t\tif (shouldTruncateToFirstLine && top !== prevCharTop) {\n\t\t\t\t\t\t\tdidTruncate = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// otherwise add the span to the list ready to start a new one\n\t\t\t\t\t\tspans.push(currentSpan)\n\t\t\t\t\t}\n\n\t\t\t\t\t// start a new span\n\t\t\t\t\tcurrentSpan = {\n\t\t\t\t\t\tbox: { x: left, y: top, w: rect.width, h: rect.height },\n\t\t\t\t\t\ttext: char,\n\t\t\t\t\t}\n\t\t\t\t\tprevCharLeftForRTLTest = left\n\t\t\t\t} else {\n\t\t\t\t\t// Looks like we're in RTL mode, so we need to adjust the left position.\n\t\t\t\t\tif (isRTL) {\n\t\t\t\t\t\tcurrentSpan.box.x = left\n\t\t\t\t\t}\n\n\t\t\t\t\t// otherwise we just need to extend the current span with the next character\n\t\t\t\t\tcurrentSpan.box.w = isRTL ? currentSpan.box.w + rect.width : right - currentSpan.box.x\n\t\t\t\t\tcurrentSpan.text += char\n\t\t\t\t}\n\n\t\t\t\tif (char === '\\n') {\n\t\t\t\t\tprevCharLeftForRTLTest = 0\n\t\t\t\t}\n\n\t\t\t\tprevCharWasSpaceCharacter = isSpaceCharacter\n\t\t\t\tprevCharTop = top\n\t\t\t\tidx += char.length\n\t\t\t}\n\t\t}\n\n\t\t// Add the last span\n\t\tif (currentSpan) {\n\t\t\tspans.push(currentSpan)\n\t\t}\n\n\t\treturn { spans, didTruncate }\n\t}\n\n\t/**\n\t * Measure text into individual spans. Spans are created by rendering the\n\t * text, then dividing it up according to line breaks and word boundaries.\n\t *\n\t * It works by having the browser render the text, then measuring the\n\t * position of each character. You can use this to replicate the text-layout\n\t * algorithm of the current browser in e.g. an SVG export.\n\t */\n\tmeasureTextSpans(\n\t\ttextToMeasure: string,\n\t\topts: TLMeasureTextSpanOpts\n\t): { text: string; box: BoxModel }[] {\n\t\tif (textToMeasure === '') return []\n\n\t\tconst elm = this.baseElem.cloneNode() as HTMLDivElement\n\t\tthis.editor.getContainer().appendChild(elm)\n\n\t\tconst elementWidth = Math.ceil(opts.width - opts.padding * 2)\n\t\telm.setAttribute('dir', 'auto')\n\t\t// N.B. This property, while discouraged (\"intended for Document Type Definition (DTD) designers\")\n\t\t// is necessary for ensuring correct mixed RTL/LTR behavior when exporting SVGs.\n\t\telm.style.setProperty('unicode-bidi', 'plaintext')\n\t\telm.style.setProperty('width', `${elementWidth}px`)\n\t\telm.style.setProperty('height', 'min-content')\n\t\telm.style.setProperty('font-size', `${opts.fontSize}px`)\n\t\telm.style.setProperty('font-family', opts.fontFamily)\n\t\telm.style.setProperty('font-weight', opts.fontWeight)\n\t\telm.style.setProperty('line-height', `${opts.lineHeight * opts.fontSize}px`)\n\t\telm.style.setProperty('text-align', textAlignmentsForLtr[opts.textAlign])\n\t\telm.style.setProperty('font-style', opts.fontStyle)\n\t\tif (opts.otherStyles) {\n\t\t\tfor (const [key, value] of Object.entries(opts.otherStyles)) {\n\t\t\t\telm.style.setProperty(key, value)\n\t\t\t}\n\t\t}\n\n\t\tconst shouldTruncateToFirstLine =\n\t\t\topts.overflow === 'truncate-ellipsis' || opts.overflow === 'truncate-clip'\n\n\t\tif (shouldTruncateToFirstLine) {\n\t\t\telm.style.setProperty('overflow-wrap', 'anywhere')\n\t\t\telm.style.setProperty('word-break', 'break-all')\n\t\t}\n\n\t\tconst normalizedText = normalizeTextForDom(textToMeasure)\n\n\t\t// Render the text into the measurement element:\n\t\telm.textContent = normalizedText\n\n\t\t// actually measure the text:\n\t\tconst { spans, didTruncate } = this.measureElementTextNodeSpans(elm, {\n\t\t\tshouldTruncateToFirstLine,\n\t\t})\n\n\t\tif (opts.overflow === 'truncate-ellipsis' && didTruncate) {\n\t\t\t// we need to measure the ellipsis to know how much space it takes up\n\t\t\telm.textContent = '\u2026'\n\t\t\tconst ellipsisWidth = Math.ceil(this.measureElementTextNodeSpans(elm).spans[0].box.w)\n\n\t\t\t// then, we need to subtract that space from the width we have and measure again:\n\t\t\telm.style.setProperty('width', `${elementWidth - ellipsisWidth}px`)\n\t\t\telm.textContent = normalizedText\n\t\t\tconst truncatedSpans = this.measureElementTextNodeSpans(elm, {\n\t\t\t\tshouldTruncateToFirstLine: true,\n\t\t\t}).spans\n\n\t\t\t// Finally, we add in our ellipsis at the end of the last span. We\n\t\t\t// have to do this after measuring, not before, because adding the\n\t\t\t// ellipsis changes how whitespace might be getting collapsed by the\n\t\t\t// browser.\n\t\t\tconst lastSpan = truncatedSpans[truncatedSpans.length - 1]!\n\t\t\ttruncatedSpans.push({\n\t\t\t\ttext: '\u2026',\n\t\t\t\tbox: {\n\t\t\t\t\tx: Math.min(lastSpan.box.x + lastSpan.box.w, opts.width - opts.padding - ellipsisWidth),\n\t\t\t\t\ty: lastSpan.box.y,\n\t\t\t\t\tw: ellipsisWidth,\n\t\t\t\t\th: lastSpan.box.h,\n\t\t\t\t},\n\t\t\t})\n\t\t\treturn truncatedSpans\n\t\t}\n\n\t\telm.remove()\n\n\t\treturn spans\n\t}\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,MAAM,cAAc;AAEpB,SAAS,oBAAoB,MAAc;AAC1C,SAAO,KACL,QAAQ,aAAa,IAAI,EACzB,MAAM,IAAI,EACV,IAAI,CAAC,MAAM,KAAK,GAAG,EACnB,KAAK,IAAI;AACZ;AAEA,MAAM,uBAAuB;AAAA,EAC5B,OAAO;AAAA,EACP,gBAAgB;AAAA,EAChB,QAAQ;AAAA,EACR,iBAAiB;AAAA,EACjB,KAAK;AAAA,EACL,cAAc;AACf;AAiBA,MAAM,sBAAsB;AAGrB,MAAM,YAAY;AAAA,EAGxB,YAAmB,QAAgB;AAAhB;AAClB,SAAK,WAAW,SAAS,cAAc,KAAK;AAC5C,SAAK,SAAS,UAAU,IAAI,SAAS;AACrC,SAAK,SAAS,UAAU,IAAI,iBAAiB;AAC7C,SAAK,SAAS,WAAW;AAAA,EAC1B;AAAA,EAPQ;AAAA,EASR,YACC,eACA,MAgBqC;AACrC,UAAM,MAAM,SAAS,cAAc,KAAK;AACxC,QAAI,cAAc,oBAAoB,aAAa;AACnD,WAAO,KAAK,YAAY,IAAI,WAAW,IAAI;AAAA,EAC5C;AAAA,EAEA,YACC,MACA,MAiBqC;AAErC,UAAM,aAAa,KAAK,SAAS,UAAU;AAC3C,SAAK,OAAO,aAAa,EAAE,YAAY,UAAU;AACjD,eAAW,YAAY;AACvB,SAAK,SAAS,sBAAsB,YAAY,UAAU;AAE1D,eAAW,aAAa,OAAO,MAAM;AAGrC,eAAW,MAAM,YAAY,gBAAgB,WAAW;AACxD,eAAW,MAAM,YAAY,eAAe,KAAK,UAAU;AAC3D,eAAW,MAAM,YAAY,cAAc,KAAK,SAAS;AACzD,eAAW,MAAM,YAAY,eAAe,KAAK,UAAU;AAC3D,eAAW,MAAM,YAAY,aAAa,KAAK,WAAW,IAAI;AAC9D,eAAW,MAAM,YAAY,eAAe,KAAK,aAAa,KAAK,WAAW,IAAI;AAClF,eAAW,MAAM,YAAY,aAAa,KAAK,aAAa,OAAO,OAAO,KAAK,WAAW,IAAI;AAC9F,eAAW,MAAM,YAAY,aAAa,KAAK,aAAa,OAAO,OAAO,KAAK,WAAW,IAAI;AAC9F,eAAW,MAAM,YAAY,WAAW,KAAK,OAAO;AACpD,eAAW,MAAM;AAAA,MAChB;AAAA,MACA,KAAK,8BAA8B,WAAW;AAAA,IAC/C;AACA,QAAI,KAAK,aAAa;AACrB,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,WAAW,GAAG;AAC5D,mBAAW,MAAM,YAAY,KAAK,KAAK;AAAA,MACxC;AAAA,IACD;AAEA,UAAM,cAAc,WAAW;AAC/B,UAAM,OAAO,WAAW,sBAAsB;AAC9C,eAAW,OAAO;AAElB,WAAO;AAAA,MACN,GAAG;AAAA,MACH,GAAG;AAAA,MACH,GAAG,KAAK;AAAA,MACR,GAAG,KAAK;AAAA,MACR;AAAA,IACD;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,4BACC,SACA,EAAE,4BAA4B,MAAM,IAA6C,CAAC,GACb;AACrE,UAAM,QAAQ,CAAC;AAGf,UAAM,YAAY,QAAQ,sBAAsB;AAChD,UAAM,UAAU,CAAC,UAAU;AAC3B,UAAM,UAAU,CAAC,UAAU;AAG3B,UAAM,QAAQ,IAAI,MAAM;AACxB,UAAM,WAAW,QAAQ,WAAW,CAAC;AACrC,QAAI,MAAM;AAEV,QAAI,cAAc;AAClB,QAAI,4BAA4B;AAChC,QAAI,cAAc;AAClB,QAAI,yBAAyB;AAC7B,QAAI,cAAc;AAClB,eAAW,aAAa,QAAQ,YAAY;AAC3C,UAAI,UAAU,aAAa,KAAK,UAAW;AAE3C,iBAAW,QAAQ,UAAU,eAAe,IAAI;AAE/C,cAAM,SAAS,UAAU,GAAG;AAC5B,cAAM,OAAO,UAAU,MAAM,KAAK,MAAM;AAIxC,cAAM,QAAQ,MAAM,eAAe;AACnC,cAAM,OAAO,MAAM,MAAM,SAAS,CAAC;AAGnC,cAAM,MAAM,KAAK,MAAM;AACvB,cAAM,OAAO,KAAK,OAAO;AACzB,cAAM,QAAQ,KAAK,QAAQ;AAC3B,cAAM,QAAQ,OAAO;AAErB,cAAM,mBAAmB,oBAAoB,KAAK,IAAI;AACtD;AAAA;AAAA,UAEC,qBAAqB;AAAA,UAErB,QAAQ;AAAA,UAER,CAAC;AAAA,UACA;AAGD,cAAI,aAAa;AAEhB,gBAAI,6BAA6B,QAAQ,aAAa;AACrD,4BAAc;AACd;AAAA,YACD;AAEA,kBAAM,KAAK,WAAW;AAAA,UACvB;AAGA,wBAAc;AAAA,YACb,KAAK,EAAE,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,OAAO,GAAG,KAAK,OAAO;AAAA,YACtD,MAAM;AAAA,UACP;AACA,mCAAyB;AAAA,QAC1B,OAAO;AAEN,cAAI,OAAO;AACV,wBAAY,IAAI,IAAI;AAAA,UACrB;AAGA,sBAAY,IAAI,IAAI,QAAQ,YAAY,IAAI,IAAI,KAAK,QAAQ,QAAQ,YAAY,IAAI;AACrF,sBAAY,QAAQ;AAAA,QACrB;AAEA,YAAI,SAAS,MAAM;AAClB,mCAAyB;AAAA,QAC1B;AAEA,oCAA4B;AAC5B,sBAAc;AACd,eAAO,KAAK;AAAA,MACb;AAAA,IACD;AAGA,QAAI,aAAa;AAChB,YAAM,KAAK,WAAW;AAAA,IACvB;AAEA,WAAO,EAAE,OAAO,YAAY;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,iBACC,eACA,MACoC;AACpC,QAAI,kBAAkB,GAAI,QAAO,CAAC;AAElC,UAAM,MAAM,KAAK,SAAS,UAAU;AACpC,SAAK,OAAO,aAAa,EAAE,YAAY,GAAG;AAE1C,UAAM,eAAe,KAAK,KAAK,KAAK,QAAQ,KAAK,UAAU,CAAC;AAC5D,QAAI,aAAa,OAAO,MAAM;AAG9B,QAAI,MAAM,YAAY,gBAAgB,WAAW;AACjD,QAAI,MAAM,YAAY,SAAS,GAAG,YAAY,IAAI;AAClD,QAAI,MAAM,YAAY,UAAU,aAAa;AAC7C,QAAI,MAAM,YAAY,aAAa,GAAG,KAAK,QAAQ,IAAI;AACvD,QAAI,MAAM,YAAY,eAAe,KAAK,UAAU;AACpD,QAAI,MAAM,YAAY,eAAe,KAAK,UAAU;AACpD,QAAI,MAAM,YAAY,eAAe,GAAG,KAAK,aAAa,KAAK,QAAQ,IAAI;AAC3E,QAAI,MAAM,YAAY,cAAc,qBAAqB,KAAK,SAAS,CAAC;AACxE,QAAI,MAAM,YAAY,cAAc,KAAK,SAAS;AAClD,QAAI,KAAK,aAAa;AACrB,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,WAAW,GAAG;AAC5D,YAAI,MAAM,YAAY,KAAK,KAAK;AAAA,MACjC;AAAA,IACD;AAEA,UAAM,4BACL,KAAK,aAAa,uBAAuB,KAAK,aAAa;AAE5D,QAAI,2BAA2B;AAC9B,UAAI,MAAM,YAAY,iBAAiB,UAAU;AACjD,UAAI,MAAM,YAAY,cAAc,WAAW;AAAA,IAChD;AAEA,UAAM,iBAAiB,oBAAoB,aAAa;AAGxD,QAAI,cAAc;AAGlB,UAAM,EAAE,OAAO,YAAY,IAAI,KAAK,4BAA4B,KAAK;AAAA,MACpE;AAAA,IACD,CAAC;AAED,QAAI,KAAK,aAAa,uBAAuB,aAAa;AAEzD,UAAI,cAAc;AAClB,YAAM,gBAAgB,KAAK,KAAK,KAAK,4BAA4B,GAAG,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC;AAGpF,UAAI,MAAM,YAAY,SAAS,GAAG,eAAe,aAAa,IAAI;AAClE,UAAI,cAAc;AAClB,YAAM,iBAAiB,KAAK,4BAA4B,KAAK;AAAA,QAC5D,2BAA2B;AAAA,MAC5B,CAAC,EAAE;AAMH,YAAM,WAAW,eAAe,eAAe,SAAS,CAAC;AACzD,qBAAe,KAAK;AAAA,QACnB,MAAM;AAAA,QACN,KAAK;AAAA,UACJ,GAAG,KAAK,IAAI,SAAS,IAAI,IAAI,SAAS,IAAI,GAAG,KAAK,QAAQ,KAAK,UAAU,aAAa;AAAA,UACtF,GAAG,SAAS,IAAI;AAAA,UAChB,GAAG;AAAA,UACH,GAAG,SAAS,IAAI;AAAA,QACjB;AAAA,MACD,CAAC;AACD,aAAO;AAAA,IACR;AAEA,QAAI,OAAO;AAEX,WAAO;AAAA,EACR;AACD;",
4
+ "sourcesContent": ["import { BoxModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'\nimport { Editor } from '../../Editor'\n\nconst fixNewLines = /\\r?\\n|\\r/g\n\nfunction normalizeTextForDom(text: string) {\n\treturn text\n\t\t.replace(fixNewLines, '\\n')\n\t\t.split('\\n')\n\t\t.map((x) => x || ' ')\n\t\t.join('\\n')\n}\n\nconst textAlignmentsForLtr = {\n\tstart: 'left',\n\t'start-legacy': 'left',\n\tmiddle: 'center',\n\t'middle-legacy': 'center',\n\tend: 'right',\n\t'end-legacy': 'right',\n}\n\n/** @public */\nexport interface TLMeasureTextOpts {\n\tfontStyle: string\n\tfontWeight: string\n\tfontFamily: string\n\tfontSize: number\n\tlineHeight: number\n\t/**\n\t * When maxWidth is a number, the text will be wrapped to that maxWidth. When maxWidth\n\t * is null, the text will be measured without wrapping, but explicit line breaks and\n\t * space are preserved.\n\t */\n\tmaxWidth: null | number\n\tminWidth?: null | number\n\t// todo: make this a number so that it is consistent with other TLMeasureTextSpanOpts\n\tpadding: string\n\totherStyles?: Record<string, string>\n\tdisableOverflowWrapBreaking?: boolean\n\tmeasureScrollWidth?: boolean\n}\n\n/** @public */\nexport interface TLMeasureTextSpanOpts {\n\toverflow: 'wrap' | 'truncate-ellipsis' | 'truncate-clip'\n\twidth: number\n\theight: number\n\tpadding: number\n\tfontSize: number\n\tfontWeight: string\n\tfontFamily: string\n\tfontStyle: string\n\tlineHeight: number\n\ttextAlign: TLDefaultHorizontalAlignStyle\n\totherStyles?: Record<string, string>\n\tmeasureScrollWidth?: boolean\n}\n\nconst spaceCharacterRegex = /\\s/\n\n/** @public */\nexport class TextManager {\n\tprivate elm: HTMLDivElement\n\tprivate defaultStyles: Record<string, string | null>\n\n\tconstructor(public editor: Editor) {\n\t\tconst elm = document.createElement('div')\n\t\telm.classList.add('tl-text')\n\t\telm.classList.add('tl-text-measure')\n\t\telm.setAttribute('dir', 'auto')\n\t\telm.tabIndex = -1\n\t\tthis.editor.getContainer().appendChild(elm)\n\n\t\t// we need to save the default styles so that we can restore them when we're done\n\t\t// these must be the css names, not the js names for the styles\n\t\tthis.defaultStyles = {\n\t\t\t'word-break': 'auto',\n\t\t\twidth: null,\n\t\t\theight: null,\n\t\t\t'max-width': null,\n\t\t\t'min-width': null,\n\t\t}\n\n\t\tthis.elm = elm\n\t}\n\n\tdispose() {\n\t\treturn this.elm.remove()\n\t}\n\n\tprivate resetElmStyles() {\n\t\tconst { elm, defaultStyles } = this\n\t\tfor (const key in defaultStyles) {\n\t\t\telm.style.setProperty(key, defaultStyles[key])\n\t\t}\n\t}\n\n\tmeasureText(textToMeasure: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {\n\t\tconst div = document.createElement('div')\n\t\tdiv.textContent = normalizeTextForDom(textToMeasure)\n\t\treturn this.measureHtml(div.innerHTML, opts)\n\t}\n\n\tmeasureHtml(html: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {\n\t\tconst { elm } = this\n\n\t\tif (opts.otherStyles) {\n\t\t\tfor (const key in opts.otherStyles) {\n\t\t\t\tif (!this.defaultStyles[key]) {\n\t\t\t\t\t// we need to save the original style so that we can restore it when we're done\n\t\t\t\t\tthis.defaultStyles[key] = elm.style.getPropertyValue(key)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\telm.innerHTML = html\n\n\t\t// Apply the default styles to the element (for all styles here or that were ever seen in opts.otherStyles)\n\t\tthis.resetElmStyles()\n\n\t\telm.style.setProperty('font-family', opts.fontFamily)\n\t\telm.style.setProperty('font-style', opts.fontStyle)\n\t\telm.style.setProperty('font-weight', opts.fontWeight)\n\t\telm.style.setProperty('font-size', opts.fontSize + 'px')\n\t\telm.style.setProperty('line-height', opts.lineHeight * opts.fontSize + 'px')\n\t\telm.style.setProperty('padding', opts.padding)\n\n\t\tif (opts.maxWidth) {\n\t\t\telm.style.setProperty('max-width', opts.maxWidth + 'px')\n\t\t}\n\n\t\tif (opts.minWidth) {\n\t\t\telm.style.setProperty('min-width', opts.minWidth + 'px')\n\t\t}\n\n\t\tif (opts.disableOverflowWrapBreaking) {\n\t\t\telm.style.setProperty('overflow-wrap', 'normal')\n\t\t}\n\n\t\tif (opts.otherStyles) {\n\t\t\tfor (const [key, value] of Object.entries(opts.otherStyles)) {\n\t\t\t\telm.style.setProperty(key, value)\n\t\t\t}\n\t\t}\n\n\t\tconst scrollWidth = opts.measureScrollWidth ? elm.scrollWidth : 0\n\t\tconst rect = elm.getBoundingClientRect()\n\n\t\treturn {\n\t\t\tx: 0,\n\t\t\ty: 0,\n\t\t\tw: rect.width,\n\t\t\th: rect.height,\n\t\t\tscrollWidth,\n\t\t}\n\t}\n\n\t/**\n\t * Given an html element, measure the position of each span of unbroken\n\t * word/white-space characters within any text nodes it contains.\n\t */\n\tmeasureElementTextNodeSpans(\n\t\telement: HTMLElement,\n\t\t{ shouldTruncateToFirstLine = false }: { shouldTruncateToFirstLine?: boolean } = {}\n\t): { spans: { box: BoxModel; text: string }[]; didTruncate: boolean } {\n\t\tconst spans = []\n\n\t\t// Measurements of individual spans are relative to the containing element\n\t\tconst elmBounds = element.getBoundingClientRect()\n\t\tconst offsetX = -elmBounds.left\n\t\tconst offsetY = -elmBounds.top\n\n\t\t// we measure by creating a range that spans each character in the elements text node\n\t\tconst range = new Range()\n\t\tconst textNode = element.childNodes[0]\n\t\tlet idx = 0\n\n\t\tlet currentSpan = null\n\t\tlet prevCharWasSpaceCharacter = null\n\t\tlet prevCharTop = 0\n\t\tlet prevCharLeftForRTLTest = 0\n\t\tlet didTruncate = false\n\t\tfor (const childNode of element.childNodes) {\n\t\t\tif (childNode.nodeType !== Node.TEXT_NODE) continue\n\n\t\t\tfor (const char of childNode.textContent ?? '') {\n\t\t\t\t// place the range around the characters we're interested in\n\t\t\t\trange.setStart(textNode, idx)\n\t\t\t\trange.setEnd(textNode, idx + char.length)\n\t\t\t\t// measure the range. some browsers return multiple rects for the\n\t\t\t\t// first char in a new line - one for the line break, and one for\n\t\t\t\t// the character itself. we're only interested in the character.\n\t\t\t\tconst rects = range.getClientRects()\n\t\t\t\tconst rect = rects[rects.length - 1]!\n\n\t\t\t\t// calculate the position of the character relative to the element\n\t\t\t\tconst top = rect.top + offsetY\n\t\t\t\tconst left = rect.left + offsetX\n\t\t\t\tconst right = rect.right + offsetX\n\t\t\t\tconst isRTL = left < prevCharLeftForRTLTest\n\n\t\t\t\tconst isSpaceCharacter = spaceCharacterRegex.test(char)\n\t\t\t\tif (\n\t\t\t\t\t// If we're at a word boundary...\n\t\t\t\t\tisSpaceCharacter !== prevCharWasSpaceCharacter ||\n\t\t\t\t\t// ...or we're on a different line...\n\t\t\t\t\ttop !== prevCharTop ||\n\t\t\t\t\t// ...or we're at the start of the text and haven't created a span yet...\n\t\t\t\t\t!currentSpan\n\t\t\t\t) {\n\t\t\t\t\t// ...then we're at a span boundary!\n\n\t\t\t\t\tif (currentSpan) {\n\t\t\t\t\t\t// if we're truncating to a single line & we just finished the first line, stop there\n\t\t\t\t\t\tif (shouldTruncateToFirstLine && top !== prevCharTop) {\n\t\t\t\t\t\t\tdidTruncate = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// otherwise add the span to the list ready to start a new one\n\t\t\t\t\t\tspans.push(currentSpan)\n\t\t\t\t\t}\n\n\t\t\t\t\t// start a new span\n\t\t\t\t\tcurrentSpan = {\n\t\t\t\t\t\tbox: { x: left, y: top, w: rect.width, h: rect.height },\n\t\t\t\t\t\ttext: char,\n\t\t\t\t\t}\n\t\t\t\t\tprevCharLeftForRTLTest = left\n\t\t\t\t} else {\n\t\t\t\t\t// Looks like we're in RTL mode, so we need to adjust the left position.\n\t\t\t\t\tif (isRTL) {\n\t\t\t\t\t\tcurrentSpan.box.x = left\n\t\t\t\t\t}\n\n\t\t\t\t\t// otherwise we just need to extend the current span with the next character\n\t\t\t\t\tcurrentSpan.box.w = isRTL ? currentSpan.box.w + rect.width : right - currentSpan.box.x\n\t\t\t\t\tcurrentSpan.text += char\n\t\t\t\t}\n\n\t\t\t\tif (char === '\\n') {\n\t\t\t\t\tprevCharLeftForRTLTest = 0\n\t\t\t\t}\n\n\t\t\t\tprevCharWasSpaceCharacter = isSpaceCharacter\n\t\t\t\tprevCharTop = top\n\t\t\t\tidx += char.length\n\t\t\t}\n\t\t}\n\n\t\t// Add the last span\n\t\tif (currentSpan) {\n\t\t\tspans.push(currentSpan)\n\t\t}\n\n\t\treturn { spans, didTruncate }\n\t}\n\n\t/**\n\t * Measure text into individual spans. Spans are created by rendering the\n\t * text, then dividing it up according to line breaks and word boundaries.\n\t *\n\t * It works by having the browser render the text, then measuring the\n\t * position of each character. You can use this to replicate the text-layout\n\t * algorithm of the current browser in e.g. an SVG export.\n\t */\n\tmeasureTextSpans(\n\t\ttextToMeasure: string,\n\t\topts: TLMeasureTextSpanOpts\n\t): { text: string; box: BoxModel }[] {\n\t\tif (textToMeasure === '') return []\n\n\t\tconst { elm } = this\n\n\t\tif (opts.otherStyles) {\n\t\t\tfor (const key in opts.otherStyles) {\n\t\t\t\tif (!this.defaultStyles[key]) {\n\t\t\t\t\t// we need to save the original style so that we can restore it when we're done\n\t\t\t\t\tthis.defaultStyles[key] = elm.style.getPropertyValue(key)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.resetElmStyles()\n\n\t\telm.style.setProperty('font-family', opts.fontFamily)\n\t\telm.style.setProperty('font-style', opts.fontStyle)\n\t\telm.style.setProperty('font-weight', opts.fontWeight)\n\t\telm.style.setProperty('font-size', opts.fontSize + 'px')\n\t\telm.style.setProperty('line-height', opts.lineHeight * opts.fontSize + 'px')\n\n\t\tconst elementWidth = Math.ceil(opts.width - opts.padding * 2)\n\t\telm.style.setProperty('width', `${elementWidth}px`)\n\t\telm.style.setProperty('height', 'min-content')\n\t\telm.style.setProperty('text-align', textAlignmentsForLtr[opts.textAlign])\n\n\t\tconst shouldTruncateToFirstLine =\n\t\t\topts.overflow === 'truncate-ellipsis' || opts.overflow === 'truncate-clip'\n\n\t\tif (shouldTruncateToFirstLine) {\n\t\t\telm.style.setProperty('overflow-wrap', 'anywhere')\n\t\t\telm.style.setProperty('word-break', 'break-all')\n\t\t}\n\n\t\tif (opts.otherStyles) {\n\t\t\tfor (const [key, value] of Object.entries(opts.otherStyles)) {\n\t\t\t\telm.style.setProperty(key, value)\n\t\t\t}\n\t\t}\n\n\t\tconst normalizedText = normalizeTextForDom(textToMeasure)\n\n\t\t// Render the text into the measurement element:\n\t\telm.textContent = normalizedText\n\n\t\t// actually measure the text:\n\t\tconst { spans, didTruncate } = this.measureElementTextNodeSpans(elm, {\n\t\t\tshouldTruncateToFirstLine,\n\t\t})\n\n\t\tif (opts.overflow === 'truncate-ellipsis' && didTruncate) {\n\t\t\t// we need to measure the ellipsis to know how much space it takes up\n\t\t\telm.textContent = '\u2026'\n\t\t\tconst ellipsisWidth = Math.ceil(this.measureElementTextNodeSpans(elm).spans[0].box.w)\n\n\t\t\t// then, we need to subtract that space from the width we have and measure again:\n\t\t\telm.style.setProperty('width', `${elementWidth - ellipsisWidth}px`)\n\t\t\telm.textContent = normalizedText\n\t\t\tconst truncatedSpans = this.measureElementTextNodeSpans(elm, {\n\t\t\t\tshouldTruncateToFirstLine: true,\n\t\t\t}).spans\n\n\t\t\t// Finally, we add in our ellipsis at the end of the last span. We\n\t\t\t// have to do this after measuring, not before, because adding the\n\t\t\t// ellipsis changes how whitespace might be getting collapsed by the\n\t\t\t// browser.\n\t\t\tconst lastSpan = truncatedSpans[truncatedSpans.length - 1]!\n\t\t\ttruncatedSpans.push({\n\t\t\t\ttext: '\u2026',\n\t\t\t\tbox: {\n\t\t\t\t\tx: Math.min(lastSpan.box.x + lastSpan.box.w, opts.width - opts.padding - ellipsisWidth),\n\t\t\t\t\ty: lastSpan.box.y,\n\t\t\t\t\tw: ellipsisWidth,\n\t\t\t\t\th: lastSpan.box.h,\n\t\t\t\t},\n\t\t\t})\n\n\t\t\treturn truncatedSpans\n\t\t}\n\n\t\treturn spans\n\t}\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -39,7 +39,7 @@ class GroupShapeUtil extends import_ShapeUtil.ShapeUtil {
39
39
  return false;
40
40
  }
41
41
  canResize() {
42
- return false;
42
+ return true;
43
43
  }
44
44
  canResizeChildren() {
45
45
  return true;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/lib/editor/shapes/group/GroupShapeUtil.tsx"],
4
- "sourcesContent": ["import { TLGroupShape, groupShapeMigrations, groupShapeProps } from '@tldraw/tlschema'\nimport { SVGContainer } from '../../../components/SVGContainer'\nimport { Geometry2d } from '../../../primitives/geometry/Geometry2d'\nimport { Group2d } from '../../../primitives/geometry/Group2d'\nimport { Rectangle2d } from '../../../primitives/geometry/Rectangle2d'\nimport { ShapeUtil } from '../ShapeUtil'\nimport { DashedOutlineBox } from './DashedOutlineBox'\n\n/** @public */\nexport class GroupShapeUtil extends ShapeUtil<TLGroupShape> {\n\tstatic override type = 'group' as const\n\tstatic override props = groupShapeProps\n\tstatic override migrations = groupShapeMigrations\n\n\toverride hideSelectionBoundsFg() {\n\t\treturn true\n\t}\n\n\toverride canBind() {\n\t\treturn false\n\t}\n\n\tcanResize() {\n\t\treturn false\n\t}\n\n\tcanResizeChildren() {\n\t\treturn true\n\t}\n\n\tgetDefaultProps(): TLGroupShape['props'] {\n\t\treturn {}\n\t}\n\n\tgetGeometry(shape: TLGroupShape): Geometry2d {\n\t\tconst children = this.editor.getSortedChildIdsForParent(shape.id)\n\t\tif (children.length === 0) {\n\t\t\treturn new Rectangle2d({ width: 1, height: 1, isFilled: false })\n\t\t}\n\n\t\treturn new Group2d({\n\t\t\tchildren: children.map((childId) => {\n\t\t\t\tconst shape = this.editor.getShape(childId)!\n\t\t\t\treturn this.editor\n\t\t\t\t\t.getShapeGeometry(childId)\n\t\t\t\t\t.transform(this.editor.getShapeLocalTransform(shape)!, { isLabel: false })\n\t\t\t}),\n\t\t})\n\t}\n\n\tcomponent(shape: TLGroupShape) {\n\t\tconst isErasing = this.editor.getErasingShapeIds().includes(shape.id)\n\n\t\tconst { hintingShapeIds } = this.editor.getCurrentPageState()\n\t\tconst isHintingOtherGroup =\n\t\t\thintingShapeIds.length > 0 &&\n\t\t\thintingShapeIds.some(\n\t\t\t\t(id) =>\n\t\t\t\t\tid !== shape.id &&\n\t\t\t\t\tthis.editor.isShapeOfType<TLGroupShape>(this.editor.getShape(id)!, 'group')\n\t\t\t)\n\n\t\tconst isFocused = this.editor.getCurrentPageState().focusedGroupId !== shape.id\n\n\t\tif (\n\t\t\t!isErasing && // always show the outline while we're erasing the group\n\t\t\t// show the outline while the group is focused unless something outside of the group is being hinted\n\t\t\t// this happens dropping shapes from a group onto some outside group\n\t\t\t(isFocused || isHintingOtherGroup)\n\t\t) {\n\t\t\treturn null\n\t\t}\n\n\t\tconst bounds = this.editor.getShapeGeometry(shape).bounds\n\n\t\treturn (\n\t\t\t<SVGContainer>\n\t\t\t\t<DashedOutlineBox className=\"tl-group\" bounds={bounds} />\n\t\t\t</SVGContainer>\n\t\t)\n\t}\n\n\tindicator(shape: TLGroupShape) {\n\t\t// Not a class component, but eslint can't tell that :(\n\t\tconst bounds = this.editor.getShapeGeometry(shape).bounds\n\t\treturn <DashedOutlineBox className=\"\" bounds={bounds} />\n\t}\n\n\toverride onChildrenChange(group: TLGroupShape) {\n\t\tconst children = this.editor.getSortedChildIdsForParent(group.id)\n\t\tif (children.length === 0) {\n\t\t\tif (this.editor.getCurrentPageState().focusedGroupId === group.id) {\n\t\t\t\tthis.editor.popFocusedGroupId()\n\t\t\t}\n\t\t\tthis.editor.deleteShapes([group.id])\n\t\t\treturn\n\t\t} else if (children.length === 1) {\n\t\t\tif (this.editor.getCurrentPageState().focusedGroupId === group.id) {\n\t\t\t\tthis.editor.popFocusedGroupId()\n\t\t\t}\n\t\t\tthis.editor.reparentShapes(children, group.parentId)\n\t\t\tthis.editor.deleteShapes([group.id])\n\t\t\treturn\n\t\t}\n\t}\n}\n"],
4
+ "sourcesContent": ["import { TLGroupShape, groupShapeMigrations, groupShapeProps } from '@tldraw/tlschema'\nimport { SVGContainer } from '../../../components/SVGContainer'\nimport { Geometry2d } from '../../../primitives/geometry/Geometry2d'\nimport { Group2d } from '../../../primitives/geometry/Group2d'\nimport { Rectangle2d } from '../../../primitives/geometry/Rectangle2d'\nimport { ShapeUtil } from '../ShapeUtil'\nimport { DashedOutlineBox } from './DashedOutlineBox'\n\n/** @public */\nexport class GroupShapeUtil extends ShapeUtil<TLGroupShape> {\n\tstatic override type = 'group' as const\n\tstatic override props = groupShapeProps\n\tstatic override migrations = groupShapeMigrations\n\n\toverride hideSelectionBoundsFg() {\n\t\treturn true\n\t}\n\n\toverride canBind() {\n\t\treturn false\n\t}\n\n\tcanResize() {\n\t\treturn true\n\t}\n\n\tcanResizeChildren() {\n\t\treturn true\n\t}\n\n\tgetDefaultProps(): TLGroupShape['props'] {\n\t\treturn {}\n\t}\n\n\tgetGeometry(shape: TLGroupShape): Geometry2d {\n\t\tconst children = this.editor.getSortedChildIdsForParent(shape.id)\n\t\tif (children.length === 0) {\n\t\t\treturn new Rectangle2d({ width: 1, height: 1, isFilled: false })\n\t\t}\n\n\t\treturn new Group2d({\n\t\t\tchildren: children.map((childId) => {\n\t\t\t\tconst shape = this.editor.getShape(childId)!\n\t\t\t\treturn this.editor\n\t\t\t\t\t.getShapeGeometry(childId)\n\t\t\t\t\t.transform(this.editor.getShapeLocalTransform(shape)!, { isLabel: false })\n\t\t\t}),\n\t\t})\n\t}\n\n\tcomponent(shape: TLGroupShape) {\n\t\tconst isErasing = this.editor.getErasingShapeIds().includes(shape.id)\n\n\t\tconst { hintingShapeIds } = this.editor.getCurrentPageState()\n\t\tconst isHintingOtherGroup =\n\t\t\thintingShapeIds.length > 0 &&\n\t\t\thintingShapeIds.some(\n\t\t\t\t(id) =>\n\t\t\t\t\tid !== shape.id &&\n\t\t\t\t\tthis.editor.isShapeOfType<TLGroupShape>(this.editor.getShape(id)!, 'group')\n\t\t\t)\n\n\t\tconst isFocused = this.editor.getCurrentPageState().focusedGroupId !== shape.id\n\n\t\tif (\n\t\t\t!isErasing && // always show the outline while we're erasing the group\n\t\t\t// show the outline while the group is focused unless something outside of the group is being hinted\n\t\t\t// this happens dropping shapes from a group onto some outside group\n\t\t\t(isFocused || isHintingOtherGroup)\n\t\t) {\n\t\t\treturn null\n\t\t}\n\n\t\tconst bounds = this.editor.getShapeGeometry(shape).bounds\n\n\t\treturn (\n\t\t\t<SVGContainer>\n\t\t\t\t<DashedOutlineBox className=\"tl-group\" bounds={bounds} />\n\t\t\t</SVGContainer>\n\t\t)\n\t}\n\n\tindicator(shape: TLGroupShape) {\n\t\t// Not a class component, but eslint can't tell that :(\n\t\tconst bounds = this.editor.getShapeGeometry(shape).bounds\n\t\treturn <DashedOutlineBox className=\"\" bounds={bounds} />\n\t}\n\n\toverride onChildrenChange(group: TLGroupShape) {\n\t\tconst children = this.editor.getSortedChildIdsForParent(group.id)\n\t\tif (children.length === 0) {\n\t\t\tif (this.editor.getCurrentPageState().focusedGroupId === group.id) {\n\t\t\t\tthis.editor.popFocusedGroupId()\n\t\t\t}\n\t\t\tthis.editor.deleteShapes([group.id])\n\t\t\treturn\n\t\t} else if (children.length === 1) {\n\t\t\tif (this.editor.getCurrentPageState().focusedGroupId === group.id) {\n\t\t\t\tthis.editor.popFocusedGroupId()\n\t\t\t}\n\t\t\tthis.editor.reparentShapes(children, group.parentId)\n\t\t\tthis.editor.deleteShapes([group.id])\n\t\t\treturn\n\t\t}\n\t}\n}\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA6EI;AA7EJ,sBAAoE;AACpE,0BAA6B;AAE7B,qBAAwB;AACxB,yBAA4B;AAC5B,uBAA0B;AAC1B,8BAAiC;AAG1B,MAAM,uBAAuB,2BAAwB;AAAA,EAC3D,OAAgB,OAAO;AAAA,EACvB,OAAgB,QAAQ;AAAA,EACxB,OAAgB,aAAa;AAAA,EAEpB,wBAAwB;AAChC,WAAO;AAAA,EACR;AAAA,EAES,UAAU;AAClB,WAAO;AAAA,EACR;AAAA,EAEA,YAAY;AACX,WAAO;AAAA,EACR;AAAA,EAEA,oBAAoB;AACnB,WAAO;AAAA,EACR;AAAA,EAEA,kBAAyC;AACxC,WAAO,CAAC;AAAA,EACT;AAAA,EAEA,YAAY,OAAiC;AAC5C,UAAM,WAAW,KAAK,OAAO,2BAA2B,MAAM,EAAE;AAChE,QAAI,SAAS,WAAW,GAAG;AAC1B,aAAO,IAAI,+BAAY,EAAE,OAAO,GAAG,QAAQ,GAAG,UAAU,MAAM,CAAC;AAAA,IAChE;AAEA,WAAO,IAAI,uBAAQ;AAAA,MAClB,UAAU,SAAS,IAAI,CAAC,YAAY;AACnC,cAAMA,SAAQ,KAAK,OAAO,SAAS,OAAO;AAC1C,eAAO,KAAK,OACV,iBAAiB,OAAO,EACxB,UAAU,KAAK,OAAO,uBAAuBA,MAAK,GAAI,EAAE,SAAS,MAAM,CAAC;AAAA,MAC3E,CAAC;AAAA,IACF,CAAC;AAAA,EACF;AAAA,EAEA,UAAU,OAAqB;AAC9B,UAAM,YAAY,KAAK,OAAO,mBAAmB,EAAE,SAAS,MAAM,EAAE;AAEpE,UAAM,EAAE,gBAAgB,IAAI,KAAK,OAAO,oBAAoB;AAC5D,UAAM,sBACL,gBAAgB,SAAS,KACzB,gBAAgB;AAAA,MACf,CAAC,OACA,OAAO,MAAM,MACb,KAAK,OAAO,cAA4B,KAAK,OAAO,SAAS,EAAE,GAAI,OAAO;AAAA,IAC5E;AAED,UAAM,YAAY,KAAK,OAAO,oBAAoB,EAAE,mBAAmB,MAAM;AAE7E,QACC,CAAC;AAAA;AAAA;AAAA,KAGA,aAAa,sBACb;AACD,aAAO;AAAA,IACR;AAEA,UAAM,SAAS,KAAK,OAAO,iBAAiB,KAAK,EAAE;AAEnD,WACC,4CAAC,oCACA,sDAAC,4CAAiB,WAAU,YAAW,QAAgB,GACxD;AAAA,EAEF;AAAA,EAEA,UAAU,OAAqB;AAE9B,UAAM,SAAS,KAAK,OAAO,iBAAiB,KAAK,EAAE;AACnD,WAAO,4CAAC,4CAAiB,WAAU,IAAG,QAAgB;AAAA,EACvD;AAAA,EAES,iBAAiB,OAAqB;AAC9C,UAAM,WAAW,KAAK,OAAO,2BAA2B,MAAM,EAAE;AAChE,QAAI,SAAS,WAAW,GAAG;AAC1B,UAAI,KAAK,OAAO,oBAAoB,EAAE,mBAAmB,MAAM,IAAI;AAClE,aAAK,OAAO,kBAAkB;AAAA,MAC/B;AACA,WAAK,OAAO,aAAa,CAAC,MAAM,EAAE,CAAC;AACnC;AAAA,IACD,WAAW,SAAS,WAAW,GAAG;AACjC,UAAI,KAAK,OAAO,oBAAoB,EAAE,mBAAmB,MAAM,IAAI;AAClE,aAAK,OAAO,kBAAkB;AAAA,MAC/B;AACA,WAAK,OAAO,eAAe,UAAU,MAAM,QAAQ;AACnD,WAAK,OAAO,aAAa,CAAC,MAAM,EAAE,CAAC;AACnC;AAAA,IACD;AAAA,EACD;AACD;",
6
6
  "names": ["shape"]
7
7
  }
@@ -18,17 +18,22 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var richText_exports = {};
20
20
  __export(richText_exports, {
21
- getFontsFromRichText: () => getFontsFromRichText
21
+ getFontsFromRichText: () => getFontsFromRichText,
22
+ getTipTapSchema: () => getTipTapSchema
22
23
  });
23
24
  module.exports = __toCommonJS(richText_exports);
24
25
  var import_core = require("@tiptap/core");
25
26
  var import_model = require("@tiptap/pm/model");
26
27
  var import_utils = require("@tldraw/utils");
28
+ const schemaCache = new import_utils.WeakCache();
29
+ function getTipTapSchema(tipTapConfig) {
30
+ return schemaCache.get(tipTapConfig, () => (0, import_core.getSchema)(tipTapConfig.extensions ?? []));
31
+ }
27
32
  function getFontsFromRichText(editor, richText, initialState) {
28
33
  const { tipTapConfig, addFontsFromNode } = editor.getTextOptions();
29
34
  (0, import_utils.assert)(tipTapConfig, "textOptions.tipTapConfig must be set to use rich text");
30
35
  (0, import_utils.assert)(addFontsFromNode, "textOptions.addFontsFromNode must be set to use rich text");
31
- const schema = (0, import_core.getSchema)(tipTapConfig.extensions ?? []);
36
+ const schema = getTipTapSchema(tipTapConfig);
32
37
  const rootNode = import_model.Node.fromJSON(schema, richText);
33
38
  const fonts = /* @__PURE__ */ new Set();
34
39
  function addFont(font) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/utils/richText.ts"],
4
- "sourcesContent": ["import { getSchema, JSONContent, Editor as TTEditor } from '@tiptap/core'\nimport { Node } from '@tiptap/pm/model'\nimport { EditorProviderProps } from '@tiptap/react'\nimport { TLRichText } from '@tldraw/tlschema'\nimport { assert } from '@tldraw/utils'\nimport { Editor } from '../editor/Editor'\nimport { TLFontFace } from '../editor/managers/FontManager/FontManager'\n\n/**\n * This is the TipTap editor! Docs are {@link https://tiptap.dev/docs}.\n *\n * @public\n */\nexport type TiptapEditor = TTEditor\n\n/**\n * A TipTap node. See {@link https://tiptap.dev/docs}.\n * @public\n */\nexport type TiptapNode = Node\n\n/** @public */\nexport interface TLTextOptions {\n\ttipTapConfig?: EditorProviderProps\n\taddFontsFromNode?: RichTextFontVisitor\n}\n\n/** @public */\nexport interface RichTextFontVisitorState {\n\treadonly family: string\n\treadonly weight: string\n\treadonly style: string\n}\n\n/** @public */\nexport type RichTextFontVisitor = (\n\tnode: TiptapNode,\n\tstate: RichTextFontVisitorState,\n\taddFont: (font: TLFontFace) => void\n) => RichTextFontVisitorState\n\n/** @public */\nexport function getFontsFromRichText(\n\teditor: Editor,\n\trichText: TLRichText,\n\tinitialState: RichTextFontVisitorState\n) {\n\tconst { tipTapConfig, addFontsFromNode } = editor.getTextOptions()\n\tassert(tipTapConfig, 'textOptions.tipTapConfig must be set to use rich text')\n\tassert(addFontsFromNode, 'textOptions.addFontsFromNode must be set to use rich text')\n\n\tconst schema = getSchema(tipTapConfig.extensions ?? [])\n\tconst rootNode = Node.fromJSON(schema, richText as JSONContent)\n\n\tconst fonts = new Set<TLFontFace>()\n\n\tfunction addFont(font: TLFontFace) {\n\t\tfonts.add(font)\n\t}\n\n\tfunction visit(node: TiptapNode, state: RichTextFontVisitorState) {\n\t\tstate = addFontsFromNode!(node, state, addFont)\n\n\t\tfor (const child of node.children) {\n\t\t\tvisit(child, state)\n\t\t}\n\t}\n\n\tvisit(rootNode, initialState)\n\n\treturn Array.from(fonts)\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAA2D;AAC3D,mBAAqB;AAGrB,mBAAuB;AAsChB,SAAS,qBACf,QACA,UACA,cACC;AACD,QAAM,EAAE,cAAc,iBAAiB,IAAI,OAAO,eAAe;AACjE,2BAAO,cAAc,uDAAuD;AAC5E,2BAAO,kBAAkB,2DAA2D;AAEpF,QAAM,aAAS,uBAAU,aAAa,cAAc,CAAC,CAAC;AACtD,QAAM,WAAW,kBAAK,SAAS,QAAQ,QAAuB;AAE9D,QAAM,QAAQ,oBAAI,IAAgB;AAElC,WAAS,QAAQ,MAAkB;AAClC,UAAM,IAAI,IAAI;AAAA,EACf;AAEA,WAAS,MAAM,MAAkB,OAAiC;AACjE,YAAQ,iBAAkB,MAAM,OAAO,OAAO;AAE9C,eAAW,SAAS,KAAK,UAAU;AAClC,YAAM,OAAO,KAAK;AAAA,IACnB;AAAA,EACD;AAEA,QAAM,UAAU,YAAY;AAE5B,SAAO,MAAM,KAAK,KAAK;AACxB;",
4
+ "sourcesContent": ["import { getSchema, JSONContent, Editor as TTEditor } from '@tiptap/core'\nimport { Node, Schema } from '@tiptap/pm/model'\nimport { EditorProviderProps } from '@tiptap/react'\nimport { TLRichText } from '@tldraw/tlschema'\nimport { assert, WeakCache } from '@tldraw/utils'\nimport { Editor } from '../editor/Editor'\nimport { TLFontFace } from '../editor/managers/FontManager/FontManager'\n\n/**\n * This is the TipTap editor! Docs are {@link https://tiptap.dev/docs}.\n *\n * @public\n */\nexport type TiptapEditor = TTEditor\n\n/**\n * A TipTap node. See {@link https://tiptap.dev/docs}.\n * @public\n */\nexport type TiptapNode = Node\n\n/** @public */\nexport interface TLTextOptions {\n\ttipTapConfig?: EditorProviderProps\n\taddFontsFromNode?: RichTextFontVisitor\n}\n\n/** @public */\nexport interface RichTextFontVisitorState {\n\treadonly family: string\n\treadonly weight: string\n\treadonly style: string\n}\n\n/** @public */\nexport type RichTextFontVisitor = (\n\tnode: TiptapNode,\n\tstate: RichTextFontVisitorState,\n\taddFont: (font: TLFontFace) => void\n) => RichTextFontVisitorState\n\nconst schemaCache = new WeakCache<EditorProviderProps, Schema>()\nexport function getTipTapSchema(tipTapConfig: EditorProviderProps) {\n\treturn schemaCache.get(tipTapConfig, () => getSchema(tipTapConfig.extensions ?? []))\n}\n\n/** @public */\nexport function getFontsFromRichText(\n\teditor: Editor,\n\trichText: TLRichText,\n\tinitialState: RichTextFontVisitorState\n) {\n\tconst { tipTapConfig, addFontsFromNode } = editor.getTextOptions()\n\tassert(tipTapConfig, 'textOptions.tipTapConfig must be set to use rich text')\n\tassert(addFontsFromNode, 'textOptions.addFontsFromNode must be set to use rich text')\n\n\tconst schema = getTipTapSchema(tipTapConfig)\n\n\tconst rootNode = Node.fromJSON(schema, richText as JSONContent)\n\n\tconst fonts = new Set<TLFontFace>()\n\n\tfunction addFont(font: TLFontFace) {\n\t\tfonts.add(font)\n\t}\n\n\tfunction visit(node: TiptapNode, state: RichTextFontVisitorState) {\n\t\tstate = addFontsFromNode!(node, state, addFont)\n\n\t\tfor (const child of node.children) {\n\t\t\tvisit(child, state)\n\t\t}\n\t}\n\n\tvisit(rootNode, initialState)\n\n\treturn Array.from(fonts)\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAA2D;AAC3D,mBAA6B;AAG7B,mBAAkC;AAqClC,MAAM,cAAc,IAAI,uBAAuC;AACxD,SAAS,gBAAgB,cAAmC;AAClE,SAAO,YAAY,IAAI,cAAc,UAAM,uBAAU,aAAa,cAAc,CAAC,CAAC,CAAC;AACpF;AAGO,SAAS,qBACf,QACA,UACA,cACC;AACD,QAAM,EAAE,cAAc,iBAAiB,IAAI,OAAO,eAAe;AACjE,2BAAO,cAAc,uDAAuD;AAC5E,2BAAO,kBAAkB,2DAA2D;AAEpF,QAAM,SAAS,gBAAgB,YAAY;AAE3C,QAAM,WAAW,kBAAK,SAAS,QAAQ,QAAuB;AAE9D,QAAM,QAAQ,oBAAI,IAAgB;AAElC,WAAS,QAAQ,MAAkB;AAClC,UAAM,IAAI,IAAI;AAAA,EACf;AAEA,WAAS,MAAM,MAAkB,OAAiC;AACjE,YAAQ,iBAAkB,MAAM,OAAO,OAAO;AAE9C,eAAW,SAAS,KAAK,UAAU;AAClC,YAAM,OAAO,KAAK;AAAA,IACnB;AAAA,EACD;AAEA,QAAM,UAAU,YAAY;AAE5B,SAAO,MAAM,KAAK,KAAK;AACxB;",
6
6
  "names": []
7
7
  }
@@ -22,10 +22,10 @@ __export(version_exports, {
22
22
  version: () => version
23
23
  });
24
24
  module.exports = __toCommonJS(version_exports);
25
- const version = "3.14.0-canary.2f3caa391d5d";
25
+ const version = "3.14.0-canary.306affbc4326";
26
26
  const publishDates = {
27
27
  major: "2024-09-13T14:36:29.063Z",
28
- minor: "2025-06-11T14:23:44.452Z",
29
- patch: "2025-06-11T14:23:44.452Z"
28
+ minor: "2025-06-16T13:31:38.369Z",
29
+ patch: "2025-06-16T13:31:38.369Z"
30
30
  };
31
31
  //# sourceMappingURL=version.js.map
@@ -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 = '3.14.0-canary.2f3caa391d5d'\nexport const publishDates = {\n\tmajor: '2024-09-13T14:36:29.063Z',\n\tminor: '2025-06-11T14:23:44.452Z',\n\tpatch: '2025-06-11T14:23:44.452Z',\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 = '3.14.0-canary.306affbc4326'\nexport const publishDates = {\n\tmajor: '2024-09-13T14:36:29.063Z',\n\tminor: '2025-06-16T13:31:38.369Z',\n\tpatch: '2025-06-16T13:31:38.369Z',\n}\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGO,MAAM,UAAU;AAChB,MAAM,eAAe;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACR;",
6
6
  "names": []
7
7
  }
@@ -5724,43 +5724,15 @@ export declare const TAB_ID: string;
5724
5724
  /** @public */
5725
5725
  export declare class TextManager {
5726
5726
  editor: Editor;
5727
- private baseElem;
5727
+ private elm;
5728
+ private defaultStyles;
5728
5729
  constructor(editor: Editor);
5729
- measureText(textToMeasure: string, opts: {
5730
- /**
5731
- * When maxWidth is a number, the text will be wrapped to that maxWidth. When maxWidth
5732
- * is null, the text will be measured without wrapping, but explicit line breaks and
5733
- * space are preserved.
5734
- */
5735
- maxWidth: null | number;
5736
- disableOverflowWrapBreaking?: boolean;
5737
- fontFamily: string;
5738
- fontSize: number;
5739
- fontStyle: string;
5740
- fontWeight: string;
5741
- lineHeight: number;
5742
- minWidth?: null | number;
5743
- padding: string;
5744
- }): BoxModel & {
5730
+ dispose(): void;
5731
+ private resetElmStyles;
5732
+ measureText(textToMeasure: string, opts: TLMeasureTextOpts): BoxModel & {
5745
5733
  scrollWidth: number;
5746
5734
  };
5747
- measureHtml(html: string, opts: {
5748
- /**
5749
- * When maxWidth is a number, the text will be wrapped to that maxWidth. When maxWidth
5750
- * is null, the text will be measured without wrapping, but explicit line breaks and
5751
- * space are preserved.
5752
- */
5753
- maxWidth: null | number;
5754
- disableOverflowWrapBreaking?: boolean;
5755
- fontFamily: string;
5756
- fontSize: number;
5757
- fontStyle: string;
5758
- fontWeight: string;
5759
- lineHeight: number;
5760
- minWidth?: null | number;
5761
- otherStyles?: Record<string, string>;
5762
- padding: string;
5763
- }): BoxModel & {
5735
+ measureHtml(html: string, opts: TLMeasureTextOpts): BoxModel & {
5764
5736
  scrollWidth: number;
5765
5737
  };
5766
5738
  /**
@@ -6785,6 +6757,26 @@ export declare interface TLLoadSnapshotOptions {
6785
6757
  forceOverwriteSessionState?: boolean;
6786
6758
  }
6787
6759
 
6760
+ /** @public */
6761
+ export declare interface TLMeasureTextOpts {
6762
+ fontStyle: string;
6763
+ fontWeight: string;
6764
+ fontFamily: string;
6765
+ fontSize: number;
6766
+ lineHeight: number;
6767
+ /**
6768
+ * When maxWidth is a number, the text will be wrapped to that maxWidth. When maxWidth
6769
+ * is null, the text will be measured without wrapping, but explicit line breaks and
6770
+ * space are preserved.
6771
+ */
6772
+ maxWidth: null | number;
6773
+ minWidth?: null | number;
6774
+ padding: string;
6775
+ otherStyles?: Record<string, string>;
6776
+ disableOverflowWrapBreaking?: boolean;
6777
+ measureScrollWidth?: boolean;
6778
+ }
6779
+
6788
6780
  /** @public */
6789
6781
  export declare interface TLMeasureTextSpanOpts {
6790
6782
  overflow: 'truncate-clip' | 'truncate-ellipsis' | 'wrap';
@@ -6798,6 +6790,7 @@ export declare interface TLMeasureTextSpanOpts {
6798
6790
  lineHeight: number;
6799
6791
  textAlign: TLDefaultHorizontalAlignStyle;
6800
6792
  otherStyles?: Record<string, string>;
6793
+ measureScrollWidth?: boolean;
6801
6794
  }
6802
6795
 
6803
6796
  /** @public */
@@ -292,7 +292,7 @@ function debugEnableLicensing() {
292
292
  }
293
293
  registerTldrawLibraryVersion(
294
294
  "@tldraw/editor",
295
- "3.14.0-canary.2f3caa391d5d",
295
+ "3.14.0-canary.306affbc4326",
296
296
  "esm"
297
297
  );
298
298
  export {