mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
refactor(ui): own Display control subscriptions and cleanup
This commit is contained in:
1 parent
795ed68192
commit
ffc65dd844
11 files changed
+307
-166
No files matched your search
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Bind Display controls to application actions. Inputs remain native DOM controls;
|
||||
* the caller owns settings and effect state. Destroy before replacing the view.
|
||||
* @param {{ elements: object, actions: object }} options
|
||||
* @returns {{ destroy(): void }}
|
||||
*/
|
||||
export function bindDisplayControls({ elements, actions }) {
|
||||
const removers = [];
|
||||
const listen = (element, type, action, read) => {
|
||||
if (!element) return;
|
||||
const handler = () => actions[action](...(read ? [read(element)] : []));
|
||||
element.addEventListener(type, handler);
|
||||
removers.push(() => element.removeEventListener(type, handler));
|
||||
};
|
||||
const integer = (element) => parseInt(element.value, 10);
|
||||
for (const [name, action] of [
|
||||
['bloomButton', 'toggleBloom'], ['sharpenButton', 'toggleSharpen'],
|
||||
['scopeButton', 'toggleScope'], ['cleanViewButton', 'toggleCleanView'],
|
||||
['cleanViewExitButton', 'exitCleanView'], ['celestialButton', 'toggleCelestial'],
|
||||
['hudButton', 'toggleHud'], ['detectionButton', 'cycleDetection'],
|
||||
['modelsButton', 'toggleModels'],
|
||||
]) listen(elements[name], 'click', action);
|
||||
for (const [name, action] of [
|
||||
['bloomSlider', 'setBloomIntensity'], ['sharpenSlider', 'setSharpenIntensity'],
|
||||
['scopeFeatherSlider', 'setScopeFeather'],
|
||||
]) listen(elements[name], 'input', action, integer);
|
||||
listen(elements.densitySlider, 'input', 'setDensity', (el) => el.value);
|
||||
listen(elements.hudLayout, 'change', 'setHudLayout', (el) => el.value);
|
||||
for (const el of elements.styleButtons || [])
|
||||
listen(el, 'click', 'setStyle', (el) => el.dataset.style);
|
||||
for (const el of elements.allocationButtons || [])
|
||||
listen(el, 'click', 'setAllocation', (el) => el.dataset.allocation);
|
||||
for (const el of elements.modelModeButtons || [])
|
||||
listen(el, 'click', 'setModelsMode', (el) => el.dataset.mode === 'all' ? 'all' : 'proximity');
|
||||
for (const el of elements.fadeSliders || []) listen(el, 'input', 'setFade');
|
||||
return {
|
||||
destroy() {
|
||||
for (const remove of removers.splice(0)) remove();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { bindDisplayControls } from './displayControls.js';
|
||||
|
||||
function element(value = '') {
|
||||
const el = new EventTarget();
|
||||
el.value = value;
|
||||
el.dataset = {};
|
||||
return el;
|
||||
}
|
||||
|
||||
test('controls read current values without preventing native input behavior', () => {
|
||||
const bloomSlider = element('24');
|
||||
const hudLayout = element('tactical');
|
||||
const calls = [];
|
||||
const control = bindDisplayControls({ elements: { bloomSlider, hudLayout }, actions: {
|
||||
setBloomIntensity: (value) => calls.push(value),
|
||||
setHudLayout: (value) => calls.push(value),
|
||||
} });
|
||||
const event = new Event('input', { cancelable: true });
|
||||
bloomSlider.dispatchEvent(event);
|
||||
bloomSlider.value = '37';
|
||||
bloomSlider.dispatchEvent(new Event('input'));
|
||||
hudLayout.value = 'minimal';
|
||||
hudLayout.dispatchEvent(new Event('change'));
|
||||
assert.deepEqual(calls, [24, 37, 'minimal']);
|
||||
assert.equal(event.defaultPrevented, false);
|
||||
control.destroy();
|
||||
});
|
||||
|
||||
test('destroyed and replaced controls cannot issue stale actions', () => {
|
||||
const bloomButton = element();
|
||||
let oldCalls = 0;
|
||||
let newCalls = 0;
|
||||
const first = bindDisplayControls({ elements: { bloomButton }, actions: { toggleBloom: () => oldCalls++ } });
|
||||
bloomButton.dispatchEvent(new Event('click'));
|
||||
first.destroy();
|
||||
const second = bindDisplayControls({ elements: { bloomButton }, actions: { toggleBloom: () => newCalls++ } });
|
||||
first.destroy();
|
||||
bloomButton.dispatchEvent(new Event('click'));
|
||||
assert.equal(oldCalls, 1);
|
||||
assert.equal(newCalls, 1);
|
||||
second.destroy();
|
||||
bloomButton.dispatchEvent(new Event('click'));
|
||||
assert.equal(newCalls, 1);
|
||||
});
|
||||
|
||||
test('style, allocation and model choices retain their current data attributes', () => {
|
||||
const style = element();
|
||||
const allocation = element();
|
||||
const mode = element();
|
||||
style.dataset.style = 'thermal';
|
||||
allocation.dataset.allocation = 'balanced';
|
||||
mode.dataset.mode = 'all';
|
||||
const calls = [];
|
||||
const control = bindDisplayControls({ elements: { styleButtons: [style], allocationButtons: [allocation], modelModeButtons: [mode] }, actions: {
|
||||
setStyle: (value) => calls.push(value), setAllocation: (value) => calls.push(value), setModelsMode: (value) => calls.push(value),
|
||||
} });
|
||||
for (const el of [style, allocation, mode]) el.dispatchEvent(new Event('click'));
|
||||
mode.dataset.mode = 'unknown';
|
||||
mode.dispatchEvent(new Event('click'));
|
||||
assert.deepEqual(calls, ['thermal', 'balanced', 'all', 'proximity']);
|
||||
control.destroy();
|
||||
});
|
||||
|
||||
test('optional controls are absent safely and subscriptions stay instance-owned', () => {
|
||||
const calls = [];
|
||||
const a = element();
|
||||
const b = element();
|
||||
const one = bindDisplayControls({ elements: { fadeSliders: [null, a] }, actions: { setFade: () => calls.push('a') } });
|
||||
const two = bindDisplayControls({ elements: { fadeSliders: [b] }, actions: { setFade: () => calls.push('b') } });
|
||||
one.destroy();
|
||||
a.dispatchEvent(new Event('input'));
|
||||
b.dispatchEvent(new Event('input'));
|
||||
assert.deepEqual(calls, ['b']);
|
||||
two.destroy();
|
||||
});
|
||||
Reference in new issue
Block a user