add column/row adding, removing

This commit is contained in:
Sergey Krylov 2022-07-17 16:50:22 +05:00
parent e12efe60b1
commit 3bb80c1ddf
24 changed files with 693 additions and 139 deletions

View File

@ -0,0 +1,124 @@
import { BaseComponentOption } from 'components/excel/Excel';
import { $, Dom } from 'core/Dom';
import { ExcelComponent } from 'core/ExcelComponent';
export type ContextSelectType = {
event: ContextEventType,
target: Dom,
};
type ContextEventType =
'add-row-before' |
'remove-row' |
'add-row-after' |
'add-col-before' |
'remove-col' |
'add-col-after';
export class ContextMenu extends ExcelComponent {
static className = 'excel__contextmenu_layer';
private contextMenu: Dom;
private contextType: string;
private contextFor: Dom;
constructor($root: Dom, options: BaseComponentOption) {
super($root, {
...options,
name: 'ContextMenu',
eventListeners: ['click', 'mouseup'],
});
this.$onEventFromObserver('table:contextmenu', this.initContext);
this.$onEventFromObserver('table:rerendercontext', this.rerender);
this.contextType = 'row';
}
toHTML(): string {
return this.createContextMenu();
}
afterRender() {
super.afterRender();
this.contextMenu = $('#excel__contextmenu');
}
transformContextMenuCoords(coords: { left: number, top: number }) {
this.contextMenu.css({
left: `${coords.left.toString()}px`,
top: `${coords.top.toString()}px`,
});
}
hideContextMenu() {
this.transformContextMenuCoords({ left: 0, top: -100 });
this.$root.removeClass('active');
}
initContext = (event: MouseEvent) => {
const $target = $(event.target);
if (!$target.closest('[data-header]').isExist) return;
event.preventDefault();
this.contextFor = $target;
if ($target.data.header && $target.data.header !== this.contextType) {
this.contextType = $target.data.header;
this.$root.html(this.createContextMenu());
this.contextMenu = $('#excel__contextmenu');
}
this.transformContextMenuCoords({ top: event.clientY, left: event.clientX });
this.$root.addClass('active');
};
emitSelectItem(contextItem: Dom) {
if (!contextItem.isExist) return;
const contextEvent = contextItem?.data.contextitem;
if (!contextEvent) return;
const select = {
target: this.contextFor,
event: contextEvent,
};
this.$emitEventToObserver('context-menu: select', select);
}
createContextMenu() {
if (this.contextType === 'row') {
return `
<div class="excel__contextmenu" id="excel__contextmenu">
<div class="excel__contextmenu_item" tabindex="-1" data-contextitem="add-row-before">Добавить строку сверху</div>
<div class="excel__contextmenu_item" tabindex="-1" data-contextitem="remove-row">Удалить строку</div>
<div class="excel__contextmenu_item" tabindex="-1" data-contextitem="add-row-after">Добавить строку снизу</div>
</div>
`;
}
return `
<div class="excel__contextmenu" id="excel__contextmenu">
<div class="excel__contextmenu_item" tabindex="-1" data-contextitem="add-col-before">Добавить столбец слева</div>
<div class="excel__contextmenu_item" tabindex="-1" data-contextitem="remove-col">Удалить столбец</div>
<div class="excel__contextmenu_item" tabindex="-1" data-contextitem="add-col-after">Добавить столбец справа</div>
</div>
`;
}
onClick(e: MouseEvent) {
const $target = $(e.target);
if ($target.closest('#excel__contextmenu').isExist) {
this.emitSelectItem($target);
return;
}
this.hideContextMenu();
}
onMouseup() {
this.hideContextMenu();
}
}

View File

@ -1,19 +1,26 @@
import { $ } from 'core/Dom';
import { ContextMenu } from 'components/ContextMenu/ContextMenu';
import { Formula } from 'components/formula/Formula';
import { Header } from 'components/header/Header';
import { Table } from 'components/table/Table';
import { Toolbar } from 'components/toolbar/Toolbar';
import { ComponentManager } from 'core/ComponentManager';
import { Observer } from 'core/Observer';
import { ExcelComponent } from 'core/ExcelComponent';
import { Store } from 'core/store/Store';
import { StoreSubscriber } from 'core/StoreSubscriber';
import { updateOpenDate } from 'redux/action-creators';
// TODO type for components array
interface ExcelOptionsType {
components: any[],
components: ComponentType[],
store: Store,
}
type BaseComponentOption = {
export type ComponentType = typeof Header | typeof Toolbar | typeof Formula | typeof Table | typeof ContextMenu;
export type BaseComponentOption = {
observer: Observer;
store: Store;
componentManager: ComponentManager;
};
export class Excel {
@ -21,44 +28,38 @@ export class Excel {
observer: Observer;
store: Store;
subscriber: StoreSubscriber;
componentManage: ComponentManager;
constructor(options: ExcelOptionsType) {
this.components = options.components;
this.observer = new Observer();
this.store = options.store;
this.components = options.components;
this.subscriber = new StoreSubscriber(this.store);
this.observer = new Observer();
this.componentManage = new ComponentManager();
}
getRoot() {
const $root = $.create('div', 'excel');
const componentOptions: BaseComponentOption = {
observer: this.observer,
store: this.store,
componentManager: this.componentManage,
};
this.components = this.components.map(Component => this.componentManage.createComponent(Component, componentOptions));
this.componentManage.addComponentsToRoot(this.components);
this.components = this.components.map(Component => {
const $el = $.create('div', Component.className);
const component: ExcelComponent = new Component($el, componentOptions);
$el.html(component.toHTML());
$root.append($el.$el);
return component;
});
return $root;
return this.componentManage.$rootExcelElement;
}
init() {
afterRender() {
this.subscriber.subscribeComponents(this.components);
this.components.forEach(component => component.init());
this.components.forEach(component => component.afterRender());
this.store.dispatchToStore(updateOpenDate(Date.now().toString()));
}
destroy() {
this.subscriber.unsubscribeFromStore();
this.components.forEach(component => component.destroy());
this.componentManage.destroyComponents();
}
}

View File

@ -1,12 +1,13 @@
import { BaseComponentOption } from 'components/excel/Excel';
import { Dom } from 'core/Dom';
import { ExcelComponent, ComponentOptionsType } from 'core/ExcelComponent';
import { ExcelComponent } from 'core/ExcelComponent';
export class Formula extends ExcelComponent {
static className = 'excel__formula';
private formulaInput: Dom;
constructor($root: Dom, options: ComponentOptionsType) {
constructor($root: Dom, options: BaseComponentOption) {
super($root, {
...options,
eventListeners: ['input', 'keydown'],
@ -22,8 +23,8 @@ export class Formula extends ExcelComponent {
`;
}
init() {
super.init();
afterRender() {
super.afterRender();
this.formulaInput = this.$root.find('#formula-input');

View File

@ -1,4 +1,5 @@
import { ComponentOptionsType, ExcelComponent } from 'core/ExcelComponent';
import { BaseComponentOption } from 'components/excel/Excel';
import { ExcelComponent } from 'core/ExcelComponent';
import * as actions from 'redux/action-creators';
import { $, Dom } from 'core/Dom';
import { ActiveRoute } from 'core/routes/ActiveRoute';
@ -7,7 +8,7 @@ import { deleteTable } from 'redux/action-creators';
export class Header extends ExcelComponent {
static className = 'excel__header';
constructor($root: Dom, options: ComponentOptionsType) {
constructor($root: Dom, options: BaseComponentOption) {
super($root, {
...options,
name: 'Header',

View File

@ -1,11 +1,21 @@
import { ContextSelectType } from 'components/ContextMenu/ContextMenu';
import { BaseComponentOption } from 'components/excel/Excel';
import { startCellId } from 'components/table/table.functions';
import * as actions from 'redux/action-creators';
import { $, Dom } from 'core/Dom';
import { ComponentOptionsType, ExcelComponent } from 'core/ExcelComponent';
import { ExcelComponent } from 'core/ExcelComponent';
import { TableSelection } from 'components/table/TableSelection';
import { changeCurrentStyles, changeCurrentText, changeTableSize, removeRowFromTable } from 'redux/action-creators';
import { createTable, getNewRowHTML } from 'components/table/table.template';
import { initialState, initialStyleState } from 'src/constants';
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 { initialStyleState } from 'src/constants';
import { getCellId, parse } from 'core/utils';
import { resizeHandler } from 'components/table/handlers/table.resize';
import { selectHandler } from 'components/table/handlers/table.select.handler';
@ -15,28 +25,39 @@ export class Table extends ExcelComponent {
private selection: TableSelection;
private isMouseDowned: boolean;
private tableResizing = false;
public tableSize = { row: initialState.tableSize.row, col: initialState.tableSize.col };
private tableResizing: boolean;
public tableSize: TableSizeType;
constructor($root: Dom, options: ComponentOptionsType) {
constructor($root: Dom, options: BaseComponentOption) {
super($root, {
...options,
name: 'Table',
eventListeners: ['mousedown', 'keydown', 'input', 'mouseover', 'mouseup'],
eventListeners: ['mousedown', 'keydown', 'input', 'mouseover', 'mouseup', 'contextmenu'],
});
this.isMouseDowned = false;
const { col, row } = this.getTableSize();
this.tableSize = { col, row };
}
toHTML(): string {
console.log('Table size', this.tableSize);
return createTable(this.tableSize.row, this.tableSize.col);
}
prepare() {
beforeRender() {
this.selection = new TableSelection(this);
this.tableResizing = false;
this.isMouseDowned = false;
this.tableSize = this.getTableSize();
}
afterRender() {
super.afterRender();
this.initTable();
this.$onEventFromObserver('formula:input', this.updateTextInCell);
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);
this.$onEventFromObserver('context-menu: select', this.contextMenuHandler);
}
getTableSize() {
@ -49,25 +70,13 @@ export class Table extends ExcelComponent {
col: Math.max(maxColFromState, col),
};
if ((maxColFromState !== this.tableSize.col) || (maxRowFromState !== this.tableSize.row)) {
if ((maxColFromState !== this.tableSize?.col) || (maxRowFromState !== this.tableSize?.row)) {
this.dispatchToStore(changeTableSize(normalTableSize));
}
return normalTableSize;
}
init() {
super.init();
this.initTable();
this.$onEventFromObserver('formula:input', this.updateTextInCell);
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);
}
initTable() {
this.initTableSize();
this.initTableContentAndStyles();
@ -156,7 +165,6 @@ export class Table extends ExcelComponent {
};
updateTextInCell = (text: string, $cell = this.selection.$focusedCell) => {
// eslint-disable-next-line no-param-reassign
$cell.attr('data-value', text);
// eslint-disable-next-line no-param-reassign
$cell.text = parse(text);
@ -165,15 +173,91 @@ export class Table extends ExcelComponent {
text,
id: $cell.data.id || startCellId,
}));
this.selection.focusToCell($cell);
};
addNewRowHandler = () => {
this.tableSize.row++;
this.$root.$el.insertAdjacentHTML('beforeend', getNewRowHTML(this.tableSize.row, this.tableSize.col));
const $newRow = $(`[data-row="${this.tableSize.row - 1}"]`);
this.initColSizes($newRow);
this.addNewColRow('row', 'after', this.tableSize.row);
};
this.dispatchToStore(changeTableSize(this.tableSize));
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);
// this.selection.selectByCellId({ col: 2, row: 2 });
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) {
@ -198,16 +282,12 @@ export class Table extends ExcelComponent {
this.isMouseDowned = false;
}
removeRowHandler = () => {
const cell = this.selection.$focusedCell;
const cellId = getCellId(cell);
if (!cellId) return;
onContextmenu(event: MouseEvent) {
const $target = $(event.target);
const { row } = cellId;
const nodeToRemove = this.$root.find(`[data-row='${row}']`);
this.$root.removeChild(nodeToRemove);
this.selection.clearSelection();
this.selection.selectHeadRowCol($target);
this.dispatchToStore(removeRowFromTable(+row));
};
this.$emitEventToObserver('table:contextmenu', event);
}
}

View File

@ -30,7 +30,7 @@ export class TableSelection {
const range = new Range();
const node = $cell.$el;
range.setStartAfter(node.childNodes[0]);
range.setStartAfter(node.childNodes[node.childNodes.length - 1]);
window.getSelection()?.removeAllRanges();
window.getSelection()?.addRange(range);
@ -143,4 +143,23 @@ export class TableSelection {
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

@ -33,22 +33,7 @@ export function selectHandler(event: MouseEvent | KeyboardEvent, selection: Tabl
else if (event.ctrlKey) selection.addCellToSelection(target);
else selection.select(target);
} else {
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));
selection.selectGroupies($cells);
} else if (col.$el && !resizer.$el) {
const colNumber = col.data.col;
const columns = selection.rootTable.$root.findAll(`[data-col="${colNumber}"]`);
const $cells = Array.from(columns).filter(el => el !== col.$el).map(el => $(el as HTMLElement));
selection.selectGroupies($cells);
}
selection.selectHeadRowCol(target);
}
}

View File

@ -62,12 +62,3 @@ export function createTable(rowsCount = 10, columnCount = 10) {
return rows.join('');
}
export function getNewRowHTML(rowIndex: number, colCount: number): string {
const cells = new Array(colCount)
.fill('')
.map((el, colIndex) => createCell('', colIndex, rowIndex - 1))
.join('');
return createRow(cells, rowIndex.toString(), true, rowIndex - 1);
}

View File

@ -1,7 +1,7 @@
import { BaseComponentOption } from 'components/excel/Excel';
import { startCellId } from 'components/table/table.functions';
import { $, Dom } from 'core/Dom';
import { ExcelComponentState } from 'core/ExcelComponentState';
import { ComponentOptionsType } from 'core/ExcelComponent';
import { createToolbar } from 'components/toolbar/toolbar.template';
import { StateType } from 'redux/types';
import { fontSizes, initialStyleState } from 'src/constants';
@ -9,7 +9,7 @@ import { fontSizes, initialStyleState } from 'src/constants';
export class Toolbar extends ExcelComponentState {
static className = 'excel__toolbar';
constructor($root: Dom, options: ComponentOptionsType) {
constructor($root: Dom, options: BaseComponentOption) {
super($root, {
...options,
eventListeners: ['click', 'change'],
@ -18,7 +18,7 @@ export class Toolbar extends ExcelComponentState {
});
}
prepare() {
beforeRender() {
const currentToolbarState = this.toolbarState;
this.initComponentState(currentToolbarState);

View File

@ -1,8 +1,7 @@
import { startCellId } from 'components/table/table.functions';
import { ToolbarStateType } from 'components/toolbar/toolbar-types';
import { storageName } from 'pages/ExcelPage';
import { StateType } from 'redux/types';
import { storage } from 'core/utils';
import { storage, storageName } from 'core/utils';
export const fontSizes = [
'12px',

View File

@ -1,5 +1,4 @@
import { storage } from 'core/utils';
import { storageName } from 'pages/ExcelPage';
import { storage, storageName } from 'core/utils';
import { StateType } from 'redux/types';
import { getNormalizeInitialState } from 'src/constants';

View File

@ -0,0 +1,58 @@
import { BaseComponentOption, ComponentType } from 'components/excel/Excel';
import { $, Dom } from 'core/Dom';
import { ExcelComponent } from 'core/ExcelComponent';
export class ComponentManager {
private components: ExcelComponent[];
private componentConstructors: {
[k: string]: ComponentType;
};
public $rootExcelElement: Dom;
constructor() {
this.componentConstructors = {};
this.$rootExcelElement = $.create('div', 'excel');
this.components = [];
// for debug
// @ts-ignore
window.componentRoots = this.componentConstructors;
}
createComponent(Component: ComponentType, componentOptions: BaseComponentOption) {
const $el = $.create('div', Component.className);
const componentInstance: ExcelComponent = new Component($el, componentOptions);
$el.html(componentInstance.toHTML());
this.componentConstructors[componentInstance.name] = Component;
this.components.push(componentInstance);
return componentInstance;
}
addComponentsToRoot(components: ExcelComponent[]) {
components.forEach(component => this.$rootExcelElement.append(component.$root));
}
rerenderComponent(component: ExcelComponent) {
const componentConstructor = this.findComponentConstructor(component);
const componentOptions: BaseComponentOption = {
observer: component.observer,
store: component.store,
componentManager: this,
};
const $newComponent = this.createComponent(componentConstructor, componentOptions);
this.$rootExcelElement.replaceChild($newComponent.$root, component.$root);
component.destroy();
$newComponent.afterRender();
}
findComponentConstructor(component: ExcelComponent): ComponentType {
return this.componentConstructors[component.name];
}
destroyComponents() {
Object.values(this.components).forEach(component => component.destroy());
}
}

View File

@ -32,8 +32,8 @@ export class Dom implements DomClass {
set text(text: string) {
if (!this.$el) return;
if (!text) this.$el.textContent = '';
this.$el.textContent = text;
if (!text) this.$el.innerText = '';
this.$el.innerText = text;
}
get text() {
@ -47,7 +47,6 @@ export class Dom implements DomClass {
return this;
}
// FIXME: any
append(node: HTMLElement | Dom) {
let child = node;
@ -126,7 +125,7 @@ export class Dom implements DomClass {
}
attr(name: string, value: string) {
if (value) {
if (value !== undefined) {
this.$el.setAttribute(name, value);
return this;
}
@ -138,6 +137,10 @@ export class Dom implements DomClass {
this.$el.removeChild($child.$el);
}
replaceChild($newChild: Dom, $oldChild: Dom) {
this.$el.replaceChild($newChild.$el, $oldChild.$el);
}
get isExist(): boolean {
return !!this.$el;
}

View File

@ -6,7 +6,7 @@ import { getMethodNameByEventName } from 'core/utils';
export class DomListener {
$root: Dom;
eventListeners: string[];
protected name: string;
name: string;
constructor($root: Dom, eventNames: string[]) {
if (!$root) throw new Error('Не передали корневой элемент');

View File

@ -1,35 +1,36 @@
import { BaseComponentOption } from 'components/excel/Excel';
import { ComponentManager } from 'core/ComponentManager';
import { ActionType, CallbackType, StateType } from 'redux/types';
import { Dom } from 'core/Dom';
import { DomListener } from 'core/DomListener';
import { Observer } from 'core/Observer';
import { Store } from 'core/store/Store';
export type ComponentOptionsType = {
export type ComponentOptionsType = BaseComponentOption & {
eventListeners: string[];
name: string;
observer: Observer;
store: Store;
subscribe: (keyof StateType)[],
subscribe?: (keyof StateType)[],
};
export abstract class ExcelComponent extends DomListener {
private observer: Observer;
observer: Observer;
public store: Store;
private subscribe: (keyof StateType)[];
private unsubscribers: CallbackType[];
private componentManager: ComponentManager;
protected constructor($root: Dom, options: ComponentOptionsType) {
super($root, options.eventListeners);
this.name = options.name;
this.observer = options.observer;
this.store = options.store;
this.subscribe = options.subscribe;
this.subscribe = options?.subscribe || [];
this.componentManager = options.componentManager;
this.unsubscribers = [];
this.prepare();
this.beforeRender();
}
prepare() {
beforeRender() {
}
@ -58,7 +59,7 @@ export abstract class ExcelComponent extends DomListener {
return this.subscribe?.includes(key);
}
init() {
afterRender() {
this.initDOMListeners();
}
@ -66,4 +67,8 @@ export abstract class ExcelComponent extends DomListener {
this.removeDOMListeners();
this.unsubscribers.forEach(unsub => unsub());
}
rerender() {
this.componentManager.rerenderComponent(this);
}
}

View File

@ -75,3 +75,7 @@ export function getCellId($cell: Dom): { row: string, col:string } | false {
const [row, col] = id;
return { row, col };
}
export function storageName(param: string) {
return `excel:${param}`;
}

View File

@ -1,3 +1,4 @@
import { ContextMenu } from 'components/ContextMenu/ContextMenu';
import { AbstractPage } from 'pages/AbstractPage';
import { Excel } from 'components/excel/Excel';
import { Formula } from 'components/formula/Formula';
@ -10,10 +11,6 @@ import { Table } from 'components/table/Table';
import { Toolbar } from 'components/toolbar/Toolbar';
import { rootReducer } from 'redux/rootReducer';
export function storageName(param: string) {
return `excel:${param}`;
}
export class ExcelPage extends AbstractPage {
private excel: Excel;
private storeSub: SubscribeType | null;
@ -35,7 +32,7 @@ export class ExcelPage extends AbstractPage {
this.storeSub = store.subscribeToStore(this.processor.listen);
this.excel = new Excel({
components: [Header, Toolbar, Formula, Table],
components: [Header, Toolbar, Formula, Table, ContextMenu],
store,
});
@ -43,7 +40,7 @@ export class ExcelPage extends AbstractPage {
}
afterRender() {
this.excel.init();
this.excel.afterRender();
}
destroy() {

View File

@ -8,3 +8,6 @@ export const UPDATE_DATE = 'UPDATE_DATE';
export const CHANGE_CURRENT_TEXT = 'CHANGE_CURRENT_TEXT';
export const CHANGE_TABLE_SIZE = 'CHANGE_TABLE_SIZE';
export const REMOVE_ROW_FROM_TABLE = 'REMOVE_ROW_FROM_TABLE';
export const REMOVE_COL_FROM_TABLE = 'REMOVE_COL_FROM_TABLE';
export const ADD_ROW_TO_TABLE = 'ADD_ROW_TO_TABLE';
export const ADD_COL_TO_TABLE = 'ADD_COL_TO_TABLE';

View File

@ -7,7 +7,13 @@ import {
APPLY_STYLES,
CHANGE_TITLE,
DELETE_TABLE,
UPDATE_DATE, CHANGE_CURRENT_TEXT, CHANGE_TABLE_SIZE, REMOVE_ROW_FROM_TABLE,
UPDATE_DATE,
CHANGE_CURRENT_TEXT,
CHANGE_TABLE_SIZE,
REMOVE_ROW_FROM_TABLE,
ADD_ROW_TO_TABLE,
ADD_COL_TO_TABLE,
REMOVE_COL_FROM_TABLE,
} from 'redux/action-constants';
export function tableResize(resizeData: ResizeReturnDataType): ActionType {
@ -73,9 +79,30 @@ export function changeTableSize(data: { col: number, row: number }): ActionType
};
}
export function addRow(data: { position: 'after' | 'before', targetIndex: number }): ActionType {
return {
type: ADD_ROW_TO_TABLE,
data,
};
}
export function addCol(data: { position: 'after' | 'before', targetIndex: number }): ActionType {
return {
type: ADD_COL_TO_TABLE,
data,
};
}
export function removeRowFromTable(removedRowNumber: number) {
return {
type: REMOVE_ROW_FROM_TABLE,
data: removedRowNumber,
};
}
export function removeColFromTable(removedColNumber: number) {
return {
type: REMOVE_COL_FROM_TABLE,
data: removedColNumber,
};
}

View File

@ -1,7 +1,7 @@
import { ToolbarStateType } from 'components/toolbar/toolbar-types';
import { storageName } from 'core/utils';
import { ActionType, StateType } from 'redux/types';
import { ActiveRoute } from 'core/routes/ActiveRoute';
import { storageName } from 'pages/ExcelPage';
import {
CHANGE_TEXT,
CHANGE_STYLES,
@ -9,7 +9,13 @@ import {
APPLY_STYLES,
CHANGE_TITLE,
DELETE_TABLE,
UPDATE_DATE, CHANGE_CURRENT_TEXT, CHANGE_TABLE_SIZE, REMOVE_ROW_FROM_TABLE,
UPDATE_DATE,
CHANGE_CURRENT_TEXT,
CHANGE_TABLE_SIZE,
REMOVE_ROW_FROM_TABLE,
ADD_ROW_TO_TABLE,
ADD_COL_TO_TABLE,
REMOVE_COL_FROM_TABLE,
} from 'redux/action-constants';
export function rootReducer(state: StateType, action: ActionType): StateType {
@ -75,6 +81,142 @@ export function rootReducer(state: StateType, action: ActionType): StateType {
return { ...state, tableSize: action.data };
}
case ADD_ROW_TO_TABLE: {
// needs to be redefined next params:
// rowState: { [k: number]: number };
// dataState: { [k: string]: string };
// stylesState: { [k: string]: ToolbarStateType };
// tableSize: { col: number, row: number }
const newRowStateEntries: [number, number][] = [];
const newDataStateEntries: [string, string][] = [];
const newStylesStateEntries: [string, ToolbarStateType][] = [];
// rowState change
Object.entries(state.rowState).forEach(([key, value]) => {
switch (true) {
case action.data.position === 'before' && +key < action.data.targetIndex:
case action.data.position === 'after' && +key <= action.data.targetIndex:
newRowStateEntries.push([+key, value]);
break;
case action.data.position === 'before' && +key >= action.data.targetIndex:
case action.data.position === 'after' && +key > action.data.targetIndex:
newRowStateEntries.push([+key + 1, value]);
break;
default: break;
}
});
const newRowState = Object.fromEntries(newRowStateEntries);
// dataState change
Object.entries(state.dataState).forEach(([key, value]) => {
const [row, col] = key.split(':');
switch (true) {
case action.data.position === 'before' && +row < action.data.targetIndex:
case action.data.position === 'after' && +row <= action.data.targetIndex:
newDataStateEntries.push([key, value.toString()]);
break;
case action.data.position === 'before' && +row >= action.data.targetIndex:
case action.data.position === 'after' && +row > action.data.targetIndex:
newDataStateEntries.push([`${+row + 1}:${col}`, value.toString()]);
break;
default: break;
}
});
const newDataState = Object.fromEntries(newDataStateEntries);
// stylesState change
Object.entries(state.stylesState).forEach(([key, value]) => {
const [row, col] = key.split(':');
switch (true) {
case action.data.position === 'before' && +row < action.data.targetIndex:
case action.data.position === 'after' && +row <= action.data.targetIndex:
newStylesStateEntries.push([key, value]);
break;
case action.data.position === 'before' && +row >= action.data.targetIndex:
case action.data.position === 'after' && +row > action.data.targetIndex:
newStylesStateEntries.push([`${+row + 1}:${col}`, value]);
break;
default: break;
}
});
const newStylesState = Object.fromEntries(newStylesStateEntries);
// tableSize
const newTableSize = { col: state.tableSize.col, row: state.tableSize.row + 1 };
// debugger
return { ...state, rowState: newRowState, dataState: newDataState, stylesState: newStylesState, tableSize: newTableSize };
}
case ADD_COL_TO_TABLE: {
// needs to be redefined next params:
// colState: { [k: number]: number };
// dataState: { [k: string]: string };
// stylesState: { [k: string]: ToolbarStateType };
// tableSize: { col: number, row: number }
const newColStateEntries: [number, number][] = [];
const newDataStateEntries: [string, string][] = [];
const newStylesStateEntries: [string, ToolbarStateType][] = [];
// colState change
Object.entries(state.colState).forEach(([key, value]) => {
switch (true) {
case action.data.position === 'before' && +key < action.data.targetIndex:
case action.data.position === 'after' && +key <= action.data.targetIndex:
newColStateEntries.push([+key, value]);
break;
case action.data.position === 'before' && +key >= action.data.targetIndex:
case action.data.position === 'after' && +key > action.data.targetIndex:
newColStateEntries.push([+key + 1, value]);
break;
default: break;
}
});
const newColState = Object.fromEntries(newColStateEntries);
// dataState change
Object.entries(state.dataState).forEach(([key, value]) => {
const [row, col] = key.split(':');
switch (true) {
case action.data.position === 'before' && +col < action.data.targetIndex:
case action.data.position === 'after' && +col <= action.data.targetIndex:
newDataStateEntries.push([key, value.toString()]);
break;
case action.data.position === 'before' && +col >= action.data.targetIndex:
case action.data.position === 'after' && +col > action.data.targetIndex:
newDataStateEntries.push([`${row}:${+col + 1}`, value.toString()]);
break;
default: break;
}
});
const newDataState = Object.fromEntries(newDataStateEntries);
// stylesState change
Object.entries(state.stylesState).forEach(([key, value]) => {
const [row, col] = key.split(':');
switch (true) {
case action.data.position === 'before' && +col < action.data.targetIndex:
case action.data.position === 'after' && +col <= action.data.targetIndex:
newStylesStateEntries.push([key, value]);
break;
case action.data.position === 'before' && +col >= action.data.targetIndex:
case action.data.position === 'after' && +col > action.data.targetIndex:
newStylesStateEntries.push([`${row}:${+col + 1}`, value]);
break;
default: break;
}
});
const newStylesState = Object.fromEntries(newStylesStateEntries);
// tableSize
const newTableSize = { col: state.tableSize.col + 1, row: state.tableSize.row };
// debugger
return { ...state, colState: newColState, dataState: newDataState, stylesState: newStylesState, tableSize: newTableSize };
}
case REMOVE_ROW_FROM_TABLE: {
// needs to be redefined next params:
// rowState: { [k: number]: number };
@ -135,10 +277,72 @@ export function rootReducer(state: StateType, action: ActionType): StateType {
// tableSize
const newTableSize = { col: state.tableSize.col, row: state.tableSize.row - 1 };
// const newRowState = Object.keys(state.rowState).filter(rowIndex => rowIndex.toString() !== action.data.toString()).
return { ...state, rowState: newRowState, dataState: newDataState, stylesState: newStylesState, tableSize: newTableSize };
}
case REMOVE_COL_FROM_TABLE: {
// needs to be redefined next params:
// colState: { [k: number]: number };
// dataState: { [k: string]: string };
// stylesState: { [k: string]: ToolbarStateType };
// tableSize: { col: number, row: number }
const newColStateEntries: [number, number][] = [];
const newDataStateEntries: [string, string][] = [];
const newStylesStateEntries: [string, ToolbarStateType][] = [];
// colState change
Object.entries(state.colState).forEach(([key, value]) => {
switch (true) {
case key < action.data:
newColStateEntries.push([+key, value]);
break;
case key > action.data:
newColStateEntries.push([+key - 1, value]);
break;
default: break;
}
});
const newColState = Object.fromEntries(newColStateEntries);
// dataState change
Object.entries(state.dataState).forEach(([key, value]) => {
const [row, col] = key.split(':');
switch (true) {
case col < action.data:
newDataStateEntries.push([key, value.toString()]);
break;
case col > action.data:
newDataStateEntries.push([`${row}:${+col - 1}`, value.toString()]);
break;
default: break;
}
});
const newDataState = Object.fromEntries(newDataStateEntries);
// stylesState change
Object.entries(state.stylesState).forEach(([key, value]) => {
const [row, col] = key.split(':');
switch (true) {
case col < action.data:
newStylesStateEntries.push([key, value]);
break;
case col > action.data:
newStylesStateEntries.push([`${row}:${+col - 1}`, value]);
break;
default: break;
}
});
const newStylesState = Object.fromEntries(newStylesStateEntries);
// tableSize
const newTableSize = { col: state.tableSize.col - 1, row: state.tableSize.row };
return { ...state, colState: newColState, dataState: newDataState, stylesState: newStylesState, tableSize: newTableSize };
}
default: return state;
}
}

10
src/redux/types.d.ts vendored
View File

@ -5,6 +5,11 @@ export type ActionType = {
[k: string]: any
};
export type TableSizeType = {
col: number;
row: number;
};
export type StateType = {
colState: { [k: number]: number };
rowState: { [k: number]: number };
@ -15,10 +20,7 @@ export type StateType = {
stylesState: { [k: string]: ToolbarStateType };
title: string;
currentText: string;
tableSize: {
col: number;
row: number;
};
tableSize: TableSizeType;
};
export type ReducerType = (state: StateType, action: ActionType) => StateType | null;

View File

@ -0,0 +1,43 @@
@import "../_variables";
@import "../_mixins";
.excel__contextmenu_layer {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 10;
user-select: none;
&.active {
height: 100vh;
}
}
.excel__contextmenu {
position: absolute;
z-index: 20;
left: 0;
top: -100px;
width: 170px;
height: 100px;
background: #ffffff;
display: flex;
flex-direction: column;
.excel__contextmenu_item {
flex: 1;
border: 2px solid #cecece;
cursor: pointer;
font-weight: bold;
display: flex;
align-items: center;
padding: 5px;
&:hover {
background: rgba(206, 206, 206, 1);
}
}
}

View File

@ -9,6 +9,7 @@
top: $header-height + $toolbar-height + $formula-height;
overflow: auto;
font-size: $default-cell-font-size;
padding-bottom: 5px;
.row{
display: flex;
@ -49,7 +50,7 @@
border: 1px solid #e1e2e3;
border-top: 0;
border-left: 0;
white-space: nowrap;
white-space: normal;
overflow: hidden;
outline: none;
display: flex;
@ -93,7 +94,7 @@
}
}
[data-header="row"] {
cursor: e-resize;
cursor: pointer;
&.selected {
border-right: 2px solid #3c74ff;
@ -101,15 +102,21 @@
}
[data-header="col"] {
cursor: n-resize;
cursor: pointer;
&.selected {
border-bottom: 2px solid #3c74ff;
}
}
[data-header="col"].selected, [data-header="row"].selected {
[data-header="col"], [data-header="row"] {
&.selected {
font-weight: bold;
background: rgba(60, 116, 255, 0.4);
}
&:hover {
background: #ccc;
}
}
}

View File

@ -5,6 +5,7 @@
@import './components/toolbar';
@import './components/formula';
@import './components/table';
@import './components/contextmenu';
@import './components/dashboard';
@import './components/loader';