193 lines
6.4 KiB
JavaScript
193 lines
6.4 KiB
JavaScript
(function (root, factory) {
|
|
const api = factory();
|
|
if (typeof module === 'object' && module.exports) module.exports = api;
|
|
else root.createWorkRoute = api;
|
|
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
'use strict';
|
|
|
|
const repositoryPart = /^[A-Za-z0-9_.-]+$/;
|
|
const queueFilters = ['today', 'agenda', 'attention', 'filed', 'update', 'later', 'draft'];
|
|
|
|
function positiveInteger(value) {
|
|
const number = Number(value);
|
|
return Number.isSafeInteger(number) && number > 0 ? number : null;
|
|
}
|
|
|
|
function parse(fragment) {
|
|
const parts = String(fragment || '').split('/');
|
|
if (parts[0] !== '#' || parts[1] !== 'my-work') return null;
|
|
const queue = parts[2]?.replace(/s$/, '');
|
|
if (queueFilters.includes(queue) && parts.length === 3) {
|
|
return { kind: 'queue', filter: queue };
|
|
}
|
|
if (queue === 'agenda' && parts[3] === 'protect-today' && parts.length === 4) {
|
|
return { kind: 'queue', filter: 'agenda', action: 'protect-today' };
|
|
}
|
|
if (parts[2] === 'update' && parts.length === 4) {
|
|
const notificationId = positiveInteger(parts[3]);
|
|
return notificationId ? { kind: 'update', notification_id: notificationId } : null;
|
|
}
|
|
if (!['issue', 'filed', 'pull', 'review'].includes(parts[2]) || parts.length !== 6) return null;
|
|
if (!repositoryPart.test(parts[3]) || !repositoryPart.test(parts[4])) return null;
|
|
const number = positiveInteger(parts[5]);
|
|
return number ? { kind: parts[2], repository: parts[3] + '/' + parts[4], number } : null;
|
|
}
|
|
|
|
function serialize(item) {
|
|
if (item?.kind === 'update') {
|
|
const notificationId = positiveInteger(item.notification_id);
|
|
return notificationId ? '#/my-work/update/' + notificationId : '';
|
|
}
|
|
const kind = item?.is_filed && !item?.is_assigned ? 'filed' : item?.kind;
|
|
if (!['issue', 'filed', 'pull', 'review'].includes(kind)) return '';
|
|
const repository = String(item.repository || '').split('/');
|
|
const number = positiveInteger(item.number);
|
|
if (repository.length !== 2 || !repository.every(part => repositoryPart.test(part)) || !number) return '';
|
|
return '#/my-work/' + kind + '/' + repository.join('/') + '/' + number;
|
|
}
|
|
|
|
function sameRoute(item, route) {
|
|
if (route.kind === 'queue') return false;
|
|
if (route.kind === 'update') {
|
|
return Number(item.notification_id) === route.notification_id;
|
|
}
|
|
const itemKind = item.is_review ? 'review' :
|
|
(item.is_filed && !item.is_assigned ? 'filed' : item.kind);
|
|
return itemKind === route.kind && item.repository === route.repository &&
|
|
Number(item.number) === route.number;
|
|
}
|
|
|
|
function createController({
|
|
location, history, eventTarget, onOpen, onClose, onInvalid,
|
|
onQueue = function () {},
|
|
resolve, onResolving = function () {}, onError = function () {},
|
|
}) {
|
|
let items = [];
|
|
let started = false;
|
|
let active = '';
|
|
let ready = false;
|
|
let resolving = '';
|
|
let resolution = 0;
|
|
|
|
function resolveMissing(fragment, route) {
|
|
if (typeof resolve !== 'function') {
|
|
onInvalid();
|
|
return;
|
|
}
|
|
if (resolving === fragment) return;
|
|
resolving = fragment;
|
|
const request = ++resolution;
|
|
onResolving(route);
|
|
Promise.resolve(resolve(route)).then(item => {
|
|
if (request !== resolution || String(location.hash || '') !== fragment) return;
|
|
resolving = '';
|
|
if (!item || !sameRoute(item, route)) {
|
|
onInvalid();
|
|
return;
|
|
}
|
|
active = fragment;
|
|
onOpen({ ...item, kind: route.kind });
|
|
}).catch(error => {
|
|
if (request !== resolution || String(location.hash || '') !== fragment) return;
|
|
resolving = '';
|
|
if (error?.unavailable) onInvalid();
|
|
else onError(error, route);
|
|
});
|
|
}
|
|
|
|
function sync() {
|
|
const fragment = String(location.hash || '');
|
|
if (!fragment) {
|
|
resolution += 1;
|
|
resolving = '';
|
|
if (active) onClose();
|
|
active = '';
|
|
return;
|
|
}
|
|
const route = parse(fragment);
|
|
if (!route) {
|
|
resolution += 1;
|
|
resolving = '';
|
|
if (fragment.startsWith('#/my-work/')) onInvalid();
|
|
active = '';
|
|
return;
|
|
}
|
|
if (route.kind === 'queue') {
|
|
if (!ready) return;
|
|
resolution += 1;
|
|
resolving = '';
|
|
if (active && active !== fragment) onClose();
|
|
if (active === fragment) return;
|
|
active = fragment;
|
|
onQueue(route.filter, route.action || null);
|
|
return;
|
|
}
|
|
const item = items.find(candidate => sameRoute(candidate, route));
|
|
if (!item) {
|
|
if (ready) resolveMissing(fragment, route);
|
|
return;
|
|
}
|
|
if (active === fragment) return;
|
|
resolution += 1;
|
|
resolving = '';
|
|
active = fragment;
|
|
onOpen({ ...item, kind: route.kind });
|
|
}
|
|
|
|
return {
|
|
start() {
|
|
if (started) return;
|
|
started = true;
|
|
eventTarget.addEventListener('popstate', sync);
|
|
eventTarget.addEventListener('hashchange', sync);
|
|
sync();
|
|
},
|
|
setItems(nextItems) {
|
|
items = Array.isArray(nextItems) ? nextItems.slice() : [];
|
|
ready = true;
|
|
sync();
|
|
},
|
|
open(item, options = {}) {
|
|
const fragment = serialize(item);
|
|
if (!fragment) return false;
|
|
const method = options.replace ? 'replaceState' : 'pushState';
|
|
history[method]({ workRoute: fragment }, '', fragment);
|
|
active = fragment;
|
|
onOpen(item);
|
|
return true;
|
|
},
|
|
queue(filter) {
|
|
if (!queueFilters.includes(filter)) return false;
|
|
const name = filter + (['update', 'draft'].includes(filter) ? 's' : '');
|
|
const fragment = '#/my-work/' + name;
|
|
if (location.hash !== fragment) history.pushState(null, '', fragment);
|
|
active = fragment;
|
|
onQueue(filter);
|
|
return true;
|
|
},
|
|
close() {
|
|
if (parse(location.hash)) history.back();
|
|
else {
|
|
active = '';
|
|
onClose();
|
|
}
|
|
},
|
|
sync,
|
|
};
|
|
}
|
|
|
|
async function share(url, navigatorObject, clipboard) {
|
|
if (typeof navigatorObject?.share === 'function') {
|
|
await navigatorObject.share({ url });
|
|
return 'shared';
|
|
}
|
|
if (typeof clipboard?.writeText === 'function') {
|
|
await clipboard.writeText(url);
|
|
return 'copied';
|
|
}
|
|
throw new Error('Sharing is unavailable.');
|
|
}
|
|
|
|
return { parse, serialize, createController, share };
|
|
});
|