Files
tolaria/src/components/tldrawTextMeasurementGuard.ts

73 lines
2.2 KiB
TypeScript

import type { Editor } from 'tldraw'
type TextMeasure = Pick<Editor, 'textMeasure'>['textMeasure']
type MeasureElementTextNodeSpans = TextMeasure['measureElementTextNodeSpans']
type TextMeasurement = ReturnType<MeasureElementTextNodeSpans>
type TextMeasurementOptions = Parameters<MeasureElementTextNodeSpans>[1]
interface TextMeasurementHost {
textMeasure: {
measureElementTextNodeSpans: MeasureElementTextNodeSpans
}
}
function isMissingRangeRectError(error: unknown): boolean {
return error instanceof TypeError && error.message.includes('top')
}
function finiteSize(value: number): number {
return Number.isFinite(value) && value > 0 ? value : 1
}
function firstRenderedLine(text: string, options: TextMeasurementOptions): {
didTruncate: boolean
text: string
} {
if (!options?.shouldTruncateToFirstLine) return { didTruncate: false, text }
const lines = text.split(/\r?\n|\r/)
return {
didTruncate: lines.length > 1,
text: lines[0] ?? '',
}
}
function fallbackTextNodeSpans(element: HTMLElement, options: TextMeasurementOptions): TextMeasurement {
const measuredText = firstRenderedLine(element.textContent ?? '', options)
if (!measuredText.text) return { didTruncate: measuredText.didTruncate, spans: [] }
const rect = element.getBoundingClientRect()
return {
didTruncate: measuredText.didTruncate,
spans: [{
box: {
h: finiteSize(rect.height),
w: finiteSize(rect.width),
x: 0,
y: 0,
},
text: measuredText.text,
}],
}
}
export function installTldrawTextMeasurementGuard(host: TextMeasurementHost): () => void {
const { textMeasure } = host
const originalMeasure = textMeasure.measureElementTextNodeSpans
const guardedMeasure: MeasureElementTextNodeSpans = (element, options) => {
try {
return originalMeasure.call(textMeasure, element, options)
} catch (error) {
if (!isMissingRangeRectError(error)) throw error
return fallbackTextNodeSpans(element, options)
}
}
textMeasure.measureElementTextNodeSpans = guardedMeasure
return () => {
if (textMeasure.measureElementTextNodeSpans === guardedMeasure) {
textMeasure.measureElementTextNodeSpans = originalMeasure
}
}
}