John Cappiello - Dojo.common-0.4.1

Documentation | Source
dojo.provide("dojo.widget.PageContainer");

dojo.require("dojo.lang.func");
dojo.require("dojo.widget.*");
dojo.require("dojo.event.*");
dojo.require("dojo.html.selection");

dojo.widget.defineWidget("dojo.widget.PageContainer", dojo.widget.HtmlWidget, {
	// summary
	//	A container that has multiple children, but shows only
	//	one child at a time (like looking at the pages in a book one by one).
	//
	//	Publishes topics <widgetId>-addChild, <widgetId>-removeChild, and <widgetId>-selectChild
	//
	//	Can be base class for container, Wizard, Show, etc.

	isContainer: true,

	// doLayout: Boolean
	//  if true, change the size of my currently displayed child to match my size
	doLayout: true,

	templateString: "<div dojoAttachPoint='containerNode'></div>",

	// selectedChild: String
	//   id of the currently shown page
	selectedChild: "",

	fillInTemplate: function(args, frag) {
		// Copy style info from input node to output node
		var source = this.getFragNodeRef(frag);
		dojo.html.copyStyle(this.domNode, source);
		dojo.widget.PageContainer.superclass.fillInTemplate.apply(this, arguments);
	},

	postCreate: function(args, frag) {
		if(this.children.length){
			// Setup each page panel
			dojo.lang.forEach(this.children, this._setupChild, this);

			// Figure out which child to initially display
			var initialChild;
			if(this.selectedChild){
				this.selectChild(this.selectedChild);
			}else{
				for(var i=0; i<this.children.length; i++){
					if(this.children[i].selected){
						this.selectChild(this.children[i]);
						break;
					}
				}
				if(!this.selectedChildWidget){
					this.selectChild(this.children[0]);
				}
			}
		}
	},

	addChild: function(child){
		dojo.widget.PageContainer.superclass.addChild.apply(this, arguments);
		this._setupChild(child);

		// in case the tab labels have overflowed from one line to two lines
		this.onResized();

		// if this is the first child, then select it
		if(!this.selectedChildWidget){
			this.selectChild(child);
		}
	},

	_setupChild: function(/*Widget*/ page){
		// Summary: Add the given child to this page container

		page.hide();
		
		// since we are setting the width/height of the child elements, they need
		// to be position:relative, or IE has problems (See bug #2033)
		page.domNode.style.position="relative";

		// publish the addChild event for panes added via addChild(), and the original panes too
		dojo.event.topic.publish(this.widgetId+"-addChild", page);
	},

	removeChild: function(/*Widget*/ page){
		dojo.widget.PageContainer.superclass.removeChild.apply(this, arguments);

		// If we are being destroyed than don't run the code below (to select another page), because we are deleting
		// every page one by one
		if(this._beingDestroyed){ return; }

		// this will notify any tablists to remove a button; do this first because it may affect sizing
		dojo.event.topic.publish(this.widgetId+"-removeChild", page);

		// in case the tab labels now take up one line instead of two lines
		this.onResized();

		if (this.selectedChildWidget === page) {
			this.selectedChildWidget = undefined;
			if (this.children.length > 0) {
				this.selectChild(this.children[0], true);
			}
		}
	},

	selectChild: function(/*Widget*/ page, /*Widget*/ callingWidget){
		// summary
		//	Show the given widget (which must be one of my children)
		page = dojo.widget.byId(page);
		this.correspondingPageButton = callingWidget;

		// Deselect old page and select new one
		if(this.selectedChildWidget){
			this._hideChild(this.selectedChildWidget);
		}
		this.selectedChildWidget = page;
		this.selectedChild = page.widgetId;
		this._showChild(page);
		page.isFirstChild = (page == this.children[0]);
		page.isLastChild = (page == this.children[this.children.length-1]);
		dojo.event.topic.publish(this.widgetId+"-selectChild", page);
	},

	forward: function(){
		// Summary: advance to next page
		var index = dojo.lang.find(this.children, this.selectedChildWidget);
		this.selectChild(this.children[index+1]);
	},

	back: function(){
		// Summary: go back to previous page
		var index = dojo.lang.find(this.children, this.selectedChildWidget);
		this.selectChild(this.children[index-1]);
	},

	onResized: function(){
		// Summary: called when any page is shown, to make it fit the container correctly
		if(this.doLayout && this.selectedChildWidget){
			with(this.selectedChildWidget.domNode.style){
				top = dojo.html.getPixelValue(this.containerNode, "padding-top", true);
				left = dojo.html.getPixelValue(this.containerNode, "padding-left", true);
			}
			var content = dojo.html.getContentBox(this.containerNode);
			this.selectedChildWidget.resizeTo(content.width, content.height);
		}
	},

	_showChild: function(/*Widget*/ page) {
		// size the current page (in case this is the first time it's being shown, or I have been resized)
		if(this.doLayout){
			var content = dojo.html.getContentBox(this.containerNode);
			page.resizeTo(content.width, content.height);
		}

		page.selected=true;
		page.show();
	},

	_hideChild: function(/*Widget*/ page) {
		page.selected=false;
		page.hide();
	},

	closeChild: function(/*Widget*/ page) {
		// summary
		//	callback when user clicks the [X] to remove a page
		//	if onClose() returns true then remove and destroy the childd
		var remove = page.onClose(this, page);
		if(remove) {
			this.removeChild(page);
			// makes sure we can clean up executeScripts in ContentPane onUnLoad
			page.destroy();
		}
	},

	destroy: function(){
		this._beingDestroyed = true;
		dojo.event.topic.destroy(this.widgetId+"-addChild");
		dojo.event.topic.destroy(this.widgetId+"-removeChild");
		dojo.event.topic.destroy(this.widgetId+"-selectChild");
		dojo.widget.PageContainer.superclass.destroy.apply(this, arguments);
	}
});


dojo.widget.defineWidget(
    "dojo.widget.PageController",
    dojo.widget.HtmlWidget,
	{
		// summary
		//	Set of buttons to select a page in a page list.
		//	Monitors the specified PageContaine, and whenever a page is
		//	added, deleted, or selected, updates itself accordingly.

		templateString: "<span wairole='tablist' dojoAttachEvent='onKey'></span>",
		isContainer: true,

		// containerId: String
		//	the id of the page container that I point to
		containerId: "",

		// buttonWidget: String
		//	the name of the button widget to create to correspond to each page
		buttonWidget: "PageButton",

		// class: String
		//	Class name to apply to the top dom node
		"class": "dojoPageController",

		fillInTemplate: function() {
			dojo.html.addClass(this.domNode, this["class"]);  // "class" is a reserved word in JS
			dojo.widget.wai.setAttr(this.domNode, "waiRole", "role", "tablist");
		},

		postCreate: function(){
			this.pane2button = {};		// mapping from panes to buttons

			// If children have already been added to the page container then create buttons for them
			var container = dojo.widget.byId(this.containerId);
			if(container){
				dojo.lang.forEach(container.children, this.onAddChild, this);
			}

			dojo.event.topic.subscribe(this.containerId+"-addChild", this, "onAddChild");
			dojo.event.topic.subscribe(this.containerId+"-removeChild", this, "onRemoveChild");
			dojo.event.topic.subscribe(this.containerId+"-selectChild", this, "onSelectChild");
		},

		destroy: function(){
			dojo.event.topic.unsubscribe(this.containerId+"-addChild", this, "onAddChild");
			dojo.event.topic.unsubscribe(this.containerId+"-removeChild", this, "onRemoveChild");
			dojo.event.topic.unsubscribe(this.containerId+"-selectChild", this, "onSelectChild");
			dojo.widget.PageController.superclass.destroy.apply(this, arguments);
		},

		onAddChild: function(/*Widget*/ page){
			// summary
			//   Called whenever a page is added to the container.
			//   Create button corresponding to the page.
			var button = dojo.widget.createWidget(this.buttonWidget,
				{
					label: page.label,
					closeButton: page.closable
				});
			this.addChild(button);
			this.domNode.appendChild(button.domNode);
			this.pane2button[page]=button;
			page.controlButton = button;	// this value might be overwritten if two tabs point to same container

			var _this = this;
			dojo.event.connect(button, "onClick", function(){ _this.onButtonClick(page); });
			dojo.event.connect(button, "onCloseButtonClick", function(){ _this.onCloseButtonClick(page); });
		},

		onRemoveChild: function(/*Widget*/ page){
			// summary
			//   Called whenever a page is removed from the container.
			//   Remove the button corresponding to the page.
			if(this._currentChild == page){ this._currentChild = null; }
			var button = this.pane2button[page];
			if(button){
				button.destroy();
			}
			this.pane2button[page] = null;
		},

		onSelectChild: function(/*Widget*/ page){
			// Summary
			//	Called when a page has been selected in the PageContainer, either by me or by another PageController
			if(this._currentChild){
				var oldButton=this.pane2button[this._currentChild];
				oldButton.clearSelected();
			}
			var newButton=this.pane2button[page];
			newButton.setSelected();
			this._currentChild=page;
		},

		onButtonClick: function(/*Widget*/ page){
			// summary
			//   Called whenever one of my child buttons is pressed in an attempt to select a page
			var container = dojo.widget.byId(this.containerId);	// TODO: do this via topics?
			container.selectChild(page, false, this);
		},

		onCloseButtonClick: function(/*Widget*/ page){
			// summary
			//   Called whenever one of my child buttons [X] is pressed in an attempt to close a page
			var container = dojo.widget.byId(this.containerId);
			container.closeChild(page);
		},

		onKey: function(/*Event*/ evt){
			// summary:
			//   Handle keystrokes on the page list, for advancing to next/previous button

			if( (evt.keyCode == evt.KEY_RIGHT_ARROW)||
				(evt.keyCode == evt.KEY_LEFT_ARROW) ){
				var current = 0;
				var next = null;	// the next button to focus on
				
				// find currently focused button in children array
				var current = dojo.lang.find(this.children, this.pane2button[this._currentChild]);
				
				// pick next button to focus on
				if(evt.keyCode == evt.KEY_RIGHT_ARROW){
					next = this.children[ (current+1) % this.children.length ]; 
				}else{ // is LEFT_ARROW
					next = this.children[ (current+ (this.children.length-1)) % this.children.length ];
				}
				
				dojo.event.browser.stopEvent(evt);
				next.onClick();
			}
		}
	}
);

dojo.widget.defineWidget("dojo.widget.PageButton", dojo.widget.HtmlWidget,
{
	// summary
	//	Internal widget used by PageList.
	//	The button-like or tab-like object you click to select or delete a page

	templateString: "<span class='item'>" +
						"<span dojoAttachEvent='onClick' dojoAttachPoint='titleNode' class='selectButton'>${this.label}</span>" +
						"<span dojoAttachEvent='onClick:onCloseButtonClick' class='closeButton'>[X]</span>" +
					"</span>",

	// label: String
	//  Name to print on the button
	label: "foo",
	
	// closeButton: Boolean
	//	true iff we should also print a close icon to destroy corresponding page
	closeButton: false,

	onClick: function(){
		// summary
		//  Basically this is the attach point PageController listens to, to select the page
		this.focus();
	},

	onCloseButtonMouseOver: function(){
		// summary
		//	The close button changes color a bit when you mouse over	
		dojo.html.addClass(this.closeButtonNode, "closeHover");
	},

	onCloseButtonMouseOut: function(){
		// summary
		// 	Revert close button to normal color on mouse out
		dojo.html.removeClass(this.closeButtonNode, "closeHover");
	},

	onCloseButtonClick: function(/*Event*/ evt){
		// summary
		//	Handle clicking the close button for this tab
	},
	
	setSelected: function(){
		// summary
		//	This is run whenever the page corresponding to this button has been selected
		dojo.html.addClass(this.domNode, "current");
		this.titleNode.setAttribute("tabIndex","0");
	},
	
	clearSelected: function(){
		// summary
		//	This function is run whenever the page corresponding to this button has been deselected (and another page has been shown)
		dojo.html.removeClass(this.domNode, "current");
		this.titleNode.setAttribute("tabIndex","-1");
	},

	focus: function(){
		// summary
		//	This will focus on the this button (for accessibility you need to do this when the button is selected)
		if(this.titleNode.focus){	// mozilla 1.7 doesn't have focus() func
			this.titleNode.focus();
		}
	}
});

// These arguments can be specified for the children of a PageContainer.
// Since any widget can be specified as a PageContainer child, mix them
// into the base widget class.  (This is a hack, but it's effective.)
dojo.lang.extend(dojo.widget.Widget, {
	// label: String
	//		Label or title of this widget.  Used by TabContainer to the name the tab, etc.
	label: "",
	
	// selected: Boolean
	//		Is this child currently selected?
	selected: false,
	
	// closable: Boolean
	//		True if user can close (destroy) this child, such as (for example) clicking the X on the tab.
	closable: false,	// true if user can close this tab pane
	
	onClose: function(){
		// summary: Callback if someone tries to close the child, child will be closed if func returns true
		return true;
	}
});