mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-08-01 02:06:17 -04:00
This continues some previous fixes aimed at iframe/popup scenarios where the DOM is manipulated from a different frame. The simplest example is: iframe.contentDocument.domain should return the domain of the iframe, not the domain of the context this is being run in. For a case like this, it's easy, use self._frame rather than the calling context's frame. Besides these easy cases, there are two other changes here: 1 - ResolveURL has to get an Element's owning document's frame 2 - getElementById has to get the lookup from the element's document As part of #2, `Frame.getElementByIdFromNode` was optimized to avoid the double document lookup. Rather than calling isConnected() then finding the root node, we can just call getRootNode().
271 lines
11 KiB
Zig
271 lines
11 KiB
Zig
// 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 Frame = @import("../../../Frame.zig");
|
|
|
|
const Node = @import("../../Node.zig");
|
|
const Element = @import("../../Element.zig");
|
|
const HtmlElement = @import("../Html.zig");
|
|
const collections = @import("../../collections.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,
|
|
|
|
// Prevents submission of the form while we're in the process of submitting
|
|
// the form. You can imagine an onsubmit = () => form.submit() endless loop.
|
|
_firing_submission_events: bool = false,
|
|
|
|
// Prevents submission of the form while we're building the entry list for the
|
|
// form. You can imagine an formdata = () => form.submit() endless loop.
|
|
_constructing_entry_list: bool = false,
|
|
|
|
pub fn asHtmlElement(self: *Form) *HtmlElement {
|
|
return self._proto;
|
|
}
|
|
fn asConstElement(self: *const Form) *const Element {
|
|
return self._proto._proto;
|
|
}
|
|
pub fn asElement(self: *Form) *Element {
|
|
return self._proto._proto;
|
|
}
|
|
pub fn asNode(self: *Form) *Node {
|
|
return self.asElement().asNode();
|
|
}
|
|
|
|
pub fn getName(self: *const Form) []const u8 {
|
|
return self.asConstElement().getAttributeSafe(comptime .wrap("name")) orelse "";
|
|
}
|
|
|
|
pub fn setName(self: *Form, name: []const u8, frame: *Frame) !void {
|
|
try self.asElement().setAttributeSafe(comptime .wrap("name"), .wrap(name), frame);
|
|
}
|
|
|
|
/// Canonicalize the `method` content attribute (or its `formmethod` submitter
|
|
/// override) per WHATWG HTML "limited to only known values":
|
|
/// - missing → returns `missing_default`
|
|
/// - "post" / "dialog" → returns the lowercased keyword
|
|
/// - empty / invalid / "get" → returns "get" (invalid-value default)
|
|
pub fn normalizeMethod(attr: ?[]const u8, missing_default: []const u8) []const u8 {
|
|
const method = attr orelse return missing_default;
|
|
if (std.ascii.eqlIgnoreCase(method, "post")) return "post";
|
|
if (std.ascii.eqlIgnoreCase(method, "dialog")) return "dialog";
|
|
return "get";
|
|
}
|
|
|
|
/// Canonicalize the `enctype` content attribute (or its `formenctype` submitter
|
|
/// override) per WHATWG HTML "limited to only known values":
|
|
/// - missing → returns `missing_default`
|
|
/// - "multipart/form-data" / "text/plain" → returns the lowercased keyword
|
|
/// - empty / invalid / urlencoded → returns "application/x-www-form-urlencoded"
|
|
pub fn normalizeEnctype(attr: ?[]const u8, missing_default: []const u8) []const u8 {
|
|
const enctype = attr orelse return missing_default;
|
|
if (std.ascii.eqlIgnoreCase(enctype, "multipart/form-data")) return "multipart/form-data";
|
|
if (std.ascii.eqlIgnoreCase(enctype, "text/plain")) return "text/plain";
|
|
return "application/x-www-form-urlencoded";
|
|
}
|
|
|
|
pub fn getMethod(self: *const Form) []const u8 {
|
|
return normalizeMethod(self.asConstElement().getAttributeSafe(comptime .wrap("method")), "get");
|
|
}
|
|
|
|
pub fn setMethod(self: *Form, method: []const u8, frame: *Frame) !void {
|
|
try self.asElement().setAttributeSafe(comptime .wrap("method"), .wrap(method), frame);
|
|
}
|
|
|
|
pub fn getElements(self: *Form, frame: *Frame) !*collections.HTMLFormControlsCollection {
|
|
const node_live = self.iterator(frame);
|
|
const html_collection = try node_live.runtimeGenericWrap(frame);
|
|
|
|
return frame._factory.create(collections.HTMLFormControlsCollection{
|
|
._proto = html_collection,
|
|
});
|
|
}
|
|
|
|
pub fn iterator(self: *Form, frame: *Frame) collections.NodeLive(.form) {
|
|
const form_id = self.asElement().getAttributeSafe(comptime .wrap("id"));
|
|
const root = if (form_id != null)
|
|
self.asNode().getRootNode(.{}) // Has ID: walk entire document to find form=ID controls
|
|
else
|
|
self.asNode(); // No ID: walk only form subtree (no external controls possible)
|
|
|
|
return collections.NodeLive(.form).init(root, .{ .form = self, .form_id = form_id }, frame);
|
|
}
|
|
|
|
pub fn getAction(self: *Form, frame: *Frame) ![]const u8 {
|
|
const element = self.asElement();
|
|
const owner_url = element.asNode().ownerFrame(frame).url;
|
|
const action = element.getAttributeSafe(comptime .wrap("action")) orelse return owner_url;
|
|
if (action.len == 0) {
|
|
return owner_url;
|
|
}
|
|
return element.asNode().resolveURL(action, frame, .{});
|
|
}
|
|
|
|
pub fn setAction(self: *Form, value: []const u8, frame: *Frame) !void {
|
|
try self.asElement().setAttributeSafe(comptime .wrap("action"), .wrap(value), frame);
|
|
}
|
|
|
|
pub fn getTarget(self: *Form) []const u8 {
|
|
return self.asElement().getAttributeSafe(comptime .wrap("target")) orelse "";
|
|
}
|
|
|
|
pub fn setTarget(self: *Form, value: []const u8, frame: *Frame) !void {
|
|
try self.asElement().setAttributeSafe(comptime .wrap("target"), .wrap(value), frame);
|
|
}
|
|
|
|
pub fn getAcceptCharset(self: *Form) []const u8 {
|
|
return self.asElement().getAttributeSafe(.wrap("accept-charset")) orelse "";
|
|
}
|
|
|
|
pub fn setAcceptCharset(self: *Form, value: []const u8, frame: *Frame) !void {
|
|
try self.asElement().setAttributeSafe(.wrap("accept-charset"), .wrap(value), frame);
|
|
}
|
|
|
|
pub fn getNoValidate(self: *const Form) bool {
|
|
return self.asConstElement().getAttributeSafe(comptime .wrap("novalidate")) != null;
|
|
}
|
|
|
|
pub fn setNoValidate(self: *Form, value: bool, frame: *Frame) !void {
|
|
if (value) {
|
|
try self.asElement().setAttributeSafe(comptime .wrap("novalidate"), .wrap(""), frame);
|
|
} else {
|
|
try self.asElement().removeAttribute(comptime .wrap("novalidate"), frame);
|
|
}
|
|
}
|
|
|
|
pub fn getEnctype(self: *const Form) []const u8 {
|
|
return normalizeEnctype(self.asConstElement().getAttributeSafe(comptime .wrap("enctype")), "application/x-www-form-urlencoded");
|
|
}
|
|
|
|
pub fn setEnctype(self: *Form, value: []const u8, frame: *Frame) !void {
|
|
try self.asElement().setAttributeSafe(comptime .wrap("enctype"), .wrap(value), frame);
|
|
}
|
|
|
|
pub fn getLength(self: *Form, frame: *Frame) !u32 {
|
|
const elements = try self.getElements(frame);
|
|
return elements.length(frame);
|
|
}
|
|
|
|
pub fn submit(self: *Form, frame: *Frame) !void {
|
|
return frame.submitForm(null, self, .{ .fire_event = false });
|
|
}
|
|
|
|
/// https://html.spec.whatwg.org/multipage/forms.html#dom-form-requestsubmit
|
|
/// Like submit(), but fires the submit event and validates the form.
|
|
pub fn requestSubmit(self: *Form, submitter: ?*Element, frame: *Frame) !void {
|
|
const submitter_element = if (submitter) |s| blk: {
|
|
// The submitter must be a submit button.
|
|
if (!isSubmitButton(s)) return error.TypeError;
|
|
|
|
// The submitter's form owner must be this form element.
|
|
const submitter_form = getFormOwner(s, frame);
|
|
if (submitter_form == null or submitter_form.? != self) return error.NotFound;
|
|
|
|
break :blk s;
|
|
} else self.asElement();
|
|
|
|
return frame.submitForm(submitter_element, self, .{});
|
|
}
|
|
|
|
/// Returns true if the element is a submit button per the HTML spec:
|
|
/// - <input type="submit"> or <input type="image">
|
|
/// - <button type="submit"> (including default, since button's default type is "submit")
|
|
pub fn isSubmitButton(element: *Element) bool {
|
|
if (element.is(Input)) |input| {
|
|
return input._input_type == .submit or input._input_type == .image;
|
|
}
|
|
if (element.is(Button)) |button| {
|
|
return std.mem.eql(u8, button.getType(), "submit");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Returns the form owner of a submittable element (Input or Button).
|
|
fn getFormOwner(element: *Element, frame: *Frame) ?*Form {
|
|
if (element.is(Input)) |input| {
|
|
return input.getForm(frame);
|
|
}
|
|
if (element.is(Button)) |button| {
|
|
return button.getForm(frame);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-form-checkvalidity
|
|
/// Returns true if every submittable element in the form is valid. Fires an
|
|
/// `invalid` event on each failing element.
|
|
pub fn checkValidity(self: *Form, frame: *Frame) !bool {
|
|
var iter = self.iterator(frame);
|
|
var all_valid = true;
|
|
while (iter.next()) |element| {
|
|
const ok = try checkElementValidity(element, frame);
|
|
if (!ok) all_valid = false;
|
|
}
|
|
return all_valid;
|
|
}
|
|
|
|
/// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-form-reportvalidity
|
|
/// Headless: identical to checkValidity (no UI to draw).
|
|
pub fn reportValidity(self: *Form, frame: *Frame) !bool {
|
|
return self.checkValidity(frame);
|
|
}
|
|
|
|
fn checkElementValidity(element: *Element, frame: *Frame) !bool {
|
|
if (element.is(Input)) |input| return input.checkValidity(frame);
|
|
if (element.is(Select)) |select| return select.checkValidity(frame);
|
|
if (element.is(TextArea)) |textarea| return textarea.checkValidity(frame);
|
|
if (element.is(Button)) |button| return button.checkValidity(frame);
|
|
return true;
|
|
}
|
|
|
|
pub const JsApi = struct {
|
|
pub const bridge = js.Bridge(Form);
|
|
pub const Meta = struct {
|
|
pub const name = "HTMLFormElement";
|
|
pub const prototype_chain = bridge.prototypeChain();
|
|
pub var class_id: bridge.ClassId = undefined;
|
|
};
|
|
|
|
pub const name = bridge.accessor(Form.getName, Form.setName, .{ .ce_reactions = true });
|
|
pub const method = bridge.accessor(Form.getMethod, Form.setMethod, .{ .ce_reactions = true });
|
|
pub const action = bridge.accessor(Form.getAction, Form.setAction, .{ .ce_reactions = true });
|
|
pub const target = bridge.accessor(Form.getTarget, Form.setTarget, .{ .ce_reactions = true });
|
|
pub const acceptCharset = bridge.accessor(Form.getAcceptCharset, Form.setAcceptCharset, .{ .ce_reactions = true });
|
|
pub const enctype = bridge.accessor(Form.getEnctype, Form.setEnctype, .{ .ce_reactions = true });
|
|
pub const noValidate = bridge.accessor(Form.getNoValidate, Form.setNoValidate, .{ .ce_reactions = true });
|
|
pub const elements = bridge.accessor(Form.getElements, null, .{});
|
|
pub const length = bridge.accessor(Form.getLength, null, .{});
|
|
pub const submit = bridge.function(Form.submit, .{});
|
|
pub const requestSubmit = bridge.function(Form.requestSubmit, .{ .dom_exception = true });
|
|
pub const checkValidity = bridge.function(Form.checkValidity, .{});
|
|
pub const reportValidity = bridge.function(Form.reportValidity, .{});
|
|
};
|
|
|
|
const testing = @import("../../../../testing.zig");
|
|
test "WebApi: HTML.Form" {
|
|
try testing.htmlRunner("element/html/form.html", .{});
|
|
try testing.htmlRunner("element/html/form-validity.html", .{});
|
|
}
|