text: define([
  	'dojo/_base/declare',
  	'dojo/_base/lang',
  	'dojo/dom-class',
  	'dojo/dom-construct',
  	'dojo/on',
  	'dojo/aspect',
  	'dojo/query',
  	'dojo/has',
  	'./util/misc',
  	'dojo/_base/sniff'
  ], function (declare, lang, domClass, domConstruct, on, aspect, query, has, miscUtil) {
  	has.add('event-mousewheel', function (global, document, element) {
  		return 'onmousewheel' in element;
  	});
  	has.add('event-wheel', function (global, document, element) {
  		return 'onwheel' in element;
  	});
  
  	var colsetidAttr = 'data-dgrid-column-set-id';
  
  	function adjustScrollLeft(grid, root) {
  		// Adjusts the scroll position of each column set in each row under the given root.
  		// (root can be a row, or e.g. a tree parent row element's connected property to adjust children)
  		var scrollLefts = grid._columnSetScrollLefts;
  		query('.dgrid-column-set', root).forEach(function (element) {
  			element.scrollLeft = scrollLefts[element.getAttribute(colsetidAttr)];
  		});
  	}
  
  	function getColumnSetSubRows(subRows, columnSetId) {
  		// Builds a subRow collection that only contains columns that correspond to
  		// a given column set id.
  		if (!subRows || !subRows.length) {
  			return;
  		}
  		var subset = [];
  		var idPrefix = columnSetId + '-';
  		for (var i = 0, numRows = subRows.length; i < numRows; i++) {
  			var row = subRows[i];
  			var subsetRow = [];
  			subsetRow.className = row.className;
  			for (var k = 0, numCols = row.length; k < numCols; k++) {
  				var column = row[k];
  				// The column id begins with the column set id.
  				if (column.id != null && column.id.indexOf(idPrefix) === 0) {
  					subsetRow.push(column);
  				}
  			}
  			subset.push(subsetRow);
  		}
  		return subset;
  	}
  
  	function isRootNode(node, rootNode) {
  		// If we've reached the top-level node for the grid then there is no parent column set.
  		// This guard prevents an error when scroll is initated over some node in the grid that is not a descendant of
  		// a column set. This can happen in a grid that has empty space below its rows (grid is taller than the rows).
  		return (rootNode && node === rootNode) || domClass.contains(node, 'dgrid');
  	}
  
  	function findParentColumnSet(node, root) {
  		// WebKit will invoke mousewheel handlers with an event target of a text
  		// node; check target and if it's not an element node, start one node higher
  		// in the tree
  		if (node.nodeType !== 1) {
  			node = node.parentNode;
  		}
  
  		while (node && !query.matches(node, '.dgrid-column-set[' + colsetidAttr + ']', root)) {
  			if (isRootNode(node, root)) {
  				return null;
  			}
  			node = node.parentNode;
  		}
  
  		return node;
  	}
  
  	var pointerMap = {
  		start: 'down',
  		end: 'up'
  	};
  
  	function getTouchEventName(type) {
  		// Given 'start', 'move', or 'end', returns appropriate touch or pointer event name
  		// based on browser support.  (Assumes browser supports touch or pointer.)
  		var hasPointer = has('pointer');
  		if (hasPointer) {
  			type = pointerMap[type] || type;
  			if (hasPointer.slice(0, 2) === 'MS') {
  				return 'MSPointer' + type.slice(0, 1).toUpperCase() + type.slice(1);
  			}
  			else {
  				return 'pointer' + type;
  			}
  		}
  		return 'touch' + type;
  	}
  
  	var horizTouchMove = has('touch') && function (grid) {
  		return function (target, listener) {
  			var listeners = [
  				on(target, getTouchEventName('start'), function (event) {
  					if (!grid._currentlyTouchedColumnSet) {
  						var node = findParentColumnSet(event.target, target);
  						// If handling pointer events, only react to touch;
  						// MSPointerDown (IE10) reports 2, 3, 4 for touch, pen, mouse
  						if (node && (!event.pointerType || event.pointerType === 'touch' || event.pointerType === 2)) {
  							grid._currentlyTouchedColumnSet = node;
  							grid._lastColumnSetTouchX = event.clientX;
  							grid._lastColumnSetTouchY = event.clientY;
  						}
  					}
  				}),
  				on(target, getTouchEventName('move'), function (event) {
  					if (grid._currentlyTouchedColumnSet === null) {
  						return;
  					}
  					var node = findParentColumnSet(event.target);
  					if (!node) {
  						return;
  					}
  					listener.call(null, grid, node, grid._lastColumnSetTouchX - event.clientX);
  					grid._lastColumnSetTouchX = event.clientX;
  					grid._lastColumnSetTouchY = event.clientY;
  				}),
  				on(target, getTouchEventName('end'), function () {
  					grid._currentlyTouchedColumnSet = null;
  				})
  			];
  
  			return {
  				remove: function () {
  					for (var i = listeners.length; i--;) {
  						listeners[i].remove();
  					}
  				}
  			};
  		};
  	};
  
  	var horizMouseWheel = has('event-mousewheel') || has('event-wheel') ? function (grid) {
  		return function (target, listener) {
  			return on(target, has('event-wheel') ? 'wheel' : 'mousewheel', function (event) {
  				var node = findParentColumnSet(event.target, target),
  					deltaX;
  
  				if (!node) {
  					return;
  				}
  
  				// Normalize reported delta value:
  				// wheelDeltaX (webkit, mousewheel) needs to be negated and divided by 3
  				// deltaX (FF17+, wheel) can be used exactly as-is
  				deltaX = event.deltaX || -event.wheelDeltaX / 3;
  				if (deltaX) {
  					// only respond to horizontal movement
  					listener.call(null, grid, node, deltaX);
  				}
  			});
  		};
  	} : function (grid) {
  		return function (target, listener) {
  			return on(target, '.dgrid-column-set[' + colsetidAttr + ']:MozMousePixelScroll', function (event) {
  				if (event.axis === 1) {
  					// only respond to horizontal movement
  					listener.call(null, grid, this, event.detail);
  				}
  			});
  		};
  	};
  
  	function horizMoveHandler(grid, colsetNode, amount) {
  		var id = colsetNode.getAttribute(colsetidAttr),
  			scroller = grid._columnSetScrollers[id],
  			scrollLeft = scroller.scrollLeft + amount;
  
  		scroller.scrollLeft = scrollLeft < 0 ? 0 : scrollLeft;
  	}
  
  	return declare(null, {
  		// summary:
  		//		Provides column sets to isolate horizontal scroll of sets of
  		//		columns from each other. This mainly serves the purpose of allowing for
  		//		column locking.
  
  		postCreate: function () {
  			var self = this;
  			this.inherited(arguments);
  
  			this.on(horizMouseWheel(this), horizMoveHandler);
  			if (has('touch')) {
  				this.on(horizTouchMove(this), horizMoveHandler);
  			}
  
  			this.on('.dgrid-column-set:dgrid-cellfocusin', function (event) {
  				self._onColumnSetCellFocus(event, this);
  			});
  
  			if (typeof this.expand === 'function') {
  				aspect.after(this, 'expand', function (promise, args) {
  					promise.then(function () {
  						var row = self.row(args[0]);
  						if (self._expanded[row.id]) {
  							// scrollLeft changes can't take effect on collapsed child rows;
  							// ensure they are properly updated once re-expanded.
  							adjustScrollLeft(self, row.element.connected);
  						}
  					});
  					return promise;
  				});
  			}
  		},
  
  		columnSets: [],
  
  		createRowCells: function (tag, each, subRows, object, options) {
  			var row = domConstruct.create('table', { className: 'dgrid-row-table' });
  			var tbody = domConstruct.create('tbody', null, row);
  			var tr = domConstruct.create('tr', null, tbody);
  			for (var i = 0, l = this.columnSets.length; i < l; i++) {
  				// iterate through the columnSets
  				var cell = domConstruct.create(tag, {
  					className: 'dgrid-column-set-cell dgrid-column-set-' + i
  				}, tr);
  				cell = domConstruct.create('div', {
  					className: 'dgrid-column-set'
  				}, cell);
  				cell.setAttribute(colsetidAttr, i);
  				var subset = getColumnSetSubRows(subRows || this.subRows, i) || this.columnSets[i];
  				cell.appendChild(this.inherited(arguments, [tag, each, subset, object, options]));
  			}
  			return row;
  		},
  
  		renderArray: function () {
  			var rows = this.inherited(arguments);
  
  			for (var i = 0; i < rows.length; i++) {
  				adjustScrollLeft(this, rows[i]);
  			}
  			return rows;
  		},
  
  		insertRow: function () {
  			var row = this.inherited(arguments);
  			adjustScrollLeft(this, row);
  			return row;
  		},
  
  		renderHeader: function () {
  			// summary:
  			//		Setup the headers for the grid
  			this.inherited(arguments);
  
  			var columnSets = this.columnSets,
  				scrollers = this._columnSetScrollers,
  				grid = this,
  				i, l;
  
  			function reposition() {
  				grid._positionScrollers();
  			}
  
  			this._columnSetScrollerContents = {};
  			this._columnSetScrollLefts = {};
  
  			if (scrollers) {
  				// this isn't the first time; destroy existing scroller nodes first
  				for (i in scrollers) {
  					domConstruct.destroy(scrollers[i]);
  				}
  			} else {
  				// first-time-only operations: hook up event/aspected handlers
  				aspect.after(this, 'resize', reposition, true);
  				aspect.after(this, 'styleColumn', reposition, true);
  				this._columnSetScrollerNode = domConstruct.create('div', {
  					className: 'dgrid-column-set-scroller-container'
  				}, this.footerNode, 'after');
  			}
  
  			// reset to new object to be populated in loop below
  			scrollers = this._columnSetScrollers = {};
  
  			for (i = 0, l = columnSets.length; i < l; i++) {
  				this._putScroller(columnSets[i], i);
  			}
  
  			this._positionScrollers();
  		},
  
  		styleColumnSet: function (colsetId, css) {
  			// summary:
  			//		Dynamically creates a stylesheet rule to alter a columnset's style.
  
  			var rule = this.addCssRule('#' + miscUtil.escapeCssIdentifier(this.domNode.id) +
  				' .dgrid-column-set-' + miscUtil.escapeCssIdentifier(colsetId, '-'), css);
  			this._positionScrollers();
  			return rule;
  		},
  
  		configStructure: function () {
  			// Squash the column sets together so the grid and other dgrid extensions and mixins can
  			// configure the columns and create any needed subrows.
  			this.columns = {};
  			this.subRows = [];
  			for (var i = 0, l = this.columnSets.length; i < l; i++) {
  				var columnSet = this.columnSets[i];
  				for (var j = 0; j < columnSet.length; j++) {
  					columnSet[j] = this._configColumns(i + '-' + j + '-', columnSet[j]);
  				}
  			}
  			this.inherited(arguments);
  		},
  
  		_positionScrollers: function () {
  			var domNode = this.domNode,
  				scrollers = this._columnSetScrollers,
  				scrollerContents = this._columnSetScrollerContents,
  				columnSets = this.columnSets,
  				scrollerWidth = 0,
  				numScrollers = 0, // tracks number of visible scrollers (sets w/ overflow)
  				i, l, columnSetElement, contentWidth;
  
  			for (i = 0, l = columnSets.length; i < l; i++) {
  				// iterate through the columnSets
  				columnSetElement = query('.dgrid-column-set[' + colsetidAttr + '="' + i + '"]', domNode)[0];
  				scrollerWidth = columnSetElement.offsetWidth;
  				contentWidth = columnSetElement.firstChild.offsetWidth;
  				scrollerContents[i].style.width = contentWidth + 'px';
  				scrollers[i].style.width = scrollerWidth + 'px';
  
  				if (has('ie') < 9) {
  					// IE seems to need scroll to be set explicitly
  					scrollers[i].style.overflowX = contentWidth > scrollerWidth ? 'scroll' : 'auto';
  				}
  
  				// Keep track of how many scrollbars we're showing
  				if (contentWidth > scrollerWidth) {
  					numScrollers++;
  				}
  			}
  
  			this._columnSetScrollerNode.style.bottom = this.showFooter ? this.footerNode.offsetHeight + 'px' : '0';
  
  			// Align bottom of body node depending on whether there are scrollbars
  			this.bodyNode.style.bottom = numScrollers ?
  				(has('dom-scrollbar-height') + (has('ie') ? 1 : 0) + 'px') :
  				'0';
  		},
  
  		_putScroller: function (columnSet, i) {
  			// function called for each columnSet
  			var scroller = this._columnSetScrollers[i] = domConstruct.create('span', {
  				// IE8 needs dgrid-scrollbar-height class for scrollbar to be visible,
  				// but for some reason IE11's scrollbar arrows become unresponsive, so avoid applying it there
  				className: 'dgrid-column-set-scroller dgrid-column-set-scroller-' + i +
  					(has('ie') < 9 ? ' dgrid-scrollbar-height' : '')
  			}, this._columnSetScrollerNode);
  			scroller.setAttribute(colsetidAttr, i);
  
  			this._columnSetScrollerContents[i] = domConstruct.create('div', {
  				className: 'dgrid-column-set-scroller-content'
  			}, scroller);
  			on(scroller, 'scroll', lang.hitch(this, '_onColumnSetScroll'));
  		},
  
  		_onColumnSetScroll: function (evt) {
  			var scrollLeft = evt.target.scrollLeft,
  				colSetId = evt.target.getAttribute(colsetidAttr),
  				newScrollLeft;
  
  			if (this._columnSetScrollLefts[colSetId] !== scrollLeft) {
  				query('.dgrid-column-set[' + colsetidAttr + '="' + colSetId +
  						'"],.dgrid-column-set-scroller[' + colsetidAttr + '="' + colSetId + '"]', this.domNode
  					).forEach(function (element, i) {
  						element.scrollLeft = scrollLeft;
  						if (!i) {
  							// Compute newScrollLeft based on actual resulting
  							// value of scrollLeft, which may be different than
  							// what we assigned under certain circumstances
  							// (e.g. Chrome under 33% / 67% / 90% zoom).
  							// Only need to compute this once, as it will be the
  							// same for every row.
  							newScrollLeft = element.scrollLeft;
  						}
  					});
  				this._columnSetScrollLefts[colSetId] = newScrollLeft;
  			}
  		},
  
  		_setColumnSets: function (columnSets) {
  			this._destroyColumns();
  			this.columnSets = columnSets;
  			this._updateColumns();
  		},
  
  		_scrollColumnSet: function (nodeOrId, offsetLeft) {
  			var id = nodeOrId.tagName ? nodeOrId.getAttribute(colsetidAttr) : nodeOrId;
  			var scroller = this._columnSetScrollers[id];
  			scroller.scrollLeft = offsetLeft < 0 ? 0 : offsetLeft;
  		},
  
  		_onColumnSetCellFocus: function (event, columnSetNode) {
  			var focusedNode = event.target;
  			var columnSetId = columnSetNode.getAttribute(colsetidAttr);
  			// columnSetNode's offsetLeft is not always correct,
  			// so get the columnScroller to check offsetLeft against
  			var columnScroller = this._columnSetScrollers[columnSetId];
  			var elementEdge = focusedNode.offsetLeft - columnScroller.scrollLeft + focusedNode.offsetWidth;
  
  			if (elementEdge > columnSetNode.offsetWidth ||
  				columnScroller.scrollLeft > focusedNode.offsetLeft) {
  				this._scrollColumnSet(columnSetNode, focusedNode.offsetLeft);
  			}
  		}
  	});
  });
  