UNPKG

38.7 kBJavaScriptView Raw
1/*
2Copyright 2013-2015 ASIAL CORPORATION
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15
16*/
17
18import onsElements from '../ons/elements.js';
19import util from '../ons/util.js';
20import internal from '../ons/internal/index.js';
21import SwipeReveal from '../ons/internal/swipe-reveal.js';
22import AnimatorFactory from '../ons/internal/animator-factory.js';
23import NavigatorAnimator from './ons-navigator/animator.js';
24import IOSSlideNavigatorAnimator from './ons-navigator/ios-slide-animator.js';
25import IOSLiftNavigatorAnimator from './ons-navigator/ios-lift-animator.js';
26import IOSFadeNavigatorAnimator from './ons-navigator/ios-fade-animator.js';
27import MDSlideNavigatorAnimator from './ons-navigator/md-slide-animator.js';
28import MDLiftNavigatorAnimator from './ons-navigator/md-lift-animator.js';
29import MDFadeNavigatorAnimator from './ons-navigator/md-fade-animator.js';
30import NoneNavigatorAnimator from './ons-navigator/none-animator.js';
31import platform from '../ons/platform.js';
32import contentReady from '../ons/content-ready.js';
33import BaseElement from './base/base-element.js';
34import deviceBackButtonDispatcher from '../ons/internal/device-back-button-dispatcher.js';
35import {PageLoader, defaultPageLoader, instantPageLoader} from '../ons/page-loader.js';
36
37const _animatorDict = {
38 'default': function () { return platform.isAndroid() ? MDFadeNavigatorAnimator : IOSSlideNavigatorAnimator; },
39 'slide': function () { return platform.isAndroid() ? MDSlideNavigatorAnimator : IOSSlideNavigatorAnimator; },
40 'lift': function () { return platform.isAndroid() ? MDLiftNavigatorAnimator : IOSLiftNavigatorAnimator; },
41 'fade': function () { return platform.isAndroid() ? MDFadeNavigatorAnimator : IOSFadeNavigatorAnimator; },
42 'slide-ios': IOSSlideNavigatorAnimator,
43 'slide-md': MDSlideNavigatorAnimator,
44 'lift-ios': IOSLiftNavigatorAnimator,
45 'lift-md': MDLiftNavigatorAnimator,
46 'fade-ios': IOSFadeNavigatorAnimator,
47 'fade-md': MDFadeNavigatorAnimator,
48 'none': NoneNavigatorAnimator
49};
50
51const rewritables = {
52 /**
53 * @param {Element} navigatorSideElement
54 * @param {Function} callback
55 */
56 ready(navigatorElement, callback) {
57 callback();
58 }
59};
60
61const verifyPageElement = el => (el.nodeName !== 'ONS-PAGE') && util.throw( 'Only page elements can be children of navigator');
62
63/**
64 * @element ons-navigator
65 * @category navigation
66 * @description
67 * [en]
68 * A component that provides page stack management and navigation. Stack navigation is the most common navigation pattern for mobile apps.
69 *
70 * When a page is pushed on top of the stack it is displayed with a transition animation. When the user returns to the previous page the top page will be popped from the top of the stack and hidden with an opposite transition animation.
71 * [/en]
72 * [ja][/ja]
73 * @codepen yrhtv
74 * @tutorial vanilla/Reference/navigator
75 * @guide lifecycle.html#events
76 * [en]Overview of page events[/en]
77 * [ja]Overview of page events[/ja]
78 * @seealso ons-toolbar
79 * [en]The `<ons-toolbar>` component is used to display a toolbar on the top of a page.[/en]
80 * [ja][/ja]
81 * @seealso ons-back-button
82 * [en]The `<ons-back-button>` component lets the user return to the previous page.[/en]
83 * [ja][/ja]
84 * @example
85 * <ons-navigator id="navigator">
86 * <ons-page>
87 * <ons-toolbar>
88 * <div class="center">
89 * Title
90 * </div>
91 * </ons-toolbar>
92 * <p>
93 * <ons-button
94 * onclick="document.getElementById('navigator').pushPage('page.html')">
95 * Push page
96 * </ons-button>
97 * </p>
98 * </ons-page>
99 * </ons-navigator>
100 *
101 * <template id="page.html">
102 * <ons-page>
103 * <ons-toolbar>
104 * <div class="left">
105 * <ons-back-button>Back</ons-back-button>
106 * </div>
107 * <div class="center">
108 * Another page
109 * </div>
110 * </ons-toolbar>
111 * </ons-page>
112 * </template>
113 */
114export default class NavigatorElement extends BaseElement {
115
116 /**
117 * @attribute page
118 * @initonly
119 * @type {String}
120 * @description
121 * [en]First page to show when navigator is initialized.[/en]
122 * [ja]ナビゲーターが初期化された時に表示するページを指定します。[/ja]
123 */
124
125 /**
126 * @attribute swipeable
127 * @type {Boolean}
128 * @description
129 * [en]Enable iOS "swipe to pop" feature.[/en]
130 * [ja][/ja]
131 */
132
133 /**
134 * @attribute swipe-target-width
135 * @type {String}
136 * @default 20px
137 * @description
138 * [en]The width of swipeable area calculated from the edge (in pixels). Use this to enable swipe only when the finger touch on the screen edge.[/en]
139 * [ja]スワイプの判定領域をピクセル単位で指定します。画面の端から指定した距離に達するとページが表示されます。[/ja]
140 */
141
142 /**
143 * @attribute swipe-threshold
144 * @type {Number}
145 * @default 0.2
146 * @description
147 * [en]Specify how much the page needs to be swiped before popping. A value between `0` and `1`.[/en]
148 * [ja][/ja]
149 */
150
151 /**
152 * @attribute animation
153 * @type {String}
154 * @default default
155 * @description
156 * [en]
157 * Animation name. Available animations are `"slide"`, `"lift"`, `"fade"` and `"none"`.
158 *
159 * These are platform based animations. For fixed animations, add `"-ios"` or `"-md"` suffix to the animation name. E.g. `"lift-ios"`, `"lift-md"`. Defaults values are `"slide-ios"` and `"fade-md"` depending on the platform.
160 * [/en]
161 * [ja][/ja]
162 */
163
164 /**
165 * @attribute animation-options
166 * @type {Expression}
167 * @description
168 * [en]Specify the animation's duration, timing and delay with an object literal. E.g. `{duration: 0.2, delay: 1, timing: 'ease-in'}`[/en]
169 * [ja]アニメーション時のduration, timing, delayをオブジェクトリテラルで指定します。e.g. `{duration: 0.2, delay: 1, timing: 'ease-in'}`[/ja]
170 */
171
172 /**
173 * @property animationOptions
174 * @type {Object}
175 * @description
176 * [en]Specify the animation's duration, timing and delay with an object literal. E.g. `{duration: 0.2, delay: 1, timing: 'ease-in'}`[/en]
177 * [ja]アニメーション時のduration, timing, delayをオブジェクトリテラルで指定します。e.g. `{duration: 0.2, delay: 1, timing: 'ease-in'}`[/ja]
178 */
179
180 /**
181 * @event prepush
182 * @description
183 * [en]Fired just before a page is pushed.[/en]
184 * [ja]pageがpushされる直前に発火されます。[/ja]
185 * @param {Object} event [en]Event object.[/en]
186 * @param {Object} event.navigator
187 * [en]Component object.[/en]
188 * [ja]コンポーネントのオブジェクト。[/ja]
189 * @param {Object} event.currentPage
190 * [en]Current page object.[/en]
191 * [ja]現在のpageオブジェクト。[/ja]
192 * @param {Function} event.cancel
193 * [en]Call this function to cancel the push.[/en]
194 * [ja]この関数を呼び出すと、push処理がキャンセルされます。[/ja]
195 */
196
197 /**
198 * @event prepop
199 * @description
200 * [en]Fired just before a page is popped.[/en]
201 * [ja]pageがpopされる直前に発火されます。[/ja]
202 * @param {Object} event [en]Event object.[/en]
203 * @param {Object} event.navigator
204 * [en]Component object.[/en]
205 * [ja]コンポーネントのオブジェクト。[/ja]
206 * @param {Object} event.currentPage
207 * [en]Current page object.[/en]
208 * [ja]現在のpageオブジェクト。[/ja]
209 * @param {Function} event.cancel
210 * [en]Call this function to cancel the pop.[/en]
211 * [ja]この関数を呼び出すと、pageのpopがキャンセルされます。[/ja]
212 */
213
214 /**
215 * @event postpush
216 * @description
217 * [en]Fired just after a page is pushed.[/en]
218 * [ja]pageがpushされてアニメーションが終了してから発火されます。[/ja]
219 * @param {Object} event [en]Event object.[/en]
220 * @param {Object} event.navigator
221 * [en]Component object.[/en]
222 * [ja]コンポーネントのオブジェクト。[/ja]
223 * @param {Object} event.enterPage
224 * [en]Object of the next page.[/en]
225 * [ja]pushされたpageオブジェクト。[/ja]
226 * @param {Object} event.leavePage
227 * [en]Object of the previous page.[/en]
228 * [ja]以前のpageオブジェクト。[/ja]
229 */
230
231 /**
232 * @event postpop
233 * @description
234 * [en]Fired just after a page is popped.[/en]
235 * [ja]pageがpopされてアニメーションが終わった後に発火されます。[/ja]
236 * @param {Object} event [en]Event object.[/en]
237 * @param {Object} event.navigator
238 * [en]Component object.[/en]
239 * [ja]コンポーネントのオブジェクト。[/ja]
240 * @param {Object} event.enterPage
241 * [en]Object of the next page.[/en]
242 * [ja]popされて表示されるページのオブジェクト。[/ja]
243 * @param {Object} event.leavePage
244 * [en]Object of the previous page.[/en]
245 * [ja]popされて消えるページのオブジェクト。[/ja]
246 * @param {Object} event.swipeToPop
247 * [en]True if the pop was triggered by the user swiping to pop.[/en]
248 * [ja][/ja]
249 * @param {Object} event.onsBackButton
250 * [en]True if the pop was caused by pressing an ons-back-button.[/en]
251 * [ja][/ja]
252 */
253
254 /**
255 * @event swipe
256 * @description
257 * [en]Fired whenever the user slides the navigator (swipe-to-pop).[/en]
258 * [ja][/ja]
259 * @param {Object} event [en]Event object.[/en]
260 * @param {Object} event.ratio
261 * [en]Decimal ratio (0-1).[/en]
262 * [ja][/ja]
263 * @param {Object} event.animationOptions
264 * [en][/en]
265 * [ja][/ja]
266 */
267
268 get animatorFactory() {
269 return this._animatorFactory;
270 }
271
272 constructor() {
273 super();
274
275 this._isRunning = false;
276 this._initialized = false;
277 this._pageLoader = defaultPageLoader;
278 this._pageMap = new WeakMap();
279
280 this._updateAnimatorFactory();
281 }
282
283 /**
284 * @property pageLoader
285 * @type {PageLoader}
286 * @description
287 * [en]PageLoader instance. It can be overriden to change the way pages are loaded by this element. Useful for lib developers.[/en]
288 * [ja]PageLoaderインスタンスを格納しています。[/ja]
289 */
290 get pageLoader() {
291 return this._pageLoader;
292 }
293
294 set pageLoader(pageLoader) {
295 if (!(pageLoader instanceof PageLoader)) {
296 util.throwPageLoader();
297 }
298 this._pageLoader = pageLoader;
299 }
300
301 _getPageTarget() {
302 return this._page || this.getAttribute('page');
303 }
304
305 /**
306 * @property page
307 * @type {*}
308 * @description
309 * [en]Specify the page to be loaded during initialization. This value takes precedence over the `page` attribute. Useful for lib developers.[/en]
310 * [ja]初期化時に読み込むページを指定します。`page`属性で指定した値よりも`page`プロパティに指定した値を優先します。[/ja]
311 */
312 get page() {
313 return this._page;
314 }
315
316 set page(page) {
317 this._page = page;
318 }
319
320 connectedCallback() {
321 this.onDeviceBackButton = this._onDeviceBackButton.bind(this);
322
323 if (!platform.isAndroid() || this.getAttribute('swipeable') === 'force') {
324 let swipeAnimator;
325
326 this._swipe = new SwipeReveal({
327 element: this,
328 getThreshold: () => Math.max(0.2, parseFloat(this.getAttribute('swipe-threshold')) || 0),
329
330 swipeMax: () => {
331 const ratio = 1;
332 const animationOptions = { duration: swipeAnimator.durationSwipe, timing: swipeAnimator.timingSwipe };
333 this._onSwipe && this._onSwipe(ratio, animationOptions);
334 util.triggerElementEvent(this, 'swipe', { ratio, animationOptions });
335 this[this.swipeMax ? 'swipeMax' : 'popPage']({ animator: swipeAnimator, swipeToPop: true });
336 swipeAnimator = null;
337 },
338 swipeMid: (distance, width) => {
339 const ratio = distance / width;
340 this._onSwipe && this._onSwipe(ratio);
341 util.triggerElementEvent(this, 'swipe', { ratio });
342 swipeAnimator.translate(distance, width, this.topPage.previousElementSibling, this.topPage);
343 },
344 swipeMin: () => {
345 const ratio = 0;
346 const animationOptions = { duration: swipeAnimator.durationRestore, timing: swipeAnimator.timingSwipe };
347 this._onSwipe && this._onSwipe(ratio, animationOptions);
348 util.triggerElementEvent(this, 'swipe', { ratio, animationOptions });
349 swipeAnimator.restore(this.topPage.previousElementSibling, this.topPage);
350 swipeAnimator = null;
351 },
352
353 ignoreSwipe: (event, distance) => {
354 // Basic conditions
355 if (!this._isRunning && this.children.length > 1) {
356
357 // Area or directional issues
358 const area = parseInt(this.getAttribute('swipe-target-width') || 25, 10);
359 if (event.gesture.direction === 'right' && area > distance) {
360
361 // Swipes on ons-back-button and its children
362 const isBB = el => /ons-back-button/i.test(el.tagName);
363 if (!isBB(event.target) && !util.findParent(event.target, isBB, p => /ons-page/i.test(p.tagName))) {
364
365 // Animator is swipeable
366 const animation = (this.topPage.pushedOptions || {}).animation || this.animatorFactory._animation;
367 const Animator = _animatorDict[animation] instanceof Function
368 ? _animatorDict[animation].call()
369 : _animatorDict[animation];
370
371 if (typeof Animator !== 'undefined' && Animator.swipeable) {
372 swipeAnimator = new Animator(); // Prepare for the swipe
373 return false;
374 }
375 }
376 }
377 }
378
379 return true; // Ignore swipe
380 }
381 });
382
383 this.attributeChangedCallback('swipeable');
384 }
385
386 if (this._initialized) {
387 return;
388 }
389
390 this._initialized = true;
391
392 const deferred = util.defer();
393 this.loaded = deferred.promise;
394
395 rewritables.ready(this, () => {
396 const show = !util.hasAnyComponentAsParent(this);
397 const options = { animation: 'none', show };
398
399 if (this.pages.length === 0 && this._getPageTarget()) {
400 this.pushPage(this._getPageTarget(), options).then(() => deferred.resolve());
401 } else if (this.pages.length > 0) {
402 for (var i = 0; i < this.pages.length; i++) {
403 verifyPageElement(this.pages[i]);
404 }
405
406 if (this.topPage) {
407 contentReady(this.topPage, () =>
408 setTimeout(() => {
409 deferred.resolve();
410 show && this.topPage._show();
411 this._updateLastPageBackButton();
412 }, 0)
413 );
414 }
415 } else {
416 contentReady(this, () => {
417 if (this.pages.length === 0 && this._getPageTarget()) {
418 this.pushPage(this._getPageTarget(), options).then(() => deferred.resolve());
419 } else {
420 deferred.resolve();
421 }
422 });
423 }
424 });
425 }
426
427 _updateAnimatorFactory() {
428 this._animatorFactory = new AnimatorFactory({
429 animators: _animatorDict,
430 baseClass: NavigatorAnimator,
431 baseClassName: 'NavigatorAnimator',
432 defaultAnimation: this.getAttribute('animation')
433 });
434 }
435
436 disconnectedCallback() {
437 this._backButtonHandler.destroy();
438 this._backButtonHandler = null;
439
440 this._swipe && this._swipe.dispose();
441 this._swipe = null;
442 }
443
444 static get observedAttributes() {
445 return ['animation', 'swipeable'];
446 }
447
448 attributeChangedCallback(name, last, current) {
449 switch (name) {
450 case 'animation':
451 this._updateAnimatorFactory();
452 break;
453 case 'swipeable':
454 this._swipe && this._swipe.update();
455 break;
456 }
457 }
458
459 /**
460 * @method popPage
461 * @signature popPage([options])
462 * @param {Object} [options]
463 * [en]Parameter object.[/en]
464 * [ja]オプションを指定するオブジェクト。[/ja]
465 * @param {String} [options.animation]
466 * [en]
467 * Animation name. Available animations are `"slide"`, `"lift"`, `"fade"` and `"none"`.
468 *
469 * These are platform based animations. For fixed animations, add `"-ios"` or `"-md"` suffix to the animation name. E.g. `"lift-ios"`, `"lift-md"`. Defaults values are `"slide-ios"` and `"fade-md"`.
470 * [/en]
471 * [ja][/ja]
472 * @param {String} [options.animationOptions]
473 * [en]Specify the animation's duration, delay and timing. E.g. `{duration: 0.2, delay: 0.4, timing: 'ease-in'}`.[/en]
474 * [ja]アニメーション時のduration, delay, timingを指定します。e.g. {duration: 0.2, delay: 0.4, timing: 'ease-in'}[/ja]
475 * @param {Function} [options.callback]
476 * [en]Function that is called when the transition has ended.[/en]
477 * [ja]このメソッドによる画面遷移が終了した際に呼び出される関数オブジェクトを指定します。[/ja]
478 * @param {Object} [options.data]
479 * [en]Custom data that will be stored in the new page element.[/en]
480 * [ja][/ja]
481 * @param {Number} [options.times]
482 * [en]Number of pages to be popped. Only one animation will be shown.[/en]
483 * [ja][/ja]
484 * @return {Promise}
485 * [en]Promise which resolves to the revealed page.[/en]
486 * [ja]明らかにしたページを解決するPromiseを返します。[/ja]
487 * @description
488 * [en]Pops the current page from the page stack. The previous page will be displayed.[/en]
489 * [ja]現在表示中のページをページスタックから取り除きます。一つ前のページに戻ります。[/ja]
490 */
491 popPage(options = {}) {
492 ({options} = this._preparePageAndOptions(null, options));
493
494 if (util.isInteger(options.times) && options.times > 1) {
495 this._removePages(options.times);
496 }
497
498 const popUpdate = () => new Promise((resolve) => {
499 this._pageLoader.unload(this.pages[this.pages.length - 1]);
500 resolve();
501 });
502
503 return this._popPage(options, popUpdate);
504 }
505
506 _popPage(options, update = () => Promise.resolve()) {
507 if (this._isRunning) {
508 return Promise.reject('popPage is already running.');
509 }
510
511 if (this.pages.length <= 1) {
512 return Promise.reject('ons-navigator\'s page stack is empty.');
513 }
514
515 if (this._emitPrePopEvent()) {
516 return Promise.reject('Canceled in prepop event.');
517 }
518
519 const length = this.pages.length;
520
521 this._isRunning = true;
522
523 this.pages[length - 2].updateBackButton((length - 2) > 0);
524
525 return new Promise(resolve => {
526 const leavePage = this.pages[length - 1];
527 const enterPage = this.pages[length - 2];
528
529 options = util.extend({}, this.options || {}, options);
530
531 if (options.data) {
532 enterPage.data = util.extend({}, enterPage.data || {}, options.data || {});
533 }
534
535 const done = () => {
536 update().then(() => {
537 this._isRunning = false;
538
539 enterPage._show();
540 util.triggerElementEvent(this, 'postpop', {
541 leavePage,
542 enterPage,
543 navigator: this,
544 swipeToPop: !!options.swipeToPop, // whether the pop was triggered by the user swiping
545 onsBackButton: !!options.onsBackButton // whether the pop was triggered by clicking ons-back-button
546 });
547
548 options.callback && options.callback(enterPage);
549
550 resolve(enterPage);
551 });
552 };
553
554 leavePage._hide();
555 enterPage.style.display = '';
556
557 const animator = options.animator || this._animatorFactory.newAnimator(options);
558 animator.pop(this.pages[length - 2], this.pages[length - 1], done);
559 }).catch(() => this._isRunning = false);
560 }
561
562
563 /**
564 * @method pushPage
565 * @signature pushPage(page, [options])
566 * @param {String} page
567 * [en]Page URL. Can be either a HTML document or a template defined with the `<template>` tag.[/en]
568 * [ja]pageのURLか、もしくは`<template>`で宣言したテンプレートのid属性の値を指定できます。[/ja]
569 * @param {Object} [options]
570 * [en]Parameter object.[/en]
571 * [ja]オプションを指定するオブジェクト。[/ja]
572 * @param {String} [options.page]
573 * [en]Page URL. Only necessary if `page` parameter is null or undefined.[/en]
574 * [ja][/ja]
575 * @param {String} [options.pageHTML]
576 * [en]HTML code that will be computed as a new page. Overwrites `page` parameter.[/en]
577 * [ja][/ja]
578 * @param {String} [options.animation]
579 * [en]
580 * Animation name. Available animations are `"slide"`, `"lift"`, `"fade"` and `"none"`.
581 *
582 * These are platform based animations. For fixed animations, add `"-ios"` or `"-md"` suffix to the animation name. E.g. `"lift-ios"`, `"lift-md"`. Defaults values are `"slide-ios"` and `"fade-md"`.
583 * [/en]
584 * [ja][/ja]
585 * @param {String} [options.animationOptions]
586 * [en]Specify the animation's duration, delay and timing. E.g. `{duration: 0.2, delay: 0.4, timing: 'ease-in'}`[/en]
587 * [ja]アニメーション時のduration, delay, timingを指定します。e.g. `{duration: 0.2, delay: 0.4, timing: 'ease-in'}` [/ja]
588 * @param {Function} [options.callback]
589 * [en]Function that is called when the transition has ended.[/en]
590 * [ja]pushPage()による画面遷移が終了した時に呼び出される関数オブジェクトを指定します。[/ja]
591 * @param {Object} [options.data]
592 * [en]Custom data that will be stored in the new page element.[/en]
593 * [ja][/ja]
594 * @return {Promise}
595 * [en]Promise which resolves to the pushed page.[/en]
596 * [ja]追加したページを解決するPromiseを返します。[/ja]
597 * @description
598 * [en]Pushes the specified page into the stack.[/en]
599 * [ja]指定したpageを新しいページスタックに追加します。新しいページが表示されます。[/ja]
600 */
601 pushPage(page, options = {}) {
602 ({page, options} = this._preparePageAndOptions(page, options));
603
604 const prepare = pageElement => {
605 verifyPageElement(pageElement);
606 this._pageMap.set(pageElement, page);
607 pageElement = util.extend(pageElement, {
608 data: options.data
609 });
610 pageElement.style.visibility = 'hidden';
611 };
612
613 if (options.pageHTML) {
614 return this._pushPage(options, () => new Promise(resolve => {
615 instantPageLoader.load({page: options.pageHTML, parent: this, params: options.data}, pageElement => {
616 prepare(pageElement);
617 resolve();
618 });
619 }));
620 }
621
622 return this._pushPage(options, () => new Promise(resolve => {
623 this._pageLoader.load({page, parent: this, params: options.data}, pageElement => {
624 prepare(pageElement);
625 resolve();
626 }, error => {
627 this._isRunning = false;
628 throw error;
629 });
630 }));
631 }
632
633 _pushPage(options = {}, update = () => Promise.resolve()) {
634 if (this._isRunning) {
635 return Promise.reject('pushPage is already running.');
636 }
637
638 if (this._emitPrePushEvent()) {
639 return Promise.reject('Canceled in prepush event.');
640 }
641
642 this._isRunning = true;
643
644 const animationOptions = this.animationOptions;
645 options = util.extend({}, this.options || {}, {animationOptions}, options);
646
647 const animator = this._animatorFactory.newAnimator(options);
648
649 return update().then(() => {
650 const pageLength = this.pages.length;
651
652 const enterPage = this.pages[pageLength - 1];
653 const leavePage = options.leavePage || this.pages[pageLength - 2];
654
655 verifyPageElement(enterPage);
656
657 enterPage.updateBackButton(pageLength > (options._replacePage ? 2 : 1));
658
659 enterPage.pushedOptions = util.extend({}, enterPage.pushedOptions || {}, options || {});
660 enterPage.data = util.extend({}, enterPage.data || {}, options.data || {});
661 enterPage.unload = enterPage.unload || options.unload;
662
663 return new Promise(resolve => {
664 const done = () => {
665 this._isRunning = false;
666
667 options.show !== false && setImmediate(() => enterPage._show());
668 util.triggerElementEvent(this, 'postpush', {leavePage, enterPage, navigator: this});
669
670 if (leavePage) {
671 leavePage.style.display = 'none';
672 }
673
674 options.callback && options.callback(enterPage);
675
676 resolve(enterPage);
677 };
678
679 enterPage.style.visibility = '';
680 if (leavePage) {
681 leavePage._hide();
682 animator.push(enterPage, leavePage, done);
683 } else {
684 done();
685 }
686 });
687 }).catch((error) => {
688 this._isRunning = false;
689 throw error;
690 });
691 }
692
693 /**
694 * @method replacePage
695 * @signature replacePage(page, [options])
696 * @return {Promise}
697 * [en]Promise which resolves to the new page.[/en]
698 * [ja]新しいページを解決するPromiseを返します。[/ja]
699 * @description
700 * [en]Replaces the current top page with the specified one. Extends `pushPage()` parameters.[/en]
701 * [ja]現在表示中のページをを指定したページに置き換えます。[/ja]
702 */
703 replacePage(page, options = {}) {
704 return this.pushPage(page, options)
705 .then(resolvedValue => {
706 if (this.pages.length > 1) {
707 this._pageLoader.unload(this.pages[this.pages.length - 2]);
708 }
709 this._updateLastPageBackButton();
710
711 return Promise.resolve(resolvedValue);
712 });
713 }
714
715 /**
716 * @method insertPage
717 * @signature insertPage(index, page, [options])
718 * @param {Number} index
719 * [en]The index where it should be inserted.[/en]
720 * [ja]スタックに挿入する位置のインデックスを指定します。[/ja]
721 * @return {Promise}
722 * [en]Promise which resolves to the inserted page.[/en]
723 * [ja]指定したページを解決するPromiseを返します。[/ja]
724 * @description
725 * [en]Insert the specified page into the stack with at a position defined by the `index` argument. Extends `pushPage()` parameters.[/en]
726 * [ja]指定したpageをページスタックのindexで指定した位置に追加します。[/ja]
727 */
728 insertPage(index, page, options = {}) {
729 ({page, options} = this._preparePageAndOptions(page, options));
730 index = this._normalizeIndex(index);
731
732 if (index >= this.pages.length) {
733 return this.pushPage(page, options);
734 }
735
736 page = typeof options.pageHTML === 'string' ? options.pageHTML : page;
737 const loader = typeof options.pageHTML === 'string' ? instantPageLoader : this._pageLoader;
738
739 return new Promise(resolve => {
740 loader.load({page, parent: this}, pageElement => {
741 verifyPageElement(pageElement);
742 this._pageMap.set(pageElement, page);
743 pageElement = util.extend(pageElement, {
744 data: options.data,
745 pushedOptions: options
746 });
747
748 options.animationOptions = util.extend(
749 {},
750 this.animationOptions,
751 options.animationOptions || {}
752 );
753
754 pageElement.style.display = 'none';
755 this.insertBefore(pageElement, this.pages[index]);
756 this.topPage.updateBackButton(true);
757
758 setTimeout(() => {
759 pageElement = null;
760 resolve(this.pages[index]);
761 }, 1000 / 60);
762 });
763 });
764 }
765
766 /**
767 * @method removePage
768 * @signature removePage(index, [options])
769 * @param {Number} index
770 * [en]The index where it should be removed.[/en]
771 * [ja]スタックから削除するページのインデックスを指定します。[/ja]
772 * @return {Promise}
773 * [en]Promise which resolves to the revealed page.[/en]
774 * [ja]削除によって表示されたページを解決するPromiseを返します。[/ja]
775 * @description
776 * [en]Remove the specified page at a position in the stack defined by the `index` argument. Extends `popPage()` parameters.[/en]
777 * [ja]指定したインデックスにあるページを削除します。[/ja]
778 */
779 removePage(index, options = {}) {
780 index = this._normalizeIndex(index);
781
782 if (index < this.pages.length - 1) {
783 return new Promise(resolve => {
784 const leavePage = this.pages[index];
785 const enterPage = this.topPage;
786
787 this._pageMap.delete(leavePage);
788 this._pageLoader.unload(leavePage);
789 if (this.pages.length === 1) { // edge case
790 this.topPage.updateBackButton(false);
791 }
792
793 resolve(enterPage);
794 });
795 } else {
796 return this.popPage(options);
797 }
798 }
799
800 /**
801 * @method resetToPage
802 * @signature resetToPage(page, [options])
803 * @return {Promise}
804 * [en]Promise which resolves to the new top page.[/en]
805 * [ja]新しいトップページを解決するPromiseを返します。[/ja]
806 * @param {Boolean} [options.pop]
807 * [en]Performs 'pop' effect if `true` instead of 'push' or none. This also sets `options.animation` value to `default` instead of `none`.[/en]
808 * [ja][/ja]
809 * @description
810 * [en]Clears page stack and adds the specified page to the stack. Extends `pushPage()` parameters.[/en]
811 * [ja]ページスタックをリセットし、指定したページを表示します。[/ja]
812 */
813 resetToPage(page, options = {}) {
814 ({page, options} = this._preparePageAndOptions(page, options));
815
816 if (!options.animator && !options.animation && !options.pop) {
817 options.animation = 'none';
818 }
819
820 if (!options.page && !options.pageHTML && this._getPageTarget()) {
821 page = options.page = this._getPageTarget();
822 }
823
824 if (options.pop) {
825 this._removePages();
826 return this.insertPage(0, page, { data: options.data })
827 .then(() => this.popPage(options));
828 }
829
830 // Tip: callback runs before resolved promise
831 const callback = options.callback;
832 options.callback = newPage => {
833 this._removePages();
834 newPage.updateBackButton(false);
835 callback && callback(newPage);
836 };
837
838 return this.pushPage(page, options);
839 }
840
841 /**
842 * @method bringPageTop
843 * @signature bringPageTop(item, [options])
844 * @param {String|Number} item
845 * [en]Page URL or index of an existing page in navigator's stack.[/en]
846 * [ja]ページのURLかもしくはons-navigatorのページスタックのインデックス値を指定します。[/ja]
847 * @return {Promise}
848 * [en]Promise which resolves to the new top page.[/en]
849 * [ja]新しいトップページを解決するPromiseを返します。[/ja]
850 * @description
851 * [en]Brings the given page to the top of the page stack if it already exists or pushes it into the stack if doesn't. Extends `pushPage()` parameters.[/en]
852 * [ja]指定したページをページスタックの一番上に移動します。もし指定したページが無かった場合新しくpushされます。[/ja]
853 */
854 bringPageTop(item, options = {}) {
855 if (['number', 'string'].indexOf(typeof item) === -1) {
856 util.throw('First argument must be a page name or the index of an existing page. You supplied ' + item);
857 }
858 const index = typeof item === 'number' ? this._normalizeIndex(item) : this._lastIndexOfPage(item);
859 const page = this.pages[index];
860
861 if (index < 0) {
862 return this.pushPage(item, options);
863 }
864 ({options} = this._preparePageAndOptions(page, options));
865
866 if (index === this.pages.length - 1) {
867 return Promise.resolve(page);
868 }
869 if (!page) {
870 util.throw('Failed to find item ' + item);
871 }
872 if (this._isRunning) {
873 return Promise.reject('pushPage is already running.');
874 }
875 if (this._emitPrePushEvent()) {
876 return Promise.reject('Canceled in prepush event.');
877 }
878
879 page.style.display = '';
880 page.style.visibility = 'hidden';
881 page.parentNode.appendChild(page);
882 return this._pushPage(options);
883 }
884
885 _preparePageAndOptions(page, options = {}) {
886 if (typeof options != 'object') {
887 util.throw('options must be an object. You supplied ' + options);
888 }
889
890 if ((page === null || page === undefined) && options.page) {
891 page = options.page;
892 }
893
894 options = util.extend({}, this.options || {}, options, {page});
895
896 return {page, options};
897 }
898
899 _removePages(times) {
900 const pages = this.pages;
901 let until = times === undefined ? 0 : pages.length - times;
902 until = until < 0 ? 1 : until;
903
904 for (let i = pages.length - 2; i >= until; i--) {
905 this._pageMap.delete(pages[i]);
906 this._pageLoader.unload(pages[i]);
907 }
908 }
909
910 _updateLastPageBackButton() {
911 const index = this.pages.length - 1;
912 if (index >= 0) {
913 this.pages[index].updateBackButton(index > 0);
914 }
915 }
916
917 _normalizeIndex(index) {
918 return index >= 0 ? index : Math.abs(this.pages.length + index) % this.pages.length;
919 }
920
921 _onDeviceBackButton(event) {
922 if (this.pages.length > 1) {
923 this.popPage();
924 } else {
925 event.callParentHandler();
926 }
927 }
928
929 _lastIndexOfPage(pageName) {
930 let index;
931 for (index = this.pages.length - 1; index >= 0; index--) {
932 if (pageName === this._pageMap.get(this.pages[index])) {
933 break;
934 }
935 }
936 return index;
937 }
938
939 _emitPreEvent(name, data = {}) {
940 let isCanceled = false;
941
942 util.triggerElementEvent(this, 'pre' + name, util.extend({
943 navigator: this,
944 currentPage: this.pages[this.pages.length - 1],
945 cancel: () => isCanceled = true
946 }, data));
947
948 return isCanceled;
949 }
950
951 _emitPrePushEvent() {
952 return this._emitPreEvent('push');
953 }
954
955 _emitPrePopEvent() {
956 const l = this.pages.length;
957 return this._emitPreEvent('pop', {
958 leavePage: this.pages[l - 1],
959 enterPage: this.pages[l - 2]
960 });
961 }
962
963 // TODO: 書き直す
964 _createPageElement(templateHTML) {
965 const pageElement = util.createElement(internal.normalizePageHTML(templateHTML));
966 verifyPageElement(pageElement);
967 return pageElement;
968 }
969
970 /**
971 * @property onDeviceBackButton
972 * @type {Object}
973 * @description
974 * [en]Back-button handler.[/en]
975 * [ja]バックボタンハンドラ。[/ja]
976 */
977 get onDeviceBackButton() {
978 return this._backButtonHandler;
979 }
980
981 set onDeviceBackButton(callback) {
982 if (this._backButtonHandler) {
983 this._backButtonHandler.destroy();
984 }
985
986 this._backButtonHandler = deviceBackButtonDispatcher.createHandler(this, callback);
987 }
988
989 /**
990 * @property topPage
991 * @readonly
992 * @type {HTMLElement}
993 * @description
994 * [en]Current top page element. Use this method to access options passed by `pushPage()`-like methods.[/en]
995 * [ja]現在のページを取得します。pushPage()やresetToPage()メソッドの引数を取得できます。[/ja]
996 */
997 get topPage() {
998 let last = this.lastElementChild;
999 while (last && last.tagName !== 'ONS-PAGE') { last = last.previousElementSibling; }
1000 return last;
1001 }
1002
1003 /**
1004 * @property pages
1005 * @readonly
1006 * @type {Array}
1007 * @description
1008 * [en]Copy of the navigator's page stack.[/en]
1009 * [ja][/ja]
1010 */
1011 get pages() {
1012 return util.arrayFrom(this.children)
1013 .filter(element => element.tagName === 'ONS-PAGE');
1014 }
1015
1016 /**
1017 * @property onSwipe
1018 * @type {Function}
1019 * @description
1020 * [en]Hook called whenever the user slides the navigator (swipe-to-pop). It gets a decimal ratio (0-1) and an animationOptions object as arguments.[/en]
1021 * [ja][/ja]
1022 */
1023 get onSwipe() {
1024 return this._onSwipe;
1025 }
1026
1027 set onSwipe(value) {
1028 if (value && !(value instanceof Function)) {
1029 util.throw('"onSwipe" must be a function');
1030 }
1031 this._onSwipe = value;
1032 }
1033
1034 /**
1035 * @property options
1036 * @type {Object}
1037 * @description
1038 * [en]Default options object. Attributes have priority over this property.[/en]
1039 * [ja][/ja]
1040 */
1041
1042 /**
1043 * @property options.animation
1044 * @type {String}
1045 * @description
1046 * [en]
1047 * Animation name. Available animations are `"slide"`, `"lift"`, `"fade"` and `"none"`.
1048 * These are platform based animations. For fixed animations, add `"-ios"` or `"-md"` suffix to the animation name. E.g. `"lift-ios"`, `"lift-md"`. Defaults values are `"slide-ios"` and `"fade-md"`.
1049 * [/en]
1050 * [ja][/ja]
1051 */
1052
1053 /**
1054 * @property options.animationOptions
1055 * @type {String}
1056 * @description
1057 * [en]Specify the animation's duration, delay and timing. E.g. `{duration: 0.2, delay: 0.4, timing: 'ease-in'}`.[/en]
1058 * [ja]アニメーション時のduration, delay, timingを指定します。e.g. `{duration: 0.2, delay: 0.4, timing: 'ease-in'}` [/ja]
1059 */
1060
1061 /**
1062 * @property options.callback
1063 * @type {String}
1064 * @description
1065 * [en]Function that is called when the transition has ended.[/en]
1066 * [ja]このメソッドによる画面遷移が終了した際に呼び出される関数オブジェクトを指定します。[/ja]
1067 */
1068
1069 get options() {
1070 return this._options;
1071 }
1072 set options(object) {
1073 this._options = object;
1074 }
1075
1076 get animationOptions() {
1077 return this.hasAttribute('animation-options') ?
1078 AnimatorFactory.parseAnimationOptionsString(this.getAttribute('animation-options')) : {};
1079 }
1080
1081 set animationOptions(value) {
1082 if (value === undefined || value === null) {
1083 this.removeAttribute('animation-options');
1084 } else {
1085 this.setAttribute('animation-options', JSON.stringify(value));
1086 }
1087 }
1088
1089 set _isRunning(value) {
1090 this.setAttribute('_is-running', value ? 'true' : 'false');
1091 }
1092 get _isRunning() {
1093 return JSON.parse(this.getAttribute('_is-running'));
1094 }
1095
1096 _show() {
1097 this.loaded.then(() => this.topPage && this.topPage._show());
1098 }
1099
1100 _hide() {
1101 this.topPage && this.topPage._hide();
1102 }
1103
1104 _destroy() {
1105 for (let i = this.pages.length - 1; i >= 0; i--) {
1106 this._pageLoader.unload(this.pages[i]);
1107 }
1108
1109 this.remove();
1110 }
1111
1112 /**
1113 * @param {String} name
1114 * @param {Function} Animator
1115 */
1116 static registerAnimator(name, Animator) {
1117 if (!(Animator.prototype instanceof NavigatorAnimator)) {
1118 util.throwAnimator('Navigator');
1119 }
1120
1121 _animatorDict[name] = Animator;
1122 }
1123
1124 static get animators() {
1125 return _animatorDict;
1126 }
1127
1128 static get NavigatorAnimator() {
1129 return NavigatorAnimator;
1130 }
1131
1132 static get events() {
1133 return ['prepush', 'postpush', 'prepop', 'postpop', 'swipe'];
1134 }
1135
1136 static get rewritables() {
1137 return rewritables;
1138 }
1139}
1140
1141onsElements.Navigator = NavigatorElement;
1142customElements.define('ons-navigator', NavigatorElement);