If you don't want to use Angular UI, say for size reasons, you can roll a basic version of your own. Demo is at http://plnkr.co/edit/WnvZETQlxurhgcm1k6Hd?p=preview .
Data
You say you don't need the tabs to be dynamic, but it probably makes them a bit more reusable. So in the wrapping scope, you can have:
$scope.tabs = [{
header: 'Tab A',
content: 'Content of Tab A'
},{
header: 'Tab B',
content: 'Content of Tab B'
}, {
header: 'Tab C',
content: 'Content of Tab C'
}];
Tabs HTML
Designing the HTML structure, you can repeat over the list above for both the buttons and the content
<tabs>
<tab-buttons>
<tab-button ng-repeat="tab in tabs">{{tab.header}}</tab-button>
</tab-buttons>
<tab-contents>
<tab-content ng-repeat="tab in tabs">{{tab.content}}</tab-body>
</tab-contents>
</tabs>
Tab Directives
There are lots of ways of doing this, but one way is to register click handlers on the individual button directives, and then communicating them up to the parent tabs
controller. This can be done using the require
attribute, exposing a method on the parent controller, in this case show
, and passing up the current index of the button by passing the variable $index
that ngRepeat
adds to the scope.
app.directive('tabs', function($timeout) {
return {
restrict: 'E',
controller: function($element, $scope) {
var self = this;
this.show = function(index) {
// Show only current tab
var contents = $element.find('tab-content');
contents.removeClass('current');
angular.element(contents[index]).addClass('current');
// Mark correct header as current
var buttons = $element.find('tab-button');
buttons.removeClass('current');
angular.element(buttons[index]).addClass('current');
};
$timeout(function() {
self.show('0');
});
}
};
});
app.directive('tabButton', function() {
return {
restrict: 'E',
require: '^tabs',
link: function(scope, element, attr, tabs) {
element.on('click', function() {
tabs.show(scope.$index);
});
}
};
});
Assuming you have the right CSS in the page, specifically the styles for the .current
class, as at http://plnkr.co/edit/WnvZETQlxurhgcm1k6Hd?p=preview , at this point have a working set of tabs.
Sortable
Using HTML5 drag + drop API, you can have some basic drag + dropping without worrying about things like mouse position. The first thing to do would be to design the attributes that are needed to make it work. In this case a sortable
attribute on a parent item that references the list, and a sortable-item
attribute that contains a reference to the index of the current item.
<tabs sortable="tabs">
<tab-buttons>
<tab-button ng-repeat="tab in list" sortable-item="$index">{{tab.header}}</tab-button>
</tab-buttons>
<tab-contents>
<tab-content ng-repeat="tab in list">{{tab.content}}</tab-body>
</tab-contents>
</tabs>
The sortable
and sortableItem
directives can be as below (and more details can be found at http://www.html5rocks.com/en/tutorials/dnd/basics/ )
app.directive('sortable', function() {
return {
controller: function($scope, $attrs) {
var listModel = null;
$scope.$watch($attrs.sortable, function(sortable) {
listModel = sortable;
});
this.move = function(fromIndex, toIndex) {
// http://stackoverflow.com/a/7180095/1319998
listModel.splice(toIndex, 0, listModel.splice(fromIndex, 1)[0]);
};
}
};
});
app.directive('sortableItem', function($window) {
return {
require: '^sortable',
link: function(scope, element, attrs, sortableController) {
var index = null;
scope.$watch(attrs.sortableItem, function(newIndex) {
index = newIndex;
});
attrs.$set('draggable', true);
// Wrapped in $apply so Angular reacts to changes
var wrappedListeners = {
// On item being dragged
dragstart: function(e) {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.dropEffect = 'move';
e.dataTransfer.setData('application/json', index);
element.addClass('dragging');
},
dragend: function(e) {
e.stopPropagation();
element.removeClass('dragging');
},
// On item being dragged over / dropped onto
dragenter: function(e) {
element.addClass('hover');
},
dragleave: function(e) {
element.removeClass('hover');
},
drop: function(e) {
e.preventDefault();
e.stopPropagation();
element.removeClass('hover');
var sourceIndex = e.dataTransfer.getData('application/json');
sortableController.move(sourceIndex, index);
}
};
// For performance purposes, do not
// call $apply for these
var unwrappedListeners = {
dragover: function(e) {
e.preventDefault();
}
};
angular.forEach(wrappedListeners, function(listener, event) {
element.on(event, wrap(listener));
});
angular.forEach(unwrappedListeners, function(listener, event) {
element.on(event, listener);
});
function wrap(fn) {
return function(e) {
scope.$apply(function() {
fn(e);
});
};
}
}
};
});
The main points to note us that each sortableItem
only needs to be aware of its current index. If it detects that another item has been dropped on it, it then calls a function on the sortable
controller, which then re-orders the array on the outer scope. ngRepeat
then does its usual thing and moves the tabs about.
Although I suspect there are simpler solutions, this one has the sortable behaviour and the tab behaviour completely decoupled. You can use sortable
on elements that aren't tabs
, and you can use tabs
without the sortable behaviour.