UNPKG

handsontable

Version:

Handsontable is a JavaScript Data Grid available for React, Angular and Vue.

599 lines (574 loc) • 22.4 kB
"use strict"; exports.__esModule = true; require("core-js/modules/es.error.cause.js"); require("core-js/modules/es.array.push.js"); require("core-js/modules/esnext.iterator.constructor.js"); require("core-js/modules/esnext.iterator.some.js"); var _base = require("../base"); var _hooks = require("../../core/hooks"); var _element = require("../../helpers/dom/element"); var _object = require("../../helpers/object"); var _array = require("../../helpers/array"); var _mixed = require("../../helpers/mixed"); var _utils = require("./utils"); function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); } function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } function _assertClassBrand(e, t, n) { if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; throw new TypeError("Private element is not present on this object"); } _hooks.Hooks.getSingleton().register('modifyAutofillRange'); _hooks.Hooks.getSingleton().register('beforeAutofill'); _hooks.Hooks.getSingleton().register('afterAutofill'); const PLUGIN_KEY = exports.PLUGIN_KEY = 'autofill'; const PLUGIN_PRIORITY = exports.PLUGIN_PRIORITY = 20; const SETTING_KEYS = ['fillHandle']; const INSERT_ROW_ALTER_ACTION_NAME = 'insert_row_below'; const INTERVAL_FOR_ADDING_ROW = 200; /* eslint-disable jsdoc/require-description-complete-sentence */ /** * This plugin provides "drag-down" and "copy-down" functionalities, both operated using the small square in the right * bottom of the cell selection. * * "Drag-down" expands the value of the selected cells to the neighbouring cells when you drag the small * square in the corner. * * "Copy-down" copies the value of the selection to all empty cells below when you double click the small square. * * @class Autofill * @plugin Autofill */ var _Autofill_brand = /*#__PURE__*/new WeakSet(); class Autofill extends _base.BasePlugin { constructor() { super(...arguments); /** * Extends the fill data with the source data based on the content of the target cells. * * @param {Array} fillData The fill data to extend. * @param {Array} selectionSourceData The source data to extend the fill data with. * @param {CellCoords} startOfDragCoords The start of the drag area. * @param {CellCoords} endOfDragCoords The end of the drag area. * @returns {Array} The extended fill data. */ _classPrivateMethodInitSpec(this, _Autofill_brand); /** * Specifies if adding new row started. * * @private * @type {boolean} */ _defineProperty(this, "addingStarted", false); /** * Specifies if there was mouse down on the cell corner. * * @private * @type {boolean} */ _defineProperty(this, "mouseDownOnCellCorner", false); /** * Specifies if mouse was dragged outside Handsontable. * * @private * @type {boolean} */ _defineProperty(this, "mouseDragOutside", false); /** * Specifies how many cell levels were dragged using the handle. * * @private * @type {boolean} */ _defineProperty(this, "handleDraggedCells", 0); /** * Specifies allowed directions of drag (`'horizontal'` or '`vertical`'). * * @private * @type {string[]} */ _defineProperty(this, "directions", []); /** * Specifies if can insert new rows if needed. * * @private * @type {boolean} */ _defineProperty(this, "autoInsertRow", false); } static get PLUGIN_KEY() { return PLUGIN_KEY; } static get PLUGIN_PRIORITY() { return PLUGIN_PRIORITY; } static get SETTING_KEYS() { return [PLUGIN_KEY, ...SETTING_KEYS]; } /** * Checks if the plugin is enabled in the Handsontable settings. * * @returns {boolean} */ isEnabled() { return this.hot.getSettings().fillHandle; } /** * Enables the plugin functionality for this Handsontable instance. */ enablePlugin() { if (this.enabled) { return; } this.mapSettings(); this.registerEvents(); this.addHook('afterOnCellCornerMouseDown', event => _assertClassBrand(_Autofill_brand, this, _onAfterCellCornerMouseDown).call(this, event)); this.addHook('afterOnCellCornerDblClick', event => _assertClassBrand(_Autofill_brand, this, _onCellCornerDblClick).call(this, event)); this.addHook('beforeOnCellMouseOver', (_, coords) => _assertClassBrand(_Autofill_brand, this, _onBeforeCellMouseOver).call(this, coords)); 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: * - `autofill` * - [`fillHandle`](@/api/options.md#fillhandle) */ updatePlugin() { this.disablePlugin(); this.enablePlugin(); super.updatePlugin(); } /** * Disables the plugin functionality for this Handsontable instance. */ disablePlugin() { this.clearMappedSettings(); super.disablePlugin(); } /** * Gets selection data. * * @private * @param {boolean} [useSource=false] If `true`, returns copyable source data instead of copyable data. * @returns {object[]} Ranges Array of objects with properties `startRow`, `startCol`, `endRow` and `endCol`. */ getSelectionData() { let useSource = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; const selection = this.hot.getSelectedRangeLast(); const { row: startRow, col: startCol } = selection.getTopStartCorner(); const { row: endRow, col: endCol } = selection.getBottomEndCorner(); const copyableRanges = this.hot.runHooks('modifyCopyableRange', [{ startRow, startCol, endRow, endCol }]); const copyableRows = []; const copyableColumns = []; const data = []; (0, _array.arrayEach)(copyableRanges, range => { for (let visualRow = range.startRow; visualRow <= range.endRow; visualRow += 1) { if (copyableRows.indexOf(visualRow) === -1) { copyableRows.push(visualRow); } } for (let visualColumn = range.startCol; visualColumn <= range.endCol; visualColumn += 1) { if (copyableColumns.indexOf(visualColumn) === -1) { copyableColumns.push(visualColumn); } } }); (0, _array.arrayEach)(copyableRows, row => { const rowSet = []; (0, _array.arrayEach)(copyableColumns, column => { const sourceDataAtSource = useSource ? this.hot.getSourceDataAtCell(row, column) : null; if (useSource && (0, _object.isObject)(sourceDataAtSource)) { rowSet.push(this.hot.getCopyableSourceData(row, column)); } else { rowSet.push(this.hot.getCopyableData(row, column)); } }); data.push(rowSet); }); return data; } /** * Try to apply fill values to the area in fill border, omitting the selection border. * * @private * @returns {boolean} Reports if fill was applied. * * @fires Hooks#modifyAutofillRange * @fires Hooks#beforeAutofill * @fires Hooks#afterAutofill */ fillIn() { if (this.hot.selection.highlight.getFill().isEmpty()) { return false; } // Fill area may starts or ends with invisible cell. There won't be any information about it as highlighted // selection store just renderable indexes (It's part of Walkontable). I extrapolate where the start or/and // the end is. const [fillStartRow, fillStartColumn, fillEndRow, fillEndColumn] = this.hot.selection.highlight.getFill().getVisualCorners(); const selectionRangeLast = this.hot.getSelectedRangeLast(); const topStartCorner = selectionRangeLast.getTopStartCorner(); const bottomEndCorner = selectionRangeLast.getBottomEndCorner(); this.resetSelectionOfDraggedArea(); const cornersOfSelectedCells = [topStartCorner.row, topStartCorner.col, bottomEndCorner.row, bottomEndCorner.col]; const cornersOfSelectionAndDragAreas = this.hot.runHooks('modifyAutofillRange', [Math.min(topStartCorner.row, fillStartRow), Math.min(topStartCorner.col, fillStartColumn), Math.max(bottomEndCorner.row, fillEndRow), Math.max(bottomEndCorner.col, fillEndColumn)], cornersOfSelectedCells); const { directionOfDrag, startOfDragCoords, endOfDragCoords } = (0, _utils.getDragDirectionAndRange)(cornersOfSelectedCells, cornersOfSelectionAndDragAreas, (row, column) => this.hot._createCellCoords(row, column)); if (startOfDragCoords && startOfDragCoords.row > -1 && startOfDragCoords.col > -1) { const selectionData = this.getSelectionData(); const selectionSourceData = this.getSelectionData(true); const sourceRange = selectionRangeLast.clone(); const targetRange = this.hot._createCellRange(startOfDragCoords, startOfDragCoords, endOfDragCoords); const beforeAutofillHookResult = this.hot.runHooks('beforeAutofill', selectionData, sourceRange, targetRange, directionOfDrag); if (beforeAutofillHookResult === false) { this.hot.selection.highlight.getFill().clear(); this.hot.render(); return false; } let fillData = beforeAutofillHookResult; const res = beforeAutofillHookResult; if (['up', 'left'].indexOf(directionOfDrag) > -1 && !(res.length === 1 && res[0].length === 0)) { fillData = []; if (directionOfDrag === 'up') { const dragLength = endOfDragCoords.row - startOfDragCoords.row + 1; const fillOffset = dragLength % res.length; for (let i = 0; i < dragLength; i++) { fillData.push(res[(i + (res.length - fillOffset)) % res.length]); } } else { const dragLength = endOfDragCoords.col - startOfDragCoords.col + 1; const fillOffset = dragLength % res[0].length; for (let i = 0; i < res.length; i++) { fillData.push([]); for (let j = 0; j < dragLength; j++) { fillData[i].push(res[i][(j + (res[i].length - fillOffset)) % res[i].length]); } } } } // If the source data contains objects, we need to check every target cell for the data type. if (selectionSourceData.some(row => row.some(cell => (0, _object.isObject)(cell)))) { const fullFillData = _assertClassBrand(_Autofill_brand, this, _extendFillDataWithSourceData).call(this, fillData, selectionSourceData, startOfDragCoords, endOfDragCoords, directionOfDrag); if (fullFillData.length) { fillData = fullFillData; } } this.hot.populateFromArray(startOfDragCoords.row, startOfDragCoords.col, fillData, endOfDragCoords.row, endOfDragCoords.col, `${this.pluginName}.fill`, null); this.setSelection(cornersOfSelectionAndDragAreas); this.hot.runHooks('afterAutofill', fillData, sourceRange, targetRange, directionOfDrag); this.hot.render(); } else { // reset to avoid some range bug this.hot.view.render(); } return true; } /** * Reduces the selection area if the handle was dragged outside of the table or on headers. * * @private * @param {CellCoords} coords Indexes of selection corners. * @returns {CellCoords} */ reduceSelectionAreaIfNeeded(coords) { if (coords.row < 0) { coords.row = 0; } if (coords.col < 0) { coords.col = 0; } return coords; } /** * Gets the coordinates of the drag & drop borders. * * @private * @param {CellCoords} coordsOfSelection `CellCoords` coord object. * @returns {CellCoords} */ getCoordsOfDragAndDropBorders(coordsOfSelection) { const currentSelection = this.hot.getSelectedRangeLast(); const bottomRightCorner = currentSelection.getBottomEndCorner(); let coords = coordsOfSelection; if (this.directions.includes(_utils.DIRECTIONS.vertical) && this.directions.includes(_utils.DIRECTIONS.horizontal)) { const topStartCorner = currentSelection.getTopStartCorner(); if (bottomRightCorner.col <= coordsOfSelection.col || topStartCorner.col >= coordsOfSelection.col) { coords = this.hot._createCellCoords(bottomRightCorner.row, coordsOfSelection.col); } if (bottomRightCorner.row < coordsOfSelection.row || topStartCorner.row > coordsOfSelection.row) { coords = this.hot._createCellCoords(coordsOfSelection.row, bottomRightCorner.col); } } else if (this.directions.includes(_utils.DIRECTIONS.vertical)) { coords = this.hot._createCellCoords(coordsOfSelection.row, bottomRightCorner.col); } else if (this.directions.includes(_utils.DIRECTIONS.horizontal)) { coords = this.hot._createCellCoords(bottomRightCorner.row, coordsOfSelection.col); } else { // wrong direction return; } return this.reduceSelectionAreaIfNeeded(coords); } /** * Show the fill border. * * @private * @param {CellCoords} coordsOfSelection `CellCoords` coord object. */ showBorder(coordsOfSelection) { const coordsOfDragAndDropBorders = this.getCoordsOfDragAndDropBorders(coordsOfSelection); if (coordsOfDragAndDropBorders) { this.redrawBorders(coordsOfDragAndDropBorders); } } /** * Add new row. * * @private */ addRow() { this.hot._registerTimeout(() => { this.hot.alter(INSERT_ROW_ALTER_ACTION_NAME, undefined, 1, `${this.pluginName}.fill`); this.addingStarted = false; }, INTERVAL_FOR_ADDING_ROW); } /** * Add new rows if they are needed to continue auto-filling values. * * @private */ addNewRowIfNeeded() { if (!this.hot.selection.highlight.getFill().isEmpty() && this.addingStarted === false && this.autoInsertRow) { const cornersOfSelectedCells = this.hot.getSelectedLast(); const cornersOfSelectedDragArea = this.hot.selection.highlight.getFill().getVisualCorners(); const nrOfTableRows = this.hot.countRows(); if (cornersOfSelectedCells[2] < nrOfTableRows - 1 && cornersOfSelectedDragArea[2] === nrOfTableRows - 1) { this.addingStarted = true; this.addRow(); } } } /** * Get index of last adjacent filled in row. * * @private * @param {Array} cornersOfSelectedCells Indexes of selection corners. * @returns {number} Gives number greater than or equal to zero when selection adjacent can be applied. * Or -1 when selection adjacent can't be applied. */ getIndexOfLastAdjacentFilledInRow(cornersOfSelectedCells) { const data = this.hot.getData(); const nrOfTableRows = this.hot.countRows(); let lastFilledInRowIndex; for (let rowIndex = cornersOfSelectedCells[2] + 1; rowIndex < nrOfTableRows; rowIndex++) { for (let columnIndex = cornersOfSelectedCells[1]; columnIndex <= cornersOfSelectedCells[3]; columnIndex++) { const dataInCell = data[rowIndex][columnIndex]; if (!(0, _mixed.isEmpty)(dataInCell)) { return -1; } } const dataInNextLeftCell = data[rowIndex][cornersOfSelectedCells[1] - 1]; const dataInNextRightCell = data[rowIndex][cornersOfSelectedCells[3] + 1]; if (!(0, _mixed.isEmpty)(dataInNextLeftCell) || !(0, _mixed.isEmpty)(dataInNextRightCell)) { lastFilledInRowIndex = rowIndex; } } return lastFilledInRowIndex; } /** * Adds a selection from the start area to the specific row index. * * @private * @param {Array} selectStartArea Selection area from which we start to create more comprehensive selection. * @param {number} rowIndex The row index into the selection will be added. */ addSelectionFromStartAreaToSpecificRowIndex(selectStartArea, rowIndex) { this.hot.selection.highlight.getFill().clear().add(this.hot._createCellCoords(selectStartArea[0], selectStartArea[1])).add(this.hot._createCellCoords(rowIndex, selectStartArea[3])).commit(); } /** * Sets selection based on passed corners. * * @private * @param {Array} cornersOfArea An array witch defines selection. */ setSelection(cornersOfArea) { this.hot.selectCell(...(0, _array.arrayMap)(cornersOfArea, index => Math.max(index, 0)), false, false); } /** * Try to select cells down to the last row in the left column and then returns if selection was applied. * * @private * @returns {boolean} */ selectAdjacent() { const cornersOfSelectedCells = this.hot.getSelectedLast(); const lastFilledInRowIndex = this.getIndexOfLastAdjacentFilledInRow(cornersOfSelectedCells); if (lastFilledInRowIndex === -1 || lastFilledInRowIndex === undefined) { return false; } this.addSelectionFromStartAreaToSpecificRowIndex(cornersOfSelectedCells, lastFilledInRowIndex); return true; } /** * Resets selection of dragged area. * * @private */ resetSelectionOfDraggedArea() { this.handleDraggedCells = 0; this.hot.selection.highlight.getFill().clear(); } /** * Redraws borders. * * @private * @param {CellCoords} coords `CellCoords` coord object. */ redrawBorders(coords) { this.hot.selection.highlight.getFill().clear().add(this.hot.getSelectedRangeLast().from).add(this.hot.getSelectedRangeLast().to).add(coords).commit(); this.hot.view.render(); } /** * Get if mouse was dragged outside. * * @private * @param {MouseEvent} event `mousemove` event properties. * @returns {boolean} */ getIfMouseWasDraggedOutside(event) { const { documentElement } = this.hot.rootDocument; const tableBottom = (0, _element.offset)(this.hot.table).top - (this.hot.rootWindow.pageYOffset || documentElement.scrollTop) + (0, _element.outerHeight)(this.hot.table); const tableRight = (0, _element.offset)(this.hot.table).left - (this.hot.rootWindow.pageXOffset || documentElement.scrollLeft) + (0, _element.outerWidth)(this.hot.table); return event.clientY > tableBottom && event.clientX <= tableRight; } /** * Bind the events used by the plugin. * * @private */ registerEvents() { const { documentElement } = this.hot.rootDocument; this.eventManager.addEventListener(documentElement, 'mouseup', () => _assertClassBrand(_Autofill_brand, this, _onMouseUp).call(this)); this.eventManager.addEventListener(documentElement, 'mousemove', event => _assertClassBrand(_Autofill_brand, this, _onMouseMove).call(this, event)); } /** * Clears mapped settings. * * @private */ clearMappedSettings() { this.directions.length = 0; this.autoInsertRow = false; } /** * Map settings. * * @private */ mapSettings() { const mappedSettings = (0, _utils.getMappedFillHandleSetting)(this.hot.getSettings().fillHandle); this.directions = mappedSettings.directions; this.autoInsertRow = mappedSettings.autoInsertRow; } /** * Destroys the plugin instance. */ destroy() { super.destroy(); } } exports.Autofill = Autofill; function _extendFillDataWithSourceData(fillData, selectionSourceData, startOfDragCoords, endOfDragCoords) { const fullFillData = []; for (let rowIndex = Math.min(startOfDragCoords.row, endOfDragCoords.row); rowIndex <= Math.max(startOfDragCoords.row, endOfDragCoords.row); rowIndex += 1) { fullFillData.push([]); for (let columnIndex = Math.min(startOfDragCoords.col, endOfDragCoords.col); columnIndex <= Math.max(startOfDragCoords.col, endOfDragCoords.col); columnIndex += 1) { const sourceCell = this.hot.getSourceDataAtCell(rowIndex, columnIndex); const relativeRowIndex = rowIndex - Math.min(startOfDragCoords.row, endOfDragCoords.row); const relativeColumnIndex = columnIndex - Math.min(startOfDragCoords.col, endOfDragCoords.col); const modRelativeRowIndex = relativeRowIndex % selectionSourceData.length; const modRelativeColumnIndex = relativeColumnIndex % selectionSourceData[0].length; if ((0, _object.isObject)(sourceCell)) { fullFillData[relativeRowIndex][relativeColumnIndex] = selectionSourceData[modRelativeRowIndex][modRelativeColumnIndex]; } else { fullFillData[relativeRowIndex][relativeColumnIndex] = fillData[modRelativeRowIndex][modRelativeColumnIndex]; } } } return fullFillData; } /** * On cell corner double click callback. * * @private */ function _onCellCornerDblClick() { const selectionApplied = this.selectAdjacent(); if (selectionApplied) { this.fillIn(); } } /** * On after cell corner mouse down listener. */ function _onAfterCellCornerMouseDown() { this.handleDraggedCells = 1; this.mouseDownOnCellCorner = true; } /** * On before cell mouse over listener. * * @param {CellCoords} coords `CellCoords` coord object. */ function _onBeforeCellMouseOver(coords) { if (this.mouseDownOnCellCorner && !this.hot.view.isMouseDown() && this.handleDraggedCells) { this.handleDraggedCells += 1; this.showBorder(coords); this.addNewRowIfNeeded(); } } /** * On mouse up listener. */ function _onMouseUp() { if (this.handleDraggedCells) { if (this.handleDraggedCells > 1) { this.fillIn(); } this.handleDraggedCells = 0; this.mouseDownOnCellCorner = false; } } /** * On mouse move listener. * * @param {MouseEvent} event `mousemove` event properties. */ function _onMouseMove(event) { const mouseWasDraggedOutside = this.getIfMouseWasDraggedOutside(event); if (this.addingStarted === false && this.handleDraggedCells > 0 && mouseWasDraggedOutside) { this.mouseDragOutside = true; this.addingStarted = true; } else { this.mouseDragOutside = false; } if (this.mouseDragOutside && this.autoInsertRow) { this.addRow(); } }