+ /**
+ * @private
+ * Click handler for the shared column dropdown menu, called on beforeshow. Builds the menu
+ * which displays the list of columns for the user to show or hide.
+ */
+ beforeColMenuShow : function() {
+ var colModel = this.cm,
+ colCount = colModel.getColumnCount(),
+ colMenu = this.colMenu,
+ i;
+
+ colMenu.removeAll();
+
+ for (i = 0; i < colCount; i++) {
+ if (colModel.config[i].hideable !== false) {
+ colMenu.add(new Ext.menu.CheckItem({
+ text : colModel.getColumnHeader(i),
+ itemId : 'col-' + colModel.getColumnId(i),
+ checked : !colModel.isHidden(i),
+ disabled : colModel.config[i].hideable === false,
+ hideOnClick: false
+ }));
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Attached as the 'itemclick' handler to the header menu and the column show/hide submenu (if available).
+ * Performs sorting if the sorter buttons were clicked, otherwise hides/shows the column that was clicked.
+ */
+ handleHdMenuClick : function(item) {
+ var store = this.ds,
+ dataIndex = this.cm.getDataIndex(this.hdCtxIndex);
+
+ switch (item.getItemId()) {
+ case 'asc':
+ store.sort(dataIndex, 'ASC');
+ break;
+ case 'desc':
+ store.sort(dataIndex, 'DESC');
+ break;
+ default:
+ this.handleHdMenuClickDefault(item);
+ }
+ return true;
+ },
+
+ /**
+ * Called by handleHdMenuClick if any button except a sort ASC/DESC button was clicked. The default implementation provides
+ * the column hide/show functionality based on the check state of the menu item. A different implementation can be provided
+ * if needed.
+ * @param {Ext.menu.BaseItem} item The menu item that was clicked
+ */
+ handleHdMenuClickDefault: function(item) {
+ var colModel = this.cm,
+ itemId = item.getItemId(),
+ index = colModel.getIndexById(itemId.substr(4));
+
+ if (index != -1) {
+ if (item.checked && colModel.getColumnsBy(this.isHideableColumn, this).length <= 1) {
+ this.onDenyColumnHide();
+ return;
+ }
+ colModel.setHidden(index, item.checked);
+ }
+ },
+
+ /**
+ * @private
+ * Called when a header cell is clicked - shows the menu if the click happened over a trigger button
+ */
+ handleHdDown : function(e, target) {
+ if (Ext.fly(target).hasClass('x-grid3-hd-btn')) {
+ e.stopEvent();
+
+ var colModel = this.cm,
+ header = this.findHeaderCell(target),
+ index = this.getCellIndex(header),
+ sortable = colModel.isSortable(index),
+ menu = this.hmenu,
+ menuItems = menu.items,
+ menuCls = this.headerMenuOpenCls;
+
+ this.hdCtxIndex = index;
+
+ Ext.fly(header).addClass(menuCls);
+ menuItems.get('asc').setDisabled(!sortable);
+ menuItems.get('desc').setDisabled(!sortable);
+
+ menu.on('hide', function() {
+ Ext.fly(header).removeClass(menuCls);
+ }, this, {single:true});
+
+ menu.show(target, 'tl-bl?');
+ }
+ },
+
+ /**
+ * @private
+ * Attached to the headers' mousemove event. This figures out the CSS cursor to use based on where the mouse is currently
+ * pointed. If the mouse is currently hovered over the extreme left or extreme right of any header cell and the cell next
+ * to it is resizable it is given the resize cursor, otherwise the cursor is set to an empty string.
+ */
+ handleHdMove : function(e) {
+ var header = this.findHeaderCell(this.activeHdRef);
+
+ if (header && !this.headersDisabled) {
+ var handleWidth = this.splitHandleWidth || 5,
+ activeRegion = this.activeHdRegion,
+ headerStyle = header.style,
+ colModel = this.cm,
+ cursor = '',
+ pageX = e.getPageX();
+
+ if (this.grid.enableColumnResize !== false) {
+ var activeHeaderIndex = this.activeHdIndex,
+ previousVisible = this.getPreviousVisible(activeHeaderIndex),
+ currentResizable = colModel.isResizable(activeHeaderIndex),
+ previousResizable = previousVisible && colModel.isResizable(previousVisible),
+ inLeftResizer = pageX - activeRegion.left <= handleWidth,
+ inRightResizer = activeRegion.right - pageX <= (!this.activeHdBtn ? handleWidth : 2);
+
+ if (inLeftResizer && previousResizable) {
+ cursor = Ext.isAir ? 'move' : Ext.isWebKit ? 'e-resize' : 'col-resize'; // col-resize not always supported
+ } else if (inRightResizer && currentResizable) {
+ cursor = Ext.isAir ? 'move' : Ext.isWebKit ? 'w-resize' : 'col-resize';
+ }
+ }
+
+ headerStyle.cursor = cursor;
+ }
+ },
+
+ /**
+ * @private
+ * Returns the index of the nearest currently visible header to the left of the given index.
+ * @param {Number} index The header index
+ * @return {Number/undefined} The index of the nearest visible header
+ */
+ getPreviousVisible: function(index) {
+ while (index > 0) {
+ if (!this.cm.isHidden(index - 1)) {
+ return index;
+ }
+ index--;
+ }
+ return undefined;
+ },
+
+ /**
+ * @private
+ * Tied to the header element's mouseover event - adds the over class to the header cell if the menu is not disabled
+ * for that cell
+ */
+ handleHdOver : function(e, target) {
+ var header = this.findHeaderCell(target);
+
+ if (header && !this.headersDisabled) {
+ var fly = this.fly(header);
+
+ this.activeHdRef = target;
+ this.activeHdIndex = this.getCellIndex(header);
+ this.activeHdRegion = fly.getRegion();
+
+ if (!this.isMenuDisabled(this.activeHdIndex, fly)) {
+ fly.addClass('x-grid3-hd-over');
+ this.activeHdBtn = fly.child('.x-grid3-hd-btn');
+
+ if (this.activeHdBtn) {
+ this.activeHdBtn.dom.style.height = (header.firstChild.offsetHeight - 1) + 'px';
+ }
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Tied to the header element's mouseout event. Removes the hover class from the header cell
+ */
+ handleHdOut : function(e, target) {
+ var header = this.findHeaderCell(target);
+
+ if (header && (!Ext.isIE || !e.within(header, true))) {
+ this.activeHdRef = null;
+ this.fly(header).removeClass('x-grid3-hd-over');
+ header.style.cursor = '';
+ }
+ },
+
+ /**
+ * @private
+ * Used by {@link #handleHdOver} to determine whether or not to show the header menu class on cell hover
+ * @param {Number} cellIndex The header cell index
+ * @param {Ext.Element} el The cell element currently being hovered over
+ */
+ isMenuDisabled: function(cellIndex, el) {
+ return this.cm.isMenuDisabled(cellIndex);
+ },
+
+ /**
+ * @private
+ * Returns true if there are any rows rendered into the GridView
+ * @return {Boolean} True if any rows have been rendered
+ */
+ hasRows : function() {
+ var fc = this.mainBody.dom.firstChild;
+ return fc && fc.nodeType == 1 && fc.className != 'x-grid-empty';
+ },
+
+ /**
+ * @private
+ */
+ isHideableColumn : function(c) {
+ return !c.hidden;
+ },
+
+ /**
+ * @private
+ * DEPRECATED - will be removed in Ext JS 5.0
+ */
+ bind : function(d, c) {
+ this.initData(d, c);
+ }
+});
+
+
+// private
+// This is a support class used internally by the Grid components
+Ext.grid.GridView.SplitDragZone = Ext.extend(Ext.dd.DDProxy, {
+
+ constructor: function(grid, hd){
+ this.grid = grid;
+ this.view = grid.getView();
+ this.marker = this.view.resizeMarker;
+ this.proxy = this.view.resizeProxy;
+ Ext.grid.GridView.SplitDragZone.superclass.constructor.call(this, hd,
+ 'gridSplitters' + this.grid.getGridEl().id, {
+ dragElId : Ext.id(this.proxy.dom), resizeFrame:false
+ });
+ this.scroll = false;
+ this.hw = this.view.splitHandleWidth || 5;
+ },
+
+ b4StartDrag : function(x, y){
+ this.dragHeadersDisabled = this.view.headersDisabled;
+ this.view.headersDisabled = true;
+ var h = this.view.mainWrap.getHeight();
+ this.marker.setHeight(h);
+ this.marker.show();
+ this.marker.alignTo(this.view.getHeaderCell(this.cellIndex), 'tl-tl', [-2, 0]);
+ this.proxy.setHeight(h);
+ var w = this.cm.getColumnWidth(this.cellIndex),
+ minw = Math.max(w-this.grid.minColumnWidth, 0);
+ this.resetConstraints();
+ this.setXConstraint(minw, 1000);
+ this.setYConstraint(0, 0);
+ this.minX = x - minw;
+ this.maxX = x + 1000;
+ this.startPos = x;
+ Ext.dd.DDProxy.prototype.b4StartDrag.call(this, x, y);
+ },
+
+ allowHeaderDrag : function(e){
+ return true;
+ },
+
+ handleMouseDown : function(e){
+ var t = this.view.findHeaderCell(e.getTarget());
+ if(t && this.allowHeaderDrag(e)){
+ var xy = this.view.fly(t).getXY(),
+ x = xy[0],
+ exy = e.getXY(),
+ ex = exy[0],
+ w = t.offsetWidth,
+ adjust = false;
+
+ if((ex - x) <= this.hw){
+ adjust = -1;
+ }else if((x+w) - ex <= this.hw){
+ adjust = 0;
+ }
+ if(adjust !== false){
+ this.cm = this.grid.colModel;
+ var ci = this.view.getCellIndex(t);
+ if(adjust == -1){
+ if (ci + adjust < 0) {
+ return;
+ }
+ while(this.cm.isHidden(ci+adjust)){
+ --adjust;
+ if(ci+adjust < 0){
+ return;
+ }
+ }
+ }
+ this.cellIndex = ci+adjust;
+ this.split = t.dom;
+ if(this.cm.isResizable(this.cellIndex) && !this.cm.isFixed(this.cellIndex)){
+ Ext.grid.GridView.SplitDragZone.superclass.handleMouseDown.apply(this, arguments);
+ }
+ }else if(this.view.columnDrag){
+ this.view.columnDrag.callHandleMouseDown(e);
+ }
+ }
+ },
+
+ endDrag : function(e){
+ this.marker.hide();
+ var v = this.view,
+ endX = Math.max(this.minX, e.getPageX()),
+ diff = endX - this.startPos,
+ disabled = this.dragHeadersDisabled;
+
+ v.onColumnSplitterMoved(this.cellIndex, this.cm.getColumnWidth(this.cellIndex)+diff);
+ setTimeout(function(){
+ v.headersDisabled = disabled;
+ }, 50);
+ },
+
+ autoOffset : function(){
+ this.setDelta(0,0);
+ }
+});
+/**
+ * @class Ext.grid.PivotGridView
+ * @extends Ext.grid.GridView
+ * Specialised GridView for rendering Pivot Grid components. Config can be passed to the PivotGridView via the PivotGrid constructor's
+ * viewConfig option:
+<pre><code>
+new Ext.grid.PivotGrid({
+ viewConfig: {
+ title: 'My Pivot Grid',
+ getCellCls: function(value) {
+ return value > 10 'red' : 'green';
+ }
+ }
+});
+</code></pre>
+ * <p>Currently {@link #title} and {@link #getCellCls} are the only configuration options accepted by PivotGridView. All other
+ * interaction is performed via the {@link Ext.grid.PivotGrid PivotGrid} class.</p>
+ */
+Ext.grid.PivotGridView = Ext.extend(Ext.grid.GridView, {
+
+ /**
+ * The CSS class added to all group header cells. Defaults to 'grid-hd-group-cell'
+ * @property colHeaderCellCls
+ * @type String
+ */
+ colHeaderCellCls: 'grid-hd-group-cell',
+
+ /**
+ * @cfg {String} title Optional title to be placed in the top left corner of the PivotGrid. Defaults to an empty string.
+ */
+ title: '',
+
+ /**
+ * @cfg {Function} getCellCls Optional function which should return a CSS class name for each cell value. This is useful when
+ * color coding cells based on their value. Defaults to undefined.
+ */
+
+ /**
+ * Returns the headers to be rendered at the top of the grid. Should be a 2-dimensional array, where each item specifies the number
+ * of columns it groups (column in this case refers to normal grid columns). In the example below we have 5 city groups, which are
+ * each part of a continent supergroup. The colspan for each city group refers to the number of normal grid columns that group spans,
+ * so in this case the grid would be expected to have a total of 12 columns:
+<pre><code>
+[
+ {
+ items: [
+ {header: 'England', colspan: 5},
+ {header: 'USA', colspan: 3}
+ ]
+ },
+ {
+ items: [
+ {header: 'London', colspan: 2},
+ {header: 'Cambridge', colspan: 3},
+ {header: 'Palo Alto', colspan: 3}
+ ]
+ }
+]
+</code></pre>
+ * In the example above we have cities nested under countries. The nesting could be deeper if desired - e.g. Continent -> Country ->
+ * State -> City, or any other structure. The only constaint is that the same depth must be used throughout the structure.
+ * @return {Array} A tree structure containing the headers to be rendered. Must include the colspan property at each level, which should
+ * be the sum of all child nodes beneath this node.
+ */
+ getColumnHeaders: function() {
+ return this.grid.topAxis.buildHeaders();;
+ },
+
+ /**
+ * Returns the headers to be rendered on the left of the grid. Should be a 2-dimensional array, where each item specifies the number
+ * of rows it groups. In the example below we have 5 city groups, which are each part of a continent supergroup. The rowspan for each
+ * city group refers to the number of normal grid columns that group spans, so in this case the grid would be expected to have a
+ * total of 12 rows:
+<pre><code>
+[
+ {
+ width: 90,
+ items: [
+ {header: 'England', rowspan: 5},
+ {header: 'USA', rowspan: 3}
+ ]
+ },
+ {
+ width: 50,
+ items: [
+ {header: 'London', rowspan: 2},
+ {header: 'Cambridge', rowspan: 3},
+ {header: 'Palo Alto', rowspan: 3}
+ ]
+ }
+]
+</code></pre>
+ * In the example above we have cities nested under countries. The nesting could be deeper if desired - e.g. Continent -> Country ->
+ * State -> City, or any other structure. The only constaint is that the same depth must be used throughout the structure.
+ * @return {Array} A tree structure containing the headers to be rendered. Must include the colspan property at each level, which should
+ * be the sum of all child nodes beneath this node.
+ * Each group may specify the width it should be rendered with.
+ * @return {Array} The row groups
+ */
+ getRowHeaders: function() {
+ return this.grid.leftAxis.buildHeaders();
+ },
+
+ /**
+ * @private
+ * Renders rows between start and end indexes
+ * @param {Number} startRow Index of the first row to render
+ * @param {Number} endRow Index of the last row to render
+ */
+ renderRows : function(startRow, endRow) {
+ var grid = this.grid,
+ rows = grid.extractData(),
+ rowCount = rows.length,
+ templates = this.templates,
+ renderer = grid.renderer,
+ hasRenderer = typeof renderer == 'function',
+ getCellCls = this.getCellCls,
+ hasGetCellCls = typeof getCellCls == 'function',
+ cellTemplate = templates.cell,
+ rowTemplate = templates.row,
+ rowBuffer = [],
+ meta = {},
+ tstyle = 'width:' + this.getGridInnerWidth() + 'px;',
+ colBuffer, column, i;
+
+ startRow = startRow || 0;
+ endRow = Ext.isDefined(endRow) ? endRow : rowCount - 1;
+
+ for (i = 0; i < rowCount; i++) {
+ row = rows[i];
+ colCount = row.length;
+ colBuffer = [];
+
+ rowIndex = startRow + i;
+
+ //build up each column's HTML
+ for (j = 0; j < colCount; j++) {
+ cell = row[j];
+
+ meta.css = j === 0 ? 'x-grid3-cell-first ' : (j == (colCount - 1) ? 'x-grid3-cell-last ' : '');
+ meta.attr = meta.cellAttr = '';
+ meta.value = cell;
+
+ if (Ext.isEmpty(meta.value)) {
+ meta.value = ' ';
+ }
+
+ if (hasRenderer) {
+ meta.value = renderer(meta.value);
+ }
+
+ if (hasGetCellCls) {
+ meta.css += getCellCls(meta.value) + ' ';
+ }
+
+ colBuffer[colBuffer.length] = cellTemplate.apply(meta);
+ }
+
+ rowBuffer[rowBuffer.length] = rowTemplate.apply({
+ tstyle: tstyle,
+ cols : colCount,
+ cells : colBuffer.join(""),
+ alt : ''
+ });
+ }
+
+ return rowBuffer.join("");
+ },
+
+ /**
+ * The master template to use when rendering the GridView. Has a default template
+ * @property Ext.Template
+ * @type masterTpl
+ */
+ masterTpl: new Ext.Template(
+ '<div class="x-grid3 x-pivotgrid" hidefocus="true">',
+ '<div class="x-grid3-viewport">',
+ '<div class="x-grid3-header">',
+ '<div class="x-grid3-header-title"><span>{title}</span></div>',
+ '<div class="x-grid3-header-inner">',
+ '<div class="x-grid3-header-offset" style="{ostyle}"></div>',
+ '</div>',
+ '<div class="x-clear"></div>',
+ '</div>',
+ '<div class="x-grid3-scroller">',
+ '<div class="x-grid3-row-headers"></div>',
+ '<div class="x-grid3-body" style="{bstyle}">{body}</div>',
+ '<a href="#" class="x-grid3-focus" tabIndex="-1"></a>',
+ '</div>',
+ '</div>',
+ '<div class="x-grid3-resize-marker"> </div>',
+ '<div class="x-grid3-resize-proxy"> </div>',
+ '</div>'
+ ),
+
+ /**
+ * @private
+ * Adds a gcell template to the internal templates object. This is used to render the headers in a multi-level column header.
+ */
+ initTemplates: function() {
+ Ext.grid.PivotGridView.superclass.initTemplates.apply(this, arguments);
+
+ var templates = this.templates || {};
+ if (!templates.gcell) {
+ templates.gcell = new Ext.XTemplate(
+ '<td class="x-grid3-hd x-grid3-gcell x-grid3-td-{id} ux-grid-hd-group-row-{row} ' + this.colHeaderCellCls + '" style="{style}">',
+ '<div {tooltip} class="x-grid3-hd-inner x-grid3-hd-{id}" unselectable="on" style="{istyle}">',
+ this.grid.enableHdMenu ? '<a class="x-grid3-hd-btn" href="#"></a>' : '', '{value}',
+ '</div>',
+ '</td>'
+ );
+ }
+
+ this.templates = templates;
+ this.hrowRe = new RegExp("ux-grid-hd-group-row-(\\d+)", "");
+ },
+
+ /**
+ * @private
+ * Sets up the reference to the row headers element
+ */
+ initElements: function() {
+ Ext.grid.PivotGridView.superclass.initElements.apply(this, arguments);
+
+ /**
+ * @property rowHeadersEl
+ * @type Ext.Element
+ * The element containing all row headers
+ */
+ this.rowHeadersEl = new Ext.Element(this.scroller.child('div.x-grid3-row-headers'));
+
+ /**
+ * @property headerTitleEl
+ * @type Ext.Element
+ * The element that contains the optional title (top left section of the pivot grid)
+ */
+ this.headerTitleEl = new Ext.Element(this.mainHd.child('div.x-grid3-header-title'));
+ },
+
+ /**
+ * @private
+ * Takes row headers into account when calculating total available width
+ */
+ getGridInnerWidth: function() {
+ var previousWidth = Ext.grid.PivotGridView.superclass.getGridInnerWidth.apply(this, arguments);
+
+ return previousWidth - this.getTotalRowHeaderWidth();
+ },
+
+ /**
+ * Returns the total width of all row headers as specified by {@link #getRowHeaders}
+ * @return {Number} The total width
+ */
+ getTotalRowHeaderWidth: function() {
+ var headers = this.getRowHeaders(),
+ length = headers.length,
+ total = 0,
+ i;
+
+ for (i = 0; i< length; i++) {
+ total += headers[i].width;
+ }
+
+ return total;
+ },
+
+ /**
+ * @private
+ * Returns the total height of all column headers
+ * @return {Number} The total height
+ */
+ getTotalColumnHeaderHeight: function() {
+ return this.getColumnHeaders().length * 21;
+ },
+
+ /**
+ * @private
+ * Slight specialisation of the GridView renderUI - just adds the row headers
+ */
+ renderUI : function() {
+ var templates = this.templates,
+ innerWidth = this.getGridInnerWidth();
+
+ return templates.master.apply({
+ body : templates.body.apply({rows:' '}),
+ ostyle: 'width:' + innerWidth + 'px',
+ bstyle: 'width:' + innerWidth + 'px'
+ });
+ },
+
+ /**
+ * @private
+ * Make sure that the headers and rows are all sized correctly during layout
+ */
+ onLayout: function(width, height) {
+ Ext.grid.PivotGridView.superclass.onLayout.apply(this, arguments);
+
+ var width = this.getGridInnerWidth();
+
+ this.resizeColumnHeaders(width);
+ this.resizeAllRows(width);
+ },
+
+ /**
+ * Refreshs the grid UI
+ * @param {Boolean} headersToo (optional) True to also refresh the headers
+ */
+ refresh : function(headersToo) {
+ this.fireEvent('beforerefresh', this);
+ this.grid.stopEditing(true);
+
+ var result = this.renderBody();
+ this.mainBody.update(result).setWidth(this.getGridInnerWidth());
+ if (headersToo === true) {
+ this.updateHeaders();
+ this.updateHeaderSortState();
+ }
+ this.processRows(0, true);
+ this.layout();
+ this.applyEmptyText();
+ this.fireEvent('refresh', this);
+ },
+
+ /**
+ * @private
+ * Bypasses GridView's renderHeaders as they are taken care of separately by the PivotAxis instances
+ */
+ renderHeaders: Ext.emptyFn,
+
+ /**
+ * @private
+ * Taken care of by PivotAxis
+ */
+ fitColumns: Ext.emptyFn,
+
+ /**
+ * @private
+ * Called on layout, ensures that the width of each column header is correct. Omitting this can lead to faulty
+ * layouts when nested in a container.
+ * @param {Number} width The new width
+ */
+ resizeColumnHeaders: function(width) {
+ var topAxis = this.grid.topAxis;
+
+ if (topAxis.rendered) {
+ topAxis.el.setWidth(width);
+ }
+ },
+
+ /**
+ * @private
+ * Sets the row header div to the correct width. Should be called after rendering and reconfiguration of headers
+ */
+ resizeRowHeaders: function() {
+ var rowHeaderWidth = this.getTotalRowHeaderWidth(),
+ marginStyle = String.format("margin-left: {0}px;", rowHeaderWidth);
+
+ this.rowHeadersEl.setWidth(rowHeaderWidth);
+ this.mainBody.applyStyles(marginStyle);
+ Ext.fly(this.innerHd).applyStyles(marginStyle);
+
+ this.headerTitleEl.setWidth(rowHeaderWidth);
+ this.headerTitleEl.setHeight(this.getTotalColumnHeaderHeight());
+ },
+
+ /**
+ * @private
+ * Resizes all rendered rows to the given width. Usually called by onLayout
+ * @param {Number} width The new width
+ */
+ resizeAllRows: function(width) {
+ var rows = this.getRows(),
+ length = rows.length,
+ i;
+
+ for (i = 0; i < length; i++) {
+ Ext.fly(rows[i]).setWidth(width);
+ Ext.fly(rows[i]).child('table').setWidth(width);
+ }
+ },
+
+ /**
+ * @private
+ * Updates the Row Headers, deferring the updating of Column Headers to GridView
+ */
+ updateHeaders: function() {
+ this.renderGroupRowHeaders();
+ this.renderGroupColumnHeaders();
+ },
+
+ /**
+ * @private
+ * Renders all row header groups at all levels based on the structure fetched from {@link #getGroupRowHeaders}
+ */
+ renderGroupRowHeaders: function() {
+ var leftAxis = this.grid.leftAxis;
+
+ this.resizeRowHeaders();
+ leftAxis.rendered = false;
+ leftAxis.render(this.rowHeadersEl);
+
+ this.setTitle(this.title);