/** * @private */Ext.define('Ext.grid.header.DropZone', { extend: 'Ext.dd.DropZone', colHeaderCls: Ext.baseCSSPrefix + 'column-header', proxyOffsets: [-4, -9], constructor: function(headerCt) { var me = this; me.headerCt = headerCt; me.ddGroup = me.getDDGroup(); me.autoGroup = true; me.callParent([headerCt.el]); }, destroy: function () { Ext.destroy(this.topIndicator, this.bottomIndicator); this.callParent(); }, getDDGroup: function() { return 'header-dd-zone-' + this.headerCt.up('[scrollerOwner]').id; }, getTargetFromEvent : function(e){ return e.getTarget('.' + this.colHeaderCls); }, getTopIndicator: function() { if (!this.topIndicator) { this.topIndicator = Ext.getBody().createChild({ role: 'presentation', cls: Ext.baseCSSPrefix + "col-move-top", //<debug> // tell the spec runner to ignore this element when checking if the dom is clean "data-sticky": true, //</debug> html: " " }); this.indicatorXOffset = Math.floor((this.topIndicator.dom.offsetWidth + 1) / 2); } return this.topIndicator; }, getBottomIndicator: function() { if (!this.bottomIndicator) { this.bottomIndicator = Ext.getBody().createChild({ role: 'presentation', cls: Ext.baseCSSPrefix + "col-move-bottom", //<debug> // tell the spec runner to ignore this element when checking if the dom is clean "data-sticky": true, //</debug> html: " " }); } return this.bottomIndicator; }, getLocation: function(e, t) { var x = e.getXY()[0], region = Ext.fly(t).getRegion(), pos; if ((region.right - x) <= (region.right - region.left) / 2) { pos = "after"; } else { pos = "before"; } return { pos: pos, header: Ext.getCmp(t.id), node: t }; }, positionIndicator: function(data, node, e){ var me = this, dragHeader = data.header, dropLocation = me.getLocation(e, node), targetHeader = dropLocation.header, pos = dropLocation.pos, nextHd, prevHd, topIndicator, bottomIndicator, topAnchor, bottomAnchor, topXY, bottomXY, headerCtEl, minX, maxX, allDropZones, ln, i, dropZone; // Avoid expensive CQ lookups and DOM calculations if dropPosition has not changed if (targetHeader === me.lastTargetHeader && pos === me.lastDropPos) { return; } nextHd = dragHeader.nextSibling('gridcolumn:not([hidden])'); prevHd = dragHeader.previousSibling('gridcolumn:not([hidden])'); me.lastTargetHeader = targetHeader; me.lastDropPos = pos; // Cannot drag to before non-draggable start column if (!targetHeader.draggable && pos === 'before' && targetHeader.getIndex() === 0) { return false; } data.dropLocation = dropLocation; if ((dragHeader !== targetHeader) && ((pos === "before" && nextHd !== targetHeader) || (pos === "after" && prevHd !== targetHeader)) && !targetHeader.isDescendantOf(dragHeader)) { // As we move in between different DropZones that are in the same // group (such as the case when in a locked grid), invalidateDrop // on the other dropZones. allDropZones = Ext.dd.DragDropManager.getRelated(me); ln = allDropZones.length; i = 0; for (; i < ln; i++) { dropZone = allDropZones[i]; if (dropZone !== me && dropZone.invalidateDrop) { dropZone.invalidateDrop(); } } me.valid = true; topIndicator = me.getTopIndicator(); bottomIndicator = me.getBottomIndicator(); if (pos === 'before') { topAnchor = 'bc-tl'; bottomAnchor = 'tc-bl'; } else { topAnchor = 'bc-tr'; bottomAnchor = 'tc-br'; } // Calculate arrow positions. Offset them to align exactly with column border line topXY = topIndicator.getAlignToXY(targetHeader.el, topAnchor); bottomXY = bottomIndicator.getAlignToXY(targetHeader.el, bottomAnchor); // constrain the indicators to the viewable section headerCtEl = me.headerCt.el; minX = headerCtEl.getX() - me.indicatorXOffset; maxX = headerCtEl.getX() + headerCtEl.getWidth(); topXY[0] = Ext.Number.constrain(topXY[0], minX, maxX); bottomXY[0] = Ext.Number.constrain(bottomXY[0], minX, maxX); // position and show indicators topIndicator.setXY(topXY); bottomIndicator.setXY(bottomXY); topIndicator.show(); bottomIndicator.show(); // invalidate drop operation and hide indicators } else { me.invalidateDrop(); } }, invalidateDrop: function() { this.valid = false; this.hideIndicators(); }, onNodeOver: function(node, dragZone, e, data) { var me = this, from = data.header, doPosition, fromPanel, to, toPanel; if (data.header.el.dom === node) { doPosition = false; } else { data.isLock = data.isUnlock = data.crossPanel = false; to = me.getLocation(e, node).header; // Dragging within the same container - always valid doPosition = (from.ownerCt === to.ownerCt); // If from different containers, and they are not sealed, then continue checking if (!doPosition && (!from.ownerCt.isSealed() && !to.ownerCt.isSealed())) { doPosition = true; fromPanel = from.up('tablepanel'); toPanel = to.up('tablepanel'); if (fromPanel !== toPanel) { data.crossPanel = true; // If it's a lock operation, check that it's allowable. data.isLock = toPanel.isLocked && !fromPanel.isLocked; data.isUnlock = !toPanel.isLocked && fromPanel.isLocked; if ((data.isUnlock && from.lockable === false) || (data.isLock && !from.isLockable())) { doPosition = false; } } } } if (doPosition) { me.positionIndicator(data, node, e); } else { me.valid = false; } return me.valid ? me.dropAllowed : me.dropNotAllowed; }, hideIndicators: function() { var me = this; me.getTopIndicator().hide(); me.getBottomIndicator().hide(); me.lastTargetHeader = me.lastDropPos = null; }, onNodeOut: function() { this.hideIndicators(); }, /** * @private * Used to determine the move position for the view's data columns for nested headers at any level. */ getNestedHeader: function (header, first) { var items = header.items, pos; if (header.isGroupHeader && items.length) { pos = !first ? 'first' : 'last'; header = this.getNestedHeader(items[pos](), first); } return header; }, onNodeDrop: function(node, dragZone, e, data) { // Do not process the upcoming click after this mouseup. It's not a click gesture this.headerCt.blockNextEvent(); // Note that dropLocation.pos refers to whether the header is dropped before or after the target node! if (!this.valid) { return; } var me = this, dragHeader = data.header, dropLocation = data.dropLocation, dropPosition = dropLocation.pos, targetHeader = dropLocation.header, fromCt = dragHeader.ownerCt, fromCtRoot = fromCt.getRootHeaderCt(), toCt = targetHeader.ownerCt, // Use the full column manager here, the indices we want are for moving the actual items in the container. // The HeaderContainer translates this to visible columns for informing the view and firing events. visibleColumnManager = me.headerCt.visibleColumnManager, visibleFromIdx = visibleColumnManager.getHeaderIndex(dragHeader), visibleToIdx, colsToMove, scrollerOwner, savedWidth; // If we are dragging in between two HeaderContainers that have had the lockable mixin injected we will lock/unlock // headers in between sections, and then continue with another execution of onNodeDrop to ensure the header is // dropped into the correct group. if (data.isLock || data.isUnlock) { scrollerOwner = fromCt.up('[scrollerOwner]'); visibleToIdx = toCt.items.indexOf(targetHeader); if (dropPosition === 'after') { visibleToIdx++; } if (data.isLock) { scrollerOwner.lock(dragHeader, visibleToIdx, toCt); } else { scrollerOwner.unlock(dragHeader, visibleToIdx, toCt); } } // This is a drop within the same HeaderContainer. else { // For the after position, we need to update the visibleToIdx index. In case it's nested in one or more // grouped headers, we need to get the last header (or the first, depending on the dropPosition) in the // items collection for the most deeply-nested header, whether it be first or last in the collection. // This will yield the header index in the visibleColumnManager, which will correctly maintain a list // of all the headers. visibleToIdx = dropPosition === 'after' ? // Get the last header in the most deeply-nested header group and add one. visibleColumnManager.getHeaderIndex(me.getNestedHeader(targetHeader, 1)) + 1 : // Get the first header in the most deeply-nested header group. visibleColumnManager.getHeaderIndex(me.getNestedHeader(targetHeader, 0)); me.invalidateDrop(); // Cache the width here, we need to get it before we removed it from the DOM savedWidth = dragHeader.getWidth(); // Suspend layouts while we sort all this out. Ext.suspendLayouts(); // When removing and then adding, the owning gridpanel will be informed of column mutation twice // Both remove and add handling inform the owning grid. // The isDDMoveInGrid flag will prevent the remove operation from doing this. // See Ext.grid.header.Container#onRemove. // It's enough to inform the root container about the move fromCtRoot.isDDMoveInGrid = !data.crossPanel; // ***Move the headers*** // // If both drag and target headers are groupHeaders, we have to check and see if they are nested, i.e., // there are multiple stacked group headers with only subheaders at the lowest level: // // +-----------------------------------+ // | Group 1 | // |-----------------------------------| // | Group 2 | // other |-----------------------------------| other // headers | Group 3 | headers // |-----------------------------------| // | Field3 | Field4 | Field5 | Field6 | // |===================================| // | view | // +-----------------------------------+ // // In these cases, we need to mark the groupHeader that is the ownerCt of the targetHeader and then only // remove the headers up until that (removal of headers is recursive and assumes that any header with no // children can be safely removed, which is not a safe assumption). // See Ext.grid.header.Container#onRemove. if (dragHeader.isGroupHeader && targetHeader.isGroupHeader) { dragHeader.setNestedParent(targetHeader); } // We only need to be concerned with moving the dragHeader component before or after the targetHeader // component rather than trying to pass indices, which is too ambiguous and could refer to any // collection at any level of (grouped) header containers. if (dropPosition === 'before') { toCt.moveBefore(dragHeader, targetHeader); } else { toCt.moveAfter(dragHeader, targetHeader); } // ***Move the view data columns*** // Refresh the view if it's not the last header in a group. If it is the last header, we don't need // to refresh the view as the headers and the corrresponding data columns will already be correctly // aligned (think of the group header sitting directly atop the last header in the group). // Also, it's not necessary to refresh the view if the indices are the same. // NOTE that targetHeader can be destroyed by this point if it was a group header // and we just dragged the last column out of it; in that case header's items collection // will be nulled. if (visibleToIdx >= 0 && !(targetHeader.isGroupHeader && (!targetHeader.items || !targetHeader.items.length)) && visibleFromIdx !== visibleToIdx) { colsToMove = dragHeader.isGroupHeader ? dragHeader.query('gridcolumn:not([hidden]):not([isGroupHeader])').length : 1; // We need to adjust the visibleToIdx when both of the following conditions are met: // 1. The drag is forward, i.e., the dragHeader is being dragged to the right. // 2. There is more than one column being dragged, i.e., an entire group. if ((visibleFromIdx <= visibleToIdx) && colsToMove > 1) { visibleToIdx -= colsToMove; } // It's necessary to lookup the ancestor grid of the grouped header b/c the header could be // nested at any level. toCt.getRootHeaderCt().grid.view.moveColumn(visibleFromIdx, visibleToIdx, colsToMove); } // We need to always fire a columnmove event. Check for an .ownerCt first in case this is a // grouped header. fromCtRoot.fireEvent('columnmove', fromCt, dragHeader, visibleFromIdx, visibleToIdx); fromCtRoot.isDDMoveInGrid = false; // Group headers skrinkwrap their child headers. // Therefore a child header may not flex; it must contribute a fixed width. // But we restore the flex value when moving back into the main header container // // Note that we don't need to save the flex if coming from another group header b/c it couldn't // have had one! if (toCt.isGroupHeader && !fromCt.isGroupHeader) { // Adjust the width of the "to" group header only if we dragged in from somewhere else. // If not within the same container. if (fromCt !== toCt) { dragHeader.savedFlex = dragHeader.flex; delete dragHeader.flex; dragHeader.width = savedWidth; } } else if (!fromCt.isGroupHeader) { if (dragHeader.savedFlex) { dragHeader.flex = dragHeader.savedFlex; delete dragHeader.width; } } Ext.resumeLayouts(true); // The grid must lay out so that its headerCt lays out. // It will not be thrown into the mix by BorderLayout#getLayoutItems // if it's floated, so we have to force the issue. if (me.headerCt.grid.floated) { me.headerCt.grid.updateLayout(); } // Ext.grid.header.Container will handle the removal of empty groups, don't handle it here. } }});