diff --git a/src/browser/CustomElementReactions.zig b/src/browser/CustomElementReactions.zig
index b14d77b31..79ef08090 100644
--- a/src/browser/CustomElementReactions.zig
+++ b/src/browser/CustomElementReactions.zig
@@ -110,6 +110,10 @@ pub fn enqueueConnected(self: *Self, frame: *Frame, element: *Element) !void {
try self.route(frame, .{ .connected = element });
}
+pub fn enqueueMove(self: *Self, frame: *Frame, element: *Element) !void {
+ try self.route(frame, .{ .move = element });
+}
+
pub fn enqueueDisconnected(self: *Self, frame: *Frame, element: *Element) !void {
try self.route(frame, .{ .disconnected = element });
}
@@ -143,6 +147,7 @@ pub fn enqueueAttributeChanged(
pub const Reaction = union(enum) {
connected: *Element,
disconnected: *Element,
+ move: *Element,
adopted: Adopted,
attribute_changed: AttributeChanged,
diff --git a/src/browser/tests/node/move_before.html b/src/browser/tests/node/move_before.html
new file mode 100644
index 000000000..4f005b6e6
--- /dev/null
+++ b/src/browser/tests/node/move_before.html
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig
index 07cb80f6c..0b8dc17e5 100644
--- a/src/browser/webapi/Document.zig
+++ b/src/browser/webapi/Document.zig
@@ -619,6 +619,10 @@ pub fn replaceChildren(self: *Document, nodes: []const Node.NodeOrText, frame: *
return self.asNode().replaceChildren(nodes, frame);
}
+pub fn moveBefore(self: *Document, node: js.Value, child: js.Value, frame: *Frame) !void {
+ return self.asNode().moveBefore(node, child, frame);
+}
+
pub fn elementFromPoint(self: *Document, x: f64, y: f64, frame: *Frame) !?*Element {
// DFS in document order; topmost = last visited element whose rect contains (x, y).
//
@@ -1213,6 +1217,7 @@ pub const JsApi = struct {
pub const importNode = bridge.function(Document.importNode, .{ .dom_exception = true, .ce_reactions = true });
pub const append = bridge.function(Document.append, .{ .dom_exception = true, .ce_reactions = true });
pub const prepend = bridge.function(Document.prepend, .{ .dom_exception = true, .ce_reactions = true });
+ pub const moveBefore = bridge.function(Document.moveBefore, .{ .dom_exception = true, .ce_reactions = true });
pub const replaceChildren = bridge.function(Document.replaceChildren, .{ .dom_exception = true, .ce_reactions = true });
pub const elementFromPoint = bridge.function(Document.elementFromPoint, .{});
pub const elementsFromPoint = bridge.function(Document.elementsFromPoint, .{});
diff --git a/src/browser/webapi/DocumentFragment.zig b/src/browser/webapi/DocumentFragment.zig
index 313ceec9b..da09e9bbc 100644
--- a/src/browser/webapi/DocumentFragment.zig
+++ b/src/browser/webapi/DocumentFragment.zig
@@ -146,6 +146,10 @@ pub fn replaceChildren(self: *DocumentFragment, nodes: []const Node.NodeOrText,
return self.asNode().replaceChildren(nodes, frame);
}
+pub fn moveBefore(self: *DocumentFragment, node: js.Value, child: js.Value, frame: *Frame) !void {
+ return self.asNode().moveBefore(node, child, frame);
+}
+
pub fn getInnerHTML(self: *DocumentFragment, writer: *std.Io.Writer, frame: *Frame) !void {
const dump = @import("../dump.zig");
return dump.children(self.asNode(), .{ .shadow = .complete }, writer, frame);
@@ -213,6 +217,7 @@ pub const JsApi = struct {
pub const lastElementChild = bridge.accessor(DocumentFragment.lastElementChild, null, .{});
pub const append = bridge.function(DocumentFragment.append, .{ .dom_exception = true, .ce_reactions = true });
pub const prepend = bridge.function(DocumentFragment.prepend, .{ .dom_exception = true, .ce_reactions = true });
+ pub const moveBefore = bridge.function(DocumentFragment.moveBefore, .{ .dom_exception = true, .ce_reactions = true });
pub const replaceChildren = bridge.function(DocumentFragment.replaceChildren, .{ .dom_exception = true, .ce_reactions = true });
pub const innerHTML = bridge.accessor(_getInnerHTML, _setInnerHTML, .{ .ce_reactions = true });
diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig
index 42f00be7b..4fdeb7df8 100644
--- a/src/browser/webapi/Element.zig
+++ b/src/browser/webapi/Element.zig
@@ -1045,6 +1045,10 @@ pub fn prepend(self: *Element, nodes: []const Node.NodeOrText, frame: *Frame) !v
}
}
+pub fn moveBefore(self: *Element, node: js.Value, child: js.Value, frame: *Frame) !void {
+ return self.asNode().moveBefore(node, child, frame);
+}
+
pub fn before(self: *Element, nodes: []const Node.NodeOrText, frame: *Frame) !void {
const node = self.asNode();
const parent = node.parentNode() orelse return;
@@ -2014,6 +2018,7 @@ pub const JsApi = struct {
pub const remove = bridge.function(Element.remove, .{ .ce_reactions = true });
pub const append = bridge.function(Element.append, .{ .dom_exception = true, .ce_reactions = true });
pub const prepend = bridge.function(Element.prepend, .{ .dom_exception = true, .ce_reactions = true });
+ pub const moveBefore = bridge.function(Element.moveBefore, .{ .dom_exception = true, .ce_reactions = true });
pub const before = bridge.function(Element.before, .{ .dom_exception = true, .ce_reactions = true });
pub const after = bridge.function(Element.after, .{ .dom_exception = true, .ce_reactions = true });
pub const firstElementChild = bridge.accessor(Element.firstElementChild, null, .{});
diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig
index b61bcda0c..af1e4395c 100644
--- a/src/browser/webapi/Node.zig
+++ b/src/browser/webapi/Node.zig
@@ -692,6 +692,99 @@ pub fn replaceChild(self: *Node, new_child: *Node, old_child: *Node, frame: *Fra
return old_child;
}
+// `node` and `child` are taken as raw js.Values rather than `*Node`/`?*Node`
+// because both must be present, and `child` is nullable.
+pub fn moveBefore(self: *Node, node_val: js.Value, child_val: js.Value, frame: *Frame) !void {
+ const node = try node_val.toZig(*Node);
+ const child: ?*Node = if (child_val.isNullOrUndefined()) null else try child_val.toZig(*Node);
+
+ // parent must be a Document, DocumentFragment, or Element node.
+ switch (self._type) {
+ .document, .document_fragment, .element => {},
+ else => return error.HierarchyError,
+ }
+
+ if (node.contains(self)) {
+ return error.HierarchyError;
+ }
+
+ if (self.getRootNode(.{ .composed = true }) != node.getRootNode(.{ .composed = true })) {
+ return error.HierarchyError;
+ }
+
+ // node must be an Element or a CharacterData node.
+ switch (node._type) {
+ .element, .cdata => {},
+ else => return error.HierarchyError,
+ }
+
+ if (self._type == .document) {
+ switch (node._type) {
+ .cdata => |cd| {
+ if (cd._type == .text) {
+ // A Text node cannot be a child of a document.
+ return error.HierarchyError;
+ }
+ },
+ .element => {
+ var it = self.childrenIterator();
+ while (it.next()) |existing| {
+ if (existing._type == .element and existing != node) {
+ // A document can have at most one element child.
+ return error.HierarchyError;
+ }
+ }
+ },
+ else => {},
+ }
+ }
+
+ if (child) |c| {
+ if (c._parent != self) {
+ // If child is non-null, its parent must be parent.
+ return error.NotFound;
+ }
+ }
+
+ // Moving a node before itself is a relative no-op: the reference child
+ // becomes the node's own next sibling.
+ var ref = child;
+ if (ref) |r| {
+ if (r == node) {
+ ref = node.nextSibling();
+ }
+ }
+
+ frame.domChanged();
+
+ // selfand node share a root, so the connectedness won't change. This API
+ // should appear atomic as much as possible. We can skip the id-map
+ // management (because it won't change) and custom elements shouldn't fire
+ // disconnect/connected callbacks. But MutationObservers and ranges still
+ // fire
+ const connected = node.isConnected();
+
+ if (node._parent) |old_parent| {
+ frame.removeNode(old_parent, node, .{ .will_be_reconnected = connected });
+ }
+
+ if (ref) |r| {
+ try frame.insertNodeRelative(self, node, .{ .before = r }, .{ .child_already_connected = connected });
+ } else {
+ try frame.appendNode(self, node, .{ .child_already_connected = connected });
+ }
+
+ if (connected) {
+ // Enqueue on a move callback (if we're connected) for any nested
+ // custom element
+ const TreeWalker = @import("TreeWalker.zig");
+ var tw = TreeWalker.Full.Elements.init(node, .{});
+ while (tw.next()) |el| {
+ Element.Html.Custom.enqueueMoveCallbackOnElement(el, frame);
+ }
+ }
+}
+
pub fn getNodeValue(self: *const Node) ?String {
return switch (self._type) {
.cdata => |c| c.getData(),
diff --git a/src/browser/webapi/element/html/Custom.zig b/src/browser/webapi/element/html/Custom.zig
index 1e9130777..8880195c2 100644
--- a/src/browser/webapi/element/html/Custom.zig
+++ b/src/browser/webapi/element/html/Custom.zig
@@ -16,6 +16,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../../js/js.zig");
@@ -132,6 +133,20 @@ pub fn enqueueDisconnectedCallbackOnElement(element: *Element, frame: *Frame) vo
};
}
+// Enqueues an atomic-move reaction (moveBefore). Unlike connect/disconnect there
+// is no dedup state to flip: a move always fires, and the element's connected
+// state is unchanged by the move.
+pub fn enqueueMoveCallbackOnElement(element: *Element, frame: *Frame) void {
+ if (element.is(Custom)) |custom| {
+ if (custom._definition == null) return;
+ } else {
+ if (frame.getCustomizedBuiltInDefinition(element) == null) return;
+ }
+ frame._ce_reactions.enqueueMove(frame, element) catch |err| {
+ log.warn(.bug, "ce_reactions enqueue fail", .{ .err = err });
+ };
+}
+
pub fn enqueueAdoptedCallbackOnElement(element: *Element, old_document: *Document, new_document: *Document, frame: *Frame) void {
if (element.is(Custom)) |custom| {
if (custom._definition == null) return;
@@ -163,37 +178,36 @@ pub fn fireReaction(reaction: Reaction, frame: *Frame) void {
.connected => |el| {
if (el.is(Custom)) |custom| {
custom.invokeCallback("connectedCallback", .{}, frame);
- } else if (frame.getCustomizedBuiltInDefinition(el)) |definition| {
- invokeCallbackOnElement(el, definition, "connectedCallback", .{}, frame);
+ } else if (frame.getCustomizedBuiltInDefinition(el)) |_| {
+ invokeCallbackOnElement(el, "connectedCallback", .{}, frame);
}
},
.disconnected => |el| {
if (el.is(Custom)) |custom| {
custom.invokeCallback("disconnectedCallback", .{}, frame);
- } else if (frame.getCustomizedBuiltInDefinition(el)) |definition| {
- invokeCallbackOnElement(el, definition, "disconnectedCallback", .{}, frame);
+ } else if (frame.getCustomizedBuiltInDefinition(el)) |_| {
+ invokeCallbackOnElement(el, "disconnectedCallback", .{}, frame);
}
},
.adopted => |a| {
if (a.element.is(Custom)) |custom| {
custom.invokeCallback("adoptedCallback", .{ a.old_document, a.new_document }, frame);
- } else if (frame.getCustomizedBuiltInDefinition(a.element)) |definition| {
- invokeCallbackOnElement(a.element, definition, "adoptedCallback", .{ a.old_document, a.new_document }, frame);
+ } else if (frame.getCustomizedBuiltInDefinition(a.element)) |_| {
+ invokeCallbackOnElement(a.element, "adoptedCallback", .{ a.old_document, a.new_document }, frame);
}
},
.attribute_changed => |a| {
if (a.element.is(Custom)) |custom| {
custom.invokeCallback("attributeChangedCallback", .{ a.name, a.old_value, a.new_value, a.namespace }, frame);
- } else if (frame.getCustomizedBuiltInDefinition(a.element)) |definition| {
- invokeCallbackOnElement(a.element, definition, "attributeChangedCallback", .{ a.name, a.old_value, a.new_value, a.namespace }, frame);
+ } else if (frame.getCustomizedBuiltInDefinition(a.element)) |_| {
+ invokeCallbackOnElement(a.element, "attributeChangedCallback", .{ a.name, a.old_value, a.new_value, a.namespace }, frame);
}
},
+ .move => |el| invokeCallbackOnElement(el, "move", .{}, frame),
}
}
-fn invokeCallbackOnElement(element: *Element, definition: *CustomElementDefinition, comptime callback_name: [:0]const u8, args: anytype, frame: *Frame) void {
- _ = definition;
-
+fn invokeCallbackOnElement(element: *Element, comptime callback_name: [:0]const u8, args: anytype, frame: *Frame) void {
var ls: js.Local.Scope = undefined;
frame.js.localScope(&ls);
defer ls.deinit();
@@ -202,8 +216,20 @@ fn invokeCallbackOnElement(element: *Element, definition: *CustomElementDefiniti
const js_val = ls.local.zigValueToJs(element, .{}) catch return;
const js_element = js_val.toObject();
- // Call the callback method if it exists
- js_element.callMethod(void, callback_name, args) catch return;
+ if (comptime std.mem.eql(u8, callback_name, "move") == false) {
+ // Call the callback method if it exists
+ js_element.callMethod(void, callback_name, args) catch {};
+ return;
+ }
+
+ // for "move", we call "connectedMoveCallback" if it exists, else we fallback
+ // to "disconnectedCallback" + "connectedCallback"
+ if (js_element.has("connectedMoveCallback")) {
+ js_element.callMethod(void, "connectedMoveCallback", .{}) catch return;
+ } else {
+ js_element.callMethod(void, "disconnectedCallback", .{}) catch return;
+ js_element.callMethod(void, "connectedCallback", .{}) catch return;
+ }
}
// Check if element has "is" attribute and attach customized built-in definition