improve form element support

This commit is contained in:
Karl Seguin
2025-11-14 17:56:09 +08:00
parent 04f719c33c
commit 5ae74d6924
16 changed files with 975 additions and 50 deletions

View File

@@ -53,3 +53,76 @@
const buttonInvalidFormAttr = $('#button_invalid_form_attr') const buttonInvalidFormAttr = $('#button_invalid_form_attr')
testing.expectEqual(null, buttonInvalidFormAttr.form) testing.expectEqual(null, buttonInvalidFormAttr.form)
</script> </script>
<button id="named1" name="submit-btn"></button>
<button id="named2"></button>
<button id="required1" required></button>
<button id="required2"></button>
<script id="name_initial">
testing.expectEqual('submit-btn', $('#named1').name)
testing.expectEqual('', $('#named2').name)
</script>
<script id="name_set">
{
const button = document.createElement('button')
testing.expectEqual('', button.name)
button.name = 'action'
testing.expectEqual('action', button.name)
testing.expectEqual('action', button.getAttribute('name'))
button.name = 'submit'
testing.expectEqual('submit', button.name)
testing.expectEqual('submit', button.getAttribute('name'))
}
</script>
<script id="name_reflects_to_attribute">
{
const button = document.createElement('button')
testing.expectEqual(null, button.getAttribute('name'))
button.name = 'fieldname'
testing.expectEqual('fieldname', button.getAttribute('name'))
testing.expectTrue(button.outerHTML.includes('name="fieldname"'))
}
</script>
<script id="required_initial">
testing.expectEqual(true, $('#required1').required)
testing.expectEqual(false, $('#required2').required)
</script>
<script id="required_set">
{
const button = document.createElement('button')
testing.expectEqual(false, button.required)
button.required = true
testing.expectEqual(true, button.required)
testing.expectEqual('', button.getAttribute('required'))
button.required = false
testing.expectEqual(false, button.required)
testing.expectEqual(null, button.getAttribute('required'))
}
</script>
<script id="required_reflects_to_attribute">
{
const button = document.createElement('button')
testing.expectEqual(null, button.getAttribute('required'))
testing.expectFalse(button.outerHTML.includes('required'))
button.required = true
testing.expectEqual('', button.getAttribute('required'))
testing.expectTrue(button.outerHTML.includes('required'))
button.required = false
testing.expectEqual(null, button.getAttribute('required'))
testing.expectFalse(button.outerHTML.includes('required'))
}
</script>

View File

@@ -15,7 +15,7 @@
<input id="disabled1" type="text" disabled> <input id="disabled1" type="text" disabled>
<input id="disabled2" type="checkbox"> <input id="disabled2" type="checkbox">
<!--
<script id="type"> <script id="type">
testing.expectEqual('text', $('#text1').type) testing.expectEqual('text', $('#text1').type)
testing.expectEqual('text', $('#text2').type) testing.expectEqual('text', $('#text2').type)
@@ -243,4 +243,109 @@
testing.expectEqual('', input.getAttribute('checked')) testing.expectEqual('', input.getAttribute('checked'))
testing.expectTrue(input.outerHTML.includes('checked')) testing.expectTrue(input.outerHTML.includes('checked'))
} }
</script> -->
<script id="defaultValue_set">
{
const input = document.createElement('input')
testing.expectEqual('', input.defaultValue)
testing.expectEqual(null, input.getAttribute('value'))
input.defaultValue = 'new default'
testing.expectEqual('new default', input.defaultValue)
testing.expectEqual('new default', input.getAttribute('value'))
testing.expectEqual('new default', input.value)
}
</script> </script>
<!-- <script id="defaultChecked_set">
{
const input = document.createElement('input')
input.type = 'checkbox'
testing.expectEqual(false, input.defaultChecked)
testing.expectEqual(null, input.getAttribute('checked'))
input.defaultChecked = true
testing.expectEqual(true, input.defaultChecked)
testing.expectEqual('', input.getAttribute('checked'))
testing.expectEqual(true, input.checked)
input.checked = false
testing.expectEqual(false, input.checked)
testing.expectEqual(true, input.defaultChecked)
}
</script>
<input id="named1" type="text" name="username">
<input id="named2" type="text">
<input id="required1" type="text" required>
<input id="required2" type="text">
<script id="name_initial">
testing.expectEqual('username', $('#named1').name)
testing.expectEqual('', $('#named2').name)
</script>
<script id="name_set">
{
const input = document.createElement('input')
testing.expectEqual('', input.name)
input.name = 'email'
testing.expectEqual('email', input.name)
testing.expectEqual('email', input.getAttribute('name'))
input.name = 'password'
testing.expectEqual('password', input.name)
testing.expectEqual('password', input.getAttribute('name'))
}
</script>
<script id="name_reflects_to_attribute">
{
const input = document.createElement('input')
testing.expectEqual(null, input.getAttribute('name'))
input.name = 'fieldname'
testing.expectEqual('fieldname', input.getAttribute('name'))
testing.expectTrue(input.outerHTML.includes('name="fieldname"'))
}
</script>
<script id="required_initial">
testing.expectEqual(true, $('#required1').required)
testing.expectEqual(false, $('#required2').required)
</script>
<script id="required_set">
{
const input = document.createElement('input')
testing.expectEqual(false, input.required)
input.required = true
testing.expectEqual(true, input.required)
testing.expectEqual('', input.getAttribute('required'))
input.required = false
testing.expectEqual(false, input.required)
testing.expectEqual(null, input.getAttribute('required'))
}
</script>
<script id="required_reflects_to_attribute">
{
const input = document.createElement('input')
testing.expectEqual(null, input.getAttribute('required'))
testing.expectFalse(input.outerHTML.includes('required'))
input.required = true
testing.expectEqual('', input.getAttribute('required'))
testing.expectTrue(input.outerHTML.includes('required'))
input.required = false
testing.expectEqual(null, input.getAttribute('required'))
testing.expectFalse(input.outerHTML.includes('required'))
}
</script>
-->

View File

@@ -65,3 +65,37 @@
$('#opt4').disabled = false $('#opt4').disabled = false
testing.expectEqual(false, $('#opt4').disabled) testing.expectEqual(false, $('#opt4').disabled)
</script> </script>
<option id="named1" name="choice1">Named option</option>
<option id="named2">Unnamed option</option>
<script id="name_initial">
testing.expectEqual('choice1', $('#named1').name)
testing.expectEqual('', $('#named2').name)
</script>
<script id="name_set">
{
const option = document.createElement('option')
testing.expectEqual('', option.name)
option.name = 'opt-name'
testing.expectEqual('opt-name', option.name)
testing.expectEqual('opt-name', option.getAttribute('name'))
option.name = 'another-name'
testing.expectEqual('another-name', option.name)
testing.expectEqual('another-name', option.getAttribute('name'))
}
</script>
<script id="name_reflects_to_attribute">
{
const option = document.createElement('option')
testing.expectEqual(null, option.getAttribute('name'))
option.name = 'fieldname'
testing.expectEqual('fieldname', option.getAttribute('name'))
testing.expectTrue(option.outerHTML.includes('name="fieldname"'))
}
</script>

View File

@@ -81,3 +81,286 @@
const selectNoForm = $('#select_no_form') const selectNoForm = $('#select_no_form')
testing.expectEqual(null, selectNoForm.form) testing.expectEqual(null, selectNoForm.form)
</script> </script>
<script id="selectedIndex_initial">
{
testing.expectEqual(2, $('#select1').selectedIndex)
testing.expectEqual(0, $('#select2').selectedIndex)
}
</script>
<script id="selectedIndex_set">
{
$('#select1').selectedIndex = 2
testing.expectEqual(2, $('#select1').selectedIndex)
testing.expectEqual('val3', $('#select1').value)
const opt3 = $('#select1').querySelector('option[value="val3"]')
testing.expectEqual(true, opt3.selected)
const opt2 = $('#select1').querySelector('option[value="val2"]')
testing.expectEqual(false, opt2.selected)
}
</script>
<script id="selectedIndex_set_negative">
{
$('#select1').selectedIndex = -1
testing.expectEqual(-1, $('#select1').selectedIndex)
const opt1 = $('#select1').querySelector('option[value="val1"]')
const opt2 = $('#select1').querySelector('option[value="val2"]')
const opt3 = $('#select1').querySelector('option[value="val3"]')
testing.expectEqual(false, opt1.selected)
testing.expectEqual(false, opt2.selected)
testing.expectEqual(false, opt3.selected)
}
</script>
<script id="selectedIndex_set_out_of_bounds">
{
$('#select2').selectedIndex = 999
const optA = $('#select2').querySelector('option[value="a"]')
const optB = $('#select2').querySelector('option[value="b"]')
testing.expectEqual(false, optA.selected)
testing.expectEqual(false, optB.selected)
}
</script>
<select id="select_multi" multiple>
<option value="opt1">Option 1</option>
<option value="opt2" selected>Option 2</option>
<option value="opt3">Option 3</option>
</select>
<script id="multiple_attribute">
{
const sel = $('#select_multi')
testing.expectEqual(true, sel.multiple)
testing.expectEqual('opt2', sel.value)
testing.expectEqual(1, sel.selectedIndex)
}
</script>
<script id="multiple_setValue">
{
const sel = $('#select_multi')
sel.value = 'opt1'
const opt1 = sel.querySelector('option[value="opt1"]')
const opt2 = sel.querySelector('option[value="opt2"]')
testing.expectEqual(true, opt1.selected)
testing.expectEqual(false, opt2.selected)
}
</script>
<script id="options_collection">
{
const sel = $('#select1')
const opts = sel.options
testing.expectEqual(3, opts.length)
testing.expectEqual('HTMLOptionsCollection', opts.constructor.name)
// Test indexed access
testing.expectEqual('val1', opts[0].value)
testing.expectEqual('val2', opts[1].value)
testing.expectEqual('val3', opts[2].value)
}
</script>
<script id="options_selectedIndex">
{
// Create a fresh select to avoid state from previous tests
const sel = document.createElement('select')
sel.innerHTML = '<option value="a">A</option><option value="b" selected>B</option><option value="c">C</option>'
const opts = sel.options
// selectedIndex should forward to the select element
testing.expectEqual(1, sel.selectedIndex)
testing.expectEqual(1, opts.selectedIndex)
// Setting via options collection should update the select
opts.selectedIndex = 2
testing.expectEqual(2, sel.selectedIndex)
testing.expectEqual(2, opts.selectedIndex)
}
</script>
<script id="options_add_remove">
{
const sel = document.createElement('select')
const opts = sel.options
testing.expectEqual(0, opts.length)
// Add an option
const opt1 = document.createElement('option')
opt1.value = 'a'
opt1.textContent = 'Option A'
opts.add(opt1, null)
testing.expectEqual(1, opts.length)
testing.expectEqual('a', opts[0].value)
// Add another option
const opt2 = document.createElement('option')
opt2.value = 'b'
opt2.textContent = 'Option B'
opts.add(opt2, null)
testing.expectEqual(2, opts.length)
testing.expectEqual('b', opts[1].value)
// Add an option before the first one
const opt0 = document.createElement('option')
opt0.value = 'zero'
opt0.textContent = 'Option Zero'
opts.add(opt0, opt1)
testing.expectEqual(3, opts.length)
testing.expectEqual('zero', opts[0].value)
testing.expectEqual('a', opts[1].value)
testing.expectEqual('b', opts[2].value)
// Remove the middle option (index 1, which is 'a')
opts.remove(1)
testing.expectEqual(2, opts.length)
testing.expectEqual('zero', opts[0].value)
testing.expectEqual('b', opts[1].value)
}
</script>
<script id="selectedOptions">
{
const sel = document.createElement('select')
sel.multiple = true
sel.innerHTML = '<option value="a">A</option><option value="b" selected>B</option><option value="c" selected>C</option><option value="d">D</option>'
const selectedOpts = sel.selectedOptions
testing.expectEqual('HTMLCollection', selectedOpts.constructor.name)
testing.expectEqual(2, selectedOpts.length)
testing.expectEqual('b', selectedOpts[0].value)
testing.expectEqual('c', selectedOpts[1].value)
// Deselect one
sel.options[1].selected = false
testing.expectEqual(1, selectedOpts.length)
testing.expectEqual('c', selectedOpts[0].value)
// Select another
sel.options[3].selected = true
testing.expectEqual(2, selectedOpts.length)
testing.expectEqual('c', selectedOpts[0].value)
testing.expectEqual('d', selectedOpts[1].value)
}
</script>
<select id="named1" name="country"></select>
<select id="named2"></select>
<select id="required1" required></select>
<select id="required2"></select>
<script id="name_initial">
testing.expectEqual('country', $('#named1').name)
testing.expectEqual('', $('#named2').name)
</script>
<script id="name_set">
{
const select = document.createElement('select')
testing.expectEqual('', select.name)
select.name = 'choices'
testing.expectEqual('choices', select.name)
testing.expectEqual('choices', select.getAttribute('name'))
select.name = 'options'
testing.expectEqual('options', select.name)
testing.expectEqual('options', select.getAttribute('name'))
}
</script>
<script id="name_reflects_to_attribute">
{
const select = document.createElement('select')
testing.expectEqual(null, select.getAttribute('name'))
select.name = 'fieldname'
testing.expectEqual('fieldname', select.getAttribute('name'))
testing.expectTrue(select.outerHTML.includes('name="fieldname"'))
}
</script>
<script id="required_initial">
testing.expectEqual(true, $('#required1').required)
testing.expectEqual(false, $('#required2').required)
</script>
<script id="required_set">
{
const select = document.createElement('select')
testing.expectEqual(false, select.required)
select.required = true
testing.expectEqual(true, select.required)
testing.expectEqual('', select.getAttribute('required'))
select.required = false
testing.expectEqual(false, select.required)
testing.expectEqual(null, select.getAttribute('required'))
}
</script>
<script id="required_reflects_to_attribute">
{
const select = document.createElement('select')
testing.expectEqual(null, select.getAttribute('required'))
testing.expectFalse(select.outerHTML.includes('required'))
select.required = true
testing.expectEqual('', select.getAttribute('required'))
testing.expectTrue(select.outerHTML.includes('required'))
select.required = false
testing.expectEqual(null, select.getAttribute('required'))
testing.expectFalse(select.outerHTML.includes('required'))
}
</script>
<select id="sized1" size="5"></select>
<select id="sized2" size=" 932 asd"></select>
<select id="sized3" size="93abc"></select>
<select id="sized4" size="nope"></select>
<select id="sized5"></select>
<script id="size_initial">
testing.expectEqual(5, $('#sized1').size)
testing.expectEqual(932, $('#sized2').size)
testing.expectEqual(93, $('#sized3').size)
testing.expectEqual(0, $('#sized4').size)
testing.expectEqual(0, $('#sized5').size)
</script>
<script id="size_set">
{
const select = document.createElement('select')
testing.expectEqual(0, select.size)
select.size = 10
testing.expectEqual(10, select.size)
testing.expectEqual('10', select.getAttribute('size'))
select.size = 42
testing.expectEqual(42, select.size)
testing.expectEqual('42', select.getAttribute('size'))
}
</script>
<script id="size_reflects_to_attribute">
{
const select = document.createElement('select')
testing.expectEqual(null, select.getAttribute('size'))
select.size = 7
testing.expectEqual('7', select.getAttribute('size'))
testing.expectTrue(select.outerHTML.includes('size="7"'))
}
</script>

View File

@@ -76,3 +76,76 @@
const textareaInvalidFormAttr = $('#textarea_invalid_form_attr') const textareaInvalidFormAttr = $('#textarea_invalid_form_attr')
testing.expectEqual(null, textareaInvalidFormAttr.form) testing.expectEqual(null, textareaInvalidFormAttr.form)
</script> </script>
<textarea id="named1" name="comments"></textarea>
<textarea id="named2"></textarea>
<textarea id="required1" required></textarea>
<textarea id="required2"></textarea>
<script id="name_initial">
testing.expectEqual('comments', $('#named1').name)
testing.expectEqual('', $('#named2').name)
</script>
<script id="name_set">
{
const textarea = document.createElement('textarea')
testing.expectEqual('', textarea.name)
textarea.name = 'message'
testing.expectEqual('message', textarea.name)
testing.expectEqual('message', textarea.getAttribute('name'))
textarea.name = 'feedback'
testing.expectEqual('feedback', textarea.name)
testing.expectEqual('feedback', textarea.getAttribute('name'))
}
</script>
<script id="name_reflects_to_attribute">
{
const textarea = document.createElement('textarea')
testing.expectEqual(null, textarea.getAttribute('name'))
textarea.name = 'fieldname'
testing.expectEqual('fieldname', textarea.getAttribute('name'))
testing.expectTrue(textarea.outerHTML.includes('name="fieldname"'))
}
</script>
<script id="required_initial">
testing.expectEqual(true, $('#required1').required)
testing.expectEqual(false, $('#required2').required)
</script>
<script id="required_set">
{
const textarea = document.createElement('textarea')
testing.expectEqual(false, textarea.required)
textarea.required = true
testing.expectEqual(true, textarea.required)
testing.expectEqual('', textarea.getAttribute('required'))
textarea.required = false
testing.expectEqual(false, textarea.required)
testing.expectEqual(null, textarea.getAttribute('required'))
}
</script>
<script id="required_reflects_to_attribute">
{
const textarea = document.createElement('textarea')
testing.expectEqual(null, textarea.getAttribute('required'))
testing.expectFalse(textarea.outerHTML.includes('required'))
textarea.required = true
testing.expectEqual('', textarea.getAttribute('required'))
testing.expectTrue(textarea.outerHTML.includes('required'))
textarea.required = false
testing.expectEqual(null, textarea.getAttribute('required'))
testing.expectFalse(textarea.outerHTML.includes('required'))
}
</script>

View File

@@ -407,6 +407,7 @@ pub fn replaceChildren(self: *Element, nodes: []const Node.NodeOrText, page: *Pa
} }
pub fn remove(self: *Element, page: *Page) void { pub fn remove(self: *Element, page: *Page) void {
page.domChanged();
const node = self.asNode(); const node = self.asNode();
const parent = node._parent orelse return; const parent = node._parent orelse return;
page.removeNode(parent, node, .{ .will_be_reconnected = false }); page.removeNode(parent, node, .{ .will_be_reconnected = false });

View File

@@ -20,6 +20,7 @@ pub const NodeLive = @import("collections/node_live.zig").NodeLive;
pub const ChildNodes = @import("collections/ChildNodes.zig"); pub const ChildNodes = @import("collections/ChildNodes.zig");
pub const DOMTokenList = @import("collections/DOMTokenList.zig"); pub const DOMTokenList = @import("collections/DOMTokenList.zig");
pub const HTMLAllCollection = @import("collections/HTMLAllCollection.zig"); pub const HTMLAllCollection = @import("collections/HTMLAllCollection.zig");
pub const HTMLOptionsCollection = @import("collections/HTMLOptionsCollection.zig");
pub fn registerTypes() []const type { pub fn registerTypes() []const type {
return &.{ return &.{
@@ -31,6 +32,7 @@ pub fn registerTypes() []const type {
@import("collections/NodeList.zig").EntryIterator, @import("collections/NodeList.zig").EntryIterator,
@import("collections/HTMLAllCollection.zig"), @import("collections/HTMLAllCollection.zig"),
@import("collections/HTMLAllCollection.zig").Iterator, @import("collections/HTMLAllCollection.zig").Iterator,
HTMLOptionsCollection,
DOMTokenList, DOMTokenList,
DOMTokenList.Iterator, DOMTokenList.Iterator,
}; };

View File

@@ -29,6 +29,8 @@ const Mode = enum {
tag_name, tag_name,
class_name, class_name,
child_elements, child_elements,
child_tag,
selected_options,
}; };
const HTMLCollection = @This(); const HTMLCollection = @This();
@@ -38,6 +40,8 @@ data: union(Mode) {
tag_name: NodeLive(.tag_name), tag_name: NodeLive(.tag_name),
class_name: NodeLive(.class_name), class_name: NodeLive(.class_name),
child_elements: NodeLive(.child_elements), child_elements: NodeLive(.child_elements),
child_tag: NodeLive(.child_tag),
selected_options: NodeLive(.selected_options),
}, },
pub fn length(self: *HTMLCollection, page: *const Page) u32 { 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() }, .tag_name => |*impl| .{ .tag_name = impl._tw.clone() },
.class_name => |*impl| .{ .class_name = impl._tw.clone() }, .class_name => |*impl| .{ .class_name = impl._tw.clone() },
.child_elements => |*impl| .{ .child_elements = 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); }, page);
} }
@@ -78,6 +84,8 @@ pub const Iterator = GenericIterator(struct {
tag_name: TreeWalker.FullExcludeSelf, tag_name: TreeWalker.FullExcludeSelf,
class_name: TreeWalker.FullExcludeSelf, class_name: TreeWalker.FullExcludeSelf,
child_elements: TreeWalker.Children, child_elements: TreeWalker.Children,
child_tag: TreeWalker.Children,
selected_options: TreeWalker.Children,
}, },
pub fn next(self: *@This(), _: *Page) ?*Element { 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), .tag_name => |*impl| impl.nextTw(&self.tw.tag_name),
.class_name => |*impl| impl.nextTw(&self.tw.class_name), .class_name => |*impl| impl.nextTw(&self.tw.class_name),
.child_elements => |*impl| impl.nextTw(&self.tw.child_elements), .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); }, null);

View File

@@ -0,0 +1,106 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
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, .{});
};

View File

@@ -36,6 +36,8 @@ const Mode = enum {
tag_name, tag_name,
class_name, class_name,
child_elements, child_elements,
child_tag,
selected_options,
}; };
const Filters = union(Mode) { const Filters = union(Mode) {
@@ -43,6 +45,8 @@ const Filters = union(Mode) {
tag_name: String, tag_name: String,
class_name: []const u8, class_name: []const u8,
child_elements, child_elements,
child_tag: Element.Tag,
selected_options,
fn TypeOf(comptime mode: Mode) type { fn TypeOf(comptime mode: Mode) type {
@setEvalBranchQuota(2000); @setEvalBranchQuota(2000);
@@ -71,7 +75,7 @@ pub fn NodeLive(comptime mode: Mode) type {
const Filter = Filters.TypeOf(mode); const Filter = Filters.TypeOf(mode);
const TW = switch (mode) { const TW = switch (mode) {
.tag, .tag_name, .class_name => TreeWalker.FullExcludeSelf, .tag, .tag_name, .class_name => TreeWalker.FullExcludeSelf,
.child_elements => TreeWalker.Children, .child_elements, .child_tag, .selected_options => TreeWalker.Children,
}; };
return struct { return struct {
_tw: TW, _tw: TW,
@@ -213,6 +217,16 @@ pub fn NodeLive(comptime mode: Mode) type {
return Selector.classAttributeContains(class_attr, self._filter); return Selector.classAttributeContains(class_attr, self._filter);
}, },
.child_elements => return node._type == .element, .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 } }, .tag_name => HTMLCollection{ .data = .{ .tag_name = self } },
.class_name => HTMLCollection{ .data = .{ .class_name = self } }, .class_name => HTMLCollection{ .data = .{ .class_name = self } },
.child_elements => HTMLCollection{ .data = .{ .child_elements = 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); return page._factory.create(collection);
} }

View File

@@ -174,7 +174,7 @@ pub const List = struct {
if (is_id) { if (is_id) {
try page.document._elements_by_id.put(page.arena, entry._value.str(), element); 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; return entry;
} }

View File

@@ -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 { pub fn getForm(self: *Button, page: *Page) ?*Form {
const element = self.asElement(); const element = self.asElement();
@@ -84,6 +104,8 @@ pub const JsApi = struct {
}; };
pub const disabled = bridge.accessor(Button.getDisabled, Button.setDisabled, .{}); 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, .{}); pub const form = bridge.accessor(Button.getForm, null, .{});
}; };

View File

@@ -109,6 +109,10 @@ pub fn getDefaultValue(self: *const Input) []const u8 {
return self._default_value orelse ""; 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 { pub fn getChecked(self: *const Input) bool {
return self._checked; return self._checked;
} }
@@ -126,6 +130,14 @@ pub fn getDefaultChecked(self: *const Input) bool {
return self._default_checked; 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 { pub fn getDisabled(self: *const Input) bool {
// TODO: Also check for disabled fieldset ancestors // TODO: Also check for disabled fieldset ancestors
// (but not if we're inside a <legend> of that fieldset) // (but not if we're inside a <legend> of that fieldset)
@@ -140,6 +152,26 @@ pub fn setDisabled(self: *Input, disabled: bool, page: *Page) !void {
} }
} }
pub fn getName(self: *const Input) []const u8 {
return self.asConstElement().getAttributeSafe("name") orelse "";
}
pub fn setName(self: *Input, name: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("name", name, page);
}
pub fn getRequired(self: *const Input) bool {
return self.asConstElement().getAttributeSafe("required") != null;
}
pub fn setRequired(self: *Input, required: bool, page: *Page) !void {
if (required) {
try self.asElement().setAttributeSafe("required", "", page);
} else {
try self.asElement().removeAttribute("required", page);
}
}
pub fn getForm(self: *Input, page: *Page) ?*Form { pub fn getForm(self: *Input, page: *Page) ?*Form {
const element = self.asElement(); const element = self.asElement();
@@ -218,10 +250,12 @@ pub const JsApi = struct {
pub const @"type" = bridge.accessor(Input.getType, Input.setType, .{}); pub const @"type" = bridge.accessor(Input.getType, Input.setType, .{});
pub const value = bridge.accessor(Input.getValue, Input.setValue, .{}); pub const value = bridge.accessor(Input.getValue, Input.setValue, .{});
pub const defaultValue = bridge.accessor(Input.getDefaultValue, null, .{}); pub const defaultValue = bridge.accessor(Input.getDefaultValue, Input.setDefaultValue, .{});
pub const checked = bridge.accessor(Input.getChecked, Input.setChecked, .{}); pub const checked = bridge.accessor(Input.getChecked, Input.setChecked, .{});
pub const defaultChecked = bridge.accessor(Input.getDefaultChecked, null, .{}); pub const defaultChecked = bridge.accessor(Input.getDefaultChecked, Input.setDefaultChecked, .{});
pub const disabled = bridge.accessor(Input.getDisabled, Input.setDisabled, .{}); pub const disabled = bridge.accessor(Input.getDisabled, Input.setDisabled, .{});
pub const name = bridge.accessor(Input.getName, Input.setName, .{});
pub const required = bridge.accessor(Input.getRequired, Input.setRequired, .{});
pub const form = bridge.accessor(Input.getForm, null, .{}); pub const form = bridge.accessor(Input.getForm, null, .{});
}; };
@@ -249,13 +283,20 @@ pub const Build = struct {
} }
} }
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: *Page) !void {
const attribute = std.meta.stringToEnum(enum { type, value, checked }, name) orelse return; const attribute = std.meta.stringToEnum(enum { type, value, checked }, name) orelse return;
const self = element.as(Input); const self = element.as(Input);
switch (attribute) { switch (attribute) {
.type => self._input_type = Type.fromString(value), .type => self._input_type = Type.fromString(value),
.value => self._default_value = value, .value => self._default_value = value,
.checked => self._default_checked = true, .checked => {
self._default_checked = true;
self._checked = true;
// If setting a radio button to checked, uncheck others in the group
if (self._input_type == .radio) {
try self.uncheckRadioGroup(page);
}
},
} }
} }
@@ -265,7 +306,10 @@ pub const Build = struct {
switch (attribute) { switch (attribute) {
.type => self._input_type = .text, .type => self._input_type = .text,
.value => self._default_value = null, .value => self._default_value = null,
.checked => self._default_checked = false, .checked => {
self._default_checked = false;
self._checked = false;
},
} }
} }
}; };

View File

@@ -16,6 +16,7 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
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");
@@ -35,6 +36,9 @@ _disabled: bool = false,
pub fn asElement(self: *Option) *Element { pub fn asElement(self: *Option) *Element {
return self._proto._proto; return self._proto._proto;
} }
pub fn asConstElement(self: *const Option) *const Element {
return self._proto._proto;
}
pub fn asNode(self: *Option) *Node { pub fn asNode(self: *Option) *Node {
return self.asElement().asNode(); return self.asElement().asNode();
} }
@@ -45,7 +49,7 @@ pub fn getValue(self: *const Option) []const u8 {
} }
pub fn setValue(self: *Option, value: []const u8, page: *Page) !void { pub fn setValue(self: *Option, value: []const u8, page: *Page) !void {
const owned = try page.arena.dupe(u8, value); const owned = try page.dupeString(value);
try self.asElement().setAttributeSafe("value", owned, page); try self.asElement().setAttributeSafe("value", owned, page);
self._value = owned; self._value = owned;
} }
@@ -59,10 +63,10 @@ pub fn getSelected(self: *const Option) bool {
} }
pub fn setSelected(self: *Option, selected: bool, page: *Page) !void { pub fn setSelected(self: *Option, selected: bool, page: *Page) !void {
_ = page;
// TODO: When setting selected=true, may need to unselect other options // TODO: When setting selected=true, may need to unselect other options
// in the parent <select> if it doesn't have multiple attribute // in the parent <select> if it doesn't have multiple attribute
self._selected = selected; self._selected = selected;
page.domChanged();
} }
pub fn getDefaultSelected(self: *const Option) bool { pub fn getDefaultSelected(self: *const Option) bool {
@@ -82,6 +86,14 @@ pub fn setDisabled(self: *Option, disabled: bool, page: *Page) !void {
} }
} }
pub fn getName(self: *const Option) []const u8 {
return self.asConstElement().getAttributeSafe("name") orelse "";
}
pub fn setName(self: *Option, name: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("name", name, page);
}
pub const JsApi = struct { pub const JsApi = struct {
pub const bridge = js.Bridge(Option); pub const bridge = js.Bridge(Option);
@@ -96,6 +108,7 @@ pub const JsApi = struct {
pub const selected = bridge.accessor(Option.getSelected, Option.setSelected, .{}); pub const selected = bridge.accessor(Option.getSelected, Option.setSelected, .{});
pub const defaultSelected = bridge.accessor(Option.getDefaultSelected, null, .{}); pub const defaultSelected = bridge.accessor(Option.getDefaultSelected, null, .{});
pub const disabled = bridge.accessor(Option.getDisabled, Option.setDisabled, .{}); pub const disabled = bridge.accessor(Option.getDisabled, Option.setDisabled, .{});
pub const name = bridge.accessor(Option.getName, Option.setName, .{});
}; };
pub const Build = struct { pub const Build = struct {
@@ -126,6 +139,30 @@ pub const Build = struct {
} }
} }
} }
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);
switch (attribute) {
.value => self._value = value,
.selected => {
self._default_selected = true;
self._selected = true;
},
}
}
pub fn attributeRemove(element: *Element, name: []const u8, _: *Page) !void {
const attribute = std.meta.stringToEnum(enum { value, selected }, name) orelse return;
const self = element.as(Option);
switch (attribute) {
.value => self._value = null,
.selected => {
self._default_selected = false;
self._selected = false;
},
}
}
}; };
const testing = @import("../../../../testing.zig"); const testing = @import("../../../../testing.zig");

View File

@@ -22,12 +22,14 @@ const Page = @import("../../../Page.zig");
const Node = @import("../../Node.zig"); const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig"); const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig"); const HtmlElement = @import("../Html.zig");
const collections = @import("../../collections.zig");
const Form = @import("Form.zig"); const Form = @import("Form.zig");
const Option = @import("Option.zig"); const Option = @import("Option.zig");
const Select = @This(); const Select = @This();
_proto: *HtmlElement, _proto: *HtmlElement,
_selected_index_set: bool = false,
pub fn asElement(self: *Select) *Element { pub fn asElement(self: *Select) *Element {
return self._proto._proto; return self._proto._proto;
@@ -38,31 +40,22 @@ pub fn asConstElement(self: *const Select) *const Element {
pub fn asNode(self: *Select) *Node { pub fn asNode(self: *Select) *Node {
return self.asElement().asNode(); return self.asElement().asNode();
} }
pub fn asConstNode(self: *const Select) *const Node {
return self.asConstElement().asConstNode();
}
pub fn getValue(self: *Select) []const u8 { pub fn getValue(self: *Select) []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 child = self.asNode().firstChild(); var iter = self.asNode().childrenIterator();
while (child) |c| { while (iter.next()) |child| {
if (c.is(Element)) |el| { const option = child.is(Option) orelse continue;
switch (el._type) {
.html => |html_el| {
switch (html_el._type) {
.option => |opt| {
if (first_option == null) { if (first_option == null) {
first_option = opt; first_option = option;
} }
if (opt.getSelected()) { if (option.getSelected()) {
return opt.getValue(); return option.getValue();
} }
},
else => {},
}
},
else => {},
}
}
child = c.nextSibling();
} }
// 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| {
@@ -74,27 +67,65 @@ pub fn getValue(self: *Select) []const u8 {
pub fn setValue(self: *Select, value: []const u8, page: *Page) !void { pub fn setValue(self: *Select, value: []const u8, page: *Page) !void {
_ = page; _ = page;
// Find option with matching value and select it // Find option with matching value and select it
var child = self.asNode().firstChild(); // Note: This updates the current state (_selected), not the default state (attribute)
while (child) |c| { // Setting value always deselects all others, even for multiple selects
if (c.is(Element)) |el| { var iter = self.asNode().childrenIterator();
switch (el._type) { while (iter.next()) |child| {
.html => |html_el| { const option = child.is(Option) orelse continue;
switch (html_el._type) { option._selected = std.mem.eql(u8, option.getValue(), value);
.option => |opt| { }
const opt_value = opt.getValue(); }
if (std.mem.eql(u8, opt_value, value)) {
opt._selected = true; pub fn getSelectedIndex(self: *Select) i32 {
var index: i32 = 0;
var has_options = false;
var iter = self.asNode().childrenIterator();
while (iter.next()) |child| {
const option = child.is(Option) orelse continue;
has_options = true;
if (option.getSelected()) {
return index;
}
index += 1;
}
// If selectedIndex was explicitly set and no option is selected, return -1
// If selectedIndex was never set, return 0 (first option implicitly selected) if we have options
if (self._selected_index_set) {
return -1;
}
return if (has_options) 0 else -1;
}
pub fn setSelectedIndex(self: *Select, index: i32) !void {
// Mark that selectedIndex has been explicitly set
self._selected_index_set = true;
// Select option at given index
// Note: This updates the current state (_selected), not the default state (attribute)
const is_multiple = self.getMultiple();
var current_index: i32 = 0;
var iter = self.asNode().childrenIterator();
while (iter.next()) |child| {
const option = child.is(Option) orelse continue;
if (current_index == index) {
option._selected = true;
} else if (!is_multiple) {
// Only deselect others if not multiple
option._selected = false;
}
current_index += 1;
}
}
pub fn getMultiple(self: *const Select) bool {
return self.asConstElement().getAttributeSafe("multiple") != null;
}
pub fn setMultiple(self: *Select, multiple: bool, page: *Page) !void {
if (multiple) {
try self.asElement().setAttributeSafe("multiple", "", page);
} else { } else {
opt._selected = false; try self.asElement().removeAttribute("multiple", page);
}
},
else => {},
}
},
else => {},
}
}
child = c.nextSibling();
} }
} }
@@ -110,6 +141,65 @@ pub fn setDisabled(self: *Select, disabled: bool, page: *Page) !void {
} }
} }
pub fn getName(self: *const Select) []const u8 {
return self.asConstElement().getAttributeSafe("name") orelse "";
}
pub fn setName(self: *Select, name: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("name", name, page);
}
pub fn getSize(self: *const Select) u32 {
const s = self.asConstElement().getAttributeSafe("size") orelse return 0;
const trimmed = std.mem.trimLeft(u8, s, &std.ascii.whitespace);
var end: usize = 0;
for (trimmed) |b| {
if (!std.ascii.isDigit(b)) {
break;
}
end += 1;
}
if (end == 0) {
return 0;
}
return std.fmt.parseInt(u32, trimmed[0..end], 10) catch 0;
}
pub fn setSize(self: *Select, size: u32, page: *Page) !void {
const size_string = try std.fmt.allocPrint(page.call_arena, "{d}", .{size});
try self.asElement().setAttributeSafe("size", size_string, page);
}
pub fn getRequired(self: *const Select) bool {
return self.asConstElement().getAttributeSafe("required") != null;
}
pub fn setRequired(self: *Select, required: bool, page: *Page) !void {
if (required) {
try self.asElement().setAttributeSafe("required", "", page);
} else {
try self.asElement().removeAttribute("required", page);
}
}
pub fn getOptions(self: *Select, page: *Page) !*collections.HTMLOptionsCollection {
// For options, we use the child_tag mode to filter only <option> elements
const node_live = collections.NodeLive(.child_tag).init(null, self.asNode(), .option, page);
const html_collection = try node_live.runtimeGenericWrap(page);
// Create and return HTMLOptionsCollection
return page._factory.create(collections.HTMLOptionsCollection{
._proto = html_collection,
._select = self,
});
}
pub fn getSelectedOptions(self: *Select, page: *Page) !collections.NodeLive(.selected_options) {
return collections.NodeLive(.selected_options).init(null, self.asNode(), {}, page);
}
pub fn getForm(self: *Select, page: *Page) ?*Form { pub fn getForm(self: *Select, page: *Page) ?*Form {
const element = self.asElement(); const element = self.asElement();
@@ -144,8 +234,15 @@ pub const JsApi = struct {
}; };
pub const value = bridge.accessor(Select.getValue, Select.setValue, .{}); pub const value = bridge.accessor(Select.getValue, Select.setValue, .{});
pub const selectedIndex = bridge.accessor(Select.getSelectedIndex, Select.setSelectedIndex, .{});
pub const multiple = bridge.accessor(Select.getMultiple, Select.setMultiple, .{});
pub const disabled = bridge.accessor(Select.getDisabled, Select.setDisabled, .{}); pub const disabled = bridge.accessor(Select.getDisabled, Select.setDisabled, .{});
pub const name = bridge.accessor(Select.getName, Select.setName, .{});
pub const required = bridge.accessor(Select.getRequired, Select.setRequired, .{});
pub const options = bridge.accessor(Select.getOptions, 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 Build = struct { pub const Build = struct {

View File

@@ -65,6 +65,26 @@ pub fn setDisabled(self: *TextArea, disabled: bool, page: *Page) !void {
} }
} }
pub fn getName(self: *const TextArea) []const u8 {
return self.asConstElement().getAttributeSafe("name") orelse "";
}
pub fn setName(self: *TextArea, name: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("name", name, page);
}
pub fn getRequired(self: *const TextArea) bool {
return self.asConstElement().getAttributeSafe("required") != null;
}
pub fn setRequired(self: *TextArea, required: bool, page: *Page) !void {
if (required) {
try self.asElement().setAttributeSafe("required", "", page);
} else {
try self.asElement().removeAttribute("required", page);
}
}
pub fn getForm(self: *TextArea, page: *Page) ?*Form { pub fn getForm(self: *TextArea, page: *Page) ?*Form {
const element = self.asElement(); const element = self.asElement();
@@ -101,6 +121,8 @@ pub const JsApi = struct {
pub const value = bridge.accessor(TextArea.getValue, TextArea.setValue, .{}); pub const value = bridge.accessor(TextArea.getValue, TextArea.setValue, .{});
pub const defaultValue = bridge.accessor(TextArea.getDefaultValue, null, .{}); pub const defaultValue = bridge.accessor(TextArea.getDefaultValue, null, .{});
pub const disabled = bridge.accessor(TextArea.getDisabled, TextArea.setDisabled, .{}); pub const disabled = bridge.accessor(TextArea.getDisabled, TextArea.setDisabled, .{});
pub const name = bridge.accessor(TextArea.getName, TextArea.setName, .{});
pub const required = bridge.accessor(TextArea.getRequired, TextArea.setRequired, .{});
pub const form = bridge.accessor(TextArea.getForm, null, .{}); pub const form = bridge.accessor(TextArea.getForm, null, .{});
}; };