John Cappiello - Dojo.common-0.4.1

Documentation | Source
dojo.provide("dojo.selection.Selection");
dojo.require("dojo.lang.array");
dojo.require("dojo.lang.func");
dojo.require("dojo.lang.common");
dojo.require("dojo.math");

dojo.declare("dojo.selection.Selection", null,
	{
		initializer: function(items, isCollection){
			this.items = [];
			this.selection = [];
			this._pivotItems = [];
			this.clearItems();

			if(items) {
				if(isCollection) {
					this.setItemsCollection(items);
				} else {
					this.setItems(items);
				}
			}
		},

		// Array: items to select from, order matters for growable selections
		items: null,

		// Array: items selected, aren't stored in order (see sorted())
		selection: null, 
		lastSelected: null, // last item selected

		// Boolean: if true, grow selection will start from 0th item when nothing is selected
		allowImplicit: true, 

		// Integer: number of *selected* items
		length: 0, 

		// Boolean:
		//		if true, the selection is treated as an in-order and can grow
		//		by ranges, not just by single item
		isGrowable: true,

		_pivotItems: null, // stack of pivot items
		_pivotItem: null, // item we grow selections from, top of stack

		// event handlers
		onSelect: function(item){
			// summary: slot to be connect()'d to
		},
		onDeselect: function(item){
			// summary: slot to be connect()'d to
		},
		onSelectChange: function(item, selected){
			// summary: slot to be connect()'d to
		},

		_find: function(item, inSelection) {
			if(inSelection) {
				return dojo.lang.find(this.selection, item);
			} else {
				return dojo.lang.find(this.items, item);
			}
		},

		isSelectable: function(/*Object*/item){
			// summary:
			//		user-customizable and should be over-ridden, will filter
			//		items through this
			return true; // boolean
		},

		setItems: function(/* ... */){
			// summary:
			//		adds all passed arguments to the items array, removing any
			//		previously selected items.
			this.clearItems();
			this.addItems.call(this, arguments);
		},

 
		setItemsCollection: function(/*Object*/collection){
			// summary:
			//		like setItems, but use in case you have an active
			//		collection array-like object (i.e. getElementsByTagName
			//		collection) that manages its own order and item list
			this.items = collection;
		},

		addItems: function(/* ... */){
			// summary:
			//		adds all passed arguments to the items array
			var args = dojo.lang.unnest(arguments);
			for(var i = 0; i < args.length; i++){
				this.items.push(args[i]);
			}
		},

		addItemsAt: function(/*Object*/item, /*Object*/before /* ... */){
			// summary:
			//		add items to the array after the the passed "before" item.
			if(this.items.length == 0){ // work for empy case
				return this.addItems(dojo.lang.toArray(arguments, 2));
			}

			if(!this.isItem(item)){
				item = this.items[item];
			}
			if(!item){ throw new Error("addItemsAt: item doesn't exist"); }
			var idx = this._find(item);
			if(idx > 0 && before){ idx--; }
			for(var i = 2; i < arguments.length; i++){
				if(!this.isItem(arguments[i])){
					this.items.splice(idx++, 0, arguments[i]);
				}
			}
		},

		removeItem: function(/*Object*/item){
			// summary: remove item
			var idx = this._find(item);
			if(idx > -1) {
				this.items.splice(idx, 1);
			}
			// remove from selection
			// FIXME: do we call deselect? I don't think so because this isn't how
			// you usually want to deselect an item. For example, if you deleted an
			// item, you don't really want to deselect it -- you want it gone. -DS
			idx = this._find(item, true);
			if(idx > -1) {
				this.selection.splice(idx, 1);
			}
		},

		clearItems: function(){
			// summary: remove and uselect all items
			this.items = [];
			this.deselectAll();
		},

		isItem: function(/*Object*/item){
			// summary: do we already "know" about the passed item?
			return this._find(item) > -1; // boolean
		},

		isSelected: function(/*Object*/item){
			// summary:
			//		do we know about the item and is it selected by this
			//		selection?
			return this._find(item, true) > -1; // boolean
		},

		/**
		 * allows you to filter item in or out of the selection
		 * depending on the current selection and action to be taken
		**/
		selectFilter: function(item, selection, add, grow) {
			return true;
		},

		update: function(/*Object*/item, /*Boolean*/add, /*Boolean*/grow, noToggle) {
			// summary: manages selections, most selecting should be done here
			// item: item which may be added/grown to/only selected/deselected
			// add: behaves like ctrl in windows selection world
			// grow: behaves like shift
			// noToggle: if true, don't toggle selection on item
			if(!this.isItem(item)){ return false; } // boolean

			if(this.isGrowable && grow){
				if( (!this.isSelected(item)) && 
					this.selectFilter(item, this.selection, false, true) ){
					this.grow(item);
					this.lastSelected = item;
				}
			}else if(add){
				if(this.selectFilter(item, this.selection, true, false)){
					if(noToggle){
						if(this.select(item)){
							this.lastSelected = item;
						}
					}else if(this.toggleSelected(item)){
						this.lastSelected = item;
					}
				}
			}else{
				this.deselectAll();
				this.select(item);
			}

			this.length = this.selection.length;
			return true; // Boolean
		},

		grow: function(/*Object*/toItem, /*Object*/fromItem){
			// summary:
			//		Grow a selection. Any items in (fromItem, lastSelected]
			//		that aren't part of (fromItem, toItem] will be deselected
			// toItem: which item to grow selection to
			// fromItem: which item to start the growth from (it won't be selected)
			if(!this.isGrowable){ return; }

			if(arguments.length == 1){
				fromItem = this._pivotItem;
				if(!fromItem && this.allowImplicit){
					fromItem = this.items[0];
				}
			}
			if(!toItem || !fromItem){ return false; }

			var fromIdx = this._find(fromItem);

			// get items to deselect (fromItem, lastSelected]
			var toDeselect = {};
			var lastIdx = -1;
			if(this.lastSelected){
				lastIdx = this._find(this.lastSelected);
				var step = fromIdx < lastIdx ? -1 : 1;
				var range = dojo.math.range(lastIdx, fromIdx, step);
				for(var i = 0; i < range.length; i++){
					toDeselect[range[i]] = true;
				}
			}

			// add selection (fromItem, toItem]
			var toIdx = this._find(toItem);
			var step = fromIdx < toIdx ? -1 : 1;
			var shrink = lastIdx >= 0 && step == 1 ? lastIdx < toIdx : lastIdx > toIdx;
			var range = dojo.math.range(toIdx, fromIdx, step);
			if(range.length){
				for(var i = range.length-1; i >= 0; i--){
					var item = this.items[range[i]];
					if(this.selectFilter(item, this.selection, false, true)){
						if(this.select(item, true) || shrink){
							this.lastSelected = item;
						}
						if(range[i] in toDeselect){
							delete toDeselect[range[i]];
						}
					}
				}
			}else{
				this.lastSelected = fromItem;
			}

			// now deselect...
			for(var i in toDeselect){
				if(this.items[i] == this.lastSelected){
					//dojo.debug("oops!");
				}
				this.deselect(this.items[i]);
			}

			// make sure everything is all kosher after selections+deselections
			this._updatePivot();
		},

		growUp: function(){
			// summary: Grow selection upwards one item from lastSelected
			if(!this.isGrowable){ return; }

			var idx = this._find(this.lastSelected) - 1;
			while(idx >= 0){
				if(this.selectFilter(this.items[idx], this.selection, false, true)){
					this.grow(this.items[idx]);
					break;
				}
				idx--;
			}
		},

		growDown: function(){
			// summary: Grow selection downwards one item from lastSelected
			if(!this.isGrowable){ return; }

			var idx = this._find(this.lastSelected);
			if(idx < 0 && this.allowImplicit){
				this.select(this.items[0]);
				idx = 0;
			}
			idx++;
			while(idx > 0 && idx < this.items.length){
				if(this.selectFilter(this.items[idx], this.selection, false, true)){
					this.grow(this.items[idx]);
					break;
				}
				idx++;
			}
		},

		toggleSelected: function(/*Object*/item, /*Boolean*/noPivot){
			// summary:
			//		like it says on the tin. If noPivot is true, no selection
			//		pivot is added (or removed) from the selection. Returns 1
			//		if the item is selected, -1 if it is deselected, and 0 if
			//		the item is not under management.
			if(this.isItem(item)){
				if(this.select(item, noPivot)){ return 1; }
				if(this.deselect(item)){ return -1; }
			}
			return 0;
		},

		select: function(/*Object*/item, /*Boolean*/noPivot){
			// summary:
			//		like it says on the tin. If noPivot is true, no selection
			//		pivot is added  from the selection.
			if(this.isItem(item) && !this.isSelected(item)
				&& this.isSelectable(item)){
				this.selection.push(item);
				this.lastSelected = item;
				this.onSelect(item);
				this.onSelectChange(item, true);
				if(!noPivot){
					this._addPivot(item);
				}
				this.length = this.selection.length;
				return true;
			}
			return false;
		},

		deselect: function(item){
			// summary: deselects the item if it's selected.
			var idx = this._find(item, true);
			if(idx > -1){
				this.selection.splice(idx, 1);
				this.onDeselect(item);
				this.onSelectChange(item, false);
				if(item == this.lastSelected){
					this.lastSelected = null;
				}
				this._removePivot(item);
				this.length = this.selection.length;
				return true;
			}
			return false;
		},

		selectAll: function(){
			// summary: selects all known items
			for(var i = 0; i < this.items.length; i++){
				this.select(this.items[i]);
			}
		},

		deselectAll: function(){
			// summary: deselects all currently selected items
			while(this.selection && this.selection.length){
				this.deselect(this.selection[0]);
			}
		},

		selectNext: function(){
			// summary:
			//		clobbers the existing selection (if any) and selects the
			//		next item "below" the previous "bottom" selection. Returns
			//		whether or not selection was successful.
			var idx = this._find(this.lastSelected);
			while(idx > -1 && ++idx < this.items.length){
				if(this.isSelectable(this.items[idx])){
					this.deselectAll();
					this.select(this.items[idx]);
					return true;
				}
			}
			return false;
		},

		selectPrevious: function(){
			// summary:
			//		clobbers the existing selection (if any) and selects the
			//		item "above" the previous "top" selection. Returns whether
			//		or not selection was successful.
			var idx = this._find(this.lastSelected);
			while(idx-- > 0){
				if(this.isSelectable(this.items[idx])){
					this.deselectAll();
					this.select(this.items[idx]);
					return true;
				}
			}
			return false;
		},

		selectFirst: function(){
			// summary:
			//		select first selectable item. Returns whether or not an
			//		item was selected.
			this.deselectAll();
			var idx = 0;
			while(this.items[idx] && !this.select(this.items[idx])){
				idx++;
			}
			return this.items[idx] ? true : false;
		},

		selectLast: function(){
			// summary: select last selectable item
			this.deselectAll();
			var idx = this.items.length-1;
			while(this.items[idx] && !this.select(this.items[idx])) {
				idx--;
			}
			return this.items[idx] ? true : false;
		},

		_addPivot: function(item, andClear){
			this._pivotItem = item;
			if(andClear){
				this._pivotItems = [item];
			}else{
				this._pivotItems.push(item);
			}
		},

		_removePivot: function(item){
			var i = dojo.lang.find(this._pivotItems, item);
			if(i > -1){
				this._pivotItems.splice(i, 1);
				this._pivotItem = this._pivotItems[this._pivotItems.length-1];
			}

			this._updatePivot();
		},

		_updatePivot: function(){
			if(this._pivotItems.length == 0){
				if(this.lastSelected){
					this._addPivot(this.lastSelected);
				}
			}
		},

		sorted: function(){
			// summary: returns an array of items in sort order
			return dojo.lang.toArray(this.selection).sort(
				dojo.lang.hitch(this, function(a, b){
					var A = this._find(a), B = this._find(b);
					if(A > B){
						return 1;
					}else if(A < B){
						return -1;
					}else{
						return 0;
					}
				})
			);
		},

		updateSelected: function(){
			// summary: 
			//		remove any items from the selection that are no longer in
			//		this.items
			for(var i = 0; i < this.selection.length; i++) {
				if(this._find(this.selection[i]) < 0) {
					var removed = this.selection.splice(i, 1);

					this._removePivot(removed[0]);
				}
			}
			this.length = this.selection.length;
		}
	}
);