improve HTMLOption and HTMLOptionCollection

This commit is contained in:
Karl Seguin
2025-12-08 09:07:56 +08:00
parent eecadb3962
commit 9370e298d2
4 changed files with 69 additions and 29 deletions

View File

@@ -158,6 +158,7 @@
{ {
const sel = $('#select1') const sel = $('#select1')
const opts = sel.options const opts = sel.options
testing.expectEqual(3, sel.length)
testing.expectEqual(3, opts.length) testing.expectEqual(3, opts.length)
testing.expectEqual('HTMLOptionsCollection', opts.constructor.name) testing.expectEqual('HTMLOptionsCollection', opts.constructor.name)
@@ -165,6 +166,9 @@
testing.expectEqual('val1', opts[0].value) testing.expectEqual('val1', opts[0].value)
testing.expectEqual('val2', opts[1].value) testing.expectEqual('val2', opts[1].value)
testing.expectEqual('val3', opts[2].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);
} }
</script> </script>
@@ -224,6 +228,12 @@
testing.expectEqual(2, opts.length) testing.expectEqual(2, opts.length)
testing.expectEqual('zero', opts[0].value) testing.expectEqual('zero', opts[0].value)
testing.expectEqual('b', opts[1].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)
} }
</script> </script>
@@ -364,3 +374,16 @@
testing.expectTrue(select.outerHTML.includes('size="7"')) testing.expectTrue(select.outerHTML.includes('size="7"'))
} }
</script> </script>
<select id="no_value">
<option>d1
<option>d2
</select>
<script id="no_value_attribute">
{
const select = $('#no_value');
testing.expectEqual(2, select.length)
testing.expectEqual('d1', select.options[0].value)
testing.expectEqual('d2', select.options[1].value)
}
</script>

View File

@@ -20,6 +20,7 @@ const std = @import("std");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const Node = @import("../Node.zig");
const Element = @import("../Element.zig"); const Element = @import("../Element.zig");
const HTMLCollection = @import("HTMLCollection.zig"); const HTMLCollection = @import("HTMLCollection.zig");
const NodeLive = @import("node_live.zig").NodeLive; 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 Option = @import("../element/html/Option.zig");
const AddBeforeOption = union(enum) {
option: *Option,
index: u32,
};
// Add a new option element // 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 select_node = self._select.asNode();
const element_node = element.asElement().asNode(); const element_node = element.asElement().asNode();
if (before) |before_option| { var before_node: ?*Node = null;
const before_node = before_option.asElement().asNode(); if (before_) |before| {
_ = try select_node.insertBefore(element_node, before_node, page); switch (before) {
} else { .index => |idx| {
_ = try select_node.appendChild(element_node, page); 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 // Remove an option element by index

View File

@@ -28,7 +28,6 @@ const Option = @This();
_proto: *HtmlElement, _proto: *HtmlElement,
_value: ?[]const u8 = null, _value: ?[]const u8 = null,
_text: ?[]const u8 = null,
_selected: bool = false, _selected: bool = false,
_default_selected: bool = false, _default_selected: bool = false,
_disabled: bool = false, _disabled: bool = false,
@@ -43,9 +42,15 @@ pub fn asNode(self: *Option) *Node {
return self.asElement().asNode(); return self.asElement().asNode();
} }
pub fn getValue(self: *const Option) []const u8 { pub fn getValue(self: *Option, page: *Page) []const u8 {
// If value attribute exists, use that; otherwise use text content // If value attribute exists, use that; otherwise use text content (stripped)
return self._value orelse self._text orelse ""; 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 { 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 { 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 { pub fn getSelected(self: *const Option) bool {
@@ -112,8 +119,6 @@ pub const JsApi = struct {
}; };
pub const Build = struct { pub const Build = struct {
const CData = @import("../../CData.zig");
pub fn created(node: *Node, _: *Page) !void { pub fn created(node: *Node, _: *Page) !void {
var self = node.as(Option); var self = node.as(Option);
const element = self.asElement(); const element = self.asElement();
@@ -129,17 +134,6 @@ pub const Build = struct {
self._disabled = element.getAttributeSafe("disabled") != null; 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 { 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 attribute = std.meta.stringToEnum(enum { value, selected }, name) orelse return;
const self = element.as(Option); const self = element.as(Option);

View File

@@ -44,7 +44,7 @@ pub fn asConstNode(self: *const Select) *const Node {
return self.asConstElement().asConstNode(); 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 // Return value of first selected option, or first option if none selected
var first_option: ?*Option = null; var first_option: ?*Option = null;
var iter = self.asNode().childrenIterator(); var iter = self.asNode().childrenIterator();
@@ -54,25 +54,24 @@ pub fn getValue(self: *Select) []const u8 {
first_option = option; first_option = option;
} }
if (option.getSelected()) { if (option.getSelected()) {
return option.getValue(); return option.getValue(page);
} }
} }
// No explicitly selected option, return first option's value // No explicitly selected option, return first option's value
if (first_option) |opt| { if (first_option) |opt| {
return opt.getValue(); return opt.getValue(page);
} }
return ""; return "";
} }
pub fn setValue(self: *Select, value: []const u8, page: *Page) !void { pub fn setValue(self: *Select, value: []const u8, page: *Page) !void {
_ = page;
// Find option with matching value and select it // Find option with matching value and select it
// Note: This updates the current state (_selected), not the default state (attribute) // Note: This updates the current state (_selected), not the default state (attribute)
// Setting value always deselects all others, even for multiple selects // Setting value always deselects all others, even for multiple selects
var iter = self.asNode().childrenIterator(); var iter = self.asNode().childrenIterator();
while (iter.next()) |child| { while (iter.next()) |child| {
const option = child.is(Option) orelse continue; 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) { pub fn getSelectedOptions(self: *Select, page: *Page) !collections.NodeLive(.selected_options) {
return collections.NodeLive(.selected_options).init(null, self.asNode(), {}, page); 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 selectedOptions = bridge.accessor(Select.getSelectedOptions, null, .{});
pub const form = bridge.accessor(Select.getForm, null, .{}); pub const form = bridge.accessor(Select.getForm, null, .{});
pub const size = bridge.accessor(Select.getSize, Select.setSize, .{}); pub const size = bridge.accessor(Select.getSize, Select.setSize, .{});
pub const length = bridge.accessor(Select.getLength, null, .{});
}; };
pub const Build = struct { pub const Build = struct {