refactor, new selection/focus logic
This commit is contained in:
parent
3bb80c1ddf
commit
0d17d35fe8
@ -29,7 +29,7 @@ export class Formula extends ExcelComponent {
|
||||
this.formulaInput = this.$root.find('#formula-input');
|
||||
|
||||
this.$onEventFromObserver('table:select-cell', (cell: Dom) => {
|
||||
this.formulaInput.text = cell.data.value || '';
|
||||
this.formulaInput.text = cell.dataValue || '';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
59
src/components/table/FocusManager.ts
Normal file
59
src/components/table/FocusManager.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { Table } from 'components/table/Table';
|
||||
import { $, Dom } from 'core/Dom';
|
||||
import { parse } from 'core/utils';
|
||||
|
||||
export class FocusManager {
|
||||
private rootTable: Table;
|
||||
public $currentFocusedCell: Dom | null;
|
||||
|
||||
constructor(rootTable: Table) {
|
||||
this.rootTable = rootTable;
|
||||
|
||||
this.initManager();
|
||||
}
|
||||
|
||||
private initManager() {
|
||||
this.$currentFocusedCell = null;
|
||||
}
|
||||
|
||||
resetFocus() {
|
||||
window.getSelection()?.removeAllRanges();
|
||||
(document.activeElement as HTMLElement)?.blur();
|
||||
this.$currentFocusedCell = null;
|
||||
}
|
||||
|
||||
focusOnTable() {
|
||||
this.rootTable.$root.focus();
|
||||
}
|
||||
|
||||
focusCell($cell: Dom) {
|
||||
try {
|
||||
const r = new Range();
|
||||
const innerText = $cell.$el.childNodes[$cell.$el.childNodes.length - 1];
|
||||
|
||||
if (innerText !== undefined) {
|
||||
r.setEndAfter(innerText);
|
||||
r.setStartBefore(innerText);
|
||||
r.collapse(false);
|
||||
|
||||
const selection = window.getSelection();
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(r);
|
||||
} else {
|
||||
$cell.focus();
|
||||
}
|
||||
|
||||
this.$currentFocusedCell = $cell;
|
||||
} catch (e) {
|
||||
console.log('FocusManager: ', e.message);
|
||||
this.rootTable.selectionManager.clearAllSelection();
|
||||
}
|
||||
}
|
||||
|
||||
onFocusOut(event: FocusEvent) {
|
||||
const $target = $(event.target);
|
||||
if (!this.$currentFocusedCell?.isEqual($target)) return;
|
||||
|
||||
this.rootTable.updateTextInCell(parse($target.dataValue), $target, true);
|
||||
}
|
||||
}
|
||||
398
src/components/table/SelectionManager.ts
Normal file
398
src/components/table/SelectionManager.ts
Normal file
@ -0,0 +1,398 @@
|
||||
import { Table } from 'components/table/Table';
|
||||
import {
|
||||
getCellIdFromParams,
|
||||
getParamsFromCellId,
|
||||
isCell2,
|
||||
isSelectionKey,
|
||||
startCellId,
|
||||
} from 'components/table/table.functions';
|
||||
import { $, Dom } from 'core/Dom';
|
||||
import { getCellById, getIdByCell } from 'core/utils';
|
||||
|
||||
export class SelectionManager {
|
||||
private rootTable: Table;
|
||||
private $selection: Dom[];
|
||||
public $currentSelectedCell: Dom | null;
|
||||
private $firstSelectedCell: Dom | null;
|
||||
|
||||
private isMouseDowned: boolean;
|
||||
private static selectedClassName = 'selected';
|
||||
|
||||
private static addSelectedCellClass($cell: Dom) {
|
||||
if (!$cell || !$cell.isExist) return;
|
||||
$cell.addClass('selected');
|
||||
}
|
||||
|
||||
private static removeSelectedCellClass($cell: Dom) {
|
||||
if (!$cell || !$cell.isExist) return;
|
||||
$cell.removeClass('selected');
|
||||
}
|
||||
|
||||
private addCurrentCellClass() {
|
||||
if (!this.$firstSelectedCell?.isExist) this.$currentSelectedCell?.addClass('current');
|
||||
else this.$firstSelectedCell?.addClass('current');
|
||||
}
|
||||
|
||||
private removeCurrentCellClass() {
|
||||
if (!this.$firstSelectedCell?.isExist) this.$currentSelectedCell?.removeClass('current');
|
||||
else this.$firstSelectedCell?.removeClass('current');
|
||||
}
|
||||
|
||||
public static selectionKeys = [
|
||||
'ArrowDown',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'ArrowUp',
|
||||
'Enter',
|
||||
'Tab',
|
||||
'Delete',
|
||||
|
||||
'Control',
|
||||
'Shift',
|
||||
];
|
||||
|
||||
constructor(rootTable: Table) {
|
||||
this.rootTable = rootTable;
|
||||
|
||||
this.initManager();
|
||||
}
|
||||
|
||||
private initManager() {
|
||||
this.clearAllSelection();
|
||||
|
||||
this.isMouseDowned = false;
|
||||
}
|
||||
|
||||
get selectedIds() {
|
||||
return this.$selection.map($cell => getCellIdFromParams(getIdByCell($cell)));
|
||||
}
|
||||
|
||||
addCellToSelection($target: Dom) {
|
||||
this.$selection.push($target);
|
||||
this.currentSelectedCell = $target;
|
||||
this.selectHeader($target);
|
||||
|
||||
SelectionManager.addSelectedCellClass($target);
|
||||
}
|
||||
|
||||
private set currentSelectedCell($cell: Dom | null) {
|
||||
this.removeCurrentCellClass();
|
||||
if ($cell) {
|
||||
this.$currentSelectedCell = $cell;
|
||||
this.addCurrentCellClass();
|
||||
}
|
||||
}
|
||||
|
||||
selectFromTo($from: Dom, $cell: Dom) {
|
||||
this.clearAllSelection(false);
|
||||
|
||||
const startCellParams = getParamsFromCellId($from.data.id || startCellId);
|
||||
const selectedCellParams = getParamsFromCellId($cell.data.id || startCellId);
|
||||
|
||||
const startCol = Math.min(startCellParams.col, selectedCellParams.col);
|
||||
const endCol = Math.max(startCellParams.col, selectedCellParams.col);
|
||||
const startRow = Math.min(startCellParams.row, selectedCellParams.row);
|
||||
const endRow = Math.max(startCellParams.row, selectedCellParams.row);
|
||||
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
for (let col = startCol; col <= endCol; col++) {
|
||||
const $target = $(`[data-id="${row}:${col}"]`);
|
||||
this.addCellToSelection($target);
|
||||
}
|
||||
}
|
||||
|
||||
this.rootTable.focusManager.focusOnTable();
|
||||
}
|
||||
|
||||
selectCell($cell: Dom) {
|
||||
this.clearAllSelection();
|
||||
this.rootTable.focusManager.focusOnTable();
|
||||
this.$selection = [$cell];
|
||||
this.currentSelectedCell = $cell;
|
||||
this.$firstSelectedCell = $cell;
|
||||
this.selectHeader($cell);
|
||||
|
||||
this.rootTable.emitSelectCallback($cell);
|
||||
|
||||
SelectionManager.addSelectedCellClass($cell);
|
||||
}
|
||||
|
||||
selectCells($cells: Dom[]) {
|
||||
this.clearAllSelection();
|
||||
$cells.forEach($cell => this.addCellToSelection($cell));
|
||||
}
|
||||
|
||||
addGroupToSelectionById(cellID: { col: number | string, row: number | string }) {
|
||||
if (this.$selection.length === 1) {
|
||||
this.$firstSelectedCell = this.$selection[0];
|
||||
}
|
||||
|
||||
const { col, row } = cellID;
|
||||
const $cell = $(`[data-id="${row}:${col}"]`);
|
||||
const $lastCell = this.$selection[this.$selection.length - 1];
|
||||
|
||||
if (!$lastCell.isExist || !this.$firstSelectedCell?.isExist) {
|
||||
this.selectCell($lastCell);
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectFromTo(this.$firstSelectedCell, $cell);
|
||||
|
||||
this.$currentSelectedCell = $cell;
|
||||
}
|
||||
|
||||
clearAllSelection(clearFirstSelectedCell = true) {
|
||||
this.$selection?.forEach($cell => SelectionManager.removeSelectedCellClass($cell));
|
||||
this.$selection = [];
|
||||
this.currentSelectedCell = null;
|
||||
if (clearFirstSelectedCell) this.$firstSelectedCell = null;
|
||||
this.clearHeaderSelection();
|
||||
this.rootTable.focusManager?.resetFocus();
|
||||
}
|
||||
|
||||
selectHeadRowCol($target: Dom) {
|
||||
const row = $target.closest('[data-header="row"]');
|
||||
const col = $target.closest('[data-header="col"]');
|
||||
const resizer = $target.closest('[data-resize]');
|
||||
|
||||
if (resizer?.isExist) return;
|
||||
|
||||
let $cells: Dom[] = [];
|
||||
if (row?.isExist) {
|
||||
const cells = row.closest('[data-row]')?.findAll('[data-type="cell"]');
|
||||
if (!cells) return;
|
||||
|
||||
$cells = Array.from(cells).map(cell => $(cell as HTMLElement));
|
||||
} else if (col?.isExist) {
|
||||
const colNumber = col.data.col;
|
||||
const columns = this.rootTable.$root.findAll(`[data-col="${colNumber}"]`);
|
||||
|
||||
$cells = Array.from(columns).filter(el => !col.isEqual(el as HTMLElement)).map(el => $(el as HTMLElement));
|
||||
}
|
||||
|
||||
this.selectCells($cells);
|
||||
this.currentSelectedCell = $cells[0];
|
||||
this.$currentSelectedCell?.isExist && this.rootTable.emitSelectCallback(this.$currentSelectedCell);
|
||||
}
|
||||
|
||||
selectHeader($cell: Dom) {
|
||||
if (!$cell.isExist) return;
|
||||
|
||||
const { headerCol, headerRow } = this.findHeadOfCell($cell);
|
||||
|
||||
headerRow?.addClass(SelectionManager.selectedClassName);
|
||||
headerCol?.addClass(SelectionManager.selectedClassName);
|
||||
}
|
||||
|
||||
clearHeaderSelection() {
|
||||
this.rootTable.$root.findAll('[data-header]').forEach(header => {
|
||||
header.classList.remove(SelectionManager.selectedClassName);
|
||||
});
|
||||
}
|
||||
|
||||
getNeighbourCellBySide(side: 'left' | 'right' | 'down' | 'up', $cell = this.$currentSelectedCell): Dom | undefined {
|
||||
if (!$cell?.isExist) {
|
||||
console.error('Нет стартовой ячейки');
|
||||
return;
|
||||
}
|
||||
|
||||
let { row, col } = getIdByCell($cell);
|
||||
if (row === undefined || col === undefined) return;
|
||||
|
||||
switch (side) {
|
||||
case 'down':
|
||||
row++;
|
||||
break;
|
||||
|
||||
case 'up':
|
||||
row--;
|
||||
break;
|
||||
|
||||
case 'left':
|
||||
col--;
|
||||
break;
|
||||
|
||||
case 'right':
|
||||
col++;
|
||||
break;
|
||||
|
||||
default: break;
|
||||
}
|
||||
|
||||
const $neighbourCell = getCellById({ col, row });
|
||||
if (!$neighbourCell || !$neighbourCell.isExist) {
|
||||
console.error('Не получилось найти ячейку');
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME
|
||||
// eslint-disable-next-line consistent-return
|
||||
return $neighbourCell;
|
||||
}
|
||||
|
||||
// TODO remove
|
||||
moveSelectionTo(side: 'left' | 'right' | 'down' | 'up') {
|
||||
const $current = this.$currentSelectedCell || this.rootTable.focusManager.$currentFocusedCell || this.$currentSelectedCell?.[0];
|
||||
if (!$current?.isExist) return;
|
||||
|
||||
const $movingCell = this.getNeighbourCellBySide(side, $current);
|
||||
if (!$movingCell?.isExist || !$movingCell) {
|
||||
console.error('Ошибка, не найдена ячейка для перемещения');
|
||||
return;
|
||||
}
|
||||
this.selectCell($movingCell);
|
||||
}
|
||||
|
||||
findHeadOfCell($cell: Dom): { headerRow: Dom | undefined, headerCol: Dom | undefined } {
|
||||
const headerRow = $cell.closest('[data-row]')?.find("[data-header='row']");
|
||||
const headerCol = this.rootTable.$root.find(`[data-col="${$cell.data.id?.split(':')[1]}"]`);
|
||||
|
||||
return { headerRow, headerCol };
|
||||
}
|
||||
|
||||
onMouseDownHandler(event: MouseEvent) {
|
||||
const target = $(event.target);
|
||||
const header = target.closest('[data-header]');
|
||||
const resizer = target.closest('[data-resize]');
|
||||
event.preventDefault();
|
||||
|
||||
if (header?.isExist && !isCell2(target) && !resizer?.isExist) {
|
||||
this.selectHeadRowCol(header);
|
||||
return;
|
||||
}
|
||||
if (!isCell2(target)) return;
|
||||
if (this.$currentSelectedCell && target.isEqual(this.$currentSelectedCell)) return;
|
||||
|
||||
this.isMouseDowned = true;
|
||||
|
||||
switch (true) {
|
||||
case event.ctrlKey:
|
||||
this.addCellToSelection(target);
|
||||
break;
|
||||
|
||||
case event.shiftKey:
|
||||
if (!this.$currentSelectedCell) this.selectCell(target);
|
||||
else {
|
||||
const id = getIdByCell(target);
|
||||
if (!id) return;
|
||||
const { row, col } = id;
|
||||
if (row === undefined || col === undefined) return;
|
||||
|
||||
this.addGroupToSelectionById({ row, col });
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default: this.selectCell(target);
|
||||
}
|
||||
}
|
||||
|
||||
onKeyDownHandler(event: KeyboardEvent) {
|
||||
if (!isSelectionKey(event.key) || this.rootTable.focusManager.$currentFocusedCell?.isExist) {
|
||||
// TODO improve check
|
||||
if (event.key.length === 1) {
|
||||
this.$currentSelectedCell && this.rootTable.focusManager.focusCell(this.$currentSelectedCell);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
let $cell: Dom | undefined;
|
||||
let side: 'left' | 'right' | 'down' | 'up' = 'right';
|
||||
|
||||
switch (event.key) {
|
||||
case 'Shift':
|
||||
case 'Control':
|
||||
return;
|
||||
|
||||
case 'ArrowDown':
|
||||
side = 'down';
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
side = 'up';
|
||||
break;
|
||||
|
||||
case 'ArrowLeft':
|
||||
side = 'left';
|
||||
break;
|
||||
|
||||
case 'ArrowRight':
|
||||
side = 'right';
|
||||
break;
|
||||
|
||||
case 'Enter':
|
||||
side = event.shiftKey ? 'up' : 'down';
|
||||
break;
|
||||
|
||||
case 'Tab':
|
||||
side = event.shiftKey ? 'left' : 'right';
|
||||
break;
|
||||
|
||||
case 'Delete': {
|
||||
this.$selection.forEach($el => {
|
||||
this.rootTable.updateTextInCell('', $el);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
default: break;
|
||||
}
|
||||
|
||||
if (!side) return;
|
||||
|
||||
// $cell = this.getNeighbourCellBySide(side);
|
||||
$cell = this.getNeighbourCellBySide(side, this.$currentSelectedCell || this.$firstSelectedCell);
|
||||
|
||||
if (event.ctrlKey && $cell?.isExist) {
|
||||
this.addCellToSelection($cell);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.shiftKey && $cell?.isExist && event.key !== 'Enter' && event.key !== 'Tab') {
|
||||
const id = getIdByCell($cell);
|
||||
if (!id) return;
|
||||
|
||||
const { row, col } = id;
|
||||
if (row === undefined || col === undefined) return;
|
||||
|
||||
this.addGroupToSelectionById({ row, col });
|
||||
return;
|
||||
}
|
||||
|
||||
$cell = this.getNeighbourCellBySide(side, this.$firstSelectedCell || this.$currentSelectedCell);
|
||||
if (!$cell || !$cell.isExist) return;
|
||||
|
||||
this.selectCell($cell);
|
||||
}
|
||||
|
||||
doubleClickHandler(event: MouseEvent) {
|
||||
const target = $(event.target);
|
||||
if (!isCell2(target)) return;
|
||||
|
||||
this.rootTable.focusManager.focusCell(target);
|
||||
this.rootTable.updateTextInCell(target.dataValue, target, true);
|
||||
}
|
||||
|
||||
onMouseOverHandler(event: MouseEvent) {
|
||||
if (!this.isMouseDowned) return;
|
||||
const $target = $((event as any).toElement);
|
||||
|
||||
if (this.$firstSelectedCell?.isExist && isCell2($target)) {
|
||||
this.selectFromTo(this.$firstSelectedCell, $target);
|
||||
this.$currentSelectedCell = $target;
|
||||
}
|
||||
}
|
||||
|
||||
onMouseUpHandler() {
|
||||
this.isMouseDowned = false;
|
||||
}
|
||||
|
||||
applyStyle(style: Partial<CSSStyleDeclaration>) {
|
||||
this.$selection.forEach($cell => {
|
||||
$cell.css(style);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,38 +1,36 @@
|
||||
import { ContextSelectType } from 'components/ContextMenu/ContextMenu';
|
||||
import { BaseComponentOption } from 'components/excel/Excel';
|
||||
import { FocusManager } from 'components/table/FocusManager';
|
||||
import { SelectionManager } from 'components/table/SelectionManager';
|
||||
import { startCellId } from 'components/table/table.functions';
|
||||
import * as actions from 'redux/action-creators';
|
||||
import { $, Dom } from 'core/Dom';
|
||||
import { ExcelComponent } from 'core/ExcelComponent';
|
||||
import { TableSelection } from 'components/table/TableSelection';
|
||||
import {
|
||||
addCol,
|
||||
addRow,
|
||||
changeCurrentStyles,
|
||||
changeCurrentText,
|
||||
addRow, changeCurrentStyles, changeCurrentText,
|
||||
changeTableSize, removeColFromTable,
|
||||
removeRowFromTable,
|
||||
} from 'redux/action-creators';
|
||||
import { createTable } from 'components/table/table.template';
|
||||
import { TableSizeType } from 'redux/types';
|
||||
import { initialStyleState } from 'src/constants';
|
||||
import { getCellId, parse } from 'core/utils';
|
||||
import { parse } from 'core/utils';
|
||||
import { resizeHandler } from 'components/table/handlers/table.resize';
|
||||
import { selectHandler } from 'components/table/handlers/table.select.handler';
|
||||
import { initialStyleState } from 'src/constants';
|
||||
|
||||
export class Table extends ExcelComponent {
|
||||
static className = 'excel__table';
|
||||
|
||||
private selection: TableSelection;
|
||||
private isMouseDowned: boolean;
|
||||
private tableResizing: boolean;
|
||||
public tableSize: TableSizeType;
|
||||
selectionManager: SelectionManager;
|
||||
focusManager: FocusManager;
|
||||
|
||||
constructor($root: Dom, options: BaseComponentOption) {
|
||||
super($root, {
|
||||
...options,
|
||||
name: 'Table',
|
||||
eventListeners: ['mousedown', 'keydown', 'input', 'mouseover', 'mouseup', 'contextmenu'],
|
||||
eventListeners: ['mousedown', 'keydown', 'input', 'mouseover', 'mouseup', 'contextmenu', 'dblclick', 'focusout'],
|
||||
});
|
||||
}
|
||||
|
||||
@ -41,10 +39,10 @@ export class Table extends ExcelComponent {
|
||||
}
|
||||
|
||||
beforeRender() {
|
||||
this.selection = new TableSelection(this);
|
||||
this.tableResizing = false;
|
||||
this.isMouseDowned = false;
|
||||
this.tableSize = this.getTableSize();
|
||||
this.selectionManager = new SelectionManager(this);
|
||||
this.focusManager = new FocusManager(this);
|
||||
}
|
||||
|
||||
afterRender() {
|
||||
@ -53,7 +51,7 @@ export class Table extends ExcelComponent {
|
||||
this.initTable();
|
||||
|
||||
this.$onEventFromObserver('formula:input', this.updateTextInCell);
|
||||
this.$onEventFromObserver('formula:enter-press', () => this.selection.$currentCell.focus());
|
||||
// this.$onEventFromObserver('formula:enter-press', () => this.selection.$currentCell.focus());
|
||||
this.$onEventFromObserver('toolbar:applyStyle', this.updateCurrentStyles);
|
||||
this.$onEventFromObserver('toolbar:add-row', this.addNewRowHandler);
|
||||
this.$onEventFromObserver('toolbar:remove-row', this.removeRowHandler);
|
||||
@ -129,24 +127,24 @@ export class Table extends ExcelComponent {
|
||||
}
|
||||
|
||||
initStartCellFocus() {
|
||||
const $cell = this.$root.find(`[data-id="${startCellId}"]`);
|
||||
this.selection.select($cell);
|
||||
|
||||
this.$emitEventToObserver('table:select-cell', $cell);
|
||||
// const $cell = this.$root.find(`[data-id="${startCellId}"]`);
|
||||
// this.selection.select($cell);
|
||||
//
|
||||
// this.$emitEventToObserver('table:select-cell', $cell);
|
||||
}
|
||||
|
||||
emitSelectCallback() {
|
||||
this.$emitEventToObserver('table:select-cell', this.selection.$currentCell);
|
||||
emitSelectCallback($cell: Dom) {
|
||||
this.$emitEventToObserver('table:select-cell', $cell);
|
||||
|
||||
const styles = this.selection.$currentCell?.getStyles(Object.keys(initialStyleState));
|
||||
const styles = $cell.getStyles(Object.keys(initialStyleState));
|
||||
|
||||
this.dispatchToStore(changeCurrentStyles(styles));
|
||||
this.dispatchToStore(changeCurrentText(this.selection.$currentCell.text));
|
||||
this.dispatchToStore(changeCurrentText($cell.dataValue));
|
||||
}
|
||||
|
||||
async resizeTable(event: MouseEvent) {
|
||||
try {
|
||||
if (!$(event.target).closest('[data-resize]').isExist) return;
|
||||
if (!$(event.target).closest('[data-resize]')?.isExist) return;
|
||||
this.tableResizing = true;
|
||||
const resizeData = await resizeHandler(this.$root, event);
|
||||
this.dispatchToStore(actions.tableResize(resizeData));
|
||||
@ -157,23 +155,27 @@ export class Table extends ExcelComponent {
|
||||
}
|
||||
|
||||
updateCurrentStyles = (style: Partial<CSSStyleDeclaration>) => {
|
||||
this.selection.applyStyle(style);
|
||||
this.selectionManager.applyStyle(style);
|
||||
this.dispatchToStore(actions.applyStyle({
|
||||
value: style,
|
||||
ids: this.selection.selectedIds,
|
||||
ids: this.selectionManager.selectedIds,
|
||||
}));
|
||||
};
|
||||
|
||||
updateTextInCell = (text: string, $cell = this.selection.$focusedCell) => {
|
||||
$cell.attr('data-value', text);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
$cell.text = parse(text);
|
||||
updateTextInCell = (text: string, $cell?: Dom, changeVisibleText?: boolean) => {
|
||||
if (!$cell || !$cell.isExist) return;
|
||||
|
||||
this.dispatchToStore(actions.changeText({
|
||||
text,
|
||||
id: $cell.data.id || startCellId,
|
||||
}));
|
||||
this.selection.focusToCell($cell);
|
||||
if (!changeVisibleText) {
|
||||
$cell.attr('data-value', text);
|
||||
|
||||
this.dispatchToStore(actions.changeText({
|
||||
text: $cell.dataValue.toString(),
|
||||
id: $cell.data.id || startCellId,
|
||||
}));
|
||||
} else {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
$cell.text = text || '';
|
||||
}
|
||||
};
|
||||
|
||||
addNewRowHandler = () => {
|
||||
@ -190,12 +192,12 @@ export class Table extends ExcelComponent {
|
||||
}
|
||||
|
||||
removeRowHandler = () => {
|
||||
const cell = this.selection.$focusedCell;
|
||||
const cellId = getCellId(cell);
|
||||
if (!cellId) return;
|
||||
|
||||
const { row } = cellId;
|
||||
this.removeColRow('row', +row);
|
||||
// const cell = this.selection.$focusedCell;
|
||||
// const cellId = getCellId(cell);
|
||||
// if (!cellId) return;
|
||||
//
|
||||
// const { row } = cellId;
|
||||
// this.removeColRow('row', +row);
|
||||
};
|
||||
|
||||
removeColRow = (type: 'col' | 'row', index: number) => {
|
||||
@ -215,7 +217,6 @@ export class Table extends ExcelComponent {
|
||||
const idx = target.closest('[data-row]').data.row;
|
||||
if (!idx) return;
|
||||
this.addNewColRow('row', 'before', +idx);
|
||||
// this.selection.selectByCellId({ col: 2, row: 2 });
|
||||
break;
|
||||
}
|
||||
|
||||
@ -261,33 +262,43 @@ export class Table extends ExcelComponent {
|
||||
};
|
||||
|
||||
onMousedown(event: MouseEvent) {
|
||||
this.isMouseDowned = true;
|
||||
selectHandler(event, this.selection, this.emitSelectCallback.bind(this));
|
||||
this.resizeTable(event);
|
||||
this.selectionManager.onMouseDownHandler(event);
|
||||
}
|
||||
|
||||
onKeydown(event: KeyboardEvent) {
|
||||
selectHandler(event, this.selection, this.emitSelectCallback.bind(this));
|
||||
this.selectionManager.onKeyDownHandler(event);
|
||||
}
|
||||
|
||||
onInput(event: InputEvent) {
|
||||
this.updateTextInCell((event.target as HTMLElement).innerText);
|
||||
const $target = $(event.target);
|
||||
if (!this.focusManager.$currentFocusedCell) return;
|
||||
|
||||
this.updateTextInCell($target.text, this.focusManager.$currentFocusedCell);
|
||||
}
|
||||
|
||||
onMouseover(event: MouseEvent) {
|
||||
this.isMouseDowned && !this.tableResizing && selectHandler(event, this.selection);
|
||||
this.selectionManager.onMouseOverHandler(event);
|
||||
}
|
||||
|
||||
onMouseup() {
|
||||
this.isMouseDowned = false;
|
||||
this.selectionManager.onMouseUpHandler();
|
||||
}
|
||||
|
||||
onContextmenu(event: MouseEvent) {
|
||||
const $target = $(event.target);
|
||||
|
||||
this.selection.clearSelection();
|
||||
this.selection.selectHeadRowCol($target);
|
||||
this.selectionManager.clearAllSelection();
|
||||
this.selectionManager.selectHeadRowCol($target);
|
||||
|
||||
this.$emitEventToObserver('table:contextmenu', event);
|
||||
}
|
||||
|
||||
onDblclick(event: MouseEvent) {
|
||||
this.selectionManager.doubleClickHandler(event);
|
||||
}
|
||||
|
||||
onFocusout(event: FocusEvent) {
|
||||
this.focusManager.onFocusOut(event);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,165 +0,0 @@
|
||||
import { Table } from 'components/table/Table';
|
||||
import { $, Dom } from 'core/Dom';
|
||||
import { getParamsFromCellId, startCellId } from 'components/table/table.functions';
|
||||
import { initialStyleState } from 'src/constants';
|
||||
|
||||
export class TableSelection {
|
||||
static selectedClassName = 'selected';
|
||||
public selectedCellsGroup: Dom[];
|
||||
// TODO remove focus, when selection
|
||||
// $focusedCell and $currentCell can be different, f.e. if select cells with Shift key, currentCell
|
||||
// will be last cell, focusedCell will start cell, and can be different from selectedCellsGroup first item
|
||||
public $currentCell: Dom;
|
||||
public $focusedCell: Dom;
|
||||
public rootTable: Table;
|
||||
|
||||
constructor(rootTable: Table) {
|
||||
this.selectedCellsGroup = [];
|
||||
this.rootTable = rootTable;
|
||||
}
|
||||
|
||||
get selectedIds() {
|
||||
return this.selectedCellsGroup.map(el => el.data.id);
|
||||
}
|
||||
|
||||
// TODO make a focus manager
|
||||
focusToCell($cell: Dom) {
|
||||
try {
|
||||
this.$focusedCell = $cell;
|
||||
|
||||
const range = new Range();
|
||||
const node = $cell.$el;
|
||||
|
||||
range.setStartAfter(node.childNodes[node.childNodes.length - 1]);
|
||||
|
||||
window.getSelection()?.removeAllRanges();
|
||||
window.getSelection()?.addRange(range);
|
||||
} catch (e) {
|
||||
$cell.$el.focus();
|
||||
}
|
||||
}
|
||||
|
||||
select($cell: Dom) {
|
||||
this.clearSelection();
|
||||
this.selectedCellsGroup = [$cell];
|
||||
this.$currentCell = $cell;
|
||||
this.$focusedCell = $cell;
|
||||
$cell.addClass(TableSelection.selectedClassName);
|
||||
this.focusToCell($cell);
|
||||
this.selectHeader($cell);
|
||||
}
|
||||
|
||||
selectByCellId(cellID: { col: number, row: number }) {
|
||||
let { col, row } = cellID;
|
||||
|
||||
if (col <= 0) col = 0;
|
||||
if (row <= 0) row = 0;
|
||||
|
||||
this.select($(`[data-id="${row}:${col}"]`));
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this.selectedCellsGroup.forEach(el => el?.removeClass(TableSelection.selectedClassName));
|
||||
this.$currentCell?.removeClass(TableSelection.selectedClassName);
|
||||
this.clearHeaderSelection();
|
||||
this.selectedCellsGroup = [];
|
||||
this.rootTable.updateCurrentStyles(initialStyleState);
|
||||
}
|
||||
|
||||
selectFromTo($from: Dom, $cell: Dom) {
|
||||
const startCellParams = getParamsFromCellId($from.data.id || startCellId);
|
||||
const selectedCellParams = getParamsFromCellId($cell.data.id || startCellId);
|
||||
|
||||
const startCol = Math.min(startCellParams.col, selectedCellParams.col);
|
||||
const endCol = Math.max(startCellParams.col, selectedCellParams.col);
|
||||
const startRow = Math.min(startCellParams.row, selectedCellParams.row);
|
||||
const endRow = Math.max(startCellParams.row, selectedCellParams.row);
|
||||
|
||||
this.clearSelection();
|
||||
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
for (let col = startCol; col <= endCol; col++) {
|
||||
const $target = $(`[data-id="${row}:${col}"]`);
|
||||
this.addCellToSelection($target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectGroupies($cells: Dom[]) {
|
||||
this.clearSelection();
|
||||
$cells.forEach($cell => this.addCellToSelection($cell));
|
||||
}
|
||||
|
||||
addCellToSelection($cell: Dom) {
|
||||
this.selectedCellsGroup.push($cell);
|
||||
$cell.addClass(TableSelection.selectedClassName);
|
||||
this.selectHeader($cell);
|
||||
}
|
||||
|
||||
addGroupToSelectionById(cellID: { col: number, row: number }) {
|
||||
if (this.selectedCellsGroup.length === 1) {
|
||||
this.$focusedCell = this.selectedCellsGroup[0];
|
||||
}
|
||||
const { col, row } = cellID;
|
||||
const $cell = $(`[data-id="${row}:${col}"]`);
|
||||
const $lastCell = this.selectedCellsGroup[this.selectedCellsGroup.length - 1];
|
||||
|
||||
if (!$lastCell.isExist) {
|
||||
this.select($lastCell);
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectFromTo(this.$focusedCell, $cell);
|
||||
|
||||
this.$currentCell = $cell;
|
||||
}
|
||||
|
||||
applyStyle(style: Partial<CSSStyleDeclaration>) {
|
||||
this.selectedCellsGroup.forEach(el => el.css(style));
|
||||
}
|
||||
|
||||
selectHeader($cell: Dom) {
|
||||
if (!$cell) return;
|
||||
|
||||
const { headerCol, headerRow } = this.findHeadOfCell($cell);
|
||||
|
||||
headerRow.addClass(TableSelection.selectedClassName);
|
||||
headerCol.addClass(TableSelection.selectedClassName);
|
||||
}
|
||||
|
||||
clearHeaderSelection() {
|
||||
this.rootTable.$root.findAll('[data-header]').forEach(header => {
|
||||
header.classList.remove(TableSelection.selectedClassName);
|
||||
});
|
||||
}
|
||||
|
||||
findHeadOfCell($cell: Dom): { headerRow: Dom, headerCol: Dom } {
|
||||
const headerRow = $cell.closest('[data-row]').find("[data-header='row']");
|
||||
const headerCol = this.rootTable.$root.find(`[data-col="${$cell.data.id?.split(':')[1]}"]`);
|
||||
|
||||
return { headerRow, headerCol };
|
||||
}
|
||||
|
||||
isCellInSelection($cell: Dom) {
|
||||
return this.selectedCellsGroup.includes($cell);
|
||||
}
|
||||
|
||||
selectHeadRowCol($target: Dom) {
|
||||
const row = $target.closest('[data-header="row"]');
|
||||
const col = $target.closest('[data-header="col"]');
|
||||
const resizer = $target.closest('[data-resize]');
|
||||
|
||||
if (row.$el && !resizer.$el) {
|
||||
const cells = row.closest('[data-row]').findAll('[data-type="cell"]');
|
||||
const $cells = Array.from(cells).map(cell => $(cell as HTMLElement));
|
||||
|
||||
this.selectGroupies($cells);
|
||||
} else if (col.$el && !resizer.$el) {
|
||||
const colNumber = col.data.col;
|
||||
const columns = this.rootTable.$root.findAll(`[data-col="${colNumber}"]`);
|
||||
const $cells = Array.from(columns).filter(el => el !== col.$el).map(el => $(el as HTMLElement));
|
||||
|
||||
this.selectGroupies($cells);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,125 +0,0 @@
|
||||
import { $ } from 'core/Dom';
|
||||
import { TableSelection } from 'components/table/TableSelection';
|
||||
import { getParamsFromCellId, isCell, startCellId } from 'components/table/table.functions';
|
||||
|
||||
export function selectHandler(event: MouseEvent | KeyboardEvent, selection: TableSelection, callback?: () => void) {
|
||||
switch (event.type) {
|
||||
case 'mousedown': {
|
||||
onMouseDownHandler();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'keydown': {
|
||||
onKeyDownHandler();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'mouseover': {
|
||||
onMouseOverHandler();
|
||||
break;
|
||||
}
|
||||
|
||||
default: break;
|
||||
}
|
||||
|
||||
// Analog callback && callback();
|
||||
callback?.();
|
||||
|
||||
function onMouseDownHandler() {
|
||||
const target = $(event.target);
|
||||
|
||||
if (isCell(event)) {
|
||||
if (event.shiftKey) selection.selectFromTo(selection.$currentCell, target);
|
||||
else if (event.ctrlKey) selection.addCellToSelection(target);
|
||||
else selection.select(target);
|
||||
} else {
|
||||
selection.selectHeadRowCol(target);
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyDownHandler() {
|
||||
const { key } = event as KeyboardEvent;
|
||||
const handleKeys = [
|
||||
'ArrowDown',
|
||||
'ArrowUp',
|
||||
'ArrowRight',
|
||||
'ArrowLeft',
|
||||
'Enter',
|
||||
'Tab',
|
||||
'Delete',
|
||||
];
|
||||
|
||||
if (!selection?.$currentCell || !handleKeys.includes(key)) return;
|
||||
|
||||
// If something goes wrong, go to start line
|
||||
const currentCellId = selection.$currentCell.data.id || startCellId;
|
||||
let { row, col } = getParamsFromCellId(currentCellId);
|
||||
|
||||
switch (key) {
|
||||
case 'ArrowDown': {
|
||||
if (selection.rootTable.tableSize.row === row + 1) return;
|
||||
row++;
|
||||
break;
|
||||
}
|
||||
case 'ArrowUp': {
|
||||
row--;
|
||||
break;
|
||||
}
|
||||
case 'ArrowRight': {
|
||||
if (selection.rootTable.tableSize.col === col + 1) return;
|
||||
col++;
|
||||
break;
|
||||
}
|
||||
case 'ArrowLeft': {
|
||||
col--;
|
||||
break;
|
||||
}
|
||||
case 'Enter': {
|
||||
event.preventDefault();
|
||||
|
||||
if (event.shiftKey) row--;
|
||||
else row++;
|
||||
if (row === selection.rootTable.tableSize.row) selection.rootTable.addNewRowHandler();
|
||||
break;
|
||||
}
|
||||
case 'Tab': {
|
||||
event.preventDefault();
|
||||
|
||||
// Tab in selection must save focus in inner selection cells
|
||||
if (selection.selectedCellsGroup.length > 1) {
|
||||
const idxInSelection = selection.selectedCellsGroup.findIndex(el => el.$el === selection.$focusedCell.$el);
|
||||
const nextIdx = idxInSelection + 1 === selection.selectedCellsGroup.length ? 0 : idxInSelection + 1;
|
||||
|
||||
selection.focusToCell(selection.selectedCellsGroup[nextIdx]);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (event.shiftKey) col--;
|
||||
else col++;
|
||||
|
||||
break;
|
||||
}
|
||||
case 'Delete': {
|
||||
if (selection.selectedCellsGroup.length > 1) {
|
||||
event.preventDefault();
|
||||
|
||||
selection.selectedCellsGroup.forEach($cell => selection.rootTable.updateTextInCell('', $cell));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
|
||||
if (event.shiftKey && key !== 'Tab' && key !== 'Enter') selection.addGroupToSelectionById({ row, col });
|
||||
else selection.selectByCellId({ row, col });
|
||||
}
|
||||
|
||||
function onMouseOverHandler() {
|
||||
if (selection.$currentCell.$el) {
|
||||
// TODO find event type
|
||||
selection.selectFromTo(selection.$currentCell, $((event as any).toElement));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,18 @@
|
||||
import { $ } from 'core/Dom';
|
||||
import { SelectionManager } from 'components/table/SelectionManager';
|
||||
import { $, Dom } from 'core/Dom';
|
||||
|
||||
export function isCell(event: Event): boolean {
|
||||
return $(event.target).data.type === 'cell';
|
||||
}
|
||||
|
||||
export function isCell2($cell: Dom): boolean {
|
||||
return $cell.data.type === 'cell';
|
||||
}
|
||||
|
||||
export function isSelectionKey(key: string): boolean {
|
||||
return SelectionManager.selectionKeys.includes(key);
|
||||
}
|
||||
|
||||
export function getParamsFromCellId(cellId: string) {
|
||||
const row = +cellId.split(':')[0];
|
||||
const col = +cellId.split(':')[1];
|
||||
@ -11,4 +20,8 @@ export function getParamsFromCellId(cellId: string) {
|
||||
return { col, row };
|
||||
}
|
||||
|
||||
export function getCellIdFromParams(params: { row?: string, col?: string }) {
|
||||
return `${params.row}:${params.col}`;
|
||||
}
|
||||
|
||||
export const startCellId = '0:0';
|
||||
|
||||
@ -20,6 +20,7 @@ export class ComponentManager {
|
||||
|
||||
createComponent(Component: ComponentType, componentOptions: BaseComponentOption) {
|
||||
const $el = $.create('div', Component.className);
|
||||
$el.attr('tabindex', '-1');
|
||||
const componentInstance: ExcelComponent = new Component($el, componentOptions);
|
||||
|
||||
$el.html(componentInstance.toHTML());
|
||||
|
||||
@ -38,7 +38,7 @@ export class Dom implements DomClass {
|
||||
|
||||
get text() {
|
||||
if (this.$el.closest('input')) return (this.$el as HTMLInputElement).value;
|
||||
return (this.$el as HTMLElement).innerText;
|
||||
return (this.$el as HTMLElement).innerText || '';
|
||||
}
|
||||
|
||||
clear() {
|
||||
@ -70,10 +70,18 @@ export class Dom implements DomClass {
|
||||
this.$el.focus();
|
||||
}
|
||||
|
||||
blur() {
|
||||
this.$el.blur();
|
||||
}
|
||||
|
||||
get data() {
|
||||
return this.$el.dataset || '';
|
||||
}
|
||||
|
||||
get dataValue(): string {
|
||||
return this.$el.dataset.value || '';
|
||||
}
|
||||
|
||||
setData(name: string, value: string) {
|
||||
this.$el.setAttribute(`data-${name}`, value);
|
||||
}
|
||||
@ -124,7 +132,7 @@ export class Dom implements DomClass {
|
||||
}, {});
|
||||
}
|
||||
|
||||
attr(name: string, value: string) {
|
||||
attr(name: string, value?: string) {
|
||||
if (value !== undefined) {
|
||||
this.$el.setAttribute(name, value);
|
||||
return this;
|
||||
@ -144,6 +152,12 @@ export class Dom implements DomClass {
|
||||
get isExist(): boolean {
|
||||
return !!this.$el;
|
||||
}
|
||||
|
||||
isEqual($target: Dom | HTMLElement): boolean {
|
||||
if ($target instanceof Dom) return this.$el === $target.$el;
|
||||
|
||||
return this.$el === $target;
|
||||
}
|
||||
}
|
||||
|
||||
export function $(selector: SelectorType) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Dom } from 'core/Dom';
|
||||
import { $, Dom } from 'core/Dom';
|
||||
import { CallbackType, StateType } from 'redux/types';
|
||||
import { fontSizes } from 'src/constants';
|
||||
|
||||
@ -42,7 +42,7 @@ export function debounce(fn: CallbackType, wait: number) {
|
||||
}
|
||||
|
||||
export function parse(value: string) {
|
||||
if (value.startsWith('=')) {
|
||||
if (value.toString().startsWith('=')) {
|
||||
try {
|
||||
// eslint-disable-next-line no-eval
|
||||
return eval(value.slice(1));
|
||||
@ -68,12 +68,15 @@ export function getMethodNameByEventName(eventName: string): string {
|
||||
return `on${capitalize(eventName)}`;
|
||||
}
|
||||
|
||||
export function getCellId($cell: Dom): { row: string, col: string } | false {
|
||||
if (!$cell.isExist) return false;
|
||||
export function getIdByCell($cell: Dom): { row?: string, col?: string } {
|
||||
const id = $cell.data.id?.split(':');
|
||||
if (!Array.isArray(id)) return false;
|
||||
const [row, col] = id;
|
||||
return { row, col };
|
||||
return { row: id?.[0], col: id?.[1] };
|
||||
}
|
||||
|
||||
export function getCellById(id: { row: string | number, col: string | number }): Dom | false {
|
||||
const $cell = $(`[data-id="${id.row}:${id.col}"]`);
|
||||
if (!$cell.isExist) return false;
|
||||
return $cell;
|
||||
}
|
||||
|
||||
export function storageName(param: string) {
|
||||
|
||||
@ -60,8 +60,12 @@
|
||||
&.selected {
|
||||
border: none;
|
||||
outline: 2px solid $primary-color;
|
||||
background: #cccccc;
|
||||
z-index: 2;
|
||||
}
|
||||
&.current {
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
.col-resize{
|
||||
position: absolute;
|
||||
@ -71,7 +75,7 @@
|
||||
width: 4px;
|
||||
background: $primary-color;
|
||||
opacity: 0;
|
||||
z-index: 2;
|
||||
z-index: 50;
|
||||
|
||||
&:hover{
|
||||
cursor: col-resize;
|
||||
@ -86,7 +90,7 @@
|
||||
height: 4px;
|
||||
opacity: 0;
|
||||
background: $primary-color;
|
||||
z-index: 2;
|
||||
z-index: 50;
|
||||
|
||||
&:hover{
|
||||
cursor: row-resize;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user