generated from Labyricorn/labyricorn-project-template
95 lines
3.0 KiB
JavaScript
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
|
|
};
|
|
})();
|