@ionic/core 8.8.9-nightly.20260527 → 8.8.9-nightly.20260529
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/ion-router.js +1 -1
- package/dist/cjs/ion-route_4.cjs.entry.js +184 -18
- package/dist/collection/components/router/router.js +55 -14
- package/dist/collection/components/router/utils/dom.js +114 -1
- package/dist/collection/components/router/utils/path.js +17 -5
- package/dist/docs.json +1 -1
- package/dist/esm/ion-route_4.entry.js +185 -19
- package/dist/ionic/ionic.esm.js +1 -1
- package/dist/ionic/p-b5879e77.entry.js +4 -0
- package/dist/types/components/router/router.d.ts +7 -0
- package/dist/types/components/router/utils/dom.d.ts +10 -0
- package/dist/types/components/router/utils/interface.d.ts +2 -0
- package/dist/types/components/router/utils/path.d.ts +3 -2
- package/hydrate/index.js +183 -18
- package/hydrate/index.mjs +183 -18
- package/package.json +1 -1
- package/dist/ionic/p-771b27a5.entry.js +0 -4
package/components/ion-router.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* (C) Ionic http://ionicframework.com - MIT License
|
|
3
3
|
*/
|
|
4
|
-
import{j as t,p as n,H as o,e,f as r,t as s}from"./p-BJoMtgfR.js";import{c as i,p as a}from"./p-DgbT0exM.js";const c="root",u="forward",h=t=>"/"+t.filter((t=>t.length>0)).join("/"),l=t=>{let n,o=[""];if(null!=t){const e=t.indexOf("?");e>-1&&(n=t.substring(e+1),t=t.substring(0,e)),o=t.split("/").map((t=>t.trim())).filter((t=>t.length>0)),0===o.length&&(o=[""])}return{segments:o,queryString:n}},f=async(n,o,e,r,s=!1,a)=>{try{const t=w(n);if(r>=o.length||!t)return s;await new Promise((n=>i(t,n)));const u=o[r],h=await t.setRouteId(u.id,u.params,e,a);return h.changed&&(e=c,s=!0),s=await f(h.element,o,e,r+1,s,a),h.markVisible&&await h.markVisible(),s}catch(n){return t("[ion-router] - Exception in writeNavState:",n),!1}},d=":not([no-router]) ion-nav, :not([no-router]) ion-tabs, :not([no-router]) ion-router-outlet",w=t=>{if(!t)return;if(t.matches(d))return t;const n=t.querySelector(d);return null!=n?n:void 0},m=(t,n)=>n.find((n=>((t,n)=>{const{from:o,to:e}=n;if(void 0===e)return!1;if(o.length>t.length)return!1;for(let n=0;n<o.length;n++){const e=o[n];if("*"===e)return!0;if(e!==t[n])return!1}return o.length===t.length})(t,n))),p=(t,n)=>{const o=Math.min(t.length,n.length);let e=0;for(let r=0;r<o;r++){const o=t[r],s=n[r];if(o.id.toLowerCase()!==s.id)break;if(o.params){const t=Object.keys(o.params);if(t.length===s.segments.length){const n=t.map((t=>":"+t));for(let t=0;t<n.length&&n[t].toLowerCase()===s.segments[t];t++)e++}}e++}return e},g=(t,n)=>{const o=new R(t);let e,r=!1;for(let t=0;t<n.length;t++){const s=n[t].segments;if(""===s[0])r=!0;else{for(const n of s){const r=o.next();if(":"===n[0]){if(""===r)return null;e=e||[],(e[t]||(e[t]={}))[n.slice(1)]=r}else if(r!==n)return null}r=!1}}return r&&r!==(""===o.next())?null:e?n.map(((t,n)=>({id:t.id,segments:t.segments,params:v(t.params,e[n]),beforeEnter:t.beforeEnter,beforeLeave:t.beforeLeave}))):n},v=(t,n)=>t||n?Object.assign(Object.assign({},t),n):void 0,b=(t,n)=>{let o=null,e=0;for(const r of n){const n=g(t,r);if(null!==n){const t=y(n);t>e&&(e=t,o=n)}}return o},y=t=>{let n=1,o=1;for(const e of t)for(const t of e.segments)":"===t[0]?n+=Math.pow(1,o):""!==t&&(n+=Math.pow(2,o)),o++;return n};class R{constructor(t){this.segments=t.slice()}next(){return this.segments.length>0?this.segments.shift():""}}const E=(t,n)=>n in t?t[n]:t.hasAttribute(n)?t.getAttribute(n):null,S=t=>Array.from(t.children).filter((t=>"ION-ROUTE-REDIRECT"===t.tagName)).map((t=>{const n=E(t,"to");return{from:l(E(t,"from")).segments,to:null==n?void 0:l(n)}})),C=t=>O(j(t)),j=t=>Array.from(t.children).filter((t=>"ION-ROUTE"===t.tagName&&t.component)).map((t=>{const n=E(t,"component");return{segments:l(E(t,"url")).segments,id:n.toLowerCase(),params:t.componentProps,beforeLeave:t.beforeLeave,beforeEnter:t.beforeEnter,children:j(t)}})),O=t=>{const n=[];for(const o of t)T([],n,o);return n},T=(t,n,o)=>{if(t=[...t,{id:o.id,segments:o.segments,params:o.params,beforeLeave:o.beforeLeave,beforeEnter:o.beforeEnter}],0!==o.children.length)for(const e of o.children)T(t,n,e);else n.push(t)},k=n(class extends o{constructor(t){super(),!1!==t&&this.__registerHost(),this.ionRouteWillChange=e(this,"ionRouteWillChange",7),this.ionRouteDidChange=e(this,"ionRouteDidChange",7),this.previousPath=null,this.busy=!1,this.state=0,this.lastState=0,this.root="/",this.useHash=!0}async componentWillLoad(){await(w(document.body)?Promise.resolve():new Promise((t=>{window.addEventListener("ionNavWillLoad",(()=>t()),{once:!0})})));const t=await this.runGuards(this.getSegments());if(!0!==t){if("object"==typeof t){const{redirect:n}=t,o=l(n);this.setSegments(o.segments,c,o.queryString),await this.writeNavStateRoot(o.segments,c)}}else await this.onRoutesChanged()}componentDidLoad(){window.addEventListener("ionRouteRedirectChanged",a(this.onRedirectChanged.bind(this),10)),window.addEventListener("ionRouteDataChanged",a(this.onRoutesChanged.bind(this),100))}async onPopState(){const t=this.historyDirection();let n=this.getSegments();const o=await this.runGuards(n);if(!0!==o){if("object"!=typeof o)return!1;n=l(o.redirect).segments}return this.writeNavStateRoot(n,t)}onBackButton(t){t.detail.register(0,(t=>{this.back(),t()}))}async canTransition(){const t=await this.runGuards();return!0===t||"object"==typeof t&&t.redirect}async push(t,n="forward",o){var e;if(t.startsWith(".")){const n=null!==(e=this.previousPath)&&void 0!==e?e:"/",o=new URL(t,"https://host/"+n);t=o.pathname+o.search}let r=l(t);const s=await this.runGuards(r.segments);if(!0!==s){if("object"!=typeof s)return!1;r=l(s.redirect)}return this.setSegments(r.segments,n,r.queryString),this.writeNavStateRoot(r.segments,n,o)}back(){return window.history.back(),Promise.resolve(this.waitPromise)}async printDebug(){(t=>{console.group(`[ion-core] ROUTES[${t.length}]`);for(const n of t){const t=[];n.forEach((n=>t.push(...n.segments)));const o=n.map((t=>t.id));console.debug("%c "+h(t),"font-weight: bold; padding-left: 20px","=>\t",`(${o.join(", ")})`)}console.groupEnd()})(C(this.el)),(t=>{console.group(`[ion-core] REDIRECTS[${t.length}]`);for(const n of t)n.to&&console.debug("FROM: ","$c "+h(n.from),"font-weight: bold"," TO: ","$c "+h(n.to.segments),"font-weight: bold");console.groupEnd()})(S(this.el))}async navChanged(t){if(this.busy)return r("[ion-router] - Router is busy, navChanged was cancelled."),!1;const{ids:n,outlet:o}=await(async()=>{const t=[];let n,o=window.document.body;for(;n=w(o);){const e=await n.getRouteId();if(!e)break;o=e.element,e.element=void 0,t.push(e)}return{ids:t,outlet:n}})(),e=((t,n)=>{let o=null,e=0;for(const r of n){const n=p(t,r);n>e&&(o=r,e=n)}return o?o.map(((n,o)=>{var e;return{id:n.id,segments:n.segments,params:v(n.params,null===(e=t[o])||void 0===e?void 0:e.params)}})):null})(n,C(this.el));if(!e)return r("[ion-router] - No matching URL for",n.map((t=>t.id))),!1;const s=(t=>{const n=[];for(const o of t)for(const t of o.segments)if(":"===t[0]){const e=o.params&&o.params[t.slice(1)];if(!e)return null;n.push(e)}else""!==t&&n.push(t);return n})(e);return s?(this.setSegments(s,t),await this.safeWriteNavState(o,e,c,s,null,n.length),!0):(r("[ion-router] - Router could not match path because some required param is missing."),!1)}onRedirectChanged(){const t=this.getSegments();t&&m(t,S(this.el))&&this.writeNavStateRoot(t,c)}onRoutesChanged(){return this.writeNavStateRoot(this.getSegments(),c)}historyDirection(){var t;const n=window;null===n.history.state&&(this.state++,n.history.replaceState(this.state,n.document.title,null===(t=n.document.location)||void 0===t?void 0:t.href));const o=n.history.state,e=this.lastState;return this.lastState=o,o>e||o>=e&&e>0?u:o<e?"back":c}async writeNavStateRoot(n,o,e){if(!n)return t("[ion-router] - URL is not part of the routing set."),!1;const r=S(this.el),s=m(n,r);let i=null;if(s){const{segments:t,queryString:e}=s.to;this.setSegments(t,o,e),i=s.from,n=t}const a=C(this.el),c=b(n,a);return c?this.safeWriteNavState(document.body,c,o,n,i,0,e):(t("[ion-router] - The path does not match any route."),!1)}async safeWriteNavState(n,o,e,r,s,i=0,a){const c=await this.lock();let u=!1;try{u=await this.writeNavState(n,o,e,r,s,i,a)}catch(n){t("[ion-router] - Exception in safeWriteNavState:",n)}return c(),u}async lock(){const t=this.waitPromise;let n;return this.waitPromise=new Promise((t=>n=t)),void 0!==t&&await t,n}async runGuards(t=this.getSegments(),n){if(void 0===n&&(n=l(this.previousPath).segments),!t||!n)return!0;const o=C(this.el),e=b(n,o),r=e&&e[e.length-1].beforeLeave,s=!r||await r();if(!1===s||"object"==typeof s)return s;const i=b(t,o),a=i&&i[i.length-1].beforeEnter;return!a||a()}async writeNavState(t,n,o,e,s,i=0,a){if(this.busy)return r("[ion-router] - Router is busy, transition was cancelled."),!1;this.busy=!0;const c=this.routeChangeEvent(e,s);c&&this.ionRouteWillChange.emit(c);const u=await f(t,n,o,i,!1,a);return this.busy=!1,c&&this.ionRouteDidChange.emit(c),u}setSegments(t,n,o){this.state++,((t,n,o,e,r,s,i)=>{const a=((t,n,o)=>{let e=h(t);return n&&(e="#"+e),void 0!==o&&(e+="?"+o),e})([...l(n).segments,...e],o,i);r===u?t.pushState(s,"",a):t.replaceState(s,"",a)})(window.history,this.root,this.useHash,t,n,this.state,o)}getSegments(){return((t,n,o)=>{const e=l(this.root).segments,r=o?t.hash.slice(1):t.pathname;return((t,n)=>{if(t.length>n.length)return null;if(t.length<=1&&""===t[0])return n;for(let o=0;o<t.length;o++)if(t[o]!==n[o])return null;return n.length===t.length?[""]:n.slice(t.length)})(e,l(r).segments)})(window.location,0,this.useHash)}routeChangeEvent(t,n){const o=this.previousPath,e=h(t);return this.previousPath=e,e===o?null:{from:o,redirectedFrom:n?h(n):null,to:e}}get el(){return this}},[0,"ion-router",{root:[1],useHash:[4,"use-hash"],canTransition:[64],push:[64],back:[64],printDebug:[64],navChanged:[64]},[[8,"popstate","onPopState"],[4,"ionBackButton","onBackButton"]]]),D=k,L=function(){"undefined"!=typeof customElements&&["ion-router"].forEach((t=>{"ion-router"===t&&(customElements.get(s(t))||customElements.define(s(t),k))}))};export{D as IonRouter,L as defineCustomElement}
|
|
4
|
+
import{j as t,p as n,H as o,e,f as r,t as i}from"./p-BJoMtgfR.js";import{c as s,r as a,p as c}from"./p-DgbT0exM.js";import{a as u,i as l,g as h}from"./p-C59ryAuS.js";const f="root",d="forward",w=t=>"/"+t.filter((t=>t.length>0)).join("/"),m=t=>{let n,o,e=[""];if(null!=t){const r=t.indexOf("#");r>-1&&(o=t.substring(r+1),t=t.substring(0,r));const i=t.indexOf("?");i>-1&&(n=t.substring(i+1),t=t.substring(0,i)),e=t.split("/").map((t=>t.trim())).filter((t=>t.length>0)),0===e.length&&(e=[""])}return{segments:e,queryString:n,fragment:o}},p=async(n,o,e,r,i=!1,a)=>{try{const t=b(n);if(r>=o.length||!t)return i;await new Promise((n=>s(t,n)));const c=o[r],u=await t.setRouteId(c.id,c.params,e,a);return u.changed&&(e=f,i=!0),i=await p(u.element,o,e,r+1,i,a),u.markVisible&&await u.markVisible(),i}catch(n){return t("[ion-router] - Exception in writeNavState:",n),!1}},g=()=>new Promise((t=>a((()=>t())))),y=t=>{const n=t.closest(".ion-page");return null===n?null===document.querySelector(".ion-page"):null===n.closest(".ion-page-hidden, .tab-hidden")},v=":not([no-router]) ion-nav, :not([no-router]) ion-tabs, :not([no-router]) ion-router-outlet",b=t=>{if(!t)return;if(t.matches(v))return t;const n=t.querySelector(v);return null!=n?n:void 0},R=(t,n)=>n.find((n=>((t,n)=>{const{from:o,to:e}=n;if(void 0===e)return!1;if(o.length>t.length)return!1;for(let n=0;n<o.length;n++){const e=o[n];if("*"===e)return!0;if(e!==t[n])return!1}return o.length===t.length})(t,n))),S=(t,n)=>{const o=Math.min(t.length,n.length);let e=0;for(let r=0;r<o;r++){const o=t[r],i=n[r];if(o.id.toLowerCase()!==i.id)break;if(o.params){const t=Object.keys(o.params);if(t.length===i.segments.length){const n=t.map((t=>":"+t));for(let t=0;t<n.length&&n[t].toLowerCase()===i.segments[t];t++)e++}}e++}return e},C=(t,n)=>{const o=new O(t);let e,r=!1;for(let t=0;t<n.length;t++){const i=n[t].segments;if(""===i[0])r=!0;else{for(const n of i){const r=o.next();if(":"===n[0]){if(""===r)return null;e=e||[],(e[t]||(e[t]={}))[n.slice(1)]=r}else if(r!==n)return null}r=!1}}return r&&r!==(""===o.next())?null:e?n.map(((t,n)=>({id:t.id,segments:t.segments,params:E(t.params,e[n]),beforeEnter:t.beforeEnter,beforeLeave:t.beforeLeave}))):n},E=(t,n)=>t||n?Object.assign(Object.assign({},t),n):void 0,j=(t,n)=>{let o=null,e=0;for(const r of n){const n=C(t,r);if(null!==n){const t=T(n);t>e&&(e=t,o=n)}}return o},T=t=>{let n=1,o=1;for(const e of t)for(const t of e.segments)":"===t[0]?n+=Math.pow(1,o):""!==t&&(n+=Math.pow(2,o)),o++;return n};class O{constructor(t){this.segments=t.slice()}next(){return this.segments.length>0?this.segments.shift():""}}const k=(t,n)=>n in t?t[n]:t.hasAttribute(n)?t.getAttribute(n):null,D=t=>Array.from(t.children).filter((t=>"ION-ROUTE-REDIRECT"===t.tagName)).map((t=>{const n=k(t,"to");return{from:m(k(t,"from")).segments,to:null==n?void 0:m(n)}})),L=t=>x(N(t)),N=t=>Array.from(t.children).filter((t=>"ION-ROUTE"===t.tagName&&t.component)).map((t=>{const n=k(t,"component");return{segments:m(k(t,"url")).segments,id:n.toLowerCase(),params:t.componentProps,beforeLeave:t.beforeLeave,beforeEnter:t.beforeEnter,children:N(t)}})),x=t=>{const n=[];for(const o of t)P([],n,o);return n},P=(t,n,o)=>{if(t=[...t,{id:o.id,segments:o.segments,params:o.params,beforeLeave:o.beforeLeave,beforeEnter:o.beforeEnter}],0!==o.children.length)for(const e of o.children)P(t,n,e);else n.push(t)},B=n(class extends o{constructor(t){super(),!1!==t&&this.__registerHost(),this.ionRouteWillChange=e(this,"ionRouteWillChange",7),this.ionRouteDidChange=e(this,"ionRouteDidChange",7),this.previousPath=null,this.busy=!1,this.state=0,this.lastState=0,this.fragmentScrollToken=0,this.root="/",this.useHash=!0}async componentWillLoad(){await(b(document.body)?Promise.resolve():new Promise((t=>{window.addEventListener("ionNavWillLoad",(()=>t()),{once:!0})})));const t=await this.runGuards(this.getSegments());if(!0===t)await this.onRoutesChanged()&&this.maybeScrollToFragment();else if("object"==typeof t){const{redirect:n}=t,o=m(n);this.setSegments(o.segments,f,o.queryString,o.fragment),await this.writeNavStateRoot(o.segments,f)&&this.maybeScrollToFragment()}}componentDidLoad(){window.addEventListener("ionRouteRedirectChanged",c(this.onRedirectChanged.bind(this),10)),window.addEventListener("ionRouteDataChanged",c(this.onRoutesChanged.bind(this),100))}async onPopState(){const t=this.historyDirection();let n=this.getSegments();const o=await this.runGuards(n);if(!0!==o){if("object"!=typeof o)return!1;n=m(o.redirect).segments}const e=await this.writeNavStateRoot(n,t);return e&&this.maybeScrollToFragment(),e}onBackButton(t){t.detail.register(0,(t=>{this.back(),t()}))}async canTransition(){const t=await this.runGuards();return!0===t||"object"==typeof t&&t.redirect}async push(t,n="forward",o){var e;if(t.startsWith(".")){const n=null!==(e=this.previousPath)&&void 0!==e?e:"/",o=new URL(t,"https://host/"+n);t=o.pathname+o.search+o.hash}let r=m(t);const i=await this.runGuards(r.segments);if(!0!==i){if("object"!=typeof i)return!1;r=m(i.redirect)}this.setSegments(r.segments,n,r.queryString,r.fragment);const s=await this.writeNavStateRoot(r.segments,n,o);return s&&this.maybeScrollToFragment(),s}back(){return window.history.back(),Promise.resolve(this.waitPromise)}async printDebug(){(t=>{console.group(`[ion-core] ROUTES[${t.length}]`);for(const n of t){const t=[];n.forEach((n=>t.push(...n.segments)));const o=n.map((t=>t.id));console.debug("%c "+w(t),"font-weight: bold; padding-left: 20px","=>\t",`(${o.join(", ")})`)}console.groupEnd()})(L(this.el)),(t=>{console.group(`[ion-core] REDIRECTS[${t.length}]`);for(const n of t)n.to&&console.debug("FROM: ","$c "+w(n.from),"font-weight: bold"," TO: ","$c "+w(n.to.segments),"font-weight: bold");console.groupEnd()})(D(this.el))}async navChanged(t){if(this.busy)return r("[ion-router] - Router is busy, navChanged was cancelled."),!1;const{ids:n,outlet:o}=await(async()=>{const t=[];let n,o=window.document.body;for(;n=b(o);){const e=await n.getRouteId();if(!e)break;o=e.element,e.element=void 0,t.push(e)}return{ids:t,outlet:n}})(),e=((t,n)=>{let o=null,e=0;for(const r of n){const n=S(t,r);n>e&&(o=r,e=n)}return o?o.map(((n,o)=>{var e;return{id:n.id,segments:n.segments,params:E(n.params,null===(e=t[o])||void 0===e?void 0:e.params)}})):null})(n,L(this.el));if(!e)return r("[ion-router] - No matching URL for",n.map((t=>t.id))),!1;const i=(t=>{const n=[];for(const o of t)for(const t of o.segments)if(":"===t[0]){const e=o.params&&o.params[t.slice(1)];if(!e)return null;n.push(e)}else""!==t&&n.push(t);return n})(e);if(!i)return r("[ion-router] - Router could not match path because some required param is missing."),!1;const s=w(i)===this.previousPath?this.getFragment():void 0;return this.setSegments(i,t,void 0,s),await this.safeWriteNavState(o,e,f,i,null,n.length),!0}onRedirectChanged(){const t=this.getSegments();t&&R(t,D(this.el))&&this.writeNavStateRoot(t,f)}onRoutesChanged(){return this.writeNavStateRoot(this.getSegments(),f)}historyDirection(){var t;const n=window;null===n.history.state&&(this.state++,n.history.replaceState(this.state,n.document.title,null===(t=n.document.location)||void 0===t?void 0:t.href));const o=n.history.state,e=this.lastState;return this.lastState=o,o>e||o>=e&&e>0?d:o<e?"back":f}async writeNavStateRoot(n,o,e){if(!n)return t("[ion-router] - URL is not part of the routing set."),!1;const r=D(this.el),i=R(n,r);let s=null;if(i){const{segments:t,queryString:e,fragment:r}=i.to;this.setSegments(t,o,e,r),s=i.from,n=t}const a=L(this.el),c=j(n,a);return c?this.safeWriteNavState(document.body,c,o,n,s,0,e):(t("[ion-router] - The path does not match any route."),!1)}async safeWriteNavState(n,o,e,r,i,s=0,a){const c=await this.lock();let u=!1;try{u=await this.writeNavState(n,o,e,r,i,s,a)}catch(n){t("[ion-router] - Exception in safeWriteNavState:",n)}return c(),u}async lock(){const t=this.waitPromise;let n;return this.waitPromise=new Promise((t=>n=t)),void 0!==t&&await t,n}async runGuards(t=this.getSegments(),n){if(void 0===n&&(n=m(this.previousPath).segments),!t||!n)return!0;const o=L(this.el),e=j(n,o),r=e&&e[e.length-1].beforeLeave,i=!r||await r();if(!1===i||"object"==typeof i)return i;const s=j(t,o),a=s&&s[s.length-1].beforeEnter;return!a||a()}async writeNavState(t,n,o,e,i,s=0,a){if(this.busy)return r("[ion-router] - Router is busy, transition was cancelled."),!1;this.busy=!0;const c=this.routeChangeEvent(e,i);c&&this.ionRouteWillChange.emit(c);const u=await p(t,n,o,s,!1,a);return this.busy=!1,c&&this.ionRouteDidChange.emit(c),u}setSegments(t,n,o,e){this.state++,this.fragmentScrollToken++,((t,n,o,e,r,i,s,a)=>{const c=((t,n,o,e)=>{let r=w(t);return n&&(r="#"+r),void 0!==o&&(r+="?"+o),void 0!==e&&(r+="#"+e),r})([...m(n).segments,...e],o,s,a);r===d?t.pushState(i,"",c):t.replaceState(i,"",c)})(window.history,this.root,this.useHash,t,n,this.state,o,e)}getSegments(){return((t,n,o)=>{const e=m(this.root).segments,r=o?t.hash.slice(1):t.pathname;return((t,n)=>{if(t.length>n.length)return null;if(t.length<=1&&""===t[0])return n;for(let o=0;o<t.length;o++)if(t[o]!==n[o])return null;return n.length===t.length?[""]:n.slice(t.length)})(e,m(r).segments)})(window.location,0,this.useHash)}getFragment(){return(this.useHash?m(window.location.hash.slice(1)).fragment:window.location.hash.slice(1))||void 0}maybeScrollToFragment(){const n=this.getFragment();if(!n)return;const o=this.fragmentScrollToken;(async(n,o=()=>!0)=>{if(null==n||""===n)return!1;let e;try{e=decodeURIComponent(n)}catch(t){e=n}const r=await(async(t,n)=>{const o="undefined"!=typeof CSS&&"function"==typeof CSS.escape?CSS.escape(t):null;for(let e=0;e<30;e++){if(!n())return null;let e=[];if(null!==o)try{e=[...document.querySelectorAll(`#${o}, a[name="${o}"]`)]}catch(t){e=[...document.querySelectorAll("#"+o)]}else{const n=document.getElementById(t);null!==n&&(e=[n])}for(let t=e.length-1;t>=0;t--)if(y(e[t]))return e[t];await g()}return null})(e,o);if(!r||!o())return!1;try{const t=u(r);if(t&&l(t)){const n=t,e=await h(n);if(await g(),!o())return!1;const i=r.getBoundingClientRect(),s=e.getBoundingClientRect(),a=i.top-s.top+e.scrollTop;await n.scrollToPoint(e.scrollLeft,a,300)}else r.scrollIntoView({behavior:"smooth"});return!0}catch(n){return t("[ion-router] - Exception in scrollToFragment:",n),!1}})(n,(()=>o===this.fragmentScrollToken)).catch((()=>{}))}routeChangeEvent(t,n){const o=this.previousPath,e=w(t);return this.previousPath=e,e===o?null:{from:o,redirectedFrom:n?w(n):null,to:e}}get el(){return this}},[0,"ion-router",{root:[1],useHash:[4,"use-hash"],canTransition:[64],push:[64],back:[64],printDebug:[64],navChanged:[64]},[[8,"popstate","onPopState"],[4,"ionBackButton","onBackButton"]]]),U=B,$=function(){"undefined"!=typeof customElements&&["ion-router"].forEach((t=>{"ion-router"===t&&(customElements.get(i(t))||customElements.define(i(t),B))}))};export{U as IonRouter,$ as defineCustomElement}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
var index = require('./index-CqT-2gKy.js');
|
|
7
7
|
var helpers = require('./helpers-CxTYJdbT.js');
|
|
8
|
+
var index$1 = require('./index-MbaBbWXk.js');
|
|
8
9
|
var theme = require('./theme-CeDs6Hcv.js');
|
|
9
10
|
var ionicGlobal = require('./ionic-global-Bc3kJi1Z.js');
|
|
10
11
|
|
|
@@ -86,7 +87,7 @@ const generatePath = (segments) => {
|
|
|
86
87
|
const path = segments.filter((s) => s.length > 0).join('/');
|
|
87
88
|
return '/' + path;
|
|
88
89
|
};
|
|
89
|
-
const generateUrl = (segments, useHash, queryString) => {
|
|
90
|
+
const generateUrl = (segments, useHash, queryString, fragment) => {
|
|
90
91
|
let url = generatePath(segments);
|
|
91
92
|
if (useHash) {
|
|
92
93
|
url = '#' + url;
|
|
@@ -94,10 +95,13 @@ const generateUrl = (segments, useHash, queryString) => {
|
|
|
94
95
|
if (queryString !== undefined) {
|
|
95
96
|
url += '?' + queryString;
|
|
96
97
|
}
|
|
98
|
+
if (fragment !== undefined) {
|
|
99
|
+
url += '#' + fragment;
|
|
100
|
+
}
|
|
97
101
|
return url;
|
|
98
102
|
};
|
|
99
|
-
const writeSegments = (history, root, useHash, segments, direction, state, queryString) => {
|
|
100
|
-
const url = generateUrl([...parsePath(root).segments, ...segments], useHash, queryString);
|
|
103
|
+
const writeSegments = (history, root, useHash, segments, direction, state, queryString, fragment) => {
|
|
104
|
+
const url = generateUrl([...parsePath(root).segments, ...segments], useHash, queryString, fragment);
|
|
101
105
|
if (direction === ROUTER_INTENT_FORWARD) {
|
|
102
106
|
history.pushState(state, '', url);
|
|
103
107
|
}
|
|
@@ -164,12 +168,21 @@ const readSegments = (loc, root, useHash) => {
|
|
|
164
168
|
/**
|
|
165
169
|
* Parses the path to:
|
|
166
170
|
* - segments an array of '/' separated parts,
|
|
167
|
-
* - queryString (undefined when no query string)
|
|
171
|
+
* - queryString (undefined when no query string),
|
|
172
|
+
* - fragment (undefined when no `#`).
|
|
168
173
|
*/
|
|
169
174
|
const parsePath = (path) => {
|
|
170
175
|
let segments = [''];
|
|
171
176
|
let queryString;
|
|
177
|
+
let fragment;
|
|
172
178
|
if (path != null) {
|
|
179
|
+
// The fragment ("#") starts a section that runs to the end of the URL.
|
|
180
|
+
// Anything inside it (including "?") is part of the fragment per RFC 3986.
|
|
181
|
+
const fragStart = path.indexOf('#');
|
|
182
|
+
if (fragStart > -1) {
|
|
183
|
+
fragment = path.substring(fragStart + 1);
|
|
184
|
+
path = path.substring(0, fragStart);
|
|
185
|
+
}
|
|
173
186
|
const qsStart = path.indexOf('?');
|
|
174
187
|
if (qsStart > -1) {
|
|
175
188
|
queryString = path.substring(qsStart + 1);
|
|
@@ -183,7 +196,7 @@ const parsePath = (path) => {
|
|
|
183
196
|
segments = [''];
|
|
184
197
|
}
|
|
185
198
|
}
|
|
186
|
-
return { segments, queryString };
|
|
199
|
+
return { segments, queryString, fragment };
|
|
187
200
|
};
|
|
188
201
|
|
|
189
202
|
const printRoutes = (routes) => {
|
|
@@ -268,6 +281,118 @@ const readNavState = async (root) => {
|
|
|
268
281
|
}
|
|
269
282
|
return { ids, outlet };
|
|
270
283
|
};
|
|
284
|
+
/** Max animation frames `scrollToFragment` polls while waiting for the target to mount. */
|
|
285
|
+
const FRAGMENT_POLL_FRAMES = 30;
|
|
286
|
+
/** Duration (ms) of the smooth-scroll animation that lands on the fragment target. */
|
|
287
|
+
const FRAGMENT_SCROLL_DURATION = 300;
|
|
288
|
+
const nextFrame = () => new Promise((resolve) => helpers.raf(() => resolve()));
|
|
289
|
+
/**
|
|
290
|
+
* Returns true when `el` lives inside an active `.ion-page`. `ion-page-hidden`
|
|
291
|
+
* marks nav back-stack entries; `tab-hidden` marks inactive `ion-tab` elements.
|
|
292
|
+
* Either class on the page's ancestor chain disqualifies it. When no `.ion-page`
|
|
293
|
+
* exists in the document at all (non-router pages), the candidate is accepted
|
|
294
|
+
* so plain anchors still work.
|
|
295
|
+
*/
|
|
296
|
+
const isInActivePage = (el) => {
|
|
297
|
+
const page = el.closest('.ion-page');
|
|
298
|
+
if (page === null) {
|
|
299
|
+
return document.querySelector('.ion-page') === null;
|
|
300
|
+
}
|
|
301
|
+
return page.closest('.ion-page-hidden, .tab-hidden') === null;
|
|
302
|
+
};
|
|
303
|
+
/**
|
|
304
|
+
* Polls across animation frames for an element matching `fragment` that lives
|
|
305
|
+
* in the active page. Scoping by "last `.ion-page:not(.ion-page-hidden)`" is
|
|
306
|
+
* unreliable: inactive `ion-tab` siblings carry `.ion-page` (gated by
|
|
307
|
+
* `.tab-hidden`, not `.ion-page-hidden`) and can be ordered after the leaf.
|
|
308
|
+
* Instead, locate candidates globally and walk them from last to first,
|
|
309
|
+
* accepting the deepest one whose `.ion-page` ancestor is not hidden. The
|
|
310
|
+
* last-to-first order preserves leaf-most preference for nested outlets.
|
|
311
|
+
*/
|
|
312
|
+
const findFragmentTarget = async (fragment, shouldContinue) => {
|
|
313
|
+
// CSS.escape is unavailable on very old WebViews; the fallback path uses
|
|
314
|
+
// `getElementById` and drops the legacy `<a name>` branch.
|
|
315
|
+
const canEscape = typeof CSS !== 'undefined' && typeof CSS.escape === 'function';
|
|
316
|
+
const escaped = canEscape ? CSS.escape(fragment) : null;
|
|
317
|
+
for (let i = 0; i < FRAGMENT_POLL_FRAMES; i++) {
|
|
318
|
+
if (!shouldContinue())
|
|
319
|
+
return null;
|
|
320
|
+
let candidates = [];
|
|
321
|
+
if (escaped !== null) {
|
|
322
|
+
try {
|
|
323
|
+
candidates = [...document.querySelectorAll(`#${escaped}, a[name="${escaped}"]`)];
|
|
324
|
+
}
|
|
325
|
+
catch (_a) {
|
|
326
|
+
candidates = [...document.querySelectorAll(`#${escaped}`)];
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
const byId = document.getElementById(fragment);
|
|
331
|
+
if (byId !== null)
|
|
332
|
+
candidates = [byId];
|
|
333
|
+
}
|
|
334
|
+
for (let j = candidates.length - 1; j >= 0; j--) {
|
|
335
|
+
if (isInActivePage(candidates[j])) {
|
|
336
|
+
return candidates[j];
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
await nextFrame();
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
};
|
|
343
|
+
/**
|
|
344
|
+
* Scrolls to the element whose id matches `fragment`, falling back to a legacy
|
|
345
|
+
* `<a name="...">` target. When the target lives inside an `ion-content`, the
|
|
346
|
+
* scroll uses its smooth-animated scroll API; otherwise it falls back to
|
|
347
|
+
* `Element.scrollIntoView`.
|
|
348
|
+
*
|
|
349
|
+
* `shouldContinue` lets callers cancel in-flight scrolls when a newer
|
|
350
|
+
* navigation supersedes this one. It is checked between async steps.
|
|
351
|
+
*/
|
|
352
|
+
const scrollToFragment = async (fragment, shouldContinue = () => true) => {
|
|
353
|
+
if (fragment == null || fragment === '') {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
// URL fragments are percent-encoded but element ids are not; decode for
|
|
357
|
+
// matching per the HTML spec's indicated-element resolution.
|
|
358
|
+
let decoded;
|
|
359
|
+
try {
|
|
360
|
+
decoded = decodeURIComponent(fragment);
|
|
361
|
+
}
|
|
362
|
+
catch (_a) {
|
|
363
|
+
decoded = fragment;
|
|
364
|
+
}
|
|
365
|
+
const target = await findFragmentTarget(decoded, shouldContinue);
|
|
366
|
+
if (!target || !shouldContinue()) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
// Best-effort scroll: swallow exceptions if the page tears down mid-animation.
|
|
370
|
+
try {
|
|
371
|
+
const contentHost = index$1.findClosestIonContent(target);
|
|
372
|
+
if (contentHost && index$1.isIonContent(contentHost)) {
|
|
373
|
+
const content = contentHost;
|
|
374
|
+
const scrollEl = await index$1.getScrollElement(content);
|
|
375
|
+
// Yield one frame so the newly mounted target's layout is stable
|
|
376
|
+
// before we measure its rect.
|
|
377
|
+
await nextFrame();
|
|
378
|
+
if (!shouldContinue())
|
|
379
|
+
return false;
|
|
380
|
+
const targetRect = target.getBoundingClientRect();
|
|
381
|
+
const scrollRect = scrollEl.getBoundingClientRect();
|
|
382
|
+
const top = targetRect.top - scrollRect.top + scrollEl.scrollTop;
|
|
383
|
+
// Preserve scrollLeft so RTL and horizontally-scrolling pages aren't reset.
|
|
384
|
+
await content.scrollToPoint(scrollEl.scrollLeft, top, FRAGMENT_SCROLL_DURATION);
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
target.scrollIntoView({ behavior: 'smooth' });
|
|
388
|
+
}
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
catch (e) {
|
|
392
|
+
index.printIonError('[ion-router] - Exception in scrollToFragment:', e);
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
};
|
|
271
396
|
const waitUntilNavNode = () => {
|
|
272
397
|
if (searchNavNode(document.body)) {
|
|
273
398
|
return Promise.resolve();
|
|
@@ -609,6 +734,7 @@ const Router = class {
|
|
|
609
734
|
this.busy = false;
|
|
610
735
|
this.state = 0;
|
|
611
736
|
this.lastState = 0;
|
|
737
|
+
this.fragmentScrollToken = 0;
|
|
612
738
|
/**
|
|
613
739
|
* The root path to use when matching URLs. By default, this is set to "/", but you can specify
|
|
614
740
|
* an alternate prefix for all URL paths.
|
|
@@ -637,12 +763,17 @@ const Router = class {
|
|
|
637
763
|
if (typeof canProceed === 'object') {
|
|
638
764
|
const { redirect } = canProceed;
|
|
639
765
|
const path = parsePath(redirect);
|
|
640
|
-
this.setSegments(path.segments, ROUTER_INTENT_NONE, path.queryString);
|
|
641
|
-
await this.writeNavStateRoot(path.segments, ROUTER_INTENT_NONE);
|
|
766
|
+
this.setSegments(path.segments, ROUTER_INTENT_NONE, path.queryString, path.fragment);
|
|
767
|
+
const result = await this.writeNavStateRoot(path.segments, ROUTER_INTENT_NONE);
|
|
768
|
+
if (result) {
|
|
769
|
+
this.maybeScrollToFragment();
|
|
770
|
+
}
|
|
642
771
|
}
|
|
772
|
+
return;
|
|
643
773
|
}
|
|
644
|
-
|
|
645
|
-
|
|
774
|
+
const result = await this.onRoutesChanged();
|
|
775
|
+
if (result) {
|
|
776
|
+
this.maybeScrollToFragment();
|
|
646
777
|
}
|
|
647
778
|
}
|
|
648
779
|
componentDidLoad() {
|
|
@@ -661,7 +792,11 @@ const Router = class {
|
|
|
661
792
|
return false;
|
|
662
793
|
}
|
|
663
794
|
}
|
|
664
|
-
|
|
795
|
+
const result = await this.writeNavStateRoot(segments, direction);
|
|
796
|
+
if (result) {
|
|
797
|
+
this.maybeScrollToFragment();
|
|
798
|
+
}
|
|
799
|
+
return result;
|
|
665
800
|
}
|
|
666
801
|
onBackButton(ev) {
|
|
667
802
|
ev.detail.register(0, (processNextHandler) => {
|
|
@@ -695,7 +830,7 @@ const Router = class {
|
|
|
695
830
|
const currentPath = (_a = this.previousPath) !== null && _a !== void 0 ? _a : '/';
|
|
696
831
|
// Convert currentPath to an URL by pre-pending a protocol and a host to resolve the relative path.
|
|
697
832
|
const url = new URL(path, `https://host/${currentPath}`);
|
|
698
|
-
path = url.pathname + url.search;
|
|
833
|
+
path = url.pathname + url.search + url.hash;
|
|
699
834
|
}
|
|
700
835
|
let parsedPath = parsePath(path);
|
|
701
836
|
const canProceed = await this.runGuards(parsedPath.segments);
|
|
@@ -707,8 +842,12 @@ const Router = class {
|
|
|
707
842
|
return false;
|
|
708
843
|
}
|
|
709
844
|
}
|
|
710
|
-
this.setSegments(parsedPath.segments, direction, parsedPath.queryString);
|
|
711
|
-
|
|
845
|
+
this.setSegments(parsedPath.segments, direction, parsedPath.queryString, parsedPath.fragment);
|
|
846
|
+
const result = await this.writeNavStateRoot(parsedPath.segments, direction, animation);
|
|
847
|
+
if (result) {
|
|
848
|
+
this.maybeScrollToFragment();
|
|
849
|
+
}
|
|
850
|
+
return result;
|
|
712
851
|
}
|
|
713
852
|
/** Go back to previous page in the window.history. */
|
|
714
853
|
back() {
|
|
@@ -738,7 +877,12 @@ const Router = class {
|
|
|
738
877
|
index.printIonWarning('[ion-router] - Router could not match path because some required param is missing.');
|
|
739
878
|
return false;
|
|
740
879
|
}
|
|
741
|
-
|
|
880
|
+
// navChanged is an outlet-driven URL sync. Only keep the fragment when
|
|
881
|
+
// the path is unchanged; on a real navigation it refers to an anchor on
|
|
882
|
+
// the page being left and would be stale.
|
|
883
|
+
const newPath = generatePath(segments);
|
|
884
|
+
const fragment = newPath === this.previousPath ? this.getFragment() : undefined;
|
|
885
|
+
this.setSegments(segments, direction, undefined, fragment);
|
|
742
886
|
await this.safeWriteNavState(outlet, chain, ROUTER_INTENT_NONE, segments, null, ids.length);
|
|
743
887
|
return true;
|
|
744
888
|
}
|
|
@@ -781,8 +925,8 @@ const Router = class {
|
|
|
781
925
|
const redirect = findRouteRedirect(segments, redirects);
|
|
782
926
|
let redirectFrom = null;
|
|
783
927
|
if (redirect) {
|
|
784
|
-
const { segments: toSegments, queryString } = redirect.to;
|
|
785
|
-
this.setSegments(toSegments, direction, queryString);
|
|
928
|
+
const { segments: toSegments, queryString, fragment } = redirect.to;
|
|
929
|
+
this.setSegments(toSegments, direction, queryString, fragment);
|
|
786
930
|
redirectFrom = redirect.from;
|
|
787
931
|
segments = toSegments;
|
|
788
932
|
}
|
|
@@ -862,13 +1006,35 @@ const Router = class {
|
|
|
862
1006
|
}
|
|
863
1007
|
return changed;
|
|
864
1008
|
}
|
|
865
|
-
setSegments(segments, direction, queryString) {
|
|
1009
|
+
setSegments(segments, direction, queryString, fragment) {
|
|
866
1010
|
this.state++;
|
|
867
|
-
|
|
1011
|
+
// Every URL write invalidates any in-flight fragment scroll: a newer nav
|
|
1012
|
+
// (with or without a fragment, successful or not) should always supersede.
|
|
1013
|
+
this.fragmentScrollToken++;
|
|
1014
|
+
writeSegments(window.history, this.root, this.useHash, segments, direction, this.state, queryString, fragment);
|
|
868
1015
|
}
|
|
869
1016
|
getSegments() {
|
|
870
1017
|
return readSegments(window.location, this.root, this.useHash);
|
|
871
1018
|
}
|
|
1019
|
+
getFragment() {
|
|
1020
|
+
// In hash mode the URL fragment trails a second `#` (e.g. `#/path#anchor`);
|
|
1021
|
+
// parse the routing portion to extract it.
|
|
1022
|
+
const raw = this.useHash ? parsePath(window.location.hash.slice(1)).fragment : window.location.hash.slice(1);
|
|
1023
|
+
return raw ? raw : undefined;
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Fires a best-effort scroll to the current URL fragment. The scroll bails
|
|
1027
|
+
* if a newer `setSegments` advances `fragmentScrollToken` mid-flight.
|
|
1028
|
+
*/
|
|
1029
|
+
maybeScrollToFragment() {
|
|
1030
|
+
const fragment = this.getFragment();
|
|
1031
|
+
if (!fragment)
|
|
1032
|
+
return;
|
|
1033
|
+
const token = this.fragmentScrollToken;
|
|
1034
|
+
// Fire-and-forget; the returned promise resolves only after the scroll
|
|
1035
|
+
// animation completes, which the caller does not need to await.
|
|
1036
|
+
scrollToFragment(fragment, () => token === this.fragmentScrollToken).catch(() => { });
|
|
1037
|
+
}
|
|
872
1038
|
routeChangeEvent(toSegments, redirectFromSegments) {
|
|
873
1039
|
const from = this.previousPath;
|
|
874
1040
|
const to = generatePath(toSegments);
|
|
@@ -2,7 +2,7 @@ import { debounce } from "../../utils/helpers";
|
|
|
2
2
|
import { printIonError, printIonWarning } from "../../utils/logging/index";
|
|
3
3
|
import { ROUTER_INTENT_BACK, ROUTER_INTENT_FORWARD, ROUTER_INTENT_NONE } from "./utils/constants";
|
|
4
4
|
import { printRedirects, printRoutes } from "./utils/debug";
|
|
5
|
-
import { readNavState, waitUntilNavNode, writeNavState } from "./utils/dom";
|
|
5
|
+
import { readNavState, scrollToFragment, waitUntilNavNode, writeNavState } from "./utils/dom";
|
|
6
6
|
import { findChainForIDs, findChainForSegments, findRouteRedirect } from "./utils/matching";
|
|
7
7
|
import { readRedirects, readRoutes } from "./utils/parser";
|
|
8
8
|
import { chainToSegments, generatePath, parsePath, readSegments, writeSegments } from "./utils/path";
|
|
@@ -12,6 +12,7 @@ export class Router {
|
|
|
12
12
|
this.busy = false;
|
|
13
13
|
this.state = 0;
|
|
14
14
|
this.lastState = 0;
|
|
15
|
+
this.fragmentScrollToken = 0;
|
|
15
16
|
/**
|
|
16
17
|
* The root path to use when matching URLs. By default, this is set to "/", but you can specify
|
|
17
18
|
* an alternate prefix for all URL paths.
|
|
@@ -40,12 +41,17 @@ export class Router {
|
|
|
40
41
|
if (typeof canProceed === 'object') {
|
|
41
42
|
const { redirect } = canProceed;
|
|
42
43
|
const path = parsePath(redirect);
|
|
43
|
-
this.setSegments(path.segments, ROUTER_INTENT_NONE, path.queryString);
|
|
44
|
-
await this.writeNavStateRoot(path.segments, ROUTER_INTENT_NONE);
|
|
44
|
+
this.setSegments(path.segments, ROUTER_INTENT_NONE, path.queryString, path.fragment);
|
|
45
|
+
const result = await this.writeNavStateRoot(path.segments, ROUTER_INTENT_NONE);
|
|
46
|
+
if (result) {
|
|
47
|
+
this.maybeScrollToFragment();
|
|
48
|
+
}
|
|
45
49
|
}
|
|
50
|
+
return;
|
|
46
51
|
}
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
const result = await this.onRoutesChanged();
|
|
53
|
+
if (result) {
|
|
54
|
+
this.maybeScrollToFragment();
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
57
|
componentDidLoad() {
|
|
@@ -64,7 +70,11 @@ export class Router {
|
|
|
64
70
|
return false;
|
|
65
71
|
}
|
|
66
72
|
}
|
|
67
|
-
|
|
73
|
+
const result = await this.writeNavStateRoot(segments, direction);
|
|
74
|
+
if (result) {
|
|
75
|
+
this.maybeScrollToFragment();
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
68
78
|
}
|
|
69
79
|
onBackButton(ev) {
|
|
70
80
|
ev.detail.register(0, (processNextHandler) => {
|
|
@@ -98,7 +108,7 @@ export class Router {
|
|
|
98
108
|
const currentPath = (_a = this.previousPath) !== null && _a !== void 0 ? _a : '/';
|
|
99
109
|
// Convert currentPath to an URL by pre-pending a protocol and a host to resolve the relative path.
|
|
100
110
|
const url = new URL(path, `https://host/${currentPath}`);
|
|
101
|
-
path = url.pathname + url.search;
|
|
111
|
+
path = url.pathname + url.search + url.hash;
|
|
102
112
|
}
|
|
103
113
|
let parsedPath = parsePath(path);
|
|
104
114
|
const canProceed = await this.runGuards(parsedPath.segments);
|
|
@@ -110,8 +120,12 @@ export class Router {
|
|
|
110
120
|
return false;
|
|
111
121
|
}
|
|
112
122
|
}
|
|
113
|
-
this.setSegments(parsedPath.segments, direction, parsedPath.queryString);
|
|
114
|
-
|
|
123
|
+
this.setSegments(parsedPath.segments, direction, parsedPath.queryString, parsedPath.fragment);
|
|
124
|
+
const result = await this.writeNavStateRoot(parsedPath.segments, direction, animation);
|
|
125
|
+
if (result) {
|
|
126
|
+
this.maybeScrollToFragment();
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
115
129
|
}
|
|
116
130
|
/** Go back to previous page in the window.history. */
|
|
117
131
|
back() {
|
|
@@ -141,7 +155,12 @@ export class Router {
|
|
|
141
155
|
printIonWarning('[ion-router] - Router could not match path because some required param is missing.');
|
|
142
156
|
return false;
|
|
143
157
|
}
|
|
144
|
-
|
|
158
|
+
// navChanged is an outlet-driven URL sync. Only keep the fragment when
|
|
159
|
+
// the path is unchanged; on a real navigation it refers to an anchor on
|
|
160
|
+
// the page being left and would be stale.
|
|
161
|
+
const newPath = generatePath(segments);
|
|
162
|
+
const fragment = newPath === this.previousPath ? this.getFragment() : undefined;
|
|
163
|
+
this.setSegments(segments, direction, undefined, fragment);
|
|
145
164
|
await this.safeWriteNavState(outlet, chain, ROUTER_INTENT_NONE, segments, null, ids.length);
|
|
146
165
|
return true;
|
|
147
166
|
}
|
|
@@ -184,8 +203,8 @@ export class Router {
|
|
|
184
203
|
const redirect = findRouteRedirect(segments, redirects);
|
|
185
204
|
let redirectFrom = null;
|
|
186
205
|
if (redirect) {
|
|
187
|
-
const { segments: toSegments, queryString } = redirect.to;
|
|
188
|
-
this.setSegments(toSegments, direction, queryString);
|
|
206
|
+
const { segments: toSegments, queryString, fragment } = redirect.to;
|
|
207
|
+
this.setSegments(toSegments, direction, queryString, fragment);
|
|
189
208
|
redirectFrom = redirect.from;
|
|
190
209
|
segments = toSegments;
|
|
191
210
|
}
|
|
@@ -265,13 +284,35 @@ export class Router {
|
|
|
265
284
|
}
|
|
266
285
|
return changed;
|
|
267
286
|
}
|
|
268
|
-
setSegments(segments, direction, queryString) {
|
|
287
|
+
setSegments(segments, direction, queryString, fragment) {
|
|
269
288
|
this.state++;
|
|
270
|
-
|
|
289
|
+
// Every URL write invalidates any in-flight fragment scroll: a newer nav
|
|
290
|
+
// (with or without a fragment, successful or not) should always supersede.
|
|
291
|
+
this.fragmentScrollToken++;
|
|
292
|
+
writeSegments(window.history, this.root, this.useHash, segments, direction, this.state, queryString, fragment);
|
|
271
293
|
}
|
|
272
294
|
getSegments() {
|
|
273
295
|
return readSegments(window.location, this.root, this.useHash);
|
|
274
296
|
}
|
|
297
|
+
getFragment() {
|
|
298
|
+
// In hash mode the URL fragment trails a second `#` (e.g. `#/path#anchor`);
|
|
299
|
+
// parse the routing portion to extract it.
|
|
300
|
+
const raw = this.useHash ? parsePath(window.location.hash.slice(1)).fragment : window.location.hash.slice(1);
|
|
301
|
+
return raw ? raw : undefined;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Fires a best-effort scroll to the current URL fragment. The scroll bails
|
|
305
|
+
* if a newer `setSegments` advances `fragmentScrollToken` mid-flight.
|
|
306
|
+
*/
|
|
307
|
+
maybeScrollToFragment() {
|
|
308
|
+
const fragment = this.getFragment();
|
|
309
|
+
if (!fragment)
|
|
310
|
+
return;
|
|
311
|
+
const token = this.fragmentScrollToken;
|
|
312
|
+
// Fire-and-forget; the returned promise resolves only after the scroll
|
|
313
|
+
// animation completes, which the caller does not need to await.
|
|
314
|
+
scrollToFragment(fragment, () => token === this.fragmentScrollToken).catch(() => { });
|
|
315
|
+
}
|
|
275
316
|
routeChangeEvent(toSegments, redirectFromSegments) {
|
|
276
317
|
const from = this.previousPath;
|
|
277
318
|
const to = generatePath(toSegments);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* (C) Ionic http://ionicframework.com - MIT License
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { findClosestIonContent, getScrollElement, isIonContent } from "../../../utils/content/index";
|
|
5
|
+
import { componentOnReady, raf } from "../../../utils/helpers";
|
|
5
6
|
import { printIonError } from "../../../utils/logging/index";
|
|
6
7
|
import { ROUTER_INTENT_NONE } from "./constants";
|
|
7
8
|
/**
|
|
@@ -66,6 +67,118 @@ export const readNavState = async (root) => {
|
|
|
66
67
|
}
|
|
67
68
|
return { ids, outlet };
|
|
68
69
|
};
|
|
70
|
+
/** Max animation frames `scrollToFragment` polls while waiting for the target to mount. */
|
|
71
|
+
const FRAGMENT_POLL_FRAMES = 30;
|
|
72
|
+
/** Duration (ms) of the smooth-scroll animation that lands on the fragment target. */
|
|
73
|
+
const FRAGMENT_SCROLL_DURATION = 300;
|
|
74
|
+
const nextFrame = () => new Promise((resolve) => raf(() => resolve()));
|
|
75
|
+
/**
|
|
76
|
+
* Returns true when `el` lives inside an active `.ion-page`. `ion-page-hidden`
|
|
77
|
+
* marks nav back-stack entries; `tab-hidden` marks inactive `ion-tab` elements.
|
|
78
|
+
* Either class on the page's ancestor chain disqualifies it. When no `.ion-page`
|
|
79
|
+
* exists in the document at all (non-router pages), the candidate is accepted
|
|
80
|
+
* so plain anchors still work.
|
|
81
|
+
*/
|
|
82
|
+
const isInActivePage = (el) => {
|
|
83
|
+
const page = el.closest('.ion-page');
|
|
84
|
+
if (page === null) {
|
|
85
|
+
return document.querySelector('.ion-page') === null;
|
|
86
|
+
}
|
|
87
|
+
return page.closest('.ion-page-hidden, .tab-hidden') === null;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Polls across animation frames for an element matching `fragment` that lives
|
|
91
|
+
* in the active page. Scoping by "last `.ion-page:not(.ion-page-hidden)`" is
|
|
92
|
+
* unreliable: inactive `ion-tab` siblings carry `.ion-page` (gated by
|
|
93
|
+
* `.tab-hidden`, not `.ion-page-hidden`) and can be ordered after the leaf.
|
|
94
|
+
* Instead, locate candidates globally and walk them from last to first,
|
|
95
|
+
* accepting the deepest one whose `.ion-page` ancestor is not hidden. The
|
|
96
|
+
* last-to-first order preserves leaf-most preference for nested outlets.
|
|
97
|
+
*/
|
|
98
|
+
const findFragmentTarget = async (fragment, shouldContinue) => {
|
|
99
|
+
// CSS.escape is unavailable on very old WebViews; the fallback path uses
|
|
100
|
+
// `getElementById` and drops the legacy `<a name>` branch.
|
|
101
|
+
const canEscape = typeof CSS !== 'undefined' && typeof CSS.escape === 'function';
|
|
102
|
+
const escaped = canEscape ? CSS.escape(fragment) : null;
|
|
103
|
+
for (let i = 0; i < FRAGMENT_POLL_FRAMES; i++) {
|
|
104
|
+
if (!shouldContinue())
|
|
105
|
+
return null;
|
|
106
|
+
let candidates = [];
|
|
107
|
+
if (escaped !== null) {
|
|
108
|
+
try {
|
|
109
|
+
candidates = [...document.querySelectorAll(`#${escaped}, a[name="${escaped}"]`)];
|
|
110
|
+
}
|
|
111
|
+
catch (_a) {
|
|
112
|
+
candidates = [...document.querySelectorAll(`#${escaped}`)];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const byId = document.getElementById(fragment);
|
|
117
|
+
if (byId !== null)
|
|
118
|
+
candidates = [byId];
|
|
119
|
+
}
|
|
120
|
+
for (let j = candidates.length - 1; j >= 0; j--) {
|
|
121
|
+
if (isInActivePage(candidates[j])) {
|
|
122
|
+
return candidates[j];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
await nextFrame();
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Scrolls to the element whose id matches `fragment`, falling back to a legacy
|
|
131
|
+
* `<a name="...">` target. When the target lives inside an `ion-content`, the
|
|
132
|
+
* scroll uses its smooth-animated scroll API; otherwise it falls back to
|
|
133
|
+
* `Element.scrollIntoView`.
|
|
134
|
+
*
|
|
135
|
+
* `shouldContinue` lets callers cancel in-flight scrolls when a newer
|
|
136
|
+
* navigation supersedes this one. It is checked between async steps.
|
|
137
|
+
*/
|
|
138
|
+
export const scrollToFragment = async (fragment, shouldContinue = () => true) => {
|
|
139
|
+
if (fragment == null || fragment === '') {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
// URL fragments are percent-encoded but element ids are not; decode for
|
|
143
|
+
// matching per the HTML spec's indicated-element resolution.
|
|
144
|
+
let decoded;
|
|
145
|
+
try {
|
|
146
|
+
decoded = decodeURIComponent(fragment);
|
|
147
|
+
}
|
|
148
|
+
catch (_a) {
|
|
149
|
+
decoded = fragment;
|
|
150
|
+
}
|
|
151
|
+
const target = await findFragmentTarget(decoded, shouldContinue);
|
|
152
|
+
if (!target || !shouldContinue()) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
// Best-effort scroll: swallow exceptions if the page tears down mid-animation.
|
|
156
|
+
try {
|
|
157
|
+
const contentHost = findClosestIonContent(target);
|
|
158
|
+
if (contentHost && isIonContent(contentHost)) {
|
|
159
|
+
const content = contentHost;
|
|
160
|
+
const scrollEl = await getScrollElement(content);
|
|
161
|
+
// Yield one frame so the newly mounted target's layout is stable
|
|
162
|
+
// before we measure its rect.
|
|
163
|
+
await nextFrame();
|
|
164
|
+
if (!shouldContinue())
|
|
165
|
+
return false;
|
|
166
|
+
const targetRect = target.getBoundingClientRect();
|
|
167
|
+
const scrollRect = scrollEl.getBoundingClientRect();
|
|
168
|
+
const top = targetRect.top - scrollRect.top + scrollEl.scrollTop;
|
|
169
|
+
// Preserve scrollLeft so RTL and horizontally-scrolling pages aren't reset.
|
|
170
|
+
await content.scrollToPoint(scrollEl.scrollLeft, top, FRAGMENT_SCROLL_DURATION);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
target.scrollIntoView({ behavior: 'smooth' });
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
printIonError('[ion-router] - Exception in scrollToFragment:', e);
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
69
182
|
export const waitUntilNavNode = () => {
|
|
70
183
|
if (searchNavNode(document.body)) {
|
|
71
184
|
return Promise.resolve();
|