﻿/*
Script: Sortables.js
	Contains <Sortables> Class.

License:
	MIT-style license.
	
Authors:
	Original Author and
	Copyright (c) 2007. Craig Ruksznis [cr] <info@craigruk.com>
*/

/*
Class: Sortables
	Creates an interface for <Drag.Base> and drop, resorting of a list.

Note:
	The Sortables require an XHTML doctype.
	Code based on the original, one-dimensional sortables class released
	by Mootools.

Arguments:
	list - required, the list that will become sortable.
	options - an Object, see options below.

Options:
	handles - a collection of elements to be used for drag handles. defaults to the elements.
	
Events:
	onStart - function executed when the item starts dragging
	onComplete - function executed when the item ends dragging
*/

var Sortables = new Class({

	options: {
		handles: false,
		onStart: Class.empty,
		onComplete: Class.empty,
		ghost: true,
		snap: 3,
		onDragStart: function(element, ghost){
			ghost.setStyle('opacity', 0.7);
			element.setStyle('opacity', 0.7);
		},
		onDragComplete: function(element, ghost){
			element.setStyle('opacity', 0.999);
			ghost.remove();
			this.trash.remove();
		}
	},

	initialize: function(list, options){
		this.setOptions(options);
		this.list = $(list);
		this.elements = this.list.getChildren();
		this.handles = (this.options.handles) ? $$(this.options.handles) : this.elements;
		this.bound = {
			'start': [],
			'moveGhost': this.moveGhost.bindWithEvent(this)
		};
		for (var i = 0, l = this.handles.length; i < l; i++){
			this.bound.start[i] = this.start.bindWithEvent(this, this.elements[i]);
		}
		this.attach();
		if (this.options.initialize) this.options.initialize.call(this);
		this.bound.move = this.move.bindWithEvent(this);
		this.bound.end = this.end.bindWithEvent(this);
	},

	attach: function(){
		this.handles.each(function(handle, i){
			handle.addEvent('mousedown', this.bound.start[i]);
		}, this);
	},

	detach: function(){
		this.handles.each(function(handle, i){
			handle.removeEvent('mousedown', this.bound.start[i]);
		}, this);
	},

	start: function(event, el){
		this.active = el;
		this.coordinates = this.list.getCoordinates();
		if (this.options.ghost){
			var position = el.getPosition();
			this.offset_y = event.page.y - position.y;
			this.offset_x = event.page.x - position.x;
			this.trash = new Element('div').inject(document.body);
			this.ghost = el.clone().inject(this.trash).setStyles({
				'position': 'absolute',
				'left': event.page.x - this.offset_x,
				'top': event.page.y - this.offset_y
			});
			document.addListener('mousemove', this.bound.moveGhost);
			this.fireEvent('onDragStart', [el, this.ghost]);
		}
		document.addListener('mousemove', this.bound.move);
		document.addListener('mouseup', this.bound.end);
		this.fireEvent('onStart', el);
		event.stop();
	},

	moveGhost: function(event){
		var value_y = event.page.y - this.offset_y;
		var value_x = event.page.x - this.offset_x;
		// The next two lines contain the sortables within its box.
		// Comment them out to allow sortables anywhere on the screen.
		value_y = value_y.limit(this.coordinates.top, this.coordinates.bottom - this.ghost.offsetHeight);
		value_x = value_x.limit(this.coordinates.left, this.coordinates.right - this.ghost.offsetWidth);
		value_y = value_y.limit(0, window.getScrollHeight());
		value_x = value_x.limit(0, window.getScrollWidth());
		this.ghost.setStyle('top', value_y);
		this.ghost.setStyle('left', value_x);
		event.stop();
	},

	move: function(event){
		// set variables
		var now_y = event.page.y;
		var now_x = event.page.x;
		var prev = this.active.getPrevious();
		var next = this.active.getNext();
		
		if (this.coordinates.right >= now_x &&
			this.coordinates.left <= now_x &&
			this.coordinates.top <= now_y &&
			this.coordinates.bottom >= now_y) {
		
			if (prev && now_y < this.active.getCoordinates().top) {
				// if the new position is less than the current y's top
			
				// move element back in terms of y
				this.active.injectBefore(prev);
			
			} else if (next && now_y > this.active.getCoordinates().bottom) {
				// else if the new position is more than the current y's bottom

				// move element forward in terms of y
				this.active.injectAfter(next);
			
			} else {
				// else if no change in y
			
				if (prev && now_x < this.active.getCoordinates().left) {
					// if the new position is less in terms of x
					this.active.injectBefore(prev);
				
				} else if (next && now_x > this.active.getCoordinates().right) {
					// if the new position is more in terms of x
					this.active.injectAfter(next);
				
				}
			}
			this.outside_of_box = false;
		} else {
			this.outside_of_box = true;
		}
		
	},

	serialize: function(converter){
		return this.list.getChildren().map(converter || function(el){
			return this.elements.indexOf(el);
		}, this);
	},

	end: function(event){
		this.previous_y = null;
		this.previous_x = null;
		document.removeListener('mousemove', this.bound.move);
		document.removeListener('mouseup', this.bound.end);
		if (this.options.ghost){
			document.removeListener('mousemove', this.bound.moveGhost);
			this.fireEvent('onDragComplete', [this.active, this.ghost]);
		}
		this.fireEvent('onComplete', this.active);
	}

});

Sortables.implement(new Events, new Options);
