com.smartclient.debug.public.sc.client.language.Selection.js Maven / Gradle / Ivy
Show all versions of smartgwt Show documentation
/*
* Isomorphic SmartClient
* Version SC_SNAPSHOT-2011-08-08 (2011-08-08)
* Copyright(c) 1998 and beyond Isomorphic Software, Inc. All rights reserved.
* "SmartClient" is a trademark of Isomorphic Software, Inc.
*
* [email protected]
*
* http://smartclient.com/license
*/
//> @class Selection
//
// Maintains a 'selected' subset of a List or Array of objects, such as records in a record
// set, or widgets in a selectable header.
//
// Includes methods for selecting objects and checking which objects are selected, and also for
// selecting objects as a result of mouse events, including drag selection support.
// The selection object is used automatically to handle selection APIs on +link{class:ListGrid}
// and +link{class:TreeGrid} instances.
//
//
// @visibility external
// @treeLocation Client Reference/System
//<
//
// create the Selection class
//
isc.ClassFactory.defineClass("Selection");
// add default properties to the class
isc.Selection.addProperties({
//> @attr selection.selectionProperty (String : null : [IRA])
// Property to use to mark records as selected.
//
// Defaults to an auto-generated property name that starts with an underscore.
// @visibility serverSelection
//<
//selectionProperty:null,
//>@attr selection.data (Array | List : null : [IRWA])
// The set of data for which selection is being managed. If not specified at init time
// this can be set up via the selection.setData()
method.
// @visibility serverSelection
//<
//> @attr selection.enabledProperty (String : "enabled" : [IRA])
// Property used to indicated records as being disabled (therefore unselectable).
//<
enabledProperty:"enabled",
//> @attr selection.canSelectProperty (string : "canSelect" : [IRA])
// If set to false on a item, selection of that item is not allowed.
//<
canSelectProperty:"canSelect",
//> @attr selection.cascadeSelection (boolean : false : [IRA])
// Should children be selected when parent is selected? And should parent be
// selected when any child is selected?
//
// Note: Unloaded children are not affected and no load-on-demand is triggered.
//<
cascadeSelection:false,
// _dirty - manages whether we need to update the cache of selected records.
_dirty:true
});
isc.Selection.addClassProperties({
//> @type SelectionStyle
// Different styles of selection that a list, etc. might support
// @visibility external
// @group selection
//
// @value isc.Selection.NONE don't select at all
NONE : "none",
// @value isc.Selection.SINGLE select only one item at a time
SINGLE: "single",
// @value isc.Selection.MULTIPLE select one or more items
MULTIPLE: "multiple",
// @value isc.Selection.SIMPLE select one or more items as a toggle
// so you don't need to hold down control keys to select
// more than one object
SIMPLE: "simple",
//<
// for generating unique IDs for each Selection
_selectionID : 0
});
isc.Selection.addMethods({
//> @method selection.init() (A)
// Initialize this selection instance.
// Note: if the data
property is not set at init time, it should be passed to
// the selection using the selection.setData
method
// @group selection
// @param [all arguments] (object) objects with properties to override from default
// @visibility serverSelection
//<
init : function () {
// get unique ID and selection properties
if (!this.selectionProperty) this.selectionProperty = "_selection_"+isc.Selection._selectionID++;
this.partialSelectionProperty = "_partial" + this.selectionProperty;
// set the data object so we get notification for add and delete, etc.
// NOTE: if the data object wasn't set, use a new arrays
this.setData((this.data ? this.data : []));
},
// override destroy to clean up pointers to this.data
destroy : function () {
if (this.data) this.ignoreData(this.data);
delete this.data;
// selections aren't stored in global scope so no need to clear window[this.ID]
},
//> @method selection.setData()
// Initialize selection data.
// Call this method to associate the selection with a different data object.
// Note: No need to call this if the contents of the selection's data is modified
// @group selection
// @param newData (array) new data to maintain selection in
// @visibility serverSelection
//<
setData : function (newData) {
// if we are currently pointing to data, stop observing it
if (this.data != null) this.ignoreData(this.data);
// remember the new data
this.data = newData;
// observe the new data so we will update automatically when it changes
if (this.data != null) this.observeData(this.data);
},
//> @method selection.observeData() (A)
// Observe methods on the data so we change our state.
// Called automatically by selection.setData().
// Observes the data.dataChanged() method to invalidate the selection cache
// @group selection
//
//
// @param data (array) new data to be observed
// @visibility internal
//<
observeData : function (data) {
this.observe(data, "dataChanged", "observer.dataChanged()");
if (data.dataArrived) this.observe(data, "dataArrived", "observer.dataChanged()");
},
//> @method selection.ignoreData() (A)
// Stop observing methods on data when it goes out of scope.
// Called automatically by setData
// @group selection
//
// @param data (array) old data to be ignored
// @visibility internal
//<
ignoreData : function (data) {
if (!data) return;
if (this.isObserving(data, "dataChanged")) this.ignore(data, "dataChanged");
if (this.isObserving(data, "dataArrived")) this.ignore(data, "dataArrived");
},
dataChanged : function () {
this.markForRedraw();
},
//> @method selection.markForRedraw()
// Mark the selection as dirty, so it will be recalculated automatically
// @group selection
// @visibility internal
//<
markForRedraw : function () {
this._dirty = true;
},
//> @method selection.isSelected()
// Return true if a particular item is selected
// @group selection
//
// @param item (object) object to check
// @return (boolean) true == object is selected
// false == object is not selected
// @visibility external
//<
isSelected : function (item){
if (item == null) return false;
if (isc.isAn.XMLNode(item)) return "true" == item.getAttribute(this.selectionProperty);
return !!item[this.selectionProperty];
},
//> @method selection.isPartiallySelected()
// Returns true if a particular item is partially selected
// @group selection
//
// @param item (object) object to check
// @return (boolean) true == object is partially selected
// false == object is not partially selected
// @visibility external
//<
isPartiallySelected : function (item){
if (item == null) return false;
if (isc.isAn.XMLNode(item)) return "true" == item.getAttribute(this.partialSelectionProperty);
return !!item[this.partialSelectionProperty];
},
//> @method selection.anySelected()
// Whether at least one item is selected
// @group selection
//
// @return (boolean) true == at least one item is selected
// false == nothing at all is selected
// @visibility external
//<
anySelected : function () {
// return if the selection is non-empty
return this.getSelection().length > 0;
},
//> @method selection.multipleSelected()
// Whether multiple items are selected
// @group selection
//
// @return (boolean) true == more than one item is selected
// false == no items are selected, or only one item is selected
// @visibility external
//<
multipleSelected : function () {
return this.getSelection().length > 1;
},
//> @method selection.getSelection()
// Return an ordered array of all of the selected items
// @param [excludePartialSelections] When true, partially selected records will not be returned.
// Otherwise, all fully and partially selected records are
// returned.
// @return (array) list of selected items
// @group selection
// @visibility external
//<
getSelection : function (excludePartialSelections) {
// if the selection is dirty, cache it again
if (this._dirty) this.cacheSelection();
// return the cached selection list if possible
var selection = this._cache;
// If partial selections are excluded, built a new list of full selections only.
if (excludePartialSelections == true && selection != null && selection.length > 0) {
var cache = this._cache;
selection = [];
// Cache includes both fully and partially selected nodes.
for (var i = 0; i < cache.length; i++) {
var item = cache[i];
if (!this.isPartiallySelected(item)) {
selection[selection.length] = item;
}
}
}
return selection;
},
//> @method selection.getSelectedRecord()
// Return the first item in the list that is selected.
//
// Note that this should only be used if you know that one only one item
// may be selected, or you really don't care about items after the first one.
//
// To get all selected objects, use +link{method:selection.getSelection()}
// @group selection
//
// @return (object) first selected record, or null if nothing selected
// @visibility external
//<
getSelectedRecord : function () {
var selection = this.getSelection();
if (selection && selection.length > 0) return selection[0];
},
//> @method selection.cacheSelection() (A)
// Cache the selected records since we operate on it so often.
// Sets an internal variable _cache to hold the selection.
// @group selection
// @visibility internal
//<
cacheSelection : function () {
// create a new array to hold the cached selection
this._cache = [];
var data = this.getItemList(),
isRS = isc.isA.ResultSet != null && isc.isA.ResultSet(data),
length = data.getLength();
if (isRS && !data.lengthIsKnown()) {
this._dirty = false;
return;
}
// iterate over the records of the list, selecting those that have the selection property set
for (var i = 0; i < length; i++) {
// don't trigger fetches if working with a remote dataset
if (isRS && !data.rowIsLoaded(i)) continue;
var item = data.get(i);
if (item != null && this.isSelected(item)) {
this._cache[this._cache.length] = item
}
}
// note that the selection is no longer dirty
this._dirty = false;
},
//> @method selection.setSelected() (A)
// Select or deselect a particular item.
// All other selection routines go through this one, so by observing this routine you can
// monitor all selection changes.
// @group selection
//
// @param item (object) object to select
// @param newState (boolean) turn selection on or off
//
// @return (boolean) true == selection actually changed, false == no change
// @visibility external
//<
// We need the cascadingDirection to avoid changing direction while recursing through tree.
_$up:"up",
_$down:"down",
setSelected : function (item, newState, cascadingDirection) {
// if the item is null, just return
if (item == null) return false;
// if the item is not enabled, just return
if (item[this.enabledProperty] == false) return false;
// if the item cannot be selected, just return
if (item[this.canSelectProperty] == false) return false;
var property = this.selectionProperty,
partialProperty = this.partialSelectionProperty,
isNode = isc.isAn.XMLNode(item),
oldPartialValue = (isNode ? item.getAttribute(partialProperty) : item[partialProperty])
;
// default to selecting the item
if (newState == null) newState = true;
// Set partial property as needed.
if (this.cascadeSelection && !this.useRemoteSelection) {
// If this is a parent node and we are selecting/deselecting up the tree,
// need to determine if the selection is full or partial.
if (cascadingDirection == this._$up) {
var partialValue = false,
length = item.children.length;
for (var i = 0; i < length; i++) {
var child = item.children.get(i),
isChildNode = isc.isAn.XMLNode(child),
partialChild = (isChildNode ? child.getAttribute(partialProperty)
: child[partialProperty])
;
if (partialChild ||
(newState && !this.isSelected(child)) ||
(!newState && this.isSelected(child)))
{
partialValue = true;
break;
}
}
if (isNode) {
item.setAttribute(partialProperty, partialValue + "");
} else {
item[partialProperty] = partialValue;
}
// If deselecting but there is a partial selection, the node must still be selected.
if (newState != partialValue) newState = true;
} else if (item.children && item.children.length > 0) {
// Make sure a left over partial selection is cleared
if (isNode) {
item.removeAttribute(partialProperty);
} else {
delete item[partialProperty];
}
}
}
// get the oldState of the item, for detecting changes
var oldState = isNode ? item.getAttribute(property) : item[property];
if (oldState == null) oldState = false;
// set the state of the item
if (isNode) {
item.setAttribute(property, (newState == true) + "");
//this.logWarn("set attribute on: " + this.echoLeaf(item) + " to: " + newState +
// ", now reads: " + item.getAttribute(property));
} else {
item[property] = newState;
}
// remember that this was the last item to be selected
this.lastSelectionItem = item;
this.lastSelectionState = newState;
// if no change to state of item, simply return false
var newPartialValue = (isNode ? item.getAttribute(partialProperty) : item[partialProperty]);
if (newState == oldState && newPartialValue == oldPartialValue) return false;
// note that the selection is dirty so it can be recalculated
this.markForRedraw();
if (this.target && this.target.selectionChange) this.target.selectionChange(item, newState);
// Select/deselect parent and child records
if (this.cascadeSelection &&
!this.useRemoteSelection)
{
var lastItem = item,
lastState = newState;
// Select/deselect child records
if (cascadingDirection != this._$up && !isNode &&
item.children && item.children.length > 0)
{
this.selectList (item.children, newState, this._$down);
}
// Select/deselect parent records
if (cascadingDirection != this._$down &&
isc.isA.Tree(this.data) && this.data.getParent(item))
{
this.setSelected (this.data.getParent(item), newState, this._$up);
}
this.lastSelectionItem = lastItem;
this.lastSelectionState = lastState;
}
// return true to indicate that there was a change in the selection state
return true;
},
//> @method selection.select()
// Select a particular item
// @group selection
//
// @param item (object) object to select
// @return (boolean) true == selection actually changed, false == no change
// @visibility external
//<
select : function (item) {
return this.setSelected(item, true);
},
//> @method selection.deselect()
// Deselect a particular item
// @group selection
//
// @param item (object) object to select
// @return (boolean) true == selection actually changed, false == no change
// @visibility external
//<
deselect : function (item) {
return this.setSelected(item, false);
},
//> @method selection.selectSingle()
// Select a single item and deselect everything else
// @group selection
//
// @param item (object) object to select
// @return (boolean) true == selection actually changed, false == no change
// @visibility external
//<
selectSingle : function (item) {
this.deselectAll();
return this.select(item);
},
//> @method selection.selectList()
// Select an array of items (subset of the entire list)
// @group selection
//
// @param list (object[]) array of objects to select
// @return (boolean) true == selection actually changed, false == no change
// @visibility external
//<
selectList : function (list, newState) {
if (newState == null) newState = true;
if (!list) return false;
var anyChanged = false, length = list.getLength();
for (var i = 0; i < length; i++) {
var item = list.get(i);
if (this.isSelected(item) == newState) continue;
anyChanged = this.setSelected(item, newState) || anyChanged;
}
return anyChanged;
},
//> @method selection.deselectList()
// Deselect an array of items (subset of the entire list)
// @group selection
//
// @param list (object[]) array of objects to select
// @return (boolean) true == selection actually changed, false == no change
// @visibility external
//<
deselectList : function (list) {
this.selectList(list, false);
},
//> @method selection.selectAll()
// Select ALL records of the list
// @group selection
//
// @return (boolean) true == selection actually changed, false == no change
// @visibility external
//<
selectAll : function () {
return this.selectRange(0, this.getItemList().getLength());
},
//> @method selection.deselectAll()
// Deselect ALL records of the list
// @group selection
//
// @return (boolean) true == selection actually changed, false == no change
// @visibility external
//<
deselectAll : function () {
return this.deselectList(this.getSelection());
},
//> @method selection.selectItem()
// Select a particular item by its position in the list
//
// @param position (number) index of the item to be selected
// @return (boolean) true == selection actually changed, false == no change
// @group selection
// @visibility external
//<
selectItem : function (position) {
return this.selectRange(position, position+1);
},
//> @method selection.deselectItem()
// Deselect a particular item by its position in the list
//
// @param position (number) index of the item to be selected
// @return (boolean) true == selection actually changed, false == no change
// @group selection
// @visibility external
//<
deselectItem : function (position) {
return this.deselectRange(position, position+1);
},
//> @method selection.selectRange()
// Select range of records from start
to end
, non-inclusive.
// @group selection
//
// @param start (number) start index to select
// @param end (number) end index (non-inclusive)
// @param [newState] (boolean) optional new selection state to set. True means
// selected, false means unselected. Defaults to true.
//
// @return (boolean) true == selection actually changed, false == no change
// @visibility external
//<
selectRange : function (start, end, newState) {
if (newState == null) newState = true;
// Use visible records for range selection
var data = this.data;
if (isc.isA.ResultSet != null && isc.isA.ResultSet(data) &&
!data.rangeIsLoaded(start, end))
{
isc.warn(this.selectionRangeNotLoadedMessage);
return false; // no change
}
return this.selectList(data.getRange(start, end), newState);
},
//> @attr selection.selectionRangeNotLoadedMessage (String : Can't select that many records at once.<br><br>Please try working in smaller batches. : IRWA)
//
// Message to display to the user in a warn
dialog if +link{selection.selectRange()} is
// called for a selection on a ResultSet, where the range of records to be selected has not been
// loaded.
// @group i18nMessages
// @visibility external
//<
selectionRangeNotLoadedMessage:"Can't select that many records at once.
" +
"Please try working in smaller batches.",
//> @method selection.deselectRange()
// Deselect range of records from start
to end
, non-inclusive
//
// @group selection
//
// @param start (number) start index to select
// @param end (number) end index (non-inclusive)
//
// @return (boolean) true == selection actually changed, false == no change
// @visibility external
//<
deselectRange : function (start, end) {
return this.selectRange(start, end, false);
},
// DOCFIX: this methods shouldn't require the "target", a Canvas. Need to fix that before we make
// them public.
//> @method selection.selectOnMouseDown() (A)
// Update the selection as the result of a mouseDown event.
// Handles shift, control, etc. key selection as well.
// Call this from a mouseDown handler.
//
// @group selection, mouseEvents
//
// @param target (Canvas) target object
// @param position (number) position where mouse went down on
//
// @return (boolean) true == selection was changed, false == no change
//<
selectOnMouseDown : function (target, recordNum) {
// modify selection according to the specified style (defaulting to multiple selection)
var selectionType = target.selectionType || isc.Selection.MULTIPLE;
// if the target's selectionType is NONE, just bail
if (selectionType == isc.Selection.NONE) return false;
// remember mouseDown location in case we start drag selecting
this.startRow = this.lastRow = recordNum;
//>DEBUG
this.logDebug("selectOnMouseDown: recordNum: " + recordNum);
//= lastRecord) {
// select from the firstRecord to the clicked record
this.selectRange(firstRecord, recordNum + 1);
// else if the clicked record is the first record or before
} else if (recordNum <= firstRecord) {
// select from the clicked record to the lastRecord
this.selectRange(recordNum, lastRecord + 1);
// otherwise it's in between the start and end record
} else {
// select from the firstRecord to the clicked record
this.selectRange(firstRecord, recordNum + 1);
// and deselect form the clickedRecord + 1 to the lastRecord
this.deselectRange(recordNum + 1, lastRecord +1);
}
return true;
}
// Case 3: SIMPLE selection (toggle selection of this record, but defer deselection until
// mouseUp)
} else if (selectionType == isc.Selection.SIMPLE) {
if (!recordSelected) {
this.select(record);
return true;
} else {
this.deselectRecordOnMouseUp = true;
return false;
}
// Case 4: meta-key selection in a multiple selection range
// (simply toggle selection of this record)
} else if (metaKeyDown) {
this.setSelected(record, !recordSelected);
return true;
// Case 5: normal selection (no modifier keys) in a multiple selection range
} else {
if (!recordSelected) {
// if you click outside of the selection, select the new record and deselect
// everything else
this.selectSingle(record);
return true;
} else if (isc.EventHandler.rightButtonDown()) {
// never deselect if you right click on the selection, unless you start drag
// selecting
this.deselectOnDragMove = true;
return false;
} else {
// simpleDeselect mode: this mode is designed to make it easy to entirely get rid
// of your selection, so you don't have to know about ctrl-clicking. In a
// nutshell, if you click on the existing selection, it will be entirely
// deselected.
if (this.dragSelection) {
if (this.simpleDeselect) {
// if you click on the selection, deselect the entire selection including
// the clicked-on cell. Later, if a drag begins, select the clicked-on
// cell.
this.deselectAll();
this.selectOriginOnDragMove = true;
return true;
}
// for a drag selection, deselect others immediately; otherwise we'll be
// dragging out a new selection within/overlapping with an existing selection,
// which we only want to do on a ctrl-click. This matches Excel.
this.selectSingle(record);
return true;
} else {
if (this.simpleDeselect) {
// deselect everything on mouseUp, including the cell clicked on
this.deselectAllOnMouseUp = true;
} else {
// if we click in a multiple selection, deselect everything but the
// clicked-on item, but don't do it until mouseUp in order to allow
// dragging the current selection. This matches Windows Explorer.
this.deselectOthersOnMouseUp = (selection.length > 1);
}
return false;
}
}
}
},
//> @method selection.selectOnDragMove() (A)
// During drag selection, update the selection as a result of a dragMove event
//
// @group selection, mouseEvents
//
// @param target (Canvas) target object
// @param position (number) position where mouse went down on
//
// @return (boolean) true == selection was changed, false == no change
//
//<
selectOnDragMove : function (target, currRow) {
var startRow = this.startRow,
lastRow = this.lastRow;
// If the mouse has moved further away from the start position since the last dragMove, select
// more cells. If it's moved closer to the start position, deselect cells.
if (currRow < 0) {
//>DEBUG
this.logWarn("selectOnDragMove: got negative coordinate: " + currRow);
// startRow && startRow > lastRow) ||
(lastRow > startRow && startRow > currRow))
{
//this.logWarn("dragged from one side of start to the other");
// dragged from one side of start to the other
this.deselectAll();
// select from start to current inclusive
if (startRow > currRow) {
this.selectRange(currRow, startRow+1);
} else {
this.selectRange(startRow, currRow+1);
}
} else if (startRow >= lastRow && lastRow > currRow) {
//this.logWarn("increasing selection on the left of start");
// increasing selection on the left of start
this.selectRange(currRow, lastRow);
} else if (startRow >= currRow && currRow > lastRow) {
//this.logWarn("decreasing selection on the left of start");
// decreasing selection on the left of start
this.deselectRange(lastRow, currRow);
} else if (startRow <= currRow && currRow < lastRow) {
//this.logWarn("decreasing selection on the right of start");
// decreasing selection on the right of start
this.deselectRange(currRow+1, lastRow+1);
} else if (startRow <= lastRow && lastRow < currRow) {
//this.logWarn("increasing selection on the right of start");
// increasing selection on the right of start
this.selectRange(lastRow, currRow+1);
//>DEBUG
} else {
this.logWarn("dragMove case not handled: lastRow: " + lastRow +
", currRow: " + currRow + ", startRow " + startRow);
// @method selection.selectOnMouseUp() (A)s
// @group selection, mouseEvents
// Update the selection as the result of a mouseUp event.
// We currently use this to defer deselection for drag-and-drop of multiple records.
// Call this from a mouseUp handler.
// @see ListGrid.mouseUp()
//
// @param target (Canvas) target object
// @param recordNum (number) record number mouse went down on
//
// @return (boolean) true == selection was changed, false == no change
//<
selectOnMouseUp : function (target, recordNum) {
// if the target's selectionType is NONE, just bail
if (target.selectionType == isc.Selection.NONE) return false;
//>DEBUG
this.logDebug("selectOnMouseUp: recordNum: " + recordNum);
//