define([ 'dojo/_base/declare', 'dojo/_base/array', 'dojo/on', 'dojo/query', 'dojo/_base/lang', 'dojo/dom', 'dojo/dom-construct', 'dojo/dom-geometry', 'dojo/has', '../util/misc', 'dojo/_base/html' ], function (declare, arrayUtil, listen, query, lang, dom, domConstruct, geom, has, miscUtil) { function addRowSpan(table, span, startRow, column, id) { // loop through the rows of the table and add this column's id to // the rows' column for (var i = 1; i < span; i++) { table[startRow + i][column] = id; } } function subRowAssoc(subRows) { // Take a sub-row structure and output an object with key=>value pairs // The keys will be the column id's; the values will be the first-row column // that column's resizer should be associated with. var i = subRows.length, l = i, numCols = arrayUtil.filter(subRows[0], function (column) { return !column.hidden; }).length, table = new Array(i); // create table-like structure in an array so it can be populated // with row-spans and col-spans while (i--) { table[i] = new Array(numCols); } var associations = {}; for (i = 0; i < l; i++) { var row = table[i], subRow = subRows[i]; // j: counter for table columns // js: counter for subrow structure columns for (var j = 0, js = 0; j < numCols; j++) { var cell = subRow[js], k; // if something already exists in the table (row-span), skip this // spot and go to the next if (typeof row[j] !== 'undefined') { continue; } row[j] = cell.id; if (cell.rowSpan && cell.rowSpan > 1) { addRowSpan(table, cell.rowSpan, i, j, cell.id); } // colSpans are only applicable in the second or greater rows // and only if the colSpan is greater than 1 if (i > 0 && cell.colSpan && cell.colSpan > 1) { for (k = 1; k < cell.colSpan; k++) { // increment j and assign the id since this is a span row[++j] = cell.id; if (cell.rowSpan && cell.rowSpan > 1) { addRowSpan(table, cell.rowSpan, i, j, cell.id); } } } associations[cell.id] = subRows[0][j].id; js++; } } return associations; } function resizeColumnWidth(grid, colId, width, parentType, doResize) { // don't react to widths <= 0, e.g. for hidden columns if (width <= 0) { return; } var column = grid.columns[colId], event, rule; if (!column) { return; } event = { grid: grid, columnId: colId, width: width, bubbles: true, cancelable: true }; if (parentType) { event.parentType = parentType; } if (!grid._resizedColumns || listen.emit(grid.headerNode, 'dgrid-columnresize', event)) { // Update width on column object, then convert value for CSS if (width === 'auto') { delete column.width; } else { column.width = width; width += 'px'; } rule = grid._columnSizes[colId]; if (rule) { // Modify existing, rather than deleting + adding rule.set('width', width); } else { // Use miscUtil function directly, since we clean these up ourselves anyway rule = miscUtil.addCssRule('#' + miscUtil.escapeCssIdentifier(grid.domNode.id) + ' .dgrid-column-' + miscUtil.escapeCssIdentifier(colId, '-'), 'width: ' + width + ';'); } // keep a reference for future removal grid._columnSizes[colId] = rule; if (doResize !== false) { grid.resize(); } return true; } } // Functions for shared resizer node var resizerNode, // DOM node for resize indicator, reused between instances resizerGuardNode, // DOM node to guard against clicks registering on header cells (and inducing sort) resizableCount = 0; // Number of ColumnResizer-enabled grid instances var resizer = { // This object contains functions for manipulating the shared resizerNode create: function () { resizerNode = domConstruct.create('div', { className: 'dgrid-column-resizer' }); resizerGuardNode = domConstruct.create('div', { className: 'dgrid-resize-guard' }); }, destroy: function () { domConstruct.destroy(resizerNode); domConstruct.destroy(resizerGuardNode); resizerNode = resizerGuardNode = null; }, show: function (grid) { var pos = geom.position(grid.domNode, true); resizerNode.style.top = pos.y + 'px'; resizerNode.style.height = pos.h + 'px'; document.body.appendChild(resizerNode); grid.domNode.appendChild(resizerGuardNode); }, move: function (x) { resizerNode.style.left = x + 'px'; }, hide: function () { resizerNode.parentNode.removeChild(resizerNode); resizerGuardNode.parentNode.removeChild(resizerGuardNode); } }; return declare(null, { resizeNode: null, // minWidth: Number // Minimum column width, in px. minWidth: 40, // adjustLastColumn: Boolean // If true, adjusts the last column's width to "auto" at times where the // browser would otherwise stretch all columns to span the grid. adjustLastColumn: true, _resizedColumns: false, // flag indicating if resizer has converted column widths to px buildRendering: function () { this.inherited(arguments); // Create resizerNode when first grid w/ ColumnResizer is created if (!resizableCount) { resizer.create(); } resizableCount++; }, destroy: function () { this.inherited(arguments); // Remove any applied column size styles since we're tracking them directly for (var name in this._columnSizes) { this._columnSizes[name].remove(); } // If this is the last grid on the page with ColumnResizer, destroy the // shared resizerNode if (!--resizableCount) { resizer.destroy(); } }, resizeColumnWidth: function (colId, width) { // Summary: // calls grid's styleColumn function to add a style for the column // colId: String // column id // width: Integer // new width of the column return resizeColumnWidth(this, colId, width); }, configStructure: function () { var oldSizes = this._oldColumnSizes = lang.mixin({}, this._columnSizes), // shallow clone k; this._resizedColumns = false; this._columnSizes = {}; this.inherited(arguments); // Remove old column styles that are no longer relevant; this is specifically // done *after* calling inherited so that _columnSizes will contain keys // for all columns in the new structure that were assigned widths. for (k in oldSizes) { if (!(k in this._columnSizes)) { oldSizes[k].remove(); } } delete this._oldColumnSizes; }, _configColumn: function (column) { this.inherited(arguments); var colId = column.id, rule; if ('width' in column) { // Update or add a style rule for the specified width if ((rule = this._oldColumnSizes[colId])) { rule.set('width', column.width + 'px'); } else { rule = miscUtil.addCssRule('#' + miscUtil.escapeCssIdentifier(this.domNode.id) + ' .dgrid-column-' + miscUtil.escapeCssIdentifier(colId, '-'), 'width: ' + column.width + 'px;'); } this._columnSizes[colId] = rule; } }, renderHeader: function () { this.inherited(arguments); var grid = this; var assoc; if (this.columnSets && this.columnSets.length) { var csi = this.columnSets.length; while (csi--) { assoc = lang.mixin(assoc || {}, subRowAssoc(this.columnSets[csi])); } } else if (this.subRows && this.subRows.length > 1) { assoc = subRowAssoc(this.subRows); } var colNodes = query('.dgrid-cell', grid.headerNode), i = colNodes.length; while (i--) { var colNode = colNodes[i], id = colNode.columnId, col = grid.columns[id], childNodes = colNode.childNodes, resizeHandle; if (!col || col.resizable === false) { continue; } var headerTextNode = domConstruct.create('div', { className: 'dgrid-resize-header-container' }); colNode.contents = headerTextNode; // move all the children to the header text node while (childNodes.length > 0) { headerTextNode.appendChild(childNodes[0]); } resizeHandle = domConstruct.create('div', { className: 'dgrid-resize-handle resizeNode-' + miscUtil.escapeCssIdentifier(id, '-') }, headerTextNode); colNode.appendChild(headerTextNode); resizeHandle.columnId = assoc && assoc[id] || id; } if (!grid.mouseMoveListen) { // establish listeners for initiating, dragging, and finishing resize listen(grid.headerNode, '.dgrid-resize-handle:mousedown' + (has('touch') ? ',.dgrid-resize-handle:touchstart' : ''), function (e) { grid._resizeMouseDown(e, this); grid.mouseMoveListen.resume(); grid.mouseUpListen.resume(); } ); grid._listeners.push(grid.mouseMoveListen = listen.pausable(document, 'mousemove' + (has('touch') ? ',touchmove' : ''), miscUtil.throttleDelayed(function (e) { grid._updateResizerPosition(e); }) )); grid._listeners.push(grid.mouseUpListen = listen.pausable(document, 'mouseup' + (has('touch') ? ',touchend' : ''), function (e) { grid._resizeMouseUp(e); grid.mouseMoveListen.pause(); grid.mouseUpListen.pause(); } )); // initially pause the move/up listeners until a drag happens grid.mouseMoveListen.pause(); grid.mouseUpListen.pause(); } }, // end renderHeader _resizeMouseDown: function (e, target) { // Summary: // called when mouse button is pressed on the header // e: Object // mousedown event object // preventDefault actually seems to be enough to prevent browser selection // in all but IE < 9. setSelectable works for those. e.preventDefault(); dom.setSelectable(this.domNode, false); this._startX = this._getResizeMouseLocation(e); //position of the target this._targetCell = query('.dgrid-column-' + miscUtil.escapeCssIdentifier(target.columnId, '-'), this.headerNode)[0]; // Show resizerNode after initializing its x position this._updateResizerPosition(e); resizer.show(this); }, _resizeMouseUp: function (e) { // Summary: // called when mouse button is released // e: Object // mouseup event object var columnSizes = this._columnSizes, colNodes, colWidths, gridWidth; if (this.adjustLastColumn) { // For some reason, total column width needs to be 1 less than this gridWidth = this.headerNode.clientWidth - 1; } //This is used to set all the column widths to a static size if (!this._resizedColumns) { colNodes = query('.dgrid-cell', this.headerNode); if (this.columnSets && this.columnSets.length) { colNodes = colNodes.filter(function (node) { var idx = node.columnId.split('-'); return idx[0] === '0' && !(node.columnId in columnSizes); }); } else if (this.subRows && this.subRows.length > 1) { colNodes = colNodes.filter(function (node) { return node.columnId.charAt(0) === '0' && !(node.columnId in columnSizes); }); } // Get a set of sizes before we start mutating, to avoid // weird disproportionate measures if the grid has set // column widths, but no full grid width set colWidths = colNodes.map(function (colNode) { return colNode.offsetWidth; }); // Set a baseline size for each column based on // its original measure colNodes.forEach(function (colNode, i) { resizeColumnWidth(this, colNode.columnId, colWidths[i], null, false); }, this); this._resizedColumns = true; } dom.setSelectable(this.domNode, true); var cell = this._targetCell, delta = this._getResizeMouseLocation(e) - this._startX, //final change in position of resizer newWidth = cell.offsetWidth + delta, //the new width after resize obj = this._getResizedColumnWidths(),//get current total column widths before resize totalWidth = obj.totalWidth, lastCol = obj.lastColId, lastColWidth = query('.dgrid-column-' + miscUtil.escapeCssIdentifier(lastCol, '-'), this.headerNode)[0].offsetWidth; if (newWidth < this.minWidth) { //enforce minimum widths newWidth = this.minWidth; } if (resizeColumnWidth(this, cell.columnId, newWidth, e.type)) { if (cell.columnId !== lastCol && this.adjustLastColumn) { if (totalWidth + delta < gridWidth) { //need to set last column's width to auto resizeColumnWidth(this, lastCol, 'auto', e.type); } else if (lastColWidth - delta <= this.minWidth) { //change last col width back to px, unless it is the last column itself being resized... resizeColumnWidth(this, lastCol, this.minWidth, e.type); } } } resizer.hide(); // Clean up after the resize operation delete this._startX; delete this._targetCell; }, _updateResizerPosition: function (e) { // Summary: // updates position of resizer bar as mouse moves // e: Object // mousemove event object if (!this._targetCell) { return; // Release event was already processed } var mousePos = this._getResizeMouseLocation(e), delta = mousePos - this._startX, //change from where user clicked to where they drag width = this._targetCell.offsetWidth, left = mousePos; if (width + delta < this.minWidth) { left = this._startX - (width - this.minWidth); } resizer.move(left); }, _getResizeMouseLocation: function (e) { //Summary: // returns position of mouse relative to the left edge // e: event object // mouse move event object var posX = 0; if (e.pageX) { posX = e.pageX; } else if (e.clientX) { posX = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; } return posX; }, _getResizedColumnWidths: function () { //Summary: // returns object containing new column width and column id var totalWidth = 0, colNodes = query( (this.columnSets ? '.dgrid-column-set-cell ' : '') + 'tr:first-child .dgrid-cell', this.headerNode); var i = colNodes.length; if (!i) { return {}; } var lastColId = colNodes[i - 1].columnId; while (i--) { totalWidth += colNodes[i].offsetWidth; } return {totalWidth: totalWidth, lastColId: lastColId}; } }); });