313 lines
9.2 KiB
TypeScript
313 lines
9.2 KiB
TypeScript
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 { parse } from 'core/utils';
|
|
import * as actions from 'redux/action-creators';
|
|
import { $, Dom } from 'core/Dom';
|
|
import { ExcelComponent } from 'core/ExcelComponent';
|
|
import {
|
|
addCol,
|
|
addRow, changeCurrentStyles, changeCurrentText,
|
|
changeTableSize, removeColFromTable,
|
|
removeRowFromTable,
|
|
} from 'redux/action-creators';
|
|
import { createTable } from 'components/table/table.template';
|
|
import { TableSizeType } from 'redux/types';
|
|
import { resizeHandler } from 'components/table/handlers/table.resize';
|
|
import { initialStyleState } from 'src/constants';
|
|
|
|
export class Table extends ExcelComponent {
|
|
static className = 'excel__table';
|
|
|
|
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', 'dblclick', 'focusout'],
|
|
});
|
|
}
|
|
|
|
toHTML(): string {
|
|
return createTable(this.tableSize.row, this.tableSize.col);
|
|
}
|
|
|
|
beforeRender() {
|
|
this.tableResizing = false;
|
|
this.tableSize = this.getTableSize();
|
|
this.selectionManager = new SelectionManager(this);
|
|
this.focusManager = new FocusManager(this);
|
|
}
|
|
|
|
afterRender() {
|
|
super.afterRender();
|
|
|
|
this.initTable();
|
|
|
|
this.$onEventFromObserver('formula:input', (text) => {
|
|
if (!this.focusManager.$currentFocusedCell) this.updateTextInCell(text, this.selectionManager.$currentSelectedCell, true);
|
|
else this.updateTextInCell(text, this.focusManager.$currentFocusedCell, true);
|
|
});
|
|
|
|
this.$onEventFromObserver('formula:enter-press', () => {
|
|
this.focusManager.focusCell(this.selectionManager.$currentSelectedCell);
|
|
this.updateTextInCell(this.selectionManager.$currentSelectedCell?.text, this.focusManager.$currentFocusedCell, false);
|
|
});
|
|
|
|
this.$onEventFromObserver('toolbar:applyStyle', this.updateCurrentStyles);
|
|
this.$onEventFromObserver('toolbar:add-row', this.addNewRowHandler);
|
|
this.$onEventFromObserver('toolbar:remove-row', this.removeRowHandler);
|
|
this.$onEventFromObserver('context-menu: select', this.contextMenuHandler);
|
|
}
|
|
|
|
getTableSize() {
|
|
const { tableSize: { col, row }, colState, rowState } = this.store.getState();
|
|
const maxRowFromState = Math.max(...Object.keys(rowState).map(el => +el));
|
|
const maxColFromState = Math.max(...Object.keys(colState).map(el => +el));
|
|
|
|
const normalTableSize = {
|
|
row: Math.max(maxRowFromState, row),
|
|
col: Math.max(maxColFromState, col),
|
|
};
|
|
|
|
if ((maxColFromState !== this.tableSize?.col) || (maxRowFromState !== this.tableSize?.row)) {
|
|
this.dispatchToStore(changeTableSize(normalTableSize));
|
|
}
|
|
|
|
return normalTableSize;
|
|
}
|
|
|
|
initTable() {
|
|
this.initTableSize();
|
|
this.initTableContentAndStyles();
|
|
this.initStartCellFocus();
|
|
}
|
|
|
|
initTableSize() {
|
|
const size = {
|
|
col: this.store.getState()?.colState,
|
|
row: this.store.getState()?.rowState,
|
|
};
|
|
|
|
this.initColSizes(this.$root, size.col);
|
|
this.initRowSizes(this.$root, size.row);
|
|
}
|
|
|
|
initRowSizes(rootElem: Dom, rowState = this.store.getState()?.rowState) {
|
|
if (!rootElem || !rootElem?.$el) return;
|
|
|
|
Object.keys(rowState).forEach(key => {
|
|
const rows = rootElem.findAll(`[data-row="${key}"]`);
|
|
rows.forEach(el => $(el as HTMLElement).css({ height: `${rowState[+key]}px` }));
|
|
});
|
|
}
|
|
|
|
initColSizes(rootElem: Dom, colState = this.store.getState()?.colState) {
|
|
if (!rootElem || !rootElem?.$el) return;
|
|
|
|
Object.keys(colState).forEach(key => {
|
|
const cols = rootElem.findAll(`[data-col="${key}"]`);
|
|
cols.forEach(el => $(el as HTMLElement).css({ width: `${colState[+key]}px` }));
|
|
});
|
|
}
|
|
|
|
initTableContentAndStyles() {
|
|
const tableState = this.store.getState();
|
|
const tableContent = tableState?.dataState;
|
|
const tableStyles = tableState?.stylesState;
|
|
|
|
Object.keys(tableContent).forEach(cellId => {
|
|
const $cell = this.$root.find(`[data-id="${cellId}"]`);
|
|
const styles = tableStyles[cellId];
|
|
|
|
if (!$cell.isExist) return;
|
|
|
|
$cell.text = parse(tableContent[cellId]) || '';
|
|
$cell.setData('value', tableContent[cellId]);
|
|
$cell.css(styles);
|
|
});
|
|
}
|
|
|
|
initStartCellFocus() {
|
|
// const $cell = this.$root.find(`[data-id="${startCellId}"]`);
|
|
// this.selection.select($cell);
|
|
//
|
|
// this.$emitEventToObserver('table:select-cell', $cell);
|
|
}
|
|
|
|
emitSelectCallback($cell: Dom) {
|
|
this.$emitEventToObserver('table:select-cell', $cell);
|
|
|
|
const styles = $cell.getStyles(Object.keys(initialStyleState));
|
|
|
|
this.dispatchToStore(changeCurrentStyles(styles));
|
|
this.dispatchToStore(changeCurrentText($cell.dataValue));
|
|
}
|
|
|
|
async resizeTable(event: MouseEvent) {
|
|
try {
|
|
if (!$(event.target).closest('[data-resize]')?.isExist) return;
|
|
this.tableResizing = true;
|
|
const resizeData = await resizeHandler(this.$root, event);
|
|
this.dispatchToStore(actions.tableResize(resizeData));
|
|
this.tableResizing = false;
|
|
} catch (e) {
|
|
console.warn('Resize error', e.message);
|
|
}
|
|
}
|
|
|
|
updateCurrentStyles = (style: Partial<CSSStyleDeclaration>) => {
|
|
this.selectionManager.applyStyle(style);
|
|
this.dispatchToStore(actions.applyStyle({
|
|
value: style,
|
|
ids: this.selectionManager.selectedIds,
|
|
}));
|
|
};
|
|
|
|
updateTextInCell = (text: string, $cell?: Dom, changeVisibleText?: boolean) => {
|
|
if (!$cell || !$cell.isExist) return;
|
|
|
|
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 = () => {
|
|
this.addNewColRow('row', 'after', this.tableSize.row);
|
|
};
|
|
|
|
addNewColRow(type: 'col' | 'row', position: 'after' | 'before', targetIndex: number) {
|
|
if (targetIndex === undefined) return;
|
|
type === 'col'
|
|
? this.dispatchToStore(addCol({ position, targetIndex }))
|
|
: this.dispatchToStore(addRow({ position, targetIndex }));
|
|
|
|
this.rerender();
|
|
}
|
|
|
|
removeRowHandler = () => {
|
|
// 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) => {
|
|
if (!type || !index) return;
|
|
|
|
type === 'col'
|
|
? this.dispatchToStore(removeColFromTable(index))
|
|
: this.dispatchToStore(removeRowFromTable(index));
|
|
this.rerender();
|
|
};
|
|
|
|
contextMenuHandler = (select: ContextSelectType) => {
|
|
const { event, target } = select;
|
|
|
|
switch (event) {
|
|
case 'add-row-before': {
|
|
const idx = target.closest('[data-row]').data.row;
|
|
if (!idx) return;
|
|
this.addNewColRow('row', 'before', +idx);
|
|
break;
|
|
}
|
|
|
|
case 'add-row-after': {
|
|
const idx = target.closest('[data-row]').data.row;
|
|
if (!idx) return;
|
|
this.addNewColRow('row', 'after', +idx);
|
|
break;
|
|
}
|
|
|
|
case 'remove-row': {
|
|
const idx = target.closest('[data-row]').data.row;
|
|
if (!idx) return;
|
|
|
|
this.removeColRow('row', +idx);
|
|
break;
|
|
}
|
|
|
|
case 'add-col-before': {
|
|
const idx = target.closest('[data-col]').data.col;
|
|
if (!idx) return;
|
|
this.addNewColRow('col', 'before', +idx);
|
|
break;
|
|
}
|
|
|
|
case 'add-col-after': {
|
|
const idx = target.closest('[data-col]').data.col;
|
|
if (!idx) return;
|
|
this.addNewColRow('col', 'after', +idx);
|
|
break;
|
|
}
|
|
|
|
case 'remove-col': {
|
|
const idx = target.closest('[data-col]').data.col;
|
|
if (!idx) return;
|
|
|
|
this.removeColRow('col', +idx);
|
|
break;
|
|
}
|
|
|
|
default: break;
|
|
}
|
|
};
|
|
|
|
onMousedown(event: MouseEvent) {
|
|
this.resizeTable(event);
|
|
this.selectionManager.onMouseDownHandler(event);
|
|
}
|
|
|
|
onKeydown(event: KeyboardEvent) {
|
|
this.selectionManager.onKeyDownHandler(event);
|
|
}
|
|
|
|
onInput(event: InputEvent) {
|
|
const $target = $(event.target);
|
|
if (!this.focusManager.$currentFocusedCell) return;
|
|
|
|
this.updateTextInCell($target.text, this.focusManager.$currentFocusedCell);
|
|
}
|
|
|
|
onMouseover(event: MouseEvent) {
|
|
this.selectionManager.onMouseOverHandler(event);
|
|
}
|
|
|
|
onMouseup() {
|
|
this.selectionManager.onMouseUpHandler();
|
|
}
|
|
|
|
onContextmenu(event: MouseEvent) {
|
|
const $target = $(event.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);
|
|
}
|
|
}
|