`); the library removes it once ready. The library
* also sets `hidden` itself during loading as a fallback for authors who do
* not pre-set it.
* @param {boolean} ready
*/
function setBootState(ready) {
var preloaders = document.querySelectorAll('[stm-preloader]');
var contents = document.querySelectorAll('[stm-content]');
for (var i = 0; i < preloaders.length; i++) {
ready ? preloaders[i].setAttribute('hidden', '') : preloaders[i].removeAttribute('hidden');
}
for (var j = 0; j < contents.length; j++) {
ready ? contents[j].removeAttribute('hidden') : contents[j].setAttribute('hidden', '');
}
}
// ---------------------------------------------------------------------------
// Public state accessors
// ---------------------------------------------------------------------------
/** Build the scope used for expression evaluation: `{ typeName: public }`. */
function stateScope() {
var scope = {};
store.forEach(function (entry, type) { scope[type] = entry.snapshot.public; });
return scope;
}
/** Read a type's `public` data (what the attributes bind to). */
function read(type) {
var entry = store.get(type);
return entry ? entry.snapshot.public : undefined;
}
/** Read a type's full snapshot object. */
function getSnapshot(type) {
var entry = store.get(type);
return entry ? entry.snapshot : undefined;
}
/** Read the frame wrapping a type's snapshot. */
function getFrame(type) {
var entry = store.get(type);
return entry ? entry.frame : undefined;
}
/** Build an object of `{ typeName: public }` for every held type. */
function state() {
var result = {};
store.forEach(function (entry, type) { result[type] = entry.snapshot.public; });
return result;
}
/**
* Clear state. With a type argument, clears that type only; without, clears
* all held state.
* @param {string} [type]
*/
function clear(type) {
if (type === undefined) {
Array.from(store.keys()).forEach(function (t) { clearType(t, { broadcast: true }); });
} else {
clearType(type, { broadcast: true });
}
renderAll();
}
/**
* Perform an action. `action` may be an action object or a string path that
* resolves to an action object within the current state.
* @param {object|string} action
* @param {object} [data]
* @returns {Promise
}
*/
function act(action, data) {
var resolved = action;
if (typeof action === 'string') resolved = evalExpr(action, stateScope());
if (!resolved || typeof resolved.uri !== 'string') {
return Promise.reject(new StatumError('Invalid action'));
}
return performAction(resolved, data, null);
}
/** Attach a state listener for a type. */
function addStateListener(type, callback) {
if (typeof callback !== 'function') return;
if (!listeners.has(type)) listeners.set(type, new Set());
listeners.get(type).add(callback);
}
/** Remove a previously-attached state listener. */
function removeStateListener(type, callback) {
var set = listeners.get(type);
if (!set) return;
set.delete(callback);
if (set.size === 0) listeners.delete(type);
}
// ---------------------------------------------------------------------------
// Initialization
// ---------------------------------------------------------------------------
/**
* Scan the document for `pstm-*` attributes and `` elements, which
* are intended for server-side pre-rendering and should never reach the
* browser. Emits a single `console.warn` summarizing them.
*/
function warnServerSideMarkup() {
var found = [];
var els = document.querySelectorAll('*');
for (var i = 0; i < els.length; i++) {
var el = els[i];
var tag = el.tagName.toLowerCase();
if (tag.indexOf('pstm-') === 0) found.push('<' + tag + '>');
var attrs = el.attributes;
for (var j = 0; j < attrs.length; j++) {
var name = attrs[j].name;
if (name.indexOf('pstm-') === 0) found.push('<' + tag + '> ' + name);
}
}
if (found.length) {
console.warn('[statum] ' + found.length + ' server-side pstm-* attribute/element(s) ' +
'found in the DOM; they should have been resolved during pre-rendering: ' +
found.slice(0, 10).join(', ') + (found.length > 10 ? ' …' : ''));
}
}
/**
* Initialize Statum: read URL overrides, run the session-cookie check,
* hydrate persisted state, open the cross-tab channel, fetch the entrypoint,
* and render. Safe to call multiple times.
* @returns {Promise}
*/
async function init() {
if (initialized) return;
initialized = true;
var body = document.body;
if (body) {
if (body.hasAttribute('stm-entrypoint')) config.entrypointUrl = body.getAttribute('stm-entrypoint');
if (body.hasAttribute('stm-slots')) config.slotsUrl = body.getAttribute('stm-slots');
if (body.hasAttribute('stm-channel')) config.channelUrl = body.getAttribute('stm-channel');
if (body.hasAttribute('stm-worker')) config.workerUrl = body.getAttribute('stm-worker');
}
try { warnServerSideMarkup(); } catch (e) {}
try { initSession(); } catch (e) {}
try { hydrate(); } catch (e) {}
setupChannel();
setBootState(false);
renderAll(); // bind against any persisted/hydrated state immediately
try {
await loadEntrypoint();
} catch (err) {
console.error('[statum] entrypoint load failed', err);
} finally {
setBootState(true);
}
}
function autoInit() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/** @namespace statum */
window.statum = {
/** Library version. */
version: '0.1.0',
/** Mutable endpoint configuration. */
config: config,
/** (Re)initialize the library. Usually runs automatically on load. */
init: init,
/** Read a type's `public` data. */
read: read,
/** Read a type's full snapshot object. */
getSnapshot: getSnapshot,
/** Read the frame wrapping a type's snapshot. */
getFrame: getFrame,
/** Build `{ typeName: public }` for every held type. */
state: state,
/** Clear one type (`clear("x")`) or all state (`clear()`). */
clear: clear,
/** Perform an action object or named action. */
act: act,
/** Attach a state-changed listener for a type. */
addStateListener: addStateListener,
/** Remove a previously-attached listener. */
removeStateListener: removeStateListener,
/** Register a callback that runs before each render cycle (no arguments). */
beforeEvaluate: function (fn) { if (typeof fn === 'function') beforeHooks.push(fn); },
/** Register a callback that runs after each render cycle (no arguments). */
afterEvaluate: function (fn) { if (typeof fn === 'function') afterHooks.push(fn); },
/** The Statum error type (thrown by `act` and surfaced to handlers). */
Error: StatumError,
get onConfirmHandler() { return handlers.onConfirm; },
set onConfirmHandler(v) { handlers.onConfirm = typeof v === 'function' ? v : null; },
get onNotifyHandler() { return handlers.onNotify; },
set onNotifyHandler(v) { handlers.onNotify = typeof v === 'function' ? v : null; },
get onErrorHandler() { return handlers.onError; },
set onErrorHandler(v) { handlers.onError = typeof v === 'function' ? v : null; }
};
autoInit();
})();