From 9370e298d2286991cff83b7306870d37f84b19f5 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 8 Dec 2025 09:07:56 +0800 Subject: [PATCH] improve HTMLOption and HTMLOptionCollection --- src/browser/tests/element/html/select.html | 23 ++++++++++++++ .../collections/HTMLOptionsCollection.zig | 24 +++++++++++---- src/browser/webapi/element/html/Option.zig | 30 ++++++++----------- src/browser/webapi/element/html/Select.zig | 21 +++++++++---- 4 files changed, 69 insertions(+), 29 deletions(-) diff --git a/src/browser/tests/element/html/select.html b/src/browser/tests/element/html/select.html index ceb46c16..4c4e6ccd 100644 --- a/src/browser/tests/element/html/select.html +++ b/src/browser/tests/element/html/select.html @@ -158,6 +158,7 @@ { const sel = $('#select1') const opts = sel.options + testing.expectEqual(3, sel.length) testing.expectEqual(3, opts.length) testing.expectEqual('HTMLOptionsCollection', opts.constructor.name) @@ -165,6 +166,9 @@ testing.expectEqual('val1', opts[0].value) testing.expectEqual('val2', opts[1].value) testing.expectEqual('val3', opts[2].value) + testing.expectEqual('val1', opts.item(0).value); + testing.expectEqual('val2', opts.item(1).value); + testing.expectEqual('val3', opts.item(2).value); } @@ -224,6 +228,12 @@ testing.expectEqual(2, opts.length) testing.expectEqual('zero', opts[0].value) testing.expectEqual('b', opts[1].value) + + opts.add(opt1, 0) + testing.expectEqual(3, opts.length) + testing.expectEqual('a', opts[0].value) + testing.expectEqual('zero', opts[1].value) + testing.expectEqual('b', opts[2].value) } @@ -364,3 +374,16 @@ testing.expectTrue(select.outerHTML.includes('size="7"')) } + + + diff --git a/src/browser/webapi/collections/HTMLOptionsCollection.zig b/src/browser/webapi/collections/HTMLOptionsCollection.zig index 4c9d59c4..6a0cadc9 100644 --- a/src/browser/webapi/collections/HTMLOptionsCollection.zig +++ b/src/browser/webapi/collections/HTMLOptionsCollection.zig @@ -20,6 +20,7 @@ const std = @import("std"); const js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); +const Node = @import("../Node.zig"); const Element = @import("../Element.zig"); const HTMLCollection = @import("HTMLCollection.zig"); const NodeLive = @import("node_live.zig").NodeLive; @@ -59,17 +60,28 @@ pub fn setSelectedIndex(self: *HTMLOptionsCollection, index: i32) !void { const Option = @import("../element/html/Option.zig"); +const AddBeforeOption = union(enum) { + option: *Option, + index: u32, +}; + // Add a new option element -pub fn add(self: *HTMLOptionsCollection, element: *Option, before: ?*Option, page: *Page) !void { +pub fn add(self: *HTMLOptionsCollection, element: *Option, before_: ?AddBeforeOption, page: *Page) !void { const select_node = self._select.asNode(); const element_node = element.asElement().asNode(); - if (before) |before_option| { - const before_node = before_option.asElement().asNode(); - _ = try select_node.insertBefore(element_node, before_node, page); - } else { - _ = try select_node.appendChild(element_node, page); + var before_node: ?*Node = null; + if (before_) |before| { + switch (before) { + .index => |idx| { + if (self.getAtIndex(idx, page)) |el| { + before_node = el.asNode(); + } + }, + .option => |before_option| before_node = before_option.asNode(), + } } + _ = try select_node.insertBefore(element_node, before_node, page); } // Remove an option element by index diff --git a/src/browser/webapi/element/html/Option.zig b/src/browser/webapi/element/html/Option.zig index b5718a1e..207f4aaf 100644 --- a/src/browser/webapi/element/html/Option.zig +++ b/src/browser/webapi/element/html/Option.zig @@ -28,7 +28,6 @@ const Option = @This(); _proto: *HtmlElement, _value: ?[]const u8 = null, -_text: ?[]const u8 = null, _selected: bool = false, _default_selected: bool = false, _disabled: bool = false, @@ -43,9 +42,15 @@ pub fn asNode(self: *Option) *Node { return self.asElement().asNode(); } -pub fn getValue(self: *const Option) []const u8 { - // If value attribute exists, use that; otherwise use text content - return self._value orelse self._text orelse ""; +pub fn getValue(self: *Option, page: *Page) []const u8 { + // If value attribute exists, use that; otherwise use text content (stripped) + if (self._value) |v| { + return v; + } + + const node = self.asNode(); + const text = node.getTextContentAlloc(page.call_arena) catch return ""; + return std.mem.trim(u8, text, &std.ascii.whitespace); } pub fn setValue(self: *Option, value: []const u8, page: *Page) !void { @@ -55,7 +60,9 @@ pub fn setValue(self: *Option, value: []const u8, page: *Page) !void { } pub fn getText(self: *const Option) []const u8 { - return self._text orelse ""; + const node: *Node = @constCast(self.asConstElement().asConstNode()); + const allocator = std.heap.page_allocator; // TODO: use proper allocator + return node.getTextContentAlloc(allocator) catch ""; } pub fn getSelected(self: *const Option) bool { @@ -112,8 +119,6 @@ pub const JsApi = struct { }; pub const Build = struct { - const CData = @import("../../CData.zig"); - pub fn created(node: *Node, _: *Page) !void { var self = node.as(Option); const element = self.asElement(); @@ -129,17 +134,6 @@ pub const Build = struct { self._disabled = element.getAttributeSafe("disabled") != null; } - pub fn complete(node: *Node, _: *const Page) !void { - var self = node.as(Option); - - // Get text content - if (node.firstChild()) |child| { - if (child.is(CData.Text)) |txt| { - self._text = txt.getWholeText(); - } - } - } - pub fn attributeChange(element: *Element, name: []const u8, value: []const u8, _: *Page) !void { const attribute = std.meta.stringToEnum(enum { value, selected }, name) orelse return; const self = element.as(Option); diff --git a/src/browser/webapi/element/html/Select.zig b/src/browser/webapi/element/html/Select.zig index beb00ee5..5b3c0b9e 100644 --- a/src/browser/webapi/element/html/Select.zig +++ b/src/browser/webapi/element/html/Select.zig @@ -44,7 +44,7 @@ pub fn asConstNode(self: *const Select) *const Node { return self.asConstElement().asConstNode(); } -pub fn getValue(self: *Select) []const u8 { +pub fn getValue(self: *Select, page: *Page) []const u8 { // Return value of first selected option, or first option if none selected var first_option: ?*Option = null; var iter = self.asNode().childrenIterator(); @@ -54,25 +54,24 @@ pub fn getValue(self: *Select) []const u8 { first_option = option; } if (option.getSelected()) { - return option.getValue(); + return option.getValue(page); } } // No explicitly selected option, return first option's value if (first_option) |opt| { - return opt.getValue(); + return opt.getValue(page); } return ""; } pub fn setValue(self: *Select, value: []const u8, page: *Page) !void { - _ = page; // Find option with matching value and select it // Note: This updates the current state (_selected), not the default state (attribute) // Setting value always deselects all others, even for multiple selects var iter = self.asNode().childrenIterator(); while (iter.next()) |child| { const option = child.is(Option) orelse continue; - option._selected = std.mem.eql(u8, option.getValue(), value); + option._selected = std.mem.eql(u8, option.getValue(page), value); } } @@ -196,6 +195,17 @@ pub fn getOptions(self: *Select, page: *Page) !*collections.HTMLOptionsCollectio }); } +pub fn getLength(self: *Select) u32 { + var i: u32 = 0; + var it = self.asNode().childrenIterator(); + while (it.next()) |child| { + if (child.is(Option) != null) { + i += 1; + } + } + return i; +} + pub fn getSelectedOptions(self: *Select, page: *Page) !collections.NodeLive(.selected_options) { return collections.NodeLive(.selected_options).init(null, self.asNode(), {}, page); } @@ -243,6 +253,7 @@ pub const JsApi = struct { pub const selectedOptions = bridge.accessor(Select.getSelectedOptions, null, .{}); pub const form = bridge.accessor(Select.getForm, null, .{}); pub const size = bridge.accessor(Select.getSize, Select.setSize, .{}); + pub const length = bridge.accessor(Select.getLength, null, .{}); }; pub const Build = struct {