refactor(frontend): add shared DOM patch helpers
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user