import { getCellContent, copyTextToClipboard } from './utils'; import keyboard from 'keyboard'; import $ from './dom'; export default class CellManager { constructor(instance) { this.instance = instance; this.wrapper = this.instance.wrapper; this.options = this.instance.options; this.style = this.instance.style; this.bodyScrollable = this.instance.bodyScrollable; this.columnmanager = this.instance.columnmanager; this.rowmanager = this.instance.rowmanager; this.bindEvents(); } bindEvents() { this.bindFocusCell(); this.bindEditCell(); this.bindKeyboardSelection(); this.bindCopyCellContents(); this.bindMouseEvents(); } bindFocusCell() { this.bindKeyboardNav(); } bindEditCell() { this.$editingCell = null; $.on(this.bodyScrollable, 'dblclick', '.data-table-col', (e, cell) => { this.activateEditing(cell); }); keyboard.on('enter', (e) => { if (this.$focusedCell && !this.$editingCell) { // enter keypress on focused cell this.activateEditing(this.$focusedCell); } else if (this.$editingCell) { // enter keypress on editing cell this.submitEditing(this.$editingCell); this.deactivateEditing(); } }); $.on(document.body, 'click', e => { if (e.target.matches('.edit-cell, .edit-cell *')) return; this.deactivateEditing(); }); } bindKeyboardNav() { const focusCell = (direction) => { if (!this.$focusedCell || this.$editingCell) { return false; } let $cell = this.$focusedCell; if (direction === 'left') { $cell = this.getLeftCell$($cell); } else if (direction === 'right') { $cell = this.getRightCell$($cell); } else if (direction === 'up') { $cell = this.getAboveCell$($cell); } else if (direction === 'down') { $cell = this.getBelowCell$($cell); } this.focusCell($cell); return true; }; const focusLastCell = (direction) => { if (!this.$focusedCell || this.$editingCell) { return false; } let $cell = this.$focusedCell; const { rowIndex, colIndex } = $.data($cell); if (direction === 'left') { $cell = this.getLeftMostCell$(rowIndex); } else if (direction === 'right') { $cell = this.getRightMostCell$(rowIndex); } else if (direction === 'up') { $cell = this.getTopMostCell$(colIndex); } else if (direction === 'down') { $cell = this.getBottomMostCell$(colIndex); } this.focusCell($cell); return true; }; const scrollToCell = (direction) => { if (!this.$focusedCell) return false; if (!this.inViewport(this.$focusedCell)) { const { rowIndex } = $.data(this.$focusedCell); this.scrollToRow(rowIndex - this.getRowCountPerPage() + 2); return true; } return false; }; ['left', 'right', 'up', 'down'].map( direction => keyboard.on(direction, () => focusCell(direction)) ); ['left', 'right', 'up', 'down'].map( direction => keyboard.on('ctrl+' + direction, () => focusLastCell(direction)) ); ['left', 'right', 'up', 'down'].map( direction => keyboard.on(direction, () => scrollToCell(direction)) ); keyboard.on('esc', () => { this.deactivateEditing(); }); } bindKeyboardSelection() { const getNextSelectionCursor = (direction) => { let $selectionCursor = this.getSelectionCursor(); if (direction === 'left') { $selectionCursor = this.getLeftCell$($selectionCursor); } else if (direction === 'right') { $selectionCursor = this.getRightCell$($selectionCursor); } else if (direction === 'up') { $selectionCursor = this.getAboveCell$($selectionCursor); } else if (direction === 'down') { $selectionCursor = this.getBelowCell$($selectionCursor); } return $selectionCursor; }; ['left', 'right', 'up', 'down'].map( direction => keyboard.on('shift+' + direction, () => this.selectArea(getNextSelectionCursor(direction))) ); } bindCopyCellContents() { keyboard.on('ctrl+c', () => { this.copyCellContents(this.$focusedCell, this.$selectionCursor); }); } bindMouseEvents() { let mouseDown = null; $.on(this.bodyScrollable, 'mousedown', '.data-table-col', (e) => { mouseDown = true; this.focusCell($(e.delegatedTarget)); }); $.on(this.bodyScrollable, 'mouseup', () => { mouseDown = false; }); $.on(this.bodyScrollable, 'mousemove', '.data-table-col', (e) => { if (!mouseDown) return; this.selectArea($(e.delegatedTarget)); }); } focusCell($cell) { if (!$cell) return; // don't focus if already editing cell if ($cell === this.$editingCell) return; const { colIndex, isHeader } = $.data($cell); if (this.isStandardCell(colIndex) || isHeader) { return; } this.deactivateEditing(); this.clearSelection(); if (this.options.addCheckboxColumn && colIndex === 0) { return; } if (this.$focusedCell) { this.$focusedCell.classList.remove('selected'); } this.$focusedCell = $cell; $cell.classList.add('selected'); this.highlightRowColumnHeader($cell); } highlightRowColumnHeader($cell) { const { colIndex, rowIndex } = $.data($cell); const _colIndex = this.columnmanager.getSerialColumnIndex(); const colHeaderSelector = `.data-table-header .data-table-col[data-col-index="${colIndex}"]`; const rowHeaderSelector = `.data-table-col[data-row-index="${rowIndex}"][data-col-index="${_colIndex}"]`; if (this.lastHeaders) { $.removeStyle(this.lastHeaders, 'backgroundColor'); } const colHeader = $(colHeaderSelector, this.wrapper); const rowHeader = $(rowHeaderSelector, this.wrapper); $.style([colHeader, rowHeader], { backgroundColor: 'var(--light-bg)' }); this.lastHeaders = [colHeader, rowHeader]; } selectArea($selectionCursor) { if (!this.$focusedCell) return; if (this._selectArea(this.$focusedCell, $selectionCursor)) { // valid selection this.$selectionCursor = $selectionCursor; } }; _selectArea($cell1, $cell2) { if ($cell1 === $cell2) return false; const cells = this.getCellsInRange($cell1, $cell2); if (!cells) return false; this.clearSelection(); cells.map(index => this.getCell$(...index)).map($cell => $cell.classList.add('highlight')); return true; } getCellsInRange($cell1, $cell2) { let colIndex1, rowIndex1, colIndex2, rowIndex2; if (typeof $cell1 === 'number') { [colIndex1, rowIndex1, colIndex2, rowIndex2] = arguments; } else if (typeof $cell1 === 'object') { if (!($cell1 && $cell2)) { return false; } const cell1 = $.data($cell1); const cell2 = $.data($cell2); colIndex1 = cell1.colIndex; rowIndex1 = cell1.rowIndex; colIndex2 = cell2.colIndex; rowIndex2 = cell2.rowIndex; } if (rowIndex1 > rowIndex2) { [rowIndex1, rowIndex2] = [rowIndex2, rowIndex1]; } if (colIndex1 > colIndex2) { [colIndex1, colIndex2] = [colIndex2, colIndex1]; } if (this.isStandardCell(colIndex1) || this.isStandardCell(colIndex2)) { return false; } let cells = []; let colIndex = colIndex1; let rowIndex = rowIndex1; let rowIndices = []; while (rowIndex <= rowIndex2) { rowIndices.push(rowIndex); rowIndex++; } rowIndices.map(rowIndex => { while (colIndex <= colIndex2) { cells.push([colIndex, rowIndex]); colIndex++; } colIndex = colIndex1; }); return cells; } clearSelection() { $.each('.data-table-col.highlight', this.bodyScrollable) .map(cell => cell.classList.remove('highlight')); this.$selectionCursor = null; } getSelectionCursor() { return this.$selectionCursor || this.$focusedCell; } activateEditing($cell) { const { rowIndex, colIndex } = $.data($cell); const col = this.instance.columnmanager.getColumn(colIndex); if (col && col.editable === false) { return; } if (this.$editingCell) { const { _rowIndex, _colIndex } = $.data(this.$editingCell); if (rowIndex === _rowIndex && colIndex === _colIndex) { // editing the same cell return; } } this.$editingCell = $cell; $cell.classList.add('editing'); const $editCell = $('.edit-cell', $cell); $editCell.innerHTML = ''; const cell = this.getCell(colIndex, rowIndex); const editing = this.getEditingObject(colIndex, rowIndex, cell.content, $editCell); if (editing) { this.currentCellEditing = editing; // initialize editing input with cell value editing.initValue(cell.content); } } deactivateEditing() { if (!this.$editingCell) return; this.$editingCell.classList.remove('editing'); this.$editingCell = null; } getEditingObject(colIndex, rowIndex, value, parent) { if (this.options.editing) { return this.options.editing(colIndex, rowIndex, value, parent); } // editing fallback const $input = $.create('input', { type: 'text', inside: parent }); return { initValue(value) { $input.focus(); $input.value = value; }, getValue() { return $input.value; }, setValue(value) { $input.value = value; } }; } submitEditing($cell) { const { rowIndex, colIndex } = $.data($cell); if ($cell) { const editing = this.currentCellEditing; if (editing) { const value = editing.getValue(); const done = editing.setValue(value); if (done && done.then) { // wait for promise then update internal state done.then( () => this.updateCell(rowIndex, colIndex, value) ); } else { this.updateCell(rowIndex, colIndex, value); } } } this.currentCellEditing = null; } copyCellContents($cell1, $cell2) { const cells = this.getCellsInRange($cell1, $cell2); if (!cells) return; const values = cells // get cell objects .map(index => this.getCell(...index)) // convert to array of rows .reduce((acc, curr) => { const rowIndex = curr.rowIndex; acc[rowIndex] = acc[rowIndex] || []; acc[rowIndex].push(curr.content); return acc; }, []) // join values by tab .map(row => row.join('\t')) // join rows by newline .join('\n'); copyTextToClipboard(values); } updateCell(rowIndex, colIndex, value) { const cell = this.getCell(colIndex, rowIndex); cell.content = value; this.refreshCell(cell); } refreshCell(cell) { const selector = `.data-table-col[data-row-index="${cell.rowIndex}"][data-col-index="${cell.colIndex}"]`; const $cell = $(selector, this.bodyScrollable); $cell.innerHTML = getCellContent(cell); } isStandardCell(colIndex) { // Standard cells are in Sr. No and Checkbox column return colIndex < this.columnmanager.getFirstColumnIndex(); } getCell$(colIndex, rowIndex) { return $(`.data-table-col[data-row-index="${rowIndex}"][data-col-index="${colIndex}"]`, this.bodyScrollable); } getAboveCell$($cell) { const { colIndex } = $.data($cell); const $aboveRow = $cell.parentElement.previousElementSibling; return $(`[data-col-index="${colIndex}"]`, $aboveRow); } getBelowCell$($cell) { const { colIndex } = $.data($cell); const $belowRow = $cell.parentElement.nextElementSibling; return $(`[data-col-index="${colIndex}"]`, $belowRow); } getLeftCell$($cell) { return $cell.previousElementSibling; } getRightCell$($cell) { return $cell.nextElementSibling; } getLeftMostCell$(rowIndex) { return this.getCell$(rowIndex, this.columnmanager.getFirstColumnIndex()); } getRightMostCell$(rowIndex) { return this.getCell$(rowIndex, this.columnmanager.getLastColumnIndex()); } getTopMostCell$(colIndex) { return this.getCell$(this.rowmanager.getFirstRowIndex(), colIndex); } getBottomMostCell$(colIndex) { return this.getCell$(this.rowmanager.getLastRowIndex(), colIndex); } getCell(colIndex, rowIndex) { return this.instance.datamanager.getCell(colIndex, rowIndex); } getCellAttr($cell) { return this.instance.getCellAttr($cell); } getRowHeight() { return $.style($('.data-table-row', this.bodyScrollable), 'height'); } inViewport($cell) { let colIndex, rowIndex; // eslint-disable-line if (typeof $cell === 'number') { [colIndex, rowIndex] = arguments; } else { let cell = $.data($cell); colIndex = cell.colIndex; rowIndex = cell.rowIndex; } const viewportHeight = this.instance.getViewportHeight(); const rowHeight = this.getRowHeight(); const rowOffset = rowIndex * rowHeight; const scrollTopOffset = this.bodyScrollable.scrollTop; if (rowOffset - scrollTopOffset + rowHeight < viewportHeight) { return true; } return false; } scrollToCell($cell) { const { rowIndex } = $.data($cell); this.scrollToRow(rowIndex); } getRowCountPerPage() { return Math.ceil(this.instance.getViewportHeight() / this.getRowHeight()); } scrollToRow(rowIndex) { const offset = rowIndex * this.getRowHeight(); this.bodyScrollable.scrollTop = offset; } }