diff --git a/src/components/formula/Formula.ts b/src/components/formula/Formula.ts index 8349c2c..c37f4fd 100644 --- a/src/components/formula/Formula.ts +++ b/src/components/formula/Formula.ts @@ -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 || ''; }); } diff --git a/src/components/table/FocusManager.ts b/src/components/table/FocusManager.ts new file mode 100644 index 0000000..333b88f --- /dev/null +++ b/src/components/table/FocusManager.ts @@ -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); + } +} diff --git a/src/components/table/SelectionManager.ts b/src/components/table/SelectionManager.ts new file mode 100644 index 0000000..228d486 --- /dev/null +++ b/src/components/table/SelectionManager.ts @@ -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) { + this.$selection.forEach($cell => { + $cell.css(style); + }); + } +} diff --git a/src/components/table/Table.ts b/src/components/table/Table.ts index 0123ea9..c7417d6 100644 --- a/src/components/table/Table.ts +++ b/src/components/table/Table.ts @@ -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) => { - 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); + } } diff --git a/src/components/table/TableSelection.ts b/src/components/table/TableSelection.ts deleted file mode 100644 index 1e1bc98..0000000 --- a/src/components/table/TableSelection.ts +++ /dev/null @@ -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) { - 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); - } - } -} diff --git a/src/components/table/handlers/table.select.handler.ts b/src/components/table/handlers/table.select.handler.ts deleted file mode 100644 index 2777afd..0000000 --- a/src/components/table/handlers/table.select.handler.ts +++ /dev/null @@ -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)); - } - } -} diff --git a/src/components/table/table.functions.ts b/src/components/table/table.functions.ts index e9824be..f7eef61 100644 --- a/src/components/table/table.functions.ts +++ b/src/components/table/table.functions.ts @@ -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'; diff --git a/src/core/ComponentManager.ts b/src/core/ComponentManager.ts index 8e07419..898f82b 100644 --- a/src/core/ComponentManager.ts +++ b/src/core/ComponentManager.ts @@ -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()); diff --git a/src/core/Dom.ts b/src/core/Dom.ts index dcb370b..3912686 100644 --- a/src/core/Dom.ts +++ b/src/core/Dom.ts @@ -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) { diff --git a/src/core/utils.ts b/src/core/utils.ts index e9d73d7..ecc6f94 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -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) { diff --git a/src/styles/components/table.scss b/src/styles/components/table.scss index d8c8b35..15869db 100644 --- a/src/styles/components/table.scss +++ b/src/styles/components/table.scss @@ -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;