handsontable
Version:
Handsontable is a JavaScript Data Grid available for React, Angular and Vue.
599 lines (574 loc) • 22.4 kB
JavaScript
"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();
}
}