mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-02-04 06:23:45 +00:00
form submitt
This commit is contained in:
@@ -197,10 +197,16 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
||||
event._event_phase = .none;
|
||||
|
||||
// Execute default action if not prevented
|
||||
if (!event._prevent_default and event._type_string.eqlSlice("click")) {
|
||||
if (event._prevent_default) {
|
||||
// can't return in a defer (╯°□°)╯︵ ┻━┻
|
||||
} else if (event._type_string.eqlSlice("click")) {
|
||||
self.page.handleClick(target) catch |err| {
|
||||
log.warn(.event, "page.click", .{ .err = err });
|
||||
};
|
||||
} else if (event._type_string.eqlSlice("keydown")) {
|
||||
self.page.handleKeydown(target, event) catch |err| {
|
||||
log.warn(.event, "page.keydown", .{ .err = err });
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ const ScriptManager = @import("ScriptManager.zig");
|
||||
|
||||
const Parser = @import("parser/Parser.zig");
|
||||
|
||||
const URL = @import("webapi/URL.zig");
|
||||
const URL = @import("URL.zig");
|
||||
const Node = @import("webapi/Node.zig");
|
||||
const Event = @import("webapi/Event.zig");
|
||||
const CData = @import("webapi/CData.zig");
|
||||
@@ -62,7 +62,9 @@ const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig");
|
||||
const timestamp = @import("../datetime.zig").timestamp;
|
||||
const milliTimestamp = @import("../datetime.zig").milliTimestamp;
|
||||
|
||||
var default_url = URL{ ._raw = "about:blank" };
|
||||
const WebApiURL = @import("webapi/URL.zig");
|
||||
|
||||
var default_url = WebApiURL{ ._raw = "about:blank" };
|
||||
pub var default_location: Location = Location{ ._url = &default_url };
|
||||
|
||||
pub const BUF_SIZE = 1024;
|
||||
@@ -297,13 +299,11 @@ pub fn getTitle(self: *Page) !?[]const u8 {
|
||||
}
|
||||
|
||||
pub fn getOrigin(self: *Page, allocator: Allocator) !?[]const u8 {
|
||||
const URLRaw = @import("URL.zig");
|
||||
return try URLRaw.getOrigin(allocator, self.url);
|
||||
return try URL.getOrigin(allocator, self.url);
|
||||
}
|
||||
|
||||
pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool {
|
||||
const URLRaw = @import("URL.zig");
|
||||
const current_origin = (try URLRaw.getOrigin(self.call_arena, self.url)) orelse return false;
|
||||
const current_origin = (try URL.getOrigin(self.call_arena, self.url)) orelse return false;
|
||||
return std.mem.startsWith(u8, url, current_origin);
|
||||
}
|
||||
|
||||
@@ -440,7 +440,6 @@ pub fn scheduleNavigation(self: *Page, request_url: []const u8, opts: NavigateOp
|
||||
}
|
||||
|
||||
const session = self._session;
|
||||
const URLRaw = @import("URL.zig");
|
||||
|
||||
const resolved_url = try URL.resolve(
|
||||
session.transfer_arena,
|
||||
@@ -449,7 +448,7 @@ pub fn scheduleNavigation(self: *Page, request_url: []const u8, opts: NavigateOp
|
||||
.{ .always_dupe = true },
|
||||
);
|
||||
|
||||
if (!opts.force and URLRaw.eqlDocument(self.url, resolved_url)) {
|
||||
if (!opts.force and URL.eqlDocument(self.url, resolved_url)) {
|
||||
self.url = try self.arena.dupeZ(u8, resolved_url);
|
||||
self.window._location = try Location.init(self.url, self);
|
||||
self.document._location = self.window._location;
|
||||
@@ -2421,50 +2420,73 @@ pub fn triggerMouseClick(self: *Page, x: f64, y: f64) !void {
|
||||
pub fn handleClick(self: *Page, target: *Node) !void {
|
||||
// TODO: Also support <area> elements when implement
|
||||
const element = target.is(Element) orelse return;
|
||||
const anchor = element.is(Element.Html.Anchor) orelse return;
|
||||
const html_element = element.is(Element.Html) orelse return;
|
||||
|
||||
const href = element.getAttributeSafe("href") orelse return;
|
||||
if (href.len == 0) {
|
||||
return;
|
||||
switch (html_element._type) {
|
||||
.anchor => |anchor| {
|
||||
const href = element.getAttributeSafe("href") orelse return;
|
||||
if (href.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (std.mem.startsWith(u8, href, "#")) {
|
||||
// Hash-only links (#foo) should be handled as same-document navigation
|
||||
return;
|
||||
}
|
||||
|
||||
if (std.mem.startsWith(u8, href, "javascript:")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check target attribute - don't navigate if opening in new window/tab
|
||||
const target_val = anchor.getTarget();
|
||||
if (target_val.len > 0 and !std.mem.eql(u8, target_val, "_self")) {
|
||||
log.warn(.browser, "not implemented", .{
|
||||
.feature = "anchor with target attribute click",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (try element.hasAttribute("download", self)) {
|
||||
log.warn(.browser, "not implemented", .{
|
||||
.feature = "anchor with download attribute click",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try self.scheduleNavigation(href, .{
|
||||
.reason = .script,
|
||||
.kind = .{ .push = null },
|
||||
}, .anchor);
|
||||
},
|
||||
.input => |input| switch (input._input_type) {
|
||||
.submit => return self.submitForm(element, input.getForm(self)),
|
||||
else => self.window._document._active_element = element,
|
||||
},
|
||||
.button => |button| {
|
||||
if (std.mem.eql(u8, button.getType(), "submit")) {
|
||||
return self.submitForm(element, button.getForm(self));
|
||||
}
|
||||
},
|
||||
.select, .textarea => self.window._document._active_element = element,
|
||||
else => {},
|
||||
}
|
||||
|
||||
if (std.mem.startsWith(u8, href, "#")) {
|
||||
// Hash-only links (#foo) should be handled as same-document navigation
|
||||
return;
|
||||
}
|
||||
|
||||
if (std.mem.startsWith(u8, href, "javascript:")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check target attribute - don't navigate if opening in new window/tab
|
||||
const target_val = anchor.getTarget();
|
||||
if (target_val.len > 0 and !std.mem.eql(u8, target_val, "_self")) {
|
||||
log.warn(.browser, "not implemented", .{
|
||||
.feature = "anchor with target attribute click",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (try element.hasAttribute("download", self)) {
|
||||
log.warn(.browser, "not implemented", .{
|
||||
.feature = "anchor with download attribute click",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try self.scheduleNavigation(href, .{
|
||||
.reason = .script,
|
||||
.kind = .{ .push = null },
|
||||
}, .anchor);
|
||||
}
|
||||
|
||||
pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void {
|
||||
const element = self.window._document._active_element orelse return;
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.page, "page keydown", .{
|
||||
.url = self.url,
|
||||
.node = element,
|
||||
.key = keyboard_event._key,
|
||||
});
|
||||
}
|
||||
try self._event_manager.dispatch(element.asEventTarget(), keyboard_event.asEvent());
|
||||
}
|
||||
|
||||
pub fn handleKeydown(self: *Page, target: *Element, keyboard_event: *KeyboardEvent) !void {
|
||||
pub fn handleKeydown(self: *Page, target: *Node, event: *Event) !void {
|
||||
const keyboard_event = event.as(KeyboardEvent);
|
||||
const key = keyboard_event.getKey();
|
||||
|
||||
if (key == .Dead) {
|
||||
@@ -2473,11 +2495,7 @@ pub fn handleKeydown(self: *Page, target: *Element, keyboard_event: *KeyboardEve
|
||||
|
||||
if (target.is(Element.Html.Input)) |input| {
|
||||
if (key == .Enter) {
|
||||
if (input.getForm(self)) |form| {
|
||||
// TODO: Implement form submission
|
||||
_ = form;
|
||||
}
|
||||
return;
|
||||
return self.submitForm(input.asElement(), input.getForm(self));
|
||||
}
|
||||
|
||||
// Don't handle text input for radio/checkbox
|
||||
@@ -2496,14 +2514,59 @@ pub fn handleKeydown(self: *Page, target: *Element, keyboard_event: *KeyboardEve
|
||||
}
|
||||
|
||||
if (target.is(Element.Html.TextArea)) |textarea| {
|
||||
// zig fmt: off
|
||||
const append =
|
||||
if (key == .Enter) "\n" else if (key.isPrintable()) key.asString() else return;
|
||||
if (key == .Enter) "\n"
|
||||
else if (key.isPrintable()) key.asString()
|
||||
else return
|
||||
;
|
||||
// zig fmt: on
|
||||
const current_value = textarea.getValue();
|
||||
const new_value = try std.mem.concat(self.arena, u8, &.{ current_value, append });
|
||||
return textarea.setValue(new_value, self);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form) !void {
|
||||
const form = form_ orelse return;
|
||||
|
||||
if (submitter_) |submitter| {
|
||||
if (submitter.getAttributeSafe("disabled") != null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const form_element = form.asElement();
|
||||
|
||||
const FormData = @import("webapi/net/FormData.zig");
|
||||
// The submitter can be an input box (if enter was entered on the box)
|
||||
// I don't think this is technically correct, but FormData handles it ok
|
||||
const form_data = try FormData.init(form, submitter_, self);
|
||||
|
||||
const transfer_arena = self._session.transfer_arena;
|
||||
|
||||
const encoding = form_element.getAttributeSafe("enctype");
|
||||
|
||||
var buf = std.Io.Writer.Allocating.init(transfer_arena);
|
||||
try form_data.write(encoding, &buf.writer);
|
||||
|
||||
const method = form_element.getAttributeSafe("method") orelse "";
|
||||
var action = form_element.getAttributeSafe("action") orelse self.url;
|
||||
|
||||
var opts = NavigateOpts{
|
||||
.reason = .form,
|
||||
.kind = .{ .push = null },
|
||||
};
|
||||
if (std.ascii.eqlIgnoreCase(method, "post")) {
|
||||
opts.method = .POST;
|
||||
opts.body = buf.written();
|
||||
// form_data.write currently only supports this encoding, so we know this has to be the content type
|
||||
opts.header = "Content-Type: application/x-www-form-urlencoded";
|
||||
} else {
|
||||
action = try URL.concatQueryString(transfer_arena, action, buf.written());
|
||||
}
|
||||
return self.scheduleNavigation(action, opts, .form);
|
||||
}
|
||||
|
||||
const RequestCookieOpts = struct {
|
||||
is_http: bool = true,
|
||||
is_navigation: bool = false,
|
||||
|
||||
@@ -471,6 +471,30 @@ pub fn setHash(current: [:0]const u8, value: []const u8, allocator: Allocator) !
|
||||
return buildUrl(allocator, protocol, host, pathname, search, hash);
|
||||
}
|
||||
|
||||
pub fn concatQueryString(arena: Allocator, url: []const u8, query_string: []const u8) ![:0]const u8 {
|
||||
if (query_string.len == 0) {
|
||||
return arena.dupeZ(u8, url);
|
||||
}
|
||||
|
||||
var buf: std.ArrayList(u8) = .empty;
|
||||
|
||||
// the most space well need is the url + ('?' or '&') + the query_string + null terminator
|
||||
try buf.ensureTotalCapacity(arena, url.len + 2 + query_string.len);
|
||||
buf.appendSliceAssumeCapacity(url);
|
||||
|
||||
if (std.mem.indexOfScalar(u8, url, '?')) |index| {
|
||||
const last_index = url.len - 1;
|
||||
if (index != last_index and url[last_index] != '&') {
|
||||
buf.appendAssumeCapacity('&');
|
||||
}
|
||||
} else {
|
||||
buf.appendAssumeCapacity('?');
|
||||
}
|
||||
buf.appendSliceAssumeCapacity(query_string);
|
||||
buf.appendAssumeCapacity(0);
|
||||
return buf.items[0 .. buf.items.len - 1 :0];
|
||||
}
|
||||
|
||||
const KnownProtocol = enum {
|
||||
@"http:",
|
||||
@"https:",
|
||||
@@ -707,3 +731,33 @@ test "URL: eqlDocument" {
|
||||
try testing.expectEqual(false, eqlDocument(url1, url2));
|
||||
}
|
||||
}
|
||||
|
||||
test "URL: concatQueryString" {
|
||||
defer testing.reset();
|
||||
const arena = testing.arena_allocator;
|
||||
|
||||
{
|
||||
const url = try concatQueryString(arena, "https://www.lightpanda.io/", "");
|
||||
try testing.expectEqual("https://www.lightpanda.io/", url);
|
||||
}
|
||||
|
||||
{
|
||||
const url = try concatQueryString(arena, "https://www.lightpanda.io/index?", "");
|
||||
try testing.expectEqual("https://www.lightpanda.io/index?", url);
|
||||
}
|
||||
|
||||
{
|
||||
const url = try concatQueryString(arena, "https://www.lightpanda.io/index?", "a=b");
|
||||
try testing.expectEqual("https://www.lightpanda.io/index?a=b", url);
|
||||
}
|
||||
|
||||
{
|
||||
const url = try concatQueryString(arena, "https://www.lightpanda.io/index?1=2", "a=b");
|
||||
try testing.expectEqual("https://www.lightpanda.io/index?1=2&a=b", url);
|
||||
}
|
||||
|
||||
{
|
||||
const url = try concatQueryString(arena, "https://www.lightpanda.io/index?1=2&", "a=b");
|
||||
try testing.expectEqual("https://www.lightpanda.io/index?1=2&a=b", url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ pub fn is(self: *Element, comptime T: type) ?*T {
|
||||
const type_name = @typeName(T);
|
||||
switch (self._type) {
|
||||
.html => |el| {
|
||||
if (T == *Html) {
|
||||
if (T == Html) {
|
||||
return el;
|
||||
}
|
||||
if (comptime std.mem.startsWith(u8, type_name, "browser.webapi.element.html.")) {
|
||||
@@ -85,7 +85,7 @@ pub fn is(self: *Element, comptime T: type) ?*T {
|
||||
}
|
||||
},
|
||||
.svg => |svg| {
|
||||
if (T == *Svg) {
|
||||
if (T == Svg) {
|
||||
return svg;
|
||||
}
|
||||
if (comptime std.mem.startsWith(u8, type_name, "webapi.element.svg.")) {
|
||||
|
||||
@@ -85,6 +85,31 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn as(self: *Event, comptime T: type) *T {
|
||||
return self.is(T).?;
|
||||
}
|
||||
|
||||
pub fn is(self: *Event, comptime T: type) ?*T {
|
||||
switch (self._type) {
|
||||
.generic => return if (T == Event) self else null,
|
||||
.error_event => |e| return if (T == @import("event/ErrorEvent.zig")) e else null,
|
||||
.custom_event => |e| return if (T == @import("event/CustomEvent.zig")) e else null,
|
||||
.message_event => |e| return if (T == @import("event/MessageEvent.zig")) e else null,
|
||||
.progress_event => |e| return if (T == @import("event/ProgressEvent.zig")) e else null,
|
||||
.composition_event => |e| return if (T == @import("event/CompositionEvent.zig")) e else null,
|
||||
.navigation_current_entry_change_event => |e| return if (T == @import("event/NavigationCurrentEntryChangeEvent.zig")) e else null,
|
||||
.page_transition_event => |e| return if (T == @import("event/PageTransitionEvent.zig")) e else null,
|
||||
.pop_state_event => |e| return if (T == @import("event/PopStateEvent.zig")) e else null,
|
||||
.ui_event => |e| {
|
||||
if (T == @import("event/UIEvent.zig")) {
|
||||
return e;
|
||||
}
|
||||
return e.is(T);
|
||||
},
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getType(self: *const Event) []const u8 {
|
||||
return self._type_string.str();
|
||||
}
|
||||
|
||||
@@ -34,6 +34,16 @@ pub fn registerTypes() []const type {
|
||||
}
|
||||
|
||||
const Normalizer = *const fn ([]const u8, *Page) []const u8;
|
||||
|
||||
pub const Entry = struct {
|
||||
name: String,
|
||||
value: String,
|
||||
|
||||
pub fn format(self: Entry, writer: *std.Io.Writer) !void {
|
||||
return writer.print("{f}: {f}", .{ self.name, self.value });
|
||||
}
|
||||
};
|
||||
|
||||
pub const KeyValueList = @This();
|
||||
|
||||
_entries: std.ArrayListUnmanaged(Entry) = .empty,
|
||||
@@ -85,15 +95,6 @@ pub fn fromArray(arena: Allocator, kvs: []const [2][]const u8, comptime normaliz
|
||||
return list;
|
||||
}
|
||||
|
||||
pub const Entry = struct {
|
||||
name: String,
|
||||
value: String,
|
||||
|
||||
pub fn format(self: Entry, writer: *std.Io.Writer) !void {
|
||||
return writer.print("{f}: {f}", .{ self.name, self.value });
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init() KeyValueList {
|
||||
return .{};
|
||||
}
|
||||
@@ -172,6 +173,77 @@ pub fn items(self: *const KeyValueList) []const Entry {
|
||||
return self._entries.items;
|
||||
}
|
||||
|
||||
const URLEncodeMode = enum {
|
||||
form,
|
||||
query,
|
||||
};
|
||||
|
||||
pub fn urlEncode(self: *const KeyValueList, comptime mode: URLEncodeMode, writer: *std.Io.Writer) !void {
|
||||
const entries = self._entries.items;
|
||||
if (entries.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try urlEncodeEntry(entries[0], mode, writer);
|
||||
for (entries[1..]) |entry| {
|
||||
try writer.writeByte('&');
|
||||
try urlEncodeEntry(entry, mode, writer);
|
||||
}
|
||||
}
|
||||
|
||||
fn urlEncodeEntry(entry: Entry, comptime mode: URLEncodeMode, writer: *std.Io.Writer) !void {
|
||||
try urlEncodeValue(entry.name.str(), mode, writer);
|
||||
|
||||
// for a form, for an empty value, we'll do "spice="
|
||||
// but for a query, we do "spice"
|
||||
if ((comptime mode == .query) and entry.value.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try writer.writeByte('=');
|
||||
try urlEncodeValue(entry.value.str(), mode, writer);
|
||||
}
|
||||
|
||||
fn urlEncodeValue(value: []const u8, comptime mode: URLEncodeMode, writer: *std.Io.Writer) !void {
|
||||
if (!urlEncodeShouldEscape(value, mode)) {
|
||||
return writer.writeAll(value);
|
||||
}
|
||||
|
||||
for (value) |b| {
|
||||
if (urlEncodeUnreserved(b, mode)) {
|
||||
try writer.writeByte(b);
|
||||
} else if (b == ' ') {
|
||||
try writer.writeByte('+');
|
||||
} else if (b >= 0x80) {
|
||||
// Double-encode: treat byte as Latin-1 code point, encode to UTF-8, then percent-encode
|
||||
// For bytes 0x80-0xFF (U+0080 to U+00FF), UTF-8 encoding is 2 bytes:
|
||||
// [0xC0 | (b >> 6), 0x80 | (b & 0x3F)]
|
||||
const byte1 = 0xC0 | (b >> 6);
|
||||
const byte2 = 0x80 | (b & 0x3F);
|
||||
try writer.print("%{X:0>2}%{X:0>2}", .{ byte1, byte2 });
|
||||
} else {
|
||||
try writer.print("%{X:0>2}", .{b});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn urlEncodeShouldEscape(value: []const u8, comptime mode: URLEncodeMode) bool {
|
||||
for (value) |b| {
|
||||
if (!urlEncodeUnreserved(b, mode)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn urlEncodeUnreserved(b: u8, comptime mode: URLEncodeMode) bool {
|
||||
return switch (b) {
|
||||
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '*' => true,
|
||||
'~' => comptime mode == .form,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
pub const Iterator = struct {
|
||||
index: u32 = 0,
|
||||
kv: *KeyValueList,
|
||||
|
||||
@@ -58,6 +58,14 @@ pub fn setName(self: *Button, name: []const u8, page: *Page) !void {
|
||||
try self.asElement().setAttributeSafe("name", name, page);
|
||||
}
|
||||
|
||||
pub fn getType(self: *const Button) []const u8 {
|
||||
return self.asConstElement().getAttributeSafe("type") orelse "submit";
|
||||
}
|
||||
|
||||
pub fn setType(self: *Button, typ: []const u8, page: *Page) !void {
|
||||
try self.asElement().setAttributeSafe("type", typ, page);
|
||||
}
|
||||
|
||||
pub fn getValue(self: *const Button) []const u8 {
|
||||
return self.asConstElement().getAttributeSafe("value") orelse "";
|
||||
}
|
||||
@@ -116,6 +124,7 @@ pub const JsApi = struct {
|
||||
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 @"type" = bridge.accessor(Button.getType, Button.setType, .{});
|
||||
};
|
||||
|
||||
pub const Build = struct {
|
||||
|
||||
@@ -88,6 +88,10 @@ pub fn getLength(self: *Form, page: *Page) !u32 {
|
||||
return elements.length(page);
|
||||
}
|
||||
|
||||
pub fn submit(self: *Form, page: *Page) !void {
|
||||
return page.submitForm(null, self);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Form);
|
||||
pub const Meta = struct {
|
||||
@@ -100,6 +104,7 @@ pub const JsApi = struct {
|
||||
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, .{});
|
||||
pub const submit = bridge.function(Form.submit, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../../../testing.zig");
|
||||
|
||||
@@ -61,6 +61,19 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*UIEvent {
|
||||
return event;
|
||||
}
|
||||
|
||||
pub fn as(self: *UIEvent, comptime T: type) *T {
|
||||
return self.is(T).?;
|
||||
}
|
||||
|
||||
pub fn is(self: *UIEvent, comptime T: type) ?*T {
|
||||
switch (self._type) {
|
||||
.generic => return if (T == UIEvent) self else null,
|
||||
.mouse_event => |e| return if (T == @import("MouseEvent.zig")) e else null,
|
||||
.keyboard_event => |e| return if (T == @import("KeyboardEvent.zig")) e else null,
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn populateFromOptions(self: *UIEvent, opts: anytype) void {
|
||||
self._detail = opts.detail;
|
||||
self._view = opts.view;
|
||||
|
||||
@@ -87,6 +87,21 @@ pub fn forEach(self: *FormData, cb_: js.Function, js_this_: ?js.Object) !void {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(self: *const FormData, encoding_: ?[]const u8, writer: *std.Io.Writer) !void {
|
||||
const encoding = encoding_ orelse {
|
||||
return self._list.urlEncode(.form, writer);
|
||||
};
|
||||
|
||||
if (std.ascii.eqlIgnoreCase(encoding, "application/x-www-form-urlencoded")) {
|
||||
return self._list.urlEncode(.form, writer);
|
||||
}
|
||||
|
||||
log.debug(.not_implemented, "not implemented", .{
|
||||
.feature = "form data encoding",
|
||||
.encoding = encoding,
|
||||
});
|
||||
}
|
||||
|
||||
pub const Iterator = struct {
|
||||
index: u32 = 0,
|
||||
list: *const FormData,
|
||||
|
||||
@@ -109,22 +109,7 @@ pub fn entries(self: *URLSearchParams, page: *Page) !*KeyValueList.EntryIterator
|
||||
}
|
||||
|
||||
pub fn toString(self: *const URLSearchParams, writer: *std.Io.Writer) !void {
|
||||
const items = self._params._entries.items;
|
||||
if (items.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try writeEntry(&items[0], writer);
|
||||
for (items[1..]) |entry| {
|
||||
try writer.writeByte('&');
|
||||
try writeEntry(&entry, writer);
|
||||
}
|
||||
}
|
||||
|
||||
fn writeEntry(entry: *const KeyValueList.Entry, writer: *std.Io.Writer) !void {
|
||||
try escape(entry.name.str(), writer);
|
||||
try writer.writeByte('=');
|
||||
try escape(entry.value.str(), writer);
|
||||
return self._params.urlEncode(.query, writer);
|
||||
}
|
||||
|
||||
pub fn format(self: *const URLSearchParams, writer: *std.Io.Writer) !void {
|
||||
|
||||
Reference in New Issue
Block a user