Compare commits
No commits in common. "master" and "v1.0.0" have entirely different histories.
@ -34,14 +34,13 @@ module.exports = {
|
||||
"import/prefer-default-export": "off",
|
||||
"linebreak-style": "off",
|
||||
"max-len": "off",
|
||||
"no-alert": "off",
|
||||
"no-console": "off",
|
||||
"no-continue": "off",
|
||||
"no-new": "off",
|
||||
"no-continue": "off",
|
||||
"no-plusplus": "off",
|
||||
"no-restricted-globals": "off",
|
||||
"object-curly-newline": "off",
|
||||
"prefer-destructuring": "off",
|
||||
"no-alert": "off",
|
||||
"no-restricted-globals": "off",
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
|
||||
76
README.md
76
README.md
@ -1,76 +0,0 @@
|
||||
# Mini excel app
|
||||
## _Small application covering the basic functionality of Excel_
|
||||
|
||||
The application is a SPA consisting of two screens
|
||||
- Dashboard (Create new table or open saved)
|
||||
- Excel (Excel table editing page)
|
||||
|
||||
|
||||
Technologies used in this project:
|
||||
- Typescript
|
||||
- ES6+
|
||||
- SCSS
|
||||
- Redux
|
||||
- Webpack 5
|
||||
- Jest
|
||||
|
||||
|
||||
## Implemented functionality
|
||||
- Resize table rows/columns
|
||||
- Navigation and selection of cells using the function keys (Ctrl, Shift, Arrows), or using the mouse
|
||||
- Mathematical calculations in a formula
|
||||
- Ability to add/remove columns via context menu (right mouse button)
|
||||
- Changing cell style: size, font, alignment
|
||||
- Saving to LocalStorage, also implemented the ability to delete certain tables
|
||||
|
||||
## Installation
|
||||
|
||||
Requires [Node.js](https://nodejs.org/) v16+ to run.
|
||||
|
||||
Install the dependencies and devDependencies and start the server.
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
For development run the command to create local webpack dev server
|
||||
|
||||
```sh
|
||||
npm run start
|
||||
```
|
||||
|
||||
For build use
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
For deploy edit deploy config file `configs/deploy/sftp-configs.js`
|
||||
```sh
|
||||
module.exports = {
|
||||
config: {
|
||||
host: 'host',
|
||||
port: 'port',
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
},
|
||||
directory: 'path to directory'
|
||||
};
|
||||
```
|
||||
and run
|
||||
```sh
|
||||
npm run deploy
|
||||
```
|
||||
|
||||
Testing
|
||||
|
||||
Run jest test by
|
||||
```sh
|
||||
npm run test
|
||||
```
|
||||
or
|
||||
```sh
|
||||
npm run test-watch
|
||||
```
|
||||
## License
|
||||
|
||||
MIT
|
||||
@ -1,10 +0,0 @@
|
||||
module.exports = {
|
||||
config: {
|
||||
host: 'host',
|
||||
port: 'port',
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
},
|
||||
directory: 'directory'
|
||||
};
|
||||
|
||||
@ -3,9 +3,9 @@
|
||||
const path = require('path');
|
||||
const SftpClient = require('ssh2-sftp-client');
|
||||
const dotenv = require('dotenv');
|
||||
const { config, directory } = require('./configs/deploy/sftp-config');
|
||||
const { config } = require('./configs/deploy/sftp-config');
|
||||
|
||||
const REMOTE_DIRECTORY = directory;
|
||||
const REMOTE_DIRECTORY = '/var/www/mysite/excel';
|
||||
const LOCAL_DIRECTORY = path.join(__dirname, 'dist');
|
||||
|
||||
const dotenvPath = path.join(__dirname, '..', '.env');
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "excel-course",
|
||||
"version": "1.1.0",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.6",
|
||||
|
||||
@ -1,124 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { $, Dom } from 'core/Dom';
|
||||
import { $, Dom } from 'core/dom';
|
||||
|
||||
export function Loader(): Dom {
|
||||
return $.create('div', 'loader')
|
||||
|
||||
@ -1,65 +1,58 @@
|
||||
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 { Store } from 'core/store/Store';
|
||||
import { $ } from 'core/dom';
|
||||
import { Emitter } from 'core/Emitter';
|
||||
import { ExcelComponent } from 'core/ExcelComponent';
|
||||
import { Store } from 'core/store/createStore';
|
||||
import { StoreSubscriber } from 'core/StoreSubscriber';
|
||||
import { updateOpenDate } from 'redux/action-creators';
|
||||
import { updateOpenDate } from 'redux/actions';
|
||||
|
||||
// TODO type for components array
|
||||
interface ExcelOptionsType {
|
||||
components: ComponentType[],
|
||||
store: Store,
|
||||
components: any[],
|
||||
store: any,
|
||||
}
|
||||
|
||||
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 {
|
||||
components: any[];
|
||||
observer: Observer;
|
||||
emitter: Emitter;
|
||||
store: Store;
|
||||
subscriber: StoreSubscriber;
|
||||
componentManage: ComponentManager;
|
||||
|
||||
constructor(options: ExcelOptionsType) {
|
||||
this.store = options.store;
|
||||
this.components = options.components;
|
||||
|
||||
this.emitter = new Emitter();
|
||||
this.store = options.store;
|
||||
this.subscriber = new StoreSubscriber(this.store);
|
||||
this.observer = new Observer();
|
||||
this.componentManage = new ComponentManager();
|
||||
}
|
||||
|
||||
getRoot() {
|
||||
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);
|
||||
const $root = $.create('div', 'excel');
|
||||
|
||||
return this.componentManage.$rootExcelElement;
|
||||
const componentOptions = {
|
||||
emitter: this.emitter,
|
||||
store: this.store,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
afterRender() {
|
||||
init() {
|
||||
this.subscriber.subscribeComponents(this.components);
|
||||
this.components.forEach(component => component.afterRender());
|
||||
this.components.forEach(component => component.init());
|
||||
|
||||
this.store.dispatchToStore(updateOpenDate(Date.now().toString()));
|
||||
this.store.dispatch(updateOpenDate(Date.now().toString()));
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.subscriber.unsubscribeFromStore();
|
||||
this.componentManage.destroyComponents();
|
||||
this.components.forEach(component => component.destroy());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { BaseComponentOption } from 'components/excel/Excel';
|
||||
import { Dom } from 'core/Dom';
|
||||
import { Dom } from 'core/dom';
|
||||
import { ExcelComponent } from 'core/ExcelComponent';
|
||||
|
||||
export class Formula extends ExcelComponent {
|
||||
@ -7,12 +6,12 @@ export class Formula extends ExcelComponent {
|
||||
|
||||
private formulaInput: Dom;
|
||||
|
||||
constructor($root: Dom, options: BaseComponentOption) {
|
||||
constructor($root: Dom, options: any) {
|
||||
super($root, {
|
||||
...options,
|
||||
eventListeners: ['input', 'keydown'],
|
||||
listeners: ['input', 'keydown'],
|
||||
name: 'Formula',
|
||||
subscribe: ['currentText'],
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
@ -23,13 +22,13 @@ export class Formula extends ExcelComponent {
|
||||
`;
|
||||
}
|
||||
|
||||
afterRender() {
|
||||
super.afterRender();
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
this.formulaInput = this.$root.find('#formula-input');
|
||||
|
||||
this.$onEventFromObserver('table:select-cell', (cell: Dom) => {
|
||||
this.formulaInput.text = cell.dataValue || '';
|
||||
this.$on('table:select-cell', text => {
|
||||
this.formulaInput.text = text || '';
|
||||
});
|
||||
}
|
||||
|
||||
@ -42,8 +41,7 @@ export class Formula extends ExcelComponent {
|
||||
if (!target) return;
|
||||
|
||||
const text = (target as HTMLElement).innerText.trim();
|
||||
|
||||
this.$emitEventToObserver('formula:input', text);
|
||||
this.$emit('formula:input', text);
|
||||
}
|
||||
|
||||
onKeydown(event: KeyboardEvent) {
|
||||
@ -52,7 +50,7 @@ export class Formula extends ExcelComponent {
|
||||
if (preventedKeys.includes(event.key)) event.preventDefault();
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
this.$emitEventToObserver('formula:enter-press');
|
||||
this.$emit('formula:enter-press');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,28 +1,26 @@
|
||||
import { BaseComponentOption } from 'components/excel/Excel';
|
||||
import { ExcelComponent } from 'core/ExcelComponent';
|
||||
import * as actions from 'redux/action-creators';
|
||||
import { $, Dom } from 'core/Dom';
|
||||
import * as actions from 'redux/actions';
|
||||
import { $, Dom } from 'core/dom';
|
||||
import { ActiveRoute } from 'core/routes/ActiveRoute';
|
||||
import { deleteTable } from 'redux/action-creators';
|
||||
import { ExcelStateComponent } from 'core/ExcelStateComponent';
|
||||
import { deleteTable } from 'redux/actions';
|
||||
|
||||
export class Header extends ExcelComponent {
|
||||
export class Header extends ExcelStateComponent {
|
||||
static className = 'excel__header';
|
||||
|
||||
constructor($root: Dom, options: BaseComponentOption) {
|
||||
constructor($root: Dom, options: any) {
|
||||
super($root, {
|
||||
...options,
|
||||
name: 'Header',
|
||||
eventListeners: ['input', 'click'],
|
||||
listeners: ['input', 'click'],
|
||||
subscribe: ['title'],
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
toHTML(): string {
|
||||
const { title, id } = this.store.getState();
|
||||
const { title } = this.store.getState();
|
||||
|
||||
return `
|
||||
<input type="text" class="input" value="${title}">
|
||||
<div>ID: <strong>${id}</strong></div>
|
||||
|
||||
<div>
|
||||
<div class="button" data-button="delete-table">
|
||||
@ -35,9 +33,9 @@ export class Header extends ExcelComponent {
|
||||
}
|
||||
|
||||
onInput(event: InputEvent) {
|
||||
const $target = $(event.target);
|
||||
const $target = $(event.target as HTMLInputElement);
|
||||
|
||||
this.dispatchToStore(actions.changeTitle($target.text));
|
||||
this.$dispatch(actions.changeTitle($target.text));
|
||||
}
|
||||
|
||||
onClick(event: MouseEvent) {
|
||||
@ -56,7 +54,7 @@ export class Header extends ExcelComponent {
|
||||
}
|
||||
|
||||
case 'delete-table': {
|
||||
confirm('Действительно хочешь удалить ?') && this.dispatchToStore(deleteTable(this.store.getState().id));
|
||||
confirm('Действительно хочешь удалить ?') && this.$dispatch(deleteTable(this.store.getState().id));
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,405 +0,0 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
getSiblingCellBySide(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.getSiblingCellBySide(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]');
|
||||
|
||||
if (!this.rootTable.focusManager.$currentFocusedCell?.isExist) 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.rootTable.focusManager.$currentFocusedCell?.isExist) {
|
||||
this.$currentSelectedCell && this.rootTable.focusManager.focusCell(this.$currentSelectedCell);
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' || event.key === 'Tab') {
|
||||
let side;
|
||||
|
||||
if (event.key === 'Enter' && !event.shiftKey) side = 'down';
|
||||
if (event.key === 'Tab') side = event.shiftKey ? 'left' : 'right';
|
||||
|
||||
side && this.moveSelectionTo(side);
|
||||
}
|
||||
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.getSiblingCellBySide(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.getSiblingCellBySide(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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,92 +1,69 @@
|
||||
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 * as actions from 'redux/actions';
|
||||
import { $, Dom } from 'core/dom';
|
||||
import { ExcelComponent } from 'core/ExcelComponent';
|
||||
import {
|
||||
addCol,
|
||||
addRow, changeCurrentStyles, changeCurrentText,
|
||||
changeTableSize, removeColFromTable,
|
||||
removeRowFromTable,
|
||||
} from 'redux/action-creators';
|
||||
import { TableSelection } from 'components/table/TableSelection';
|
||||
import { changeCurrentStyles } from 'redux/actions';
|
||||
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';
|
||||
import { parse } from 'core/utils';
|
||||
import { resizeHandler } from 'components/table/handlers/table.resize';
|
||||
import { selectHandler } from 'components/table/handlers/table.select.handler';
|
||||
|
||||
export class Table extends ExcelComponent {
|
||||
static className = 'excel__table';
|
||||
|
||||
private tableResizing: boolean;
|
||||
public tableSize: TableSizeType;
|
||||
selectionManager: SelectionManager;
|
||||
focusManager: FocusManager;
|
||||
private selection: TableSelection;
|
||||
|
||||
constructor($root: Dom, options: BaseComponentOption) {
|
||||
constructor($root: Dom, options: any) {
|
||||
super($root, {
|
||||
...options,
|
||||
name: 'Table',
|
||||
eventListeners: ['mousedown', 'keydown', 'input', 'mouseover', 'mouseup', 'contextmenu', 'dblclick', 'focusout'],
|
||||
listeners: ['mousedown', 'keydown', 'input'],
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
toHTML(): string {
|
||||
return createTable(this.tableSize.row, this.tableSize.col);
|
||||
return createTable(50, 24);
|
||||
}
|
||||
|
||||
beforeRender() {
|
||||
this.tableResizing = false;
|
||||
this.tableSize = this.getTableSize();
|
||||
this.selectionManager = new SelectionManager(this);
|
||||
this.focusManager = new FocusManager(this);
|
||||
prepare() {
|
||||
this.selection = new TableSelection();
|
||||
}
|
||||
|
||||
afterRender() {
|
||||
super.afterRender();
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
const $cell = this.$root.find(`[data-id="${startCellId}"]`);
|
||||
this.selection.select($cell);
|
||||
|
||||
this.$emit('table:select-cell', $cell.data.value);
|
||||
|
||||
this.$on('formula:input', (data) => {
|
||||
this.selection.current.attr('data-value', data);
|
||||
this.selection.current.text = parse(data);
|
||||
this.updateCurrentTextInStore(data);
|
||||
});
|
||||
|
||||
this.$on('formula:enter-press', () => {
|
||||
this.selection.current.focus();
|
||||
});
|
||||
|
||||
this.$on('toolbar:applyStyle', (value) => {
|
||||
this.selection.applyStyle(value);
|
||||
|
||||
this.$dispatch(actions.applyStyle({
|
||||
value,
|
||||
ids: this.selection.selectedIds,
|
||||
}));
|
||||
});
|
||||
|
||||
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() {
|
||||
@ -95,25 +72,14 @@ export class Table extends ExcelComponent {
|
||||
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` }));
|
||||
Object.keys(size.col).forEach(key => {
|
||||
const cols = this.$root.findAll(`[data-col="${key}"]`);
|
||||
cols.forEach(el => $(el as HTMLElement).css({ width: `${size.col[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` }));
|
||||
Object.keys(size.row).forEach(key => {
|
||||
const rows = this.$root.findAll(`[data-row="${key}"]`);
|
||||
rows.forEach(el => $(el as HTMLElement).css({ height: `${size.row[key]}px` }));
|
||||
});
|
||||
}
|
||||
|
||||
@ -121,192 +87,49 @@ export class Table extends ExcelComponent {
|
||||
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() {
|
||||
this.$emit('table:select-cell', this.selection.current.data.value);
|
||||
|
||||
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));
|
||||
const styles = this.selection.current?.getStyles(Object.keys(initialStyleState));
|
||||
this.$dispatch(changeCurrentStyles(styles));
|
||||
}
|
||||
|
||||
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;
|
||||
this.$dispatch(actions.tableResize({ resizeData }));
|
||||
} 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,
|
||||
updateCurrentTextInStore(text: string) {
|
||||
this.$dispatch(actions.changeText({
|
||||
text,
|
||||
id: this.selection.current.data.id || startCellId,
|
||||
}));
|
||||
};
|
||||
|
||||
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) {
|
||||
selectHandler(event, this.selection, this.emitSelectCallback.bind(this));
|
||||
this.resizeTable(event);
|
||||
this.selectionManager.onMouseDownHandler(event);
|
||||
}
|
||||
|
||||
onKeydown(event: KeyboardEvent) {
|
||||
this.selectionManager.onKeyDownHandler(event);
|
||||
selectHandler(event, this.selection, this.emitSelectCallback.bind(this));
|
||||
}
|
||||
|
||||
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);
|
||||
this.updateCurrentTextInStore((event.target as HTMLElement).innerText.trim());
|
||||
}
|
||||
}
|
||||
|
||||
67
src/components/table/TableSelection.ts
Normal file
67
src/components/table/TableSelection.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { $, Dom } from 'core/dom';
|
||||
import { getParamsFromCellId, startCellId } from 'components/table/table.functions';
|
||||
|
||||
export class TableSelection {
|
||||
static selectedClassName = 'selected';
|
||||
private group: Dom[];
|
||||
public current: Dom;
|
||||
|
||||
constructor() {
|
||||
this.group = [];
|
||||
}
|
||||
|
||||
get selectedIds() {
|
||||
return this.group.map(el => el.data.id);
|
||||
}
|
||||
|
||||
select($el: Dom) {
|
||||
this.clearSelection();
|
||||
this.group = [$el];
|
||||
this.current = $el;
|
||||
$el.addClass(TableSelection.selectedClassName);
|
||||
$el.focus();
|
||||
}
|
||||
|
||||
selectByCellId(cellID: { col: number, row: number }) {
|
||||
let { col, row } = cellID;
|
||||
|
||||
if (col <= 0) col = 0;
|
||||
if (row <= 0) row = 0;
|
||||
|
||||
this.clearSelection();
|
||||
this.current = $(`[data-id="${row}:${col}"]`);
|
||||
this.current.focus();
|
||||
this.current.addClass(TableSelection.selectedClassName);
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this.group.forEach(el => el?.removeClass(TableSelection.selectedClassName));
|
||||
this.current?.removeClass(TableSelection.selectedClassName);
|
||||
this.group = [];
|
||||
}
|
||||
|
||||
selectGroup($el: Dom) {
|
||||
const startCellParams = getParamsFromCellId(this.current.data.id || startCellId);
|
||||
const selectedCellParams = getParamsFromCellId($el.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 cell = $(`[data-id="${row}:${col}"]`);
|
||||
|
||||
this.group.push(cell);
|
||||
cell.addClass(TableSelection.selectedClassName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applyStyle(style: CSSStyleRule) {
|
||||
this.group.forEach(el => el.css(style));
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
import { startCellId } from 'components/table/table.functions';
|
||||
import { $, Dom } from 'core/Dom';
|
||||
import { $, Dom } from 'core/dom';
|
||||
|
||||
export type ResizeReturnDataType = { value: number, id: string, type: string };
|
||||
type CustomElementType = Element & { css: any };
|
||||
type ResizeReturnDataType = { value: number, id: string, type: string };
|
||||
|
||||
export function resizeHandler($root: Dom, event: MouseEvent) {
|
||||
return new Promise<ResizeReturnDataType>(res => {
|
||||
@ -17,7 +18,13 @@ export function resizeHandler($root: Dom, event: MouseEvent) {
|
||||
|
||||
let delta: number;
|
||||
|
||||
$resizer.css({ opacity: '1' });
|
||||
(Element.prototype as CustomElementType).css = function (styles: any) {
|
||||
Object.keys(styles).forEach((key: any) => {
|
||||
this.style[key] = styles[key];
|
||||
});
|
||||
};
|
||||
|
||||
$resizer.css({ opacity: 1 });
|
||||
|
||||
document.onmousemove = e => {
|
||||
document.body.style.userSelect = 'none';
|
||||
@ -77,7 +84,7 @@ export function resizeHandler($root: Dom, event: MouseEvent) {
|
||||
|
||||
res({ value, id, type });
|
||||
|
||||
$resizer.css({ opacity: '0', bottom: '0', right: '0' });
|
||||
$resizer.css({ opacity: 0, bottom: 0, right: 0 });
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
79
src/components/table/handlers/table.select.handler.ts
Normal file
79
src/components/table/handlers/table.select.handler.ts
Normal file
@ -0,0 +1,79 @@
|
||||
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;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
|
||||
// Analog callback && callback();
|
||||
callback?.();
|
||||
|
||||
function onMouseDownHandler() {
|
||||
if (isCell(event)) {
|
||||
if (event.shiftKey) selection.selectGroup($(event.target as HTMLElement));
|
||||
else selection.select($(event.target as HTMLElement));
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyDownHandler() {
|
||||
const { key } = event as KeyboardEvent;
|
||||
const handleKeys = [
|
||||
'ArrowDown',
|
||||
'ArrowUp',
|
||||
'ArrowRight',
|
||||
'ArrowLeft',
|
||||
'Enter',
|
||||
'Tab',
|
||||
];
|
||||
|
||||
if (!selection?.current || !handleKeys.includes(key)) return;
|
||||
|
||||
// If something goes wrong, go to start line
|
||||
const currentCellId = selection.current.data.id || startCellId;
|
||||
let { row, col } = getParamsFromCellId(currentCellId);
|
||||
|
||||
switch (key) {
|
||||
case 'ArrowDown': {
|
||||
row++;
|
||||
break;
|
||||
}
|
||||
case 'ArrowUp': {
|
||||
row--;
|
||||
break;
|
||||
}
|
||||
case 'ArrowRight': {
|
||||
col++;
|
||||
break;
|
||||
}
|
||||
case 'ArrowLeft': {
|
||||
col--;
|
||||
break;
|
||||
}
|
||||
case 'Enter': {
|
||||
if (event.shiftKey) return;
|
||||
|
||||
event.preventDefault();
|
||||
row++;
|
||||
break;
|
||||
}
|
||||
case 'Tab': {
|
||||
event.preventDefault();
|
||||
col++;
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
|
||||
selection.selectByCellId({ row, col });
|
||||
}
|
||||
}
|
||||
@ -1,16 +1,7 @@
|
||||
import { SelectionManager } from 'components/table/SelectionManager';
|
||||
import { $, Dom } from 'core/Dom';
|
||||
import { $ } 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);
|
||||
return $(event.target as HTMLElement).data.type === 'cell';
|
||||
}
|
||||
|
||||
export function getParamsFromCellId(cellId: string) {
|
||||
@ -20,8 +11,4 @@ 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';
|
||||
|
||||
@ -8,7 +8,6 @@ function createCell(cellContent = '', colIndex = 1, rowIndex = 1) {
|
||||
<div
|
||||
class="cell"
|
||||
contenteditable
|
||||
spellcheck="false"
|
||||
data-col="${colIndex}"
|
||||
data-id="${rowIndex}:${colIndex}"
|
||||
data-type="cell"
|
||||
@ -21,17 +20,17 @@ function createCell(cellContent = '', colIndex = 1, rowIndex = 1) {
|
||||
|
||||
function createCol(columnContent = '', index = 1) {
|
||||
return `
|
||||
<div class="column" data-type="resizable" data-col="${index}" data-header="col">
|
||||
<div class="column" data-type="resizable" data-col="${index}">
|
||||
${columnContent}
|
||||
<div class="col-resize" data-resize="col"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function createRow(dataContent = '', infoContent = '', needResize = true, rowIndex = -1): string {
|
||||
function createRow(dataContent = '', infoContent = '', needResize = true, rowIndex = -1) {
|
||||
return `
|
||||
<div class="row" data-type="resizable" data-row="${rowIndex}">
|
||||
<div class="row-info" data-header="row">
|
||||
<div class="row-info">
|
||||
${infoContent}
|
||||
${needResize ? '<div class="row-resize" data-resize="row"></div>' : ''}
|
||||
</div>
|
||||
|
||||
@ -1,27 +1,26 @@
|
||||
import { BaseComponentOption } from 'components/excel/Excel';
|
||||
import { startCellId } from 'components/table/table.functions';
|
||||
import { $, Dom } from 'core/Dom';
|
||||
import { ExcelComponentState } from 'core/ExcelComponentState';
|
||||
import { $, Dom } from 'core/dom';
|
||||
import { ExcelStateComponent } from 'core/ExcelStateComponent';
|
||||
import { OptionsType } from 'core/ExcelComponent';
|
||||
import { createToolbar } from 'components/toolbar/toolbar.template';
|
||||
import { StateType } from 'redux/types';
|
||||
import { fontSizes, initialStyleState } from 'src/constants';
|
||||
import { initialStyleState } from 'src/constants';
|
||||
|
||||
export class Toolbar extends ExcelComponentState {
|
||||
export class Toolbar extends ExcelStateComponent {
|
||||
static className = 'excel__toolbar';
|
||||
|
||||
constructor($root: Dom, options: BaseComponentOption) {
|
||||
constructor($root: Dom, options: OptionsType) {
|
||||
super($root, {
|
||||
...options,
|
||||
eventListeners: ['click', 'change'],
|
||||
listeners: ['click'],
|
||||
name: 'Toolbar',
|
||||
subscribe: ['currentStyles'],
|
||||
});
|
||||
}
|
||||
|
||||
beforeRender() {
|
||||
prepare() {
|
||||
const currentToolbarState = this.toolbarState;
|
||||
|
||||
this.initComponentState(currentToolbarState);
|
||||
this.initState(currentToolbarState);
|
||||
}
|
||||
|
||||
get toolbarState() {
|
||||
@ -32,97 +31,27 @@ export class Toolbar extends ExcelComponentState {
|
||||
}
|
||||
|
||||
get template(): string {
|
||||
return createToolbar(this.componentState);
|
||||
return createToolbar(this.state);
|
||||
}
|
||||
|
||||
toHTML(): string {
|
||||
return this.template;
|
||||
}
|
||||
|
||||
storeChanged(args: StateType) {
|
||||
if (!args) return;
|
||||
|
||||
this.setComponentState(args.currentStyles);
|
||||
storeChanged(args?: any) {
|
||||
this.setState(args.currentStyles);
|
||||
}
|
||||
|
||||
onClick(event: MouseEvent) {
|
||||
// TODO refactor, make style handler
|
||||
const target = $(event.target);
|
||||
const target = $(event.target as HTMLElement);
|
||||
const stringValue = target?.data?.value;
|
||||
if (!stringValue) return;
|
||||
|
||||
if (target.closest('[data-add-row-btn]').isExist) {
|
||||
this.$emitEventToObserver('toolbar:add-row');
|
||||
return;
|
||||
}
|
||||
const value = JSON.parse(stringValue);
|
||||
const key = Object.keys(value)[0];
|
||||
|
||||
if (target.closest('[data-remove-row-btn]').isExist) {
|
||||
this.$emitEventToObserver('toolbar:remove-row');
|
||||
return;
|
||||
}
|
||||
this.$emit('toolbar:applyStyle', value);
|
||||
|
||||
let stringValue;
|
||||
let value;
|
||||
let key;
|
||||
|
||||
switch (true) {
|
||||
case !!target.closest('[data-change-size]').$el: {
|
||||
const el = target.closest('[data-change-size]');
|
||||
|
||||
if (el.hasClass('disable')) return;
|
||||
|
||||
const currentSize = this.store.getState().currentStyles.fontSize;
|
||||
let idx = fontSizes.findIndex(font => font === currentSize);
|
||||
let nextSize;
|
||||
|
||||
if (el.data.changeSize === 'increase') {
|
||||
nextSize = fontSizes[++idx];
|
||||
} else if (el.data.changeSize === 'decrease') {
|
||||
nextSize = fontSizes[--idx];
|
||||
}
|
||||
|
||||
value = { fontSize: nextSize };
|
||||
key = 'fontSize';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
stringValue = target?.data?.value;
|
||||
if (!stringValue) return;
|
||||
|
||||
value = JSON.parse(stringValue);
|
||||
key = Object.keys(value)[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (value && key) {
|
||||
this.$emitEventToObserver('toolbar:applyStyle', value);
|
||||
this.setComponentState({ [key]: value[key] });
|
||||
}
|
||||
}
|
||||
|
||||
// TODO find EventType
|
||||
onChange(e: any) {
|
||||
const target = $(e.target);
|
||||
let value = '';
|
||||
let key = '';
|
||||
|
||||
switch (true) {
|
||||
case !!target.closest('#button-size').$el:
|
||||
value = `${e.target.value.toString()}px`;
|
||||
key = 'fontSize';
|
||||
break;
|
||||
|
||||
case !!target.closest('#button-font').$el:
|
||||
value = e.target.value;
|
||||
key = 'fontFamily';
|
||||
break;
|
||||
|
||||
default: break;
|
||||
}
|
||||
|
||||
if (key && value) {
|
||||
this.$emitEventToObserver('toolbar:applyStyle', { [key]: value });
|
||||
this.setComponentState({ [key]: value });
|
||||
}
|
||||
this.setState({ [key]: value[key] });
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,5 @@ export type ToolbarStateType = {
|
||||
fontWeight?: 'normal' | 'bold';
|
||||
fontStyle?: 'normal' | 'italic';
|
||||
textDecoration?: 'none' | 'underline';
|
||||
justifyContent?: 'start' | 'center' | 'end';
|
||||
alignItems?: 'start' | 'center' | 'end';
|
||||
fontSize?: string;
|
||||
fontFamily?: string
|
||||
textAlign?: 'left' | 'center' | 'right';
|
||||
};
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { ToolbarStateType } from 'components/toolbar/toolbar-types';
|
||||
import { isLargestFontSize, isSmallestFontSize } from 'core/utils';
|
||||
import { fontFamilies, fontSizes, initialStyleState } from 'src/constants';
|
||||
import { initialStyleState } from 'src/constants';
|
||||
|
||||
type ButtonConfigType = {
|
||||
icon: string;
|
||||
@ -9,180 +8,66 @@ type ButtonConfigType = {
|
||||
};
|
||||
|
||||
export function createToolbar(state: ToolbarStateType): string {
|
||||
const btns: (ButtonConfigType | ButtonConfigType[])[] = [
|
||||
[
|
||||
{
|
||||
icon: 'format_bold',
|
||||
isActive: state.fontWeight === 'bold',
|
||||
value: {
|
||||
fontWeight: state.fontWeight === 'bold' ? initialStyleState.fontWeight : 'bold',
|
||||
},
|
||||
const btns: ButtonConfigType[] = [
|
||||
{
|
||||
icon: 'format_bold',
|
||||
isActive: state.fontWeight === 'bold',
|
||||
value: {
|
||||
fontWeight: state.fontWeight === 'bold' ? initialStyleState.fontWeight : 'bold',
|
||||
},
|
||||
{
|
||||
icon: 'format_italic',
|
||||
isActive: state.fontStyle === 'italic',
|
||||
value: {
|
||||
fontStyle: state.fontStyle === 'italic' ? initialStyleState.fontStyle : 'italic',
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'format_italic',
|
||||
isActive: state.fontStyle === 'italic',
|
||||
value: {
|
||||
fontStyle: state.fontStyle === 'italic' ? initialStyleState.fontStyle : 'italic',
|
||||
},
|
||||
{
|
||||
icon: 'format_underline',
|
||||
isActive: state.textDecoration === 'underline',
|
||||
value: {
|
||||
textDecoration: state.textDecoration === 'underline' ? initialStyleState.textDecoration : 'underline',
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'format_underline',
|
||||
isActive: state.textDecoration === 'underline',
|
||||
value: {
|
||||
textDecoration: state.textDecoration === 'underline' ? initialStyleState.textDecoration : 'underline',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
icon: 'format_align_left',
|
||||
isActive: state.justifyContent === 'start',
|
||||
value: {
|
||||
justifyContent: 'start',
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'format_align_left',
|
||||
isActive: state.textAlign === 'left',
|
||||
value: {
|
||||
textAlign: 'left',
|
||||
},
|
||||
{
|
||||
icon: 'format_align_center',
|
||||
isActive: state.justifyContent === 'center',
|
||||
value: {
|
||||
justifyContent: state.justifyContent === 'center' ? initialStyleState.justifyContent : 'center',
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'format_align_center',
|
||||
isActive: state.textAlign === 'center',
|
||||
value: {
|
||||
textAlign: state.textAlign === 'center' ? initialStyleState.textAlign : 'center',
|
||||
},
|
||||
{
|
||||
icon: 'format_align_right',
|
||||
isActive: state.justifyContent === 'end',
|
||||
value: {
|
||||
justifyContent: state.justifyContent === 'end' ? initialStyleState.justifyContent : 'end',
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'format_align_right',
|
||||
isActive: state.textAlign === 'right',
|
||||
value: {
|
||||
textAlign: state.textAlign === 'right' ? initialStyleState.textAlign : 'right',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
icon: 'vertical_align_bottom',
|
||||
isActive: state.alignItems === 'end',
|
||||
value: {
|
||||
alignItems: state.alignItems === 'end' ? initialStyleState.alignItems : 'end',
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'vertical_align_center',
|
||||
isActive: state.alignItems === 'center',
|
||||
value: {
|
||||
alignItems: state.alignItems === 'center' ? initialStyleState.alignItems : 'center',
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'vertical_align_top',
|
||||
isActive: state.alignItems === 'start',
|
||||
value: {
|
||||
alignItems: state.alignItems === 'start' ? initialStyleState.alignItems : 'start',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const buttons = btns.map(btn => (Array.isArray(btn) ? createButtonsFromConfigGroup(btn) : createButtonFromConfig(btn)));
|
||||
|
||||
const selectGroup = createGroup(createFontSizeButton(state.fontSize), createFontFamilyButton(state.fontFamily));
|
||||
const increaseDecreaseFontSize = createSizeUpDownButtons(state.fontSize);
|
||||
|
||||
buttons.push(increaseDecreaseFontSize);
|
||||
buttons.push(selectGroup);
|
||||
// buttons.push(createAddRowButton());
|
||||
// buttons.push(createRemoveRowButton());
|
||||
|
||||
return buttons.join('');
|
||||
return btns.map(btn => toButton(btn)).join(' ');
|
||||
}
|
||||
|
||||
function createSizeUpDownButtons(currentSize = initialStyleState.fontSize) {
|
||||
const buttons = ['increase', 'decrease'];
|
||||
|
||||
return buttons.map(btn => {
|
||||
const disable = btn === 'increase' ? isLargestFontSize(currentSize) : isSmallestFontSize(currentSize);
|
||||
return `
|
||||
<div class="button${disable ? ' disable' : ''}" data-change-size="${btn}">
|
||||
<i class="material-icons">text_${btn}</i>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function createFontSizeButton(currentSize = initialStyleState.fontSize) {
|
||||
const options = fontSizes.map(size => {
|
||||
// remove 'px' part
|
||||
const sizeValue = size.slice(0, -2);
|
||||
return size === currentSize
|
||||
? `<option value="${sizeValue}" selected>${sizeValue}</option>`
|
||||
: `<option value="${sizeValue}">${sizeValue}</option>`;
|
||||
});
|
||||
|
||||
return `
|
||||
<div class="button">
|
||||
<select class="button__size" id="button-size">
|
||||
${options.join('')}
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function createFontFamilyButton(font = initialStyleState.fontFamily) {
|
||||
const options = fontFamilies.map(fontName => createFontFamilyOption(fontName, fontName === font));
|
||||
|
||||
function createFontFamilyOption(name: string, selected: boolean) {
|
||||
return selected
|
||||
? `<option value="${name}" style="font-family: ${name}" selected >${name}</option>`
|
||||
: `<option value="${name}" style="font-family: ${name}">${name}</option>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="button">
|
||||
<select class="button__font" id="button-font">
|
||||
${options.join('')}
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function createGroup(...btns: string[]) {
|
||||
return `
|
||||
<div class="button__group">
|
||||
${btns.join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function createButtonsFromConfigGroup(buttons: ButtonConfigType[]) {
|
||||
if (buttons.length === 1) return createButtonFromConfig(buttons[0]);
|
||||
const btns = buttons.map(btn => createButtonFromConfig(btn));
|
||||
|
||||
return createGroup(...btns);
|
||||
}
|
||||
|
||||
function createButtonFromConfig(btnConfig: ButtonConfigType) {
|
||||
function toButton(button: ButtonConfigType) {
|
||||
const meta = `
|
||||
data-type="button"
|
||||
data-value='${JSON.stringify(btnConfig.value)}'
|
||||
data-value='${JSON.stringify(button.value)}'
|
||||
`;
|
||||
|
||||
return `
|
||||
<div class="button ${btnConfig.isActive ? 'active' : ''}"${meta}>
|
||||
<i class="material-icons" ${meta}>${btnConfig.icon}</i>
|
||||
<div
|
||||
class="button ${button.isActive && 'active'}"
|
||||
${meta}
|
||||
>
|
||||
<i class="material-icons" ${meta}>${button.icon}</i>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// function createAddRowButton() {
|
||||
// return `
|
||||
// <div class="button" data-add-row-btn>
|
||||
// <i class="material-icons" data-add-row-btn>add_circle</i>
|
||||
// </div>
|
||||
// `;
|
||||
// }
|
||||
//
|
||||
// function createRemoveRowButton() {
|
||||
// return `
|
||||
// <div class="button" data-remove-row-btn>
|
||||
// <i class="material-icons" data-remove-row-btn>remove_circle</i>
|
||||
// </div>
|
||||
// `;
|
||||
// }
|
||||
|
||||
@ -1,57 +1,26 @@
|
||||
import { startCellId } from 'components/table/table.functions';
|
||||
import { ToolbarStateType } from 'components/toolbar/toolbar-types';
|
||||
import { StateType } from 'redux/types';
|
||||
import { storage, storageName } from 'core/utils';
|
||||
|
||||
export const fontSizes = [
|
||||
'12px',
|
||||
'14px',
|
||||
'16px',
|
||||
'18px',
|
||||
'20px',
|
||||
'22px',
|
||||
'24px',
|
||||
'26px',
|
||||
'28px',
|
||||
'30px',
|
||||
];
|
||||
export const fontFamilies = ['Roboto', 'Cormorant SC', 'Kanit', 'Playfair Display'];
|
||||
import { storage } from 'core/utils';
|
||||
|
||||
export const initialStyleState: ToolbarStateType = {
|
||||
justifyContent: 'start',
|
||||
textAlign: 'left',
|
||||
fontWeight: 'normal',
|
||||
textDecoration: 'none',
|
||||
fontStyle: 'normal',
|
||||
fontSize: fontSizes[0],
|
||||
fontFamily: fontFamilies[0],
|
||||
alignItems: 'start',
|
||||
};
|
||||
|
||||
export const initialState: StateType = {
|
||||
colState: {},
|
||||
rowState: {},
|
||||
dataState: {},
|
||||
stylesState: {},
|
||||
title: 'New excel table',
|
||||
openDate: Date.now(),
|
||||
currentStyles: initialStyleState,
|
||||
currentText: 'initial text',
|
||||
id: '0',
|
||||
tableSize: {
|
||||
col: 20,
|
||||
row: 30,
|
||||
},
|
||||
};
|
||||
|
||||
export function getNormalizeInitialState(params: string): StateType {
|
||||
const state = storage(storageName(params));
|
||||
// if (!state) return initialState;
|
||||
|
||||
return {
|
||||
...initialState,
|
||||
...state,
|
||||
colState: {},
|
||||
rowState: {},
|
||||
dataState: {},
|
||||
currentText: '',
|
||||
stylesState: {},
|
||||
title: 'New excel table',
|
||||
id: params,
|
||||
currentStyles: state?.stylesState?.[startCellId] || initialStyleState,
|
||||
currentText: state?.dataState?.[startCellId],
|
||||
openDate: Date.now(),
|
||||
...storage(`excel:${params}`),
|
||||
currentStyles: { ...storage(`excel:${params}`)?.stylesState?.[startCellId] },
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
import { storage, storageName } from 'core/utils';
|
||||
import { storage } from 'core/utils';
|
||||
import { storageName } from 'pages/ExcelPage';
|
||||
import { StateType } from 'redux/types';
|
||||
import { getNormalizeInitialState } from 'src/constants';
|
||||
|
||||
export interface ClientDataType {
|
||||
save: (state: StateType) => Promise<void>;
|
||||
get: () => Promise<StateType>;
|
||||
save: (state: StateType) => Promise<any>;
|
||||
get: () => Promise<any>;
|
||||
}
|
||||
|
||||
export class LocalStorageClient implements ClientDataType {
|
||||
private readonly name: string;
|
||||
private name: string;
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
@ -19,10 +20,12 @@ export class LocalStorageClient implements ClientDataType {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
get(): Promise<StateType> {
|
||||
get() {
|
||||
const data = storage(storageName(this.name)) || getNormalizeInitialState(this.name);
|
||||
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve(getNormalizeInitialState(this.name));
|
||||
resolve(data);
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
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);
|
||||
$el.attr('tabindex', '-1');
|
||||
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());
|
||||
}
|
||||
}
|
||||
@ -1,40 +1,41 @@
|
||||
import { Dom } from 'core/Dom';
|
||||
import { getMethodNameByEventName } from 'core/utils';
|
||||
|
||||
// TODO fix types
|
||||
import { Dom } from 'core/dom';
|
||||
import { capitalize } from 'core/utils';
|
||||
|
||||
export class DomListener {
|
||||
$root: Dom;
|
||||
eventListeners: string[];
|
||||
name: string;
|
||||
listeners: string[];
|
||||
|
||||
constructor($root: Dom, eventNames: string[]) {
|
||||
constructor($root: Dom, listeners: string[]) {
|
||||
if (!$root) throw new Error('Не передали корневой элемент');
|
||||
|
||||
this.$root = $root;
|
||||
this.eventListeners = eventNames;
|
||||
this.listeners = listeners;
|
||||
}
|
||||
|
||||
initDOMListeners() {
|
||||
if (!this.eventListeners) return;
|
||||
if (!this.listeners) return;
|
||||
|
||||
this.eventListeners.forEach((listener: string) => {
|
||||
const method = getMethodNameByEventName(listener);
|
||||
// @ts-ignore
|
||||
this.listeners.forEach((listener: string) => {
|
||||
const method: any = getMethodName(listener);
|
||||
// @ts-ignore FIXME:
|
||||
this[method] = this[method]?.bind(this);
|
||||
// @ts-ignore
|
||||
// @ts-ignore FIXME:
|
||||
if (!this[method]) throw new Error(`Отсутствует метод ${method} в компоненте ${this?.name}`);
|
||||
|
||||
// @ts-ignore
|
||||
// @ts-ignore FIXME:
|
||||
this.$root.on(listener, this[method]);
|
||||
});
|
||||
}
|
||||
|
||||
removeDOMListeners() {
|
||||
this.eventListeners.forEach(listener => {
|
||||
const method = getMethodNameByEventName(listener);
|
||||
// @ts-ignore
|
||||
this.listeners.forEach(listener => {
|
||||
// @ts-ignore FIXME:
|
||||
const method: any = getMethodName(listener);
|
||||
// @ts-ignore FIXME:
|
||||
this.$root.off(listener, this[method]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getMethodName(eventName: string): string {
|
||||
return `on${capitalize(eventName)}`;
|
||||
}
|
||||
|
||||
@ -1,15 +1,23 @@
|
||||
import { CallbackType } from 'redux/types';
|
||||
|
||||
export class Observer {
|
||||
private readonly listeners: {
|
||||
[k: string]: Array<CallbackType>
|
||||
export class Emitter {
|
||||
private listeners: {
|
||||
[k: string]: Array<(args?: any) => any>
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.listeners = {};
|
||||
}
|
||||
|
||||
subscribe(eventName: string, callback: CallbackType) {
|
||||
emit(eventName: string, args: any[]) {
|
||||
if (!Array.isArray(this.listeners[eventName])) return false;
|
||||
|
||||
this.listeners[eventName].forEach(listener => {
|
||||
listener(args);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
subscribe(eventName: string, callback: (args?: any) => any) {
|
||||
this.listeners[eventName] = this.listeners[eventName] || [];
|
||||
this.listeners[eventName].push(callback);
|
||||
|
||||
@ -17,12 +25,4 @@ export class Observer {
|
||||
this.listeners[eventName] = this.listeners[eventName].filter(listener => listener !== callback);
|
||||
};
|
||||
}
|
||||
|
||||
emit(eventName: string, args: any[]) {
|
||||
if (!Array.isArray(this.listeners[eventName])) return false;
|
||||
|
||||
this.listeners[eventName].forEach(listener => listener(args));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -1,36 +1,42 @@
|
||||
import { BaseComponentOption } from 'components/excel/Excel';
|
||||
import { ComponentManager } from 'core/ComponentManager';
|
||||
import { ActionType, CallbackType, StateType } from 'redux/types';
|
||||
import { Dom } from 'core/Dom';
|
||||
import { ActionType } from 'redux/types';
|
||||
import { Dom } from 'core/dom';
|
||||
import { DomListener } from 'core/DomListener';
|
||||
import { Observer } from 'core/Observer';
|
||||
import { Store } from 'core/store/Store';
|
||||
import { Emitter } from 'core/Emitter';
|
||||
import { Store } from 'core/store/createStore';
|
||||
|
||||
export type ComponentOptionsType = BaseComponentOption & {
|
||||
eventListeners: string[];
|
||||
interface ExcelComponentClass {
|
||||
toHTML: () => string;
|
||||
prepare: () => void;
|
||||
storeChanged?: (args: any) => void;
|
||||
}
|
||||
|
||||
export type OptionsType = {
|
||||
listeners: string[];
|
||||
name: string;
|
||||
subscribe?: (keyof StateType)[],
|
||||
emitter: Emitter;
|
||||
store: Store;
|
||||
subscribe: string[],
|
||||
};
|
||||
|
||||
export abstract class ExcelComponent extends DomListener {
|
||||
observer: Observer;
|
||||
export abstract class ExcelComponent extends DomListener implements ExcelComponentClass {
|
||||
private name: string;
|
||||
private emitter: Emitter;
|
||||
public store: Store;
|
||||
private subscribe: (keyof StateType)[];
|
||||
private unsubscribers: CallbackType[];
|
||||
private componentManager: ComponentManager;
|
||||
private subscribe: string[];
|
||||
private unsubscribers: ((args?: any) => any)[];
|
||||
|
||||
protected constructor($root: Dom, options: ComponentOptionsType) {
|
||||
super($root, options.eventListeners);
|
||||
constructor($root: Dom, options: OptionsType) {
|
||||
super($root, options.listeners);
|
||||
this.name = options.name;
|
||||
this.observer = options.observer;
|
||||
this.emitter = options.emitter;
|
||||
this.store = options.store;
|
||||
this.subscribe = options?.subscribe || [];
|
||||
this.componentManager = options.componentManager;
|
||||
this.subscribe = options.subscribe;
|
||||
|
||||
this.unsubscribers = [];
|
||||
this.beforeRender();
|
||||
this.prepare();
|
||||
}
|
||||
|
||||
beforeRender() {
|
||||
prepare() {
|
||||
|
||||
}
|
||||
|
||||
@ -38,29 +44,28 @@ export abstract class ExcelComponent extends DomListener {
|
||||
return '';
|
||||
}
|
||||
|
||||
$emitEventToObserver(event: string, args?: any): boolean {
|
||||
return this.observer?.emit(event, args);
|
||||
$emit(event: string, args?: any): void {
|
||||
this.emitter?.emit(event, args);
|
||||
}
|
||||
|
||||
$onEventFromObserver(event: string, callback: (args: any) => any) {
|
||||
const unsub = this.observer?.subscribe(event, callback);
|
||||
$on(event: string, callback: (args: any) => any) {
|
||||
const unsub = this.emitter?.subscribe(event, callback);
|
||||
unsub && this.unsubscribers.push(unsub);
|
||||
}
|
||||
|
||||
dispatchToStore(action: ActionType) {
|
||||
this.store?.dispatchToStore(action);
|
||||
$dispatch(action: ActionType) {
|
||||
this.store?.dispatch(action);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
storeChanged(args: StateType) {
|
||||
// console.log('CHANGE STORE: ', args, ' in component ', this.name);
|
||||
storeChanged(args?: any) {
|
||||
console.log('CHANGE STORE: ', args);
|
||||
}
|
||||
|
||||
isWatching(key: keyof StateType) {
|
||||
isWatching(key: string) {
|
||||
return this.subscribe?.includes(key);
|
||||
}
|
||||
|
||||
afterRender() {
|
||||
init() {
|
||||
this.initDOMListeners();
|
||||
}
|
||||
|
||||
@ -68,8 +73,4 @@ export abstract class ExcelComponent extends DomListener {
|
||||
this.removeDOMListeners();
|
||||
this.unsubscribers.forEach(unsub => unsub());
|
||||
}
|
||||
|
||||
rerender() {
|
||||
this.componentManager.rerenderComponent(this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
import { Dom } from 'core/Dom';
|
||||
import { ExcelComponent, ComponentOptionsType } from 'core/ExcelComponent';
|
||||
|
||||
type ExcelComponentStateType = {
|
||||
[k: string]: any;
|
||||
};
|
||||
|
||||
export abstract class ExcelComponentState extends ExcelComponent {
|
||||
componentState: ExcelComponentStateType;
|
||||
|
||||
protected constructor(root: Dom, options: ComponentOptionsType) {
|
||||
super(root, options);
|
||||
}
|
||||
|
||||
get template(): string {
|
||||
return JSON.stringify(this.componentState, null, 2);
|
||||
}
|
||||
|
||||
initComponentState(initialState = {}) {
|
||||
this.componentState = { ...initialState };
|
||||
}
|
||||
|
||||
setComponentState(newState: ExcelComponentStateType) {
|
||||
if (!newState) return;
|
||||
|
||||
this.componentState = { ...this.componentState, ...newState };
|
||||
this.$root.html(this.template);
|
||||
}
|
||||
}
|
||||
27
src/core/ExcelStateComponent.ts
Normal file
27
src/core/ExcelStateComponent.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Dom } from 'core/dom';
|
||||
import { ExcelComponent, OptionsType } from 'core/ExcelComponent';
|
||||
|
||||
type ExcelComponentStateType = {
|
||||
[k: string]: any;
|
||||
};
|
||||
|
||||
export abstract class ExcelStateComponent extends ExcelComponent {
|
||||
state: ExcelComponentStateType;
|
||||
|
||||
protected constructor(root: Dom, options: OptionsType) {
|
||||
super(root, options);
|
||||
}
|
||||
|
||||
get template(): string {
|
||||
return JSON.stringify(this.state, null, 2);
|
||||
}
|
||||
|
||||
initState(initialState = {}) {
|
||||
this.state = { ...initialState };
|
||||
}
|
||||
|
||||
setState(newState: ExcelComponentStateType) {
|
||||
this.state = { ...this.state, ...newState };
|
||||
this.$root.html(this.template);
|
||||
}
|
||||
}
|
||||
@ -5,9 +5,9 @@ import { StateType } from 'redux/types';
|
||||
export class StateProcessor {
|
||||
private client: ClientDataType;
|
||||
|
||||
constructor(client: ClientDataType, delay = 300) {
|
||||
constructor(client: ClientDataType, dalay = 300) {
|
||||
this.client = client;
|
||||
this.listen = debounce(this.listen.bind(this), delay);
|
||||
this.listen = debounce(this.listen.bind(this), dalay);
|
||||
}
|
||||
|
||||
listen(state: StateType) {
|
||||
|
||||
@ -1,36 +1,37 @@
|
||||
import { StateType, SubscribeType } from 'redux/types';
|
||||
import { Store } from 'core/store/Store';
|
||||
import { StateType } from 'redux/types';
|
||||
import { Store } from 'core/store/createStore';
|
||||
import { isEqual } from 'core/utils';
|
||||
|
||||
export class StoreSubscriber {
|
||||
sub: SubscribeType | null;
|
||||
currentState: StateType;
|
||||
sub: any;
|
||||
prevState: StateType;
|
||||
|
||||
constructor(private store: Store) {
|
||||
this.sub = null;
|
||||
this.prevState = {};
|
||||
}
|
||||
|
||||
subscribeComponents(components: any[]) {
|
||||
this.currentState = this.store.getState();
|
||||
this.prevState = this.store.getState();
|
||||
|
||||
this.sub = this.store.subscribeToStore((newState: StateType) => {
|
||||
if (!newState) return;
|
||||
this.sub = this.store.subscribe((state: StateType) => {
|
||||
if (!state) return;
|
||||
|
||||
Object.keys(newState).forEach((key) => {
|
||||
if (!isEqual(this.currentState[key as keyof StateType], newState[key as keyof StateType])) {
|
||||
Object.keys(state).forEach(key => {
|
||||
if (!isEqual(this.prevState[key], state[key])) {
|
||||
components.forEach(component => {
|
||||
if (component.isWatching(key)) {
|
||||
component.storeChanged({ [key]: newState[key as keyof StateType] });
|
||||
const changes = { [key]: state[key] };
|
||||
component.storeChanged(changes);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.currentState = this.store.getState();
|
||||
this.prevState = this.store.getState();
|
||||
});
|
||||
}
|
||||
|
||||
unsubscribeFromStore() {
|
||||
this.sub?.unsubscribe();
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { CallbackType } from 'redux/types';
|
||||
import { ToolbarStateType } from 'components/toolbar/toolbar-types';
|
||||
import { initialStyleState } from 'src/constants';
|
||||
|
||||
export type SelectorType = string | HTMLElement | EventTarget | null;
|
||||
export type SelectorType = string | HTMLElement;
|
||||
|
||||
export interface DomClass {
|
||||
html(html?: string): string | DomClass;
|
||||
@ -13,16 +13,13 @@ export class Dom implements DomClass {
|
||||
$el: HTMLElement;
|
||||
|
||||
constructor(selector: SelectorType) {
|
||||
try {
|
||||
if (typeof selector === 'string') {
|
||||
const elementFromDOM = document.querySelector(selector);
|
||||
if (!elementFromDOM) throw new Error(`Can't find element with "${selector}" selector`);
|
||||
else this.$el = elementFromDOM as HTMLElement;
|
||||
} else {
|
||||
this.$el = selector as HTMLElement;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e.message);
|
||||
// Could not find element with selector in DOM, need a check
|
||||
if (typeof selector === 'string') {
|
||||
const elementFromDOM = document.querySelector(selector);
|
||||
if (!elementFromDOM) throw new Error(`Can't find element with "${selector}" selector`);
|
||||
else this.$el = elementFromDOM as HTMLElement;
|
||||
} else {
|
||||
this.$el = selector;
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,15 +30,13 @@ export class Dom implements DomClass {
|
||||
}
|
||||
|
||||
set text(text: string) {
|
||||
if (!this.$el) return;
|
||||
|
||||
if (!text) this.$el.innerText = '';
|
||||
this.$el.innerText = text;
|
||||
if (!text) this.$el.textContent = '';
|
||||
this.$el.textContent = text;
|
||||
}
|
||||
|
||||
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() {
|
||||
@ -50,22 +45,23 @@ export class Dom implements DomClass {
|
||||
return this;
|
||||
}
|
||||
|
||||
append(node: HTMLElement | Dom) {
|
||||
// FIXME: any
|
||||
append(node: any) {
|
||||
let child = node;
|
||||
|
||||
if (node instanceof Dom) child = node.$el;
|
||||
|
||||
if (this.$el.append) this.$el.append(child as HTMLElement);
|
||||
else this.$el.appendChild(child as HTMLElement);
|
||||
if (this.$el.append) this.$el.append(child);
|
||||
else this.$el.appendChild(child);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
on(eventType: string, callback: CallbackType) {
|
||||
on(eventType: string, callback: any) {
|
||||
this.$el.addEventListener(eventType, callback);
|
||||
}
|
||||
|
||||
off(eventType: string, callback: CallbackType) {
|
||||
off(eventType: string, callback: any) {
|
||||
this.$el.removeEventListener(eventType, callback);
|
||||
}
|
||||
|
||||
@ -73,18 +69,10 @@ 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);
|
||||
}
|
||||
@ -105,11 +93,11 @@ export class Dom implements DomClass {
|
||||
return this.$el.querySelectorAll(selector);
|
||||
}
|
||||
|
||||
css(styles: Partial<CSSStyleDeclaration>) {
|
||||
css(styles: any) {
|
||||
if (!styles) return;
|
||||
|
||||
Object.keys(styles)?.forEach((key: any) => {
|
||||
this.$el.style[key] = styles[key] as string;
|
||||
this.$el.style[key] = styles[key];
|
||||
});
|
||||
}
|
||||
|
||||
@ -117,50 +105,25 @@ export class Dom implements DomClass {
|
||||
this.$el?.classList.add(className);
|
||||
}
|
||||
|
||||
hasClass(className: string) {
|
||||
return Array.from(this.$el.classList).includes(className);
|
||||
}
|
||||
|
||||
removeClass(className: string) {
|
||||
this.$el?.classList.remove(className);
|
||||
}
|
||||
|
||||
getStyles(styles: string[]): Partial<CSSStyleDeclaration> {
|
||||
getStyles(styles: any[]) {
|
||||
return styles.reduce((res, s) => {
|
||||
// replace all need if case style value have 2 or more word, this.$el.style[s] return ""word value""
|
||||
// for example font-family
|
||||
// @ts-ignore
|
||||
res[s] = this.$el.style[s].replaceAll('"', '') || initialStyleState[s];
|
||||
res[s] = this.$el.style[s] || initialStyleState[s as keyof ToolbarStateType];
|
||||
return res;
|
||||
}, {});
|
||||
}
|
||||
|
||||
attr(name: string, value?: string) {
|
||||
if (value !== undefined) {
|
||||
attr(name: string, value: string) {
|
||||
if (value) {
|
||||
this.$el.setAttribute(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
return this.$el.getAttribute(name);
|
||||
}
|
||||
|
||||
removeChild($child: Dom) {
|
||||
this.$el.removeChild($child.$el);
|
||||
}
|
||||
|
||||
replaceChild($newChild: Dom, $oldChild: Dom) {
|
||||
this.$el.replaceChild($newChild.$el, $oldChild.$el);
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -1,4 +1,4 @@
|
||||
import { $, Dom, SelectorType } from 'core/Dom';
|
||||
import { $, Dom, SelectorType } from 'core/dom';
|
||||
import { ActiveRoute } from 'core/routes/ActiveRoute';
|
||||
import { DashboardPage } from 'pages/DashboardPage';
|
||||
import { ExcelPage } from 'pages/ExcelPage';
|
||||
@ -12,14 +12,15 @@ type RoutesType = {
|
||||
export class Router {
|
||||
private $placeholder: Dom;
|
||||
private routes: RoutesType;
|
||||
private page: DashboardPage | ExcelPage;
|
||||
private readonly loader: Dom;
|
||||
private page: DashboardPage | ExcelPage | null;
|
||||
private loader: Dom;
|
||||
|
||||
constructor(selector: SelectorType, routes: RoutesType) {
|
||||
if (!selector) throw new Error('Selector not provided');
|
||||
|
||||
this.$placeholder = $(selector);
|
||||
this.routes = routes;
|
||||
this.page = null;
|
||||
this.loader = Loader();
|
||||
|
||||
this.changePageHandler = this.changePageHandler.bind(this);
|
||||
@ -34,8 +35,8 @@ export class Router {
|
||||
}
|
||||
|
||||
async changePageHandler() {
|
||||
this.page?.destroy();
|
||||
this.$placeholder.clear().append(this.loader);
|
||||
this.page?.destroy();
|
||||
|
||||
let Page;
|
||||
|
||||
@ -49,13 +50,14 @@ export class Router {
|
||||
Page = this.routes.dashboard;
|
||||
break;
|
||||
}
|
||||
// @ts-ignore
|
||||
this.page = new Page(ActiveRoute.param);
|
||||
|
||||
const root = await this.page.getRoot();
|
||||
const root = await this.page?.getRoot();
|
||||
|
||||
this.$placeholder.clear().append(root);
|
||||
|
||||
this.page.afterRender();
|
||||
this.page?.afterRender();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
@ -1,17 +1,16 @@
|
||||
import { ActionType, CallbackType, ReducerType, StateType, SubscribeType } from 'redux/types';
|
||||
import { ActionType, ReducerType, StateType, SubscribeType } from 'redux/types';
|
||||
|
||||
export class Store {
|
||||
state: StateType | null;
|
||||
listeners: CallbackType[];
|
||||
state: StateType;
|
||||
listeners: ((args?: any) => void)[];
|
||||
|
||||
constructor(private reducer: ReducerType, initialState: StateType) {
|
||||
this.state = reducer({ ...initialState }, { type: '__INIT__' });
|
||||
this.listeners = [];
|
||||
}
|
||||
|
||||
subscribeToStore(fn: (state: StateType) => void): SubscribeType {
|
||||
subscribe(fn: (state: StateType) => void): SubscribeType {
|
||||
this.listeners.push(fn);
|
||||
|
||||
return {
|
||||
unsubscribe: () => {
|
||||
this.listeners = this.listeners.filter((l: any) => l !== fn);
|
||||
@ -19,14 +18,12 @@ export class Store {
|
||||
};
|
||||
}
|
||||
|
||||
dispatchToStore(action: ActionType) {
|
||||
if (!this.state || !action.type) return;
|
||||
|
||||
dispatch(action: ActionType) {
|
||||
this.state = this.reducer(this.state, action);
|
||||
this.listeners.forEach(listener => listener(this.state));
|
||||
}
|
||||
|
||||
getState(): StateType {
|
||||
getState() {
|
||||
return JSON.parse(JSON.stringify(this.state));
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,10 @@
|
||||
import { $, Dom } from 'core/Dom';
|
||||
import { CallbackType, StateType } from 'redux/types';
|
||||
import { fontSizes } from 'src/constants';
|
||||
|
||||
export function capitalize(string: string): string {
|
||||
if (!string) return '';
|
||||
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
export function storage(key: string, data: StateType | null = null): any {
|
||||
export function storage(key: string, data: any = null): any {
|
||||
if (!data) {
|
||||
const localData = localStorage.getItem(key);
|
||||
return localData ? JSON.parse(localData) : false;
|
||||
@ -27,7 +23,7 @@ export function isEqual(a: any, b: any) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
export function debounce(fn: CallbackType, wait: number) {
|
||||
export function debounce(fn: (fnArgs?: any) => void, wait: number) {
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
return function (...args: any) {
|
||||
@ -42,7 +38,7 @@ export function debounce(fn: CallbackType, wait: number) {
|
||||
}
|
||||
|
||||
export function parse(value: string) {
|
||||
if (value.toString().startsWith('=')) {
|
||||
if (value.startsWith('=')) {
|
||||
try {
|
||||
// eslint-disable-next-line no-eval
|
||||
return eval(value.slice(1));
|
||||
@ -53,32 +49,3 @@ export function parse(value: string) {
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function isLargestFontSize(fontSize?: string): number | boolean {
|
||||
if (!fontSize) return false;
|
||||
return fontSizes.length - 1 === fontSizes.findIndex(el => el === fontSize);
|
||||
}
|
||||
|
||||
export function isSmallestFontSize(fontSize?: string): number | boolean {
|
||||
if (!fontSize) return false;
|
||||
return fontSizes.findIndex(el => el === fontSize) === 0;
|
||||
}
|
||||
|
||||
export function getMethodNameByEventName(eventName: string): string {
|
||||
return `on${capitalize(eventName)}`;
|
||||
}
|
||||
|
||||
export function getIdByCell($cell: Dom): { row?: string, col?: string } {
|
||||
const id = $cell.data.id?.split(':');
|
||||
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) {
|
||||
return `excel:${param}`;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import './styles/index.scss';
|
||||
import { Router } from 'core/routes/Router';
|
||||
import { Router } from 'core/routes/router';
|
||||
import { DashboardPage } from 'pages/DashboardPage';
|
||||
import { ExcelPage } from 'pages/ExcelPage';
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
export abstract class AbstractPage {
|
||||
params: string[];
|
||||
params: any;
|
||||
|
||||
constructor(params: string[]) {
|
||||
constructor(params: any) {
|
||||
this.params = params || Date.now().toString();
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { $, Dom } from 'core/Dom';
|
||||
import { $ } from 'core/dom';
|
||||
import { AbstractPage } from 'pages/AbstractPage';
|
||||
import { storage } from 'core/utils';
|
||||
|
||||
export class DashboardPage extends AbstractPage {
|
||||
getRoot(): Dom {
|
||||
getRoot() {
|
||||
const id = Date.now().toString();
|
||||
|
||||
return $.create('div', 'db').html(
|
||||
@ -24,7 +24,7 @@ export class DashboardPage extends AbstractPage {
|
||||
}
|
||||
}
|
||||
|
||||
function toHtml(key: string): string {
|
||||
function toHtml(key: string) {
|
||||
const params = +key.split(':')[1];
|
||||
const state = storage(key);
|
||||
const link = `#excel/${params}`;
|
||||
@ -38,7 +38,7 @@ function toHtml(key: string): string {
|
||||
`;
|
||||
}
|
||||
|
||||
export function createRecordsTable(): string {
|
||||
export function createRecordsTable() {
|
||||
const keys = getAllKeys();
|
||||
if (!keys.length) return '<p>Пока не создали ни одной таблицы</p>';
|
||||
|
||||
|
||||
@ -1,22 +1,25 @@
|
||||
import { ContextMenu } from 'components/ContextMenu/ContextMenu';
|
||||
import { AbstractPage } from 'pages/AbstractPage';
|
||||
import { Excel } from 'components/excel/Excel';
|
||||
import { Formula } from 'components/formula/Formula';
|
||||
import { Header } from 'components/header/Header';
|
||||
import { LocalStorageClient } from 'core/Clients';
|
||||
import { StateProcessor } from 'core/StateProcessor';
|
||||
import { Store } from 'core/store/Store';
|
||||
import { Store } from 'core/store/createStore';
|
||||
import { SubscribeType } from 'redux/types';
|
||||
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;
|
||||
private processor: StateProcessor;
|
||||
|
||||
constructor(props: string[]) {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.storeSub = null;
|
||||
@ -26,13 +29,13 @@ export class ExcelPage extends AbstractPage {
|
||||
}
|
||||
|
||||
async getRoot() {
|
||||
const state = await this.processor.get();
|
||||
const store = new Store(rootReducer, state);
|
||||
const normalizeState = await this.processor.get();
|
||||
const store = new Store(rootReducer, normalizeState);
|
||||
|
||||
this.storeSub = store.subscribeToStore(this.processor.listen);
|
||||
this.storeSub = store.subscribe(this.processor.listen);
|
||||
|
||||
this.excel = new Excel({
|
||||
components: [Header, Toolbar, Formula, Table, ContextMenu],
|
||||
components: [Header, Toolbar, Formula, Table],
|
||||
store,
|
||||
});
|
||||
|
||||
@ -40,7 +43,7 @@ export class ExcelPage extends AbstractPage {
|
||||
}
|
||||
|
||||
afterRender() {
|
||||
this.excel.afterRender();
|
||||
this.excel.init();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
export const APPLY_STYLES = 'APPLY_STYLES';
|
||||
export const CHANGE_STYLES = 'CURRENT_STYLES';
|
||||
export const CHANGE_TEXT = 'CHANGE_TEXT';
|
||||
export const CHANGE_TITLE = 'CHANGE_TITLE';
|
||||
export const DELETE_TABLE = 'DELETE_TABLE';
|
||||
export const TABLE_RESIZE = 'TABLE_RESIZE';
|
||||
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';
|
||||
@ -1,108 +0,0 @@
|
||||
import { ResizeReturnDataType } from 'components/table/handlers/table.resize';
|
||||
import { ActionType } from 'redux/types';
|
||||
import {
|
||||
CHANGE_TEXT,
|
||||
CHANGE_STYLES,
|
||||
TABLE_RESIZE,
|
||||
APPLY_STYLES,
|
||||
CHANGE_TITLE,
|
||||
DELETE_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 {
|
||||
return {
|
||||
type: TABLE_RESIZE,
|
||||
resizeData,
|
||||
};
|
||||
}
|
||||
|
||||
export function changeText(data: { text: string, id: string }): ActionType {
|
||||
return {
|
||||
type: CHANGE_TEXT,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function changeCurrentStyles(data: Partial<CSSStyleDeclaration>): ActionType {
|
||||
return {
|
||||
type: CHANGE_STYLES,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function applyStyle(data: { ids: (string | undefined)[], value: Partial<CSSStyleDeclaration> }): ActionType {
|
||||
return {
|
||||
type: APPLY_STYLES,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function changeTitle(data: string): ActionType {
|
||||
return {
|
||||
type: CHANGE_TITLE,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteTable(data: string): ActionType {
|
||||
return {
|
||||
type: DELETE_TABLE,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateOpenDate(data: string): ActionType {
|
||||
return {
|
||||
type: UPDATE_DATE,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function changeCurrentText(data: string): ActionType {
|
||||
return {
|
||||
type: CHANGE_CURRENT_TEXT,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function changeTableSize(data: { col: number, row: number }): ActionType {
|
||||
return {
|
||||
type: CHANGE_TABLE_SIZE,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
8
src/redux/actions-types.d.ts
vendored
Normal file
8
src/redux/actions-types.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
export type ResizePayloadType = {
|
||||
colState: {
|
||||
[k in number]: number
|
||||
},
|
||||
rowState: {
|
||||
[k in number]: number
|
||||
},
|
||||
};
|
||||
59
src/redux/actions.ts
Normal file
59
src/redux/actions.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { ActionType } from 'redux/types';
|
||||
import {
|
||||
CHANGE_TEXT,
|
||||
CHANGE_STYLES,
|
||||
TABLE_RESIZE,
|
||||
APPLY_STYLES,
|
||||
CHANGE_TITLE,
|
||||
DELETE_TABLE,
|
||||
UPDATE_DATE,
|
||||
} from 'redux/constants';
|
||||
|
||||
export function tableResize(resizeData: any) {
|
||||
return {
|
||||
type: TABLE_RESIZE,
|
||||
...resizeData,
|
||||
};
|
||||
}
|
||||
|
||||
export function changeText(data: { text: string, id: string }) {
|
||||
return {
|
||||
type: CHANGE_TEXT,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function changeCurrentStyles(data: any) {
|
||||
return {
|
||||
type: CHANGE_STYLES,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function applyStyle(data: any) {
|
||||
return {
|
||||
type: APPLY_STYLES,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function changeTitle(data: string) {
|
||||
return {
|
||||
type: CHANGE_TITLE,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteTable(data: string) {
|
||||
return {
|
||||
type: DELETE_TABLE,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateOpenDate(data: string): ActionType {
|
||||
return {
|
||||
type: UPDATE_DATE,
|
||||
data,
|
||||
};
|
||||
}
|
||||
7
src/redux/constants.ts
Normal file
7
src/redux/constants.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export const APPLY_STYLES = 'APPLY_STYLES';
|
||||
export const CHANGE_STYLES = 'CURRENT_STYLES';
|
||||
export const CHANGE_TEXT = 'CHANGE_TEXT';
|
||||
export const CHANGE_TITLE = 'CHANGE_TITLE';
|
||||
export const DELETE_TABLE = 'DELETE_TABLE';
|
||||
export const TABLE_RESIZE = 'TABLE_RESIZE';
|
||||
export const UPDATE_DATE = 'UPDATE_DATE';
|
||||
@ -1,7 +1,6 @@
|
||||
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,
|
||||
@ -10,27 +9,21 @@ import {
|
||||
CHANGE_TITLE,
|
||||
DELETE_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';
|
||||
} from 'redux/constants';
|
||||
|
||||
export function rootReducer(state: StateType, action: ActionType): StateType {
|
||||
export function rootReducer(state: StateType, action: ActionType) {
|
||||
switch (action.type) {
|
||||
case TABLE_RESIZE: {
|
||||
const newState: StateType = { ...state };
|
||||
const fieldName = `${action.resizeData?.type}State`;
|
||||
|
||||
newState[fieldName as 'colState' | 'rowState'][action.resizeData?.id] = action.resizeData?.value;
|
||||
newState[fieldName][action.resizeData?.id] = action.resizeData?.value;
|
||||
|
||||
return { ...state, ...newState };
|
||||
}
|
||||
|
||||
case CHANGE_TEXT: {
|
||||
const newState: { [k: string]: string } = state.dataState || {};
|
||||
const newState: StateType = state.dataState || {};
|
||||
const fieldName = action.data.id;
|
||||
|
||||
newState[fieldName] = action.data.text;
|
||||
@ -66,281 +59,11 @@ export function rootReducer(state: StateType, action: ActionType): StateType {
|
||||
localStorage.removeItem(name);
|
||||
ActiveRoute.navigateTo = '';
|
||||
|
||||
return null as any;
|
||||
return null;
|
||||
}
|
||||
|
||||
case UPDATE_DATE: {
|
||||
return { ...state, openDate: action.data };
|
||||
}
|
||||
|
||||
case CHANGE_CURRENT_TEXT: {
|
||||
return { ...state, currentText: action.data };
|
||||
}
|
||||
|
||||
case CHANGE_TABLE_SIZE: {
|
||||
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 };
|
||||
// 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 key < action.data:
|
||||
newRowStateEntries.push([+key, value]);
|
||||
break;
|
||||
|
||||
case key > action.data:
|
||||
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 row < action.data:
|
||||
newDataStateEntries.push([key, value.toString()]);
|
||||
break;
|
||||
|
||||
case row > action.data:
|
||||
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 row < action.data:
|
||||
newStylesStateEntries.push([key, value]);
|
||||
break;
|
||||
|
||||
case row > action.data:
|
||||
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 };
|
||||
|
||||
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 };
|
||||
return { ...this.state, openDate: action.data };
|
||||
}
|
||||
|
||||
default: return state;
|
||||
|
||||
22
src/redux/types.d.ts
vendored
22
src/redux/types.d.ts
vendored
@ -1,32 +1,14 @@
|
||||
import { ToolbarStateType } from 'components/toolbar/toolbar-types';
|
||||
|
||||
export type ActionType = {
|
||||
type: string
|
||||
[k: string]: any
|
||||
};
|
||||
|
||||
export type TableSizeType = {
|
||||
col: number;
|
||||
row: number;
|
||||
};
|
||||
|
||||
export type StateType = {
|
||||
colState: { [k: number]: number };
|
||||
rowState: { [k: number]: number };
|
||||
currentStyles: ToolbarStateType;
|
||||
dataState: { [k: string]: string };
|
||||
id: string;
|
||||
openDate: number;
|
||||
stylesState: { [k: string]: ToolbarStateType };
|
||||
title: string;
|
||||
currentText: string;
|
||||
tableSize: TableSizeType;
|
||||
[k: string]: any
|
||||
};
|
||||
|
||||
export type ReducerType = (state: StateType, action: ActionType) => StateType | null;
|
||||
export type ReducerType = (state: StateType, action: ActionType) => StateType;
|
||||
|
||||
export type SubscribeType = {
|
||||
unsubscribe: () => void
|
||||
};
|
||||
|
||||
export type CallbackType = (...args: any[]) => void;
|
||||
|
||||
@ -7,4 +7,3 @@ $info-cell-width: 40px;
|
||||
$row-height: 25px;
|
||||
$toolbar-height: 40px;
|
||||
$primary-color: #3c74ff;
|
||||
$default-cell-font-size: 12px;
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -23,6 +23,5 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
@ -5,12 +5,8 @@
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-height: calc(100vh - ($header-height + $toolbar-height + $formula-height));
|
||||
top: $header-height + $toolbar-height + $formula-height;
|
||||
overflow: auto;
|
||||
font-size: $default-cell-font-size;
|
||||
padding-bottom: 5px;
|
||||
|
||||
.row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -50,22 +46,16 @@
|
||||
border: 1px solid #e1e2e3;
|
||||
border-top: 0;
|
||||
border-left: 0;
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
outline: none;
|
||||
display: flex;
|
||||
&:hover:not(.selected) {
|
||||
cursor: cell;
|
||||
}
|
||||
&.selected {
|
||||
border: none;
|
||||
outline: 2px solid $primary-color;
|
||||
background: #cccccc;
|
||||
z-index: 2;
|
||||
}
|
||||
&.current {
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
.col-resize{
|
||||
position: absolute;
|
||||
@ -75,13 +65,14 @@
|
||||
width: 4px;
|
||||
background: $primary-color;
|
||||
opacity: 0;
|
||||
z-index: 50;
|
||||
z-index: 2;
|
||||
|
||||
&:hover{
|
||||
cursor: col-resize;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.row-resize{
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
@ -90,37 +81,11 @@
|
||||
height: 4px;
|
||||
opacity: 0;
|
||||
background: $primary-color;
|
||||
z-index: 50;
|
||||
z-index: 2;
|
||||
|
||||
&:hover{
|
||||
cursor: row-resize;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
[data-header="row"] {
|
||||
cursor: pointer;
|
||||
|
||||
&.selected {
|
||||
border-right: 2px solid #3c74ff;
|
||||
}
|
||||
}
|
||||
|
||||
[data-header="col"] {
|
||||
cursor: pointer;
|
||||
|
||||
&.selected {
|
||||
border-bottom: 2px solid #3c74ff;
|
||||
}
|
||||
}
|
||||
|
||||
[data-header="col"], [data-header="row"] {
|
||||
&.selected {
|
||||
font-weight: bold;
|
||||
background: rgba(60, 116, 255, 0.4);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #ccc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,23 +15,4 @@
|
||||
.button {
|
||||
@include button(green)
|
||||
}
|
||||
|
||||
.button.disable {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.button__group {
|
||||
border-right: 1px solid #c0c0c0;
|
||||
&:last-child {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.button__size {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.button__font {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,10 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Cormorant+SC&family=Kanit&family=Playfair+Display&family=Roboto&display=swap');;
|
||||
@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
|
||||
@import "~normalize.css";
|
||||
|
||||
@import './components/header';
|
||||
@import './components/toolbar';
|
||||
@import './components/formula';
|
||||
@import './components/table';
|
||||
@import './components/contextmenu';
|
||||
@import './components/dashboard';
|
||||
@import './components/loader';
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { Router } from '../src/core/routes/Router';
|
||||
import { Router } from '../src/core/routes/router';
|
||||
import { AbstractPage } from '../src/pages/AbstractPage';
|
||||
|
||||
class DashboardPage extends AbstractPage {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Store } from '../src/core/store/Store';
|
||||
import { Store } from '../src/core/store/createStore';
|
||||
|
||||
const initialState = {
|
||||
count: 0,
|
||||
@ -23,8 +23,8 @@ describe('Create store', () => {
|
||||
|
||||
test('should return store object', () => {
|
||||
expect(store).toBeDefined();
|
||||
expect(store.dispatchToStore).toBeDefined();
|
||||
expect(store.subscribeToStore).toBeDefined();
|
||||
expect(store.dispatch).toBeDefined();
|
||||
expect(store.subscribe).toBeDefined();
|
||||
expect(store.getState).not.toBeUndefined();
|
||||
});
|
||||
|
||||
@ -37,27 +37,27 @@ describe('Create store', () => {
|
||||
});
|
||||
|
||||
test('should change state if actions exist', () => {
|
||||
store.dispatchToStore({ type: 'ADD' });
|
||||
store.dispatch({ type: 'ADD' });
|
||||
expect(store.getState().count).toBe(1);
|
||||
});
|
||||
|
||||
test("should NOT change state if actions don't exist", () => {
|
||||
store.dispatchToStore({ type: 'NOT_EXISTING_TYPE' });
|
||||
store.dispatch({ type: 'NOT_EXISTING_TYPE' });
|
||||
expect(store.getState().count).toBe(0);
|
||||
});
|
||||
|
||||
test('should call subscriber', () => {
|
||||
store.subscribeToStore(handler);
|
||||
store.dispatchToStore({ type: 'ADD' });
|
||||
store.subscribe(handler);
|
||||
store.dispatch({ type: 'ADD' });
|
||||
|
||||
expect(handler).toHaveBeenCalled();
|
||||
expect(handler).toHaveBeenCalledWith(store.getState());
|
||||
});
|
||||
|
||||
test('should NOT call sub if unsubscribe', () => {
|
||||
const unsub = store.subscribeToStore(handler);
|
||||
const unsub = store.subscribe(handler);
|
||||
unsub.unsubscribe();
|
||||
store.dispatchToStore({ type: 'ADD' });
|
||||
store.dispatch({ type: 'ADD' });
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
@ -65,7 +65,7 @@ describe('Create store', () => {
|
||||
test('should dispatch in async way', () => {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
store.dispatchToStore({ type: 'ADD' });
|
||||
store.dispatch({ type: 'ADD' });
|
||||
}, 500);
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user