From 9b3107d4fe5041077ca94504650444d771c11e00 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 15 Dec 2025 12:31:30 +0800 Subject: [PATCH] build FormData from optional form and optional submitter --- src/browser/tests/net/form_data.html | 249 ++++++++++++++++++- src/browser/webapi/Element.zig | 6 +- src/browser/webapi/collections/node_live.zig | 6 +- src/browser/webapi/element/Html.zig | 4 +- src/browser/webapi/element/html/Button.zig | 9 + src/browser/webapi/element/html/Form.zig | 8 +- src/browser/webapi/element/html/Select.zig | 10 +- src/browser/webapi/net/FormData.zig | 177 ++++++------- 8 files changed, 357 insertions(+), 112 deletions(-) diff --git a/src/browser/tests/net/form_data.html b/src/browser/tests/net/form_data.html index 515b8d93..97814209 100644 --- a/src/browser/tests/net/form_data.html +++ b/src/browser/tests/net/form_data.html @@ -349,7 +349,7 @@ testing.expectEqual([['b', '3']], acc); - + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 7de5f14f..97a2ecb4 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -202,7 +202,7 @@ pub fn getTagNameLower(self: *const Element) []const u8 { .slot => "slot", .style => "style", .template => "template", - .text_area => "textarea", + .textarea => "textarea", .title => "title", .ul => "ul", .unknown => |e| e._tag_name.str(), @@ -254,7 +254,7 @@ pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 { .slot => "SLOT", .style => "STYLE", .template => "TEMPLATE", - .text_area => "TEXTAREA", + .textarea => "TEXTAREA", .title => "TITLE", .ul => "UL", .unknown => |e| switch (self._namespace) { @@ -1097,7 +1097,7 @@ pub fn getTag(self: *const Element) Tag { .slot => .slot, .option => .option, .template => .template, - .text_area => .textarea, + .textarea => .textarea, .input => .input, .link => .link, .meta => .meta, diff --git a/src/browser/webapi/collections/node_live.zig b/src/browser/webapi/collections/node_live.zig index 5975f428..68ca3b73 100644 --- a/src/browser/webapi/collections/node_live.zig +++ b/src/browser/webapi/collections/node_live.zig @@ -194,6 +194,10 @@ pub fn NodeLive(comptime mode: Mode) type { return null; } + pub fn next(self: *Self) ?*Element { + return self.nextTw(&self._tw); + } + pub fn nextTw(self: *Self, tw: *TW) ?*Element { while (tw.next()) |node| { if (self.matches(node)) { @@ -297,7 +301,7 @@ pub fn NodeLive(comptime mode: Mode) type { if (el._type != .html) return false; const html = el._type.html; return switch (html._type) { - .input, .button, .select, .text_area => true, + .input, .button, .select, .textarea => true, else => false, }; } diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index 8016c8be..fe88e96c 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -100,7 +100,7 @@ pub const Type = union(enum) { slot: *Slot, style: *Style, template: *Template, - text_area: *TextArea, + textarea: *TextArea, title: *Title, ul: *UL, unknown: *Unknown, @@ -156,7 +156,7 @@ pub fn className(self: *const HtmlElement) []const u8 { .slot => "[object HTMLSlotElement]", .style => "[object HTMLSyleElement]", .template => "[object HTMLTemplateElement]", - .text_area => "[object HTMLTextAreaElement]", + .textarea => "[object HTMLTextAreaElement]", .title => "[object HTMLTitleElement]", .ul => "[object HTMLULElement]", .unknown => "[object HTMLUnknownElement]", diff --git a/src/browser/webapi/element/html/Button.zig b/src/browser/webapi/element/html/Button.zig index acf076e6..d58e0cbb 100644 --- a/src/browser/webapi/element/html/Button.zig +++ b/src/browser/webapi/element/html/Button.zig @@ -58,6 +58,14 @@ pub fn setName(self: *Button, name: []const u8, page: *Page) !void { try self.asElement().setAttributeSafe("name", name, page); } +pub fn getValue(self: *const Button) []const u8 { + return self.asConstElement().getAttributeSafe("value") orelse ""; +} + +pub fn setValue(self: *Button, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe("value", value, page); +} + pub fn getRequired(self: *const Button) bool { return self.asConstElement().getAttributeSafe("required") != null; } @@ -107,6 +115,7 @@ pub const JsApi = struct { 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 value = bridge.accessor(Button.getValue, Button.setValue, .{}); }; pub const Build = struct { diff --git a/src/browser/webapi/element/html/Form.zig b/src/browser/webapi/element/html/Form.zig index 4e3186a2..ac89948b 100644 --- a/src/browser/webapi/element/html/Form.zig +++ b/src/browser/webapi/element/html/Form.zig @@ -25,10 +25,10 @@ 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"); -const Select = @import("Select.zig"); -const TextArea = @import("TextArea.zig"); +pub const Input = @import("Input.zig"); +pub const Button = @import("Button.zig"); +pub const Select = @import("Select.zig"); +pub const TextArea = @import("TextArea.zig"); const Form = @This(); _proto: *HtmlElement, diff --git a/src/browser/webapi/element/html/Select.zig b/src/browser/webapi/element/html/Select.zig index 8a5ef4d2..ddac4b6d 100644 --- a/src/browser/webapi/element/html/Select.zig +++ b/src/browser/webapi/element/html/Select.zig @@ -24,7 +24,7 @@ const Element = @import("../../Element.zig"); const HtmlElement = @import("../Html.zig"); const collections = @import("../../collections.zig"); const Form = @import("Form.zig"); -const Option = @import("Option.zig"); +pub const Option = @import("Option.zig"); const Select = @This(); @@ -50,12 +50,16 @@ pub fn getValue(self: *Select, page: *Page) []const u8 { var iter = self.asNode().childrenIterator(); while (iter.next()) |child| { const option = child.is(Option) orelse continue; - if (first_option == null) { - first_option = option; + if (option.getDisabled()) { + continue; } + if (option.getSelected()) { return option.getValue(page); } + if (first_option == null) { + first_option = option; + } } // No explicitly selected option, return first option's value if (first_option) |opt| { diff --git a/src/browser/webapi/net/FormData.zig b/src/browser/webapi/net/FormData.zig index f8655246..1c5b8314 100644 --- a/src/browser/webapi/net/FormData.zig +++ b/src/browser/webapi/net/FormData.zig @@ -26,19 +26,17 @@ const Form = @import("../element/html/Form.zig"); const Element = @import("../Element.zig"); const KeyValueList = @import("../KeyValueList.zig"); -const Alloctor = std.mem.Allocator; +const Allocator = std.mem.Allocator; const FormData = @This(); -_arena: Alloctor, +_arena: Allocator, _list: KeyValueList, -pub fn init(form_: ?*Form, submitter_: ?*Element, page: *Page) !*FormData { - _ = form_; - _ = submitter_; +pub fn init(form: ?*Form, submitter: ?*Element, page: *Page) !*FormData { return page._factory.create(FormData{ ._arena = page.arena, - ._list = KeyValueList.init(), + ._list = try collectForm(page.arena, form, submitter, page), }); } @@ -108,6 +106,82 @@ pub const Iterator = struct { } }; +fn collectForm(arena: Allocator, form_: ?*Form, submitter_: ?*Element, page: *Page) !KeyValueList { + var list: KeyValueList = .empty; + const form = form_ orelse return list; + + var elements = try form.getElements(page); + var it = try elements.iterator(); + while (it.next()) |element| { + if (element.getAttributeSafe("disabled") != null) { + continue; + } + + // Handle image submitters first - they can submit without a name + if (element.is(Form.Input)) |input| { + if (input._input_type == .image) { + const submitter = submitter_ orelse continue; + if (submitter != element) { + continue; + } + + const name = element.getAttributeSafe("name"); + const x_key = if (name) |n| try std.fmt.allocPrint(arena, "{s}.x", .{n}) else "x"; + const y_key = if (name) |n| try std.fmt.allocPrint(arena, "{s}.y", .{n}) else "y"; + try list.append(arena, x_key, "0"); + try list.append(arena, y_key, "0"); + continue; + } + } + + const name = element.getAttributeSafe("name") orelse continue; + const value = blk: { + if (element.is(Form.Input)) |input| { + const input_type = input._input_type; + if (input_type == .checkbox or input_type == .radio) { + if (!input.getChecked()) { + continue; + } + } + if (input_type == .submit) { + const submitter = submitter_ orelse continue; + if (submitter != element) { + continue; + } + } + break :blk input.getValue(); + } + + if (element.is(Form.Select)) |select| { + if (select.getMultiple() == false) { + break :blk select.getValue(page); + } + + var options = try select.getSelectedOptions(page); + while (options.next()) |option| { + try list.append(arena, name, option.as(Form.Select.Option).getValue(page)); + } + continue; + } + + if (element.is(Form.TextArea)) |textarea| { + break :blk textarea.getValue(); + } + + if (submitter_) |submitter| { + if (submitter == element) { + // The form iterator only yields form controls. If we're here + // all other control types have been handled. So the cast is safe. + break :blk element.as(Form.Button).getValue(); + } + } + continue; + }; + try list.append(arena, name, value); + } + return list; +} + pub const JsApi = struct { pub const bridge = js.Bridge(FormData); @@ -131,97 +205,6 @@ 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", .{});