mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 07:03:29 +00:00
Merge pull request #669 from lightpanda-io/form_data_from_form
FormData constructor form & submitter parameter
This commit is contained in:
@@ -62,9 +62,9 @@ pub const SessionState = struct {
|
||||
// exists for the entire rendering of the page
|
||||
call_arena: std.mem.Allocator = undefined,
|
||||
|
||||
pub fn getNodeWrapper(self: *SessionState, comptime T: type, node: *parser.Node) !*T {
|
||||
if (parser.nodeGetEmbedderData(node)) |wrap| {
|
||||
return @alignCast(@ptrCast(wrap));
|
||||
pub fn getOrCreateNodeWrapper(self: *SessionState, comptime T: type, node: *parser.Node) !*T {
|
||||
if (try self.getNodeWrapper(T, node)) |wrap| {
|
||||
return wrap;
|
||||
}
|
||||
|
||||
const wrap = try self.arena.create(T);
|
||||
@@ -73,4 +73,11 @@ pub const SessionState = struct {
|
||||
parser.nodeSetEmbedderData(node, wrap);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
pub fn getNodeWrapper(_: *SessionState, comptime T: type, node: *parser.Node) !?*T {
|
||||
if (parser.nodeGetEmbedderData(node)) |wrap| {
|
||||
return @alignCast(@ptrCast(wrap));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -187,7 +187,7 @@ pub const HTMLDocument = struct {
|
||||
}
|
||||
|
||||
pub fn get_readyState(node: *parser.DocumentHTML, state: *SessionState) ![]const u8 {
|
||||
const self = try state.getNodeWrapper(HTMLDocument, @ptrCast(node));
|
||||
const self = try state.getOrCreateNodeWrapper(HTMLDocument, @ptrCast(node));
|
||||
return @tagName(self.ready_state);
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ pub const HTMLDocument = struct {
|
||||
}
|
||||
|
||||
pub fn documentIsLoaded(html_doc: *parser.DocumentHTML, state: *SessionState) !void {
|
||||
const self = try state.getNodeWrapper(HTMLDocument, @ptrCast(html_doc));
|
||||
const self = try state.getOrCreateNodeWrapper(HTMLDocument, @ptrCast(html_doc));
|
||||
self.ready_state = .interactive;
|
||||
|
||||
const evt = try parser.eventCreate();
|
||||
@@ -277,7 +277,7 @@ pub const HTMLDocument = struct {
|
||||
}
|
||||
|
||||
pub fn documentIsComplete(html_doc: *parser.DocumentHTML, state: *SessionState) !void {
|
||||
const self = try state.getNodeWrapper(HTMLDocument, @ptrCast(html_doc));
|
||||
const self = try state.getOrCreateNodeWrapper(HTMLDocument, @ptrCast(html_doc));
|
||||
self.ready_state = .complete;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -47,7 +47,6 @@ pub const Interfaces = .{
|
||||
HTMLEmbedElement,
|
||||
HTMLFieldSetElement,
|
||||
HTMLFontElement,
|
||||
HTMLFormElement,
|
||||
HTMLFrameElement,
|
||||
HTMLFrameSetElement,
|
||||
HTMLHRElement,
|
||||
@@ -77,7 +76,6 @@ pub const Interfaces = .{
|
||||
HTMLProgressElement,
|
||||
HTMLQuoteElement,
|
||||
HTMLScriptElement,
|
||||
HTMLSelectElement,
|
||||
HTMLSourceElement,
|
||||
HTMLSpanElement,
|
||||
HTMLStyleElement,
|
||||
@@ -95,6 +93,9 @@ pub const Interfaces = .{
|
||||
HTMLUListElement,
|
||||
HTMLVideoElement,
|
||||
CSSProperties,
|
||||
|
||||
@import("form.zig").HTMLFormElement,
|
||||
@import("select.zig").HTMLSelectElement,
|
||||
};
|
||||
|
||||
pub const Union = generate.Union(Interfaces);
|
||||
@@ -516,12 +517,6 @@ pub const HTMLFontElement = struct {
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLFormElement = struct {
|
||||
pub const Self = parser.Form;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLFrameElement = struct {
|
||||
pub const Self = parser.Frame;
|
||||
pub const prototype = *HTMLElement;
|
||||
@@ -806,12 +801,6 @@ pub const HTMLScriptElement = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLSelectElement = struct {
|
||||
pub const Self = parser.Select;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLSourceElement = struct {
|
||||
pub const Self = parser.Source;
|
||||
pub const prototype = *HTMLElement;
|
||||
|
||||
54
src/browser/html/form.zig
Normal file
54
src/browser/html/form.zig
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright (C) 2023-2024 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 Allocator = std.mem.Allocator;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const HTMLElement = @import("elements.zig").HTMLElement;
|
||||
const FormData = @import("../xhr/form_data.zig").FormData;
|
||||
|
||||
pub const HTMLFormElement = struct {
|
||||
pub const Self = parser.Form;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn _requestSubmit(self: *parser.Form) !void {
|
||||
try parser.formElementSubmit(self);
|
||||
}
|
||||
|
||||
pub fn _reset(self: *parser.Form) !void {
|
||||
try parser.formElementReset(self);
|
||||
}
|
||||
};
|
||||
|
||||
pub const Submission = struct {
|
||||
method: ?[]const u8,
|
||||
form_data: FormData,
|
||||
};
|
||||
|
||||
pub fn processSubmission(arena: Allocator, form: *parser.Form) !?Submission {
|
||||
const form_element: *parser.Element = @ptrCast(form);
|
||||
const method = try parser.elementGetAttribute(form_element, "method");
|
||||
|
||||
return .{
|
||||
.method = method,
|
||||
.form_data = try FormData.fromForm(arena, form),
|
||||
};
|
||||
}
|
||||
|
||||
// Check xhr/form_data.zig for tests
|
||||
158
src/browser/html/select.zig
Normal file
158
src/browser/html/select.zig
Normal file
@@ -0,0 +1,158 @@
|
||||
// Copyright (C) 2023-2024 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 parser = @import("../netsurf.zig");
|
||||
const HTMLElement = @import("elements.zig").HTMLElement;
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
pub const HTMLSelectElement = struct {
|
||||
pub const Self = parser.Select;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
// By default, if no option is explicitly selected, the first option should
|
||||
// be selected. However, libdom doesn't do this, and it sets the
|
||||
// selectedIndex to -1, which is a valid value for "nothing selected".
|
||||
// Therefore, when libdom says the selectedIndex == -1, we don't know if
|
||||
// it means that nothing is selected, or if the first option is selected by
|
||||
// default.
|
||||
// There are cases where this won't work, but when selectedIndex is
|
||||
// explicitly set, we set this boolean flag. Then, when we're getting then
|
||||
// selectedIndex, if this flag is == false, which is to say that if
|
||||
// selectedIndex hasn't been explicitly set AND if we have at least 1 option
|
||||
// AND if it isn't a multi select, we can make the 1st item selected by
|
||||
// default (by returning selectedIndex == 0).
|
||||
explicit_index_set: bool = false,
|
||||
|
||||
pub fn get_length(select: *parser.Select) !u32 {
|
||||
return parser.selectGetLength(select);
|
||||
}
|
||||
|
||||
pub fn get_form(select: *parser.Select) !?*parser.Form {
|
||||
return parser.selectGetForm(select);
|
||||
}
|
||||
|
||||
pub fn get_name(select: *parser.Select) ![]const u8 {
|
||||
return parser.selectGetName(select);
|
||||
}
|
||||
pub fn set_name(select: *parser.Select, name: []const u8) !void {
|
||||
return parser.selectSetName(select, name);
|
||||
}
|
||||
|
||||
pub fn get_disabled(select: *parser.Select) !bool {
|
||||
return parser.selectGetDisabled(select);
|
||||
}
|
||||
pub fn set_disabled(select: *parser.Select, disabled: bool) !void {
|
||||
return parser.selectSetDisabled(select, disabled);
|
||||
}
|
||||
|
||||
pub fn get_multiple(select: *parser.Select) !bool {
|
||||
return parser.selectGetMultiple(select);
|
||||
}
|
||||
pub fn set_multiple(select: *parser.Select, multiple: bool) !void {
|
||||
return parser.selectSetMultiple(select, multiple);
|
||||
}
|
||||
|
||||
pub fn get_selectedIndex(select: *parser.Select, state: *SessionState) !i32 {
|
||||
const self = try state.getOrCreateNodeWrapper(HTMLSelectElement, @ptrCast(select));
|
||||
const selected_index = try parser.selectGetSelectedIndex(select);
|
||||
|
||||
// See the explicit_index_set field documentation
|
||||
if (!self.explicit_index_set) {
|
||||
if (selected_index == -1) {
|
||||
if (try parser.selectGetMultiple(select) == false) {
|
||||
if (try get_length(select) > 0) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected_index;
|
||||
}
|
||||
|
||||
// Libdom's dom_html_select_select_set_selected_index will crash if index
|
||||
// is out of range, and it doesn't properly unset options
|
||||
pub fn set_selectedIndex(select: *parser.Select, index: i32, state: *SessionState) !void {
|
||||
var self = try state.getOrCreateNodeWrapper(HTMLSelectElement, @ptrCast(select));
|
||||
self.explicit_index_set = true;
|
||||
|
||||
const options = try parser.selectGetOptions(select);
|
||||
const len = try parser.optionCollectionGetLength(options);
|
||||
for (0..len) |i| {
|
||||
const option = try parser.optionCollectionItem(options, @intCast(i));
|
||||
try parser.optionSetSelected(option, false);
|
||||
}
|
||||
if (index >= 0 and index < try get_length(select)) {
|
||||
const option = try parser.optionCollectionItem(options, @intCast(index));
|
||||
try parser.optionSetSelected(option, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.Select" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
|
||||
\\ <form id=f1>
|
||||
\\ <select id=s1 name=s1><option>o1<option>o2</select>
|
||||
\\ </form>
|
||||
\\ <select id=s2></select>
|
||||
});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const s = document.getElementById('s1');", null },
|
||||
.{ "s.form", "[object HTMLFormElement]" },
|
||||
|
||||
.{ "document.getElementById('s2').form", "null" },
|
||||
|
||||
.{ "s.disabled", "false" },
|
||||
.{ "s.disabled = true", null },
|
||||
.{ "s.disabled", "true" },
|
||||
.{ "s.disabled = false", null },
|
||||
.{ "s.disabled", "false" },
|
||||
|
||||
.{ "s.multiple", "false" },
|
||||
.{ "s.multiple = true", null },
|
||||
.{ "s.multiple", "true" },
|
||||
.{ "s.multiple = false", null },
|
||||
.{ "s.multiple", "false" },
|
||||
|
||||
.{ "s.name;", "s1" },
|
||||
.{ "s.name = 'sel1';", null },
|
||||
.{ "s.name", "sel1" },
|
||||
|
||||
.{ "s.length;", "2" },
|
||||
|
||||
.{ "s.selectedIndex", "0" },
|
||||
.{ "s.selectedIndex = 2", null }, // out of range
|
||||
.{ "s.selectedIndex", "-1" },
|
||||
|
||||
.{ "s.selectedIndex = -1", null },
|
||||
.{ "s.selectedIndex", "-1" },
|
||||
|
||||
.{ "s.selectedIndex = 0", null },
|
||||
.{ "s.selectedIndex", "0" },
|
||||
|
||||
.{ "s.selectedIndex = 1", null },
|
||||
.{ "s.selectedIndex", "1" },
|
||||
|
||||
.{ "s.selectedIndex = -323", null },
|
||||
.{ "s.selectedIndex", "-1" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -1303,6 +1303,15 @@ pub inline fn nodeToDocument(node: *Node) *Document {
|
||||
return @as(*Document, @ptrCast(node));
|
||||
}
|
||||
|
||||
// Combination of nodeToElement + elementHTMLGetTagType
|
||||
pub fn nodeHTMLGetTagType(node: *Node) !?Tag {
|
||||
if (try nodeType(node) != .element) {
|
||||
return null;
|
||||
}
|
||||
const html_element: *ElementHTML = @ptrCast(node);
|
||||
return try elementHTMLGetTagType(html_element);
|
||||
}
|
||||
|
||||
// CharacterData
|
||||
pub const CharacterData = c.dom_characterdata;
|
||||
|
||||
@@ -1818,6 +1827,8 @@ pub const Title = c.dom_html_title_element;
|
||||
pub const Track = struct { base: *c.dom_html_element };
|
||||
pub const UList = c.dom_html_u_list_element;
|
||||
pub const Video = struct { base: *c.dom_html_element };
|
||||
pub const HTMLCollection = c.dom_html_collection;
|
||||
pub const OptionCollection = c.dom_html_options_collection;
|
||||
|
||||
// Document Fragment
|
||||
pub const DocumentFragment = c.dom_document_fragment;
|
||||
@@ -2341,3 +2352,160 @@ pub fn documentHTMLGetLocation(T: type, doc: *DocumentHTML) !?*T {
|
||||
pub fn validateName(name: []const u8) !bool {
|
||||
return c._dom_validate_name(try strFromData(name));
|
||||
}
|
||||
|
||||
// Form
|
||||
pub fn formElementSubmit(form: *Form) !void {
|
||||
const err = c.dom_html_form_element_submit(form);
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn formElementReset(form: *Form) !void {
|
||||
const err = c.dom_html_form_element_reset(form);
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn formGetCollection(form: *Form) !*HTMLCollection {
|
||||
var collection: ?*HTMLCollection = null;
|
||||
const err = c.dom_html_form_element_get_elements(form, &collection);
|
||||
try DOMErr(err);
|
||||
return collection.?;
|
||||
}
|
||||
|
||||
// TextArea
|
||||
pub fn textareaGetValue(textarea: *TextArea) ![]const u8 {
|
||||
var s_: ?*String = null;
|
||||
const err = c.dom_html_text_area_element_get_value(textarea, &s_);
|
||||
try DOMErr(err);
|
||||
const s = s_ orelse return "";
|
||||
return strToData(s);
|
||||
}
|
||||
|
||||
// Select
|
||||
pub fn selectGetOptions(select: *Select) !*OptionCollection {
|
||||
var collection: ?*OptionCollection = null;
|
||||
const err = c.dom__html_select_element_get_options(select, &collection);
|
||||
try DOMErr(err);
|
||||
return collection.?;
|
||||
}
|
||||
|
||||
pub fn selectGetDisabled(select: *Select) !bool {
|
||||
var disabled: bool = false;
|
||||
const err = c.dom_html_select_element_get_disabled(select, &disabled);
|
||||
try DOMErr(err);
|
||||
return disabled;
|
||||
}
|
||||
|
||||
pub fn selectSetDisabled(select: *Select, disabled: bool) !void {
|
||||
const err = c.dom_html_select_element_set_disabled(select, disabled);
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn selectGetMultiple(select: *Select) !bool {
|
||||
var multiple: bool = false;
|
||||
const err = c.dom_html_select_element_get_multiple(select, &multiple);
|
||||
try DOMErr(err);
|
||||
return multiple;
|
||||
}
|
||||
|
||||
pub fn selectSetMultiple(select: *Select, multiple: bool) !void {
|
||||
const err = c.dom_html_select_element_set_multiple(select, multiple);
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn selectGetName(select: *Select) ![]const u8 {
|
||||
var s_: ?*String = null;
|
||||
const err = c.dom_html_select_element_get_name(select, &s_);
|
||||
try DOMErr(err);
|
||||
const s = s_ orelse return "";
|
||||
return strToData(s);
|
||||
}
|
||||
|
||||
pub fn selectSetName(select: *Select, name: []const u8) !void {
|
||||
const err = c.dom_html_select_element_set_name(select, try strFromData(name));
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn selectGetLength(select: *Select) !u32 {
|
||||
var length: u32 = 0;
|
||||
const err = c.dom_html_select_element_get_length(select, &length);
|
||||
try DOMErr(err);
|
||||
return length;
|
||||
}
|
||||
|
||||
pub fn selectGetSelectedIndex(select: *Select) !i32 {
|
||||
var index: i32 = 0;
|
||||
const err = c.dom_html_select_element_get_selected_index(select, &index);
|
||||
try DOMErr(err);
|
||||
return index;
|
||||
}
|
||||
|
||||
pub fn selectSetSelectedIndex(select: *Select, index: i32) !void {
|
||||
const err = c.dom_html_select_element_set_selected_index(select, index);
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn selectGetForm(select: *Select) !?*Form {
|
||||
var form: ?*Form = null;
|
||||
const err = c.dom_html_select_element_get_form(select, &form);
|
||||
try DOMErr(err);
|
||||
return form;
|
||||
}
|
||||
|
||||
// OptionCollection
|
||||
pub fn optionCollectionGetLength(collection: *OptionCollection) !u32 {
|
||||
var len: u32 = 0;
|
||||
const err = c.dom_html_options_collection_get_length(collection, &len);
|
||||
try DOMErr(err);
|
||||
return len;
|
||||
}
|
||||
|
||||
pub fn optionCollectionItem(collection: *OptionCollection, index: u32) !*Option {
|
||||
var node: ?*NodeExternal = undefined;
|
||||
const err = c.dom_html_options_collection_item(collection, index, &node);
|
||||
try DOMErr(err);
|
||||
return @ptrCast(node.?);
|
||||
}
|
||||
|
||||
// Option
|
||||
pub fn optionGetValue(option: *Option) ![]const u8 {
|
||||
var s_: ?*String = null;
|
||||
const err = c.dom_html_option_element_get_value(option, &s_);
|
||||
try DOMErr(err);
|
||||
const s = s_ orelse return "";
|
||||
return strToData(s);
|
||||
}
|
||||
|
||||
pub fn optionGetSelected(option: *Option) !bool {
|
||||
var selected: bool = false;
|
||||
const err = c.dom_html_option_element_get_selected(option, &selected);
|
||||
try DOMErr(err);
|
||||
return selected;
|
||||
}
|
||||
|
||||
pub fn optionSetSelected(option: *Option, selected: bool) !void {
|
||||
const err = c.dom_html_option_element_set_selected(option, selected);
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
// Input
|
||||
pub fn inputGetChecked(input: *Input) !bool {
|
||||
var b: bool = false;
|
||||
const err = c.dom_html_input_element_get_checked(input, &b);
|
||||
try DOMErr(err);
|
||||
return b;
|
||||
}
|
||||
|
||||
// HtmlCollection
|
||||
pub fn htmlCollectionGetLength(collection: *HTMLCollection) !u32 {
|
||||
var len: u32 = 0;
|
||||
const err = c.dom_html_collection_get_length(collection, &len);
|
||||
try DOMErr(err);
|
||||
return len;
|
||||
}
|
||||
|
||||
pub fn htmlCollectionItem(collection: *HTMLCollection, index: u32) !*Node {
|
||||
var node: ?*NodeExternal = undefined;
|
||||
const err = c.dom_html_collection_item(collection, index, &node);
|
||||
try DOMErr(err);
|
||||
return @ptrCast(node.?);
|
||||
}
|
||||
|
||||
@@ -515,14 +515,9 @@ pub const Page = struct {
|
||||
|
||||
fn _windowClicked(self: *Page, event: *parser.Event) !void {
|
||||
const target = (try parser.eventTarget(event)) orelse return;
|
||||
|
||||
const node = parser.eventTargetToNode(target);
|
||||
if (try parser.nodeType(node) != .element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const html_element: *parser.ElementHTML = @ptrCast(node);
|
||||
switch (try parser.elementHTMLGetTagType(html_element)) {
|
||||
const tag = (try parser.nodeHTMLGetTagType(node)) orelse return;
|
||||
switch (tag) {
|
||||
.a => {
|
||||
const element: *parser.Element = @ptrCast(node);
|
||||
const href = (try parser.elementGetAttribute(element, "href")) orelse return;
|
||||
|
||||
@@ -20,9 +20,12 @@ const std = @import("std");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const iterator = @import("../iterator/iterator.zig");
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
const log = std.log.scoped(.form_data);
|
||||
|
||||
pub const Interfaces = .{
|
||||
FormData,
|
||||
KeyIterable,
|
||||
@@ -51,10 +54,20 @@ pub const Interfaces = .{
|
||||
pub const FormData = struct {
|
||||
entries: std.ArrayListUnmanaged(Entry),
|
||||
|
||||
pub fn constructor() FormData {
|
||||
return .{
|
||||
.entries = .empty,
|
||||
pub fn constructor(form_: ?*parser.Form, submitter_: ?*parser.ElementHTML, state: *SessionState) !FormData {
|
||||
const form = form_ orelse return .{ .entries = .empty };
|
||||
return fromForm(form, submitter_, state, .{});
|
||||
}
|
||||
|
||||
const FromFormOpts = struct {
|
||||
// Uses the state.arena if null. This is needed for when we're handling
|
||||
// form submission from the Page, and we want to capture the form within
|
||||
// the session's transfer_arena.
|
||||
allocator: ?Allocator = null,
|
||||
};
|
||||
pub fn fromForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, state: *SessionState, opts: FromFormOpts) !FormData {
|
||||
const entries = try collectForm(opts.allocator orelse state.arena, form, submitter_, state);
|
||||
return .{ .entries = entries };
|
||||
}
|
||||
|
||||
pub fn _get(self: *const FormData, key: []const u8) ?[]const u8 {
|
||||
@@ -186,9 +199,181 @@ const EntryIterator = struct {
|
||||
}
|
||||
};
|
||||
|
||||
fn collectForm(arena: Allocator, form: *parser.Form, submitter_: ?*parser.ElementHTML, state: *SessionState) !std.ArrayListUnmanaged(Entry) {
|
||||
const collection = try parser.formGetCollection(form);
|
||||
const len = try parser.htmlCollectionGetLength(collection);
|
||||
|
||||
var entries: std.ArrayListUnmanaged(Entry) = .empty;
|
||||
try entries.ensureTotalCapacity(arena, len);
|
||||
|
||||
const submitter_name_ = try getSubmitterName(submitter_);
|
||||
|
||||
for (0..len) |i| {
|
||||
const node = try parser.htmlCollectionItem(collection, @intCast(i));
|
||||
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.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(element)));
|
||||
switch (tag) {
|
||||
.input => {
|
||||
const tpe = try parser.elementGetAttribute(element, "type") orelse "";
|
||||
if (std.ascii.eqlIgnoreCase(tpe, "image")) {
|
||||
if (submitter_name_) |submitter_name| {
|
||||
if (std.mem.eql(u8, submitter_name, name)) {
|
||||
try entries.append(arena, .{
|
||||
.key = try std.fmt.allocPrint(arena, "{s}.x", .{name}),
|
||||
.value = "0",
|
||||
});
|
||||
try entries.append(arena, .{
|
||||
.key = try std.fmt.allocPrint(arena, "{s}.y", .{name}),
|
||||
.value = "0",
|
||||
});
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
const value = (try parser.elementGetAttribute(element, "value")) orelse "";
|
||||
try entries.append(arena, .{ .key = name, .value = value });
|
||||
},
|
||||
.select => {
|
||||
const select: *parser.Select = @ptrCast(node);
|
||||
try collectSelectValues(arena, select, name, &entries, state);
|
||||
},
|
||||
.textarea => {
|
||||
const textarea: *parser.TextArea = @ptrCast(node);
|
||||
const value = try parser.textareaGetValue(textarea);
|
||||
try entries.append(arena, .{ .key = name, .value = 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.append(arena, .{ .key = name, .value = value });
|
||||
}
|
||||
},
|
||||
else => {
|
||||
log.warn("unsupported form element: {s}\n", .{@tagName(tag)});
|
||||
continue;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
fn collectSelectValues(arena: Allocator, select: *parser.Select, name: []const u8, entries: *std.ArrayListUnmanaged(Entry), state: *SessionState) !void {
|
||||
const HTMLSelectElement = @import("../html/select.zig").HTMLSelectElement;
|
||||
|
||||
// Go through the HTMLSelectElement because it has specific logic for handling
|
||||
// the default selected option, which libdom doesn't properly handle
|
||||
const selected_index = try HTMLSelectElement.get_selectedIndex(select, state);
|
||||
if (selected_index == -1) {
|
||||
return;
|
||||
}
|
||||
std.debug.assert(selected_index >= 0);
|
||||
|
||||
const options = try parser.selectGetOptions(select);
|
||||
const is_multiple = try parser.selectGetMultiple(select);
|
||||
if (is_multiple == false) {
|
||||
const option = try parser.optionCollectionItem(options, @intCast(selected_index));
|
||||
|
||||
if (try parser.elementGetAttribute(@ptrCast(option), "disabled") != null) {
|
||||
return;
|
||||
}
|
||||
const value = try parser.optionGetValue(option);
|
||||
return entries.append(arena, .{ .key = name, .value = value });
|
||||
}
|
||||
|
||||
const len = try parser.optionCollectionGetLength(options);
|
||||
|
||||
// we can go directly to the first one
|
||||
for (@intCast(selected_index)..len) |i| {
|
||||
const option = try parser.optionCollectionItem(options, @intCast(i));
|
||||
if (try parser.elementGetAttribute(@ptrCast(option), "disabled") != null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (try parser.optionGetSelected(option)) {
|
||||
const value = try parser.optionGetValue(option);
|
||||
try entries.append(arena, .{ .key = name, .value = value });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn getSubmitterName(submitter_: ?*parser.ElementHTML) !?[]const u8 {
|
||||
const submitter = submitter_ orelse return null;
|
||||
|
||||
const tag = try parser.elementHTMLGetTagType(submitter);
|
||||
const element: *parser.Element = @ptrCast(submitter);
|
||||
const name = try parser.elementGetAttribute(element, "name");
|
||||
|
||||
switch (tag) {
|
||||
.button => return name,
|
||||
.input => {
|
||||
const tpe = (try parser.elementGetAttribute(element, "type")) orelse "";
|
||||
// only an image type can be a sumbitter
|
||||
if (std.ascii.eqlIgnoreCase(tpe, "image") or std.ascii.eqlIgnoreCase(tpe, "submit")) {
|
||||
return name;
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
return error.InvalidArgument;
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "FormData" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
test "Browser.FormData" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
|
||||
\\ <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-2-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>
|
||||
});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
@@ -244,4 +429,31 @@ test "FormData" {
|
||||
.{ "acc = [];", null },
|
||||
.{ "for (const entry of f) { acc.push(entry) }; acc;", "b,3" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let form1 = document.getElementById('form1')", null },
|
||||
.{ "let submit1 = document.getElementById('s1')", null },
|
||||
.{ "let f2 = new FormData(form1, submit1)", null },
|
||||
.{ "acc = '';", null },
|
||||
.{
|
||||
\\ for (const entry of f2) {
|
||||
\\ acc += entry[0] + '=' + entry[1] + '\n';
|
||||
\\ };
|
||||
\\ acc.slice(0, -1)
|
||||
,
|
||||
\\txt-1=txt-1-v
|
||||
\\txt-2=txt-2-v
|
||||
\\chk-3=chk-3-vb
|
||||
\\chk-3=chk-3-vc
|
||||
\\rdi-1=rdi-1-vc
|
||||
\\ta-1= ta-1-v
|
||||
\\ta=
|
||||
\\h1=h1-v
|
||||
\\sel-1=blue
|
||||
\\sel-2=sel-2-v
|
||||
\\mlt-2=water
|
||||
\\mlt-2=tea
|
||||
\\s1=s1-v
|
||||
},
|
||||
}, .{});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user