mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-31 17:55:59 -04:00
webapi: moveBefore
Add Element/DocuemntFragment/Document moveBefore. The API is relatively complicated compared to other node-moving APIs, so there are still some edge cases. But this implementation should be at a level where having the API fixes more things than it breaks.
This commit is contained in:
@@ -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,
|
||||
|
||||
|
||||
109
src/browser/tests/node/move_before.html
Normal file
109
src/browser/tests/node/move_before.html
Normal file
@@ -0,0 +1,109 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
<div id=parent>
|
||||
<div id=a></div>
|
||||
<div id=b></div>
|
||||
<div id=c></div>
|
||||
</div>
|
||||
|
||||
<script id=moveBefore_reorder>
|
||||
{
|
||||
function ids(parent) {
|
||||
return Array.from(parent.children).map((e) => e.id);
|
||||
}
|
||||
|
||||
const parent = $('#parent');
|
||||
const a = $('#a');
|
||||
const b = $('#b');
|
||||
const c = $('#c');
|
||||
|
||||
// moveBefore returns undefined and reorders in place
|
||||
testing.expectEqual(undefined, parent.moveBefore(c, a));
|
||||
testing.expectEqual(['c', 'a', 'b'], ids(parent));
|
||||
|
||||
// null reference child appends
|
||||
parent.moveBefore(c, null);
|
||||
testing.expectEqual(['a', 'b', 'c'], ids(parent));
|
||||
|
||||
// moving a node before itself is a no-op
|
||||
parent.moveBefore(a, a);
|
||||
testing.expectEqual(['a', 'b', 'c'], ids(parent));
|
||||
parent.moveBefore(c, c);
|
||||
testing.expectEqual(['a', 'b', 'c'], ids(parent));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=moveBefore_validation>
|
||||
{
|
||||
const parent = $('#parent');
|
||||
const a = $('#a');
|
||||
|
||||
// Missing second argument is a TypeError (child is required but nullable).
|
||||
testing.expectError('TypeError', () => parent.moveBefore(a));
|
||||
testing.expectError('TypeError', () => parent.moveBefore(null, null));
|
||||
testing.expectError('TypeError', () => parent.moveBefore({}, null));
|
||||
|
||||
// Reference child whose parent is not `parent` -> NotFoundError.
|
||||
const other = document.createElement('div');
|
||||
document.body.appendChild(other);
|
||||
testing.withError((err) => {
|
||||
testing.expectEqual('NotFoundError', err.name);
|
||||
}, () => parent.moveBefore(a, other));
|
||||
other.remove();
|
||||
|
||||
// Moving a node into its own descendant -> HierarchyRequestError.
|
||||
testing.withError((err) => {
|
||||
testing.expectEqual('HierarchyRequestError', err.name);
|
||||
}, () => a.moveBefore(parent, null));
|
||||
|
||||
// Connected target into a disconnected parent (different roots) -> throws.
|
||||
const disconnected = document.createElement('div');
|
||||
testing.withError((err) => {
|
||||
testing.expectEqual('HierarchyRequestError', err.name);
|
||||
}, () => disconnected.moveBefore(a, null));
|
||||
|
||||
// A DocumentFragment is neither Element nor CharacterData -> throws.
|
||||
testing.withError((err) => {
|
||||
testing.expectEqual('HierarchyRequestError', err.name);
|
||||
}, () => parent.moveBefore(new DocumentFragment(), null));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=moveBefore_disconnected_same_tree>
|
||||
{
|
||||
// A move entirely within one disconnected tree is allowed.
|
||||
const root = document.createElement('div');
|
||||
const dest = root.appendChild(document.createElement('div'));
|
||||
const p = root.appendChild(document.createElement('p'));
|
||||
|
||||
dest.moveBefore(p, null);
|
||||
testing.expectEqual(p, dest.firstChild);
|
||||
testing.expectEqual(dest, p.parentNode);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=moveBefore_connected_move_callback>
|
||||
{
|
||||
// When connectedMoveCallback is defined, an atomic move fires it instead of
|
||||
// disconnectedCallback/connectedCallback.
|
||||
let log = [];
|
||||
class MoveAware extends HTMLElement {
|
||||
connectedMoveCallback() { log.push('move'); }
|
||||
connectedCallback() { log.push('connected'); }
|
||||
disconnectedCallback() { log.push('disconnected'); }
|
||||
}
|
||||
customElements.define('mb-move-aware', MoveAware);
|
||||
|
||||
const from = document.body.appendChild(document.createElement('div'));
|
||||
const to = document.body.appendChild(document.createElement('div'));
|
||||
const el = from.appendChild(document.createElement('mb-move-aware'));
|
||||
|
||||
log = [];
|
||||
to.moveBefore(el, null);
|
||||
testing.expectEqual(['move'], log);
|
||||
testing.expectEqual(to, el.parentNode);
|
||||
|
||||
from.remove();
|
||||
to.remove();
|
||||
}
|
||||
</script>
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const 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
|
||||
|
||||
Reference in New Issue
Block a user