Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save aifarfa/bcf4c7f48617f34ab6f4 to your computer and use it in GitHub Desktop.
Save aifarfa/bcf4c7f48617f34ab6f4 to your computer and use it in GitHub Desktop.
angular fixed column XY scrollable table

angular fixed column XY scrollable table

sample directive: table content is scrollable both horizontal / vertical while table header and first column is fixed position.

cross browser tested on: IE8-11, Chrome, FF

A Pen by Pasit R. on CodePen.

License.

<div class="container" ng-app="xyApp" ng-controller="xyCtrl as ctrl">
<h2>dragable content with fixed column</h2>
<p>try drag the green area</p>
<ul>
<li>mouse wheel: scroll vertical Y-axis</li>
<li>SHIFT + mouse wheel: scroll horizontal X-axis</li>
</ul>
<div id="target" xy-scroll x="ctrl.x" y="ctrl.y" drag-x="true" drag-y="false" reset="target.reset" resize="target.resize">
<div class="xy-corner">
<div class="text-center">item name</div>
</div>
<div class="xy-header-left">
<table class="table fixed">
<colgroup>
<col class="col-sm" />
</colgroup>
<tbody>
<tr ng-repeat="h in ctrl.stuff track by $index">
<th class="text-center" ng-bind="h.name"></th>
</tr>
</tbody>
</table>
</div>
<div class="xy-header-top">
<table class="table fixed">
<colgroup>
<col ng-repeat="col in ctrl.fields" class="col-sm" />
</colgroup>
<tr>
<th ng-repeat="col in ctrl.fields">
<div class="text-center" ng-bind="col"></div>
</th>
</tr>
</table>
</div>
<div class="xy-content">
<table class="table fixed">
<colgroup>
<col ng-repeat="col in ctrl.fields" class="col-sm" />
</colgroup>
<tr ng-repeat="item in ctrl.stuff track by $index">
<td ng-bind="item.0"></td>
<td ng-bind="item.1"></td>
<td ng-bind="item.2"></td>
<td ng-bind="item.3"></td>
<td ng-bind="item.4"></td>
<td ng-bind="item.5"></td>
<td ng-bind="item.6"></td>
<td ng-bind="item.7"></td>
<td ng-bind="item.8"></td>
<td ng-bind="item.9"></td>
<td ng-bind="item.10"></td>
</tr>
</table>
</div>
</div>
<!-- ctrl info -->
<div class="row">
<div class="col-sm-4 col-md-4 form-group">
<lable class="control-label">position-X:</lable>
<input class="form-control input-sm" ng-model="ctrl.x" type="number" />
</div>
<div class="col-sm-4 col-md-4 form-group">
<lable class="control-label">position-Y:</lable>
<input class="form-control input-sm" ng-model="ctrl.y" type="number" />
</div>
<div class="col-sm-8 col-md-8"></div>
<div class="col-sm-8 col-md-8">
<button class="btn btn-sm" ng-click="ctrl.random()" type="button">random</button>
<button class="btn btn-sm" ng-click="ctrl.empty()" type="button">empty</button>
<button class="btn btn-sm" ng-click="ctrl.refresh()" type="button">refresh</button>
</div>
</div>
<p>footer content, angular ctrl status: {{ctrl.status}}</p>
</div>
angular.module('xyScroll', []);
angular.module('xyScroll').directive('xyScroll', ['$log', '$document', '$timeout', function($log, $document, $timeout) {
return {
scope: {
x: '=?',
y: '=?',
dragX: '=?',
dragY: '=?'
},
template: '<div class="xy-outer"><div class="xy-inner" ng-transclude></div>'
+ '<div class="xy-scroll xy-scroll-x" ng-class="{active: isActive || dragging}">'
+ '<div class="xy-bar xy-bar-x" ng-style="scrollX" ng-mousedown="beginDragX($event)"></div></div>'
+ '<div class="xy-scroll xy-scroll-y" ng-class="{active: isActive || dragging}" ng-style="{paddingTop: topOffset}">'
+ '<div class="xy-bar xy-bar-y" ng-style="scrollY" ng-mousedown="beginDragY($event)"></div></div></div>',
transclude: true,
link: function(scope, element, attrs) {
// ...
var startX = 0,
startY = 0,
x = 0,
y = 0;
var accelerationTimer = null,
accelerationDelay = null,
speed = 0.5,
speedX = 1,
speedY = 1;
var content = element.find('.xy-content'),
header = element.find('.xy-header-left'),
top = element.find('.xy-header-top'),
corner = element.find('.xy-corner'),
inner = content.parent(), //element.find('.xy-inner'),
child = content.children();
scope.dragging = false;
scope.isActive = false;
scope.scrollX = {
left: 0,
width: 100,
// opacity: 1
};
scope.scrollY = {
top: 0,
height: 30,
// opacity: 1
};
scope.topOffset = 0;
$timeout(function() {
setup();
resize();
});
function setup() {
inner.on('mousewheel wheel', mousewheel);
if (scope.dragX || scope.dragY) {
content.on('mousedown', mousedown);
}
content.on('mouseover', mouseover);
content.on('mouseout', mouseout);
content.on('touchstart', touchstart); //touch test
var resizeEvent = attrs["resize"];
if (resizeEvent) { //optional resize via scope.$boardcast
scope.$on(resizeEvent, function() {
$timeout(resize, 0);
});
}
var resetEvent = attrs["reset"];
if (resetEvent) {
scope.$on(resetEvent, function() {
$timeout(restart, 0);
});
}
}
function resize() {
//top header
scope.topOffset = getTopOffset();
corner.height(scope.topOffset);
top.height(scope.topOffset);
//scrollbar
scope.scrollX.width = getScrollWidth();
scope.scrollY.height = getScrollHeight();
speedX = getRatioX();
speedY = getRatioY();
//set position
scrollX();
scrollY();
}
function restart() {
x = 0;
y = 0;
scrollX();
scrollY();
update();
}
function mousewheel(event) {
event.preventDefault();
// support non-webkit browsers
if (event.originalEvent != undefined) {
event = event.originalEvent;
}
var delta = getWheelDelta(event); // event.wheelDeltaY || event.wheelDelta;
delta = Math.floor(delta * accelerate());
// shift or cmd(Mac) is pressed?
if (event.shiftKey || event.metaKey) {
moveX(-delta);
} else {
moveY(-delta);
}
update();
}
function getWheelDelta(event) {
if (event.type == "wheel" || event.deltaY) {
var delta = event.deltaX || event.deltaY;
return (event.deltaMode == 0) ? -delta : -delta * 40; //line mode
}
if (event.type == "mousewheel") {
return event.wheelDeltaY || event.wheelDelta;
}
}
scope.beginDragX = function(event) {
event.preventDefault();
scope.dragging = true;
startX = event.pageX;
$document.on('mousemove', dragX);
$document.one('mouseup', endDragX);
}
scope.beginDragY = function(event) {
event.preventDefault();
scope.dragging = true;
startY = event.pageY;
$document.on('mousemove', dragY);
$document.one('mouseup', endDragY);
}
function dragX(event) {
event.preventDefault();
var delta = event.pageX - startX;
startX += delta;
moveX(delta * speedX);
update();
}
function dragY(event) {
event.preventDefault();
var delta = event.pageY - startY;
startY += delta;
moveY(delta * speedY);
update();
}
function endDragX(e) {
scope.dragging = false;
scope.$apply();
$document.off('mousemove', dragX);
$document.off('mouseup', endDragX);
}
function endDragY(e) {
scope.dragging = false;
scope.$apply();
$document.off('mousemove', dragY);
$document.off('mouseup', endDragY);
}
function mousedown(event) {
// Prevent default dragging of selected content
event.preventDefault();
scope.dragging = true;
startX = event.pageX - x;
startY = event.pageY - y;
$document.on('mousemove', mousemove);
$document.one('mouseup', mouseup);
}
function mousemove(event) {
event.preventDefault();
if (scope.dragX) {
x = limitX(event.pageX - startX);
scrollX();
}
if (scope.dragY) {
y = limitY(event.pageY - startY);
scrollY();
}
update();
}
function mouseup() {
scope.dragging = false;
scope.$apply();
$document.off('mousemove', mousemove);
$document.off('mouseup', mouseup);
}
function mouseover(event) {
scope.isActive = true;
scope.$apply();
}
function mouseout(event) {
scope.isActive = false;
scope.$apply();
}
function touchstart(event) {
var touch = getTouchEvent(event);
$log.log('touchstart', touch);
startX = touch.pageX - x;
startY = touch.pageY - y;
$document.on('touchmove', touchmove);
$document.on('touchend', touchend);
}
function touchmove(event) {
event.preventDefault();
var touch = getTouchEvent(event);
x = limitX(touch.pageX - startX);
y = limitY(touch.pageY - startY);
scrollY();
scrollX();
update();
}
function touchend(event) {
$document.off('touchmove', touchmove);
$document.off('touchend', touchend);
$log.log('touchend.');
}
function getTouchEvent(event) {
if (event.originalEvent != undefined) {
event = event.originalEvent;
}
if (!event.touches) {
return;
}
return event.touches[0]; //single touch
}
function moveX(distance) {
x = limitX(x - distance);
scrollX();
}
function moveY(distance) {
y = limitY(y - distance);
scrollY();
}
function scrollX() {
var pos = {
left: x + 'px'
};
top.css(pos);
content.css(pos);
}
function scrollY() {
var yOffset = y + scope.topOffset; //left header
header.css({
top: yOffset + 'px'
});
content.css({
top: y + 'px'
});
}
function limitX(value) {
//value is negative
var limit = maxX();
return Math.min(0, Math.max(-limit, value));
}
function limitY(value) {
var limit = maxY();
return Math.min(0, Math.max(-limit, value));
}
function maxX() {
var limit = actualWidth() - inner.width();
return limit;
}
function maxY() {
var viewHeight = element.height();
var height = actualHeight();
return height - viewHeight;
}
function actualHeight() {
var h = child.height();
return h + scope.topOffset;
}
function actualWidth() {
var h = top.children().width();
var w = child.width();
return Math.max(h, w);
}
function getTopOffset() {
return Math.max(top.children().height(), corner.height());
}
function getBarPositionX() {
var viewWidth = inner.width();
var barSize = scope.scrollX.width;
var max = viewWidth - barSize;
var xMax = maxX();
var ratio = -x / xMax;
return Math.floor(ratio * max);
}
function getBarPositionY() {
var height = element.height();
var barSize = scope.scrollY.height;
var offset = scope.topOffset;
var max = height - barSize - offset;
var yMax = maxY();
var ratio = -y / yMax;
return Math.floor(ratio * max);
}
function getScrollWidth() {
var w = inner.width(); //.xy-inner width
var len = actualWidth();
return w * w / len;
}
function getScrollHeight() {
var h = element.height() - top.height();
var len = actualHeight();
return h * h / len;
}
function getRatioX() {
var viewLength = inner.width();
var actualLength = actualWidth();
return actualLength / viewLength;
}
function getRatioY() {
var viewLength = element.height() - top.height();
var actualLength = actualHeight();
return actualLength / viewLength;
}
function update() {
scope.x = x;
scope.y = y;
scope.scrollX.left = getBarPositionX();
scope.scrollY.top = getBarPositionY();
scope.$apply();
}
/** acceleration effect on mouse wheel */
function accelerate() {
if (accelerationTimer) {
//keep moving state
$timeout.cancel(accelerationTimer);
if (accelerationDelay) {
//keep acceleration
$timeout.cancel(accelerationDelay);
}
accelerationDelay = $timeout(function() {
speed += 0.1; //increase speed every 0.2s
}, 200);
}
accelerationTimer = $timeout(function() {
speed = 0.5;
}, 400);
return Math.min(speed, 1);
}
}
};
}]);
angular.module('xyApp', ['xyScroll']).controller('xyCtrl', function($scope, $log) {
var _this = this;
this.isWorks = true
this.x = 0;
this.y = 0;
this.status = 'it works!';
this.fields = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K'];
this.stuff = mockData();
this.empty = function(){
this.stuff = [];
this.refresh();
};
this.random = function(){
$log.debug('random new stuff..');
var total = Math.floor(Math.random() * 30) + 1;
this.stuff = mockData(total);
this.refresh();
$log.debug('total', this.stuff.length);
};
this.refresh = function(){
$log.debug('refresh..');
$scope.$broadcast('target.resize');
$scope.$broadcast('target.reset');
};
function mockData(total) {
total = total || 20;
var data = [];
for (var i = 0; i < total; ++i) {
var item = {
name: 'Item ' + i
};
for (var j = 0; j < _this.fields.length; j++) {
item[j] = 'column ' + _this.fields[j] + i;
}
data.push(item);
}
return data;
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.28/angular.min.js"></script>
$sidebar-width: 25%;
$inner-width: 75%;
$inner-height: 260px;
$scroll-bar-size: 5px;
$scroll-bar-color: #000;
$scroll-bar-rgba: rgba(0, 0, 0, 0);
$bar-color: #aaa;
$bar-rgba: rgba(100, 100, 100, 0.5);
$bar-radius: 2px;
.xy-outer {
position: relative;
overflow-x: hidden;
overflow-y: hidden;
}
.xy-inner {
width: $inner-width;
height: $inner-height;
margin-left: $sidebar-width;
overflow: hidden;
}
.xy-header-top {
position: relative;
z-index: 1;
}
.xy-header-left {
position: absolute;
left: 0;
top: 0;
width: $sidebar-width;
}
.xy-corner {
position: absolute;
top: 0;
left: 0;
width: $sidebar-width;
z-index: 1;
}
.xy-content {
cursor: move;
position: relative;
left: 0;
top: 0;
}
.xy-scroll {
background-color: $scroll-bar-color;
background-color: $scroll-bar-rgba;
box-sizing: border-box;
position: absolute;
bottom: 0;
right: 0;
}
.xy-scroll-x {
padding-left: $sidebar-width;
height: $scroll-bar-size;
width: 100%;
}
.xy-scroll-y {
//padding-top: $header-height;
width: $scroll-bar-size;
height: 100%;
}
.xy-bar {
background-color: $bar-color;
background-color: $bar-rgba;
border-radius: $bar-radius;
cursor: pointer;
position: relative;
opacity: 0.3;
transition: opacity .3s ease-in-out;
.active & {
opacity: 1;
}
}
.xy-bar-x {
height: $scroll-bar-size;
width: 100px;
bottom: 0;
left: 0;
}
.xy-bar-y {
width: $scroll-bar-size;
height: 50px;
top: 50px;
}
#target {
width: 640px;
.xy-header-top {
//height: 50px;
}
.xy-header-top .table {
background: #fafafa;
}
.xy-corner div {
background: #dadada;
padding: 8px;
}
.fixed {
table-layout: fixed;
/* width: 100%; */
}
.fixed .col-sm {
width: 100px;
}
.fixed td,
.fixed th {
vertical-align: top;
width: 100px;
}
.fixed th {
border-bottom: 0 none;
}
}
$header-height: 37px;
$sidebar-width: 25%;
$inner-width: 75%;
$inner-height: 250px;
$scroll-bar-size: 5px;
$outer-width: 640px;
//colorize
$inner-color: #dfd;
$scroll-bar-color: #000;
$scroll-bar-rgba: rgba(0, 0, 0, 0);
$bar-color: #aaa;
$bar-rgba: rgba(100, 100, 100, 0.5);
$bar-radius: 2px;
.xy-outer {
position: relative;
overflow-x: hidden;
overflow-y: hidden;
//width: $outer-width;
}
.xy-inner {
//background-color: $inner-color;
width: $inner-width;
height: $inner-height;
margin-left: $sidebar-width;
overflow: hidden;
}
.xy-header-top {
position: relative;
//height: $header-height;
z-index: 1;
}
.xy-header-left {
position: absolute;
left: 0;
top: 0;
width: $sidebar-width;
}
.xy-corner {
position: absolute;
top: 0;
left: 0;
width: $sidebar-width;
z-index: 1;
}
.xy-content {
cursor: move;
position: relative;
left: 0;
top: 0;
}
.xy-scroll {
background-color: $scroll-bar-color;
background-color: $scroll-bar-rgba;
box-sizing: border-box;
position: absolute;
bottom: 0;
right: 0;
}
.xy-scroll-x {
padding-left: $sidebar-width;
height: $scroll-bar-size;
width: 100%;
}
.xy-scroll-y {
padding-top: $header-height;
width: $scroll-bar-size;
height: 100%;
}
.xy-bar {
background-color: $bar-color;
background-color: $bar-rgba;
border-radius: $bar-radius;
cursor: pointer;
position: relative;
transition: opacity .3s ease-in-out;
}
.xy-bar-x {
height: $scroll-bar-size;
width: 100px;
bottom: 0;
left: 0;
}
.xy-bar-y {
width: $scroll-bar-size;
height: 50px;
top: 50px;
}
#target {
width: 640px;
.xy-header-top {
height: $header-height;
}
.xy-header-top .table {
background: #fafafa;
}
.xy-corner div {
background: #dadada;
padding: 8px;
}
.fixed {
table-layout: fixed;
/* width: 100%; */
}
.fixed .col-sm {
width: 100px;
}
.fixed td,
.fixed th {
vertical-align: top;
width: 100px;
}
.fixed th {
border-bottom: 0 none;
}
}
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet" />

angular fixed column XY scrollable table

sample directive: table content is scrollable both horizontal / vertical (xy-axis) while table header and first column is fixed position.

cross browser tested: IE8-11, Chrome

A Pen by Pasit R. on CodePen.

License.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment