define([
|
'dojo/_base/declare',
|
'dojo/_base/lang',
|
'dojo/_base/array',
|
'dojo/aspect',
|
'dojo/Deferred',
|
'dojo/dom-construct',
|
'dojo/dom-class',
|
'dojo/on',
|
'dojo/promise/all',
|
'dojo/query',
|
'dojo/when',
|
'./util/has-css3',
|
'./Grid',
|
'dojo/has!touch?./util/touch'
|
], function (declare, lang, arrayUtil, aspect, Deferred, domConstruct, domClass, on, all, querySelector, when, has,
|
Grid, touchUtil) {
|
|
return declare(null, {
|
// collapseOnRefresh: Boolean
|
// Whether to collapse all expanded nodes any time refresh is called.
|
collapseOnRefresh: false,
|
|
// enableTreeTransitions: Boolean
|
// Enables/disables all expand/collapse CSS transitions.
|
enableTreeTransitions: true,
|
|
// treeIndentWidth: Number
|
// Width (in pixels) of each level of indentation.
|
treeIndentWidth: 9,
|
|
constructor: function () {
|
this._treeColumnListeners = [];
|
},
|
|
shouldExpand: function (row, level, previouslyExpanded) {
|
// summary:
|
// Function called after each row is inserted to determine whether
|
// expand(rowElement, true) should be automatically called.
|
// The default implementation re-expands any rows that were expanded
|
// the last time they were rendered (if applicable).
|
|
return previouslyExpanded;
|
},
|
|
expand: function (target, expand, noTransition, lastRowsFirst) {
|
// summary:
|
// Expands the row corresponding to the given target.
|
// target: Object
|
// Row object (or something resolvable to one) to expand/collapse.
|
// expand: Boolean?
|
// If specified, designates whether to expand or collapse the row;
|
// if unspecified, toggles the current state.
|
|
if (!this._treeColumn) {
|
return;
|
}
|
|
var grid = this,
|
row = target.element ? target : this.row(target),
|
isExpanded = !!this._expanded[row.id],
|
hasTransitionend = has('transitionend'),
|
promise;
|
|
function processScroll() {
|
if (!expanded) {
|
grid._processScroll && grid._processScroll();
|
}
|
}
|
|
target = row.element;
|
target = target.className.indexOf('dgrid-expando-icon') > -1 ? target :
|
querySelector('.dgrid-expando-icon', target)[0];
|
|
noTransition = noTransition || !this.enableTreeTransitions;
|
|
if (target && target.mayHaveChildren && (noTransition || expand !== isExpanded)) {
|
// toggle or set expand/collapsed state based on optional 2nd argument
|
var expanded = expand === undefined ? !this._expanded[row.id] : expand;
|
|
// Update _expanded map.
|
var pos = this.getScrollPosition();
|
this._resetExpanded(row.id, expanded);
|
|
// update the expando display
|
domClass.replace(target, 'ui-icon-triangle-1-' + (expanded ? 'se' : 'e'),
|
'ui-icon-triangle-1-' + (expanded ? 'e' : 'se'));
|
domClass.toggle(row.element, 'dgrid-row-expanded', expanded);
|
|
var rowElement = row.element,
|
container = rowElement.connected,
|
containerStyle,
|
scrollHeight,
|
options = {};
|
|
if (!container) {
|
// if the children have not been created, create a container, a preload node and do the
|
// query for the children
|
container = options.container = rowElement.connected =
|
domConstruct.create('div', { className: 'dgrid-tree-container' }, rowElement, 'after');
|
var query = function (options) {
|
var childCollection = grid._renderedCollection.getChildren(row.data),
|
results;
|
if (grid.sort && grid.sort.length > 0) {
|
childCollection = childCollection.sort(grid.sort);
|
}
|
if (childCollection.track && grid.shouldTrackCollection) {
|
container._rows = options.rows = [];
|
|
childCollection = childCollection.track();
|
|
// remember observation handles so they can be removed when the parent row is destroyed
|
container._handles = [
|
childCollection.tracking,
|
grid._observeCollection(childCollection, container, options)
|
];
|
}
|
query.collection = childCollection;
|
if ('start' in options) {
|
var rangeArgs = {
|
start: options.start,
|
end: options.start + options.count
|
};
|
results = childCollection.fetchRange(rangeArgs);
|
} else {
|
results = childCollection.fetch();
|
}
|
return results;
|
};
|
if ('level' in target) {
|
// Include level information on query for renderQuery case;
|
// include on container for insertRow to detect in other cases
|
container.level = query.level = target.level + 1;
|
}
|
|
// Add the query to the promise chain
|
if (this.renderQuery) {
|
if (lastRowsFirst) {
|
promise = grid._renderedCollection.getChildren(row.data)
|
.fetchRange({ start: 0, end: 1 }).totalLength.then(function (total) {
|
options.start = total - grid.minRowsPerPage;
|
options.end = total - 1;
|
options.count = grid.minRowsPerPage;
|
|
grid._previousScrollPosition = pos;
|
|
return grid.renderQuery(query, options);
|
});
|
} else {
|
promise = this.renderQuery(query, options);
|
}
|
|
}
|
else {
|
// If not using OnDemandList, we don't need preload nodes,
|
// but we still need a beforeNode to pass to renderArray,
|
// so create a temporary one
|
var firstChild = domConstruct.create('div', null, container);
|
promise = this._trackError(function () {
|
return grid.renderQueryResults(
|
query(options),
|
firstChild,
|
lang.mixin({ rows: options.rows },
|
'level' in query ? { queryLevel: query.level } : null
|
)
|
).then(function (rows) {
|
domConstruct.destroy(firstChild);
|
return rows;
|
});
|
});
|
}
|
|
if (hasTransitionend) {
|
// Update height whenever a collapse/expand transition ends.
|
// (This handler is only registered when each child container is first created.)
|
on(container, hasTransitionend, this._onTreeTransitionEnd);
|
}
|
}
|
|
// Show or hide all the children.
|
|
container.hidden = !expanded;
|
containerStyle = container.style;
|
|
// make sure it is visible so we can measure it
|
if (!hasTransitionend || noTransition) {
|
containerStyle.display = expanded ? 'block' : 'none';
|
containerStyle.height = '';
|
processScroll();
|
}
|
else {
|
on.once(container, hasTransitionend, processScroll);
|
if (expanded) {
|
containerStyle.display = 'block';
|
scrollHeight = container.scrollHeight;
|
containerStyle.height = '0px';
|
}
|
else {
|
// if it will be hidden we need to be able to give a full height
|
// without animating it, so it has the right starting point to animate to zero
|
domClass.add(container, 'dgrid-tree-resetting');
|
containerStyle.height = container.scrollHeight + 'px';
|
}
|
// Perform a transition for the expand or collapse.
|
setTimeout(function () {
|
domClass.remove(container, 'dgrid-tree-resetting');
|
containerStyle.height =
|
expanded ? (scrollHeight ? scrollHeight + 'px' : 'auto') : '0px';
|
}, 0);
|
}
|
}
|
|
// Always return a promise
|
return when(promise);
|
},
|
|
_configColumns: function () {
|
var columnArray = this.inherited(arguments);
|
|
// Set up hash to store IDs of expanded rows (here rather than in
|
// _configureTreeColumn so nothing breaks if no column has renderExpando)
|
this._resetExpanded();
|
|
for (var i = 0, l = columnArray.length; i < l; i++) {
|
if (columnArray[i].renderExpando) {
|
this._configureTreeColumn(columnArray[i]);
|
break; // Allow only one tree column.
|
}
|
}
|
return columnArray;
|
},
|
|
insertRow: function (object, container, beforeNode, i, options) {
|
options = options || {};
|
|
var level = options.queryLevel = 'queryLevel' in options ? options.queryLevel :
|
'level' in container ? container.level : 0;
|
|
var rowElement = this.inherited(arguments);
|
|
// Auto-expand (shouldExpand) considerations
|
var row = this.row(rowElement),
|
expanded = this.shouldExpand(row, level, this._expanded[row.id]);
|
|
if (expanded) {
|
this._expandWhenInDom(rowElement, options);
|
}
|
|
if (expanded || (!this.collection.mayHaveChildren || this.collection.mayHaveChildren(object))) {
|
domClass.add(rowElement, 'dgrid-row-expandable');
|
}
|
|
return rowElement; // pass return value through
|
},
|
|
_expandWhenInDom: function (rowElement, options, dfd) {
|
// Expand a row after it has been inserted into the DOM. This is necessary because
|
// the OnDemandList code that manages the preload nodes needs the nodes to be in the DOM
|
// to create a correctly ordered linked list.;
|
|
if (rowElement.offsetHeight) {
|
var expandPromise = this.expand(rowElement, true, true, options.scrollingUp);
|
if (dfd) {
|
expandPromise.then(function () {
|
dfd.resolve();
|
});
|
}
|
} else {
|
if (rowElement.parentNode && this.domNode.offsetHeight) {
|
if (this._expandPromises && !dfd) {
|
dfd = new Deferred();
|
this._expandPromises.push(dfd.promise);
|
}
|
// Continue to try to expand the row only while it is inserted into a document fragment.
|
setTimeout(this._expandWhenInDom.bind(this, rowElement, options, dfd), 0);
|
}
|
}
|
},
|
|
_queueNodeForDeletion: function (node) {
|
this.inherited(arguments);
|
|
var connected = node.connected;
|
if (connected) {
|
this._deleteQueue.push(connected);
|
}
|
},
|
|
_pruneRow: function (rowElement, removeBelow) {
|
var connected = rowElement.connected;
|
var preloadNode;
|
var preload;
|
if (connected) {
|
var rowId = this.row(rowElement).id;
|
if (this._expanded[rowId]) {
|
preloadNode = querySelector('>.dgrid-preload', connected)[removeBelow ? 1 : 0];
|
if (preloadNode) {
|
preload = this._findPreload(preloadNode);
|
preload = removeBelow ? preload.next : preload.previous;
|
if (!preload.expandedContent) {
|
preload.expandedContent = {};
|
}
|
preload.expandedContent[rowId] = connected.offsetHeight;
|
}
|
}
|
}
|
|
this.inherited(arguments, [rowElement, removeBelow, {
|
treePrune: true,
|
removeBelow: removeBelow
|
}]);
|
},
|
|
refresh: function (options) {
|
// Restoring the previous scroll position with OnDemandList is not possible in some cases with
|
// nested expanded nodes. In those cases, restoring the position would require scrolling and
|
// loading rows incrementally to make sure the expanded rows are loaded and expanded. dgrid is not
|
// currently written to do that. If there are expanded rows, then do not allow the position to be
|
// restored.
|
var refreshResult;
|
this._expandPromises = [];
|
var keepScrollPosition = this.keepScrollPosition || (options && options.keepScrollPosition);
|
if (keepScrollPosition && Object.keys(this._expanded).length) {
|
refreshResult = this.inherited(arguments, lang.mixin(options || {}, { keepScrollPosition: false }));
|
} else {
|
refreshResult = this.inherited(arguments);
|
}
|
return when(refreshResult).then(function () {
|
var promises = this._expandPromises;
|
delete this._expandPromises;
|
return all(promises);
|
}.bind(this));
|
},
|
|
removeRow: function (rowElement, preserveDom, options) {
|
var connected = rowElement.connected,
|
childOptions = {},
|
childRows,
|
preloadNodes,
|
firstIndex,
|
lastIndex;
|
if (connected) {
|
if (connected._handles) {
|
arrayUtil.forEach(connected._handles, function (handle) {
|
handle.remove();
|
});
|
delete connected._handles;
|
}
|
|
if (connected._rows) {
|
childOptions.rows = connected._rows;
|
}
|
|
childRows = querySelector('>.dgrid-row', connected);
|
preloadNodes = querySelector('>.dgrid-preload', connected);
|
if (childRows && childRows.length) {
|
|
if (this._releaseRange) {
|
firstIndex = childRows[0].rowIndex;
|
lastIndex = childRows[childRows.length - 1].rowIndex;
|
this._releaseRange(this._findPreload(preloadNodes[0]), false, firstIndex, lastIndex);
|
}
|
|
childRows.forEach(function (element) {
|
if (options && options.treePrune) {
|
this._pruneRow(element, options.removeBelow);
|
} else {
|
this.removeRow(element, true, childOptions);
|
}
|
}, this);
|
}
|
this._removePreloads && this._removePreloads(preloadNodes);
|
|
if (connected._rows) {
|
connected._rows.length = 0;
|
delete connected._rows;
|
}
|
|
if (preserveDom) {
|
this._queueNodeForDeletion(connected);
|
} else {
|
domConstruct.destroy(connected);
|
}
|
}
|
|
this.inherited(arguments);
|
},
|
|
_refreshCellFromItem: function (cell, item) {
|
if (!cell.column.renderExpando) {
|
return this.inherited(arguments);
|
}
|
|
this.inherited(arguments, [cell, item, {
|
queryLevel: querySelector('.dgrid-expando-icon', cell.element)[0].level
|
}]);
|
},
|
|
cleanup: function () {
|
this.inherited(arguments);
|
|
if (this.collapseOnRefresh) {
|
// Clear out the _expanded hash on each call to cleanup
|
// (which generally coincides with refreshes, as well as destroy)
|
this._resetExpanded();
|
}
|
},
|
|
_destroyColumns: function () {
|
this.inherited(arguments);
|
var listeners = this._treeColumnListeners;
|
|
for (var i = listeners.length; i--;) {
|
listeners[i].remove();
|
}
|
this._treeColumnListeners = [];
|
this._treeColumn = null;
|
},
|
|
_calcRowHeight: function (rowElement) {
|
// Override this method to provide row height measurements that
|
// include the children of a row
|
var connected = rowElement.connected;
|
// if connected, need to consider this in the total row height
|
return this.inherited(arguments) + (connected ? connected.offsetHeight : 0);
|
},
|
|
_configureTreeColumn: function (column) {
|
// summary:
|
// Adds tree navigation capability to a column.
|
|
var grid = this;
|
var colSelector = '.dgrid-content .dgrid-column-' + column.id;
|
var clicked; // tracks row that was clicked (for expand dblclick event handling)
|
|
this._treeColumn = column;
|
if (!column._isConfiguredTreeColumn) {
|
var originalRenderCell = column.renderCell || this._defaultRenderCell;
|
column._isConfiguredTreeColumn = true;
|
column.renderCell = function (object, value, td, options) {
|
// summary:
|
// Renders a cell that can be expanded, creating more rows
|
|
var level = options && 'queryLevel' in options ? options.queryLevel : 0,
|
mayHaveChildren = !grid.collection.mayHaveChildren || grid.collection.mayHaveChildren(object),
|
expando, node;
|
|
expando = column.renderExpando(level, mayHaveChildren,
|
grid._expanded[grid.collection.getIdentity(object)], object);
|
expando.level = level;
|
expando.mayHaveChildren = mayHaveChildren;
|
|
node = originalRenderCell.call(column, object, value, td, options);
|
if (node && node.nodeType) {
|
td.appendChild(expando);
|
td.appendChild(node);
|
}
|
else {
|
td.insertBefore(expando, td.firstChild);
|
}
|
};
|
|
if (typeof column.renderExpando !== 'function') {
|
column.renderExpando = this._defaultRenderExpando;
|
}
|
}
|
|
var treeColumnListeners = this._treeColumnListeners;
|
if (treeColumnListeners.length === 0) {
|
// Set up the event listener once and use event delegation for better memory use.
|
treeColumnListeners.push(this.on(column.expandOn ||
|
'.dgrid-expando-icon:click,' + colSelector + ':dblclick,' + colSelector + ':keydown',
|
function (event) {
|
var row = grid.row(event);
|
if ((!grid.collection.mayHaveChildren || grid.collection.mayHaveChildren(row.data)) &&
|
(event.type !== 'keydown' || event.keyCode === 32) && !(event.type === 'dblclick' &&
|
clicked && clicked.count > 1 && row.id === clicked.id &&
|
event.target.className.indexOf('dgrid-expando-icon') > -1)) {
|
grid.expand(row);
|
}
|
|
// If the expando icon was clicked, update clicked object to prevent
|
// potential over-triggering on dblclick (all tested browsers but IE < 9).
|
if (event.target.className.indexOf('dgrid-expando-icon') > -1) {
|
if (clicked && clicked.id === grid.row(event).id) {
|
clicked.count++;
|
}
|
else {
|
clicked = {
|
id: grid.row(event).id,
|
count: 1
|
};
|
}
|
}
|
})
|
);
|
|
if (has('touch')) {
|
// Also listen on double-taps of the cell.
|
treeColumnListeners.push(this.on(touchUtil.selector(colSelector, touchUtil.dbltap),
|
function () {
|
grid.expand(this);
|
}));
|
}
|
}
|
},
|
|
_defaultRenderExpando: function (level, hasChildren, expanded) {
|
// summary:
|
// Default implementation for column.renderExpando.
|
// NOTE: Called in context of the column definition object.
|
// level: Number
|
// Level of indentation for this row (0 for top-level)
|
// hasChildren: Boolean
|
// Whether this item may have children (in most cases this determines
|
// whether an expando icon should be rendered)
|
// expanded: Boolean
|
// Whether this item is currently in expanded state
|
// object: Object
|
// The item that this expando pertains to
|
|
var dir = this.grid.isRTL ? 'right' : 'left',
|
cls = 'dgrid-expando-icon';
|
if (hasChildren) {
|
cls += ' ui-icon ui-icon-triangle-1-' + (expanded ? 'se' : 'e');
|
}
|
return domConstruct.create('div', {
|
className: cls,
|
innerHTML: ' ',
|
style: 'margin-' + dir + ': ' + (level * this.grid.treeIndentWidth) + 'px; float: ' + dir + ';'
|
});
|
},
|
|
_onNotification: function (rows, event) {
|
if (event.type === 'delete') {
|
this._resetExpanded(event.id);
|
}
|
this.inherited(arguments);
|
},
|
|
_onTreeTransitionEnd: function (event) {
|
var container = this,
|
height = this.style.height;
|
if (height) {
|
// After expansion, ensure display is correct;
|
// after collapse, set display to none to improve performance
|
this.style.display = height === '0px' ? 'none' : 'block';
|
}
|
|
// Reset height to be auto, so future height changes (from children
|
// expansions, for example), will expand to the right height.
|
if (event) {
|
// For browsers with CSS transition support, setting the height to
|
// auto or "" will cause an animation to zero height for some
|
// reason, so temporarily set the transition to be zero duration
|
domClass.add(this, 'dgrid-tree-resetting');
|
setTimeout(function () {
|
// Turn off the zero duration transition after we have let it render
|
domClass.remove(container, 'dgrid-tree-resetting');
|
}, 0);
|
}
|
// Now set the height to auto
|
this.style.height = '';
|
},
|
|
_resetPlaceHolder: function (rowId) {
|
var headPreload = this._getHeadPreload && this._getHeadPreload();
|
var preload;
|
var grid = this;
|
|
if (!headPreload) {
|
return;
|
}
|
|
function remove(rowId) {
|
var preload = headPreload;
|
while (preload) {
|
var expandedContent = preload.expandedContent;
|
if (expandedContent && expandedContent[rowId]) {
|
delete expandedContent[rowId];
|
grid._adjustPreloadHeight(preload);
|
return;
|
}
|
preload = preload.next;
|
}
|
}
|
|
if (rowId != null) {
|
remove(rowId);
|
} else {
|
preload = headPreload;
|
while (preload) {
|
if (preload.expandedContent) {
|
delete preload.expandedContent;
|
grid._adjustPreloadHeight(preload);
|
}
|
preload = preload.next;
|
}
|
}
|
},
|
|
_resetExpanded: function (rowId, expanded) {
|
// Always remove the place holder(s).
|
this._resetPlaceHolder(rowId);
|
if (rowId == null) {
|
this._expanded = {};
|
} else {
|
if (expanded) {
|
this._expanded[rowId] = true;
|
} else {
|
delete this._expanded[rowId];
|
}
|
}
|
},
|
|
_calculatePreloadHeight: function (preload) {
|
var newHeight = this.inherited(arguments);
|
var expandedContent = preload.expandedContent;
|
if (expandedContent) {
|
Object.keys(expandedContent).forEach(function (key) {
|
newHeight += expandedContent[key];
|
});
|
}
|
return newHeight;
|
},
|
|
_getRenderedCollection: function (preload) {
|
if (preload.level) {
|
return preload.query.collection;
|
} else {
|
return this.inherited(arguments);
|
}
|
}
|
});
|
});
|