frontend-patterns/research/experiments/navigation-history/ownership-policy.js
tegwick e8ccb73a12 Test competing navigation ownership and review research thread
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0cffe-206f-7323-89d3-b20ca4cf4bf9
2026-09-27 00:45:54 +02:00

31 lines
2.4 KiB
JavaScript

// Experimental adapter pinned to htmx 2.0.11; not a production router.
window.probe=[];
let generation=0,focusEpoch=0,historyMoveFocus=true;
const pending=new WeakMap();
document.addEventListener('focusin',()=>focusEpoch++);
window.addEventListener('popstate',()=>{generation++;},true);
const notice=text=>{document.querySelector('#feedback').textContent=text;};
function complete(move){const h=document.querySelector('#heading');document.title=`${h.textContent} — Catalog`;notice(`Loaded ${h.textContent}`);if(move)h.focus();}
document.addEventListener('htmx:beforeRequest',e=>{
if(e.detail.target.id==='content')pending.set(e.detail.xhr,{owner:++generation,source:e.detail.elt,move:false});
});
document.addEventListener('htmx:beforeOnLoad',e=>{
const state=pending.get(e.detail.xhr);
if(state&&state.owner!==generation){e.preventDefault();pending.delete(e.detail.xhr);window.raceLog.push({event:'discard-navigation',url:e.detail.xhr.responseURL});}
});
document.addEventListener('htmx:beforeSwap',e=>{const state=pending.get(e.detail.xhr);if(state)state.move=document.activeElement===state.source;});
document.addEventListener('htmx:afterSwap',e=>{const state=pending.get(e.detail.xhr);if(state){complete(state.move);pending.delete(e.detail.xhr);}});
document.addEventListener('htmx:responseError',e=>{if(pending.has(e.detail.xhr)){notice('Navigation failed. Current article retained. Use Native recovery to continue.');pending.delete(e.detail.xhr);}});
document.addEventListener('htmx:historyCacheHit',()=>{historyMoveFocus=true;});
document.addEventListener('htmx:historyCacheMiss',e=>{
const owner=generation,focusAtStart=focusEpoch,path=e.detail.path;
const xhr=e.detail.xhr,original=xhr.onload;
// The public miss event exposes XHR; wrapping its existing handler relies on the pinned implementation.
xhr.onload=function(event){
if(owner!==generation||location.pathname+location.search!==path){window.raceLog.push({event:'discard-history',url:xhr.responseURL});return;}
historyMoveFocus=focusEpoch===focusAtStart;
return original.call(this,event);
};
});
document.addEventListener('htmx:historyRestore',()=>complete(historyMoveFocus));
for(const name of ['htmx:afterSettle','htmx:historyRestore','htmx:responseError'])document.addEventListener(name,()=>window.probe.push({name,focus:document.activeElement.id||document.activeElement.tagName,title:document.title,url:location.href,feedback:document.querySelector('#feedback').textContent}));