@tldraw/editor 3.7.0-canary.d87af567ddf9 → 3.7.0-canary.db18f4c24992
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.d.ts +3 -1
- package/dist-cjs/index.js +2 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +1 -1
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/components/LiveCollaborators.js +2 -1
- package/dist-cjs/lib/components/LiveCollaborators.js.map +2 -2
- package/dist-cjs/lib/components/Shape.js +6 -4
- package/dist-cjs/lib/components/Shape.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +8 -2
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager.js +9 -15
- package/dist-cjs/lib/editor/managers/TextManager.js.map +2 -2
- package/dist-cjs/lib/hooks/useEvent.js +18 -1
- package/dist-cjs/lib/hooks/useEvent.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +3 -1
- package/dist-esm/index.mjs +3 -2
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +1 -1
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/components/LiveCollaborators.mjs +2 -1
- package/dist-esm/lib/components/LiveCollaborators.mjs.map +2 -2
- package/dist-esm/lib/components/Shape.mjs +6 -4
- package/dist-esm/lib/components/Shape.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +9 -2
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager.mjs +9 -15
- package/dist-esm/lib/editor/managers/TextManager.mjs.map +2 -2
- package/dist-esm/lib/hooks/useEvent.mjs +18 -1
- package/dist-esm/lib/hooks/useEvent.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +7 -7
- package/src/index.ts +1 -1
- package/src/lib/TldrawEditor.tsx +1 -1
- package/src/lib/components/LiveCollaborators.tsx +3 -1
- package/src/lib/components/Shape.tsx +22 -10
- package/src/lib/editor/Editor.ts +12 -5
- package/src/lib/editor/managers/TextManager.ts +9 -17
- package/src/lib/hooks/useEvent.tsx +29 -0
- package/src/version.ts +3 -3
|
@@ -14,21 +14,15 @@ const spaceCharacterRegex = /\s/;
|
|
|
14
14
|
class TextManager {
|
|
15
15
|
constructor(editor) {
|
|
16
16
|
this.editor = editor;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
elm.tabIndex = -1;
|
|
22
|
-
container.appendChild(elm);
|
|
23
|
-
this.baseElm = elm;
|
|
24
|
-
editor.disposables.add(() => {
|
|
25
|
-
elm.remove();
|
|
26
|
-
});
|
|
17
|
+
this.baseElem = document.createElement("div");
|
|
18
|
+
this.baseElem.classList.add("tl-text");
|
|
19
|
+
this.baseElem.classList.add("tl-text-measure");
|
|
20
|
+
this.baseElem.tabIndex = -1;
|
|
27
21
|
}
|
|
28
|
-
|
|
22
|
+
baseElem;
|
|
29
23
|
measureText(textToMeasure, opts) {
|
|
30
|
-
const elm = this.
|
|
31
|
-
this.
|
|
24
|
+
const elm = this.baseElem.cloneNode();
|
|
25
|
+
this.editor.getContainer().appendChild(elm);
|
|
32
26
|
elm.setAttribute("dir", "auto");
|
|
33
27
|
elm.style.setProperty("unicode-bidi", "plaintext");
|
|
34
28
|
elm.style.setProperty("font-family", opts.fontFamily);
|
|
@@ -132,8 +126,8 @@ class TextManager {
|
|
|
132
126
|
*/
|
|
133
127
|
measureTextSpans(textToMeasure, opts) {
|
|
134
128
|
if (textToMeasure === "") return [];
|
|
135
|
-
const elm = this.
|
|
136
|
-
this.
|
|
129
|
+
const elm = this.baseElem.cloneNode();
|
|
130
|
+
this.editor.getContainer().appendChild(elm);
|
|
137
131
|
const elementWidth = Math.ceil(opts.width - opts.padding * 2);
|
|
138
132
|
elm.setAttribute("dir", "auto");
|
|
139
133
|
elm.style.setProperty("unicode-bidi", "plaintext");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/lib/editor/managers/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}\n\nconst spaceCharacterRegex = /\\s/\n\n/** @public */\nexport class TextManager {\n\tbaseElm: HTMLDivElement\n\n\tconstructor(public editor: Editor) {\n\t\tconst container = this.editor.getContainer()\n\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.tabIndex = -1\n\t\tcontainer.appendChild(elm)\n\n\t\tthis.baseElm = elm\n\t\teditor.disposables.add(() => {\n\t\t\telm.remove()\n\t\t})\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\t// Duplicate our base element; we don't need to clone deep\n\t\tconst elm = this.baseElm?.cloneNode() as HTMLDivElement\n\t\tthis.baseElm.insertAdjacentElement('afterend', elm)\n\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('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('max-width', opts.maxWidth === null ? null : opts.maxWidth + 'px')\n\t\telm.style.setProperty('min-width', opts.minWidth === null ? null : opts.minWidth + 'px')\n\t\telm.style.setProperty('padding', opts.padding)\n\t\telm.style.setProperty(\n\t\t\t'overflow-wrap',\n\t\t\topts.disableOverflowWrapBreaking ? 'normal' : 'break-word'\n\t\t)\n\n\t\telm.textContent = normalizeTextForDom(textToMeasure)\n\t\tconst scrollWidth = elm.scrollWidth\n\t\tconst rect = elm.getBoundingClientRect()\n\t\telm.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.baseElm?.cloneNode() as HTMLDivElement\n\t\tthis.baseElm.insertAdjacentElement('afterend', 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\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": "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;AAgBA,MAAM,sBAAsB;AAGrB,MAAM,YAAY;AAAA,EAGxB,YAAmB,QAAgB;AAAhB;AAClB,
|
|
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}\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\t// Duplicate our base element; we don't need to clone deep\n\t\tconst elm = this.baseElem.cloneNode() as HTMLDivElement\n\t\tthis.editor.getContainer().appendChild(elm)\n\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('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('max-width', opts.maxWidth === null ? null : opts.maxWidth + 'px')\n\t\telm.style.setProperty('min-width', opts.minWidth === null ? null : opts.minWidth + 'px')\n\t\telm.style.setProperty('padding', opts.padding)\n\t\telm.style.setProperty(\n\t\t\t'overflow-wrap',\n\t\t\topts.disableOverflowWrapBreaking ? 'normal' : 'break-word'\n\t\t)\n\n\t\telm.textContent = normalizeTextForDom(textToMeasure)\n\t\tconst scrollWidth = elm.scrollWidth\n\t\tconst rect = elm.getBoundingClientRect()\n\t\telm.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\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": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useAtom } from "@tldraw/state-react";
|
|
1
2
|
import { assert } from "@tldraw/utils";
|
|
2
3
|
import { useCallback, useDebugValue, useLayoutEffect, useRef } from "react";
|
|
3
4
|
function useEvent(handler) {
|
|
@@ -12,7 +13,23 @@ function useEvent(handler) {
|
|
|
12
13
|
return fn(...args);
|
|
13
14
|
}, []);
|
|
14
15
|
}
|
|
16
|
+
function useReactiveEvent(handler) {
|
|
17
|
+
const handlerAtom = useAtom("useReactiveEvent", () => handler);
|
|
18
|
+
useLayoutEffect(() => {
|
|
19
|
+
handlerAtom.set(handler);
|
|
20
|
+
});
|
|
21
|
+
useDebugValue(handler);
|
|
22
|
+
return useCallback(
|
|
23
|
+
(...args) => {
|
|
24
|
+
const fn = handlerAtom.get();
|
|
25
|
+
assert(fn, "fn does not exist");
|
|
26
|
+
return fn(...args);
|
|
27
|
+
},
|
|
28
|
+
[handlerAtom]
|
|
29
|
+
);
|
|
30
|
+
}
|
|
15
31
|
export {
|
|
16
|
-
useEvent
|
|
32
|
+
useEvent,
|
|
33
|
+
useReactiveEvent
|
|
17
34
|
};
|
|
18
35
|
//# sourceMappingURL=useEvent.mjs.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/hooks/useEvent.tsx"],
|
|
4
|
-
"sourcesContent": ["import { assert } from '@tldraw/utils'\nimport { useCallback, useDebugValue, useLayoutEffect, useRef } from 'react'\n\n/**\n * Allows you to define event handlers that can read the latest props/state but has a stable\n * function identity.\n *\n * These event callbacks may not be called in React render functions! An error won't be thrown, but\n * in the real implementation it would be!\n *\n * Uses a modified version of the user-land implementation included in the [`useEvent()` RFC][1].\n * Our version until such a hook is available natively.\n *\n * The RFC was closed on 27 September 2022, the React team plans to come up with a new RFC to\n * provide similar functionality in the future. We will migrate to this functionality when\n * available.\n *\n * IMPORTANT CAVEAT: You should not call event callbacks in layout effects of React component\n * children! Internally this hook uses a layout effect and parent component layout effects run after\n * child component layout effects. Use this hook responsibly.\n *\n * [1]: https://github.com/reactjs/rfcs/pull/220\n *\n * @internal\n */\nexport function useEvent<Args extends Array<unknown>, Result>(\n\thandler: (...args: Args) => Result\n): (...args: Args) => Result {\n\tconst handlerRef = useRef<(...args: Args) => Result>()\n\n\t// In a real implementation, this would run before layout effects\n\tuseLayoutEffect(() => {\n\t\thandlerRef.current = handler\n\t})\n\n\tuseDebugValue(handler)\n\n\treturn useCallback((...args: Args) => {\n\t\t// In a real implementation, this would throw if called during render\n\t\tconst fn = handlerRef.current\n\t\tassert(fn, 'fn does not exist')\n\t\treturn fn(...args)\n\t}, [])\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,cAAc;AACvB,SAAS,aAAa,eAAe,iBAAiB,cAAc;AAwB7D,SAAS,SACf,SAC4B;AAC5B,QAAM,aAAa,OAAkC;AAGrD,kBAAgB,MAAM;AACrB,eAAW,UAAU;AAAA,EACtB,CAAC;AAED,gBAAc,OAAO;AAErB,SAAO,YAAY,IAAI,SAAe;AAErC,UAAM,KAAK,WAAW;AACtB,WAAO,IAAI,mBAAmB;AAC9B,WAAO,GAAG,GAAG,IAAI;AAAA,EAClB,GAAG,CAAC,CAAC;AACN;",
|
|
4
|
+
"sourcesContent": ["import { useAtom } from '@tldraw/state-react'\nimport { assert } from '@tldraw/utils'\nimport { useCallback, useDebugValue, useLayoutEffect, useRef } from 'react'\n\n/**\n * Allows you to define event handlers that can read the latest props/state but has a stable\n * function identity.\n *\n * These event callbacks may not be called in React render functions! An error won't be thrown, but\n * in the real implementation it would be!\n *\n * Uses a modified version of the user-land implementation included in the [`useEvent()` RFC][1].\n * Our version until such a hook is available natively.\n *\n * The RFC was closed on 27 September 2022, the React team plans to come up with a new RFC to\n * provide similar functionality in the future. We will migrate to this functionality when\n * available.\n *\n * IMPORTANT CAVEAT: You should not call event callbacks in layout effects of React component\n * children! Internally this hook uses a layout effect and parent component layout effects run after\n * child component layout effects. Use this hook responsibly.\n *\n * [1]: https://github.com/reactjs/rfcs/pull/220\n *\n * @internal\n */\nexport function useEvent<Args extends Array<unknown>, Result>(\n\thandler: (...args: Args) => Result\n): (...args: Args) => Result {\n\tconst handlerRef = useRef<(...args: Args) => Result>()\n\n\t// In a real implementation, this would run before layout effects\n\tuseLayoutEffect(() => {\n\t\thandlerRef.current = handler\n\t})\n\n\tuseDebugValue(handler)\n\n\treturn useCallback((...args: Args) => {\n\t\t// In a real implementation, this would throw if called during render\n\t\tconst fn = handlerRef.current\n\t\tassert(fn, 'fn does not exist')\n\t\treturn fn(...args)\n\t}, [])\n}\n\n/**\n * like {@link useEvent}, but for use in reactive contexts - when the handler function changes, it\n * will invalidate any reactive contexts that call it.\n * @internal\n */\nexport function useReactiveEvent<Args extends Array<unknown>, Result>(\n\thandler: (...args: Args) => Result\n): (...args: Args) => Result {\n\tconst handlerAtom = useAtom<(...args: Args) => Result>('useReactiveEvent', () => handler)\n\n\t// In a real implementation, this would run before layout effects\n\tuseLayoutEffect(() => {\n\t\thandlerAtom.set(handler)\n\t})\n\n\tuseDebugValue(handler)\n\n\treturn useCallback(\n\t\t(...args: Args) => {\n\t\t\t// In a real implementation, this would throw if called during render\n\t\t\tconst fn = handlerAtom.get()\n\t\t\tassert(fn, 'fn does not exist')\n\t\t\treturn fn(...args)\n\t\t},\n\t\t[handlerAtom]\n\t)\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,eAAe;AACxB,SAAS,cAAc;AACvB,SAAS,aAAa,eAAe,iBAAiB,cAAc;AAwB7D,SAAS,SACf,SAC4B;AAC5B,QAAM,aAAa,OAAkC;AAGrD,kBAAgB,MAAM;AACrB,eAAW,UAAU;AAAA,EACtB,CAAC;AAED,gBAAc,OAAO;AAErB,SAAO,YAAY,IAAI,SAAe;AAErC,UAAM,KAAK,WAAW;AACtB,WAAO,IAAI,mBAAmB;AAC9B,WAAO,GAAG,GAAG,IAAI;AAAA,EAClB,GAAG,CAAC,CAAC;AACN;AAOO,SAAS,iBACf,SAC4B;AAC5B,QAAM,cAAc,QAAmC,oBAAoB,MAAM,OAAO;AAGxF,kBAAgB,MAAM;AACrB,gBAAY,IAAI,OAAO;AAAA,EACxB,CAAC;AAED,gBAAc,OAAO;AAErB,SAAO;AAAA,IACN,IAAI,SAAe;AAElB,YAAM,KAAK,YAAY,IAAI;AAC3B,aAAO,IAAI,mBAAmB;AAC9B,aAAO,GAAG,GAAG,IAAI;AAAA,IAClB;AAAA,IACA,CAAC,WAAW;AAAA,EACb;AACD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist-esm/version.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
const version = "3.7.0-canary.
|
|
1
|
+
const version = "3.7.0-canary.db18f4c24992";
|
|
2
2
|
const publishDates = {
|
|
3
3
|
major: "2024-09-13T14:36:29.063Z",
|
|
4
|
-
minor: "2024-12-
|
|
5
|
-
patch: "2024-12-
|
|
4
|
+
minor: "2024-12-16T16:10:25.781Z",
|
|
5
|
+
patch: "2024-12-16T16:10:25.781Z"
|
|
6
6
|
};
|
|
7
7
|
export {
|
|
8
8
|
publishDates,
|
package/dist-esm/version.mjs.map
CHANGED
|
@@ -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.7.0-canary.
|
|
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.7.0-canary.db18f4c24992'\nexport const publishDates = {\n\tmajor: '2024-09-13T14:36:29.063Z',\n\tminor: '2024-12-16T16:10:25.781Z',\n\tpatch: '2024-12-16T16:10:25.781Z',\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": "A tiny little drawing app (editor).",
|
|
4
|
-
"version": "3.7.0-canary.
|
|
4
|
+
"version": "3.7.0-canary.db18f4c24992",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@tldraw/state": "3.7.0-canary.
|
|
49
|
-
"@tldraw/state-react": "3.7.0-canary.
|
|
50
|
-
"@tldraw/store": "3.7.0-canary.
|
|
51
|
-
"@tldraw/tlschema": "3.7.0-canary.
|
|
52
|
-
"@tldraw/utils": "3.7.0-canary.
|
|
53
|
-
"@tldraw/validate": "3.7.0-canary.
|
|
48
|
+
"@tldraw/state": "3.7.0-canary.db18f4c24992",
|
|
49
|
+
"@tldraw/state-react": "3.7.0-canary.db18f4c24992",
|
|
50
|
+
"@tldraw/store": "3.7.0-canary.db18f4c24992",
|
|
51
|
+
"@tldraw/tlschema": "3.7.0-canary.db18f4c24992",
|
|
52
|
+
"@tldraw/utils": "3.7.0-canary.db18f4c24992",
|
|
53
|
+
"@tldraw/validate": "3.7.0-canary.db18f4c24992",
|
|
54
54
|
"@types/core-js": "^2.5.5",
|
|
55
55
|
"@use-gesture/react": "^10.2.27",
|
|
56
56
|
"classnames": "^2.3.2",
|
package/src/index.ts
CHANGED
|
@@ -271,7 +271,7 @@ export { getCursor } from './lib/hooks/useCursor'
|
|
|
271
271
|
export { EditorContext, useEditor, useMaybeEditor } from './lib/hooks/useEditor'
|
|
272
272
|
export { useEditorComponents } from './lib/hooks/useEditorComponents'
|
|
273
273
|
export type { TLEditorComponents } from './lib/hooks/useEditorComponents'
|
|
274
|
-
export { useEvent } from './lib/hooks/useEvent'
|
|
274
|
+
export { useEvent, useReactiveEvent } from './lib/hooks/useEvent'
|
|
275
275
|
export { useGlobalMenuIsOpen } from './lib/hooks/useGlobalMenuIsOpen'
|
|
276
276
|
export { useShallowArrayIdentity, useShallowObjectIdentity } from './lib/hooks/useIdentity'
|
|
277
277
|
export { useIsCropping } from './lib/hooks/useIsCropping'
|
package/src/lib/TldrawEditor.tsx
CHANGED
|
@@ -554,7 +554,7 @@ function TldrawEditorWithReadyStore({
|
|
|
554
554
|
) : (
|
|
555
555
|
<EditorProvider editor={editor}>
|
|
556
556
|
<Layout onMount={onMount}>
|
|
557
|
-
{children ?? (Canvas ? <Canvas /> : null)}
|
|
557
|
+
{children ?? (Canvas ? <Canvas key={editor.contextId} /> : null)}
|
|
558
558
|
<Watermark />
|
|
559
559
|
</Layout>
|
|
560
560
|
</EditorProvider>
|
|
@@ -75,6 +75,8 @@ const Collaborator = track(function Collaborator({
|
|
|
75
75
|
const { userId, chatMessage, brush, scribbles, selectedShapeIds, userName, cursor, color } =
|
|
76
76
|
latestPresence
|
|
77
77
|
|
|
78
|
+
if (!cursor) return null
|
|
79
|
+
|
|
78
80
|
// Add a little padding to the top-left of the viewport
|
|
79
81
|
// so that the cursor doesn't get cut off
|
|
80
82
|
const isCursorInViewport = !(
|
|
@@ -171,7 +173,7 @@ function useCollaboratorState(editor: Editor, latestPresence: TLInstancePresence
|
|
|
171
173
|
if (latestPresence) {
|
|
172
174
|
// We can do this on every render, it's free and cheaper than an effect
|
|
173
175
|
// remember, there can be lots and lots of cursors moving around all the time
|
|
174
|
-
rLastActivityTimestamp.current = latestPresence.lastActivityTimestamp
|
|
176
|
+
rLastActivityTimestamp.current = latestPresence.lastActivityTimestamp ?? Infinity
|
|
175
177
|
}
|
|
176
178
|
|
|
177
179
|
return state
|
|
@@ -171,13 +171,19 @@ export const Shape = memo(function Shape({
|
|
|
171
171
|
|
|
172
172
|
export const InnerShape = memo(
|
|
173
173
|
function InnerShape<T extends TLShape>({ shape, util }: { shape: T; util: ShapeUtil<T> }) {
|
|
174
|
-
return useStateTracking(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
return useStateTracking(
|
|
175
|
+
'InnerShape:' + shape.type,
|
|
176
|
+
() =>
|
|
177
|
+
// always fetch the latest shape from the store even if the props/meta have not changed, to avoid
|
|
178
|
+
// calling the render method with stale data.
|
|
179
|
+
util.component(util.editor.store.unsafeGetWithoutCapture(shape.id) as T),
|
|
180
|
+
[util, shape.id]
|
|
178
181
|
)
|
|
179
182
|
},
|
|
180
|
-
(prev, next) =>
|
|
183
|
+
(prev, next) =>
|
|
184
|
+
prev.shape.props === next.shape.props &&
|
|
185
|
+
prev.shape.meta === next.shape.meta &&
|
|
186
|
+
prev.util === next.util
|
|
181
187
|
)
|
|
182
188
|
|
|
183
189
|
export const InnerShapeBackground = memo(
|
|
@@ -188,11 +194,17 @@ export const InnerShapeBackground = memo(
|
|
|
188
194
|
shape: T
|
|
189
195
|
util: ShapeUtil<T>
|
|
190
196
|
}) {
|
|
191
|
-
return useStateTracking(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
197
|
+
return useStateTracking(
|
|
198
|
+
'InnerShape:' + shape.type,
|
|
199
|
+
() =>
|
|
200
|
+
// always fetch the latest shape from the store even if the props/meta have not changed, to avoid
|
|
201
|
+
// calling the render method with stale data.
|
|
202
|
+
util.backgroundComponent?.(util.editor.store.unsafeGetWithoutCapture(shape.id) as T),
|
|
203
|
+
[util, shape.id]
|
|
195
204
|
)
|
|
196
205
|
},
|
|
197
|
-
(prev, next) =>
|
|
206
|
+
(prev, next) =>
|
|
207
|
+
prev.shape.props === next.shape.props &&
|
|
208
|
+
prev.shape.meta === next.shape.meta &&
|
|
209
|
+
prev.util === next.util
|
|
198
210
|
)
|
package/src/lib/editor/Editor.ts
CHANGED
|
@@ -77,6 +77,7 @@ import {
|
|
|
77
77
|
hasOwnProperty,
|
|
78
78
|
last,
|
|
79
79
|
lerp,
|
|
80
|
+
maxBy,
|
|
80
81
|
sortById,
|
|
81
82
|
sortByIndex,
|
|
82
83
|
structuredClone,
|
|
@@ -2264,6 +2265,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2264
2265
|
const leaderPresence = this.getCollaborators().find((c) => c.userId === followingUserId)
|
|
2265
2266
|
if (!leaderPresence) return null
|
|
2266
2267
|
|
|
2268
|
+
if (!leaderPresence.camera || !leaderPresence.screenBounds) return null
|
|
2269
|
+
|
|
2267
2270
|
// Fit their viewport inside of our screen bounds
|
|
2268
2271
|
// 1. calculate their viewport in page space
|
|
2269
2272
|
const { w: lw, h: lh } = leaderPresence.screenBounds
|
|
@@ -3161,6 +3164,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3161
3164
|
|
|
3162
3165
|
if (!presence) return this
|
|
3163
3166
|
|
|
3167
|
+
const cursor = presence.cursor
|
|
3168
|
+
if (!cursor) return this
|
|
3169
|
+
|
|
3164
3170
|
this.run(() => {
|
|
3165
3171
|
// If we're following someone, stop following them
|
|
3166
3172
|
if (this.getInstanceState().followingUserId !== null) {
|
|
@@ -3178,7 +3184,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3178
3184
|
opts.animation = undefined
|
|
3179
3185
|
}
|
|
3180
3186
|
|
|
3181
|
-
this.centerOnPoint(
|
|
3187
|
+
this.centerOnPoint(cursor, opts)
|
|
3182
3188
|
|
|
3183
3189
|
// Highlight the user's cursor
|
|
3184
3190
|
const { highlightedUserIds } = this.getInstanceState()
|
|
@@ -3389,10 +3395,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3389
3395
|
if (!allPresenceRecords.length) return EMPTY_ARRAY
|
|
3390
3396
|
const userIds = [...new Set(allPresenceRecords.map((c) => c.userId))].sort()
|
|
3391
3397
|
return userIds.map((id) => {
|
|
3392
|
-
const latestPresence =
|
|
3393
|
-
.filter((c) => c.userId === id)
|
|
3394
|
-
|
|
3395
|
-
|
|
3398
|
+
const latestPresence = maxBy(
|
|
3399
|
+
allPresenceRecords.filter((c) => c.userId === id),
|
|
3400
|
+
(p) => p.lastActivityTimestamp ?? 0
|
|
3401
|
+
)
|
|
3402
|
+
return latestPresence!
|
|
3396
3403
|
})
|
|
3397
3404
|
}
|
|
3398
3405
|
|
|
@@ -38,21 +38,13 @@ const spaceCharacterRegex = /\s/
|
|
|
38
38
|
|
|
39
39
|
/** @public */
|
|
40
40
|
export class TextManager {
|
|
41
|
-
|
|
41
|
+
private baseElem: HTMLDivElement
|
|
42
42
|
|
|
43
43
|
constructor(public editor: Editor) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
elm.classList.add('tl-text-measure')
|
|
49
|
-
elm.tabIndex = -1
|
|
50
|
-
container.appendChild(elm)
|
|
51
|
-
|
|
52
|
-
this.baseElm = elm
|
|
53
|
-
editor.disposables.add(() => {
|
|
54
|
-
elm.remove()
|
|
55
|
-
})
|
|
44
|
+
this.baseElem = document.createElement('div')
|
|
45
|
+
this.baseElem.classList.add('tl-text')
|
|
46
|
+
this.baseElem.classList.add('tl-text-measure')
|
|
47
|
+
this.baseElem.tabIndex = -1
|
|
56
48
|
}
|
|
57
49
|
|
|
58
50
|
measureText(
|
|
@@ -75,8 +67,8 @@ export class TextManager {
|
|
|
75
67
|
}
|
|
76
68
|
): BoxModel & { scrollWidth: number } {
|
|
77
69
|
// Duplicate our base element; we don't need to clone deep
|
|
78
|
-
const elm = this.
|
|
79
|
-
this.
|
|
70
|
+
const elm = this.baseElem.cloneNode() as HTMLDivElement
|
|
71
|
+
this.editor.getContainer().appendChild(elm)
|
|
80
72
|
|
|
81
73
|
elm.setAttribute('dir', 'auto')
|
|
82
74
|
// N.B. This property, while discouraged ("intended for Document Type Definition (DTD) designers")
|
|
@@ -223,8 +215,8 @@ export class TextManager {
|
|
|
223
215
|
): { text: string; box: BoxModel }[] {
|
|
224
216
|
if (textToMeasure === '') return []
|
|
225
217
|
|
|
226
|
-
const elm = this.
|
|
227
|
-
this.
|
|
218
|
+
const elm = this.baseElem.cloneNode() as HTMLDivElement
|
|
219
|
+
this.editor.getContainer().appendChild(elm)
|
|
228
220
|
|
|
229
221
|
const elementWidth = Math.ceil(opts.width - opts.padding * 2)
|
|
230
222
|
elm.setAttribute('dir', 'auto')
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useAtom } from '@tldraw/state-react'
|
|
1
2
|
import { assert } from '@tldraw/utils'
|
|
2
3
|
import { useCallback, useDebugValue, useLayoutEffect, useRef } from 'react'
|
|
3
4
|
|
|
@@ -42,3 +43,31 @@ export function useEvent<Args extends Array<unknown>, Result>(
|
|
|
42
43
|
return fn(...args)
|
|
43
44
|
}, [])
|
|
44
45
|
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* like {@link useEvent}, but for use in reactive contexts - when the handler function changes, it
|
|
49
|
+
* will invalidate any reactive contexts that call it.
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
export function useReactiveEvent<Args extends Array<unknown>, Result>(
|
|
53
|
+
handler: (...args: Args) => Result
|
|
54
|
+
): (...args: Args) => Result {
|
|
55
|
+
const handlerAtom = useAtom<(...args: Args) => Result>('useReactiveEvent', () => handler)
|
|
56
|
+
|
|
57
|
+
// In a real implementation, this would run before layout effects
|
|
58
|
+
useLayoutEffect(() => {
|
|
59
|
+
handlerAtom.set(handler)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
useDebugValue(handler)
|
|
63
|
+
|
|
64
|
+
return useCallback(
|
|
65
|
+
(...args: Args) => {
|
|
66
|
+
// In a real implementation, this would throw if called during render
|
|
67
|
+
const fn = handlerAtom.get()
|
|
68
|
+
assert(fn, 'fn does not exist')
|
|
69
|
+
return fn(...args)
|
|
70
|
+
},
|
|
71
|
+
[handlerAtom]
|
|
72
|
+
)
|
|
73
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// This file is automatically generated by internal/scripts/refresh-assets.ts.
|
|
2
2
|
// Do not edit manually. Or do, I'm a comment, not a cop.
|
|
3
3
|
|
|
4
|
-
export const version = '3.7.0-canary.
|
|
4
|
+
export const version = '3.7.0-canary.db18f4c24992'
|
|
5
5
|
export const publishDates = {
|
|
6
6
|
major: '2024-09-13T14:36:29.063Z',
|
|
7
|
-
minor: '2024-12-
|
|
8
|
-
patch: '2024-12-
|
|
7
|
+
minor: '2024-12-16T16:10:25.781Z',
|
|
8
|
+
patch: '2024-12-16T16:10:25.781Z',
|
|
9
9
|
}
|