Merge pull request #4222 from lightpanda-io/range-getClientRects

webapi: Fix Range.getClientRects
This commit is contained in:
Karl Seguin authored and GitHub committed 2026-10-28 06:36:43 +08:00
commit 9fd20750fd
2 files changed
+127 -8

No files matched your search

+47 -4
View File
@@ -1,4 +1,5 @@
<!DOCTYPE html>
<meta name=lp-mode content=test>
<script src="testing.js"></script>
<body>
<div id="test-content">
@@ -1196,11 +1197,53 @@
<script id=getClientRects_element>
{
// One rect per selected element whose parent isn't selected, plus one per
// selected piece of text, in tree order
const host = document.createElement('div');
document.body.appendChild(host);
host.innerHTML = '<p><span>a</span>b</p>';
const p = host.firstChild;
const span = p.firstChild;
const range = new Range();
const p = document.getElementById('p1');
// span, its text, b
range.selectNodeContents(p);
const rects = range.getClientRects();
const elemRects = p.getClientRects();
testing.expectEqual(elemRects.length, rects.length);
let rects = range.getClientRects();
testing.expectEqual(3, rects.length);
testing.expectEqual(span.getBoundingClientRect().width, rects[0].width);
// p covers span, so: p, a, b
range.selectNode(p);
rects = range.getClientRects();
testing.expectEqual(3, rects.length);
testing.expectEqual(p.getBoundingClientRect().width, rects[0].width);
// From inside span: only text, span is partly selected
range.setStart(span.firstChild, 0);
range.setEnd(p.lastChild, 1);
testing.expectEqual(2, range.getClientRects().length);
// An ancestor of the end is partly selected too: span, ab, c
host.innerHTML = '<div><span>ab</span><i>cd</i></div>';
const div = host.firstChild;
range.setStart(div, 0);
range.setEnd(div.lastChild.firstChild, 1);
testing.expectEqual(3, range.getClientRects().length);
// A partial text rect measures only its part
const text = div.lastChild.firstChild;
range.setStart(text, 0);
range.setEnd(text, 1);
const part = range.getClientRects();
testing.expectEqual(1, part.length);
range.setEnd(text, 2);
testing.expectTrue(range.getClientRects()[0].width > part[0].width);
// Comments and boxless elements add nothing
host.innerHTML = '<p>a<!--x--><span style="display:none">x</span>b</p>';
range.selectNodeContents(host.firstChild);
testing.expectEqual(2, range.getClientRects().length);
host.remove();
}
</script>
+80 -4
View File
@@ -835,11 +835,87 @@ pub fn getBoundingClientRect(self: *const Range, frame: *Frame) !*DOMRect {
return DOMRect.create(rect, frame._factory);
}
// One rect per selected element whose parent isn't selected, and one per
// selected piece of text, including text inside those elements.
fn getClientRects(self: *const Range, frame: *Frame) ![]*DOMRect {
const rect = try self.clientRect(frame) orelse return &.{};
const rects = try frame.local_arena.alloc(*DOMRect, 1);
rects[0] = try DOMRect.create(rect, frame._factory);
return rects;
if (self._proto.getCollapsed()) {
return &.{};
}
const start_node = self._proto._start_container;
const end_node = self._proto._end_container;
const start_offset = self._proto._start_offset;
const end_offset = self._proto._end_offset;
var rects: std.ArrayList(*DOMRect) = .empty;
if (start_node == end_node) {
if (start_node.is(Node.CData)) |cdata| {
try appendTextRect(&rects, cdata, start_offset, end_offset, frame);
return rects.items;
}
}
if (start_node.is(Node.CData)) |cdata| {
try appendTextRect(&rects, cdata, start_offset, null, frame);
}
const root = self._proto.getCommonAncestorContainer();
var current: ?*Node = if (start_node.is(Node.CData) != null)
nextInTreeOrder(start_node, root)
else
start_node.getChildAt(start_offset) orelse nextAfterSubtree(start_node, root);
const walk_end: ?*Node = if (end_node.is(Node.CData) != null)
end_node
else
end_node.getChildAt(end_offset) orelse nextAfterSubtree(end_node, root);
// The last element whose rect was added: its descendants add only text
var covering: ?*Node = null;
while (current) |node| : (current = nextInTreeOrder(node, root)) {
if (node == walk_end) {
break;
}
if (node.is(Node.CData)) |cdata| {
try appendTextRect(&rects, cdata, 0, null, frame);
continue;
}
const el = node.is(Node.Element) orelse continue;
if (covering) |c| {
if (c.contains(node)) {
continue;
}
covering = null;
}
// An ancestor of the end boundary is only partly selected
if (node.contains(end_node)) {
continue;
}
covering = node;
if (el.isVisible(frame)) {
try rects.append(frame.local_arena, try DOMRect.create(el.boundingClientRectValuesForVisible(frame), frame._factory));
}
}
if (start_node != end_node) {
if (end_node.is(Node.CData)) |cdata| {
try appendTextRect(&rects, cdata, 0, end_offset, frame);
}
}
return rects.items;
}
// The rect of cdata's text between two UTF-16 offsets; a null end is its end.
fn appendTextRect(rects: *std.ArrayList(*DOMRect), cdata: *Node.CData, start: u32, end: ?u32, frame: *Frame) !void {
if (isCommentOrPI(cdata)) {
return;
}
const parent = cdata.asNode().parentNode() orelse return;
const element = layoutElement(parent) orelse return;
const data = cdata.getData().str();
const s = byteOffset(data, start);
const e = if (end) |offset| byteOffset(data, offset) else data.len;
const rect = measureText(element, data[s..e], frame) orelse return;
try rects.append(frame.local_arena, try DOMRect.create(rect, frame._factory));
}
// null when nothing in the range has a box