(function (root, factory) { const api = factory(); if (typeof module === 'object' && module.exports) module.exports = api; else root.createTaskOverlayHistory = api; })(typeof globalThis !== 'undefined' ? globalThis : this, function () { 'use strict'; const allowed = new Set(['new', 'find', 'search', 'search-preview', 'plan-today', 'plan-today-preview', 'today-readiness']); const searchKinds = new Set(['search', 'search-preview']); const maxQueryLength = 200; function cleanQuery(value) { const query = typeof value === 'string' ? value.trim() : ''; return query.length <= maxQueryLength ? query : ''; } function cleanScope(value) { const repository = typeof value?.repository === 'string' && /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(value.repository.trim()) ? value.repository.trim() : ''; return { kind:['all', 'issue', 'pull'].includes(value?.kind) ? value.kind : 'all', state:['all', 'open', 'closed'].includes(value?.state) ? value.state : 'all', repository, }; } function cleanPreview(value) { if (!value || !['issue', 'pull'].includes(value.kind)) return null; const repository = typeof value.repository === 'string' ? value.repository : ''; const parts = repository.split('/'); const validPart = part => /^[A-Za-z0-9_.-]+$/.test(part); const number = Number(value.number); if (parts.length !== 2 || !parts.every(validPart) || !Number.isInteger(number) || number < 1) return null; return { kind:value.kind, repository, number }; } function previewToken(preview) { return preview ? `${preview.kind}:${preview.repository}:${preview.number}` : ''; } function parsePreview(value) { const match = /^(issue|pull):([^/:]+\/[^/:]+):(\d+)$/.exec(value || ''); return match ? cleanPreview({ kind:match[1], repository:match[2], number:Number(match[3]) }) : null; } return function createTaskOverlayHistory({ history, location, eventTarget, onChange }) { let started = false; let restoredFromUrl = false; function stateDetail(state = history.state) { const kind = allowed.has(state?.taskOverlay) ? state.taskOverlay : null; if (!searchKinds.has(kind)) return { kind }; const query = cleanQuery(state.searchQuery); const scope = state.searchScope === undefined ? null : cleanScope(state.searchScope); const preview = kind === 'search-preview' ? cleanPreview(state.searchPreview) : null; if (kind === 'search-preview' && state.searchPreview !== undefined && !preview) { return { kind:'search', ...(query ? { query } : {}) }; } return { kind, ...(query ? { query } : {}), ...(scope ? { scope } : {}), ...(preview ? { preview } : {}) }; } function urlDetail() { if (!location || typeof URLSearchParams === 'undefined') return { kind:null }; const params = new URLSearchParams(location.search || ''); const rawQuery = params.get('search'); if (rawQuery === null) return { kind:null }; const query = cleanQuery(rawQuery); if (rawQuery.trim() && !query) return { kind:null }; const preview = parsePreview(params.get('preview')); const hasScope = params.has('search_kind') || params.has('search_state') || params.has('search_repository'); const scope = hasScope ? cleanScope({ kind:params.get('search_kind'), state:params.get('search_state'), repository:params.get('search_repository'), }) : null; return { kind:preview ? 'search-preview' : 'search', ...(query ? { query } : {}), ...(scope ? { scope } : {}), ...(preview ? { preview } : {}), }; } function toState(detail, base = history.state) { const state = { ...(base || {}), taskOverlay:detail.kind }; delete state.searchQuery; delete state.searchPreview; delete state.searchScope; if (searchKinds.has(detail.kind)) { if (detail.query) state.searchQuery = detail.query; if (detail.scope) state.searchScope = cleanScope(detail.scope); if (detail.kind === 'search-preview' && detail.preview) state.searchPreview = detail.preview; } return state; } function urlFor(detail) { if (!location || typeof URLSearchParams === 'undefined') return undefined; const params = new URLSearchParams(location.search || ''); params.delete('search'); params.delete('preview'); params.delete('search_kind'); params.delete('search_state'); params.delete('search_repository'); if (searchKinds.has(detail.kind)) { params.set('search', detail.query || ''); if (detail.scope) { params.set('search_kind', detail.scope.kind); params.set('search_state', detail.scope.state); if (detail.scope.repository) params.set('search_repository', detail.scope.repository); } if (detail.kind === 'search-preview' && detail.preview) params.set('preview', previewToken(detail.preview)); } const query = params.toString(); return `${location.pathname || ''}${query ? `?${query}` : ''}${location.hash || ''}`; } let initial = stateDetail(); if (!initial.kind) { const fromUrl = urlDetail(); if (fromUrl.kind) { initial = fromUrl; restoredFromUrl = true; history.replaceState(toState(fromUrl), '', urlFor(fromUrl)); } } let active = initial; function apply(state) { const next = stateDetail(state); if (JSON.stringify(next) === JSON.stringify(active)) return; const previous = active.kind; active = next; onChange(next.kind, previous, next); } return { start() { if (started) return; started = true; eventTarget.addEventListener('popstate', event => apply(event.state)); if (restoredFromUrl) onChange(active.kind, null, active); }, open(kind, detail = {}) { if (!allowed.has(kind)) return false; const next = stateDetail(toState({ kind, query:cleanQuery(detail.query), scope:detail.scope === undefined ? undefined : cleanScope(detail.scope), preview:cleanPreview(detail.preview), })); if (active.kind === next.kind && JSON.stringify(active) === JSON.stringify(next)) return true; const previous = active.kind; history.pushState(toState(next), '', urlFor(next)); active = next; onChange(next.kind, previous, next); return true; }, update(detail = {}) { if (!searchKinds.has(active.kind)) return false; const next = stateDetail(toState({ kind:active.kind, query:detail.query === undefined ? active.query : cleanQuery(detail.query), scope:detail.scope === undefined ? active.scope : cleanScope(detail.scope), preview:detail.preview === undefined ? active.preview : cleanPreview(detail.preview), })); history.replaceState(toState(next), '', urlFor(next)); active = next; return true; }, close() { if (!active.kind) return false; history.back(); return true; }, leave() { if (!active.kind) return false; const previous = active.kind; const state = { ...(history.state || {}) }; delete state.taskOverlay; delete state.searchQuery; delete state.searchPreview; delete state.searchScope; active = { kind:null }; history.replaceState(state, '', urlFor(active)); onChange(null, previous, active); return true; }, current() { return active.kind; }, currentState() { return { ...active, ...(active.preview ? { preview:{ ...active.preview } } : {}) }; }, }; }; });