ion-nav.js 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960
  1. /*!
  2. * (C) Ionic http://ionicframework.com - MIT License
  3. */
  4. import { proxyCustomElement, HTMLElement, createEvent, h, Build } from '@stencil/core/internal/client';
  5. import { g as getTimeGivenProgression } from './cubic-bezier.js';
  6. import { n as assert, s as shallowEqualStringMap } from './helpers.js';
  7. import { a as printIonWarning, c as config } from './index4.js';
  8. import { l as lifecycle, t as transition, s as setPageHidden, d as LIFECYCLE_WILL_UNLOAD, b as LIFECYCLE_WILL_LEAVE, c as LIFECYCLE_DID_LEAVE } from './index2.js';
  9. import { b as getIonMode } from './ionic-global.js';
  10. import { a as attachComponent } from './framework-delegate.js';
  11. const VIEW_STATE_NEW = 1;
  12. const VIEW_STATE_ATTACHED = 2;
  13. const VIEW_STATE_DESTROYED = 3;
  14. // TODO(FW-2832): types
  15. class ViewController {
  16. constructor(component, params) {
  17. this.component = component;
  18. this.params = params;
  19. this.state = VIEW_STATE_NEW;
  20. }
  21. async init(container) {
  22. this.state = VIEW_STATE_ATTACHED;
  23. if (!this.element) {
  24. const component = this.component;
  25. this.element = await attachComponent(this.delegate, container, component, ['ion-page', 'ion-page-invisible'], this.params);
  26. }
  27. }
  28. /**
  29. * DOM WRITE
  30. */
  31. _destroy() {
  32. assert(this.state !== VIEW_STATE_DESTROYED, 'view state must be ATTACHED');
  33. const element = this.element;
  34. if (element) {
  35. if (this.delegate) {
  36. this.delegate.removeViewFromDom(element.parentElement, element);
  37. }
  38. else {
  39. element.remove();
  40. }
  41. }
  42. this.nav = undefined;
  43. this.state = VIEW_STATE_DESTROYED;
  44. }
  45. }
  46. const matches = (view, id, params) => {
  47. if (!view) {
  48. return false;
  49. }
  50. if (view.component !== id) {
  51. return false;
  52. }
  53. return shallowEqualStringMap(view.params, params);
  54. };
  55. const convertToView = (page, params) => {
  56. if (!page) {
  57. return null;
  58. }
  59. if (page instanceof ViewController) {
  60. return page;
  61. }
  62. return new ViewController(page, params);
  63. };
  64. const convertToViews = (pages) => {
  65. return pages
  66. .map((page) => {
  67. if (page instanceof ViewController) {
  68. return page;
  69. }
  70. if ('component' in page) {
  71. return convertToView(page.component, page.componentProps === null ? undefined : page.componentProps);
  72. }
  73. return convertToView(page, undefined);
  74. })
  75. .filter((v) => v !== null);
  76. };
  77. const navCss = ":host{left:0;right:0;top:0;bottom:0;position:absolute;contain:layout size style;z-index:0}";
  78. const IonNavStyle0 = navCss;
  79. const Nav = /*@__PURE__*/ proxyCustomElement(class Nav extends HTMLElement {
  80. constructor() {
  81. super();
  82. this.__registerHost();
  83. this.__attachShadow();
  84. this.ionNavWillLoad = createEvent(this, "ionNavWillLoad", 7);
  85. this.ionNavWillChange = createEvent(this, "ionNavWillChange", 3);
  86. this.ionNavDidChange = createEvent(this, "ionNavDidChange", 3);
  87. this.transInstr = [];
  88. this.gestureOrAnimationInProgress = false;
  89. this.useRouter = false;
  90. this.isTransitioning = false;
  91. this.destroyed = false;
  92. this.views = [];
  93. this.didLoad = false;
  94. this.delegate = undefined;
  95. this.swipeGesture = undefined;
  96. this.animated = true;
  97. this.animation = undefined;
  98. this.rootParams = undefined;
  99. this.root = undefined;
  100. }
  101. swipeGestureChanged() {
  102. if (this.gesture) {
  103. this.gesture.enable(this.swipeGesture === true);
  104. }
  105. }
  106. rootChanged() {
  107. const isDev = Build.isDev;
  108. if (this.root === undefined) {
  109. return;
  110. }
  111. if (this.didLoad === false) {
  112. /**
  113. * If the component has not loaded yet, we can skip setting up the root component.
  114. * It will be called when `componentDidLoad` fires.
  115. */
  116. return;
  117. }
  118. if (!this.useRouter) {
  119. if (this.root !== undefined) {
  120. this.setRoot(this.root, this.rootParams);
  121. }
  122. }
  123. else if (isDev) {
  124. printIonWarning('[ion-nav] - A root attribute is not supported when using ion-router.', this.el);
  125. }
  126. }
  127. componentWillLoad() {
  128. this.useRouter = document.querySelector('ion-router') !== null && this.el.closest('[no-router]') === null;
  129. if (this.swipeGesture === undefined) {
  130. const mode = getIonMode(this);
  131. this.swipeGesture = config.getBoolean('swipeBackEnabled', mode === 'ios');
  132. }
  133. this.ionNavWillLoad.emit();
  134. }
  135. async componentDidLoad() {
  136. // We want to set this flag before any watch callbacks are manually called
  137. this.didLoad = true;
  138. this.rootChanged();
  139. this.gesture = (await import('./swipe-back.js')).createSwipeBackGesture(this.el, this.canStart.bind(this), this.onStart.bind(this), this.onMove.bind(this), this.onEnd.bind(this));
  140. this.swipeGestureChanged();
  141. }
  142. connectedCallback() {
  143. this.destroyed = false;
  144. }
  145. disconnectedCallback() {
  146. for (const view of this.views) {
  147. lifecycle(view.element, LIFECYCLE_WILL_UNLOAD);
  148. view._destroy();
  149. }
  150. // Release swipe back gesture and transition.
  151. if (this.gesture) {
  152. this.gesture.destroy();
  153. this.gesture = undefined;
  154. }
  155. this.transInstr.length = 0;
  156. this.views.length = 0;
  157. this.destroyed = true;
  158. }
  159. /**
  160. * Push a new component onto the current navigation stack. Pass any additional
  161. * information along as an object. This additional information is accessible
  162. * through NavParams.
  163. *
  164. * @param component The component to push onto the navigation stack.
  165. * @param componentProps Any properties of the component.
  166. * @param opts The navigation options.
  167. * @param done The transition complete function.
  168. */
  169. push(component, componentProps, opts, done) {
  170. return this.insert(-1, component, componentProps, opts, done);
  171. }
  172. /**
  173. * Inserts a component into the navigation stack at the specified index.
  174. * This is useful to add a component at any point in the navigation stack.
  175. *
  176. * @param insertIndex The index to insert the component at in the stack.
  177. * @param component The component to insert into the navigation stack.
  178. * @param componentProps Any properties of the component.
  179. * @param opts The navigation options.
  180. * @param done The transition complete function.
  181. */
  182. insert(insertIndex, component, componentProps, opts, done) {
  183. return this.insertPages(insertIndex, [{ component, componentProps }], opts, done);
  184. }
  185. /**
  186. * Inserts an array of components into the navigation stack at the specified index.
  187. * The last component in the array will become instantiated as a view, and animate
  188. * in to become the active view.
  189. *
  190. * @param insertIndex The index to insert the components at in the stack.
  191. * @param insertComponents The components to insert into the navigation stack.
  192. * @param opts The navigation options.
  193. * @param done The transition complete function.
  194. */
  195. insertPages(insertIndex, insertComponents, opts, done) {
  196. return this.queueTrns({
  197. insertStart: insertIndex,
  198. insertViews: insertComponents,
  199. opts,
  200. }, done);
  201. }
  202. /**
  203. * Pop a component off of the navigation stack. Navigates back from the current
  204. * component.
  205. *
  206. * @param opts The navigation options.
  207. * @param done The transition complete function.
  208. */
  209. pop(opts, done) {
  210. return this.removeIndex(-1, 1, opts, done);
  211. }
  212. /**
  213. * Pop to a specific index in the navigation stack.
  214. *
  215. * @param indexOrViewCtrl The index or view controller to pop to.
  216. * @param opts The navigation options.
  217. * @param done The transition complete function.
  218. */
  219. popTo(indexOrViewCtrl, opts, done) {
  220. const ti = {
  221. removeStart: -1,
  222. removeCount: -1,
  223. opts,
  224. };
  225. if (typeof indexOrViewCtrl === 'object' && indexOrViewCtrl.component) {
  226. ti.removeView = indexOrViewCtrl;
  227. ti.removeStart = 1;
  228. }
  229. else if (typeof indexOrViewCtrl === 'number') {
  230. ti.removeStart = indexOrViewCtrl + 1;
  231. }
  232. return this.queueTrns(ti, done);
  233. }
  234. /**
  235. * Navigate back to the root of the stack, no matter how far back that is.
  236. *
  237. * @param opts The navigation options.
  238. * @param done The transition complete function.
  239. */
  240. popToRoot(opts, done) {
  241. return this.removeIndex(1, -1, opts, done);
  242. }
  243. /**
  244. * Removes a component from the navigation stack at the specified index.
  245. *
  246. * @param startIndex The number to begin removal at.
  247. * @param removeCount The number of components to remove.
  248. * @param opts The navigation options.
  249. * @param done The transition complete function.
  250. */
  251. removeIndex(startIndex, removeCount = 1, opts, done) {
  252. return this.queueTrns({
  253. removeStart: startIndex,
  254. removeCount,
  255. opts,
  256. }, done);
  257. }
  258. /**
  259. * Set the root for the current navigation stack to a component.
  260. *
  261. * @param component The component to set as the root of the navigation stack.
  262. * @param componentProps Any properties of the component.
  263. * @param opts The navigation options.
  264. * @param done The transition complete function.
  265. */
  266. setRoot(component, componentProps, opts, done) {
  267. return this.setPages([{ component, componentProps }], opts, done);
  268. }
  269. /**
  270. * Set the views of the current navigation stack and navigate to the last view.
  271. * By default animations are disabled, but they can be enabled by passing options
  272. * to the navigation controller. Navigation parameters can also be passed to the
  273. * individual pages in the array.
  274. *
  275. * @param views The list of views to set as the navigation stack.
  276. * @param opts The navigation options.
  277. * @param done The transition complete function.
  278. */
  279. setPages(views, opts, done) {
  280. opts !== null && opts !== void 0 ? opts : (opts = {});
  281. // if animation wasn't set to true then default it to NOT animate
  282. if (opts.animated !== true) {
  283. opts.animated = false;
  284. }
  285. return this.queueTrns({
  286. insertStart: 0,
  287. insertViews: views,
  288. removeStart: 0,
  289. removeCount: -1,
  290. opts,
  291. }, done);
  292. }
  293. /**
  294. * Called by the router to update the view.
  295. *
  296. * @param id The component tag.
  297. * @param params The component params.
  298. * @param direction A direction hint.
  299. * @param animation an AnimationBuilder.
  300. *
  301. * @return the status.
  302. * @internal
  303. */
  304. setRouteId(id, params, direction, animation) {
  305. const active = this.getActiveSync();
  306. if (matches(active, id, params)) {
  307. return Promise.resolve({
  308. changed: false,
  309. element: active.element,
  310. });
  311. }
  312. let resolve;
  313. const promise = new Promise((r) => (resolve = r));
  314. let finish;
  315. const commonOpts = {
  316. updateURL: false,
  317. viewIsReady: (enteringEl) => {
  318. let mark;
  319. const p = new Promise((r) => (mark = r));
  320. resolve({
  321. changed: true,
  322. element: enteringEl,
  323. markVisible: async () => {
  324. mark();
  325. await finish;
  326. },
  327. });
  328. return p;
  329. },
  330. };
  331. if (direction === 'root') {
  332. finish = this.setRoot(id, params, commonOpts);
  333. }
  334. else {
  335. // Look for a view matching the target in the view stack.
  336. const viewController = this.views.find((v) => matches(v, id, params));
  337. if (viewController) {
  338. finish = this.popTo(viewController, Object.assign(Object.assign({}, commonOpts), { direction: 'back', animationBuilder: animation }));
  339. }
  340. else if (direction === 'forward') {
  341. finish = this.push(id, params, Object.assign(Object.assign({}, commonOpts), { animationBuilder: animation }));
  342. }
  343. else if (direction === 'back') {
  344. finish = this.setRoot(id, params, Object.assign(Object.assign({}, commonOpts), { direction: 'back', animated: true, animationBuilder: animation }));
  345. }
  346. }
  347. return promise;
  348. }
  349. /**
  350. * Called by <ion-router> to retrieve the current component.
  351. *
  352. * @internal
  353. */
  354. async getRouteId() {
  355. const active = this.getActiveSync();
  356. if (active) {
  357. return {
  358. id: active.element.tagName,
  359. params: active.params,
  360. element: active.element,
  361. };
  362. }
  363. return undefined;
  364. }
  365. /**
  366. * Get the active view.
  367. */
  368. async getActive() {
  369. return this.getActiveSync();
  370. }
  371. /**
  372. * Get the view at the specified index.
  373. *
  374. * @param index The index of the view.
  375. */
  376. async getByIndex(index) {
  377. return this.views[index];
  378. }
  379. /**
  380. * Returns `true` if the current view can go back.
  381. *
  382. * @param view The view to check.
  383. */
  384. async canGoBack(view) {
  385. return this.canGoBackSync(view);
  386. }
  387. /**
  388. * Get the previous view.
  389. *
  390. * @param view The view to get.
  391. */
  392. async getPrevious(view) {
  393. return this.getPreviousSync(view);
  394. }
  395. /**
  396. * Returns the number of views in the stack.
  397. */
  398. async getLength() {
  399. return Promise.resolve(this.views.length);
  400. }
  401. getActiveSync() {
  402. return this.views[this.views.length - 1];
  403. }
  404. canGoBackSync(view = this.getActiveSync()) {
  405. return !!(view && this.getPreviousSync(view));
  406. }
  407. getPreviousSync(view = this.getActiveSync()) {
  408. if (!view) {
  409. return undefined;
  410. }
  411. const views = this.views;
  412. const index = views.indexOf(view);
  413. return index > 0 ? views[index - 1] : undefined;
  414. }
  415. /**
  416. * Adds a navigation stack change to the queue and schedules it to run.
  417. *
  418. * @returns Whether the transition succeeds.
  419. */
  420. async queueTrns(ti, done) {
  421. var _a, _b;
  422. if (this.isTransitioning && ((_a = ti.opts) === null || _a === void 0 ? void 0 : _a.skipIfBusy)) {
  423. return false;
  424. }
  425. const promise = new Promise((resolve, reject) => {
  426. ti.resolve = resolve;
  427. ti.reject = reject;
  428. });
  429. ti.done = done;
  430. /**
  431. * If using router, check to see if navigation hooks
  432. * will allow us to perform this transition. This
  433. * is required in order for hooks to work with
  434. * the ion-back-button or swipe to go back.
  435. */
  436. if (ti.opts && ti.opts.updateURL !== false && this.useRouter) {
  437. const router = document.querySelector('ion-router');
  438. if (router) {
  439. const canTransition = await router.canTransition();
  440. if (canTransition === false) {
  441. return false;
  442. }
  443. if (typeof canTransition === 'string') {
  444. router.push(canTransition, ti.opts.direction || 'back');
  445. return false;
  446. }
  447. }
  448. }
  449. // Normalize empty
  450. if (((_b = ti.insertViews) === null || _b === void 0 ? void 0 : _b.length) === 0) {
  451. ti.insertViews = undefined;
  452. }
  453. // Enqueue transition instruction
  454. this.transInstr.push(ti);
  455. // if there isn't a transition already happening
  456. // then this will kick off this transition
  457. this.nextTrns();
  458. return promise;
  459. }
  460. success(result, ti) {
  461. if (this.destroyed) {
  462. this.fireError('nav controller was destroyed', ti);
  463. return;
  464. }
  465. if (ti.done) {
  466. ti.done(result.hasCompleted, result.requiresTransition, result.enteringView, result.leavingView, result.direction);
  467. }
  468. ti.resolve(result.hasCompleted);
  469. if (ti.opts.updateURL !== false && this.useRouter) {
  470. const router = document.querySelector('ion-router');
  471. if (router) {
  472. const direction = result.direction === 'back' ? 'back' : 'forward';
  473. router.navChanged(direction);
  474. }
  475. }
  476. }
  477. failed(rejectReason, ti) {
  478. if (this.destroyed) {
  479. this.fireError('nav controller was destroyed', ti);
  480. return;
  481. }
  482. this.transInstr.length = 0;
  483. this.fireError(rejectReason, ti);
  484. }
  485. fireError(rejectReason, ti) {
  486. if (ti.done) {
  487. ti.done(false, false, rejectReason);
  488. }
  489. if (ti.reject && !this.destroyed) {
  490. ti.reject(rejectReason);
  491. }
  492. else {
  493. ti.resolve(false);
  494. }
  495. }
  496. /**
  497. * Consumes the next transition in the queue.
  498. *
  499. * @returns whether the transition is executed.
  500. */
  501. nextTrns() {
  502. // this is the framework's bread 'n butta function
  503. // only one transition is allowed at any given time
  504. if (this.isTransitioning) {
  505. return false;
  506. }
  507. // there is no transition happening right now, executes the next instructions.
  508. const ti = this.transInstr.shift();
  509. if (!ti) {
  510. return false;
  511. }
  512. this.runTransition(ti);
  513. return true;
  514. }
  515. /** Executes all the transition instruction from the queue. */
  516. async runTransition(ti) {
  517. try {
  518. // set that this nav is actively transitioning
  519. this.ionNavWillChange.emit();
  520. this.isTransitioning = true;
  521. this.prepareTI(ti);
  522. const leavingView = this.getActiveSync();
  523. const enteringView = this.getEnteringView(ti, leavingView);
  524. if (!leavingView && !enteringView) {
  525. throw new Error('no views in the stack to be removed');
  526. }
  527. if (enteringView && enteringView.state === VIEW_STATE_NEW) {
  528. await enteringView.init(this.el);
  529. }
  530. this.postViewInit(enteringView, leavingView, ti);
  531. // Needs transition?
  532. const requiresTransition = (ti.enteringRequiresTransition || ti.leavingRequiresTransition) && enteringView !== leavingView;
  533. if (requiresTransition && ti.opts && leavingView) {
  534. const isBackDirection = ti.opts.direction === 'back';
  535. /**
  536. * If heading back, use the entering page's animation
  537. * unless otherwise specified by the developer.
  538. */
  539. if (isBackDirection) {
  540. ti.opts.animationBuilder = ti.opts.animationBuilder || (enteringView === null || enteringView === void 0 ? void 0 : enteringView.animationBuilder);
  541. }
  542. leavingView.animationBuilder = ti.opts.animationBuilder;
  543. }
  544. let result;
  545. if (requiresTransition) {
  546. result = await this.transition(enteringView, leavingView, ti);
  547. }
  548. else {
  549. // transition is not required, so we are already done!
  550. // they're inserting/removing the views somewhere in the middle or
  551. // beginning, so visually nothing needs to animate/transition
  552. // resolve immediately because there's no animation that's happening
  553. result = {
  554. hasCompleted: true,
  555. requiresTransition: false,
  556. };
  557. }
  558. this.success(result, ti);
  559. this.ionNavDidChange.emit();
  560. }
  561. catch (rejectReason) {
  562. this.failed(rejectReason, ti);
  563. }
  564. this.isTransitioning = false;
  565. this.nextTrns();
  566. }
  567. prepareTI(ti) {
  568. var _a, _b;
  569. var _c;
  570. const viewsLength = this.views.length;
  571. (_a = ti.opts) !== null && _a !== void 0 ? _a : (ti.opts = {});
  572. (_b = (_c = ti.opts).delegate) !== null && _b !== void 0 ? _b : (_c.delegate = this.delegate);
  573. if (ti.removeView !== undefined) {
  574. assert(ti.removeStart !== undefined, 'removeView needs removeStart');
  575. assert(ti.removeCount !== undefined, 'removeView needs removeCount');
  576. const index = this.views.indexOf(ti.removeView);
  577. if (index < 0) {
  578. throw new Error('removeView was not found');
  579. }
  580. ti.removeStart += index;
  581. }
  582. if (ti.removeStart !== undefined) {
  583. if (ti.removeStart < 0) {
  584. ti.removeStart = viewsLength - 1;
  585. }
  586. if (ti.removeCount < 0) {
  587. ti.removeCount = viewsLength - ti.removeStart;
  588. }
  589. ti.leavingRequiresTransition = ti.removeCount > 0 && ti.removeStart + ti.removeCount === viewsLength;
  590. }
  591. if (ti.insertViews) {
  592. // allow -1 to be passed in to auto push it on the end
  593. // and clean up the index if it's larger then the size of the stack
  594. if (ti.insertStart < 0 || ti.insertStart > viewsLength) {
  595. ti.insertStart = viewsLength;
  596. }
  597. ti.enteringRequiresTransition = ti.insertStart === viewsLength;
  598. }
  599. const insertViews = ti.insertViews;
  600. if (!insertViews) {
  601. return;
  602. }
  603. assert(insertViews.length > 0, 'length can not be zero');
  604. const viewControllers = convertToViews(insertViews);
  605. if (viewControllers.length === 0) {
  606. throw new Error('invalid views to insert');
  607. }
  608. // Check all the inserted view are correct
  609. for (const view of viewControllers) {
  610. view.delegate = ti.opts.delegate;
  611. const nav = view.nav;
  612. if (nav && nav !== this) {
  613. throw new Error('inserted view was already inserted');
  614. }
  615. if (view.state === VIEW_STATE_DESTROYED) {
  616. throw new Error('inserted view was already destroyed');
  617. }
  618. }
  619. ti.insertViews = viewControllers;
  620. }
  621. /**
  622. * Returns the view that will be entered considering the transition instructions.
  623. *
  624. * @param ti The instructions.
  625. * @param leavingView The view being left or undefined if none.
  626. *
  627. * @returns The view that will be entered, undefined if none.
  628. */
  629. getEnteringView(ti, leavingView) {
  630. // The last inserted view will be entered when view are inserted.
  631. const insertViews = ti.insertViews;
  632. if (insertViews !== undefined) {
  633. return insertViews[insertViews.length - 1];
  634. }
  635. // When views are deleted, we will enter the last view that is not removed and not the view being left.
  636. const removeStart = ti.removeStart;
  637. if (removeStart !== undefined) {
  638. const views = this.views;
  639. const removeEnd = removeStart + ti.removeCount;
  640. for (let i = views.length - 1; i >= 0; i--) {
  641. const view = views[i];
  642. if ((i < removeStart || i >= removeEnd) && view !== leavingView) {
  643. return view;
  644. }
  645. }
  646. }
  647. return undefined;
  648. }
  649. /**
  650. * Adds and Removes the views from the navigation stack.
  651. *
  652. * @param enteringView The view being entered.
  653. * @param leavingView The view being left.
  654. * @param ti The instructions.
  655. */
  656. postViewInit(enteringView, leavingView, ti) {
  657. var _a, _b, _c;
  658. assert(leavingView || enteringView, 'Both leavingView and enteringView are null');
  659. assert(ti.resolve, 'resolve must be valid');
  660. assert(ti.reject, 'reject must be valid');
  661. // Compute the views to remove.
  662. const opts = ti.opts;
  663. const { insertViews, removeStart, removeCount } = ti;
  664. /** Records the view to destroy */
  665. let destroyQueue;
  666. // there are views to remove
  667. if (removeStart !== undefined && removeCount !== undefined) {
  668. assert(removeStart >= 0, 'removeStart can not be negative');
  669. assert(removeCount >= 0, 'removeCount can not be negative');
  670. destroyQueue = [];
  671. for (let i = removeStart; i < removeStart + removeCount; i++) {
  672. const view = this.views[i];
  673. if (view !== undefined && view !== enteringView && view !== leavingView) {
  674. destroyQueue.push(view);
  675. }
  676. }
  677. // default the direction to "back"
  678. (_a = opts.direction) !== null && _a !== void 0 ? _a : (opts.direction = 'back');
  679. }
  680. const finalNumViews = this.views.length + ((_b = insertViews === null || insertViews === void 0 ? void 0 : insertViews.length) !== null && _b !== void 0 ? _b : 0) - (removeCount !== null && removeCount !== void 0 ? removeCount : 0);
  681. assert(finalNumViews >= 0, 'final balance can not be negative');
  682. if (finalNumViews === 0) {
  683. printIonWarning(`[ion-nav] - You can't remove all the pages in the navigation stack. nav.pop() is probably called too many times.`, this, this.el);
  684. throw new Error('navigation stack needs at least one root page');
  685. }
  686. // At this point the transition can not be rejected, any throw should be an error
  687. // Insert the new views in the stack.
  688. if (insertViews) {
  689. // add the views to the
  690. let insertIndex = ti.insertStart;
  691. for (const view of insertViews) {
  692. this.insertViewAt(view, insertIndex);
  693. insertIndex++;
  694. }
  695. if (ti.enteringRequiresTransition) {
  696. // default to forward if not already set
  697. (_c = opts.direction) !== null && _c !== void 0 ? _c : (opts.direction = 'forward');
  698. }
  699. }
  700. // if the views to be removed are in the beginning or middle
  701. // and there is not a view that needs to visually transition out
  702. // then just destroy them and don't transition anything
  703. // batch all of lifecycles together
  704. // let's make sure, callbacks are zoned
  705. if (destroyQueue && destroyQueue.length > 0) {
  706. for (const view of destroyQueue) {
  707. lifecycle(view.element, LIFECYCLE_WILL_LEAVE);
  708. lifecycle(view.element, LIFECYCLE_DID_LEAVE);
  709. lifecycle(view.element, LIFECYCLE_WILL_UNLOAD);
  710. }
  711. // once all lifecycle events has been delivered, we can safely detroy the views
  712. for (const view of destroyQueue) {
  713. this.destroyView(view);
  714. }
  715. }
  716. }
  717. async transition(enteringView, leavingView, ti) {
  718. // we should animate (duration > 0) if the pushed page is not the first one (startup)
  719. // or if it is a portal (modal, actionsheet, etc.)
  720. const opts = ti.opts;
  721. const progressCallback = opts.progressAnimation
  722. ? (ani) => {
  723. /**
  724. * Because this progress callback is called asynchronously
  725. * it is possible for the gesture to start and end before
  726. * the animation is ever set. In that scenario, we should
  727. * immediately call progressEnd so that the transition promise
  728. * resolves and the gesture does not get locked up.
  729. */
  730. if (ani !== undefined && !this.gestureOrAnimationInProgress) {
  731. this.gestureOrAnimationInProgress = true;
  732. ani.onFinish(() => {
  733. this.gestureOrAnimationInProgress = false;
  734. }, { oneTimeCallback: true });
  735. /**
  736. * Playing animation to beginning
  737. * with a duration of 0 prevents
  738. * any flickering when the animation
  739. * is later cleaned up.
  740. */
  741. ani.progressEnd(0, 0, 0);
  742. }
  743. else {
  744. this.sbAni = ani;
  745. }
  746. }
  747. : undefined;
  748. const mode = getIonMode(this);
  749. const enteringEl = enteringView.element;
  750. // eslint-disable-next-line @typescript-eslint/prefer-optional-chain
  751. const leavingEl = leavingView && leavingView.element;
  752. const animationOpts = Object.assign(Object.assign({ mode, showGoBack: this.canGoBackSync(enteringView), baseEl: this.el, progressCallback, animated: this.animated && config.getBoolean('animated', true), enteringEl,
  753. leavingEl }, opts), { animationBuilder: opts.animationBuilder || this.animation || config.get('navAnimation') });
  754. const { hasCompleted } = await transition(animationOpts);
  755. return this.transitionFinish(hasCompleted, enteringView, leavingView, opts);
  756. }
  757. transitionFinish(hasCompleted, enteringView, leavingView, opts) {
  758. /**
  759. * If the transition did not complete, the leavingView will still be the active
  760. * view on the stack. Otherwise unmount all the views after the enteringView.
  761. */
  762. const activeView = hasCompleted ? enteringView : leavingView;
  763. if (activeView) {
  764. this.unmountInactiveViews(activeView);
  765. }
  766. return {
  767. hasCompleted,
  768. requiresTransition: true,
  769. enteringView,
  770. leavingView,
  771. direction: opts.direction,
  772. };
  773. }
  774. /**
  775. * Inserts a view at the specified index.
  776. *
  777. * When the view already is in the stack it will be moved to the new position.
  778. *
  779. * @param view The view to insert.
  780. * @param index The index where to insert the view.
  781. */
  782. insertViewAt(view, index) {
  783. const views = this.views;
  784. const existingIndex = views.indexOf(view);
  785. if (existingIndex > -1) {
  786. assert(view.nav === this, 'view is not part of the nav');
  787. // The view already in the stack, removes it.
  788. views.splice(existingIndex, 1);
  789. // and add it back at the requested index.
  790. views.splice(index, 0, view);
  791. }
  792. else {
  793. assert(!view.nav, 'nav is used');
  794. // this is a new view to add to the stack
  795. // create the new entering view
  796. view.nav = this;
  797. views.splice(index, 0, view);
  798. }
  799. }
  800. /**
  801. * Removes a view from the stack.
  802. *
  803. * @param view The view to remove.
  804. */
  805. removeView(view) {
  806. assert(view.state === VIEW_STATE_ATTACHED || view.state === VIEW_STATE_DESTROYED, 'view state should be loaded or destroyed');
  807. const views = this.views;
  808. const index = views.indexOf(view);
  809. assert(index > -1, 'view must be part of the stack');
  810. if (index >= 0) {
  811. views.splice(index, 1);
  812. }
  813. }
  814. destroyView(view) {
  815. view._destroy();
  816. this.removeView(view);
  817. }
  818. /**
  819. * Unmounts all inactive views after the specified active view.
  820. *
  821. * DOM WRITE
  822. *
  823. * @param activeView The view that is actively visible in the stack. Used to calculate which views to unmount.
  824. */
  825. unmountInactiveViews(activeView) {
  826. // ok, cleanup time!! Destroy all of the views that are
  827. // INACTIVE and come after the active view
  828. // only do this if the views exist, though
  829. if (this.destroyed) {
  830. return;
  831. }
  832. const views = this.views;
  833. const activeViewIndex = views.indexOf(activeView);
  834. for (let i = views.length - 1; i >= 0; i--) {
  835. const view = views[i];
  836. /**
  837. * When inserting multiple views via insertPages
  838. * the last page will be transitioned to, but the
  839. * others will not be. As a result, a DOM element
  840. * will only be created for the last page inserted.
  841. * As a result, it is possible to have views in the
  842. * stack that do not have `view.element` yet.
  843. */
  844. const element = view.element;
  845. if (element) {
  846. if (i > activeViewIndex) {
  847. // this view comes after the active view
  848. // let's unload it
  849. lifecycle(element, LIFECYCLE_WILL_UNLOAD);
  850. this.destroyView(view);
  851. }
  852. else if (i < activeViewIndex) {
  853. // this view comes before the active view
  854. // and it is not a portal then ensure it is hidden
  855. setPageHidden(element, true);
  856. }
  857. }
  858. }
  859. }
  860. canStart() {
  861. return (!this.gestureOrAnimationInProgress &&
  862. !!this.swipeGesture &&
  863. !this.isTransitioning &&
  864. this.transInstr.length === 0 &&
  865. this.canGoBackSync());
  866. }
  867. onStart() {
  868. this.gestureOrAnimationInProgress = true;
  869. this.pop({ direction: 'back', progressAnimation: true });
  870. }
  871. onMove(stepValue) {
  872. if (this.sbAni) {
  873. this.sbAni.progressStep(stepValue);
  874. }
  875. }
  876. onEnd(shouldComplete, stepValue, dur) {
  877. if (this.sbAni) {
  878. this.sbAni.onFinish(() => {
  879. this.gestureOrAnimationInProgress = false;
  880. }, { oneTimeCallback: true });
  881. // Account for rounding errors in JS
  882. let newStepValue = shouldComplete ? -0.001 : 0.001;
  883. /**
  884. * Animation will be reversed here, so need to
  885. * reverse the easing curve as well
  886. *
  887. * Additionally, we need to account for the time relative
  888. * to the new easing curve, as `stepValue` is going to be given
  889. * in terms of a linear curve.
  890. */
  891. if (!shouldComplete) {
  892. this.sbAni.easing('cubic-bezier(1, 0, 0.68, 0.28)');
  893. newStepValue += getTimeGivenProgression([0, 0], [1, 0], [0.68, 0.28], [1, 1], stepValue)[0];
  894. }
  895. else {
  896. newStepValue += getTimeGivenProgression([0, 0], [0.32, 0.72], [0, 1], [1, 1], stepValue)[0];
  897. }
  898. this.sbAni.progressEnd(shouldComplete ? 1 : 0, newStepValue, dur);
  899. }
  900. else {
  901. this.gestureOrAnimationInProgress = false;
  902. }
  903. }
  904. render() {
  905. return h("slot", { key: '188d0abd6c047d235380f07aac81223b757010e8' });
  906. }
  907. get el() { return this; }
  908. static get watchers() { return {
  909. "swipeGesture": ["swipeGestureChanged"],
  910. "root": ["rootChanged"]
  911. }; }
  912. static get style() { return IonNavStyle0; }
  913. }, [1, "ion-nav", {
  914. "delegate": [16],
  915. "swipeGesture": [1028, "swipe-gesture"],
  916. "animated": [4],
  917. "animation": [16],
  918. "rootParams": [16],
  919. "root": [1],
  920. "push": [64],
  921. "insert": [64],
  922. "insertPages": [64],
  923. "pop": [64],
  924. "popTo": [64],
  925. "popToRoot": [64],
  926. "removeIndex": [64],
  927. "setRoot": [64],
  928. "setPages": [64],
  929. "setRouteId": [64],
  930. "getRouteId": [64],
  931. "getActive": [64],
  932. "getByIndex": [64],
  933. "canGoBack": [64],
  934. "getPrevious": [64],
  935. "getLength": [64]
  936. }, undefined, {
  937. "swipeGesture": ["swipeGestureChanged"],
  938. "root": ["rootChanged"]
  939. }]);
  940. function defineCustomElement$1() {
  941. if (typeof customElements === "undefined") {
  942. return;
  943. }
  944. const components = ["ion-nav"];
  945. components.forEach(tagName => { switch (tagName) {
  946. case "ion-nav":
  947. if (!customElements.get(tagName)) {
  948. customElements.define(tagName, Nav);
  949. }
  950. break;
  951. } });
  952. }
  953. const IonNav = Nav;
  954. const defineCustomElement = defineCustomElement$1;
  955. export { IonNav, defineCustomElement };