mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-02-04 06:23:45 +00:00
add remaining functions to Selection
This commit is contained in:
@@ -406,3 +406,165 @@
|
||||
testing.expectEqual("Range", sel.type);
|
||||
}
|
||||
</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>
|
||||
|
||||
@@ -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._direction = .none;
|
||||
if (reset_direction) {
|
||||
self._direction = .none;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn removeAllRanges(self: *Selection) void {
|
||||
self.removeAllRangesInner(true);
|
||||
}
|
||||
|
||||
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.setEnd(last_node, last_offset);
|
||||
|
||||
self.removeAllRanges();
|
||||
self.removeAllRangesInner(true);
|
||||
try self._ranges.append(page.arena, range);
|
||||
self._direction = .none;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
self.removeAllRanges();
|
||||
self.removeAllRangesInner(true);
|
||||
try self._ranges.append(page.arena, range);
|
||||
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 {
|
||||
if (index >= self.getRangeCount()) {
|
||||
return error.IndexSizeError;
|
||||
@@ -219,15 +222,129 @@ pub fn getRangeAt(self: *Selection, index: u32) !*Range {
|
||||
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 {
|
||||
const node = _node orelse {
|
||||
self.removeAllRanges();
|
||||
self.removeAllRangesInner(true);
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -241,9 +358,8 @@ pub fn setPosition(self: *Selection, _node: ?*Node, _offset: ?u32, page: *Page)
|
||||
try range.setStart(node, offset);
|
||||
try range.setEnd(node, offset);
|
||||
|
||||
self.removeAllRanges();
|
||||
self.removeAllRangesInner(true);
|
||||
try self._ranges.append(page.arena, range);
|
||||
self._direction = .none;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
@@ -272,13 +388,13 @@ pub const JsApi = struct {
|
||||
pub const deleteFromDocument = bridge.function(Selection.deleteFromDocument, .{});
|
||||
pub const empty = bridge.function(Selection.removeAllRanges, .{});
|
||||
pub const extend = bridge.function(Selection.extend, .{ .dom_exception = true });
|
||||
// getComposedRanges
|
||||
// unimplemented: getComposedRanges
|
||||
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 removeRange = bridge.function(Selection.removeRange, .{});
|
||||
// selectAllChildren
|
||||
// setBaseAndExtent
|
||||
pub const selectAllChildren = bridge.function(Selection.selectAllChildren, .{});
|
||||
pub const setBaseAndExtent = bridge.function(Selection.setBaseAndExtent, .{ .dom_exception = true });
|
||||
pub const setPosition = bridge.function(Selection.setPosition, .{});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user