/** * Sortable lists * @module Ink.UI.SortableList_1 * @version 1 */ Ink.createModule('Ink.UI.SortableList', '1', ['Ink.UI.Common_1','Ink.Dom.Css_1','Ink.Dom.Event_1','Ink.Dom.Element_1','Ink.Dom.Selector_1'], function( Common, Css, Events, Element, Selector ) { 'use strict'; var hasTouch = (('ontouchstart' in window) || // html5 browsers (navigator.maxTouchPoints > 0) || // future IE (navigator.msMaxTouchPoints > 0)); /** * Adds sortable behaviour to any list. * * @class Ink.UI.SortableList * @constructor * @version 1 * @param {String|DOMElement} selector * @param {String} [options.placeholderClass] CSS class added to the "ghost" element being dragged around. Defaults to 'placeholder'. * @param {String} [options.draggedClass] CSS class added to the original element being dragged around. Defaults to 'hide-all'. * @param {String} [options.draggingClass] CSS class added to the html element when the user is dragging. Defaults to 'dragging'. * @param {String} [options.dragSelector] CSS selector for the drag enabled nodes. Defaults to 'li'. * @param {String} [options.handleSelector] CSS selector for the drag handle. If present, you can only drag nodes by this selector. * @param {String} [options.moveSelector] CSS selector to validate a node move. If present, you can only move nodes inside this selector. * @param {Boolean} [options.swap] Flag to swap dragged element and target element instead of reordering it. * @param {Boolean} [options.cancelMouseOut] Flag to cancel draggin if mouse leaves the container element. * @param {Function} [options.onDrop] Callback to be executed after dropping an element. Receives { droppedElement: DOMElement } as an argument. * * @sample Ink_UI_SortableList_1.html */ function SortableList() { Common.BaseUIComponent.apply(this, arguments); } SortableList._name = 'SortableList_1'; SortableList._optionDefinition = { 'placeholderClass': ['String', 'placeholder'], 'draggedClass': ['String', 'hide-all'], 'draggingClass': ['String', 'dragging'], 'dragSelector': ['String', 'li'], 'dragObject': ['String', null], // Deprecated. Use handleSelector instead. 'handleSelector': ['String', null], 'moveSelector': ['String', false], 'swap': ['Boolean', false], 'cancelMouseOut': ['Boolean', false], 'onDrop': ['Function', function(){}] }; SortableList.prototype = { /** * Init function called by the constructor. * * @method _init * @private */ _init: function() { if (this._options.dragObject != null) { // [3.0.0] Remove this deprecation notice and stop providing backwards compatibility Ink.warn('Ink.UI.SortableList: options.dragObject is now deprecated. ' + 'Please use options.handleSelector instead.'); this._options.handleSelector = this._options.handleSelector || this._options.dragObject; } this._handlers = { down: Ink.bind(this._onDown, this), move: Ink.bind(this._onMove, this), up: Ink.bind(this._onUp, this) }; this._isMoving = false; this._down = hasTouch ? 'touchstart mousedown' : 'mousedown'; this._move = hasTouch ? 'touchmove mousemove' : 'mousemove'; this._up = hasTouch ? 'touchend mouseup' : 'mouseup'; this._observe(); }, /** * Sets the event handlers. * * @method _observe * @private */ _observe: function() { Events.on(this._element, this._down, this._options.dragSelector, this._handlers.down); Events.on(this._element, this._move, this._options.dragSelector, this._handlers.move); if(this._options.cancelMouseOut) { Events.on(this._element, 'mouseleave', Ink.bind(this.stopMoving, this)); } Events.on(document.documentElement, this._up, this._handlers.up); }, /** * Mousedown or touchstart handler * * @method _onDown * @param {Event} ev * @private */ _onDown: function(ev) { if (this._isMoving || this._placeholder) { return; } if(this._options.handleSelector && !Selector.matchesSelector(ev.target, this._options.handleSelector)) { return; } var tgtEl = ev.currentTarget; this._isMoving = tgtEl; this._placeholder = tgtEl.cloneNode(true); this._movePlaceholder(tgtEl); this._addMovingClasses(); return false; }, /** * Mousemove or touchmove handler * * @method _onMove * @param {Event} ev * @private */ _onMove: function(ev) { var target = ev.currentTarget; // Touch events give you the element where the finger touched first, // not the element under it like mouse events. if (ev.type === 'touchmove') { var touch = ev.touches[0]; target = document.elementFromPoint(touch.clientX, touch.clientY); target = Element.findUpwardsBySelector(target, this._options.dragSelector); } this.validateMove(target); ev.preventDefault(); }, /** * Mouseup or touchend handler * * @method _onUp * @param {Event} ev * @private */ _onUp: function(ev) { if (!this._isMoving || !this._placeholder) { return; } if (ev.currentTarget === this._isMoving) { return; } if (ev.currentTarget === this._placeholder) { return; } Element.insertBefore(this._isMoving, this._placeholder); this.stopMoving(); this._options.onDrop.call(this, { droppedElement: ev.currentTarget }); return false; }, /** * Adds the CSS classes to interactive elements * * @method _addMovingClasses * @private */ _addMovingClasses: function(){ Css.addClassName(this._placeholder, this._options.placeholderClass); Css.addClassName(this._isMoving, this._options.draggedClass); Css.addClassName(document.documentElement, this._options.draggingClass); }, /** * Removes the CSS classes from interactive elements * * @method _removeMovingClasses * @private */ _removeMovingClasses: function(){ if(this._isMoving) { Css.removeClassName(this._isMoving, this._options.draggedClass); } if(this._placeholder) { Css.removeClassName(this._placeholder, this._options.placeholderClass); } Css.removeClassName(document.documentElement, this._options.draggingClass); }, /** * Moves the placeholder element relative to the target element * * @method _movePlaceholder * @param {Element} target_position * @private */ _movePlaceholder: function(target){ var placeholder = this._placeholder, target_position, placeholder_position, from_top, from_left; if(!placeholder) { Element.insertAfter(placeholder, target); } else if(this._options.swap){ Element.insertAfter(placeholder, target); Element.insertBefore(target, this._isMoving); Element.insertBefore(this._isMoving, placeholder); } else { target_position = Element.offset(target); placeholder_position = Element.offset(this._placeholder); from_top = target_position[1] > placeholder_position[1]; from_left = target_position[0] > placeholder_position[0]; if( ( from_top && from_left ) || ( !from_top && !from_left ) ) { Element.insertBefore(placeholder, target); } else { Element.insertAfter(placeholder, target); } Element.insertBefore(this._isMoving, placeholder); } }, /************** * PUBLIC API * **************/ /** * Unregisters the component and removes its markup * * @method destroy * @public */ destroy: Common.destroyComponent, /** * Visually stops moving. * Removes the placeholder as well as the styling classes. * * @method _movePlaceholder * @public */ stopMoving: function(){ this._removeMovingClasses(); Element.remove(this._placeholder); this._placeholder = false; this._isMoving = false; }, /** * Validate a move. * This method is used by the move handler * * @method _movePlaceholder * @param {Element} elem * @public */ validateMove: function(elem){ if (!elem || !this._isMoving || !this._placeholder) { return; } if (elem === this._placeholder) { return; } if (elem === this._isMoving) { return; } if(!this._options.moveSelector || Selector.matchesSelector(elem, this._options.moveSelector)){ this._movePlaceholder(elem); } else { this.stopMoving(); } } }; Common.createUIComponent(SortableList); return SortableList; });