mirror of
https://github.com/omniscale/magnacarto.git
synced 2025-06-16 13:00:22 +02:00
1081 lines
38 KiB
JavaScript
1081 lines
38 KiB
JavaScript
/*
|
|
The MIT License (MIT)
|
|
|
|
Copyright (c) 2014 Muhammed Ashik
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all
|
|
copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
SOFTWARE.
|
|
*/
|
|
|
|
/*jshint indent: 2 */
|
|
/*global angular: false */
|
|
|
|
(function () {
|
|
'use strict';
|
|
angular.module('as.sortable', [])
|
|
.constant('sortableConfig', {
|
|
itemClass: 'as-sortable-item',
|
|
handleClass: 'as-sortable-item-handle',
|
|
placeHolderClass: 'as-sortable-placeholder',
|
|
dragClass: 'as-sortable-drag',
|
|
hiddenClass: 'as-sortable-hidden',
|
|
dragging: 'as-sortable-dragging'
|
|
});
|
|
}());
|
|
|
|
/*jshint indent: 2 */
|
|
/*global angular: false */
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
var mainModule = angular.module('as.sortable');
|
|
|
|
/**
|
|
* Helper factory for sortable.
|
|
*/
|
|
mainModule.factory('$helper', ['$document', '$window',
|
|
function ($document, $window) {
|
|
return {
|
|
|
|
/**
|
|
* Get the height of an element.
|
|
*
|
|
* @param {Object} element Angular element.
|
|
* @returns {String} Height
|
|
*/
|
|
height: function (element) {
|
|
return element[0].getBoundingClientRect().height;
|
|
},
|
|
|
|
/**
|
|
* Get the width of an element.
|
|
*
|
|
* @param {Object} element Angular element.
|
|
* @returns {String} Width
|
|
*/
|
|
width: function (element) {
|
|
return element[0].getBoundingClientRect().width;
|
|
},
|
|
|
|
/**
|
|
* Get the offset values of an element.
|
|
*
|
|
* @param {Object} element Angular element.
|
|
* @param {Object} [scrollableContainer] Scrollable container object for calculating relative top & left (optional, defaults to Document)
|
|
* @returns {Object} Object with properties width, height, top and left
|
|
*/
|
|
offset: function (element, scrollableContainer) {
|
|
var boundingClientRect = element[0].getBoundingClientRect();
|
|
if (!scrollableContainer) {
|
|
scrollableContainer = $document[0].documentElement;
|
|
}
|
|
|
|
return {
|
|
width: boundingClientRect.width || element.prop('offsetWidth'),
|
|
height: boundingClientRect.height || element.prop('offsetHeight'),
|
|
top: boundingClientRect.top + ($window.pageYOffset || scrollableContainer.scrollTop - scrollableContainer.offsetTop),
|
|
left: boundingClientRect.left + ($window.pageXOffset || scrollableContainer.scrollLeft - scrollableContainer.offsetLeft)
|
|
};
|
|
},
|
|
|
|
/**
|
|
* get the event object for touch.
|
|
*
|
|
* @param {Object} event the touch event
|
|
* @return {Object} the touch event object.
|
|
*/
|
|
eventObj: function (event) {
|
|
var obj = event;
|
|
if (event.targetTouches !== undefined) {
|
|
obj = event.targetTouches.item(0);
|
|
} else if (event.originalEvent !== undefined && event.originalEvent.targetTouches !== undefined) {
|
|
obj = event.originalEvent.targetTouches.item(0);
|
|
}
|
|
return obj;
|
|
},
|
|
|
|
/**
|
|
* Checks whether the touch is valid and multiple.
|
|
*
|
|
* @param event the event object.
|
|
* @returns {boolean} true if touch is multiple.
|
|
*/
|
|
isTouchInvalid: function (event) {
|
|
|
|
var touchInvalid = false;
|
|
if (event.touches !== undefined && event.touches.length > 1) {
|
|
touchInvalid = true;
|
|
} else if (event.originalEvent !== undefined &&
|
|
event.originalEvent.touches !== undefined && event.originalEvent.touches.length > 1) {
|
|
touchInvalid = true;
|
|
}
|
|
return touchInvalid;
|
|
},
|
|
|
|
/**
|
|
* Get the start position of the target element according to the provided event properties.
|
|
*
|
|
* @param {Object} event Event
|
|
* @param {Object} target Target element
|
|
* @param {Object} [scrollableContainer] (optional) Scrollable container object
|
|
* @returns {Object} Object with properties offsetX, offsetY.
|
|
*/
|
|
positionStarted: function (event, target, scrollableContainer) {
|
|
var pos = {};
|
|
pos.offsetX = event.pageX - this.offset(target, scrollableContainer).left;
|
|
pos.offsetY = event.pageY - this.offset(target, scrollableContainer).top;
|
|
pos.startX = pos.lastX = event.pageX;
|
|
pos.startY = pos.lastY = event.pageY;
|
|
pos.nowX = pos.nowY = pos.distX = pos.distY = pos.dirAx = 0;
|
|
pos.dirX = pos.dirY = pos.lastDirX = pos.lastDirY = pos.distAxX = pos.distAxY = 0;
|
|
return pos;
|
|
},
|
|
|
|
/**
|
|
* Calculates the event position and sets the direction
|
|
* properties.
|
|
*
|
|
* @param pos the current position of the element.
|
|
* @param event the move event.
|
|
*/
|
|
calculatePosition: function (pos, event) {
|
|
// mouse position last events
|
|
pos.lastX = pos.nowX;
|
|
pos.lastY = pos.nowY;
|
|
|
|
// mouse position this events
|
|
pos.nowX = event.pageX;
|
|
pos.nowY = event.pageY;
|
|
|
|
// distance mouse moved between events
|
|
pos.distX = pos.nowX - pos.lastX;
|
|
pos.distY = pos.nowY - pos.lastY;
|
|
|
|
// direction mouse was moving
|
|
pos.lastDirX = pos.dirX;
|
|
pos.lastDirY = pos.dirY;
|
|
|
|
// direction mouse is now moving (on both axis)
|
|
pos.dirX = pos.distX === 0 ? 0 : pos.distX > 0 ? 1 : -1;
|
|
pos.dirY = pos.distY === 0 ? 0 : pos.distY > 0 ? 1 : -1;
|
|
|
|
// axis mouse is now moving on
|
|
var newAx = Math.abs(pos.distX) > Math.abs(pos.distY) ? 1 : 0;
|
|
|
|
// calc distance moved on this axis (and direction)
|
|
if (pos.dirAx !== newAx) {
|
|
pos.distAxX = 0;
|
|
pos.distAxY = 0;
|
|
} else {
|
|
pos.distAxX += Math.abs(pos.distX);
|
|
if (pos.dirX !== 0 && pos.dirX !== pos.lastDirX) {
|
|
pos.distAxX = 0;
|
|
}
|
|
|
|
pos.distAxY += Math.abs(pos.distY);
|
|
if (pos.dirY !== 0 && pos.dirY !== pos.lastDirY) {
|
|
pos.distAxY = 0;
|
|
}
|
|
}
|
|
pos.dirAx = newAx;
|
|
},
|
|
|
|
/**
|
|
* Move the position by applying style.
|
|
*
|
|
* @param event the event object
|
|
* @param element - the dom element
|
|
* @param pos - current position
|
|
* @param container - the bounding container.
|
|
* @param containerPositioning - absolute or relative positioning.
|
|
* @param {Object} [scrollableContainer] (optional) Scrollable container object
|
|
*/
|
|
movePosition: function (event, element, pos, container, containerPositioning, scrollableContainer) {
|
|
var bounds;
|
|
var useRelative = (containerPositioning === 'relative');
|
|
|
|
element.x = event.pageX - pos.offsetX;
|
|
element.y = event.pageY - pos.offsetY;
|
|
|
|
if (container) {
|
|
bounds = this.offset(container, scrollableContainer);
|
|
|
|
if (useRelative) {
|
|
// reduce positioning by bounds
|
|
element.x -= bounds.left;
|
|
element.y -= bounds.top;
|
|
|
|
// reset bounds
|
|
bounds.left = 0;
|
|
bounds.top = 0;
|
|
}
|
|
|
|
if (element.x < bounds.left) {
|
|
element.x = bounds.left;
|
|
} else if (element.x >= bounds.width + bounds.left - this.offset(element).width) {
|
|
element.x = bounds.width + bounds.left - this.offset(element).width;
|
|
}
|
|
if (element.y < bounds.top) {
|
|
element.y = bounds.top;
|
|
} else if (element.y >= bounds.height + bounds.top - this.offset(element).height) {
|
|
element.y = bounds.height + bounds.top - this.offset(element).height;
|
|
}
|
|
}
|
|
|
|
element.css({
|
|
'left': element.x + 'px',
|
|
'top': element.y + 'px'
|
|
});
|
|
|
|
this.calculatePosition(pos, event);
|
|
},
|
|
|
|
/**
|
|
* The drag item info and functions.
|
|
* retains the item info before and after move.
|
|
* holds source item and target scope.
|
|
*
|
|
* @param item - the drag item
|
|
* @returns {{index: *, parent: *, source: *,
|
|
* sourceInfo: {index: *, itemScope: (*|.dragItem.sourceInfo.itemScope|$scope.itemScope|itemScope), sortableScope: *},
|
|
* moveTo: moveTo, isSameParent: isSameParent, isOrderChanged: isOrderChanged, eventArgs: eventArgs, apply: apply}}
|
|
*/
|
|
dragItem: function (item) {
|
|
|
|
return {
|
|
index: item.index(),
|
|
parent: item.sortableScope,
|
|
source: item,
|
|
sourceInfo: {
|
|
index: item.index(),
|
|
itemScope: item.itemScope,
|
|
sortableScope: item.sortableScope
|
|
},
|
|
moveTo: function (parent, index) { // Move the item to a new position
|
|
this.parent = parent;
|
|
//If source Item is in the same Parent.
|
|
if (this.isSameParent() && this.source.index() < index) { // and target after
|
|
index = index - 1;
|
|
}
|
|
this.index = index;
|
|
},
|
|
isSameParent: function () {
|
|
return this.parent.element === this.sourceInfo.sortableScope.element;
|
|
},
|
|
isOrderChanged: function () {
|
|
return this.index !== this.sourceInfo.index;
|
|
},
|
|
eventArgs: function () {
|
|
return {
|
|
source: this.sourceInfo,
|
|
dest: {
|
|
index: this.index,
|
|
sortableScope: this.parent
|
|
}
|
|
};
|
|
},
|
|
apply: function () {
|
|
this.sourceInfo.sortableScope.removeItem(this.sourceInfo.index); // Remove from source.
|
|
this.parent.insertItem(this.index, this.source.modelValue); // Insert in to destination.
|
|
}
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Check the drag is not allowed for the element.
|
|
*
|
|
* @param element - the element to check
|
|
* @returns {boolean} - true if drag is not allowed.
|
|
*/
|
|
noDrag: function (element) {
|
|
return element.attr('no-drag') !== undefined || element.attr('data-no-drag') !== undefined;
|
|
},
|
|
|
|
/**
|
|
* Helper function to find the first ancestor with a given selector
|
|
* @param el - angular element to start looking at
|
|
* @param selector - selector to find the parent
|
|
* @returns {Object} - Angular element of the ancestor or body if not found
|
|
* @private
|
|
*/
|
|
findAncestor: function (el, selector) {
|
|
el = el[0];
|
|
var matches = Element.matches || Element.prototype.mozMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.oMatchesSelector || Element.prototype.webkitMatchesSelector;
|
|
while ((el = el.parentElement) && !matches.call(el, selector)) {
|
|
}
|
|
return el ? angular.element(el) : angular.element(document.body);
|
|
}
|
|
};
|
|
}
|
|
]);
|
|
|
|
}());
|
|
/*jshint undef: false, unused: false, indent: 2*/
|
|
/*global angular: false */
|
|
|
|
(function () {
|
|
|
|
'use strict';
|
|
var mainModule = angular.module('as.sortable');
|
|
|
|
/**
|
|
* Controller for Sortable.
|
|
* @param $scope - the sortable scope.
|
|
*/
|
|
mainModule.controller('as.sortable.sortableController', ['$scope', function ($scope) {
|
|
|
|
this.scope = $scope;
|
|
|
|
$scope.modelValue = null; // sortable list.
|
|
$scope.callbacks = null;
|
|
$scope.type = 'sortable';
|
|
$scope.options = {};
|
|
$scope.isDisabled = false;
|
|
|
|
/**
|
|
* Inserts the item in to the sortable list.
|
|
*
|
|
* @param index - the item index.
|
|
* @param itemData - the item model data.
|
|
*/
|
|
$scope.insertItem = function (index, itemData) {
|
|
$scope.modelValue.splice(index, 0, itemData);
|
|
};
|
|
|
|
/**
|
|
* Removes the item from the sortable list.
|
|
*
|
|
* @param index - index to be removed.
|
|
* @returns {*} - removed item.
|
|
*/
|
|
$scope.removeItem = function (index) {
|
|
var removedItem = null;
|
|
if (index > -1) {
|
|
removedItem = $scope.modelValue.splice(index, 1)[0];
|
|
}
|
|
return removedItem;
|
|
};
|
|
|
|
/**
|
|
* Checks whether the sortable list is empty.
|
|
*
|
|
* @returns {null|*|$scope.modelValue|boolean}
|
|
*/
|
|
$scope.isEmpty = function () {
|
|
return ($scope.modelValue && $scope.modelValue.length === 0);
|
|
};
|
|
|
|
/**
|
|
* Wrapper for the accept callback delegates to callback.
|
|
*
|
|
* @param sourceItemHandleScope - drag item handle scope.
|
|
* @param destScope - sortable target scope.
|
|
* @param destItemScope - sortable destination item scope.
|
|
* @returns {*|boolean} - true if drop is allowed for the drag item in drop target.
|
|
*/
|
|
$scope.accept = function (sourceItemHandleScope, destScope, destItemScope) {
|
|
return $scope.callbacks.accept(sourceItemHandleScope, destScope, destItemScope);
|
|
};
|
|
|
|
}]);
|
|
|
|
/**
|
|
* Sortable directive - defines callbacks.
|
|
* Parent directive for draggable and sortable items.
|
|
* Sets modelValue, callbacks, element in scope.
|
|
*/
|
|
mainModule.directive('asSortable',
|
|
function () {
|
|
return {
|
|
require: 'ngModel', // get a hold of NgModelController
|
|
restrict: 'A',
|
|
scope: true,
|
|
controller: 'as.sortable.sortableController',
|
|
link: function (scope, element, attrs, ngModelController) {
|
|
|
|
var ngModel, callbacks;
|
|
|
|
ngModel = ngModelController;
|
|
|
|
if (!ngModel) {
|
|
return; // do nothing if no ng-model
|
|
}
|
|
|
|
// Set the model value in to scope.
|
|
ngModel.$render = function () {
|
|
scope.modelValue = ngModel.$modelValue;
|
|
};
|
|
//set the element in scope to be accessed by its sub scope.
|
|
scope.element = element;
|
|
element.data('_scope',scope); // #144, work with angular debugInfoEnabled(false)
|
|
|
|
callbacks = {accept: null, orderChanged: null, itemMoved: null, dragStart: null, dragMove:null, dragCancel: null, dragEnd: null};
|
|
|
|
/**
|
|
* Invoked to decide whether to allow drop.
|
|
*
|
|
* @param sourceItemHandleScope - the drag item handle scope.
|
|
* @param destSortableScope - the drop target sortable scope.
|
|
* @param destItemScope - the drop target item scope.
|
|
* @returns {boolean} - true if allowed for drop.
|
|
*/
|
|
callbacks.accept = function (sourceItemHandleScope, destSortableScope, destItemScope) {
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Invoked when order of a drag item is changed.
|
|
*
|
|
* @param event - the event object.
|
|
*/
|
|
callbacks.orderChanged = function (event) {
|
|
};
|
|
|
|
/**
|
|
* Invoked when the item is moved to other sortable.
|
|
*
|
|
* @param event - the event object.
|
|
*/
|
|
callbacks.itemMoved = function (event) {
|
|
};
|
|
|
|
/**
|
|
* Invoked when the drag started successfully.
|
|
*
|
|
* @param event - the event object.
|
|
*/
|
|
callbacks.dragStart = function (event) {
|
|
};
|
|
|
|
/**
|
|
* Invoked when the drag started successfully.
|
|
*
|
|
* @param event - the event object.
|
|
*/
|
|
callbacks.dragMove = function (event) {
|
|
};
|
|
|
|
/**
|
|
* Invoked when the drag cancelled.
|
|
*
|
|
* @param event - the event object.
|
|
*/
|
|
callbacks.dragCancel = function (event) {
|
|
};
|
|
|
|
/**
|
|
* Invoked when the drag stopped.
|
|
*
|
|
* @param event - the event object.
|
|
*/
|
|
callbacks.dragEnd = function (event) {
|
|
};
|
|
|
|
//Set the sortOptions callbacks else set it to default.
|
|
scope.$watch(attrs.asSortable, function (newVal, oldVal) {
|
|
angular.forEach(newVal, function (value, key) {
|
|
if (callbacks[key]) {
|
|
if (typeof value === 'function') {
|
|
callbacks[key] = value;
|
|
}
|
|
} else {
|
|
scope.options[key] = value;
|
|
}
|
|
});
|
|
scope.callbacks = callbacks;
|
|
}, true);
|
|
|
|
// Set isDisabled if attr is set, if undefined isDisabled = false
|
|
if (angular.isDefined(attrs.isDisabled)) {
|
|
scope.$watch(attrs.isDisabled, function (newVal, oldVal) {
|
|
if (!angular.isUndefined(newVal)) {
|
|
scope.isDisabled = newVal;
|
|
}
|
|
}, true);
|
|
}
|
|
}
|
|
};
|
|
});
|
|
|
|
}());
|
|
|
|
/*jshint indent: 2 */
|
|
/*global angular: false */
|
|
|
|
(function () {
|
|
|
|
'use strict';
|
|
var mainModule = angular.module('as.sortable');
|
|
|
|
/**
|
|
* Controller for sortableItemHandle
|
|
*
|
|
* @param $scope - item handle scope.
|
|
*/
|
|
mainModule.controller('as.sortable.sortableItemHandleController', ['$scope', function ($scope) {
|
|
|
|
this.scope = $scope;
|
|
|
|
$scope.itemScope = null;
|
|
$scope.type = 'handle';
|
|
}]);
|
|
|
|
/**
|
|
* Directive for sortable item handle.
|
|
*/
|
|
mainModule.directive('asSortableItemHandle', ['sortableConfig', '$helper', '$window', '$document',
|
|
function (sortableConfig, $helper, $window, $document) {
|
|
return {
|
|
require: '^asSortableItem',
|
|
scope: true,
|
|
restrict: 'A',
|
|
controller: 'as.sortable.sortableItemHandleController',
|
|
link: function (scope, element, attrs, itemController) {
|
|
|
|
var dragElement, //drag item element.
|
|
placeHolder, //place holder class element.
|
|
placeElement,//hidden place element.
|
|
itemPosition, //drag item element position.
|
|
dragItemInfo, //drag item data.
|
|
containment,//the drag container.
|
|
containerPositioning, // absolute or relative positioning.
|
|
dragListen,// drag listen event.
|
|
scrollableContainer, //the scrollable container
|
|
dragStart,// drag start event.
|
|
dragMove,//drag move event.
|
|
dragEnd,//drag end event.
|
|
dragCancel,//drag cancel event.
|
|
isDraggable,//is element draggable.
|
|
placeHolderIndex,//placeholder index in items elements.
|
|
bindDrag,//bind drag events.
|
|
unbindDrag,//unbind drag events.
|
|
bindEvents,//bind the drag events.
|
|
unBindEvents,//unbind the drag events.
|
|
hasTouch,// has touch support.
|
|
dragHandled, //drag handled.
|
|
createPlaceholder,//create place holder.
|
|
isPlaceHolderPresent,//is placeholder present.
|
|
isDisabled = false; // drag enabled
|
|
|
|
hasTouch = $window.hasOwnProperty('ontouchstart');
|
|
|
|
if (sortableConfig.handleClass) {
|
|
element.addClass(sortableConfig.handleClass);
|
|
}
|
|
|
|
scope.itemScope = itemController.scope;
|
|
element.data('_scope', scope); // #144, work with angular debugInfoEnabled(false)
|
|
|
|
scope.$watch('sortableScope.isDisabled', function (newVal) {
|
|
if (isDisabled !== newVal) {
|
|
isDisabled = newVal;
|
|
if (isDisabled) {
|
|
unbindDrag();
|
|
} else {
|
|
bindDrag();
|
|
}
|
|
}
|
|
});
|
|
|
|
createPlaceholder = function (itemScope) {
|
|
if (typeof scope.sortableScope.options.placeholder === 'function') {
|
|
return angular.element(scope.sortableScope.options.placeholder(itemScope));
|
|
} else if (typeof scope.sortableScope.options.placeholder === 'string') {
|
|
return angular.element(scope.sortableScope.options.placeholder);
|
|
} else {
|
|
return angular.element($document[0].createElement(itemScope.element.prop('tagName')));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Listens for a 10px movement before
|
|
* dragStart is called to allow for
|
|
* a click event on the element.
|
|
*
|
|
* @param event - the event object.
|
|
*/
|
|
dragListen = function (event) {
|
|
|
|
var unbindMoveListen = function () {
|
|
angular.element($document).unbind('mousemove', moveListen);
|
|
angular.element($document).unbind('touchmove', moveListen);
|
|
element.unbind('mouseup', unbindMoveListen);
|
|
element.unbind('touchend', unbindMoveListen);
|
|
element.unbind('touchcancel', unbindMoveListen);
|
|
};
|
|
|
|
var startPosition;
|
|
var moveListen = function (e) {
|
|
e.preventDefault();
|
|
var eventObj = $helper.eventObj(e);
|
|
if (!startPosition) {
|
|
startPosition = { clientX: eventObj.clientX, clientY: eventObj.clientY };
|
|
}
|
|
if (Math.abs(eventObj.clientX - startPosition.clientX) + Math.abs(eventObj.clientY - startPosition.clientY) > 10) {
|
|
unbindMoveListen();
|
|
dragStart(event);
|
|
}
|
|
};
|
|
|
|
angular.element($document).bind('mousemove', moveListen);
|
|
angular.element($document).bind('touchmove', moveListen);
|
|
element.bind('mouseup', unbindMoveListen);
|
|
element.bind('touchend', unbindMoveListen);
|
|
element.bind('touchcancel', unbindMoveListen);
|
|
};
|
|
|
|
/**
|
|
* Triggered when drag event starts.
|
|
*
|
|
* @param event the event object.
|
|
*/
|
|
dragStart = function (event) {
|
|
|
|
var eventObj, tagName;
|
|
|
|
if (!hasTouch && (event.button === 2 || event.which === 3)) {
|
|
// disable right click
|
|
return;
|
|
}
|
|
if (hasTouch && $helper.isTouchInvalid(event)) {
|
|
return;
|
|
}
|
|
if (dragHandled || !isDraggable(event)) {
|
|
// event has already fired in other scope.
|
|
return;
|
|
}
|
|
// Set the flag to prevent other items from inheriting the drag event
|
|
dragHandled = true;
|
|
event.preventDefault();
|
|
eventObj = $helper.eventObj(event);
|
|
|
|
// (optional) Scrollable container as reference for top & left offset calculations, defaults to Document
|
|
scrollableContainer = angular.element($document[0].querySelector(scope.sortableScope.options.scrollableContainer)).length > 0 ?
|
|
$document[0].querySelector(scope.sortableScope.options.scrollableContainer) : $document[0].documentElement;
|
|
|
|
containment = (scope.sortableScope.options.containment)? $helper.findAncestor(element, scope.sortableScope.options.containment):angular.element($document[0].body);
|
|
//capture mouse move on containment.
|
|
containment.css('cursor', 'move');
|
|
containment.addClass('as-sortable-un-selectable');
|
|
|
|
// container positioning
|
|
containerPositioning = scope.sortableScope.options.containerPositioning || 'absolute';
|
|
|
|
dragItemInfo = $helper.dragItem(scope);
|
|
tagName = scope.itemScope.element.prop('tagName');
|
|
|
|
dragElement = angular.element($document[0].createElement(scope.sortableScope.element.prop('tagName')))
|
|
.addClass(scope.sortableScope.element.attr('class')).addClass(sortableConfig.dragClass);
|
|
dragElement.css('width', $helper.width(scope.itemScope.element) + 'px');
|
|
dragElement.css('height', $helper.height(scope.itemScope.element) + 'px');
|
|
|
|
placeHolder = createPlaceholder(scope.itemScope)
|
|
.addClass(sortableConfig.placeHolderClass).addClass(scope.sortableScope.options.additionalPlaceholderClass);
|
|
placeHolder.css('width', $helper.width(scope.itemScope.element) + 'px');
|
|
placeHolder.css('height', $helper.height(scope.itemScope.element) + 'px');
|
|
|
|
placeElement = angular.element($document[0].createElement(tagName));
|
|
if (sortableConfig.hiddenClass) {
|
|
placeElement.addClass(sortableConfig.hiddenClass);
|
|
}
|
|
|
|
itemPosition = $helper.positionStarted(eventObj, scope.itemScope.element, scrollableContainer);
|
|
//fill the immediate vacuum.
|
|
scope.itemScope.element.after(placeHolder);
|
|
//hidden place element in original position.
|
|
scope.itemScope.element.after(placeElement);
|
|
dragElement.append(scope.itemScope.element);
|
|
|
|
containment.append(dragElement);
|
|
$helper.movePosition(eventObj, dragElement, itemPosition, containment, containerPositioning, scrollableContainer);
|
|
|
|
scope.sortableScope.$apply(function () {
|
|
scope.callbacks.dragStart(dragItemInfo.eventArgs());
|
|
});
|
|
bindEvents();
|
|
};
|
|
|
|
/**
|
|
* Allow Drag if it is a proper item-handle element.
|
|
*
|
|
* @param event - the event object.
|
|
* @return boolean - true if element is draggable.
|
|
*/
|
|
isDraggable = function (event) {
|
|
|
|
var elementClicked, sourceScope, isDraggable;
|
|
|
|
elementClicked = angular.element(event.target);
|
|
|
|
// look for the handle on the current scope or parent scopes
|
|
sourceScope = fetchScope(elementClicked);
|
|
|
|
isDraggable = (sourceScope && sourceScope.type === 'handle');
|
|
|
|
//If a 'no-drag' element inside item-handle if any.
|
|
while (isDraggable && elementClicked[0] !== element[0]) {
|
|
if ($helper.noDrag(elementClicked)) {
|
|
isDraggable = false;
|
|
}
|
|
elementClicked = elementClicked.parent();
|
|
}
|
|
return isDraggable;
|
|
};
|
|
|
|
/**
|
|
* Inserts the placeHolder in to the targetScope.
|
|
*
|
|
* @param targetElement the target element
|
|
* @param targetScope the target scope
|
|
*/
|
|
function insertBefore(targetElement, targetScope) {
|
|
targetElement[0].parentNode.insertBefore(placeHolder[0], targetElement[0]);
|
|
dragItemInfo.moveTo(targetScope.sortableScope, targetScope.index());
|
|
}
|
|
|
|
/**
|
|
* Inserts the placeHolder next to the targetScope.
|
|
*
|
|
* @param targetElement the target element
|
|
* @param targetScope the target scope
|
|
*/
|
|
function insertAfter(targetElement, targetScope) {
|
|
targetElement.after(placeHolder);
|
|
dragItemInfo.moveTo(targetScope.sortableScope, targetScope.index() + 1);
|
|
}
|
|
|
|
/**
|
|
* Triggered when drag is moving.
|
|
*
|
|
* @param event - the event object.
|
|
*/
|
|
dragMove = function (event) {
|
|
|
|
var eventObj, targetX, targetY, targetScope, targetElement;
|
|
|
|
if (hasTouch && $helper.isTouchInvalid(event)) {
|
|
return;
|
|
}
|
|
// Ignore event if not handled
|
|
if (!dragHandled) {
|
|
return;
|
|
}
|
|
if (dragElement) {
|
|
|
|
event.preventDefault();
|
|
|
|
eventObj = $helper.eventObj(event);
|
|
scope.sortableScope.$apply(function () {
|
|
scope.callbacks.dragMove(itemPosition, containment);
|
|
});
|
|
$helper.movePosition(eventObj, dragElement, itemPosition, containment, containerPositioning, scrollableContainer);
|
|
|
|
targetX = eventObj.pageX - $document[0].documentElement.scrollLeft;
|
|
targetY = eventObj.pageY - ($window.pageYOffset || $document[0].documentElement.scrollTop);
|
|
|
|
//IE fixes: hide show element, call element from point twice to return pick correct element.
|
|
dragElement.addClass(sortableConfig.hiddenClass);
|
|
$document[0].elementFromPoint(targetX, targetY);
|
|
targetElement = angular.element($document[0].elementFromPoint(targetX, targetY));
|
|
dragElement.removeClass(sortableConfig.hiddenClass);
|
|
|
|
//Set Class as dragging starts
|
|
dragElement.addClass(sortableConfig.dragging);
|
|
|
|
targetScope = fetchScope(targetElement);
|
|
|
|
if (!targetScope || !targetScope.type) {
|
|
return;
|
|
}
|
|
if (targetScope.type === 'handle') {
|
|
targetScope = targetScope.itemScope;
|
|
}
|
|
if (targetScope.type !== 'item' && targetScope.type !== 'sortable') {
|
|
return;
|
|
}
|
|
|
|
if (targetScope.type === 'item' && targetScope.accept(scope, targetScope.sortableScope, targetScope)) {
|
|
// decide where to insert placeholder based on target element and current placeholder if is present
|
|
targetElement = targetScope.element;
|
|
|
|
var placeholderIndex = placeHolderIndex(targetScope.sortableScope.element);
|
|
if (placeholderIndex < 0) {
|
|
insertBefore(targetElement, targetScope);
|
|
} else {
|
|
if (placeholderIndex <= targetScope.index()) {
|
|
insertAfter(targetElement, targetScope);
|
|
} else {
|
|
insertBefore(targetElement, targetScope);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (targetScope.type === 'sortable') {//sortable scope.
|
|
if (targetScope.accept(scope, targetScope) &&
|
|
targetElement[0].parentNode !== targetScope.element[0]) {
|
|
//moving over sortable bucket. not over item.
|
|
if (!isPlaceHolderPresent(targetElement)) {
|
|
targetElement[0].appendChild(placeHolder[0]);
|
|
dragItemInfo.moveTo(targetScope, targetScope.modelValue.length);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Fetch scope from element or parents
|
|
* @param {object} element Source element
|
|
* @return {object} Scope, or null if not found
|
|
*/
|
|
function fetchScope(element) {
|
|
var scope;
|
|
while (!scope && element.length) {
|
|
scope = element.data('_scope');
|
|
if (!scope) {
|
|
element = element.parent();
|
|
}
|
|
}
|
|
return scope;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get position of place holder among item elements in itemScope.
|
|
* @param targetElement the target element to check with.
|
|
* @returns {*} -1 if placeholder is not present, index if yes.
|
|
*/
|
|
placeHolderIndex = function (targetElement) {
|
|
var itemElements, i;
|
|
|
|
itemElements = targetElement.children();
|
|
for (i = 0; i < itemElements.length; i += 1) {
|
|
//TODO may not be accurate when elements contain other siblings than item elements
|
|
//solve by adding 1 to model index of previous item element
|
|
if (angular.element(itemElements[i]).hasClass(sortableConfig.placeHolderClass)) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
};
|
|
|
|
|
|
/**
|
|
* Check there is no place holder placed by itemScope.
|
|
* @param targetElement the target element to check with.
|
|
* @returns {*} true if place holder present.
|
|
*/
|
|
isPlaceHolderPresent = function (targetElement) {
|
|
return placeHolderIndex(targetElement) >= 0;
|
|
};
|
|
|
|
/**
|
|
* Rollback the drag data changes.
|
|
*/
|
|
|
|
function rollbackDragChanges() {
|
|
placeElement.replaceWith(scope.itemScope.element);
|
|
placeHolder.remove();
|
|
dragElement.remove();
|
|
dragElement = null;
|
|
dragHandled = false;
|
|
containment.css('cursor', '');
|
|
containment.removeClass('as-sortable-un-selectable');
|
|
}
|
|
|
|
/**
|
|
* triggered while drag ends.
|
|
*
|
|
* @param event - the event object.
|
|
*/
|
|
dragEnd = function (event) {
|
|
// Ignore event if not handled
|
|
if (!dragHandled) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
if (dragElement) {
|
|
//rollback all the changes.
|
|
rollbackDragChanges();
|
|
// update model data
|
|
dragItemInfo.apply();
|
|
scope.sortableScope.$apply(function () {
|
|
if (dragItemInfo.isSameParent()) {
|
|
if (dragItemInfo.isOrderChanged()) {
|
|
scope.callbacks.orderChanged(dragItemInfo.eventArgs());
|
|
}
|
|
} else {
|
|
scope.callbacks.itemMoved(dragItemInfo.eventArgs());
|
|
}
|
|
});
|
|
scope.sortableScope.$apply(function () {
|
|
scope.callbacks.dragEnd(dragItemInfo.eventArgs());
|
|
});
|
|
dragItemInfo = null;
|
|
}
|
|
unBindEvents();
|
|
};
|
|
|
|
/**
|
|
* triggered while drag is cancelled.
|
|
*
|
|
* @param event - the event object.
|
|
*/
|
|
dragCancel = function (event) {
|
|
// Ignore event if not handled
|
|
if (!dragHandled) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
|
|
if (dragElement) {
|
|
//rollback all the changes.
|
|
rollbackDragChanges();
|
|
scope.sortableScope.$apply(function () {
|
|
scope.callbacks.dragCancel(dragItemInfo.eventArgs());
|
|
});
|
|
dragItemInfo = null;
|
|
}
|
|
unBindEvents();
|
|
};
|
|
|
|
/**
|
|
* Binds the drag start events.
|
|
*/
|
|
bindDrag = function () {
|
|
element.bind('touchstart', dragListen);
|
|
element.bind('mousedown', dragListen);
|
|
};
|
|
|
|
/**
|
|
* Unbinds the drag start events.
|
|
*/
|
|
unbindDrag = function () {
|
|
element.unbind('touchstart', dragListen);
|
|
element.unbind('mousedown', dragListen);
|
|
};
|
|
|
|
//bind drag start events.
|
|
bindDrag();
|
|
|
|
//Cancel drag on escape press.
|
|
angular.element($document[0].body).bind('keydown', function (event) {
|
|
if (event.keyCode === 27) {
|
|
dragCancel(event);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Binds the events based on the actions.
|
|
*/
|
|
bindEvents = function () {
|
|
angular.element($document).bind('touchmove', dragMove);
|
|
angular.element($document).bind('touchend', dragEnd);
|
|
angular.element($document).bind('touchcancel', dragCancel);
|
|
angular.element($document).bind('mousemove', dragMove);
|
|
angular.element($document).bind('mouseup', dragEnd);
|
|
};
|
|
|
|
/**
|
|
* Un binds the events for drag support.
|
|
*/
|
|
unBindEvents = function () {
|
|
angular.element($document).unbind('touchend', dragEnd);
|
|
angular.element($document).unbind('touchcancel', dragCancel);
|
|
angular.element($document).unbind('touchmove', dragMove);
|
|
angular.element($document).unbind('mouseup', dragEnd);
|
|
angular.element($document).unbind('mousemove', dragMove);
|
|
};
|
|
}
|
|
};
|
|
}]);
|
|
}());
|
|
|
|
/*jshint indent: 2 */
|
|
/*global angular: false */
|
|
|
|
(function () {
|
|
|
|
'use strict';
|
|
var mainModule = angular.module('as.sortable');
|
|
|
|
/**
|
|
* Controller for sortable item.
|
|
*
|
|
* @param $scope - drag item scope
|
|
*/
|
|
mainModule.controller('as.sortable.sortableItemController', ['$scope', function ($scope) {
|
|
|
|
this.scope = $scope;
|
|
|
|
$scope.sortableScope = null;
|
|
$scope.modelValue = null; // sortable item.
|
|
$scope.type = 'item';
|
|
|
|
/**
|
|
* returns the index of the drag item from the sortable list.
|
|
*
|
|
* @returns {*} - index value.
|
|
*/
|
|
$scope.index = function () {
|
|
return $scope.$index;
|
|
};
|
|
|
|
/**
|
|
* Returns the item model data.
|
|
*
|
|
* @returns {*} - item model value.
|
|
*/
|
|
$scope.itemData = function () {
|
|
return $scope.sortableScope.modelValue[$scope.$index];
|
|
};
|
|
|
|
}]);
|
|
|
|
/**
|
|
* sortableItem directive.
|
|
*/
|
|
mainModule.directive('asSortableItem', ['sortableConfig',
|
|
function (sortableConfig) {
|
|
return {
|
|
require: ['^asSortable', '?ngModel'],
|
|
restrict: 'A',
|
|
controller: 'as.sortable.sortableItemController',
|
|
link: function (scope, element, attrs, ctrl) {
|
|
var sortableController = ctrl[0];
|
|
var ngModelController = ctrl[1];
|
|
if (sortableConfig.itemClass) {
|
|
element.addClass(sortableConfig.itemClass);
|
|
}
|
|
scope.sortableScope = sortableController.scope;
|
|
if (ngModelController) {
|
|
ngModelController.$render = function () {
|
|
scope.modelValue = ngModelController.$modelValue;
|
|
};
|
|
} else {
|
|
scope.modelValue = sortableController.scope.modelValue[scope.$index];
|
|
}
|
|
scope.element = element;
|
|
element.data('_scope',scope); // #144, work with angular debugInfoEnabled(false)
|
|
}
|
|
};
|
|
}]);
|
|
|
|
}());
|