ui: adjusting slot placement and classes

This commit is contained in:
nick-delirium 2025-06-05 17:18:00 +02:00
parent 7a85f318b6
commit 8603439716
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
4 changed files with 98 additions and 61 deletions

View file

@ -110,7 +110,7 @@ abstract class VParent<T extends Node = Node> extends VNode<T> {
/* Inserting */
this.mountChildren();
if (this.notMontedChildren.size !== 0) {
console.error('VParent: Something went wrong with children insertion');
console.error('VParent: Something went wrong with children insertion', this.notMontedChildren);
}
/* Removing in-between */
const { node } = this;
@ -155,62 +155,6 @@ export class VDocument extends VParent<Document> {
}
}
export class VSlot extends VElement {
assignedNodes: VChild[] = [];
addAssigned(child: VChild) {
if (this.assignedNodes.indexOf(child) === -1) {
this.assignedNodes.push(child);
this.notMontedChildren.add(child);
}
}
removeAssigned(child: VChild) {
this.assignedNodes = this.assignedNodes.filter((c) => c !== child);
this.notMontedChildren.delete(child);
}
private mountAssigned() {
let nextMounted: VChild | null = null;
for (let i = this.assignedNodes.length - 1; i >= 0; i--) {
const child = this.assignedNodes[i];
if (this.notMontedChildren.has(child)) {
this.node.insertBefore(
child.node,
nextMounted ? nextMounted.node : null,
);
this.notMontedChildren.delete(child);
}
if (!this.notMontedChildren.has(child)) {
nextMounted = child;
}
}
}
applyChanges() {
if (this.assignedNodes.length > 0) {
this.assignedNodes.forEach((c) => c.applyChanges());
this.mountAssigned();
const { node } = this;
const realChildren = node.childNodes;
if (realChildren.length > 0) {
for (let j = 0; j < this.assignedNodes.length; j++) {
while (realChildren[j] !== this.assignedNodes[j].node) {
if (isNode(realChildren[j])) {
node.removeChild(realChildren[j]);
}
}
}
}
while (realChildren.length > this.assignedNodes.length) {
node.removeChild(node.lastChild as Node);
}
} else {
super.applyChanges();
}
}
}
export class VShadowRoot extends VParent<ShadowRoot> {
constructor(protected readonly createNode: () => ShadowRoot) {
super();
@ -353,6 +297,62 @@ export class VElement extends VParent<Element> {
}
}
export class VSlot extends VElement {
assignedNodes: VChild[] = [];
addAssigned(child: VChild) {
if (this.assignedNodes.indexOf(child) === -1) {
this.assignedNodes.push(child);
this.notMontedChildren.add(child);
}
}
removeAssigned(child: VChild) {
this.assignedNodes = this.assignedNodes.filter((c) => c !== child);
this.notMontedChildren.delete(child);
}
private mountAssigned() {
let nextMounted: VChild | null = null;
for (let i = this.assignedNodes.length - 1; i >= 0; i--) {
const child = this.assignedNodes[i];
if (this.notMontedChildren.has(child)) {
this.node.insertBefore(
child.node,
nextMounted ? nextMounted.node : null,
);
this.notMontedChildren.delete(child);
}
if (!this.notMontedChildren.has(child)) {
nextMounted = child;
}
}
}
applyChanges() {
if (this.assignedNodes.length > 0) {
this.assignedNodes.forEach((c) => c.applyChanges());
this.mountAssigned();
const { node } = this;
const realChildren = node.childNodes;
if (realChildren.length > 0) {
for (let j = 0; j < this.assignedNodes.length; j++) {
while (realChildren[j] !== this.assignedNodes[j].node) {
if (isNode(realChildren[j])) {
node.removeChild(realChildren[j]);
}
}
}
}
while (realChildren.length > this.assignedNodes.length) {
node.removeChild(node.lastChild as Node);
}
} else {
super.applyChanges();
}
}
}
export class VHTMLElement extends VElement {
constructor(node: HTMLElement) {
super('HTML', false);

View file

@ -1,7 +1,7 @@
{
"name": "@openreplay/tracker",
"description": "The OpenReplay tracker main package",
"version": "17.0.0-beta.0",
"version": "17.1.0",
"keywords": [
"logging",
"replay"

View file

@ -45,6 +45,7 @@ type TagTypeMap = {
style: HTMLStyleElement | SVGStyleElement
link: HTMLLinkElement
canvas: HTMLCanvasElement
slot: HTMLSlotElement
}
export function hasTag<T extends keyof TagTypeMap>(
el: Node,

View file

@ -11,7 +11,8 @@ import {
UnbindNodes,
SetNodeAttribute,
AdoptedSSInsertRuleURLBased,
AdoptedSSAddOwner
AdoptedSSAddOwner,
SetNodeSlot,
} from '../messages.gen.js'
import App from '../index.js'
import {
@ -199,6 +200,7 @@ export default abstract class Observer {
private readonly indexes: Array<number> = []
private readonly attributesMap: Map<number, Set<string>> = new Map()
private readonly textSet: Set<number> = new Set()
private readonly slotMap: Map<number, number | undefined> = new Map()
private readonly disableSprites: boolean = false
/**
* this option means that, instead of using link element with href to load css,
@ -428,6 +430,18 @@ export default abstract class Observer {
private bindNode(node: Node): void {
const [id, isNew] = this.app.nodes.registerNode(node)
if (isElementNode(node) && hasTag(node, 'slot')) {
this.app.nodes.attachNodeListener(node, 'slotchange', () => {
const sl = node as HTMLSlotElement
sl.assignedNodes({ flatten: true }).forEach((n) => {
const nid = this.app.nodes.getID(n)
if (nid !== undefined) {
this.recents.set(nid, RecentsType.Removed)
this.commitNode(nid)
}
})
})
}
if (isNew) {
this.recents.set(id, RecentsType.New)
} else if (this.recents.get(id) !== RecentsType.New) {
@ -463,6 +477,9 @@ export default abstract class Observer {
private unbindTree(node: Node) {
const id = this.app.nodes.unregisterNode(node)
if (id !== undefined) {
this.slotMap.delete(id)
}
if (id !== undefined && this.recents.get(id) === RecentsType.Removed) {
// Sending RemoveNode only for parent to maintain
this.app.send(RemoveNode(id))
@ -501,8 +518,8 @@ export default abstract class Observer {
if (isRootNode(node)) {
return true
}
// @ts-ignore SALESFORCE
const parent = node.assignedSlot ? node.assignedSlot : node.parentNode
const slot = (node as any).assignedSlot as HTMLSlotElement | null
const parent = node.parentNode
let parentID: number | undefined
// Disable parent check for the upper context HTMLHtmlElement, because it is root there... (before)
@ -576,10 +593,29 @@ export default abstract class Observer {
this.app.send(CreateTextNode(id, parentID as number, index))
this.throttledSetNodeData(id, parent as Element, node.data)
}
if (slot) {
const slotID = this.app.nodes.getID(slot)
console.log('Openreplay: slotID', slotID, 'for node', id, node, 'slot', slot)
if (slotID !== undefined) {
this.slotMap.set(id, slotID)
this.app.send(SetNodeSlot(id, slotID))
}
}
return true
}
if (recentsType === RecentsType.Removed && parentID !== undefined) {
this.app.send(MoveNode(id, parentID, index))
console.log('RM Openreplay', id, node, 'slot', slot)
if (slot) {
const slotID = this.app.nodes.getID(slot)
if (slotID !== undefined && this.slotMap.get(id) !== slotID) {
this.slotMap.set(id, slotID)
this.app.send(SetNodeSlot(id, slotID))
}
} else if (this.slotMap.has(id)) {
this.slotMap.delete(id)
this.app.send(SetNodeSlot(id, 0))
}
}
const attr = this.attributesMap.get(id)
if (attr !== undefined) {