<div ng-controller="mainCtrl" class="demo">
<tab-layout scrollable selected="current">
<tab><i class="material-icons">home</i></tab>
<tab><i class="material-icons">favorite</i></tab>
<tab><i class="material-icons">backup</i></tab>
<tab><i class="material-icons">camera</i> camera</tab>
</tab-layout>
<pages selected="current">
<page>A</page>
<page>B</page>
<page>C</page>
<page>D</page>
</pages>
</div>
*{font-family: 'Roboto', 'Noto', sans-serif;}
body{
display: flex;
display: -webkit-flex;
display: -ms-flex;
width: 100%;
height: 100%;
-webkit-justify-content: center;
-ms-justify-content: center;
justify-content: center;
-webkit-align-items: center;
-ms-align-items: center;
align-items: center;
}
/* pages css */
pages {
color: white;
text-align: center;
display: block;
-webkit-overflow-scrolling: touch;
overflow: hidden;
white-space: nowrap;
}
pages .container {
white-space: nowrap;
font-size: 0px;
}
pages page{
display: inline-block;
-webkit-transition: 800ms ease-out opacity;
-moz-transition: 800ms ease-out opacity;
-ms-transition: 800ms ease-out opacity;
transition: 800ms ease-out opacity;
font-size: 50px;
}
pages page.active {
opacity: 1 !important;
}
/* tabs css */
tab-layout {
background-color: #00bcd4;
display: flex;
display: -webkit-flex;
display: -ms-flex;
-webkit-align-items: center;
-ms-flex-align: center;
-webkit-align-items: center;
-ms-align-items: center;
align-items: center;
height: 48px;
font-size: 14px;
font-weight: 500;
overflow: hidden;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-tap-highlight-color: transparent;
-webkit-flex-direction: row-reverse;
-ms-flex-direction: row-reverse;
flex-direction: row-reverse;
}
tab-layout .tabs_container{
position: relative;
height: 100%;
white-space: nowrap;
overflow: hidden;
-webkit-flex: 1 1 auto;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
touch-action: pan-y;
}
tab-layout .tabs_content{
position: absolute;
white-space: nowrap;
height: 100%;
-moz-flex-basis: auto;
-ms-flex-basis: auto;
flex-basis: auto;
}
tab-layout[scrollable] .tabs_content{
position: absolute;
white-space: nowrap;
}
tab-layout tab .tab-content {
height: 100%;
-webkit-transform: translateZ(0);
transform: translateZ(0);
-webkit-transition: opacity 0.1s cubic-bezier(0.4, 0.0, 1, 1);
-moz-transition: opacity 0.1s cubic-bezier(0.4, 0.0, 1, 1);
-ms-transition: opacity 0.1s cubic-bezier(0.4, 0.0, 1, 1);
transition: opacity 0.1s cubic-bezier(0.4, 0.0, 1, 1);
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-flex: 1 1 auto;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
}
tab-layout tab{
display: -ms-inline-flexbox;
display: -webkit-inline-flex;
display: inline-flex;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-flex: 1 1 auto;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
position: relative;
padding: 0 12px;
overflow: hidden;
cursor: pointer;
-webkit-font-smoothing: antialiased;
color: rgba(255, 255, 255, 0.8);
height: 100%;
}
tab-layout .tabs_content{
padding-left: 16px;
}
tab-layout tab[aria-selected="true"]{
color: rgba(255, 255, 255, 1);
}
tab-layout .indicator{
height: 2px;
background-color: #ffff8d;
position: absolute;
bottom: 0;
}
tab-layout .indicator.right{
transition: right 200ms;
}
tab-layout .indicator.left{
transition: left 200ms;
}
/* demo */
.demo{
width:420px;
}
.demo pages page{
width: 100%;
height: 100%;
padding: 80px 0;
}
.demo pages page:nth-child(1) {
background-color: #4285f4;
}
.demo pages page:nth-child(2) {
background-color: #db4437;
}
.demo pages page:nth-child(3) {
background-color: #0f9d58;
}
.demo pages page:nth-child(4) {
background-color: #76daff;
}
var appname = 'layoutjs';
var app = angular.module(appname, []);
// Bootstrap angularjs app
angular.element(document).ready(function () {
console.log("App: bootstraping.");
angular.bootstrap(document, [appname]);
});
app.controller('mainCtrl',['$scope', function($scope){
// main controller
$scope.current = 0;
}]);
// pages directive
'use strict';
app.directive('pages', function(){
return{
restrict: 'E',
template: '<div class="container" ng-transclude></div>',
transclude: true,
scope:{
selected: '='
},
controller: function($scope, $element){
var SLIDE_WIDTH = $element.width();
var currentSlide = this.selected;
var maxSlides = 0;
var speed = 500;
var slide = $element.children('.container');
slide.swipe({
triggerOnTouchEnd: true,
swipeStatus: swipeStatus,
allowPageScroll: "vertical",
threshold: 75
});
this.selected = $scope.selected;
var pages = [];
this.addPage = function($element){
pages.push($element);
}
this.count = function(){
maxSlides = pages.length;
return pages.length;
}
this.setActive = function($e){
$element.find('page[aria-selected]').each(function(i, e){
var $e = angular.element(e);
if($e.attr('aria-selected') !== undefined){
$e.removeAttr('aria-selected');
}
})
$e.attr('aria-selected', 'true');
//$e.focus();
/*$element.scrollTo($e[0], 100, {
onAfter : function () {
requestAnimationFrame(function () {
$e.addClass("active");
});
}
});*/
};
var self = this;
$scope.$watch('selected', function(n, o){
if(n === undefined || n > self.count() || n < 0) return;
currentSlide = n;
$element.find('page').each(function(i, e){
var $e = angular.element(e);
if($e.data('id') === n){
scrollSlides(SLIDE_WIDTH * n, speed);
self.setActive(pages[currentSlide]);
return;
}
})
});
/**
* Catch each phase of the swipe.
* move : we drag the div
* cancel : we animate back to where we were
* end : we animate to the next image
*/
function swipeStatus(event, phase, direction, distance) {
//If we are moving before swipe, and we are going L or R in X mode, or U or D in Y mode then drag.
if (phase == "move" && (direction == "left" || direction == "right")) {
var duration = 0;
if (direction == "left") {
scrollSlides((SLIDE_WIDTH * currentSlide) + distance, duration);
} else if (direction == "right") {
scrollSlides((SLIDE_WIDTH * currentSlide) - distance, duration);
}
} else if (phase == "cancel") {
scrollSlides(SLIDE_WIDTH * currentSlide, speed);
} else if (phase == "end") {
if (direction == "right") {
previousSlide();
} else if (direction == "left") {
nextSlide();
}
}
}
function previousSlide() {
currentSlide = Math.max(currentSlide - 1, 0);
//scrollSlides(SLIDE_WIDTH * currentSlide, speed);
$scope.selected = currentSlide;
$scope.$apply();
}
function nextSlide() {
currentSlide = Math.min(currentSlide + 1, maxSlides - 1);
//scrollSlides(SLIDE_WIDTH * currentSlide, speed);
$scope.selected = currentSlide;
$scope.$apply();
}
/**
* Manually update the position of the imgs on drag
*/
function scrollSlides(distance, duration) {
slide.css("transition-duration", (duration / 1000).toFixed(1) + "s");
//inverse the number we set in the css
var value = (distance < 0 ? "" : "-") + Math.abs(distance).toString();
slide.css({
transform: "translate(" + value + "px,0)",
MozTransform: "translate(" + value + "px,0)",
WebkitTransform: "translate(" + value + "px,0)",
msTransform: "translate(" + value + "px,0)"
});
}
}
}
})
app.directive('page', function(){
return{
restrict: 'E',
require: '^pages',
link: function($scope, $element, $attrs, pages){
var id = pages.count();
$element.data('id', id);
pages.addPage($element);
}
}
});
// tabLayout directive
app.directive('tabLayout', function($timeout){
return {
restrict: 'E',
template: '\
<div class="tabs_container">\
<div class="indicator"></div>\
<div class="tabs_content" ng-transclude></div>\
</div>',
transclude: true,
scope: {
selected: '='
},
controller: function($scope, $element){
$element
.attr('role', 'tablist')
.attr('tabindex', 0);
var self = this;
this.currentTab = $scope.selected;
var tabs = [];
this.addTab = function($element){
tabs.push($element);
}
this.count = function(){
return tabs.length;
}
var indicator = $element.find('.indicator');
var tabsContainer = $element.find('.tabs_container');
var lastLeft = 0;
var attachIndicator = function(element){
var pos = element.position();
var left = pos.left + tabsContainer.offset().left;
var moveRight = lastLeft > pos.left;
var tabOffsetLeft = 0;
lastLeft = pos.left;
if(moveRight && !indicator.hasClass('right')){
indicator.addClass('right');
if(indicator.hasClass('left')) indicator.removeClass('left');
}else if(!moveRight && !indicator.hasClass('left')){
indicator.addClass('left');
if(indicator.hasClass('right')) indicator.removeClass('right');
}
var right = $element.outerWidth() - pos.left - element.outerWidth() - tabOffsetLeft;
if(!moveRight){
indicator.css('left', left)
indicator.css('right', right);
$timeout(function(){
indicator.css('left', pos.left);
}, 10);
} else {
left = pos.left;
indicator.css('left', left)
$timeout(function(){
indicator.css('right', right);
}, 10);
};
// fix right side scroll
var scrollOffset = (pos.left + element.outerWidth()) - $element.outerWidth() + tabOffsetLeft;
if(scrollOffset > 0)
{
tabsContainer.scrollLeft(scrollOffset);
}
// fix left side scroll
if(tabsContainer.scrollLeft() > pos.left){
tabsContainer.scrollLeft(-pos.left);
}
};
this.selected = function(tab, apply){
$scope.selected = tab.data('id');
tab.focus();
if(apply) $scope.$apply();
};
var apply = function(tab){
attachIndicator(tab);
if($scope.multi === undefined){
$element.find('tab').each(function(i, e){
var $e = angular.element(e);
if($e.attr('aria-selected') !== undefined){
$e.removeAttr('aria-selected');
$e.attr('tabindex', -1);
}
});
tab.attr('aria-selected', 'true');
tab.attr('tabindex', 0);
}
};
$scope.$watch('selected', function(n, o){
if(n === undefined || n > self.count() || n < 0) return;
self.currentTab = n;
apply(tabs[n]);
});
}
}
});
app.directive('tab', function(){
return {
restrict: 'E',
require: '^tabLayout',
template: '<div class="tab-content" ng-transclude>',
transclude: true,
link: function($scope, $element, $attrs, ngCtrl){
$element.attr('role', 'tab');
var disabled = $element.attr('disabled') !== undefined && $element.attr('disabled') !== false;
if(disabled) return;
// init
var id = ngCtrl.count();
$element.data('id', id);
if(id === ngCtrl.currentTab){
ngCtrl.selected($element);
}
ngCtrl.addTab($element);
applyElementEvents($element, -1);
$element.bind('click', function(){
ngCtrl.selected($element, true);
});
}
}
});
// helpers
/*
apply events to elements
*/
const ATTR_PRESSED = 'pressed';
const ATTR_FOCUSED = 'focused';
const ATTR_DISABLED = 'disabled';
const ATTR_AREA_DISABLED = 'aria-disabled';
function applyElementEvents($element, tabindex){
tabindex = tabindex || 0;
$element.attr('tabindex', tabindex);
var disabled = $element.attr(ATTR_DISABLED) !== undefined && $element.attr(ATTR_DISABLED) !== false;
$element.attr(ATTR_AREA_DISABLED, disabled);
if(disabled) $element.attr(ATTR_DISABLED, '');
// add pressed attr when mousedown
$element.bind('mousedown', function(){
if($element.attr(ATTR_PRESSED) === undefined){
$element.attr(ATTR_PRESSED, '');
}
});
// remove pressed attr when mousedown
$element.bind('mouseup', function(){
if($element.attr(ATTR_PRESSED) != undefined)
$element.removeAttr(ATTR_PRESSED);
});
// add pressed attr when keybord space and enter key pressed
$element.bind('keydown', function(event){
if(event.keyCode === 13 || event.keyCode === 32){ // enter or space
if($element.attr(ATTR_PRESSED) === undefined){
$element.attr(ATTR_PRESSED, '');
}
event.stopPropagation();
event.preventDefault();
return;
}
});
// remove pressed attr when keybord space and enter key pressed
$element.bind('keyup', function(event){
if(event.keyCode === 13 || event.keyCode === 32){ // enter or space
if($element.attr(ATTR_PRESSED) !== undefined){
$element.removeAttr(ATTR_PRESSED);
}
event.stopPropagation();
event.preventDefault();
return;
}
});
// add focused attr when focus
$element.bind('focus', function(event){
if($element.attr(ATTR_FOCUSED) === undefined)
$element.attr(ATTR_FOCUSED, '');
});
// remove focused attr when blur
$element.bind('blur', function(event){
if($element.attr(ATTR_FOCUSED) !== undefined)
$element.removeAttr(ATTR_FOCUSED);
});
};
This Pen doesn't use any external CSS resources.