mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-31 01:36:15 -04:00
Merge pull request #2787 from lightpanda-io/frame_breakup
cleanup: Break various frame behavior into their own little utility f…
This commit is contained in:
@@ -200,11 +200,11 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
|
||||
if (event._prevent_default) {
|
||||
// can't return in a defer (╯°□°)╯︵ ┻━┻
|
||||
} else if (event._type_string.eql(comptime .wrap("click"))) {
|
||||
frame.handleClick(target) catch |err| {
|
||||
Frame.user_input.handleClick(frame, target) catch |err| {
|
||||
log.warn(.event, "frame.click", .{ .err = err });
|
||||
};
|
||||
} else if (event._type_string.eql(comptime .wrap("keydown"))) {
|
||||
frame.handleKeydown(target, event) catch |err| {
|
||||
Frame.user_input.handleKeydown(frame, target, event) catch |err| {
|
||||
log.warn(.event, "frame.keydown", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1148
src/browser/frame/node_factory.zig
Normal file
1148
src/browser/frame/node_factory.zig
Normal file
File diff suppressed because it is too large
Load Diff
258
src/browser/frame/observers.zig
Normal file
258
src/browser/frame/observers.zig
Normal file
@@ -0,0 +1,258 @@
|
||||
// Copyright (C) 2023-2026 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/>.
|
||||
|
||||
// Per-frame MutationObserver and IntersectionObserver bookkeeping: registration,
|
||||
// the scheduling of the microtask deliveries, and broadcasting DOM mutations to
|
||||
// the registered observers. The state lives on the Frame (frame._mutation /
|
||||
// frame._intersection); these functions operate on it.
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const Frame = @import("../Frame.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
|
||||
const Node = @import("../webapi/Node.zig");
|
||||
const Event = @import("../webapi/Event.zig");
|
||||
const Element = @import("../webapi/Element.zig");
|
||||
const MutationObserver = @import("../webapi/MutationObserver.zig");
|
||||
const IntersectionObserver = @import("../webapi/IntersectionObserver.zig");
|
||||
|
||||
const log = lp.log;
|
||||
const String = lp.String;
|
||||
|
||||
// MutationObserver bookkeeping for a frame.
|
||||
pub const Mutation = struct {
|
||||
// List of active MutationObservers
|
||||
observers: std.DoublyLinkedList = .{},
|
||||
delivery_scheduled: bool = false,
|
||||
delivery_depth: u32 = 0,
|
||||
};
|
||||
|
||||
// IntersectionObserver bookkeeping for a frame.
|
||||
pub const Intersection = struct {
|
||||
// List of active IntersectionObservers
|
||||
observers: std.ArrayList(*IntersectionObserver) = .{},
|
||||
check_scheduled: bool = false,
|
||||
delivery_scheduled: bool = false,
|
||||
};
|
||||
|
||||
// Releases the frame's references to its registered observers. Called from
|
||||
// Frame.deinit.
|
||||
pub fn deinit(frame: *Frame, page: *Page) void {
|
||||
var node: ?*std.DoublyLinkedList.Node = frame._mutation.observers.first;
|
||||
while (node) |n| {
|
||||
node = n.next; // capture before we potentially delete observer
|
||||
const observer: *MutationObserver = @fieldParentPtr("node", n);
|
||||
observer.releaseRef(page);
|
||||
}
|
||||
|
||||
for (frame._intersection.observers.items) |observer| {
|
||||
observer.releaseRef(page);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn registerMutationObserver(frame: *Frame, observer: *MutationObserver) !void {
|
||||
observer.acquireRef();
|
||||
frame._mutation.observers.append(&observer.node);
|
||||
}
|
||||
|
||||
pub fn unregisterMutationObserver(frame: *Frame, observer: *MutationObserver) void {
|
||||
observer.releaseRef(frame._page);
|
||||
frame._mutation.observers.remove(&observer.node);
|
||||
}
|
||||
|
||||
pub fn registerIntersectionObserver(frame: *Frame, observer: *IntersectionObserver) !void {
|
||||
observer.acquireRef();
|
||||
try frame._intersection.observers.append(frame.arena, observer);
|
||||
}
|
||||
|
||||
pub fn unregisterIntersectionObserver(frame: *Frame, observer: *IntersectionObserver) void {
|
||||
for (frame._intersection.observers.items, 0..) |obs, i| {
|
||||
if (obs == observer) {
|
||||
observer.releaseRef(frame._page);
|
||||
_ = frame._intersection.observers.swapRemove(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hasMutationObservers(frame: *const Frame) bool {
|
||||
return frame._mutation.observers.first != null;
|
||||
}
|
||||
|
||||
pub fn checkIntersections(frame: *Frame) !void {
|
||||
for (frame._intersection.observers.items) |observer| {
|
||||
try observer.checkIntersections(frame);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scheduleMutationDelivery(frame: *Frame) !void {
|
||||
if (frame._mutation.delivery_scheduled) {
|
||||
return;
|
||||
}
|
||||
frame._mutation.delivery_scheduled = true;
|
||||
try frame.js.queueMutationDelivery();
|
||||
}
|
||||
|
||||
pub fn scheduleIntersectionDelivery(frame: *Frame) !void {
|
||||
if (frame._intersection.delivery_scheduled) {
|
||||
return;
|
||||
}
|
||||
frame._intersection.delivery_scheduled = true;
|
||||
try frame.js.queueIntersectionDelivery();
|
||||
}
|
||||
|
||||
pub fn performScheduledIntersectionChecks(frame: *Frame) void {
|
||||
if (!frame._intersection.check_scheduled) {
|
||||
return;
|
||||
}
|
||||
frame._intersection.check_scheduled = false;
|
||||
checkIntersections(frame) catch |err| {
|
||||
log.err(.frame, "frame.schedIntersectChecks", .{ .err = err, .type = frame._type, .url = frame.url });
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deliverIntersections(frame: *Frame) void {
|
||||
if (!frame._intersection.delivery_scheduled) {
|
||||
return;
|
||||
}
|
||||
frame._intersection.delivery_scheduled = false;
|
||||
|
||||
// Iterate backwards to handle observers that disconnect during their callback
|
||||
var i = frame._intersection.observers.items.len;
|
||||
while (i > 0) {
|
||||
i -= 1;
|
||||
const observer = frame._intersection.observers.items[i];
|
||||
observer.deliverEntries(frame) catch |err| {
|
||||
log.err(.frame, "frame.deliverIntersections", .{ .err = err, .type = frame._type, .url = frame.url });
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deliverMutations(frame: *Frame) void {
|
||||
if (!frame._mutation.delivery_scheduled) {
|
||||
return;
|
||||
}
|
||||
frame._mutation.delivery_scheduled = false;
|
||||
|
||||
frame._mutation.delivery_depth += 1;
|
||||
defer if (!frame._mutation.delivery_scheduled) {
|
||||
// reset the depth once nothing is left to be scheduled
|
||||
frame._mutation.delivery_depth = 0;
|
||||
};
|
||||
|
||||
if (frame._mutation.delivery_depth > 100) {
|
||||
log.err(.frame, "frame.MutationLimit", .{ .type = frame._type, .url = frame.url });
|
||||
frame._mutation.delivery_depth = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// snapshot the pending slots to deliver. We'll deliver these AFTER the mutation
|
||||
// but new pending slots that land during mutation should only be delivered
|
||||
// on the microtask tick.
|
||||
const slots = frame.call_arena.dupe(*Element.Html.Slot, frame._slots_pending_slotchange.keys()) catch |err| blk: {
|
||||
log.err(.frame, "deliverMutations.slots", .{ .err = err, .type = frame._type, .url = frame.url });
|
||||
break :blk &.{};
|
||||
};
|
||||
frame._slots_pending_slotchange.clearRetainingCapacity();
|
||||
|
||||
// We only deliver notifications for observers that have records BEFORE
|
||||
// we started the delivery. So we need to snapshot this. Any observers which
|
||||
// get records during this phase will only be processed on the next microtask tick.
|
||||
var notify: std.ArrayList(*MutationObserver) = .empty;
|
||||
var it: ?*std.DoublyLinkedList.Node = frame._mutation.observers.first;
|
||||
while (it) |node| : (it = node.next) {
|
||||
const observer: *MutationObserver = @fieldParentPtr("node", node);
|
||||
if (observer._pending_records.items.len == 0) {
|
||||
continue;
|
||||
}
|
||||
notify.append(frame.call_arena, observer) catch |err| {
|
||||
log.err(.frame, "deliverMutations.notify", .{ .err = err, .type = frame._type, .url = frame.url });
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
for (notify.items) |observer| {
|
||||
observer.deliverRecords(frame) catch |err| {
|
||||
log.err(.frame, "frame.deliverMutations", .{ .err = err, .type = frame._type, .url = frame.url });
|
||||
};
|
||||
}
|
||||
|
||||
// slotchange events fire after the observer callbacks (spec step order)
|
||||
for (slots) |slot| {
|
||||
const event = Event.initTrusted(comptime .wrap("slotchange"), .{ .bubbles = true }, frame._page) catch |err| {
|
||||
log.err(.frame, "deliverSlotchange.init", .{ .err = err, .type = frame._type, .url = frame.url });
|
||||
continue;
|
||||
};
|
||||
const target = slot.asNode().asEventTarget();
|
||||
frame._event_manager.dispatch(target, event) catch |err| {
|
||||
log.err(.frame, "deliverSlotchange.dispatch", .{ .err = err, .type = frame._type, .url = frame.url });
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast an attribute change to every registered MutationObserver. The
|
||||
// caller (Frame.attributeChange / attributeRemove) handles the non-observer
|
||||
// side effects (build hooks, custom-element callbacks, slot/popover updates).
|
||||
pub fn notifyAttributeChange(frame: *Frame, element: *Element, name: String, old_value: ?String) void {
|
||||
var it: ?*std.DoublyLinkedList.Node = frame._mutation.observers.first;
|
||||
while (it) |node| : (it = node.next) {
|
||||
const observer: *MutationObserver = @fieldParentPtr("node", node);
|
||||
observer.notifyAttributeChange(element, name, old_value, frame) catch |err| {
|
||||
log.err(.frame, "attributeChange.notifyObserver", .{ .err = err, .type = frame._type, .url = frame.url });
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notifyCharacterDataChange(frame: *Frame, target: *Node, old_value: String) void {
|
||||
var it: ?*std.DoublyLinkedList.Node = frame._mutation.observers.first;
|
||||
while (it) |node| : (it = node.next) {
|
||||
const observer: *MutationObserver = @fieldParentPtr("node", node);
|
||||
observer.notifyCharacterDataChange(target, old_value, frame) catch |err| {
|
||||
log.err(.frame, "cdataChange.notifyObserver", .{ .err = err, .type = frame._type, .url = frame.url });
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notifyChildListChange(
|
||||
frame: *Frame,
|
||||
target: *Node,
|
||||
added_nodes: []const *Node,
|
||||
removed_nodes: []const *Node,
|
||||
previous_sibling: ?*Node,
|
||||
next_sibling: ?*Node,
|
||||
) void {
|
||||
// Filter out HTML wrapper element during fragment parsing (html5ever quirk)
|
||||
if (frame._parse_mode == .fragment and added_nodes.len == 1) {
|
||||
if (added_nodes[0].is(Element.Html.Html) != null) {
|
||||
// This is the temporary HTML wrapper, added by html5ever
|
||||
// that will be unwrapped, see:
|
||||
// https://github.com/servo/html5ever/issues/583
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var it: ?*std.DoublyLinkedList.Node = frame._mutation.observers.first;
|
||||
while (it) |node| : (it = node.next) {
|
||||
const observer: *MutationObserver = @fieldParentPtr("node", node);
|
||||
observer.notifyChildListChange(target, added_nodes, removed_nodes, previous_sibling, next_sibling, frame) catch |err| {
|
||||
log.err(.frame, "childListChange.notifyObserver", .{ .err = err, .type = frame._type, .url = frame.url });
|
||||
};
|
||||
}
|
||||
}
|
||||
477
src/browser/frame/user_input.zig
Normal file
477
src/browser/frame/user_input.zig
Normal file
@@ -0,0 +1,477 @@
|
||||
// Copyright (C) 2023-2026 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/>.
|
||||
|
||||
// Synthetic user input driving the DOM: mouse, wheel, keyboard, focus
|
||||
// navigation and text insertion. These are mostly fed by CDP's Input domain
|
||||
// (src/cdp/domains/input.zig) and by EventManager's default activation
|
||||
// behavior. Form submission itself lives on the Frame (it's a navigation
|
||||
// concern); the activation paths here call into it.
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const Frame = @import("../Frame.zig");
|
||||
|
||||
const Node = @import("../webapi/Node.zig");
|
||||
const Event = @import("../webapi/Event.zig");
|
||||
const Element = @import("../webapi/Element.zig");
|
||||
const TreeWalker = @import("../webapi/TreeWalker.zig");
|
||||
const MouseEvent = @import("../webapi/event/MouseEvent.zig");
|
||||
const WheelEvent = @import("../webapi/event/WheelEvent.zig");
|
||||
const KeyboardEvent = @import("../webapi/event/KeyboardEvent.zig");
|
||||
|
||||
const log = lp.log;
|
||||
const IS_DEBUG = builtin.mode == .Debug;
|
||||
|
||||
// DOM MouseEvent.button values.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
|
||||
pub const mouse_button = struct {
|
||||
pub const main: i32 = 0; // left
|
||||
pub const auxiliary: i32 = 1; // middle
|
||||
pub const secondary: i32 = 2; // right
|
||||
pub const fourth: i32 = 3; // back
|
||||
pub const fifth: i32 = 4; // forward
|
||||
};
|
||||
|
||||
// Dispatch a single trusted mouse event of the given type on `target`, carrying
|
||||
// the pressed button and pointer position. `detail` is the click count (used for
|
||||
// click/dblclick); 0 for events where it does not apply.
|
||||
fn dispatchMouseEventOn(frame: *Frame, target: *Element, comptime typ: []const u8, x: f64, y: f64, button: i32, detail: u32) !void {
|
||||
const event: *MouseEvent = try .initTrusted(comptime .wrap(typ), .{
|
||||
.bubbles = true,
|
||||
.cancelable = true,
|
||||
.composed = true,
|
||||
.clientX = x,
|
||||
.clientY = y,
|
||||
.button = button,
|
||||
.detail = detail,
|
||||
}, frame);
|
||||
try frame._event_manager.dispatch(target.asEventTarget(), event.asEvent());
|
||||
}
|
||||
|
||||
pub fn triggerMousePress(frame: *Frame, x: f64, y: f64, button: i32) !void {
|
||||
const target = (try frame.window._document.elementFromPoint(x, y, frame)) orelse return;
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.frame, "frame mouse press", .{
|
||||
.url = frame.url,
|
||||
.node = target,
|
||||
.x = x,
|
||||
.y = y,
|
||||
.button = button,
|
||||
.type = frame._type,
|
||||
});
|
||||
}
|
||||
try dispatchMouseEventOn(frame, target, "mousedown", x, y, button, 0);
|
||||
}
|
||||
|
||||
pub fn triggerMouseMove(frame: *Frame, x: f64, y: f64) !void {
|
||||
const target = (try frame.window._document.elementFromPoint(x, y, frame)) orelse return;
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.frame, "frame mouse move", .{
|
||||
.url = frame.url,
|
||||
.node = target,
|
||||
.x = x,
|
||||
.y = y,
|
||||
.type = frame._type,
|
||||
});
|
||||
}
|
||||
|
||||
const move_event: *MouseEvent = try .initTrusted(comptime .wrap("mousemove"), .{
|
||||
.bubbles = true,
|
||||
.cancelable = true,
|
||||
.composed = true,
|
||||
.clientX = x,
|
||||
.clientY = y,
|
||||
}, frame);
|
||||
try frame._event_manager.dispatch(target.asEventTarget(), move_event.asEvent());
|
||||
|
||||
const over_event: *MouseEvent = try .initTrusted(comptime .wrap("mouseover"), .{
|
||||
.bubbles = true,
|
||||
.cancelable = true,
|
||||
.composed = true,
|
||||
.clientX = x,
|
||||
.clientY = y,
|
||||
}, frame);
|
||||
try frame._event_manager.dispatch(target.asEventTarget(), over_event.asEvent());
|
||||
|
||||
const enter_event: *MouseEvent = try .initTrusted(comptime .wrap("mouseenter"), .{
|
||||
.composed = true,
|
||||
.clientX = x,
|
||||
.clientY = y,
|
||||
}, frame);
|
||||
try frame._event_manager.dispatch(target.asEventTarget(), enter_event.asEvent());
|
||||
}
|
||||
|
||||
pub fn triggerMouseRelease(frame: *Frame, x: f64, y: f64, button: i32, click_count: i32) !void {
|
||||
const target = (try frame.window._document.elementFromPoint(x, y, frame)) orelse return;
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.frame, "frame mouse release", .{
|
||||
.url = frame.url,
|
||||
.node = target,
|
||||
.x = x,
|
||||
.y = y,
|
||||
.button = button,
|
||||
.type = frame._type,
|
||||
});
|
||||
}
|
||||
|
||||
const detail: u32 = if (click_count > 0) @intCast(click_count) else 1;
|
||||
|
||||
try dispatchMouseEventOn(frame, target, "mouseup", x, y, button, detail);
|
||||
|
||||
// After mouseup, the activation event depends on the button.
|
||||
switch (button) {
|
||||
mouse_button.main => {
|
||||
try dispatchMouseEventOn(frame, target, "click", x, y, button, detail);
|
||||
// A second click in quick succession also fires dblclick.
|
||||
if (click_count == 2) {
|
||||
try dispatchMouseEventOn(frame, target, "dblclick", x, y, button, detail);
|
||||
}
|
||||
},
|
||||
mouse_button.auxiliary => try dispatchMouseEventOn(frame, target, "auxclick", x, y, button, detail),
|
||||
mouse_button.secondary => try dispatchMouseEventOn(frame, target, "contextmenu", x, y, button, detail),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn triggerMouseWheel(frame: *Frame, x: f64, y: f64, delta_x: f64, delta_y: f64) !void {
|
||||
const target = (try frame.window._document.elementFromPoint(x, y, frame)) orelse return;
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.frame, "frame mouse wheel", .{
|
||||
.url = frame.url,
|
||||
.node = target,
|
||||
.x = x,
|
||||
.y = y,
|
||||
.delta_x = delta_x,
|
||||
.delta_y = delta_y,
|
||||
.type = frame._type,
|
||||
});
|
||||
}
|
||||
|
||||
const wheel_event: *WheelEvent = try .initTrusted("wheel", .{
|
||||
.bubbles = true,
|
||||
.cancelable = true,
|
||||
.composed = true,
|
||||
.clientX = x,
|
||||
.clientY = y,
|
||||
.deltaX = delta_x,
|
||||
.deltaY = delta_y,
|
||||
}, frame);
|
||||
|
||||
// Keep the event alive past dispatch so we can read _prevent_default.
|
||||
wheel_event.asEvent().acquireRef();
|
||||
defer _ = wheel_event.asEvent().releaseRef(frame._page);
|
||||
try frame._event_manager.dispatch(target.asEventTarget(), wheel_event.asEvent());
|
||||
|
||||
if (wheel_event.asEvent()._prevent_default) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply the scroll and fire a trusted scroll event, mirroring WebDriver wheel.
|
||||
// CDP deltas are untrusted, so guard NaN and saturate the addition.
|
||||
const new_left: i32 = @as(i32, @intCast(target.getScrollLeft(frame))) +| deltaToScroll(delta_x);
|
||||
const new_top: i32 = @as(i32, @intCast(target.getScrollTop(frame))) +| deltaToScroll(delta_y);
|
||||
try target.setScrollLeft(new_left, frame);
|
||||
try target.setScrollTop(new_top, frame);
|
||||
|
||||
const scroll_event = try Event.initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, frame._page);
|
||||
try frame._event_manager.dispatch(target.asEventTarget(), scroll_event);
|
||||
}
|
||||
|
||||
fn deltaToScroll(d: f64) i32 {
|
||||
if (std.math.isNan(d)) return 0;
|
||||
return @intFromFloat(std.math.clamp(d, std.math.minInt(i32), std.math.maxInt(i32)));
|
||||
}
|
||||
|
||||
// callback when the "click" event reaches the frame.
|
||||
pub fn handleClick(frame: *Frame, target: *Node) !void {
|
||||
// TODO: Also support <area> elements when implement
|
||||
const element = target.is(Element) orelse return;
|
||||
const html_element = element.is(Element.Html) orelse return;
|
||||
|
||||
switch (html_element._type) {
|
||||
.anchor => |anchor| {
|
||||
const href = element.getAttributeSafe(comptime .wrap("href")) orelse return;
|
||||
if (href.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (std.mem.startsWith(u8, href, "javascript:")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (try element.hasAttribute(comptime .wrap("download"), frame)) {
|
||||
log.warn(.browser, "a.download", .{ .type = frame._type, .url = frame.url });
|
||||
return;
|
||||
}
|
||||
|
||||
const target_frame = blk: {
|
||||
const target_name = anchor.getTarget();
|
||||
if (target_name.len == 0) {
|
||||
break :blk target.ownerFrame(frame);
|
||||
}
|
||||
break :blk frame.resolveTargetFrame(target_name) orelse {
|
||||
log.warn(.not_implemented, "target", .{ .type = frame._type, .url = frame.url, .target = target_name });
|
||||
return;
|
||||
};
|
||||
};
|
||||
|
||||
try element.focus(frame);
|
||||
try frame.scheduleNavigation(href, .{
|
||||
.reason = .script,
|
||||
.kind = .{ .push = null },
|
||||
}, .{ .anchor = target_frame });
|
||||
},
|
||||
.input => |input| {
|
||||
try element.focus(frame);
|
||||
// Per HTML §4.10.18.6.4 "Image Button state (type=image)", clicking an
|
||||
// image button submits its form. The form-data set already gets the
|
||||
// submitter's coordinate fields appended via FormData.collectForm
|
||||
// (see src/browser/webapi/net/FormData.zig).
|
||||
if (input._input_type == .submit or input._input_type == .image) {
|
||||
return frame.submitForm(element, input.getForm(frame), .{});
|
||||
}
|
||||
},
|
||||
.button => |button| {
|
||||
try element.focus(frame);
|
||||
if (std.mem.eql(u8, button.getType(), "submit")) {
|
||||
return frame.submitForm(element, button.getForm(frame), .{});
|
||||
}
|
||||
},
|
||||
.select, .textarea => try element.focus(frame),
|
||||
.label => |label| {
|
||||
// Per HTML §4.10.4 "The label element", a label's activation
|
||||
// behavior is to run the synthetic click activation steps on the
|
||||
// labeled control. Mirrors Chrome's HTMLLabelElement::DefaultEventHandler.
|
||||
const control = label.getControl(frame) orelse return;
|
||||
const control_html = control.is(Element.Html) orelse return;
|
||||
try control_html.click(frame);
|
||||
},
|
||||
.generic => |generic| {
|
||||
switch (generic._tag) {
|
||||
.summary => {
|
||||
const parent_el = target.parentElement() orelse return;
|
||||
const details = parent_el.is(Element.Html.Details) orelse return;
|
||||
var maybe_prev = element.previousElementSibling();
|
||||
while (maybe_prev) |prev| {
|
||||
if (prev.getTag() == .summary) {
|
||||
// we found a summary element before the clicked one
|
||||
return;
|
||||
}
|
||||
maybe_prev = prev.previousElementSibling();
|
||||
}
|
||||
try details.setOpen(!details.getOpen(), frame);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn triggerKeyboard(frame: *Frame, keyboard_event: *KeyboardEvent) !void {
|
||||
const event = keyboard_event.asEvent();
|
||||
// Dispatch to the effective active element. When nothing is explicitly
|
||||
// focused this resolves to <body> (matching `document.activeElement`), so
|
||||
// the keydown still fires and its default action — e.g. sequential focus
|
||||
// navigation on Tab — can run.
|
||||
const element = frame.window._document.getActiveElement() orelse {
|
||||
event.deinit(frame._page);
|
||||
return;
|
||||
};
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.frame, "frame keydown", .{
|
||||
.url = frame.url,
|
||||
.node = element,
|
||||
.key = keyboard_event._key,
|
||||
.type = frame._type,
|
||||
});
|
||||
}
|
||||
try frame._event_manager.dispatch(element.asEventTarget(), event);
|
||||
}
|
||||
|
||||
pub fn handleKeydown(frame: *Frame, target: *Node, event: *Event) !void {
|
||||
const keyboard_event = event.is(KeyboardEvent) orelse return;
|
||||
const key = keyboard_event.getKey();
|
||||
|
||||
if (key == .Dead) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key == .Tab) {
|
||||
// tab -> forward, shift+tab -> backwards
|
||||
return moveFocus(frame, keyboard_event.getShiftKey() == false);
|
||||
}
|
||||
|
||||
if (target.is(Element.Html.Input)) |input| {
|
||||
if (key == .Enter) {
|
||||
return frame.submitForm(input.asElement(), input.getForm(frame), .{});
|
||||
}
|
||||
|
||||
// Don't handle text input for radio/checkbox
|
||||
const input_type = input._input_type;
|
||||
if (input_type == .radio or input_type == .checkbox) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle printable characters
|
||||
if (key.isPrintable()) {
|
||||
try input.innerInsert(key.asString(), frame);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.is(Element.Html.TextArea)) |textarea| {
|
||||
// zig fmt: off
|
||||
const append =
|
||||
if (key == .Enter) "\n"
|
||||
else if (key.isPrintable()) key.asString()
|
||||
else return
|
||||
;
|
||||
// zig fmt: on
|
||||
return textarea.innerInsert(append, frame);
|
||||
}
|
||||
}
|
||||
|
||||
// Sequential focus navigation: move `document.activeElement` to the next (Tab)
|
||||
// or previous (Shift+Tab) focusable element, firing the usual blur/focus events
|
||||
// via `Element.focus`. The order is fully determined by tabindex + document
|
||||
// position, so no layout is needed:
|
||||
// 1. elements with a positive tabindex, in ascending tabindex order;
|
||||
// 2. then elements with tabindex 0 (or a natively-focusable default), in
|
||||
// document order.
|
||||
// Ties within a group break on document order, and Tab wraps around at the ends.
|
||||
// https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation
|
||||
fn moveFocus(frame: *Frame, forward: bool) !void {
|
||||
const document = frame.document;
|
||||
const current = document._active_element;
|
||||
|
||||
const current_tab_index = blk: {
|
||||
const cur = current orelse break :blk 0;
|
||||
const current_html = cur.is(Element.Html) orelse break :blk 0;
|
||||
break :blk current_html.getTabIndex();
|
||||
};
|
||||
|
||||
// Single document-order pass tracking two candidates:
|
||||
// edge — the global first (forward) / last (backward) focusable element,
|
||||
// used to wrap around when `current` is at an end, or as the
|
||||
// landing spot when nothing is focused yet.
|
||||
// chosen — the closest focusable element strictly past `current` in the
|
||||
// travel direction.
|
||||
var edge: ?*Element = null;
|
||||
var edge_tab_index: i32 = 0;
|
||||
|
||||
var chosen: ?*Element = null;
|
||||
var chosen_tab_index: i32 = 0;
|
||||
|
||||
var tw = TreeWalker.Full.Elements.init(document.asNode(), .{});
|
||||
while (tw.next()) |candidate| {
|
||||
if (candidate.isDisabled()) {
|
||||
continue;
|
||||
}
|
||||
if (candidate.is(Element.Html) == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidate_tab_index = blk: {
|
||||
if (candidate.getAttributeSafe(comptime .wrap("tabindex"))) |attr| {
|
||||
if (Element.Html.parseInteger(attr)) |tab_index| {
|
||||
if (tab_index < 0) {
|
||||
continue;
|
||||
}
|
||||
break :blk tab_index;
|
||||
}
|
||||
break :blk 0;
|
||||
}
|
||||
|
||||
// no tab index, maybe this item isn't focusable..
|
||||
const focusable = switch (candidate.getTag()) {
|
||||
.button, .select, .textarea, .iframe => true,
|
||||
.input => candidate.as(Element.Html.Input)._input_type != .hidden,
|
||||
.anchor, .area => candidate.getAttributeSafe(comptime .wrap("href")) != null,
|
||||
else => false,
|
||||
};
|
||||
if (focusable == false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
break :blk 0;
|
||||
};
|
||||
|
||||
if (edge == null or focusOrderBefore(candidate, candidate_tab_index, edge.?, edge_tab_index) == forward) {
|
||||
edge = candidate;
|
||||
edge_tab_index = candidate_tab_index;
|
||||
}
|
||||
|
||||
const cur = current orelse continue;
|
||||
|
||||
if (candidate == cur) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const past = if (forward) focusOrderBefore(cur, current_tab_index, candidate, candidate_tab_index) else focusOrderBefore(candidate, candidate_tab_index, cur, current_tab_index);
|
||||
if (!past) {
|
||||
continue;
|
||||
}
|
||||
if (chosen == null or focusOrderBefore(candidate, candidate_tab_index, chosen.?, chosen_tab_index) == forward) {
|
||||
chosen = candidate;
|
||||
chosen_tab_index = candidate_tab_index;
|
||||
}
|
||||
}
|
||||
|
||||
const next = chosen orelse edge orelse return;
|
||||
try next.focus(frame);
|
||||
}
|
||||
|
||||
// Orders two focusable elements by sequential focus navigation order: positive
|
||||
// tabindex first (ascending), then tabindex 0, ties broken by document order.
|
||||
fn focusOrderBefore(a: *Element, a_tab_index: i32, b: *Element, b_tab_index: i32) bool {
|
||||
if (a_tab_index == b_tab_index) {
|
||||
// Equal tabindex → document order: `a` precedes `b` when `b` follows `a`.
|
||||
const FOLLOWING: u16 = 0x04;
|
||||
return (a.asNode().compareDocumentPosition(b.asNode()) & FOLLOWING) != 0;
|
||||
}
|
||||
|
||||
const group_a: u8 = if (a_tab_index > 0) 0 else 1;
|
||||
const group_b: u8 = if (b_tab_index > 0) 0 else 1;
|
||||
if (group_a != group_b) {
|
||||
return group_a < group_b;
|
||||
}
|
||||
|
||||
return a_tab_index < b_tab_index;
|
||||
}
|
||||
|
||||
// insertText is a shortcut to insert text into the active element.
|
||||
pub fn insertText(frame: *Frame, v: []const u8) !void {
|
||||
const html_element = frame.document._active_element orelse return;
|
||||
|
||||
if (html_element.is(Element.Html.Input)) |input| {
|
||||
const input_type = input._input_type;
|
||||
if (input_type == .radio or input_type == .checkbox) {
|
||||
return;
|
||||
}
|
||||
|
||||
return input.innerInsert(v, frame);
|
||||
}
|
||||
|
||||
if (html_element.is(Element.Html.TextArea)) |textarea| {
|
||||
return textarea.innerInsert(v, frame);
|
||||
}
|
||||
}
|
||||
@@ -1039,7 +1039,7 @@ pub fn queueMutationDelivery(self: *Context) !void {
|
||||
self.enqueueMicrotask(struct {
|
||||
fn run(ctx: *Context) void {
|
||||
switch (ctx.global) {
|
||||
.frame => |frame| frame.deliverMutations(),
|
||||
.frame => |frame| Frame.observers.deliverMutations(frame),
|
||||
.worker => unreachable,
|
||||
}
|
||||
}
|
||||
@@ -1050,7 +1050,7 @@ pub fn queueIntersectionChecks(self: *Context) !void {
|
||||
self.enqueueMicrotask(struct {
|
||||
fn run(ctx: *Context) void {
|
||||
switch (ctx.global) {
|
||||
.frame => |frame| frame.performScheduledIntersectionChecks(),
|
||||
.frame => |frame| Frame.observers.performScheduledIntersectionChecks(frame),
|
||||
.worker => unreachable,
|
||||
}
|
||||
}
|
||||
@@ -1061,7 +1061,7 @@ pub fn queueIntersectionDelivery(self: *Context) !void {
|
||||
self.enqueueMicrotask(struct {
|
||||
fn run(ctx: *Context) void {
|
||||
switch (ctx.global) {
|
||||
.frame => |frame| frame.deliverIntersections(),
|
||||
.frame => |frame| Frame.observers.deliverIntersections(frame),
|
||||
.worker => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ fn appendTextChunk(self: *Parser, parent: *Node, txt: []const u8) !void {
|
||||
|
||||
// Fresh text run: the first chunk lives on _data only. buf stays empty
|
||||
// until (and unless) a second chunk arrives.
|
||||
const new_text = try self.frame.createTextNode(txt);
|
||||
const new_text = try Frame.node_factory.createTextNode(self.frame, txt);
|
||||
try self.frame.appendNew(parent, new_text);
|
||||
self.pending_text = .{
|
||||
.parent = parent,
|
||||
@@ -446,7 +446,7 @@ fn _createElementCallback(self: *Parser, data: *anyopaque, qname: h5e.QualName,
|
||||
const name = qname.local.slice();
|
||||
const namespace_string = qname.ns.slice();
|
||||
const namespace = if (namespace_string.len == 0) default_namespace else Element.Namespace.parse(namespace_string);
|
||||
const node = try frame.createElementNS(namespace, name, attributes);
|
||||
const node = try Frame.node_factory.createElementNS(frame, namespace, name, attributes);
|
||||
|
||||
const pn = try self.arena.create(ParsedNode);
|
||||
pn.* = .{
|
||||
@@ -467,7 +467,7 @@ fn createCommentCallback(ctx: *anyopaque, str: h5e.StringSlice) callconv(.c) ?*a
|
||||
}
|
||||
fn _createCommentCallback(self: *Parser, str: []const u8) !*anyopaque {
|
||||
const frame = self.frame;
|
||||
const node = try frame.createComment(str);
|
||||
const node = try Frame.node_factory.createComment(frame, str);
|
||||
const pn = try self.arena.create(ParsedNode);
|
||||
pn.* = .{
|
||||
.data = null,
|
||||
@@ -487,7 +487,7 @@ fn createProcessingInstruction(ctx: *anyopaque, target: h5e.StringSlice, data: h
|
||||
}
|
||||
fn _createProcessingInstruction(self: *Parser, target: []const u8, data: []const u8) !*anyopaque {
|
||||
const frame = self.frame;
|
||||
const node = try frame.createProcessingInstruction(target, data);
|
||||
const node = try Frame.node_factory.createProcessingInstruction(frame, target, data);
|
||||
const pn = try self.arena.create(ParsedNode);
|
||||
pn.* = .{
|
||||
.data = null,
|
||||
@@ -697,7 +697,7 @@ fn _appendBeforeSiblingCallback(self: *Parser, sibling: *Node, node_or_text: h5e
|
||||
}
|
||||
break :blk child;
|
||||
},
|
||||
.text => |txt| try self.frame.createTextNode(txt),
|
||||
.text => |txt| try Frame.node_factory.createTextNode(self.frame, txt),
|
||||
};
|
||||
try self.frame.insertNodeRelative(parent, node, .{ .before = sibling }, .{});
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ pub fn setData(self: *CData, value: ?[]const u8, frame: *Frame) !void {
|
||||
self._data = .empty;
|
||||
}
|
||||
|
||||
frame.characterDataChange(self.asNode(), old_value);
|
||||
Frame.observers.notifyCharacterDataChange(frame, self.asNode(), old_value);
|
||||
}
|
||||
|
||||
/// JS bridge wrapper for `data` setter.
|
||||
@@ -305,7 +305,7 @@ pub fn deleteData(self: *CData, offset: usize, count: usize, frame: *Frame) !voi
|
||||
old_value[range.end..],
|
||||
});
|
||||
}
|
||||
frame.characterDataChange(self.asNode(), old_data);
|
||||
Frame.observers.notifyCharacterDataChange(frame, self.asNode(), old_data);
|
||||
}
|
||||
|
||||
pub fn insertData(self: *CData, offset: usize, data: []const u8, frame: *Frame) !void {
|
||||
@@ -321,7 +321,7 @@ pub fn insertData(self: *CData, offset: usize, data: []const u8, frame: *Frame)
|
||||
data,
|
||||
existing[byte_offset..],
|
||||
});
|
||||
frame.characterDataChange(self.asNode(), old_value);
|
||||
Frame.observers.notifyCharacterDataChange(frame, self.asNode(), old_value);
|
||||
}
|
||||
|
||||
pub fn replaceData(self: *CData, offset: usize, count: usize, data: []const u8, frame: *Frame) !void {
|
||||
@@ -340,7 +340,7 @@ pub fn replaceData(self: *CData, offset: usize, count: usize, data: []const u8,
|
||||
data,
|
||||
existing[range.end..],
|
||||
});
|
||||
frame.characterDataChange(self.asNode(), old_value);
|
||||
Frame.observers.notifyCharacterDataChange(frame, self.asNode(), old_value);
|
||||
}
|
||||
|
||||
pub fn substringData(self: *const CData, offset: usize, count: usize) ![]const u8 {
|
||||
|
||||
@@ -44,20 +44,20 @@ pub fn createHTMLDocument(_: *const DOMImplementation, title: ?js.NullableString
|
||||
_ = try document.asNode().appendChild(doctype.asNode(), frame);
|
||||
}
|
||||
|
||||
const html_node = try frame.createElementNS(.html, "html", null);
|
||||
const html_node = try Frame.node_factory.createElementNS(frame, .html, "html", null);
|
||||
_ = try document.asNode().appendChild(html_node, frame);
|
||||
|
||||
const head_node = try frame.createElementNS(.html, "head", null);
|
||||
const head_node = try Frame.node_factory.createElementNS(frame, .html, "head", null);
|
||||
_ = try html_node.appendChild(head_node, frame);
|
||||
|
||||
if (title) |t| {
|
||||
const title_node = try frame.createElementNS(.html, "title", null);
|
||||
const title_node = try Frame.node_factory.createElementNS(frame, .html, "title", null);
|
||||
_ = try head_node.appendChild(title_node, frame);
|
||||
const text_node = try frame.createTextNode(t.value);
|
||||
const text_node = try Frame.node_factory.createTextNode(frame, t.value);
|
||||
_ = try title_node.appendChild(text_node, frame);
|
||||
}
|
||||
|
||||
const body_node = try frame.createElementNS(.html, "body", null);
|
||||
const body_node = try Frame.node_factory.createElementNS(frame, .html, "body", null);
|
||||
_ = try html_node.appendChild(body_node, frame);
|
||||
|
||||
return document;
|
||||
@@ -77,7 +77,7 @@ pub fn createDocument(_: *const DOMImplementation, namespace_: ?[]const u8, qual
|
||||
if (qualified_name) |qname| {
|
||||
if (qname.len > 0) {
|
||||
const namespace = Node.Element.Namespace.parse(namespace_);
|
||||
const root = try frame.createElementNS(namespace, qname, null);
|
||||
const root = try Frame.node_factory.createElementNS(frame, namespace, qname, null);
|
||||
_ = try document.asNode().appendChild(root, frame);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElement
|
||||
};
|
||||
// HTML documents are case-insensitive - lowercase the tag name
|
||||
|
||||
const node = try frame.createElementNS(ns, normalized_name, null);
|
||||
const node = try Frame.node_factory.createElementNS(frame, ns, normalized_name, null);
|
||||
const element = node.as(Element);
|
||||
|
||||
// Track owner document if it's not the main document
|
||||
@@ -186,7 +186,7 @@ pub fn createElementNS(self: *Document, namespace: ?[]const u8, name: []const u8
|
||||
try validateElementName(name);
|
||||
const ns = Element.Namespace.parse(namespace);
|
||||
// Per spec, createElementNS does NOT lowercase (unlike createElement).
|
||||
const node = try frame.createElementNS(ns, name, null);
|
||||
const node = try Frame.node_factory.createElementNS(frame, ns, name, null);
|
||||
|
||||
// Store original URI for unknown namespaces so lookupNamespaceURI can return it
|
||||
if (ns == .unknown) {
|
||||
@@ -317,7 +317,7 @@ pub fn createDocumentFragment(self: *Document, frame: *Frame) !*Node.DocumentFra
|
||||
}
|
||||
|
||||
pub fn createComment(self: *Document, data: []const u8, frame: *Frame) !*Node {
|
||||
const node = try frame.createComment(data);
|
||||
const node = try Frame.node_factory.createComment(frame, data);
|
||||
// Track owner document if it's not the main document
|
||||
if (self != frame.document) {
|
||||
try frame.setNodeOwnerDocument(node, self);
|
||||
@@ -326,7 +326,7 @@ pub fn createComment(self: *Document, data: []const u8, frame: *Frame) !*Node {
|
||||
}
|
||||
|
||||
pub fn createTextNode(self: *Document, data: []const u8, frame: *Frame) !*Node {
|
||||
const node = try frame.createTextNode(data);
|
||||
const node = try Frame.node_factory.createTextNode(frame, data);
|
||||
// Track owner document if it's not the main document
|
||||
if (self != frame.document) {
|
||||
try frame.setNodeOwnerDocument(node, self);
|
||||
@@ -337,8 +337,8 @@ pub fn createTextNode(self: *Document, data: []const u8, frame: *Frame) !*Node {
|
||||
pub fn createCDATASection(self: *Document, data: []const u8, frame: *Frame) !*Node {
|
||||
const node = switch (self._type) {
|
||||
.html => return error.NotSupported, // cannot create a CDataSection in an HTMLDocument
|
||||
.xml => try frame.createCDATASection(data),
|
||||
.generic => try frame.createCDATASection(data),
|
||||
.xml => try Frame.node_factory.createCDATASection(frame, data),
|
||||
.generic => try Frame.node_factory.createCDATASection(frame, data),
|
||||
};
|
||||
// Track owner document if it's not the main document
|
||||
if (self != frame.document) {
|
||||
@@ -348,7 +348,7 @@ pub fn createCDATASection(self: *Document, data: []const u8, frame: *Frame) !*No
|
||||
}
|
||||
|
||||
pub fn createProcessingInstruction(self: *Document, target: []const u8, data: []const u8, frame: *Frame) !*Node {
|
||||
const node = try frame.createProcessingInstruction(target, data);
|
||||
const node = try Frame.node_factory.createProcessingInstruction(frame, target, data);
|
||||
// Track owner document if it's not the main document
|
||||
if (self != frame.document) {
|
||||
try frame.setNodeOwnerDocument(node, self);
|
||||
@@ -1129,9 +1129,9 @@ fn _injectBlank(self: *Document, frame: *Frame) !void {
|
||||
std.debug.assert(self.asNode()._children == null);
|
||||
}
|
||||
|
||||
const html = try frame.createElementNS(.html, "html", null);
|
||||
const head = try frame.createElementNS(.html, "head", null);
|
||||
const body = try frame.createElementNS(.html, "body", null);
|
||||
const html = try Frame.node_factory.createElementNS(frame, .html, "html", null);
|
||||
const head = try Frame.node_factory.createElementNS(frame, .html, "head", null);
|
||||
const body = try Frame.node_factory.createElementNS(frame, .html, "body", null);
|
||||
try frame.appendNode(html, head, .{});
|
||||
try frame.appendNode(html, body, .{});
|
||||
try frame.appendNode(self.asNode(), html, .{});
|
||||
|
||||
@@ -797,7 +797,7 @@ pub fn insertAdjacentText(
|
||||
data: []const u8,
|
||||
frame: *Frame,
|
||||
) !void {
|
||||
const text_node = try frame.createTextNode(data);
|
||||
const text_node = try Frame.node_factory.createTextNode(frame, data);
|
||||
const target_node, const prev_node = try self.asNode().findAdjacentNodes(where);
|
||||
_ = try target_node.insertBefore(text_node, prev_node, frame);
|
||||
}
|
||||
@@ -1494,7 +1494,7 @@ pub fn getElementsByClassName(self: *Element, class_name: []const u8, frame: *Fr
|
||||
|
||||
pub fn clone(self: *Element, deep: bool, frame: *Frame) !*Node {
|
||||
const tag_name = self.getTagNameDump();
|
||||
const node = try frame.createElementNS(self._namespace, tag_name, self._attributes);
|
||||
const node = try Frame.node_factory.createElementNS(frame, self._namespace, tag_name, self._attributes);
|
||||
|
||||
// Allow element-specific types to copy their runtime state
|
||||
_ = Element.Build.call(node.as(Element), "cloned", .{ self, node.as(Element), deep, frame }) catch |err| {
|
||||
|
||||
@@ -66,7 +66,7 @@ pub fn setBody(self: *HTMLDocument, html: []const u8, frame: *Frame) !void {
|
||||
|
||||
// Build a fresh <body> holding the parsed HTML as its children. Fragment
|
||||
// parsing strips any <html>/<body>/<head> wrappers the author included.
|
||||
const new_body_node = try frame.createElementNS(.html, "body", null);
|
||||
const new_body_node = try Frame.node_factory.createElementNS(frame, .html, "body", null);
|
||||
if (html.len > 0) {
|
||||
try frame.parseHtmlAsChildren(new_body_node, html);
|
||||
}
|
||||
@@ -153,7 +153,7 @@ pub fn setTitle(self: *HTMLDocument, title: []const u8, frame: *Frame) !void {
|
||||
}
|
||||
|
||||
// No title element found, create one
|
||||
const title_node = try frame.createElementNS(.html, "title", null);
|
||||
const title_node = try Frame.node_factory.createElementNS(frame, .html, "title", null);
|
||||
const title_element = title_node.as(Element);
|
||||
|
||||
// Only add text if non-empty
|
||||
|
||||
@@ -139,7 +139,7 @@ pub fn observe(self: *IntersectionObserver, target: *Element, frame: *Frame) !vo
|
||||
|
||||
try self._observing.append(self._arena, target);
|
||||
if (self._observing.items.len == 1) {
|
||||
try frame.registerIntersectionObserver(self);
|
||||
try Frame.observers.registerIntersectionObserver(frame, self);
|
||||
}
|
||||
|
||||
try self._tracked.put(self._arena, target, {});
|
||||
@@ -147,7 +147,7 @@ pub fn observe(self: *IntersectionObserver, target: *Element, frame: *Frame) !vo
|
||||
// Check intersection for this new target and schedule delivery
|
||||
try self.checkIntersection(target, frame);
|
||||
if (self._pending_entries.items.len > 0) {
|
||||
try frame.scheduleIntersectionDelivery();
|
||||
try Frame.observers.scheduleIntersectionDelivery(frame);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element, frame: *Frame) v
|
||||
}
|
||||
|
||||
if (original_length > 0 and self._observing.items.len == 0) {
|
||||
frame.unregisterIntersectionObserver(self);
|
||||
Frame.observers.unregisterIntersectionObserver(frame, self);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ pub fn disconnect(self: *IntersectionObserver, frame: *Frame) void {
|
||||
self._tracked.clearRetainingCapacity();
|
||||
|
||||
if (self._observing.items.len > 0) {
|
||||
frame.unregisterIntersectionObserver(self);
|
||||
Frame.observers.unregisterIntersectionObserver(frame, self);
|
||||
}
|
||||
self._observing.clearRetainingCapacity();
|
||||
}
|
||||
@@ -298,7 +298,7 @@ pub fn checkIntersections(self: *IntersectionObserver, frame: *Frame) !void {
|
||||
}
|
||||
|
||||
if (self._pending_entries.items.len > 0) {
|
||||
try frame.scheduleIntersectionDelivery();
|
||||
try Frame.observers.scheduleIntersectionDelivery(frame);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
|
||||
});
|
||||
|
||||
if (self._observing.items.len == 1) {
|
||||
try frame.registerMutationObserver(self);
|
||||
try Frame.observers.registerMutationObserver(frame, self);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ pub fn disconnect(self: *MutationObserver, frame: *Frame) void {
|
||||
self._pending_records.clearRetainingCapacity();
|
||||
|
||||
if (self._observing.items.len > 0) {
|
||||
frame.unregisterMutationObserver(self);
|
||||
Frame.observers.unregisterMutationObserver(frame, self);
|
||||
}
|
||||
self._observing.clearRetainingCapacity();
|
||||
}
|
||||
@@ -244,7 +244,7 @@ pub fn notifyAttributeChange(
|
||||
|
||||
try self._pending_records.append(self._arena, record);
|
||||
|
||||
try frame.scheduleMutationDelivery();
|
||||
try Frame.observers.scheduleMutationDelivery(frame);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -288,7 +288,7 @@ pub fn notifyCharacterDataChange(
|
||||
|
||||
try self._pending_records.append(self._arena, record);
|
||||
|
||||
try frame.scheduleMutationDelivery();
|
||||
try Frame.observers.scheduleMutationDelivery(frame);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -332,7 +332,7 @@ pub fn notifyChildListChange(
|
||||
|
||||
try self._pending_records.append(self._arena, record);
|
||||
|
||||
try frame.scheduleMutationDelivery();
|
||||
try Frame.observers.scheduleMutationDelivery(frame);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -622,12 +622,12 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, frame: *Fra
|
||||
if (new_node == ref_node_) {
|
||||
frame.domChanged();
|
||||
|
||||
if (frame.hasMutationObservers()) {
|
||||
if (Frame.observers.hasMutationObservers(frame)) {
|
||||
const parent = new_node._parent.?;
|
||||
const previous_sibling = new_node.previousSibling();
|
||||
const next_sibling = new_node.nextSibling();
|
||||
const replaced = [_]*Node{new_node};
|
||||
frame.childListChange(parent, &replaced, &replaced, previous_sibling, next_sibling);
|
||||
Frame.observers.notifyChildListChange(frame, parent, &replaced, &replaced, previous_sibling, next_sibling);
|
||||
}
|
||||
|
||||
return new_node;
|
||||
@@ -832,10 +832,10 @@ pub fn cloneNode(self: *Node, deep_: ?bool, frame: *Frame) CloneError!*Node {
|
||||
.cdata => |cd| {
|
||||
const data = cd.getData().str();
|
||||
return switch (cd._type) {
|
||||
.text => frame.createTextNode(data),
|
||||
.cdata_section => frame.createCDATASection(data),
|
||||
.comment => frame.createComment(data),
|
||||
.processing_instruction => |pi| frame.createProcessingInstruction(pi._target, data),
|
||||
.text => Frame.node_factory.createTextNode(frame, data),
|
||||
.cdata_section => Frame.node_factory.createCDATASection(frame, data),
|
||||
.comment => Frame.node_factory.createComment(frame, data),
|
||||
.processing_instruction => |pi| Frame.node_factory.createProcessingInstruction(frame, pi._target, data),
|
||||
};
|
||||
},
|
||||
.element => |el| return el.clone(deep, frame),
|
||||
@@ -1314,7 +1314,7 @@ pub const NodeOrText = union(enum) {
|
||||
pub fn toNode(self: *const NodeOrText, frame: *Frame) !*Node {
|
||||
return switch (self.*) {
|
||||
.node => |n| n,
|
||||
.text => |txt| frame.createTextNode(txt),
|
||||
.text => |txt| Frame.node_factory.createTextNode(frame, txt),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -353,8 +353,8 @@ pub fn insertNode(self: *Range, node: *Node, frame: *Frame) !void {
|
||||
const before_text = text_data[0..offset];
|
||||
const after_text = text_data[offset..];
|
||||
|
||||
const before = try frame.createTextNode(before_text);
|
||||
const after = try frame.createTextNode(after_text);
|
||||
const before = try Frame.node_factory.createTextNode(frame, before_text);
|
||||
const after = try Frame.node_factory.createTextNode(frame, after_text);
|
||||
|
||||
_ = try parent.replaceChild(before, container, frame);
|
||||
_ = try parent.insertBefore(node, before.nextSibling(), frame);
|
||||
@@ -390,7 +390,7 @@ pub fn deleteContents(self: *Range, frame: *Frame) !void {
|
||||
frame.arena,
|
||||
&.{ text_data[0..self._proto._start_offset], text_data[self._proto._end_offset..] },
|
||||
);
|
||||
frame.characterDataChange(self._proto._start_container, old_value);
|
||||
Frame.observers.notifyCharacterDataChange(frame, self._proto._start_container, old_value);
|
||||
} else {
|
||||
// Delete child nodes in range.
|
||||
// Capture count before the loop: removeChild triggers live range
|
||||
@@ -459,7 +459,7 @@ pub fn cloneContents(self: *const Range, frame: *Frame) !*DocumentFragment {
|
||||
const text_data = self._proto._start_container.getData().str();
|
||||
if (self._proto._start_offset < text_data.len and self._proto._end_offset <= text_data.len) {
|
||||
const cloned_text = text_data[self._proto._start_offset..self._proto._end_offset];
|
||||
const text_node = try frame.createTextNode(cloned_text);
|
||||
const text_node = try Frame.node_factory.createTextNode(frame, cloned_text);
|
||||
_ = try fragment.asNode().appendChild(text_node, frame);
|
||||
}
|
||||
} else {
|
||||
@@ -481,7 +481,7 @@ pub fn cloneContents(self: *const Range, frame: *Frame) !*DocumentFragment {
|
||||
if (self._proto._start_offset < text_data.len) {
|
||||
// Clone from start_offset to end of text
|
||||
const cloned_text = text_data[self._proto._start_offset..];
|
||||
const text_node = try frame.createTextNode(cloned_text);
|
||||
const text_node = try Frame.node_factory.createTextNode(frame, cloned_text);
|
||||
_ = try fragment.asNode().appendChild(text_node, frame);
|
||||
}
|
||||
}
|
||||
@@ -504,7 +504,7 @@ pub fn cloneContents(self: *const Range, frame: *Frame) !*DocumentFragment {
|
||||
if (self._proto._end_offset > 0 and self._proto._end_offset <= text_data.len) {
|
||||
// Clone from start to end_offset
|
||||
const cloned_text = text_data[0..self._proto._end_offset];
|
||||
const text_node = try frame.createTextNode(cloned_text);
|
||||
const text_node = try Frame.node_factory.createTextNode(frame, cloned_text);
|
||||
_ = try fragment.asNode().appendChild(text_node, frame);
|
||||
}
|
||||
}
|
||||
@@ -550,9 +550,9 @@ pub fn createContextualFragment(self: *const Range, html: []const u8, frame: *Fr
|
||||
// Create a temporary element of the same type as the context for parsing
|
||||
// This preserves the parsing context without modifying the original node
|
||||
const temp_node = if (context_node.is(Node.Element)) |el|
|
||||
try frame.createElementNS(el._namespace, el.getTagNameLower(), null)
|
||||
try Frame.node_factory.createElementNS(frame, el._namespace, el.getTagNameLower(), null)
|
||||
else
|
||||
try frame.createElementNS(.html, "div", null);
|
||||
try Frame.node_factory.createElementNS(frame, .html, "div", null);
|
||||
|
||||
try frame.parseContextualFragment(temp_node, html);
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ const Comment = @This();
|
||||
_proto: *CData,
|
||||
|
||||
pub fn init(str: ?js.NullableString, frame: *Frame) !*Comment {
|
||||
const node = try frame.createComment(if (str) |s| s.value else "");
|
||||
const node = try Frame.node_factory.createComment(frame, if (str) |s| s.value else "");
|
||||
return node.as(Comment);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ const Text = @This();
|
||||
_proto: *CData,
|
||||
|
||||
pub fn init(str: ?js.NullableString, frame: *Frame) !*Text {
|
||||
const node = try frame.createTextNode(if (str) |s| s.value else "");
|
||||
const node = try Frame.node_factory.createTextNode(frame, if (str) |s| s.value else "");
|
||||
return node.as(Text);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ pub fn splitText(self: *Text, offset: usize, frame: *Frame) !*Text {
|
||||
const byte_offset = CData.utf16OffsetToUtf8(data, offset) catch return error.IndexSizeError;
|
||||
|
||||
const new_data = data[byte_offset..];
|
||||
const new_node = try frame.createTextNode(new_data);
|
||||
const new_node = try Frame.node_factory.createTextNode(frame, new_data);
|
||||
const new_text = new_node.as(Text);
|
||||
|
||||
const node = self._proto.asNode();
|
||||
|
||||
@@ -116,7 +116,7 @@ pub fn construct(new_target: js.Function, frame: *Frame) !*Element {
|
||||
if (frame._upgrading_element) |node| {
|
||||
return node.is(Element) orelse return error.IllegalConstructor;
|
||||
}
|
||||
return frame.constructCustomElement(new_target);
|
||||
return Frame.node_factory.constructCustomElement(frame, new_target);
|
||||
}
|
||||
|
||||
pub const Type = union(enum) {
|
||||
@@ -288,7 +288,7 @@ pub fn setInnerText(self: *HtmlElement, text: []const u8, frame: *Frame) !void {
|
||||
}
|
||||
|
||||
// Create and append text node
|
||||
const text_node = try frame.createTextNode(text);
|
||||
const text_node = try Frame.node_factory.createTextNode(frame, text);
|
||||
try frame.appendNode(parent, text_node, .{ .child_already_connected = false });
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ const Audio = @This();
|
||||
_proto: *Media,
|
||||
|
||||
pub fn constructor(maybe_url: ?String, frame: *Frame) !*Media {
|
||||
const node = try frame.createElementNS(.html, "audio", null);
|
||||
const node = try Frame.node_factory.createElementNS(frame, .html, "audio", null);
|
||||
const el = node.as(Element);
|
||||
|
||||
const list = try el.getOrCreateAttributeList(frame);
|
||||
|
||||
@@ -9,7 +9,7 @@ const Image = @This();
|
||||
_proto: *HtmlElement,
|
||||
|
||||
pub fn constructor(w_: ?u32, h_: ?u32, frame: *Frame) !*Image {
|
||||
const node = try frame.createElementNS(.html, "img", null);
|
||||
const node = try Frame.node_factory.createElementNS(frame, .html, "img", null);
|
||||
const el = node.as(Element);
|
||||
|
||||
if (w_) |w| blk: {
|
||||
|
||||
@@ -106,7 +106,7 @@ pub fn setDefaultValue(self: *TextArea, value: []const u8, frame: *Frame) !void
|
||||
}
|
||||
|
||||
// No text child exists, create one
|
||||
const text_node = try frame.createTextNode(value);
|
||||
const text_node = try Frame.node_factory.createTextNode(frame, value);
|
||||
_ = try node.appendChild(text_node, frame);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,9 @@
|
||||
|
||||
const std = @import("std");
|
||||
const CDP = @import("../CDP.zig");
|
||||
const Frame = @import("../../browser/Frame.zig");
|
||||
|
||||
const dom_button = @import("../../browser/Frame.zig").mouse_button;
|
||||
const dom_button = Frame.user_input.mouse_button;
|
||||
|
||||
pub fn processMessage(cmd: *CDP.Command) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
@@ -74,7 +75,7 @@ fn dispatchKeyEvent(cmd: *CDP.Command) !void {
|
||||
.metaKey = params.modifiers & 4 == 4,
|
||||
.shiftKey = params.modifiers & 8 == 8,
|
||||
}, frame);
|
||||
try frame.triggerKeyboard(keyboard_event);
|
||||
try Frame.user_input.triggerKeyboard(frame, keyboard_event);
|
||||
// result already sent
|
||||
}
|
||||
|
||||
@@ -124,10 +125,10 @@ fn dispatchMouseEvent(cmd: *CDP.Command) !void {
|
||||
};
|
||||
|
||||
switch (params.type) {
|
||||
.mousePressed => try frame.triggerMousePress(params.x, params.y, button),
|
||||
.mouseReleased => try frame.triggerMouseRelease(params.x, params.y, button, params.clickCount),
|
||||
.mouseMoved => try frame.triggerMouseMove(params.x, params.y),
|
||||
.mouseWheel => try frame.triggerMouseWheel(params.x, params.y, params.deltaX, params.deltaY),
|
||||
.mousePressed => try Frame.user_input.triggerMousePress(frame, params.x, params.y, button),
|
||||
.mouseReleased => try Frame.user_input.triggerMouseRelease(frame, params.x, params.y, button, params.clickCount),
|
||||
.mouseMoved => try Frame.user_input.triggerMouseMove(frame, params.x, params.y),
|
||||
.mouseWheel => try Frame.user_input.triggerMouseWheel(frame, params.x, params.y, params.deltaX, params.deltaY),
|
||||
}
|
||||
// result already sent
|
||||
}
|
||||
@@ -141,7 +142,7 @@ fn insertText(cmd: *CDP.Command) !void {
|
||||
const bc = cmd.browser_context orelse return;
|
||||
const frame = bc.session.currentFrame() orelse return;
|
||||
|
||||
try frame.insertText(params.text);
|
||||
try Frame.user_input.insertText(frame, params.text);
|
||||
|
||||
try cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user