mirror of
				https://github.com/lightpanda-io/browser.git
				synced 2025-10-29 15:13:28 +00:00 
			
		
		
		
	Merge pull request #1022 from lightpanda-io/slot
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				e2e-test / zig build release (push) Has been cancelled
				
			
		
			
				
	
				e2e-test / demo-scripts (push) Has been cancelled
				
			
		
			
				
	
				e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
				
			
		
			
				
	
				e2e-test / perf-fmt (push) Has been cancelled
				
			
		
			
				
	
				zig-test / zig build dev (push) Has been cancelled
				
			
		
			
				
	
				zig-test / browser fetch (push) Has been cancelled
				
			
		
			
				
	
				zig-test / zig test (push) Has been cancelled
				
			
		
			
				
	
				zig-test / perf-fmt (push) Has been cancelled
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	e2e-test / zig build release (push) Has been cancelled
				
			e2e-test / demo-scripts (push) Has been cancelled
				
			e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
				
			e2e-test / perf-fmt (push) Has been cancelled
				
			zig-test / zig build dev (push) Has been cancelled
				
			zig-test / browser fetch (push) Has been cancelled
				
			zig-test / zig test (push) Has been cancelled
				
			zig-test / perf-fmt (push) Has been cancelled
				
			Start working on HTMLSlotElement
This commit is contained in:
		| @@ -560,6 +560,17 @@ pub const Element = struct { | ||||
|         state.shadow_root = sr; | ||||
|         parser.documentFragmentSetHost(sr.proto, @ptrCast(@alignCast(self))); | ||||
|  | ||||
|         // Storing the ShadowRoot on the element makes sense, it's the ShadowRoot's | ||||
|         // parent. When we render, we go top-down, so we'll have the element, get | ||||
|         // its shadowroot, and go on. that's what the above code does. | ||||
|         // But we sometimes need to go bottom-up, e.g when we have a slot element | ||||
|         // and want to find the containing parent. Unforatunately , we don't have | ||||
|         // that link, so we need to create it. In the DOM, the ShadowRoot is | ||||
|         // represented by this DocumentFragment (it's the ShadowRoot's base prototype) | ||||
|         // So we can also store the ShadowRoot in the DocumentFragment's state. | ||||
|         const fragment_state = try page.getOrCreateNodeState(@ptrCast(@alignCast(fragment))); | ||||
|         fragment_state.shadow_root = sr; | ||||
|  | ||||
|         return sr; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -37,6 +37,7 @@ const DocumentFragment = @import("document_fragment.zig").DocumentFragment; | ||||
| const HTMLCollection = @import("html_collection.zig").HTMLCollection; | ||||
| const HTMLAllCollection = @import("html_collection.zig").HTMLAllCollection; | ||||
| const HTMLCollectionIterator = @import("html_collection.zig").HTMLCollectionIterator; | ||||
| const ShadowRoot = @import("shadow_root.zig").ShadowRoot; | ||||
| const Walker = @import("walker.zig").WalkerDepthFirst; | ||||
|  | ||||
| // HTML | ||||
| @@ -354,11 +355,23 @@ pub const Node = struct { | ||||
|     // - An Element inside a standard web page will return an HTMLDocument object representing the entire page (or <iframe>). | ||||
|     // - An Element inside a shadow DOM will return the associated ShadowRoot. | ||||
|     // - An Element that is not attached to a document or a shadow tree will return the root of the DOM tree it belongs to | ||||
|     pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }) !Union { | ||||
|     const GetRootNodeResult = union(enum) { | ||||
|         shadow_root: *ShadowRoot, | ||||
|         node: Union, | ||||
|     }; | ||||
|     pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }, page: *Page) !GetRootNodeResult { | ||||
|         if (options) |options_| if (options_.composed) { | ||||
|             log.warn(.web_api, "not implemented", .{ .feature = "getRootNode composed" }); | ||||
|         }; | ||||
|         return try Node.toInterface(try parser.nodeGetRootNode(self)); | ||||
|  | ||||
|         const root = try parser.nodeGetRootNode(self); | ||||
|         if (page.getNodeState(root)) |state| { | ||||
|             if (state.shadow_root) |sr| { | ||||
|                 return .{ .shadow_root = sr }; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return .{ .node = try Node.toInterface(root) }; | ||||
|     } | ||||
|  | ||||
|     pub fn _hasChildNodes(self: *parser.Node) !bool { | ||||
|   | ||||
| @@ -26,6 +26,7 @@ const Page = @import("../page.zig").Page; | ||||
| const urlStitch = @import("../../url.zig").URL.stitch; | ||||
| const URL = @import("../url/url.zig").URL; | ||||
| const Node = @import("../dom/node.zig").Node; | ||||
| const NodeUnion = @import("../dom/node.zig").Union; | ||||
| const Element = @import("../dom/element.zig").Element; | ||||
| const DataSet = @import("DataSet.zig"); | ||||
|  | ||||
| @@ -85,6 +86,7 @@ pub const Interfaces = .{ | ||||
|     HTMLScriptElement, | ||||
|     HTMLSourceElement, | ||||
|     HTMLSpanElement, | ||||
|     HTMLSlotElement, | ||||
|     HTMLStyleElement, | ||||
|     HTMLTableElement, | ||||
|     HTMLTableCaptionElement, | ||||
| @@ -1008,6 +1010,101 @@ pub const HTMLSpanElement = struct { | ||||
|     pub const subtype = .node; | ||||
| }; | ||||
|  | ||||
| pub const HTMLSlotElement = struct { | ||||
|     pub const Self = parser.Slot; | ||||
|     pub const prototype = *HTMLElement; | ||||
|     pub const subtype = .node; | ||||
|  | ||||
|     pub fn get_name(self: *parser.Slot) !?[]const u8 { | ||||
|         return (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "name")) orelse ""; | ||||
|     } | ||||
|  | ||||
|     pub fn set_name(self: *parser.Slot, value: []const u8) !void { | ||||
|         return parser.elementSetAttribute(@ptrCast(@alignCast(self)), "name", value); | ||||
|     } | ||||
|  | ||||
|     const AssignedNodesOpts = struct { | ||||
|         flatten: bool = false, | ||||
|     }; | ||||
|     pub fn _assignedNodes(self: *parser.Slot, opts_: ?AssignedNodesOpts, page: *Page) ![]NodeUnion { | ||||
|         const opts = opts_ orelse AssignedNodesOpts{ .flatten = false }; | ||||
|  | ||||
|         if (try findAssignedSlotNodes(self, opts, page)) |nodes| { | ||||
|             return nodes; | ||||
|         } | ||||
|  | ||||
|         if (!opts.flatten) { | ||||
|             return &.{}; | ||||
|         } | ||||
|  | ||||
|         const node: *parser.Node = @ptrCast(@alignCast(self)); | ||||
|         const nl = try parser.nodeGetChildNodes(node); | ||||
|         const len = try parser.nodeListLength(nl); | ||||
|         if (len == 0) { | ||||
|             return &.{}; | ||||
|         } | ||||
|  | ||||
|         var assigned = try page.call_arena.alloc(NodeUnion, len); | ||||
|         var i: usize = 0; | ||||
|         while (true) : (i += 1) { | ||||
|             const child = try parser.nodeListItem(nl, @intCast(i)) orelse break; | ||||
|             assigned[i] = try Node.toInterface(child); | ||||
|         } | ||||
|         return assigned[0..i]; | ||||
|     } | ||||
|  | ||||
|     fn findAssignedSlotNodes(self: *parser.Slot, opts: AssignedNodesOpts, page: *Page) !?[]NodeUnion { | ||||
|         if (opts.flatten) { | ||||
|             log.warn(.web_api, "not implemented", .{ .feature = "HTMLSlotElement flatten assignedNodes" }); | ||||
|         } | ||||
|  | ||||
|         const slot_name = try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "name"); | ||||
|         const node: *parser.Node = @ptrCast(@alignCast(self)); | ||||
|         var root = try parser.nodeGetRootNode(node); | ||||
|         if (page.getNodeState(root)) |state| { | ||||
|             if (state.shadow_root) |sr| { | ||||
|                 root = @ptrCast(@alignCast(sr.host)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         var arr: std.ArrayList(NodeUnion) = .empty; | ||||
|         const w = @import("../dom/walker.zig").WalkerChildren{}; | ||||
|         var next: ?*parser.Node = null; | ||||
|         while (true) { | ||||
|             next = try w.get_next(root, next) orelse break; | ||||
|             if (try parser.nodeType(next.?) != .element) { | ||||
|                 if (slot_name == null) { | ||||
|                     // default slot (with no name), takes everything | ||||
|                     try arr.append(page.call_arena, try Node.toInterface(next.?)); | ||||
|                 } | ||||
|                 continue; | ||||
|             } | ||||
|             const el: *parser.Element = @ptrCast(@alignCast(next.?)); | ||||
|             const element_slot = try parser.elementGetAttribute(el, "slot"); | ||||
|  | ||||
|             if (nullableStringsAreEqual(slot_name, element_slot)) { | ||||
|                 // either they're the same string or they are both null | ||||
|                 try arr.append(page.call_arena, try Node.toInterface(next.?)); | ||||
|                 continue; | ||||
|             } | ||||
|         } | ||||
|         return if (arr.items.len == 0) null else arr.items; | ||||
|     } | ||||
|  | ||||
|     fn nullableStringsAreEqual(a: ?[]const u8, b: ?[]const u8) bool { | ||||
|         if (a == null and b == null) { | ||||
|             return true; | ||||
|         } | ||||
|         if (a) |aa| { | ||||
|             const bb = b orelse return false; | ||||
|             return std.mem.eql(u8, aa, bb); | ||||
|         } | ||||
|  | ||||
|         // a is null, but b isn't (else the first guard clause would have hit) | ||||
|         return false; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| pub const HTMLStyleElement = struct { | ||||
|     pub const Self = parser.Style; | ||||
|     pub const prototype = *HTMLElement; | ||||
| @@ -1168,6 +1265,7 @@ pub fn toInterfaceFromTag(comptime T: type, e: *parser.Element, tag: parser.Tag) | ||||
|         .select => .{ .HTMLSelectElement = @as(*parser.Select, @ptrCast(e)) }, | ||||
|         .source => .{ .HTMLSourceElement = @as(*parser.Source, @ptrCast(e)) }, | ||||
|         .span => .{ .HTMLSpanElement = @as(*parser.Span, @ptrCast(e)) }, | ||||
|         .slot => .{ .HTMLSlotElement = @as(*parser.Slot, @ptrCast(e)) }, | ||||
|         .style => .{ .HTMLStyleElement = @as(*parser.Style, @ptrCast(e)) }, | ||||
|         .table => .{ .HTMLTableElement = @as(*parser.Table, @ptrCast(e)) }, | ||||
|         .caption => .{ .HTMLTableCaptionElement = @as(*parser.TableCaption, @ptrCast(e)) }, | ||||
| @@ -1484,6 +1582,10 @@ test "Browser: HTML.HTMLScriptElement" { | ||||
|     try testing.htmlRunner("html/script/inline_defer.html"); | ||||
| } | ||||
|  | ||||
| test "Browser: HTML.HTMLSlotElement" { | ||||
|     try testing.htmlRunner("html/html_slot_element.html"); | ||||
| } | ||||
|  | ||||
| const Check = struct { | ||||
|     input: []const u8, | ||||
|     expected: ?[]const u8 = null, // Needed when input != expected | ||||
|   | ||||
| @@ -259,6 +259,7 @@ pub const Tag = enum(u8) { | ||||
|     _var = c.DOM_HTML_ELEMENT_TYPE_VAR, | ||||
|     video = c.DOM_HTML_ELEMENT_TYPE_VIDEO, | ||||
|     wbr = c.DOM_HTML_ELEMENT_TYPE_WBR, | ||||
|     slot = c.DOM_HTML_ELEMENT_TYPE_SLOT, | ||||
|     undef = c.DOM_HTML_ELEMENT_TYPE__UNKNOWN, | ||||
|  | ||||
|     pub fn all() []Tag { | ||||
| @@ -1973,6 +1974,7 @@ pub const Script = c.dom_html_script_element; | ||||
| pub const Select = c.dom_html_select_element; | ||||
| pub const Source = struct { base: *c.dom_html_element }; | ||||
| pub const Span = struct { base: *c.dom_html_element }; | ||||
| pub const Slot = c.dom_html_slot_element; | ||||
| pub const Style = c.dom_html_style_element; | ||||
| pub const Table = c.dom_html_table_element; | ||||
| pub const TableCaption = c.dom_html_table_caption_element; | ||||
|   | ||||
							
								
								
									
										66
									
								
								src/tests/html/html_slot_element.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/tests/html/html_slot_element.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| <script src="../testing.js"></script> | ||||
| <script> | ||||
|   class LightPanda extends HTMLElement { | ||||
|     constructor() { | ||||
|       super(); | ||||
|     } | ||||
|  | ||||
|     connectedCallback() { | ||||
|       const shadow = this.attachShadow({ mode: "open" }); | ||||
|  | ||||
|       const slot1 = document.createElement('slot'); | ||||
|       slot1.name = 'slot-1'; | ||||
|       shadow.appendChild(slot1); | ||||
|  | ||||
|       switch (this.getAttribute('mode')) { | ||||
|         case '1': | ||||
|           slot1.innerHTML = 'hello'; | ||||
|           break; | ||||
|         case '2': | ||||
|           const slot2 = document.createElement('slot'); | ||||
|           shadow.appendChild(slot2); | ||||
|           break; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   window.customElements.define("lp-test", LightPanda); | ||||
| </script> | ||||
|  | ||||
| <lp-test id=lp1 mode=1></lp-test> | ||||
| <lp-test id=lp2 mode=0></lp-test> | ||||
| <lp-test id=lp3 mode=0>default</lp-test> | ||||
| <lp-test id=lp4 mode=1><p slot=other>default</p></lp-test> | ||||
| <lp-test id=lp5 mode=1><p slot=slot-1>default</p> xx <b slot=slot-1>other</b></lp-test> | ||||
| <lp-test id=lp6 mode=2>More <p slot=slot-1>default2</p> <span>!!</span></lp-test> | ||||
|  | ||||
| <script id=HTMLSlotElement> | ||||
|   function assertNodes(expected, actual) { | ||||
|     actual = actual.map((n) => n.id || n.textContent) | ||||
|     testing.expectEqual(expected, actual); | ||||
|   } | ||||
|  | ||||
|   for (let idx of [1, 2, 3, 4]) { | ||||
|     const lp = $(`#lp${idx}`); | ||||
|     const slot = lp.shadowRoot.querySelector('slot'); | ||||
|  | ||||
|     assertNodes([], slot.assignedNodes()); | ||||
|     assertNodes([], slot.assignedNodes({})); | ||||
|     assertNodes([], slot.assignedNodes({flatten: false})); | ||||
|     if (lp.getAttribute('mode') === '1') { | ||||
|       assertNodes(['hello'], slot.assignedNodes({flatten: true})); | ||||
|     } else { | ||||
|       assertNodes([], slot.assignedNodes({flatten: true})); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   const lp5 = $('#lp5'); | ||||
|   const s5 = lp5.shadowRoot.querySelector('slot'); | ||||
|   assertNodes(['default', 'other'], s5.assignedNodes()); | ||||
|  | ||||
|   const lp6 = $('#lp6'); | ||||
|   const s6 = lp6.shadowRoot.querySelectorAll('slot'); | ||||
|   assertNodes(['default2'], s6[0].assignedNodes({})); | ||||
|   assertNodes(['default2'], s6[0].assignedNodes({flatten: true})); | ||||
|   assertNodes(['More ', ' ', '!!'], s6[1].assignedNodes({})); | ||||
|   assertNodes(['More ', ' ', '!!'], s6[1].assignedNodes({flatten: true})); | ||||
| </script> | ||||
		Reference in New Issue
	
	Block a user
	 Karl Seguin
					Karl Seguin