UNPKG

22.9 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 autoStyle from '../ons/autostyle.js';
22import Swiper from '../ons/internal/swiper.js';
23import ModifierUtil from '../ons/internal/modifier-util.js';
24import BaseElement from './base/base-element.js';
25import contentReady from '../ons/content-ready.js';
26
27const scheme = {
28 '.tabbar__content': 'tabbar--*__content',
29 '.tabbar__border': 'tabbar--*__border',
30 '.tabbar': 'tabbar--*'
31};
32
33const rewritables = {
34 /**
35 * @param {Element} tabbarElement
36 * @param {Function} callback
37 */
38 ready(tabbarElement, callback) {
39 callback();
40 }
41};
42
43const nullPage = internal.nullElement;
44const lerp = (x0, x1, t) => (1 - t) * x0 + t * x1;
45
46/**
47 * @element ons-tabbar
48 * @category tabbar
49 * @description
50 * [en]A component to display a tab bar on the bottom of a page. Used with `<ons-tab>` to manage pages using tabs.[/en]
51 * [ja]タブバーをページ下部に表示するためのコンポーネントです。ons-tabと組み合わせて使うことで、ページを管理できます。[/ja]
52 * @codepen pGuDL
53 * @tutorial vanilla/Reference/tabbar
54 * @modifier material
55 * [en]A tabbar in Material Design.[/en]
56 * [ja][/ja]
57 * @modifier autogrow
58 * [en]Tabs automatically grow depending on their content instead of having a fixed width.[/en]
59 * [ja][/ja]
60 * @modifier top-border
61 * [en]Shows a static border-bottom in tabs for iOS top tabbars.[/en]
62 * [ja][/ja]
63 * @guide fundamentals.html#managing-pages
64 * [en]Managing multiple pages.[/en]
65 * [ja]複数のページを管理する[/ja]
66 * @seealso ons-tab
67 * [en]The `<ons-tab>` component.[/en]
68 * [ja]ons-tabコンポーネント[/ja]
69 * @seealso ons-page
70 * [en]The `<ons-page>` component.[/en]
71 * [ja]ons-pageコンポーネント[/ja]
72 * @example
73 * <ons-tabbar>
74 * <ons-tab
75 * page="home.html"
76 * label="Home"
77 * active>
78 * </ons-tab>
79 * <ons-tab
80 * page="settings.html"
81 * label="Settings"
82 * active>
83 * </ons-tab>
84 * </ons-tabbar>
85 *
86 * <template id="home.html">
87 * ...
88 * </template>
89 *
90 * <template id="settings.html">
91 * ...
92 * </template>
93 */
94export default class TabbarElement extends BaseElement {
95
96 /**
97 * @event prechange
98 * @description
99 * [en]Fires just before the tab is changed.[/en]
100 * [ja]アクティブなタブが変わる前に発火します。[/ja]
101 * @param {Object} event
102 * [en]Event object.[/en]
103 * [ja]イベントオブジェクト。[/ja]
104 * @param {Number} event.index
105 * [en]Current index.[/en]
106 * [ja]現在アクティブになっているons-tabのインデックスを返します。[/ja]
107 * @param {Object} event.tabItem
108 * [en]Tab item object.[/en]
109 * [ja]tabItemオブジェクト。[/ja]
110 * @param {Function} event.cancel
111 * [en]Call this function to cancel the change event.[/en]
112 * [ja]この関数を呼び出すと、アクティブなタブの変更がキャンセルされます。[/ja]
113 */
114
115 /**
116 * @event postchange
117 * @description
118 * [en]Fires just after the tab is changed.[/en]
119 * [ja]アクティブなタブが変わった後に発火します。[/ja]
120 * @param {Object} event
121 * [en]Event object.[/en]
122 * [ja]イベントオブジェクト。[/ja]
123 * @param {Number} event.index
124 * [en]Current index.[/en]
125 * [ja]現在アクティブになっているons-tabのインデックスを返します。[/ja]
126 * @param {Object} event.tabItem
127 * [en]Tab item object.[/en]
128 * [ja]tabItemオブジェクト。[/ja]
129 */
130
131 /**
132 * @event reactive
133 * @description
134 * [en]Fires if the already open tab is tapped again.[/en]
135 * [ja]すでにアクティブになっているタブがもう一度タップやクリックされた場合に発火します。[/ja]
136 * @param {Object} event
137 * [en]Event object.[/en]
138 * [ja]イベントオブジェクト。[/ja]
139 * @param {Number} event.index
140 * [en]Current index.[/en]
141 * [ja]現在アクティブになっているons-tabのインデックスを返します。[/ja]
142 * @param {Object} event.tabItem
143 * [en]Tab item object.[/en]
144 * [ja]tabItemオブジェクト。[/ja]
145 */
146
147 /**
148 * @event swipe
149 * @description
150 * [en]Fires when the tabbar swipes.[/en]
151 * [ja][/ja]
152 * @param {Object} event
153 * [en]Event object.[/en]
154 * [ja]イベントオブジェクト。[/ja]
155 * @param {Number} event.index
156 * [en]Current index.[/en]
157 * [ja]現在アクティブになっているons-tabのインデックスを返します。[/ja]
158 * @param {Object} event.options
159 * [en]Animation options object.[/en]
160 * [ja][/ja]
161 */
162
163 /**
164 * @attribute animation
165 * @type {String}
166 * @default none
167 * @description
168 * [en]If this attribute is set to `"none"` the transitions will not be animated.[/en]
169 * [ja][/ja]
170 */
171
172 /**
173 * @attribute animation-options
174 * @type {Expression}
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 * @property animationOptions
182 * @type {Object}
183 * @description
184 * [en]Specify the animation's duration, timing and delay with an object literal. E.g. `{duration: 0.2, delay: 1, timing: 'ease-in'}`.[/en]
185 * [ja]アニメーション時のduration, timing, delayをオブジェクトリテラルで指定します。e.g. {duration: 0.2, delay: 1, timing: 'ease-in'}[/ja]
186 */
187
188 /**
189 * @attribute position
190 * @initonly
191 * @type {String}
192 * @default bottom
193 * @description
194 * [en]Tabbar's position. Available values are `"bottom"` and `"top"`. Use `"auto"` to choose position depending on platform (bottom for iOS flat design, top for Material Design).[/en]
195 * [ja]タブバーの位置を指定します。"bottom"もしくは"top"を選択できます。デフォルトは"bottom"です。[/ja]
196 */
197
198 /**
199 * @attribute swipeable
200 * @description
201 * [en]If this attribute is set the tab bar can be scrolled by drag or swipe.[/en]
202 * [ja]この属性がある時、タブバーをスワイプやドラッグで移動できるようになります。[/ja]
203 */
204
205 /**
206 * @attribute ignore-edge-width
207 * @type {Number}
208 * @default 20
209 * @description
210 * [en]Distance in pixels from both edges. Swiping on these areas will prioritize parent components such as `ons-splitter` or `ons-navigator`.[/en]
211 * [ja][/ja]
212 */
213
214 /**
215 * @attribute active-index
216 * @type {Number}
217 * @default 0
218 * @description
219 * [en]The index of the tab that is currently active.[/en]
220 * [ja][/ja]
221 */
222
223 /**
224 * @property activeIndex
225 * @type {Number}
226 * @default 0
227 * @description
228 * [en]The index of the tab that is currently active.[/en]
229 * [ja][/ja]
230 */
231
232 /**
233 * @attribute hide-tabs
234 * @description
235 * [en]Whether to hide the tabs.[/en]
236 * [ja]タブを非表示にする場合に指定します。[/ja]
237 */
238
239 /**
240 * @property hideTabs
241 * @description
242 * [en]Whether to hide the tabs.[/en]
243 * [ja]タブを非表示にする場合に指定します。[/ja]
244 */
245
246 /**
247 * @attribute tab-border
248 * @description
249 * [en]If this attribute is set the tabs show a dynamic bottom border. Only works for iOS flat design since the border is always visible in Material Design.[/en]
250 * [ja][/ja]
251 */
252
253 /**
254 * @property tabBorder
255 * @type {Boolean}
256 * @description
257 * [en]If this property is set the tabs show a dynamic bottom border. Only works for iOS flat design since the border is always visible in Material Design.[/en]
258 * [ja][/ja]
259 */
260
261 /**
262 * @attribute modifier
263 * @type {String}
264 * @description
265 * [en]The appearance of the tabbar.[/en]
266 * [ja]タブバーの表現を指定します。[/ja]
267 */
268
269 constructor() {
270 super();
271 this._loadInactive = util.defer(); // Improves #2324
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(); // This tabbar is the top component
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; // Base ratio
384 const modifier = size / 300 * (matches ? -1 : 1); // Based on screen size
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'); // Hides material border
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); // Triggers ons-tab connectedCallback
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; // Visual fix for some devices
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 // Refresh content top - Fix for iOS 8
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 * @method setActiveTab
484 * @signature setActiveTab(index, [options])
485 * @param {Number} index
486 * [en]Tab index.[/en]
487 * [ja]タブのインデックスを指定します。[/ja]
488 * @param {Object} [options]
489 * [en]Parameter object.[/en]
490 * [ja]オプションを指定するオブジェクト。[/ja]
491 * @param {Function} [options.callback]
492 * [en]Function that runs when the new page has loaded.[/en]
493 * [ja][/ja]
494 * @param {String} [options.animation]
495 * [en]If this option is "none", the transition won't slide.[/en]
496 * [ja][/ja]
497 * @param {String} [options.animationOptions]
498 * [en]Specify the animation's duration, delay and timing. E.g. `{duration: 0.2, delay: 0.4, timing: 'ease-in'}`.[/en]
499 * [ja]アニメーション時のduration, delay, timingを指定します。e.g. {duration: 0.2, delay: 0.4, timing: 'ease-in'}[/ja]
500 * @description
501 * [en]Show specified tab page. Animations and their options can be specified by the second parameter.[/en]
502 * [ja]指定したインデックスのタブを表示します。アニメーションなどのオプションを指定できます。[/ja]
503 * @return {Promise}
504 * [en]A promise that resolves to the new page element.[/en]
505 * [ja][/ja]
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 // FIXME: nextTab.loaded is broken in Zone.js promises (Angular2)
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 * @method setTabbarVisibility
550 * @signature setTabbarVisibility(visible)
551 * @param {Boolean} visible
552 * @description
553 * [en]Used to hide or show the tab bar.[/en]
554 * [ja][/ja]
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 * @property visible
579 * @readonly
580 * @type {Boolean}
581 * @description
582 * [en]Whether the tabbar is visible or not.[/en]
583 * [ja]タブバーが見える場合に`true`。[/ja]
584 */
585 get visible() {
586 return this._tabbarElement.style.display !== 'none';
587 }
588
589 /**
590 * @property swipeable
591 * @type {Boolean}
592 * @description
593 * [en]Enable swipe interaction.[/en]
594 * [ja]swipeableであればtrueを返します。[/ja]
595 */
596
597 /**
598 * @property onSwipe
599 * @type {Function}
600 * @description
601 * [en]Hook called whenever the user slides the tabbar. It gets a decimal index and an animationOptions object as arguments.[/en]
602 * [ja][/ja]
603 */
604
605 /**
606 * @method getActiveTabIndex
607 * @signature getActiveTabIndex()
608 * @return {Number}
609 * [en]The index of the currently active tab.[/en]
610 * [ja]現在アクティブになっているタブのインデックスを返します。[/ja]
611 * @description
612 * [en]Returns tab index on current active tab. If active tab is not found, returns -1.[/en]
613 * [ja]現在アクティブになっているタブのインデックスを返します。現在アクティブなタブがない場合には-1を返します。[/ja]
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
704util.defineBooleanProperties(TabbarElement, ['hide-tabs', 'swipeable', 'tab-border']);
705
706onsElements.Tabbar = TabbarElement;
707customElements.define('ons-tabbar', TabbarElement);