Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 41x 41x 41x 41x 41x 41x 11x 11x 11x 11x 11x 11x 11x 6x 6x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 79x 79x 79x 5x 5x 5x 5x 5x 5x 5x 5x 5x 74x 74x 79x 33x 33x 74x 74x 74x 74x 74x 74x 74x 74x 79x 64x 64x 64x 64x 64x 64x 64x 14x 14x 64x 49x 49x 17x 6x 6x 49x 32x 32x 49x 50x 1x 1x 1x 1x 64x 64x 74x 74x 79x 9x 9x 3x 3x 9x 9x 79x 11x 11x 11x 11x 11x 11x 50x 50x 11x 11x 11x 11x 11x 11x 38x 38x 38x 38x 38x 36x 36x 36x 38x 38x 38x 36x 36x 2x 2x 36x 4x 4x 36x 38x 11x | /**
* Handles patching the DOM with new HTML content using a simple diffing algorithm.
* This approach is more efficient than innerHTML as it preserves existing DOM nodes.
*/
export class DomPatcher {
/**
* Patches the target element with the provided HTML.
* @param {Element} target - The element to patch.
* @param {string} html - The new HTML content.
*/
patch(target, html) {
const parser = new DOMParser();
const newDoc = parser.parseFromString(html, 'text/html');
const newRoot = newDoc.body;
this.#patchNode(target, newRoot, true, true);
}
/**
* Patches an existing element with a new element structure in-place.
* @param {Element} oldElement - The existing element.
* @param {Element} newElement - The new element structure.
*/
patchElement(oldElement, newElement) {
this.#patchNode(oldElement, newElement, false, true);
}
/**
* Recursively diffs and patches two nodes.
* @param {Node} oldNode - The existing DOM node.
* @param {Node} newNode - The new node structure.
* @param {boolean} [isBodyWrapper=false] - Whether the new node is a temporary body wrapper.
* @param {boolean} [isPatchRoot=false] - Whether this is the root node of the patching operation.
* @private
*/
#patchNode(oldNode, newNode, isBodyWrapper = false, isPatchRoot = false) {
if (!isPatchRoot && oldNode.nodeType === Node.ELEMENT_NODE && oldNode.nodeName === 'SLOT' && oldNode.hasAttribute('data-avenx-transcluded')) {
if (newNode.nodeType === Node.ELEMENT_NODE) {
this.#patchAttributes(oldNode, newNode);
oldNode.setAttribute('data-avenx-transcluded', 'true');
}
return;
}
if (!isPatchRoot && oldNode.nodeType === Node.ELEMENT_NODE && oldNode.hasAttribute('data-avenx-comp')) {
if (newNode.nodeType === Node.ELEMENT_NODE) {
this.#patchAttributes(oldNode, newNode);
const compInstance = oldNode.__avenx_comp_instance;
if (compInstance && typeof compInstance.__updateTranscludedContent === 'function') {
compInstance.__updateTranscludedContent(newNode.childNodes);
}
}
return;
}
// 1. Update attributes if it's an element (skip if it is the temporary body wrapper)
if (!isBodyWrapper && oldNode.nodeType === Node.ELEMENT_NODE && newNode.nodeType === Node.ELEMENT_NODE) {
this.#patchAttributes(oldNode, newNode);
}
// 2. Diff children
const oldChildren = Array.from(oldNode.childNodes);
const newChildren = Array.from(newNode.childNodes);
let oldIndex = 0;
let newIndex = 0;
while (newIndex < newChildren.length) {
const newChild = newChildren[newIndex];
let oldChild = oldChildren[oldIndex];
// Skip items managed by ListManager in the old DOM
while (oldChild && oldChild.nodeType === Node.ELEMENT_NODE && oldChild.hasAttribute('data-ax-list-item')) {
oldIndex++;
oldChild = oldChildren[oldIndex];
}
if (!oldChild) {
// Add remaining new children
oldNode.appendChild(newChild.cloneNode(true));
} else if (this.#isSameNodeType(oldChild, newChild)) {
// Nodes are same type, patch them
if (oldChild.nodeType === Node.TEXT_NODE) {
if (oldChild.textContent !== newChild.textContent) {
oldChild.textContent = newChild.textContent;
}
} else {
this.#patchNode(oldChild, newChild);
}
oldIndex++;
} else {
// Nodes are different, replace
oldNode.replaceChild(newChild.cloneNode(true), oldChild);
oldIndex++;
}
newIndex++;
}
// Remove remaining old children (that are not managed by ListManager)
while (oldIndex < oldChildren.length) {
const oldChild = oldChildren[oldIndex];
if (!(oldChild.nodeType === Node.ELEMENT_NODE && oldChild.hasAttribute('data-ax-list-item'))) {
oldNode.removeChild(oldChild);
}
oldIndex++;
}
}
/**
* Checks if two nodes are of the same type and name.
* @private
*/
#isSameNodeType(nodeA, nodeB) {
return nodeA.nodeType === nodeB.nodeType && nodeA.nodeName === nodeB.nodeName;
}
/**
* Syncs attributes from newNode to oldNode.
* @private
*/
#patchAttributes(oldNode, newNode) {
const oldAttrs = oldNode.attributes;
const newAttrs = newNode.attributes;
// Remove old attributes that are gone
for (let i = oldAttrs.length - 1; i >= 0; i--) {
const attr = oldAttrs[i];
if (!newNode.hasAttribute(attr.name)) {
oldNode.removeAttribute(attr.name);
if (attr.name === 'value' && ['INPUT', 'TEXTAREA', 'SELECT'].includes(oldNode.nodeName)) {
oldNode.value = '';
}
}
}
// Add or update attributes
for (let i = 0; i < newAttrs.length; i++) {
const attr = newAttrs[i];
if (oldNode.getAttribute(attr.name) !== attr.value) {
oldNode.setAttribute(attr.name, attr.value);
}
if (attr.name === 'value' && ['INPUT', 'TEXTAREA', 'SELECT'].includes(oldNode.nodeName)) {
if (oldNode.value !== attr.value) {
oldNode.value = attr.value;
}
}
}
}
}
|