@tldraw/editor
Version:
A tiny little drawing app (editor).
206 lines (205 loc) • 8.13 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var TextManager_exports = {};
__export(TextManager_exports, {
TextManager: () => TextManager
});
module.exports = __toCommonJS(TextManager_exports);
const fixNewLines = /\r?\n|\r/g;
function normalizeTextForDom(text) {
return text.replace(fixNewLines, "\n").split("\n").map((x) => x || " ").join("\n");
}
const textAlignmentsForLtr = {
start: "left",
"start-legacy": "left",
middle: "center",
"middle-legacy": "center",
end: "right",
"end-legacy": "right"
};
const spaceCharacterRegex = /\s/;
class TextManager {
constructor(editor) {
this.editor = editor;
this.baseElem = document.createElement("div");
this.baseElem.classList.add("tl-text");
this.baseElem.classList.add("tl-text-measure");
this.baseElem.tabIndex = -1;
}
baseElem;
measureText(textToMeasure, opts) {
const div = document.createElement("div");
div.textContent = normalizeTextForDom(textToMeasure);
return this.measureHtml(div.innerHTML, opts);
}
measureHtml(html, opts) {
const wrapperElm = this.baseElem.cloneNode();
this.editor.getContainer().appendChild(wrapperElm);
wrapperElm.innerHTML = html;
this.baseElem.insertAdjacentElement("afterend", wrapperElm);
wrapperElm.setAttribute("dir", "auto");
wrapperElm.style.setProperty("unicode-bidi", "plaintext");
wrapperElm.style.setProperty("font-family", opts.fontFamily);
wrapperElm.style.setProperty("font-style", opts.fontStyle);
wrapperElm.style.setProperty("font-weight", opts.fontWeight);
wrapperElm.style.setProperty("font-size", opts.fontSize + "px");
wrapperElm.style.setProperty("line-height", opts.lineHeight * opts.fontSize + "px");
wrapperElm.style.setProperty("max-width", opts.maxWidth === null ? null : opts.maxWidth + "px");
wrapperElm.style.setProperty("min-width", opts.minWidth === null ? null : opts.minWidth + "px");
wrapperElm.style.setProperty("padding", opts.padding);
wrapperElm.style.setProperty(
"overflow-wrap",
opts.disableOverflowWrapBreaking ? "normal" : "break-word"
);
const scrollWidth = wrapperElm.scrollWidth;
const rect = wrapperElm.getBoundingClientRect();
wrapperElm.remove();
return {
x: 0,
y: 0,
w: rect.width,
h: rect.height,
scrollWidth
};
}
/**
* Given an html element, measure the position of each span of unbroken
* word/white-space characters within any text nodes it contains.
*/
measureElementTextNodeSpans(element, { shouldTruncateToFirstLine = false } = {}) {
const spans = [];
const elmBounds = element.getBoundingClientRect();
const offsetX = -elmBounds.left;
const offsetY = -elmBounds.top;
const range = new Range();
const textNode = element.childNodes[0];
let idx = 0;
let currentSpan = null;
let prevCharWasSpaceCharacter = null;
let prevCharTop = 0;
let prevCharLeftForRTLTest = 0;
let didTruncate = false;
for (const childNode of element.childNodes) {
if (childNode.nodeType !== Node.TEXT_NODE) continue;
for (const char of childNode.textContent ?? "") {
range.setStart(textNode, idx);
range.setEnd(textNode, idx + char.length);
const rects = range.getClientRects();
const rect = rects[rects.length - 1];
const top = rect.top + offsetY;
const left = rect.left + offsetX;
const right = rect.right + offsetX;
const isRTL = left < prevCharLeftForRTLTest;
const isSpaceCharacter = spaceCharacterRegex.test(char);
if (
// If we're at a word boundary...
isSpaceCharacter !== prevCharWasSpaceCharacter || // ...or we're on a different line...
top !== prevCharTop || // ...or we're at the start of the text and haven't created a span yet...
!currentSpan
) {
if (currentSpan) {
if (shouldTruncateToFirstLine && top !== prevCharTop) {
didTruncate = true;
break;
}
spans.push(currentSpan);
}
currentSpan = {
box: { x: left, y: top, w: rect.width, h: rect.height },
text: char
};
prevCharLeftForRTLTest = left;
} else {
if (isRTL) {
currentSpan.box.x = left;
}
currentSpan.box.w = isRTL ? currentSpan.box.w + rect.width : right - currentSpan.box.x;
currentSpan.text += char;
}
if (char === "\n") {
prevCharLeftForRTLTest = 0;
}
prevCharWasSpaceCharacter = isSpaceCharacter;
prevCharTop = top;
idx += char.length;
}
}
if (currentSpan) {
spans.push(currentSpan);
}
return { spans, didTruncate };
}
/**
* Measure text into individual spans. Spans are created by rendering the
* text, then dividing it up according to line breaks and word boundaries.
*
* It works by having the browser render the text, then measuring the
* position of each character. You can use this to replicate the text-layout
* algorithm of the current browser in e.g. an SVG export.
*/
measureTextSpans(textToMeasure, opts) {
if (textToMeasure === "") return [];
const elm = this.baseElem.cloneNode();
this.editor.getContainer().appendChild(elm);
const elementWidth = Math.ceil(opts.width - opts.padding * 2);
elm.setAttribute("dir", "auto");
elm.style.setProperty("unicode-bidi", "plaintext");
elm.style.setProperty("width", `${elementWidth}px`);
elm.style.setProperty("height", "min-content");
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]);
elm.style.setProperty("font-style", opts.fontStyle);
const shouldTruncateToFirstLine = opts.overflow === "truncate-ellipsis" || opts.overflow === "truncate-clip";
if (shouldTruncateToFirstLine) {
elm.style.setProperty("overflow-wrap", "anywhere");
elm.style.setProperty("word-break", "break-all");
}
const normalizedText = normalizeTextForDom(textToMeasure);
elm.textContent = normalizedText;
const { spans, didTruncate } = this.measureElementTextNodeSpans(elm, {
shouldTruncateToFirstLine
});
if (opts.overflow === "truncate-ellipsis" && didTruncate) {
elm.textContent = "\u2026";
const ellipsisWidth = Math.ceil(this.measureElementTextNodeSpans(elm).spans[0].box.w);
elm.style.setProperty("width", `${elementWidth - ellipsisWidth}px`);
elm.textContent = normalizedText;
const truncatedSpans = this.measureElementTextNodeSpans(elm, {
shouldTruncateToFirstLine: true
}).spans;
const lastSpan = truncatedSpans[truncatedSpans.length - 1];
truncatedSpans.push({
text: "\u2026",
box: {
x: Math.min(lastSpan.box.x + lastSpan.box.w, opts.width - opts.padding - ellipsisWidth),
y: lastSpan.box.y,
w: ellipsisWidth,
h: lastSpan.box.h
}
});
return truncatedSpans;
}
elm.remove();
return spans;
}
}
//# sourceMappingURL=TextManager.js.map