stylus/js/router.js
2022-09-06 01:07:14 +03:00

119 lines
3.3 KiB
JavaScript

/* global $ */// dom.js
/* global deepEqual */// toolbox.js
/* global msg */
'use strict';
const router = {
buffer: (history.state || {}).buffer || [],
watchers: [],
getSearch(key) {
return new URLSearchParams(location.search).get(key);
},
/** When showing the UI, `showHide` function must resolve only when the UI is closed */
makeToggle(hashId, showHide, deps) {
const hash = '#' + hashId;
const selector = '.' + hashId;
router.watch({hash}, async state => {
const el = $(selector);
if (!state === !el) return;
if (state && deps) await require(deps);
await showHide(state, el, selector);
if (state) router.updateHash('');
});
return router.updateHash.bind(router, hash);
},
push(url) {
const state = history.state || {};
state.buffer = router.buffer;
history.pushState(state, null, url);
},
update(replace) {
const {buffer} = router;
if (!buffer.length) {
buffer.push(location.href);
} else if (buffer[buffer.length - 1] === location.href) {
return;
} else if (replace) {
buffer[buffer.length - 1] = location.href;
} else if (buffer.length > 1 && buffer[buffer.length - 2] === location.href) {
buffer.pop();
} else {
buffer.push(location.href);
}
for (const {options, callback} of router.watchers) {
let state;
if (options.hash) {
state = options.hash === location.hash;
} else if (options.search) {
const search = new URLSearchParams(location.search);
state = options.search.map(key => search.get(key));
}
if (!deepEqual(state, options.currentState)) {
options.currentState = state;
callback(state);
}
}
},
/**
* @param {string} hash - empty string removes the hash
*/
updateHash(hash) {
const {buffer} = router;
if (buffer.length > 1) {
if (!hash && !buffer[buffer.length - 2].includes('#') ||
hash && buffer[buffer.length - 2].endsWith(hash)) {
history.back();
return;
}
}
if (!hash) {
hash = ' ';
}
router.push(hash);
router.update();
},
/**
* @param {Object|string} what - an object or a single key
* @param {string} [value] - for `key` mode
*/
updateSearch(what, value) {
const u = new URL(location);
const entries = typeof what === 'object' ? Object.entries(what) : [[what, value]];
for (const [key, val] of entries) {
u.searchParams[val ? 'set' : 'delete'](key, val);
}
history.replaceState(history.state, null, `${u}`);
router.update(true);
},
watch(options, callback) {
/* Watch search params or hash and get notified on change.
options: {search?: Array<key: String>, hash?: String}
callback: (Array<value: String | null> | Boolean) => void
`hash` should always start with '#'.
When watching search params, callback receives a list of values.
When watching hash, callback receives a boolean.
*/
router.watchers.push({options, callback});
},
};
document.on('DOMContentLoaded', () => router.update());
window.on('popstate', () => router.update());
window.on('hashchange', () => router.update());
msg.on(e => {
if (e.method === 'pushState' && e.url !== location.href) {
router.push(e.url);
router.update();
return true;
}
});