/* Addressable state — S3 / T4.2. --------------------------------------------------------------------------- Before this file there was no pushState anywhere in the suite. Every page read its query string once at boot and never wrote one again, so: • you could not send anyone a link to WP07 — the URL said the same thing whatever you were looking at; • a refresh dropped you back at the default view; • Back left the app entirely, because the app had never added a history entry. CR-011 and CR-014 both promise an email containing a direct link to a work package (X1). Those emails cannot exist until a work package has an address, which is what this provides. WHAT IT IS NOT: a router. Nothing here intercepts navigation or renders anything. It is the query string, treated as state that can be read, merged, written and subscribed to. Pages keep their own rendering. Query parameters, not a hash: the server serves these paths already, so a hash would be a workaround for a problem this app does not have, and hashes are not sent to the server — which matters the day a link needs to be resolved before the page boots. Nothing secret goes in the URL. It is copied into emails, chat and tickets. */ (function (window, document) { 'use strict'; var listeners = []; var LAST = serialize(current()); function current() { var out = {}; try { new URLSearchParams(window.location.search).forEach(function (v, k) { out[k] = v; }); } catch (e) {} return out; } function serialize(state) { var keys = Object.keys(state).filter(function (k) { return state[k] !== '' && state[k] != null && state[k] !== false; }).sort(); var sp = new URLSearchParams(); keys.forEach(function (k) { sp.set(k, String(state[k])); }); return sp.toString(); } // Merge a patch over the current state. Undefined/null/'' removes a key, so a // caller can clear `wp` without having to know what else is in the URL — the // usual reason ad-hoc URL building loses the active project. function merge(patch) { var next = current(); Object.keys(patch || {}).forEach(function (k) { var v = patch[k]; if (v === undefined || v === null || v === '' || v === false) delete next[k]; else next[k] = v; }); return next; } function href(patch) { var qs = serialize(merge(patch)); return window.location.pathname + (qs ? '?' + qs : '') + window.location.hash; } function apply(patch, opts) { opts = opts || {}; var next = merge(patch); var qs = serialize(next); if (qs === LAST && !opts.force) return false; // nothing to record var url = window.location.pathname + (qs ? '?' + qs : '') + window.location.hash; try { if (opts.replace) window.history.replaceState({ wpurl: qs }, '', url); else window.history.pushState({ wpurl: qs }, '', url); } catch (e) { return false; // file:// and the like } LAST = qs; return true; } function notify(state, viaPop) { listeners.forEach(function (fn) { try { fn(state, viaPop); } catch (e) { /* one bad subscriber must not stop the rest */ } }); } window.addEventListener('popstate', function () { LAST = serialize(current()); notify(current(), true); }); window.WPUrl = { // Read one parameter, or everything. get: function (name) { var s = current(); return name == null ? s : (s[name] || ''); }, all: current, /* Record a state change in history. Merges over what is already there. WPUrl.push({ wp: id }) -> new history entry, Back returns WPUrl.push({ wp: '' }) -> clears it WPUrl.replace({ view: 'form' }) -> corrects the URL without a new entry replace() is for normalising on load or for a change the user did not ask for; push() is for one they did, because Back should undo exactly the things they chose to do. */ push: function (patch) { return apply(patch, { replace: false }); }, replace: function (patch) { return apply(patch, { replace: true }); }, // A URL string for the same merge, without navigating. For hrefs and for the // links that go into CR-011 / CR-014 emails. href: href, absolute: function (patch) { return window.location.origin + href(patch); }, /* Subscribe to state changes. Called on Back/Forward with viaPop === true. Returns an unsubscribe function. */ onChange: function (fn) { listeners.push(fn); return function () { var i = listeners.indexOf(fn); if (i >= 0) listeners.splice(i, 1); }; }, }; })(window, document);