little refactor

This commit is contained in:
Sergey Krylov 2022-07-10 23:05:25 +05:00
parent f7e04814a5
commit 3bd194816e
30 changed files with 120 additions and 110 deletions

View File

@ -1,4 +1,4 @@
import { $, Dom } from 'core/dom';
import { $, Dom } from 'core/Dom';
export function Loader(): Dom {
return $.create('div', 'loader')

View File

@ -1,5 +1,5 @@
import { $ } from 'core/dom';
import { Emitter } from 'core/Emitter';
import { $ } from 'core/Dom';
import { Observer } from 'core/Observer';
import { ExcelComponent } from 'core/ExcelComponent';
import { Store } from 'core/store/Store';
import { StoreSubscriber } from 'core/StoreSubscriber';
@ -10,15 +10,20 @@ interface ExcelOptionsType {
store: any,
}
type BaseComponentOption = {
observer: Observer;
store: Store;
};
export class Excel {
components: any[];
emitter: Emitter;
observer: Observer;
store: Store;
subscriber: StoreSubscriber;
constructor(options: ExcelOptionsType) {
this.components = options.components;
this.emitter = new Emitter();
this.observer = new Observer();
this.store = options.store;
this.subscriber = new StoreSubscriber(this.store);
}
@ -26,8 +31,8 @@ export class Excel {
getRoot() {
const $root = $.create('div', 'excel');
const componentOptions = {
emitter: this.emitter,
const componentOptions: BaseComponentOption = {
observer: this.observer,
store: this.store,
};

View File

@ -1,4 +1,4 @@
import { Dom } from 'core/dom';
import { Dom } from 'core/Dom';
import { ExcelComponent, ComponentOptionsType } from 'core/ExcelComponent';
export class Formula extends ExcelComponent {
@ -8,13 +8,10 @@ export class Formula extends ExcelComponent {
constructor($root: Dom, options: ComponentOptionsType) {
super($root, {
// @ts-ignore next-line
eventListeners: ['input', 'keydown'],
// @ts-ignore next-line
name: 'Formula',
// @ts-ignore next-line
subscribe: ['currentText'],
...options,
eventListeners: ['input', 'keydown'],
name: 'Formula',
subscribe: ['currentText'],
});
}
@ -30,7 +27,7 @@ export class Formula extends ExcelComponent {
this.formulaInput = this.$root.find('#formula-input');
this.$on('table:select-cell', (cell: Dom) => {
this.$onEventFromObserver('table:select-cell', (cell: Dom) => {
this.formulaInput.text = cell.data.value || '';
});
}
@ -44,7 +41,8 @@ export class Formula extends ExcelComponent {
if (!target) return;
const text = (target as HTMLElement).innerText.trim();
this.$emit('formula:input', text);
this.$emitEventToObserver('formula:input', text);
}
onKeydown(event: KeyboardEvent) {
@ -53,7 +51,7 @@ export class Formula extends ExcelComponent {
if (preventedKeys.includes(event.key)) event.preventDefault();
if (event.key === 'Enter') {
this.$emit('formula:enter-press');
this.$emitEventToObserver('formula:enter-press');
}
}
}

View File

@ -1,18 +1,18 @@
import { ComponentOptionsType, ExcelComponent } from 'core/ExcelComponent';
import * as actions from 'redux/action-creators';
import { $, Dom } from 'core/dom';
import { $, Dom } from 'core/Dom';
import { ActiveRoute } from 'core/routes/ActiveRoute';
import { ExcelComponentState } from 'core/ExcelComponentState';
import { deleteTable } from 'redux/action-creators';
export class Header extends ExcelComponentState {
export class Header extends ExcelComponent {
static className = 'excel__header';
constructor($root: Dom, options: any) {
constructor($root: Dom, options: ComponentOptionsType) {
super($root, {
...options,
name: 'Header',
eventListeners: ['input', 'click'],
subscribe: ['title'],
...options,
});
}

View File

@ -1,7 +1,7 @@
import { startCellId } from 'components/table/table.functions';
import * as actions from 'redux/action-creators';
import { $, Dom } from 'core/dom';
import { ExcelComponent } from 'core/ExcelComponent';
import { $, Dom } from 'core/Dom';
import { ComponentOptionsType, ExcelComponent } from 'core/ExcelComponent';
import { TableSelection } from 'components/table/TableSelection';
import { changeCurrentStyles } from 'redux/action-creators';
import { createTable } from 'components/table/table.template';
@ -15,11 +15,11 @@ export class Table extends ExcelComponent {
private selection: TableSelection;
constructor($root: Dom, options: any) {
constructor($root: Dom, options: ComponentOptionsType) {
super($root, {
...options,
name: 'Table',
eventListeners: ['mousedown', 'keydown', 'input'],
...options,
});
}
@ -36,9 +36,9 @@ export class Table extends ExcelComponent {
this.initTable();
this.$on('formula:input', this.updateCurrentText);
this.$on('formula:enter-press', () => this.selection.current.focus());
this.$on('toolbar:applyStyle', this.updateCurrentStyles);
this.$onEventFromObserver('formula:input', this.updateCurrentText);
this.$onEventFromObserver('formula:enter-press', () => this.selection.current.focus());
this.$onEventFromObserver('toolbar:applyStyle', this.updateCurrentStyles);
}
initTable() {
@ -55,12 +55,12 @@ export class Table extends ExcelComponent {
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` }));
cols.forEach(el => $(el as HTMLElement).css({ width: `${size.col[+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` }));
rows.forEach(el => $(el as HTMLElement).css({ height: `${size.row[+key]}px` }));
});
}
@ -83,11 +83,11 @@ export class Table extends ExcelComponent {
const $cell = this.$root.find(`[data-id="${startCellId}"]`);
this.selection.select($cell);
this.$emit('table:select-cell', $cell);
this.$emitEventToObserver('table:select-cell', $cell);
}
emitSelectCallback() {
this.$emit('table:select-cell', this.selection.current);
this.$emitEventToObserver('table:select-cell', this.selection.current);
const styles = this.selection.current?.getStyles(Object.keys(initialStyleState));
this.dispatchToStore(changeCurrentStyles(styles));

View File

@ -1,4 +1,4 @@
import { $, Dom } from 'core/dom';
import { $, Dom } from 'core/Dom';
import { getParamsFromCellId, startCellId } from 'components/table/table.functions';
export class TableSelection {

View File

@ -1,5 +1,5 @@
import { startCellId } from 'components/table/table.functions';
import { $, Dom } from 'core/dom';
import { $, Dom } from 'core/Dom';
type CustomElementType = Element & { css: any };
type ResizeReturnDataType = { value: number, id: string, type: string };

View File

@ -1,4 +1,4 @@
import { $ } from 'core/dom';
import { $ } from 'core/Dom';
import { TableSelection } from 'components/table/TableSelection';
import { getParamsFromCellId, isCell, startCellId } from 'components/table/table.functions';
@ -68,7 +68,10 @@ export function selectHandler(event: MouseEvent | KeyboardEvent, selection: Tabl
}
case 'Tab': {
event.preventDefault();
col++;
if (event.shiftKey) col--;
else col++;
break;
}
default: break;

View File

@ -1,4 +1,4 @@
import { $ } from 'core/dom';
import { $ } from 'core/Dom';
export function isCell(event: Event): boolean {
return $(event.target as HTMLElement).data.type === 'cell';

View File

@ -1,5 +1,5 @@
import { startCellId } from 'components/table/table.functions';
import { $, Dom } from 'core/dom';
import { $, Dom } from 'core/Dom';
import { ExcelComponentState } from 'core/ExcelComponentState';
import { ComponentOptionsType } from 'core/ExcelComponent';
import { createToolbar } from 'components/toolbar/toolbar.template';
@ -50,14 +50,14 @@ export class Toolbar extends ExcelComponentState {
const value = JSON.parse(stringValue);
const key = Object.keys(value)[0];
this.$emit('toolbar:applyStyle', value);
this.$emitEventToObserver('toolbar:applyStyle', value);
this.setComponentState({ [key]: value[key] });
}
onChange(e: any) {
const value = `${e.target.value.toString()}px`;
this.$emit('toolbar:applyStyle', { fontSize: value });
this.$emitEventToObserver('toolbar:applyStyle', { fontSize: value });
this.setComponentState({ fontSize: value });
}
}

View File

@ -4,5 +4,5 @@ export type ToolbarStateType = {
textDecoration?: 'none' | 'underline';
justifyContent?: 'start' | 'center' | 'end';
alignItems?: 'start' | 'center' | 'end';
fontSize?: any;
fontSize?: string;
};

View File

@ -85,7 +85,7 @@ export function createToolbar(state: ToolbarStateType): string {
return buttons.join(' ');
}
function createFontSizeButton(currentSize: string) {
function createFontSizeButton(currentSize = '12px') {
const fontSizeInPixels = +currentSize.slice(0, -2);
const options = [];
@ -95,9 +95,11 @@ function createFontSizeButton(currentSize: string) {
}
return `
<div class="button">
<select class="button__size" id="button-size">
${options.join('')}
</select>
</div>
`;
}

View File

@ -31,7 +31,7 @@ export class LocalStorageClient implements ClientDataType {
});
}
norm(state) {
norm(state: StateType) {
return {
...state,
currentStyles: { ...state.stylesState?.[startCellId] },

View File

@ -1,4 +1,4 @@
import { Dom } from 'core/dom';
import { Dom } from 'core/Dom';
import { capitalize } from 'core/utils';
export class DomListener {

View File

@ -1,34 +1,28 @@
import { ActionType } from 'redux/types';
import { Dom } from 'core/dom';
import { ActionType, StateType } from 'redux/types';
import { Dom } from 'core/Dom';
import { DomListener } from 'core/DomListener';
import { Emitter } from 'core/Emitter';
import { Observer } from 'core/Observer';
import { Store } from 'core/store/Store';
interface ExcelComponentClass {
toHTML: () => string;
prepare: () => void;
storeChanged?: (args: any) => void;
}
export type ComponentOptionsType = {
eventListeners: string[];
name: string;
emitter: Emitter;
observer: Observer;
store: Store;
subscribe: string[],
subscribe: (keyof StateType)[],
};
export abstract class ExcelComponent extends DomListener implements ExcelComponentClass {
export abstract class ExcelComponent extends DomListener {
private name: string;
private emitter: Emitter;
private observer: Observer;
public store: Store;
private subscribe: string[];
private subscribe: (keyof StateType)[];
private unsubscribers: ((args?: any) => any)[];
constructor($root: Dom, options: ComponentOptionsType) {
super($root, options.eventListeners);
this.name = options.name;
this.emitter = options.emitter;
this.observer = options.observer;
this.store = options.store;
this.subscribe = options.subscribe;
@ -44,12 +38,12 @@ export abstract class ExcelComponent extends DomListener implements ExcelCompone
return '';
}
$emit(event: string, args?: any): void {
this.emitter?.emit(event, args);
$emitEventToObserver(event: string, args?: any): void {
this.observer?.emit(event, args);
}
$on(event: string, callback: (args: any) => any) {
const unsub = this.emitter?.subscribe(event, callback);
$onEventFromObserver(event: string, callback: (args: any) => any) {
const unsub = this.observer?.subscribe(event, callback);
unsub && this.unsubscribers.push(unsub);
}
@ -58,10 +52,10 @@ export abstract class ExcelComponent extends DomListener implements ExcelCompone
}
storeChanged(args?: any) {
console.log('CHANGE STORE: ', args);
console.log('CHANGE STORE: ', args, ' in component ', this.name);
}
isWatching(key: string) {
isWatching(key: keyof StateType) {
return this.subscribe?.includes(key);
}

View File

@ -1,4 +1,4 @@
import { Dom } from 'core/dom';
import { Dom } from 'core/Dom';
import { ExcelComponent, ComponentOptionsType } from 'core/ExcelComponent';
type ExcelComponentStateType = {

View File

@ -1,4 +1,4 @@
export class Emitter {
export class Observer {
private listeners: {
[k: string]: Array<(args?: any) => any>
};
@ -7,16 +7,6 @@ export class Emitter {
this.listeners = {};
}
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);
@ -25,4 +15,12 @@ export class Emitter {
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;
}
}

View File

@ -5,9 +5,9 @@ import { StateType } from 'redux/types';
export class StateProcessor {
private client: ClientDataType;
constructor(client: ClientDataType, dalay = 300) {
constructor(client: ClientDataType, delay = 300) {
this.client = client;
this.listen = debounce(this.listen.bind(this), dalay);
this.listen = debounce(this.listen.bind(this), delay);
}
listen(state: StateType) {

View File

@ -4,30 +4,29 @@ import { isEqual } from 'core/utils';
export class StoreSubscriber {
sub: any;
prevState: StateType;
currentState: StateType;
constructor(private store: Store) {
this.sub = null;
this.prevState = {};
}
subscribeComponents(components: any[]) {
this.prevState = this.store.getState();
this.currentState = this.store.getState();
this.sub = this.store.subscribeFromStore((state: StateType) => {
if (!state) return;
this.sub = this.store.subscribeToStore((newState: StateType) => {
if (!newState) return;
Object.keys(state).forEach(key => {
if (!isEqual(this.prevState[key], state[key])) {
Object.keys(newState).forEach((key) => {
if (!isEqual(this.currentState[key as keyof StateType], newState[key as keyof StateType])) {
components.forEach(component => {
if (component.isWatching(key)) {
const changes = { [key]: state[key] };
component.storeChanged(changes);
component.storeChanged({ [key]: newState[key as keyof StateType] });
}
});
}
});
this.prevState = this.store.getState();
this.currentState = this.store.getState();
});
}

View File

@ -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';
@ -34,8 +34,8 @@ export class Router {
}
async changePageHandler() {
this.$placeholder.clear().append(this.loader);
this.page?.destroy();
this.$placeholder.clear().append(this.loader);
let Page;

View File

@ -9,8 +9,9 @@ export class Store {
this.listeners = [];
}
subscribeFromStore(fn: (state: StateType) => void): SubscribeType {
subscribeToStore(fn: (state: StateType) => void): SubscribeType {
this.listeners.push(fn);
return {
unsubscribe: () => {
this.listeners = this.listeners.filter((l: any) => l !== fn);
@ -23,7 +24,7 @@ export class Store {
this.listeners.forEach(listener => listener(this.state));
}
getState() {
getState(): StateType {
return JSON.parse(JSON.stringify(this.state));
}
}

View File

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

View File

@ -1,7 +1,7 @@
export abstract class AbstractPage {
params: any;
params: string[];
constructor(params: any) {
constructor(params: string[]) {
this.params = params || Date.now().toString();
}

View File

@ -1,4 +1,4 @@
import { $ } from 'core/dom';
import { $ } from 'core/Dom';
import { AbstractPage } from 'pages/AbstractPage';
import { storage } from 'core/utils';

View File

@ -32,7 +32,7 @@ export class ExcelPage extends AbstractPage {
const state = await this.processor.get();
const store = new Store(rootReducer, state);
this.storeSub = store.subscribeFromStore(this.processor.listen);
this.storeSub = store.subscribeToStore(this.processor.listen);
this.excel = new Excel({
components: [Header, Toolbar, Formula, Table],

View File

@ -17,13 +17,13 @@ export function rootReducer(state: StateType, action: ActionType) {
const newState: StateType = { ...state };
const fieldName = `${action.resizeData?.type}State`;
newState[fieldName][action.resizeData?.id] = action.resizeData?.value;
newState[fieldName as 'colState' | 'rowState'][action.resizeData?.id] = action.resizeData?.value;
return { ...state, ...newState };
}
case CHANGE_TEXT: {
const newState: StateType = state.dataState || {};
const newState: { [k: string]: string } = state.dataState || {};
const fieldName = action.data.id;
newState[fieldName] = action.data.text;

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

@ -1,10 +1,20 @@
import { ToolbarStateType } from 'components/toolbar/toolbar-types';
export type ActionType = {
type: string
[k: string]: any
};
export type StateType = {
[k: string]: any
colState: { [k: number]: number };
rowState: { [k: number]: number };
currentStyles: ToolbarStateType;
dataState: { [k: string]: string };
id: string;
openDate: string;
stylesState: { [k: string]: ToolbarStateType };
title: string;
currentText: string;
};
export type ReducerType = (state: StateType, action: ActionType) => StateType;

View File

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

View File

@ -1,4 +1,4 @@
import { Store } from '../src/core/store/createStore';
import { Store } from '../src/core/store/Store';
const initialState = {
count: 0,
@ -24,7 +24,7 @@ describe('Create store', () => {
test('should return store object', () => {
expect(store).toBeDefined();
expect(store.dispatchToStore).toBeDefined();
expect(store.subscribeFromStore).toBeDefined();
expect(store.subscribeToStore).toBeDefined();
expect(store.getState).not.toBeUndefined();
});
@ -47,7 +47,7 @@ describe('Create store', () => {
});
test('should call subscriber', () => {
store.subscribeFromStore(handler);
store.subscribeToStore(handler);
store.dispatchToStore({ type: 'ADD' });
expect(handler).toHaveBeenCalled();
@ -55,7 +55,7 @@ describe('Create store', () => {
});
test('should NOT call sub if unsubscribe', () => {
const unsub = store.subscribeFromStore(handler);
const unsub = store.subscribeToStore(handler);
unsub.unsubscribe();
store.dispatchToStore({ type: 'ADD' });