[richfaces-svn-commits] JBoss Rich Faces SVN: r550 - in trunk/sandbox/scrollable-grid: src and 36 other directories.

richfaces-svn-commits at lists.jboss.org richfaces-svn-commits at lists.jboss.org
Wed Apr 25 10:14:52 EDT 2007


Author: abelevich
Date: 2007-04-25 10:14:52 -0400 (Wed, 25 Apr 2007)
New Revision: 550

Added:
   trunk/sandbox/scrollable-grid/generatescript.xml
   trunk/sandbox/scrollable-grid/pom.xml
   trunk/sandbox/scrollable-grid/src/
   trunk/sandbox/scrollable-grid/src/main/
   trunk/sandbox/scrollable-grid/src/main/config/
   trunk/sandbox/scrollable-grid/src/main/config/component/
   trunk/sandbox/scrollable-grid/src/main/config/component/scrollable-grid.xml
   trunk/sandbox/scrollable-grid/src/main/java/
   trunk/sandbox/scrollable-grid/src/main/java/META-INF/
   trunk/sandbox/scrollable-grid/src/main/java/META-INF/MANIFEST.MF
   trunk/sandbox/scrollable-grid/src/main/java/org/
   trunk/sandbox/scrollable-grid/src/main/java/org/richfaces/
   trunk/sandbox/scrollable-grid/src/main/java/org/richfaces/component/
   trunk/sandbox/scrollable-grid/src/main/java/org/richfaces/component/UIScrollableGrid.java
   trunk/sandbox/scrollable-grid/src/main/java/org/richfaces/renderkit/
   trunk/sandbox/scrollable-grid/src/main/java/org/richfaces/renderkit/ScrollableGridBaseRenderer.java
   trunk/sandbox/scrollable-grid/src/main/javascript/
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/box/
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/box/Box.js
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/box/InlineBox.js
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/box/ScrollableBox.js
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/box/Substrate.js
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/utils/
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/utils/CustomEvent.js
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/utils/StringBuilder.js
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/utils/Validators.js
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/ArrayDataModel.js
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/CellsStrip.js
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/DataModel.js
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/DefaultColumnModel.js
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/Grid.js
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/GridBody.js
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/GridFooter.js
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/GridHeader.js
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/layouts/
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/layouts/GridLayoutManager.js
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/layouts/LayoutManager.js
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/layouts/VLayoutManager.js
   trunk/sandbox/scrollable-grid/src/main/javascript/ClientUILib.js
   trunk/sandbox/scrollable-grid/src/main/javascript/common/
   trunk/sandbox/scrollable-grid/src/main/javascript/common/prototype/
   trunk/sandbox/scrollable-grid/src/main/javascript/common/prototype/ext/
   trunk/sandbox/scrollable-grid/src/main/javascript/common/prototype/ext/extend.js
   trunk/sandbox/scrollable-grid/src/main/javascript/common/prototype/prototype.js
   trunk/sandbox/scrollable-grid/src/main/javascript/common/scriptaculous/
   trunk/sandbox/scrollable-grid/src/main/resources/
   trunk/sandbox/scrollable-grid/src/main/resources/org/
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/css/
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/css/grid.xcss
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/arrow-left-white.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/arrow-right-white.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/done.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/drop-no.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/drop-yes.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/footer-bg.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/grid-blue-hd.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/grid-blue-split.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/grid-loading.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/grid-split-h.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/grid-split.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/grid-vista-hd.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/invalid_line.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/loading.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/mso-hd.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/nowait.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-first-disabled.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-first.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-last-disabled.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-last.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-next-disabled.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-next.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-prev-disabled.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-prev.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/pick-button.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/refresh.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/sort_asc.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/sort_desc.gif
   trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/wait.gif
   trunk/sandbox/scrollable-grid/src/main/templates/
   trunk/sandbox/scrollable-grid/src/main/templates/README
   trunk/sandbox/scrollable-grid/src/main/templates/org/
   trunk/sandbox/scrollable-grid/src/main/templates/org/richfaces/
   trunk/sandbox/scrollable-grid/src/main/templates/org/richfaces/scrollable-grid.jspx
   trunk/sandbox/scrollable-grid/src/test/
   trunk/sandbox/scrollable-grid/src/test/java/
   trunk/sandbox/scrollable-grid/src/test/java/org/
   trunk/sandbox/scrollable-grid/src/test/java/org/richfaces/
   trunk/sandbox/scrollable-grid/src/test/java/org/richfaces/component/
   trunk/sandbox/scrollable-grid/src/test/java/org/richfaces/component/JSFComponentTest.java
   trunk/sandbox/scrollable-grid/target/
   trunk/sandbox/scrollable-grid/target/test-classes/
Log:


Added: trunk/sandbox/scrollable-grid/generatescript.xml
===================================================================
--- trunk/sandbox/scrollable-grid/generatescript.xml	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/generatescript.xml	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<!-- ====================================================================== 
+     12.11.2005 23:40:43                                                        
+
+     assemblescripts    
+     Assemble all javaScript library files to one data-grid.js
+                   
+     maksim         
+                                                     
+     ====================================================================== -->
+
+<project name="assemblescripts" default="merge-scripts">
+	
+	<description>
+            Assemble all javaScript library files to one scrollable-grid.js 
+    </description>
+
+		<target name="merge-scripts">
+			<property name="script-path" value="org/richfaces/renderkit/html/scripts"></property>
+			<property name="gen-script-name" value="scrollable-grid.js"></property>	
+			<property name="gen-script-full-name" value="${target-dir}/${script-path}/${gen-script-name}"></property>
+			<concat append="false" binary="false"  destfile="${gen-script-full-name}">
+				<filelist dir="${resources-dir}">
+					<file name="/common/prototype/ext/extend.js"/>
+					<file name="ClientUILib.js"/>
+					<file name="/ClientUI/common/utils/StringBuilder.js"/>
+					<file name="/ClientUI/common/utils/Validators.js"/>
+					<file name="/ClientUI/common/box/Box.js"/>
+					<file name="/ClientUI/common/box/InlineBox.js"/>
+					<file name="/ClientUI/common/utils/CustomEvent.js"/>
+					<file name="/ClientUI/common/box/ScrollableBox.js"/>
+					<file name="/ClientUI/controls/grid/DataModel.js"/>
+					<file name="/ClientUI/controls/grid/ArrayDataModel.js"/>
+					<file name="/ClientUI/layouts/LayoutManager.js"/>
+					<file name="/ClientUI/common/box/Substrate.js"/>
+					<file name="/ClientUI/layouts/VLayoutManager.js"/>
+					<file name="/ClientUI/layouts/GridLayoutManager.js"/>
+					<file name="/ClientUI/controls/grid/GridHeader.js"/>
+					<file name="/ClientUI/controls/grid/GridBody.js"/>
+					<file name="/ClientUI/controls/grid/GridFooter.js"/>
+					<file name="/ClientUI/controls/grid/CellsStrip.js"/>
+				</filelist>
+			</concat>
+		</target>
+</project>
+

Added: trunk/sandbox/scrollable-grid/pom.xml
===================================================================
--- trunk/sandbox/scrollable-grid/pom.xml	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/pom.xml	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,133 @@
+<?xml version="1.0"?><project>
+  <parent>
+    <artifactId>richfaces-parent</artifactId>
+    <groupId>org.richfaces</groupId>
+    <version>3.0.1-SNAPSHOT</version>
+  </parent>
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.richfaces</groupId>
+  <artifactId>scrollable-grid</artifactId>
+  <version>1.0.0-SNAPSHOT</version>
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.ajax4jsf.cdk</groupId>
+        <artifactId>maven-cdk-plugin</artifactId>
+        <version>1.1.1-SNAPSHOT</version>
+        <executions>
+          <execution>
+            <phase>generate-sources</phase>
+            <goals>
+              <goal>generate</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <library>
+            <prefix>org.richfaces</prefix>
+            <taglib>
+              <shortName>scrollable-grid</shortName>
+            </taglib>
+          </library>
+        </configuration>
+      </plugin>
+	<plugin>
+				<artifactId>maven-antrun-plugin</artifactId>
+				<executions>
+					<execution>
+						<phase>generate-resources</phase>
+						<goals>
+							<goal>run</goal>
+						</goals>
+						<configuration>
+							<tasks>
+								<ant antfile="${basedir}/generatescript.xml" inheritRefs="true">
+									<target name="merge-scripts" />
+									<property name="target-dir"
+										value="${project.build.directory}/generated-component/resources">
+									</property>
+									<property name="resources-dir"
+										value="${basedir}/src/main/javascript">
+									</property>
+								</ant>
+							</tasks>
+					<resourceRoot>
+						${project.build.directory}/generated-component/resources
+					</resourceRoot>
+				</configuration>
+			</execution>
+		</executions>
+	</plugin>
+  </plugins>
+  </build>
+  <repositories>
+  <!--
+  <repository>
+      <releases />
+      <snapshots>
+        <enabled>false</enabled>
+        <updatePolicy>never</updatePolicy>
+      </snapshots>
+      <id>maven2-repository.dev.java.net</id>
+      <name>Java.net Repository for Maven</name>
+      <url>https://maven-repository.dev.java.net/nonav/repository</url>
+    </repository> -->
+    <repository>
+      <releases>
+        <enabled>false</enabled>
+      </releases>
+      <snapshots>
+		<updatePolicy>never</updatePolicy>
+	</snapshots>
+      <id>maven2-snapshots.ajax4jsf.org</id>
+      <name>Ajax4jsf Repository for Maven Snapshots</name>
+      <url>https://ajax4jsf.dev.java.net/nonav/snapshots</url>
+    </repository>
+  </repositories>
+  <pluginRepositories>
+    <pluginRepository>
+      <releases>
+        <enabled>false</enabled>
+      </releases>
+      <snapshots>
+		<updatePolicy>never</updatePolicy>
+      </snapshots>
+      <id>maven2-snapshots.ajax4jsf.org</id>
+      <name>Ajax4jsf Repository for Maven Snapshots</name>
+      <url>https://ajax4jsf.dev.java.net/nonav/snapshots</url>
+    </pluginRepository>
+  </pluginRepositories>
+  <dependencies>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>3.8.1</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.sun.facelets</groupId>
+      <artifactId>jsf-facelets</artifactId>
+      <version>1.1.6</version>
+    </dependency>
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>servlet-api</artifactId>
+      <version>2.4</version>
+    </dependency>
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>jsp-api</artifactId>
+      <version>2.0</version>
+    </dependency>
+    <dependency>
+      <groupId>javax.faces</groupId>
+      <artifactId>jsf-api</artifactId>
+      <version>1.1_02</version>
+    </dependency>
+    <dependency>
+      <groupId>org.ajax4jsf</groupId>
+      <artifactId>ajax4jsf</artifactId>
+      <version>1.1.1-SNAPSHOT</version>
+    </dependency>
+  </dependencies>
+</project>
\ No newline at end of file

Added: trunk/sandbox/scrollable-grid/src/main/config/component/scrollable-grid.xml
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/config/component/scrollable-grid.xml	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/config/component/scrollable-grid.xml	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE components PUBLIC "-//AJAX4JSF//CDK Generator config/EN"  "https://ajax4jsf.dev.java.net/nonav/dtds/component-config.dtd" >
+<components>
+	<component>
+		<name>org.richfaces.component.ScrollableGrid</name>
+		<family>org.richfaces.component.ScrollableGrid</family>
+	    <classname>
+            org.richfaces.component.html.HtmlScrollableGrid
+        </classname>
+        <superclass>org.richfaces.component.UIScrollableGrid</superclass>
+   		<description>
+   			<![CDATA[ Scrollable Grid  ]]>
+   		</description>
+
+		<renderer generate="true" override="true">
+			<name>org.richfaces.ScrollableGridRenderer</name>
+			<template>org/richfaces/scrollable-grid.jspx</template>
+		</renderer>
+
+
+		<tag>
+			<name>scrollable-grid</name>
+			<classname>org.richfaces.taglib.ScrollableGridTag</classname>
+			<superclass>
+				org.ajax4jsf.framework.taglib.HtmlComponentTagBase
+			</superclass>
+		</tag>
+		&ui_component_attributes;
+	</component>
+</components>		
\ No newline at end of file

Added: trunk/sandbox/scrollable-grid/src/main/java/META-INF/MANIFEST.MF
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/java/META-INF/MANIFEST.MF	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/java/META-INF/MANIFEST.MF	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,3 @@
+Manifest-Version: 1.0
+Class-Path: 
+

Added: trunk/sandbox/scrollable-grid/src/main/java/org/richfaces/component/UIScrollableGrid.java
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/java/org/richfaces/component/UIScrollableGrid.java	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/java/org/richfaces/component/UIScrollableGrid.java	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,23 @@
+/**
+ * 
+ */
+package org.richfaces.component;
+
+import javax.faces.component.UIComponentBase;
+
+/**
+ * @author Anton Belevich
+ *
+ */
+public class UIScrollableGrid extends UIComponentBase {
+
+	/* (non-Javadoc)
+	 * @see javax.faces.component.UIComponent#getFamily()
+	 */
+	
+	public String getFamily() {
+		// TODO Auto-generated method stub
+		return null;
+	}
+
+}

Added: trunk/sandbox/scrollable-grid/src/main/java/org/richfaces/renderkit/ScrollableGridBaseRenderer.java
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/java/org/richfaces/renderkit/ScrollableGridBaseRenderer.java	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/java/org/richfaces/renderkit/ScrollableGridBaseRenderer.java	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,6 @@
+package org.richfaces.renderkit;
+
+import org.ajax4jsf.framework.renderer.AjaxComponentRendererBase;
+
+public abstract class ScrollableGridBaseRenderer extends AjaxComponentRendererBase{
+}

Added: trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/box/Box.js
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/box/Box.js	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/box/Box.js	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,251 @@
+/**
+ * Box.js		Date created: 6.04.2007
+ * Copyright (c) 2007 Exadel Inc.
+ * @author Denis Morozov <dmorozov at exadel.com>
+ */
+ClientUILib.declarePackage("ClientUI.common.box.Box");
+
+
+/*
+ * Base class for all ui controls 
+ *
+ * TODO: description of control 
+ *
+ * TODO: usage description
+ * Usage: 
+ *  ClientUILib.declarePackage("ClientUI.common");
+ *  ClientUILib.requireClass("ClientUI.common.box.Box");
+ * 	var ClientUI.MyControl = Class.create({
+ * 		CLASSDEF : {
+ * 			name:  'ClientUI.MyControl',
+ * 			parent: ClientUI.common.box.Box
+ * 		}, 		
+ *		initialize:function() {
+ *			this.parentClass().constructor().call(this)
+ *			alert("A new " + this.getClass().className + " was created")
+ *		}				
+ *	})
+ */
+ClientUI.common.box.Box = Class.create({
+	CLASSDEF: {
+		name: 'ClientUI.common.box.Box'
+	}	
+});
+
+Object.extend(ClientUI.common.box.Box.prototype, {
+
+	initialize: function(element, parentElement, dontUpdateStyles) {
+		this.element = $(element);
+		if(!this.element) {
+			this.element = $(document.createElement("div"));
+			if($(parentElement)) {
+      			$(parentElement).appendChild(this.element);
+			}
+      		else {
+	      		document.body.appendChild(this.element);
+      		}
+      	}
+		this.element.wrapper = this;
+		if(!this.element.parentNode && $(parentElement)) {
+			$(parentElement).appendChild(this.element);
+		}
+
+      	if(!this.element.id) {
+			this.element.id = "ClientUI_Box" + ClientUI_common_box_Box_idGenerator++;
+		}
+		if(!dontUpdateStyles) {
+	      	this.element.setStyle({overflow: 'hidden'});
+	      	this.element.setStyle({whiteSpace: 'nowrap'});
+	      	
+			// if the element isn't positioned, make it relative
+		    var position = this.element.getStyle('position');
+		    if(position != 'absolute' && position != 'fixed'){
+		        this.element.setStyle({position: 'relative'});
+		    }			
+		}
+	},
+	
+	setParent: function(newParent) {
+		if(this.element.parentNode) {
+			this.element.parentNode.removeChild(this.element);
+		}
+		if(newParent) {
+			if(newParent.getElement) {
+				newParent = newParent.getElement();
+			}
+			$(newParent).appendChild(this.element);			
+		}
+		return this;
+	},
+	getElement: function() {
+		return this.element;
+	},
+	getHeight: function() {
+		if(this.getElement().tagName.toLowerCase() != "body")
+			return Element.getHeight(this.element);
+
+		if (self.innerHeight) { // all except Explorer
+			return self.innerHeight;
+		}
+		else if (document.documentElement && document.documentElement.clientHeight) {
+			// Explorer 6 Strict Mode
+			return document.documentElement.clientHeight;
+		}
+		else if (document.body) { // other Explorers
+			return document.body.clientHeight;
+		}
+	},
+	isModified: false,
+	setHeight: function(newHeight) {
+		if(Validators.IsNumber(newHeight)) {
+			if(newHeight<0) newHeight = 0;
+			newHeight += "px";	
+		}
+		this.element.setStyle({height: newHeight});
+		isModified = true;
+		return this;
+	},
+	getWidth: function() {
+		if(this.getElement().tagName.toLowerCase() != "body")
+			return Element.getWidth(this.element);
+			
+		if (self.innerHeight) {// all except Explorer
+			return self.innerWidth;
+		}
+		else if (document.documentElement && document.documentElement.clientHeight) {
+			// Explorer 6 Strict Mode
+			return document.documentElement.clientWidth;
+		}
+		else if (document.body) { // other Explorers
+			return document.body.clientWidth;
+		}			
+	},
+	setWidth: function(newWidth) {
+		if(Validators.IsNumber(newWidth)) {
+			if(newWidth<0) newWidth = 0;
+			newWidth += "px";	
+		}
+		this.element.setStyle({width: newWidth});
+		isModified = true;
+		return this;
+	},
+	moveToX: function(x) {
+		if(Validators.IsNumber(x)) {x += "px";}
+		this.getElement().setStyle({left: x});
+		isModified = true;
+		return this;		
+	},
+	moveToY: function(y) {
+		if(Validators.IsNumber(y)) {y += "px";}
+		this.getElement().setStyle({top: y});
+		isModified = true;
+		return this;
+	},
+	moveTo: function(x, y) {
+		this.moveToX(x);
+		this.moveToY(y);
+		return this;
+	},
+	hide: function() {
+		Element.hide(this.element);
+		isModified = true;
+		return this;
+	},
+	show: function() {
+		Element.show(this.element);
+		isModified = true;
+		return this;
+	},
+	updateLayout: function() {
+		isModified = false;
+		return this;
+	},
+	getViewportWidth: function() {
+		if(this.getElement().tagName.toLowerCase() != "body") {
+			var width = 0;
+			if( this.getElement().clientWidth ) {
+			    width = this.getElement().clientWidth;
+			}
+			else if( this.getElement().innerWidth ) {
+			    width = this.getElement().innerWidth - getScrollerWidth();
+			}
+			  
+			if(ClientUILib.isGecko) {
+			  	width -= this.getPadding("lr");
+			}
+			return width;
+		}
+		
+		return this.getWidth();
+	},
+	getViewportHeight: function() {
+		if(this.getElement().tagName.toLowerCase() != "body") {
+			var height = 0;
+			if( this.getElement().clientHeight ) {
+			    height = this.getElement().clientHeight;
+			}
+			else if( this.getElement().innerHeight ) {
+			    height = this.getElement().innerHeight - getScrollerWidth();
+			}
+			  
+			if(ClientUILib.isGecko) {
+			  	height -= this.getPadding("tb");
+			}
+			return height;
+		}
+		return this.getHeight();
+	},	
+	/**
+     * Gets the width of the border(s) for the specified side(s)
+     * @param {String} side Can be t, l, r, b or any combination of those to add multiple values. For example, 
+     * passing lr would get the border (l)eft width + the border (r)ight width.
+     * @return {Number} The width of the sides passed added together
+     */
+    getBorderWidth : function(side){
+        return this.getStyles(side, this.borders);
+    },
+    
+    /**
+     * Gets the width of the padding(s) for the specified side(s)
+     * @param {String} side Can be t, l, r, b or any combination of those to add multiple values. For example, 
+     * passing lr would get the padding (l)eft + the padding (r)ight.
+     * @return {Number} The padding of the sides passed added together
+     */
+    getPadding : function(side){
+        return this.getStyles(side, this.paddings);
+    },	
+	getStyles : function(sides, styles){
+        var val = 0;
+        for(var i = 0, len = sides.length; i < len; i++){
+             var w = parseInt(this.getElement().getStyle(styles[sides.charAt(i)]), 10);
+             if(!isNaN(w)) val += w;
+        }
+        return val;
+    },
+	makeAbsolute: function(keepPos) {
+		if(keepPos) {
+			Position.absolutize(this.getElement());	
+		}
+		else {
+			this.getElement().setStyle({position: 'absolute'});
+		}
+		return this;
+	},
+	getX: function() {
+		return this.getElement().offsetLeft;
+	},
+	getY: function() {
+		return this.getElement().offsetTop;
+	},
+	setStyle: function(style) {
+		this.getElement().setStyle(style);
+		return this;
+	},
+	
+	borders: {l: 'border-left-width', r: 'border-right-width', t: 'border-top-width', b: 'border-bottom-width'},
+	paddings: {l: 'padding-left', r: 'padding-right', t: 'padding-top', b: 'padding-bottom'},
+	margins: {l: 'margin-left', r: 'margin-right', t: 'margin-top', b: 'margin-bottom'}
+	
+});
+
+var ClientUI_common_box_Box_idGenerator = 0;

Added: trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/box/InlineBox.js
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/box/InlineBox.js	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/box/InlineBox.js	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,45 @@
+/**
+ * InlineBox.js		Date created: 6.04.2007
+ * Copyright (c) 2007 Exadel Inc.
+ * @author Denis Morozov <dmorozov at exadel.com>
+ */
+ClientUILib.declarePackage("ClientUI.common.box.InlineBox");
+
+ClientUILib.requireClass("ClientUI.common.box.Box");
+
+/**
+ * Base class that wrap work with inline blocks like span
+ */
+ClientUI.common.box.InlineBox = Class.create({
+	CLASSDEF: {
+		name: 'ClientUI.common.box.InlineBox',
+		parent: ClientUI.common.box.Box
+	}
+});
+
+Object.extend(ClientUI.common.box.InlineBox.prototype, {
+
+	initialize: function(element, parentElement, dontUpdateStyles) {
+		if(!element) {
+			element = $(document.createElement("span"));
+			if($(parentElement)) {
+      			$(parentElement).appendChild(element);
+			}
+      		else {
+	      		document.body.appendChild(element);
+      		}
+      	}		
+      	if(!element.id) {
+			element.id = "ClientUI_InlineBox" + ClientUI_common_box_InlineBox_idGenerator++;
+		}
+		
+		ClientUI.common.box.InlineBox.parentClass.constructor().call(this, element, parentElement, dontUpdateStyles);
+		
+		// additional styles
+		if(!dontUpdateStyles) {
+			this.element.setStyle({display: 'block'});	
+		}
+	}
+});
+
+var ClientUI_common_box_InlineBox_idGenerator = 0;
\ No newline at end of file

Added: trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/box/ScrollableBox.js
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/box/ScrollableBox.js	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/box/ScrollableBox.js	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,139 @@
+/**
+ * ScrollableBox.js		Date created: 6.04.2007
+ * Copyright (c) 2007 Exadel Inc.
+ * @author Denis Morozov <dmorozov at exadel.com>
+ */
+ClientUILib.declarePackage("ClientUI.common.box.ScrollableBox");
+
+ClientUILib.requireClass("ClientUI.common.box.Box");
+ClientUILib.requireClass("ClientUI.common.utils.CustomEvent");
+
+/**
+ * This class target to manage scrollable box object.
+ */
+ClientUI.common.box.ScrollableBox = Class.create({
+	CLASSDEF: {
+		name: 'ClientUI.common.box.ScrollableBox',
+		parent: ClientUI.common.box.Box 
+	}
+	
+});
+
+Object.extend(ClientUI.common.box.ScrollableBox.prototype, {
+
+	// Custom events
+	/**
+	 * Occured when content scrolled in horizontal
+	 */
+	eventHScroll: {},
+	/**
+	 * Occured when content scrolled in vertical
+	 */
+	eventVScroll: {},
+	
+	//Constructor
+	initialize: function(element, parentElement) {
+		ClientUI.common.box.ScrollableBox.parentClass.constructor().call(this, element, parentElement);
+		this.element.setStyle({overflow: 'auto'});
+		
+		// Create custom event producers
+		this.eventHScroll = new ClientUI.common.utils.CustomEvent('OnHScroll');
+		this.eventVScroll = new ClientUI.common.utils.CustomEvent('OnVScroll');
+
+		this.eventOnScroll = this.scrollContent.bindAsEventListener(this);
+		Event.observe(this.element, 'scroll', this.eventOnScroll);
+	},
+	scrollContent: function(event) {
+		this.updateScrollPos();
+	},
+	updateScrollPos: function() {
+		this.timer = null;
+		
+		// process horizontal scrolling
+		if(this.scrollLeft!==this.getViewportScrollX()) {
+			this.scrollLeft = this.getViewportScrollX();
+			this.eventHScroll.fire(this.getViewportScrollX());
+		}
+		
+		// process vertical scrolling		
+		if(this.scrollTop!==this.getViewportScrollY()) {
+			this.scrollTop = this.getViewportScrollY();
+			this.eventVScroll.fire(this.getViewportScrollY());
+		}
+	},
+	updateLayout: function() {
+		// NOTE: not implemented in this class
+		ClientUI.common.box.ScrollableBox.parentClass.method("updateLayout").call(this);
+		ClientUILib.log(ClientUILogger.INFO, "ScrollableBox::updateLayout");
+	},
+	getViewportScrollX: function() {
+		var scrollX = 0;
+		if( this.getElement().scrollLeft ) {
+			scrollX = this.getElement().scrollLeft;
+		}
+		else if( this.getElement().pageXOffset ) {
+			scrollX = this.getElement().pageXOffset;
+		}
+		else if( this.getElement().scrollX ) {
+			scrollX = this.getElement().scrollX;
+		}
+		return scrollX;
+	},
+	getViewportScrollY: function() {
+		var scrollY = 0;
+		if( this.getElement().scrollTop ) {
+			scrollY = this.getElement().scrollTop;
+		}
+		else if( this.getElement().pageYOffset ) {
+			scrollY = this.getElement().pageYOffset;
+		}
+		else if( this.getElement().scrollY ) {
+			scrollY = this.getElement().scrollY;
+		}
+		return scrollY;
+	},
+	getScrollerWidth: function() {
+		if(this.scrollerWidth && this.scrollerWidth > 0)
+			return this.scrollerWidth;
+			
+	    var scr = null;
+	    var inn = null;
+	    var wNoScroll = 0;
+	    var wScroll = 0;
+	
+	    // Outer scrolling div
+	    scr = document.createElement('div');
+	    scr.style.position = 'absolute';
+	    scr.style.top = '-1000px';
+	    scr.style.left = '-1000px';
+	    scr.style.width = '100px';
+	    scr.style.height = '50px';
+	    // Start with no scrollbar
+	    scr.style.overflow = 'hidden';
+	
+	    // Inner content div
+	    inn = document.createElement('div');
+	    inn.style.width = '100%';
+	    inn.style.height = '200px';
+	
+	    // Put the inner div in the scrolling div
+	    scr.appendChild(inn);
+	    // Append the scrolling div to the doc
+	    document.body.appendChild(scr);
+	
+	    // Width of the inner div sans scrollbar
+	    wNoScroll = inn.offsetWidth;
+	    // Add the scrollbar
+	    scr.style.overflow = 'auto';
+	    // Width of the inner div width scrollbar
+	    wScroll = inn.offsetWidth;
+	
+	    // Remove the scrolling div from the doc
+	    document.body.removeChild(
+	        document.body.lastChild);
+	
+	    // Pixel width of the scroller
+	    this.scrollerWidth = (wNoScroll - wScroll);
+	    return this.scrollerWidth || 0;
+	}	
+})
\ No newline at end of file

Added: trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/box/Substrate.js
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/box/Substrate.js	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/box/Substrate.js	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,39 @@
+/**
+ * Substrate.js		Date created: 21.04.2007
+ * Copyright (c) 2007 Exadel Inc.
+ * @author Denis Morozov <dmorozov at exadel.com>
+ */
+ClientUILib.declarePackage("ClientUI.common.box.Substrate");
+
+ClientUILib.requireClass("ClientUI.common.box.Box");
+
+/**
+ * Base class that wrap work with inline blocks like span
+ */
+ClientUI.common.box.Substrate = Class.create({
+	CLASSDEF: {
+		name: 'ClientUI.common.box.Substrate',
+		parent: ClientUI.common.box.Box
+	}
+	
+});
+
+Object.extend(ClientUI.common.box.Substrate.prototype, {
+
+	initialize: function(element, parentElement, dontUpdateStyles) {
+		if(!element) {			
+			var fakeElement = $(document.createElement("div"));
+			fakeElement.innerHTML = '<iframe id="'+'ClientUI_Substrate' + (ClientUI_common_box_Substrate_idGenerator++) +'" src="" scrolling="no" frameborder="0" style="filter:Alpha(opacity=0);position:absolute;top:0px;left:0px;display:block"></iframe>';
+			element = $(fakeElement.getElementsByTagName("iframe")[0]);
+			fakeElement.removeChild(element);
+      	}		
+		
+		ClientUI.common.box.InlineBox.parentClass.constructor().call(this, element, parentElement, dontUpdateStyles);
+		
+		// additional styles
+		if(!dontUpdateStyles) {
+		}
+	}
+});
+
+var ClientUI_common_box_Substrate_idGenerator = 0;
\ No newline at end of file

Added: trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/utils/CustomEvent.js
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/utils/CustomEvent.js	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/utils/CustomEvent.js	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,104 @@
+/**
+ * CustomEvent.js		Date created: 6.04.2007
+ * Copyright (c) 2007 Exadel Inc.
+ * @author Denis Morozov <dmorozov at exadel.com>
+ */
+ClientUILib.declarePackage("ClientUI.common.utils.CustomEvent");
+
+ClientUI.common.utils.CustomEvent = Class.create({
+	CLASSDEF: {
+		name: 'ClientUI.common.utils.CustomEvent'
+	}
+	
+});
+
+Object.extend(ClientUI.common.utils.CustomEvent.prototype, {
+	_eventName: 'undefined',
+	_id: -1,
+	
+	// Constructor
+	initialize: function(event) {
+		this._eventName = event;
+		// WARNING: If too many events will be in system with dynamic 
+		// observation/stop observation than integer idGenerator can be overflowed
+		this._id = ClientUI_common_utils_CustomEvent_idGenerator++;
+	},
+	getName: function() {
+		return this._eventName;
+	},
+	getId: function() {
+		return this._id;
+	},
+	
+	// Fire this event and notifies the subscribers.
+	fire: function() {
+        var len = Event.subscribers.length;
+        if (!len) {
+            return true;
+        }
+
+		var id = this.getId();
+		var subscribers = Event.subscribers;
+		var subscribersToNotify = Event.subscribers.findAll( 
+			function(subscriber) {
+				return subscriber.event.getId()===id; 
+			});
+
+		var args = $A(arguments);
+		subscribersToNotify.each(function(subscriber) {
+			subscriber.callback.apply(subscriber.callback, args);
+		});
+	}
+});
+
+Event._observe = Event.observe;
+Event.observe  = function( element, name, observer, useCapture ) {
+	if(element && element.getName && element.getId) {
+		var customEvent = element;
+		if (!this.subscribers) {
+			this.subscribers = [];	
+		}
+		
+		this.subscribers.push({
+			event: customEvent,
+			desc: name,
+			callback: observer
+		});
+	}
+	else {
+		Event._observe(element, name, observer, useCapture);	
+	}
+};
+
+Event._stopObserving = Event.stopObserving;
+Event.stopObserving  = function( element, name, observer, useCapture ) {
+	if(element && element.getName && element.getId) {
+		var customEvent = element;
+		if (this.subscribers && this.subscribers.length && this.subscribers.length!==0) {
+			var id = customEvent.getId();
+			this.subscribers = this.subscribers.select(
+				function(subscriber) {
+					return subscriber.event.getId()!==id; 
+				});
+		}
+	}
+	else {
+		Event._stopObserving(element, name, observer, useCapture);	
+	}
+};
+
+Event._unloadCache = Event.unloadCache; 
+Event.unloadCache = function() {
+    if (Event.subscribers) {
+	    for (var i = 0, length = Event.subscribers.length; i < length; i++) {
+	      Event.stopObserving.apply(this, Event.subscribers[i]);
+	      Event.subscribers[i][0] = null;
+	    }
+	    Event.subscribers = false;
+	}
+	Event._unloadCache();
+};
+  
+var ClientUI_common_utils_CustomEvent_idGenerator = 0;
+
+ 
\ No newline at end of file

Added: trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/utils/StringBuilder.js
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/utils/StringBuilder.js	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/utils/StringBuilder.js	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,64 @@
+/*
+ * TODO: Copyright (c) 2007 Denis Morozov <dmorozov at exadel.com>
+ *
+ * ...
+ */
+ClientUILib.declarePackage("ClientUI.common.utils.StringBuilder");
+
+/*
+/* sbuilder.js - Helper class to improve strings concatenation perfomance 
+ * by Denis Morozov <dmorozov at exadel.com> distributed under the BSD license. 
+ *
+ * Usage:
+ * var sb = new StringBuilder();
+ * sb.append("String 1").append("String 2");
+ * sb.append("String 3");
+ * var str = sb.toString();
+ */
+StringBuilder = Class.create({
+	CLASSDEF: {
+		name: 'StringBuilder'
+	}
+	
+});
+
+Object.extend(StringBuilder.prototype, {
+	length: 	0,
+	
+	// private
+	_current:	0,
+	_parts:		[],
+	_string: 	null,	// used to cache the string
+	
+	initialize: function(str) {
+		this._string = null;
+		this._current = 0;
+		this._parts = [];
+		this.length = 0;
+		
+		if(str != null)
+			this.append(str);
+	},
+	append: function (str) {
+		// append argument
+		//this.length += (this._parts[this._current++] = String(str)).length;
+		this._parts.push(String(str));
+		
+		// reset cache
+		this._string = null;
+		return this;
+	},	
+	toString: function () {
+		if (this._string != null)
+			return this._string;
+		
+		var s = this._parts.join("");
+		this._parts = [s];
+		this._current = 1;
+		this.length = s.length;
+		
+		return this._string = s;
+	}
+})
+
+

Added: trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/utils/Validators.js
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/utils/Validators.js	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/common/utils/Validators.js	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,122 @@
+/**
+ * Validators.js		Date created: 14.04.2007
+ * Copyright (c) 2007 Exadel Inc.
+ * @author Denis Morozov <dmorozov at exadel.com>
+ */
+ 
+var Validators = {
+	/**
+	 * Internet Explorer holds references to objects that are not actually javascript objects. So if we use the 
+	 * objects in javascript it will give error. But the typeof operator identifies them as javascript objects( problem!!!). 
+	 * Here we can use the isIEObject() function to identify those objects.
+	 */
+	isIEObject: function(a) {
+		return this.isObject(a) && typeof a.constructor != 'function';
+	},
+
+	/**
+	 * This function returns true if a is an array, meaning that it was produced by the Array constructor or by 
+	 * the [ ] array literal notation.
+	 */
+	isArray: function(a) {
+		return this.isObject(a) && a.constructor == Array;
+	},
+ 
+	/**
+	 * This function returns true if a is one of the Boolean values, true or false.
+	 */
+	isBoolean: function(a) {
+		return typeof a == 'boolean';
+	},
+	
+	getBoolean: function(val, defVal) {
+		if(this.isBoolean(val))
+			return val;
+		if(val == "true") return true;
+		else if(val == "false") return false;
+		
+		return defVal;
+	},
+
+	/**
+	 * This function returns true if a is an object or array or function containing no enumerable members.
+	 */
+	isEmpty: function(o) {
+		if (this.isObject(o)) {
+           for (var i in o) {
+                return false;
+           }
+		}
+		else if(this.isString(o) && o.length > 0) {
+			return false;
+		}
+		
+		return !this.IsNumber(o);
+	},
+
+	/**
+	 * This function returns true if a is a function. Beware that some native functions in IE were made to look 
+	 * like objects instead of functions. This function does not detect that.Netscape is better behaved in this regard.
+	 */
+	isFunction: function(a) {
+		return typeof a == 'function';
+	},
+
+	/**
+	 * This function returns true if a is the null value.
+	 */
+	isNull: function(a) {
+		return typeof a == 'object' && !a;
+	},
+
+	/**
+	 * This function returns true if <code>data</code> is a finite number. It returns false if <code>data</code> is NaN or Infinite.
+	 */
+	IsNumber: function(data) {
+		if(typeof data == 'number' && isFinite(data)) {
+			return true;
+		}
+		
+		// if it is a string that contains a number
+		var re = /(^-?[1-9](\d{1,2}(\,\d{3})*|\d*)|^0{1})$/;
+		if ( re.test(data) ) {
+			return true;
+		}
+		return false;
+	},
+	IsFormattedNumber: function(data) {
+		// Regular expression should match number with commas or not
+		//1. ^-? <-- '-' is optional at the beginning
+		//2. \d{1,3} <-- with or without comma, first 3 digits
+		//3. \d{1,3}(\,\d{3})* <-- with comma, at least one digit with max of three before repeating like ',ddd'
+		//4. \d+ <-- without comma, match any number of integer(shouldn't be though)
+		var re = /^-?(\d{1,3}|\d{1,3}(\,\d{3})*|\d*)$/g;
+		if ( ! re.test(data) ) {
+			return false;
+		}
+		return true;		
+	},
+
+	/**
+	 * This function returns true if a is an object, array, or function. It returns false if a is a string, 
+	 * number, Boolean, null, or undefined.
+	 */
+	isObject: function(a) {
+		return (typeof a == 'object' && !!a) || this.isFunction(a);
+	},
+
+	/**
+	 * This function returns true if a is a string.
+	 */
+	isString: function(a) {
+		return typeof a == 'string';
+	},
+
+	/**
+	 * This function returns true if a is the undefined value. You can get the undefined value from an uninitialized 
+	 * variable or from an object's missing member.
+	 */
+	isUndefined: function(a) {
+		return typeof a == 'undefined';
+	}
+};
\ No newline at end of file

Added: trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/ArrayDataModel.js
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/ArrayDataModel.js	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/ArrayDataModel.js	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,39 @@
+/*
+ * TODO: Copyright (c) 2007 Denis Morozov <dmorozov at exadel.com>
+ *
+ * ...
+ */
+ClientUILib.declarePackage("ClientUI.controls.grid.ArrayDataModel");
+
+ClientUILib.requireClass("ClientUI.controls.grid.DataModel");
+/*
+ * ArrayDataModel.js - Array datamodel for grid control 
+ * by Denis Morozov <dmorozov at exadel.com> distributed under the BSD license. 
+ *
+ */
+ClientUI.controls.grid.ArrayDataModel = Class.create({
+	CLASSDEF: {
+		name: 'ClientUI.controls.grid.ArrayDataModel',
+		parent: ClientUI.controls.grid.DataModel
+	}
+	
+});
+
+Object.extend(ClientUI.controls.grid.ArrayDataModel.prototype, {
+	initialize: function(data) {
+		this.parentClass().constructor().call(this);
+		this.data = $A(data || []);
+	},
+	getRow: function(index) {
+		return this.data[index];
+	},
+	getRows: function(indexFrom, indexTo) {
+		return $A(this.data.slice(indexFrom, indexTo)); 
+	},
+	getCount: function() {
+		return this.data.length;
+	},
+	getRequestDelay: function() {
+		return 50;
+	}
+})
\ No newline at end of file

Added: trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/CellsStrip.js
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/CellsStrip.js	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/CellsStrip.js	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,126 @@
+/**
+ * CellsStrip.js		Date created: 6.04.2007
+ * Copyright (c) 2007 Exadel Inc.
+ * @author Denis Morozov <dmorozov at exadel.com>
+ */
+ClientUILib.declarePackage("ClientUI.controls.grid.CellsStrip");
+
+ClientUI.controls.grid.CellsStrip = Class.create({
+	CLASSDEF: {
+		name: 'ClientUI.controls.grid.CellsStrip'
+	}
+	
+});
+
+Object.extend(ClientUI.controls.grid.CellsStrip.prototype, {
+	columnWidth: 0,
+	basePos: 0,
+	currOffset: 0,
+	
+	initialize: function(options) {
+		this._hash = [];
+		this.columnWidth = 0;
+		this.basePos = 0;
+		
+		if(options) {
+			Object.extend(this, options);
+		}
+	},
+	
+	length: function() {
+		return this._hash.length;
+	},
+	
+	add: function(key, value) {
+		value.key = key;
+		this._hash.push(value);
+	},
+	
+	remove: function(key) {
+		var count = this._hash.length;
+		for(var i=0; i<count; i++) {
+			if(this._hash[i].key == key) {
+				this._hash.slice(i, i);
+				break;
+			}
+		}
+	},
+	
+	// set cells offset
+	setOffset: function(offset, updateBasePos, silent) {
+		if(!silent) {
+			var basePos = this.basePos;
+			this._hash.each(function(item){
+				item.setOffset(basePos + offset);	
+			});
+		}
+				
+		this.currOffset = offset;
+		if(updateBasePos) {
+			this.basePos += offset;
+			this.currOffset = 0;	
+		}
+	},
+	// set cells width
+	setWidth: function(newWidth, silent) {
+		if(!silent) {		
+			this._hash.each(function(item){
+				item.setWidth(newWidth);
+			});
+		}
+		this.columnWidth = newWidth;
+	},
+	
+	getWidth: function() {
+		return this.columnWidth;
+	},
+	
+	getOffset: function() {
+		return this.currOffset;
+	}
+});
+
+ClientUI.controls.grid.ColumnCell = Class.create({
+	CLASSDEF: {
+		name: 'ClientUI.controls.grid.ColumnCell'
+	},
+	initialize: function(cellObj, parentStrip) {
+		this.cellObj = new ClientUI.common.box.Box(cellObj, null, true);
+		this.parentStrip = parentStrip;
+	},
+	
+	// shift column cell in horizontal
+	setOffset: function(offset, updateBasePos) {
+		var newOffset = offset;
+		if(this.separator) {
+			newOffset += this.parentStrip.getWidth() - this.cellObj.getWidth()/2 - 1;
+		}
+		this.cellObj.moveToX(newOffset);
+	},
+	
+	// set column width
+	setWidth: function(newWidth) {
+		if(!this.separator) {
+			this.cellObj.setWidth(newWidth);
+			if(this.cellObj.getElement().childNodes.length > 0) {
+				var childs = $A(this.cellObj.getElement().childNodes);
+				
+				var fixedWidth = newWidth;
+				if(ClientUILib.isGecko) {
+					var oldElement = this.cellObj.getElement();
+					var i = 0;
+					while(i<childs.length && !childs[i].tagName) i++;
+					if(i<childs.length) {
+						this.cellObj.element = childs[i];
+						fixedWidth -= this.cellObj.getBorderWidth("lr") + this.cellObj.getPadding("lr");
+						$(childs[i]).setStyle({width: fixedWidth + "px"});
+					}
+					this.cellObj.element = oldElement;
+				}
+			}
+		}
+		else {
+			this.setOffset(this.parentStrip.basePos + (newWidth - this.parentStrip.getWidth()));
+		}
+	}
+});

Added: trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/DataModel.js
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/DataModel.js	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/DataModel.js	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,55 @@
+/*
+ * TODO: Copyright (c) 2007 Denis Morozov <dmorozov at exadel.com>
+ *
+ * ...
+ */
+ClientUILib.declarePackage("ClientUI.controls.grid.DataModel");
+
+ClientUILib.requireClass("ClientUI.common.utils.CustomEvent");
+
+/*
+ * DataModel.js - Base datamodel class for grid control 
+ * by Denis Morozov <dmorozov at exadel.com> distributed under the BSD license. 
+ *
+ */
+ClientUI.controls.grid.DataModel = Class.create({
+	CLASSDEF: {
+		name: 'ClientUI.controls.grid.DataModel'
+	}
+});
+
+Object.extend(ClientUI.controls.grid.DataModel.prototype, {
+	eventDataReady: {},	
+	
+	initialize: function() {
+		// constructor
+		this.eventDataReady = new ClientUI.common.utils.CustomEvent('DataModel::OnDataReady');
+	},	
+	
+	// interface method
+	getRow: function(index) {
+		return [];
+	},
+	
+	// interface method
+	getRows: function(indexFrom, indexTo) {
+		return [];
+	},
+	
+	// count of all data rows
+	getCount: function() {
+		return 0;
+	},
+		
+	// start rows loading
+	loadRows: function(options) {
+		this.eventDataReady.fire(options);
+	},
+	
+	// regulate requsts frequency
+	getRequestDelay: function() {
+		return 1000;
+	}
+})
+
+			

Added: trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/DefaultColumnModel.js
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/DefaultColumnModel.js	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/DefaultColumnModel.js	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,265 @@
+/*
+ * TODO: Copyright (c) 2007 Denis Morozov <dmorozov at exadel.com>
+ *
+ * ...
+ */
+ClientUILib.declarePackage("ClientUI.controls.grid.DefaultColumnModel");
+
+
+ClientUI.controls.grid.DefaultColumnModel = Class.create({
+	CLASSDEF: {
+		name: 'ClientUI.controls.grid.DefaultColumnModel'
+	}
+	
+});
+
+Object.extend(ClientUI.controls.grid.DefaultColumnModel.prototype, {
+	initialize: function(config) {
+		this.config = config;
+		
+		// default properties
+    	this.defaultWidth = 100;
+    	this.defaultSortable = false;		
+	},
+	
+	// generic events arised from columns
+	onWidthChange: function(){},
+    onHeaderChange: function(){},
+	onHiddenChange: function(){},
+	
+	// fire event
+    fireWidthChange : function(colIndex, newWidth){
+		this.onWidthChange(this, colIndex, newWidth);
+	},
+	
+	fireHeaderChange : function(colIndex, newHeader){
+		this.onHeaderChange(this, colIndex, newHeader);
+	},
+	
+	fireHiddenChange : function(colIndex, hidden){
+		this.onHiddenChange(this, colIndex, hidden);
+	},
+
+	/**
+	 * Returns the number of columns.
+	 * @return {Number}
+	 */
+    getColumnCount : function(){
+        return this.config.length;
+    },
+    
+    /**
+     * Returns true if the specified column is sortable.
+     * @param {Number} col The column index
+     * @return {Boolean}
+     */
+    isSortable : function(col){
+        if(typeof this.config[col].sortable == 'undefined'){
+            return this.defaultSortable;
+        }
+        return this.config[col].sortable;
+    },
+    
+    /**
+     * Interface method - Returns true if the specified column is hidden.
+     * @param {Number} col The column index
+     * @return {Boolean}
+     */
+    isHidden : function(col){
+        return false;
+    },
+        
+    /**
+     * Returns the rendering (formatting) function defined for the column.
+     * @param {Number} col The column index
+     * @return {Function}
+     */
+    getRenderer : function(col){
+        if(!this.config[col].renderer){
+            return ClientUI.controls.grid.DefaultColumnModel.defaultRenderer;
+        }
+        return this.config[col].renderer;
+    },
+        
+    /**
+     * Sets the rendering (formatting) function for a column.
+     * @param {Number} col The column index
+     * @param {Function} fn
+     */
+    setRenderer : function(col, fn){
+        this.config[col].renderer = fn;
+    },
+        
+    /**
+     * Returns the width for the specified column.
+     * @param {Number} col The column index
+     * @return {Number}
+     */
+    getColumnWidth : function(col){
+        return this.config[col].width || this.defaultWidth;
+    },
+        
+    /**
+     * Sets the width for a column.
+     * @param {Number} col The column index
+     * @param {Number} width The new width
+     */
+    setColumnWidth : function(col, width, suppressEvent){
+        this.config[col].width = width;
+        this.totalWidth = null;
+        if(!suppressEvent){
+             this.fireWidthChange(col, width);
+        }
+    },
+                
+    /**
+     * Returns the total width of all columns.
+     * @param {Boolean} includeHidden True to include hidden column widths
+     * @return {Number}
+     */
+    getTotalWidth : function(includeHidden){
+        if(!this.totalWidth){
+            this.totalWidth = 0;
+            for(var i = 0; i < this.config.length; i++){
+                if(includeHidden || !this.isHidden(i)){
+                    this.totalWidth += this.getColumnWidth(i);
+                }
+            }
+        }
+        return this.totalWidth;
+    },
+    
+    /**
+     * Returns the header for the specified column.
+     * @param {Number} col The column index
+     * @return {String}
+     */
+    getColumnHeader : function(col){
+        return this.config[col].header;
+    },
+         
+    /**
+     * Sets the header for a column.
+     * @param {Number} col The column index
+     * @param {String} header The new header
+     */
+    setColumnHeader : function(col, header){
+        this.config[col].header = header;
+        this.fireHeaderChange(col, header);
+    },
+    
+    /**
+     * Returns the tooltip for the specified column.
+     * @param {Number} col The column index
+     * @return {String}
+     */
+    getColumnTooltip : function(col){
+            return this.config[col].tooltip;
+    },
+    /**
+     * Sets the tooltip for a column.
+     * @param {Number} col The column index
+     * @param {String} tooltip The new tooltip
+     */
+    setColumnTooltip : function(col, header){
+            this.config[col].tooltip = tooltip;
+    },
+        
+    /**
+     * Returns the dataIndex for the specified column.
+     * @param {Number} col The column index
+     * @return {Number}
+     */
+    getDataIndex : function(col){
+        if(typeof this.config[col].dataIndex != 'number'){
+            return col;
+        }
+        return this.config[col].dataIndex;
+    },
+         
+    /**
+     * Sets the dataIndex for a column.
+     * @param {Number} col The column index
+     * @param {Number} dataIndex The new dataIndex
+     */
+    setDataIndex : function(col, dataIndex){
+        this.config[col].dataIndex = dataIndex;
+    },
+    /**
+     * Returns true if the cell is editable.
+     * @param {Number} colIndex The column index
+     * @param {Number} rowIndex The row index
+     * @return {Boolean}
+     */
+    isCellEditable : function(colIndex, rowIndex){
+        return this.config[colIndex].editable || (typeof this.config[colIndex].editable == 'undefined' && this.config[colIndex].editor);
+    },
+    
+    /**
+     * Returns the editor defined for the cell/column.
+     * @param {Number} colIndex The column index
+     * @param {Number} rowIndex The row index
+     * @return {Object}
+     */
+    getCellEditor : function(colIndex, rowIndex){
+        return this.config[colIndex].editor;
+    },
+       
+    /**
+     * Sets if a column is editable.
+     * @param {Number} col The column index
+     * @param {Boolean} editable True if the column is editable
+     */
+    setEditable : function(col, editable){
+        this.config[col].editable = editable;
+    },
+        
+    /**
+     * Returns true if the column is hidden.
+     * @param {Number} colIndex The column index
+     * @return {Boolean}
+     */
+    isHidden : function(colIndex){
+        return this.config[colIndex].hidden;
+    },    
+    
+    /**
+     * Returns true if the column width cannot be changed
+     */
+    isFixed : function(colIndex){
+        return this.config[colIndex].fixed;
+    },
+    
+    /**
+     * Returns true if the column cannot be resized
+     * @return {Boolean}
+     */
+    isResizable : function(colIndex){
+        return this.config[colIndex].resizable !== false;
+    },
+    /**
+     * Sets if a column is hidden.
+     * @param {Number} colIndex The column index
+     */
+    setHidden : function(colIndex, hidden){
+        this.config[colIndex].hidden = hidden;
+        this.totalWidth = null;
+        this.fireHiddenChange(colIndex, hidden);
+    },
+    
+    /**
+     * Sets the editor for a column.
+     * @param {Number} col The column index
+     * @param {Object} editor The editor object
+     */
+    setEditor : function(col, editor){
+        this.config[col].editor = editor;
+    }
+});
+
+ClientUI.controls.grid.DefaultColumnModel.defaultRenderer = function(value){
+	if(typeof value == 'string' && value.length < 1){
+	    return '&#160;';
+	}
+	return value;
+}
\ No newline at end of file

Added: trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/Grid.js
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/Grid.js	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/Grid.js	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,309 @@
+/**
+ * Grid.js		Date created: 6.04.2007
+ * Copyright (c) 2007 Exadel Inc.
+ * @author Denis Morozov <dmorozov at exadel.com>
+ */
+ClientUILib.declarePackage("ClientUI.controls.grid.Grid");
+
+ClientUILib.requireClass("ClientUI.common.box.Box");
+ClientUILib.requireClass("ClientUI.controls.grid.GridHeader");
+ClientUILib.requireClass("ClientUI.controls.grid.GridBody");
+ClientUILib.requireClass("ClientUI.controls.grid.GridFooter");
+ClientUILib.requireClass("ClientUI.controls.grid.CellsStrip");
+
+/*
+ * grid.js - Grid library on top of Prototype 
+ * by Denis Morozov <dmorozov at exadel.com> distributed under the BSD license. 
+ *
+ * TODO: description of control 
+ *
+ * TODO: usage description
+ * Usage: 
+ *   <script src="/javascripts/prototype.js" type="text/javascript"></script>
+ *   <script src="/javascripts/extend.js" type="text/javascript"></script>
+ *   <script src="/javascripts/grid.js" type="text/javascript"></script>
+ *   <script type="text/javascript">
+ *     // with valid DOM id
+ *     var grid = new ClientUI.controls.grid.Grid('id_of_trigger_element', 'id_of_tooltip_to_show_element')
+ *
+ *     // with text
+ *     var grid = new ClientUI.controls.grid.Grid('id_of_trigger_element', 'a nice description')
+ *   </script>
+ */
+ClientUI.controls.grid.Grid = Class.create({
+	CLASSDEF: {
+		name: 'ClientUI.controls.grid.Grid',
+		parent: ClientUI.common.box.Box
+	}
+
+});
+
+Object.extend(ClientUI.controls.grid.Grid.prototype, {
+	// Custom events
+	/**
+	 * Occured when content header clicked
+	 */
+	eventOnSort: {},
+		
+	initialize: function(element, dataModel, templates, options) {
+		ClientUI.controls.grid.Grid.parentClass.constructor().call(this, element);
+		if(!element || !element.id)
+			this.element.id = "ClientUI_Grid" + ClientUI_controls_grid_Grid_idGenerator++;
+		
+		this.dataModel = dataModel;
+		this.templates = $A(templates);
+		
+		this.eventOnSort = new ClientUI.common.utils.CustomEvent('OnSort');
+		
+		// grid options
+		this.options = {
+			showIndexColumn: false,
+			indexColumnWidth: 30
+		};		
+		if(options) { Object.extend(this.options, options); }
+		
+		this.createControl();
+	},
+	createControl: function() {
+		var grid = this;
+		var layout = new ClientUI.layouts.GridLayoutManager(null, this.getElement());
+		this.layout = layout;
+
+		this.templates.each(function(item) {
+			switch(item.pane) {
+				case GridLayout_Enum.HEADER:
+					var header = new ClientUI.controls.grid.GridHeader($(item.ref), grid);
+					layout.addPane(GridLayout_Enum.HEADER, header);
+					break;
+				case GridLayout_Enum.BODY:
+					var body = new ClientUI.controls.grid.GridBody($(item.ref), grid);
+					layout.addPane(GridLayout_Enum.BODY, body);
+					break;
+				case GridLayout_Enum.FOOTER:
+					var footer = new ClientUI.controls.grid.GridFooter($(item.ref), grid);
+					layout.addPane(GridLayout_Enum.FOOTER, footer);
+					break;
+			}
+		});
+		
+		// create cells strips
+		this._columnsStrip = [];
+		var columns = this.getHeader().getColumns();
+		for(var i=0; i<columns.length; i++) {
+			this._columnsStrip[i] = new ClientUI.controls.grid.CellsStrip({
+				columnWidth: columns[i].object.getWidth(), 
+				basePos: columns[i].object.getX(),
+				frozen: columns[i].frozen
+			});
+		}
+		
+		this._parseRowAndAddToStrips("header", this.getHeader().getRow());
+		if(this.getFooter()) {
+			this._parseRowAndAddToStrips("footer", this.getFooter().getRow());	
+		}
+		
+		this.currentScrollPos = 0;
+		this.controlCreated = true;
+		this.updateLayout();
+	},
+	updateLayout: function() {
+		if(!this.controlCreated) {
+			return;
+		}
+		ClientUI.controls.grid.Grid.parentClass.method("updateLayout").call(this);
+		if(this.layout) {
+			this.layout.updateLayout();
+		}
+	},
+	getHeader: function() {
+		return this.layout.getPane(GridLayout_Enum.HEADER);
+	},
+	getFooter: function() {
+		return this.layout.getPane(GridLayout_Enum.FOOTER);
+	},
+	getBody: function() {
+		return this.layout.getPane(GridLayout_Enum.BODY);		
+	},
+	_getVisibleHeaderControls: function() {
+		var controls = [];
+		var columns = this.getHeader().getColumns();
+		columns.each(function(column){
+			var ctrls = $A(column.body.getElement().getElementsByTagName("select"));
+			ctrls.each(function(ctrl){
+				if(Element.visible(ctrl)) {
+					controls.push(ctrl);
+				}
+			});
+		});
+		return controls;
+	},
+	adjustColumnWidth: function(index, width) {
+		var oldWidth = this.getHeader().getColumns()[index].object.getWidth();
+		
+		// 1. set new width
+		this._columnsStrip[index].setWidth(width);
+		
+		// Hide controls in IE that flipped in other case
+		var ctrlsIE = [];
+		if(ClientUILib.isIE) {
+			ctrlsIE = this._getVisibleHeaderControls();
+			ctrlsIE.each(function(ctrl){
+				Element.hide(ctrl);
+			});	
+		}
+		
+		// 2. shift right side columns
+		if(index < this._columnsStrip.length-1) {
+			var count = this._columnsStrip.length;			
+			if(this._columnsStrip[index].frozen) {
+				// if current column is frozen, lets find last frozen column index
+				var i=index+1;
+				while(this._columnsStrip[i].frozen && i<count) {
+					i++;
+				}
+				count = i;
+			}
+			
+			var offset = width - oldWidth;
+			for(var i=index+1; i<count; i++) {
+				this._columnsStrip[i].setOffset(offset, true);
+			}		
+		}
+		
+		this.updateLayout();
+		
+		if(ClientUILib.isIE) {
+			ctrlsIE.each(function(ctrl){
+				Element.show(ctrl);
+			});	
+		}		
+	},
+	adjustScrollPosition: function(pos) {
+		if(pos<0) {pos = 0;}
+		this.currentScrollPos = pos;
+		var i=0;		
+		while(i<this._columnsStrip.lenth && this._columnsStrip[i].frozen) i++;
+		if(i<this._columnsStrip.length) {
+			while(i<this._columnsStrip.length) {
+				this._columnsStrip[i++].setOffset(pos, false, true);
+			}
+		}
+		this.getHeader().adjustScrollPosition(pos);
+		this.getBody().adjustScrollPosition(pos);
+		if(this.getFooter()) {this.getFooter().adjustScrollPosition(pos);}
+	},
+	loadData: function() {
+		this.getBody().adjustDataPosition(0);
+	},
+	frozeColumn: function(index, froze) {
+		if(index<0 || index>=this.getHeader().getColumns().length)
+			return false;
+		this.getHeader().getColumns()[index].frozen = froze;
+	},
+	setColumnMinWidth: function(index, width) {
+		if(index<0 || index>=this.getHeader().getColumns().length)
+			return false;
+		this.getHeader().getColumns()[index].minWidth = width;
+		return true;		
+	},
+	_isSeparator: function(element) {
+		return Element.hasClassName(element, this.getHeader().CLASSDEF.sepStyleClass);
+	},
+	_isCell: function(element) {
+		return element.tagName && element.tagName.toLowerCase()!="iframe"; 
+	},
+	// helper method parse row of elements and add separately into rows and columns list
+	_parseRowAndAddToStrips: function(id, object) {
+		var i=0, j=0, cells, cell, count=0;	
+		if(object.frozen && object.normal) {
+			// process frozen columns
+			cells = object.frozen.getElement().childNodes;
+			count =  cells.length;
+			for(i=0, j=0; i<count; i++) {
+				cell = cells[i];
+				if(this._isCell(cell) && !this._isSeparator(cell)) {
+					this._columnsStrip[j].add(id, new ClientUI.controls.grid.ColumnCell(
+						cell,
+						this._columnsStrip[j]));
+					j++;
+				}
+			}
+			var sripIndex = j;
+			for(i=0, j=0; i<count; i++) { // process separators
+				cell = cells[i];
+				if(this._isSeparator(cell)) {
+					var columnCell = new ClientUI.controls.grid.ColumnCell(
+							cell,
+							this._columnsStrip[j]);
+					columnCell.separator = true;
+					this._columnsStrip[j].add(id, columnCell);
+					j++;
+				}
+			}
+				
+			// process normal columns		
+			i = sripIndex;
+			cells = object.normal.getElement().childNodes;
+			for(j=0; j<cells.length; j++) {
+				cell = cells[j];
+				if(this._isCell(cell) && !this._isSeparator(cell)) {
+					this._columnsStrip[i].add(id, new ClientUI.controls.grid.ColumnCell(
+						cell,
+						this._columnsStrip[i]));
+					i++;
+				}
+			}
+			i = sripIndex;
+			for(j=0; j<cells.length; j++) { // process separators
+				cell = cells[j];
+				if(this._isSeparator(cell)) {
+					var columnCell = new ClientUI.controls.grid.ColumnCell(
+							cell,
+							this._columnsStrip[i]);
+					columnCell.separator = true;
+					this._columnsStrip[i].add(id, columnCell);
+					i++;
+				}
+			}
+		}
+	},
+	_getParsedRowsCount: function() {
+		return this._columnsStrip[0].length();
+	},
+	_removeRowFromStrips: function(id) {
+		this._columnsStrip.each(function(strip){
+				strip.remove(id);
+		});		
+	},
+	getColumnScrollX: function(index) {
+		if(index) {
+			return index<this._columnsStrip.length ? this._columnsStrip[index].currOffset : this.currentScrollPos;			
+		}
+		
+		return this.currentScrollPos;		
+	},
+	getColumnsTotalWidth: function() {
+		var totalWidth = 0;
+		this.getHeader().getColumns().each(function(column){
+			totalWidth += column.object.getWidth();
+		});
+		
+		return totalWidth;
+	},
+	getColumnsFrozenWidth: function() {
+		var totalWidth = 0;
+		var columns = this.getHeader().getColumns();
+		var i = 0;
+		while(i<columns.length && columns[i].frozen) {
+			totalWidth += columns[i++].object.getWidth();
+		}
+		return totalWidth;
+		
+	},
+	invalidate: function(params) {
+		this.getBody().invalidate(params);
+	}
+})
+
+var ClientUI_controls_grid_Grid_idGenerator = 0;
+			

Added: trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/GridBody.js
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/GridBody.js	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/GridBody.js	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,738 @@
+/*
+ * TODO: Copyright (c) 2007 Denis Morozov <dmorozov at exadel.com>
+ *
+ * ...
+ */
+ClientUILib.declarePackage("ClientUI.controls.grid.GridBody");
+
+ClientUILib.requireClass("ClientUI.common.box.Box");
+
+/*
+ * GridHeader.js - Grid control header pane
+ * TODO: add comments
+ */
+ClientUI.controls.grid.GridBody = Class.create({
+	CLASSDEF: {
+		name: 'ClientUI.controls.grid.GridBody',
+		parent: ClientUI.common.box.Box
+	}
+	
+});
+
+Object.extend(ClientUI.controls.grid.GridBody.prototype, {
+	/**
+	 * Count of rows can be viewed in the same time in grid
+	 */
+	dataVisible: 50,
+	
+	/**
+	 * Count of rows loaded additianally to dataVisible rows
+	 */
+	dataDelta: 5,
+	
+	/**
+	 * Contains loaded data
+	 */
+	dataView: [],
+	
+	/**
+	 * Pool for rows. Targeted to improve perfomance
+	 */
+	rowsPool: [],
+		
+	/**
+	 * Current data position
+	 */
+	currentPos: 0,
+	
+	/**
+	 * Constructor
+	 * @param {Object} template for Grid body row
+	 * @param {Object} grid parent grid object
+	 */
+	initialize: function(template, grid) {
+		this.grid = grid;
+		ClientUI.controls.grid.GridBody.parentClass.constructor().call(this, null, grid.getElement());
+		this.element.id = this.grid.getElement().id + "_Body";
+
+		// declare event listeners
+		this._eventOnHScroll = this._onContentHScroll.bind(this);
+		this._eventOnVScroll = this._onContentVScroll.bind(this);
+		this._eventOnDataReady = this._onDataReady.bind(this);
+		
+		this.createControl(template);
+		this.registerEvents();
+		this.updateLayout();
+	},
+	// event listeners
+	_onContentHScroll: function(xpos) {
+		this.grid.adjustScrollPosition(xpos);
+	},
+	_onContentVScroll: function(ypos) {
+		this.helpObject1.moveToY(this.sizeBox.getHeight()+ this.defaultRowHeight + 5);
+		this.helpObject2.moveToY(this.sizeBox.getHeight()+ this.defaultRowHeight + 5);
+		this.setScrollPos(ypos);
+		this.adjustDataPosition(ypos);
+	},
+	_onDataReady: function(options) {
+		// load rows data
+		var loadRow = this.loadRow.bind(this);
+		var byIndex = options.indexes!==null;
+		var count = options.indexes!==null ? options.indexes.length : (options.ids!==null?options.ids.lenth : 0);
+		var i;
+		for(i=0; i<count; i++) {
+			loadRow(
+				options.indexes!==null ? options.indexes[i] : null, 
+				options.ids!==null ? options.ids[i] : null);
+		} 
+		
+		// This trick to force IE rerender rows
+		if(ClientUILib.isIE && !ClientUILib.isIE7) {
+			this.setScrollPos(this.currentPos);
+			this.adjustScrollPosition(this.grid.getColumnScrollX());
+		}		
+	},
+	registerEvents: function() {
+		Event.observe(this.scrollBox.eventHScroll, "grid body hscroll", this._eventOnHScroll);
+		Event.observe(this.scrollBox.eventVScroll, "grid body vscroll", this._eventOnVScroll);
+		Event.observe(this.grid.dataModel.eventDataReady, "grid data is loaded", this._eventOnDataReady);
+	},
+	destroy: function() {
+		Event.stopObserving(this.scrollBox.eventHScroll, "grid body hscroll", this._eventOnHScroll);
+		Event.stopObserving(this.scrollBox.eventVScroll, "grid body vscroll", this._eventOnVScroll);
+		Event.stopObserving(this.grid.dataModel.eventDataReady, "grid data is loaded", this._eventOnDataReady);
+	},
+	createControl: function(template) {
+		// validate template
+		var ch = $(template).firstChild;
+		while(ch) {
+			if(ch.tagName && ch.tagName.toLowerCase()=="table") {
+				this.template = $(ch);
+				break;
+			}
+			ch = ch.nextSibling;
+		}
+
+		template.parentNode.removeChild(template);
+		document.body.appendChild(template);
+
+		var errMsg = "";
+		if(!this.template) {
+			errMsg = "Invalid template specified. GridFooter requires template specified over table element.";
+			ClientUILib.log(ClientUILogger.ERROR, errMsg);
+			throw(errMsg);
+		}
+		if(!this.parseTemplate(this.template)) {
+			errMsg = "Unable to parse template. GridFooter requires template specified over table element with one row.";
+			ClientUILib.log(ClientUILogger.ERROR, errMsg);
+			throw(errMsg);
+		}
+
+		// Remove template
+		document.body.removeChild(template);
+
+		// create scroll box
+		this.scrollBox = new ClientUI.common.box.ScrollableBox(null, this.getElement());
+		this.scrollBox.makeAbsolute();
+		this.contentBox = new ClientUI.common.box.Box(null, this.getElement());
+		this.contentBox.makeAbsolute();
+		this.frozenContentBox = new ClientUI.common.box.Box(null, this.getElement());
+		this.frozenContentBox.makeAbsolute();
+		this.sizeBox = new ClientUI.common.box.Box(null, this.scrollBox.getElement());
+		this.sizeBox.makeAbsolute();
+
+		this.helpObject1 = new ClientUI.common.box.Box($(document.createElement("img")), this.contentBox.getElement());
+		this.helpObject1.setWidth(10);
+		this.helpObject1.setHeight(10);
+		this.helpObject1.makeAbsolute();
+		this.helpObject2 = new ClientUI.common.box.Box($(document.createElement("img")), this.frozenContentBox.getElement());
+		this.helpObject2.setWidth(10);
+		this.helpObject2.setHeight(10);
+		this.helpObject2.makeAbsolute();
+		
+		// create row template
+		this._createRowTemplate();
+		this.controlCreated = true;		
+	},
+	parseTemplate: function(template) {
+		if(!template || !template.rows || template.rows.length===0) {
+			return false;
+		}
+
+		var columns = [];
+		
+		// Get columns information
+		var i = 0;
+		if(this.grid.options.showIndexColumn) {
+			columns[i++] = {
+				value: "$(row)",
+				styleClass: "",
+				id: "",
+				align: "left",
+				valign: "middle",
+				indexRow: true
+			};
+		}
+			
+		var cells = $A(template.rows[0].cells);		
+		cells.each(function(cell) {
+			columns[i++] = {
+				value: cell.innerHTML,
+				styleClass: cell.className,
+				id: cell.id,
+				align: cell.align,
+				valign: cell.vAlign
+			};
+			
+			Element.addClassName(cell, "ClientUI_Grid_BCBody");
+		});
+		this._columns = columns;
+		this.defaultRowHeight = Element.getHeight(template.rows[0].cells[0]);
+		if(ClientUILib.isGecko) {
+			this.defaultRowHeight -= this.getBorderWidth("tb") + this.getPadding("tb");
+		}		
+		return true;		
+	},
+	getColumns: function() {
+		return this._columns;
+	},
+	setScrollPos: function(pos) {
+		this.contentBox.getElement().scrollTop = pos;
+		this.frozenContentBox.getElement().scrollTop = pos;
+		if(ClientUILib.isIE && !ClientUILib.isIE7) {
+			this.contentBox.getElement().scrollTop = pos;
+			this.frozenContentBox.getElement().scrollTop = pos;
+		}
+	},
+	updateLayout: function() {
+		if(!this.controlCreated || !this.grid.controlCreated) {
+			return;
+		}
+		ClientUI.controls.grid.GridBody.parentClass.method("updateLayout").call(this);
+		if(!this.scrollBox || !this.contentBox || !this.sizeBox) {
+			return;			
+		}
+				
+		var scrollLeft = this.contentBox.getElement().scrollLeft;
+		var height = this.scrollBox.getViewportHeight();
+		var fixH = this.grid.getFooter() ? this.grid.getFooter().getHeight() : 0;
+		if(fixH > height) fixH = 0;
+		var totalWidth = this.grid.getColumnsTotalWidth();
+		var frozenContentWidth = this.grid.getColumnsFrozenWidth();
+		
+		this.scrollBox.moveTo(0, 0);
+		this.sizeBox.moveTo(0, 0);
+		this.frozenContentBox.moveTo(0, 0);
+		this.contentBox.moveTo(frozenContentWidth, 0);		
+				
+		this.sizeBox.setWidth(totalWidth);
+		this.sizeBox.setHeight(this.defaultRowHeight * this.grid.dataModel.getCount() + fixH);
+		this.helpObject1.moveToY(this.sizeBox.getHeight()+ this.defaultRowHeight + 5);
+		this.helpObject2.moveToY(this.sizeBox.getHeight()+ this.defaultRowHeight + 5);
+		
+		// NOTE: this needed to force this.scrollBox update scrolled area
+		this.scrollBox.setWidth(this.getWidth()+1);
+		this.scrollBox.setHeight(this.getHeight()+1);
+		this.scrollBox.setWidth(this.getWidth());
+		this.scrollBox.setHeight(this.getHeight());		
+
+		this.contentBox.setWidth(this.scrollBox.getViewportWidth()-frozenContentWidth);
+		this.contentBox.setHeight(height - fixH);		
+		this.frozenContentBox.setWidth(frozenContentWidth);
+		this.frozenContentBox.setHeight(height - fixH);		
+
+		var scrollPos = Math.min(totalWidth - frozenContentWidth - this.contentBox.getWidth(), scrollLeft);
+		this.grid.adjustScrollPosition(scrollPos);
+		
+		this.dataVisible = parseInt(this.contentBox.getHeight() / this.defaultRowHeight, 10) + 1;
+		if(height > 0) {
+			this.adjustDataPosition(this.currentPos);	
+		}		
+	},
+	adjustScrollPosition: function(pos) {
+		this.contentBox.getElement().scrollLeft = pos;	
+	},
+	getScrollXPosition: function() {
+		return this.contentBox.getElement().scrollLeft;
+	},
+	getScrollYPosition: function() {
+		return this.contentBox.getElement().scrollTop;
+	},
+	_createRowTemplate: function() {
+		var headerRow = new ClientUI.common.box.InlineBox(null, this.contentBox.getElement());  		
+		headerRow.getElement().addClassName("ClientUI_Grid_BR");
+		headerRow.setWidth(10000);
+		headerRow.setHeight(this.defaultRowHeight);
+		headerRow.makeAbsolute();
+		headerRow.moveTo(0, -100);
+		headerRow.rowindex = -1;
+		
+		var headerFrozenRow  = new ClientUI.common.box.InlineBox(headerRow.getElement().cloneNode(true));
+		headerFrozenRow.setParent(this.frozenContentBox.getElement());
+
+		var frozenContentBox = this.frozenContentBox;
+		var rowClientHeight = headerRow.getViewportHeight();		
+		var bodyWidthFix = 0;
+		var bodyHeightFix = 0;
+		 				
+		var columnZIndex = this.grid.getHeader().getColumns().length*3 + 10;		
+		var calculateFixes = true;
+		var defaultWidth = 0;
+		var defaultWidthFrozen = 0;
+		var columns = this._columns;
+		var i = 0;
+		
+		this.grid.getHeader().getColumns().each(function(headerColumnDesc) {
+			
+			var columnDesc = columns[i++];
+			var width = headerColumnDesc.object.getWidth();
+			var height = rowClientHeight;
+			
+			var parent = headerColumnDesc.frozen ? headerFrozenRow.getElement() : headerRow.getElement(); 
+			var col = new ClientUI.common.box.InlineBox(null, parent);
+			col.makeAbsolute();
+			var styles = columnDesc.indexRow ? "ClientUI_Grid_BCIndex" : "ClientUI_Grid_BC";
+			col.getElement().addClassName(styles);
+			
+			if(columnDesc.indexRow) {
+				height -= col.getBorderWidth("tb");
+				//height = height + "px !important";
+			}
+			
+			col.getElement().setStyle({zIndex: columnZIndex});
+			columnZIndex -= 3;			
+			col.getElement().columnindex = i-1;
+			col.getElement().hidefocus = "true";
+			col.getElement().tabindex="0";
+			col.setWidth(width);
+			col.setHeight(height);
+			
+			if(!headerColumnDesc.frozen) {
+				col.moveToX(defaultWidth);
+				defaultWidth += width;
+			}
+			else {
+				col.moveToX(defaultWidthFrozen);
+				defaultWidthFrozen += width;
+			}
+			
+			var colBody = new ClientUI.common.box.InlineBox(null, col.getElement());
+			if(!Validators.isEmpty(columnDesc.styleClass)) {
+				colBody.getElement().addClassName(columnDesc.styleClass);
+			}			
+			colBody.getElement().addClassName("ClientUI_Grid_BCBody");
+			if(calculateFixes && ClientUILib.isGecko) {
+				bodyWidthFix = colBody.getBorderWidth("lr") + colBody.getPadding("lr");
+				bodyHeightFix = colBody.getBorderWidth("tb") + colBody.getPadding("tb");
+				calculateFixes = false;
+			}
+			colBody.setWidth(width - bodyWidthFix);
+			colBody.setHeight(height - bodyHeightFix);
+			if(!Validators.isEmpty(columnDesc.valign)) {
+				colBody.getElement().setStyle({verticalAlign: columnDesc.valign});
+			}
+			if(!Validators.isEmpty(columnDesc.align)) {
+				colBody.getElement().setStyle({textAlign: columnDesc.align});
+			}
+		});
+
+		//if(ClientUILib.isGecko) {
+			headerRow.hide();
+		//}
+		this.headerRowTemplate = headerRow;
+		this.headerFrozenRowTemplate = headerFrozenRow;
+	},
+	_getRowDesc: function(parent, idPrefix, headerRow, headerFrozenRow) {
+		var rowDesc = {
+			id: headerRow.getElement().id,
+			idprefix: idPrefix,
+			parent: parent,
+			frozen: headerFrozenRow,
+			normal: headerRow,
+			moveToY: function(y) {
+				this.frozen.moveToY(y);
+				this.normal.moveToY(y);
+			},
+			getId: function() {
+				return this.id;
+			},
+			setId: function(id) {
+				this.id = id;
+				this.frozen.getElement().id = id;
+				this.normal.getElement().id = id;
+				
+				var cellObj, cB;
+				var i, index = 0;
+				var cnt = this.frozen.getElement().childNodes.length;
+				for(i=0; i<cnt; i++) {
+					cellObj = this.frozen.getElement().childNodes[i];
+					cellObj.id = "c_"+ this.rowindex +"_" + index++;
+					cB = document.getElementsByClassName("ClientUI_Grid_BCBody", cellObj);
+					cB[0].id = "b"+ cellObj.id;
+					
+				}
+				cnt = this.normal.getElement().childNodes.length;
+				for(i=0; i<cnt; i++) {
+					cellObj = this.normal.getElement().childNodes[i];
+					cellObj.id = "c_"+ this.rowindex +"_" + index++;
+					cB = document.getElementsByClassName("ClientUI_Grid_BCBody", cellObj);
+					cB[0].id = "b"+ cellObj.id;
+				}
+			},
+			setRowIndex: function(index) {
+				this.rowindex = index;				
+				this.frozen.getElement().rowindex = index;
+				this.normal.getElement().rowindex = index;
+				this.setId(this.idprefix + this.rowindex);
+			},			
+			setRowClass: function(isOdd) {
+				if(isOdd) {
+					this.frozen.getElement().removeClassName("ClientUI_Grid_BREven");
+					this.normal.getElement().removeClassName("ClientUI_Grid_BREven");
+					this.frozen.getElement().addClassName("ClientUI_Grid_BROdd");
+					this.normal.getElement().addClassName("ClientUI_Grid_BROdd");
+				}
+				else {
+					this.frozen.getElement().removeClassName("ClientUI_Grid_BROdd");
+					this.normal.getElement().removeClassName("ClientUI_Grid_BROdd");
+					this.frozen.getElement().addClassName("ClientUI_Grid_BREven");
+					this.normal.getElement().addClassName("ClientUI_Grid_BREven");					
+				}
+			},
+			setCellValue: function(index, newValue) {
+				var i = index;
+				var cnt1 = this.frozen.getElement().childNodes.length;
+				if(i < cnt1) {
+					var cellObj = this.frozen.getElement().childNodes[i];
+					Element.update(cellObj.firstChild, newValue);
+					return true;
+				}
+				i -= cnt1;
+				var cellObj = this.normal.getElement().childNodes[i];
+				Element.update(cellObj.firstChild, newValue);
+				return true; 
+			},
+			show: function() {
+				this.frozen.show();
+				this.normal.show();
+			},
+			showWaitData: function(show) {
+				// process frozen
+				var cells = this.frozen.getElement().childNodes;
+				var i = 0, startIndex = 0;
+				if(this.parent.grid.options.showIndexColumn) {
+					startIndex = 1;
+					this.setCellValue(0, ""+this.rowindex);					
+				}
+				var method = !show ? Element.show : Element.hide;
+				for(i=startIndex;i<cells.length; i++) {
+					method(cells[i]);
+				}
+				
+				//process normal
+				cells = this.normal.getElement().childNodes;
+				for(i=0;i<cells.length; i++) {
+					method(cells[i]);
+				}				
+			},
+			hide: function() {
+				this.frozen.hide();
+				this.normal.hide();
+			},
+			showEmpty: function() {
+				this.moveToY(this.parent.defaultRowHeight * this.rowindex);
+				if(!(ClientUILib.isIE && !ClientUILib.isIE7)) {
+					this.setRowClass(this.rowindex%2 ? false : true);
+					this.show();
+					this.showWaitData(true);
+				}
+			},
+			showNormal: function() {
+				this.show();
+				if(ClientUILib.isIE && !ClientUILib.isIE7) {
+					this.setRowClass(this.rowindex%2 ? false : true);
+					if(this.parent.grid.options.showIndexColumn) {
+						this.setCellValue(0, ""+this.rowindex);					
+					}					
+				}
+				else {
+					this.showWaitData(false);	
+				}
+			},
+			loadRowData: function(row, index, rowData) {
+
+				// replace data patterns in content with real data from row
+				var count = this.parent.grid.getHeader().getColumns().length;
+				var columns = this.parent.getColumns();
+				var i = this.parent.grid.options.showIndexColumn ? 1 : 0;
+				for(; i<count; i++) {
+					var value = this.parent.parseExpression(columns[i].value, rowData, {
+						row: index,
+						cid: row.getId() + ":" + i,
+						rid: row.getId()
+					});
+					this.setCellValue(i, value);
+				}
+				
+				this.showNormal();
+				return true;
+			},
+			getCellsToUpdate: function() {
+				var fcs = $A(document.getElementsByClassName("ClientUI_Grid_BCBody", this.frozen.getElement()));
+				var ncs = $A(document.getElementsByClassName("ClientUI_Grid_BCBody", this.normal.getElement()));
+				var ids = [];
+				fcs.each(function (cell){ids.push(cell.id);});
+				ncs.each(function (cell){ids.push(cell.id);});
+				return ids;
+			}
+		};	
+		
+		return rowDesc;	
+	},
+	createRow: function() {
+		var row = null;
+		var rowFrozen = null;
+		if(this.templateRow) { // use existing template to inherit cell's width & offset
+			row = this.templateRow.normal.getElement().cloneNode(true);
+			rowFrozen = this.templateRow.frozen.getElement().cloneNode(true);
+		}
+		else { // first time
+			row = this.headerRowTemplate.getElement().cloneNode(true);
+			rowFrozen = this.headerFrozenRowTemplate.getElement().cloneNode(true);	
+		}
+		
+		var headerRow = new ClientUI.common.box.InlineBox(row, this.contentBox.getElement(), true);
+		headerRow.setParent(this.contentBox.getElement());
+		var headerFrozenRow = new ClientUI.common.box.InlineBox(rowFrozen, this.frozenContentBox.getElement(), true);
+		headerFrozenRow.setParent(this.frozenContentBox.getElement());
+		
+		var rowDesc = this._getRowDesc(this, this.getElement().id + "BR", headerRow, headerFrozenRow);
+		
+		// create sample row that will be cloned
+		if(!this.templateRow) {
+			this.templateRow = rowDesc;
+			this.templateRow.setId("trow");
+			this.grid._parseRowAndAddToStrips(this.templateRow.id, this.templateRow);
+			this.templateRow.setRowIndex(-100);
+			this.templateRow.showEmpty();
+			this.templateRow.hide();
+		}
+		
+		this.grid._parseRowAndAddToStrips("" + (this.grid._getParsedRowsCount()+1), rowDesc);
+		return rowDesc;
+	},
+	/**
+	 * Parse values of cell's value and title
+	 * Predefined patterns:
+	 * 	- $(row) Index of current row
+	 *  - $(cid) Cell's DOM id
+	 *  - $(rid) Row's DOM id 
+	 *  - $(N) Row Data index
+	 * @param {Object} expr Value to parse. Can be defined over expression in next manner: "$(index_0) and $(index_4) will be over $(index_2) higher!". Where 1,5,3 - indexes in <code>data</code> array param.   
+	 * @param {Object} data Data to replace within expression. 
+	 */
+	parseExpression: function(expr, data, params) {
+		var pattern = /\$\(row\)/i;
+		var rez = expr.gsub(pattern, function(item) {
+			return params.row;
+		});
+		pattern = /\$\(cid\)/i;
+		rez = rez.gsub(pattern, function(item) {
+			return params.cid;
+		})
+		pattern = /\$\(rid\)/i;
+		rez = rez.gsub(pattern, function(item) {
+			return params.rid;
+		})
+				
+		pattern = /\$\((\d*)\)/i;
+		rez = rez.gsub(pattern, function(item) {
+			if(!item || !item[1]) {
+				return "!ERROR!";
+			}
+			var index = parseFloat(item[1]);
+			return data.length > index ? data[index] : "!ERROR!";
+		});
+		
+		return rez;		
+	},
+	loadRow: function(index, id) {
+		var row = null;
+		if(this.dataViewHash) {
+			row = this.dataViewHash[index];
+		} 
+		if(row===null && index!==null) {
+			row = this.dataView.findAll(function(item){return item && item.rowindex==index;});
+			row = row && row.length && row.length>0 ? row[0] : null;
+		}
+		if(row===null && id!==null){
+			var rowEl = document.getElementById(id);
+			if(rowEl!==null) {
+				index = rowEl.length ? rowEl[0].rowindex : rowEl.rowindex;
+				row = this.dataView.findAll(function(item){return item && item.rowindex==index;});
+			}
+		}
+		if(row) {
+			var rowData = this.grid.dataModel.getRow(index);
+			if(rowData && rowData.length && rowData.length > 0) {
+				row.loadRowData(row, index, rowData);
+			}
+		}
+	},
+	getRow: function() {
+		return this.rowsPool.length>0 ? this.rowsPool.pop() : this.createRow();
+	},
+	deleteRow: function(row) {
+		row.hide();
+		row.rowindex = -1;
+		this.rowsPool.push(row);
+	},	
+	adjustDataPosition: function (pos) {
+		// small improvement
+		if(this.dataView.length > 0 && Math.abs(this.currentPos - pos)/this.defaultRowHeigh < this.dataDelta/2) {
+			return;
+		}	
+	
+		// 1. calculate direction and range to load next data 
+		var forwardDir = this.currentPos <= pos;
+		this.currentPos = pos;
+		
+		// first visible row index
+		var first = parseInt(pos / this.defaultRowHeight) - 1;
+		if(first < 0) first = 0;
+		
+		// TODO: check direction and predict some next rows
+		var from = Math.max(first - this.dataDelta, 0);
+		var to = Math.min(first + this.dataVisible+this.dataDelta, this.grid.dataModel.getCount());
+		var range = $R(from, to);
+
+		if(from === to) {
+			ClientUILib.log(ClientUILogger.WARNING, "!!! GridBody: adjustDataPosition. Pos: " + pos + ", From:" + from + ", To:" + to);
+			return;			
+		}
+			
+		// 3. hide invisible rows
+		var dataViewHash = [];
+		var deleteRow = this.deleteRow.bind(this);
+		this.dataView.each(function(item) {
+			if(item && item.rowindex>=0) {
+				if(!range.include(item.rowindex)) {
+					deleteRow(item);
+				}
+				else {
+					dataViewHash[item.rowindex] = item;
+				}
+			}
+		});		
+		// make some cleanup of deleted rows		
+		this.dataView = this.dataView.findAll(function(item) {
+			return item && item.rowindex>=0; 
+		});		
+
+		// 4. show empty rows
+		var rowIndex = 0;
+		var rowsToLoad = [];
+		var rowsToLoadIdx = [];
+		var row, i;
+		for(i=range.start; i<=range.end; i++) {
+			if(!dataViewHash[i]) {
+				row = this.getRow();
+				row.setRowIndex(i);
+				row.showEmpty();
+				this.dataView.push(row);
+				dataViewHash[i] = row;
+				rowsToLoadIdx.push(i);
+				rowsToLoad.push(row.getCellsToUpdate());
+				rowIndex++
+			}
+		}
+		this.dataViewHash = dataViewHash;
+
+		if(rowIndex > 0) {
+			// stop timed adjusting
+			var task = this._getPendingTask();
+			clearTimeout(task.timer);
+			task.timer = null;
+			
+			while(task.rowsToLoadIdx.length) {
+				var index = task.rowsToLoadIdx.pop();
+				var ids = task.rowsToLoad.pop();
+				if(dataViewHash[index]) {
+					rowsToLoadIdx.push(index);
+					rowsToLoad.push(ids);
+				}
+			}
+			
+			task.rowsToLoadIdx = rowsToLoadIdx;
+			task.rowsToLoad = rowsToLoad;
+			this._setPendingTask(task);
+		}		
+	},
+	_getPendingTask: function() {
+		if(!this.pendingTask) {
+			this.pendingTask = {
+				timer: null,
+				rowsToLoad: [],
+				rowsToLoadIdx: []				
+			};
+		}
+		return this.pendingTask;
+	},
+	_setPendingTask: function(task) {
+		// and plan other agjusting over the time
+		task.timer = setTimeout(function() {
+			
+			//ClientUILib.log(ClientUILogger.INFO, "Load data indexes: " + task.rowsToLoadIdx.inspect());
+			//ClientUILib.log(ClientUILogger.INFO, "ViewSize: " + this.dataView.length);
+			ClientUILib.log(ClientUILogger.INFO, "Load data indexes: " + task.rowsToLoad.inspect());
+			
+			// 4. start data loading
+			this.grid.dataModel.loadRows({
+				indexes: task.rowsToLoadIdx,
+				ids: task.rowsToLoad});
+				
+			task.timer = null;
+			task.rowsToLoad = [];
+			task.rowsToLoadIdx = [];
+		}.bind(this), this.grid.dataModel.getRequestDelay());
+		
+		this.pendingTask = task;		
+	},
+	invalidate: function(options) {
+		// load rows data
+		var byIndex = options.indexes!==null;
+		var count = options.indexes!==null ? options.indexes.length : (options.ids!==null?options.ids.lenth : 0);
+		var i,index,id,row;
+		for(i=0; i<count; i++) {
+			index = options.indexes!==null ? options.indexes[i] : null;
+			id = options.ids!==null ? options.ids[i] : null;
+			row = null;
+
+			if(index!==null) {
+				if(this.dataViewHash) {
+					row = this.dataViewHash[index];
+				} 
+				if(!row) {
+					row = this.dataView.findAll(function(item){return item && item.rowindex==index;});
+					row = row && row.length && row.length>0 ? row[0] : null;
+				}
+			}
+			if(row===null && id!==null){
+				var rowEl = document.getElementById(id);
+				if(rowEl!==null) {
+					index = rowEl.length ? rowEl[0].rowindex : rowEl.rowindex;
+					row = this.dataView.findAll(function(item){return item && item.rowindex==index;});
+				}
+			}
+			if(row) {
+				row.showNormal();
+			}
+		}
+		
+		// This trick to force IE rerender rows
+		if(ClientUILib.isIE && !ClientUILib.isIE7) {
+			this.setScrollPos(this.currentPos);
+			this.adjustScrollPosition(this.grid.getColumnScrollX());
+		}
+	}	
+});

Added: trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/GridFooter.js
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/GridFooter.js	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/GridFooter.js	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,260 @@
+/*
+ * TODO: Copyright (c) 2007 Denis Morozov <dmorozov at exadel.com>
+ *
+ * ...
+ */
+ClientUILib.declarePackage("ClientUI.controls.grid.GridFooter");
+
+ClientUILib.requireClass("ClientUI.common.box.Box");
+
+/*
+/* GridHeader.js - Grid control header pane
+ * TODO: add comments
+ */
+ClientUI.controls.grid.GridFooter = Class.create({
+	CLASSDEF: {
+		name: 'ClientUI.controls.grid.GridFooter',
+		parent: ClientUI.common.box.Box
+	}
+	
+});
+
+Object.extend(ClientUI.controls.grid.GridFooter.prototype, {
+	initialize: function(template, grid) {
+		this.grid = grid;
+		ClientUI.controls.grid.GridFooter.parentClass.constructor().call(this);
+		this.element.id = this.grid.getElement().id + "_Footer";
+		
+		this.createControl(template);
+	},
+	createControl: function(template) {
+		// validate template
+		var ch = $(template).firstChild;
+		while(ch) {
+			if(ch.tagName && ch.tagName.toLowerCase()=="table") {
+				this.template = $(ch);
+				break;
+			}
+			ch = ch.nextSibling;
+		}
+
+		template.parentNode.removeChild(template);
+		document.body.appendChild(template);
+
+		if(!this.template) {
+			var errMsg = "Invalid template specified. GridFooter requires template specified over table element.";
+			ClientUILib.log(ClientUILogger.ERROR, errMsg);
+			throw(errMsg);
+		}
+		if(!this.parseTemplate(this.template)) {
+			var errMsg = "Unable to parse template. GridFooter requires template specified over table element with one row.";
+			ClientUILib.log(ClientUILogger.ERROR, errMsg);
+			throw(errMsg);
+		}
+
+		this.contentBox = new ClientUI.common.box.Box(null, this.getElement());
+		this.contentBox.makeAbsolute();
+		this.frozenContentBox = new ClientUI.common.box.Box(null, this.getElement());
+		this.frozenContentBox.makeAbsolute();
+
+		// generate DOM objects tree
+		this.generateDOMTree();
+
+		// Remove template
+		document.body.removeChild(template);
+		
+		// Set dimensions
+		this.setHeight(this.defaultHeight);
+		this.setWidth(this.defaultWidth);
+		this.controlCreated = true;
+		this.updateLayout();	
+	},
+	getRow: function() {
+		if(!this.footerRow) {		
+			var rowDesc = {
+				id: "footer",
+				frozen: this.headerFrozenRow,
+				normal: this.headerRow,
+				moveToY: function(y) {
+					this.frozen.moveToY(y);
+					this.normal.moveToY(y);
+				}
+			};
+			
+			this.footerRow = rowDesc;
+		}
+		
+		return this.footerRow;
+	},
+	parseTemplate: function(template) {
+		if(!template || !template.rows || template.rows.length===0) {
+			return false;
+		}
+		
+		var columns = [];
+		
+		// Get columns information
+		var i = 0;
+		if(this.grid.options.showIndexColumn) {
+			columns[i++] = {
+				innerHTML: "&nbsp;",
+				styleClass: "",
+				id: "",
+				align: "",
+				valign: "",
+				title: ""
+			};
+		}
+			
+		var cells = $A(template.rows[0].cells);		
+		cells.each(function(cell) {
+			columns[i++] = {
+				innerHTML: cell.innerHTML,
+				styleClass: cell.className,
+				id: cell.id,
+				align: cell.align,
+				valign: cell.vAlign,
+				title: cell.title
+			};			
+			Element.addClassName(cell, "ClientUI_Grid_FCBody");
+		});
+		
+		this._columns = columns;
+		this.defaultHeight = Element.getHeight(template.rows[0].cells[0]);
+		if(ClientUILib.isGecko) {
+			this.defaultHeight -= this.getBorderWidth("tb") + this.getPadding("tb");
+		}
+		
+		return true;
+	},
+	generateDOMTree: function() {
+		var headerRow = new ClientUI.common.box.InlineBox(null, this.contentBox.getElement());
+  		headerRow.getElement().addClassName("ClientUI_Grid_FR");
+      	headerRow.getElement().id = this.getElement().id + "FR";
+		headerRow.setHeight(this.defaultHeight)
+
+		var headerFrozenRow  = new ClientUI.common.box.InlineBox(headerRow.getElement().cloneNode(true));
+		headerFrozenRow.setParent(this.frozenContentBox.getElement());
+		headerFrozenRow.getElement().setAttribute("id", this.getElement().id + "FRFroz");
+
+		var columnZIndex = this.grid.getHeader().getColumns().length*2 + 10;
+		var lastFrozenZIndex = columnZIndex;
+		var frozenContentBox = this.frozenContentBox;
+		var rowHeight = headerRow.getViewportHeight();
+		var defaultWidth = 0;
+		var defaultWidthFrozen = 0;
+		var columns = this._columns;
+		var i = 0;
+		this.grid.getHeader().getColumns().each(function(headerColumnDesc) {
+			var columnDesc = columns[i++];
+			
+			// create footer cell
+			var parent = headerColumnDesc.frozen ? headerFrozenRow.getElement() : headerRow.getElement();
+			var col = new ClientUI.common.box.InlineBox(null, parent);
+			col.getElement().addClassName("ClientUI_Grid_FC");
+			col.setStyle({zIndex: columnZIndex});
+			if(headerColumnDesc.frozen) {
+				lastFrozenZIndex = columnZIndex;
+			}			
+			columnZIndex -= 2;
+			
+			if(!Validators.isEmpty(columnDesc.styleClass)) {
+				col.getElement().addClassName(columnDesc.styleClass);	
+			}
+			if(!Validators.isEmpty(columnDesc.id)) {
+				col.getElement().id = columnDesc.id;	
+			}
+			if(!Validators.isEmpty(columnDesc.title)) {
+				col.getElement().title = columnDesc.title;	
+			}
+			if(!Validators.isEmpty(columnDesc.valign)) {
+				col.getElement().setStyle({verticalAlign: columnDesc.valign});
+			}
+
+			// get cell width from header
+			var width = headerColumnDesc.object.getWidth();
+			col.setWidth(width);
+			col.setHeight(rowHeight);
+			col.makeAbsolute();
+			columnDesc.object = col;			
+			if(!headerColumnDesc.frozen) {
+				col.moveToX(defaultWidth);
+				defaultWidth += width;
+			}
+			else {
+				col.moveToX(defaultWidthFrozen);
+				defaultWidthFrozen += width;
+			}
+			
+			// header cell body			
+			var colBody = new ClientUI.common.box.InlineBox(null, col.getElement());
+			colBody.getElement().addClassName("ClientUI_Grid_FCBody");
+			// glue cell content
+			var sb = new StringBuilder('<table width="100%" ');
+			if(!Validators.isEmpty(columnDesc.align)) {
+				sb.append('align="').append(columnDesc.align).append('" ');
+			}		
+			sb.append('cellspacing="0" cellpadding="0" border="0"><tbody><tr><td width="100%"><span style="width:100%">');
+			if(!Validators.isEmpty(columnDesc.innerHTML)) {
+				sb.append(columnDesc.innerHTML);	
+			}
+			sb.append('</span></td></tr></tbody></table>');
+			colBody.getElement().update(sb.toString());
+			colBody.moveTo(0, 0);
+			var bodyWidth = width;
+			var bodyHeight = rowHeight;
+			if(ClientUILib.isGecko) {
+				bodyWidth -= colBody.getBorderWidth("lr") + colBody.getPadding("lr");
+				bodyHeight -= colBody.getBorderWidth("tb") + colBody.getPadding("tb");
+			}
+			colBody.setWidth(bodyWidth);
+			colBody.setHeight(bodyHeight);
+		});
+		
+		this.defaultWidth = defaultWidth;
+		if(ClientUILib.isGecko) {
+			this.defaultWidth -= this.getBorderWidth("lr") + this.getPadding("lr");
+		}
+		
+		this.headerRow = headerRow;
+		this.headerFrozenRow = headerFrozenRow;
+		if(ClientUILib.isIE) {
+			this.substrate = new ClientUI.common.box.Substrate(null, this.getElement());
+			this.substrate.getElement().name = this.getElement().id + "FRFrm";
+			this.substrate.setStyle({zIndex: lastFrozenZIndex-1});
+			this.substrate.setHeight(rowHeight);			
+		}
+	},	
+	getColumns: function() {
+		return this._columns;
+	},
+	updateLayout: function() {
+		if(!this.controlCreated || !this.grid.controlCreated) {
+			return;
+		}
+		ClientUI.controls.grid.GridFooter.parentClass.method("updateLayout").call(this);
+	
+		var height = this.getViewportHeight();
+		var totalWidth = this.grid.getColumnsTotalWidth();
+		var frozenContentWidth = this.grid.getColumnsFrozenWidth();
+
+		this.contentBox.setWidth(Math.max(this.getWidth(), totalWidth));
+		this.contentBox.setHeight(height);
+		this.contentBox.moveTo(frozenContentWidth, 0);
+		this.frozenContentBox.setWidth(frozenContentWidth);
+		this.frozenContentBox.setHeight(height);
+		this.frozenContentBox.moveTo(0, 0);
+		var row = this.getRow();
+		row.frozen.setWidth(frozenContentWidth);
+		row.normal.setWidth(Math.max(this.getWidth(), totalWidth));
+		var frozenContentWidth = this.grid.getBody().frozenContentBox.getWidth();
+		var width = frozenContentWidth+this.grid.getBody().contentBox.getWidth(); 
+		this.setWidth(width);
+		if(ClientUILib.isIE)
+			this.substrate.setWidth(frozenContentWidth);
+		this.adjustScrollPosition(this.grid.getColumnScrollX());
+	},
+	adjustScrollPosition: function(pos) {
+		this.contentBox.moveToX(this.grid.getColumnsFrozenWidth()-pos);	
+	}	
+})

Added: trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/GridHeader.js
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/GridHeader.js	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/controls/grid/GridHeader.js	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,423 @@
+/*
+ * TODO: Copyright (c) 2007 Denis Morozov <dmorozov at exadel.com>
+ *
+ * ...
+ */
+ClientUILib.declarePackage("ClientUI.controls.grid.GridHeader");
+
+ClientUILib.requireClass("ClientUI.common.box.Box");
+ClientUILib.requireClass("ClientUI.common.box.InlineBox");
+
+/*
+ * GridHeader.js - Grid control header pane
+ * TODO: add comments
+ */
+ClientUI.controls.grid.GridHeader = Class.create({
+	CLASSDEF: {
+		name: 'ClientUI.controls.grid.GridHeader',
+		parent: ClientUI.common.box.Box,
+		sepStyleClass: "ClientUI_Grid_HSep"
+	}
+	
+});
+
+Object.extend(ClientUI.controls.grid.GridHeader.prototype, {
+	// internal variables
+	_columns: [],
+	
+	// constructor
+	initialize: function(template, grid) {
+		this.grid = grid;
+		ClientUI.controls.grid.GridHeader.parentClass.constructor().call(this);
+		this.element.id = this.grid.getElement().id + "_Header";
+
+		// register event handlers
+		this.eventSepDblClick = this.OnSepDblClick.bindAsEventListener(this);
+		this.eventSepMouseDown = this.OnSepMouseDown.bindAsEventListener(this);
+		this.eventSepMouseUp = this.OnSepMouseUp.bindAsEventListener(this);
+		this.eventSepMouseMove = this.OnSepMouseMove.bindAsEventListener(this);
+		this.eventCellMouseDown = this.OnCellMouseDown.bindAsEventListener(this);
+		Event.observe(document, 'mousemove', this.eventSepMouseMove, true);
+		Event.observe(document, 'mouseup', this.eventSepMouseUp, true);
+
+		this.createControl(template);
+	},
+	
+	// create grid header control
+	createControl: function(template) {
+				
+		// validate template
+		var ch = $(template).firstChild;
+		while(ch) {
+			if(ch.tagName && ch.tagName.toLowerCase()=="table") {
+				this.template = $(ch);
+				break;
+			}
+			ch = ch.nextSibling;
+		}
+
+		template.parentNode.removeChild(template);
+		document.body.appendChild(template);
+
+		var errMsg = "";
+		if(!this.template) {
+			errMsg = "Invalid template specified. GridHeader requires template specified over table element.";
+			ClientUILib.log(ClientUILogger.ERROR, errMsg);
+			throw(errMsg);
+		}
+		if(!this.parseTemplate(this.template)) {
+			errMsg = "Unable to parse template. GridHeader requires template specified over table element with one row.";
+			ClientUILib.log(ClientUILogger.ERROR, errMsg);
+			throw(errMsg);
+		}
+		
+		this.contentBox = new ClientUI.common.box.Box(null, this.getElement());
+		this.contentBox.makeAbsolute();
+		this.frozenContentBox = new ClientUI.common.box.Box(null, this.getElement());
+		this.frozenContentBox.makeAbsolute();
+		
+		// generate DOM objects tree
+		this.generateDOMTree();
+		
+		// Remove template
+		document.body.removeChild(template);
+		
+		// Set dimensions
+		this.setHeight(this.defaultHeight);
+		this.setWidth(this.defaultWidth);
+		this.controlCreated = true;
+		this.updateLayout();
+	},
+	parseTemplate: function(template) {
+		if(!template || !template.rows || template.rows.length===0) {
+			return false;
+		}
+
+		var columns = [];
+		var defaultWidth = 0;
+		
+		var i = 0;
+		if(this.grid.options.showIndexColumn) {
+			columns[i++] = {
+				width: this.grid.options.indexColumnWidth,
+				innerHTML: "&nbsp;",
+				styleClass: "",
+				id: "",
+				align: "",
+				valign: "",
+				title: "",
+				minWidth: this.grid.options.indexColumnWidth,
+				frozen: true,
+				fixedWidth: true,
+				sortable: false,
+				sorted: "asc"
+			};
+		}
+		
+		// Get columns information
+		var cells = $A(template.rows[0].cells);		
+		cells.each(function(cell) {
+			columns[i++] = {
+				width: Element.getWidth(cell),
+				innerHTML: cell.innerHTML,
+				styleClass: cell.className,
+				id: cell.id,
+				align: cell.align,
+				valign: cell.vAlign,
+				title: cell.title,
+				minWidth: 10,
+				frozen: Validators.getBoolean(cell.getAttribute("frozen"), false),
+				fixedWidth: Validators.getBoolean(cell.getAttribute("fixedWidth"), false),
+				sortable: Validators.getBoolean(cell.getAttribute("sortable"), true),
+				sorted: Validators.getBoolean(cell.getAttribute("sorted"), "desc")
+			};
+			
+			defaultWidth += Element.getWidth(cell);
+			Element.addClassName(cell, "ClientUI_Grid_HCBody");
+		});
+		
+		this._columns = columns;
+		this.defaultHeight = Element.getHeight(template.rows[0].cells[0]);
+		this.defaultWidth = defaultWidth;
+		if(ClientUILib.isGecko) {
+			this.defaultWidth -= this.getBorderWidth("lr") + this.getPadding("lr");
+			this.defaultHeight -= this.getBorderWidth("tb") + this.getPadding("tb");
+		}
+		
+		return true;
+	},
+	generateDOMTree: function() {
+		var headerRow = new ClientUI.common.box.InlineBox(null, this.contentBox.getElement());
+  		headerRow.getElement().addClassName("ClientUI_Grid_HR");
+      	headerRow.getElement().id = this.getElement().id + "HR";
+		headerRow.setWidth(this.defaultWidth + 10000);
+		headerRow.setHeight(this.defaultHeight);
+   
+		var headerFrozenRow  = new ClientUI.common.box.InlineBox(headerRow.getElement().cloneNode(true));
+		headerFrozenRow.setParent(this.frozenContentBox.getElement());
+		headerFrozenRow.getElement().setAttribute("id", this.getElement().id + "FRFroz");
+   
+		var eventSepDblClick = this.eventSepDblClick.bind(this);
+		var eventSepMouseDown = this.eventSepMouseDown.bind(this);
+		var eventCellMouseDown = this.eventCellMouseDown.bind(this);
+		 
+		// create all columns
+		var columnZIndex = this._columns.length*3 + 10;
+		var lastFrozenZIndex = columnZIndex;
+		var frozenContentBox = this.frozenContentBox;
+		var defaultWidth = 0;
+		var defaultWidthFrozen = 0;
+		var index = 0;
+		var height = headerRow.getViewportHeight();
+		this._columns.each(function(columnDesc) {
+			var width = !Validators.isEmpty(columnDesc.width) ? columnDesc.width : 100;
+			
+			var parent = columnDesc.frozen ? headerFrozenRow.getElement() : headerRow.getElement();
+			var col = new ClientUI.common.box.InlineBox(null, parent);
+			col.collindex = index;
+			col.getElement().addClassName("ClientUI_Grid_HC");
+			col.getElement().columnIndex = index;
+			col.getElement().setStyle({zIndex: columnZIndex-1});			
+			if(!Validators.isEmpty(columnDesc.id)) {
+				col.getElement().id = columnDesc.id;	
+			}
+			if(!Validators.isEmpty(columnDesc.title)) {
+				col.getElement().title = columnDesc.title;	
+			}
+			if(!Validators.isEmpty(columnDesc.valign)) {
+				col.getElement().setStyle({verticalAlign: columnDesc.valign});
+			}
+	
+			col.setWidth(width);
+			col.setHeight(height);
+			col.makeAbsolute();
+			columnDesc.object = col;
+			
+			if(columnDesc.sortable) {
+				Event.observe(col.getElement(), 'click',  eventCellMouseDown);
+			}
+			
+			if(!columnDesc.frozen) {
+				col.moveToX(defaultWidth);
+			}
+			else {
+				col.moveToX(defaultWidthFrozen);
+			}
+			
+			// header cell body			
+			var colBody = new ClientUI.common.box.InlineBox(null, col.getElement());
+			columnDesc.body = colBody;			
+			if(!Validators.isEmpty(columnDesc.styleClass)) {
+				colBody.getElement().addClassName(columnDesc.styleClass);	
+			}
+			colBody.getElement().addClassName("ClientUI_Grid_HCBody");
+			
+			// glue cell content
+			var sb = new StringBuilder('<table width="100%" ');
+			if(!Validators.isEmpty(columnDesc.align)) {
+				sb.append('align="').append(columnDesc.align).append('" ');
+			}		
+			sb.append('cellspacing="0" cellpadding="0" border="0"><tbody><tr><td width="100%"><span style="width:100%">');
+			if(!Validators.isEmpty(columnDesc.innerHTML)) {
+				sb.append(columnDesc.innerHTML);	
+			}
+			sb.append('</span></td><td><span class="sort-desc"></span><span class="sort-asc"></span></td></tr></tbody></table>');
+			colBody.getElement().update(sb.toString());
+			colBody.moveTo(0, 0);
+			var bodyWidth = width;
+			var bodyHeight = height;
+			if(ClientUILib.isGecko) {
+				bodyWidth -= colBody.getBorderWidth("lr") + colBody.getPadding("lr");
+				bodyHeight -= colBody.getBorderWidth("tb") + colBody.getPadding("tb");
+			}
+			colBody.setWidth(bodyWidth);
+			colBody.setHeight(bodyHeight);
+			columnDesc.sortAsc = document.getElementsByClassName("sort-asc", colBody.getElement())[0];
+			columnDesc.sortDesc = document.getElementsByClassName("sort-desc", colBody.getElement())[0];
+
+			// create separator between header's cells
+			var sep = new ClientUI.common.box.InlineBox(null, parent);
+			sep.getElement().addClassName("ClientUI_Grid_HSep");
+			sep.makeAbsolute();			
+			if(!columnDesc.frozen) {
+				sep.moveToX(defaultWidth + width - sep.getWidth()/2 - 1);
+			}
+			else {
+				sep.moveToX(defaultWidthFrozen + width - sep.getWidth()/2 - 1);
+				lastFrozenZIndex = columnZIndex;
+			}
+			sep.getElement().columnIndex = index;
+			sep.getElement().setStyle({zIndex: columnZIndex});										
+			columnDesc.sep = sep;
+			
+			if(!columnDesc.fixedWidth) {
+				Event.observe(sep.getElement(), 'dblclick',  eventSepDblClick);
+				Event.observe(sep.getElement(), 'mousedown', eventSepMouseDown);
+			}
+			else {
+				sep.getElement().setStyle({cursor: 'auto'});
+			}
+			
+			columnZIndex -= 3;
+			if(!columnDesc.frozen) {
+				defaultWidth += width;
+			}
+			else {
+				defaultWidthFrozen += width;
+			}
+			index++;
+		});
+		
+		this.headerRow = headerRow;
+		this.headerFrozenRow = headerFrozenRow;
+		this.frozenSubstrate = new ClientUI.common.box.Substrate(null, headerFrozenRow.getElement());
+		this.frozenSubstrate.getElement().name = this.getElement().id + "HRFrm";
+		this.frozenSubstrate.setStyle({zIndex: lastFrozenZIndex-2});
+		this.frozenSubstrate.setHeight(headerRow.getViewportHeight());
+	},
+	getRow: function() {
+		if(!this.footerRow) {		
+			var rowDesc = {
+				id: "header",
+				frozen: this.headerFrozenRow,
+				normal: this.headerRow,
+				moveToY: function(y) {
+					this.frozen.moveToY(y);
+					this.normal.moveToY(y);
+				}
+			};
+			
+			this.footerRow = rowDesc;
+		}
+		
+		return this.footerRow;
+	},	
+	updateLayout: function() {
+		if(!this.controlCreated || !this.grid.controlCreated) {
+			return;
+		}
+		ClientUI.controls.grid.GridHeader.parentClass.method("updateLayout").call(this);
+		
+		var height = this.getViewportHeight();
+		var totalWidth = this.grid.getColumnsTotalWidth();
+		var frozenContentWidth = this.grid.getColumnsFrozenWidth();
+
+		this.contentBox.setWidth(Math.max(this.getWidth(), totalWidth));
+		this.contentBox.setHeight(height);
+		this.contentBox.moveTo(frozenContentWidth, 0);
+		this.frozenContentBox.setWidth(frozenContentWidth);
+		this.frozenContentBox.setHeight(height);
+		this.frozenContentBox.moveTo(0, 0);
+		this.frozenSubstrate.setWidth(frozenContentWidth);
+		var row = this.getRow();
+		row.frozen.setWidth(frozenContentWidth);
+		row.normal.setWidth(Math.max(this.getWidth(), totalWidth));		
+	},
+	getColumns: function() {
+		return this._columns;
+	},
+	// lets implement column resizer
+	OnSepMouseDown: function(event) {
+		this.dragColumnInfo = {
+			srcElement: Event.element(event),
+			dragStarted: false,
+			mouseDown: true,
+			startX: Event.pointerX(event),
+			originalX: 0
+		};		
+		this.dragColumnInfo.object = this.getColumns()[this.dragColumnInfo.srcElement.columnIndex].object;
+		this.dragColumnInfo.sep = this.getColumns()[this.dragColumnInfo.srcElement.columnIndex].sep;
+		this.dragColumnInfo.minWidth = this.getColumns()[this.dragColumnInfo.srcElement.columnIndex].minWidth;
+		
+		Event.stop(event);
+	},
+	OnSepMouseUp: function(event) {
+		if(this.dragColumnInfo && this.dragColumnInfo.dragStarted) {
+			this.dragColumnInfo.dragStarted = false;
+			this.dragColumnInfo.mouseDown = false;
+			var delta = Event.pointerX(event) - this.dragColumnInfo.startX;
+			var newWidth = this.dragColumnInfo.object.getWidth() + delta;
+			setTimeout(function() {
+				this.grid.adjustColumnWidth(this.dragColumnInfo.srcElement.columnIndex, newWidth); 
+			}.bind(this), 10);
+		}
+		this._hideSplitter();
+	},
+	OnSepMouseMove: function(event) {
+		if(this.dragColumnInfo && this.dragColumnInfo.mouseDown) {
+			if(!this.dragColumnInfo.dragStarted) {
+				this.dragColumnInfo.dragStarted = true;
+				this._showSplitter(this.dragColumnInfo.srcElement.columnIndex);
+			}
+			else {
+				var delta = Event.pointerX(event) - this.dragColumnInfo.startX;
+				var minColumnWidth = this.dragColumnInfo.object.getWidth() - this.dragColumnInfo.minWidth;
+				if(delta >= -minColumnWidth) {
+					var x = this.dragColumnInfo.originalX + delta;
+					this.columnSplitter.moveToX(x - 6);
+				}
+			}
+			Event.stop(event);
+		}
+	},
+	OnSepDblClick: function(event) {
+		ClientUILib.log(ClientUILogger.INFO, "OnSepDblClick");
+	},
+	_showSplitter: function(index) {
+		if(!this.columnSplitter) {
+			this._createSplitter();
+		}
+
+		var pos = this.dragColumnInfo.sep.getX();
+		if(!this.getColumns()[index].frozen) {
+			pos += this.grid.getColumnsFrozenWidth() - this.grid.getColumnScrollX(index);
+		}
+		this.dragColumnInfo.originalX = pos;
+		this.columnSplitter.show();
+		this.columnSplitter.setHeight(this.defaultHeight + this.grid.getBody().contentBox.getHeight());
+		this.columnSplitter.moveTo(pos, 0);
+	},
+	_hideSplitter: function() {
+		if(this.columnSplitter) {
+			this.columnSplitter.hide();
+		}
+	},
+	_createSplitter: function() {
+		this.columnSplitter = new ClientUI.common.box.Box(null, this.grid.getElement());
+		this.columnSplitter.makeAbsolute();
+		this.columnSplitter.getElement().addClassName("ClientUI_Grid_HSplit");
+		this.columnSplitter.setWidth(10);
+		this.columnSplitter.getElement().setStyle({backgroundColor: ''});
+		this.columnSplitter.getElement().setStyle({zIndex: '100'});
+		this.columnSplitter.hide();
+	},
+	adjustScrollPosition: function(pos) {
+		this.contentBox.moveToX(this.grid.getColumnsFrozenWidth()-pos);
+	},
+	OnCellMouseDown: function(event) {
+		var el = Event.element(event);
+		while(el && !Element.hasClassName(el, "ClientUI_Grid_HC")) {
+			el = el.parentNode;
+		}
+		
+		if(el && el.columnIndex>=0) {
+			var order = this.getColumns()[el.columnIndex].sorted;
+			order = (order == "asc") ? "desc" : "asc";
+			this.getColumns()[el.columnIndex].sorted = order;
+			
+	        for(var i = 0, len = this.getColumns().length; i < len; i++) {
+	            var h = this.getColumns()[i];
+	            if(i != el.columnIndex) {
+					Element.setStyle(h.sortDesc, {display: 'none'});
+					Element.setStyle(h.sortAsc, {display: 'none'});
+	            } else{
+					Element.setStyle(h.sortDesc, {display: (order == 'desc' ? 'block' : 'none')});
+					Element.setStyle(h.sortAsc, {display: (order == 'asc' ? 'block' : 'none')});
+	            }
+	        }
+
+			this.grid.eventOnSort.fire(el.columnIndex, order);
+			Event.stop(event);
+		}		
+	}
+});

Added: trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/layouts/GridLayoutManager.js
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/layouts/GridLayoutManager.js	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/layouts/GridLayoutManager.js	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,75 @@
+/*
+ * TODO: Copyright (c) 2007 Denis Morozov <dmorozov at exadel.com>
+ *
+ * ...
+ */
+ClientUILib.declarePackage("ClientUI.layouts.GridLayoutManager");
+
+ClientUILib.requireClass("ClientUI.common.box.Box");
+ClientUILib.requireClass("ClientUI.layouts.VLayoutManager");
+
+var GridLayout_Enum = {
+	HEADER: 	1,
+	BODY: 		2,
+	FOOTER: 	3
+};
+
+/*
+ * LayoutManager.js - Base class for all layout managers
+ * by Denis Morozov <dmorozov at exadel.com> distributed under the BSD license. 
+ *
+ * TODO: description of control 
+ */
+ClientUI.layouts.GridLayoutManager = Class.create({
+	CLASSDEF: {
+		name: 'ClientUI.layouts.GridLayoutManager',
+		parent: ClientUI.layouts.VLayoutManager
+	}
+	
+});
+
+Object.extend(ClientUI.layouts.GridLayoutManager.prototype, {
+	initialize: function(element, parentElement, config) {
+		ClientUI.layouts.GridLayoutManager.parentClass.constructor().call(this, element, parentElement);		
+	},
+	updateLayout: function() {
+		ClientUI.layouts.LayoutManager.method("updateLayout").call(this);
+		
+		var parentBox = this.getContainer();
+		var height = parentBox.getViewportHeight();
+		var width = parentBox.getViewportWidth();
+		
+		// NOTE: not implemented in this class
+	  	if(this.panels) {
+	  		var headerHeight = 0;
+	  		var footerHeight = 0;
+			var bodyBottom = 0;
+	  		
+	  		// first get size of header
+	  		if(this.panels[GridLayout_Enum.HEADER]) {
+	  			headerHeight = this.panels[GridLayout_Enum.HEADER].getHeight();
+	  			this.panels[GridLayout_Enum.HEADER].moveTo(0, 0);
+				this.panels[GridLayout_Enum.HEADER].setWidth(width);
+	  			this.panels[GridLayout_Enum.HEADER].updateLayout();
+	  		}	  		
+	  		// than calculate size of body
+	  		if(this.panels[GridLayout_Enum.BODY]) {
+				var body = this.panels[GridLayout_Enum.BODY];
+	  			body.setWidth(width);
+	  			var bodyHeight = height - headerHeight;
+	  			body.setHeight(bodyHeight);
+	  			body.moveTo(0, headerHeight);
+	  			body.updateLayout();
+				bodyBottom = body.getY() + body.contentBox.getY() + body.scrollBox.getViewportHeight();
+	  		}
+			
+	  		if(this.panels[GridLayout_Enum.FOOTER]) {
+	  			footerHeight = this.panels[GridLayout_Enum.FOOTER].getHeight();
+	  			this.panels[GridLayout_Enum.FOOTER].moveTo(0, bodyBottom - footerHeight);
+				this.panels[GridLayout_Enum.FOOTER].setWidth(width);
+	  			this.panels[GridLayout_Enum.FOOTER].updateLayout();
+	  		}
+			
+	  	}
+	}	
+});
\ No newline at end of file

Added: trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/layouts/LayoutManager.js
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/layouts/LayoutManager.js	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/layouts/LayoutManager.js	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,71 @@
+/*
+ * TODO: Copyright (c) 2007 Denis Morozov <dmorozov at exadel.com>
+ *
+ * ...
+ */
+ClientUILib.declarePackage("ClientUI.layouts.LayoutManager");
+
+/*
+/* LayoutManager.js - Base class for all layout managers
+ * by Denis Morozov <dmorozov at exadel.com> distributed under the BSD license. 
+ *
+ * TODO: description of control 
+ */
+ClientUI.layouts.LayoutManager = Class.create({
+	CLASSDEF: {
+		name: 'ClientUI.layouts.LayoutManager',
+		parent: ClientUI.common.box.Box
+	}
+
+});
+
+Object.extend(ClientUI.layouts.LayoutManager.prototype, {	
+	initialize: function(element, parentElement) {
+		ClientUI.layouts.LayoutManager.parentClass.constructor().call(this, element, parentElement);
+		
+		// store container element to look after
+		this.container = parentElement;
+		if(this.container)
+			this.container = new ClientUI.common.box.Box($(this.container));
+
+		// declare event listeners
+		this.eventContainerResize = this.containerResize.bindAsEventListener(this);
+		
+		this.registerEvents();
+	},
+	registerEvents: function() {
+		if(this.container)
+			Event.observe(window, "resize", this.eventContainerResize);
+	},
+	destroy: function() {
+		if(this.container)
+			Event.stopObserving(window, "resize", this.eventContainerResize);
+	},
+   	containerResize: function(event) {
+	  	//Event.stop(event);
+	  	// TODO: 
+	  	this.updateLayout();
+	},
+	updateLayout: function() {
+		if(this.container) {
+			this.setWidth(this.container.getWidth());
+			this.setHeight(this.container.getHeight());
+		}
+		ClientUI.layouts.LayoutManager.parentClass.method("updateLayout").call(this);		
+	},
+	getContainer: function() {
+		return this.container;
+	}
+})
+
+/*
+ Vertically and horizontally centering a div that follow scrolling:
+ 
+	if(navigator.appName.search(/opera/gi) != -1) { // Using Opera
+	    _element.style.top = Math.round(((document.body.clientHeight-document.documentElement.offsetHeight)/2)-(_element.style.height/2)+document.documentElement.scrollTop)+'px';
+	}
+	else { // Using IE / Firefox / Other
+		_element.style.left = Math.round((document.documentElement.clientWidth/2)-(_element.style.width/2))+"px";
+		_element.style.top = Math.round((document.documentElement.clientHeight/2)-(_element.style.height/2)+document.documentElement.scrollTop)+"px";
+	}
+ */
\ No newline at end of file

Added: trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/layouts/VLayoutManager.js
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/layouts/VLayoutManager.js	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/javascript/ClientUI/layouts/VLayoutManager.js	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,98 @@
+/*
+ * TODO: Copyright (c) 2007 Denis Morozov <dmorozov at exadel.com>
+ *
+ * ...
+ */
+ClientUILib.declarePackage("ClientUI.layouts.VLayoutManager");
+
+ClientUILib.requireClass("ClientUI.common.box.Box");
+ClientUILib.requireClass("ClientUI.layouts.LayoutManager");
+
+var GridLayout_Enum = {
+	HEADER: 	1,
+	BODY: 		2,
+	FOOTER: 	3
+};
+
+/*
+ * LayoutManager.js - Base class for all layout managers
+ * by Denis Morozov <dmorozov at exadel.com> distributed under the BSD license. 
+ *
+ * TODO: description of control 
+ */
+ClientUI.layouts.VLayoutManager = Class.create({
+	CLASSDEF: {
+		name: 'ClientUI.layouts.VLayoutManager',
+		parent: ClientUI.layouts.LayoutManager
+	}
+
+});
+
+Object.extend(ClientUI.layouts.VLayoutManager.prototype, {	
+	initialize: function(element, parentElement, config) {
+		ClientUI.layouts.VLayoutManager.parentClass.constructor().call(this, element, parentElement);		
+		if(!element || !element.id) {
+			this.element.id = "ClientUI_VLayoutManager" + ClientUI_layouts_VLayoutManager_idGenerator++;
+		}
+
+		this.registerEvents();
+	},
+	registerEvents: function() {
+		ClientUI.layouts.VLayoutManager.parentClass.method("registerEvents").call(this);
+	},
+	destroy: function() {
+		ClientUI.layouts.VLayoutManager.parentClass.method("destroy").call(this);
+	},
+   	containerResize: function(event) {
+   		ClientUI.layouts.VLayoutManager.parentClass.method("containerResize").call(this, event);
+   		this.updateLayout();
+	},
+	addPane: function(align, pane) {
+		if(!this.panels) { this.panels = []; }
+
+		this.panels[align] = pane;
+		this.panels[align].makeAbsolute();
+		this.panels[align].setParent(this);
+	},
+	getPane: function(align) {
+		return this.panels[align];
+	},
+	updateLayout: function() {
+		ClientUI.layouts.VLayoutManager.parentClass.method("updateLayout").call(this);
+		
+		var parentBox = this.getContainer();
+		var height = parentBox.getViewportHeight();
+		var width = parentBox.getViewportWidth();
+		
+		// NOTE: not implemented in this class
+	  	if(this.panels) {
+	  		var headerHeight = 0;
+	  		var footerHeight = 0;
+	  		
+	  		// first get size of header
+	  		if(this.panels[GridLayout_Enum.HEADER]) {
+	  			headerHeight = this.panels[GridLayout_Enum.HEADER].getHeight();
+	  			this.panels[GridLayout_Enum.HEADER].moveTo(0, 0);
+				this.panels[GridLayout_Enum.HEADER].setWidth(width);
+	  			this.panels[GridLayout_Enum.HEADER].updateLayout();
+	  		}
+	  		if(this.panels[GridLayout_Enum.FOOTER]) {
+	  			footerHeight = this.panels[GridLayout_Enum.FOOTER].getHeight();
+	  			this.panels[GridLayout_Enum.FOOTER].moveTo(0, height - footerHeight);
+				this.panels[GridLayout_Enum.FOOTER].setWidth(width);
+	  			this.panels[GridLayout_Enum.FOOTER].updateLayout();
+	  		}
+	  		// than calculate size of body
+	  		if(this.panels[GridLayout_Enum.BODY]) {
+				var body = this.panels[GridLayout_Enum.BODY];
+	  			body.setWidth(width);
+	  			var bodyHeight = height - (headerHeight + footerHeight);
+	  			body.setHeight(bodyHeight);
+	  			body.moveTo(0, headerHeight);
+	  			body.updateLayout();
+	  		}			
+	  	}
+	}	
+});
+
+var ClientUI_layouts_VLayoutManager_idGenerator = 0;
\ No newline at end of file

Added: trunk/sandbox/scrollable-grid/src/main/javascript/ClientUILib.js
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/javascript/ClientUILib.js	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/javascript/ClientUILib.js	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,209 @@
+// ClientUILib base.js v1.0.0, Fri Jan 19 19:16:36 CET 2007
+
+// TODO: Copyright (c) 2007, Denis Morozov (dmorozov at exadel.com)
+// ...
+
+var ClientUILib = {
+	Version: '1.0.0',
+	Name: 'ClientUILib',
+	LibraryPath: './',	
+	packages: [],
+	load: function(showLog) {
+	  // Check for Prototype JavaScript framework
+	  if((typeof Prototype=='undefined') || 
+	     (typeof Element == 'undefined') || 
+	     (typeof Element.Methods=='undefined') ||
+	     parseFloat(Prototype.Version.split(".")[0] + "." +
+	                Prototype.Version.split(".")[1]) < 1.5)
+	     throw("ClientUILib requires the Prototype JavaScript framework >= 1.5.0");
+	  	
+	  // Check for Extend JavaScript library
+	  if((typeof Extend=='undefined') ||
+	  	Extend.VERSION < 1.1)
+	     throw("ClientUILib requires the Extend JavaScript library >= 1.1");
+
+	  $A(document.getElementsByTagName("script")).findAll( function(s) {
+	    return (s.src && s.src.match(/ClientUILib\.js(\?.*)?$/))
+	  }).each( function(s) {
+	    LibraryPath = s.src.replace(/ClientUILib\.js(\?.*)?$/,'');
+	  });
+	  
+	  if(showLog) {
+		  ClientUILogger.create("ClientUILogger");
+		  this.startTime = (new Date()).getTime();
+	  }
+	  
+	  this.initBrowser();
+	},
+ 	include: function(libraryPackageName) {
+		if(!this.packages)
+			this.packages=[];
+		if(!this.packages[libraryPackageName]) {
+			this.packages[libraryPackageName] = true;
+			var re = /\./g; // Replace all '.' in package name
+			var packagePath = LibraryPath + libraryPackageName.replace(re, "/");
+			document.write('<script type="text/javascript" src="' + packagePath + '.js"></script>');
+		}
+	},
+	include2: function(libraryPackageName) {
+		if(!this.packages)
+			this.packages=[];
+		if(!this.packages[libraryPackageName]) {
+			this.packages[libraryPackageName] = true;
+			var re = /\./g; // Replace all '.' in package name
+			var packagePath = LibraryPath + libraryPackageName.replace(re, "/");
+			var e = document.createElement("script");
+		   	e.src = packagePath+".js";
+		   	e.type="text/javascript";
+		   	document.getElementsByTagName("head")[0].appendChild(e);		
+		}
+	},
+	requireClass: function(libName) {
+		// required class not included before
+		if(!this.packages[libName]) {
+			//this.include2(libName);
+			ClientUILib.log(ClientUILogger.ERROR, "Library '" + libName + "' required!!!");
+			throw("Package '" + libName + "' is required!");
+		}
+	},
+	declarePackage: function(libName) {
+		var pckg = null;
+		var packages = $A(libName.split("."));
+		packages.each( function(s) {
+			if(pckg == null)
+				pckg = eval(s);
+			else {
+				if(!pckg[s]) pckg[s] = {};
+				pckg = pckg[s];
+			}
+	  	});
+	  	this.packages[libName] = true;
+	  	ClientUILib.log(ClientUILogger.INFO, "ClientUILib::declarePackage '" + libName + "'");
+	},
+	log: function(level, infoText) {
+		if(ClientUILogger.isCreated)
+			ClientUILogger.log(level, infoText);
+	},
+	initBrowser: function() {
+		var ua = navigator.userAgent.toLowerCase();
+		/** @type Boolean */
+		this.isOpera = (ua.indexOf('opera') > -1);
+	   	/** @type Boolean */
+		this.isSafari = (ua.indexOf('webkit') > -1);
+	   	/** @type Boolean */
+		this.isIE = (window.ActiveXObject);
+	   	/** @type Boolean */
+		this.isIE7 = (ua.indexOf('msie 7') > -1);
+	   	/** @type Boolean */
+		this.isGecko = !this.isSafari && (ua.indexOf('gecko') > -1);
+		
+		if(ua.indexOf("windows") != -1 || ua.indexOf("win32") != -1){
+		    /** @type Boolean */
+		    this.isWindows = true;
+		}else if(ua.indexOf("macintosh") != -1){
+			/** @type Boolean */
+		    this.isMac = true;
+		}
+		if(this.isIE && !this.isIE7){
+	        try{
+	            document.execCommand("BackgroundImageCache", false, true);
+	        }catch(e){}
+	    }
+	}	
+};
+
+var ClientUILogger = {
+	// log level
+	INFO: 		1,
+	WARNING: 	2,
+	ERROR: 		3,
+	
+	// flag logger is initialized
+	isCreated: false,
+	width: 500,
+	height: 150,
+	create: function() {
+		this.logElement = $(document.createElement("div"));
+		this.logElement.setStyle({position: 'absolute'});
+		this.logElement.setStyle({overflow: 'auto'});
+		this.logElement.setStyle({whiteSpace: 'nowrap'});
+		
+		Event.observe(window, 'load', ClientUILogger.init);
+		Event.observe(window, 'resize', ClientUILogger.resizeWindow);
+		this.isCreated = true;
+	},
+	init: function() {
+		if(ClientUILogger.logElement)
+			document.body.appendChild(ClientUILogger.logElement);
+		ClientUILogger.show();
+	},
+	resizeWindow: function() {
+		ClientUILogger.show();
+	},
+	show: function() {
+		if(this.logElement) {
+			Element.show(this.logElement);
+			this.logElement.setStyle({width: this.width + 'px'});
+			this.logElement.setStyle({height: this.height + 'px'});
+			this.logElement.setStyle({top: (this.getWindowHeight() - this.height - 10) + 'px'});
+			this.logElement.setStyle({left: (this.getWindowWidth() - this.width - 10) + 'px'});
+		}
+	},
+	log: function(level, infoText) {
+		var msg = $(document.createElement("div"));
+		this.logElement.appendChild(msg);
+		msg.setStyle({width: '100%'});
+		
+		var font = "bold normal bold 10pt Arial";
+		var fontColor = "red";
+		
+		switch(level) {
+			case ClientUILogger.INFO: 
+				fontColor = "black";
+				font = "normal normal normal 10pt Arial";
+				break;
+			case ClientUILogger.WARNING: 
+				fontColor = "blue";
+				font = "italic normal normal 10pt Arial";
+				break;
+			case ClientUILogger.ERROR: 
+				fontColor = "red";
+				font = "normal normal bold 10pt Arial";
+				break;
+			default:
+				infoText = "UNRESOLVED: level=" + level + ", msg=" + infoText;
+		}
+		msg.setStyle({font: font});
+		msg.setStyle({color: fontColor});
+		msg.appendChild(document.createTextNode("> " + infoText));
+		
+		this.logElement.scrollTop = this.logElement.scrollHeight;
+	},
+	getWindowWidth: function(){
+	    var innerWidth;
+		  if (navigator.appVersion.indexOf('MSIE')>0) {
+			  innerWidth = document.body.clientWidth;
+	    } else {
+			  innerWidth = window.innerWidth;
+	    }
+	    return innerWidth;	
+	},
+	getWindowHeight: function(){
+	    var innerHeight;
+		  if (navigator.appVersion.indexOf('MSIE')>0) {
+			  innerHeight = document.body.clientHeight;
+	    } else {
+			  innerHeight = window.innerHeight;
+	    }
+	    return innerHeight;	
+	}	
+};
+
+ClientUILib.load(true);
+
+// declare predefined packages
+var ClientUI = {
+	controls: {},
+	layouts: {}
+};
+

Added: trunk/sandbox/scrollable-grid/src/main/javascript/common/prototype/ext/extend.js
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/javascript/common/prototype/ext/extend.js	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/javascript/common/prototype/ext/extend.js	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,186 @@
+// vim: tw=80 ts=4 sw=4 noet
+// ----------------------------------------------------------------------------
+// Project   : Extend - Prototype OOP extension
+// URL       : <http://www.ivy.fr/js/extend>
+// ----------------------------------------------------------------------------
+// Author    : Sebastien Pierre                              <sebastien at ivy.fr>
+// License   : Revised BSD License
+// ----------------------------------------------------------------------------
+// Creation  : 08-Sep-2006
+// Last mod  : 17-Nov-2006
+// ----------------------------------------------------------------------------
+
+// The Extend object holds all the information required to implement the
+// inheritance and other OO-goodness.
+Extend = {
+	VERSION:           1.1,
+	CLASSDEF:          "CLASSDEF",
+	DELETE:            "DELETE",
+	// These are a list of methods of class instances that are reserved by the
+	// OO layer (see the reparent method for more info)
+	INSTANCE_RESERVED: {
+		CLASSDEF:    true,
+		getClass:    true,
+		parentClass: true
+	},
+
+	// Sets up a class
+	setupClass: function( _class, declaration )
+	{
+		// We create an empty prototype if the user did not provide one
+		declaration        = declaration || {}
+		_class.prototype   = declaration
+		// We clone the given method definition, because they will be augmented
+		// with the ones defined in the parent class
+		_class.methods     = {} 
+		for ( var key in declaration ) { _class.methods[key] = declaration[key] }
+		_class.inherited   = {}
+		_class.parentClass = undefined
+		if ( declaration[Extend.CLASSDEF] )
+		{ _class.className = declaration[Extend.CLASSDEF].name }
+		else
+		{ _class.className = undefined }
+		_class.subclasses  = _class.subclasses || []
+		_class.constructor = Extend.Operations.constructor
+		_class.reparent    = Extend.Operations.reparent
+		_class.method      = Extend.Operations.method
+		_class.update      = Extend.Operations.update
+		if ( declaration[Extend.CLASSDEF] )
+		{ _class.reparent(declaration[Extend.CLASSDEF].parent) }
+		// We update the class methods with an `ofClass` method that returns the
+		// class, so that instances will have a proper
+		declaration.getClass    = function() {return _class}
+		declaration.parentClass = function() {return this.getClass().parentClass}
+		declaration.parentCall  = function() {
+			var new_args = []
+			for ( var i=1;i<arguments.length;i++ ) {new_args.push(arguments[i])}
+			return this.parentClass().method(arguments[0]).apply(this, new_args)
+		}
+		declaration.setClass    = function(newClass) {
+			return this.getClass().parentClass
+		}
+		// We reparent the subclasses if any
+		for ( var i=0 ; i<_class.subclasses ; i++ ) {
+			_class.subclasses[i].reparent(_class)
+		}
+		return _class
+	},
+	// These are operations that will be "mixed-in" with the new classes
+	Operations: {
+		constructor: function() {
+			return this.prototype.initialize || function() {}
+		},
+		// Reparents this class
+		reparent: function( newParentClass )
+		{
+			if ( this.parentClass )
+			{
+				var this_index = this.subclasses.indexOf(this)
+				this.parentClass.subclasses.splice(this_index, 1)
+				for ( var method_name in this.inherited ) {
+					this.method(method_name, null, this.parentClass)
+				}
+			}
+			this.parentClass   = newParentClass
+			if ( !newParentClass ) return
+			var parent_methods = newParentClass.prototype
+			// We iterate on all the parent methods
+			for ( parent_method_name in parent_methods ) {
+				// If the method is a reserved one, we skip it
+				if ( Extend.INSTANCE_RESERVED[parent_method_name] == true ) { continue }
+				// If the method is not directly defined in the current class, we add it
+				if ( this.methods[parent_method_name] == undefined )
+				{
+					this.method( parent_method_name,
+						parent_methods[parent_method_name],
+						newParentClass.inherited[parent_method_name] || newParentClass
+					)
+				}
+			}
+			newParentClass.subclasses.push(this)
+		},
+		update: function(newPrototype) {
+			Extend.setupClass(this, newPrototype||this.prototype)
+		},
+		// Returns, sets or deletes a method in this class
+		method: function( name, body, declaredIn ) {
+			if ( body == undefined )
+			{
+				var method = this.prototype[name]
+				if ( name == undefined ) throw new Error("Method not found: "+name)
+				return method
+			}
+			else
+			{
+				declaredIn = declaredIn || this
+				// If the method is declared in this class
+				if ( declaredIn == this )
+				{
+					if ( body == Extend.DELETE ) {
+						delete this.methods[name]
+						delete this.inherited[name]
+						delete this.prototype[name]
+						// If the method is defined in the parent we set it
+						if ( this.parentClass ) {
+							var parent_method = this.parentClass.method(name)
+							if ( parent_method ) {
+								this.method(name, parent_method, this.parentClass.inherited[name] || this.parentClass)
+							}
+						}
+					} else {
+						this.methods[name]   = body
+						this.prototype[name] = body
+						delete this.inherited[name]
+					}
+				}
+				// Or if its declared in another class
+				else
+				{
+					if ( body == Extend.DELETE ) {
+						delete this.inherited[name]
+						delete this.methods[name]
+						delete this.prototype[name]
+						// If the method is defined in the parent we set it
+						if ( this.parentClass ) {
+							var parent_method = this.parentClass.method(name)
+							if ( parent_method ) {
+								this.method(name, parent_method, this.parentClass.inherited[name] || this.parentClass)
+							}
+						}
+					}
+					else {
+						if ( this.methods[name] == undefined ) {
+							this.inherited[name] = declaredIn
+							this.prototype[name] = body
+						}
+					}
+				}
+				for ( var i=0 ; i<this.subclasses.length ; i++ )
+				{
+					this.subclasses[i].method(name,body,declaredIn)
+				}
+			}
+		}
+	}
+}
+
+// In case prototype is not available, we use this instead
+try {
+	Class = Class
+} catch ( Error ) {
+	Class = {create:function() {return function() {this.initialize.apply(this, arguments)}}}
+}
+Class._create = Class.create
+Class.create  = function( declaration ) {
+	var new_class = Extend.setupClass(Class._create(declaration), declaration)
+	 // The following only works on FireFox
+	/*
+	new_class.watch("prototype", function(id,oldval,newval) {
+		new_class.prototype = newval
+		Extend.setupClass(new_class, newval)
+		return newval
+	})*/
+	return new_class
+}
+
+// EOF

Added: trunk/sandbox/scrollable-grid/src/main/javascript/common/prototype/prototype.js
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/javascript/common/prototype/prototype.js	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/javascript/common/prototype/prototype.js	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,2515 @@
+/*  Prototype JavaScript framework, version 1.5.0
+ *  (c) 2005-2007 Sam Stephenson
+ *
+ *  Prototype is freely distributable under the terms of an MIT-style license.
+ *  For details, see the Prototype web site: http://prototype.conio.net/
+ *
+/*--------------------------------------------------------------------------*/
+
+var Prototype = {
+  Version: '1.5.0',
+  BrowserFeatures: {
+    XPath: !!document.evaluate
+  },
+
+  ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)',
+  emptyFunction: function() {},
+  K: function(x) { return x }
+}
+
+var Class = {
+  create: function() {
+    return function() {
+      this.initialize.apply(this, arguments);
+    }
+  }
+}
+
+var Abstract = new Object();
+
+Object.extend = function(destination, source) {
+  for (var property in source) {
+    destination[property] = source[property];
+  }
+  return destination;
+}
+
+Object.extend(Object, {
+  inspect: function(object) {
+    try {
+      if (object === undefined) return 'undefined';
+      if (object === null) return 'null';
+      return object.inspect ? object.inspect() : object.toString();
+    } catch (e) {
+      if (e instanceof RangeError) return '...';
+      throw e;
+    }
+  },
+
+  keys: function(object) {
+    var keys = [];
+    for (var property in object)
+      keys.push(property);
+    return keys;
+  },
+
+  values: function(object) {
+    var values = [];
+    for (var property in object)
+      values.push(object[property]);
+    return values;
+  },
+
+  clone: function(object) {
+    return Object.extend({}, object);
+  }
+});
+
+Function.prototype.bind = function() {
+  var __method = this, args = $A(arguments), object = args.shift();
+  return function() {
+    return __method.apply(object, args.concat($A(arguments)));
+  }
+}
+
+Function.prototype.bindAsEventListener = function(object) {
+  var __method = this, args = $A(arguments), object = args.shift();
+  return function(event) {
+    return __method.apply(object, [( event || window.event)].concat(args).concat($A(arguments)));
+  }
+}
+
+Object.extend(Number.prototype, {
+  toColorPart: function() {
+    var digits = this.toString(16);
+    if (this < 16) return '0' + digits;
+    return digits;
+  },
+
+  succ: function() {
+    return this + 1;
+  },
+
+  times: function(iterator) {
+    $R(0, this, true).each(iterator);
+    return this;
+  }
+});
+
+var Try = {
+  these: function() {
+    var returnValue;
+
+    for (var i = 0, length = arguments.length; i < length; i++) {
+      var lambda = arguments[i];
+      try {
+        returnValue = lambda();
+        break;
+      } catch (e) {}
+    }
+
+    return returnValue;
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var PeriodicalExecuter = Class.create();
+PeriodicalExecuter.prototype = {
+  initialize: function(callback, frequency) {
+    this.callback = callback;
+    this.frequency = frequency;
+    this.currentlyExecuting = false;
+
+    this.registerCallback();
+  },
+
+  registerCallback: function() {
+    this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+  },
+
+  stop: function() {
+    if (!this.timer) return;
+    clearInterval(this.timer);
+    this.timer = null;
+  },
+
+  onTimerEvent: function() {
+    if (!this.currentlyExecuting) {
+      try {
+        this.currentlyExecuting = true;
+        this.callback(this);
+      } finally {
+        this.currentlyExecuting = false;
+      }
+    }
+  }
+}
+String.interpret = function(value){
+  return value == null ? '' : String(value);
+}
+
+Object.extend(String.prototype, {
+  gsub: function(pattern, replacement) {
+    var result = '', source = this, match;
+    replacement = arguments.callee.prepareReplacement(replacement);
+
+    while (source.length > 0) {
+      if (match = source.match(pattern)) {
+        result += source.slice(0, match.index);
+        result += String.interpret(replacement(match));
+        source  = source.slice(match.index + match[0].length);
+      } else {
+        result += source, source = '';
+      }
+    }
+    return result;
+  },
+
+  sub: function(pattern, replacement, count) {
+    replacement = this.gsub.prepareReplacement(replacement);
+    count = count === undefined ? 1 : count;
+
+    return this.gsub(pattern, function(match) {
+      if (--count < 0) return match[0];
+      return replacement(match);
+    });
+  },
+
+  scan: function(pattern, iterator) {
+    this.gsub(pattern, iterator);
+    return this;
+  },
+
+  truncate: function(length, truncation) {
+    length = length || 30;
+    truncation = truncation === undefined ? '...' : truncation;
+    return this.length > length ?
+      this.slice(0, length - truncation.length) + truncation : this;
+  },
+
+  strip: function() {
+    return this.replace(/^\s+/, '').replace(/\s+$/, '');
+  },
+
+  stripTags: function() {
+    return this.replace(/<\/?[^>]+>/gi, '');
+  },
+
+  stripScripts: function() {
+    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
+  },
+
+  extractScripts: function() {
+    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
+    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
+    return (this.match(matchAll) || []).map(function(scriptTag) {
+      return (scriptTag.match(matchOne) || ['', ''])[1];
+    });
+  },
+
+  evalScripts: function() {
+    return this.extractScripts().map(function(script) { return eval(script) });
+  },
+
+  escapeHTML: function() {
+    var div = document.createElement('div');
+    var text = document.createTextNode(this);
+    div.appendChild(text);
+    return div.innerHTML;
+  },
+
+  unescapeHTML: function() {
+    var div = document.createElement('div');
+    div.innerHTML = this.stripTags();
+    return div.childNodes[0] ? (div.childNodes.length > 1 ?
+      $A(div.childNodes).inject('',function(memo,node){ return memo+node.nodeValue }) :
+      div.childNodes[0].nodeValue) : '';
+  },
+
+  toQueryParams: function(separator) {
+    var match = this.strip().match(/([^?#]*)(#.*)?$/);
+    if (!match) return {};
+
+    return match[1].split(separator || '&').inject({}, function(hash, pair) {
+      if ((pair = pair.split('='))[0]) {
+        var name = decodeURIComponent(pair[0]);
+        var value = pair[1] ? decodeURIComponent(pair[1]) : undefined;
+
+        if (hash[name] !== undefined) {
+          if (hash[name].constructor != Array)
+            hash[name] = [hash[name]];
+          if (value) hash[name].push(value);
+        }
+        else hash[name] = value;
+      }
+      return hash;
+    });
+  },
+
+  toArray: function() {
+    return this.split('');
+  },
+
+  succ: function() {
+    return this.slice(0, this.length - 1) +
+      String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
+  },
+
+  camelize: function() {
+    var parts = this.split('-'), len = parts.length;
+    if (len == 1) return parts[0];
+
+    var camelized = this.charAt(0) == '-'
+      ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
+      : parts[0];
+
+    for (var i = 1; i < len; i++)
+      camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);
+
+    return camelized;
+  },
+
+  capitalize: function(){
+    return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
+  },
+
+  underscore: function() {
+    return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
+  },
+
+  dasherize: function() {
+    return this.gsub(/_/,'-');
+  },
+
+  inspect: function(useDoubleQuotes) {
+    var escapedString = this.replace(/\\/g, '\\\\');
+    if (useDoubleQuotes)
+      return '"' + escapedString.replace(/"/g, '\\"') + '"';
+    else
+      return "'" + escapedString.replace(/'/g, '\\\'') + "'";
+  }
+});
+
+String.prototype.gsub.prepareReplacement = function(replacement) {
+  if (typeof replacement == 'function') return replacement;
+  var template = new Template(replacement);
+  return function(match) { return template.evaluate(match) };
+}
+
+String.prototype.parseQuery = String.prototype.toQueryParams;
+
+var Template = Class.create();
+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
+Template.prototype = {
+  initialize: function(template, pattern) {
+    this.template = template.toString();
+    this.pattern  = pattern || Template.Pattern;
+  },
+
+  evaluate: function(object) {
+    return this.template.gsub(this.pattern, function(match) {
+      var before = match[1];
+      if (before == '\\') return match[2];
+      return before + String.interpret(object[match[3]]);
+    });
+  }
+}
+
+var $break    = new Object();
+var $continue = new Object();
+
+var Enumerable = {
+  each: function(iterator) {
+    var index = 0;
+    try {
+      this._each(function(value) {
+        try {
+          iterator(value, index++);
+        } catch (e) {
+          if (e != $continue) throw e;
+        }
+      });
+    } catch (e) {
+      if (e != $break) throw e;
+    }
+    return this;
+  },
+
+  eachSlice: function(number, iterator) {
+    var index = -number, slices = [], array = this.toArray();
+    while ((index += number) < array.length)
+      slices.push(array.slice(index, index+number));
+    return slices.map(iterator);
+  },
+
+  all: function(iterator) {
+    var result = true;
+    this.each(function(value, index) {
+      result = result && !!(iterator || Prototype.K)(value, index);
+      if (!result) throw $break;
+    });
+    return result;
+  },
+
+  any: function(iterator) {
+    var result = false;
+    this.each(function(value, index) {
+      if (result = !!(iterator || Prototype.K)(value, index))
+        throw $break;
+    });
+    return result;
+  },
+
+  collect: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      results.push((iterator || Prototype.K)(value, index));
+    });
+    return results;
+  },
+
+  detect: function(iterator) {
+    var result;
+    this.each(function(value, index) {
+      if (iterator(value, index)) {
+        result = value;
+        throw $break;
+      }
+    });
+    return result;
+  },
+
+  findAll: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      if (iterator(value, index))
+        results.push(value);
+    });
+    return results;
+  },
+
+  grep: function(pattern, iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      var stringValue = value.toString();
+      if (stringValue.match(pattern))
+        results.push((iterator || Prototype.K)(value, index));
+    })
+    return results;
+  },
+
+  include: function(object) {
+    var found = false;
+    this.each(function(value) {
+      if (value == object) {
+        found = true;
+        throw $break;
+      }
+    });
+    return found;
+  },
+
+  inGroupsOf: function(number, fillWith) {
+    fillWith = fillWith === undefined ? null : fillWith;
+    return this.eachSlice(number, function(slice) {
+      while(slice.length < number) slice.push(fillWith);
+      return slice;
+    });
+  },
+
+  inject: function(memo, iterator) {
+    this.each(function(value, index) {
+      memo = iterator(memo, value, index);
+    });
+    return memo;
+  },
+
+  invoke: function(method) {
+    var args = $A(arguments).slice(1);
+    return this.map(function(value) {
+      return value[method].apply(value, args);
+    });
+  },
+
+  max: function(iterator) {
+    var result;
+    this.each(function(value, index) {
+      value = (iterator || Prototype.K)(value, index);
+      if (result == undefined || value >= result)
+        result = value;
+    });
+    return result;
+  },
+
+  min: function(iterator) {
+    var result;
+    this.each(function(value, index) {
+      value = (iterator || Prototype.K)(value, index);
+      if (result == undefined || value < result)
+        result = value;
+    });
+    return result;
+  },
+
+  partition: function(iterator) {
+    var trues = [], falses = [];
+    this.each(function(value, index) {
+      ((iterator || Prototype.K)(value, index) ?
+        trues : falses).push(value);
+    });
+    return [trues, falses];
+  },
+
+  pluck: function(property) {
+    var results = [];
+    this.each(function(value, index) {
+      results.push(value[property]);
+    });
+    return results;
+  },
+
+  reject: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      if (!iterator(value, index))
+        results.push(value);
+    });
+    return results;
+  },
+
+  sortBy: function(iterator) {
+    return this.map(function(value, index) {
+      return {value: value, criteria: iterator(value, index)};
+    }).sort(function(left, right) {
+      var a = left.criteria, b = right.criteria;
+      return a < b ? -1 : a > b ? 1 : 0;
+    }).pluck('value');
+  },
+
+  toArray: function() {
+    return this.map();
+  },
+
+  zip: function() {
+    var iterator = Prototype.K, args = $A(arguments);
+    if (typeof args.last() == 'function')
+      iterator = args.pop();
+
+    var collections = [this].concat(args).map($A);
+    return this.map(function(value, index) {
+      return iterator(collections.pluck(index));
+    });
+  },
+
+  size: function() {
+    return this.toArray().length;
+  },
+
+  inspect: function() {
+    return '#<Enumerable:' + this.toArray().inspect() + '>';
+  }
+}
+
+Object.extend(Enumerable, {
+  map:     Enumerable.collect,
+  find:    Enumerable.detect,
+  select:  Enumerable.findAll,
+  member:  Enumerable.include,
+  entries: Enumerable.toArray
+});
+var $A = Array.from = function(iterable) {
+  if (!iterable) return [];
+  if (iterable.toArray) {
+    return iterable.toArray();
+  } else {
+    var results = [];
+    for (var i = 0, length = iterable.length; i < length; i++)
+      results.push(iterable[i]);
+    return results;
+  }
+}
+
+Object.extend(Array.prototype, Enumerable);
+
+if (!Array.prototype._reverse)
+  Array.prototype._reverse = Array.prototype.reverse;
+
+Object.extend(Array.prototype, {
+  _each: function(iterator) {
+    for (var i = 0, length = this.length; i < length; i++)
+      iterator(this[i]);
+  },
+
+  clear: function() {
+    this.length = 0;
+    return this;
+  },
+
+  first: function() {
+    return this[0];
+  },
+
+  last: function() {
+    return this[this.length - 1];
+  },
+
+  compact: function() {
+    return this.select(function(value) {
+      return value != null;
+    });
+  },
+
+  flatten: function() {
+    return this.inject([], function(array, value) {
+      return array.concat(value && value.constructor == Array ?
+        value.flatten() : [value]);
+    });
+  },
+
+  without: function() {
+    var values = $A(arguments);
+    return this.select(function(value) {
+      return !values.include(value);
+    });
+  },
+
+  indexOf: function(object) {
+    for (var i = 0, length = this.length; i < length; i++)
+      if (this[i] == object) return i;
+    return -1;
+  },
+
+  reverse: function(inline) {
+    return (inline !== false ? this : this.toArray())._reverse();
+  },
+
+  reduce: function() {
+    return this.length > 1 ? this : this[0];
+  },
+
+  uniq: function() {
+    return this.inject([], function(array, value) {
+      return array.include(value) ? array : array.concat([value]);
+    });
+  },
+
+  clone: function() {
+    return [].concat(this);
+  },
+
+  size: function() {
+    return this.length;
+  },
+
+  inspect: function() {
+    return '[' + this.map(Object.inspect).join(', ') + ']';
+  }
+});
+
+Array.prototype.toArray = Array.prototype.clone;
+
+function $w(string){
+  string = string.strip();
+  return string ? string.split(/\s+/) : [];
+}
+
+if(window.opera){
+  Array.prototype.concat = function(){
+    var array = [];
+    for(var i = 0, length = this.length; i < length; i++) array.push(this[i]);
+    for(var i = 0, length = arguments.length; i < length; i++) {
+      if(arguments[i].constructor == Array) {
+        for(var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
+          array.push(arguments[i][j]);
+      } else {
+        array.push(arguments[i]);
+      }
+    }
+    return array;
+  }
+}
+var Hash = function(obj) {
+  Object.extend(this, obj || {});
+};
+
+Object.extend(Hash, {
+  toQueryString: function(obj) {
+    var parts = [];
+
+	  this.prototype._each.call(obj, function(pair) {
+      if (!pair.key) return;
+
+      if (pair.value && pair.value.constructor == Array) {
+        var values = pair.value.compact();
+        if (values.length < 2) pair.value = values.reduce();
+        else {
+        	key = encodeURIComponent(pair.key);
+          values.each(function(value) {
+            value = value != undefined ? encodeURIComponent(value) : '';
+            parts.push(key + '=' + encodeURIComponent(value));
+          });
+          return;
+        }
+      }
+      if (pair.value == undefined) pair[1] = '';
+      parts.push(pair.map(encodeURIComponent).join('='));
+	  });
+
+    return parts.join('&');
+  }
+});
+
+Object.extend(Hash.prototype, Enumerable);
+Object.extend(Hash.prototype, {
+  _each: function(iterator) {
+    for (var key in this) {
+      var value = this[key];
+      if (value && value == Hash.prototype[key]) continue;
+
+      var pair = [key, value];
+      pair.key = key;
+      pair.value = value;
+      iterator(pair);
+    }
+  },
+
+  keys: function() {
+    return this.pluck('key');
+  },
+
+  values: function() {
+    return this.pluck('value');
+  },
+
+  merge: function(hash) {
+    return $H(hash).inject(this, function(mergedHash, pair) {
+      mergedHash[pair.key] = pair.value;
+      return mergedHash;
+    });
+  },
+
+  remove: function() {
+    var result;
+    for(var i = 0, length = arguments.length; i < length; i++) {
+      var value = this[arguments[i]];
+      if (value !== undefined){
+        if (result === undefined) result = value;
+        else {
+          if (result.constructor != Array) result = [result];
+          result.push(value)
+        }
+      }
+      delete this[arguments[i]];
+    }
+    return result;
+  },
+
+  toQueryString: function() {
+    return Hash.toQueryString(this);
+  },
+
+  inspect: function() {
+    return '#<Hash:{' + this.map(function(pair) {
+      return pair.map(Object.inspect).join(': ');
+    }).join(', ') + '}>';
+  }
+});
+
+function $H(object) {
+  if (object && object.constructor == Hash) return object;
+  return new Hash(object);
+};
+ObjectRange = Class.create();
+Object.extend(ObjectRange.prototype, Enumerable);
+Object.extend(ObjectRange.prototype, {
+  initialize: function(start, end, exclusive) {
+    this.start = start;
+    this.end = end;
+    this.exclusive = exclusive;
+  },
+
+  _each: function(iterator) {
+    var value = this.start;
+    while (this.include(value)) {
+      iterator(value);
+      value = value.succ();
+    }
+  },
+
+  include: function(value) {
+    if (value < this.start)
+      return false;
+    if (this.exclusive)
+      return value < this.end;
+    return value <= this.end;
+  }
+});
+
+var $R = function(start, end, exclusive) {
+  return new ObjectRange(start, end, exclusive);
+}
+
+var Ajax = {
+  getTransport: function() {
+    return Try.these(
+      function() {return new XMLHttpRequest()},
+      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
+      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
+    ) || false;
+  },
+
+  activeRequestCount: 0
+}
+
+Ajax.Responders = {
+  responders: [],
+
+  _each: function(iterator) {
+    this.responders._each(iterator);
+  },
+
+  register: function(responder) {
+    if (!this.include(responder))
+      this.responders.push(responder);
+  },
+
+  unregister: function(responder) {
+    this.responders = this.responders.without(responder);
+  },
+
+  dispatch: function(callback, request, transport, json) {
+    this.each(function(responder) {
+      if (typeof responder[callback] == 'function') {
+        try {
+          responder[callback].apply(responder, [request, transport, json]);
+        } catch (e) {}
+      }
+    });
+  }
+};
+
+Object.extend(Ajax.Responders, Enumerable);
+
+Ajax.Responders.register({
+  onCreate: function() {
+    Ajax.activeRequestCount++;
+  },
+  onComplete: function() {
+    Ajax.activeRequestCount--;
+  }
+});
+
+Ajax.Base = function() {};
+Ajax.Base.prototype = {
+  setOptions: function(options) {
+    this.options = {
+      method:       'post',
+      asynchronous: true,
+      contentType:  'application/x-www-form-urlencoded',
+      encoding:     'UTF-8',
+      parameters:   ''
+    }
+    Object.extend(this.options, options || {});
+
+    this.options.method = this.options.method.toLowerCase();
+    if (typeof this.options.parameters == 'string')
+      this.options.parameters = this.options.parameters.toQueryParams();
+  }
+}
+
+Ajax.Request = Class.create();
+Ajax.Request.Events =
+  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
+  _complete: false,
+
+  initialize: function(url, options) {
+    this.transport = Ajax.getTransport();
+    this.setOptions(options);
+    this.request(url);
+  },
+
+  request: function(url) {
+    this.url = url;
+    this.method = this.options.method;
+    var params = this.options.parameters;
+
+    if (!['get', 'post'].include(this.method)) {
+      // simulate other verbs over post
+      params['_method'] = this.method;
+      this.method = 'post';
+    }
+
+    params = Hash.toQueryString(params);
+    if (params && /Konqueror|Safari|KHTML/.test(navigator.userAgent)) params += '&_='
+
+    // when GET, append parameters to URL
+    if (this.method == 'get' && params)
+      this.url += (this.url.indexOf('?') > -1 ? '&' : '?') + params;
+
+    try {
+      Ajax.Responders.dispatch('onCreate', this, this.transport);
+
+      this.transport.open(this.method.toUpperCase(), this.url,
+        this.options.asynchronous);
+
+      if (this.options.asynchronous)
+        setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10);
+
+      this.transport.onreadystatechange = this.onStateChange.bind(this);
+      this.setRequestHeaders();
+
+      var body = this.method == 'post' ? (this.options.postBody || params) : null;
+
+      this.transport.send(body);
+
+      /* Force Firefox to handle ready state 4 for synchronous requests */
+      if (!this.options.asynchronous && this.transport.overrideMimeType)
+        this.onStateChange();
+
+    }
+    catch (e) {
+      this.dispatchException(e);
+    }
+  },
+
+  onStateChange: function() {
+    var readyState = this.transport.readyState;
+    if (readyState > 1 && !((readyState == 4) && this._complete))
+      this.respondToReadyState(this.transport.readyState);
+  },
+
+  setRequestHeaders: function() {
+    var headers = {
+      'X-Requested-With': 'XMLHttpRequest',
+      'X-Prototype-Version': Prototype.Version,
+      'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
+    };
+
+    if (this.method == 'post') {
+      headers['Content-type'] = this.options.contentType +
+        (this.options.encoding ? '; charset=' + this.options.encoding : '');
+
+      /* Force "Connection: close" for older Mozilla browsers to work
+       * around a bug where XMLHttpRequest sends an incorrect
+       * Content-length header. See Mozilla Bugzilla #246651.
+       */
+      if (this.transport.overrideMimeType &&
+          (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
+            headers['Connection'] = 'close';
+    }
+
+    // user-defined headers
+    if (typeof this.options.requestHeaders == 'object') {
+      var extras = this.options.requestHeaders;
+
+      if (typeof extras.push == 'function')
+        for (var i = 0, length = extras.length; i < length; i += 2)
+          headers[extras[i]] = extras[i+1];
+      else
+        $H(extras).each(function(pair) { headers[pair.key] = pair.value });
+    }
+
+    for (var name in headers)
+      this.transport.setRequestHeader(name, headers[name]);
+  },
+
+  success: function() {
+    return !this.transport.status
+        || (this.transport.status >= 200 && this.transport.status < 300);
+  },
+
+  respondToReadyState: function(readyState) {
+    var state = Ajax.Request.Events[readyState];
+    var transport = this.transport, json = this.evalJSON();
+
+    if (state == 'Complete') {
+      try {
+        this._complete = true;
+        (this.options['on' + this.transport.status]
+         || this.options['on' + (this.success() ? 'Success' : 'Failure')]
+         || Prototype.emptyFunction)(transport, json);
+      } catch (e) {
+        this.dispatchException(e);
+      }
+
+      if ((this.getHeader('Content-type') || 'text/javascript').strip().
+        match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i))
+          this.evalResponse();
+    }
+
+    try {
+      (this.options['on' + state] || Prototype.emptyFunction)(transport, json);
+      Ajax.Responders.dispatch('on' + state, this, transport, json);
+    } catch (e) {
+      this.dispatchException(e);
+    }
+
+    if (state == 'Complete') {
+      // avoid memory leak in MSIE: clean up
+      this.transport.onreadystatechange = Prototype.emptyFunction;
+    }
+  },
+
+  getHeader: function(name) {
+    try {
+      return this.transport.getResponseHeader(name);
+    } catch (e) { return null }
+  },
+
+  evalJSON: function() {
+    try {
+      var json = this.getHeader('X-JSON');
+      return json ? eval('(' + json + ')') : null;
+    } catch (e) { return null }
+  },
+
+  evalResponse: function() {
+    try {
+      return eval(this.transport.responseText);
+    } catch (e) {
+      this.dispatchException(e);
+    }
+  },
+
+  dispatchException: function(exception) {
+    (this.options.onException || Prototype.emptyFunction)(this, exception);
+    Ajax.Responders.dispatch('onException', this, exception);
+  }
+});
+
+Ajax.Updater = Class.create();
+
+Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
+  initialize: function(container, url, options) {
+    this.container = {
+      success: (container.success || container),
+      failure: (container.failure || (container.success ? null : container))
+    }
+
+    this.transport = Ajax.getTransport();
+    this.setOptions(options);
+
+    var onComplete = this.options.onComplete || Prototype.emptyFunction;
+    this.options.onComplete = (function(transport, param) {
+      this.updateContent();
+      onComplete(transport, param);
+    }).bind(this);
+
+    this.request(url);
+  },
+
+  updateContent: function() {
+    var receiver = this.container[this.success() ? 'success' : 'failure'];
+    var response = this.transport.responseText;
+
+    if (!this.options.evalScripts) response = response.stripScripts();
+
+    if (receiver = $(receiver)) {
+      if (this.options.insertion)
+        new this.options.insertion(receiver, response);
+      else
+        receiver.update(response);
+    }
+
+    if (this.success()) {
+      if (this.onComplete)
+        setTimeout(this.onComplete.bind(this), 10);
+    }
+  }
+});
+
+Ajax.PeriodicalUpdater = Class.create();
+Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
+  initialize: function(container, url, options) {
+    this.setOptions(options);
+    this.onComplete = this.options.onComplete;
+
+    this.frequency = (this.options.frequency || 2);
+    this.decay = (this.options.decay || 1);
+
+    this.updater = {};
+    this.container = container;
+    this.url = url;
+
+    this.start();
+  },
+
+  start: function() {
+    this.options.onComplete = this.updateComplete.bind(this);
+    this.onTimerEvent();
+  },
+
+  stop: function() {
+    this.updater.options.onComplete = undefined;
+    clearTimeout(this.timer);
+    (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
+  },
+
+  updateComplete: function(request) {
+    if (this.options.decay) {
+      this.decay = (request.responseText == this.lastText ?
+        this.decay * this.options.decay : 1);
+
+      this.lastText = request.responseText;
+    }
+    this.timer = setTimeout(this.onTimerEvent.bind(this),
+      this.decay * this.frequency * 1000);
+  },
+
+  onTimerEvent: function() {
+    this.updater = new Ajax.Updater(this.container, this.url, this.options);
+  }
+});
+function $(element) {
+  if (arguments.length > 1) {
+    for (var i = 0, elements = [], length = arguments.length; i < length; i++)
+      elements.push($(arguments[i]));
+    return elements;
+  }
+  if (typeof element == 'string')
+    element = document.getElementById(element);
+  return Element.extend(element);
+}
+
+if (Prototype.BrowserFeatures.XPath) {
+  document._getElementsByXPath = function(expression, parentElement) {
+    var results = [];
+    var query = document.evaluate(expression, $(parentElement) || document,
+      null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
+    for (var i = 0, length = query.snapshotLength; i < length; i++)
+      results.push(query.snapshotItem(i));
+    return results;
+  };
+}
+
+document.getElementsByClassName = function(className, parentElement) {
+  if (Prototype.BrowserFeatures.XPath) {
+    var q = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]";
+    return document._getElementsByXPath(q, parentElement);
+  } else {
+    var children = ($(parentElement) || document.body).getElementsByTagName('*');
+    var elements = [], child;
+    for (var i = 0, length = children.length; i < length; i++) {
+      child = children[i];
+      if (Element.hasClassName(child, className))
+        elements.push(Element.extend(child));
+    }
+    return elements;
+  }
+};
+
+/*--------------------------------------------------------------------------*/
+
+if (!window.Element)
+  var Element = new Object();
+
+Element.extend = function(element) {
+  if (!element || _nativeExtensions || element.nodeType == 3) return element;
+
+  if (!element._extended && element.tagName && element != window) {
+    var methods = Object.clone(Element.Methods), cache = Element.extend.cache;
+
+    if (element.tagName == 'FORM')
+      Object.extend(methods, Form.Methods);
+    if (['INPUT', 'TEXTAREA', 'SELECT'].include(element.tagName))
+      Object.extend(methods, Form.Element.Methods);
+
+    Object.extend(methods, Element.Methods.Simulated);
+
+    for (var property in methods) {
+      var value = methods[property];
+      if (typeof value == 'function' && !(property in element))
+        element[property] = cache.findOrStore(value);
+    }
+  }
+
+  element._extended = true;
+  return element;
+};
+
+Element.extend.cache = {
+  findOrStore: function(value) {
+    return this[value] = this[value] || function() {
+      return value.apply(null, [this].concat($A(arguments)));
+    }
+  }
+};
+
+Element.Methods = {
+  visible: function(element) {
+    return $(element).style.display != 'none';
+  },
+
+  toggle: function(element) {
+    element = $(element);
+    Element[Element.visible(element) ? 'hide' : 'show'](element);
+    return element;
+  },
+
+  hide: function(element) {
+    $(element).style.display = 'none';
+    return element;
+  },
+
+  show: function(element) {
+    $(element).style.display = '';
+    return element;
+  },
+
+  remove: function(element) {
+    element = $(element);
+    element.parentNode.removeChild(element);
+    return element;
+  },
+
+  update: function(element, html) {
+    html = typeof html == 'undefined' ? '' : html.toString();
+    $(element).innerHTML = html.stripScripts();
+    setTimeout(function() {html.evalScripts()}, 10);
+    return element;
+  },
+
+  replace: function(element, html) {
+    element = $(element);
+    html = typeof html == 'undefined' ? '' : html.toString();
+    if (element.outerHTML) {
+      element.outerHTML = html.stripScripts();
+    } else {
+      var range = element.ownerDocument.createRange();
+      range.selectNodeContents(element);
+      element.parentNode.replaceChild(
+        range.createContextualFragment(html.stripScripts()), element);
+    }
+    setTimeout(function() {html.evalScripts()}, 10);
+    return element;
+  },
+
+  inspect: function(element) {
+    element = $(element);
+    var result = '<' + element.tagName.toLowerCase();
+    $H({'id': 'id', 'className': 'class'}).each(function(pair) {
+      var property = pair.first(), attribute = pair.last();
+      var value = (element[property] || '').toString();
+      if (value) result += ' ' + attribute + '=' + value.inspect(true);
+    });
+    return result + '>';
+  },
+
+  recursivelyCollect: function(element, property) {
+    element = $(element);
+    var elements = [];
+    while (element = element[property])
+      if (element.nodeType == 1)
+        elements.push(Element.extend(element));
+    return elements;
+  },
+
+  ancestors: function(element) {
+    return $(element).recursivelyCollect('parentNode');
+  },
+
+  descendants: function(element) {
+    return $A($(element).getElementsByTagName('*'));
+  },
+
+  immediateDescendants: function(element) {
+    if (!(element = $(element).firstChild)) return [];
+    while (element && element.nodeType != 1) element = element.nextSibling;
+    if (element) return [element].concat($(element).nextSiblings());
+    return [];
+  },
+
+  previousSiblings: function(element) {
+    return $(element).recursivelyCollect('previousSibling');
+  },
+
+  nextSiblings: function(element) {
+    return $(element).recursivelyCollect('nextSibling');
+  },
+
+  siblings: function(element) {
+    element = $(element);
+    return element.previousSiblings().reverse().concat(element.nextSiblings());
+  },
+
+  match: function(element, selector) {
+    if (typeof selector == 'string')
+      selector = new Selector(selector);
+    return selector.match($(element));
+  },
+
+  up: function(element, expression, index) {
+    return Selector.findElement($(element).ancestors(), expression, index);
+  },
+
+  down: function(element, expression, index) {
+    return Selector.findElement($(element).descendants(), expression, index);
+  },
+
+  previous: function(element, expression, index) {
+    return Selector.findElement($(element).previousSiblings(), expression, index);
+  },
+
+  next: function(element, expression, index) {
+    return Selector.findElement($(element).nextSiblings(), expression, index);
+  },
+
+  getElementsBySelector: function() {
+    var args = $A(arguments), element = $(args.shift());
+    return Selector.findChildElements(element, args);
+  },
+
+  getElementsByClassName: function(element, className) {
+    return document.getElementsByClassName(className, element);
+  },
+
+  readAttribute: function(element, name) {
+    element = $(element);
+    if (document.all && !window.opera) {
+      var t = Element._attributeTranslations;
+      if (t.values[name]) return t.values[name](element, name);
+      if (t.names[name])  name = t.names[name];
+      var attribute = element.attributes[name];
+      if(attribute) return attribute.nodeValue;
+    }
+    return element.getAttribute(name);
+  },
+
+  getHeight: function(element) {
+    return $(element).getDimensions().height;
+  },
+
+  getWidth: function(element) {
+    return $(element).getDimensions().width;
+  },
+
+  classNames: function(element) {
+    return new Element.ClassNames(element);
+  },
+
+  hasClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    var elementClassName = element.className;
+    if (elementClassName.length == 0) return false;
+    if (elementClassName == className ||
+        elementClassName.match(new RegExp("(^|\\s)" + className + "(\\s|$)")))
+      return true;
+    return false;
+  },
+
+  addClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    Element.classNames(element).add(className);
+    return element;
+  },
+
+  removeClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    Element.classNames(element).remove(className);
+    return element;
+  },
+
+  toggleClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    Element.classNames(element)[element.hasClassName(className) ? 'remove' : 'add'](className);
+    return element;
+  },
+
+  observe: function() {
+    Event.observe.apply(Event, arguments);
+    return $A(arguments).first();
+  },
+
+  stopObserving: function() {
+    Event.stopObserving.apply(Event, arguments);
+    return $A(arguments).first();
+  },
+
+  // removes whitespace-only text node children
+  cleanWhitespace: function(element) {
+    element = $(element);
+    var node = element.firstChild;
+    while (node) {
+      var nextNode = node.nextSibling;
+      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
+        element.removeChild(node);
+      node = nextNode;
+    }
+    return element;
+  },
+
+  empty: function(element) {
+    return $(element).innerHTML.match(/^\s*$/);
+  },
+
+  descendantOf: function(element, ancestor) {
+    element = $(element), ancestor = $(ancestor);
+    while (element = element.parentNode)
+      if (element == ancestor) return true;
+    return false;
+  },
+
+  scrollTo: function(element) {
+    element = $(element);
+    var pos = Position.cumulativeOffset(element);
+    window.scrollTo(pos[0], pos[1]);
+    return element;
+  },
+
+  getStyle: function(element, style) {
+    element = $(element);
+    if (['float','cssFloat'].include(style))
+      style = (typeof element.style.styleFloat != 'undefined' ? 'styleFloat' : 'cssFloat');
+    style = style.camelize();
+    var value = element.style[style];
+    if (!value) {
+      if (document.defaultView && document.defaultView.getComputedStyle) {
+        var css = document.defaultView.getComputedStyle(element, null);
+        value = css ? css[style] : null;
+      } else if (element.currentStyle) {
+        value = element.currentStyle[style];
+      }
+    }
+
+    if((value == 'auto') && ['width','height'].include(style) && (element.getStyle('display') != 'none'))
+      value = element['offset'+style.capitalize()] + 'px';
+
+    if (window.opera && ['left', 'top', 'right', 'bottom'].include(style))
+      if (Element.getStyle(element, 'position') == 'static') value = 'auto';
+    if(style == 'opacity') {
+      if(value) return parseFloat(value);
+      if(value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
+        if(value[1]) return parseFloat(value[1]) / 100;
+      return 1.0;
+    }
+    return value == 'auto' ? null : value;
+  },
+
+  setStyle: function(element, style) {
+    element = $(element);
+    for (var name in style) {
+      var value = style[name];
+      if(name == 'opacity') {
+        if (value == 1) {
+          value = (/Gecko/.test(navigator.userAgent) &&
+            !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 0.999999 : 1.0;
+          if(/MSIE/.test(navigator.userAgent) && !window.opera)
+            element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'');
+        } else if(value == '') {
+          if(/MSIE/.test(navigator.userAgent) && !window.opera)
+            element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'');
+        } else {
+          if(value < 0.00001) value = 0;
+          if(/MSIE/.test(navigator.userAgent) && !window.opera)
+            element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') +
+              'alpha(opacity='+value*100+')';
+        }
+      } else if(['float','cssFloat'].include(name)) name = (typeof element.style.styleFloat != 'undefined') ? 'styleFloat' : 'cssFloat';
+      element.style[name.camelize()] = value;
+    }
+    return element;
+  },
+
+  getDimensions: function(element) {
+    element = $(element);
+    var display = $(element).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 els = element.style;
+    var originalVisibility = els.visibility;
+    var originalPosition = els.position;
+    var originalDisplay = els.display;
+    els.visibility = 'hidden';
+    els.position = 'absolute';
+    els.display = 'block';
+    var originalWidth = element.clientWidth;
+    var originalHeight = element.clientHeight;
+    els.display = originalDisplay;
+    els.position = originalPosition;
+    els.visibility = originalVisibility;
+    return {width: originalWidth, height: originalHeight};
+  },
+
+  makePositioned: function(element) {
+    element = $(element);
+    var pos = Element.getStyle(element, 'position');
+    if (pos == 'static' || !pos) {
+      element._madePositioned = true;
+      element.style.position = 'relative';
+      // Opera returns the offset relative to the positioning context, when an
+      // element is position relative but top and left have not been defined
+      if (window.opera) {
+        element.style.top = 0;
+        element.style.left = 0;
+      }
+    }
+    return element;
+  },
+
+  undoPositioned: function(element) {
+    element = $(element);
+    if (element._madePositioned) {
+      element._madePositioned = undefined;
+      element.style.position =
+        element.style.top =
+        element.style.left =
+        element.style.bottom =
+        element.style.right = '';
+    }
+    return element;
+  },
+
+  makeClipping: function(element) {
+    element = $(element);
+    if (element._overflow) return element;
+    element._overflow = element.style.overflow || 'auto';
+    if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden')
+      element.style.overflow = 'hidden';
+    return element;
+  },
+
+  undoClipping: function(element) {
+    element = $(element);
+    if (!element._overflow) return element;
+    element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
+    element._overflow = null;
+    return element;
+  }
+};
+
+Object.extend(Element.Methods, {childOf: Element.Methods.descendantOf});
+
+Element._attributeTranslations = {};
+
+Element._attributeTranslations.names = {
+  colspan:   "colSpan",
+  rowspan:   "rowSpan",
+  valign:    "vAlign",
+  datetime:  "dateTime",
+  accesskey: "accessKey",
+  tabindex:  "tabIndex",
+  enctype:   "encType",
+  maxlength: "maxLength",
+  readonly:  "readOnly",
+  longdesc:  "longDesc"
+};
+
+Element._attributeTranslations.values = {
+  _getAttr: function(element, attribute) {
+    return element.getAttribute(attribute, 2);
+  },
+
+  _flag: function(element, attribute) {
+    return $(element).hasAttribute(attribute) ? attribute : null;
+  },
+
+  style: function(element) {
+    return element.style.cssText.toLowerCase();
+  },
+
+  title: function(element) {
+    var node = element.getAttributeNode('title');
+    return node.specified ? node.nodeValue : null;
+  }
+};
+
+Object.extend(Element._attributeTranslations.values, {
+  href: Element._attributeTranslations.values._getAttr,
+  src:  Element._attributeTranslations.values._getAttr,
+  disabled: Element._attributeTranslations.values._flag,
+  checked:  Element._attributeTranslations.values._flag,
+  readonly: Element._attributeTranslations.values._flag,
+  multiple: Element._attributeTranslations.values._flag
+});
+
+Element.Methods.Simulated = {
+  hasAttribute: function(element, attribute) {
+    var t = Element._attributeTranslations;
+    attribute = t.names[attribute] || attribute;
+    return $(element).getAttributeNode(attribute).specified;
+  }
+};
+
+// IE is missing .innerHTML support for TABLE-related elements
+if (document.all && !window.opera){
+  Element.Methods.update = function(element, html) {
+    element = $(element);
+    html = typeof html == 'undefined' ? '' : html.toString();
+    var tagName = element.tagName.toUpperCase();
+    if (['THEAD','TBODY','TR','TD'].include(tagName)) {
+      var div = document.createElement('div');
+      switch (tagName) {
+        case 'THEAD':
+        case 'TBODY':
+          div.innerHTML = '<table><tbody>' +  html.stripScripts() + '</tbody></table>';
+          depth = 2;
+          break;
+        case 'TR':
+          div.innerHTML = '<table><tbody><tr>' +  html.stripScripts() + '</tr></tbody></table>';
+          depth = 3;
+          break;
+        case 'TD':
+          div.innerHTML = '<table><tbody><tr><td>' +  html.stripScripts() + '</td></tr></tbody></table>';
+          depth = 4;
+      }
+      $A(element.childNodes).each(function(node){
+        element.removeChild(node)
+      });
+      depth.times(function(){ div = div.firstChild });
+
+      $A(div.childNodes).each(
+        function(node){ element.appendChild(node) });
+    } else {
+      element.innerHTML = html.stripScripts();
+    }
+    setTimeout(function() {html.evalScripts()}, 10);
+    return element;
+  }
+};
+
+Object.extend(Element, Element.Methods);
+
+var _nativeExtensions = false;
+
+if(/Konqueror|Safari|KHTML/.test(navigator.userAgent))
+  ['', 'Form', 'Input', 'TextArea', 'Select'].each(function(tag) {
+    var className = 'HTML' + tag + 'Element';
+    if(window[className]) return;
+    var klass = window[className] = {};
+    klass.prototype = document.createElement(tag ? tag.toLowerCase() : 'div').__proto__;
+  });
+
+Element.addMethods = function(methods) {
+  Object.extend(Element.Methods, methods || {});
+
+  function copy(methods, destination, onlyIfAbsent) {
+    onlyIfAbsent = onlyIfAbsent || false;
+    var cache = Element.extend.cache;
+    for (var property in methods) {
+      var value = methods[property];
+      if (!onlyIfAbsent || !(property in destination))
+        destination[property] = cache.findOrStore(value);
+    }
+  }
+
+  if (typeof HTMLElement != 'undefined') {
+    copy(Element.Methods, HTMLElement.prototype);
+    copy(Element.Methods.Simulated, HTMLElement.prototype, true);
+    copy(Form.Methods, HTMLFormElement.prototype);
+    [HTMLInputElement, HTMLTextAreaElement, HTMLSelectElement].each(function(klass) {
+      copy(Form.Element.Methods, klass.prototype);
+    });
+    _nativeExtensions = true;
+  }
+}
+
+var Toggle = new Object();
+Toggle.display = Element.toggle;
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.Insertion = function(adjacency) {
+  this.adjacency = adjacency;
+}
+
+Abstract.Insertion.prototype = {
+  initialize: function(element, content) {
+    this.element = $(element);
+    this.content = content.stripScripts();
+
+    if (this.adjacency && this.element.insertAdjacentHTML) {
+      try {
+        this.element.insertAdjacentHTML(this.adjacency, this.content);
+      } catch (e) {
+        var tagName = this.element.tagName.toUpperCase();
+        if (['TBODY', 'TR'].include(tagName)) {
+          this.insertContent(this.contentFromAnonymousTable());
+        } else {
+          throw e;
+        }
+      }
+    } else {
+      this.range = this.element.ownerDocument.createRange();
+      if (this.initializeRange) this.initializeRange();
+      this.insertContent([this.range.createContextualFragment(this.content)]);
+    }
+
+    setTimeout(function() {content.evalScripts()}, 10);
+  },
+
+  contentFromAnonymousTable: function() {
+    var div = document.createElement('div');
+    div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>';
+    return $A(div.childNodes[0].childNodes[0].childNodes);
+  }
+}
+
+var Insertion = new Object();
+
+Insertion.Before = Class.create();
+Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
+  initializeRange: function() {
+    this.range.setStartBefore(this.element);
+  },
+
+  insertContent: function(fragments) {
+    fragments.each((function(fragment) {
+      this.element.parentNode.insertBefore(fragment, this.element);
+    }).bind(this));
+  }
+});
+
+Insertion.Top = Class.create();
+Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
+  initializeRange: function() {
+    this.range.selectNodeContents(this.element);
+    this.range.collapse(true);
+  },
+
+  insertContent: function(fragments) {
+    fragments.reverse(false).each((function(fragment) {
+      this.element.insertBefore(fragment, this.element.firstChild);
+    }).bind(this));
+  }
+});
+
+Insertion.Bottom = Class.create();
+Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {
+  initializeRange: function() {
+    this.range.selectNodeContents(this.element);
+    this.range.collapse(this.element);
+  },
+
+  insertContent: function(fragments) {
+    fragments.each((function(fragment) {
+      this.element.appendChild(fragment);
+    }).bind(this));
+  }
+});
+
+Insertion.After = Class.create();
+Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
+  initializeRange: function() {
+    this.range.setStartAfter(this.element);
+  },
+
+  insertContent: function(fragments) {
+    fragments.each((function(fragment) {
+      this.element.parentNode.insertBefore(fragment,
+        this.element.nextSibling);
+    }).bind(this));
+  }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Element.ClassNames = Class.create();
+Element.ClassNames.prototype = {
+  initialize: function(element) {
+    this.element = $(element);
+  },
+
+  _each: function(iterator) {
+    this.element.className.split(/\s+/).select(function(name) {
+      return name.length > 0;
+    })._each(iterator);
+  },
+
+  set: function(className) {
+    this.element.className = className;
+  },
+
+  add: function(classNameToAdd) {
+    if (this.include(classNameToAdd)) return;
+    this.set($A(this).concat(classNameToAdd).join(' '));
+  },
+
+  remove: function(classNameToRemove) {
+    if (!this.include(classNameToRemove)) return;
+    this.set($A(this).without(classNameToRemove).join(' '));
+  },
+
+  toString: function() {
+    return $A(this).join(' ');
+  }
+};
+
+Object.extend(Element.ClassNames.prototype, Enumerable);
+var Selector = Class.create();
+Selector.prototype = {
+  initialize: function(expression) {
+    this.params = {classNames: []};
+    this.expression = expression.toString().strip();
+    this.parseExpression();
+    this.compileMatcher();
+  },
+
+  parseExpression: function() {
+    function abort(message) { throw 'Parse error in selector: ' + message; }
+
+    if (this.expression == '')  abort('empty expression');
+
+    var params = this.params, expr = this.expression, match, modifier, clause, rest;
+    while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) {
+      params.attributes = params.attributes || [];
+      params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''});
+      expr = match[1];
+    }
+
+    if (expr == '*') return this.params.wildcard = true;
+
+    while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) {
+      modifier = match[1], clause = match[2], rest = match[3];
+      switch (modifier) {
+        case '#':       params.id = clause; break;
+        case '.':       params.classNames.push(clause); break;
+        case '':
+        case undefined: params.tagName = clause.toUpperCase(); break;
+        default:        abort(expr.inspect());
+      }
+      expr = rest;
+    }
+
+    if (expr.length > 0) abort(expr.inspect());
+  },
+
+  buildMatchExpression: function() {
+    var params = this.params, conditions = [], clause;
+
+    if (params.wildcard)
+      conditions.push('true');
+    if (clause = params.id)
+      conditions.push('element.readAttribute("id") == ' + clause.inspect());
+    if (clause = params.tagName)
+      conditions.push('element.tagName.toUpperCase() == ' + clause.inspect());
+    if ((clause = params.classNames).length > 0)
+      for (var i = 0, length = clause.length; i < length; i++)
+        conditions.push('element.hasClassName(' + clause[i].inspect() + ')');
+    if (clause = params.attributes) {
+      clause.each(function(attribute) {
+        var value = 'element.readAttribute(' + attribute.name.inspect() + ')';
+        var splitValueBy = function(delimiter) {
+          return value + ' && ' + value + '.split(' + delimiter.inspect() + ')';
+        }
+
+        switch (attribute.operator) {
+          case '=':       conditions.push(value + ' == ' + attribute.value.inspect()); break;
+          case '~=':      conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break;
+          case '|=':      conditions.push(
+                            splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect()
+                          ); break;
+          case '!=':      conditions.push(value + ' != ' + attribute.value.inspect()); break;
+          case '':
+          case undefined: conditions.push('element.hasAttribute(' + attribute.name.inspect() + ')'); break;
+          default:        throw 'Unknown operator ' + attribute.operator + ' in selector';
+        }
+      });
+    }
+
+    return conditions.join(' && ');
+  },
+
+  compileMatcher: function() {
+    this.match = new Function('element', 'if (!element.tagName) return false; \
+      element = $(element); \
+      return ' + this.buildMatchExpression());
+  },
+
+  findElements: function(scope) {
+    var element;
+
+    if (element = $(this.params.id))
+      if (this.match(element))
+        if (!scope || Element.childOf(element, scope))
+          return [element];
+
+    scope = (scope || document).getElementsByTagName(this.params.tagName || '*');
+
+    var results = [];
+    for (var i = 0, length = scope.length; i < length; i++)
+      if (this.match(element = scope[i]))
+        results.push(Element.extend(element));
+
+    return results;
+  },
+
+  toString: function() {
+    return this.expression;
+  }
+}
+
+Object.extend(Selector, {
+  matchElements: function(elements, expression) {
+    var selector = new Selector(expression);
+    return elements.select(selector.match.bind(selector)).map(Element.extend);
+  },
+
+  findElement: function(elements, expression, index) {
+    if (typeof expression == 'number') index = expression, expression = false;
+    return Selector.matchElements(elements, expression || '*')[index || 0];
+  },
+
+  findChildElements: function(element, expressions) {
+    return expressions.map(function(expression) {
+      return expression.match(/[^\s"]+(?:"[^"]*"[^\s"]+)*/g).inject([null], function(results, expr) {
+        var selector = new Selector(expr);
+        return results.inject([], function(elements, result) {
+          return elements.concat(selector.findElements(result || element));
+        });
+      });
+    }).flatten();
+  }
+});
+
+function $$() {
+  return Selector.findChildElements(document, $A(arguments));
+}
+var Form = {
+  reset: function(form) {
+    $(form).reset();
+    return form;
+  },
+
+  serializeElements: function(elements, getHash) {
+    var data = elements.inject({}, function(result, element) {
+      if (!element.disabled && element.name) {
+        var key = element.name, value = $(element).getValue();
+        if (value != undefined) {
+          if (result[key]) {
+            if (result[key].constructor != Array) result[key] = [result[key]];
+            result[key].push(value);
+          }
+          else result[key] = value;
+        }
+      }
+      return result;
+    });
+
+    return getHash ? data : Hash.toQueryString(data);
+  }
+};
+
+Form.Methods = {
+  serialize: function(form, getHash) {
+    return Form.serializeElements(Form.getElements(form), getHash);
+  },
+
+  getElements: function(form) {
+    return $A($(form).getElementsByTagName('*')).inject([],
+      function(elements, child) {
+        if (Form.Element.Serializers[child.tagName.toLowerCase()])
+          elements.push(Element.extend(child));
+        return elements;
+      }
+    );
+  },
+
+  getInputs: function(form, typeName, name) {
+    form = $(form);
+    var inputs = form.getElementsByTagName('input');
+
+    if (!typeName && !name) return $A(inputs).map(Element.extend);
+
+    for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
+      var input = inputs[i];
+      if ((typeName && input.type != typeName) || (name && input.name != name))
+        continue;
+      matchingInputs.push(Element.extend(input));
+    }
+
+    return matchingInputs;
+  },
+
+  disable: function(form) {
+    form = $(form);
+    form.getElements().each(function(element) {
+      element.blur();
+      element.disabled = 'true';
+    });
+    return form;
+  },
+
+  enable: function(form) {
+    form = $(form);
+    form.getElements().each(function(element) {
+      element.disabled = '';
+    });
+    return form;
+  },
+
+  findFirstElement: function(form) {
+    return $(form).getElements().find(function(element) {
+      return element.type != 'hidden' && !element.disabled &&
+        ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+    });
+  },
+
+  focusFirstElement: function(form) {
+    form = $(form);
+    form.findFirstElement().activate();
+    return form;
+  }
+}
+
+Object.extend(Form, Form.Methods);
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element = {
+  focus: function(element) {
+    $(element).focus();
+    return element;
+  },
+
+  select: function(element) {
+    $(element).select();
+    return element;
+  }
+}
+
+Form.Element.Methods = {
+  serialize: function(element) {
+    element = $(element);
+    if (!element.disabled && element.name) {
+      var value = element.getValue();
+      if (value != undefined) {
+        var pair = {};
+        pair[element.name] = value;
+        return Hash.toQueryString(pair);
+      }
+    }
+    return '';
+  },
+
+  getValue: function(element) {
+    element = $(element);
+    var method = element.tagName.toLowerCase();
+    return Form.Element.Serializers[method](element);
+  },
+
+  clear: function(element) {
+    $(element).value = '';
+    return element;
+  },
+
+  present: function(element) {
+    return $(element).value != '';
+  },
+
+  activate: function(element) {
+    element = $(element);
+    element.focus();
+    if (element.select && ( element.tagName.toLowerCase() != 'input' ||
+      !['button', 'reset', 'submit'].include(element.type) ) )
+      element.select();
+    return element;
+  },
+
+  disable: function(element) {
+    element = $(element);
+    element.disabled = true;
+    return element;
+  },
+
+  enable: function(element) {
+    element = $(element);
+    element.blur();
+    element.disabled = false;
+    return element;
+  }
+}
+
+Object.extend(Form.Element, Form.Element.Methods);
+var Field = Form.Element;
+var $F = Form.Element.getValue;
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element.Serializers = {
+  input: function(element) {
+    switch (element.type.toLowerCase()) {
+      case 'checkbox':
+      case 'radio':
+        return Form.Element.Serializers.inputSelector(element);
+      default:
+        return Form.Element.Serializers.textarea(element);
+    }
+  },
+
+  inputSelector: function(element) {
+    return element.checked ? element.value : null;
+  },
+
+  textarea: function(element) {
+    return element.value;
+  },
+
+  select: function(element) {
+    return this[element.type == 'select-one' ?
+      'selectOne' : 'selectMany'](element);
+  },
+
+  selectOne: function(element) {
+    var index = element.selectedIndex;
+    return index >= 0 ? this.optionValue(element.options[index]) : null;
+  },
+
+  selectMany: function(element) {
+    var values, length = element.length;
+    if (!length) return null;
+
+    for (var i = 0, values = []; i < length; i++) {
+      var opt = element.options[i];
+      if (opt.selected) values.push(this.optionValue(opt));
+    }
+    return values;
+  },
+
+  optionValue: function(opt) {
+    // extend element because hasAttribute may not be native
+    return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.TimedObserver = function() {}
+Abstract.TimedObserver.prototype = {
+  initialize: function(element, frequency, callback) {
+    this.frequency = frequency;
+    this.element   = $(element);
+    this.callback  = callback;
+
+    this.lastValue = this.getValue();
+    this.registerCallback();
+  },
+
+  registerCallback: function() {
+    setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+  },
+
+  onTimerEvent: function() {
+    var value = this.getValue();
+    var changed = ('string' == typeof this.lastValue && 'string' == typeof value
+      ? this.lastValue != value : String(this.lastValue) != String(value));
+    if (changed) {
+      this.callback(this.element, value);
+      this.lastValue = value;
+    }
+  }
+}
+
+Form.Element.Observer = Class.create();
+Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+  getValue: function() {
+    return Form.Element.getValue(this.element);
+  }
+});
+
+Form.Observer = Class.create();
+Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+  getValue: function() {
+    return Form.serialize(this.element);
+  }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.EventObserver = function() {}
+Abstract.EventObserver.prototype = {
+  initialize: function(element, callback) {
+    this.element  = $(element);
+    this.callback = callback;
+
+    this.lastValue = this.getValue();
+    if (this.element.tagName.toLowerCase() == 'form')
+      this.registerFormCallbacks();
+    else
+      this.registerCallback(this.element);
+  },
+
+  onElementEvent: function() {
+    var value = this.getValue();
+    if (this.lastValue != value) {
+      this.callback(this.element, value);
+      this.lastValue = value;
+    }
+  },
+
+  registerFormCallbacks: function() {
+    Form.getElements(this.element).each(this.registerCallback.bind(this));
+  },
+
+  registerCallback: function(element) {
+    if (element.type) {
+      switch (element.type.toLowerCase()) {
+        case 'checkbox':
+        case 'radio':
+          Event.observe(element, 'click', this.onElementEvent.bind(this));
+          break;
+        default:
+          Event.observe(element, 'change', this.onElementEvent.bind(this));
+          break;
+      }
+    }
+  }
+}
+
+Form.Element.EventObserver = Class.create();
+Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+  getValue: function() {
+    return Form.Element.getValue(this.element);
+  }
+});
+
+Form.EventObserver = Class.create();
+Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+  getValue: function() {
+    return Form.serialize(this.element);
+  }
+});
+if (!window.Event) {
+  var Event = new Object();
+}
+
+Object.extend(Event, {
+  KEY_BACKSPACE: 8,
+  KEY_TAB:       9,
+  KEY_RETURN:   13,
+  KEY_ESC:      27,
+  KEY_LEFT:     37,
+  KEY_UP:       38,
+  KEY_RIGHT:    39,
+  KEY_DOWN:     40,
+  KEY_DELETE:   46,
+  KEY_HOME:     36,
+  KEY_END:      35,
+  KEY_PAGEUP:   33,
+  KEY_PAGEDOWN: 34,
+
+  element: function(event) {
+    return event.target || event.srcElement;
+  },
+
+  isLeftClick: function(event) {
+    return (((event.which) && (event.which == 1)) ||
+            ((event.button) && (event.button == 1)));
+  },
+
+  pointerX: function(event) {
+    return event.pageX || (event.clientX +
+      (document.documentElement.scrollLeft || document.body.scrollLeft));
+  },
+
+  pointerY: function(event) {
+    return event.pageY || (event.clientY +
+      (document.documentElement.scrollTop || document.body.scrollTop));
+  },
+
+  stop: function(event) {
+    if (event.preventDefault) {
+      event.preventDefault();
+      event.stopPropagation();
+    } else {
+      event.returnValue = false;
+      event.cancelBubble = true;
+    }
+  },
+
+  // find the first node with the given tagName, starting from the
+  // node the event was triggered on; traverses the DOM upwards
+  findElement: function(event, tagName) {
+    var element = Event.element(event);
+    while (element.parentNode && (!element.tagName ||
+        (element.tagName.toUpperCase() != tagName.toUpperCase())))
+      element = element.parentNode;
+    return element;
+  },
+
+  observers: false,
+
+  _observeAndCache: function(element, name, observer, useCapture) {
+    if (!this.observers) this.observers = [];
+    if (element.addEventListener) {
+      this.observers.push([element, name, observer, useCapture]);
+      element.addEventListener(name, observer, useCapture);
+    } else if (element.attachEvent) {
+      this.observers.push([element, name, observer, useCapture]);
+      element.attachEvent('on' + name, observer);
+    }
+  },
+
+  unloadCache: function() {
+    if (!Event.observers) return;
+    for (var i = 0, length = Event.observers.length; i < length; i++) {
+      Event.stopObserving.apply(this, Event.observers[i]);
+      Event.observers[i][0] = null;
+    }
+    Event.observers = false;
+  },
+
+  observe: function(element, name, observer, useCapture) {
+    element = $(element);
+    useCapture = useCapture || false;
+
+    if (name == 'keypress' &&
+        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
+        || element.attachEvent))
+      name = 'keydown';
+
+    Event._observeAndCache(element, name, observer, useCapture);
+  },
+
+  stopObserving: function(element, name, observer, useCapture) {
+    element = $(element);
+    useCapture = useCapture || false;
+
+    if (name == 'keypress' &&
+        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
+        || element.detachEvent))
+      name = 'keydown';
+
+    if (element.removeEventListener) {
+      element.removeEventListener(name, observer, useCapture);
+    } else if (element.detachEvent) {
+      try {
+        element.detachEvent('on' + name, observer);
+      } catch (e) {}
+    }
+  }
+});
+
+/* prevent memory leaks in IE */
+if (navigator.appVersion.match(/\bMSIE\b/))
+  Event.observe(window, 'unload', Event.unloadCache, false);
+var Position = {
+  // set to true if needed, warning: firefox performance problems
+  // NOT neeeded for page scrolling, only if draggable contained in
+  // scrollable elements
+  includeScrollOffsets: false,
+
+  // must be called before calling withinIncludingScrolloffset, every time the
+  // page is scrolled
+  prepare: function() {
+    this.deltaX =  window.pageXOffset
+                || document.documentElement.scrollLeft
+                || document.body.scrollLeft
+                || 0;
+    this.deltaY =  window.pageYOffset
+                || document.documentElement.scrollTop
+                || document.body.scrollTop
+                || 0;
+  },
+
+  realOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.scrollTop  || 0;
+      valueL += element.scrollLeft || 0;
+      element = element.parentNode;
+    } while (element);
+    return [valueL, valueT];
+  },
+
+  cumulativeOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+    } while (element);
+    return [valueL, valueT];
+  },
+
+  positionedOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+      if (element) {
+        if(element.tagName=='BODY') break;
+        var p = Element.getStyle(element, 'position');
+        if (p == 'relative' || p == 'absolute') break;
+      }
+    } while (element);
+    return [valueL, valueT];
+  },
+
+  offsetParent: function(element) {
+    if (element.offsetParent) return element.offsetParent;
+    if (element == document.body) return element;
+
+    while ((element = element.parentNode) && element != document.body)
+      if (Element.getStyle(element, 'position') != 'static')
+        return element;
+
+    return document.body;
+  },
+
+  // caches x/y coordinate pair to use with overlap
+  within: function(element, x, y) {
+    if (this.includeScrollOffsets)
+      return this.withinIncludingScrolloffsets(element, x, y);
+    this.xcomp = x;
+    this.ycomp = y;
+    this.offset = this.cumulativeOffset(element);
+
+    return (y >= this.offset[1] &&
+            y <  this.offset[1] + element.offsetHeight &&
+            x >= this.offset[0] &&
+            x <  this.offset[0] + element.offsetWidth);
+  },
+
+  withinIncludingScrolloffsets: function(element, x, y) {
+    var offsetcache = this.realOffset(element);
+
+    this.xcomp = x + offsetcache[0] - this.deltaX;
+    this.ycomp = y + offsetcache[1] - this.deltaY;
+    this.offset = this.cumulativeOffset(element);
+
+    return (this.ycomp >= this.offset[1] &&
+            this.ycomp <  this.offset[1] + element.offsetHeight &&
+            this.xcomp >= this.offset[0] &&
+            this.xcomp <  this.offset[0] + element.offsetWidth);
+  },
+
+  // within must be called directly before
+  overlap: function(mode, element) {
+    if (!mode) return 0;
+    if (mode == 'vertical')
+      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
+        element.offsetHeight;
+    if (mode == 'horizontal')
+      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
+        element.offsetWidth;
+  },
+
+  page: function(forElement) {
+    var valueT = 0, valueL = 0;
+
+    var element = forElement;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+
+      // Safari fix
+      if (element.offsetParent==document.body)
+        if (Element.getStyle(element,'position')=='absolute') break;
+
+    } while (element = element.offsetParent);
+
+    element = forElement;
+    do {
+      if (!window.opera || element.tagName=='BODY') {
+        valueT -= element.scrollTop  || 0;
+        valueL -= element.scrollLeft || 0;
+      }
+    } while (element = element.parentNode);
+
+    return [valueL, valueT];
+  },
+
+  clone: function(source, target) {
+    var options = Object.extend({
+      setLeft:    true,
+      setTop:     true,
+      setWidth:   true,
+      setHeight:  true,
+      offsetTop:  0,
+      offsetLeft: 0
+    }, arguments[2] || {})
+
+    // find page position of source
+    source = $(source);
+    var p = Position.page(source);
+
+    // find coordinate system to use
+    target = $(target);
+    var delta = [0, 0];
+    var parent = null;
+    // delta [0,0] will do fine with position: fixed elements,
+    // position:absolute needs offsetParent deltas
+    if (Element.getStyle(target,'position') == 'absolute') {
+      parent = Position.offsetParent(target);
+      delta = Position.page(parent);
+    }
+
+    // correct by body offsets (fixes Safari)
+    if (parent == document.body) {
+      delta[0] -= document.body.offsetLeft;
+      delta[1] -= document.body.offsetTop;
+    }
+
+    // set position
+    if(options.setLeft)   target.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
+    if(options.setTop)    target.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
+    if(options.setWidth)  target.style.width = source.offsetWidth + 'px';
+    if(options.setHeight) target.style.height = source.offsetHeight + 'px';
+  },
+
+  absolutize: function(element) {
+    element = $(element);
+    if (element.style.position == 'absolute') return;
+    Position.prepare();
+
+    var offsets = Position.positionedOffset(element);
+    var top     = offsets[1];
+    var left    = offsets[0];
+    var width   = element.clientWidth;
+    var height  = element.clientHeight;
+
+    element._originalLeft   = left - parseFloat(element.style.left  || 0);
+    element._originalTop    = top  - parseFloat(element.style.top || 0);
+    element._originalWidth  = element.style.width;
+    element._originalHeight = element.style.height;
+
+    element.style.position = 'absolute';
+    element.style.top    = top + 'px';
+    element.style.left   = left + 'px';
+    element.style.width  = width + 'px';
+    element.style.height = height + 'px';
+  },
+
+  relativize: function(element) {
+    element = $(element);
+    if (element.style.position == 'relative') return;
+    Position.prepare();
+
+    element.style.position = 'relative';
+    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
+    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
+
+    element.style.top    = top + 'px';
+    element.style.left   = left + 'px';
+    element.style.height = element._originalHeight;
+    element.style.width  = element._originalWidth;
+  }
+}
+
+// Safari returns margins on body which is incorrect if the child is absolutely
+// positioned.  For performance reasons, redefine Position.cumulativeOffset for
+// KHTML/WebKit only.
+if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
+  Position.cumulativeOffset = function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      if (element.offsetParent == document.body)
+        if (Element.getStyle(element, 'position') == 'absolute') break;
+
+      element = element.offsetParent;
+    } while (element);
+
+    return [valueL, valueT];
+  }
+}
+
+Element.addMethods();
\ No newline at end of file

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/css/grid.xcss
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/css/grid.xcss	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/css/grid.xcss	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,320 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<f:template 
+	xmlns:f="http://jsf.exadel.com/template"
+   	xmlns:u="http://jsf.exadel.com/template/util" 
+   	xmlns="http://www.w3.org/1999/xhtml">	
+
+
+/*
+	XPTable
+*/
+
+<u:selector name=".ClientUI_Grid_HC .sort-asc">
+		<u:style name="background-image">
+			<f:resource f:key="/org/richfaces/renderkit/html/images/sort_asc.gif"/>
+		</u:style>
+</u:selector>
+
+<u:selector name=".ClientUI_Grid_HC .sort-desc">
+		<u:style name="background-image">
+			<f:resource f:key="/org/richfaces/renderkit/html/images/sort_desc.gif"/>
+		</u:style>
+</u:selector>
+
+<u:selector name=".ClientUI_Grid_HSep">
+		<u:style name="background-image">
+			<f:resource f:key="/org/richfaces/renderkit/html/images/grid-split.gif"/>
+		</u:style>
+</u:selector>
+
+<u:selector name=".CustomHeaderVSep">
+		<u:style name="background-image">
+			<f:resource f:key="/org/richfaces/renderkit/html/images/grid-split.gif"/>
+		</u:style>
+</u:selector>
+
+<u:selector name=".CustomHeaderHSep">
+		<u:style name="background-image">
+			<f:resource f:key="/org/richfaces/renderkit/html/images/grid-split-h.gif"/>
+		</u:style>
+</u:selector>
+
+
+<f:verbatim>
+
+<![CDATA[
+
+.ClientUI_Grid {
+	font: menu;
+	background-color: white;
+	padding: 0px 0px;
+	margin: 0px;
+	border-collapse: collapse;
+}
+
+/**
+ * ---------------------------------------------
+ *	Grid Header declaration
+ * ---------------------------------------------
+ */
+
+/**
+ * Header row
+ */
+.ClientUI_Grid_HR {
+	background-color: #ebeadb;
+	height: 22px;
+	width: 10000px;
+	z-index:2;
+}
+	
+/**
+ * Header cell
+ */
+.ClientUI_Grid_HC {
+	-o-text-overflow: ellipsis;
+	text-overflow: ellipsis;	
+	font: normal 8pt arial;
+	background-color: #ebeadb;
+	box-sizing: border-box;
+	-moz-box-sizing: border-box;
+	border-bottom: 1px solid #cbc7b8;
+}
+
+.ClientUI_Grid_HC .sort-asc {
+/*	background-image: url(sort_asc.gif);*/
+	background-position: right;
+	background-repeat: no-repeat;
+	display: none;
+	height: 14px;
+	width: 16px;
+}
+
+.ClientUI_Grid_HC .sort-desc {
+/*	background-image: url(sort_desc.gif);*/
+	background-position: right;
+	background-repeat: no-repeat;
+	display: none;
+	height: 14px;
+	width: 16px;
+}
+
+/**
+ * Header cells separator
+ */
+.ClientUI_Grid_HSep {
+	
+/*	background-image: url(grid-split.gif);*/
+	background-position: center;
+	background-repeat: repeat-y;
+	cursor: e-resize;	
+	font-size: 1px;
+	top: 15%;
+	width: 6px;
+	height: 70%;
+}
+.ClientUI_Grid_HSplit {
+	width:1px;
+	border-right:1px dashed #6593cf;
+	background:none;
+	cursor: col-resize;
+}
+
+/**
+ * Header cell body
+ */
+.ClientUI_Grid_HCBody {
+	cursor: default;
+	font: normal 8pt arial;
+	padding: 3px 5px;
+	white-space: nowrap;
+	border-bottom: 1px solid #d6d2c2;
+}
+.ClientUI_Grid_HCBodyContent {
+	cursor: default;
+	font: normal 8pt arial;
+	padding: 1px 2px;
+	white-space: nowrap;
+}
+.ClientUI_Grid_HCBody span {
+	font: normal 8pt arial;
+	white-space: nowrap; 
+}
+
+.ClientUI_Grid_HC_Over{
+	border-bottom: 1px solid #fcc247;
+}
+
+.ClientUI_Grid_HC_Over .ClientUI_Grid_HCBody{
+	background-color: #faf9f4;
+	border-bottom: 1px solid #f9a900;
+}
+
+/**
+ * ---------------------------------------------
+ * Grid Footer declaration
+ * ---------------------------------------------
+ */
+ 
+/**
+ * Footer row
+ */
+.ClientUI_Grid_FR {
+	background-color: #fcfaf6;
+	font: normal 8pt arial;
+	height: 22px;
+	width: 10000px;
+	z-index:2;
+}
+
+/**
+ * Footer cell
+ */
+.ClientUI_Grid_FC {
+	-o-text-overflow: ellipsis;
+	text-overflow: ellipsis;	
+	font: normal 8pt arial;
+	cursor: default;
+	box-sizing: border-box;
+	-moz-box-sizing: border-box;
+	-moz-outline: none;
+	-moz-user-focus: normal;
+	border-right: 1px solid #f1efe2;
+	border-top: 1px solid #cbc7b8;
+}
+
+/**
+ * Footer cell body
+ */
+.ClientUI_Grid_FCBody {
+	cursor: default;
+	font: normal 8pt arial;
+	padding: 3px 5px;
+	white-space: nowrap;
+}
+.ClientUI_Grid_FCBodyContent {
+	cursor: default;
+	font: normal 8pt arial;
+	padding: 1px 2px;
+	white-space: nowrap;
+}
+.ClientUI_Grid_FCBody span {
+	font: normal 8pt arial;
+	white-space: nowrap; 
+}
+
+/**
+ * ---------------------------------------------
+ * Body styles
+ * ---------------------------------------------
+ */
+ 
+/**
+ * Body cell declaration
+ * ClientUI_Grid_BC - reqired
+ * ClientUI_Grid_BCDef - default, used if no custom styles defined in body template
+ */
+.ClientUI_Grid_BC {
+	box-sizing: border-box;
+	-moz-box-sizing: border-box;
+	-moz-outline: none;
+	-moz-user-focus: normal;
+	cursor: default;
+	height:21px !important;
+	border-right: 1px solid #f1efe2;	
+}
+.ClientUI_Grid_BCIndex {
+	background-color: #ebeadb;
+	box-sizing: border-box;
+	-moz-box-sizing: border-box;
+	-moz-outline: none;
+	-moz-user-focus: normal;
+	cursor: default;
+	border-left: 1px solid #f1efe2;	
+	border-bottom: 1px solid #cbc7b8; 
+	border-right: 1px solid #cbc7b8;
+}
+
+/**
+ * Body row style
+ */
+.ClientUI_Grid_BR {
+	font: normal 8pt arial;
+	border-bottom: 1px solid #f1efe2;
+	white-space: nowrap;
+	height:21px;
+	width:10000px;
+	box-sizing: border-box;
+	-moz-box-sizing: border-box;
+}
+.ClientUI_Grid_BROdd {
+	background-color: #FFFFFF;
+	font: normal 8pt arial;
+	height: 22px;
+	width: 10000px;
+	z-index:2;
+}
+
+.ClientUI_Grid_BREven {
+	background-color: #fcfaf6;
+	font: normal 8pt arial;
+	height: 22px;
+	width: 10000px;
+	z-index:2;			
+}
+
+.ClientUI_Grid_BCBody {
+	cursor: default;
+	font: normal 8pt arial;
+	padding: 3px 5px;
+	white-space: nowrap;
+}
+
+
+/** 
+--------------------------
+*/
+.CustomHeaderVSep {
+	
+/*	background-image: url(themes/xp/grid-split.gif);*/
+	
+	background-position: top;
+	background-repeat: repeat-y;
+	font-size: 1px;
+	top: 0px;
+	width: 6px;
+	height: 85%;
+	z-index: 3;			
+}
+		
+.CustomHeaderHSep {
+	
+/*	background-image: url(themes/xp/grid-split-h.gif);*/ 
+	
+	background-position: center;
+	background-repeat: repeat-x;
+	font-size: 1px;
+	top: 0px;
+	left: 0px;
+	width: 100%;
+	height: 2px;
+	z-index: 3;
+	position: relative;
+}
+		
+.FirstClolumn {
+	font-weight: bold;
+	color: blue; 
+}
+		
+.RowIndexCell {
+	background-color: #ebeadb;
+	padding: 0 0 0 0;
+	margin: 0 0 0 0;
+	font: normal 8pt arial;
+	border-bottom: 1px solid #cbc7b8; 
+	border-right: 1px solid #cbc7b8;
+}
+]]>
+	</f:verbatim>
+</f:template>
\ No newline at end of file

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/arrow-left-white.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/arrow-left-white.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/arrow-right-white.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/arrow-right-white.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/done.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/done.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/drop-no.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/drop-no.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/drop-yes.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/drop-yes.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/footer-bg.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/footer-bg.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/grid-blue-hd.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/grid-blue-hd.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/grid-blue-split.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/grid-blue-split.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/grid-loading.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/grid-loading.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/grid-split-h.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/grid-split-h.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/grid-split.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/grid-split.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/grid-vista-hd.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/grid-vista-hd.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/invalid_line.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/invalid_line.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/loading.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/loading.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/mso-hd.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/mso-hd.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/nowait.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/nowait.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-first-disabled.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-first-disabled.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-first.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-first.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-last-disabled.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-last-disabled.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-last.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-last.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-next-disabled.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-next-disabled.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-next.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-next.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-prev-disabled.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-prev-disabled.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-prev.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/page-prev.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/pick-button.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/pick-button.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/refresh.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/refresh.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/sort_asc.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/sort_asc.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/sort_desc.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/sort_desc.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/wait.gif
===================================================================
(Binary files differ)


Property changes on: trunk/sandbox/scrollable-grid/src/main/resources/org/richfaces/renderkit/html/images/wait.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/sandbox/scrollable-grid/src/main/templates/README
===================================================================

Added: trunk/sandbox/scrollable-grid/src/main/templates/org/richfaces/scrollable-grid.jspx
===================================================================
--- trunk/sandbox/scrollable-grid/src/main/templates/org/richfaces/scrollable-grid.jspx	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/main/templates/org/richfaces/scrollable-grid.jspx	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<f:root 
+	xmlns:f="http://ajax4jsf.org/cdk/template" 
+	xmlns:c=" http://java.sun.com/jsf/core" 
+	xmlns:ui=" http://ajax4jsf.org/cdk/ui"
+	xmlns:u=" http://ajax4jsf.org/cdk/u"
+	xmlns:x=" http://ajax4jsf.org/cdk/x"
+	xmlns:h=" http://ajax4jsf.org/cdk/headers"
+	xmlns:vcp="http://ajax4jsf.org/cdk/vcp"	
+	class="org.richfaces.renderkit.html.ScrollableGridRenderer"
+	baseclass="org.richfaces.renderkit.ScrollableGridBaseRenderer"
+	component="org.richfaces.component.UIScrollableGrid" 
+	>	
+	
+	<h:styles>
+		/org/richfaces/renderkit/html/css/grid.xcss
+	</h:styles>
+	
+	<h:scripts>
+		new org.ajax4jsf.framework.resource.PrototypeScript(),
+		/org/richfaces/renderkit/html/scripts/scrollable-grid.js
+	</h:scripts>
+		
+	<div id="GridContainer" style="width:60%; height:300px;" class="ClientUI_Grid">
+		<div id="GridHeaderTemplate">
+			<table class="TestGridHeader" cellpadding="0" cellspacing="0" border="1">
+				<tr>
+					<td frozen="true" id="testHeaderCell" align="right" valign="middle" width="100px">Column 1</td>
+					<td class="SecondClolumn" width="150px">
+						<table cellpadding="0" cellspacing="0" style="width:100%;">
+							<tr><td style="width:100%;" align="center" class="ClientUI_Grid_HCBodyContent">Column 2</td></tr>
+							<tr>
+								<td style="width:100%;">
+									<table class="ClientUI_Grid_HCBodyContent" cellpadding="0" cellspacing="0" style="width:100%;">
+										<tr>
+											<td style="font-size: 1px;" colspan="3"><div class="CustomHeaderHSep">&#160;</div></td>
+										</tr>
+										<tr>
+											<td class="ClientUI_Grid_HCBodyContent" align="center" width="50%">Column 2.1</td>
+											<td class="CustomHeaderVSep">&#160;&#160;</td>
+											<td class="ClientUI_Grid_HCBodyContent" align="center" width="50%">Column 2.2</td>
+										</tr>
+									</table>							
+								</td>
+							</tr>
+						</table>
+					</td>
+					<td width="100px" align="right" valign="middle">Column 3</td>
+					<td width="100px" valign="middle" align="center">
+						<select id="ColumnFilter" style="width: 90px">
+							<option id="opt1">Select option 1</option>
+							<option id="opt2" selected="true">Select option 2</option>
+							<option id="opt3">Select option 3</option>
+							<option id="opt4">Select option 4</option>
+							<option id="opt5">Select option 5</option>
+							<option id="opt6">Select option 6</option>
+						</select>
+					</td>
+					<td width="100px" align="right" valign="middle">Column 5</td>
+					<td width="100px">
+						<table cellpadding="0" cellspacing="0" style="width:100%;">
+							<tr><td style="width:100%;" align="center" class="ClientUI_Grid_HCBodyContent">Column 6</td></tr>
+							<tr>
+								<td style="width:100%;">
+									<table class="ClientUI_Grid_HCBodyContent" cellpadding="0" cellspacing="0" style="width:100%;">
+										<tr>
+											<td style="font-size: 1px;" colspan="3"><div class="CustomHeaderHSep">&#160;</div></td>
+										</tr>
+										<tr>
+											<td class="ClientUI_Grid_HCBodyContent" align="center" width="50%">Column 6.1</td>
+											<td class="CustomHeaderVSep">&#160;&#160;</td>
+											<td class="ClientUI_Grid_HCBodyContent" align="center" width="50%">Column 6.2</td>
+										</tr>
+									</table>							
+								</td>
+							</tr>
+						</table>
+					</td>
+					<td width="100px" align="right" valign="middle">Column 7</td>
+				</tr>
+			</table>
+		</div>
+		<div id="GridBodyTemplate">
+			<table cellpadding="0" cellspacing="0">
+				<tr>
+					<td class="FirstClolumn">$(0)</td>
+					<td align="center">
+						$(1)
+						<script type="text/javascript">
+							processCellValue("$(cid)", "$(1)");
+						</script>
+					</td>
+					<td>
+						<div style="position:relative; top:-2px;left:0px; width:100%; height: 20px">
+							<select id="dddd$(row)" style="width: 100%; height: 100%; font:normal 8pt arial;">
+								<option value="value 1">text 1</option>
+								<option value="value 2">text 2</option>
+							</select>
+						</div>
+					</td>
+					<td>$(3)</td>
+					<td>
+						<div style="position:relative; top:-2px;left:-2px; width:100%; height: 20px">
+							<input type="text" value="$(4)" style="border:1px solid blue; width: 100%; height: 18px; font:normal 8pt arial;" />
+						</div>
+					</td>
+					<td>$(5)</td>
+					<td>$(6)</td>
+				</tr>
+			</table>
+			<!--
+			<table cellpadding="0" cellspacing="0">
+				<tr>
+					<td>$(index_0)</td>
+					<td align="center">$(index_1)</td>
+					<td>$(index_2)</td>
+					<td>$(index_3)</td>
+					<td>$(index_4)</td>
+					<td>$(index_5)</td>
+					<td>$(index_6)</td>
+				</tr>
+			</table>
+			-->
+		</div>
+		<div id="GridFooterTemplate">
+			<table cellpadding="0" cellspacing="0">
+				<tr>
+					<td style="width: 100px">Footer 1</td>
+					<td style="width: 100px">Footer 2</td>
+					<td style="width: 100px">Footer 3</td>
+					<td style="width: 100px">Footer 4</td>
+					<td style="width: 100px">Footer 5</td>
+					<td style="width: 100px">Footer 6</td>
+					<td style="width: 100px">Footer 7</td>
+				</tr>
+			</table>
+		</div>
+	</div>	
+</f:root>	
+	
+	
+	
\ No newline at end of file

Added: trunk/sandbox/scrollable-grid/src/test/java/org/richfaces/component/JSFComponentTest.java
===================================================================
--- trunk/sandbox/scrollable-grid/src/test/java/org/richfaces/component/JSFComponentTest.java	                        (rev 0)
+++ trunk/sandbox/scrollable-grid/src/test/java/org/richfaces/component/JSFComponentTest.java	2007-04-25 14:14:52 UTC (rev 550)
@@ -0,0 +1,32 @@
+package org.richfaces.component;
+
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+import javax.faces.component.UIComponent;
+
+/**
+ * Unit test for simple Component.
+ */
+public class JSFComponentTest 
+    extends TestCase
+{
+    /**
+     * Create the test case
+     *
+     * @param testName name of the test case
+     */
+    public JSFComponentTest( String testName )
+    {
+        super( testName );
+    }
+
+
+    /**
+     * Rigourous Test :-)
+     */
+    public void testComponent()
+    {
+        assertTrue( true );
+    }
+}




More information about the richfaces-svn-commits mailing list