@frehner/apphistory
Advanced tools
Comparing version 0.0.4 to 0.0.5
@@ -46,4 +46,5 @@ function _defineProperty(obj, key, value) { | ||
this.current = new AppHistoryEntry({ | ||
url: "TODO FIX DEFAULT URL" | ||
url: window.location.href | ||
}); | ||
this.current.finished = true; | ||
@@ -115,3 +116,4 @@ this.current.__updateEntry(undefined, 0); | ||
async push(param1, param2) { | ||
// used in the currentchange event | ||
const previousEntry = this.current; // used in the currentchange event | ||
const startTime = performance.now(); | ||
@@ -124,4 +126,3 @@ const options = this.getOptionsFromParams(param1, param2); | ||
const oldCurrent = this.current; | ||
const oldCurrentIndex = this.entries.findIndex(entry => entry.key === oldCurrent.key); | ||
const previousEntryIndex = this.entries.findIndex(entry => entry.key === previousEntry.key); | ||
const upcomingURL = new URL(upcomingEntry.url, window.location.origin + window.location.pathname); | ||
@@ -142,3 +143,13 @@ | ||
this.entries.slice(oldCurrentIndex + 1).forEach(disposedEntry => { | ||
if (!previousEntry.finished) { | ||
// we fire the abort here for previous entry. | ||
previousEntry.__fireAbortForAssociatedEvent(); | ||
} | ||
let thisEntrysAbortError; | ||
upcomingEntry.__getAssociatedAbortSignal()?.addEventListener("abort", () => { | ||
thisEntrysAbortError = new DOMException(`A new entry was added before the promises passed to respondWith() resolved for entry with url ${upcomingEntry.url}`, "AbortError"); | ||
this.sendNavigateErrorEvent(thisEntrysAbortError); | ||
}); | ||
this.entries.slice(previousEntryIndex + 1).forEach(disposedEntry => { | ||
disposedEntry.__updateEntry(undefined, -1); | ||
@@ -148,3 +159,3 @@ | ||
}); | ||
this.entries = [...this.entries.slice(0, oldCurrentIndex + 1), this.current].map((entry, entryIndex) => { | ||
this.entries = [...this.entries.slice(0, previousEntryIndex + 1), this.current].map((entry, entryIndex) => { | ||
entry.__updateEntry(undefined, entryIndex); | ||
@@ -155,2 +166,6 @@ | ||
return Promise.all(respondWithPromiseArray).then(() => { | ||
if (thisEntrysAbortError) { | ||
throw thisEntrysAbortError; | ||
} | ||
upcomingEntry.finished = true; | ||
@@ -162,2 +177,7 @@ | ||
}).catch(error => { | ||
if (error && error === thisEntrysAbortError) { | ||
// abort errors don't change finished or fire the finish event. the navigateError event was already fired | ||
throw error; | ||
} | ||
upcomingEntry.finished = true; | ||
@@ -172,15 +192,15 @@ | ||
onnavigate(callback) { | ||
set onnavigate(callback) { | ||
this.addOnEventListener("navigate", callback); | ||
} | ||
oncurrentchange(callback) { | ||
set oncurrentchange(callback) { | ||
this.addOnEventListener("currentchange", callback); | ||
} | ||
onnavigatesuccess(callback) { | ||
set onnavigatesuccess(callback) { | ||
this.addOnEventListener("navigatesuccess", callback); | ||
} | ||
onnavigateerror(callback) { | ||
set onnavigateerror(callback) { | ||
this.addOnEventListener("navigateerror", callback); | ||
@@ -199,3 +219,6 @@ } | ||
this.onEventListeners[eventName] = callback; | ||
this.addEventListener(eventName, callback); | ||
if (callback) { | ||
this.addEventListener(eventName, callback); | ||
} | ||
} | ||
@@ -287,12 +310,20 @@ | ||
if (canRespond) { | ||
destinationEntry.sameDocument = true; | ||
respondWithResponses.push(respondWithPromise); | ||
} else { | ||
throw new Error("You cannot respond to this this event. Check event.canRespond before using respondWith"); | ||
throw new DOMException("Cannot call AppHistoryNavigateEvent.respondWith() if AppHistoryNavigateEvent.canRespond is false", "SecurityError"); | ||
} | ||
} | ||
}); | ||
}); // associate the event to the entry so that we can call the abort controller if necessary in the future | ||
destinationEntry.__associateNavigateEvent(navigateEvent); | ||
this.eventListeners.navigate.forEach(listener => { | ||
try { | ||
listener.call(this, navigateEvent); | ||
} catch (error) {} | ||
} catch (error) { | ||
setTimeout(() => { | ||
throw error; | ||
}); | ||
} | ||
}); | ||
@@ -314,3 +345,7 @@ | ||
})); | ||
} catch (error) {} | ||
} catch (error) { | ||
setTimeout(() => { | ||
throw error; | ||
}); | ||
} | ||
}); | ||
@@ -323,3 +358,7 @@ } | ||
listener(new CustomEvent("TODO figure out the correct event")); | ||
} catch (error) {} | ||
} catch (error) { | ||
setTimeout(() => { | ||
throw error; | ||
}); | ||
} | ||
}); | ||
@@ -336,3 +375,7 @@ } | ||
})); | ||
} catch (error) {} | ||
} catch (error) { | ||
setTimeout(() => { | ||
throw error; | ||
}); | ||
} | ||
}); | ||
@@ -357,2 +400,4 @@ } | ||
_defineProperty(this, "latestNavigateEvent", void 0); | ||
_defineProperty(this, "eventListeners", { | ||
@@ -374,3 +419,3 @@ navigateto: [], | ||
this.finished = false; | ||
const upcomingUrl = options?.url ?? previousEntry?.url ?? ""; | ||
const upcomingUrl = options?.url ?? previousEntry?.url ?? window.location.pathname; | ||
this.url = upcomingUrl; | ||
@@ -423,6 +468,28 @@ const upcomingUrlObj = new URL(upcomingUrl, window.location.origin + window.location.pathname); | ||
listener(newEvent); | ||
} catch (error) {} | ||
} catch (error) { | ||
setTimeout(() => { | ||
throw error; | ||
}); | ||
} | ||
}); | ||
} | ||
/** DO NOT USE; for internal purposes only */ | ||
__associateNavigateEvent(event) { | ||
this.latestNavigateEvent = event; | ||
} | ||
/** DO NOT USE; for internal purposes only */ | ||
__fireAbortForAssociatedEvent() { | ||
this.latestNavigateEvent?.__abort(); | ||
} | ||
/** DO NOT USE; for internal purposes only */ | ||
__getAssociatedAbortSignal() { | ||
return this.latestNavigateEvent?.signal; | ||
} | ||
} | ||
@@ -448,2 +515,6 @@ | ||
_defineProperty(this, "signal", void 0); | ||
_defineProperty(this, "abortController", void 0); | ||
this.userInitiated = eventInit.userInitiated ?? false; | ||
@@ -456,4 +527,11 @@ this.hashChange = eventInit.hashChange ?? false; | ||
this.info = eventInit.info; | ||
this.abortController = new AbortController(); | ||
this.signal = this.abortController.signal; | ||
} | ||
/** DO NOT USE; for internal purposes only */ | ||
__abort() { | ||
this.abortController.abort(); | ||
} | ||
} | ||
@@ -493,13 +571,19 @@ | ||
}); | ||
window.addEventListener("click", evt => { | ||
if (evt.target && evt.target instanceof HTMLElement) { | ||
// on anchor/area clicks, fire 'appHistory.push()' | ||
const linkTag = evt.target.nodeName === "A" || evt.target.nodeName === "AREA" ? evt.target : evt.target.closest("a") ?? evt.target.closest("area"); | ||
window.addEventListener("click", windowClickHandler); | ||
} | ||
if (linkTag) { | ||
evt.preventDefault(); | ||
window.appHistory.push(linkTag.href); | ||
} | ||
function windowClickHandler(evt) { | ||
if (evt.target && evt.target instanceof HTMLElement) { | ||
// on anchor/area clicks, fire 'appHistory.push()' | ||
const linkTag = evt.target.nodeName === "A" || evt.target.nodeName === "AREA" ? evt.target : evt.target.closest("a") ?? evt.target.closest("area"); | ||
if (linkTag) { | ||
evt.preventDefault(); | ||
window.appHistory.push(linkTag.href).catch(err => { | ||
setTimeout(() => { | ||
throw err; | ||
}); | ||
}); | ||
} | ||
}); | ||
} | ||
} | ||
@@ -506,0 +590,0 @@ |
@@ -1,2 +0,2 @@ | ||
function t(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}class e{constructor(){t(this,"current",void 0),t(this,"entries",void 0),t(this,"canGoBack",void 0),t(this,"canGoForward",void 0),t(this,"eventListeners",{navigate:[],currentchange:[],navigatesuccess:[],navigateerror:[]}),t(this,"onEventListeners",{navigate:null,currentchange:null,navigatesuccess:null,navigateerror:null}),this.current=new n({url:"TODO FIX DEFAULT URL"}),this.current.__updateEntry(void 0,0),this.entries=[this.current],this.canGoBack=!1,this.canGoForward=!1}getOptionsFromParams(t,e){let n;switch(typeof t){case"string":e&&"object"==typeof e?(n=e,n.url=t):n={url:t};break;case"object":t&&(n=t)}return n}async update(t,e){const n=performance.now(),i=this.getOptionsFromParams(t,e);this.current.__updateEntry(i??{}),this.current.finished=!1;const s=this.sendNavigateEvent(this.current,i?.navigateInfo);return this.sendCurrentChangeEvent(n),Promise.all(s).then((()=>{this.current.finished=!0,this.current.__fireEventListenersForEvent("finish"),this.sendNavigateSuccessEvent()})).catch((t=>{throw this.current.finished=!0,this.current.__fireEventListenersForEvent("finish"),this.sendNavigateErrorEvent(t),t}))}async push(t,e){const i=performance.now(),s=this.getOptionsFromParams(t,e),r=new n(s,this.current),a=this.sendNavigateEvent(r,s?.navigateInfo);this.current.__fireEventListenersForEvent("navigatefrom");const o=this.current,h=this.entries.findIndex((t=>t.key===o.key));return new URL(r.url,window.location.origin+window.location.pathname).origin===window.location.origin?window.history.pushState(null,"",r.url):window.location.assign(r.url),this.current=r,this.canGoBack=!0,this.canGoForward=!1,this.sendCurrentChangeEvent(i),this.current.__fireEventListenersForEvent("navigateto"),this.entries.slice(h+1).forEach((t=>{t.__updateEntry(void 0,-1),t.__fireEventListenersForEvent("dispose")})),this.entries=[...this.entries.slice(0,h+1),this.current].map(((t,e)=>(t.__updateEntry(void 0,e),t))),Promise.all(a).then((()=>{r.finished=!0,r.__fireEventListenersForEvent("finish"),this.sendNavigateSuccessEvent()})).catch((t=>{throw r.finished=!0,r.__fireEventListenersForEvent("finish"),this.sendNavigateErrorEvent(t),t}))}onnavigate(t){this.addOnEventListener("navigate",t)}oncurrentchange(t){this.addOnEventListener("currentchange",t)}onnavigatesuccess(t){this.addOnEventListener("navigatesuccess",t)}onnavigateerror(t){this.addOnEventListener("navigateerror",t)}addOnEventListener(t,e){this.onEventListeners[t]&&("navigate"===t?this.eventListeners.navigate=this.eventListeners.navigate.filter((t=>t!==this.onEventListeners.navigate)):this.eventListeners[t]=this.eventListeners[t].filter((e=>e!==this.onEventListeners[t]))),this.onEventListeners[t]=e,this.addEventListener(t,e)}addEventListener(t,e){if("navigate"!==t&&"currentchange"!==t&&"navigatesuccess"!==t&&"navigateerror"!==t)throw new Error("appHistory does not listen for that event at this time");!function(t,e){return"navigate"===t}(t)?this.eventListeners[t].includes(e)||this.eventListeners[t].push(e):this.eventListeners.navigate.includes(e)||this.eventListeners.navigate.push(e)}async navigateTo(t,e){const n=this.entries.findIndex((e=>e.key===t));if(-1===n)throw new DOMException("InvalidStateError");const i=this.entries[n];await this.changeCurrentEntry(i,e)}async back(t){const e=this.entries.findIndex((t=>t.key===this.current.key));if(0===e)throw new DOMException("InvalidStateError");const n=this.entries[e-1];await this.changeCurrentEntry(n,t)}async forward(t){const e=this.entries.findIndex((t=>t.key===this.current.key));if(e===this.entries.length-1)throw new DOMException("InvalidStateError");const n=this.entries[e+1];await this.changeCurrentEntry(n,t)}async changeCurrentEntry(t,e){await this.sendNavigateEvent(t,e?.navigateInfo),this.current.__fireEventListenersForEvent("navigatefrom"),this.current=t,this.current.__fireEventListenersForEvent("navigateto"),this.canGoBack=this.current.index>0,this.canGoForward=this.current.index<this.entries.length-1}sendNavigateEvent(t,e){const n=[],s=new URL(t.url,window.location.origin+window.location.pathname),r=s.origin===window.location.origin,a=new i({cancelable:!0,userInitiated:!0,hashChange:t.sameDocument&&s.hash!==window.location.hash,destination:t,info:e,canRespond:r,respondWith:t=>{if(!r)throw new Error("You cannot respond to this this event. Check event.canRespond before using respondWith");n.push(t)}});if(this.eventListeners.navigate.forEach((t=>{try{t.call(this,a)}catch(t){}})),a.defaultPrevented)throw new DOMException("AbortError");return n}sendCurrentChangeEvent(t){this.eventListeners.currentchange.forEach((e=>{try{e.call(this,new s({startTime:t}))}catch(t){}}))}sendNavigateSuccessEvent(){this.eventListeners.navigatesuccess.forEach((t=>{try{t(new CustomEvent("TODO figure out the correct event"))}catch(t){}}))}sendNavigateErrorEvent(t){this.eventListeners.navigateerror.forEach((e=>{try{e(new CustomEvent("TODO figure out the correct event",{detail:{error:t}}))}catch(t){}}))}}class n{constructor(e,n){t(this,"key",void 0),t(this,"url",void 0),t(this,"sameDocument",void 0),t(this,"index",void 0),t(this,"_state",void 0),t(this,"finished",void 0),t(this,"eventListeners",{navigateto:[],navigatefrom:[],dispose:[],finish:[]}),this._state=null,e?.state&&(this._state=e.state),this.key=Math.random().toString(36).substr(2,10),this.index=-1,this.finished=!1;const i=e?.url??n?.url??"";this.url=i;const s=new URL(i,window.location.origin+window.location.pathname);this.sameDocument=s.origin===window.location.origin&&s.pathname===window.location.pathname}getState(){return JSON.parse(JSON.stringify(this._state))}addEventListener(t,e){this.eventListeners[t].includes(e)||this.eventListeners[t].push(e)}__updateEntry(t,e){void 0!==t?.state&&(this._state=t.state),t?.url&&(this.url=t.url),"number"==typeof e&&(this.index=e)}__fireEventListenersForEvent(t){const e=new r({detail:{target:this}},t);this.eventListeners[t].map((t=>{try{t(e)}catch(t){}}))}}class i extends Event{constructor(e){super("AppHistoryNavigateEvent",e),t(this,"userInitiated",void 0),t(this,"hashChange",void 0),t(this,"destination",void 0),t(this,"formData",void 0),t(this,"info",void 0),t(this,"canRespond",void 0),t(this,"respondWith",void 0),this.userInitiated=e.userInitiated??!1,this.hashChange=e.hashChange??!1,this.destination=e.destination,this.formData=e.formData,this.canRespond=e.canRespond,this.respondWith=e.respondWith,this.info=e.info}}class s extends Event{constructor(e){super("AppHistoryCurrentChangeEvent",e),t(this,"startTime",void 0),this.startTime=e.startTime}}class r extends CustomEvent{constructor(t,e){super(e,t)}}function a(t){"appHistory"in window||(Object.defineProperty(window,"appHistory",{value:new e,enumerable:!0,configurable:t?.configurable??!1}),window.addEventListener("click",(t=>{if(t.target&&t.target instanceof HTMLElement){const e="A"===t.target.nodeName||"AREA"===t.target.nodeName?t.target:t.target.closest("a")??t.target.closest("area");e&&(t.preventDefault(),window.appHistory.push(e.href))}})))}export{e as AppHistory,a as useBrowserPolyfill}; | ||
function t(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}class e{constructor(){t(this,"current",void 0),t(this,"entries",void 0),t(this,"canGoBack",void 0),t(this,"canGoForward",void 0),t(this,"eventListeners",{navigate:[],currentchange:[],navigatesuccess:[],navigateerror:[]}),t(this,"onEventListeners",{navigate:null,currentchange:null,navigatesuccess:null,navigateerror:null}),this.current=new n({url:window.location.href}),this.current.finished=!0,this.current.__updateEntry(void 0,0),this.entries=[this.current],this.canGoBack=!1,this.canGoForward=!1}getOptionsFromParams(t,e){let n;switch(typeof t){case"string":e&&"object"==typeof e?(n=e,n.url=t):n={url:t};break;case"object":t&&(n=t)}return n}async update(t,e){const n=performance.now(),i=this.getOptionsFromParams(t,e);this.current.__updateEntry(i??{}),this.current.finished=!1;const s=this.sendNavigateEvent(this.current,i?.navigateInfo);return this.sendCurrentChangeEvent(n),Promise.all(s).then((()=>{this.current.finished=!0,this.current.__fireEventListenersForEvent("finish"),this.sendNavigateSuccessEvent()})).catch((t=>{throw this.current.finished=!0,this.current.__fireEventListenersForEvent("finish"),this.sendNavigateErrorEvent(t),t}))}async push(t,e){const i=this.current,s=performance.now(),r=this.getOptionsFromParams(t,e),a=new n(r,this.current),o=this.sendNavigateEvent(a,r?.navigateInfo);this.current.__fireEventListenersForEvent("navigatefrom");const h=this.entries.findIndex((t=>t.key===i.key));let c;return new URL(a.url,window.location.origin+window.location.pathname).origin===window.location.origin?window.history.pushState(null,"",a.url):window.location.assign(a.url),this.current=a,this.canGoBack=!0,this.canGoForward=!1,this.sendCurrentChangeEvent(s),this.current.__fireEventListenersForEvent("navigateto"),i.finished||i.__fireAbortForAssociatedEvent(),a.__getAssociatedAbortSignal()?.addEventListener("abort",(()=>{c=new DOMException(`A new entry was added before the promises passed to respondWith() resolved for entry with url ${a.url}`,"AbortError"),this.sendNavigateErrorEvent(c)})),this.entries.slice(h+1).forEach((t=>{t.__updateEntry(void 0,-1),t.__fireEventListenersForEvent("dispose")})),this.entries=[...this.entries.slice(0,h+1),this.current].map(((t,e)=>(t.__updateEntry(void 0,e),t))),Promise.all(o).then((()=>{if(c)throw c;a.finished=!0,a.__fireEventListenersForEvent("finish"),this.sendNavigateSuccessEvent()})).catch((t=>{if(t&&t===c)throw t;throw a.finished=!0,a.__fireEventListenersForEvent("finish"),this.sendNavigateErrorEvent(t),t}))}set onnavigate(t){this.addOnEventListener("navigate",t)}set oncurrentchange(t){this.addOnEventListener("currentchange",t)}set onnavigatesuccess(t){this.addOnEventListener("navigatesuccess",t)}set onnavigateerror(t){this.addOnEventListener("navigateerror",t)}addOnEventListener(t,e){this.onEventListeners[t]&&("navigate"===t?this.eventListeners.navigate=this.eventListeners.navigate.filter((t=>t!==this.onEventListeners.navigate)):this.eventListeners[t]=this.eventListeners[t].filter((e=>e!==this.onEventListeners[t]))),this.onEventListeners[t]=e,e&&this.addEventListener(t,e)}addEventListener(t,e){if("navigate"!==t&&"currentchange"!==t&&"navigatesuccess"!==t&&"navigateerror"!==t)throw new Error("appHistory does not listen for that event at this time");!function(t,e){return"navigate"===t}(t)?this.eventListeners[t].includes(e)||this.eventListeners[t].push(e):this.eventListeners.navigate.includes(e)||this.eventListeners.navigate.push(e)}async navigateTo(t,e){const n=this.entries.findIndex((e=>e.key===t));if(-1===n)throw new DOMException("InvalidStateError");const i=this.entries[n];await this.changeCurrentEntry(i,e)}async back(t){const e=this.entries.findIndex((t=>t.key===this.current.key));if(0===e)throw new DOMException("InvalidStateError");const n=this.entries[e-1];await this.changeCurrentEntry(n,t)}async forward(t){const e=this.entries.findIndex((t=>t.key===this.current.key));if(e===this.entries.length-1)throw new DOMException("InvalidStateError");const n=this.entries[e+1];await this.changeCurrentEntry(n,t)}async changeCurrentEntry(t,e){await this.sendNavigateEvent(t,e?.navigateInfo),this.current.__fireEventListenersForEvent("navigatefrom"),this.current=t,this.current.__fireEventListenersForEvent("navigateto"),this.canGoBack=this.current.index>0,this.canGoForward=this.current.index<this.entries.length-1}sendNavigateEvent(t,e){const n=[],s=new URL(t.url,window.location.origin+window.location.pathname),r=s.origin===window.location.origin,a=new i({cancelable:!0,userInitiated:!0,hashChange:t.sameDocument&&s.hash!==window.location.hash,destination:t,info:e,canRespond:r,respondWith:e=>{if(!r)throw new DOMException("Cannot call AppHistoryNavigateEvent.respondWith() if AppHistoryNavigateEvent.canRespond is false","SecurityError");t.sameDocument=!0,n.push(e)}});if(t.__associateNavigateEvent(a),this.eventListeners.navigate.forEach((t=>{try{t.call(this,a)}catch(t){setTimeout((()=>{throw t}))}})),a.defaultPrevented)throw new DOMException("AbortError");return n}sendCurrentChangeEvent(t){this.eventListeners.currentchange.forEach((e=>{try{e.call(this,new s({startTime:t}))}catch(t){setTimeout((()=>{throw t}))}}))}sendNavigateSuccessEvent(){this.eventListeners.navigatesuccess.forEach((t=>{try{t(new CustomEvent("TODO figure out the correct event"))}catch(t){setTimeout((()=>{throw t}))}}))}sendNavigateErrorEvent(t){this.eventListeners.navigateerror.forEach((e=>{try{e(new CustomEvent("TODO figure out the correct event",{detail:{error:t}}))}catch(t){setTimeout((()=>{throw t}))}}))}}class n{constructor(e,n){t(this,"key",void 0),t(this,"url",void 0),t(this,"sameDocument",void 0),t(this,"index",void 0),t(this,"_state",void 0),t(this,"finished",void 0),t(this,"latestNavigateEvent",void 0),t(this,"eventListeners",{navigateto:[],navigatefrom:[],dispose:[],finish:[]}),this._state=null,e?.state&&(this._state=e.state),this.key=Math.random().toString(36).substr(2,10),this.index=-1,this.finished=!1;const i=e?.url??n?.url??window.location.pathname;this.url=i;const s=new URL(i,window.location.origin+window.location.pathname);this.sameDocument=s.origin===window.location.origin&&s.pathname===window.location.pathname}getState(){return JSON.parse(JSON.stringify(this._state))}addEventListener(t,e){this.eventListeners[t].includes(e)||this.eventListeners[t].push(e)}__updateEntry(t,e){void 0!==t?.state&&(this._state=t.state),t?.url&&(this.url=t.url),"number"==typeof e&&(this.index=e)}__fireEventListenersForEvent(t){const e=new r({detail:{target:this}},t);this.eventListeners[t].map((t=>{try{t(e)}catch(t){setTimeout((()=>{throw t}))}}))}__associateNavigateEvent(t){this.latestNavigateEvent=t}__fireAbortForAssociatedEvent(){this.latestNavigateEvent?.__abort()}__getAssociatedAbortSignal(){return this.latestNavigateEvent?.signal}}class i extends Event{constructor(e){super("AppHistoryNavigateEvent",e),t(this,"userInitiated",void 0),t(this,"hashChange",void 0),t(this,"destination",void 0),t(this,"formData",void 0),t(this,"info",void 0),t(this,"canRespond",void 0),t(this,"respondWith",void 0),t(this,"signal",void 0),t(this,"abortController",void 0),this.userInitiated=e.userInitiated??!1,this.hashChange=e.hashChange??!1,this.destination=e.destination,this.formData=e.formData,this.canRespond=e.canRespond,this.respondWith=e.respondWith,this.info=e.info,this.abortController=new AbortController,this.signal=this.abortController.signal}__abort(){this.abortController.abort()}}class s extends Event{constructor(e){super("AppHistoryCurrentChangeEvent",e),t(this,"startTime",void 0),this.startTime=e.startTime}}class r extends CustomEvent{constructor(t,e){super(e,t)}}function a(t){"appHistory"in window||(Object.defineProperty(window,"appHistory",{value:new e,enumerable:!0,configurable:t?.configurable??!1}),window.addEventListener("click",o))}function o(t){if(t.target&&t.target instanceof HTMLElement){const e="A"===t.target.nodeName||"AREA"===t.target.nodeName?t.target:t.target.closest("a")??t.target.closest("area");e&&(t.preventDefault(),window.appHistory.push(e.href).catch((t=>{setTimeout((()=>{throw t}))})))}}export{e as AppHistory,a as useBrowserPolyfill}; | ||
//# sourceMappingURL=appHistory.min.js.map |
@@ -9,13 +9,11 @@ export declare class AppHistory { | ||
private getOptionsFromParams; | ||
update(callback?: () => AppHistoryPushOrUpdateFullOptions): Promise<undefined>; | ||
update(fullOptions?: AppHistoryPushOrUpdateFullOptions): Promise<undefined>; | ||
update(url?: string, options?: AppHistoryPushOrUpdateOptions): Promise<undefined>; | ||
push(callback?: () => AppHistoryPushOrUpdateFullOptions): Promise<undefined>; | ||
push(fullOptions?: AppHistoryPushOrUpdateFullOptions): Promise<undefined>; | ||
push(url?: string, options?: AppHistoryPushOrUpdateOptions): Promise<undefined>; | ||
private onEventListeners; | ||
onnavigate(callback: AppHistoryNavigateEventListener): void; | ||
oncurrentchange(callback: EventListener): void; | ||
onnavigatesuccess(callback: EventListener): void; | ||
onnavigateerror(callback: EventListener): void; | ||
set onnavigate(callback: AppHistoryNavigateEventListener); | ||
set oncurrentchange(callback: EventListener); | ||
set onnavigatesuccess(callback: EventListener); | ||
set onnavigateerror(callback: EventListener); | ||
private addOnEventListener; | ||
@@ -40,2 +38,3 @@ addEventListener(eventName: keyof AppHistoryEventListeners, callback: AppHistoryNavigateEventListener | EventListener): void; | ||
finished: boolean; | ||
private latestNavigateEvent?; | ||
private eventListeners; | ||
@@ -49,2 +48,8 @@ /** Provides a JSON.parse(JSON.stringify()) copy of the Entry's state. */ | ||
__fireEventListenersForEvent(eventName: keyof AppHistoryEntryEventListeners): void; | ||
/** DO NOT USE; for internal purposes only */ | ||
__associateNavigateEvent(event: AppHistoryNavigateEvent): void; | ||
/** DO NOT USE; for internal purposes only */ | ||
__fireAbortForAssociatedEvent(): void; | ||
/** DO NOT USE; for internal purposes only */ | ||
__getAssociatedAbortSignal(): AbortSignal | undefined; | ||
} | ||
@@ -92,3 +97,7 @@ declare type AppHistoryNavigateEventListener = (event: AppHistoryNavigateEvent) => void; | ||
respondWith: (respondWithPromise: Promise<undefined>) => void; | ||
readonly signal: AbortSignal; | ||
private abortController; | ||
/** DO NOT USE; for internal purposes only */ | ||
__abort(): void; | ||
} | ||
export {}; |
# AppHistory | ||
## 0.0.5 | ||
- add signal to navigate event and fire abort if a new entry is added before the promises given to respondWith() resolve. The promise returned from appHistory.push will reject, and entry.finished will remain false | ||
- fix default url for first entry | ||
- fix on{event} handler signatures | ||
- add test case for ensuring sameDocument is turned into true if respondWith is call, and throws a SecurityError if you can't respond (cross-origin situations) | ||
## 0.0.4 | ||
@@ -4,0 +11,0 @@ |
@@ -131,3 +131,3 @@ /* | ||
// A list of paths to modules that run some code to configure or set up the testing framework before each test | ||
// setupFilesAfterEnv: [], | ||
setupFilesAfterEnv: ["./jest.setup.js"], | ||
@@ -170,3 +170,3 @@ // The number of seconds after which a test is considered as slow and reported as such in the results. | ||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href | ||
// testURL: "http://localhost", | ||
testURL: "http://localhost", | ||
@@ -196,4 +196,2 @@ // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" | ||
// watchman: true, | ||
testURL: "http://localhost", | ||
}; |
{ | ||
"name": "@frehner/apphistory", | ||
"version": "0.0.4", | ||
"version": "0.0.5", | ||
"description": "A polyfill for the appHistory proposal. Not ready for production", | ||
@@ -5,0 +5,0 @@ "main": "build/esm/appHistory.min.js", |
import { AppHistory } from "./appHistory"; | ||
beforeEach(() => { | ||
window.history.pushState(null, null, "/"); | ||
}); | ||
describe("appHistory constructor", () => { | ||
@@ -19,2 +23,4 @@ it("should initialize with a current and entries", () => { | ||
const appHistory = new AppHistory(); | ||
expect(appHistory.current.sameDocument).toBe(true); | ||
await appHistory.push("/newUrl"); | ||
@@ -29,2 +35,15 @@ expect(appHistory.current.sameDocument).toBe(false); | ||
// any cross-document navigations are turned into same-document navigations if respondWith receives a promise | ||
let navigateEvent = null; | ||
appHistory.onnavigate = (evt) => { | ||
navigateEvent = evt; | ||
evt.respondWith(Promise.resolve()); | ||
}; | ||
await appHistory.push("/url2"); | ||
expect(navigateEvent.destination.sameDocument).toBe(true); | ||
expect(appHistory.current.sameDocument).toBe(true); | ||
expect(appHistory.current.url).toBe("/url2"); | ||
appHistory.onnavigate = null; | ||
MockLocation.mock(); | ||
@@ -140,3 +159,3 @@ | ||
expect(appHistory.entries.map((entry) => entry.url)).toEqual([ | ||
"TODO FIX DEFAULT URL", | ||
"http://localhost/", | ||
"/newTest1", | ||
@@ -196,5 +215,3 @@ "/test2", | ||
it.todo( | ||
"should take in a callback function that can return AppHistoryEntryFullOptions. Skipping for now because of unclear spec" | ||
); | ||
it.todo("updates in https://github.com/WICG/app-history/pull/68/files"); | ||
@@ -251,3 +268,3 @@ it("only state: should overwrite the state and copy the previous URL", async () => { | ||
expect(appHistory.entries.map((entry) => entry.url)).toEqual([ | ||
"TODO FIX DEFAULT URL", | ||
"http://localhost/", | ||
"/temp1", | ||
@@ -275,3 +292,3 @@ "/temp2", | ||
expect(appHistory.entries.map((entry) => entry.url)).toEqual([ | ||
"TODO FIX DEFAULT URL", | ||
"http://localhost/", | ||
"/temp3", | ||
@@ -321,5 +338,2 @@ ]); | ||
describe("navigate", () => { | ||
beforeEach(() => { | ||
window.history.pushState(null, "", "/"); | ||
}); | ||
it("should add an event listener", async (done) => { | ||
@@ -399,3 +413,3 @@ const appHistory = new AppHistory(); | ||
it("should handle if a listener throws and continue to call other listeners", async () => { | ||
it.skip("should handle if a listener throws and continue to call other listeners", async () => { | ||
const appHistory = new AppHistory(); | ||
@@ -414,4 +428,2 @@ | ||
await appHistory.push(); | ||
expect(listenerEvents).toEqual(["1", "2"]); | ||
@@ -425,9 +437,9 @@ }); | ||
appHistory.onnavigate(() => { | ||
appHistory.onnavigate = () => { | ||
timesCalled++; | ||
}); | ||
}; | ||
appHistory.onnavigate(() => { | ||
appHistory.onnavigate = () => { | ||
timesCalled++; | ||
}); | ||
}; | ||
@@ -475,5 +487,76 @@ await appHistory.push(); | ||
it.skip("should throw a SecurityError DOMError if you use respondWith() when canRespond=false", async (done) => { | ||
const appHistory = new AppHistory(); | ||
appHistory.addEventListener("navigate", (evt) => { | ||
expect(evt.canRespond).toBe(false); | ||
expect(() => evt.respondWith(Promise.resolve())).rejects.toThrowError( | ||
new DOMException() | ||
); | ||
done(); | ||
}); | ||
window.onerror = (err) => { | ||
console.log("caught"); | ||
}; | ||
MockLocation.mock(); | ||
await appHistory.push("https://example.com"); | ||
MockLocation.restore(); | ||
}); | ||
it("should have a signal that isn't aborted by default", async (done) => { | ||
const appHistory = new AppHistory(); | ||
appHistory.onnavigate = (evt) => { | ||
expect(evt.signal.aborted).toBe(false); | ||
done(); | ||
}; | ||
await appHistory.push("/test"); | ||
}); | ||
it("should fire abort if another push event happened while the previous respondWith promise is in flight", async (done) => { | ||
const appHistory = new AppHistory(); | ||
let timesCalled = 0; | ||
appHistory.onnavigate = (evt) => { | ||
evt.respondWith( | ||
(async () => { | ||
if (timesCalled === 0) { | ||
timesCalled++; | ||
await new Promise((resolve) => setTimeout(resolve, 10)); | ||
expect(evt.signal.aborted).toBe(true); | ||
expect(timesCalled).toBe(2); | ||
done(); | ||
} else { | ||
timesCalled++; | ||
} | ||
})() | ||
); | ||
}; | ||
let slowEntry; | ||
try { | ||
// intentionally don't call await here so we can fire the next push before this one finishes | ||
// however, the promise will reject, so we need to handle that | ||
const slowPromise = appHistory.push("/slowUrl"); | ||
slowEntry = appHistory.current; | ||
await new Promise((resolve) => setTimeout(resolve)); | ||
await appHistory.push("/newerUrl"); | ||
expect(appHistory.current.url).toBe("/newerUrl"); | ||
await slowPromise; | ||
} catch (error) { | ||
expect(error).toBeInstanceOf(DOMException); | ||
expect(slowEntry.finished).toBe(false); | ||
} | ||
}); | ||
it.todo("add a case for when search params come after the hash?"); | ||
it.todo("all the 'canRespond' cases"); | ||
it.todo( | ||
"all the 'canRespond' cases from https://github.com/WICG/app-history#appendix-types-of-navigations" | ||
); | ||
@@ -547,3 +630,3 @@ describe("respondWith", () => { | ||
it("should handle if a listener throws and continue to call other listeners", async () => { | ||
it.skip("should handle if a listener throws and continue to call other listeners", async () => { | ||
const appHistory = new AppHistory(); | ||
@@ -572,9 +655,9 @@ | ||
appHistory.oncurrentchange(() => { | ||
appHistory.oncurrentchange = () => { | ||
timesCalled++; | ||
}); | ||
}; | ||
appHistory.oncurrentchange(() => { | ||
appHistory.oncurrentchange = () => { | ||
timesCalled++; | ||
}); | ||
}; | ||
@@ -718,3 +801,3 @@ await appHistory.push(); | ||
appHistory.onnavigate((evt) => { | ||
appHistory.onnavigate = (evt) => { | ||
evt.respondWith( | ||
@@ -725,5 +808,5 @@ new Promise((resolve) => { | ||
); | ||
}); | ||
}; | ||
const pushPromise = appHistory.push("newUrl"); | ||
const pushPromise = appHistory.push("/newUrl"); | ||
@@ -730,0 +813,0 @@ appHistory.current.addEventListener("finish", () => { |
@@ -5,3 +5,4 @@ import { fakeRandomId } from "./helpers"; | ||
constructor() { | ||
this.current = new AppHistoryEntry({ url: "TODO FIX DEFAULT URL" }); | ||
this.current = new AppHistoryEntry({ url: window.location.href }); | ||
this.current.finished = true; | ||
this.current.__updateEntry(undefined, 0); | ||
@@ -47,5 +48,2 @@ this.entries = [this.current]; | ||
// TODO: add case for 'function' | ||
// waiting on spec clarity to implement though | ||
default: | ||
@@ -59,5 +57,2 @@ break; | ||
async update( | ||
callback?: () => AppHistoryPushOrUpdateFullOptions | ||
): Promise<undefined>; | ||
async update( | ||
fullOptions?: AppHistoryPushOrUpdateFullOptions | ||
@@ -105,5 +100,2 @@ ): Promise<undefined>; | ||
async push( | ||
callback?: () => AppHistoryPushOrUpdateFullOptions | ||
): Promise<undefined>; | ||
async push( | ||
fullOptions?: AppHistoryPushOrUpdateFullOptions | ||
@@ -119,2 +111,4 @@ ): Promise<undefined>; | ||
) { | ||
const previousEntry = this.current; | ||
// used in the currentchange event | ||
@@ -133,5 +127,4 @@ const startTime = performance.now(); | ||
this.current.__fireEventListenersForEvent("navigatefrom"); | ||
const oldCurrent = this.current; | ||
const oldCurrentIndex = this.entries.findIndex( | ||
(entry) => entry.key === oldCurrent.key | ||
const previousEntryIndex = this.entries.findIndex( | ||
(entry) => entry.key === previousEntry.key | ||
); | ||
@@ -156,3 +149,19 @@ | ||
this.entries.slice(oldCurrentIndex + 1).forEach((disposedEntry) => { | ||
if (!previousEntry.finished) { | ||
// we fire the abort here for previous entry. | ||
previousEntry.__fireAbortForAssociatedEvent(); | ||
} | ||
let thisEntrysAbortError: DOMException | undefined; | ||
upcomingEntry | ||
.__getAssociatedAbortSignal() | ||
?.addEventListener("abort", () => { | ||
thisEntrysAbortError = new DOMException( | ||
`A new entry was added before the promises passed to respondWith() resolved for entry with url ${upcomingEntry.url}`, | ||
"AbortError" | ||
); | ||
this.sendNavigateErrorEvent(thisEntrysAbortError); | ||
}); | ||
this.entries.slice(previousEntryIndex + 1).forEach((disposedEntry) => { | ||
disposedEntry.__updateEntry(undefined, -1); | ||
@@ -163,3 +172,3 @@ disposedEntry.__fireEventListenersForEvent("dispose"); | ||
this.entries = [ | ||
...this.entries.slice(0, oldCurrentIndex + 1), | ||
...this.entries.slice(0, previousEntryIndex + 1), | ||
this.current, | ||
@@ -173,2 +182,5 @@ ].map((entry, entryIndex) => { | ||
.then(() => { | ||
if (thisEntrysAbortError) { | ||
throw thisEntrysAbortError; | ||
} | ||
upcomingEntry.finished = true; | ||
@@ -179,2 +191,6 @@ upcomingEntry.__fireEventListenersForEvent("finish"); | ||
.catch((error) => { | ||
if (error && error === thisEntrysAbortError) { | ||
// abort errors don't change finished or fire the finish event. the navigateError event was already fired | ||
throw error; | ||
} | ||
upcomingEntry.finished = true; | ||
@@ -197,15 +213,15 @@ upcomingEntry.__fireEventListenersForEvent("finish"); | ||
onnavigate(callback: AppHistoryNavigateEventListener): void { | ||
set onnavigate(callback: AppHistoryNavigateEventListener) { | ||
this.addOnEventListener("navigate", callback); | ||
} | ||
oncurrentchange(callback: EventListener): void { | ||
set oncurrentchange(callback: EventListener) { | ||
this.addOnEventListener("currentchange", callback); | ||
} | ||
onnavigatesuccess(callback: EventListener): void { | ||
set onnavigatesuccess(callback: EventListener) { | ||
this.addOnEventListener("navigatesuccess", callback); | ||
} | ||
onnavigateerror(callback: EventListener): void { | ||
set onnavigateerror(callback: EventListener) { | ||
this.addOnEventListener("navigateerror", callback); | ||
@@ -216,3 +232,3 @@ } | ||
eventName: keyof AppHistoryEventListeners, | ||
callback: AppHistoryNavigateEventListener | EventListener | ||
callback: AppHistoryNavigateEventListener | EventListener | null | ||
) { | ||
@@ -233,3 +249,5 @@ if (this.onEventListeners[eventName]) { | ||
this.onEventListeners[eventName] = callback; | ||
this.addEventListener(eventName, callback); | ||
if (callback) { | ||
this.addEventListener(eventName, callback); | ||
} | ||
} | ||
@@ -346,6 +364,8 @@ | ||
if (canRespond) { | ||
destinationEntry.sameDocument = true; | ||
respondWithResponses.push(respondWithPromise); | ||
} else { | ||
throw new Error( | ||
"You cannot respond to this this event. Check event.canRespond before using respondWith" | ||
throw new DOMException( | ||
"Cannot call AppHistoryNavigateEvent.respondWith() if AppHistoryNavigateEvent.canRespond is false", | ||
"SecurityError" | ||
); | ||
@@ -356,6 +376,13 @@ } | ||
// associate the event to the entry so that we can call the abort controller if necessary in the future | ||
destinationEntry.__associateNavigateEvent(navigateEvent); | ||
this.eventListeners.navigate.forEach((listener) => { | ||
try { | ||
listener.call(this, navigateEvent); | ||
} catch (error) {} | ||
} catch (error) { | ||
setTimeout(() => { | ||
throw error; | ||
}); | ||
} | ||
}); | ||
@@ -375,3 +402,7 @@ | ||
listener.call(this, new AppHistoryCurrentChangeEvent({ startTime })); | ||
} catch (error) {} | ||
} catch (error) { | ||
setTimeout(() => { | ||
throw error; | ||
}); | ||
} | ||
}); | ||
@@ -384,3 +415,7 @@ } | ||
listener(new CustomEvent("TODO figure out the correct event")); | ||
} catch (error) {} | ||
} catch (error) { | ||
setTimeout(() => { | ||
throw error; | ||
}); | ||
} | ||
}); | ||
@@ -397,3 +432,7 @@ } | ||
); | ||
} catch (error) {} | ||
} catch (error) { | ||
setTimeout(() => { | ||
throw error; | ||
}); | ||
} | ||
}); | ||
@@ -416,3 +455,4 @@ } | ||
const upcomingUrl = options?.url ?? previousEntry?.url ?? ""; | ||
const upcomingUrl = | ||
options?.url ?? previousEntry?.url ?? window.location.pathname; | ||
this.url = upcomingUrl; | ||
@@ -435,2 +475,3 @@ | ||
finished: boolean; | ||
private latestNavigateEvent?: AppHistoryNavigateEvent; | ||
@@ -489,5 +530,24 @@ private eventListeners: AppHistoryEntryEventListeners = { | ||
listener(newEvent); | ||
} catch (error) {} | ||
} catch (error) { | ||
setTimeout(() => { | ||
throw error; | ||
}); | ||
} | ||
}); | ||
} | ||
/** DO NOT USE; for internal purposes only */ | ||
__associateNavigateEvent(event: AppHistoryNavigateEvent): void { | ||
this.latestNavigateEvent = event; | ||
} | ||
/** DO NOT USE; for internal purposes only */ | ||
__fireAbortForAssociatedEvent(): void { | ||
this.latestNavigateEvent?.__abort(); | ||
} | ||
/** DO NOT USE; for internal purposes only */ | ||
__getAssociatedAbortSignal(): AbortSignal | undefined { | ||
return this.latestNavigateEvent?.signal; | ||
} | ||
} | ||
@@ -511,6 +571,3 @@ | ||
type UpdatePushParam1Types = | ||
| string | ||
| (() => AppHistoryPushOrUpdateFullOptions) | ||
| AppHistoryPushOrUpdateFullOptions; | ||
type UpdatePushParam1Types = string | AppHistoryPushOrUpdateFullOptions; | ||
@@ -551,2 +608,4 @@ export type AppHistoryEntryKey = string; | ||
this.info = eventInit.info; | ||
this.abortController = new AbortController(); | ||
this.signal = this.abortController.signal; | ||
} | ||
@@ -560,2 +619,9 @@ readonly userInitiated: boolean; | ||
respondWith: (respondWithPromise: Promise<undefined>) => void; | ||
readonly signal: AbortSignal; | ||
private abortController: AbortController; | ||
/** DO NOT USE; for internal purposes only */ | ||
__abort(): void { | ||
this.abortController.abort(); | ||
} | ||
} | ||
@@ -562,0 +628,0 @@ |
import { useBrowserPolyfill } from "./polyfill"; | ||
beforeEach(() => { | ||
document.body.innerHTML = ""; | ||
delete window.appHistory; | ||
}); | ||
// change these tests to use playwright? | ||
describe("useBrowserPolyfill", () => { | ||
afterEach(() => { | ||
delete window.appHistory; | ||
}); | ||
it("should not do anything if appHistory is already on window", () => { | ||
@@ -36,3 +38,3 @@ const fakeAppHistory = {}; | ||
document.body.innerHTML = `<div><a href="/page"><span>Page<span></a></div>`; | ||
document.body.innerHTML = `<div><a href="/page"><span>Page</span></a></div>`; | ||
@@ -59,6 +61,10 @@ document.querySelector("span").click(); | ||
useBrowserPolyfill({ configurable: true }); | ||
window.addEventListener("click", (evt) => { | ||
expect(evt.defaultPrevented).toBe(true); | ||
done(); | ||
}); | ||
window.addEventListener( | ||
"click", | ||
(evt) => { | ||
expect(evt.defaultPrevented).toBe(true); | ||
done(); | ||
}, | ||
{ once: true } | ||
); | ||
@@ -70,2 +76,25 @@ document.body.innerHTML = `<div><a href="/page">Page</a></div>`; | ||
it("should abort a previous anchor click if the promise isn't complete yet", async () => { | ||
useBrowserPolyfill({ configurable: true }); | ||
let firstRespondWith; | ||
window.appHistory.addEventListener("navigate", (evt) => { | ||
if (evt.destination.url === "/page1") { | ||
firstRespondWith = new Promise((resolve) => setTimeout(resolve, 10)); | ||
evt.respondWith(firstRespondWith); | ||
} | ||
}); | ||
document.body.innerHTML = `<div><a href="/page1">Page1</a><a href="/page2">Page2</a></div>`; | ||
[...document.querySelectorAll("a")].forEach((ele) => ele.click()); | ||
await firstRespondWith; | ||
expect(window.appHistory.entries.length).toBe(3); | ||
expect(window.appHistory.current.url).toBe("http://localhost/page2"); | ||
expect(window.appHistory.entries[1].url).toBe("http://localhost/page1"); | ||
expect(window.appHistory.entries[1].finished).toBe(false); | ||
}); | ||
it("should not error out if no param passed", () => { | ||
@@ -72,0 +101,0 @@ useBrowserPolyfill(); |
@@ -17,15 +17,21 @@ import { AppHistory } from "./appHistory"; | ||
window.addEventListener("click", (evt) => { | ||
if (evt.target && evt.target instanceof HTMLElement) { | ||
// on anchor/area clicks, fire 'appHistory.push()' | ||
const linkTag = | ||
evt.target.nodeName === "A" || evt.target.nodeName === "AREA" | ||
? (evt.target as HTMLAreaElement | HTMLAnchorElement) | ||
: evt.target.closest("a") ?? evt.target.closest("area"); | ||
if (linkTag) { | ||
evt.preventDefault(); | ||
window.appHistory.push(linkTag.href); | ||
} | ||
window.addEventListener("click", windowClickHandler); | ||
} | ||
function windowClickHandler(evt: Event) { | ||
if (evt.target && evt.target instanceof HTMLElement) { | ||
// on anchor/area clicks, fire 'appHistory.push()' | ||
const linkTag = | ||
evt.target.nodeName === "A" || evt.target.nodeName === "AREA" | ||
? (evt.target as HTMLAreaElement | HTMLAnchorElement) | ||
: evt.target.closest("a") ?? evt.target.closest("area"); | ||
if (linkTag) { | ||
evt.preventDefault(); | ||
window.appHistory.push(linkTag.href).catch((err) => { | ||
setTimeout(() => { | ||
throw err; | ||
}); | ||
}); | ||
} | ||
}); | ||
} | ||
} | ||
@@ -32,0 +38,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
172923
25
2470