refactor(frontend): add shared DOM patch helpers

This commit is contained in:
Dariusz L
2026-08-04 20:16:30 +02:00
parent 0d6ad98121
commit 9fb0357e59
2 changed files with 74 additions and 0 deletions
+30
View File
@@ -3,6 +3,11 @@ import path from 'node:path';
import test from 'node:test';
import assert from 'node:assert/strict';
import { Window } from 'happy-dom';
import {
bindEventOnce,
setTextIfChanged,
syncElementAttributes,
} from '../web/resolver/utils/dom_patch_utils.js';
const projectRoot = path.resolve(import.meta.dirname, '..');
const resolveDownloadMethodsSource = fs.readFileSync(
@@ -59,6 +64,31 @@ const patchQueuedSelections = eval(`(${extractMethod(queueMethodsSource, 'patchQ
const patchDownloadsPanelElement = eval(`(${extractMethod(queueMethodsSource, 'patchDownloadsPanelElement')})`);
const patchLoadedModelsProgress = eval(`(${extractMethod(renderFormatMethodsSource, 'patchLoadedModelsProgress')})`);
test('shared DOM helpers synchronize attributes, text, and one-time listeners', () => {
const window = new Window();
const current = window.document.createElement('button');
const next = window.document.createElement('button');
current.setAttribute('class', 'old');
current.setAttribute('data-stale', '1');
next.setAttribute('class', 'new');
next.setAttribute('data-value', '2');
syncElementAttributes(current, next);
assert.equal(current.getAttribute('class'), 'new');
assert.equal(current.hasAttribute('data-stale'), false);
assert.equal(current.getAttribute('data-value'), '2');
assert.equal(setTextIfChanged(current, 'Ready'), true);
assert.equal(setTextIfChanged(current, 'Ready'), false);
let clicks = 0;
const handler = () => { clicks += 1; };
assert.equal(bindEventOnce(current, 'click', handler, 'ready'), true);
assert.equal(bindEventOnce(current, 'click', handler, 'ready'), false);
current.click();
assert.equal(clicks, 1);
});
function createDialog(window) {
return {
syncSearchElementAttributes,
+44
View File
@@ -0,0 +1,44 @@
const boundEventKeys = new WeakMap();
export function syncElementAttributes(current, next) {
if (!current || !next) return current;
const nextAttributeNames = new Set(
Array.from(next.attributes || [], attribute => attribute.name)
);
for (const attribute of Array.from(current.attributes || [])) {
if (!nextAttributeNames.has(attribute.name)) {
current.removeAttribute(attribute.name);
}
}
for (const attribute of Array.from(next.attributes || [])) {
if (current.getAttribute(attribute.name) !== attribute.value) {
current.setAttribute(attribute.name, attribute.value);
}
}
return current;
}
export function setTextIfChanged(element, value) {
if (!element) return false;
const nextText = String(value ?? '');
if (element.textContent === nextText) return false;
element.textContent = nextText;
return true;
}
export function bindEventOnce(element, eventName, handler, key = eventName) {
if (!element?.addEventListener || !eventName || typeof handler !== 'function') return false;
let elementKeys = boundEventKeys.get(element);
if (!elementKeys) {
elementKeys = new Set();
boundEventKeys.set(element, elementKeys);
}
const bindingKey = `${eventName}:${String(key)}`;
if (elementKeys.has(bindingKey)) return false;
elementKeys.add(bindingKey);
element.addEventListener(eventName, handler);
return true;
}