(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', 'following']; const sections = { issue: ['overview', 'conversation', 'reply', 'actions'], filed: ['overview', 'conversation', 'reply', 'actions'], pull: ['overview', 'conversation', 'reply', 'review'], review: ['overview', 'files', 'feedback', 'history'], update: ['activity', 'conversation', 'context', 'reply'], }; 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' && [4, 5].includes(parts.length)) { const notificationId = positiveInteger(parts[3]); const section = parts[4]; if (!notificationId || (section && !sections.update.includes(section))) return null; return { kind: 'update', notification_id: notificationId, ...(section ? { section } : {}) }; } if (!['issue', 'filed', 'pull', 'review'].includes(parts[2]) || ![6, 7].includes(parts.length)) return null; if (!repositoryPart.test(parts[3]) || !repositoryPart.test(parts[4])) return null; const number = positiveInteger(parts[5]); const section = parts[6]; if (!number || (section && !sections[parts[2]].includes(section))) return null; return { kind: parts[2], repository: parts[3] + '/' + parts[4], number, ...(section ? { section } : {}), }; } function serialize(item) { if (item?.kind === 'update') { const notificationId = positiveInteger(item.notification_id); if (!notificationId || (item.section && !sections.update.includes(item.section))) return ''; return '#/my-work/update/' + notificationId + (item.section ? '/' + item.section : ''); } 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 ''; if (item.section && !sections[kind].includes(item.section)) return ''; return '#/my-work/' + kind + '/' + repository.join('/') + '/' + number + (item.section ? '/' + item.section : ''); } 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 () {}, onSection = 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, ...(route.section ? { section: route.section } : {}) }); }).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; const activeRoute = parse(active); if (activeRoute && sameRoute(item, activeRoute)) { active = fragment; onSection(route.section || sections[route.kind][0], { restore: true }); return; } resolution += 1; resolving = ''; active = fragment; onOpen({ ...item, kind: route.kind, ...(route.section ? { section: route.section } : {}) }); } 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; }, section(name, options = {}) { const route = parse(location.hash); if (!route || route.kind === 'queue' || !sections[route.kind].includes(name)) return false; if ((route.section || sections[route.kind][0]) === name) return true; const fragment = serialize({ ...route, section: name }); const method = options.replace ? 'replaceState' : 'pushState'; history[method]({ workRoute: fragment }, '', fragment); active = fragment; onSection(name, { restore: false }); 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 }; });