"use strict"; exports.__esModule = true; require("core-js/modules/es.array.push.js"); var _base = require("../base"); var _pluginHooks = _interopRequireDefault(require("../../pluginHooks")); var _array = require("../../helpers/array"); var _element = require("../../helpers/dom/element"); var _number = require("../../helpers/number"); var _eventManager = _interopRequireDefault(require("../../eventManager")); var _backlight = _interopRequireDefault(require("./ui/backlight")); var _guideline = _interopRequireDefault(require("./ui/guideline")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } _pluginHooks.default.getSingleton().register('beforeRowMove'); _pluginHooks.default.getSingleton().register('afterRowMove'); const PLUGIN_KEY = 'manualRowMove'; exports.PLUGIN_KEY = PLUGIN_KEY; const PLUGIN_PRIORITY = 140; exports.PLUGIN_PRIORITY = PLUGIN_PRIORITY; const privatePool = new WeakMap(); const CSS_PLUGIN = 'ht__manualRowMove'; const CSS_SHOW_UI = 'show-ui'; const CSS_ON_MOVING = 'on-moving--rows'; const CSS_AFTER_SELECTION = 'after-selection--rows'; /* eslint-disable jsdoc/require-description-complete-sentence */ /** * @plugin ManualRowMove * @class ManualRowMove * * @description * This plugin allows to change rows order. To make rows order persistent the {@link Options#persistentState} * plugin should be enabled. * * API: * - `moveRow` - move single row to the new position. * - `moveRows` - move many rows (as an array of indexes) to the new position. * - `dragRow` - drag single row to the new position. * - `dragRows` - drag many rows (as an array of indexes) to the new position. * * [Documentation](@/guides/rows/row-moving.md) explain differences between drag and move actions. Please keep in mind that if you want apply visual changes, * you have to call manually the `render` method on the instance of Handsontable. * * The plugin creates additional components to make moving possibly using user interface: * - backlight - highlight of selected rows. * - guideline - line which shows where rows has been moved. * * @class ManualRowMove * @plugin ManualRowMove */ class ManualRowMove extends _base.BasePlugin { static get PLUGIN_KEY() { return PLUGIN_KEY; } static get PLUGIN_PRIORITY() { return PLUGIN_PRIORITY; } constructor(hotInstance) { super(hotInstance); /** * Set up WeakMap of plugin to sharing private parameters;. */ privatePool.set(this, { rowsToMove: [], pressed: void 0, target: { eventPageY: void 0, coords: void 0, TD: void 0, row: void 0 }, cachedDropIndex: void 0 }); /** * Event Manager object. * * @private * @type {object} */ this.eventManager = new _eventManager.default(this); /** * Backlight UI object. * * @private * @type {object} */ this.backlight = new _backlight.default(hotInstance); /** * Guideline UI object. * * @private * @type {object} */ this.guideline = new _guideline.default(hotInstance); } /** * Checks if the plugin is enabled in the handsontable settings. This method is executed in {@link Hooks#beforeInit} * hook and if it returns `true` then the {@link ManualRowMove#enablePlugin} method is called. * * @returns {boolean} */ isEnabled() { return !!this.hot.getSettings()[PLUGIN_KEY]; } /** * Enables the plugin functionality for this Handsontable instance. */ enablePlugin() { var _this = this; if (this.enabled) { return; } this.addHook('beforeOnCellMouseDown', function () { return _this.onBeforeOnCellMouseDown(...arguments); }); this.addHook('beforeOnCellMouseOver', function () { return _this.onBeforeOnCellMouseOver(...arguments); }); this.addHook('afterScrollHorizontally', () => this.onAfterScrollHorizontally()); this.addHook('afterLoadData', function () { return _this.onAfterLoadData(...arguments); }); this.buildPluginUI(); this.registerEvents(); // TODO: move adding plugin classname to BasePlugin. (0, _element.addClass)(this.hot.rootElement, CSS_PLUGIN); super.enablePlugin(); } /** * Updates the plugin's state. * * This method is executed when [`updateSettings()`](@/api/core.md#updatesettings) is invoked with any of the following configuration options: * - [`manualRowMove`](@/api/options.md#manualrowmove) */ updatePlugin() { this.disablePlugin(); this.enablePlugin(); this.moveBySettingsOrLoad(); super.updatePlugin(); } /** * Disables the plugin functionality for this Handsontable instance. */ disablePlugin() { (0, _element.removeClass)(this.hot.rootElement, CSS_PLUGIN); this.unregisterEvents(); this.backlight.destroy(); this.guideline.destroy(); super.disablePlugin(); } /** * Moves a single row. * * To see the outcome, rerender your grid by calling [`render()`](@/api/core.md#render). * * @param {number} row Visual row index to be moved. * @param {number} finalIndex Visual row index, being a start index for the moved rows. Points to where the elements will be placed after the moving action. * To check the visualization of the final index, please take a look at [documentation](@/guides/rows/row-moving.md#drag-and-move-actions-of-manualrowmove-plugin). * @fires Hooks#beforeRowMove * @fires Hooks#afterRowMove * @returns {boolean} */ moveRow(row, finalIndex) { return this.moveRows([row], finalIndex); } /** * Moves multiple rows. * * To see the outcome, rerender your grid by calling [`render()`](@/api/core.md#render). * * @param {Array} rows Array of visual row indexes to be moved. * @param {number} finalIndex Visual row index, being a start index for the moved rows. Points to where the elements will be placed after the moving action. * To check the visualization of the final index, please take a look at [documentation](@/guides/rows/row-moving.md#drag-and-move-actions-of-manualrowmove-plugin). * @fires Hooks#beforeRowMove * @fires Hooks#afterRowMove * @returns {boolean} */ moveRows(rows, finalIndex) { const priv = privatePool.get(this); const dropIndex = priv.cachedDropIndex; const movePossible = this.isMovePossible(rows, finalIndex); const beforeMoveHook = this.hot.runHooks('beforeRowMove', rows, finalIndex, dropIndex, movePossible); priv.cachedDropIndex = void 0; if (beforeMoveHook === false) { return; } if (movePossible) { this.hot.rowIndexMapper.moveIndexes(rows, finalIndex); } const movePerformed = movePossible && this.isRowOrderChanged(rows, finalIndex); this.hot.runHooks('afterRowMove', rows, finalIndex, dropIndex, movePossible, movePerformed); return movePerformed; } /** * Drag a single row to drop index position. * * @param {number} row Visual row index to be dragged. * @param {number} dropIndex Visual row index, being a drop index for the moved rows. Points to where we are going to drop the moved elements. * To check visualization of drop index please take a look at [documentation](@/guides/rows/row-moving.md#drag-and-move-actions-of-manualrowmove-plugin). * @fires Hooks#beforeRowMove * @fires Hooks#afterRowMove * @returns {boolean} */ dragRow(row, dropIndex) { return this.dragRows([row], dropIndex); } /** * Drag multiple rows to drop index position. * * @param {Array} rows Array of visual row indexes to be dragged. * @param {number} dropIndex Visual row index, being a drop index for the moved rows. Points to where we are going to drop the moved elements. * To check visualization of drop index please take a look at [documentation](@/guides/rows/row-moving.md#drag-and-move-actions-of-manualrowmove-plugin). * @fires Hooks#beforeRowMove * @fires Hooks#afterRowMove * @returns {boolean} */ dragRows(rows, dropIndex) { const finalIndex = this.countFinalIndex(rows, dropIndex); const priv = privatePool.get(this); priv.cachedDropIndex = dropIndex; return this.moveRows(rows, finalIndex); } /** * Indicates if it's possible to move rows to the desired position. Some of the actions aren't possible, i.e. You can’t move more than one element to the last position. * * @param {Array} movedRows Array of visual row indexes to be moved. * @param {number} finalIndex Visual row index, being a start index for the moved rows. Points to where the elements will be placed after the moving action. * To check the visualization of the final index, please take a look at [documentation](@/guides/rows/row-moving.md#drag-and-move-actions-of-manualrowmove-plugin). * @returns {boolean} */ isMovePossible(movedRows, finalIndex) { const length = this.hot.rowIndexMapper.getNotTrimmedIndexesLength(); // An attempt to transfer more rows to start destination than is possible (only when moving from the top to the bottom). const tooHighDestinationIndex = movedRows.length + finalIndex > length; const tooLowDestinationIndex = finalIndex < 0; const tooLowMovedRowIndex = movedRows.some(movedRow => movedRow < 0); const tooHighMovedRowIndex = movedRows.some(movedRow => movedRow >= length); if (tooHighDestinationIndex || tooLowDestinationIndex || tooLowMovedRowIndex || tooHighMovedRowIndex) { return false; } return true; } /** * Indicates if order of rows was changed. * * @private * @param {Array} movedRows Array of visual row indexes to be moved. * @param {number} finalIndex Visual row index, being a start index for the moved rows. Points to where the elements will be placed after the moving action. * To check the visualization of the final index, please take a look at [documentation](@/guides/rows/row-moving.md#drag-and-move-actions-of-manualrowmove-plugin). * @returns {boolean} */ isRowOrderChanged(movedRows, finalIndex) { return movedRows.some((row, nrOfMovedElement) => row - nrOfMovedElement !== finalIndex); } /** * Count the final row index from the drop index. * * @private * @param {Array} movedRows Array of visual row indexes to be moved. * @param {number} dropIndex Visual row index, being a drop index for the moved rows. * @returns {number} Visual row index, being a start index for the moved rows. */ countFinalIndex(movedRows, dropIndex) { const numberOfRowsLowerThanDropIndex = (0, _array.arrayReduce)(movedRows, (numberOfRows, currentRowIndex) => { if (currentRowIndex < dropIndex) { numberOfRows += 1; } return numberOfRows; }, 0); return dropIndex - numberOfRowsLowerThanDropIndex; } /** * Gets the sum of the heights of rows in the provided range. * * @private * @param {number} fromRow Visual row index. * @param {number} toRow Visual row index. * @returns {number} */ getRowsHeight(fromRow, toRow) { const rowMapper = this.hot.rowIndexMapper; let rowsHeight = 0; for (let visualRowIndex = fromRow; visualRowIndex <= toRow; visualRowIndex++) { const renderableIndex = rowMapper.getRenderableFromVisualIndex(visualRowIndex); if (renderableIndex !== null) { rowsHeight += this.hot.view._wt.wtTable.getRowHeight(renderableIndex) || 23; } } return rowsHeight; } /** * Loads initial settings when persistent state is saved or when plugin was initialized as an array. * * @private */ moveBySettingsOrLoad() { const pluginSettings = this.hot.getSettings()[PLUGIN_KEY]; if (Array.isArray(pluginSettings)) { this.moveRows(pluginSettings, 0); } else if (pluginSettings !== void 0) { const persistentState = this.persistentStateLoad(); if (persistentState.length) { this.moveRows(persistentState, 0); } } } /** * Checks if the provided row is in the fixedRowsTop section. * * @private * @param {number} row Visual row index to check. * @returns {boolean} */ isFixedRowTop(row) { return row < this.hot.getSettings().fixedRowsTop; } /** * Checks if the provided row is in the fixedRowsBottom section. * * @private * @param {number} row Visual row index to check. * @returns {boolean} */ isFixedRowBottom(row) { return row > this.hot.countRows() - 1 - this.hot.getSettings().fixedRowsBottom; } /** * Saves the manual row positions to the persistent state (the {@link Options#persistentState} option has to be enabled). * * @private * @fires Hooks#persistentStateSave */ persistentStateSave() { // The `PersistentState` plugin should be refactored. this.hot.runHooks('persistentStateSave', 'manualRowMove', this.hot.rowIndexMapper.getIndexesSequence()); } /** * Loads the manual row positions from the persistent state (the {@link Options#persistentState} option has to be enabled). * * @private * @fires Hooks#persistentStateLoad * @returns {Array} Stored state. */ persistentStateLoad() { const storedState = {}; this.hot.runHooks('persistentStateLoad', 'manualRowMove', storedState); return storedState.value ? storedState.value : []; } /** * Prepares an array of indexes based on actual selection. * * @private * @returns {Array} */ prepareRowsToMoving() { const selection = this.hot.getSelectedRangeLast(); const selectedRows = []; if (!selection) { return selectedRows; } const { from, to } = selection; const start = Math.min(from.row, to.row); const end = Math.max(from.row, to.row); (0, _number.rangeEach)(start, end, i => { selectedRows.push(i); }); return selectedRows; } /** * Update the UI visual position. * * @private */ refreshPositions() { const priv = privatePool.get(this); const coords = priv.target.coords; const firstVisible = this.hot.view.getFirstFullyVisibleRow(); const lastVisible = this.hot.view.getLastFullyVisibleRow(); const countRows = this.hot.countRows(); if (this.isFixedRowTop(coords.row) && firstVisible > 0) { this.hot.scrollViewportTo(this.hot.rowIndexMapper.getNearestNotHiddenIndex(firstVisible - 1, -1)); } if (this.isFixedRowBottom(coords.row) && lastVisible < countRows) { this.hot.scrollViewportTo(this.hot.rowIndexMapper.getNearestNotHiddenIndex(lastVisible + 1, 1), undefined, true); } const wtTable = this.hot.view._wt.wtTable; const TD = priv.target.TD; const rootElement = this.hot.rootElement; const rootElementOffset = (0, _element.offset)(rootElement); const trimmingContainer = (0, _element.getTrimmingContainer)(rootElement); const tableScroll = wtTable.holder.scrollTop; let trimmingContainerScroll; // Trimming container is the `window` element. if (this.hot.rootWindow === trimmingContainer) { trimmingContainerScroll = trimmingContainer.scrollY; } else { trimmingContainerScroll = trimmingContainer.scrollTop; } const pixelsAbove = rootElementOffset.top - trimmingContainerScroll; const pixelsRelToTableStart = priv.target.eventPageY - pixelsAbove + tableScroll; const hiderHeight = wtTable.hider.offsetHeight; const tbodyOffsetTop = wtTable.TBODY.offsetTop; const backlightElemMarginTop = this.backlight.getOffset().top; const backlightElemHeight = this.backlight.getSize().height; const tdMiddle = TD.offsetHeight / 2; const tdHeight = TD.offsetHeight; let tdStartPixel = this.hot.view.THEAD.offsetHeight + this.getRowsHeight(0, coords.row - 1); const isBelowTable = pixelsRelToTableStart >= tdStartPixel + tdMiddle; if (this.isFixedRowTop(coords.row)) { tdStartPixel += wtTable.holder.scrollTop; } if (coords.row < 0) { // if hover on colHeader priv.target.row = firstVisible > 0 ? firstVisible - 1 : firstVisible; } else if (isBelowTable) { // if hover on lower part of TD priv.target.row = coords.row + 1; // unfortunately first row is bigger than rest tdStartPixel += coords.row === 0 ? tdHeight - 1 : tdHeight; } else { // elsewhere on table priv.target.row = coords.row; } let backlightTop = pixelsRelToTableStart; let guidelineTop = tdStartPixel; if (pixelsRelToTableStart + backlightElemHeight + backlightElemMarginTop >= hiderHeight) { // prevent display backlight below table backlightTop = hiderHeight - backlightElemHeight - backlightElemMarginTop; } else if (pixelsRelToTableStart + backlightElemMarginTop < tbodyOffsetTop) { // prevent display above below table backlightTop = tbodyOffsetTop + Math.abs(backlightElemMarginTop); } if (tdStartPixel >= hiderHeight - 1) { // prevent display guideline below table guidelineTop = hiderHeight - 1; } this.backlight.setPosition(backlightTop); this.guideline.setPosition(guidelineTop); } /** * Binds the events used by the plugin. * * @private */ registerEvents() { const { documentElement } = this.hot.rootDocument; this.eventManager.addEventListener(documentElement, 'mousemove', event => this.onMouseMove(event)); this.eventManager.addEventListener(documentElement, 'mouseup', () => this.onMouseUp()); } /** * Unbinds the events used by the plugin. * * @private */ unregisterEvents() { this.eventManager.clear(); } /** * Change the behavior of selection / dragging. * * @private * @param {MouseEvent} event `mousedown` event properties. * @param {CellCoords} coords Visual cell coordinates where was fired event. * @param {HTMLElement} TD Cell represented as HTMLElement. * @param {object} controller An object with properties `row`, `column` and `cell`. Each property contains * a boolean value that allows or disallows changing the selection for that particular area. */ onBeforeOnCellMouseDown(event, coords, TD, controller) { const { wtTable, wtViewport } = this.hot.view._wt; const isHeaderSelection = this.hot.selection.isSelectedByRowHeader(); const selection = this.hot.getSelectedRangeLast(); const priv = privatePool.get(this); if (!selection || !isHeaderSelection || priv.pressed || event.button !== 0) { priv.pressed = false; priv.rowsToMove.length = 0; (0, _element.removeClass)(this.hot.rootElement, [CSS_ON_MOVING, CSS_SHOW_UI]); return; } const guidelineIsNotReady = this.guideline.isBuilt() && !this.guideline.isAppended(); const backlightIsNotReady = this.backlight.isBuilt() && !this.backlight.isAppended(); if (guidelineIsNotReady && backlightIsNotReady) { this.guideline.appendTo(wtTable.hider); this.backlight.appendTo(wtTable.hider); } const { from, to } = selection; const start = Math.min(from.row, to.row); const end = Math.max(from.row, to.row); if (coords.col < 0 && coords.row >= start && coords.row <= end) { controller.row = true; priv.pressed = true; priv.target.eventPageY = event.pageY; priv.target.coords = coords; priv.target.TD = TD; priv.rowsToMove = this.prepareRowsToMoving(); const leftPos = wtTable.holder.scrollLeft + wtViewport.getRowHeaderWidth(); this.backlight.setPosition(null, leftPos); this.backlight.setSize(wtTable.hider.offsetWidth - leftPos, this.getRowsHeight(start, end)); this.backlight.setOffset((this.getRowsHeight(start, coords.row - 1) + event.offsetY) * -1, null); (0, _element.addClass)(this.hot.rootElement, CSS_ON_MOVING); this.refreshPositions(); } else { (0, _element.removeClass)(this.hot.rootElement, CSS_AFTER_SELECTION); priv.pressed = false; priv.rowsToMove.length = 0; } } /** * 'mouseMove' event callback. Fired when pointer move on document.documentElement. * * @private * @param {MouseEvent} event `mousemove` event properties. */ onMouseMove(event) { const priv = privatePool.get(this); if (!priv.pressed) { return; } // callback for browser which doesn't supports CSS pointer-event: none if (event.target === this.backlight.element) { const height = this.backlight.getSize().height; this.backlight.setSize(null, 0); setTimeout(function () { this.backlight.setPosition(null, height); }); } priv.target.eventPageY = event.pageY; this.refreshPositions(); } /** * 'beforeOnCellMouseOver' hook callback. Fired when pointer was over cell. * * @private * @param {MouseEvent} event `mouseover` event properties. * @param {CellCoords} coords Visual cell coordinates where was fired event. * @param {HTMLElement} TD Cell represented as HTMLElement. * @param {object} controller An object with properties `row`, `column` and `cell`. Each property contains * a boolean value that allows or disallows changing the selection for that particular area. */ onBeforeOnCellMouseOver(event, coords, TD, controller) { const selectedRange = this.hot.getSelectedRangeLast(); const priv = privatePool.get(this); if (!selectedRange || !priv.pressed) { return; } if (priv.rowsToMove.indexOf(coords.row) > -1) { (0, _element.removeClass)(this.hot.rootElement, CSS_SHOW_UI); } else { (0, _element.addClass)(this.hot.rootElement, CSS_SHOW_UI); } controller.row = true; controller.column = true; controller.cell = true; priv.target.coords = coords; priv.target.TD = TD; } /** * `onMouseUp` hook callback. * * @private */ onMouseUp() { const priv = privatePool.get(this); const target = priv.target.row; const rowsLen = priv.rowsToMove.length; priv.pressed = false; priv.backlightHeight = 0; (0, _element.removeClass)(this.hot.rootElement, [CSS_ON_MOVING, CSS_SHOW_UI, CSS_AFTER_SELECTION]); if (this.hot.selection.isSelectedByRowHeader()) { (0, _element.addClass)(this.hot.rootElement, CSS_AFTER_SELECTION); } if (rowsLen < 1 || target === void 0) { return; } const firstMovedVisualRow = priv.rowsToMove[0]; const firstMovedPhysicalRow = this.hot.toPhysicalRow(firstMovedVisualRow); const movePerformed = this.dragRows(priv.rowsToMove, target); priv.rowsToMove.length = 0; if (movePerformed === true) { this.persistentStateSave(); this.hot.render(); this.hot.view.adjustElementsSize(true); const selectionStart = this.hot.toVisualRow(firstMovedPhysicalRow); const selectionEnd = selectionStart + rowsLen - 1; this.hot.selectRows(selectionStart, selectionEnd); } } /** * `afterScrollHorizontally` hook callback. Fired the table was scrolled horizontally. * * @private */ onAfterScrollHorizontally() { const wtTable = this.hot.view._wt.wtTable; const headerWidth = this.hot.view._wt.wtViewport.getRowHeaderWidth(); const scrollLeft = wtTable.holder.scrollLeft; const posLeft = headerWidth + scrollLeft; this.backlight.setPosition(null, posLeft); this.backlight.setSize(wtTable.hider.offsetWidth - posLeft); } /** * Builds the plugin's UI. * * @private */ buildPluginUI() { this.backlight.build(); this.guideline.build(); } /** * Callback for the `afterLoadData` hook. * * @private */ onAfterLoadData() { this.moveBySettingsOrLoad(); } /** * Destroys the plugin instance. */ destroy() { this.backlight.destroy(); this.guideline.destroy(); super.destroy(); } } exports.ManualRowMove = ManualRowMove;