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.$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 { 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);
}
}

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 {
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';

View File

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

View File

@ -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) {

View File

@ -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) {

View File

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