mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-30 09:16:07 -04:00
Merge pull request #2959 from lightpanda-io/dom-pr-18-html-misc
webapi: response content type, element scroll events, translate, SVGAElement.relList
This commit is contained in:
@@ -236,6 +236,10 @@ _factory: *Factory,
|
||||
|
||||
_load_state: LoadState = .waiting,
|
||||
|
||||
// The navigation response's MIME essence, recorded when the first chunk
|
||||
// arrives and applied to the new document (document.contentType) once the
|
||||
// navigation commits. null means the text/html default.
|
||||
_pending_content_type: ?[]const u8 = null,
|
||||
_parse_state: ParseState = .pre,
|
||||
|
||||
/// `frameErrorCallback` swallows the failure into a placeholder page;
|
||||
@@ -1427,6 +1431,22 @@ fn frameDataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void {
|
||||
});
|
||||
}
|
||||
|
||||
// Record the response's MIME essence so the resulting document
|
||||
// reports it (document.contentType). text/html keeps the default, so an
|
||||
// HTML response (parsed from the header, or sniffed when there's no
|
||||
// header) never needs an override; inside this branch the essence can
|
||||
// no longer be text/html.
|
||||
self._pending_content_type = null;
|
||||
if (mime.content_type != .text_html) {
|
||||
if (transfer.contentType()) |ct| {
|
||||
const end = std.mem.indexOfScalarPos(u8, ct, 0, ';') orelse ct.len;
|
||||
const essence = std.mem.trim(u8, ct[0..end], " \t");
|
||||
if (essence.len > 0) {
|
||||
self._pending_content_type = try std.ascii.allocLowerString(self.arena, essence);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (mime.content_type) {
|
||||
.text_html => {
|
||||
// Normalize and store the charset using encoding_rs canonical names
|
||||
@@ -1501,6 +1521,11 @@ fn frameDoneCallback(ctx: *anyopaque) !void {
|
||||
//We need to handle different navigation types differently.
|
||||
try self._session.navigation.commitNavigation(self);
|
||||
|
||||
if (self._pending_content_type) |content_type| {
|
||||
self.document._content_type = content_type;
|
||||
self._pending_content_type = null;
|
||||
}
|
||||
|
||||
defer if (comptime IS_DEBUG) {
|
||||
log.debug(.frame, "frame load complete", .{
|
||||
.url = self.url,
|
||||
|
||||
@@ -56,6 +56,9 @@ pub const NamespaceUriLookup = std.AutoHashMapUnmanaged(*Element, []const u8);
|
||||
pub const ScrollPosition = struct {
|
||||
x: u32 = 0,
|
||||
y: u32 = 0,
|
||||
// Throttle state for the async scroll/scrollend dispatch (mirrors
|
||||
// Window._scroll_pos.state).
|
||||
state: enum { scroll, end, done } = .done,
|
||||
};
|
||||
pub const ScrollPositionLookup = std.AutoHashMapUnmanaged(*Element, ScrollPosition);
|
||||
|
||||
@@ -1334,30 +1337,47 @@ pub fn getClientRects(self: *Element, frame: *Frame) ![]*DOMRect {
|
||||
return rects;
|
||||
}
|
||||
|
||||
// Scroll positions live in the map of the element's own frame — not the
|
||||
// caller's, which differs when a same-origin script scrolls an element in
|
||||
// another frame (e.g. inside an iframe). All scroll accessors resolve the
|
||||
// owner frame first so the state, the fired events and the document
|
||||
// comparison stay in the element's frame.
|
||||
pub fn getScrollTop(self: *Element, frame: *Frame) u32 {
|
||||
const pos = frame._element_scroll_positions.get(self) orelse return 0;
|
||||
const owner = self.ownerFrame(frame);
|
||||
const pos = owner._element_scroll_positions.get(self) orelse return 0;
|
||||
return pos.y;
|
||||
}
|
||||
|
||||
pub fn setScrollTop(self: *Element, value: i32, frame: *Frame) !void {
|
||||
const gop = try frame._element_scroll_positions.getOrPut(frame.arena, self);
|
||||
const owner = self.ownerFrame(frame);
|
||||
const gop = try owner._element_scroll_positions.getOrPut(owner.arena, self);
|
||||
if (!gop.found_existing) {
|
||||
gop.value_ptr.* = .{};
|
||||
}
|
||||
gop.value_ptr.y = @intCast(@max(0, value));
|
||||
const new_y: u32 = @intCast(@max(0, value));
|
||||
if (gop.value_ptr.y != new_y) {
|
||||
gop.value_ptr.y = new_y;
|
||||
try self.scheduleScrollEvents(owner);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getScrollLeft(self: *Element, frame: *Frame) u32 {
|
||||
const pos = frame._element_scroll_positions.get(self) orelse return 0;
|
||||
const owner = self.ownerFrame(frame);
|
||||
const pos = owner._element_scroll_positions.get(self) orelse return 0;
|
||||
return pos.x;
|
||||
}
|
||||
|
||||
pub fn setScrollLeft(self: *Element, value: i32, frame: *Frame) !void {
|
||||
const gop = try frame._element_scroll_positions.getOrPut(frame.arena, self);
|
||||
const owner = self.ownerFrame(frame);
|
||||
const gop = try owner._element_scroll_positions.getOrPut(owner.arena, self);
|
||||
if (!gop.found_existing) {
|
||||
gop.value_ptr.* = .{};
|
||||
}
|
||||
gop.value_ptr.x = @intCast(@max(0, value));
|
||||
const new_x: u32 = @intCast(@max(0, value));
|
||||
if (gop.value_ptr.x != new_x) {
|
||||
gop.value_ptr.x = new_x;
|
||||
try self.scheduleScrollEvents(owner);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getScrollHeight(self: *Element, frame: *Frame) f64 {
|
||||
@@ -1643,10 +1663,13 @@ const ScrollToOpts = union(enum) {
|
||||
|
||||
pub fn scrollTo(self: *Element, opts: ?ScrollToOpts, y: ?i32, frame: *Frame) !void {
|
||||
const o = opts orelse return;
|
||||
const gop = try frame._element_scroll_positions.getOrPut(frame.arena, self);
|
||||
const owner = self.ownerFrame(frame);
|
||||
const gop = try owner._element_scroll_positions.getOrPut(owner.arena, self);
|
||||
if (!gop.found_existing) {
|
||||
gop.value_ptr.* = .{};
|
||||
}
|
||||
const old_x = gop.value_ptr.x;
|
||||
const old_y = gop.value_ptr.y;
|
||||
switch (o) {
|
||||
.x => |x| {
|
||||
gop.value_ptr.x = @intCast(@max(0, x));
|
||||
@@ -1657,12 +1680,16 @@ pub fn scrollTo(self: *Element, opts: ?ScrollToOpts, y: ?i32, frame: *Frame) !vo
|
||||
if (dict.top) |top| gop.value_ptr.y = @intCast(@max(0, top));
|
||||
},
|
||||
}
|
||||
if (gop.value_ptr.x != old_x or gop.value_ptr.y != old_y) {
|
||||
try self.scheduleScrollEvents(owner);
|
||||
}
|
||||
}
|
||||
|
||||
// scrollBy(): like scrollTo() but relative to the current position.
|
||||
pub fn scrollBy(self: *Element, opts: ?ScrollToOpts, y: ?i32, frame: *Frame) !void {
|
||||
const o = opts orelse return;
|
||||
const gop = try frame._element_scroll_positions.getOrPut(frame.arena, self);
|
||||
const owner = self.ownerFrame(frame);
|
||||
const gop = try owner._element_scroll_positions.getOrPut(owner.arena, self);
|
||||
if (!gop.found_existing) {
|
||||
gop.value_ptr.* = .{};
|
||||
}
|
||||
@@ -1672,8 +1699,108 @@ pub fn scrollBy(self: *Element, opts: ?ScrollToOpts, y: ?i32, frame: *Frame) !vo
|
||||
};
|
||||
gop.value_ptr.x = @intCast(@max(0, @as(i32, @intCast(gop.value_ptr.x)) + dx));
|
||||
gop.value_ptr.y = @intCast(@max(0, @as(i32, @intCast(gop.value_ptr.y)) + dy));
|
||||
if (dx != 0 or dy != 0) {
|
||||
try self.scheduleScrollEvents(owner);
|
||||
}
|
||||
}
|
||||
|
||||
// Scrolling an element fires a scroll event and then a scrollend event,
|
||||
// asynchronously and throttled, mirroring Window.scrollTo. Scrolls of the
|
||||
// scrolling element (the root) are fired at the document instead.
|
||||
// `frame` is the element's owner frame (resolved by the public accessors).
|
||||
fn scheduleScrollEvents(self: *Element, frame: *Frame) !void {
|
||||
const gop = try frame._element_scroll_positions.getOrPut(frame.arena, self);
|
||||
if (!gop.found_existing) {
|
||||
gop.value_ptr.* = .{};
|
||||
}
|
||||
gop.value_ptr.state = .scroll;
|
||||
|
||||
const task = try frame._factory.create(ScrollEventTask{ .frame = frame, .element = self });
|
||||
try frame.js.scheduler.add(task, ScrollEventTask.dispatchScroll, 10, .{ .low_priority = true });
|
||||
try frame.js.scheduler.add(task, ScrollEventTask.dispatchScrollEnd, 20, .{ .low_priority = true, .finalizer = ScrollEventTask.cancelled });
|
||||
}
|
||||
|
||||
const ScrollEventTask = struct {
|
||||
frame: *Frame,
|
||||
element: *Element,
|
||||
|
||||
fn cancelled(ctx: *anyopaque) void {
|
||||
const self: *ScrollEventTask = @ptrCast(@alignCast(ctx));
|
||||
self.deinit();
|
||||
}
|
||||
|
||||
fn deinit(self: *ScrollEventTask) void {
|
||||
self.frame._factory.destroy(self);
|
||||
}
|
||||
|
||||
fn dispatchScroll(ctx: *anyopaque) anyerror!?u32 {
|
||||
const self: *ScrollEventTask = @ptrCast(@alignCast(ctx));
|
||||
const f = self.frame;
|
||||
const pos = f._element_scroll_positions.getPtr(self.element) orelse return null;
|
||||
if (pos.state != .scroll) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Event = @import("Event.zig");
|
||||
const event = try Event.initTrusted(comptime .wrap("scroll"), .{ .bubbles = self.bubbles() }, f._page);
|
||||
try f._event_manager.dispatch(self.eventTarget(), event);
|
||||
|
||||
// can't use gop on _element_scroll_positions above, since the JS callback
|
||||
// can mutate _element_scroll_positions and invalidate any pointers
|
||||
if (f._element_scroll_positions.getPtr(self.element)) |p| {
|
||||
p.state = .end;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn dispatchScrollEnd(ctx: *anyopaque) anyerror!?u32 {
|
||||
const self: *ScrollEventTask = @ptrCast(@alignCast(ctx));
|
||||
|
||||
const f = self.frame;
|
||||
const pos = f._element_scroll_positions.getPtr(self.element) orelse {
|
||||
self.deinit();
|
||||
return null;
|
||||
};
|
||||
|
||||
switch (pos.state) {
|
||||
// The scroll event is still pending; retry in 10ms. Must not destroy
|
||||
// the task here — the entry is re-scheduled and runs again.
|
||||
.scroll => return 10,
|
||||
.end => {},
|
||||
.done => {
|
||||
self.deinit();
|
||||
return null;
|
||||
},
|
||||
}
|
||||
|
||||
defer self.deinit();
|
||||
const Event = @import("Event.zig");
|
||||
const event = try Event.initTrusted(comptime .wrap("scrollend"), .{ .bubbles = self.bubbles() }, f._page);
|
||||
try f._event_manager.dispatch(self.eventTarget(), event);
|
||||
|
||||
// can't use gop on _element_scroll_positions above, since the JS callback
|
||||
// can mutate _element_scroll_positions and invalidate any pointers
|
||||
if (f._element_scroll_positions.getPtr(self.element)) |p| {
|
||||
p.state = .done;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn eventTarget(self: *ScrollEventTask) *EventTarget {
|
||||
if (self.frame.document.getDocumentElement() == self.element) {
|
||||
return self.frame.document.asEventTarget();
|
||||
}
|
||||
return self.element.asEventTarget();
|
||||
}
|
||||
|
||||
// Scroll events fired at an element don't bubble; only document-level
|
||||
// scrolls (the scrolling element, dispatched at the document) do.
|
||||
fn bubbles(self: *ScrollEventTask) bool {
|
||||
return self.frame.document.getDocumentElement() == self.element;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn format(self: *Element, writer: *std.Io.Writer) !void {
|
||||
try writer.writeByte('<');
|
||||
try writer.writeAll(self.getTagNameDump());
|
||||
|
||||
@@ -345,6 +345,28 @@ pub fn setHidden(self: *HtmlElement, hidden: bool, frame: *Frame) !void {
|
||||
}
|
||||
}
|
||||
|
||||
// The translate IDL attribute reflects the element's translation mode:
|
||||
// translate="yes"/"" enables it, "no" disables it, anything else (or no
|
||||
// attribute) inherits from the parent, defaulting to enabled.
|
||||
pub fn getTranslate(self: *HtmlElement) bool {
|
||||
var node: ?*Node = self.asElement().asNode();
|
||||
while (node) |n| : (node = n.parentNode()) {
|
||||
const el = n.is(Element) orelse continue;
|
||||
const value = el.getAttributeSafe(comptime .wrap("translate")) orelse continue;
|
||||
if (value.len == 0 or std.ascii.eqlIgnoreCase(value, "yes")) {
|
||||
return true;
|
||||
}
|
||||
if (std.ascii.eqlIgnoreCase(value, "no")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn setTranslate(self: *HtmlElement, translate: bool, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("translate"), .wrap(if (translate) "yes" else "no"), frame);
|
||||
}
|
||||
|
||||
pub fn getPopover(self: *HtmlElement) ?[]const u8 {
|
||||
const s = popover.getState(self.asElement()) orelse return null;
|
||||
return @tagName(s);
|
||||
@@ -1707,6 +1729,7 @@ pub const JsApi = struct {
|
||||
pub const autofocus = bridge.accessor(HtmlElement.getAutofocus, HtmlElement.setAutofocus, .{ .ce_reactions = true });
|
||||
pub const dir = bridge.accessor(HtmlElement.getDir, HtmlElement.setDir, .{ .ce_reactions = true });
|
||||
pub const hidden = bridge.accessor(HtmlElement.getHidden, HtmlElement.setHidden, .{ .ce_reactions = true });
|
||||
pub const translate = bridge.accessor(HtmlElement.getTranslate, HtmlElement.setTranslate, .{ .ce_reactions = true });
|
||||
pub const popover = bridge.accessor(HtmlElement.getPopover, HtmlElement.setPopover, .{ .ce_reactions = true });
|
||||
pub const showPopover = bridge.function(HtmlElement.showPopover, .{});
|
||||
pub const hidePopover = bridge.function(HtmlElement.hidePopover, .{});
|
||||
|
||||
@@ -22,6 +22,7 @@ const Frame = @import("../../../Frame.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const AnimatedString = @import("../../svg/AnimatedString.zig");
|
||||
const DOMTokenList = @import("../../collections.zig").DOMTokenList;
|
||||
|
||||
const Graphics = @import("Graphics.zig");
|
||||
|
||||
@@ -35,6 +36,10 @@ pub fn asNode(self: *A) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
pub fn getRelList(self: *A, frame: *Frame) !*DOMTokenList {
|
||||
return self.asElement().getRelList(frame);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(A);
|
||||
|
||||
@@ -48,4 +53,6 @@ pub const JsApi = struct {
|
||||
fn _href(self: *A, frame: *Frame) !*AnimatedString {
|
||||
return AnimatedString.getOrCreate(self.asElement(), .href, frame);
|
||||
}
|
||||
|
||||
pub const relList = bridge.accessor(A.getRelList, null, .{});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user