Files
tubestation/waterfox/browser/components/sidebar/extlib/dom-updater.js
2025-11-06 14:13:52 +00:00

146 lines
5.0 KiB
JavaScript

/*
license: The MIT License, Copyright (c) 2020 YUKI "Piro" Hiroshi
*/
import { SequenceMatcher } from './diff.js';
export const DOMUpdater = {
/**
* method
* @param before {Node} - the node to be updated, e.g. Element
* @param after {Node} - the node describing updated state,
* e.g. DocumentFragment
* @return count {number} - the count of appied changes
*/
update(before, after, context) {
let topLevel = false;
if (!context) {
topLevel = true;
context = {
count: 0,
beforeRange: before.ownerDocument.createRange(),
afterRange: after.ownerDocument.createRange()
};
}
const { beforeRange, afterRange } = context;
if (before.nodeValue !== null ||
after.nodeValue !== null) {
if (before.nodeValue != after.nodeValue) {
//console.log('node value: ', after.nodeValue);
before.nodeValue = after.nodeValue;
context.count++;
}
return context.count;
}
const beforeNodes = Array.from(before.childNodes, this._getDiffableNodeString);
const afterNodes = Array.from(after.childNodes, this._getDiffableNodeString);
const nodeOerations = (new SequenceMatcher(beforeNodes, afterNodes)).operations();
// Update from back to front for safety!
for (const operation of nodeOerations.reverse()) {
const [tag, fromStart, fromEnd, toStart, toEnd] = operation;
switch (tag) {
case 'equal':
for (let i = 0, maxi = fromEnd - fromStart; i < maxi; i++) {
this.update(
before.childNodes[fromStart + i],
after.childNodes[toStart + i],
context
);
}
break;
case 'delete':
beforeRange.setStart(before, fromStart);
beforeRange.setEnd(before, fromEnd);
beforeRange.deleteContents();
context.count++;
break;
case 'insert':
beforeRange.setStart(before, fromStart);
beforeRange.setEnd(before, fromEnd);
afterRange.setStart(after, toStart);
afterRange.setEnd(after, toEnd);
beforeRange.insertNode(afterRange.cloneContents());
context.count++;
break;
case 'replace':
beforeRange.setStart(before, fromStart);
beforeRange.setEnd(before, fromEnd);
beforeRange.deleteContents();
context.count++;
afterRange.setStart(after, toStart);
afterRange.setEnd(after, toEnd);
beforeRange.insertNode(afterRange.cloneContents());
context.count++;
break;
}
}
if (topLevel) {
beforeRange.detach();
afterRange.detach();
}
if (before.nodeType == before.ELEMENT_NODE &&
after.nodeType == after.ELEMENT_NODE) {
const beforeAttrs = Array.from(before.attributes, attr => `${attr.name}:${attr.value}`). sort();
const afterAttrs = Array.from(after.attributes, attr => `${attr.name}:${attr.value}`). sort();
const attrOerations = (new SequenceMatcher(beforeAttrs, afterAttrs)).operations();
for (const operation of attrOerations) {
const [tag, fromStart, fromEnd, toStart, toEnd] = operation;
switch (tag) {
case 'equal':
break;
case 'delete':
for (let i = fromStart; i < fromEnd; i++) {
const name = beforeAttrs[i].split(':')[0];
//console.log('delete: delete attr: ', name);
before.removeAttribute(name);
context.count++;
}
break;
case 'insert':
for (let i = toStart; i < toEnd; i++) {
const attr = afterAttrs[i].split(':');
const name = attr[0];
const value = attr.slice(1).join(':');
//console.log('insert: set attr: ', name, value);
before.setAttribute(name, value);
context.count++;
}
break;
case 'replace':
const insertedAttrs = new Set();
for (let i = toStart; i < toEnd; i++) {
const attr = afterAttrs[i].split(':');
const name = attr[0];
const value = attr.slice(1).join(':');
//console.log('replace: set attr: ', name, value);
before.setAttribute(name, value);
insertedAttrs.add(name);
context.count++;
}
for (let i = fromStart; i < fromEnd; i++) {
const name = beforeAttrs[i].split(':')[0];
if (insertedAttrs.has(name))
continue;
//console.log('replace: delete attr: ', name);
before.removeAttribute(name);
context.count++;
}
break;
}
}
}
return context.count;
},
_getDiffableNodeString(node) {
if (node.nodeType == node.ELEMENT_NODE)
return `element:${node.tagName}#${node.id}#${node.getAttribute('anonid')}`;
else
return `node:${node.nodeType}`;
}
};