/******************************************************************************
HomeBox v0.1
Author: Harel Malka  
http://www.harelmalka.com 
harel@harelmalka.com
Bugs: Leave comment on the site
----------------------------------------------------------------------------
A good real life demo of this component in action can be found at 
http://www.freecrm.com
You would need to register for a free account to see it though. 
----------------------------------------------------------------------------

Like most things, this component was created out of necessity. 
I was looking for a component to handle home page display for freecrm.com, 
with movable configurable boxes in the style of iGoogle. 
Javascript frameworks are nice, but I was after something stand alone, with 
as little dependency on external frameworks as possible, and withot too many 
frills. It had to perform a single task and nothing else. There was nothing
like that 'out there'. 

You'll find the following files in this component:

index.html -> this is the html layout for the home page. Its arranged over 
3 columns which are governed by a table. Each box is made out of a few divs. 
One to encapsulate the box, one to act as the handle (which you use to drag 
the box with), and a few others. It also contains some style definitions which 
govern how the boxes look. You'll have to take a look at this file to see how its
all arranged. There's nothing fancy there. Just some HTML. The only thing to note there
is that each box has some custom attributes in some of its DIVs, including the box's id
and a prefix of 'homebox_'. Keep the prefix - but change the id to what you need.

homebox.js -> This file. The main HomeBox object where all the magic comes from.

domdrag.js -> I didn't want to re-invent the wheel, so the only external code 
is youngpupps wonderful domdrag object to handle all the drag and drops. 

Some images to act as window controls. You can use those or replace them (they were
all taken from free icon sets)

You start by initializing the HomeBox object, after the page has finished loading 
but jumping on the onLoad event for the window. At this stage we can setup some
custom javascript functions to be called when a window is dropped, moved or removed. 
The drop and move functions provide you with the name (id) of the box, the column its at 
now (1, 2 or 3) and its new order. Order is a number incremented by 5 for each vertical 
position. For example, if you drag a box to the top of a column its order would be 5. 
The second box would be 10 etc. The purpose of this order property is to simply allow
you to sort the boxes in a column via a numeric value. 
Each box can be dragged, but can also add custom right-left-up-down buttons which are a 
1-click box movers, instead of drag. Sort of a backup for people who have a "drag problem".
What happens when these event handles fire is up to you. In freecrm.com I save their position 
in the database for each user using some RPC calls. If you don't define an event handler, 
the default behaviour is to do nothing, but if HomeBox.debugMode is true, to alert you the 
parameters passed. I also load the box positions and genearate my equivalent of index.html 
dynamically. You can do the same, or perhaps save their positions in cookies, or not at all. 
Its up to you and your application requirements. 

NOTE: You'll need to include the domdrag.js script provided. It handles all the drag&drop 
functionality. Also note that index.html also contains some CSS classes that determine the look 
and feel of the boxes. 

window.onload = function () 
{ 
	HomeBox.init(); 
	// setup a custom onBoxDrop event handler to be fired when a box is dropped after drag.
	HomeBox.onBoxDrop = function(name,column,position) { 
							alert('Box dropped: ' + name + ", " + column + ", "+ position);
							};
	HomeBox.onBoxMove = function(name,column,position) { 
							alert('Box moved: ' + name + ", " + column + ", "+ position);
							};
	HomeBox.onBoxRemove = function(x) {
							alert('Box removed: ' + x )
							};
}


******************************************************************************/
var HomeBox = {
	name:         "HomeBox v0.1 by Harel Malka",
	ctrlImgRoot:  "./",
	ctrlImages:   {left:"arrow_left.png", up:"arrow_up.png", right:"arrow_right.png",  down:"arrow_down.png"},
	columns:      { homeColumnLeft:   {id:1, xStart:0, xEnd:0}, 
					homeColumnCenter: {id:2, xStart:0, xEnd:0},
					homeColumnRight:  {id:3, xStart:0, xEnd:0} },
	
	debugMode: false, // set to true to trigger some debuging action
	dbgWin:    null,
	dragObj:   null,
	rect:      null,
	outline:   null,
	boxes:     null,
	lastX:     0,
	lastY:     0,
	mouseX:    0,
	mouseY:    0,
	gridSize:  10,
	dropPos:   0, // dropped box position
	tmp:       0, // various tmp neesd
	boxPrefix: 'homebox_', // you can change this if you need to change the html div prefixes
	
	init: function () {
		this.boxes = new Object();
		this.createFrame();
		this.registerBoxes();
		if (this.debugMode) {
			this.dbgWin = window.open('', 'dbg','width=500,height=700,scrollbars,status');
			this.dbgWin.document.clear();
			this.dbgWin.document.write("<textarea id='txt' rows=40 cols=60></textarea>");
			this.debug("Started");
		}
		
	},

	registerBoxes: function () {
		var boxes = document.getElementsByTagName("DIV");
		var thisDiv = null;
		for (b=0; b<boxes.length; b++) {
			thisDiv = boxes[b];
			isBox = thisDiv.getAttribute("homebox") || false;
			if (isBox) { 
				this.registerBox(thisDiv, isBox); 
			}	
		}	
		this.dropPos = 0;
		this.tmp = 0;
	},

	// receives a div object and generates a HomeBox out of it
	registerBox: function(obj, boxName) { 
		var pos = this.findPosition(obj); 
		var handleId = obj.getAttribute("handle") || null; 
		var handle = document.getElementById(handleId);
		var thisCol = this.getColumn(obj);
		if (handle) handle.rootId = obj.id;
		if (thisCol!=this.tmp) {
			this.dropPos = 10;
			this.tmp = thisCol; 
		} else { 
			this.dropPos += 10;
		}
		
		var box = {
			id:       obj.id,
			boxName:  boxName, 
			handleId: handleId,
			handle:   handle,
			root:     obj,
			startX:   pos.x,
			startY:   pos.y,
			endX:     pos.x + obj.offsetWidth,
			endY:     pos.y + obj.offsetWidth, 
			width:    obj.offsetWidth,
			height:   obj.offsetWidth,
			zIndex:   obj.style.zIndex,
			column:   thisCol,
			position: this.dropPos
		}; 
		this.setDragHandler(box);
		this.boxes[obj.id] = box;
	},
	
	getBox: function (id) {
		return this.boxes[id];
	},	
	
	getColumn: function (obj) {
		var idName = obj.parentNode.id;
		var colNum = this.columns[idName].id; 
		if (!isNaN(colNum)) {
			return colNum;
		} else {
			return -1;
		}
	},

	setDragHandler: function(box) {
		var root = box.root;
		var handle = box.handle;
		if (handle != null) {
			var startX = box.startX;
			var startY = box.startY;
			var zIndex = box.zIndex;
			var rootId = box.id;
			handle.rootId = rootId;
			Drag.init( handle, root ); 
			box.root.onDrag      = function(x, y) { HomeBox.boxDrag(rootId, x, y); };
			box.root.onDragStart = function(x, y) { HomeBox.boxClick(rootId, x , y); };
			box.root.onDragEnd   = function()     { HomeBox.boxDrop(rootId); };
		}
	},
	  
	createFrame: function() {
		//var box = document.getElementById(rootId);
		var rect = document.createElement("DIV");
		rect.style.position = 'relative';
		rect.style.zIndex = 0;
		rect.className = 'homeBoxFrame';
		rect.style.display = 'none';
		rect.id = "homeboxLocator";
		document.body.appendChild(rect);
		return rect;
	},
	
	moveFrame: function (rootId, lastBox) {
		var box = document.getElementById(rootId);
		var rect = document.getElementById('homeboxLocator');
		if (rect==null) rect = this.createFrame();
		rect.style.display = 'block';
		if (box.parentNode.nodeName == 'DIV') {
			if (lastBox) {
				box.parentNode.appendChild(rect);
			} else {
				box.parentNode.insertBefore(rect, box);
			}
		} else {
			box.appendChild(rect);
		}
		rect.style.height = this.dragObj.offsetHeight + 'px'; 
		rect.style.width  = this.dragObj.offsetWidth + 'px'; 
	},
	
	removeFrame: function() {
		var rect = document.getElementById('homeboxLocator');
		if (rect) rect.style.display = 'none'; 
	},
	
	setMousePos: function(e) {
		HomeBox.mouseX = document.all ? event.clientX + document.body.scrollLeft : e.clientX;
		HomeBox.mouseY = document.all ? event.clientY + document.body.scrollTop  : e.clientY;  
	},
	
	position: function (obj, x, y) {
		obj.style.left = x + 'px';
		obj.style.top  = y + 'px';
	},
	
	boxClick: function (rootId, x, y) {
		this.bindEvent('mousemove', document, HomeBox.setMousePos);
		this.setColumnPositions();
		var box = document.getElementById(rootId);
		this.dragObj = box;
		this.moveFrame(rootId);
		//var rect = document.getElementById('homeboxLocator');
		box.style.zIndex=199;
		box.style.width = box.offsetWidth + 'px';
		box.style.top = this.mouseY;
		box.style.left = this.mouseX;
		box.style.textAlign = 'left';
		box.style.filter = 'alpha(opacity=70)';
		box.style.opacity = '0.7'; 		
	},
	
	boxDrag: function(rootId, x, y) {   
		this.findBoxUnder(rootId, x, y);
	},
	
	boxDrop: function (id) {
		this.unbindEvent('mousemove', document, HomeBox.setMousePos);
		var homebox = this.boxes[id];
		var box = document.getElementById(id);
		var rect = document.getElementById('homeboxLocator');
		var col = rect.parentNode;
		var colNum = this.columns[col.id].id;
		this.boxes[id].column=colNum;
		box.style.zIndex = homebox.zIndex;
		box.style.position = 'relative';
		box.style.left = '0px';
		box.style.top  = '0px'; 
		box.style.filter = null;
		box.style.opacity = null;
		box.style.width = '100%';
		col.replaceChild(box, rect ); 
		// perform the ondrop event handler passing box name, column and vertical order.
		this.onBoxDrop(homebox.boxName, this.columns[col.id].id, this.dropPos-5);
		this.dragObj = null;
	},
	
	setColumnPositions: function () {
		var col, pos;
		var prevCol = false;
		for (var c in this.columns) {	
			col = document.getElementById(c);
			pos = this.findPosition(col);
			this.columns[c].xStart = pos.x;
			if (prevCol && prevCol!=c) this.columns[prevCol].xEnd = pos.x-5;
			prevCol = c;
		}
	},
	
	findColumnUnder: function (xLeft, xRight) {
		for (var c in this.columns) {
			if (this.mouseX > this.columns[c].xStart && ( this.columns[c].xEnd==0 || this.mouseX < this.columns[c].xEnd)) {
				return c;
			}
		}
		return '';
	},
	
	findBoxUnder: function (id, x, y) { 
		// work with a set grid to enhance performance. No need to calculate per pixel here.
		if (Math.abs(this.lastX - this.mouseX) > this.gridSize || Math.abs(this.lastY - this.mouseY) > this.gridSize) {
			this.lastX = this.mouseX;
			this.lastY = this.mouseY;
		} else {
			return false;
		}
		var thisBox, i, b;
		var dragBox = this.boxes[id];
		var box = document.getElementById(id);
		var pos = this.findPosition(box); 
		var mW, mH; // median width, median height
		var topRight = pos.x+box.offsetWidth;
		var bottomLeft = pos.y + box.offsetHeight;
		var colName = this.findColumnUnder(pos.x, topRight); 
		box.style.position = 'absolute';
		this.position(box, this.mouseX - 20, this.mouseY - 10);
		
		if (colName!='') {
			var colNum = this.columns[colName].id;
			var colBoxes = this.getBoxesFromColumn(colNum);
			if (colBoxes.length == 0) {
				this.moveFrame(colName);
				this.dropPos = 1;
				return true;
			} else {
				for (i=0; i<colBoxes.length; i++) {
					b = colBoxes[i];
					thisBox = this.boxes[b];  
					thisRoot = document.getElementById(b);
					mW = Math.round(thisRoot.offsetWidth/2) + thisRoot.offsetLeft;
					mH = Math.round(thisRoot.offsetHeight/2) + thisRoot.offsetTop;
					if (pos.x  < mW && pos.y < mH) { 
						this.moveFrame(b);
						this.dropPos = thisBox.position;
						return true;
					}  
					// handle last box in a column
					if (i == colBoxes.length-1 && pos.x < mW && pos.y > mH) {
						this.moveFrame(b, true);
						this.dropPos = thisBox.position + 20;
						return true;
					}
				}
			}
		}  
		return false;
	}, 
	
	getBoxesFromColumn: function (colNum) {
		var boxes = new Array(0);
		for (var b in this.boxes) {
			if (this.boxes[b].column==colNum) {
				boxes.push(b); 
			}
		}
		return boxes;
	},
	
	findPosition: function (obj) {
		var lft = 0;
		var top = 0;
		if (obj.offsetParent) {
			lft = obj.offsetLeft
			top = obj.offsetTop
			while (obj = obj.offsetParent) {
				lft += obj.offsetLeft
				top += obj.offsetTop
			}
		}
		return {x:lft,y:top};
	}, 
	
	showControlPanel: function (parentObj, boxId) {
		if (typeof this.ctrlImages.left != 'object') this.loadCtrlImages();
		this.removeControlPanel();
		var ctrlHandlers = {left:"moveLeft", up:"moveUp", right:"moveRight",  down:"moveDown"};
		var ctrl = document.createElement("DIV");
		var box = this.boxes[boxId];
		var width = 0;
		var img;
		ctrl.id = 'homeBoxCtrlPanel';
		ctrl.style.height=this.ctrlImages.left.height + 'px';
		ctrl.style.top  = parentObj.style.top;
		ctrl.style.left =  parentObj.style.left;
		ctrl.className = 'homeBoxCtrl';
		for (img in this.ctrlImages) {
			width += this.ctrlImages[img].width;  
			this.ctrlImages[img].boxId = boxId;
			this.ctrlImages[img].onclick = HomeBox[ctrlHandlers[img]];
			ctrl.appendChild(this.ctrlImages[img]);
		}
		ctrl.style.width = width + 'px'; 
		var pos = this.findPosition(parentObj);
		ctrl.style.left = parentObj.style.left;
		ctrl.style.top  = parentObj.style.top;
		parentObj.parentNode.appendChild(ctrl);
	},
	
	removeControlPanel: function() {
		var p = document.getElementById('homeBoxCtrlPanel');
		if (p!=null && p!='undefined') {
			p.parentNode.removeChild(p);
		}
	},
	
	loadCtrlImages: function () {
		for (var i in this.ctrlImages) {
			var img = document.createElement("IMG");
			img.src=this.ctrlImgRoot + this.ctrlImages[i];
			img.border=0;
			img.id = 'ctrl_'+i;
			img.style.cursor = document.all ? 'hand' : 'pointer'; 
			this.ctrlImages[i] = img;
		}
	},
	
	boxDataFromCtrl: function (e) {
		if (!e) e = window.event;
		var boxId = e.target ? e.target.boxId : e.srcElement.boxId;
		var homebox = HomeBox.boxes[boxId];
		var box = document.getElementById(boxId);
		var col = document.getElementById(boxId).parentNode;
		var colNum = HomeBox.columns[col.id].id;
		return  {
					boxId: boxId,
					homebox: homebox,
					box: box,
					col: col,
					colNum: colNum
				};
	},
	
	moveBox: function (name, col, pos ){
		if (col!=null) {
			this.onBoxMove(name, col, pos);
		}
	},
	
	moveRight: function (e) {  
		var boxData = HomeBox.boxDataFromCtrl(e);
		var newCol = boxData.colNum >= HomeBox.columns.length ? null : boxData.colNum+1;
		HomeBox.moveBox(boxData.homebox.boxName, newCol, boxData.homebox.position);
	},
	
	moveLeft: function(e) {
		var boxData = HomeBox.boxDataFromCtrl(e);
		var newCol = boxData.colNum == 1 ? null : boxData.colNum-1;
		HomeBox.moveBox(boxData.homebox.boxName, newCol, boxData.homebox.position);
	},
	
	moveUp: function(e) {
		var boxData = HomeBox.boxDataFromCtrl(e); 
		var newPos = boxData.homebox.position-15;
		if (newPos <= 0) newPos = 5;
		HomeBox.moveBox(boxData.homebox.boxName, boxData.colNum, newPos);
	}, 
	
	moveDown: function(e) {
		var boxData = HomeBox.boxDataFromCtrl(e); 
		var newPos = boxData.homebox.position+15;
		HomeBox.moveBox(boxData.homebox.boxName, boxData.colNum, newPos);
	},
	
	// handles rpc calls. This is not really used here but you can use it in your 
	// event handles should you need it. 
	request: function (url, callBack) {
		if (window.ActiveXObject) { // The M$ 'standard'
			try {
				this.req = new ActiveXObject("Msxml2.XMLHTTP");
			} catch(e) {
				this.req = new ActiveXObject("Microsoft.XMLHTTP");
			}
			if (this.req) { 
				this.req.onreadystatechange =   callBack;
				this.req.open("GET", url, true);
				this.req.send();
			}
		} else if (window.XMLHttpRequest) { // native XMLHttpRequest
			this.req = new XMLHttpRequest();
			this.req.onreadystatechange =  callBack; 
			this.req.open("GET", url, true);
			this.req.send(null);
		} 
	}, 
	  
	bindEvent: function (evt, obj, act) {
		if (obj.addEventListener) {
			obj.addEventListener(evt, act, false);
		} else if (obj.attachEvent) {
			obj.attachEvent('on'+evt, act);
		}
	},
	
	unbindEvent: function (evt, obj, act) {
		if (obj.removeEventListener) {
			obj.removeEventListener(evt, act, false);
		} else if (obj.detachEvent) {
			obj.detachEvent('on'+evt, act);
		}
	},
	
	// Returns the dimensions of an element on screen. Lifted from the wonderful 
    // prototype framework
    getDimensions: function(obj) {
        //var display = obj.getStyle('display');
        //if (display != 'none' && display != null) // Safari bug
        //  return {width: element.offsetWidth, height: element.offsetHeight};

        // All *Width and *Height properties give 0 on elements with display none,
        // so enable the element temporarily
        var objStyle = obj.style;
        var originalVisibility = objStyle.visibility;
        var originalPosition = objStyle.position;
        var originalDisplay = objStyle.display;
        objStyle.visibility = 'hidden';
        objStyle.position = 'absolute';
        objStyle.display = 'block';
        var originalWidth = obj.clientWidth;
        var originalHeight = obj.clientHeight;
        objStyle.display = originalDisplay;
        objStyle.position = originalPosition;
        objStyle.visibility = originalVisibility;
        return {width: originalWidth, height: originalHeight};
    },
    
    removeBox: function (boxid) {
    	document.getElementById('homebox_' + boxid).style.display = 'none';
    	this.onBoxRemove(boxid);
    },
	
	onBoxRemove: function (boxid) {
		if (this.debugMode) alert("Default onBoxRemove called. Arguments: boxId="+boxid);
	},
		
	onBoxDrop: function (name, col, pos) {
		if (this.debugMode) alert("Default onBoxDrop called. Arguments: name="+name+", col="+col+", pos="+pos);
	},
	
	onBoxMove: function (name, col, pos) {
		if (this.debugMode) alert("Default onMoveBox.  Arguments: name="+name+", col="+col+", pos="+pos);
	},
	
 	debug: function (str) {
		if (this.debugMode) {
			var d = this.dbgWin.document.getElementById('txt');
			d.value = str+'\n'+d.value;
		}
	}
}