fix(composer): preserve mounted geometry fallbacks

Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
Taylor Ho
2026-08-25 18:55:15 -07:00
committed by Carl
parent 1179198074
commit 5b88a8b7cc
2 changed files with 98 additions and 27 deletions

View File

@@ -28,3 +28,66 @@ test("selection geometry returns null instead of throwing after view teardown",
test("link geometry returns null instead of throwing after view teardown", () => {
assert.equal(getEditorLinkRect(unmountedEditor(), 1, 2), null);
});
function mountedEditorWithCaretGeometry() {
const coords = new Map([
[1, { left: 10, right: 11, top: 20, bottom: 32 }],
[2, { left: 30, right: 31, top: 20, bottom: 32 }],
]);
return {
view: {
domAtPos() {
return { node: {}, offset: 99 };
},
coordsAtPos(pos) {
return coords.get(pos);
},
},
};
}
function withThrowingRange(callback) {
const previousDocument = globalThis.document;
globalThis.document = {
createRange() {
throw new DOMException("stale DOM offset", "IndexSizeError");
},
};
try {
callback();
} finally {
globalThis.document = previousDocument;
}
}
test("selection geometry keeps the caret fallback when Range construction fails", () => {
withThrowingRange(() => {
assert.deepEqual(
getEditorSelectionRect(mountedEditorWithCaretGeometry(), 1, 2),
{
left: 10,
top: 20,
width: 21,
height: 12,
right: 31,
bottom: 32,
},
);
});
});
test("link geometry keeps the caret fallback when Range construction fails", () => {
withThrowingRange(() => {
assert.deepEqual(
getEditorLinkRect(mountedEditorWithCaretGeometry(), 1, 2),
{
left: 10,
top: 20,
width: 1,
height: 12,
right: 11,
bottom: 32,
},
);
});
});

View File

@@ -15,18 +15,22 @@ export function getEditorSelectionRect(
to: number,
): EditorRect | null {
try {
const range = document.createRange();
const start = editor.view.domAtPos(from);
const end = editor.view.domAtPos(to);
range.setStart(start.node, start.offset);
range.setEnd(end.node, end.offset);
try {
const range = document.createRange();
const start = editor.view.domAtPos(from);
const end = editor.view.domAtPos(to);
range.setStart(start.node, start.offset);
range.setEnd(end.node, end.offset);
const clientRects = Array.from(range.getClientRects()).filter(
(rect) => rect.width > 0 || rect.height > 0,
);
const rect = clientRects[0] ?? range.getBoundingClientRect();
range.detach();
if (rect.width > 0 || rect.height > 0) return rect;
const clientRects = Array.from(range.getClientRects()).filter(
(rect) => rect.width > 0 || rect.height > 0,
);
const rect = clientRects[0] ?? range.getBoundingClientRect();
range.detach();
if (rect.width > 0 || rect.height > 0) return rect;
} catch {
// A mounted view can still have stale Range offsets; use caret geometry.
}
const startCoords = editor.view.coordsAtPos(from);
const endCoords = editor.view.coordsAtPos(to);
@@ -55,23 +59,27 @@ export function getEditorLinkRect(
to: number,
): EditorRect | null {
try {
const range = document.createRange();
const start = editor.view.domAtPos(from);
const end = editor.view.domAtPos(to);
range.setStart(start.node, start.offset);
range.setEnd(end.node, end.offset);
try {
const range = document.createRange();
const start = editor.view.domAtPos(from);
const end = editor.view.domAtPos(to);
range.setStart(start.node, start.offset);
range.setEnd(end.node, end.offset);
const rect = range.getBoundingClientRect();
range.detach();
if (rect.width > 0 || rect.height > 0) {
return {
left: rect.left,
top: rect.top,
width: rect.width,
height: rect.height,
right: rect.right,
bottom: rect.bottom,
};
const rect = range.getBoundingClientRect();
range.detach();
if (rect.width > 0 || rect.height > 0) {
return {
left: rect.left,
top: rect.top,
width: rect.width,
height: rect.height,
right: rect.right,
bottom: rect.bottom,
};
}
} catch {
// A mounted view can still have stale Range offsets; use caret geometry.
}
const coords = editor.view.coordsAtPos(from);