refactor, new selection/focus logic

This commit is contained in:
Sergey Krylov 2022-07-18 14:19:55 +05:00
parent 3bb80c1ddf
commit 0d17d35fe8
11 changed files with 564 additions and 351 deletions

View File

@ -29,7 +29,7 @@ export class Formula extends ExcelComponent {
this.formulaInput = this.$root.find('#formula-input'); this.formulaInput = this.$root.find('#formula-input');
this.$onEventFromObserver('table:select-cell', (cell: Dom) => { this.$onEventFromObserver('table:select-cell', (cell: Dom) => {
this.formulaInput.text = cell.data.value || ''; this.formulaInput.text = cell.dataValue || '';
}); });
} }

View 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);
}
}

View 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);
});
}
}

View File

@ -1,38 +1,36 @@
import { ContextSelectType } from 'components/ContextMenu/ContextMenu'; import { ContextSelectType } from 'components/ContextMenu/ContextMenu';
import { BaseComponentOption } from 'components/excel/Excel'; 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 { startCellId } from 'components/table/table.functions';
import * as actions from 'redux/action-creators'; import * as actions from 'redux/action-creators';
import { $, Dom } from 'core/Dom'; import { $, Dom } from 'core/Dom';
import { ExcelComponent } from 'core/ExcelComponent'; import { ExcelComponent } from 'core/ExcelComponent';
import { TableSelection } from 'components/table/TableSelection';
import { import {
addCol, addCol,
addRow, addRow, changeCurrentStyles, changeCurrentText,
changeCurrentStyles,
changeCurrentText,
changeTableSize, removeColFromTable, changeTableSize, removeColFromTable,
removeRowFromTable, removeRowFromTable,
} from 'redux/action-creators'; } from 'redux/action-creators';
import { createTable } from 'components/table/table.template'; import { createTable } from 'components/table/table.template';
import { TableSizeType } from 'redux/types'; import { TableSizeType } from 'redux/types';
import { initialStyleState } from 'src/constants'; import { parse } from 'core/utils';
import { getCellId, parse } from 'core/utils';
import { resizeHandler } from 'components/table/handlers/table.resize'; 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 { export class Table extends ExcelComponent {
static className = 'excel__table'; static className = 'excel__table';
private selection: TableSelection;
private isMouseDowned: boolean;
private tableResizing: boolean; private tableResizing: boolean;
public tableSize: TableSizeType; public tableSize: TableSizeType;
selectionManager: SelectionManager;
focusManager: FocusManager;
constructor($root: Dom, options: BaseComponentOption) { constructor($root: Dom, options: BaseComponentOption) {
super($root, { super($root, {
...options, ...options,
name: 'Table', 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() { beforeRender() {
this.selection = new TableSelection(this);
this.tableResizing = false; this.tableResizing = false;
this.isMouseDowned = false;
this.tableSize = this.getTableSize(); this.tableSize = this.getTableSize();
this.selectionManager = new SelectionManager(this);
this.focusManager = new FocusManager(this);
} }
afterRender() { afterRender() {
@ -53,7 +51,7 @@ export class Table extends ExcelComponent {
this.initTable(); this.initTable();
this.$onEventFromObserver('formula:input', this.updateTextInCell); 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:applyStyle', this.updateCurrentStyles);
this.$onEventFromObserver('toolbar:add-row', this.addNewRowHandler); this.$onEventFromObserver('toolbar:add-row', this.addNewRowHandler);
this.$onEventFromObserver('toolbar:remove-row', this.removeRowHandler); this.$onEventFromObserver('toolbar:remove-row', this.removeRowHandler);
@ -129,24 +127,24 @@ export class Table extends ExcelComponent {
} }
initStartCellFocus() { initStartCellFocus() {
const $cell = this.$root.find(`[data-id="${startCellId}"]`); // const $cell = this.$root.find(`[data-id="${startCellId}"]`);
this.selection.select($cell); // this.selection.select($cell);
//
this.$emitEventToObserver('table:select-cell', $cell); // this.$emitEventToObserver('table:select-cell', $cell);
} }
emitSelectCallback() { emitSelectCallback($cell: Dom) {
this.$emitEventToObserver('table:select-cell', this.selection.$currentCell); 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(changeCurrentStyles(styles));
this.dispatchToStore(changeCurrentText(this.selection.$currentCell.text)); this.dispatchToStore(changeCurrentText($cell.dataValue));
} }
async resizeTable(event: MouseEvent) { async resizeTable(event: MouseEvent) {
try { try {
if (!$(event.target).closest('[data-resize]').isExist) return; if (!$(event.target).closest('[data-resize]')?.isExist) return;
this.tableResizing = true; this.tableResizing = true;
const resizeData = await resizeHandler(this.$root, event); const resizeData = await resizeHandler(this.$root, event);
this.dispatchToStore(actions.tableResize(resizeData)); this.dispatchToStore(actions.tableResize(resizeData));
@ -157,23 +155,27 @@ export class Table extends ExcelComponent {
} }
updateCurrentStyles = (style: Partial<CSSStyleDeclaration>) => { updateCurrentStyles = (style: Partial<CSSStyleDeclaration>) => {
this.selection.applyStyle(style); this.selectionManager.applyStyle(style);
this.dispatchToStore(actions.applyStyle({ this.dispatchToStore(actions.applyStyle({
value: style, value: style,
ids: this.selection.selectedIds, ids: this.selectionManager.selectedIds,
})); }));
}; };
updateTextInCell = (text: string, $cell = this.selection.$focusedCell) => { updateTextInCell = (text: string, $cell?: Dom, changeVisibleText?: boolean) => {
$cell.attr('data-value', text); if (!$cell || !$cell.isExist) return;
// eslint-disable-next-line no-param-reassign
$cell.text = parse(text);
this.dispatchToStore(actions.changeText({ if (!changeVisibleText) {
text, $cell.attr('data-value', text);
id: $cell.data.id || startCellId,
})); this.dispatchToStore(actions.changeText({
this.selection.focusToCell($cell); text: $cell.dataValue.toString(),
id: $cell.data.id || startCellId,
}));
} else {
// eslint-disable-next-line no-param-reassign
$cell.text = text || '';
}
}; };
addNewRowHandler = () => { addNewRowHandler = () => {
@ -190,12 +192,12 @@ export class Table extends ExcelComponent {
} }
removeRowHandler = () => { removeRowHandler = () => {
const cell = this.selection.$focusedCell; // const cell = this.selection.$focusedCell;
const cellId = getCellId(cell); // const cellId = getCellId(cell);
if (!cellId) return; // if (!cellId) return;
//
const { row } = cellId; // const { row } = cellId;
this.removeColRow('row', +row); // this.removeColRow('row', +row);
}; };
removeColRow = (type: 'col' | 'row', index: number) => { removeColRow = (type: 'col' | 'row', index: number) => {
@ -215,7 +217,6 @@ export class Table extends ExcelComponent {
const idx = target.closest('[data-row]').data.row; const idx = target.closest('[data-row]').data.row;
if (!idx) return; if (!idx) return;
this.addNewColRow('row', 'before', +idx); this.addNewColRow('row', 'before', +idx);
// this.selection.selectByCellId({ col: 2, row: 2 });
break; break;
} }
@ -261,33 +262,43 @@ export class Table extends ExcelComponent {
}; };
onMousedown(event: MouseEvent) { onMousedown(event: MouseEvent) {
this.isMouseDowned = true;
selectHandler(event, this.selection, this.emitSelectCallback.bind(this));
this.resizeTable(event); this.resizeTable(event);
this.selectionManager.onMouseDownHandler(event);
} }
onKeydown(event: KeyboardEvent) { onKeydown(event: KeyboardEvent) {
selectHandler(event, this.selection, this.emitSelectCallback.bind(this)); this.selectionManager.onKeyDownHandler(event);
} }
onInput(event: InputEvent) { 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) { onMouseover(event: MouseEvent) {
this.isMouseDowned && !this.tableResizing && selectHandler(event, this.selection); this.selectionManager.onMouseOverHandler(event);
} }
onMouseup() { onMouseup() {
this.isMouseDowned = false; this.selectionManager.onMouseUpHandler();
} }
onContextmenu(event: MouseEvent) { onContextmenu(event: MouseEvent) {
const $target = $(event.target); const $target = $(event.target);
this.selection.clearSelection(); this.selectionManager.clearAllSelection();
this.selection.selectHeadRowCol($target); this.selectionManager.selectHeadRowCol($target);
this.$emitEventToObserver('table:contextmenu', event); this.$emitEventToObserver('table:contextmenu', event);
} }
onDblclick(event: MouseEvent) {
this.selectionManager.doubleClickHandler(event);
}
onFocusout(event: FocusEvent) {
this.focusManager.onFocusOut(event);
}
} }

View File

@ -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);
}
}
}

View File

@ -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));
}
}
}

View File

@ -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 { export function isCell(event: Event): boolean {
return $(event.target).data.type === 'cell'; 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) { export function getParamsFromCellId(cellId: string) {
const row = +cellId.split(':')[0]; const row = +cellId.split(':')[0];
const col = +cellId.split(':')[1]; const col = +cellId.split(':')[1];
@ -11,4 +20,8 @@ export function getParamsFromCellId(cellId: string) {
return { col, row }; return { col, row };
} }
export function getCellIdFromParams(params: { row?: string, col?: string }) {
return `${params.row}:${params.col}`;
}
export const startCellId = '0:0'; export const startCellId = '0:0';

View File

@ -20,6 +20,7 @@ export class ComponentManager {
createComponent(Component: ComponentType, componentOptions: BaseComponentOption) { createComponent(Component: ComponentType, componentOptions: BaseComponentOption) {
const $el = $.create('div', Component.className); const $el = $.create('div', Component.className);
$el.attr('tabindex', '-1');
const componentInstance: ExcelComponent = new Component($el, componentOptions); const componentInstance: ExcelComponent = new Component($el, componentOptions);
$el.html(componentInstance.toHTML()); $el.html(componentInstance.toHTML());

View File

@ -38,7 +38,7 @@ export class Dom implements DomClass {
get text() { get text() {
if (this.$el.closest('input')) return (this.$el as HTMLInputElement).value; if (this.$el.closest('input')) return (this.$el as HTMLInputElement).value;
return (this.$el as HTMLElement).innerText; return (this.$el as HTMLElement).innerText || '';
} }
clear() { clear() {
@ -70,10 +70,18 @@ export class Dom implements DomClass {
this.$el.focus(); this.$el.focus();
} }
blur() {
this.$el.blur();
}
get data() { get data() {
return this.$el.dataset || ''; return this.$el.dataset || '';
} }
get dataValue(): string {
return this.$el.dataset.value || '';
}
setData(name: string, value: string) { setData(name: string, value: string) {
this.$el.setAttribute(`data-${name}`, value); 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) { if (value !== undefined) {
this.$el.setAttribute(name, value); this.$el.setAttribute(name, value);
return this; return this;
@ -144,6 +152,12 @@ export class Dom implements DomClass {
get isExist(): boolean { get isExist(): boolean {
return !!this.$el; 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) { export function $(selector: SelectorType) {

View File

@ -1,4 +1,4 @@
import { Dom } from 'core/Dom'; import { $, Dom } from 'core/Dom';
import { CallbackType, StateType } from 'redux/types'; import { CallbackType, StateType } from 'redux/types';
import { fontSizes } from 'src/constants'; import { fontSizes } from 'src/constants';
@ -42,7 +42,7 @@ export function debounce(fn: CallbackType, wait: number) {
} }
export function parse(value: string) { export function parse(value: string) {
if (value.startsWith('=')) { if (value.toString().startsWith('=')) {
try { try {
// eslint-disable-next-line no-eval // eslint-disable-next-line no-eval
return eval(value.slice(1)); return eval(value.slice(1));
@ -68,12 +68,15 @@ export function getMethodNameByEventName(eventName: string): string {
return `on${capitalize(eventName)}`; return `on${capitalize(eventName)}`;
} }
export function getCellId($cell: Dom): { row: string, col: string } | false { export function getIdByCell($cell: Dom): { row?: string, col?: string } {
if (!$cell.isExist) return false;
const id = $cell.data.id?.split(':'); const id = $cell.data.id?.split(':');
if (!Array.isArray(id)) return false; return { row: id?.[0], col: id?.[1] };
const [row, col] = id; }
return { row, col };
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) { export function storageName(param: string) {

View File

@ -60,8 +60,12 @@
&.selected { &.selected {
border: none; border: none;
outline: 2px solid $primary-color; outline: 2px solid $primary-color;
background: #cccccc;
z-index: 2; z-index: 2;
} }
&.current {
background: #ffffff;
}
} }
.col-resize{ .col-resize{
position: absolute; position: absolute;
@ -71,7 +75,7 @@
width: 4px; width: 4px;
background: $primary-color; background: $primary-color;
opacity: 0; opacity: 0;
z-index: 2; z-index: 50;
&:hover{ &:hover{
cursor: col-resize; cursor: col-resize;
@ -86,7 +90,7 @@
height: 4px; height: 4px;
opacity: 0; opacity: 0;
background: $primary-color; background: $primary-color;
z-index: 2; z-index: 50;
&:hover{ &:hover{
cursor: row-resize; cursor: row-resize;