improve Form, notably form.elements

This commit is contained in:
Karl Seguin
2025-12-14 20:02:39 +08:00
parent f93403d3dc
commit 6040cd3338
10 changed files with 749 additions and 136 deletions

View File

@@ -0,0 +1,299 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<!-- Test fixtures for form.name -->
<form id="form_with_name" name="myForm"></form>
<form id="form_without_name"></form>
<script id="name_initial">
{
testing.expectEqual('myForm', $('#form_with_name').name)
testing.expectEqual('', $('#form_without_name').name)
}
</script>
<script id="name_set">
{
const form = document.createElement('form')
testing.expectEqual('', form.name)
form.name = 'testForm'
testing.expectEqual('testForm', form.name)
testing.expectEqual('testForm', form.getAttribute('name'))
}
</script>
<!-- Test fixtures for form.method -->
<form id="form_get" method="get"></form>
<form id="form_post" method="post"></form>
<form id="form_dialog" method="dialog"></form>
<form id="form_default"></form>
<script id="method_initial">
{
testing.expectEqual('get', $('#form_get').method)
testing.expectEqual('post', $('#form_post').method)
testing.expectEqual('dialog', $('#form_dialog').method)
testing.expectEqual('get', $('#form_default').method)
}
</script>
<script id="method_set">
{
const form = document.createElement('form')
testing.expectEqual('get', form.method)
form.method = 'post'
testing.expectEqual('post', form.method)
testing.expectEqual('post', form.getAttribute('method'))
}
</script>
<script id="method_normalization">
{
const form = document.createElement('form')
// Test uppercase normalization
form.setAttribute('method', 'POST')
testing.expectEqual('post', form.method)
form.setAttribute('method', 'GeT')
testing.expectEqual('get', form.method)
form.setAttribute('method', 'DIALOG')
testing.expectEqual('dialog', form.method)
// Test invalid value defaults to "get"
form.setAttribute('method', 'invalid')
testing.expectEqual('get', form.method)
}
</script>
<!-- Test fixtures for form.elements -->
<form id="form1">
<input name="field1" value="value1">
<button name="btn1" type="submit">Submit</button>
<select name="select1">
<option value="opt1">Option 1</option>
</select>
<textarea name="text1">Text</textarea>
</form>
<!-- Control outside form with form=ID -->
<input id="external1" name="external" form="form1">
<!-- Control outside form without form attribute -->
<input id="orphan" name="orphan">
<script id="elements_collection">
{
const form = $('#form1')
const elements = form.elements
testing.expectEqual('HTMLFormControlsCollection', elements.constructor.name)
testing.expectEqual(5, elements.length)
}
</script>
<script id="length">
{
testing.expectEqual(5, $('#form1').length)
}
</script>
<script id="elements_indexed_access">
{
const form = $('#form1')
const elements = form.elements
testing.expectEqual('field1', elements[0].name)
testing.expectEqual('INPUT', elements[0].tagName)
testing.expectEqual('btn1', elements[1].name)
testing.expectEqual('BUTTON', elements[1].tagName)
testing.expectEqual('select1', elements[2].name)
testing.expectEqual('SELECT', elements[2].tagName)
testing.expectEqual('text1', elements[3].name)
testing.expectEqual('TEXTAREA', elements[3].tagName)
testing.expectEqual('external', elements[4].name)
testing.expectEqual('INPUT', elements[4].tagName)
}
</script>
<script id="elements_named_access">
{
const form = $('#form1')
const elements = form.elements
testing.expectEqual('field1', elements.field1.name)
testing.expectEqual('btn1', elements.btn1.name)
testing.expectEqual('select1', elements.select1.name)
testing.expectEqual('text1', elements.text1.name)
testing.expectEqual('external', elements.external.name)
testing.expectEqual('field1', elements.namedItem('field1').name)
}
</script>
<script id="elements_excludes_orphans">
{
const form = $('#form1')
const elements = form.elements
let foundOrphan = false
for (let i = 0; i < elements.length; i++) {
if (elements[i].id === 'orphan') {
foundOrphan = true
}
}
testing.expectEqual(false, foundOrphan)
}
</script>
<form id="form2"></form>
<script id="elements_live_collection">
{
const form = $('#form2')
testing.expectEqual(0, form.elements.length)
const input = document.createElement('input')
input.name = 'dynamic'
form.appendChild(input)
testing.expectEqual(1, form.elements.length)
testing.expectEqual('dynamic', form.elements[0].name)
input.remove()
testing.expectEqual(0, form.elements.length)
}
</script>
<!-- Test with controls that have form attribute pointing to different form -->
<form id="form3"></form>
<input id="belongs_to_3" name="field3" form="form3">
<script id="form_attribute_different_form">
{
const form1 = $('#form1')
const form3 = $('#form3')
const belongs3 = $('#belongs_to_3')
let inForm1 = false
for (let i = 0; i < form1.elements.length; i++) {
if (form1.elements[i].id === 'belongs_to_3') {
inForm1 = true
}
}
testing.expectEqual(false, inForm1)
let inForm3 = false
for (let i = 0; i < form3.elements.length; i++) {
if (form3.elements[i].id === 'belongs_to_3') {
inForm3 = true
}
}
testing.expectEqual(true, inForm3)
}
</script>
<!-- CRITICAL TEST: Nested control with form attribute pointing elsewhere -->
<form id="outer_form">
<input id="nested_but_reassigned" name="reassigned" form="form3">
<input id="nested_normal" name="normal">
</form>
<script id="nested_control_with_form_attribute">
{
// This test prevents a dangerous optimization bug:
// Even though nested_but_reassigned is physically nested in outer_form,
// it has form="form3", so it belongs to form3, NOT outer_form.
// We MUST check the form attribute even for nested controls!
const outerForm = $('#outer_form')
const form3 = $('#form3')
// outer_form should have only 1 element (nested_normal)
testing.expectEqual(1, outerForm.elements.length)
testing.expectEqual('nested_normal', outerForm.elements[0].id)
// form3 should have 2 elements (belongs_to_3 and nested_but_reassigned)
testing.expectEqual(2, form3.elements.length)
let foundReassigned = false
for (let i = 0; i < form3.elements.length; i++) {
if (form3.elements[i].id === 'nested_but_reassigned') {
foundReassigned = true
}
}
testing.expectEqual(true, foundReassigned)
}
</script>
<!-- Test radio buttons with same name -->
<form id="radio_form">
<input type="radio" name="choice" value="a">
<input type="radio" name="choice" value="b">
<input type="radio" name="choice" value="c">
</form>
<script id="radio_buttons_in_elements">
{
const form = $('#radio_form')
testing.expectEqual(3, form.elements.length)
testing.expectEqual('a', form.elements[0].value)
testing.expectEqual('b', form.elements[1].value)
testing.expectEqual('c', form.elements[2].value)
// Note: In spec-compliant browsers, namedItem with duplicate names returns RadioNodeList
// RadioNodeList.value returns the checked radio's value (or "" if none checked)
// Our implementation currently returns the first element (TODO: implement RadioNodeList)
// For now, test that we can access by index which works in all browsers
testing.expectEqual('choice', form.elements[0].name)
}
</script>
<!-- Test disabled controls -->
<script id="disabled_controls_included">
{
const form = document.createElement('form')
const input1 = document.createElement('input')
input1.name = 'enabled'
const input2 = document.createElement('input')
input2.name = 'disabled'
input2.disabled = true
form.appendChild(input1)
form.appendChild(input2)
testing.expectEqual(2, form.elements.length)
testing.expectEqual('enabled', form.elements[0].name)
testing.expectEqual('disabled', form.elements[1].name)
}
</script>
<!-- Test empty form -->
<form id="empty_form"></form>
<script id="empty_form_elements">
{
const form = $('#empty_form')
testing.expectEqual(0, form.elements.length)
testing.expectEqual(0, form.length)
}
</script>
<!-- Test form without id can't have external controls -->
<form id="form_no_id_attr"></form>
<input id="orphan2" name="orphan2" form="nonexistent">
<script id="form_without_id_no_external">
{
const form = $('#form_no_id_attr')
testing.expectEqual(0, form.elements.length)
}
</script>

View File

@@ -1,5 +1,42 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<form id="form1">
<input id="has_no_name" value="nope1">
<input id="is_disabled" disabled value="nope2">
<input name="txt-1" value="txt-1-v">
<input name="txt-2" value="txt-~-v" type=password>
<input name="chk-3" value="chk-3-va" type=checkbox>
<input name="chk-3" value="chk-3-vb" type=checkbox checked>
<input name="chk-3" value="chk-3-vc" type=checkbox checked>
<input name="chk-4" value="chk-4-va" type=checkbox>
<input name="chk-4" value="chk-4-va" type=checkbox>
<input name="rdi-1" value="rdi-1-va" type=radio>
<input name="rdi-1" value="rdi-1-vb" type=radio>
<input name="rdi-1" value="rdi-1-vc" type=radio checked>
<input name="rdi-2" value="rdi-2-va" type=radio>
<input name="rdi-2" value="rdi-2-vb" type=radio>
<textarea name="ta-1"> ta-1-v</textarea>
<textarea name="ta"></textarea>
<input type=hidden name=h1 value="h1-v">
<input type=hidden name=h2 value="h2-v" disabled=disabled>
<select name="sel-1"><option>blue<option>red</select>
<select name="sel-2"><option>blue<option value=sel-2-v selected>red</select>
<select name="sel-3"><option disabled>nope1<option>nope2</select>
<select name="mlt-1" multiple><option>water<option>tea</select>
<select name="mlt-2" multiple><option selected>water<option selected>tea<option>coffee</select>
<input type=submit id=s1 name=s1 value=s1-v>
<input type=submit name=s2 value=s2-v>
<input type=image name=i1 value=i1-v>
</form>
<input type=text name=abc value=123 form=form1>
<script id=formData>
let f = new FormData();
testing.expectEqual(null, f.get('a'));
@@ -87,44 +124,10 @@
testing.expectEqual(['h1', 'h1-v'], acc[7]);
testing.expectEqual(['sel-1', 'blue'], acc[8]);
testing.expectEqual(['sel-2', 'sel-2-v'], acc[9]);
testing.expectEqual(['mlt-2', 'water'], acc[10]);
testing.expectEqual(['mlt-2', 'tea'], acc[11]);
testing.expectEqual(['s1', 's1-v'], acc[12]);
testing.expectEqual(['dyn', 'dyn-v'], acc[13]);
testing.expectEqual(['sel-3', 'nope2'], acc[10]);
testing.expectEqual(['mlt-2', 'water'], acc[11]);
testing.expectEqual(['mlt-2', 'tea'], acc[12]);
testing.expectEqual(['s1', 's1-v'], acc[13]);
</script>
<form id="form1">
<input id="has_no_name" value="nope1">
<input id="is_disabled" disabled value="nope2">
<input name="txt-1" value="txt-1-v">
<input name="txt-2" value="txt-~-v" type=password>
<input name="chk-3" value="chk-3-va" type=checkbox>
<input name="chk-3" value="chk-3-vb" type=checkbox checked>
<input name="chk-3" value="chk-3-vc" type=checkbox checked>
<input name="chk-4" value="chk-4-va" type=checkbox>
<input name="chk-4" value="chk-4-va" type=checkbox>
<input name="rdi-1" value="rdi-1-va" type=radio>
<input name="rdi-1" value="rdi-1-vb" type=radio>
<input name="rdi-1" value="rdi-1-vc" type=radio checked>
<input name="rdi-2" value="rdi-2-va" type=radio>
<input name="rdi-2" value="rdi-2-vb" type=radio>
<textarea name="ta-1"> ta-1-v</textarea>
<textarea name="ta"></textarea>
<input type=hidden name=h1 value="h1-v">
<input type=hidden name=h2 value="h2-v" disabled=disabled>
<select name="sel-1"><option>blue<option>red</select>
<select name="sel-2"><option>blue<option value=sel-2-v selected>red</select>
<select name="sel-3"><option disabled>nope1<option>nope2</select>
<select name="mlt-1" multiple><option>water<option>tea</select>
<select name="mlt-2" multiple><option selected>water<option selected>tea<option>coffee</select>
<input type=submit id=s1 name=s1 value=s1-v>
<input type=submit name=s2 value=s2-v>
<input type=image name=i1 value=i1-v>
</form>
<input type=text name=abc value=123 form=form1>

View File

@@ -250,3 +250,134 @@
testing.expectEqual(3, context.sum);
}
</script>
<form id="form1">
<input id="has_no_name" value="nope1">
<input id="is_disabled" disabled value="nope2">
<input name="txt-1" value="txt-1-v">
<input name="txt-2" value="txt-~-v" type=password>
<input name="chk-3" value="chk-3-va" type=checkbox>
<input name="chk-3" value="chk-3-vb" type=checkbox checked>
<input name="chk-3" value="chk-3-vc" type=checkbox checked>
<input name="chk-4" value="chk-4-va" type=checkbox>
<input name="chk-4" value="chk-4-va" type=checkbox>
<input name="rdi-1" value="rdi-1-va" type=radio>
<input name="rdi-1" value="rdi-1-vb" type=radio>
<input name="rdi-1" value="rdi-1-vc" type=radio checked>
<input name="rdi-2" value="rdi-2-va" type=radio>
<input name="rdi-2" value="rdi-2-vb" type=radio>
<textarea name="ta-1"> ta-1-v</textarea>
<textarea name="ta"></textarea>
<input type=hidden name=h1 value="h1-v">
<input type=hidden name=h2 value="h2-v" disabled=disabled>
<select name="sel-1"><option>blue<option>red</select>
<select name="sel-2"><option>blue<option value=sel-2-v selected>red</select>
<select name="sel-3"><option disabled>nope1<option>nope2</select>
<select name="mlt-1" multiple><option>water<option>tea</select>
<select name="mlt-2" multiple><option selected>water<option selected>tea<option>coffee</select>
<input type=submit id=s1 name=s1 value=s1-v>
<input type=submit name=s2 value=s2-v>
<input type=image name=i1 value=i1-v>
</form>
<input type=text name=abc value=123 form=form1>
<script id=formData>
let f = new FormData();
testing.expectEqual(null, f.get('a'));
testing.expectEqual(false, f.has('a'));
testing.expectEqual([], f.getAll('a'));
testing.expectEqual(undefined, f.delete('a'));
f.set('a', 1);
testing.expectEqual(true, f.has('a'));
testing.expectEqual('1', f.get('a'));
testing.expectEqual(['1'], f.getAll('a'));
f.append('a', 2);
testing.expectEqual(true, f.has('a'));
testing.expectEqual('1', f.get('a'));
testing.expectEqual(['1', '2'], f.getAll('a'));
f.append('b', '3');
testing.expectEqual(true, f.has('a'));
testing.expectEqual('1', f.get('a'));
testing.expectEqual(['1', '2'], f.getAll('a'));
testing.expectEqual(true, f.has('b'));
testing.expectEqual('3', f.get('b'));
testing.expectEqual(['3'], f.getAll('b'));
let acc = [];
for (const key of f.keys()) { acc.push(key) }
testing.expectEqual(['a', 'a', 'b'], acc);
acc = [];
for (const value of f.values()) { acc.push(value) }
testing.expectEqual(['1', '2', '3'], acc);
acc = [];
for (const entry of f.entries()) { acc.push(entry) }
testing.expectEqual([['a', '1'], ['a', '2'], ['b', '3']], acc);
acc = [];
for (const entry of f) { acc.push(entry) };
testing.expectEqual([['a', '1'], ['a', '2'], ['b', '3']], acc);
f.delete('a');
testing.expectEqual(false, f.has('a'));
testing.expectEqual(true, f.has('b'));
acc = [];
for (const key of f.keys()) { acc.push(key) }
testing.expectEqual(['b'], acc);
acc = [];
for (const value of f.values()) { acc.push(value) }
testing.expectEqual(['3'], acc);
acc = [];
for (const entry of f.entries()) { acc.push(entry) }
testing.expectEqual([['b', '3']], acc);
acc = [];
for (const entry of f) { acc.push(entry) }
testing.expectEqual([['b', '3']], acc);
</script>
<!-- <script id=serialize>
{
let form1 = $('#form1');
let submit1 = $('#s1');
let input = document.createElement('input');
input.name = 'dyn';
input.value = 'dyn-v';
form1.appendChild(input);
let f2 = new FormData(form1, submit1);
acc = [];
for (const entry of f2) {
acc.push(entry);
};
testing.expectEqual(['txt-1', 'txt-1-v'], acc[0]);
testing.expectEqual(['txt-2', 'txt-~-v'], acc[1]);
testing.expectEqual(['chk-3', 'chk-3-vb'], acc[2]);
testing.expectEqual(['chk-3', 'chk-3-vc'], acc[3]);
testing.expectEqual(['rdi-1', 'rdi-1-vc'], acc[4]);
testing.expectEqual(['ta-1', ' ta-1-v'], acc[5]);
testing.expectEqual(['ta', ''], acc[6]);
testing.expectEqual(['h1', 'h1-v'], acc[7]);
testing.expectEqual(['sel-1', 'blue'], acc[8]);
testing.expectEqual(['sel-2', 'sel-2-v'], acc[9]);
testing.expectEqual(['sel-3', 'nope2'], acc[10]);
testing.expectEqual(['mlt-2', 'water'], acc[11]);
testing.expectEqual(['mlt-2', 'tea'], acc[12]);
testing.expectEqual(['s1', 's1-v'], acc[13]);
}
</script> -->

View File

@@ -339,7 +339,7 @@ pub fn isConnected(self: *const Node) bool {
const GetRootNodeOpts = struct {
composed: bool = false,
};
pub fn getRootNode(self: *const Node, opts_: ?GetRootNodeOpts) *const Node {
pub fn getRootNode(self: *Node, opts_: ?GetRootNodeOpts) *Node {
const opts = opts_ orelse GetRootNodeOpts{};
var root = self;
@@ -613,7 +613,7 @@ pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) error{ OutOfMemory, Str
}
}
pub fn compareDocumentPosition(self: *const Node, other: *const Node) u16 {
pub fn compareDocumentPosition(self: *Node, other: *Node) u16 {
const DISCONNECTED: u16 = 0x01;
const PRECEDING: u16 = 0x02;
const FOLLOWING: u16 = 0x04;

View File

@@ -21,6 +21,7 @@ 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 const HTMLFormControlsCollection = @import("collections/HTMLFormControlsCollection.zig");
pub fn registerTypes() []const type {
return &.{
@@ -33,6 +34,7 @@ pub fn registerTypes() []const type {
@import("collections/HTMLAllCollection.zig"),
@import("collections/HTMLAllCollection.zig").Iterator,
HTMLOptionsCollection,
HTMLFormControlsCollection,
DOMTokenList,
DOMTokenList.KeyIterator,
DOMTokenList.ValueIterator,

View File

@@ -23,6 +23,7 @@ const Page = @import("../../Page.zig");
const Element = @import("../Element.zig");
const TreeWalker = @import("../TreeWalker.zig");
const NodeLive = @import("node_live.zig").NodeLive;
const Form = @import("../element/html/Form.zig");
const Mode = enum {
tag,
@@ -35,11 +36,13 @@ const Mode = enum {
selected_options,
links,
anchors,
form,
};
const HTMLCollection = @This();
data: union(Mode) {
_type: Type = .{ .generic = {} },
_data: union(Mode) {
tag: NodeLive(.tag),
tag_name: NodeLive(.tag_name),
class_name: NodeLive(.class_name),
@@ -50,22 +53,28 @@ data: union(Mode) {
selected_options: NodeLive(.selected_options),
links: NodeLive(.links),
anchors: NodeLive(.anchors),
form: NodeLive(.form),
},
const Type = union(enum) {
generic: void,
form: *Form,
};
pub fn length(self: *HTMLCollection, page: *const Page) u32 {
return switch (self.data) {
return switch (self._data) {
inline else => |*impl| impl.length(page),
};
}
pub fn getAtIndex(self: *HTMLCollection, index: usize, page: *const Page) ?*Element {
return switch (self.data) {
return switch (self._data) {
inline else => |*impl| impl.getAtIndex(index, page),
};
}
pub fn getByName(self: *HTMLCollection, name: []const u8, page: *Page) ?*Element {
return switch (self.data) {
return switch (self._data) {
inline else => |*impl| impl.getByName(name, page),
};
}
@@ -73,7 +82,7 @@ pub fn getByName(self: *HTMLCollection, name: []const u8, page: *Page) ?*Element
pub fn iterator(self: *HTMLCollection, page: *Page) !*Iterator {
return Iterator.init(.{
.list = self,
.tw = switch (self.data) {
.tw = switch (self._data) {
.tag => |*impl| .{ .tag = impl._tw.clone() },
.tag_name => |*impl| .{ .tag_name = impl._tw.clone() },
.class_name => |*impl| .{ .class_name = impl._tw.clone() },
@@ -84,6 +93,7 @@ pub fn iterator(self: *HTMLCollection, page: *Page) !*Iterator {
.selected_options => |*impl| .{ .selected_options = impl._tw.clone() },
.links => |*impl| .{ .links = impl._tw.clone() },
.anchors => |*impl| .{ .anchors = impl._tw.clone() },
.form => |*impl| .{ .form = impl._tw.clone() },
},
}, page);
}
@@ -102,10 +112,11 @@ pub const Iterator = GenericIterator(struct {
selected_options: TreeWalker.Children,
links: TreeWalker.FullExcludeSelf,
anchors: TreeWalker.FullExcludeSelf,
form: TreeWalker.FullExcludeSelf,
},
pub fn next(self: *@This(), _: *Page) ?*Element {
return switch (self.list.data) {
return switch (self.list._data) {
.tag => |*impl| impl.nextTw(&self.tw.tag),
.tag_name => |*impl| impl.nextTw(&self.tw.tag_name),
.class_name => |*impl| impl.nextTw(&self.tw.class_name),
@@ -116,6 +127,7 @@ pub const Iterator = GenericIterator(struct {
.selected_options => |*impl| impl.nextTw(&self.tw.selected_options),
.links => |*impl| impl.nextTw(&self.tw.links),
.anchors => |*impl| impl.nextTw(&self.tw.anchors),
.form => |*impl| impl.nextTw(&self.tw.form),
};
}
}, null);

View File

@@ -0,0 +1,57 @@
// 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 HTMLFormControlsCollection = @This();
_proto: *HTMLCollection,
pub fn length(self: *HTMLFormControlsCollection, page: *Page) u32 {
return self._proto.length(page);
}
pub fn getAtIndex(self: *HTMLFormControlsCollection, index: usize, page: *Page) ?*Element {
return self._proto.getAtIndex(index, page);
}
pub fn namedItem(self: *HTMLFormControlsCollection, name: []const u8, page: *Page) ?*Element {
// TODO: When multiple elements have same name (radio buttons),
// should return RadioNodeList instead of first element
return self._proto.getByName(name, page);
}
pub const JsApi = struct {
pub const bridge = js.Bridge(HTMLFormControlsCollection);
pub const Meta = struct {
pub const name = "HTMLFormControlsCollection";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const manage = false;
};
pub const length = bridge.accessor(HTMLFormControlsCollection.length, null, .{});
pub const @"[int]" = bridge.indexed(HTMLFormControlsCollection.getAtIndex, .{ .null_as_undefined = true });
pub const @"[str]" = bridge.namedIndexed(HTMLFormControlsCollection.namedItem, null, null, .{ .null_as_undefined = true });
pub const namedItem = bridge.function(HTMLFormControlsCollection.namedItem, .{});
};

View File

@@ -28,6 +28,7 @@ const Node = @import("../Node.zig");
const Element = @import("../Element.zig");
const TreeWalker = @import("../TreeWalker.zig");
const Selector = @import("../selector/Selector.zig");
const Form = @import("../element/html/Form.zig");
const Allocator = std.mem.Allocator;
@@ -42,6 +43,7 @@ const Mode = enum {
selected_options,
links,
anchors,
form,
};
const Filters = union(Mode) {
@@ -55,6 +57,7 @@ const Filters = union(Mode) {
selected_options,
links,
anchors,
form: *Form,
fn TypeOf(comptime mode: Mode) type {
@setEvalBranchQuota(2000);
@@ -82,7 +85,7 @@ const Filters = union(Mode) {
pub fn NodeLive(comptime mode: Mode) type {
const Filter = Filters.TypeOf(mode);
const TW = switch (mode) {
.tag, .tag_name, .class_name, .name, .all_elements, .links, .anchors => TreeWalker.FullExcludeSelf,
.tag, .tag_name, .class_name, .name, .all_elements, .links, .anchors, .form => TreeWalker.FullExcludeSelf,
.child_elements, .child_tag, .selected_options => TreeWalker.Children,
};
return struct {
@@ -259,9 +262,46 @@ pub fn NodeLive(comptime mode: Mode) type {
if (el.is(Anchor) == null) return false;
return el.hasAttributeSafe("name");
},
.form => {
const el = node.is(Element) orelse return false;
if (!isFormControl(el)) {
return false;
}
if (el.getAttributeSafe("form")) |form_attr| {
const form_id = self._filter.asElement().getAttributeSafe("id") orelse return false;
return std.mem.eql(u8, form_attr, form_id);
}
// No form attribute - match if descendant of our form
// This does an O(depth) ancestor walk for each control in the form.
//
// TODO: If profiling shows this is a bottleneck:
// When we first encounter the form element during tree walk, we could
// do a one-time reverse walk to find the LAST control that belongs to
// this form (checking both form controls and their form= attributes).
// Store that element in a new FormState. Then as we traverse
// forward:
// - Set is_within_form = true when we enter the form element
// - Return true immediately for any control while is_within_form
// - Set is_within_form = false when we reach that last element
// This trades one O(form_size) reverse walk for N O(depth) ancestor
// checks, where N = number of controls. For forms with many nested
// controls, this could be significantly faster.
return self._filter.asNode().contains(node);
},
}
}
fn isFormControl(el: *Element) bool {
if (el._type != .html) return false;
const html = el._type.html;
return switch (html._type) {
.input, .button, .select, .text_area => true,
else => false,
};
}
fn versionCheck(self: *Self, page: *const Page) bool {
const current = page.version;
if (current == self._cached_version) {
@@ -278,16 +318,17 @@ pub fn NodeLive(comptime mode: Mode) type {
const HTMLCollection = @import("HTMLCollection.zig");
pub fn runtimeGenericWrap(self: Self, page: *Page) !*HTMLCollection {
const collection = switch (mode) {
.tag => HTMLCollection{ .data = .{ .tag = self } },
.tag_name => HTMLCollection{ .data = .{ .tag_name = self } },
.class_name => HTMLCollection{ .data = .{ .class_name = self } },
.name => HTMLCollection{ .data = .{ .name = self } },
.all_elements => HTMLCollection{ .data = .{ .all_elements = self } },
.child_elements => HTMLCollection{ .data = .{ .child_elements = self } },
.child_tag => HTMLCollection{ .data = .{ .child_tag = self } },
.selected_options => HTMLCollection{ .data = .{ .selected_options = self } },
.links => HTMLCollection{ .data = .{ .links = self } },
.anchors => HTMLCollection{ .data = .{ .anchors = self } },
.tag => HTMLCollection{ ._data = .{ .tag = self } },
.tag_name => HTMLCollection{ ._data = .{ .tag_name = self } },
.class_name => HTMLCollection{ ._data = .{ .class_name = self } },
.name => HTMLCollection{ ._data = .{ .name = self } },
.all_elements => HTMLCollection{ ._data = .{ .all_elements = self } },
.child_elements => HTMLCollection{ ._data = .{ .child_elements = self } },
.child_tag => HTMLCollection{ ._data = .{ .child_tag = self } },
.selected_options => HTMLCollection{ ._data = .{ .selected_options = self } },
.links => HTMLCollection{ ._data = .{ .links = self } },
.anchors => HTMLCollection{ ._data = .{ .anchors = self } },
.form => HTMLCollection{ ._type = .{ .form = self._filter }, ._data = .{ .form = self } },
};
return page._factory.create(collection);
}

View File

@@ -23,6 +23,7 @@ const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig");
const TreeWalker = @import("../../TreeWalker.zig");
const collections = @import("../../collections.zig");
const Input = @import("Input.zig");
const Button = @import("Button.zig");
@@ -32,6 +33,9 @@ const TextArea = @import("TextArea.zig");
const Form = @This();
_proto: *HtmlElement,
fn asConstElement(self: *const Form) *const Element {
return self._proto._proto;
}
pub fn asElement(self: *Form) *Element {
return self._proto._proto;
}
@@ -39,90 +43,49 @@ pub fn asNode(self: *Form) *Node {
return self.asElement().asNode();
}
// Untested / unused right now. Iterates over all the controls of a form,
// including those outside the <form>...</form> but with a form=$FORM_ID attribute
pub const Iterator = struct {
_form_id: ?[]const u8,
_walkers: union(enum) {
nested: TreeWalker.FullExcludeSelf,
names: TreeWalker.FullExcludeSelf,
},
pub fn getName(self: *const Form) []const u8 {
return self.asConstElement().getAttributeSafe("name") orelse "";
}
pub fn init(form: *Form) Iterator {
const form_element = form.asElement();
const form_id = form_element.getAttributeSafe("id");
pub fn setName(self: *Form, name: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("name", name, page);
}
return .{
._form_id = form_id,
._walkers = .{
.nested = TreeWalker.FullExcludeSelf.init(form.asNode(), .{}),
},
};
pub fn getMethod(self: *const Form) []const u8 {
const method = self.asConstElement().getAttributeSafe("method") orelse return "get";
if (std.ascii.eqlIgnoreCase(method, "post")) {
return "post";
}
pub fn next(self: *Iterator) ?FormControl {
switch (self._walkers) {
.nested => |*tw| {
// find controls nested directly in the form
while (tw.next()) |node| {
const element = node.is(Element) orelse continue;
const control = asFormControl(element) orelse continue;
// Skip if it has a form attribute (will be handled in phase 2)
if (element.getAttributeSafe("form") == null) {
return control;
}
}
if (self._form_id == null) {
return null;
}
const doc = tw._root.getRootNode();
self._walkers = .{
.names = TreeWalker.FullExcludeSelf(doc, .{}),
};
return self.next();
},
.names => |*tw| {
// find controls with a name matching the form id
while (tw.next()) |node| {
const input = node.is(Input) orelse continue;
if (input._type != .radio) {
continue;
}
const input_form = input.asElement().getAttributeSafe("form") orelse continue;
// must have a self._form_id, else we never would have transitioned
// from a nested walker to a namew walker
if (!std.mem.eql(u8, input_form, self._form_id.?)) {
continue;
}
return .{ .input = input };
}
return null;
},
}
if (std.ascii.eqlIgnoreCase(method, "dialog")) {
return "dialog";
}
};
// invalid, or it was get all along
return "get";
}
pub const FormControl = union(enum) {
input: *Input,
button: *Button,
select: *Select,
textarea: *TextArea,
};
pub fn setMethod(self: *Form, method: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("method", method, page);
}
fn asFormControl(element: *Element) ?FormControl {
if (element._type != .html) {
return null;
}
const html = element._type.html;
switch (html._type) {
.input => |cntrl| return .{ .input = cntrl },
.button => |cntrl| return .{ .button = cntrl },
.select => |cntrl| return .{ .select = cntrl },
.textarea => |cntrl| return .{ .textarea = cntrl },
else => return null,
}
pub fn getElements(self: *Form, page: *Page) !*collections.HTMLFormControlsCollection {
const form_id = self.asElement().getAttributeSafe("id");
const root = if (form_id != null)
self.asNode().getRootNode(null) // Has ID: walk entire document to find form=ID controls
else
self.asNode(); // No ID: walk only form subtree (no external controls possible)
const node_live = collections.NodeLive(.form).init(root, self, page);
const html_collection = try node_live.runtimeGenericWrap(page);
return page._factory.create(collections.HTMLFormControlsCollection{
._proto = html_collection,
});
}
pub fn getLength(self: *Form, page: *Page) !u32 {
const elements = try self.getElements(page);
return elements.length(page);
}
pub const JsApi = struct {
@@ -132,4 +95,14 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const name = bridge.accessor(Form.getName, Form.setName, .{});
pub const method = bridge.accessor(Form.getMethod, Form.setMethod, .{});
pub const elements = bridge.accessor(Form.getElements, null, .{});
pub const length = bridge.accessor(Form.getLength, null, .{});
};
const testing = @import("../../../../testing.zig");
test "WebApi: HTML.Form" {
try testing.htmlRunner("element/html/form.html", .{});
}

View File

@@ -22,6 +22,8 @@ const log = @import("../../../log.zig");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Form = @import("../element/html/Form.zig");
const Element = @import("../Element.zig");
const KeyValueList = @import("../KeyValueList.zig");
const Alloctor = std.mem.Allocator;
@@ -31,7 +33,9 @@ const FormData = @This();
_arena: Alloctor,
_list: KeyValueList,
pub fn init(page: *Page) !*FormData {
pub fn init(form_: ?*Form, submitter_: ?*Element, page: *Page) !*FormData {
_ = form_;
_ = submitter_;
return page._factory.create(FormData{
._arena = page.arena,
._list = KeyValueList.init(),
@@ -127,6 +131,97 @@ pub const JsApi = struct {
pub const forEach = bridge.function(FormData.forEach, .{});
};
// fn collectForm(form: *Form, submitter_: ?*Element, page: *Page) !KeyValueList {
// const arena = page.arena;
// // Don't use libdom's formGetCollection (aka dom_html_form_element_get_elements)
// // It doesn't work with dynamically added elements, because their form
// // property doesn't get set. We should fix that.
// // However, even once fixed, there are other form-collection features we
// // probably want to implement (like disabled fieldsets), so we might want
// // to stick with our own walker even if fix libdom to properly support
// // dynamically added elements.
// const node_list = try @import("../dom/css.zig").querySelectorAll(arena, @ptrCast(@alignCast(form)), "input,select,button,textarea");
// const nodes = node_list.nodes.items;
// var entries: kv.List = .{};
// try entries.ensureTotalCapacity(arena, nodes.len);
// var submitter_included = false;
// const submitter_name_ = try getSubmitterName(submitter_);
// for (nodes) |node| {
// const element = parser.nodeToElement(node);
// // must have a name
// const name = try parser.elementGetAttribute(element, "name") orelse continue;
// if (try parser.elementGetAttribute(element, "disabled") != null) {
// continue;
// }
// const tag = try parser.elementTag(element);
// switch (tag) {
// .input => {
// const tpe = try parser.inputGetType(@ptrCast(element));
// if (std.ascii.eqlIgnoreCase(tpe, "image")) {
// if (submitter_name_) |submitter_name| {
// if (std.mem.eql(u8, submitter_name, name)) {
// const key_x = try std.fmt.allocPrint(arena, "{s}.x", .{name});
// const key_y = try std.fmt.allocPrint(arena, "{s}.y", .{name});
// try entries.appendOwned(arena, key_x, "0");
// try entries.appendOwned(arena, key_y, "0");
// submitter_included = true;
// }
// }
// continue;
// }
// if (std.ascii.eqlIgnoreCase(tpe, "checkbox") or std.ascii.eqlIgnoreCase(tpe, "radio")) {
// if (try parser.inputGetChecked(@ptrCast(element)) == false) {
// continue;
// }
// }
// if (std.ascii.eqlIgnoreCase(tpe, "submit")) {
// if (submitter_name_ == null or !std.mem.eql(u8, submitter_name_.?, name)) {
// continue;
// }
// submitter_included = true;
// }
// const value = try parser.inputGetValue(@ptrCast(element));
// try entries.appendOwned(arena, name, value);
// },
// .select => {
// const select: *parser.Select = @ptrCast(node);
// try collectSelectValues(arena, select, name, &entries, page);
// },
// .textarea => {
// const textarea: *parser.TextArea = @ptrCast(node);
// const value = try parser.textareaGetValue(textarea);
// try entries.appendOwned(arena, name, value);
// },
// .button => if (submitter_name_) |submitter_name| {
// if (std.mem.eql(u8, submitter_name, name)) {
// const value = (try parser.elementGetAttribute(element, "value")) orelse "";
// try entries.appendOwned(arena, name, value);
// submitter_included = true;
// }
// },
// else => unreachable,
// }
// }
// if (submitter_included == false) {
// if (submitter_name_) |submitter_name| {
// // this can happen if the submitter is outside the form, but associated
// // with the form via a form=ID attribute
// const value = (try parser.elementGetAttribute(@ptrCast(submitter_.?), "value")) orelse "";
// try entries.appendOwned(arena, submitter_name, value);
// }
// }
// return entries;
// }
const testing = @import("../../../testing.zig");
test "WebApi: FormData" {
try testing.htmlRunner("net/form_data.html", .{});