define([
|
'dojo/_base/declare',
|
'dojo/dom-class',
|
'dojo/on',
|
'dojo/has',
|
'dojo/aspect',
|
'./List',
|
'dojo/has!touch?./util/touch',
|
'dojo/query',
|
'dojo/_base/sniff',
|
'dojo/dom' // for has('css-user-select') in 1.8.2+
|
], function (declare, domClass, on, has, aspect, List, touchUtil) {
|
|
has.add('dom-comparedocumentposition', function (global, doc, element) {
|
return !!element.compareDocumentPosition;
|
});
|
|
// Add a feature test for the onselectstart event, which offers a more
|
// graceful fallback solution than node.unselectable.
|
has.add('dom-selectstart', typeof document.onselectstart !== 'undefined');
|
|
var ctrlEquiv = has('mac') ? 'metaKey' : 'ctrlKey',
|
hasUserSelect = has('css-user-select'),
|
hasPointer = has('pointer'),
|
hasMSPointer = hasPointer && hasPointer.slice(0, 2) === 'MS',
|
downType = hasPointer ? hasPointer + (hasMSPointer ? 'Down' : 'down') : 'mousedown',
|
upType = hasPointer ? hasPointer + (hasMSPointer ? 'Up' : 'up') : 'mouseup';
|
|
if (hasUserSelect === 'WebkitUserSelect' && typeof document.documentElement.style.msUserSelect !== 'undefined') {
|
// Edge defines both webkit and ms prefixes, rendering feature detects as brittle as UA sniffs...
|
hasUserSelect = false;
|
}
|
|
function makeUnselectable(node, unselectable) {
|
// Utility function used in fallback path for recursively setting unselectable
|
var value = node.unselectable = unselectable ? 'on' : '',
|
elements = node.getElementsByTagName('*'),
|
i = elements.length;
|
|
while (--i) {
|
if (elements[i].tagName === 'INPUT' || elements[i].tagName === 'TEXTAREA') {
|
continue; // Don't prevent text selection in text input fields.
|
}
|
elements[i].unselectable = value;
|
}
|
}
|
|
function setSelectable(grid, selectable) {
|
// Alternative version of dojo/dom.setSelectable based on feature detection.
|
|
// For FF < 21, use -moz-none, which will respect -moz-user-select: text on
|
// child elements (e.g. form inputs). In FF 21, none behaves the same.
|
// See https://developer.mozilla.org/en-US/docs/CSS/user-select
|
var node = grid.bodyNode,
|
value = selectable ? 'text' : has('ff') < 21 ? '-moz-none' : 'none';
|
|
// In IE10+, -ms-user-select: none will block selection from starting within the
|
// element, but will not block an existing selection from entering the element.
|
// When using a modifier key, IE will select text inside of the element as well
|
// as outside of the element, because it thinks the selection started outside.
|
// Therefore, fall back to other means of blocking selection for IE10+.
|
// Newer versions of Dojo do not even report msUserSelect (see https://github.com/dojo/dojo/commit/7ae2a43).
|
if (hasUserSelect && hasUserSelect !== 'msUserSelect') {
|
node.style[hasUserSelect] = value;
|
}
|
else if (has('dom-selectstart')) {
|
// For browsers that don't support user-select but support selectstart (IE<10),
|
// we can hook up an event handler as necessary. Since selectstart bubbles,
|
// it will handle any child elements as well.
|
// Note, however, that both this and the unselectable fallback below are
|
// incapable of preventing text selection from outside the targeted node.
|
if (!selectable && !grid._selectstartHandle) {
|
grid._selectstartHandle = on(node, 'selectstart', function (evt) {
|
var tag = evt.target && evt.target.tagName;
|
|
// Prevent selection except where a text input field is involved.
|
if (tag !== 'INPUT' && tag !== 'TEXTAREA') {
|
evt.preventDefault();
|
}
|
});
|
}
|
else if (selectable && grid._selectstartHandle) {
|
grid._selectstartHandle.remove();
|
delete grid._selectstartHandle;
|
}
|
}
|
else {
|
// For browsers that don't support either user-select or selectstart (Opera),
|
// we need to resort to setting the unselectable attribute on all nodes
|
// involved. Since this doesn't automatically apply to child nodes, we also
|
// need to re-apply it whenever rows are rendered.
|
makeUnselectable(node, !selectable);
|
if (!selectable && !grid._unselectableHandle) {
|
grid._unselectableHandle = aspect.after(grid, 'renderRow', function (row) {
|
makeUnselectable(row, true);
|
return row;
|
});
|
}
|
else if (selectable && grid._unselectableHandle) {
|
grid._unselectableHandle.remove();
|
delete grid._unselectableHandle;
|
}
|
}
|
}
|
|
return declare(null, {
|
// summary:
|
// Add selection capabilities to a grid. The grid will have a selection property and
|
// fire "dgrid-select" and "dgrid-deselect" events.
|
|
// selectionDelegate: String
|
// Selector to delegate to as target of selection events.
|
selectionDelegate: '.dgrid-row',
|
|
// selectionEvents: String|Function
|
// Event (or comma-delimited events, or extension event) to listen on
|
// to trigger select logic.
|
selectionEvents: downType + ',' + upType + ',dgrid-cellfocusin',
|
|
// selectionTouchEvents: String|Function
|
// Event (or comma-delimited events, or extension event) to listen on
|
// in addition to selectionEvents for touch devices.
|
selectionTouchEvents: has('touch') ? touchUtil.tap : null,
|
|
// deselectOnRefresh: Boolean
|
// If true, the selection object will be cleared when refresh is called.
|
deselectOnRefresh: true,
|
|
// allowSelectAll: Boolean
|
// If true, allow ctrl/cmd+A to select all rows.
|
// Also consulted by the selector plugin for showing select-all checkbox.
|
allowSelectAll: false,
|
|
// selection:
|
// An object where the property names correspond to
|
// object ids and values are true or false depending on whether an item is selected
|
selection: {},
|
|
// selectionMode: String
|
// The selection mode to use, can be "none", "multiple", "single", or "extended".
|
selectionMode: 'extended',
|
|
// allowTextSelection: Boolean
|
// Whether to still allow text within cells to be selected. The default
|
// behavior is to allow text selection only when selectionMode is none;
|
// setting this property to either true or false will explicitly set the
|
// behavior regardless of selectionMode.
|
allowTextSelection: undefined,
|
|
// _selectionTargetType: String
|
// Indicates the property added to emitted events for selected targets;
|
// overridden in CellSelection
|
_selectionTargetType: 'rows',
|
|
create: function () {
|
this.selection = {};
|
return this.inherited(arguments);
|
},
|
postCreate: function () {
|
this.inherited(arguments);
|
|
this._initSelectionEvents();
|
|
// Force selectionMode setter to run
|
var selectionMode = this.selectionMode;
|
this.selectionMode = '';
|
this._setSelectionMode(selectionMode);
|
},
|
|
destroy: function () {
|
this.inherited(arguments);
|
|
// Remove any extra handles added by Selection.
|
if (this._selectstartHandle) {
|
this._selectstartHandle.remove();
|
}
|
if (this._unselectableHandle) {
|
this._unselectableHandle.remove();
|
}
|
if (this._removeDeselectSignals) {
|
this._removeDeselectSignals();
|
}
|
},
|
|
_setSelectionMode: function (mode) {
|
// summary:
|
// Updates selectionMode, resetting necessary variables.
|
|
if (mode === this.selectionMode) {
|
return;
|
}
|
|
// Start selection fresh when switching mode.
|
this.clearSelection();
|
|
this.selectionMode = mode;
|
|
// Compute name of selection handler for this mode once
|
// (in the form of _fooSelectionHandler)
|
this._selectionHandlerName = '_' + mode + 'SelectionHandler';
|
|
// Also re-run allowTextSelection setter in case it is in automatic mode.
|
this._setAllowTextSelection(this.allowTextSelection);
|
},
|
|
_setAllowTextSelection: function (allow) {
|
if (typeof allow !== 'undefined') {
|
setSelectable(this, allow);
|
}
|
else {
|
setSelectable(this, this.selectionMode === 'none');
|
}
|
this.allowTextSelection = allow;
|
},
|
|
_handleSelect: function (event, target) {
|
// Don't run if selection mode doesn't have a handler (incl. "none"), target can't be selected,
|
// or if coming from a dgrid-cellfocusin from a mousedown
|
if (!this[this._selectionHandlerName] || !this.allowSelect(this.row(target)) ||
|
(event.type === 'dgrid-cellfocusin' && event.parentType === 'mousedown') ||
|
(event.type === upType && target !== this._waitForMouseUp)) {
|
return;
|
}
|
this._waitForMouseUp = null;
|
this._selectionTriggerEvent = event;
|
|
// Don't call select handler for ctrl+navigation
|
if (!event.keyCode || !event.ctrlKey || event.keyCode === 32) {
|
// If clicking a selected item, wait for mouseup so that drag n' drop
|
// is possible without losing our selection
|
if (!event.shiftKey && event.type === downType && this.isSelected(target)) {
|
this._waitForMouseUp = target;
|
}
|
else {
|
this[this._selectionHandlerName](event, target);
|
}
|
}
|
this._selectionTriggerEvent = null;
|
},
|
|
_singleSelectionHandler: function (event, target) {
|
// summary:
|
// Selection handler for "single" mode, where only one target may be
|
// selected at a time.
|
|
var ctrlKey = event.keyCode ? event.ctrlKey : event[ctrlEquiv];
|
if (this._lastSelected === target) {
|
// Allow ctrl to toggle selection, even within single select mode.
|
this.select(target, null, !ctrlKey || !this.isSelected(target));
|
}
|
else {
|
this.clearSelection();
|
this.select(target);
|
this._lastSelected = target;
|
}
|
},
|
|
_multipleSelectionHandler: function (event, target) {
|
// summary:
|
// Selection handler for "multiple" mode, where shift can be held to
|
// select ranges, ctrl/cmd can be held to toggle, and clicks/keystrokes
|
// without modifier keys will add to the current selection.
|
|
var lastRow = this._lastSelected,
|
ctrlKey = event.keyCode ? event.ctrlKey : event[ctrlEquiv],
|
value;
|
|
if (!event.shiftKey) {
|
// Toggle if ctrl is held; otherwise select
|
value = ctrlKey ? null : true;
|
lastRow = null;
|
}
|
this.select(target, lastRow, value);
|
|
if (!lastRow) {
|
// Update reference for potential subsequent shift+select
|
// (current row was already selected above)
|
this._lastSelected = target;
|
}
|
},
|
|
_extendedSelectionHandler: function (event, target) {
|
// summary:
|
// Selection handler for "extended" mode, which is like multiple mode
|
// except that clicks/keystrokes without modifier keys will clear
|
// the previous selection.
|
|
// Clear selection first for right-clicks outside selection and non-ctrl-clicks;
|
// otherwise, extended mode logic is identical to multiple mode
|
if (event.button === 2 ? !this.isSelected(target) :
|
!(event.keyCode ? event.ctrlKey : event[ctrlEquiv])) {
|
this.clearSelection(null, true);
|
}
|
this._multipleSelectionHandler(event, target);
|
},
|
|
_toggleSelectionHandler: function (event, target) {
|
// summary:
|
// Selection handler for "toggle" mode which simply toggles the selection
|
// of the given target. Primarily useful for touch input.
|
|
this.select(target, null, null);
|
},
|
|
_initSelectionEvents: function () {
|
// summary:
|
// Performs first-time hookup of event handlers containing logic
|
// required for selection to operate.
|
|
var grid = this,
|
contentNode = this.contentNode,
|
selector = this.selectionDelegate;
|
|
this._selectionEventQueues = {
|
deselect: [],
|
select: []
|
};
|
|
if (has('touch') && !has('pointer') && this.selectionTouchEvents) {
|
// Listen for taps, and also for mouse/keyboard, making sure not
|
// to trigger both for the same interaction
|
on(contentNode, touchUtil.selector(selector, this.selectionTouchEvents), function (evt) {
|
grid._handleSelect(evt, this);
|
grid._ignoreMouseSelect = this;
|
});
|
on(contentNode, on.selector(selector, this.selectionEvents), function (event) {
|
if (grid._ignoreMouseSelect !== this) {
|
grid._handleSelect(event, this);
|
}
|
else if (event.type === upType) {
|
grid._ignoreMouseSelect = null;
|
}
|
});
|
}
|
else {
|
// Listen for mouse/keyboard actions that should cause selections
|
on(contentNode, on.selector(selector, this.selectionEvents), function (event) {
|
grid._handleSelect(event, this);
|
});
|
}
|
|
// Also hook up spacebar (for ctrl+space)
|
if (this.addKeyHandler) {
|
this.addKeyHandler(32, function (event) {
|
grid._handleSelect(event, event.target);
|
});
|
}
|
|
// If allowSelectAll is true, bind ctrl/cmd+A to (de)select all rows,
|
// unless the event was received from an editor component.
|
// (Handler further checks against _allowSelectAll, which may be updated
|
// if selectionMode is changed post-init.)
|
if (this.allowSelectAll) {
|
this.on('keydown', function (event) {
|
if (event[ctrlEquiv] && event.keyCode === 65 &&
|
!/\bdgrid-input\b/.test(event.target.className)) {
|
event.preventDefault();
|
grid[grid.allSelected ? 'clearSelection' : 'selectAll']();
|
}
|
});
|
}
|
|
// Update aspects if there is a collection change
|
if (this._setCollection) {
|
aspect.before(this, '_setCollection', function (collection) {
|
grid._updateDeselectionAspect(collection);
|
});
|
}
|
this._updateDeselectionAspect();
|
},
|
|
_updateDeselectionAspect: function (collection) {
|
// summary:
|
// Hooks up logic to handle deselection of removed items.
|
// Aspects to a trackable collection's notify method if applicable,
|
// or to the list/grid's removeRow method otherwise.
|
|
var self = this,
|
signals;
|
|
function ifSelected(rowArg, methodName) {
|
// Calls a method if the row corresponding to the object is selected.
|
var row = self.row(rowArg),
|
selection = row && self.selection[row.id];
|
// Is the row currently in the selection list.
|
if (selection) {
|
self[methodName](row);
|
}
|
}
|
|
// Remove anything previously configured
|
if (this._removeDeselectSignals) {
|
this._removeDeselectSignals();
|
}
|
|
if (collection && collection.track && this._observeCollection) {
|
signals = [
|
aspect.before(this, '_observeCollection', function (collection) {
|
signals.push(
|
collection.on('delete', function (event) {
|
if (typeof event.index === 'undefined') {
|
// Call deselect on the row if the object is being removed. This allows the
|
// deselect event to reference the row element while it still exists in the DOM.
|
ifSelected(event.id, 'deselect');
|
}
|
})
|
);
|
}),
|
aspect.after(this, '_observeCollection', function (collection) {
|
signals.push(
|
collection.on('update', function (event) {
|
if (typeof event.index !== 'undefined') {
|
// When List updates an item, the row element is removed and a new one inserted.
|
// If at this point the object is still in grid.selection,
|
// then call select on the row so the element's CSS is updated.
|
ifSelected(collection.getIdentity(event.target), 'select');
|
}
|
})
|
);
|
}, true)
|
];
|
}
|
else {
|
signals = [
|
aspect.before(this, 'removeRow', function (rowElement, preserveDom) {
|
var row;
|
if (!preserveDom) {
|
row = this.row(rowElement);
|
// if it is a real row removal for a selected item, deselect it
|
if (row && (row.id in this.selection)) {
|
this.deselect(row);
|
}
|
}
|
})
|
];
|
}
|
|
this._removeDeselectSignals = function () {
|
for (var i = signals.length; i--;) {
|
signals[i].remove();
|
}
|
signals = [];
|
};
|
},
|
|
allowSelect: function () {
|
// summary:
|
// A method that can be overriden to determine whether or not a row (or
|
// cell) can be selected. By default, all rows (or cells) are selectable.
|
// target: Object
|
// Row object (for Selection) or Cell object (for CellSelection) for the
|
// row/cell in question
|
return true;
|
},
|
|
_fireSelectionEvent: function (type) {
|
// summary:
|
// Fires an event for the accumulated rows once a selection
|
// operation is finished (whether singular or for a range)
|
|
var queue = this._selectionEventQueues[type],
|
triggerEvent = this._selectionTriggerEvent,
|
eventObject;
|
|
eventObject = {
|
bubbles: true,
|
grid: this
|
};
|
if (triggerEvent) {
|
eventObject.parentType = triggerEvent.type;
|
}
|
eventObject[this._selectionTargetType] = queue;
|
|
// Clear the queue so that the next round of (de)selections starts anew
|
this._selectionEventQueues[type] = [];
|
|
on.emit(this.contentNode, 'dgrid-' + type, eventObject);
|
},
|
|
_fireSelectionEvents: function () {
|
var queues = this._selectionEventQueues,
|
type;
|
|
for (type in queues) {
|
if (queues[type].length) {
|
this._fireSelectionEvent(type);
|
}
|
}
|
},
|
|
_select: function (row, toRow, value) {
|
// summary:
|
// Contains logic for determining whether to select targets, but
|
// does not emit events. Called from select, deselect, selectAll,
|
// and clearSelection.
|
|
var selection,
|
previousValue,
|
element,
|
toElement,
|
direction;
|
|
if (typeof value === 'undefined') {
|
// default to true
|
value = true;
|
}
|
if (!row.element) {
|
row = this.row(row);
|
}
|
|
// Check whether we're allowed to select the given row before proceeding.
|
// If a deselect operation is being performed, this check is skipped,
|
// to avoid errors when changing column definitions, and since disabled
|
// rows shouldn't ever be selected anyway.
|
if (value === false || this.allowSelect(row)) {
|
selection = this.selection;
|
previousValue = !!selection[row.id];
|
if (value === null) {
|
// indicates a toggle
|
value = !previousValue;
|
}
|
element = row.element;
|
if (!value && !this.allSelected) {
|
delete this.selection[row.id];
|
}
|
else {
|
selection[row.id] = value;
|
}
|
if (element) {
|
// add or remove classes as appropriate
|
if (value) {
|
domClass.add(element, 'dgrid-selected' +
|
(this.addUiClasses ? ' ui-state-active' : ''));
|
}
|
else {
|
domClass.remove(element, 'dgrid-selected ui-state-active');
|
}
|
}
|
if (value !== previousValue && element) {
|
// add to the queue of row events
|
this._selectionEventQueues[(value ? '' : 'de') + 'select'].push(row);
|
}
|
|
if (toRow) {
|
if (!toRow.element) {
|
toRow = this.row(toRow);
|
}
|
|
if (!toRow) {
|
this._lastSelected = element;
|
console.warn('The selection range has been reset because the ' +
|
'beginning of the selection is no longer in the DOM. ' +
|
'If you are using OnDemandList, you may wish to increase ' +
|
'farOffRemoval to avoid this, but note that keeping more nodes ' +
|
'in the DOM may impact performance.');
|
return;
|
}
|
|
toElement = toRow.element;
|
if (toElement) {
|
direction = this._determineSelectionDirection(element, toElement);
|
if (!direction) {
|
// The original element was actually replaced
|
toElement = document.getElementById(toElement.id);
|
direction = this._determineSelectionDirection(element, toElement);
|
}
|
while (row.element !== toElement && (row = this[direction](row))) {
|
this._select(row, null, value);
|
}
|
}
|
}
|
}
|
},
|
|
// Implement _determineSelectionDirection differently based on whether the
|
// browser supports element.compareDocumentPosition; use sourceIndex for IE<9
|
_determineSelectionDirection: has('dom-comparedocumentposition') ? function (from, to) {
|
var result = to.compareDocumentPosition(from);
|
if (result & 1) {
|
return false; // Out of document
|
}
|
return result === 2 ? 'down' : 'up';
|
} : function (from, to) {
|
if (to.sourceIndex < 1) {
|
return false; // Out of document
|
}
|
return to.sourceIndex > from.sourceIndex ? 'down' : 'up';
|
},
|
|
select: function (row, toRow, value) {
|
// summary:
|
// Selects or deselects the given row or range of rows.
|
// row: Mixed
|
// Row object (or something that can resolve to one) to (de)select
|
// toRow: Mixed
|
// If specified, the inclusive range between row and toRow will
|
// be (de)selected
|
// value: Boolean|Null
|
// Whether to select (true/default), deselect (false), or toggle
|
// (null) the row
|
|
this._select(row, toRow, value);
|
this._fireSelectionEvents();
|
},
|
deselect: function (row, toRow) {
|
// summary:
|
// Deselects the given row or range of rows.
|
// row: Mixed
|
// Row object (or something that can resolve to one) to deselect
|
// toRow: Mixed
|
// If specified, the inclusive range between row and toRow will
|
// be deselected
|
|
this.select(row, toRow, false);
|
},
|
|
clearSelection: function (exceptId, dontResetLastSelected) {
|
// summary:
|
// Deselects any currently-selected items.
|
// exceptId: Mixed?
|
// If specified, the given id will not be deselected.
|
|
this.allSelected = false;
|
for (var id in this.selection) {
|
if (exceptId !== id) {
|
this._select(id, null, false);
|
}
|
}
|
if (!dontResetLastSelected) {
|
this._lastSelected = null;
|
}
|
this._fireSelectionEvents();
|
},
|
selectAll: function () {
|
this.allSelected = true;
|
this.selection = {}; // we do this to clear out pages from previous sorts
|
for (var i in this._rowIdToObject) {
|
var row = this.row(this._rowIdToObject[i]);
|
this._select(row.id, null, true);
|
}
|
this._fireSelectionEvents();
|
},
|
|
isSelected: function (object) {
|
// summary:
|
// Returns true if the indicated row is selected.
|
|
if (typeof object === 'undefined' || object === null) {
|
return false;
|
}
|
if (!object.element) {
|
object = this.row(object);
|
}
|
|
// First check whether the given row is indicated in the selection hash;
|
// failing that, check if allSelected is true (testing against the
|
// allowSelect method if possible)
|
return (object.id in this.selection) ? !!this.selection[object.id] :
|
this.allSelected && (!object.data || this.allowSelect(object));
|
},
|
|
refresh: function () {
|
if (this.deselectOnRefresh) {
|
this.clearSelection();
|
}
|
this._lastSelected = null;
|
return this.inherited(arguments);
|
},
|
|
renderArray: function () {
|
var rows = this.inherited(arguments),
|
selection = this.selection,
|
i,
|
row,
|
selected;
|
|
for (i = 0; i < rows.length; i++) {
|
row = this.row(rows[i]);
|
selected = row.id in selection ? selection[row.id] : this.allSelected;
|
if (selected) {
|
this.select(row, null, selected);
|
}
|
}
|
this._fireSelectionEvents();
|
return rows;
|
}
|
});
|
});
|