You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@struts.apache.org by mu...@apache.org on 2007/03/03 06:49:21 UTC

svn commit: r514083 [33/49] - in /struts/struts2/trunk/plugins/dojo: ./ src/ src/main/ src/main/java/ src/main/java/org/ src/main/java/org/apache/ src/main/java/org/apache/struts2/ src/main/java/org/apache/struts2/components/ src/main/java/org/apache/s...

Added: struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/FilteringTable.js
URL: http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/FilteringTable.js?view=auto&rev=514083
==============================================================================
--- struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/FilteringTable.js (added)
+++ struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/FilteringTable.js Fri Mar  2 21:48:54 2007
@@ -0,0 +1,961 @@
+/*
+	Copyright (c) 2004-2006, The Dojo Foundation
+	All Rights Reserved.
+
+	Licensed under the Academic Free License version 2.1 or above OR the
+	modified BSD license. For more information on Dojo licensing, see:
+
+		http://dojotoolkit.org/community/licensing.shtml
+*/
+
+dojo.provide("dojo.widget.FilteringTable");
+
+dojo.require("dojo.date.format");
+dojo.require("dojo.collections.Store");
+dojo.require("dojo.html.*");
+dojo.require("dojo.html.util");
+dojo.require("dojo.html.style");
+dojo.require("dojo.html.selection");
+dojo.require("dojo.event.*");
+dojo.require("dojo.widget.*");
+dojo.require("dojo.widget.HtmlWidget");
+
+dojo.widget.defineWidget(
+	"dojo.widget.FilteringTable", 
+	dojo.widget.HtmlWidget, 
+	function(){
+		//	summary
+		//	Initializes all properties for the widget.
+		this.store=new dojo.collections.Store();
+
+		//declare per instance changeable widget properties
+		this.valueField="Id";
+		this.multiple=false;
+		this.maxSelect=0;
+		this.maxSortable=1;  // how many columns can be sorted at once.
+		this.minRows=0;
+		this.defaultDateFormat = "%D";
+		this.isInitialized=false;
+		this.alternateRows=false;
+
+		this.columns=[];
+		this.sortInformation=[{
+			index:0,
+			direction:0
+		}];
+
+		// CSS definitions
+		this.headClass="";
+		this.tbodyClass="";
+		this.headerClass="";
+		this.headerUpClass="selectedUp";
+		this.headerDownClass="selectedDown";
+		this.rowClass="";
+		this.rowAlternateClass="alt";
+		this.rowSelectedClass="selected";
+		this.columnSelected="sorted-column";
+	},
+{
+	//	dojo widget properties
+	isContainer: false,
+	templatePath: null,
+	templateCssPath: null,
+
+	//	methods.
+	getTypeFromString: function(/* string */s){
+		//	summary
+		//	Gets a function based on the passed string.
+		var parts = s.split("."), i = 0, obj = dj_global; 
+		do{ 
+			obj = obj[parts[i++]]; 
+		} while (i < parts.length && obj); 
+		return (obj != dj_global) ? obj : null;	//	function
+	},
+
+	//	custom data access.
+	getByRow: function(/*HTMLTableRow*/row){
+		//	summary
+		//	Returns the data object based on the passed row.
+		return this.store.getByKey(dojo.html.getAttribute(row, "value"));	//	object
+	},
+	getDataByRow: function(/*HTMLTableRow*/row){
+		//	summary
+		//	Returns the source data object based on the passed row.
+		return this.store.getDataByKey(dojo.html.getAttribute(row, "value")); // object
+	},
+
+	getRow: function(/* Object */ obj){
+		//	summary
+		//	Finds the row in the table based on the passed data object.
+		var rows = this.domNode.tBodies[0].rows;
+		for(var i=0; i<rows.length; i++){
+			if(this.store.getDataByKey(dojo.html.getAttribute(rows[i], "value")) == obj){
+				return rows[i];	//	HTMLTableRow
+			}
+		}
+		return null;	//	HTMLTableRow
+	},
+	getColumnIndex: function(/* string */fieldPath){
+		//	summary
+		//	Returns index of the column that represents the passed field path.
+		for(var i=0; i<this.columns.length; i++){
+			if(this.columns[i].getField() == fieldPath){
+				return i;	//	integer
+			}
+		}
+		return -1;	//	integer
+	},
+
+	getSelectedData: function(){
+		//	summary
+		//	returns all objects that are selected.
+		var data=this.store.get();
+		var a=[];
+		for(var i=0; i<data.length; i++){
+			if(data[i].isSelected){
+				a.push(data[i].src);
+			}
+		}
+		if(this.multiple){
+			return a;		//	array
+		} else {
+			return a[0];	//	object
+		}
+	},
+	
+	isSelected: function(/* object */obj){
+		//	summary
+		//	Returns whether the passed object is currently selected.
+		var data = this.store.get();
+		for(var i=0; i<data.length; i++){
+			if(data[i].src == obj){
+				return true;	//	boolean
+			}
+		}
+		return false;	//	boolean
+	},
+	isValueSelected: function(/* string */val){
+		//	summary
+		//	Returns the object represented by key "val" is selected.
+		var v = this.store.getByKey(val);
+		if(v){
+			return v.isSelected;	//	boolean
+		}
+		return false;	//	boolean
+	},
+	isIndexSelected: function(/* number */idx){
+		//	summary
+		//	Returns the object represented by integer "idx" is selected.
+		var v = this.store.getByIndex(idx);
+		if(v){
+			return v.isSelected;	//	boolean
+		}
+		return false;	//	boolean
+	},
+	isRowSelected: function(/* HTMLTableRow */row){
+		//	summary
+		//	Returns if the passed row is selected.
+		var v = this.getByRow(row);
+		if(v){
+			return v.isSelected;	//	boolean
+		}
+		return false;	//	boolean
+	},
+
+	reset: function(){
+		//	summary
+		//	Resets the widget to its initial internal state.
+		this.store.clearData();
+		this.columns = [];
+		this.sortInformation = [ {index:0, direction:0} ];
+		this.resetSelections();
+		this.isInitialized = false;
+		this.onReset();
+	},
+	resetSelections: function(){
+		//	summary
+		//	Unselects all data objects.
+		this.store.forEach(function(element){
+			element.isSelected = false;
+		});
+	},
+	onReset:function(){ 
+		//	summary
+		//	Stub for onReset event.
+	},
+
+	//	selection and toggle functions
+	select: function(/*object*/ obj){
+		//	summary
+		//	selects the passed object.
+		var data = this.store.get();
+		for(var i=0; i<data.length; i++){
+			if(data[i].src == obj){
+				data[i].isSelected = true;
+				break;
+			}
+		}
+		this.onDataSelect(obj);
+	},
+	selectByValue: function(/*string*/ val){
+		//	summary
+		//	selects the object represented by key "val".
+		this.select(this.store.getDataByKey(val));
+	},
+	selectByIndex: function(/*number*/ idx){
+		//	summary
+		//	selects the object represented at index "idx".
+		this.select(this.store.getDataByIndex(idx));
+	},
+	selectByRow: function(/*HTMLTableRow*/ row){
+		//	summary
+		//	selects the object represented by HTMLTableRow row.
+		this.select(this.getDataByRow(row));
+	},
+	selectAll: function(){
+		//	summary
+		//	selects all objects.
+		this.store.forEach(function(element){
+			element.isSelected = true;
+		});
+	},
+	onDataSelect: function(/* object */obj){ 
+		//	summary
+		//	Stub for onDataSelect event.
+	},
+
+	toggleSelection: function(/*object*/obj){
+		//	summary
+		//	Flips the selection state of passed obj.
+		var data = this.store.get();
+		for(var i=0; i<data.length; i++){
+			if(data[i].src == obj){
+				data[i].isSelected = !data[i].isSelected;
+				break;
+			}
+		}
+		this.onDataToggle(obj);
+	},
+	toggleSelectionByValue: function(/*string*/val){
+		//	summary
+		//	Flips the selection state of object represented by val.
+		this.toggleSelection(this.store.getDataByKey(val));
+	},
+	toggleSelectionByIndex: function(/*number*/idx){
+		//	summary
+		//	Flips the selection state of object at index idx.
+		this.toggleSelection(this.store.getDataByIndex(idx));
+	},
+	toggleSelectionByRow: function(/*HTMLTableRow*/row){
+		//	summary
+		//	Flips the selection state of object represented by row.
+		this.toggleSelection(this.getDataByRow(row));
+	},
+	toggleAll: function(){
+		//	summary
+		//	Flips the selection state of all objects.
+		this.store.forEach(function(element){
+			element.isSelected = !element.isSelected;
+		});
+	},
+	onDataToggle: function(/* object */obj){ 
+		//	summary
+		//	Stub for onDataToggle event.
+	},
+
+	//	parsing functions, from HTML to metadata/SimpleStore
+	_meta:{
+		field:null,
+		format:null,
+		filterer:null,
+		noSort:false,
+		sortType:"String",
+		dataType:String,
+		sortFunction:null,
+		filterFunction:null,
+		label:null,
+		align:"left",
+		valign:"middle",
+		getField:function(){ 
+			return this.field || this.label; 
+		},
+		getType:function(){ 
+			return this.dataType; 
+		}
+	},
+	createMetaData: function(/* object */obj){
+		//	summary
+		//	Take a JSON-type structure and make it into a ducktyped metadata object.
+		for(var p in this._meta){
+			//	rudimentary mixin
+			if(!obj[p]){
+				obj[p] = this._meta[p];
+			}
+		}
+		if(!obj.label){
+			obj.label=obj.field;
+		}
+		if(!obj.filterFunction){
+			obj.filterFunction=this._defaultFilter;
+		}
+		return obj;	//	object
+	},
+	parseMetadata: function(/* HTMLTableHead */head){
+		//	summary
+		//	Parses the passed HTMLTableHead element to create meta data.
+		this.columns=[];
+		this.sortInformation=[];
+		var row = head.getElementsByTagName("tr")[0];
+		var cells = row.getElementsByTagName("td");
+		if (cells.length == 0){
+			cells = row.getElementsByTagName("th");
+		}
+		for(var i=0; i<cells.length; i++){
+			var o = this.createMetaData({ });
+			
+			//	presentation attributes
+			if(dojo.html.hasAttribute(cells[i], "align")){
+				o.align = dojo.html.getAttribute(cells[i],"align");
+			}
+			if(dojo.html.hasAttribute(cells[i], "valign")){
+				o.valign = dojo.html.getAttribute(cells[i],"valign");
+			}
+			if(dojo.html.hasAttribute(cells[i], "nosort")){
+				o.noSort = (dojo.html.getAttribute(cells[i],"nosort")=="true");
+			}
+			if(dojo.html.hasAttribute(cells[i], "sortusing")){
+				var trans = dojo.html.getAttribute(cells[i],"sortusing");
+				var f = this.getTypeFromString(trans);
+				if (f != null && f != window && typeof(f)=="function"){
+					o.sortFunction=f;
+				}
+			}
+			o.label = dojo.html.renderedTextContent(cells[i]);
+			if(dojo.html.hasAttribute(cells[i], "field")){
+				o.field=dojo.html.getAttribute(cells[i],"field");
+			} else if(o.label.length > 0){
+				o.field=o.label;
+			} else {
+				o.field = "field" + i;
+			}
+			if(dojo.html.hasAttribute(cells[i], "format")){
+				o.format=dojo.html.getAttribute(cells[i],"format");
+			}
+			if(dojo.html.hasAttribute(cells[i], "dataType")){
+				var sortType = dojo.html.getAttribute(cells[i],"dataType");
+				if(sortType.toLowerCase()=="html" || sortType.toLowerCase()=="markup"){
+					o.sortType = "__markup__";	//	always convert to "__markup__"
+				}else{
+					var type = this.getTypeFromString(sortType);
+					if(type){
+						o.sortType = sortType;
+						o.dataType = type;
+					}
+				}
+			}
+
+			//	TODO: set up filtering mechanisms here.
+			if(dojo.html.hasAttribute(cells[i], "filterusing")){
+				var trans = dojo.html.getAttribute(cells[i],"filterusing");
+				var f = this.getTypeFromString(trans);
+				if (f != null && f != window && typeof(f)=="function"){
+					o.filterFunction=f;
+				}
+			}
+			
+			this.columns.push(o);
+
+			//	check to see if there's a default sort, and set the properties necessary
+			if(dojo.html.hasAttribute(cells[i], "sort")){
+				var info = {
+					index:i,
+					direction:0
+				};
+				var dir = dojo.html.getAttribute(cells[i], "sort");
+				if(!isNaN(parseInt(dir))){
+					dir = parseInt(dir);
+					info.direction = (dir != 0) ? 1 : 0;
+				}else{
+					info.direction = (dir.toLowerCase() == "desc") ? 1 : 0;
+				}
+				this.sortInformation.push(info);
+			}
+		}
+		if(this.sortInformation.length == 0){
+			this.sortInformation.push({
+				index:0,
+				direction:0
+			});
+		} else if (this.sortInformation.length > this.maxSortable){
+			this.sortInformation.length = this.maxSortable;
+		}
+	},
+	parseData: function(/* HTMLTableBody */body){
+		//	summary
+		//	Parse HTML data into native JSON structure for the store.
+		if(body.rows.length == 0 && this.columns.length == 0){
+			return;	//	there's no data, ignore me.
+		}
+
+		//	create a data constructor based on what we've got for the fields.
+		var self=this;
+		this["__selected__"] = [];
+		var arr = this.store.getFromHtml(this.columns, body, function(obj, row){
+			obj[self.valueField] = dojo.html.getAttribute(row, "value");
+			if(dojo.html.getAttribute(row, "selected")=="true"){
+				self["__selected__"].push(obj);
+			}
+		});
+		this.store.setData(arr);
+		for(var i=0; i<this["__selected__"].length; i++){
+			this.select(this["__selected__"][i]);
+		}
+		this.renderSelections();
+
+		delete this["__selected__"];
+
+		//	say that we are already initialized so that we don't kill anything
+		this.isInitialized=true;
+	},
+
+	//	standard events
+	onSelect: function(/* HTMLEvent */e){
+		//	summary
+		//	Handles the onclick event of any element.
+		var row = dojo.html.getParentByType(e.target,"tr");
+		if(dojo.html.hasAttribute(row,"emptyRow")){
+			return;
+		}
+		var body = dojo.html.getParentByType(row,"tbody");
+		if(this.multiple){
+			if(e.shiftKey){
+				var startRow;
+				var rows=body.rows;
+				for(var i=0;i<rows.length;i++){
+					if(rows[i]==row){
+						break;
+					}
+					if(this.isRowSelected(rows[i])){
+						startRow=rows[i];
+					}
+				}
+				if(!startRow){
+					startRow = row;
+					for(; i<rows.length; i++){
+						if(this.isRowSelected(rows[i])){
+							row = rows[i];
+							break;
+						}
+					}
+				}
+				this.resetSelections();
+				if(startRow == row){
+					this.toggleSelectionByRow(row);
+				} else {
+					var doSelect = false;
+					for(var i=0; i<rows.length; i++){
+						if(rows[i] == startRow){
+							doSelect=true;
+						}
+						if(doSelect){
+							this.selectByRow(rows[i]);
+						}
+						if(rows[i] == row){
+							doSelect = false;
+						}
+					}
+				}
+			} else {
+				this.toggleSelectionByRow(row);
+			}
+		} else {
+			this.resetSelections();
+			this.toggleSelectionByRow(row);
+		}
+		this.renderSelections();
+	},
+	onSort: function(/* HTMLEvent */e){
+		//	summary
+		//	Sort the table based on the column selected.
+		var oldIndex=this.sortIndex;
+		var oldDirection=this.sortDirection;
+		
+		var source=e.target;
+		var row=dojo.html.getParentByType(source,"tr");
+		var cellTag="td";
+		if(row.getElementsByTagName(cellTag).length==0){
+			cellTag="th";
+		}
+
+		var headers=row.getElementsByTagName(cellTag);
+		var header=dojo.html.getParentByType(source,cellTag);
+		
+		for(var i=0; i<headers.length; i++){
+			dojo.html.setClass(headers[i], this.headerClass);
+			if(headers[i]==header){
+				if(this.sortInformation[0].index != i){
+					this.sortInformation.unshift({ 
+						index:i, 
+						direction:0
+					});
+				} else {
+					this.sortInformation[0] = {
+						index:i,
+						direction:(~this.sortInformation[0].direction)&1
+					};
+				}
+			}
+		}
+
+		this.sortInformation.length = Math.min(this.sortInformation.length, this.maxSortable);
+		for(var i=0; i<this.sortInformation.length; i++){
+			var idx=this.sortInformation[i].index;
+			var dir=(~this.sortInformation[i].direction)&1;
+			dojo.html.setClass(headers[idx], dir==0?this.headerDownClass:this.headerUpClass);
+		}
+		this.render();
+	},
+	onFilter: function(){
+		//	summary
+		//	show or hide rows based on the parameters of the passed filter.
+	},
+
+	//	Filtering methods
+	_defaultFilter: function(/* Object */obj){
+		//	summary
+		//	Always return true as the result of the default filter.
+		return true;
+	},
+	setFilter: function(/* string */field, /* function */fn){
+		//	summary
+		//	set a filtering function on the passed field.
+		for(var i=0; i<this.columns.length; i++){
+			if(this.columns[i].getField() == field){
+				this.columns[i].filterFunction=fn;
+				break;
+			}
+		}
+		this.applyFilters();
+	},
+	setFilterByIndex: function(/* number */idx, /* function */fn){
+		//	summary
+		//	set a filtering function on the passed column index.
+		this.columns[idx].filterFunction=fn;
+		this.applyFilters();
+	},
+	clearFilter: function(/* string */field){
+		//	summary
+		//	clear a filtering function on the passed field.
+		for(var i=0; i<this.columns.length; i++){
+			if(this.columns[i].getField() == field){
+				this.columns[i].filterFunction=this._defaultFilter;
+				break;
+			}
+		}
+		this.applyFilters();
+	}, 
+	clearFilterByIndex: function(/* number */idx){
+		//	summary
+		//	clear a filtering function on the passed column index.
+		this.columns[idx].filterFunction=this._defaultFilter;
+		this.applyFilters();
+	}, 
+	clearFilters: function(){
+		//	summary
+		//	clears all filters.
+		for(var i=0; i<this.columns.length; i++){
+			this.columns[i].filterFunction=this._defaultFilter;
+		}
+		//	we'll do the clear manually, it will be faster.
+		var rows=this.domNode.tBodies[0].rows;
+		for(var i=0; i<rows.length; i++){
+			rows[i].style.display="";
+			if(this.alternateRows){
+				dojo.html[((i % 2 == 1)?"addClass":"removeClass")](rows[i], this.rowAlternateClass);
+			}
+		}
+		this.onFilter();
+	},
+	applyFilters: function(){
+		//	summary
+		//	apply all filters to the table.
+		var alt=0;
+		var rows=this.domNode.tBodies[0].rows;
+		for(var i=0; i<rows.length; i++){
+			var b=true;
+			var row=rows[i];
+			for(var j=0; j<this.columns.length; j++){
+				var value = this.store.getField(this.getDataByRow(row), this.columns[j].getField());
+				if(this.columns[j].getType() == Date && value != null && !value.getYear){
+					value = new Date(value);
+				}
+				if(!this.columns[j].filterFunction(value)){
+					b=false;
+					break;
+				}
+			}
+			row.style.display=(b?"":"none");
+			if(b && this.alternateRows){
+				dojo.html[((alt++ % 2 == 1)?"addClass":"removeClass")](row, this.rowAlternateClass);
+			}
+		}
+		this.onFilter();
+	},
+
+	//	sorting functionality
+	createSorter: function(/* array */info){
+		//	summary
+		//	creates a custom function to be used for sorting.
+		var self=this;
+		var sortFunctions=[];	//	our function stack.
+	
+		function createSortFunction(fieldIndex, dir){
+			var meta=self.columns[fieldIndex];
+			var field=meta.getField();
+			return function(rowA, rowB){
+				if(dojo.html.hasAttribute(rowA,"emptyRow") || dojo.html.hasAttribute(rowB,"emptyRow")){
+					return -1;
+				}
+				//	TODO: check for markup and compare by rendered text.
+				var a = self.store.getField(self.getDataByRow(rowA), field);
+				var b = self.store.getField(self.getDataByRow(rowB), field);
+				var ret = 0;
+				if(a > b) ret = 1;
+				if(a < b) ret = -1;
+				return dir * ret;
+			}
+		}
+
+		var current=0;
+		var max = Math.min(info.length, this.maxSortable, this.columns.length);
+		while(current < max){
+			var direction = (info[current].direction == 0) ? 1 : -1;
+			sortFunctions.push(
+				createSortFunction(info[current].index, direction)
+			);
+			current++;
+		}
+
+		return function(rowA, rowB){
+			var idx=0;
+			while(idx < sortFunctions.length){
+				var ret = sortFunctions[idx++](rowA, rowB);
+				if(ret != 0) return ret;
+			}
+			//	if we got here then we must be equal.
+			return 0; 	
+		};	//	function
+	},
+
+	//	rendering
+	createRow: function(/* object */obj){
+		//	summary
+		//	Create an HTML row based on the passed object
+		var row=document.createElement("tr");
+		dojo.html.disableSelection(row);
+		if(obj.key != null){
+			row.setAttribute("value", obj.key);
+		}
+		for(var j=0; j<this.columns.length; j++){
+			var cell=document.createElement("td");
+			cell.setAttribute("align", this.columns[j].align);
+			cell.setAttribute("valign", this.columns[j].valign);
+			dojo.html.disableSelection(cell);
+			var val = this.store.getField(obj.src, this.columns[j].getField());
+			if(typeof(val)=="undefined"){
+				val="";
+			}
+			this.fillCell(cell, this.columns[j], val);
+			row.appendChild(cell);
+		}
+		return row;	//	HTMLTableRow
+	},
+	fillCell: function(/* HTMLTableCell */cell, /* object */meta, /* object */val){
+		//	summary
+		//	Fill the passed cell with value, based on the passed meta object.
+		if(meta.sortType=="__markup__"){
+			cell.innerHTML=val;
+		} else {
+			if(meta.getType()==Date) {
+				val=new Date(val);
+				if(!isNaN(val)){
+					var format = this.defaultDateFormat;
+					if(meta.format){
+						format = meta.format;
+					}
+					cell.innerHTML = dojo.date.strftime(val, format);
+				} else {
+					cell.innerHTML = val;
+				}
+			} else if ("Number number int Integer float Float".indexOf(meta.getType())>-1){
+				//	TODO: number formatting
+				if(val.length == 0){
+					val="0";
+				}
+				var n = parseFloat(val, 10) + "";
+				//	TODO: numeric formatting + rounding :)
+				if(n.indexOf(".")>-1){
+					n = dojo.math.round(parseFloat(val,10),2);
+				}
+				cell.innerHTML = n;
+			}else{
+				cell.innerHTML = val;
+			}
+		}
+	},
+	prefill: function(){
+		//	summary
+		//	if there's no data in the table, then prefill it with this.minRows.
+		this.isInitialized = false;
+		var body = this.domNode.tBodies[0];
+		while (body.childNodes.length > 0){
+			body.removeChild(body.childNodes[0]);
+		}
+		
+		if(this.minRows>0){
+			for(var i=0; i < this.minRows; i++){
+				var row = document.createElement("tr");
+				if(this.alternateRows){
+					dojo.html[((i % 2 == 1)?"addClass":"removeClass")](row, this.rowAlternateClass);
+				}
+				row.setAttribute("emptyRow","true");
+				for(var j=0; j<this.columns.length; j++){
+					var cell = document.createElement("td");
+					cell.innerHTML = "&nbsp;";
+					row.appendChild(cell);
+				}
+				body.appendChild(row);
+			}
+		}
+	},
+	init: function(){
+		//	summary
+		//	initializes the table of data
+		this.isInitialized=false;
+
+		//	if there is no thead, create it now.
+		var head=this.domNode.getElementsByTagName("thead")[0];
+		if(head.getElementsByTagName("tr").length == 0){
+			//	render the column code.
+			var row=document.createElement("tr");
+			for(var i=0; i<this.columns.length; i++){
+				var cell=document.createElement("td");
+				cell.setAttribute("align", this.columns[i].align);
+				cell.setAttribute("valign", this.columns[i].valign);
+				dojo.html.disableSelection(cell);
+				cell.innerHTML=this.columns[i].label;
+				row.appendChild(cell);
+
+				//	attach the events.
+				if(!this.columns[i].noSort){
+					dojo.event.connect(cell, "onclick", this, "onSort");
+				}
+			}
+			dojo.html.prependChild(row, head);
+		}
+		
+		if(this.store.get().length == 0){
+			return false;
+		}
+
+		var idx=this.domNode.tBodies[0].rows.length;
+		if(!idx || idx==0 || this.domNode.tBodies[0].rows[0].getAttribute("emptyRow")=="true"){
+			idx = 0;
+			var body = this.domNode.tBodies[0];
+			while(body.childNodes.length>0){
+				body.removeChild(body.childNodes[0]);
+			}
+
+			var data = this.store.get();
+			for(var i=0; i<data.length; i++){
+				var row = this.createRow(data[i]);
+				dojo.event.connect(row, "onclick", this, "onSelect");
+				body.appendChild(row);
+				idx++;
+			}
+		}
+
+		//	add empty rows
+		if(this.minRows > 0 && idx < this.minRows){
+			idx = this.minRows - idx;
+			for(var i=0; i<idx; i++){
+				row=document.createElement("tr");
+				row.setAttribute("emptyRow","true");
+				for(var j=0; j<this.columns.length; j++){
+					cell=document.createElement("td");
+					cell.innerHTML="&nbsp;";
+					row.appendChild(cell);
+				}
+				body.appendChild(row);
+			}
+		}
+
+		//	last but not least, show any columns that have sorting already on them.
+		var row=this.domNode.getElementsByTagName("thead")[0].rows[0];
+		var cellTag="td";
+		if(row.getElementsByTagName(cellTag).length==0) cellTag="th";
+		var headers=row.getElementsByTagName(cellTag);
+		for(var i=0; i<headers.length; i++){
+			dojo.html.setClass(headers[i], this.headerClass);
+		}
+		for(var i=0; i<this.sortInformation.length; i++){
+			var idx=this.sortInformation[i].index;
+			var dir=(~this.sortInformation[i].direction)&1;
+			dojo.html.setClass(headers[idx], dir==0?this.headerDownClass:this.headerUpClass);
+		}
+
+		this.isInitialized=true;
+		return this.isInitialized;
+	},
+	render: function(){
+		//	summary
+		//	Renders the actual table data.
+
+	/*	The method that should be called once underlying changes
+	 *	are made, including sorting, filtering, data changes.
+	 *	Rendering the selections themselves are a different method,
+	 *	which render() will call as the last step.
+	 ****************************************************************/
+		if(!this.isInitialized){
+			var b = this.init();
+			if(!b){
+				this.prefill();
+				return;
+			}
+		}
+		
+		//	do the sort
+		var rows=[];
+		var body=this.domNode.tBodies[0];
+		var emptyRowIdx=-1;
+		for(var i=0; i<body.rows.length; i++){
+			rows.push(body.rows[i]);
+		}
+
+		//	build the sorting function, and do the sorting.
+		var sortFunction = this.createSorter(this.sortInformation);
+		if(sortFunction){
+			rows.sort(sortFunction);
+		}
+
+		//	append the rows without killing them, this should help with the HTML problems.
+		for(var i=0; i<rows.length; i++){
+			if(this.alternateRows){
+				dojo.html[((i%2==1)?"addClass":"removeClass")](rows[i], this.rowAlternateClass);
+			}
+			dojo.html[(this.isRowSelected(body.rows[i])?"addClass":"removeClass")](body.rows[i], this.rowSelectedClass);
+			body.appendChild(rows[i]);
+		}
+	},
+	renderSelections: function(){
+		//	summary
+		//	Render all selected objects using CSS.
+		var body=this.domNode.tBodies[0];
+		for(var i=0; i<body.rows.length; i++){
+			dojo.html[(this.isRowSelected(body.rows[i])?"addClass":"removeClass")](body.rows[i], this.rowSelectedClass);
+		}
+	},
+
+	//	widget lifetime handlers
+	initialize: function(){ 
+		//	summary
+		//	Initializes the widget.
+		var self=this;
+		//	connect up binding listeners here.
+		dojo.event.connect(this.store, "onSetData", function(){
+			self.store.forEach(function(element){
+				element.isSelected = false;
+			});
+			self.isInitialized=false;
+			var body = self.domNode.tBodies[0];
+			if(body){
+				while(body.childNodes.length>0){
+					body.removeChild(body.childNodes[0]);
+				}
+			}
+			self.render();
+		});
+		dojo.event.connect(this.store, "onClearData", function(){
+			self.render();
+		});
+		dojo.event.connect(this.store, "onAddData", function(addedObject){
+			var row=self.createRow(addedObject);
+			dojo.event.connect(row, "onclick", self, "onSelect");
+			self.domNode.tBodies[0].appendChild(row);
+			self.render();
+		});
+		dojo.event.connect(this.store, "onAddDataRange", function(arr){
+			for(var i=0; i<arr.length; i++){
+				arr[i].isSelected=false;
+				var row=self.createRow(arr[i]);
+				dojo.event.connect(row, "onclick", self, "onSelect");
+				self.domNode.tBodies[0].appendChild(row);
+			};
+			self.render();
+		});
+		dojo.event.connect(this.store, "onRemoveData", function(removedObject){
+			var rows = self.domNode.tBodies[0].rows;
+			for(var i=0; i<rows.length; i++){
+				if(self.getDataByRow(rows[i]) == removedObject.src){
+					rows[i].parentNode.removeChild(rows[i]);
+					break;
+				}
+			}
+			self.render();
+		});
+		dojo.event.connect(this.store, "onUpdateField", function(obj, fieldPath, val){
+			var row = self.getRow(obj);
+			var idx = self.getColumnIndex(fieldPath);
+			if(row && row.cells[idx] && self.columns[idx]){
+				self.fillCell(row.cells[idx], self.columns[idx], val);
+			}
+		});
+	},
+	postCreate: function(){
+		//	summary
+		//	finish widget initialization.
+		this.store.keyField = this.valueField;
+
+		if(this.domNode){
+			//	start by making sure domNode is a table element;
+			if(this.domNode.nodeName.toLowerCase() != "table"){
+			}
+
+			//	see if there is columns set up already
+			if(this.domNode.getElementsByTagName("thead")[0]){
+				var head=this.domNode.getElementsByTagName("thead")[0];
+				if(this.headClass.length > 0){
+					head.className = this.headClass;
+				}
+				dojo.html.disableSelection(this.domNode);
+				this.parseMetadata(head);
+
+				var header="td";
+				if(head.getElementsByTagName(header).length==0){
+					header="th";
+				}
+				var headers = head.getElementsByTagName(header);
+				for(var i=0; i<headers.length; i++){
+					if(!this.columns[i].noSort){
+						dojo.event.connect(headers[i], "onclick", this, "onSort");
+					}
+				}
+			} else {
+				this.domNode.appendChild(document.createElement("thead"));
+			}
+
+			// if the table doesn't have a tbody already, add one and grab a reference to it
+			if (this.domNode.tBodies.length < 1) {
+				var body = document.createElement("tbody");
+				this.domNode.appendChild(body);
+			} else {
+				var body = this.domNode.tBodies[0];
+			}
+
+			if (this.tbodyClass.length > 0){
+				body.className = this.tbodyClass;
+			}
+			this.parseData(body);
+		}
+	}
+});

Added: struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/FisheyeList.js
URL: http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/FisheyeList.js?view=auto&rev=514083
==============================================================================
--- struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/FisheyeList.js (added)
+++ struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/FisheyeList.js Fri Mar  2 21:48:54 2007
@@ -0,0 +1,742 @@
+/*
+	Copyright (c) 2004-2006, The Dojo Foundation
+	All Rights Reserved.
+
+	Licensed under the Academic Free License version 2.1 or above OR the
+	modified BSD license. For more information on Dojo licensing, see:
+
+		http://dojotoolkit.org/community/licensing.shtml
+*/
+
+dojo.provide("dojo.widget.FisheyeList");
+
+//
+// TODO
+// fix SVG support, and turn it on only if the browser supports it
+// fix really long labels in vertical mode
+//
+
+dojo.require("dojo.widget.*");
+dojo.require("dojo.widget.HtmlWidget");
+dojo.require("dojo.html.style");
+dojo.require("dojo.html.selection");
+dojo.require("dojo.html.util");
+dojo.require("dojo.event.*");
+
+/*
+ * summary
+ *	Menu similar to the fish eye menu on the Mac OS
+ * usage
+ *	<div dojoType="FisheyeList"
+ *	itemWidth="40" itemHeight="40"
+ *	itemMaxWidth="150" itemMaxHeight="150"
+ *	orientation="horizontal"
+ *	effectUnits="2"
+ *	itemPadding="10"
+ *	attachEdge="center"
+ *	labelEdge="bottom">
+ *
+ *		<div dojoType="FisheyeListItem"
+ *			id="item1"
+ *			onclick="alert('click on' + this.caption + '(from widget id ' + this.widgetId + ')!');"
+ *			caption="Item 1"
+ *			iconsrc="images/fisheye_1.png">
+ *		</div>
+ *		...
+ *	</div>
+ */
+dojo.widget.defineWidget(
+	"dojo.widget.FisheyeList",
+	dojo.widget.HtmlWidget,
+function(){
+	this.pos = {x: -1, y: -1};		// current cursor position, relative to the grid
+
+	this.EDGE = {
+		CENTER: 0,
+		LEFT: 1,
+		RIGHT: 2,
+		TOP: 3,
+		BOTTOM: 4
+	};
+	
+	// for conservative trigger mode, when triggered, timerScale is gradually increased from 0 to 1
+	this.timerScale = 1.0;
+
+},
+{
+	templateString: '<div class="dojoHtmlFisheyeListBar"></div>',
+	templateCssPath: dojo.uri.dojoUri("src/widget/templates/FisheyeList.css"),
+
+	isContainer: true,
+	snarfChildDomOutput: true,
+
+	// Integer
+	//	width of menu item (in pixels) in it's dormant state (when the mouse is far away)
+	itemWidth: 40,
+
+	// Integer
+	//	height of menu item (in pixels) in it's dormant state (when the mouse is far away)
+	itemHeight: 40,
+
+	// Integer
+	//	width of menu item (in pixels) in it's fully enlarged state (when the mouse is directly over it)
+	itemMaxWidth: 150,
+
+	// Integer
+	//	height of menu item (in pixels) in it's fully enlarged state (when the mouse is directly over it)
+	itemMaxHeight: 150,
+
+
+	// String
+	//	orientation of the menu, either "horizontal" or "vertical"
+	orientation: 'horizontal',
+
+	// Boolean
+	//	if true, don't start enlarging menu items until mouse is over an image;
+	//	if false, start enlarging menu items as the mouse moves near them.
+	conservativeTrigger: false,
+
+	// Number
+	//	controls how much reaction the menu makes, relative to the distance of the mouse from the menu
+	effectUnits: 2,
+	
+	// Integer
+	//	padding (in pixels) betweeen each menu item
+	itemPadding: 10,
+
+	// String
+	//	controls the border that the menu items don't expand past;
+	//	for example, if set to "top", then the menu items will drop downwards as they expand.
+	// values
+	//	"center", "left", "right", "top", "bottom".
+	attachEdge: 'center',
+
+	// String
+	//	controls were the labels show up in relation to the menu item icons
+	// values
+	//	"center", "left", "right", "top", "bottom".
+	labelEdge: 'bottom',
+
+	// Boolean
+	//	for browsers that support svg, use the svg image (specified in FisheyeListIem.svgSrc)
+	//	rather than the iconSrc image attribute
+	enableCrappySvgSupport: false,
+
+	fillInTemplate: function() {
+		dojo.html.disableSelection(this.domNode);
+
+		this.isHorizontal = (this.orientation == 'horizontal');
+		this.selectedNode = -1;
+
+		this.isOver = false;
+		this.hitX1 = -1;
+		this.hitY1 = -1;
+		this.hitX2 = -1;
+		this.hitY2 = -1;
+
+		//
+		// only some edges make sense...
+		//
+		this.anchorEdge = this._toEdge(this.attachEdge, this.EDGE.CENTER);
+		this.labelEdge  = this._toEdge(this.labelEdge,  this.EDGE.TOP);
+
+		if ( this.isHorizontal && (this.anchorEdge == this.EDGE.LEFT  )){ this.anchorEdge = this.EDGE.CENTER; }
+		if ( this.isHorizontal && (this.anchorEdge == this.EDGE.RIGHT )){ this.anchorEdge = this.EDGE.CENTER; }
+		if (!this.isHorizontal && (this.anchorEdge == this.EDGE.TOP   )){ this.anchorEdge = this.EDGE.CENTER; }
+		if (!this.isHorizontal && (this.anchorEdge == this.EDGE.BOTTOM)){ this.anchorEdge = this.EDGE.CENTER; }
+
+		if (this.labelEdge == this.EDGE.CENTER){ this.labelEdge = this.EDGE.TOP; }
+		if ( this.isHorizontal && (this.labelEdge == this.EDGE.LEFT  )){ this.labelEdge = this.EDGE.TOP; }
+		if ( this.isHorizontal && (this.labelEdge == this.EDGE.RIGHT )){ this.labelEdge = this.EDGE.TOP; }
+		if (!this.isHorizontal && (this.labelEdge == this.EDGE.TOP   )){ this.labelEdge = this.EDGE.LEFT; }
+		if (!this.isHorizontal && (this.labelEdge == this.EDGE.BOTTOM)){ this.labelEdge = this.EDGE.LEFT; }
+
+		//
+		// figure out the proximity size
+		//
+		this.proximityLeft   = this.itemWidth  * (this.effectUnits - 0.5);
+		this.proximityRight  = this.itemWidth  * (this.effectUnits - 0.5);
+		this.proximityTop    = this.itemHeight * (this.effectUnits - 0.5);
+		this.proximityBottom = this.itemHeight * (this.effectUnits - 0.5);
+
+		if (this.anchorEdge == this.EDGE.LEFT){
+			this.proximityLeft = 0;
+		}
+		if (this.anchorEdge == this.EDGE.RIGHT){
+			this.proximityRight = 0;
+		}
+		if (this.anchorEdge == this.EDGE.TOP){
+			this.proximityTop = 0;
+		}
+		if (this.anchorEdge == this.EDGE.BOTTOM){
+			this.proximityBottom = 0;
+		}
+		if (this.anchorEdge == this.EDGE.CENTER){
+			this.proximityLeft   /= 2;
+			this.proximityRight  /= 2;
+			this.proximityTop    /= 2;
+			this.proximityBottom /= 2;
+		}
+	},
+	
+	postCreate: function() {
+		this._initializePositioning();
+
+		//
+		// in liberal trigger mode, activate menu whenever mouse is close
+		//
+		if( !this.conservativeTrigger ){
+			dojo.event.connect(document.documentElement, "onmousemove", this, "_onMouseMove");
+		}
+		
+		// Deactivate the menu if mouse is moved off screen (doesn't work for FF?)
+		dojo.event.connect(document.documentElement, "onmouseout", this, "_onBodyOut");
+		dojo.event.connect(this, "addChild", this, "_initializePositioning");
+	},
+
+	_initializePositioning: function(){
+		this.itemCount = this.children.length;
+
+		this.barWidth  = (this.isHorizontal ? this.itemCount : 1) * this.itemWidth;
+		this.barHeight = (this.isHorizontal ? 1 : this.itemCount) * this.itemHeight;
+
+		this.totalWidth  = this.proximityLeft + this.proximityRight  + this.barWidth;
+		this.totalHeight = this.proximityTop  + this.proximityBottom + this.barHeight;
+
+		//
+		// calculate effect ranges for each item
+		//
+		for (var i=0; i<this.children.length; i++){
+
+			this.children[i].posX = this.itemWidth  * (this.isHorizontal ? i : 0);
+			this.children[i].posY = this.itemHeight * (this.isHorizontal ? 0 : i);
+
+			this.children[i].cenX = this.children[i].posX + (this.itemWidth  / 2);
+			this.children[i].cenY = this.children[i].posY + (this.itemHeight / 2);
+
+			var isz = this.isHorizontal ? this.itemWidth : this.itemHeight;
+			var r = this.effectUnits * isz;
+			var c = this.isHorizontal ? this.children[i].cenX : this.children[i].cenY;
+			var lhs = this.isHorizontal ? this.proximityLeft : this.proximityTop;
+			var rhs = this.isHorizontal ? this.proximityRight : this.proximityBottom;
+			var siz = this.isHorizontal ? this.barWidth : this.barHeight;
+
+			var range_lhs = r;
+			var range_rhs = r;
+
+			if (range_lhs > c+lhs){ range_lhs = c+lhs; }
+			if (range_rhs > (siz-c+rhs)){ range_rhs = siz-c+rhs; }
+
+			this.children[i].effectRangeLeft = range_lhs / isz;
+			this.children[i].effectRangeRght = range_rhs / isz;
+
+			//dojo.debug('effect range for '+i+' is '+range_lhs+'/'+range_rhs);
+		}
+
+		//
+		// create the bar
+		//
+		this.domNode.style.width = this.barWidth + 'px';
+		this.domNode.style.height = this.barHeight + 'px';
+
+		//
+		// position the items
+		//
+		for (var i=0; i<this.children.length; i++){
+			var itm = this.children[i];
+			var elm = itm.domNode;
+			elm.style.left   = itm.posX + 'px';
+			elm.style.top    = itm.posY + 'px';
+			elm.style.width  = this.itemWidth + 'px';
+			elm.style.height = this.itemHeight + 'px';
+			
+			if ( itm.svgNode ) {
+				itm.svgNode.style.position = 'absolute';
+				itm.svgNode.style.left = this.itemPadding+'%';
+				itm.svgNode.style.top = this.itemPadding+'%';
+				itm.svgNode.style.width = (100 - 2 * this.itemPadding) + '%';
+				itm.svgNode.style.height = (100 - 2 * this.itemPadding) + '%';
+				itm.svgNode.style.zIndex = 1;
+	
+				itm.svgNode.setSize(this.itemWidth, this.itemHeight);
+			} else {
+				itm.imgNode.style.left = this.itemPadding+'%';
+				itm.imgNode.style.top = this.itemPadding+'%';
+				itm.imgNode.style.width = (100 - 2 * this.itemPadding) + '%';
+				itm.imgNode.style.height = (100 - 2 * this.itemPadding) + '%';
+			}
+		}
+
+		//
+		// calc the grid
+		//
+		this._calcHitGrid();
+	},
+
+	_onBodyOut: function(/*Event*/ e){
+		// clicking over an object inside of body causes this event to fire; ignore that case
+		if( dojo.html.overElement(dojo.body(), e) ){
+			return;
+		}
+		this._setDormant(e);
+	},
+
+	_setDormant: function(/*Event*/ e){
+		// summary: called when mouse moves out of menu's range
+
+		if( !this.isOver ){ return; }	// already dormant?
+		this.isOver = false;
+
+		if ( this.conservativeTrigger ) {
+			// user can't re-trigger the menu expansion
+			// until he mouses over a icon again
+			dojo.event.disconnect(document.documentElement, "onmousemove", this, "_onMouseMove");
+		}
+		this._onGridMouseMove(-1, -1);
+	},
+
+	_setActive: function(/*Event*/ e){
+		// summary: called when mouse is moved into menu's range
+
+		if( this.isOver ){ return; }	// already activated?
+		this.isOver = true;
+
+		if ( this.conservativeTrigger ) {
+			// switch event handlers so that we handle mouse events from anywhere near
+			// the menu
+			dojo.event.connect(document.documentElement, "onmousemove", this, "_onMouseMove");
+
+			this.timerScale=0.0;
+
+			// call mouse handler to do some initial necessary calculations/positioning
+			this._onMouseMove(e);
+
+			// slowly expand the icon size so it isn't jumpy
+			this._expandSlowly();
+		}
+	},
+
+	_onMouseMove: function(/*Event*/ e) {
+		// summary: called when mouse is moved
+		if ((e.pageX >= this.hitX1) && (e.pageX <= this.hitX2) &&
+			(e.pageY >= this.hitY1) && (e.pageY <= this.hitY2)){
+			if( !this.isOver ){
+				this._setActive(e);
+			}
+			this._onGridMouseMove(e.pageX-this.hitX1, e.pageY-this.hitY1);
+		}else{
+			if (this.isOver){
+				this._setDormant(e);
+			}
+		}
+	},
+
+	onResized: function() {
+		this._calcHitGrid();
+	},
+
+	_onGridMouseMove: function(x, y){
+		// summary: called when mouse is moved in the vicinity of the menu
+		this.pos = {x:x, y:y};
+		this._paint();
+	},
+	
+	_paint: function(){
+		var x=this.pos.x;
+		var y=this.pos.y;
+
+		if( this.itemCount <= 0 ){ return; }
+
+		//
+		// figure out our main index
+		//
+		var pos = this.isHorizontal ? x : y;
+		var prx = this.isHorizontal ? this.proximityLeft : this.proximityTop;
+		var siz = this.isHorizontal ? this.itemWidth : this.itemHeight;
+		var sim = this.isHorizontal ? 
+			(1.0-this.timerScale)*this.itemWidth + this.timerScale*this.itemMaxWidth :
+			(1.0-this.timerScale)*this.itemHeight + this.timerScale*this.itemMaxHeight ;
+
+		var cen = ((pos - prx) / siz) - 0.5;
+		var max_off_cen = (sim / siz) - 0.5;
+
+		if (max_off_cen > this.effectUnits){ max_off_cen = this.effectUnits; }
+
+		//
+		// figure out our off-axis weighting
+		//
+		var off_weight = 0;
+
+		if (this.anchorEdge == this.EDGE.BOTTOM){
+			var cen2 = (y - this.proximityTop) / this.itemHeight;
+			off_weight = (cen2 > 0.5) ? 1 : y / (this.proximityTop + (this.itemHeight / 2));
+		}
+		if (this.anchorEdge == this.EDGE.TOP){
+			var cen2 = (y - this.proximityTop) / this.itemHeight;
+			off_weight = (cen2 < 0.5) ? 1 : (this.totalHeight - y) / (this.proximityBottom + (this.itemHeight / 2));
+		}
+		if (this.anchorEdge == this.EDGE.RIGHT){
+			var cen2 = (x - this.proximityLeft) / this.itemWidth;
+			off_weight = (cen2 > 0.5) ? 1 : x / (this.proximityLeft + (this.itemWidth / 2));
+		}
+		if (this.anchorEdge == this.EDGE.LEFT){
+			var cen2 = (x - this.proximityLeft) / this.itemWidth;
+			off_weight = (cen2 < 0.5) ? 1 : (this.totalWidth - x) / (this.proximityRight + (this.itemWidth / 2));
+		}
+		if (this.anchorEdge == this.EDGE.CENTER){
+			if (this.isHorizontal){
+				off_weight = y / (this.totalHeight);
+			}else{
+				off_weight = x / (this.totalWidth);
+			}
+
+			if (off_weight > 0.5){
+				off_weight = 1 - off_weight;
+			}
+
+			off_weight *= 2;
+		}
+
+		//
+		// set the sizes
+		//
+		for(var i=0; i<this.itemCount; i++){
+			var weight = this._weighAt(cen, i);
+			if (weight < 0){weight = 0;}
+			this._setItemSize(i, weight * off_weight);
+		}
+
+		//
+		// set the positions
+		//
+
+		var main_p = Math.round(cen);
+		var offset = 0;
+
+		if (cen < 0){
+			main_p = 0;
+
+		}else if (cen > this.itemCount - 1){
+
+			main_p = this.itemCount -1;
+
+		}else{
+
+			offset = (cen - main_p) * ((this.isHorizontal ? this.itemWidth : this.itemHeight) - this.children[main_p].sizeMain);
+		}
+
+		this._positionElementsFrom(main_p, offset);
+	},
+
+	_weighAt: function(/*Integer*/ cen, /*Integer*/ i){
+		var dist = Math.abs(cen - i);
+		var limit = ((cen - i) > 0) ? this.children[i].effectRangeRght : this.children[i].effectRangeLeft;
+		return (dist > limit) ? 0 : (1 - dist / limit);			// Integer
+	},
+
+	_setItemSize: function(p, scale){
+		scale *= this.timerScale;
+		var w = Math.round(this.itemWidth  + ((this.itemMaxWidth  - this.itemWidth ) * scale));
+		var h = Math.round(this.itemHeight + ((this.itemMaxHeight - this.itemHeight) * scale));
+
+		if (this.isHorizontal){
+
+			this.children[p].sizeW = w;
+			this.children[p].sizeH = h;
+
+			this.children[p].sizeMain = w;
+			this.children[p].sizeOff  = h;
+
+			var y = 0;
+			if (this.anchorEdge == this.EDGE.TOP){
+				y = (this.children[p].cenY - (this.itemHeight / 2));
+			}else if (this.anchorEdge == this.EDGE.BOTTOM){
+				y = (this.children[p].cenY - (h - (this.itemHeight / 2)));
+			}else{
+				y = (this.children[p].cenY - (h / 2));
+			}
+
+			this.children[p].usualX = Math.round(this.children[p].cenX - (w / 2));
+			this.children[p].domNode.style.top  = y + 'px';
+			this.children[p].domNode.style.left  = this.children[p].usualX + 'px';
+
+		}else{
+
+			this.children[p].sizeW = w;
+			this.children[p].sizeH = h;
+
+			this.children[p].sizeOff  = w;
+			this.children[p].sizeMain = h;
+
+			var x = 0;
+			if (this.anchorEdge == this.EDGE.LEFT){
+				x = this.children[p].cenX - (this.itemWidth / 2);
+			}else if (this.anchorEdge == this.EDGE.RIGHT){
+				x = this.children[p].cenX - (w - (this.itemWidth / 2));
+			}else{
+				x = this.children[p].cenX - (w / 2);
+			}
+
+			this.children[p].domNode.style.left = x + 'px';
+			this.children[p].usualY = Math.round(this.children[p].cenY - (h / 2));
+
+			this.children[p].domNode.style.top  = this.children[p].usualY + 'px';
+		}
+
+		this.children[p].domNode.style.width  = w + 'px';
+		this.children[p].domNode.style.height = h + 'px';
+
+		if (this.children[p].svgNode){
+			this.children[p].svgNode.setSize(w, h);
+		}
+	},
+
+	_positionElementsFrom: function(p, offset){
+
+		var pos = 0;
+
+		if (this.isHorizontal){
+			pos = Math.round(this.children[p].usualX + offset);
+			this.children[p].domNode.style.left = pos + 'px';
+		}else{
+			pos = Math.round(this.children[p].usualY + offset);
+			this.children[p].domNode.style.top = pos + 'px';
+		}
+		this._positionLabel(this.children[p]);
+
+
+		//
+		// position before
+		//
+		var bpos = pos;
+		for(var i=p-1; i>=0; i--){
+			bpos -= this.children[i].sizeMain;
+
+			if (this.isHorizontal){
+				this.children[i].domNode.style.left = bpos + 'px';
+			}else{
+				this.children[i].domNode.style.top = bpos + 'px';
+			}
+			this._positionLabel(this.children[i]);
+		}
+
+		//
+		// position after
+		//
+		var apos = pos;
+		for(var i=p+1; i<this.itemCount; i++){
+			apos += this.children[i-1].sizeMain;
+
+			if (this.isHorizontal){
+				this.children[i].domNode.style.left = apos + 'px';
+			}else{
+				this.children[i].domNode.style.top = apos + 'px';
+			}
+			this._positionLabel(this.children[i]);
+		}
+
+	},
+
+	_positionLabel: function(itm){
+
+		var x = 0;
+		var y = 0;
+		
+		var mb = dojo.html.getMarginBox(itm.lblNode);
+
+		if (this.labelEdge == this.EDGE.TOP){
+			x = Math.round((itm.sizeW / 2) - (mb.width / 2));
+			y = -mb.height;
+		}
+
+		if (this.labelEdge == this.EDGE.BOTTOM){
+			x = Math.round((itm.sizeW / 2) - (mb.width / 2));
+			y = itm.sizeH;
+		}
+
+		if (this.labelEdge == this.EDGE.LEFT){
+			x = -mb.width;
+			y = Math.round((itm.sizeH / 2) - (mb.height / 2));
+		}
+
+		if (this.labelEdge == this.EDGE.RIGHT){
+			x = itm.sizeW;
+			y = Math.round((itm.sizeH / 2) - (mb.height / 2));
+		}
+
+		itm.lblNode.style.left = x + 'px';
+		itm.lblNode.style.top  = y + 'px';
+	},
+
+	_calcHitGrid: function(){
+
+		var pos = dojo.html.getAbsolutePosition(this.domNode, true);
+
+		this.hitX1 = pos.x - this.proximityLeft;
+		this.hitY1 = pos.y - this.proximityTop;
+		this.hitX2 = this.hitX1 + this.totalWidth;
+		this.hitY2 = this.hitY1 + this.totalHeight;
+
+		//dojo.debug(this.hitX1+','+this.hitY1+' // '+this.hitX2+','+this.hitY2);
+	},
+
+	_toEdge: function(inp, def){
+		return this.EDGE[inp.toUpperCase()] || def;
+	},
+	
+	_expandSlowly: function(){
+		// summary: slowly expand the image to user specified max size
+		if( !this.isOver ){ return; }
+		this.timerScale += 0.2;
+		this._paint();
+		if ( this.timerScale<1.0 ) {
+			dojo.lang.setTimeout(this, "_expandSlowly", 10);
+		}
+	},
+
+	destroy: function(){
+		// need to disconnect when we destroy
+		dojo.event.disconnect(document.documentElement, "onmouseout", this, "_onBodyOut");
+		dojo.event.disconnect(document.documentElement, "onmousemove", this, "_onMouseMove");
+		dojo.widget.FisheyeList.superclass.destroy.call(this);
+	}
+});
+
+/*
+ * summary
+ *	Menu item inside of a FisheyeList.
+ *	See FisheyeList documentation for details on usage.
+ */
+dojo.widget.defineWidget(
+	"dojo.widget.FisheyeListItem",
+	dojo.widget.HtmlWidget,
+{
+	// String
+	//	pathname to image file (jpg, gif, png, etc.) of icon for this menu item
+	iconSrc: "",
+
+	// String
+	//	pathname to svg file of icon for this menu item
+	svgSrc: "",
+	
+	// String
+	//	label to print next to the icon, when it is moused-over
+	caption: "",
+
+	// String
+	//	will be set to the id of the orginal div element
+	id: "",
+
+	_blankImgPath: dojo.uri.dojoUri("src/widget/templates/images/blank.gif"),
+
+	templateString:
+		'<div class="dojoHtmlFisheyeListItem">' +
+		'  <img class="dojoHtmlFisheyeListItemImage" dojoAttachPoint="imgNode" dojoAttachEvent="onMouseOver;onMouseOut;onClick">' +
+		'  <div class="dojoHtmlFisheyeListItemLabel" dojoAttachPoint="lblNode"></div>' +
+		'</div>',
+	
+	fillInTemplate: function() {
+		//
+		// set image
+		// TODO: turn on/off SVG support based on browser version.
+		// this.parent.enableCrappySvgSupport is not available to this function
+		//
+		if (this.svgSrc != ""){
+			this.svgNode = this._createSvgNode(this.svgSrc);
+			this.domNode.appendChild(this.svgNode);
+			this.imgNode.style.display = 'none';
+		} else if((this.iconSrc.toLowerCase().substring(this.iconSrc.length-4)==".png")&&(dojo.render.html.ie)&&(!dojo.render.html.ie70)){
+			/* we set the id of the new fisheyeListItem to the id of the div defined in the HTML */
+			if (dojo.dom.hasParent(this.imgNode) && this.id != ""){
+				var parent = this.imgNode.parentNode;
+				parent.setAttribute("id", this.id);
+			}
+			this.imgNode.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='"+this.iconSrc+"', sizingMethod='scale')";
+			this.imgNode.src = this._blankImgPath.toString();
+		} else {
+			if (dojo.dom.hasParent(this.imgNode) && this.id != ""){
+				var parent = this.imgNode.parentNode;
+				parent.setAttribute("id", this.id);
+			}
+			this.imgNode.src = this.iconSrc;
+		}
+
+		//
+		// Label
+		//
+		if ( this.lblNode ) {
+			this.lblNode.appendChild(document.createTextNode(this.caption));
+		}
+		dojo.html.disableSelection(this.domNode);
+	},
+	
+	_createSvgNode: function(src){
+		var elm = document.createElement('embed');
+		elm.src = src;
+		elm.type = 'image/svg+xml';
+		//elm.style.border = '1px solid black';
+		elm.style.width = '1px';
+		elm.style.height = '1px';
+		elm.loaded = 0;
+		elm.setSizeOnLoad = false;
+
+		elm.onload = function(){
+			this.svgRoot = this.getSVGDocument().rootElement;
+			this.svgDoc = this.getSVGDocument().documentElement;
+			this.zeroWidth = this.svgRoot.width.baseVal.value;
+			this.zeroHeight = this.svgRoot.height.baseVal.value;
+			this.loaded = true;
+
+			if (this.setSizeOnLoad){
+				this.setSize(this.setWidth, this.setHeight);
+			}
+		}
+
+		elm.setSize = function(w, h){
+			if (!this.loaded){
+				this.setWidth = w;
+				this.setHeight = h;
+				this.setSizeOnLoad = true;
+				return;
+			}
+
+			this.style.width = w+'px';
+			this.style.height = h+'px';
+			this.svgRoot.width.baseVal.value = w;
+			this.svgRoot.height.baseVal.value = h;
+
+			var scale_x = w / this.zeroWidth;
+			var scale_y = h / this.zeroHeight;
+
+			for(var i=0; i<this.svgDoc.childNodes.length; i++){
+				if (this.svgDoc.childNodes[i].setAttribute){
+					this.svgDoc.childNodes[i].setAttribute( "transform", "scale("+scale_x+","+scale_y+")" );
+				}
+			}
+		}
+
+		return elm;
+	},
+
+	onMouseOver: function(/*Event*/ e) {
+		// summary: callback when user moves mouse over this menu item
+		// in conservative mode, don't activate the menu until user mouses over an icon
+		if( !this.parent.isOver ){
+			this.parent._setActive(e);
+		}
+		if ( this.caption != "" ) {
+			dojo.html.addClass(this.lblNode, "selected");
+			this.parent._positionLabel(this);
+		}
+	},
+	
+	onMouseOut: function(/*Event*/ e) {
+		// summary: callback when user moves mouse off of this menu item
+		dojo.html.removeClass(this.lblNode, "selected");
+	},
+
+	onClick: function(/*Event*/ e) {
+		// summary: user overridable callback when user clicks this menu item
+	}
+});
+

Added: struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/FloatingPane.js
URL: http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/FloatingPane.js?view=auto&rev=514083
==============================================================================
--- struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/FloatingPane.js (added)
+++ struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/FloatingPane.js Fri Mar  2 21:48:54 2007
@@ -0,0 +1,441 @@
+/*
+	Copyright (c) 2004-2006, The Dojo Foundation
+	All Rights Reserved.
+
+	Licensed under the Academic Free License version 2.1 or above OR the
+	modified BSD license. For more information on Dojo licensing, see:
+
+		http://dojotoolkit.org/community/licensing.shtml
+*/
+
+dojo.provide("dojo.widget.FloatingPane");
+
+dojo.require("dojo.widget.*");
+dojo.require("dojo.widget.Manager");
+dojo.require("dojo.html.*");
+dojo.require("dojo.html.layout");
+dojo.require("dojo.html.iframe");
+dojo.require("dojo.html.selection");
+dojo.require("dojo.lfx.shadow");
+dojo.require("dojo.widget.html.layout");
+dojo.require("dojo.widget.ContentPane");
+dojo.require("dojo.dnd.HtmlDragMove");
+dojo.require("dojo.widget.Dialog");		// for ModalFloatingPane
+dojo.require("dojo.widget.ResizeHandle");
+
+// summary
+//	Base class for FloatingPane, ModalFloatingPane
+dojo.declare(
+	"dojo.widget.FloatingPaneBase",
+	null,
+	{
+		// String
+		//	text to display in floating pane's title bar (ex: "My Window")
+		title: '',
+		
+		// String
+		//	path of icon to display in floating pane's title bar
+		iconSrc: '',
+		
+		// Boolean
+		//	if true, display a shadow behind the floating pane
+		hasShadow: false,
+		
+		// Boolean
+		//	if true, and the floating pane is inside another container (ContentPane, another FloatingPane, etc.),
+		//	then don't allow the floating pane to be dragged outside of it's container
+		constrainToContainer: false,
+		
+		// String
+		//	widget id of TaskBar widget;
+		//	if specified, then an icon for this FloatingPane will be added to the specified TaskBar
+		taskBarId: "",
+		
+		// Boolean
+		//	if true, allow user to resize floating pane
+		resizable: true,
+		
+		// Boolean
+		//	if true, display title bar for this floating pane
+		titleBarDisplay: true,
+
+		// String
+		//	control whether window is initially not displayed ("minimized"), displayed full screen ("maximized"),
+		//	or just displayed normally ("normal")
+		// Values
+		//	"normal", "maximized", "minimized"
+		windowState: "normal",
+		
+		// Boolean
+		//	display button to close window
+		displayCloseAction: false,
+		
+		// Boolean
+		//	display button to minimize window (ie, window disappears so only the taskbar item remains)
+		displayMinimizeAction: false,
+
+		// Boolean
+		//	display button to maximize window (ie, to take up the full screen)
+		displayMaximizeAction: false,
+
+		// Related to connecting to taskbar
+		// TODO: use topics rather than repeated connect attempts?
+		_max_taskBarConnectAttempts: 5,
+		_taskBarConnectAttempts: 0,
+
+		templatePath: dojo.uri.dojoUri("src/widget/templates/FloatingPane.html"),
+		templateCssPath: dojo.uri.dojoUri("src/widget/templates/FloatingPane.css"),
+
+		fillInFloatingPaneTemplate: function(args, frag){
+			// summary: this should be called by fillInTemplate() of the widget that I'm mixed into
+
+			// Copy style info from input node to output node
+			var source = this.getFragNodeRef(frag);
+			dojo.html.copyStyle(this.domNode, source);
+	
+			// necessary for safari, khtml (for computing width/height)
+			dojo.body().appendChild(this.domNode);
+	
+			// if display:none then state=minimized, otherwise state=normal
+			if(!this.isShowing()){
+				this.windowState="minimized";
+			}
+	
+			// <img src=""> can hang IE!  better get rid of it
+			if(this.iconSrc==""){
+				dojo.html.removeNode(this.titleBarIcon);
+			}else{
+				this.titleBarIcon.src = this.iconSrc.toString();// dojo.uri.Uri obj req. toString()
+			}
+	
+			if(this.titleBarDisplay){	
+				this.titleBar.style.display="";
+				dojo.html.disableSelection(this.titleBar);
+	
+				this.titleBarIcon.style.display = (this.iconSrc=="" ? "none" : "");
+	
+				this.minimizeAction.style.display = (this.displayMinimizeAction ? "" : "none");
+				this.maximizeAction.style.display= 
+					(this.displayMaximizeAction && this.windowState!="maximized" ? "" : "none");
+				this.restoreAction.style.display= 
+					(this.displayMaximizeAction && this.windowState=="maximized" ? "" : "none");
+				this.closeAction.style.display= (this.displayCloseAction ? "" : "none");
+	
+				this.drag = new dojo.dnd.HtmlDragMoveSource(this.domNode);	
+				if (this.constrainToContainer) {
+					this.drag.constrainTo();
+				}
+				this.drag.setDragHandle(this.titleBar);
+	
+				var self = this;
+	
+				dojo.event.topic.subscribe("dragMove",
+					function (info){
+						if (info.source.domNode == self.domNode){
+							dojo.event.topic.publish('floatingPaneMove', { source: self } );
+						}
+					}
+				);
+			}
+	
+			if(this.resizable){
+				this.resizeBar.style.display="";
+				this.resizeHandle = dojo.widget.createWidget("ResizeHandle", {targetElmId: this.widgetId, id:this.widgetId+"_resize"});
+				this.resizeBar.appendChild(this.resizeHandle.domNode);
+			}
+	
+			// add a drop shadow
+			if(this.hasShadow){
+				this.shadow=new dojo.lfx.shadow(this.domNode);
+			}
+	
+			// Prevent IE bleed-through problem
+			this.bgIframe = new dojo.html.BackgroundIframe(this.domNode);
+	
+			if( this.taskBarId ){
+				this._taskBarSetup();
+			}
+	
+			// counteract body.appendChild above
+			dojo.body().removeChild(this.domNode);
+		},
+	
+		postCreate: function(){
+			if (dojo.hostenv.post_load_) {
+				this._setInitialWindowState();
+			} else {
+				dojo.addOnLoad(this, "_setInitialWindowState");
+			}
+		},
+	
+		maximizeWindow: function(/*Event*/ evt) {
+			// summary: maximize the window
+			var mb = dojo.html.getMarginBox(this.domNode);
+			this.previous={
+				width: mb.width || this.width,
+				height: mb.height || this.height,
+				left: this.domNode.style.left,
+				top: this.domNode.style.top,
+				bottom: this.domNode.style.bottom,
+				right: this.domNode.style.right
+			};
+			if(this.domNode.parentNode.style.overflow.toLowerCase() != 'hidden'){
+				this.parentPrevious={
+					overflow: this.domNode.parentNode.style.overflow
+				};
+				dojo.debug(this.domNode.parentNode.style.overflow);
+				this.domNode.parentNode.style.overflow = 'hidden';
+			}
+
+			this.domNode.style.left =
+				dojo.html.getPixelValue(this.domNode.parentNode, "padding-left", true) + "px";
+			this.domNode.style.top =
+				dojo.html.getPixelValue(this.domNode.parentNode, "padding-top", true) + "px";
+
+			if ((this.domNode.parentNode.nodeName.toLowerCase() == 'body')) {
+				var viewport = dojo.html.getViewport();
+				var padding = dojo.html.getPadding(dojo.body());
+				this.resizeTo(viewport.width-padding.width, viewport.height-padding.height);
+			} else {
+				var content = dojo.html.getContentBox(this.domNode.parentNode);
+				this.resizeTo(content.width, content.height);
+			}
+			this.maximizeAction.style.display="none";
+			this.restoreAction.style.display="";
+
+			//disable resize and drag
+			if(this.resizeHandle){
+				this.resizeHandle.domNode.style.display="none";
+			}
+			this.drag.setDragHandle(null);
+
+			this.windowState="maximized";
+		},
+	
+		minimizeWindow: function(/*Event*/ evt) {
+			// summary: hide the window so that only the icon in the taskbar is shown
+			this.hide();
+			for(var attr in this.parentPrevious){
+				this.domNode.parentNode.style[attr] = this.parentPrevious[attr];
+			}
+			this.lastWindowState = this.windowState;
+			this.windowState = "minimized";
+		},
+	
+		restoreWindow: function(/*Event*/ evt) {
+			// summary: set the winow to normal size (neither maximized nor minimized)
+			if (this.windowState=="minimized") {
+				this.show();
+				if(this.lastWindowState == "maximized"){
+					this.domNode.parentNode.style.overflow = 'hidden';
+					this.windowState="maximized";
+				}else{ //normal
+					this.windowState="normal";
+				}
+			} else if (this.windowState=="maximized"){
+				for(var attr in this.previous){
+					this.domNode.style[attr] = this.previous[attr];
+				}
+				for(var attr in this.parentPrevious){
+					this.domNode.parentNode.style[attr] = this.parentPrevious[attr];
+				}
+				this.resizeTo(this.previous.width, this.previous.height);
+				this.previous=null;
+				this.parentPrevious=null;
+
+				this.restoreAction.style.display="none";
+				this.maximizeAction.style.display=this.displayMaximizeAction ? "" : "none";
+
+				if(this.resizeHandle){
+					this.resizeHandle.domNode.style.display="";
+				}
+				this.drag.setDragHandle(this.titleBar);
+				this.windowState="normal";
+			} else { //normal
+				// do nothing
+			}
+		},
+
+		toggleDisplay: function(){
+			// summary: switch between hidden mode and displayed mode (either maximized or normal, depending on state before window was minimized)
+			if(this.windowState=="minimized"){
+				this.restoreWindow();
+			}else{
+				this.minimizeWindow();
+			}
+		},
+
+		closeWindow: function(/*Event*/ evt) {
+			// summary: destroy this window
+			dojo.html.removeNode(this.domNode);
+			this.destroy();
+		},
+	
+		onMouseDown: function(/*Event*/ evt) {
+			// summary: callback when user clicks anywhere on the floating pane
+			this.bringToTop();
+		},
+	
+		bringToTop: function() {
+			// summary
+			//	all the floating panes are stacked in z-index order; bring this floating pane to the top of that stack,
+			//	so that it's displayed in front of all the other floating panes
+			var floatingPanes= dojo.widget.manager.getWidgetsByType(this.widgetType);
+			var windows = [];
+			for (var x=0; x<floatingPanes.length; x++) {
+				if (this.widgetId != floatingPanes[x].widgetId) {
+						windows.push(floatingPanes[x]);
+				}
+			}
+	
+			windows.sort(function(a,b) {
+				return a.domNode.style.zIndex - b.domNode.style.zIndex;
+			});
+			
+			windows.push(this);
+	
+			var floatingPaneStartingZ = 100;
+			for (x=0; x<windows.length;x++) {
+				windows[x].domNode.style.zIndex = floatingPaneStartingZ + x*2;
+			}
+		},
+	
+		_setInitialWindowState: function() {
+			if(this.isShowing()){
+				this.width=-1;	// force resize
+				var mb = dojo.html.getMarginBox(this.domNode);
+				this.resizeTo(mb.width, mb.height);
+			}
+			if (this.windowState == "maximized") {
+				this.maximizeWindow();
+				this.show();
+				return;
+			}
+	
+			if (this.windowState=="normal") {
+				this.show();
+				return;
+			}
+	
+			if (this.windowState=="minimized") {
+				this.hide();
+				return;
+			}
+	
+			this.windowState="minimized";
+		},
+	
+		_taskBarSetup: function() {
+			// summary: add icon to task bar, connected to me
+			var taskbar = dojo.widget.getWidgetById(this.taskBarId);
+			if (!taskbar){
+				if (this._taskBarConnectAttempts <  this._max_taskBarConnectAttempts) {
+					dojo.lang.setTimeout(this, this._taskBarSetup, 50);
+					this._taskBarConnectAttempts++;
+				} else {
+					dojo.debug("Unable to connect to the taskBar");
+				}
+				return;
+			}
+			taskbar.addChild(this);
+		},
+
+		showFloatingPane: function(){
+			// summary:
+			//	bring this floating pane to the top
+			this.bringToTop();
+		},
+
+		onFloatingPaneShow: function(){
+			// summary: callback for when someone calls FloatingPane.show
+			var mb = dojo.html.getMarginBox(this.domNode);
+			this.resizeTo(mb.width, mb.height);
+		},
+	
+		// summary: set the floating pane to the given size
+		resizeTo: function(/*Integer*/ width, /*Integer*/ height){
+			dojo.html.setMarginBox(this.domNode, { width: width, height: height });
+	
+			dojo.widget.html.layout(this.domNode,
+				[
+				  {domNode: this.titleBar, layoutAlign: "top"},
+				  {domNode: this.resizeBar, layoutAlign: "bottom"},
+				  {domNode: this.containerNode, layoutAlign: "client"}
+				] );
+	
+			// If any of the children have layoutAlign specified, obey it
+			dojo.widget.html.layout(this.containerNode, this.children, "top-bottom");
+			
+			this.bgIframe.onResized();
+			if(this.shadow){ this.shadow.size(width, height); }
+			this.onResized();
+		},
+	
+		checkSize: function() {
+			// summary
+			//	checkSize() is called when the user has resized the browser window,
+			// 	but that doesn't affect this widget (or this widget's children)
+			// 	so it can be safely ignored...
+			// TODO: unless we are maximized.  then we should resize ourself.
+		}
+	}
+);
+
+// summary
+//	A non-modal floating window.
+//	Attaches to a Taskbar which has an icon for each window.
+//	Must specify size (like style="width: 500px; height: 500px;"),
+dojo.widget.defineWidget(
+	"dojo.widget.FloatingPane",
+	[dojo.widget.ContentPane, dojo.widget.FloatingPaneBase], 
+{
+	fillInTemplate: function(args, frag){	
+		this.fillInFloatingPaneTemplate(args, frag);
+		dojo.widget.FloatingPane.superclass.fillInTemplate.call(this, args, frag);
+	},
+	postCreate: function(){
+		dojo.widget.FloatingPaneBase.prototype.postCreate.apply(this, arguments);
+		dojo.widget.FloatingPane.superclass.postCreate.apply(this, arguments);
+	},
+	show: function(){
+		dojo.widget.FloatingPane.superclass.show.apply(this, arguments);
+		this.showFloatingPane();
+	},
+	onShow: function(){
+		dojo.widget.FloatingPane.superclass.onShow.call(this);
+		this.onFloatingPaneShow();
+	}
+});
+
+
+// summary
+//	A modal floating window.
+//	This widget is similar to the Dialog widget, but the window, unlike the Dialog, can be moved.
+//	Must specify size (like style="width: 500px; height: 500px;"),
+dojo.widget.defineWidget(
+	"dojo.widget.ModalFloatingPane",
+	[dojo.widget.FloatingPane, dojo.widget.ModalDialogBase],
+	{
+		windowState: "minimized",
+		displayCloseAction: true,
+		postCreate: function(){
+			dojo.widget.ModalDialogBase.prototype.postCreate.call(this);
+			dojo.widget.ModalFloatingPane.superclass.postCreate.call(this);
+		},
+		show: function(){
+			dojo.widget.ModalFloatingPane.superclass.show.apply(this, arguments);
+			this.showModalDialog();
+			this.placeModalDialog();
+			//place the background div under this modal pane
+			this.shared.bg.style.zIndex = this.domNode.style.zIndex-1;
+		},
+		hide: function(){
+			this.hideModalDialog();
+			dojo.widget.ModalFloatingPane.superclass.hide.apply(this, arguments);
+		},
+		closeWindow: function(){
+			this.hide();
+			dojo.widget.ModalFloatingPane.superclass.closeWindow.apply(this, arguments);
+		}
+	}
+);

Added: struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/Form.js
URL: http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/Form.js?view=auto&rev=514083
==============================================================================
--- struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/Form.js (added)
+++ struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/Form.js Fri Mar  2 21:48:54 2007
@@ -0,0 +1,257 @@
+/*
+	Copyright (c) 2004-2006, The Dojo Foundation
+	All Rights Reserved.
+
+	Licensed under the Academic Free License version 2.1 or above OR the
+	modified BSD license. For more information on Dojo licensing, see:
+
+		http://dojotoolkit.org/community/licensing.shtml
+*/
+
+dojo.provide("dojo.widget.Form");
+
+dojo.require("dojo.widget.*");
+dojo.require("dojo.widget.HtmlWidget");
+
+dojo.widget.defineWidget("dojo.widget.Form", dojo.widget.HtmlWidget,
+	{
+		isContainer: true,
+   		templateString: "<form dojoAttachPoint='containerNode' dojoAttachEvent='onSubmit:onSubmit'></form>",
+		formElements: [],
+
+		postCreate: function(args,frag){
+			for (var key in args) {
+				if (key == "dojotype") continue;
+				var attr= document.createAttribute(key);
+      				attr.nodeValue=args[key];
+      				this.containerNode.setAttributeNode(attr);
+    			}
+  		},
+		_createFormElements: function() {
+   			if(dojo.render.html.safari) {
+				// bug in safari (not registering form-elements)
+				var elems = ["INPUT", "SELECT", "TEXTAREA"];
+				for (var k=0; k < elems.length; k++) {
+					var list = this.containerNode.getElementsByTagName(elems[k]);
+					for (var j=0,len2=list.length; j<len2; j++) {
+						this.formElements.push(list[j]);
+					}
+				}
+				// fixed in safari nightly
+			} else {
+				this.formElements=this.containerNode.elements;
+			}
+		},
+		onSubmit: function(e) {
+    			e.preventDefault();
+  		},
+
+		submit: function() {
+			this.containerNode.submit();
+		},
+
+		_getFormElement: function(name) {
+			for(var i=0, len=this.formElements.length; i<len; i++) {
+				var element = this.formElements[i];
+				if (element.name == name) {
+					return element;
+				} // if
+			} // for
+			return null;
+		},
+
+		_getObject: function(obj, searchString) {
+			var namePath = [];
+			namePath=searchString.split(".");
+			var myObj=obj;
+			var name=namePath[namePath.length-1];
+
+			for(var j=0, len=namePath.length;j<len;++j) {
+				var p=namePath[j];
+				if (typeof(myObj[p]) == "undefined") {
+					myObj[p]={};
+				}
+				myObj=myObj[p];
+			}
+			return myObj;
+		},
+		_setToContainers: function (obj, widget) {
+			for(var i=0, len=widget.children.length; i<len; ++i) {
+				if (widget.children[i].widgetType == "Repeater") {
+					var rIndex=widget.children[i].pattern;
+					var rIndexPos=rIndex.indexOf("%{index}");
+					rIndex=rIndex.substr(0,rIndexPos-1);
+					var myObj = this._getObject(obj, rIndex);
+					if (typeof(myObj) == "object" && typeof(myObj.length) == "undefined") {
+						myObj=[];
+					}
+					var rowCount = widget.children[i].getRowCount();
+					if (myObj.length > rowCount) {
+						for (var j=rowCount, len2=myObj.length; j<len2; j++) {
+							widget.children[i].addRow();
+						}
+					} else if (myObj.length < rowCount) {
+						for (var j=rowCount, len2=myObj.length; j>len2; j--) {
+							widget.children[i].deleteRow(0);
+						}
+					}
+					for (var j=0, len2=myObj.length;j<len2; ++j) {
+						for (var key in myObj[j]) {
+							var prefix = dojo.string.substituteParams(widget.children[i].index, {"index": "" + j});
+							this._getFormElement(prefix+"."+key).value=myObj[j][key];
+						}
+					}
+				}
+
+				if (widget.children[i].isContainer) {
+					this._setToContainers (obj, widget.children[i]);
+					continue;
+				}
+
+				switch(widget.children[i].widgetType) {
+					case "Checkbox":
+						continue;
+						break;
+					case "DropdownDatePicker":
+						if(widget.children[i].valueNode.value == "") {
+							widget.children[i].inputNode.value="";
+							widget.children[i].datePicker.storedDate="";
+						} else {
+							widget.children[i].datePicker.setDate(widget.children[i].valueNode.value);
+							//widget.children[i].datePicker.date=dojo.widget.DatePicker.util.fromRfcDate(widget.children[i].valueNode.value);
+							widget.children[i].onSetDate();
+						}
+						break;
+					case "Select":
+						//widget.children[i].setValue(myObj[name]);
+						continue;
+						break;
+					case "ComboBox":
+						//widget.children[i].setSelectedValue(myObj[name]);
+						continue;
+						break;
+					default:
+						break;
+				}
+			}
+		},
+		setValues: function(obj) {
+			this._createFormElements();
+			for(var i=0, len=this.formElements.length; i<len; i++) {
+				var element = this.formElements[i];
+				if (element.name == '') {continue};
+				var namePath = new Array();
+				namePath=element.name.split(".");
+				var myObj=obj;
+				var name=namePath[namePath.length-1];
+				for(var j=1,len2=namePath.length;j<len2;++j) {
+					var p=namePath[j - 1];
+					if(typeof(myObj[p]) == "undefined") {
+						myObj=undefined;
+						break;
+						//myObj[p]={}
+					};
+					myObj=myObj[p];
+				}
+
+				if (typeof(myObj) == "undefined") {
+					continue;
+				}
+
+				var type=element.type;
+				if (type == "hidden" || type == "text" || type == "textarea" || type == "password") {
+					type = "text";
+				}
+				switch(type) {
+					case "checkbox":
+						this.formElements[i].checked=false;
+						if (typeof(myObj[name]) == 'undefined') continue;
+						for (var j=0,len2=myObj[name].length; j<len2; ++j) {
+							if(element.value == myObj[name][j]) {
+								element.checked=true;
+							}
+						}
+						break;
+					case "radio":
+						this.formElements[i].checked=false;
+						if (typeof(myObj[name]) == 'undefined') {continue};
+						if (myObj[name] == this.formElements[i].value) {
+							this.formElements[i].checked=true;
+						}
+						break;
+					case "select-one":
+						this.formElements[i].selectedIndex="0";
+						for (var j=0,len2=element.options.length; j<len2; ++j) {
+							if (element.options[j].value == myObj[name]) {
+								element.options[j].selected=true;
+							} else {
+								//element.options[j].selected=false;
+							}
+						}
+						break;
+					case "text":
+						var value="";
+						if (typeof(myObj[name]) == 'string') {
+							value = myObj[name];
+						}
+						this.formElements[i].value=value;
+						break;
+					default:
+						//dojo.debug("Not supported type ("+type+")");
+						break;
+				}
+      			}
+			this._setToContainers(obj,this);
+		},
+		getValues: function() {
+			this._createFormElements();
+			var obj = { };
+
+			for(var i=0,len=this.formElements.length; i<len; i++) {
+				// FIXME: would be better to give it an attachPoint:
+				var elm = this.formElements[i];
+				var namePath = [];
+				if (elm.name == '') { continue;}
+				namePath=elm.name.split(".");
+				var myObj=obj;
+				var name=namePath[namePath.length-1];
+				for(var j=1,len2=namePath.length;j<len2;++j) {
+					var nameIndex = null;
+					var p=namePath[j - 1];
+					var nameA=p.split("[");
+					if (nameA.length > 1) {
+						if(typeof(myObj[nameA[0]]) == "undefined") {
+							myObj[nameA[0]]=[ ];
+						} // if
+						nameIndex=parseInt(nameA[1]);
+						if(typeof(myObj[nameA[0]][nameIndex]) == "undefined") {
+							myObj[nameA[0]][nameIndex]={};
+						}
+					} else if(typeof(myObj[nameA[0]]) == "undefined") {
+						myObj[nameA[0]]={}
+					} // if
+
+					if (nameA.length == 1) {
+						myObj=myObj[nameA[0]];
+					} else {
+						myObj=myObj[nameA[0]][nameIndex];
+					} // if
+				} // for
+
+				if ((elm.type != "checkbox" && elm.type != "radio") || (elm.type=="radio" && elm.checked)) {
+					if(name == name.split("[")[0]) {
+						myObj[name]=elm.value;
+					} else {
+						// can not set value when there is no name
+					}
+				} else if (elm.type == "checkbox" && elm.checked) {
+					if(typeof(myObj[name]) == 'undefined') {
+						myObj[name]=[ ];
+					}
+					myObj[name].push(elm.value);
+				} // if
+				name=undefined;
+			} // for
+		return obj;
+	}
+});

Added: struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/GoogleMap.js
URL: http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/GoogleMap.js?view=auto&rev=514083
==============================================================================
--- struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/GoogleMap.js (added)
+++ struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/GoogleMap.js Fri Mar  2 21:48:54 2007
@@ -0,0 +1,229 @@
+/*
+	Copyright (c) 2004-2006, The Dojo Foundation
+	All Rights Reserved.
+
+	Licensed under the Academic Free License version 2.1 or above OR the
+	modified BSD license. For more information on Dojo licensing, see:
+
+		http://dojotoolkit.org/community/licensing.shtml
+*/
+
+dojo.provide("dojo.widget.GoogleMap");
+dojo.require("dojo.event.*");
+dojo.require("dojo.math");
+dojo.require("dojo.widget.*");
+dojo.require("dojo.uri.Uri");
+dojo.require("dojo.widget.HtmlWidget");
+
+(function(){
+	var gkey = djConfig["gMapKey"]||djConfig["googleMapKey"];
+
+	//	the Google API key mechanism sucks.  We're hardcoding here for love and affection but I don't like it.
+	var uri=new dojo.uri.Uri(window.location.href);
+	if(uri.host=="www.dojotoolkit.org"){
+		gkey="ABQIAAAACUNdgv_7FGOmUslbm9l6_hRqjp7ri2mNiOEYqetD3xnFHpt5rBSjszDd1sdufPyQKUTyCf_YxoIxvw";
+	}
+	else if(uri.host=="blog.dojotoolkit.org"){
+		gkey="ABQIAAAACUNdgv_7FGOmUslbm9l6_hSkep6Av1xaMhVn3yCLkorJeXeLARQ6fammI_P3qSGleTJhoI5_1JmP_Q";
+	}
+	else if(uri.host=="archive.dojotoolkit.org"){
+		gkey="ABQIAAAACUNdgv_7FGOmUslbm9l6_hTaQpDt0dyGLIHbXMPTzg1kWeAfwRTwZNyrUfbfxYE9yIvRivEjcXoDTg";
+	}
+	else if(uri.host=="dojotoolkit.org"){
+		gkey="ABQIAAAACUNdgv_7FGOmUslbm9l6_hSaOaO_TgJ5c3mtQFnk5JO2zD5dZBRZk-ieqVs7BORREYNzAERmcJoEjQ";
+	}
+
+	if(!dojo.hostenv.post_load_){
+		if(!gkey || gkey==""){
+			dojo.raise("dojo.widget.GoogleMap: The Google Map widget requires a proper API key in order to be used.");
+		}
+		var tag = "<scr"+"ipt src='http://maps.google.com/maps?file=api&amp;v=2&amp;key="+gkey+"'></scri"+"pt>";
+		if(!dj_global["GMap2"]){
+			document.write(tag);
+		}
+	}else{
+		dojo.debug("Cannot initialize Google Map system after the page has been loaded! Please either manually include the script block provided by Google in your page or require() the GoogleMap widget before onload has fired.");
+	}
+})();
+
+dojo.widget.defineWidget(
+	"dojo.widget.GoogleMap",
+	dojo.widget.HtmlWidget,
+	function(){
+		//	summary
+		//	initializer/constructor for the simple GoogleMap widget.
+		this.map=null;
+		this.geocoder=null;
+		this.data=[];
+		this.datasrc="";
+		this.controls=["largemap","scale","maptype"];
+	},
+{
+	templatePath:null,
+	templateCssPath:null,
+	isContainer: false,
+
+	_defaultPoint:{lat:39.10662, lng: -94.578209},
+
+	setControls:function(){
+		//	summary
+		//	Set any controls on the map in question.
+		var methodmap={
+			largemap:GLargeMapControl,
+			smallmap:GSmallMapControl,
+			smallzoom:GSmallZoomControl,
+			scale:GScaleControl,
+			maptype:GMapTypeControl,
+			overview:GOverviewMapControl
+		};
+		for(var i=0; i<this.controls.length; i++){
+			this.map.addControl(new (methodmap[this.controls[i].toLowerCase()])());
+		}
+	},
+	
+	findCenter:function(/* GLatLngBounds */bounds){
+		//	summary
+		//	Returns the center point given the Bounds object.
+		if(this.data.length==1){
+			return (new GLatLng(this.data[0].lat, this.data[0].lng));	//	GLatLng
+		}
+		var clat=(bounds.getNorthEast().lat()+bounds.getSouthWest().lat())/2;
+		var clng=(bounds.getNorthEast().lng()+bounds.getSouthWest().lng())/2;
+		return (new GLatLng(clat,clng));	//	GLatLng
+	},
+
+	createPinpoint:function(/* GLatLng */pt, /* string? */overlay){
+		//	summary
+		//	Creates a marker at the given point, with an optional overlay HTML string.
+		var m=new GMarker(pt);
+		if(overlay){
+			GEvent.addListener(m,"click",function(){
+				m.openInfoWindowHtml("<div>"+overlay+"</div>");
+			});
+		}
+		return m;	//	GMarker
+	},
+	plot:function(/* object */obj){
+		//	summary
+		//	Plots a point at given lat/lng coordinate
+		var p=new GLatLng(obj.lat,obj.lng);
+		var d=obj.description||null;
+		var m=this.createPinpoint(p,d);
+		this.map.addOverlay(m);
+	},
+	plotAddress:function(/* string */address){
+		//	summary
+		//	Calls the Google Geocoder to get a lat/lng coordinate at string address
+		var self=this;
+		this.geocoder.getLocations(address, function(response){
+			if(!response || response.Status.code != 200){
+				alert("The address \"" + address + "\" was not found.");
+				return;
+			}
+			var obj={
+				lat:response.Placemark[0].Point.coordinates[1],
+				lng:response.Placemark[0].Point.coordinates[0],
+				description:response.Placemark[0].address
+			};
+			self.data.push(obj);
+			self.render();
+		});
+	},
+
+	parse:function(/* HTMLTable */table){
+		//	summary
+		//	Parses the passed table for data to plot on this map.
+		this.data=[];
+
+		//	get the column indices
+		var h=table.getElementsByTagName("thead")[0];
+		if(!h){
+			return;
+		}
+
+		var a=[];
+		var cols=h.getElementsByTagName("td");
+		if(cols.length==0){
+			cols=h.getElementsByTagName("th");
+		}
+		for(var i=0; i<cols.length; i++){
+			var c=cols[i].innerHTML.toLowerCase();
+			if(c=="long") c="lng";
+			a.push(c);
+		}
+		
+		//	parse the data
+		var b=table.getElementsByTagName("tbody")[0];
+		if(!b){
+			return;
+		}
+		for(var i=0; i<b.childNodes.length; i++){
+			if(!(b.childNodes[i].nodeName&&b.childNodes[i].nodeName.toLowerCase()=="tr")){
+				continue;
+			}
+			var cells=b.childNodes[i].getElementsByTagName("td");
+			var o={};
+			for(var j=0; j<a.length; j++){
+				var col=a[j];
+				if(col=="lat"||col=="lng"){
+					o[col]=parseFloat(cells[j].innerHTML);					
+				}else{
+					o[col]=cells[j].innerHTML;
+				}
+			}
+			this.data.push(o);
+		}
+	},
+	render:function(){
+		//	summary
+		//	Plots all acutal points in the current data array.
+		if(this.data.length==0){
+			this.map.setCenter(new GLatLng(this._defaultPoint.lat, this._defaultPoint.lng), 4);
+			return;
+		}
+
+		//	remove all overlays
+		this.map.clearOverlays();
+
+		var bounds=new GLatLngBounds();
+		var d=this.data;
+		for(var i=0; i<d.length; i++){
+			bounds.extend(new GLatLng(d[i].lat,d[i].lng));
+		}
+		var zoom=Math.min((this.map.getBoundsZoomLevel(bounds)-1),14);
+		this.map.setCenter(this.findCenter(bounds), zoom);
+
+		for(var i=0; i<this.data.length; i++){
+			this.plot(this.data[i]);
+		}
+	},
+
+	initialize:function(/* object */args, /* object */frag){
+		//	summary
+		//	initializes the widget
+		if(this.datasrc){
+			this.parse(dojo.byId(this.datasrc));
+		}
+		else if(this.domNode.getElementsByTagName("table")[0]){
+			this.parse(this.domNode.getElementsByTagName("table")[0]);
+		}
+	},
+	postCreate:function(){
+		//	summary
+		//	Sets up and renders the widget.
+
+		//	clean the domNode before creating the map.
+		while(this.domNode.childNodes.length>0){
+			this.domNode.removeChild(this.domNode.childNodes[0]);
+		}
+		if(this.domNode.style.position!="absolute"){
+			this.domNode.style.position="relative";
+		}
+		this.map=new GMap2(this.domNode);
+		try{
+			this.geocoder=new GClientGeocoder();
+		}catch(ex){}
+		this.render();
+		this.setControls();
+	}
+});

Added: struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/HtmlWidget.js
URL: http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/HtmlWidget.js?view=auto&rev=514083
==============================================================================
--- struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/HtmlWidget.js (added)
+++ struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/src/widget/HtmlWidget.js Fri Mar  2 21:48:54 2007
@@ -0,0 +1,170 @@
+/*
+	Copyright (c) 2004-2006, The Dojo Foundation
+	All Rights Reserved.
+
+	Licensed under the Academic Free License version 2.1 or above OR the
+	modified BSD license. For more information on Dojo licensing, see:
+
+		http://dojotoolkit.org/community/licensing.shtml
+*/
+
+dojo.provide("dojo.widget.HtmlWidget");
+dojo.require("dojo.widget.DomWidget");
+dojo.require("dojo.html.util");
+dojo.require("dojo.html.display");
+dojo.require("dojo.html.layout");
+dojo.require("dojo.lang.extras");
+dojo.require("dojo.lang.func");
+dojo.require("dojo.lfx.toggle");
+
+dojo.declare("dojo.widget.HtmlWidget", dojo.widget.DomWidget, {								 
+	widgetType: "HtmlWidget",
+
+	templateCssPath: null,
+	templatePath: null,
+
+	lang: "",
+	// for displaying/hiding widget
+	toggle: "plain",
+	toggleDuration: 150,
+
+	animationInProgress: false,
+
+	initialize: function(args, frag){
+	},
+
+	postMixInProperties: function(args, frag){
+		if(this.lang === ""){this.lang = null;}
+		// now that we know the setting for toggle, get toggle object
+		// (default to plain toggler if user specified toggler not present)
+		this.toggleObj =
+			dojo.lfx.toggle[this.toggle.toLowerCase()] || dojo.lfx.toggle.plain;
+	},
+
+	getContainerHeight: function(){
+		// NOTE: container height must be returned as the INNER height
+		dojo.unimplemented("dojo.widget.HtmlWidget.getContainerHeight");
+	},
+
+	getContainerWidth: function(){
+		return this.parent.domNode.offsetWidth;
+	},
+
+	setNativeHeight: function(height){
+		var ch = this.getContainerHeight();
+	},
+
+	createNodesFromText: function(txt, wrap){
+		return dojo.html.createNodesFromText(txt, wrap);
+	},
+
+	destroyRendering: function(finalize){
+		try{
+			if(!finalize && this.domNode){
+				dojo.event.browser.clean(this.domNode);
+			}
+			this.domNode.parentNode.removeChild(this.domNode);
+			delete this.domNode;
+		}catch(e){ /* squelch! */ }
+	},
+
+	/////////////////////////////////////////////////////////
+	// Displaying/hiding the widget
+	/////////////////////////////////////////////////////////
+	isShowing: function(){
+		return dojo.html.isShowing(this.domNode);
+	},
+
+	toggleShowing: function(){
+		// dojo.html.toggleShowing(this.domNode);
+		if(this.isHidden){
+			this.show();
+		}else{
+			this.hide();
+		}
+	},
+
+	show: function(){
+		this.animationInProgress=true;
+		this.isHidden = false;
+		this.toggleObj.show(this.domNode, this.toggleDuration, null,
+			dojo.lang.hitch(this, this.onShow), this.explodeSrc);
+	},
+
+	// called after the show() animation has completed
+	onShow: function(){
+		this.animationInProgress=false;
+		this.checkSize();
+	},
+
+	hide: function(){
+		this.animationInProgress = true;
+		this.isHidden = true;
+		this.toggleObj.hide(this.domNode, this.toggleDuration, null,
+			dojo.lang.hitch(this, this.onHide), this.explodeSrc);
+	},
+
+	// called after the hide() animation has completed
+	onHide: function(){
+		this.animationInProgress=false;
+	},
+
+	//////////////////////////////////////////////////////////////////////////////
+	// Sizing related methods
+	//  If the parent changes size then for each child it should call either
+	//   - resizeTo(): size the child explicitly
+	//   - or checkSize(): notify the child the the parent has changed size
+	//////////////////////////////////////////////////////////////////////////////
+
+	// Test if my size has changed.
+	// If width & height are specified then that's my new size; otherwise,
+	// query outerWidth/outerHeight of my domNode
+	_isResized: function(w, h){
+		// If I'm not being displayed then disregard (show() must
+		// check if the size has changed)
+		if(!this.isShowing()){ return false; }
+
+		// If my parent has been resized and I have style="height: 100%"
+		// or something similar then my size has changed too.
+		var wh = dojo.html.getMarginBox(this.domNode);
+		var width=w||wh.width;
+		var height=h||wh.height;
+		if(this.width == width && this.height == height){ return false; }
+
+		this.width=width;
+		this.height=height;
+		return true;
+	},
+
+	// Called when my parent has changed size, but my parent won't call resizeTo().
+	// This is useful if my size is height:100% or something similar.
+	// Also called whenever I am shown, because the first time I am shown I may need
+	// to do size calculations.
+	checkSize: function(){
+		if(!this._isResized()){ return; }
+		this.onResized();
+	},
+
+	// Explicitly set this widget's size (in pixels).
+	resizeTo: function(w, h){
+		dojo.html.setMarginBox(this.domNode, { width: w, height: h });
+		
+		// can't do sizing if widget is hidden because referencing node.offsetWidth/node.offsetHeight returns 0.
+		// do sizing on show() instead.
+		if(this.isShowing()){
+			this.onResized();
+		}
+	},
+
+	resizeSoon: function(){
+		if(this.isShowing()){
+			dojo.lang.setTimeout(this, this.onResized, 0);
+		}
+	},
+
+	// Called when my size has changed.
+	// Must notify children if their size has (possibly) changed
+	onResized: function(){
+		dojo.lang.forEach(this.children, function(child){ if(child.checkSize){child.checkSize();} });
+	}
+});