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", .{});