Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Tab component for 0.9+
<div class-rtabs {{yield extra-attributes}}>
<div class-tab-window {{#if .direction === 'left'}}class-going-left{{else}}class-going-right{{/if}}>
<div class-tabs>
<div class="left-tabs">
{{#each .tabs}}{{#unless .right}}{{>tab}}{{/unless}}{{/each}}
</div>
<div class="right-tabs">
{{#each .tabs}}{{#if .right}}{{>tab}}{{/if}}{{/each}}
</div>
<div class="selection-indicator" style-left="{{.selectedLeft}}px" {{#if .selectedRight !== undefined}}style-right="{{.selectedRight}}px"{{/if}} />
</div>
</div>
<div class-content-window
{{#if .transition === 'fade'}}
style-opacity="{{opacity}}"
class-trans-fade
{{elseif .transition === 'slide'}}
class-trans-slide
{{/if}}
>
<div class-contents style-left="{{.selectedContent * -100}}%">
{{#each .tabs}}{{>tab-content}}{{/each}}
</div>
</div>
</div>
<template id="tab">
<div
class-tab
{{#unless .button}}class-selected="~/selected === @index"{{/unless}}
{{#if .disabled}}class-disabled
{{elseif !.button}}on-click=['select', @index]{{/if}}
as-registered="@index"
{{#if .extraTab}}{{yield .extraTab}}{{/if}}
>
{{#if typeof .title === 'string'}}{{title}}{{elseif .title}}{{yield .title}}{{/if}}
{{#if .closable && !.button}}<div class-close on-click=['close', @index]>&times;</div>{{/if}}
</div>
</template>
<template id="tab-content">
{{#if !.button}}
<div
class-tab
class-selected="~/selectedContent === @index"
{{#if .extra}}{{yield .extra}}{{/if}}
>
{{yield .template}}
</div>
{{else}}<div class-tab />
{{/if}}
</template>
<script>
import Ractive from 'ractive';
let resizer;
const instances = [];
class Tabs extends Ractive {
constructor(opts) {
super(opts);
}
updateIndicator() {
const node = this._tabs[this.get('selected')];
if (node) {
const start = this.get('selectedLeft');
if (start === undefined) {
this.set({
selectedLeft: node.offsetLeft,
selectedRight: node.offsetParent.clientWidth - (node.offsetLeft + node.offsetWidth)
});
} else {
const max = node.offsetParent.clientWidth;
const left = node.offsetLeft, width = node.clientWidth, right = max - left - width;
this.set({
direction: left < start ? 'left' : 'right',
selectedLeft: left,
selectedRight: right
});
}
} else {
this.set({
selectedLeft: 0,
selectedRight: this.find('.tabs').offsetWidth
});
}
}
}
const tabAttrs = ['closable', 'disabled', 'title', 'right', 'button'];
Ractive.extendWith(Tabs, {
template: $TEMPLATE,
cssId: 'tabs',
noCssTransform: true,
css: $CSS,
attributes: ['transition'],
data() {
return {
tabs: [],
rightTabs: [],
selected: 0,
selectedContent: 0,
opacity: 1
}
},
on: {
config() {
const tpl = this.partials.content;
if (tpl) {
const tabs = tpl.filter(n => n.e === 'tab').map(t => {
const tab = {
template: { t: t.f }
};
const extra = [];
const extraTab = [];
t.m.forEach(a => {
if (a.t === 13 && ~tabAttrs.indexOf(a.n)) tab[a.n] = a.f === 0 ? true : typeof a.f === 'string' ? a.f : { t: a.f };
else if (a.t === 70) extraTab.push(a);
else extra.push(a);
});
if (extra.length) tab.extra = { t: extra };
if (extraTab.length) tab.extraTab = { t: extraTab };
return tab;
});
tabs.unshift('tabs');
this.push.apply(this, tabs);
if (!resizer && typeof window !== undefined) {
resizer = true;
window.addEventListener('resize', () => {
instances.forEach(i => i.updateIndicator());
});
}
instances.push(this);
}
},
select(ctx, idx) {
const current = this.get('selected');
const prs = [];
const node = this.find('.contents');
const trans = this.get('transition');
if (current !== idx) {
if (trans === 'fade') {
this.set({
opacity: 0,
selected: idx
});
this.updateIndicator();
setTimeout(() => {
this.set({
selectedContent: idx,
opacity: 1
});
}, 300);
} else if (trans === 'slide') {
this.set('selected', idx);
this.set('selectedContent', idx);
this.updateIndicator();
} else {
this.set({
selected: idx,
selectedContent: idx
});
this.updateIndicator();
}
}
},
close(ctx, idx) {
const tab = this.getContext(this._tabs[idx]);
let ok = true;
if (tab.element.events.find(e => e.events.find(e => e.name === 'close'))) {
ok = tab.raise('close');
}
if (ok) this.splice('tabs', idx, 1);
return false;
},
teardown() {
instances.splice(instances.indexOf(this), 1);
}
},
decorators: {
registered(node, idx) {
const me = this;
if (!this._tabs) this._tabs = [];
this._tabs[idx] = node;
this.updateIndicator();
return {
teardown() {},
invalidate() {
me.updateIndicator();
},
update(idx) {
me._tabs[idx] = node;
setTimeout(() => me.updateIndicator());
}
};
}
}
});
export default Tabs;
</script>
<style>
.rtabs {
position: relative;
}
.rtabs > .tab-window {
overflow-y: hidden;
overflow-x: auto;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 1px 5px 0 rgba(0, 0, 0, 0.12),
0 3px 1px -2px rgba(0, 0, 0, 0.2);
}
.rtabs > .tab-window .tabs {
display: table;
position: relative;
min-width: 100%;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
}
.rtabs > .tab-window .tab {
display: inline-block;
box-sizing: border-box;
padding: 0.5em 1em;
height: 2.5em;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s ease-in-out;
user-select: none;
}
.rtabs > .tab-window .tab:hover {
opacity: 1;
}
.rtabs > .tab-window .tab.selected {
opacity: 1;
}
.rtabs > .tab-window .tab.disabled {
opacity: 0.4;
}
.rtabs > .tab-window .right-tabs {
text-align: right;
display: table-cell;
}
.rtabs > .tab-window .left-tabs {
text-align: left;
display: table-cell;
}
.rtabs > .tab-window .tab > .close {
display: inline-block;
margin-right: -0.5em;
font-weight: 700;
}
.rtabs > .tab-window .selection-indicator {
position: absolute;
bottom: 0;
height: 2px;
background-color: blue;
}
.rtabs > .tab-window.going-left .selection-indicator {
transition: left 0.2s ease-in-out, right 0.2s ease-in-out 0.1s;
}
.rtabs > .tab-window.going-right .selection-indicator {
transition: left 0.2s ease-in-out 0.1s, right 0.2s ease-in-out;
}
.rtabs > .content-window {
width: 100%;
overflow: hidden;
}
.rtabs > .content-window > .contents {
list-style: none;
padding: 0;
margin: 0;
position: relative;
white-space: nowrap;
left: 0;
}
.rtabs > .content-window.trans-slide > .contents {
transition: left 0.45s ease-in-out;
}
.rtabs > .content-window.trans-fade {
transition: opacity 0.3s ease;
}
.rtabs > .content-window > .contents > .tab {
display: inline-block;
width: 100%;
vertical-align: top;
white-space: initial;
transition: opacity 0.1s ease-in-out;
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.