1205 lines
49 KiB
JavaScript
1205 lines
49 KiB
JavaScript
import "core-js/modules/es.array.push.js";
|
|||
|
|
import "core-js/modules/es.error.cause.js";
|
||
|
|
function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); }
|
||
|
|
function _checkPrivateRedeclaration(obj, privateCollection) { if (privateCollection.has(obj)) { throw new TypeError("Cannot initialize the same private elements twice on an object"); } }
|
||
|
|
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
||
|
|
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
|
||
|
|
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
|
||
|
|
function _classPrivateFieldGet(receiver, privateMap) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "get"); return _classApplyDescriptorGet(receiver, descriptor); }
|
||
|
|
function _classApplyDescriptorGet(receiver, descriptor) { if (descriptor.get) { return descriptor.get.call(receiver); } return descriptor.value; }
|
||
|
|
function _classPrivateFieldSet(receiver, privateMap, value) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "set"); _classApplyDescriptorSet(receiver, descriptor, value); return value; }
|
||
|
|
function _classExtractFieldDescriptor(receiver, privateMap, action) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to " + action + " private field on non-instance"); } return privateMap.get(receiver); }
|
||
|
|
function _classApplyDescriptorSet(receiver, descriptor, value) { if (descriptor.set) { descriptor.set.call(receiver, value); } else { if (!descriptor.writable) { throw new TypeError("attempted to set read only private field"); } descriptor.value = value; } }
|
||
|
|
import { addClass, clearTextSelection, empty, fastInnerHTML, fastInnerText, getScrollbarWidth, hasClass, isChildOf, isInput, isOutsideInput } from "./helpers/dom/element.mjs";
|
||
|
|
import EventManager from "./eventManager.mjs";
|
||
|
|
import { isImmediatePropagationStopped, isRightClick, isLeftClick } from "./helpers/dom/event.mjs";
|
||
|
|
import Walkontable from "./3rdparty/walkontable/src/index.mjs";
|
||
|
|
import { handleMouseEvent } from "./selection/mouseEventHandler.mjs";
|
||
|
|
import { isRootInstance } from "./utils/rootInstance.mjs";
|
||
|
|
const privatePool = new WeakMap();
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @class TableView
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
var _columnHeadersCount = /*#__PURE__*/new WeakMap();
|
||
|
|
var _rowHeadersCount = /*#__PURE__*/new WeakMap();
|
||
|
|
class TableView {
|
||
|
|
/**
|
||
|
|
* @param {Hanstontable} instance Instance of {@link Handsontable}.
|
||
|
|
*/
|
||
|
|
constructor(instance) {
|
||
|
|
/**
|
||
|
|
* Instance of {@link Handsontable}.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
* @type {Handsontable}
|
||
|
|
*/
|
||
|
|
_defineProperty(this, "instance", void 0);
|
||
|
|
/**
|
||
|
|
* Instance of {@link EventManager}.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
* @type {EventManager}
|
||
|
|
*/
|
||
|
|
_defineProperty(this, "eventManager", void 0);
|
||
|
|
/**
|
||
|
|
* Current Handsontable's GridSettings object.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
* @type {GridSettings}
|
||
|
|
*/
|
||
|
|
_defineProperty(this, "settings", void 0);
|
||
|
|
/**
|
||
|
|
* Main <THEAD> element.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
* @type {HTMLTableSectionElement}
|
||
|
|
*/
|
||
|
|
_defineProperty(this, "THEAD", void 0);
|
||
|
|
/**
|
||
|
|
* Main <TBODY> element.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
* @type {HTMLTableSectionElement}
|
||
|
|
*/
|
||
|
|
_defineProperty(this, "TBODY", void 0);
|
||
|
|
/**
|
||
|
|
* Main Walkontable instance.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
* @type {Walkontable}
|
||
|
|
*/
|
||
|
|
_defineProperty(this, "_wt", void 0);
|
||
|
|
/**
|
||
|
|
* Main Walkontable instance.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
* @type {Walkontable}
|
||
|
|
*/
|
||
|
|
_defineProperty(this, "activeWt", void 0);
|
||
|
|
/**
|
||
|
|
* The total number of the column header renderers applied to the table through the
|
||
|
|
* `afterGetColumnHeaderRenderers` hook.
|
||
|
|
*
|
||
|
|
* @type {number}
|
||
|
|
*/
|
||
|
|
_classPrivateFieldInitSpec(this, _columnHeadersCount, {
|
||
|
|
writable: true,
|
||
|
|
value: 0
|
||
|
|
});
|
||
|
|
/**
|
||
|
|
* The total number of the row header renderers applied to the table through the
|
||
|
|
* `afterGetRowHeaderRenderers` hook.
|
||
|
|
*
|
||
|
|
* @type {number}
|
||
|
|
*/
|
||
|
|
_classPrivateFieldInitSpec(this, _rowHeadersCount, {
|
||
|
|
writable: true,
|
||
|
|
value: 0
|
||
|
|
});
|
||
|
|
/**
|
||
|
|
* The flag determines if the `adjustElementsSize` method call was made during
|
||
|
|
* the render suspending. If true, the method has to be triggered once after render
|
||
|
|
* resuming.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
* @type {boolean}
|
||
|
|
*/
|
||
|
|
_defineProperty(this, "postponedAdjustElementsSize", false);
|
||
|
|
this.instance = instance;
|
||
|
|
this.eventManager = new EventManager(this.instance);
|
||
|
|
this.settings = this.instance.getSettings();
|
||
|
|
privatePool.set(this, {
|
||
|
|
/**
|
||
|
|
* Defines if the text should be selected during mousemove.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
* @type {boolean}
|
||
|
|
*/
|
||
|
|
selectionMouseDown: false,
|
||
|
|
/**
|
||
|
|
* @private
|
||
|
|
* @type {boolean}
|
||
|
|
*/
|
||
|
|
mouseDown: void 0,
|
||
|
|
/**
|
||
|
|
* Main <TABLE> element.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
* @type {HTMLTableElement}
|
||
|
|
*/
|
||
|
|
table: void 0,
|
||
|
|
/**
|
||
|
|
* Cached width of the rootElement.
|
||
|
|
*
|
||
|
|
* @type {number}
|
||
|
|
*/
|
||
|
|
lastWidth: 0,
|
||
|
|
/**
|
||
|
|
* Cached height of the rootElement.
|
||
|
|
*
|
||
|
|
* @type {number}
|
||
|
|
*/
|
||
|
|
lastHeight: 0
|
||
|
|
});
|
||
|
|
this.createElements();
|
||
|
|
this.registerEvents();
|
||
|
|
this.initializeWalkontable();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Renders WalkontableUI.
|
||
|
|
*/
|
||
|
|
render() {
|
||
|
|
if (!this.instance.isRenderSuspended()) {
|
||
|
|
this.instance.runHooks('beforeRender', this.instance.forceFullRender);
|
||
|
|
if (this.postponedAdjustElementsSize) {
|
||
|
|
this.postponedAdjustElementsSize = false;
|
||
|
|
this.adjustElementsSize(true);
|
||
|
|
}
|
||
|
|
this._wt.draw(!this.instance.forceFullRender);
|
||
|
|
this.instance.runHooks('afterRender', this.instance.forceFullRender);
|
||
|
|
this.instance.forceFullRender = false;
|
||
|
|
this.instance.renderCall = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Adjust overlays elements size and master table size.
|
||
|
|
*
|
||
|
|
* @param {boolean} [force=false] When `true`, it adjust the DOM nodes sizes for all overlays.
|
||
|
|
*/
|
||
|
|
adjustElementsSize() {
|
||
|
|
let force = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
|
||
|
|
if (this.instance.isRenderSuspended()) {
|
||
|
|
this.postponedAdjustElementsSize = true;
|
||
|
|
} else {
|
||
|
|
this._wt.wtOverlays.adjustElementsSize(force);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns td object given coordinates.
|
||
|
|
*
|
||
|
|
* @param {CellCoords} coords Renderable cell coordinates.
|
||
|
|
* @param {boolean} topmost Indicates whether the cell should be calculated from the topmost.
|
||
|
|
* @returns {HTMLTableCellElement|null}
|
||
|
|
*/
|
||
|
|
getCellAtCoords(coords, topmost) {
|
||
|
|
const td = this._wt.getCell(coords, topmost);
|
||
|
|
if (td < 0) {
|
||
|
|
// there was an exit code (cell is out of bounds)
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
return td;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Scroll viewport to a cell.
|
||
|
|
*
|
||
|
|
* @param {CellCoords} coords Renderable cell coordinates.
|
||
|
|
* @param {boolean} [snapToTop] If `true`, viewport is scrolled to show the cell on the top of the table.
|
||
|
|
* @param {boolean} [snapToRight] If `true`, viewport is scrolled to show the cell on the right side of the table.
|
||
|
|
* @param {boolean} [snapToBottom] If `true`, viewport is scrolled to show the cell on the bottom side of the table.
|
||
|
|
* @param {boolean} [snapToLeft] If `true`, viewport is scrolled to show the cell on the left side of the table.
|
||
|
|
* @returns {boolean}
|
||
|
|
*/
|
||
|
|
scrollViewport(coords, snapToTop, snapToRight, snapToBottom, snapToLeft) {
|
||
|
|
return this._wt.scrollViewport(coords, snapToTop, snapToRight, snapToBottom, snapToLeft);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Scroll viewport to a column.
|
||
|
|
*
|
||
|
|
* @param {number} column Renderable column index.
|
||
|
|
* @param {boolean} [snapToRight] If `true`, viewport is scrolled to show the cell on the right side of the table.
|
||
|
|
* @param {boolean} [snapToLeft] If `true`, viewport is scrolled to show the cell on the left side of the table.
|
||
|
|
* @returns {boolean}
|
||
|
|
*/
|
||
|
|
scrollViewportHorizontally(column, snapToRight, snapToLeft) {
|
||
|
|
return this._wt.scrollViewportHorizontally(column, snapToRight, snapToLeft);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Scroll viewport to a row.
|
||
|
|
*
|
||
|
|
* @param {number} row Renderable row index.
|
||
|
|
* @param {boolean} [snapToTop] If `true`, viewport is scrolled to show the cell on the top of the table.
|
||
|
|
* @param {boolean} [snapToBottom] If `true`, viewport is scrolled to show the cell on the bottom side of the table.
|
||
|
|
* @returns {boolean}
|
||
|
|
*/
|
||
|
|
scrollViewportVertically(row, snapToTop, snapToBottom) {
|
||
|
|
return this._wt.scrollViewportVertically(row, snapToTop, snapToBottom);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Prepares DOMElements and adds correct className to the root element.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
createElements() {
|
||
|
|
const priv = privatePool.get(this);
|
||
|
|
const {
|
||
|
|
rootElement,
|
||
|
|
rootDocument
|
||
|
|
} = this.instance;
|
||
|
|
const originalStyle = rootElement.getAttribute('style');
|
||
|
|
if (originalStyle) {
|
||
|
|
rootElement.setAttribute('data-originalstyle', originalStyle); // needed to retrieve original style in jsFiddle link generator in HT examples. may be removed in future versions
|
||
|
|
}
|
||
|
|
|
||
|
|
addClass(rootElement, 'handsontable');
|
||
|
|
priv.table = rootDocument.createElement('TABLE');
|
||
|
|
addClass(priv.table, 'htCore');
|
||
|
|
if (this.instance.getSettings().tableClassName) {
|
||
|
|
addClass(priv.table, this.instance.getSettings().tableClassName);
|
||
|
|
}
|
||
|
|
this.THEAD = rootDocument.createElement('THEAD');
|
||
|
|
priv.table.appendChild(this.THEAD);
|
||
|
|
this.TBODY = rootDocument.createElement('TBODY');
|
||
|
|
priv.table.appendChild(this.TBODY);
|
||
|
|
this.instance.table = priv.table;
|
||
|
|
this.instance.container.insertBefore(priv.table, this.instance.container.firstChild);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Attaches necessary listeners.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
registerEvents() {
|
||
|
|
const priv = privatePool.get(this);
|
||
|
|
const {
|
||
|
|
rootElement,
|
||
|
|
rootDocument,
|
||
|
|
selection
|
||
|
|
} = this.instance;
|
||
|
|
const documentElement = rootDocument.documentElement;
|
||
|
|
this.eventManager.addEventListener(rootElement, 'mousedown', event => {
|
||
|
|
priv.selectionMouseDown = true;
|
||
|
|
if (!this.isTextSelectionAllowed(event.target)) {
|
||
|
|
const {
|
||
|
|
rootWindow
|
||
|
|
} = this.instance;
|
||
|
|
clearTextSelection(rootWindow);
|
||
|
|
event.preventDefault();
|
||
|
|
rootWindow.focus(); // make sure that window that contains HOT is active. Important when HOT is in iframe.
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
this.eventManager.addEventListener(rootElement, 'mouseup', () => {
|
||
|
|
priv.selectionMouseDown = false;
|
||
|
|
});
|
||
|
|
this.eventManager.addEventListener(rootElement, 'mousemove', event => {
|
||
|
|
if (priv.selectionMouseDown && !this.isTextSelectionAllowed(event.target)) {
|
||
|
|
// Clear selection only when fragmentSelection is enabled, otherwise clearing selection breaks the IME editor.
|
||
|
|
if (this.settings.fragmentSelection) {
|
||
|
|
clearTextSelection(this.instance.rootWindow);
|
||
|
|
}
|
||
|
|
event.preventDefault();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
this.eventManager.addEventListener(documentElement, 'keyup', event => {
|
||
|
|
// TODO: is it the best place and way to finish cell selection?
|
||
|
|
if (selection.isInProgress() && !event.shiftKey) {
|
||
|
|
selection.finish();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
this.eventManager.addEventListener(documentElement, 'mouseup', event => {
|
||
|
|
if (selection.isInProgress() && isLeftClick(event)) {
|
||
|
|
// is left mouse button
|
||
|
|
selection.finish();
|
||
|
|
}
|
||
|
|
priv.mouseDown = false;
|
||
|
|
if (isOutsideInput(rootDocument.activeElement) || !selection.isSelected() && !selection.isSelectedByAnyHeader() && !rootElement.contains(event.target) && !isRightClick(event)) {
|
||
|
|
this.instance.unlisten();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
this.eventManager.addEventListener(documentElement, 'contextmenu', event => {
|
||
|
|
if (selection.isInProgress() && isRightClick(event)) {
|
||
|
|
selection.finish();
|
||
|
|
priv.mouseDown = false;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
this.eventManager.addEventListener(documentElement, 'touchend', () => {
|
||
|
|
if (selection.isInProgress()) {
|
||
|
|
selection.finish();
|
||
|
|
}
|
||
|
|
priv.mouseDown = false;
|
||
|
|
});
|
||
|
|
this.eventManager.addEventListener(documentElement, 'mousedown', event => {
|
||
|
|
const originalTarget = event.target;
|
||
|
|
const eventX = event.x || event.clientX;
|
||
|
|
const eventY = event.y || event.clientY;
|
||
|
|
let next = event.target;
|
||
|
|
if (priv.mouseDown || !rootElement || !this.instance.view) {
|
||
|
|
return; // it must have been started in a cell
|
||
|
|
}
|
||
|
|
|
||
|
|
// immediate click on "holder" means click on the right side of vertical scrollbar
|
||
|
|
const {
|
||
|
|
holder
|
||
|
|
} = this.instance.view._wt.wtTable;
|
||
|
|
if (next === holder) {
|
||
|
|
const scrollbarWidth = getScrollbarWidth(rootDocument);
|
||
|
|
if (rootDocument.elementFromPoint(eventX + scrollbarWidth, eventY) !== holder || rootDocument.elementFromPoint(eventX, eventY + scrollbarWidth) !== holder) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
while (next !== documentElement) {
|
||
|
|
if (next === null) {
|
||
|
|
if (event.isTargetWebComponent) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
// click on something that was a row but now is detached (possibly because your click triggered a rerender)
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (next === rootElement) {
|
||
|
|
// click inside container
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
next = next.parentNode;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// function did not return until here, we have an outside click!
|
||
|
|
const outsideClickDeselects = typeof this.settings.outsideClickDeselects === 'function' ? this.settings.outsideClickDeselects(originalTarget) : this.settings.outsideClickDeselects;
|
||
|
|
if (outsideClickDeselects) {
|
||
|
|
this.instance.deselectCell();
|
||
|
|
} else {
|
||
|
|
this.instance.destroyEditor(false, false);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
this.eventManager.addEventListener(priv.table, 'selectstart', event => {
|
||
|
|
if (this.settings.fragmentSelection || isInput(event.target)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
// https://github.com/handsontable/handsontable/issues/160
|
||
|
|
// Prevent text from being selected when performing drag down.
|
||
|
|
event.preventDefault();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Translate renderable cell coordinates to visual coordinates.
|
||
|
|
*
|
||
|
|
* @param {CellCoords} coords The cell coordinates.
|
||
|
|
* @returns {CellCoords}
|
||
|
|
*/
|
||
|
|
translateFromRenderableToVisualCoords(_ref) {
|
||
|
|
let {
|
||
|
|
row,
|
||
|
|
col
|
||
|
|
} = _ref;
|
||
|
|
// TODO: To consider an idea to reusing the CellCoords instance instead creating new one.
|
||
|
|
return this.instance._createCellCoords(...this.translateFromRenderableToVisualIndex(row, col));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Translate renderable row and column indexes to visual row and column indexes.
|
||
|
|
*
|
||
|
|
* @param {number} renderableRow Renderable row index.
|
||
|
|
* @param {number} renderableColumn Renderable columnIndex.
|
||
|
|
* @returns {number[]}
|
||
|
|
*/
|
||
|
|
translateFromRenderableToVisualIndex(renderableRow, renderableColumn) {
|
||
|
|
// TODO: Some helper may be needed.
|
||
|
|
// We perform translation for indexes (without headers).
|
||
|
|
let visualRow = renderableRow >= 0 ? this.instance.rowIndexMapper.getVisualFromRenderableIndex(renderableRow) : renderableRow;
|
||
|
|
let visualColumn = renderableColumn >= 0 ? this.instance.columnIndexMapper.getVisualFromRenderableIndex(renderableColumn) : renderableColumn;
|
||
|
|
if (visualRow === null) {
|
||
|
|
visualRow = renderableRow;
|
||
|
|
}
|
||
|
|
if (visualColumn === null) {
|
||
|
|
visualColumn = renderableColumn;
|
||
|
|
}
|
||
|
|
return [visualRow, visualColumn];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns the number of renderable indexes.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
* @param {IndexMapper} indexMapper The IndexMapper instance for specific axis.
|
||
|
|
* @param {number} maxElements Maximum number of elements (rows or columns).
|
||
|
|
*
|
||
|
|
* @returns {number|*}
|
||
|
|
*/
|
||
|
|
countRenderableIndexes(indexMapper, maxElements) {
|
||
|
|
const consideredElements = Math.min(indexMapper.getNotTrimmedIndexesLength(), maxElements);
|
||
|
|
// Don't take hidden indexes into account. We are looking just for renderable indexes.
|
||
|
|
const firstNotHiddenIndex = indexMapper.getNearestNotHiddenIndex(consideredElements - 1, -1);
|
||
|
|
|
||
|
|
// There are no renderable indexes.
|
||
|
|
if (firstNotHiddenIndex === null) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
return indexMapper.getRenderableFromVisualIndex(firstNotHiddenIndex) + 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns the number of renderable columns.
|
||
|
|
*
|
||
|
|
* @returns {number}
|
||
|
|
*/
|
||
|
|
countRenderableColumns() {
|
||
|
|
return this.countRenderableIndexes(this.instance.columnIndexMapper, this.settings.maxCols);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns the number of renderable rows.
|
||
|
|
*
|
||
|
|
* @returns {number}
|
||
|
|
*/
|
||
|
|
countRenderableRows() {
|
||
|
|
return this.countRenderableIndexes(this.instance.rowIndexMapper, this.settings.maxRows);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns number of not hidden row indexes counting from the passed starting index.
|
||
|
|
* The counting direction can be controlled by `incrementBy` argument.
|
||
|
|
*
|
||
|
|
* @param {number} visualIndex The visual index from which the counting begins.
|
||
|
|
* @param {number} incrementBy If `-1` then counting is backwards or forward when `1`.
|
||
|
|
* @returns {number}
|
||
|
|
*/
|
||
|
|
countNotHiddenRowIndexes(visualIndex, incrementBy) {
|
||
|
|
return this.countNotHiddenIndexes(visualIndex, incrementBy, this.instance.rowIndexMapper, this.countRenderableRows());
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns number of not hidden column indexes counting from the passed starting index.
|
||
|
|
* The counting direction can be controlled by `incrementBy` argument.
|
||
|
|
*
|
||
|
|
* @param {number} visualIndex The visual index from which the counting begins.
|
||
|
|
* @param {number} incrementBy If `-1` then counting is backwards or forward when `1`.
|
||
|
|
* @returns {number}
|
||
|
|
*/
|
||
|
|
countNotHiddenColumnIndexes(visualIndex, incrementBy) {
|
||
|
|
return this.countNotHiddenIndexes(visualIndex, incrementBy, this.instance.columnIndexMapper, this.countRenderableColumns());
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns number of not hidden indexes counting from the passed starting index.
|
||
|
|
* The counting direction can be controlled by `incrementBy` argument.
|
||
|
|
*
|
||
|
|
* @param {number} visualIndex The visual index from which the counting begins.
|
||
|
|
* @param {number} incrementBy If `-1` then counting is backwards or forward when `1`.
|
||
|
|
* @param {IndexMapper} indexMapper The IndexMapper instance for specific axis.
|
||
|
|
* @param {number} renderableIndexesCount Total count of renderable indexes for specific axis.
|
||
|
|
* @returns {number}
|
||
|
|
*/
|
||
|
|
countNotHiddenIndexes(visualIndex, incrementBy, indexMapper, renderableIndexesCount) {
|
||
|
|
if (isNaN(visualIndex) || visualIndex < 0) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
const firstVisibleIndex = indexMapper.getNearestNotHiddenIndex(visualIndex, incrementBy);
|
||
|
|
const renderableIndex = indexMapper.getRenderableFromVisualIndex(firstVisibleIndex);
|
||
|
|
if (!Number.isInteger(renderableIndex)) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
let notHiddenIndexes = 0;
|
||
|
|
if (incrementBy < 0) {
|
||
|
|
// Zero-based numbering for renderable indexes corresponds to a number of not hidden indexes.
|
||
|
|
notHiddenIndexes = renderableIndex + 1;
|
||
|
|
} else if (incrementBy > 0) {
|
||
|
|
notHiddenIndexes = renderableIndexesCount - renderableIndex;
|
||
|
|
}
|
||
|
|
return notHiddenIndexes;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The function returns the number of not hidden column indexes that fit between the first and
|
||
|
|
* last fixed column in the left (or right in RTL mode) overlay.
|
||
|
|
*
|
||
|
|
* @returns {number}
|
||
|
|
*/
|
||
|
|
countNotHiddenFixedColumnsStart() {
|
||
|
|
const countCols = this.instance.countCols();
|
||
|
|
const visualFixedColumnsStart = Math.min(parseInt(this.settings.fixedColumnsStart, 10), countCols) - 1;
|
||
|
|
return this.countNotHiddenColumnIndexes(visualFixedColumnsStart, -1);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The function returns the number of not hidden row indexes that fit between the first and
|
||
|
|
* last fixed row in the top overlay.
|
||
|
|
*
|
||
|
|
* @returns {number}
|
||
|
|
*/
|
||
|
|
countNotHiddenFixedRowsTop() {
|
||
|
|
const countRows = this.instance.countRows();
|
||
|
|
const visualFixedRowsTop = Math.min(parseInt(this.settings.fixedRowsTop, 10), countRows) - 1;
|
||
|
|
return this.countNotHiddenRowIndexes(visualFixedRowsTop, -1);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The function returns the number of not hidden row indexes that fit between the first and
|
||
|
|
* last fixed row in the bottom overlay.
|
||
|
|
*
|
||
|
|
* @returns {number}
|
||
|
|
*/
|
||
|
|
countNotHiddenFixedRowsBottom() {
|
||
|
|
const countRows = this.instance.countRows();
|
||
|
|
const visualFixedRowsBottom = Math.max(countRows - parseInt(this.settings.fixedRowsBottom, 10), 0);
|
||
|
|
return this.countNotHiddenRowIndexes(visualFixedRowsBottom, 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Checks if at least one cell than belongs to the main table is not covered by the top, left or
|
||
|
|
* bottom overlay.
|
||
|
|
*
|
||
|
|
* @returns {boolean}
|
||
|
|
*/
|
||
|
|
isMainTableNotFullyCoveredByOverlays() {
|
||
|
|
const fixedAllRows = this.countNotHiddenFixedRowsTop() + this.countNotHiddenFixedRowsBottom();
|
||
|
|
const fixedAllColumns = this.countNotHiddenFixedColumnsStart();
|
||
|
|
return this.instance.countRenderedRows() > fixedAllRows && this.instance.countRenderedCols() > fixedAllColumns;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Defines default configuration and initializes WalkOnTable instance.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
initializeWalkontable() {
|
||
|
|
const priv = privatePool.get(this);
|
||
|
|
const walkontableConfig = {
|
||
|
|
rtlMode: this.instance.isRtl(),
|
||
|
|
externalRowCalculator: this.instance.getPlugin('autoRowSize') && this.instance.getPlugin('autoRowSize').isEnabled(),
|
||
|
|
table: priv.table,
|
||
|
|
isDataViewInstance: () => isRootInstance(this.instance),
|
||
|
|
preventOverflow: () => this.settings.preventOverflow,
|
||
|
|
preventWheel: () => this.settings.preventWheel,
|
||
|
|
stretchH: () => this.settings.stretchH,
|
||
|
|
data: (renderableRow, renderableColumn) => {
|
||
|
|
return this.instance.getDataAtCell(...this.translateFromRenderableToVisualIndex(renderableRow, renderableColumn));
|
||
|
|
},
|
||
|
|
totalRows: () => this.countRenderableRows(),
|
||
|
|
totalColumns: () => this.countRenderableColumns(),
|
||
|
|
// Number of renderable columns for the left overlay.
|
||
|
|
fixedColumnsStart: () => this.countNotHiddenFixedColumnsStart(),
|
||
|
|
// Number of renderable rows for the top overlay.
|
||
|
|
fixedRowsTop: () => this.countNotHiddenFixedRowsTop(),
|
||
|
|
// Number of renderable rows for the bottom overlay.
|
||
|
|
fixedRowsBottom: () => this.countNotHiddenFixedRowsBottom(),
|
||
|
|
// Enable the inline start overlay when conditions are met.
|
||
|
|
shouldRenderInlineStartOverlay: () => {
|
||
|
|
return this.settings.fixedColumnsStart > 0 || walkontableConfig.rowHeaders().length > 0;
|
||
|
|
},
|
||
|
|
// Enable the top overlay when conditions are met.
|
||
|
|
shouldRenderTopOverlay: () => {
|
||
|
|
return this.settings.fixedRowsTop > 0 || walkontableConfig.columnHeaders().length > 0;
|
||
|
|
},
|
||
|
|
// Enable the bottom overlay when conditions are met.
|
||
|
|
shouldRenderBottomOverlay: () => {
|
||
|
|
return this.settings.fixedRowsBottom > 0;
|
||
|
|
},
|
||
|
|
minSpareRows: () => this.settings.minSpareRows,
|
||
|
|
renderAllRows: this.settings.renderAllRows,
|
||
|
|
rowHeaders: () => {
|
||
|
|
const headerRenderers = [];
|
||
|
|
if (this.instance.hasRowHeaders()) {
|
||
|
|
headerRenderers.push((renderableRowIndex, TH) => {
|
||
|
|
// TODO: Some helper may be needed.
|
||
|
|
// We perform translation for row indexes (without row headers).
|
||
|
|
const visualRowIndex = renderableRowIndex >= 0 ? this.instance.rowIndexMapper.getVisualFromRenderableIndex(renderableRowIndex) : renderableRowIndex;
|
||
|
|
this.appendRowHeader(visualRowIndex, TH);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
this.instance.runHooks('afterGetRowHeaderRenderers', headerRenderers);
|
||
|
|
_classPrivateFieldSet(this, _rowHeadersCount, headerRenderers.length);
|
||
|
|
return headerRenderers;
|
||
|
|
},
|
||
|
|
columnHeaders: () => {
|
||
|
|
const headerRenderers = [];
|
||
|
|
if (this.instance.hasColHeaders()) {
|
||
|
|
headerRenderers.push((renderedColumnIndex, TH) => {
|
||
|
|
// TODO: Some helper may be needed.
|
||
|
|
// We perform translation for columns indexes (without column headers).
|
||
|
|
const visualColumnsIndex = renderedColumnIndex >= 0 ? this.instance.columnIndexMapper.getVisualFromRenderableIndex(renderedColumnIndex) : renderedColumnIndex;
|
||
|
|
this.appendColHeader(visualColumnsIndex, TH);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
this.instance.runHooks('afterGetColumnHeaderRenderers', headerRenderers);
|
||
|
|
_classPrivateFieldSet(this, _columnHeadersCount, headerRenderers.length);
|
||
|
|
return headerRenderers;
|
||
|
|
},
|
||
|
|
columnWidth: renderedColumnIndex => {
|
||
|
|
const visualIndex = this.instance.columnIndexMapper.getVisualFromRenderableIndex(renderedColumnIndex);
|
||
|
|
|
||
|
|
// It's not a bug that we can't find visual index for some handled by method indexes. The function is called also
|
||
|
|
// for indexes that are not displayed (indexes that are beyond the grid's boundaries), i.e. when `fixedColumnsStart` > `startCols` (wrong config?) or
|
||
|
|
// scrolling and dataset is empty (scroll should handle that?).
|
||
|
|
return this.instance.getColWidth(visualIndex === null ? renderedColumnIndex : visualIndex);
|
||
|
|
},
|
||
|
|
rowHeight: renderedRowIndex => {
|
||
|
|
const visualIndex = this.instance.rowIndexMapper.getVisualFromRenderableIndex(renderedRowIndex);
|
||
|
|
return this.instance.getRowHeight(visualIndex === null ? renderedRowIndex : visualIndex);
|
||
|
|
},
|
||
|
|
cellRenderer: (renderedRowIndex, renderedColumnIndex, TD) => {
|
||
|
|
const [visualRowIndex, visualColumnIndex] = this.translateFromRenderableToVisualIndex(renderedRowIndex, renderedColumnIndex);
|
||
|
|
|
||
|
|
// Coords may be modified. For example, by the `MergeCells` plugin. It should affect cell value and cell meta.
|
||
|
|
const modifiedCellCoords = this.instance.runHooks('modifyGetCellCoords', visualRowIndex, visualColumnIndex);
|
||
|
|
let visualRowToCheck = visualRowIndex;
|
||
|
|
let visualColumnToCheck = visualColumnIndex;
|
||
|
|
if (Array.isArray(modifiedCellCoords)) {
|
||
|
|
[visualRowToCheck, visualColumnToCheck] = modifiedCellCoords;
|
||
|
|
}
|
||
|
|
const cellProperties = this.instance.getCellMeta(visualRowToCheck, visualColumnToCheck);
|
||
|
|
const prop = this.instance.colToProp(visualColumnToCheck);
|
||
|
|
let value = this.instance.getDataAtRowProp(visualRowToCheck, prop);
|
||
|
|
if (this.instance.hasHook('beforeValueRender')) {
|
||
|
|
value = this.instance.runHooks('beforeValueRender', value, cellProperties);
|
||
|
|
}
|
||
|
|
this.instance.runHooks('beforeRenderer', TD, visualRowIndex, visualColumnIndex, prop, value, cellProperties);
|
||
|
|
this.instance.getCellRenderer(cellProperties)(this.instance, TD, visualRowIndex, visualColumnIndex, prop, value, cellProperties);
|
||
|
|
this.instance.runHooks('afterRenderer', TD, visualRowIndex, visualColumnIndex, prop, value, cellProperties);
|
||
|
|
},
|
||
|
|
selections: this.instance.selection.highlight,
|
||
|
|
hideBorderOnMouseDownOver: () => this.settings.fragmentSelection,
|
||
|
|
onWindowResize: () => {
|
||
|
|
if (this.instance && !this.instance.isDestroyed) {
|
||
|
|
this.instance.refreshDimensions();
|
||
|
|
}
|
||
|
|
},
|
||
|
|
onContainerElementResize: () => {
|
||
|
|
if (this.instance && !this.instance.isDestroyed) {
|
||
|
|
this.instance.refreshDimensions();
|
||
|
|
}
|
||
|
|
},
|
||
|
|
onCellMouseDown: (event, coords, TD, wt) => {
|
||
|
|
const visualCoords = this.translateFromRenderableToVisualCoords(coords);
|
||
|
|
const controller = {
|
||
|
|
row: false,
|
||
|
|
column: false,
|
||
|
|
cell: false
|
||
|
|
};
|
||
|
|
this.instance.listen();
|
||
|
|
this.activeWt = wt;
|
||
|
|
priv.mouseDown = true;
|
||
|
|
this.instance.runHooks('beforeOnCellMouseDown', event, visualCoords, TD, controller);
|
||
|
|
if (isImmediatePropagationStopped(event)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
handleMouseEvent(event, {
|
||
|
|
coords: visualCoords,
|
||
|
|
selection: this.instance.selection,
|
||
|
|
controller,
|
||
|
|
cellCoordsFactory: (row, column) => this.instance._createCellCoords(row, column)
|
||
|
|
});
|
||
|
|
this.instance.runHooks('afterOnCellMouseDown', event, visualCoords, TD);
|
||
|
|
this.activeWt = this._wt;
|
||
|
|
},
|
||
|
|
onCellContextMenu: (event, coords, TD, wt) => {
|
||
|
|
const visualCoords = this.translateFromRenderableToVisualCoords(coords);
|
||
|
|
this.activeWt = wt;
|
||
|
|
priv.mouseDown = false;
|
||
|
|
if (this.instance.selection.isInProgress()) {
|
||
|
|
this.instance.selection.finish();
|
||
|
|
}
|
||
|
|
this.instance.runHooks('beforeOnCellContextMenu', event, visualCoords, TD);
|
||
|
|
if (isImmediatePropagationStopped(event)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.instance.runHooks('afterOnCellContextMenu', event, visualCoords, TD);
|
||
|
|
this.activeWt = this._wt;
|
||
|
|
},
|
||
|
|
onCellMouseOut: (event, coords, TD, wt) => {
|
||
|
|
const visualCoords = this.translateFromRenderableToVisualCoords(coords);
|
||
|
|
this.activeWt = wt;
|
||
|
|
this.instance.runHooks('beforeOnCellMouseOut', event, visualCoords, TD);
|
||
|
|
if (isImmediatePropagationStopped(event)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.instance.runHooks('afterOnCellMouseOut', event, visualCoords, TD);
|
||
|
|
this.activeWt = this._wt;
|
||
|
|
},
|
||
|
|
onCellMouseOver: (event, coords, TD, wt) => {
|
||
|
|
const visualCoords = this.translateFromRenderableToVisualCoords(coords);
|
||
|
|
const controller = {
|
||
|
|
row: false,
|
||
|
|
column: false,
|
||
|
|
cell: false
|
||
|
|
};
|
||
|
|
this.activeWt = wt;
|
||
|
|
this.instance.runHooks('beforeOnCellMouseOver', event, visualCoords, TD, controller);
|
||
|
|
if (isImmediatePropagationStopped(event)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (priv.mouseDown) {
|
||
|
|
handleMouseEvent(event, {
|
||
|
|
coords: visualCoords,
|
||
|
|
selection: this.instance.selection,
|
||
|
|
controller,
|
||
|
|
cellCoordsFactory: (row, column) => this.instance._createCellCoords(row, column)
|
||
|
|
});
|
||
|
|
}
|
||
|
|
this.instance.runHooks('afterOnCellMouseOver', event, visualCoords, TD);
|
||
|
|
this.activeWt = this._wt;
|
||
|
|
},
|
||
|
|
onCellMouseUp: (event, coords, TD, wt) => {
|
||
|
|
const visualCoords = this.translateFromRenderableToVisualCoords(coords);
|
||
|
|
this.activeWt = wt;
|
||
|
|
this.instance.runHooks('beforeOnCellMouseUp', event, visualCoords, TD);
|
||
|
|
|
||
|
|
// TODO: The second condition check is a workaround. Callback corresponding the method `updateSettings`
|
||
|
|
// disable plugin and enable it again. Disabling plugin closes the menu. Thus, calling the
|
||
|
|
// `updateSettings` in a body of any callback executed right after some context-menu action
|
||
|
|
// breaks the table (#7231).
|
||
|
|
if (isImmediatePropagationStopped(event) || this.instance.isDestroyed) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.instance.runHooks('afterOnCellMouseUp', event, visualCoords, TD);
|
||
|
|
this.activeWt = this._wt;
|
||
|
|
},
|
||
|
|
onCellCornerMouseDown: event => {
|
||
|
|
event.preventDefault();
|
||
|
|
this.instance.runHooks('afterOnCellCornerMouseDown', event);
|
||
|
|
},
|
||
|
|
onCellCornerDblClick: event => {
|
||
|
|
event.preventDefault();
|
||
|
|
this.instance.runHooks('afterOnCellCornerDblClick', event);
|
||
|
|
},
|
||
|
|
beforeDraw: (force, skipRender) => this.beforeRender(force, skipRender),
|
||
|
|
onDraw: force => this.afterRender(force),
|
||
|
|
onScrollVertically: () => this.instance.runHooks('afterScrollVertically'),
|
||
|
|
onScrollHorizontally: () => this.instance.runHooks('afterScrollHorizontally'),
|
||
|
|
onBeforeRemoveCellClassNames: () => this.instance.runHooks('beforeRemoveCellClassNames'),
|
||
|
|
onBeforeHighlightingRowHeader: (renderableRow, headerLevel, highlightMeta) => {
|
||
|
|
const rowMapper = this.instance.rowIndexMapper;
|
||
|
|
const visualRow = rowMapper.getVisualFromRenderableIndex(renderableRow);
|
||
|
|
const newVisualRow = this.instance.runHooks('beforeHighlightingRowHeader', visualRow, headerLevel, highlightMeta);
|
||
|
|
return rowMapper.getRenderableFromVisualIndex(rowMapper.getNearestNotHiddenIndex(newVisualRow, 1));
|
||
|
|
},
|
||
|
|
onBeforeHighlightingColumnHeader: (renderableColumn, headerLevel, highlightMeta) => {
|
||
|
|
const columnMapper = this.instance.columnIndexMapper;
|
||
|
|
const visualColumn = columnMapper.getVisualFromRenderableIndex(renderableColumn);
|
||
|
|
const newVisualColumn = this.instance.runHooks('beforeHighlightingColumnHeader', visualColumn, headerLevel, highlightMeta);
|
||
|
|
return columnMapper.getRenderableFromVisualIndex(columnMapper.getNearestNotHiddenIndex(newVisualColumn, 1));
|
||
|
|
},
|
||
|
|
onAfterDrawSelection: (currentRow, currentColumn, layerLevel) => {
|
||
|
|
let cornersOfSelection;
|
||
|
|
const [visualRowIndex, visualColumnIndex] = this.translateFromRenderableToVisualIndex(currentRow, currentColumn);
|
||
|
|
const selectedRange = this.instance.selection.getSelectedRange();
|
||
|
|
const selectionRangeSize = selectedRange.size();
|
||
|
|
if (selectionRangeSize > 0) {
|
||
|
|
// Selection layers are stored from the "oldest" to the "newest". We should calculate the offset.
|
||
|
|
// Please look at the `SelectedRange` class and it's method for getting selection's layer for more information.
|
||
|
|
const selectionOffset = (layerLevel !== null && layerLevel !== void 0 ? layerLevel : 0) + 1 - selectionRangeSize;
|
||
|
|
const selectionForLayer = selectedRange.peekByIndex(selectionOffset);
|
||
|
|
cornersOfSelection = [selectionForLayer.from.row, selectionForLayer.from.col, selectionForLayer.to.row, selectionForLayer.to.col];
|
||
|
|
}
|
||
|
|
return this.instance.runHooks('afterDrawSelection', visualRowIndex, visualColumnIndex, cornersOfSelection, layerLevel);
|
||
|
|
},
|
||
|
|
onBeforeDrawBorders: (corners, borderClassName) => {
|
||
|
|
const [startRenderableRow, startRenderableColumn, endRenderableRow, endRenderableColumn] = corners;
|
||
|
|
const visualCorners = [this.instance.rowIndexMapper.getVisualFromRenderableIndex(startRenderableRow), this.instance.columnIndexMapper.getVisualFromRenderableIndex(startRenderableColumn), this.instance.rowIndexMapper.getVisualFromRenderableIndex(endRenderableRow), this.instance.columnIndexMapper.getVisualFromRenderableIndex(endRenderableColumn)];
|
||
|
|
return this.instance.runHooks('beforeDrawBorders', visualCorners, borderClassName);
|
||
|
|
},
|
||
|
|
onBeforeTouchScroll: () => this.instance.runHooks('beforeTouchScroll'),
|
||
|
|
onAfterMomentumScroll: () => this.instance.runHooks('afterMomentumScroll'),
|
||
|
|
onBeforeStretchingColumnWidth: (stretchedWidth, renderedColumnIndex) => {
|
||
|
|
const visualColumnIndex = this.instance.columnIndexMapper.getVisualFromRenderableIndex(renderedColumnIndex);
|
||
|
|
return this.instance.runHooks('beforeStretchingColumnWidth', stretchedWidth, visualColumnIndex);
|
||
|
|
},
|
||
|
|
onModifyRowHeaderWidth: rowHeaderWidth => this.instance.runHooks('modifyRowHeaderWidth', rowHeaderWidth),
|
||
|
|
onModifyGetCellCoords: (renderableRowIndex, renderableColumnIndex, topmost) => {
|
||
|
|
const rowMapper = this.instance.rowIndexMapper;
|
||
|
|
const columnMapper = this.instance.columnIndexMapper;
|
||
|
|
|
||
|
|
// Callback handle also headers. We shouldn't translate them.
|
||
|
|
const visualColumnIndex = renderableColumnIndex >= 0 ? columnMapper.getVisualFromRenderableIndex(renderableColumnIndex) : renderableColumnIndex;
|
||
|
|
const visualRowIndex = renderableRowIndex >= 0 ? rowMapper.getVisualFromRenderableIndex(renderableRowIndex) : renderableRowIndex;
|
||
|
|
const visualIndexes = this.instance.runHooks('modifyGetCellCoords', visualRowIndex, visualColumnIndex, topmost);
|
||
|
|
if (Array.isArray(visualIndexes)) {
|
||
|
|
const [visualRowFrom, visualColumnFrom, visualRowTo, visualColumnTo] = visualIndexes;
|
||
|
|
|
||
|
|
// Result of the hook is handled by the Walkontable (renderable indexes).
|
||
|
|
return [visualRowFrom >= 0 ? rowMapper.getRenderableFromVisualIndex(rowMapper.getNearestNotHiddenIndex(visualRowFrom, 1)) : visualRowFrom, visualColumnFrom >= 0 ? columnMapper.getRenderableFromVisualIndex(columnMapper.getNearestNotHiddenIndex(visualColumnFrom, 1)) : visualColumnFrom, visualRowTo >= 0 ? rowMapper.getRenderableFromVisualIndex(rowMapper.getNearestNotHiddenIndex(visualRowTo, -1)) : visualRowTo, visualColumnTo >= 0 ? columnMapper.getRenderableFromVisualIndex(columnMapper.getNearestNotHiddenIndex(visualColumnTo, -1)) : visualColumnTo];
|
||
|
|
}
|
||
|
|
},
|
||
|
|
viewportRowCalculatorOverride: calc => {
|
||
|
|
let viewportOffset = this.settings.viewportRowRenderingOffset;
|
||
|
|
if (viewportOffset === 'auto' && this.settings.fixedRowsTop) {
|
||
|
|
viewportOffset = 10;
|
||
|
|
}
|
||
|
|
if (viewportOffset > 0 || viewportOffset === 'auto') {
|
||
|
|
const renderableRows = this.countRenderableRows();
|
||
|
|
const firstRenderedRow = calc.startRow;
|
||
|
|
const lastRenderedRow = calc.endRow;
|
||
|
|
if (typeof viewportOffset === 'number') {
|
||
|
|
calc.startRow = Math.max(firstRenderedRow - viewportOffset, 0);
|
||
|
|
calc.endRow = Math.min(lastRenderedRow + viewportOffset, renderableRows - 1);
|
||
|
|
} else if (viewportOffset === 'auto') {
|
||
|
|
const offset = Math.ceil(lastRenderedRow / renderableRows * 12);
|
||
|
|
calc.startRow = Math.max(firstRenderedRow - offset, 0);
|
||
|
|
calc.endRow = Math.min(lastRenderedRow + offset, renderableRows - 1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
this.instance.runHooks('afterViewportRowCalculatorOverride', calc);
|
||
|
|
},
|
||
|
|
viewportColumnCalculatorOverride: calc => {
|
||
|
|
let viewportOffset = this.settings.viewportColumnRenderingOffset;
|
||
|
|
if (viewportOffset === 'auto' && this.settings.fixedColumnsStart) {
|
||
|
|
viewportOffset = 10;
|
||
|
|
}
|
||
|
|
if (viewportOffset > 0 || viewportOffset === 'auto') {
|
||
|
|
const renderableColumns = this.countRenderableColumns();
|
||
|
|
const firstRenderedColumn = calc.startColumn;
|
||
|
|
const lastRenderedColumn = calc.endColumn;
|
||
|
|
if (typeof viewportOffset === 'number') {
|
||
|
|
calc.startColumn = Math.max(firstRenderedColumn - viewportOffset, 0);
|
||
|
|
calc.endColumn = Math.min(lastRenderedColumn + viewportOffset, renderableColumns - 1);
|
||
|
|
}
|
||
|
|
if (viewportOffset === 'auto') {
|
||
|
|
const offset = Math.ceil(lastRenderedColumn / renderableColumns * 6);
|
||
|
|
calc.startColumn = Math.max(firstRenderedColumn - offset, 0);
|
||
|
|
calc.endColumn = Math.min(lastRenderedColumn + offset, renderableColumns - 1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
this.instance.runHooks('afterViewportColumnCalculatorOverride', calc);
|
||
|
|
},
|
||
|
|
rowHeaderWidth: () => this.settings.rowHeaderWidth,
|
||
|
|
columnHeaderHeight: () => {
|
||
|
|
const columnHeaderHeight = this.instance.runHooks('modifyColumnHeaderHeight');
|
||
|
|
return this.settings.columnHeaderHeight || columnHeaderHeight;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
this.instance.runHooks('beforeInitWalkontable', walkontableConfig);
|
||
|
|
this._wt = new Walkontable(walkontableConfig);
|
||
|
|
this.activeWt = this._wt;
|
||
|
|
const spreader = this._wt.wtTable.spreader;
|
||
|
|
// We have to cache width and height after Walkontable initialization.
|
||
|
|
const {
|
||
|
|
width,
|
||
|
|
height
|
||
|
|
} = this.instance.rootElement.getBoundingClientRect();
|
||
|
|
this.setLastSize(width, height);
|
||
|
|
this.eventManager.addEventListener(spreader, 'mousedown', event => {
|
||
|
|
// right mouse button exactly on spreader means right click on the right hand side of vertical scrollbar
|
||
|
|
if (event.target === spreader && event.which === 3) {
|
||
|
|
event.stopPropagation();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
this.eventManager.addEventListener(spreader, 'contextmenu', event => {
|
||
|
|
// right mouse button exactly on spreader means right click on the right hand side of vertical scrollbar
|
||
|
|
if (event.target === spreader && event.which === 3) {
|
||
|
|
event.stopPropagation();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
this.eventManager.addEventListener(this.instance.rootDocument.documentElement, 'click', () => {
|
||
|
|
if (this.settings.observeDOMVisibility) {
|
||
|
|
if (this._wt.drawInterrupted) {
|
||
|
|
this.instance.forceFullRender = true;
|
||
|
|
this.render();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Checks if it's possible to create text selection in element.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
* @param {HTMLElement} el The element to check.
|
||
|
|
* @returns {boolean}
|
||
|
|
*/
|
||
|
|
isTextSelectionAllowed(el) {
|
||
|
|
if (isInput(el)) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
const isChildOfTableBody = isChildOf(el, this.instance.view._wt.wtTable.spreader);
|
||
|
|
if (this.settings.fragmentSelection === true && isChildOfTableBody) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
if (this.settings.fragmentSelection === 'cell' && this.isSelectedOnlyCell() && isChildOfTableBody) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
if (!this.settings.fragmentSelection && this.isCellEdited() && this.isSelectedOnlyCell()) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Checks if user's been called mousedown.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
* @returns {boolean}
|
||
|
|
*/
|
||
|
|
isMouseDown() {
|
||
|
|
return privatePool.get(this).mouseDown;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if selected only one cell.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
* @returns {boolean}
|
||
|
|
*/
|
||
|
|
isSelectedOnlyCell() {
|
||
|
|
var _this$instance$getSel, _this$instance$getSel2;
|
||
|
|
return (_this$instance$getSel = (_this$instance$getSel2 = this.instance.getSelectedRangeLast()) === null || _this$instance$getSel2 === void 0 ? void 0 : _this$instance$getSel2.isSingle()) !== null && _this$instance$getSel !== void 0 ? _this$instance$getSel : false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Checks if active cell is editing.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
* @returns {boolean}
|
||
|
|
*/
|
||
|
|
isCellEdited() {
|
||
|
|
const activeEditor = this.instance.getActiveEditor();
|
||
|
|
return activeEditor && activeEditor.isOpened();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* `beforeDraw` callback.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
* @param {boolean} force If `true` rendering was triggered by a change of settings or data or `false` if
|
||
|
|
* rendering was triggered by scrolling or moving selection.
|
||
|
|
* @param {object} skipRender Object with `skipRender` property, if it is set to `true ` the next rendering
|
||
|
|
* cycle will be skipped.
|
||
|
|
*/
|
||
|
|
beforeRender(force, skipRender) {
|
||
|
|
if (force) {
|
||
|
|
// this.instance.forceFullRender = did Handsontable request full render?
|
||
|
|
this.instance.runHooks('beforeViewRender', this.instance.forceFullRender, skipRender);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* `afterRender` callback.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
* @param {boolean} force If `true` rendering was triggered by a change of settings or data or `false` if
|
||
|
|
* rendering was triggered by scrolling or moving selection.
|
||
|
|
*/
|
||
|
|
afterRender(force) {
|
||
|
|
if (force) {
|
||
|
|
// this.instance.forceFullRender = did Handsontable request full render?
|
||
|
|
this.instance.runHooks('afterViewRender', this.instance.forceFullRender);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Append row header to a TH element.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
* @param {number} visualRowIndex The visual row index.
|
||
|
|
* @param {HTMLTableHeaderCellElement} TH The table header element.
|
||
|
|
*/
|
||
|
|
appendRowHeader(visualRowIndex, TH) {
|
||
|
|
if (TH.firstChild) {
|
||
|
|
const container = TH.firstChild;
|
||
|
|
if (!hasClass(container, 'relative')) {
|
||
|
|
empty(TH);
|
||
|
|
this.appendRowHeader(visualRowIndex, TH);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.updateCellHeader(container.querySelector('.rowHeader'), visualRowIndex, this.instance.getRowHeader);
|
||
|
|
} else {
|
||
|
|
const {
|
||
|
|
rootDocument,
|
||
|
|
getRowHeader
|
||
|
|
} = this.instance;
|
||
|
|
const div = rootDocument.createElement('div');
|
||
|
|
const span = rootDocument.createElement('span');
|
||
|
|
div.className = 'relative';
|
||
|
|
span.className = 'rowHeader';
|
||
|
|
this.updateCellHeader(span, visualRowIndex, getRowHeader);
|
||
|
|
div.appendChild(span);
|
||
|
|
TH.appendChild(div);
|
||
|
|
}
|
||
|
|
this.instance.runHooks('afterGetRowHeader', visualRowIndex, TH);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Append column header to a TH element.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
* @param {number} visualColumnIndex Visual column index.
|
||
|
|
* @param {HTMLTableCellElement} TH The table header element.
|
||
|
|
* @param {Function} [label] The function that returns the header label.
|
||
|
|
* @param {number} [headerLevel=0] The index of header level counting from the top (positive
|
||
|
|
* values counting from 0 to N).
|
||
|
|
*/
|
||
|
|
appendColHeader(visualColumnIndex, TH) {
|
||
|
|
let label = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.instance.getColHeader;
|
||
|
|
let headerLevel = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0;
|
||
|
|
if (TH.firstChild) {
|
||
|
|
const container = TH.firstChild;
|
||
|
|
if (hasClass(container, 'relative')) {
|
||
|
|
this.updateCellHeader(container.querySelector('.colHeader'), visualColumnIndex, label, headerLevel);
|
||
|
|
} else {
|
||
|
|
empty(TH);
|
||
|
|
this.appendColHeader(visualColumnIndex, TH, label, headerLevel);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
const {
|
||
|
|
rootDocument
|
||
|
|
} = this.instance;
|
||
|
|
const div = rootDocument.createElement('div');
|
||
|
|
const span = rootDocument.createElement('span');
|
||
|
|
div.className = 'relative';
|
||
|
|
span.className = 'colHeader';
|
||
|
|
this.updateCellHeader(span, visualColumnIndex, label, headerLevel);
|
||
|
|
div.appendChild(span);
|
||
|
|
TH.appendChild(div);
|
||
|
|
}
|
||
|
|
this.instance.runHooks('afterGetColHeader', visualColumnIndex, TH, headerLevel);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Updates header cell content.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
* @param {HTMLElement} element Element to update.
|
||
|
|
* @param {number} index Row index or column index.
|
||
|
|
* @param {Function} content Function which should be returns content for this cell.
|
||
|
|
* @param {number} [headerLevel=0] The index of header level counting from the top (positive
|
||
|
|
* values counting from 0 to N).
|
||
|
|
*/
|
||
|
|
updateCellHeader(element, index, content) {
|
||
|
|
let headerLevel = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0;
|
||
|
|
let renderedIndex = index;
|
||
|
|
const parentOverlay = this._wt.wtOverlays.getParentOverlay(element) || this._wt;
|
||
|
|
|
||
|
|
// prevent wrong calculations from SampleGenerator
|
||
|
|
if (element.parentNode) {
|
||
|
|
if (hasClass(element, 'colHeader')) {
|
||
|
|
renderedIndex = parentOverlay.wtTable.columnFilter.sourceToRendered(index);
|
||
|
|
} else if (hasClass(element, 'rowHeader')) {
|
||
|
|
renderedIndex = parentOverlay.wtTable.rowFilter.sourceToRendered(index);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (renderedIndex > -1) {
|
||
|
|
fastInnerHTML(element, content(index, headerLevel));
|
||
|
|
} else {
|
||
|
|
// workaround for https://github.com/handsontable/handsontable/issues/1946
|
||
|
|
fastInnerText(element, String.fromCharCode(160));
|
||
|
|
addClass(element, 'cornerHeader');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Given a element's left (or right in RTL mode) position relative to the viewport, returns maximum
|
||
|
|
* element width until the right (or left) edge of the viewport (before scrollbar).
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
* @param {number} inlineOffset The left (or right in RTL mode) offset.
|
||
|
|
* @returns {number}
|
||
|
|
*/
|
||
|
|
maximumVisibleElementWidth(inlineOffset) {
|
||
|
|
const workspaceWidth = this._wt.wtViewport.getWorkspaceWidth();
|
||
|
|
const maxWidth = workspaceWidth - inlineOffset;
|
||
|
|
return maxWidth > 0 ? maxWidth : 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Given a element's top position relative to the viewport, returns maximum element height until the bottom
|
||
|
|
* edge of the viewport (before scrollbar).
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
* @param {number} topOffset The top offset.
|
||
|
|
* @returns {number}
|
||
|
|
*/
|
||
|
|
maximumVisibleElementHeight(topOffset) {
|
||
|
|
const workspaceHeight = this._wt.wtViewport.getWorkspaceHeight();
|
||
|
|
const maxHeight = workspaceHeight - topOffset;
|
||
|
|
return maxHeight > 0 ? maxHeight : 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Sets new dimensions of the container.
|
||
|
|
*
|
||
|
|
* @param {number} width The table width.
|
||
|
|
* @param {number} height The table height.
|
||
|
|
*/
|
||
|
|
setLastSize(width, height) {
|
||
|
|
const priv = privatePool.get(this);
|
||
|
|
[priv.lastWidth, priv.lastHeight] = [width, height];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns cached dimensions.
|
||
|
|
*
|
||
|
|
* @returns {object}
|
||
|
|
*/
|
||
|
|
getLastSize() {
|
||
|
|
const priv = privatePool.get(this);
|
||
|
|
return {
|
||
|
|
width: priv.lastWidth,
|
||
|
|
height: priv.lastHeight
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns the first fully visible row in the table viewport.
|
||
|
|
*
|
||
|
|
* @returns {number}
|
||
|
|
*/
|
||
|
|
getFirstFullyVisibleRow() {
|
||
|
|
return this.instance.rowIndexMapper.getVisualFromRenderableIndex(this.instance.view._wt.wtScroll.getFirstVisibleRow());
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns the last fully visible row in the table viewport.
|
||
|
|
*
|
||
|
|
* @returns {number}
|
||
|
|
*/
|
||
|
|
getLastFullyVisibleRow() {
|
||
|
|
return this.instance.rowIndexMapper.getVisualFromRenderableIndex(this.instance.view._wt.wtScroll.getLastVisibleRow());
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns the first fully visible column in the table viewport.
|
||
|
|
*
|
||
|
|
* @returns {number}
|
||
|
|
*/
|
||
|
|
getFirstFullyVisibleColumn() {
|
||
|
|
return this.instance.columnIndexMapper.getVisualFromRenderableIndex(this.instance.view._wt.wtScroll.getFirstVisibleColumn());
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns the last fully visible column in the table viewport.
|
||
|
|
*
|
||
|
|
* @returns {number}
|
||
|
|
*/
|
||
|
|
getLastFullyVisibleColumn() {
|
||
|
|
return this.instance.columnIndexMapper.getVisualFromRenderableIndex(this.instance.view._wt.wtScroll.getLastVisibleColumn());
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns the total count of the rendered column headers.
|
||
|
|
*
|
||
|
|
* @returns {number}
|
||
|
|
*/
|
||
|
|
getColumnHeadersCount() {
|
||
|
|
return _classPrivateFieldGet(this, _columnHeadersCount);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns the total count of the rendered row headers.
|
||
|
|
*
|
||
|
|
* @returns {number}
|
||
|
|
*/
|
||
|
|
getRowHeadersCount() {
|
||
|
|
return _classPrivateFieldGet(this, _rowHeadersCount);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Destroys internal WalkOnTable's instance. Detaches all of the bonded listeners.
|
||
|
|
*
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
destroy() {
|
||
|
|
this._wt.destroy();
|
||
|
|
this.eventManager.destroy();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
export default TableView;
|