/* Copyright Bogdan Ionescu, 2003 - * http://funnyscripts.tripod.com/demo **/ /* Some possible FAQ: Q. What does it do? A. It provides a series of functions that help you give your pages a 'dynamic touch'. 1. It can figure out the absolute position of any desired element in your HTML page and it allows you to drag that element (text, image, etc) 2. It can do the same thing for a container. A container would be a HTML element that contains children elements. (div, document.body, etc) 3. It can restore the modified container to its initial state. 4. It can hold, in the cookie, information (the new positions for dragged elements) about the changes in your desired container. Q. What functions do I have to call? A. There are 2 sort of public functions. 1. MoveElement - for moving a single element. It receives the element as parameter. 2. MoveContainer - for moving all the elements in a container. It has 3 parameters and the first one is mandatory the other 2 are false by default First is the container, second is a boolean(true enables use of cookies), and third parameter is another boolean (true disables the dragging handler) Note: you might as well take advantage of the other stuff, but normally those 2 functions are enough. Q. How do I drag an element? A. Move the mouse over it, hold Ctrl key pressed and then move the mouse. The cursor should change to crosshair type when an element can be moved. Q. What browsers are supported? A. The code should work fine with IE 5.0+, Opera7 and Mozilla 1.2.1 Example 1: "MoveElement(document.getElementById('mydiv'));" will enable dragging of the specified element; Example 2: "MoveContainer(document.body)" will enable allow you to drag the elements that are found in the body of your document. Example 3: "MoveContainer(document.getElementById('cont'))" will do the same thing for the elements found in the container with the id 'cont' Example 4: "MoveContainer(document.body, true)" same as Example 2, but it enables the use of cookies. This means that the next time when you call this function (with cookies enabled), the initial positions of the dragged elements inside document.body will be those saved in cookies Example 5: "var mv = new MoveContainer(document.body); {...dostuff...} mv.restore()" This is how you restore the container to its initial state For other FAQs feel free to email me. If you have ideas, feel free to do the same thing. /bogdan */ var Document=document; var setDocument = function(Id){ while(Id.nodeName != '#document') Id=Id.parentNode; Document=Id; } function GetValues() { this.browserType; //0 IE, 1-netscape/mozilla 2-opera this.quirk_mode = true; this.getCompatMode = function(){ this.browserType = (navigator.userAgent.indexOf('Gecko') != -1) + 2*(navigator.userAgent.indexOf('Opera') != -1); //assuming IE if its not opera or netscape this.quirk_mode = (!Document.compatMode || Document.compatMode == "BackCompat" || Document.compatMode == "QuirksMode"); } this.getCompatMode(); /**************************************************** end of 'quirk stuff' *****************************************************/ } GetValues.prototype.Int = function(src){ var val = parseInt(src); if(isNaN(val)) return 0; else return val; } GetValues.prototype.Px = function(val){ return val + 'px'; } GetValues.prototype.getElementStyle = function(id){ if(!(id.currentStyle || (Document.defaultView && Document.defaultView.getComputedStyle))){ return id.style; //for opera7 } else{ return id.currentStyle ? id.currentStyle : Document.defaultView.getComputedStyle(id,''); //IE/Mozilla } } GetValues.prototype.getAbsolutePosition = function (id) { var x = y = 0; /* * Workaround for IE. Some very strange bug occurs when the first child of a node * has styleFloat/cssFloat set to 'right'. The offsetLeft of the Node somehow is * set to the offsetLeft of its child, therefore here comes the workaround */ // if(!Getter.browserType && id.childNodes.length) { var firstElement = id.childNodes.item(0); if(firstElement.currentStyle != null && firstElement.currentStyle.styleFloat == 'right'){ firstElement.style.styleFloat="none"; x += id.offsetLeft; y += id.offsetTop; firstElement.style.styleFloat="right"; id=id.offsetParent; } } // while(id){ x += id.offsetLeft; y += id.offsetTop; id=id.offsetParent; if( id ) { var st=Getter.getElementStyle(id); x+= this.Int(st.borderLeftWidth); y+= this.Int(st.borderTopWidth); } } return new Array(x,y) }; GetValues.prototype.getWidth = function (element) { var st = this.getElementStyle(element); var dw = this.Int(st.borderLeftWidth) + this.Int(st.borderRightWidth) + this.Int(st.paddingLeft) + this.Int(st.paddingRight); return element.offsetWidth - dw; } GetValues.prototype.getHeight = function (element) { var st = this.getElementStyle(element); var dh = this.Int(st.borderTopWidth) + this.Int(st.borderBottomWidth) + this.Int(st.paddingTop) + this.Int(st.paddingBottom); return element.offsetHeight - dh; } var Getter = new GetValues(); /**************************************************** the cookie manipulation *****************************************************/ /* * separates information inside the cookie using the document name * preventing wrong parsing of the cookie from multiple pages in the * same domain/path */ function clientCookie(init) { var writeCookie = function(name, val, expire) { Document.cookie = name + '=' + val + '; expires = ' + expire +';'; } var __getPath = function() { var loc = new String(Document.location).split(':'); return loc[loc.length-1]; } this.hours = 24; this.minutes = 60; this.days = 30; this.getTimeString = function(expire) { var tmp = new Date(); tmp.setTime(tmp.getTime() + this.days * this.hours * this.minutes * 60 * 1000 * (expire?expire:1)); return tmp.toGMTString(); } var length; var cookies = new Array(); this.path = __getPath(); this.write = function() { var val = new String(this.toString()); writeCookie(this.path, val.length + this.toString(), this.getTimeString()); } this.expire = function() { writeCookie(this.path, '', this.getTimeString(-1)); } this.read = function() { var cookie = Document.cookie.split(this.path+'=')[1]; if(!cookie) { return; } var length = parseInt(cookie); var objects = (cookie.substr(new String(length).length,length)).split(' ('); for(var i=1; i < objects.length;i++) { this.add(objects[i].split(') ')[0], objects[i].split(') ')[1]) } } this.add = function(name, value) { cookies[name] = value; } this.get = function(name) { return cookies[name]; } this.remove = function(name) { cookies.splice(name, 1); } this.toString = function() { var ret = new String(); for(var i in cookies) { ret += (' ('+ i + ') ' + cookies[i]); } return ret; } if(init) this.read(); } /** * base class for some cookie classes, defines common behaviour **/ function __cookieBase(){ var __separator = ','; this.setSeparator = function(separator){ if(separator) __separator = separator; } this.getSeparator = function(){ return __separator; } this.toString = function(){return("cookie base object");} } /* * each element has a __cookieEntry attached. It holds the position, the id * and the nodeName of the specified element * the toString function formats the information (in a cookie like format) * and __initFromString reads the cookieEntry members from a formatted string * The position is updated when the assigned element is moved */ function __cookieEntry(id, tag , x, y){ if(__cookieBase.call) __cookieBase.call(this); var is_empty = arguments.length != 4; this.toString = function (){ return this.id + this.getSeparator() + this.tag + this.getSeparator() + this.x + this.getSeparator() + this.y; } this.__initFromString = function (cookie){ var tmp = new Array; tmp = new String(cookie).split(this.getSeparator()); this.id = tmp[0]; this.tag = tmp[1]; this.x = tmp[2]; this.y = tmp[3]; delete tmp; is_empty = false; } this.init = function (id,tag,x,y){ this.id = id; this.tag = tag; this.x = x; this.y = y; is_empty = false; } this.isEmpty = function(){ return is_empty; } if(!is_empty) this.init(id,tag,x,y); } __cookieEntry.prototype = new __cookieBase(); /* * It holds cookieEntries. Each MovingContainer contains a cookieList * in order to have information about all elements that are moveable * A cookieList has functions for adding/removing entries, it also * formats the information held inside (so it can be written in the cookie) * A formatted string is something like ID:entry1:entry2:entry3 * where ID is the id of the container assigned and ':' is the separator */ function __cookieList(parentId/*Id of the container*/){ if(__cookieBase.call) __cookieBase.call(this); var cookies = new Array(); if(parentId) cookies[0] = parentId; this.getPosition = function(cookieEntry) { for(var i = 1; i < cookies.length && cookies[i] != cookieEntry; i++); return (i == cookies.length ? -1 : i); } this.add = function(cookieEntry, pos) { if(cookieEntry != null && this.getPosition(cookieEntry) == -1){ cookies.splice((pos != null ? pos: cookies.length), 0, cookieEntry); } } this.remove = function(cookieEntry) { var pos = this.getPosition(cookieEntry); if(pos == -1) return -1; cookies.splice(pos,1); } this.get = function(index) { return cookies[index]; } this.__getId = function() { return cookies[0]; } this.toString = function() { return cookies.join(this.getSeparator()); } this.__initFromString = function(str) { cookies = str.split(this.getSeparator()); for(i =1;i //for some reason Opera doesn't support cssText as the other 2 browsers do style.left = initialState[0]; style.top = initialState[1]; style.width = initialState[2]; style.height = initialState[3]; style.margin = initialState[4]; style.cursor = initialState[5]; style.position = initialState[6]; } } this.removeHandler = function(){ this.id.onmousemove = this.oldMouseMove; if(this.__cookie != null) { delete this.cookie; } Document.__MovingObjects--; } this.restore = function(){ this.id.__counter--; if(!this.id.__counter) { if(this.disableHandler == null) { this.removeHandler(); } restoreInitialState(self.id.style); this.id.__getWrapper = null; } } var translatePosition = function(id, __cookieRef){ var x_; var y_; var pcs = Getter.getElementStyle(id.parentNode); if( pcs.position != "absolute" && pcs.position != "relative" ) { var initialPos_ = Getter.getAbsolutePosition(id); x_ = initialPos_[0]; y_ = initialPos_[1]; } else { x_ = id.offsetLeft; y_ = id.offsetTop; if(Getter.browserType == 2){ //opera7 workaround :( x_ -= Getter.Int(pcs.borderLeftWidth); y_ -= Getter.Int(pcs.borderTopWidth); } } var xx = id.offsetWidth; var yy = id.offsetHeight; if(Getter.browserType == 1 || !Getter.quirk_mode){ xx = Getter.getWidth(id); yy = Getter.getHeight(id); } if(Getter.browserType == 2 && Getter.quirk_mode) { yy = Getter.getHeight(id); //another opera7 workaround } id.style.position = 'absolute'; if(__cookieRef != null){ if(__cookieRef.isEmpty()){ __cookieRef.init((id.id?id.id:""), id.nodeName, x_, y_); } self.__cookie = __cookieRef; bst.left = Getter.Px(self.__cookie.x); bst.top = Getter.Px(self.__cookie.y); } else{ bst.left = Getter.Px(x_); bst.top = Getter.Px(y_); } bst.width = Getter.Px(xx); bst.height = Getter.Px(yy); bst.margin = Getter.Px(0); } /******************************************************* * end misc functions *******************************************************/ /******************************************************* * mouse handler related stuff ********************************************************/ this.moveObject = function (ev){ if(ev == null) var ev = Document.parentWindow.event; if(!x) return; //to prevent some rare javascript error var t = this.id.offsetTop + ev.clientY - y; var l = this.id.offsetLeft + ev.clientX - x bst.top = Getter.Px(t); bst.left = Getter.Px(l); if(this.__cookie){ this.__cookie.x = l; this.__cookie.y = t; } } this.updateCursor = function(ev){ var __pressed = rightCombination(ev); if( __pressed ){ __setMouseDragsElementStyle(bst); this.moveObject(ev); } else { __setMouseOverElementStyle(bst); } x=ev.clientX; y=ev.clientY; return __pressed; } this.initHandler = function(useCookie){ if(Document.__currentMovingObject == null){ Document.__currentMovingObject = ""; Document.__MovingObjects = 1; var __oldMouseMove = Document.onmousemove; //backup Document.onmousemove = function (ev){ if(ev == null) var ev = Document.parentWindow.event; if(this.__currentMovingObject != "" ){ if(!this.__currentMovingObject.updateCursor(ev)){ this.__currentMovingObject = ""; //resets the 'moving' Object when the right combination does not occur if(useCookie) DC.write(); } } if(__oldMouseMove != null) __oldMouseMove();//calling the default onmousemove } } else Document.__MovingObjects++; this.oldMouseMove = this.id.onmousemove;//backup this.id.onmousemove = function(ev){ if(Document.__currentMovingObject == "") Document.__currentMovingObject = self; if(this.oldMouseMove) this.oldMouseMove(ev); } } /******************************************************* * end mouse handler related stuff ********************************************************/ translatePosition(id, __cookieRef); if(disableHandler == null) this.initHandler(__cookieRef != null); } /* * Moves the elements inside the specified container * If useCookie is true it will read elements' positions * from the cookie (if they were previously saved) * If disableHandler is anything but null, it will disable * the movement of the elements, and it will just set their * absolute positions (usually used for automatic movement) * The function parses the children of the specified container * trying to match them with the information read from the cookie * If a child is of type #text, it will create a SPAN which * will become the child. * In the end it will hold a list of the MovingObjects, one for * each child of the container (divs) * */ function MoveContainer(container, useCookie, disableHandler){ if(!container) return; var self = this; if(container.__getWrapperContainer) { return container.__getWrapperContainer(); } else container.__getWrapperContainer = function(){ return self; } var Container = container; var id_ = container.id ? container.id : 'BODY'; //setDocument(container); var __okToCook = function () { return (Container.id || Container == Document.body); } if(useCookie && __okToCook()){ var cookieList = DC.get(id_); if(cookieList == null){ var nullList = true; cookieList = new __cookieList(id_); DC.add(cookieList); } } if(disableHandler != null) var disableHandler=true; var divs = new Array(); var taken = new Array(); var __index = 0; /* * Workaround for mozilla which creates an 'empty' textNode for each element. * Since there is no point in dragging them, they are ignored */ var isNullString = function(text) { for(var i=0; i= 0){ if(divs[pos].__cookie) cookieList.remove(divs[pos].__cookie); divs[pos].restore(); divs.splice(pos,1); //remove the item } return -1; } /* * this function is used internally. It attempts to match the childElement with * some information present in cookie (used when the page was modified by the designer) */ this.getCookie = function(__childElement, pos){ var depth = nullList?0:5; var ppos = pos; var j=1; for(var i = 0; i < depth;){ ppos = pos + j*i; if(!taken[ppos]){ ce = cookieList.get(ppos); if(ce && (ce.id == __childElement.id && ce.tag == __childElement.nodeName)){break;} } j=-j; i+=(j==1); } taken[ppos] = true; if(i==depth){ ce = new __cookieEntry(); cookieList.add(ce, pos); } return ce; } /* * This function is used at initialization and it can also be used after the initial list * was made. The 'replace' parameter specifies that the function is used internally * If the '__childElement' is a valid element one wrapper (MovingObject) will be created * for it. The function will return unexpectedly if the element does not exist, if it * cannot be moved or if its an empty text generated by Mozilla/Netscape */ this.add = function(__childElement, replace){ if(replace == null && this.getElementPos(__childElement) != -1) return -1; var __childName = __childElement.nodeName; if(__childName == "#text") { //putting the texts in spans, so they can be dragged around var di = Document.createElement("span"); if(isNullString(__childElement)) return; Container.insertBefore(di, __childElement); di.appendChild(__childElement); var __childElement = di; } else if(!elementIsMovable(__childName)) { return; } i = divs.length; /* * Calls MoveElement passing the current element, the cookieEntry (if we are using cookies) and disableHandler */ divs[i] = new MoveElement(__childElement, ( cookieList ? this.getCookie(__childElement, i+1) : null ), disableHandler); } var oW = container.style.width; var oH = container.style.height;//saving the original values for restoring container.style.width = Getter.Px(Getter.getWidth(container)); container.style.height = Getter.Px(Getter.getHeight(container)); //preventing shrinking caused by absolute positioning for( __index = 0; __index < container.childNodes.length; __index++){ this.add(container.childNodes.item(container.childNodes.length - __index - 1),1); } if(useCookie){DC.write();} /* * Restores the container to its original state. */ this.restore = function() { if(cookieList) { //DC.remove(cookieList); delete cookieList; } while(divs.length) divs.pop().restore(); delete divs; Container.__getWrapperContainer = null; Container.style.width = oW; Container.style.height = oH; } this.getElements = function() { return divs; } return this; } /*******************************************/ /* Adding some functions missing in IE5 */ /*******************************************/ if(Array.join == null) Array.prototype.join= function ( separator ){ if(separator == null) var separator = ','; var ret = new String; for(var i = 0;i < this.length;i++){ ret+= this[i]?this[i]:""; if(i < this.length -1) ret+= separator; } return ret; } if(Array.push == null) Array.prototype.push = function (){ for(var i = 0;i < arguments.length;i++)this[this.length] = arguments[i]; return this.length; } if(Array.pop == null) Array.prototype.pop = function (){ var ret=this[this.length -1]; this.length--; return ret; } if(Array.splice == null) Array.prototype.splice = function (){ var addedElements = arguments.length - 2; var removedElements = arguments[1]; var diff = addedElements - removedElements; var len = this.length + diff; var position = arguments[0]; var p1 = position + removedElements; if(addedElements > removedElements) for(var i = this.length-1;i >= p1;) this[i+diff] = this[i--]; else if(addedElements < removedElements) for(var i = position + addedElements;i < len ;) this[i] = this[++i - diff]; for(var i = 0; i < addedElements; i++) this[position + i] = arguments[i + 2]; this.length = len; return this.length; };