add remaining functions to Selection

This commit is contained in:
Muki Kiboigo
2026-01-14 07:27:13 -08:00
parent be1d463775
commit 505e0799da
2 changed files with 295 additions and 17 deletions

View File

@@ -406,3 +406,165 @@
testing.expectEqual("Range", sel.type); testing.expectEqual("Range", sel.type);
} }
</script> </script>
<script id=selectAllChildren>
{
const sel = window.getSelection();
sel.removeAllRanges();
const nested = document.getElementById("nested");
const s1 = document.getElementById("s1");
const s2 = document.getElementById("s2");
// Select all children of nested div
sel.selectAllChildren(nested);
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual("Range", sel.type);
testing.expectEqual(false, sel.isCollapsed);
// Anchor and focus should be on the parent node
testing.expectEqual(nested, sel.anchorNode);
testing.expectEqual(nested, sel.focusNode);
// Should start at offset 0 (before first child)
testing.expectEqual(0, sel.anchorOffset);
const childrenCount = nested.childNodes.length;
// Should end at offset equal to number of children (after last child)
testing.expectEqual(childrenCount, sel.focusOffset);
// Direction should be none
testing.expectEqual("none", sel.direction);
// Should contain the children (fully)
testing.expectEqual(true, sel.containsNode(s1, false));
testing.expectEqual(true, sel.containsNode(s2, false));
// Should not fully contain the parent itself
testing.expectEqual(false, sel.containsNode(nested, false));
// But should partially contain the parent
testing.expectEqual(true, sel.containsNode(nested, true));
// Verify the range
const range = sel.getRangeAt(0);
testing.expectEqual(nested, range.startContainer);
testing.expectEqual(nested, range.endContainer);
testing.expectEqual(0, range.startOffset);
testing.expectEqual(childrenCount, range.endOffset);
}
</script>
<script id=selectAllChildrenEmpty>
{
const sel = window.getSelection();
sel.removeAllRanges();
// Create an empty element
const empty = document.createElement("div");
document.body.appendChild(empty);
// Select all children of empty element
sel.selectAllChildren(empty);
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual("Caret", sel.type); // Collapsed because no children
testing.expectEqual(true, sel.isCollapsed);
testing.expectEqual(empty, sel.anchorNode);
testing.expectEqual(0, sel.anchorOffset);
testing.expectEqual(0, sel.focusOffset);
// Clean up
document.body.removeChild(empty);
}
</script>
<script id=selectAllChildrenReplacesSelection>
{
const sel = window.getSelection();
sel.removeAllRanges();
// Start with an existing selection
const p1 = document.getElementById("p1");
sel.selectAllChildren(p1);
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual(p1, sel.anchorNode);
// selectAllChildren should replace the existing selection
const p2 = document.getElementById("p2");
sel.selectAllChildren(p2);
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual(p2, sel.anchorNode);
testing.expectEqual(p2, sel.focusNode);
// Verify old selection is gone
const range = sel.getRangeAt(0);
testing.expectEqual(p2, range.startContainer);
testing.expectEqual(false, p1 == range.startContainer);
}
</script>
<script id=setBaseAndExtent>
{
const sel = window.getSelection();
sel.removeAllRanges();
const p1 = document.getElementById("p1");
const textNode = p1.firstChild;
// Forward selection (anchor before focus)
sel.setBaseAndExtent(textNode, 4, textNode, 15);
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual("Range", sel.type);
testing.expectEqual(false, sel.isCollapsed);
testing.expectEqual(textNode, sel.anchorNode);
testing.expectEqual(4, sel.anchorOffset);
testing.expectEqual(textNode, sel.focusNode);
testing.expectEqual(15, sel.focusOffset);
testing.expectEqual("forward", sel.direction);
// Backward selection (anchor after focus)
sel.setBaseAndExtent(textNode, 15, textNode, 4);
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual("Range", sel.type);
testing.expectEqual(textNode, sel.anchorNode);
testing.expectEqual(15, sel.anchorOffset);
testing.expectEqual(textNode, sel.focusNode);
testing.expectEqual(4, sel.focusOffset);
testing.expectEqual("backward", sel.direction);
// Collapsed selection (anchor equals focus)
sel.setBaseAndExtent(textNode, 10, textNode, 10);
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual("Caret", sel.type);
testing.expectEqual(true, sel.isCollapsed);
testing.expectEqual(10, sel.anchorOffset);
testing.expectEqual(10, sel.focusOffset);
testing.expectEqual("none", sel.direction);
// Across different nodes
const p2 = document.getElementById("p2");
const textNode2 = p2.firstChild;
sel.setBaseAndExtent(textNode, 4, textNode2, 5);
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual(textNode, sel.anchorNode);
testing.expectEqual(4, sel.anchorOffset);
testing.expectEqual(textNode2, sel.focusNode);
testing.expectEqual(5, sel.focusOffset);
testing.expectEqual("forward", sel.direction);
// Should replace existing selection
sel.setBaseAndExtent(textNode, 0, textNode, 3);
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual(0, sel.anchorOffset);
testing.expectEqual(3, sel.focusOffset);
}
</script>

View File

@@ -108,9 +108,15 @@ pub fn removeRange(self: *Selection, range: *Range) void {
} }
} }
pub fn removeAllRanges(self: *Selection) void { fn removeAllRangesInner(self: *Selection, reset_direction: bool) void {
self._ranges.clearRetainingCapacity(); self._ranges.clearRetainingCapacity();
if (reset_direction) {
self._direction = .none; self._direction = .none;
}
}
pub fn removeAllRanges(self: *Selection) void {
self.removeAllRangesInner(true);
} }
pub fn collapseToEnd(self: *Selection, page: *Page) !void { pub fn collapseToEnd(self: *Selection, page: *Page) !void {
@@ -124,9 +130,8 @@ pub fn collapseToEnd(self: *Selection, page: *Page) !void {
try range.setStart(last_node, last_offset); try range.setStart(last_node, last_offset);
try range.setEnd(last_node, last_offset); try range.setEnd(last_node, last_offset);
self.removeAllRanges(); self.removeAllRangesInner(true);
try self._ranges.append(page.arena, range); try self._ranges.append(page.arena, range);
self._direction = .none;
} }
pub fn collapseToStart(self: *Selection, page: *Page) !void { pub fn collapseToStart(self: *Selection, page: *Page) !void {
@@ -140,7 +145,7 @@ pub fn collapseToStart(self: *Selection, page: *Page) !void {
try range.setStart(first_node, first_offset); try range.setStart(first_node, first_offset);
try range.setStart(first_node, first_offset); try range.setStart(first_node, first_offset);
self.removeAllRanges(); self.removeAllRangesInner(true);
try self._ranges.append(page.arena, range); try self._ranges.append(page.arena, range);
self._direction = .none; self._direction = .none;
} }
@@ -209,8 +214,6 @@ pub fn extend(self: *Selection, node: *Node, _offset: ?u32) !void {
} }
} }
// TODO: getComposedRanges
pub fn getRangeAt(self: *Selection, index: u32) !*Range { pub fn getRangeAt(self: *Selection, index: u32) !*Range {
if (index >= self.getRangeCount()) { if (index >= self.getRangeCount()) {
return error.IndexSizeError; return error.IndexSizeError;
@@ -219,15 +222,129 @@ pub fn getRangeAt(self: *Selection, index: u32) !*Range {
return self._ranges.items[index]; return self._ranges.items[index];
} }
// TODO: modify const ModifyAlter = enum {
move,
extend,
// TODO: selectAllChildren pub fn fromString(str: []const u8) ?ModifyAlter {
return std.meta.stringToEnum(ModifyAlter, str);
}
};
// TODO: setBaseAndExtent const ModifyDirection = enum {
forward,
backward,
left,
right,
pub fn fromString(str: []const u8) ?ModifyDirection {
return std.meta.stringToEnum(ModifyDirection, str);
}
};
const ModifyGranularity = enum {
character,
word,
line,
paragraph,
lineboundary,
// Firefox doesn't implement:
// - sentence
// - paragraph
// - sentenceboundary
// - paragraphboundary
// - documentboundary
// so we won't either for now.
pub fn fromString(str: []const u8) ?ModifyGranularity {
return std.meta.stringToEnum(ModifyGranularity, str);
}
};
pub fn modify(
self: *Selection,
alter_str: []const u8,
direction_str: []const u8,
granularity_str: []const u8,
) !void {
const alter = ModifyAlter.fromString(alter_str) orelse return error.InvalidParams;
const direction = ModifyDirection.fromString(direction_str) orelse return error.InvalidParams;
const granularity = ModifyGranularity.fromString(granularity_str) orelse return error.InvalidParams;
if (self._ranges.items.len == 0) return;
log.warn(.not_implemented, "Selection.modify", .{
.alter = alter,
.direction = direction,
.granularity = granularity,
});
}
pub fn selectAllChildren(self: *Selection, parent: *Node, page: *Page) !void {
if (parent._type == .document_type) {
return error.InvalidNodeType;
}
const range = try Range.init(page);
try range.setStart(parent, 0);
const child_count = parent.getLength();
try range.setEnd(parent, @intCast(child_count));
self.removeAllRangesInner(true);
try self._ranges.append(page.arena, range);
}
pub fn setBaseAndExtent(
self: *Selection,
anchor_node: *Node,
anchor_offset: u32,
focus_node: *Node,
focus_offset: u32,
page: *Page,
) !void {
if (anchor_offset > anchor_node.getLength()) {
return error.IndexSizeError;
}
if (focus_offset > focus_node.getLength()) {
return error.IndexSizeError;
}
const cmp = AbstractRange.compareBoundaryPoints(
anchor_node,
anchor_offset,
focus_node,
focus_offset,
);
const range = try Range.init(page);
switch (cmp) {
.before => {
try range.setStart(anchor_node, anchor_offset);
try range.setEnd(focus_node, focus_offset);
self._direction = .forward;
},
.after => {
try range.setStart(focus_node, focus_offset);
try range.setEnd(anchor_node, anchor_offset);
self._direction = .backward;
},
.equal => {
try range.setStart(anchor_node, anchor_offset);
try range.setEnd(anchor_node, anchor_offset);
self._direction = .none;
},
}
self.removeAllRangesInner(false);
try self._ranges.append(page.arena, range);
}
pub fn setPosition(self: *Selection, _node: ?*Node, _offset: ?u32, page: *Page) !void { pub fn setPosition(self: *Selection, _node: ?*Node, _offset: ?u32, page: *Page) !void {
const node = _node orelse { const node = _node orelse {
self.removeAllRanges(); self.removeAllRangesInner(true);
return; return;
}; };
@@ -241,9 +358,8 @@ pub fn setPosition(self: *Selection, _node: ?*Node, _offset: ?u32, page: *Page)
try range.setStart(node, offset); try range.setStart(node, offset);
try range.setEnd(node, offset); try range.setEnd(node, offset);
self.removeAllRanges(); self.removeAllRangesInner(true);
try self._ranges.append(page.arena, range); try self._ranges.append(page.arena, range);
self._direction = .none;
} }
pub const JsApi = struct { pub const JsApi = struct {
@@ -272,13 +388,13 @@ pub const JsApi = struct {
pub const deleteFromDocument = bridge.function(Selection.deleteFromDocument, .{}); pub const deleteFromDocument = bridge.function(Selection.deleteFromDocument, .{});
pub const empty = bridge.function(Selection.removeAllRanges, .{}); pub const empty = bridge.function(Selection.removeAllRanges, .{});
pub const extend = bridge.function(Selection.extend, .{ .dom_exception = true }); pub const extend = bridge.function(Selection.extend, .{ .dom_exception = true });
// getComposedRanges // unimplemented: getComposedRanges
pub const getRangeAt = bridge.function(Selection.getRangeAt, .{ .dom_exception = true }); pub const getRangeAt = bridge.function(Selection.getRangeAt, .{ .dom_exception = true });
// modify pub const modify = bridge.function(Selection.modify, .{});
pub const removeAllRanges = bridge.function(Selection.removeAllRanges, .{}); pub const removeAllRanges = bridge.function(Selection.removeAllRanges, .{});
pub const removeRange = bridge.function(Selection.removeRange, .{}); pub const removeRange = bridge.function(Selection.removeRange, .{});
// selectAllChildren pub const selectAllChildren = bridge.function(Selection.selectAllChildren, .{});
// setBaseAndExtent pub const setBaseAndExtent = bridge.function(Selection.setBaseAndExtent, .{ .dom_exception = true });
pub const setPosition = bridge.function(Selection.setPosition, .{}); pub const setPosition = bridge.function(Selection.setPosition, .{});
}; };