diff --git a/src/browser/tests/element/html/button.html b/src/browser/tests/element/html/button.html
index dc7d5855..76e5be8b 100644
--- a/src/browser/tests/element/html/button.html
+++ b/src/browser/tests/element/html/button.html
@@ -53,3 +53,76 @@
const buttonInvalidFormAttr = $('#button_invalid_form_attr')
testing.expectEqual(null, buttonInvalidFormAttr.form)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/tests/element/html/input.html b/src/browser/tests/element/html/input.html
index 9f762d42..dbd79aa3 100644
--- a/src/browser/tests/element/html/input.html
+++ b/src/browser/tests/element/html/input.html
@@ -15,7 +15,7 @@
-
+
+
+
+
+
diff --git a/src/browser/tests/element/html/option.html b/src/browser/tests/element/html/option.html
index 30d02378..6e7f72c8 100644
--- a/src/browser/tests/element/html/option.html
+++ b/src/browser/tests/element/html/option.html
@@ -65,3 +65,37 @@
$('#opt4').disabled = false
testing.expectEqual(false, $('#opt4').disabled)
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/tests/element/html/select.html b/src/browser/tests/element/html/select.html
index a6a835a6..ceb46c16 100644
--- a/src/browser/tests/element/html/select.html
+++ b/src/browser/tests/element/html/select.html
@@ -81,3 +81,286 @@
const selectNoForm = $('#select_no_form')
testing.expectEqual(null, selectNoForm.form)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/tests/element/html/textarea.html b/src/browser/tests/element/html/textarea.html
index f20e182e..f820288e 100644
--- a/src/browser/tests/element/html/textarea.html
+++ b/src/browser/tests/element/html/textarea.html
@@ -76,3 +76,76 @@
const textareaInvalidFormAttr = $('#textarea_invalid_form_attr')
testing.expectEqual(null, textareaInvalidFormAttr.form)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig
index 9f0fdd5f..0ea4783e 100644
--- a/src/browser/webapi/Element.zig
+++ b/src/browser/webapi/Element.zig
@@ -407,6 +407,7 @@ pub fn replaceChildren(self: *Element, nodes: []const Node.NodeOrText, page: *Pa
}
pub fn remove(self: *Element, page: *Page) void {
+ page.domChanged();
const node = self.asNode();
const parent = node._parent orelse return;
page.removeNode(parent, node, .{ .will_be_reconnected = false });
diff --git a/src/browser/webapi/collections.zig b/src/browser/webapi/collections.zig
index 0e091cbd..eead81b9 100644
--- a/src/browser/webapi/collections.zig
+++ b/src/browser/webapi/collections.zig
@@ -20,6 +20,7 @@ pub const NodeLive = @import("collections/node_live.zig").NodeLive;
pub const ChildNodes = @import("collections/ChildNodes.zig");
pub const DOMTokenList = @import("collections/DOMTokenList.zig");
pub const HTMLAllCollection = @import("collections/HTMLAllCollection.zig");
+pub const HTMLOptionsCollection = @import("collections/HTMLOptionsCollection.zig");
pub fn registerTypes() []const type {
return &.{
@@ -31,6 +32,7 @@ pub fn registerTypes() []const type {
@import("collections/NodeList.zig").EntryIterator,
@import("collections/HTMLAllCollection.zig"),
@import("collections/HTMLAllCollection.zig").Iterator,
+ HTMLOptionsCollection,
DOMTokenList,
DOMTokenList.Iterator,
};
diff --git a/src/browser/webapi/collections/HTMLCollection.zig b/src/browser/webapi/collections/HTMLCollection.zig
index e3f42a90..54c99bff 100644
--- a/src/browser/webapi/collections/HTMLCollection.zig
+++ b/src/browser/webapi/collections/HTMLCollection.zig
@@ -29,6 +29,8 @@ const Mode = enum {
tag_name,
class_name,
child_elements,
+ child_tag,
+ selected_options,
};
const HTMLCollection = @This();
@@ -38,6 +40,8 @@ data: union(Mode) {
tag_name: NodeLive(.tag_name),
class_name: NodeLive(.class_name),
child_elements: NodeLive(.child_elements),
+ child_tag: NodeLive(.child_tag),
+ selected_options: NodeLive(.selected_options),
},
pub fn length(self: *HTMLCollection, page: *const Page) u32 {
@@ -66,6 +70,8 @@ pub fn iterator(self: *HTMLCollection, page: *Page) !*Iterator {
.tag_name => |*impl| .{ .tag_name = impl._tw.clone() },
.class_name => |*impl| .{ .class_name = impl._tw.clone() },
.child_elements => |*impl| .{ .child_elements = impl._tw.clone() },
+ .child_tag => |*impl| .{ .child_tag = impl._tw.clone() },
+ .selected_options => |*impl| .{ .selected_options = impl._tw.clone() },
},
}, page);
}
@@ -78,6 +84,8 @@ pub const Iterator = GenericIterator(struct {
tag_name: TreeWalker.FullExcludeSelf,
class_name: TreeWalker.FullExcludeSelf,
child_elements: TreeWalker.Children,
+ child_tag: TreeWalker.Children,
+ selected_options: TreeWalker.Children,
},
pub fn next(self: *@This(), _: *Page) ?*Element {
@@ -86,6 +94,8 @@ pub const Iterator = GenericIterator(struct {
.tag_name => |*impl| impl.nextTw(&self.tw.tag_name),
.class_name => |*impl| impl.nextTw(&self.tw.class_name),
.child_elements => |*impl| impl.nextTw(&self.tw.child_elements),
+ .child_tag => |*impl| impl.nextTw(&self.tw.child_tag),
+ .selected_options => |*impl| impl.nextTw(&self.tw.selected_options),
};
}
}, null);
diff --git a/src/browser/webapi/collections/HTMLOptionsCollection.zig b/src/browser/webapi/collections/HTMLOptionsCollection.zig
new file mode 100644
index 00000000..4c9d59c4
--- /dev/null
+++ b/src/browser/webapi/collections/HTMLOptionsCollection.zig
@@ -0,0 +1,106 @@
+// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+const std = @import("std");
+
+const js = @import("../../js/js.zig");
+const Page = @import("../../Page.zig");
+const Element = @import("../Element.zig");
+const HTMLCollection = @import("HTMLCollection.zig");
+const NodeLive = @import("node_live.zig").NodeLive;
+
+const HTMLOptionsCollection = @This();
+
+_proto: *HTMLCollection,
+_select: *@import("../element/html/Select.zig"),
+
+pub fn deinit(self: *HTMLOptionsCollection) void {
+ const page = Page.current;
+ page._factory.destroy(self);
+}
+
+// Forward length to HTMLCollection
+pub fn length(self: *HTMLOptionsCollection, page: *Page) u32 {
+ return self._proto.length(page);
+}
+
+// Forward indexed access to HTMLCollection
+pub fn getAtIndex(self: *HTMLOptionsCollection, index: usize, page: *Page) ?*Element {
+ return self._proto.getAtIndex(index, page);
+}
+
+pub fn getByName(self: *HTMLOptionsCollection, name: []const u8, page: *Page) ?*Element {
+ return self._proto.getByName(name, page);
+}
+
+// Forward selectedIndex to the owning select element
+pub fn getSelectedIndex(self: *const HTMLOptionsCollection) i32 {
+ return self._select.getSelectedIndex();
+}
+
+pub fn setSelectedIndex(self: *HTMLOptionsCollection, index: i32) !void {
+ return self._select.setSelectedIndex(index);
+}
+
+const Option = @import("../element/html/Option.zig");
+
+// Add a new option element
+pub fn add(self: *HTMLOptionsCollection, element: *Option, before: ?*Option, 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);
+ }
+}
+
+// Remove an option element by index
+pub fn remove(self: *HTMLOptionsCollection, index: i32, page: *Page) void {
+ if (index < 0) {
+ return;
+ }
+
+ if (self._proto.getAtIndex(@intCast(index), page)) |element| {
+ element.remove(page);
+ }
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(HTMLOptionsCollection);
+
+ pub const Meta = struct {
+ pub const name = "HTMLOptionsCollection";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ pub const finalizer = HTMLOptionsCollection.deinit;
+ pub const manage = false;
+ };
+
+ pub const length = bridge.accessor(HTMLOptionsCollection.length, null, .{});
+
+ // Indexed access
+ pub const @"[int]" = bridge.indexed(HTMLOptionsCollection.getAtIndex, .{ .null_as_undefined = true });
+ pub const @"[str]" = bridge.namedIndexed(HTMLOptionsCollection.getByName, null, null, .{ .null_as_undefined = true });
+
+ pub const selectedIndex = bridge.accessor(HTMLOptionsCollection.getSelectedIndex, HTMLOptionsCollection.setSelectedIndex, .{});
+ pub const add = bridge.function(HTMLOptionsCollection.add, .{});
+ pub const remove = bridge.function(HTMLOptionsCollection.remove, .{});
+};
diff --git a/src/browser/webapi/collections/node_live.zig b/src/browser/webapi/collections/node_live.zig
index 45eea51c..ee123b4c 100644
--- a/src/browser/webapi/collections/node_live.zig
+++ b/src/browser/webapi/collections/node_live.zig
@@ -36,6 +36,8 @@ const Mode = enum {
tag_name,
class_name,
child_elements,
+ child_tag,
+ selected_options,
};
const Filters = union(Mode) {
@@ -43,6 +45,8 @@ const Filters = union(Mode) {
tag_name: String,
class_name: []const u8,
child_elements,
+ child_tag: Element.Tag,
+ selected_options,
fn TypeOf(comptime mode: Mode) type {
@setEvalBranchQuota(2000);
@@ -71,7 +75,7 @@ pub fn NodeLive(comptime mode: Mode) type {
const Filter = Filters.TypeOf(mode);
const TW = switch (mode) {
.tag, .tag_name, .class_name => TreeWalker.FullExcludeSelf,
- .child_elements => TreeWalker.Children,
+ .child_elements, .child_tag, .selected_options => TreeWalker.Children,
};
return struct {
_tw: TW,
@@ -213,6 +217,16 @@ pub fn NodeLive(comptime mode: Mode) type {
return Selector.classAttributeContains(class_attr, self._filter);
},
.child_elements => return node._type == .element,
+ .child_tag => {
+ const el = node.is(Element) orelse return false;
+ return el.getTag() == self._filter;
+ },
+ .selected_options => {
+ const el = node.is(Element) orelse return false;
+ const Option = Element.Html.Option;
+ const opt = el.is(Option) orelse return false;
+ return opt.getSelected();
+ },
}
}
@@ -236,6 +250,8 @@ pub fn NodeLive(comptime mode: Mode) type {
.tag_name => HTMLCollection{ .data = .{ .tag_name = self } },
.class_name => HTMLCollection{ .data = .{ .class_name = self } },
.child_elements => HTMLCollection{ .data = .{ .child_elements = self } },
+ .child_tag => HTMLCollection{ .data = .{ .child_tag = self } },
+ .selected_options => HTMLCollection{ .data = .{ .selected_options = self } },
};
return page._factory.create(collection);
}
diff --git a/src/browser/webapi/element/Attribute.zig b/src/browser/webapi/element/Attribute.zig
index 66357754..3d37f173 100644
--- a/src/browser/webapi/element/Attribute.zig
+++ b/src/browser/webapi/element/Attribute.zig
@@ -174,7 +174,7 @@ pub const List = struct {
if (is_id) {
try page.document._elements_by_id.put(page.arena, entry._value.str(), element);
}
- page.attributeChange(element, result.normalized, value);
+ page.attributeChange(element, result.normalized, entry._value.str());
return entry;
}
diff --git a/src/browser/webapi/element/html/Button.zig b/src/browser/webapi/element/html/Button.zig
index 2e1a4016..acf076e6 100644
--- a/src/browser/webapi/element/html/Button.zig
+++ b/src/browser/webapi/element/html/Button.zig
@@ -50,6 +50,26 @@ pub fn setDisabled(self: *Button, disabled: bool, page: *Page) !void {
}
}
+pub fn getName(self: *const Button) []const u8 {
+ return self.asConstElement().getAttributeSafe("name") orelse "";
+}
+
+pub fn setName(self: *Button, name: []const u8, page: *Page) !void {
+ try self.asElement().setAttributeSafe("name", name, page);
+}
+
+pub fn getRequired(self: *const Button) bool {
+ return self.asConstElement().getAttributeSafe("required") != null;
+}
+
+pub fn setRequired(self: *Button, required: bool, page: *Page) !void {
+ if (required) {
+ try self.asElement().setAttributeSafe("required", "", page);
+ } else {
+ try self.asElement().removeAttribute("required", page);
+ }
+}
+
pub fn getForm(self: *Button, page: *Page) ?*Form {
const element = self.asElement();
@@ -84,6 +104,8 @@ pub const JsApi = struct {
};
pub const disabled = bridge.accessor(Button.getDisabled, Button.setDisabled, .{});
+ pub const name = bridge.accessor(Button.getName, Button.setName, .{});
+ pub const required = bridge.accessor(Button.getRequired, Button.setRequired, .{});
pub const form = bridge.accessor(Button.getForm, null, .{});
};
diff --git a/src/browser/webapi/element/html/Input.zig b/src/browser/webapi/element/html/Input.zig
index d805ba6f..9c4593a9 100644
--- a/src/browser/webapi/element/html/Input.zig
+++ b/src/browser/webapi/element/html/Input.zig
@@ -109,6 +109,10 @@ pub fn getDefaultValue(self: *const Input) []const u8 {
return self._default_value orelse "";
}
+pub fn setDefaultValue(self: *Input, value: []const u8, page: *Page) !void {
+ try self.asElement().setAttributeSafe("value", value, page);
+}
+
pub fn getChecked(self: *const Input) bool {
return self._checked;
}
@@ -126,6 +130,14 @@ pub fn getDefaultChecked(self: *const Input) bool {
return self._default_checked;
}
+pub fn setDefaultChecked(self: *Input, checked: bool, page: *Page) !void {
+ if (checked) {
+ try self.asElement().setAttributeSafe("checked", "", page);
+ } else {
+ try self.asElement().removeAttribute("checked", page);
+ }
+}
+
pub fn getDisabled(self: *const Input) bool {
// TODO: Also check for disabled fieldset ancestors
// (but not if we're inside a