Skip to content

Instantly share code, notes, and snippets.

@cmackay
Last active December 30, 2015 17:59
Show Gist options
  • Save cmackay/7864569 to your computer and use it in GitHub Desktop.
Save cmackay/7864569 to your computer and use it in GitHub Desktop.
Angular Hammer.js Sortable
<!DOCTYPE html>
<html ng-app='app'>
<head>
<meta name="description" content="Angular Hammer.js Sortable" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title></title>
<!-- Styles -->
<style>
body {
background-color: #fff;
font-family: sans-serif;
font-size: 18px;
}
body > div {
margin-bottom: 20px;
border: 1px solid #f00;
}
.placeholder {
}
.helper {
background-color: #fff;
}
.sortable {
height: 200px;
}
.drag {
border: 1px solid #ccc;
/*background-color: #fff;*/
padding: 5px;
cursor: pointer;
}
.info {
font-size: 14px;
padding: 10px;
}
</style>
</head>
<body>
<!-- Main Controller -->
<div ng-controller='MainCtrl'>
<div sortable='sortableOptions'>
<div id='{{item.name}}' class='drag' ng-repeat='item in items'>{{item.name}}</div>
</div>
<div class='info'>Items: {{items}}</div>
</div>
<div ng-controller='MainCtrl'>
<div sortable='sortableOptions'>
<div id='{{item.name}}' class='drag' ng-repeat='item in items'>{{item.name}}</div>
</div>
<div class='info'>Items: {{items}}</div>
</div>
<!-- Dependencies -->
<script src='http://cdnjs.cloudflare.com/ajax/libs/hammer.js/1.0.5/hammer.min.js'></script>
<script src='http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.1/angular.js'></script>
</body>
</html>
'use strict';
/* global angular:false */
angular.module('app', [])
/* global Hammer:false */
.constant('Hammer', Hammer)
.controller('MainCtrl', function ($scope, $log) {
$scope.items = [{
name: $scope.$id + '-1'
},{
name: $scope.$id + '-2'
},{
name: $scope.$id + '-3'
}];
$scope.sortableOptions = {
draggableClass: 'drag',
helperClass: 'helper',
forceHelperSize: true,
placeholderClass: 'placeholder',
forcePlaceholderSize: true,
connectWith: 'sortable',
zIndex: 1000
};
})
.factory('touch', function ($rootScope, $document, $log, Hammer) {
var hammerTime, events = [
'drag',
'dragstart',
'dragend'
];
return {
init: function () {
if (!hammerTime) {
hammerTime = new Hammer($document[0].body, {
prevent_default: true
}).on(events.join(' '), function(e) {
if (!e.gesture) return;
$rootScope.$broadcast('hammer:' + event.type, e);
});
}
}
};
})
.directive('sortable', function ($rootScope, $document, $log, Hammer) {
var indexOf = Array.prototype.indexOf,
dragContext;
function getIndex(el) {
$log.debug('getIndex', el);
return indexOf.call(el.parentNode.children, el);
}
function forceSize(el, otherEl) {
$log.debug('forceSize', el, otherEl);
el.css({
width: otherEl[0].clientWidth + 'px',
height: otherEl[0].clientHeight + 'px'
});
}
function DragContext(opts) {
this.config = opts.config;
this.source = opts.source;
this.sourceIndex = getIndex(opts.draggable[0]);
this.draggable = opts.draggable;
}
DragContext.prototype = {
onDragStart: function (e) {
$log.info('onDragStart', this);
var touch = e.gesture.touches[0];
this.click = {
left: touch.pageX - touch.target.offsetLeft,
top: touch.pageY - touch.target.offsetTop
};
this._createHelper();
this._createPlaceholder();
// hide the initial item
this.draggable.css('display', 'none');
},
onDrag: function (e) {
$log.info('onDrag', e);
// because of pointer events being set to none
// the srcEl should be the item behind the helper
var srcEl = angular.element(e.srcElement);
// move the helper
this.helper.css({
left: e.gesture.touches[0].pageX - this.click.left + 'px',
top: e.gesture.touches[0].pageY - this.click.top + 'px'
});
if (srcEl.hasClass(this.config.draggableClass)) {
// pointer is over a draggable
var direction = this.getDirection(e.gesture.center);
// pointer direction on the page is down
if (direction === 'down') {
// move placeholder after srcEl
srcEl[0].parentNode.insertBefore(
this.placeholder[0], srcEl[0].nextSibling);
this.lastMove = new Date();
} else if (direction === 'up') {
// move placeholder before srcEl
srcEl[0].parentNode.insertBefore(this.placeholder[0], srcEl[0]);
}
} else if (srcEl.hasClass(this.config.connectWith)) {
srcEl[0].appendChild(this.placeholder[0]);
}
this.lastCenter = e.gesture.center;
},
onDragEnd: function (e) {
$log.info('onDragEnd', e);
var source = this.source,
sourceIndex = this.sourceIndex,
sourceScope = angular.element(source).scope(),
target = this.placeholder.parent(),
targetIndex = getIndex(this.placeholder[0]),
targetScope = angular.element(target).scope();
this.draggable.css('display', '');
this.helper.remove();
this.placeholder.remove();
if (!targetScope.items) {
return;
}
if (sourceScope === targetScope) {
// move within current sortable
if (sourceIndex !== targetIndex) {
sourceScope.$apply(function () {
var items = sourceScope.items;
items.splice(targetIndex, 0, items.splice(sourceIndex, 1)[0]);
});
}
} else {
// move to a different sortable
$rootScope.$apply(function () {
var sourceItems = sourceScope.items,
targetItems = targetScope.items;
var item = sourceItems.splice(sourceIndex, 1)[0];
targetItems.splice(targetIndex, 0, item);
});
}
},
_createHelper: function () {
$log.debug('_createHelper');
// clone the draggable
var helper = this.draggable.clone();
// set the css so it that the helper can move
helper.css({
zIndex: this.config.zIndex || 1000,
position: 'absolute',
pointerEvents: 'none'
});
// set the helper class
helper.addClass(this.config.helperClass);
// set the helper size the same as the draggable
if (this.config.forceHelperSize) {
forceSize(helper, this.draggable);
}
// add the element
angular.element(this.config.appendTo || $document[0].body)
.append(helper);
this.helper = helper;
},
_createPlaceholder: function () {
$log.debug('_createPlaceholder');
// create a new element of the same node type
var nodeName = this.draggable[0].nodeName.toLowerCase(),
placeholder = angular.element('<' + nodeName + '>');
// add the placeholder class
placeholder.addClass(this.config.placeholderClass);
// add placeholder after the draggable
this.draggable[0].parentNode.insertBefore(
placeholder[0], this.draggable[0].nextSibling);
// set the size to the draggable's size
if (this.config.forcePlaceholderSize) {
forceSize(placeholder, this.draggable);
}
this.placeholder = placeholder;
},
isSource: function (source) {
return this.source === source;
},
getDirection: function (center) {
if (this.lastCenter) {
return (this.lastCenter.pageY > center.pageY) ? 'up' : 'down';
}
return '';
}
};
return {
restrict: 'A',
link: function (scope, element, attrs) {
var config = scope.$eval(attrs.sortable);
// add the sortable class
element.addClass('sortable');
scope.$on('hammer:dragstart', function (event, e) {
var draggable = angular.element(e.gesture.touches[0].target);
// only continue if the touch target has the draggable
// class and this directive's element contains the
// draggable
if (!draggable.hasClass(config.draggableClass) ||
(element[0] !== draggable[0].parentNode)) {
return;
}
dragContext = new DragContext({
config: config,
source: element,
draggable: draggable
});
dragContext.onDragStart(e);
});
scope.$on('hammer:drag', function (event, e) {
if (dragContext && dragContext.isSource(element)) {
dragContext.onDrag(e);
}
});
scope.$on('hammer:dragend', function (event, e) {
if (dragContext && dragContext.isSource(element)) {
dragContext.onDragEnd(e);
}
});
$log.info('drag - link');
}
};
})
.run(function ($log, touch) {
$log.info('app - run');
touch.init();
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment