Files
XZBT-NGN/test-fixtures/reference-exhibits/shared/exhibit-shell.js
T
2026-09-14 07:57:18 -07:00

95 lines
3.0 KiB
JavaScript

/*
* Shared presentation helpers for the reference exhibits.
*
* Generic DOM plumbing only: element lookup, safe text writing, and the
* announcement strip. It has no contract awareness and no domain knowledge,
* so an exhibit can use it, ignore it, or replace it without touching the
* contract layer.
*
* The one rule worth stating out loud: every piece of text that reaches the
* DOM goes through `textContent`. Nothing in this project ever assigns
* `innerHTML`, so a string arriving from a host or a scenario can never
* become markup or script (Contract §20, Authoring Guide §O).
*/
(function () {
'use strict';
function el(id) {
return document.getElementById(id);
}
/** Write text safely. Always textContent, never innerHTML. */
function setText(node, text) {
if (!node) return;
node.textContent = text === null || text === undefined ? '' : String(text);
}
/**
* Announcement strip. Text is data: it is truncated to the declared
* maximum and written with textContent.
*/
function Announcer(options) {
this.node = options.node;
this.idleText = options.idleText || '';
this.maxLength = options.maxLength || 120;
this._timer = null;
this.setText(this.idleText, true);
}
Announcer.prototype.setText = function (text, idle) {
if (!this.node) return;
var value = text === null || text === undefined ? '' : String(text);
if (value.length > this.maxLength) value = value.slice(0, this.maxLength);
this.node.textContent = value;
if (idle) {
this.node.classList.add('is-idle');
} else {
this.node.classList.remove('is-idle');
}
};
/** Show a transient message, then fall back to the idle text. */
Announcer.prototype.flash = function (text, ms) {
this.setText(text, false);
if (this._timer) clearTimeout(this._timer);
var self = this;
this._timer = setTimeout(function () {
self.setText(self.idleText, true);
}, ms || 2600);
};
/** Format a number for a readout, with a fixed number of decimals. */
function formatNumber(value, decimals) {
if (typeof value !== 'number' || !isFinite(value)) return '--';
return value.toFixed(decimals === undefined ? 2 : decimals);
}
/** Format a 0..1 value as a whole percentage. */
function formatPercent(value) {
if (typeof value !== 'number' || !isFinite(value)) return '--';
return Math.round(value * 100) + '%';
}
/** Format a 0..1 value as a whole number of degrees. */
function formatDegrees(value) {
if (typeof value !== 'number' || !isFinite(value)) return '--';
return Math.round(value * 360) + '\u00b0';
}
/** Format a 0..1 value as a whole number of minutes. */
function formatMinutes(value) {
if (typeof value !== 'number' || !isFinite(value)) return '--';
return Math.round(value * 60) + ' min';
}
window.XZBTShell = {
el: el,
setText: setText,
Announcer: Announcer,
formatNumber: formatNumber,
formatPercent: formatPercent,
formatDegrees: formatDegrees,
formatMinutes: formatMinutes
};
})();