ion-router.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823
  1. /*!
  2. * (C) Ionic http://ionicframework.com - MIT License
  3. */
  4. import { proxyCustomElement, HTMLElement, createEvent } from '@stencil/core/internal/client';
  5. import { c as componentOnReady, p as debounce } from './helpers.js';
  6. import { p as printIonError, a as printIonWarning } from './index4.js';
  7. const ROUTER_INTENT_NONE = 'root';
  8. const ROUTER_INTENT_FORWARD = 'forward';
  9. const ROUTER_INTENT_BACK = 'back';
  10. /** Join the non empty segments with "/". */
  11. const generatePath = (segments) => {
  12. const path = segments.filter((s) => s.length > 0).join('/');
  13. return '/' + path;
  14. };
  15. const generateUrl = (segments, useHash, queryString) => {
  16. let url = generatePath(segments);
  17. if (useHash) {
  18. url = '#' + url;
  19. }
  20. if (queryString !== undefined) {
  21. url += '?' + queryString;
  22. }
  23. return url;
  24. };
  25. const writeSegments = (history, root, useHash, segments, direction, state, queryString) => {
  26. const url = generateUrl([...parsePath(root).segments, ...segments], useHash, queryString);
  27. if (direction === ROUTER_INTENT_FORWARD) {
  28. history.pushState(state, '', url);
  29. }
  30. else {
  31. history.replaceState(state, '', url);
  32. }
  33. };
  34. /**
  35. * Transforms a chain to a list of segments.
  36. *
  37. * Notes:
  38. * - parameter segments of the form :param are replaced with their value,
  39. * - null is returned when a value is missing for any parameter segment.
  40. */
  41. const chainToSegments = (chain) => {
  42. const segments = [];
  43. for (const route of chain) {
  44. for (const segment of route.segments) {
  45. if (segment[0] === ':') {
  46. // eslint-disable-next-line @typescript-eslint/prefer-optional-chain
  47. const param = route.params && route.params[segment.slice(1)];
  48. if (!param) {
  49. return null;
  50. }
  51. segments.push(param);
  52. }
  53. else if (segment !== '') {
  54. segments.push(segment);
  55. }
  56. }
  57. }
  58. return segments;
  59. };
  60. /**
  61. * Removes the prefix segments from the path segments.
  62. *
  63. * Return:
  64. * - null when the path segments do not start with the passed prefix,
  65. * - the path segments after the prefix otherwise.
  66. */
  67. const removePrefix = (prefix, segments) => {
  68. if (prefix.length > segments.length) {
  69. return null;
  70. }
  71. if (prefix.length <= 1 && prefix[0] === '') {
  72. return segments;
  73. }
  74. for (let i = 0; i < prefix.length; i++) {
  75. if (prefix[i] !== segments[i]) {
  76. return null;
  77. }
  78. }
  79. if (segments.length === prefix.length) {
  80. return [''];
  81. }
  82. return segments.slice(prefix.length);
  83. };
  84. const readSegments = (loc, root, useHash) => {
  85. const prefix = parsePath(root).segments;
  86. const pathname = useHash ? loc.hash.slice(1) : loc.pathname;
  87. const segments = parsePath(pathname).segments;
  88. return removePrefix(prefix, segments);
  89. };
  90. /**
  91. * Parses the path to:
  92. * - segments an array of '/' separated parts,
  93. * - queryString (undefined when no query string).
  94. */
  95. const parsePath = (path) => {
  96. let segments = [''];
  97. let queryString;
  98. if (path != null) {
  99. const qsStart = path.indexOf('?');
  100. if (qsStart > -1) {
  101. queryString = path.substring(qsStart + 1);
  102. path = path.substring(0, qsStart);
  103. }
  104. segments = path
  105. .split('/')
  106. .map((s) => s.trim())
  107. .filter((s) => s.length > 0);
  108. if (segments.length === 0) {
  109. segments = [''];
  110. }
  111. }
  112. return { segments, queryString };
  113. };
  114. const printRoutes = (routes) => {
  115. console.group(`[ion-core] ROUTES[${routes.length}]`);
  116. for (const chain of routes) {
  117. const segments = [];
  118. chain.forEach((r) => segments.push(...r.segments));
  119. const ids = chain.map((r) => r.id);
  120. console.debug(`%c ${generatePath(segments)}`, 'font-weight: bold; padding-left: 20px', '=>\t', `(${ids.join(', ')})`);
  121. }
  122. console.groupEnd();
  123. };
  124. const printRedirects = (redirects) => {
  125. console.group(`[ion-core] REDIRECTS[${redirects.length}]`);
  126. for (const redirect of redirects) {
  127. if (redirect.to) {
  128. console.debug('FROM: ', `$c ${generatePath(redirect.from)}`, 'font-weight: bold', ' TO: ', `$c ${generatePath(redirect.to.segments)}`, 'font-weight: bold');
  129. }
  130. }
  131. console.groupEnd();
  132. };
  133. /**
  134. * Activates the passed route chain.
  135. *
  136. * There must be exactly one outlet per route entry in the chain.
  137. *
  138. * The methods calls setRouteId on each of the outlet with the corresponding route entry in the chain.
  139. * setRouteId will create or select the view in the outlet.
  140. */
  141. const writeNavState = async (root, chain, direction, index, changed = false, animation) => {
  142. try {
  143. // find next navigation outlet in the DOM
  144. const outlet = searchNavNode(root);
  145. // make sure we can continue interacting the DOM, otherwise abort
  146. if (index >= chain.length || !outlet) {
  147. return changed;
  148. }
  149. await new Promise((resolve) => componentOnReady(outlet, resolve));
  150. const route = chain[index];
  151. const result = await outlet.setRouteId(route.id, route.params, direction, animation);
  152. // if the outlet changed the page, reset navigation to neutral (no direction)
  153. // this means nested outlets will not animate
  154. if (result.changed) {
  155. direction = ROUTER_INTENT_NONE;
  156. changed = true;
  157. }
  158. // recursively set nested outlets
  159. changed = await writeNavState(result.element, chain, direction, index + 1, changed, animation);
  160. // once all nested outlets are visible let's make the parent visible too,
  161. // using markVisible prevents flickering
  162. if (result.markVisible) {
  163. await result.markVisible();
  164. }
  165. return changed;
  166. }
  167. catch (e) {
  168. printIonError('[ion-router] - Exception in writeNavState:', e);
  169. return false;
  170. }
  171. };
  172. /**
  173. * Recursively walks the outlet in the DOM.
  174. *
  175. * The function returns a list of RouteID corresponding to each of the outlet and the last outlet without a RouteID.
  176. */
  177. const readNavState = async (root) => {
  178. const ids = [];
  179. let outlet;
  180. let node = root;
  181. // eslint-disable-next-line no-cond-assign
  182. while ((outlet = searchNavNode(node))) {
  183. const id = await outlet.getRouteId();
  184. if (id) {
  185. node = id.element;
  186. id.element = undefined;
  187. ids.push(id);
  188. }
  189. else {
  190. break;
  191. }
  192. }
  193. return { ids, outlet };
  194. };
  195. const waitUntilNavNode = () => {
  196. if (searchNavNode(document.body)) {
  197. return Promise.resolve();
  198. }
  199. return new Promise((resolve) => {
  200. window.addEventListener('ionNavWillLoad', () => resolve(), { once: true });
  201. });
  202. };
  203. /** Selector for all the outlets supported by the router. */
  204. const OUTLET_SELECTOR = ':not([no-router]) ion-nav, :not([no-router]) ion-tabs, :not([no-router]) ion-router-outlet';
  205. const searchNavNode = (root) => {
  206. if (!root) {
  207. return undefined;
  208. }
  209. if (root.matches(OUTLET_SELECTOR)) {
  210. return root;
  211. }
  212. const outlet = root.querySelector(OUTLET_SELECTOR);
  213. return outlet !== null && outlet !== void 0 ? outlet : undefined;
  214. };
  215. /**
  216. * Returns whether the given redirect matches the given path segments.
  217. *
  218. * A redirect matches when the segments of the path and redirect.from are equal.
  219. * Note that segments are only checked until redirect.from contains a '*' which matches any path segment.
  220. * The path ['some', 'path', 'to', 'page'] matches both ['some', 'path', 'to', 'page'] and ['some', 'path', '*'].
  221. */
  222. const matchesRedirect = (segments, redirect) => {
  223. const { from, to } = redirect;
  224. if (to === undefined) {
  225. return false;
  226. }
  227. if (from.length > segments.length) {
  228. return false;
  229. }
  230. for (let i = 0; i < from.length; i++) {
  231. const expected = from[i];
  232. if (expected === '*') {
  233. return true;
  234. }
  235. if (expected !== segments[i]) {
  236. return false;
  237. }
  238. }
  239. return from.length === segments.length;
  240. };
  241. /** Returns the first redirect matching the path segments or undefined when no match found. */
  242. const findRouteRedirect = (segments, redirects) => {
  243. return redirects.find((redirect) => matchesRedirect(segments, redirect));
  244. };
  245. const matchesIDs = (ids, chain) => {
  246. const len = Math.min(ids.length, chain.length);
  247. let score = 0;
  248. for (let i = 0; i < len; i++) {
  249. const routeId = ids[i];
  250. const routeChain = chain[i];
  251. // Skip results where the route id does not match the chain at the same index
  252. if (routeId.id.toLowerCase() !== routeChain.id) {
  253. break;
  254. }
  255. if (routeId.params) {
  256. const routeIdParams = Object.keys(routeId.params);
  257. // Only compare routes with the chain that have the same number of parameters.
  258. if (routeIdParams.length === routeChain.segments.length) {
  259. // Maps the route's params into a path based on the path variable names,
  260. // to compare against the route chain format.
  261. //
  262. // Before:
  263. // ```ts
  264. // {
  265. // params: {
  266. // s1: 'a',
  267. // s2: 'b'
  268. // }
  269. // }
  270. // ```
  271. //
  272. // After:
  273. // ```ts
  274. // [':s1',':s2']
  275. // ```
  276. //
  277. const pathWithParams = routeIdParams.map((key) => `:${key}`);
  278. for (let j = 0; j < pathWithParams.length; j++) {
  279. // Skip results where the path variable is not a match
  280. if (pathWithParams[j].toLowerCase() !== routeChain.segments[j]) {
  281. break;
  282. }
  283. // Weight path matches for the same index higher.
  284. score++;
  285. }
  286. }
  287. }
  288. // Weight id matches
  289. score++;
  290. }
  291. return score;
  292. };
  293. /**
  294. * Matches the segments against the chain.
  295. *
  296. * Returns:
  297. * - null when there is no match,
  298. * - a chain with the params properties updated with the parameter segments on match.
  299. */
  300. const matchesSegments = (segments, chain) => {
  301. const inputSegments = new RouterSegments(segments);
  302. let matchesDefault = false;
  303. let allparams;
  304. for (let i = 0; i < chain.length; i++) {
  305. const chainSegments = chain[i].segments;
  306. if (chainSegments[0] === '') {
  307. matchesDefault = true;
  308. }
  309. else {
  310. for (const segment of chainSegments) {
  311. const data = inputSegments.next();
  312. // data param
  313. if (segment[0] === ':') {
  314. if (data === '') {
  315. return null;
  316. }
  317. allparams = allparams || [];
  318. const params = allparams[i] || (allparams[i] = {});
  319. params[segment.slice(1)] = data;
  320. }
  321. else if (data !== segment) {
  322. return null;
  323. }
  324. }
  325. matchesDefault = false;
  326. }
  327. }
  328. const matches = matchesDefault ? matchesDefault === (inputSegments.next() === '') : true;
  329. if (!matches) {
  330. return null;
  331. }
  332. if (allparams) {
  333. return chain.map((route, i) => ({
  334. id: route.id,
  335. segments: route.segments,
  336. params: mergeParams(route.params, allparams[i]),
  337. beforeEnter: route.beforeEnter,
  338. beforeLeave: route.beforeLeave,
  339. }));
  340. }
  341. return chain;
  342. };
  343. /**
  344. * Merges the route parameter objects.
  345. * Returns undefined when both parameters are undefined.
  346. */
  347. const mergeParams = (a, b) => {
  348. return a || b ? Object.assign(Object.assign({}, a), b) : undefined;
  349. };
  350. /**
  351. * Finds the best match for the ids in the chains.
  352. *
  353. * Returns the best match or null when no match is found.
  354. * When a chain is returned the parameters are updated from the RouteIDs.
  355. * That is they contain both the componentProps of the <ion-route> and the parameter segment.
  356. */
  357. const findChainForIDs = (ids, chains) => {
  358. let match = null;
  359. let maxMatches = 0;
  360. for (const chain of chains) {
  361. const score = matchesIDs(ids, chain);
  362. if (score > maxMatches) {
  363. match = chain;
  364. maxMatches = score;
  365. }
  366. }
  367. if (match) {
  368. return match.map((route, i) => {
  369. var _a;
  370. return ({
  371. id: route.id,
  372. segments: route.segments,
  373. params: mergeParams(route.params, (_a = ids[i]) === null || _a === void 0 ? void 0 : _a.params),
  374. });
  375. });
  376. }
  377. return null;
  378. };
  379. /**
  380. * Finds the best match for the segments in the chains.
  381. *
  382. * Returns the best match or null when no match is found.
  383. * When a chain is returned the parameters are updated from the segments.
  384. * That is they contain both the componentProps of the <ion-route> and the parameter segments.
  385. */
  386. const findChainForSegments = (segments, chains) => {
  387. let match = null;
  388. let bestScore = 0;
  389. for (const chain of chains) {
  390. const matchedChain = matchesSegments(segments, chain);
  391. if (matchedChain !== null) {
  392. const score = computePriority(matchedChain);
  393. if (score > bestScore) {
  394. bestScore = score;
  395. match = matchedChain;
  396. }
  397. }
  398. }
  399. return match;
  400. };
  401. /**
  402. * Computes the priority of a chain.
  403. *
  404. * Parameter segments are given a lower priority over fixed segments.
  405. *
  406. * Considering the following 2 chains matching the path /path/to/page:
  407. * - /path/to/:where
  408. * - /path/to/page
  409. *
  410. * The second one will be given a higher priority because "page" is a fixed segment (vs ":where", a parameter segment).
  411. */
  412. const computePriority = (chain) => {
  413. let score = 1;
  414. let level = 1;
  415. for (const route of chain) {
  416. for (const segment of route.segments) {
  417. if (segment[0] === ':') {
  418. score += Math.pow(1, level);
  419. }
  420. else if (segment !== '') {
  421. score += Math.pow(2, level);
  422. }
  423. level++;
  424. }
  425. }
  426. return score;
  427. };
  428. class RouterSegments {
  429. constructor(segments) {
  430. this.segments = segments.slice();
  431. }
  432. next() {
  433. if (this.segments.length > 0) {
  434. return this.segments.shift();
  435. }
  436. return '';
  437. }
  438. }
  439. const readProp = (el, prop) => {
  440. if (prop in el) {
  441. return el[prop];
  442. }
  443. if (el.hasAttribute(prop)) {
  444. return el.getAttribute(prop);
  445. }
  446. return null;
  447. };
  448. /**
  449. * Extracts the redirects (that is <ion-route-redirect> elements inside the root).
  450. *
  451. * The redirects are returned as a list of RouteRedirect.
  452. */
  453. const readRedirects = (root) => {
  454. return Array.from(root.children)
  455. .filter((el) => el.tagName === 'ION-ROUTE-REDIRECT')
  456. .map((el) => {
  457. const to = readProp(el, 'to');
  458. return {
  459. from: parsePath(readProp(el, 'from')).segments,
  460. to: to == null ? undefined : parsePath(to),
  461. };
  462. });
  463. };
  464. /**
  465. * Extracts all the routes (that is <ion-route> elements inside the root).
  466. *
  467. * The routes are returned as a list of chains - the flattened tree.
  468. */
  469. const readRoutes = (root) => {
  470. return flattenRouterTree(readRouteNodes(root));
  471. };
  472. /**
  473. * Reads the route nodes as a tree modeled after the DOM tree of <ion-route> elements.
  474. *
  475. * Note: routes without a component are ignored together with their children.
  476. */
  477. const readRouteNodes = (node) => {
  478. return Array.from(node.children)
  479. .filter((el) => el.tagName === 'ION-ROUTE' && el.component)
  480. .map((el) => {
  481. const component = readProp(el, 'component');
  482. return {
  483. segments: parsePath(readProp(el, 'url')).segments,
  484. id: component.toLowerCase(),
  485. params: el.componentProps,
  486. beforeLeave: el.beforeLeave,
  487. beforeEnter: el.beforeEnter,
  488. children: readRouteNodes(el),
  489. };
  490. });
  491. };
  492. /**
  493. * Flattens a RouterTree in a list of chains.
  494. *
  495. * Each chain represents a path from the root node to a terminal node.
  496. */
  497. const flattenRouterTree = (nodes) => {
  498. const chains = [];
  499. for (const node of nodes) {
  500. flattenNode([], chains, node);
  501. }
  502. return chains;
  503. };
  504. /** Flattens a route node recursively and push each branch to the chains list. */
  505. const flattenNode = (chain, chains, node) => {
  506. chain = [
  507. ...chain,
  508. {
  509. id: node.id,
  510. segments: node.segments,
  511. params: node.params,
  512. beforeLeave: node.beforeLeave,
  513. beforeEnter: node.beforeEnter,
  514. },
  515. ];
  516. if (node.children.length === 0) {
  517. chains.push(chain);
  518. return;
  519. }
  520. for (const child of node.children) {
  521. flattenNode(chain, chains, child);
  522. }
  523. };
  524. const Router = /*@__PURE__*/ proxyCustomElement(class Router extends HTMLElement {
  525. constructor() {
  526. super();
  527. this.__registerHost();
  528. this.ionRouteWillChange = createEvent(this, "ionRouteWillChange", 7);
  529. this.ionRouteDidChange = createEvent(this, "ionRouteDidChange", 7);
  530. this.previousPath = null;
  531. this.busy = false;
  532. this.state = 0;
  533. this.lastState = 0;
  534. this.root = '/';
  535. this.useHash = true;
  536. }
  537. async componentWillLoad() {
  538. await waitUntilNavNode();
  539. const canProceed = await this.runGuards(this.getSegments());
  540. if (canProceed !== true) {
  541. if (typeof canProceed === 'object') {
  542. const { redirect } = canProceed;
  543. const path = parsePath(redirect);
  544. this.setSegments(path.segments, ROUTER_INTENT_NONE, path.queryString);
  545. await this.writeNavStateRoot(path.segments, ROUTER_INTENT_NONE);
  546. }
  547. }
  548. else {
  549. await this.onRoutesChanged();
  550. }
  551. }
  552. componentDidLoad() {
  553. window.addEventListener('ionRouteRedirectChanged', debounce(this.onRedirectChanged.bind(this), 10));
  554. window.addEventListener('ionRouteDataChanged', debounce(this.onRoutesChanged.bind(this), 100));
  555. }
  556. async onPopState() {
  557. const direction = this.historyDirection();
  558. let segments = this.getSegments();
  559. const canProceed = await this.runGuards(segments);
  560. if (canProceed !== true) {
  561. if (typeof canProceed === 'object') {
  562. segments = parsePath(canProceed.redirect).segments;
  563. }
  564. else {
  565. return false;
  566. }
  567. }
  568. return this.writeNavStateRoot(segments, direction);
  569. }
  570. onBackButton(ev) {
  571. ev.detail.register(0, (processNextHandler) => {
  572. this.back();
  573. processNextHandler();
  574. });
  575. }
  576. /** @internal */
  577. async canTransition() {
  578. const canProceed = await this.runGuards();
  579. if (canProceed !== true) {
  580. if (typeof canProceed === 'object') {
  581. return canProceed.redirect;
  582. }
  583. else {
  584. return false;
  585. }
  586. }
  587. return true;
  588. }
  589. /**
  590. * Navigate to the specified path.
  591. *
  592. * @param path The path to navigate to.
  593. * @param direction The direction of the animation. Defaults to `"forward"`.
  594. */
  595. async push(path, direction = 'forward', animation) {
  596. var _a;
  597. if (path.startsWith('.')) {
  598. const currentPath = (_a = this.previousPath) !== null && _a !== void 0 ? _a : '/';
  599. // Convert currentPath to an URL by pre-pending a protocol and a host to resolve the relative path.
  600. const url = new URL(path, `https://host/${currentPath}`);
  601. path = url.pathname + url.search;
  602. }
  603. let parsedPath = parsePath(path);
  604. const canProceed = await this.runGuards(parsedPath.segments);
  605. if (canProceed !== true) {
  606. if (typeof canProceed === 'object') {
  607. parsedPath = parsePath(canProceed.redirect);
  608. }
  609. else {
  610. return false;
  611. }
  612. }
  613. this.setSegments(parsedPath.segments, direction, parsedPath.queryString);
  614. return this.writeNavStateRoot(parsedPath.segments, direction, animation);
  615. }
  616. /** Go back to previous page in the window.history. */
  617. back() {
  618. window.history.back();
  619. return Promise.resolve(this.waitPromise);
  620. }
  621. /** @internal */
  622. async printDebug() {
  623. printRoutes(readRoutes(this.el));
  624. printRedirects(readRedirects(this.el));
  625. }
  626. /** @internal */
  627. async navChanged(direction) {
  628. if (this.busy) {
  629. printIonWarning('[ion-router] - Router is busy, navChanged was cancelled.');
  630. return false;
  631. }
  632. const { ids, outlet } = await readNavState(window.document.body);
  633. const routes = readRoutes(this.el);
  634. const chain = findChainForIDs(ids, routes);
  635. if (!chain) {
  636. printIonWarning('[ion-router] - No matching URL for', ids.map((i) => i.id));
  637. return false;
  638. }
  639. const segments = chainToSegments(chain);
  640. if (!segments) {
  641. printIonWarning('[ion-router] - Router could not match path because some required param is missing.');
  642. return false;
  643. }
  644. this.setSegments(segments, direction);
  645. await this.safeWriteNavState(outlet, chain, ROUTER_INTENT_NONE, segments, null, ids.length);
  646. return true;
  647. }
  648. /** This handler gets called when a `ion-route-redirect` component is added to the DOM or if the from or to property of such node changes. */
  649. onRedirectChanged() {
  650. const segments = this.getSegments();
  651. if (segments && findRouteRedirect(segments, readRedirects(this.el))) {
  652. this.writeNavStateRoot(segments, ROUTER_INTENT_NONE);
  653. }
  654. }
  655. /** This handler gets called when a `ion-route` component is added to the DOM or if the from or to property of such node changes. */
  656. onRoutesChanged() {
  657. return this.writeNavStateRoot(this.getSegments(), ROUTER_INTENT_NONE);
  658. }
  659. historyDirection() {
  660. var _a;
  661. const win = window;
  662. if (win.history.state === null) {
  663. this.state++;
  664. win.history.replaceState(this.state, win.document.title, (_a = win.document.location) === null || _a === void 0 ? void 0 : _a.href);
  665. }
  666. const state = win.history.state;
  667. const lastState = this.lastState;
  668. this.lastState = state;
  669. if (state > lastState || (state >= lastState && lastState > 0)) {
  670. return ROUTER_INTENT_FORWARD;
  671. }
  672. if (state < lastState) {
  673. return ROUTER_INTENT_BACK;
  674. }
  675. return ROUTER_INTENT_NONE;
  676. }
  677. async writeNavStateRoot(segments, direction, animation) {
  678. if (!segments) {
  679. printIonError('[ion-router] - URL is not part of the routing set.');
  680. return false;
  681. }
  682. // lookup redirect rule
  683. const redirects = readRedirects(this.el);
  684. const redirect = findRouteRedirect(segments, redirects);
  685. let redirectFrom = null;
  686. if (redirect) {
  687. const { segments: toSegments, queryString } = redirect.to;
  688. this.setSegments(toSegments, direction, queryString);
  689. redirectFrom = redirect.from;
  690. segments = toSegments;
  691. }
  692. // lookup route chain
  693. const routes = readRoutes(this.el);
  694. const chain = findChainForSegments(segments, routes);
  695. if (!chain) {
  696. printIonError('[ion-router] - The path does not match any route.');
  697. return false;
  698. }
  699. // write DOM give
  700. return this.safeWriteNavState(document.body, chain, direction, segments, redirectFrom, 0, animation);
  701. }
  702. async safeWriteNavState(node, chain, direction, segments, redirectFrom, index = 0, animation) {
  703. const unlock = await this.lock();
  704. let changed = false;
  705. try {
  706. changed = await this.writeNavState(node, chain, direction, segments, redirectFrom, index, animation);
  707. }
  708. catch (e) {
  709. printIonError('[ion-router] - Exception in safeWriteNavState:', e);
  710. }
  711. unlock();
  712. return changed;
  713. }
  714. async lock() {
  715. const p = this.waitPromise;
  716. let resolve;
  717. this.waitPromise = new Promise((r) => (resolve = r));
  718. if (p !== undefined) {
  719. await p;
  720. }
  721. return resolve;
  722. }
  723. /**
  724. * Executes the beforeLeave hook of the source route and the beforeEnter hook of the target route if they exist.
  725. *
  726. * When the beforeLeave hook does not return true (to allow navigating) then that value is returned early and the beforeEnter is executed.
  727. * Otherwise the beforeEnterHook hook of the target route is executed.
  728. */
  729. async runGuards(to = this.getSegments(), from) {
  730. if (from === undefined) {
  731. from = parsePath(this.previousPath).segments;
  732. }
  733. if (!to || !from) {
  734. return true;
  735. }
  736. const routes = readRoutes(this.el);
  737. const fromChain = findChainForSegments(from, routes);
  738. // eslint-disable-next-line @typescript-eslint/prefer-optional-chain
  739. const beforeLeaveHook = fromChain && fromChain[fromChain.length - 1].beforeLeave;
  740. const canLeave = beforeLeaveHook ? await beforeLeaveHook() : true;
  741. if (canLeave === false || typeof canLeave === 'object') {
  742. return canLeave;
  743. }
  744. const toChain = findChainForSegments(to, routes);
  745. // eslint-disable-next-line @typescript-eslint/prefer-optional-chain
  746. const beforeEnterHook = toChain && toChain[toChain.length - 1].beforeEnter;
  747. return beforeEnterHook ? beforeEnterHook() : true;
  748. }
  749. async writeNavState(node, chain, direction, segments, redirectFrom, index = 0, animation) {
  750. if (this.busy) {
  751. printIonWarning('[ion-router] - Router is busy, transition was cancelled.');
  752. return false;
  753. }
  754. this.busy = true;
  755. // generate route event and emit will change
  756. const routeEvent = this.routeChangeEvent(segments, redirectFrom);
  757. if (routeEvent) {
  758. this.ionRouteWillChange.emit(routeEvent);
  759. }
  760. const changed = await writeNavState(node, chain, direction, index, false, animation);
  761. this.busy = false;
  762. // emit did change
  763. if (routeEvent) {
  764. this.ionRouteDidChange.emit(routeEvent);
  765. }
  766. return changed;
  767. }
  768. setSegments(segments, direction, queryString) {
  769. this.state++;
  770. writeSegments(window.history, this.root, this.useHash, segments, direction, this.state, queryString);
  771. }
  772. getSegments() {
  773. return readSegments(window.location, this.root, this.useHash);
  774. }
  775. routeChangeEvent(toSegments, redirectFromSegments) {
  776. const from = this.previousPath;
  777. const to = generatePath(toSegments);
  778. this.previousPath = to;
  779. if (to === from) {
  780. return null;
  781. }
  782. const redirectedFrom = redirectFromSegments ? generatePath(redirectFromSegments) : null;
  783. return {
  784. from,
  785. redirectedFrom,
  786. to,
  787. };
  788. }
  789. get el() { return this; }
  790. }, [0, "ion-router", {
  791. "root": [1],
  792. "useHash": [4, "use-hash"],
  793. "canTransition": [64],
  794. "push": [64],
  795. "back": [64],
  796. "printDebug": [64],
  797. "navChanged": [64]
  798. }, [[8, "popstate", "onPopState"], [4, "ionBackButton", "onBackButton"]]]);
  799. function defineCustomElement$1() {
  800. if (typeof customElements === "undefined") {
  801. return;
  802. }
  803. const components = ["ion-router"];
  804. components.forEach(tagName => { switch (tagName) {
  805. case "ion-router":
  806. if (!customElements.get(tagName)) {
  807. customElements.define(tagName, Router);
  808. }
  809. break;
  810. } });
  811. }
  812. const IonRouter = Router;
  813. const defineCustomElement = defineCustomElement$1;
  814. export { IonRouter, defineCustomElement };