UNPKG

fin-hypergrid

Version:

Canvas-based high-performance grid

675 lines (563 loc) 22.4 kB
/* eslint-env browser */ /* global requestAnimationFrame */ 'use strict'; // This feature is responsible for column drag and drop reordering. // This object is a mess and desperately needs a complete rewrite..... var Feature = require('./Feature'); var GRAB = ['grab', '-moz-grab', '-webkit-grab'], GRABBING = ['grabbing', '-moz-grabbing', '-webkit-grabbing'], setName = function(name) { this.cursor = name; }; var columnAnimationTime = 150; var dragger; var draggerCTX; var floatColumn; var floatColumnCTX; function translate(grid, x, y) { if (grid.isIE11) { var zoomFactor = grid.getBodyZoomFactor(); x *= zoomFactor; y *= zoomFactor; } return 'translate(' + x + 'px, ' + y + 'px)'; } /** * @constructor * @extends Feature */ var ColumnMoving = Feature.extend('ColumnMoving', { /** * queue up the animations that need to play so they are done synchronously * @type {Array} * @memberOf CellMoving.prototype */ floaterAnimationQueue: [], /** * am I currently auto scrolling right * @type {boolean} * @memberOf CellMoving.prototype */ columnDragAutoScrollingRight: false, /** * am I currently auto scrolling left * @type {boolean} * @memberOf CellMoving.prototype */ columnDragAutoScrollingLeft: false, /** * is the drag mechanism currently enabled ("armed") * @type {boolean} * @memberOf CellMoving.prototype */ dragArmed: false, /** * am I dragging right now * @type {boolean} * @memberOf CellMoving.prototype */ dragging: false, /** * the column index of the currently dragged column * @type {number} * @memberOf CellMoving.prototype */ dragCol: -1, /** * an offset to position the dragged item from the cursor * @type {number} * @memberOf CellMoving.prototype */ dragOffset: 0, /** * @memberOf CellMoving.prototype * @desc give me an opportunity to initialize stuff on the grid * @param {Hypergrid} grid */ initializeOn: function(grid) { this.isFloatingNow = false; this.initializeAnimationSupport(grid); if (this.next) { this.next.initializeOn(grid); } }, /** * @memberOf CellMoving.prototype * @desc initialize animation support on the grid * @param {Hypergrid} grid */ initializeAnimationSupport: function(grid) { if (!dragger) { dragger = document.createElement('canvas'); dragger.setAttribute('width', '0px'); dragger.setAttribute('height', '0px'); dragger.style.position = 'fixed'; document.body.appendChild(dragger); draggerCTX = dragger.getContext('2d', { alpha: false }); } if (!floatColumn) { floatColumn = document.createElement('canvas'); floatColumn.setAttribute('width', '0px'); floatColumn.setAttribute('height', '0px'); floatColumn.style.position = 'fixed'; document.body.appendChild(floatColumn); floatColumnCTX = floatColumn.getContext('2d', { alpha: false }); } }, /** * @memberOf CellMoving.prototype * @param {Hypergrid} grid * @param {Object} event - the event details */ handleMouseDrag: function(grid, event) { var gridCell = event.gridCell; var x; //var y; var distance = Math.abs(event.primitiveEvent.detail.dragstart.x - event.primitiveEvent.detail.mouse.x); if (distance < 10 || event.isColumnFixed) { if (this.next) { this.next.handleMouseDrag(grid, event); } return; } if (event.isHeaderCell && this.dragArmed && !this.dragging) { this.dragging = true; this.dragCol = gridCell.x; this.dragOffset = event.mousePoint.x; this.detachChain(); x = event.primitiveEvent.detail.mouse.x - this.dragOffset; //y = event.primitiveEvent.detail.mouse.y; this.createDragColumn(grid, x, this.dragCol); } else if (this.next) { this.next.handleMouseDrag(grid, event); } if (this.dragging) { x = event.primitiveEvent.detail.mouse.x - this.dragOffset; //y = event.primitiveEvent.detail.mouse.y; this.dragColumn(grid, x); } }, /** * @memberOf CellMoving.prototype * @param {Hypergrid} grid * @param {Object} event - the event details */ handleMouseDown: function(grid, event) { if ( grid.properties.columnsReorderable && !event.primitiveEvent.detail.isRightClick && !event.isColumnFixed && event.isHeaderCell ) { this.dragArmed = true; this.cursor = GRABBING; grid.clearSelections(); } else if (this.next) { this.next.handleMouseDown(grid, event); } }, /** * @memberOf CellMoving.prototype * @param {Hypergrid} grid * @param {Object} event - the event details */ handleMouseUp: function(grid, event) { //var col = event.gridCell.x; if (this.dragging) { this.cursor = null; //delay here to give other events a chance to be dropped var self = this; this.endDragColumn(grid); setTimeout(function() { self.attachChain(); }, 200); } this.dragCol = -1; this.dragging = false; this.dragArmed = false; this.cursor = null; grid.repaint(); if (this.next) { this.next.handleMouseUp(grid, event); } }, /** * @memberOf CellMoving.prototype * @param {Hypergrid} grid * @param {Object} event - the event details */ handleMouseMove: function(grid, event) { if ( grid.properties.columnsReorderable && !event.isColumnFixed && !this.dragging && event.isHeaderCell && event.mousePoint.y < grid.properties.columnGrabMargin ) { this.cursor = GRAB; } else { this.cursor = null; } if (this.next) { this.next.handleMouseMove(grid, event); } if (event.isHeaderCell && this.dragging) { this.cursor = GRABBING; } }, /** * @memberOf CellMoving.prototype * @desc this is the main event handler that manages the dragging of the column * @param {Hypergrid} grid * @param {boolean} draggedToTheRight - are we moving to the right */ floatColumnTo: function(grid, draggedToTheRight) { this.floatingNow = true; var visibleColumns = grid.renderer.visibleColumns; var scrollLeft = grid.getHScrollValue(); var floaterIndex = grid.renderOverridesCache.floater.columnIndex; var draggerIndex = grid.renderOverridesCache.dragger.columnIndex; var hdpiratio = grid.renderOverridesCache.dragger.hdpiratio; var draggerStartX; var floaterStartX; var fixedColumnCount = grid.getFixedColumnCount(); var draggerWidth = grid.getColumnWidth(draggerIndex); var floaterWidth = grid.getColumnWidth(floaterIndex); var max = grid.getVisibleColumnsCount(); var doffset = 0; var foffset = 0; if (draggerIndex >= fixedColumnCount) { doffset = scrollLeft; } if (floaterIndex >= fixedColumnCount) { foffset = scrollLeft; } if (draggedToTheRight) { draggerStartX = visibleColumns[Math.min(max, draggerIndex - doffset)].left; floaterStartX = visibleColumns[Math.min(max, floaterIndex - foffset)].left; grid.renderOverridesCache.dragger.startX = (draggerStartX + floaterWidth) * hdpiratio; grid.renderOverridesCache.floater.startX = draggerStartX * hdpiratio; } else { floaterStartX = visibleColumns[Math.min(max, floaterIndex - foffset)].left; draggerStartX = floaterStartX + draggerWidth; grid.renderOverridesCache.dragger.startX = floaterStartX * hdpiratio; grid.renderOverridesCache.floater.startX = draggerStartX * hdpiratio; } grid.swapColumns(draggerIndex, floaterIndex); grid.renderOverridesCache.dragger.columnIndex = floaterIndex; grid.renderOverridesCache.floater.columnIndex = draggerIndex; this.floaterAnimationQueue.unshift(this.doColumnMoveAnimation(grid, floaterStartX, draggerStartX)); this.doFloaterAnimation(grid); }, /** * @memberOf CellMoving.prototype * @desc manifest the column drag and drop animation * @param {Hypergrid} grid * @param {number} floaterStartX - the x start coordinate of the column underneath that floats behind the dragged column * @param {number} draggerStartX - the x start coordinate of the dragged column */ doColumnMoveAnimation: function(grid, floaterStartX, draggerStartX) { var self = this; return function() { var d = floatColumn; d.style.display = 'inline'; self.setCrossBrowserProperty(d, 'transform', translate(grid, floaterStartX, 0)); //d.style.webkit-webkit-Transform = 'translate(' + floaterStartX + 'px, ' + 0 + 'px)'; //d.style.webkit-webkit-Transform = 'translate(' + floaterStartX + 'px, ' + 0 + 'px)'; requestAnimationFrame(function() { self.setCrossBrowserProperty(d, 'transition', (self.isWebkit ? '-webkit-' : '') + 'transform ' + columnAnimationTime + 'ms ease'); self.setCrossBrowserProperty(d, 'transform', translate(grid, draggerStartX, -2)); }); grid.repaint(); //need to change this to key frames setTimeout(function() { self.setCrossBrowserProperty(d, 'transition', ''); grid.renderOverridesCache.floater = null; grid.repaint(); self.doFloaterAnimation(grid); requestAnimationFrame(function() { d.style.display = 'none'; self.isFloatingNow = false; }); }, columnAnimationTime + 50); }; }, /** * @memberOf CellMoving.prototype * @desc manifest the floater animation * @param {Hypergrid} grid */ doFloaterAnimation: function(grid) { if (this.floaterAnimationQueue.length === 0) { this.floatingNow = false; grid.repaint(); return; } var animation = this.floaterAnimationQueue.pop(); animation(); }, /** * @memberOf CellMoving.prototype * @desc create the float column at columnIndex underneath the dragged column * @param {Hypergrid} grid * @param {number} columnIndex - the index of the column that will be floating */ createFloatColumn: function(grid, columnIndex) { var fixedColumnCount = grid.getFixedColumnCount(); var scrollLeft = grid.getHScrollValue(); if (columnIndex < fixedColumnCount) { scrollLeft = 0; } var columnWidth = grid.getColumnWidth(columnIndex); var colHeight = grid.canvas.canvas.clientHeight; var d = floatColumn; var style = d.style; var location = grid.div.getBoundingClientRect(); style.top = (location.top - 2) + 'px'; style.left = location.left + 'px'; var hdpiRatio = grid.getHiDPI(); d.setAttribute('width', Math.round(columnWidth * hdpiRatio)); d.setAttribute('height', Math.round(colHeight * hdpiRatio)); style.boxShadow = '0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23)'; var zoomFactor = grid.isIE11 ? grid.getBodyZoomFactor() : 1; style.width = columnWidth * zoomFactor + 'px'; //Math.round(columnWidth / hdpiRatio) + 'px'; style.height = colHeight * zoomFactor + 'px'; //Math.round(colHeight / hdpiRatio) + 'px'; style.borderTop = '1px solid ' + grid.properties.lineColor; style.backgroundColor = grid.properties.backgroundColor; var startX = grid.renderer.visibleColumns[columnIndex - scrollLeft].left * hdpiRatio; floatColumnCTX.scale(hdpiRatio, hdpiRatio); grid.renderOverridesCache.floater = { columnIndex: columnIndex, ctx: floatColumnCTX, startX: startX, width: columnWidth, height: colHeight, hdpiratio: hdpiRatio }; style.zIndex = '4'; this.setCrossBrowserProperty(d, 'transform', translate(grid, startX, -2)); GRABBING.forEach(setName, style); grid.repaint(); }, /** * @memberOf CellMoving.prototype * @desc utility function for setting cross browser css properties * @param {HTMLElement} element - descripton * @param {string} property - the property * @param {string} value - the value to assign */ setCrossBrowserProperty: function(element, property, value) { var uProperty = property[0].toUpperCase() + property.substr(1); this.setProp(element, 'webkit' + uProperty, value); this.setProp(element, 'Moz' + uProperty, value); this.setProp(element, 'ms' + uProperty, value); this.setProp(element, 'O' + uProperty, value); this.setProp(element, property, value); }, /** * @memberOf CellMoving.prototype * @desc utility function for setting properties on HTMLElements * @param {HTMLElement} element - descripton * @param {string} property - the property * @param {string} value - the value to assign */ setProp: function(element, property, value) { if (property in element.style) { element.style[property] = value; } }, /** * @memberOf CellMoving.prototype * @desc create the dragged column at columnIndex above the floated column * @param {Hypergrid} grid * @param {number} x - the start position * @param {number} columnIndex - the index of the column that will be floating */ createDragColumn: function(grid, x, columnIndex) { var fixedColumnCount = grid.getFixedColumnCount(); var scrollLeft = grid.getHScrollValue(); if (columnIndex < fixedColumnCount) { scrollLeft = 0; } var hdpiRatio = grid.getHiDPI(); var columnWidth = grid.getColumnWidth(columnIndex); var colHeight = grid.canvas.canvas.clientHeight; var d = dragger; var location = grid.div.getBoundingClientRect(); var style = d.style; style.top = location.top + 'px'; style.left = location.left + 'px'; style.opacity = 0.85; style.boxShadow = '0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22)'; //style.zIndex = 100; style.borderTop = '1px solid ' + grid.properties.lineColor; style.backgroundColor = grid.properties.backgroundColor; d.setAttribute('width', Math.round(columnWidth * hdpiRatio)); d.setAttribute('height', Math.round(colHeight * hdpiRatio)); var zoomFactor = grid.isIE11 ? grid.getBodyZoomFactor() : 1; style.width = columnWidth * zoomFactor + 'px'; //Math.round(columnWidth / hdpiRatio) + 'px'; style.height = colHeight * zoomFactor + 'px'; //Math.round(colHeight / hdpiRatio) + 'px'; var startX = grid.renderer.visibleColumns[columnIndex - scrollLeft].left * hdpiRatio; draggerCTX.scale(hdpiRatio, hdpiRatio); grid.renderOverridesCache.dragger = { columnIndex: columnIndex, startIndex: columnIndex, ctx: draggerCTX, startX: startX, width: columnWidth, height: colHeight, hdpiratio: hdpiRatio }; this.setCrossBrowserProperty(d, 'transform', translate(grid, x, -5)); style.zIndex = '5'; GRABBING.forEach(setName, style); grid.repaint(); }, /** * @memberOf CellMoving.prototype * @desc this function is the main dragging logic * @param {Hypergrid} grid * @param {number} x - the start position */ dragColumn: function(grid, x) { //TODO: this function is overly complex, refactor this in to something more reasonable var self = this; var autoScrollingNow = this.columnDragAutoScrollingRight || this.columnDragAutoScrollingLeft; var hdpiRatio = grid.getHiDPI(); var dragColumnIndex = grid.renderOverridesCache.dragger.columnIndex; var minX = 0; var maxX = grid.renderer.getFinalVisibleColumnBoundary(); x = Math.min(x, maxX + 15); x = Math.max(minX - 15, x); //am I at my lower bound var atMin = x < minX && dragColumnIndex !== 0; //am I at my upper bound var atMax = x > maxX; var d = dragger; this.setCrossBrowserProperty(d, 'transition', (self.isWebkit ? '-webkit-' : '') + 'transform ' + 0 + 'ms ease, box-shadow ' + columnAnimationTime + 'ms ease'); this.setCrossBrowserProperty(d, 'transform', translate(grid, x, -10)); requestAnimationFrame(function() { d.style.display = 'inline'; }); var overCol = grid.renderer.getColumnFromPixelX(x + (d.width / 2 / hdpiRatio)); if (atMin) { overCol = 0; } if (atMax) { overCol = grid.getColumnCount() - 1; } var doAFloat = dragColumnIndex > overCol; doAFloat = doAFloat || (overCol - dragColumnIndex >= 1); if (doAFloat && !autoScrollingNow) { var draggedToTheRight = dragColumnIndex < overCol; // if (draggedToTheRight) { // overCol -= 1; // } if (this.isFloatingNow) { return; } this.isFloatingNow = true; this.createFloatColumn(grid, overCol); this.floatColumnTo(grid, draggedToTheRight); } else { if (x < minX - 10) { this.checkAutoScrollToLeft(grid, x); } if (x > minX - 10) { this.columnDragAutoScrollingLeft = false; } //lets check for autoscroll to right if were up against it if (atMax || x > maxX + 10) { this.checkAutoScrollToRight(grid, x); return; } if (x < maxX + 10) { this.columnDragAutoScrollingRight = false; } } }, /** * @memberOf CellMoving.prototype * @desc autoscroll to the right if necessary * @param {Hypergrid} grid * @param {number} x - the start position */ checkAutoScrollToRight: function(grid, x) { if (this.columnDragAutoScrollingRight) { return; } this.columnDragAutoScrollingRight = true; this._checkAutoScrollToRight(grid, x); }, _checkAutoScrollToRight: function(grid, x) { if (!this.columnDragAutoScrollingRight) { return; } var scrollLeft = grid.getHScrollValue(); if (!grid.dragging || scrollLeft > (grid.sbHScroller.range.max - 2)) { return; } var draggedIndex = grid.renderOverridesCache.dragger.columnIndex; grid.scrollBy(1, 0); var newIndex = draggedIndex + 1; grid.swapColumns(newIndex, draggedIndex); grid.renderOverridesCache.dragger.columnIndex = newIndex; setTimeout(this._checkAutoScrollToRight.bind(this, grid, x), 250); }, /** * @memberOf CellMoving.prototype * @desc autoscroll to the left if necessary * @param {Hypergrid} grid * @param {number} x - the start position */ checkAutoScrollToLeft: function(grid, x) { if (this.columnDragAutoScrollingLeft) { return; } this.columnDragAutoScrollingLeft = true; this._checkAutoScrollToLeft(grid, x); }, _checkAutoScrollToLeft: function(grid, x) { if (!this.columnDragAutoScrollingLeft) { return; } var scrollLeft = grid.getHScrollValue(); if (!grid.dragging || scrollLeft < 1) { return; } var draggedIndex = grid.renderOverridesCache.dragger.columnIndex; grid.swapColumns(draggedIndex + scrollLeft, draggedIndex + scrollLeft - 1); grid.scrollBy(-1, 0); setTimeout(this._checkAutoScrollToLeft.bind(this, grid, x), 250); }, /** * @memberOf CellMoving.prototype * @desc a column drag has completed, update data and cleanup * @param {Hypergrid} grid */ endDragColumn: function(grid) { var fixedColumnCount = grid.getFixedColumnCount(); var scrollLeft = grid.getHScrollValue(); var columnIndex = grid.renderOverridesCache.dragger.columnIndex; if (columnIndex < fixedColumnCount) { scrollLeft = 0; } var self = this; var startX = grid.renderer.visibleColumns[columnIndex - scrollLeft].left; var d = dragger; var changed = grid.renderOverridesCache.dragger.startIndex !== grid.renderOverridesCache.dragger.columnIndex; self.setCrossBrowserProperty(d, 'transition', (self.isWebkit ? '-webkit-' : '') + 'transform ' + columnAnimationTime + 'ms ease, box-shadow ' + columnAnimationTime + 'ms ease'); self.setCrossBrowserProperty(d, 'transform', translate(grid, startX, -1)); d.style.boxShadow = '0px 0px 0px #888888'; setTimeout(function() { grid.renderOverridesCache.dragger = null; grid.repaint(); requestAnimationFrame(function() { d.style.display = 'none'; grid.endDragColumnNotification(); //internal notification if (changed){ grid.fireSyntheticOnColumnsChangedEvent(); //public notification } }); }, columnAnimationTime + 50); } }); module.exports = ColumnMoving;