1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | import onsElements from '../ons/elements.js';
|
19 | import util from '../ons/util.js';
|
20 | import internal from '../ons/internal/index.js';
|
21 | import autoStyle from '../ons/autostyle.js';
|
22 | import Swiper from '../ons/internal/swiper.js';
|
23 | import ModifierUtil from '../ons/internal/modifier-util.js';
|
24 | import BaseElement from './base/base-element.js';
|
25 | import contentReady from '../ons/content-ready.js';
|
26 |
|
27 | const scheme = {
|
28 | '.tabbar__content': 'tabbar--*__content',
|
29 | '.tabbar__border': 'tabbar--*__border',
|
30 | '.tabbar': 'tabbar--*'
|
31 | };
|
32 |
|
33 | const rewritables = {
|
34 | |
35 |
|
36 |
|
37 |
|
38 | ready(tabbarElement, callback) {
|
39 | callback();
|
40 | }
|
41 | };
|
42 |
|
43 | const nullPage = internal.nullElement;
|
44 | const lerp = (x0, x1, t) => (1 - t) * x0 + t * x1;
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 |
|
93 |
|
94 | export default class TabbarElement extends BaseElement {
|
95 |
|
96 | |
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 | |
116 |
|
117 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
131 | |
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 |
|
146 |
|
147 | |
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 |
|
160 |
|
161 |
|
162 |
|
163 | |
164 |
|
165 |
|
166 |
|
167 |
|
168 |
|
169 |
|
170 |
|
171 |
|
172 | |
173 |
|
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 |
|
180 | |
181 |
|
182 |
|
183 |
|
184 |
|
185 |
|
186 |
|
187 |
|
188 | |
189 |
|
190 |
|
191 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 |
|
198 | |
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 |
|
205 | |
206 |
|
207 |
|
208 |
|
209 |
|
210 |
|
211 |
|
212 |
|
213 |
|
214 | |
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 | |
224 |
|
225 |
|
226 |
|
227 |
|
228 |
|
229 |
|
230 |
|
231 |
|
232 | |
233 |
|
234 |
|
235 |
|
236 |
|
237 |
|
238 |
|
239 | |
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
245 |
|
246 | |
247 |
|
248 |
|
249 |
|
250 |
|
251 |
|
252 |
|
253 | |
254 |
|
255 |
|
256 |
|
257 |
|
258 |
|
259 |
|
260 |
|
261 | |
262 |
|
263 |
|
264 |
|
265 |
|
266 |
|
267 |
|
268 |
|
269 | constructor() {
|
270 | super();
|
271 | this._loadInactive = util.defer();
|
272 | contentReady(this, () => this._compile());
|
273 |
|
274 | const {onConnected, onDisconnected} = util.defineListenerProperty(this, 'swipe');
|
275 | this._connectOnSwipe = onConnected;
|
276 | this._disconnectOnSwipe = onDisconnected;
|
277 | }
|
278 |
|
279 | connectedCallback() {
|
280 | if (!this._swiper) {
|
281 | this._swiper = new Swiper({
|
282 | getElement: () => this._contentElement,
|
283 | getInitialIndex: () => this.activeIndex || this.getAttribute('activeIndex'),
|
284 | getAutoScrollRatio: this._getAutoScrollRatio.bind(this),
|
285 | getBubbleWidth: () => parseInt(this.getAttribute('ignore-edge-width') || 25, 10),
|
286 | isAutoScrollable: () => true,
|
287 | preChangeHook: this._onPreChange.bind(this),
|
288 | postChangeHook: this._onPostChange.bind(this),
|
289 | refreshHook: this._onRefresh.bind(this),
|
290 | scrollHook: this._onScroll.bind(this)
|
291 | });
|
292 |
|
293 | contentReady(this, () => {
|
294 | this._tabbarBorder = util.findChild(this._tabbarElement, '.tabbar__border');
|
295 | this._swiper.init({ swipeable: this.hasAttribute('swipeable') });
|
296 | });
|
297 | }
|
298 |
|
299 | contentReady(this, () => {
|
300 | this._updatePosition();
|
301 | this._updateVisibility();
|
302 |
|
303 | if (!util.findParent(this, 'ons-page', p => p === document.body)) {
|
304 | this._show();
|
305 | }
|
306 | });
|
307 |
|
308 | this._connectOnSwipe();
|
309 | }
|
310 |
|
311 | disconnectedCallback() {
|
312 | if (this._swiper && this._swiper.initialized) {
|
313 | this._swiper.dispose();
|
314 | this._swiper = null;
|
315 | this._tabbarBorder = null;
|
316 | this._tabsRect = null;
|
317 | }
|
318 |
|
319 | this._disconnectOnSwipe();
|
320 | }
|
321 |
|
322 | _normalizeEvent(event) {
|
323 | return { ...event, index: event.activeIndex, tabItem: this.tabs[event.activeIndex] };
|
324 | }
|
325 |
|
326 | _onPostChange(event) {
|
327 | event = this._normalizeEvent(event);
|
328 | util.triggerElementEvent(this, 'postchange', event);
|
329 | const page = event.tabItem.pageElement;
|
330 | page && page._show();
|
331 | }
|
332 |
|
333 | _onPreChange(event) {
|
334 | event = this._normalizeEvent(event);
|
335 | event.cancel = () => event.canceled = true;
|
336 |
|
337 | util.triggerElementEvent(this, 'prechange', event);
|
338 |
|
339 | if (!event.canceled) {
|
340 | const { activeIndex, lastActiveIndex } = event;
|
341 | const tabs = this.tabs;
|
342 |
|
343 | tabs[activeIndex].setActive(true);
|
344 | if (lastActiveIndex >= 0) {
|
345 | const prevTab = tabs[lastActiveIndex];
|
346 | prevTab.setActive(false);
|
347 | prevTab.pageElement && prevTab.pageElement._hide();
|
348 | }
|
349 | }
|
350 |
|
351 | return event.canceled;
|
352 | }
|
353 |
|
354 | _onScroll(index, options = {}) {
|
355 | if (this._tabbarBorder) {
|
356 | this._tabbarBorder.style.transition = `all ${options.duration || 0}s ${options.timing || ''}`;
|
357 |
|
358 | if (this._autogrow && this._tabsRect.length > 0) {
|
359 | const a = Math.floor(index), b = Math.ceil(index), r = index % 1;
|
360 | this._tabbarBorder.style.width = lerp(this._tabsRect[a].width, this._tabsRect[b].width, r) + 'px';
|
361 | this._tabbarBorder.style.transform = `translate3d(${lerp(this._tabsRect[a].left, this._tabsRect[b].left, r)}px, 0, 0)`;
|
362 | } else {
|
363 | this._tabbarBorder.style.transform = `translate3d(${index * 100}%, 0, 0)`;
|
364 | }
|
365 | }
|
366 |
|
367 | util.triggerElementEvent(this, 'swipe', { index, options });
|
368 | }
|
369 |
|
370 | _onRefresh() {
|
371 | this._autogrow = util.hasModifier(this, 'autogrow');
|
372 | this._tabsRect = this.tabs.map(tab => tab.getBoundingClientRect());
|
373 | if (this._tabbarBorder) {
|
374 | this._tabbarBorder.style.display = this.hasAttribute('tab-border') || util.hasModifier(this, 'material') ? 'block' : 'none';
|
375 | const index = this.getActiveTabIndex();
|
376 | if (this._tabsRect.length > 0 && index >= 0) {
|
377 | this._tabbarBorder.style.width = this._tabsRect[index].width + 'px';
|
378 | }
|
379 | }
|
380 | }
|
381 |
|
382 | _getAutoScrollRatio(matches, velocity, size) {
|
383 | const ratio = .6;
|
384 | const modifier = size / 300 * (matches ? -1 : 1);
|
385 | return Math.min(1, Math.max(0, ratio + velocity * modifier));
|
386 | }
|
387 |
|
388 | get _tabbarElement() {
|
389 | return util.findChild(this, '.tabbar');
|
390 | }
|
391 |
|
392 | get _contentElement() {
|
393 | return util.findChild(this, '.tabbar__content');
|
394 | }
|
395 |
|
396 | get _targetElement() {
|
397 | const content = this._contentElement;
|
398 | return content && content.children[0] || null;
|
399 | }
|
400 |
|
401 | _compile() {
|
402 | autoStyle.prepare(this);
|
403 |
|
404 | const content = this._contentElement || util.create('.tabbar__content');
|
405 | content.classList.add('ons-tabbar__content');
|
406 | const tabbar = this._tabbarElement || util.create('.tabbar');
|
407 | tabbar.classList.add('ons-tabbar__footer');
|
408 |
|
409 | if (!tabbar.parentNode) {
|
410 | while (this.firstChild) {
|
411 | tabbar.appendChild(this.firstChild);
|
412 | }
|
413 | }
|
414 |
|
415 | if (tabbar.children.length > this.activeIndex && !util.findChild(tabbar, '[active]')) {
|
416 | tabbar.children[this.activeIndex].setAttribute('active', '');
|
417 | }
|
418 |
|
419 | this._tabbarBorder = util.findChild(tabbar, '.tabbar__border') || util.create('.tabbar__border');
|
420 | tabbar.appendChild(this._tabbarBorder);
|
421 | tabbar.classList.add('ons-swiper-tabbar');
|
422 |
|
423 | !content.children[0] && content.appendChild(document.createElement('div'));
|
424 | !content.children[1] && content.appendChild(document.createElement('div'));
|
425 | content.appendChild = content.appendChild.bind(content.children[0]);
|
426 | content.insertBefore = content.insertBefore.bind(content.children[0]);
|
427 |
|
428 | this.appendChild(content);
|
429 | this.appendChild(tabbar);
|
430 |
|
431 | ModifierUtil.initModifier(this, scheme);
|
432 | }
|
433 |
|
434 | _updatePosition(position = this.getAttribute('position')) {
|
435 | const top = this._top = position === 'top' || (position === 'auto' && util.hasModifier(this, 'material'));
|
436 | const action = top ? util.addModifier : util.removeModifier;
|
437 |
|
438 | action(this, 'top');
|
439 |
|
440 | const page = util.findParent(this, 'ons-page');
|
441 | if (page) {
|
442 | contentReady(page, () => {
|
443 | let p = 0;
|
444 | if (page.children[0] && util.match(page.children[0], 'ons-toolbar')) {
|
445 | action(page.children[0], 'noshadow');
|
446 | p = 1;
|
447 | }
|
448 |
|
449 | const content = page._getContentElement();
|
450 | const cs = window.getComputedStyle(page._getContentElement(), null);
|
451 |
|
452 | this.style.top = top ? parseInt(cs.getPropertyValue('padding-top'), 10) - p + 'px' : '';
|
453 |
|
454 |
|
455 | content.style.top = cs.top;
|
456 | content.style.top = '';
|
457 | });
|
458 | }
|
459 |
|
460 | internal.autoStatusBarFill(() => {
|
461 | const filled = util.findParent(this, e => e.hasAttribute('status-bar-fill'));
|
462 | util.toggleAttribute(this, 'status-bar-fill', top && !filled);
|
463 | });
|
464 | }
|
465 |
|
466 | get topPage() {
|
467 | const tabs = this.tabs,
|
468 | index = this.getActiveTabIndex();
|
469 | return tabs[index]
|
470 | ? tabs[index].pageElement || this.pages[0] || null
|
471 | : null;
|
472 | }
|
473 |
|
474 | get pages() {
|
475 | return util.arrayFrom(this._targetElement.children);
|
476 | }
|
477 |
|
478 | get tabs() {
|
479 | return Array.prototype.filter.call(this._tabbarElement.children, e => e.tagName === 'ONS-TAB');
|
480 | }
|
481 |
|
482 | |
483 |
|
484 |
|
485 |
|
486 |
|
487 |
|
488 |
|
489 |
|
490 |
|
491 |
|
492 |
|
493 |
|
494 |
|
495 |
|
496 |
|
497 |
|
498 |
|
499 |
|
500 |
|
501 |
|
502 |
|
503 |
|
504 |
|
505 |
|
506 |
|
507 | setActiveTab(nextIndex, options = {}) {
|
508 | const previousIndex = this.activeIndex;
|
509 |
|
510 | this._activeIndexSkipEffect = true;
|
511 | this.activeIndex = nextIndex;
|
512 |
|
513 | return this._updateActiveIndex(nextIndex, previousIndex, options);
|
514 | }
|
515 |
|
516 | _updateActiveIndex(nextIndex, prevIndex, options = {}) {
|
517 | const prevTab = this.tabs[prevIndex],
|
518 | nextTab = this.tabs[nextIndex];
|
519 |
|
520 | if (!nextTab) {
|
521 | return Promise.reject('Specified index does not match any tab.');
|
522 | }
|
523 |
|
524 | if (nextIndex === prevIndex) {
|
525 | util.triggerElementEvent(this, 'reactive', { index: nextIndex, activeIndex: nextIndex, tabItem: nextTab });
|
526 | return Promise.resolve(nextTab.pageElement);
|
527 | }
|
528 |
|
529 |
|
530 | const nextPage = nextTab.pageElement;
|
531 | return (nextPage ? Promise.resolve(nextPage) : nextTab.loaded)
|
532 | .then(nextPage => this._swiper.setActiveIndex(nextIndex, {
|
533 | reject: true,
|
534 | ...options,
|
535 | animation: prevTab && nextPage ? options.animation || this.getAttribute('animation') : 'none',
|
536 | animationOptions: util.extend(
|
537 | { duration: .3, timing: 'cubic-bezier(.4, .7, .5, 1)' },
|
538 | this.animationOptions,
|
539 | options.animationOptions || {}
|
540 | )
|
541 | }).then(() => {
|
542 | options.callback instanceof Function && options.callback(nextPage);
|
543 | return nextPage;
|
544 | }));
|
545 |
|
546 | }
|
547 |
|
548 | |
549 |
|
550 |
|
551 |
|
552 |
|
553 |
|
554 |
|
555 |
|
556 | setTabbarVisibility(visible) {
|
557 | this.hideTabs = !visible;
|
558 | }
|
559 |
|
560 | show() {
|
561 | this.hideTabs = false;
|
562 | }
|
563 |
|
564 | hide() {
|
565 | this.hideTabs = true;
|
566 | }
|
567 |
|
568 | _updateVisibility() {
|
569 | contentReady(this, () => {
|
570 | const visible = !this.hideTabs;
|
571 | this._contentElement.style[this._top ? 'top' : 'bottom'] = visible ? '' : '0px';
|
572 | this._tabbarElement.style.display = visible ? '' : 'none';
|
573 | visible && this._onRefresh();
|
574 | });
|
575 | }
|
576 |
|
577 | |
578 |
|
579 |
|
580 |
|
581 |
|
582 |
|
583 |
|
584 |
|
585 | get visible() {
|
586 | return this._tabbarElement.style.display !== 'none';
|
587 | }
|
588 |
|
589 | |
590 |
|
591 |
|
592 |
|
593 |
|
594 |
|
595 |
|
596 |
|
597 | |
598 |
|
599 |
|
600 |
|
601 |
|
602 |
|
603 |
|
604 |
|
605 | |
606 |
|
607 |
|
608 |
|
609 |
|
610 |
|
611 |
|
612 |
|
613 |
|
614 |
|
615 | getActiveTabIndex(tabs = this.tabs) {
|
616 | for (let i = 0; i < tabs.length; i++) {
|
617 | if (tabs[i] && tabs[i].tagName === 'ONS-TAB' && tabs[i].isActive()) {
|
618 | return i;
|
619 | }
|
620 | }
|
621 | return -1;
|
622 | }
|
623 |
|
624 | get activeIndex() {
|
625 | return Number(this.getAttribute('active-index'));
|
626 | }
|
627 |
|
628 | set activeIndex(value) {
|
629 | if (value !== null && value !== undefined) {
|
630 | this.setAttribute('active-index', value);
|
631 | }
|
632 | }
|
633 |
|
634 | _show() {
|
635 | this._swiper.show();
|
636 |
|
637 | setImmediate(() => {
|
638 | const tabs = this.tabs;
|
639 | const activeIndex = this.getActiveTabIndex(tabs);
|
640 | this._loadInactive.resolve();
|
641 | if (tabs.length > 0 && activeIndex >= 0) {
|
642 | tabs[activeIndex].loaded.then(el => el && setImmediate(() => el._show()));
|
643 | }
|
644 | });
|
645 | }
|
646 |
|
647 | _hide() {
|
648 | this._swiper.hide();
|
649 | const topPage = this.topPage;
|
650 | topPage && topPage._hide();
|
651 | }
|
652 |
|
653 | _destroy() {
|
654 | this.tabs.forEach(tab => tab.remove());
|
655 | this.remove();
|
656 | }
|
657 |
|
658 | static get observedAttributes() {
|
659 | return ['modifier', 'position', 'swipeable', 'tab-border', 'hide-tabs', 'active-index'];
|
660 | }
|
661 |
|
662 | attributeChangedCallback(name, last, current) {
|
663 | if (name === 'modifier') {
|
664 | ModifierUtil.onModifierChanged(last, current, this, scheme);
|
665 | const isTop = m => /(^|\s+)top($|\s+)/i.test(m);
|
666 | isTop(last) !== isTop(current) && this._updatePosition();
|
667 | } else if (name === 'position') {
|
668 | util.isAttached(this) && this._updatePosition();
|
669 | } else if (name === 'swipeable') {
|
670 | this._swiper && this._swiper.updateSwipeable(this.hasAttribute('swipeable'));
|
671 | } else if (name === 'hide-tabs') {
|
672 | this.isConnected && this._updateVisibility();
|
673 | } else if (name === 'active-index') {
|
674 | if (this._activeIndexSkipEffect) {
|
675 | this._activeIndexSkipEffect = false;
|
676 | } else if (this.isConnected) {
|
677 | contentReady(this, () => this._updateActiveIndex(current, last));
|
678 | }
|
679 | }
|
680 | }
|
681 |
|
682 | static get rewritables() {
|
683 | return rewritables;
|
684 | }
|
685 |
|
686 | static get events() {
|
687 | return ['prechange', 'postchange', 'reactive', 'swipe'];
|
688 | }
|
689 |
|
690 | get animationOptions() {
|
691 | return this.hasAttribute('animation-options') ?
|
692 | util.animationOptionsParse(this.getAttribute('animation-options')) : {};
|
693 | }
|
694 |
|
695 | set animationOptions(value) {
|
696 | if (value === undefined || value === null) {
|
697 | this.removeAttribute('animation-options');
|
698 | } else {
|
699 | this.setAttribute('animation-options', JSON.stringify(value));
|
700 | }
|
701 | }
|
702 | }
|
703 |
|
704 | util.defineBooleanProperties(TabbarElement, ['hide-tabs', 'swipeable', 'tab-border']);
|
705 |
|
706 | onsElements.Tabbar = TabbarElement;
|
707 | customElements.define('ons-tabbar', TabbarElement);
|