[Improvement] Text measurement tweaks (#2670)
This PR duplicates a template node rather than creating a new node each time or querying for a selector. Functions clean up the node that is created. ### Change Type - [x] `patch`
This commit is contained in:
parent
2610790873
commit
8b73e77ec2
2 changed files with 36 additions and 38 deletions
|
@ -787,9 +787,9 @@ input,
|
||||||
|
|
||||||
.tl-text-measure {
|
.tl-text-measure {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 999999;
|
z-index: -999999;
|
||||||
top: -9999px;
|
top: 0px;
|
||||||
right: -9999px;
|
left: 0px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { BoxModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'
|
import { BoxModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'
|
||||||
import { uniqueId } from '../../utils/uniqueId'
|
|
||||||
import { Editor } from '../Editor'
|
import { Editor } from '../Editor'
|
||||||
|
|
||||||
const fixNewLines = /\r?\n|\r/g
|
const fixNewLines = /\r?\n|\r/g
|
||||||
|
@ -38,21 +37,16 @@ type TLMeasureTextSpanOpts = {
|
||||||
const spaceCharacterRegex = /\s/
|
const spaceCharacterRegex = /\s/
|
||||||
|
|
||||||
export class TextManager {
|
export class TextManager {
|
||||||
constructor(public editor: Editor) {}
|
baseElm: HTMLDivElement
|
||||||
|
|
||||||
private getTextElement() {
|
|
||||||
const oldElm = document.querySelector('.tl-text-measure')
|
|
||||||
oldElm?.remove()
|
|
||||||
|
|
||||||
|
constructor(public editor: Editor) {
|
||||||
const elm = document.createElement('div')
|
const elm = document.createElement('div')
|
||||||
this.editor.getContainer().appendChild(elm)
|
elm.id = `tldraw_text_measure`
|
||||||
|
|
||||||
elm.id = `__textMeasure_${uniqueId()}`
|
|
||||||
elm.classList.add('tl-text')
|
elm.classList.add('tl-text')
|
||||||
elm.classList.add('tl-text-measure')
|
elm.classList.add('tl-text-measure')
|
||||||
elm.tabIndex = -1
|
elm.tabIndex = -1
|
||||||
|
this.editor.getContainer().appendChild(elm)
|
||||||
return elm
|
this.baseElm = elm
|
||||||
}
|
}
|
||||||
|
|
||||||
measureText = (
|
measureText = (
|
||||||
|
@ -73,7 +67,9 @@ export class TextManager {
|
||||||
padding: string
|
padding: string
|
||||||
}
|
}
|
||||||
): BoxModel => {
|
): BoxModel => {
|
||||||
const elm = this.getTextElement()
|
// Duplicate our base element; we don't need to clone deep
|
||||||
|
const elm = this.baseElm?.cloneNode() as HTMLDivElement
|
||||||
|
this.baseElm.insertAdjacentElement('afterend', elm)
|
||||||
|
|
||||||
elm.setAttribute('dir', 'ltr')
|
elm.setAttribute('dir', 'ltr')
|
||||||
elm.style.setProperty('font-family', opts.fontFamily)
|
elm.style.setProperty('font-family', opts.fontFamily)
|
||||||
|
@ -87,6 +83,7 @@ export class TextManager {
|
||||||
|
|
||||||
elm.textContent = normalizeTextForDom(textToMeasure)
|
elm.textContent = normalizeTextForDom(textToMeasure)
|
||||||
const rect = elm.getBoundingClientRect()
|
const rect = elm.getBoundingClientRect()
|
||||||
|
elm.remove()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: 0,
|
x: 0,
|
||||||
|
@ -198,45 +195,46 @@ export class TextManager {
|
||||||
): { text: string; box: BoxModel }[] {
|
): { text: string; box: BoxModel }[] {
|
||||||
if (textToMeasure === '') return []
|
if (textToMeasure === '') return []
|
||||||
|
|
||||||
|
const elm = this.baseElm?.cloneNode() as HTMLDivElement
|
||||||
|
this.baseElm.insertAdjacentElement('afterend', elm)
|
||||||
|
|
||||||
|
const elementWidth = Math.ceil(opts.width - opts.padding * 2)
|
||||||
|
elm.style.setProperty('width', `${elementWidth}px`)
|
||||||
|
elm.style.setProperty('height', 'min-content')
|
||||||
|
elm.style.setProperty('dir', 'ltr')
|
||||||
|
elm.style.setProperty('font-size', `${opts.fontSize}px`)
|
||||||
|
elm.style.setProperty('font-family', opts.fontFamily)
|
||||||
|
elm.style.setProperty('font-weight', opts.fontWeight)
|
||||||
|
elm.style.setProperty('line-height', `${opts.lineHeight * opts.fontSize}px`)
|
||||||
|
elm.style.setProperty('text-align', textAlignmentsForLtr[opts.textAlign])
|
||||||
|
|
||||||
const shouldTruncateToFirstLine =
|
const shouldTruncateToFirstLine =
|
||||||
opts.overflow === 'truncate-ellipsis' || opts.overflow === 'truncate-clip'
|
opts.overflow === 'truncate-ellipsis' || opts.overflow === 'truncate-clip'
|
||||||
|
|
||||||
// Create a measurement element:
|
|
||||||
const element = this.getTextElement()
|
|
||||||
const elementWidth = Math.ceil(opts.width - opts.padding * 2)
|
|
||||||
element.style.setProperty('width', `${elementWidth}px`)
|
|
||||||
element.style.setProperty('height', 'min-content')
|
|
||||||
element.style.setProperty('dir', 'ltr')
|
|
||||||
element.style.setProperty('font-size', `${opts.fontSize}px`)
|
|
||||||
element.style.setProperty('font-family', opts.fontFamily)
|
|
||||||
element.style.setProperty('font-weight', opts.fontWeight)
|
|
||||||
element.style.setProperty('line-height', `${opts.lineHeight * opts.fontSize}px`)
|
|
||||||
element.style.setProperty('text-align', textAlignmentsForLtr[opts.textAlign])
|
|
||||||
|
|
||||||
if (shouldTruncateToFirstLine) {
|
if (shouldTruncateToFirstLine) {
|
||||||
element.style.setProperty('overflow-wrap', 'anywhere')
|
elm.style.setProperty('overflow-wrap', 'anywhere')
|
||||||
element.style.setProperty('word-break', 'break-all')
|
elm.style.setProperty('word-break', 'break-all')
|
||||||
}
|
}
|
||||||
|
|
||||||
textToMeasure = normalizeTextForDom(textToMeasure)
|
const normalizedText = normalizeTextForDom(textToMeasure)
|
||||||
|
|
||||||
// Render the text into the measurement element:
|
// Render the text into the measurement element:
|
||||||
element.textContent = textToMeasure
|
elm.textContent = normalizedText
|
||||||
|
|
||||||
// actually measure the text:
|
// actually measure the text:
|
||||||
const { spans, didTruncate } = this.measureElementTextNodeSpans(element, {
|
const { spans, didTruncate } = this.measureElementTextNodeSpans(elm, {
|
||||||
shouldTruncateToFirstLine,
|
shouldTruncateToFirstLine,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (opts.overflow === 'truncate-ellipsis' && didTruncate) {
|
if (opts.overflow === 'truncate-ellipsis' && didTruncate) {
|
||||||
// we need to measure the ellipsis to know how much space it takes up
|
// we need to measure the ellipsis to know how much space it takes up
|
||||||
element.textContent = '…'
|
elm.textContent = '…'
|
||||||
const ellipsisWidth = Math.ceil(this.measureElementTextNodeSpans(element).spans[0].box.w)
|
const ellipsisWidth = Math.ceil(this.measureElementTextNodeSpans(elm).spans[0].box.w)
|
||||||
|
|
||||||
// then, we need to subtract that space from the width we have and measure again:
|
// then, we need to subtract that space from the width we have and measure again:
|
||||||
element.style.setProperty('width', `${elementWidth - ellipsisWidth}px`)
|
elm.style.setProperty('width', `${elementWidth - ellipsisWidth}px`)
|
||||||
element.textContent = textToMeasure
|
elm.textContent = normalizedText
|
||||||
const truncatedSpans = this.measureElementTextNodeSpans(element, {
|
const truncatedSpans = this.measureElementTextNodeSpans(elm, {
|
||||||
shouldTruncateToFirstLine: true,
|
shouldTruncateToFirstLine: true,
|
||||||
}).spans
|
}).spans
|
||||||
|
|
||||||
|
@ -257,7 +255,7 @@ export class TextManager {
|
||||||
return truncatedSpans
|
return truncatedSpans
|
||||||
}
|
}
|
||||||
|
|
||||||
element.remove()
|
elm.remove()
|
||||||
|
|
||||||
return spans
|
return spans
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue