diff --git a/src/components/Loader.ts b/src/components/Loader.ts index d4d6772..ce72966 100644 --- a/src/components/Loader.ts +++ b/src/components/Loader.ts @@ -1,4 +1,4 @@ -import { $, Dom } from 'core/dom'; +import { $, Dom } from 'core/Dom'; export function Loader(): Dom { return $.create('div', 'loader') diff --git a/src/components/excel/Excel.ts b/src/components/excel/Excel.ts index 7140ff8..4c30f9a 100644 --- a/src/components/excel/Excel.ts +++ b/src/components/excel/Excel.ts @@ -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, }; diff --git a/src/components/formula/Formula.ts b/src/components/formula/Formula.ts index 12f5ecc..b58d628 100644 --- a/src/components/formula/Formula.ts +++ b/src/components/formula/Formula.ts @@ -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'); } } } diff --git a/src/components/header/Header.ts b/src/components/header/Header.ts index da01b26..a291671 100644 --- a/src/components/header/Header.ts +++ b/src/components/header/Header.ts @@ -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, }); } diff --git a/src/components/table/Table.ts b/src/components/table/Table.ts index 3283101..13debbb 100644 --- a/src/components/table/Table.ts +++ b/src/components/table/Table.ts @@ -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)); diff --git a/src/components/table/TableSelection.ts b/src/components/table/TableSelection.ts index 395ffc2..92fdabd 100644 --- a/src/components/table/TableSelection.ts +++ b/src/components/table/TableSelection.ts @@ -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 { diff --git a/src/components/table/handlers/table.resize.ts b/src/components/table/handlers/table.resize.ts index 5ad56cc..9e88ed6 100644 --- a/src/components/table/handlers/table.resize.ts +++ b/src/components/table/handlers/table.resize.ts @@ -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 }; diff --git a/src/components/table/handlers/table.select.handler.ts b/src/components/table/handlers/table.select.handler.ts index 5c418be..2d05197 100644 --- a/src/components/table/handlers/table.select.handler.ts +++ b/src/components/table/handlers/table.select.handler.ts @@ -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; diff --git a/src/components/table/table.functions.ts b/src/components/table/table.functions.ts index f777276..66a8104 100644 --- a/src/components/table/table.functions.ts +++ b/src/components/table/table.functions.ts @@ -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'; diff --git a/src/components/toolbar/Toolbar.ts b/src/components/toolbar/Toolbar.ts index 5ea39e2..61566a6 100644 --- a/src/components/toolbar/Toolbar.ts +++ b/src/components/toolbar/Toolbar.ts @@ -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 }); } } diff --git a/src/components/toolbar/toolbar-types.ts b/src/components/toolbar/toolbar-types.ts index ebc85e4..44b6013 100644 --- a/src/components/toolbar/toolbar-types.ts +++ b/src/components/toolbar/toolbar-types.ts @@ -4,5 +4,5 @@ export type ToolbarStateType = { textDecoration?: 'none' | 'underline'; justifyContent?: 'start' | 'center' | 'end'; alignItems?: 'start' | 'center' | 'end'; - fontSize?: any; + fontSize?: string; }; diff --git a/src/components/toolbar/toolbar.template.ts b/src/components/toolbar/toolbar.template.ts index a088bf6..223eeb5 100644 --- a/src/components/toolbar/toolbar.template.ts +++ b/src/components/toolbar/toolbar.template.ts @@ -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 ` - +
`; } diff --git a/src/core/Clients.ts b/src/core/Clients.ts index ff9ff4b..b69277e 100644 --- a/src/core/Clients.ts +++ b/src/core/Clients.ts @@ -31,7 +31,7 @@ export class LocalStorageClient implements ClientDataType { }); } - norm(state) { + norm(state: StateType) { return { ...state, currentStyles: { ...state.stylesState?.[startCellId] }, diff --git a/src/core/dom.ts b/src/core/Dom.ts similarity index 100% rename from src/core/dom.ts rename to src/core/Dom.ts diff --git a/src/core/DomListener.ts b/src/core/DomListener.ts index 812cdfa..36e3ac6 100644 --- a/src/core/DomListener.ts +++ b/src/core/DomListener.ts @@ -1,4 +1,4 @@ -import { Dom } from 'core/dom'; +import { Dom } from 'core/Dom'; import { capitalize } from 'core/utils'; export class DomListener { diff --git a/src/core/ExcelComponent.ts b/src/core/ExcelComponent.ts index 6e61f39..79fe6f2 100644 --- a/src/core/ExcelComponent.ts +++ b/src/core/ExcelComponent.ts @@ -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); } diff --git a/src/core/ExcelComponentState.ts b/src/core/ExcelComponentState.ts index 9d8a187..0ca96ee 100644 --- a/src/core/ExcelComponentState.ts +++ b/src/core/ExcelComponentState.ts @@ -1,4 +1,4 @@ -import { Dom } from 'core/dom'; +import { Dom } from 'core/Dom'; import { ExcelComponent, ComponentOptionsType } from 'core/ExcelComponent'; type ExcelComponentStateType = { diff --git a/src/core/Emitter.ts b/src/core/Observer.ts similarity index 84% rename from src/core/Emitter.ts rename to src/core/Observer.ts index dfbe507..3b46c30 100644 --- a/src/core/Emitter.ts +++ b/src/core/Observer.ts @@ -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; + } } diff --git a/src/core/StateProcessor.ts b/src/core/StateProcessor.ts index 8d50893..2899d3c 100644 --- a/src/core/StateProcessor.ts +++ b/src/core/StateProcessor.ts @@ -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) { diff --git a/src/core/StoreSubscriber.ts b/src/core/StoreSubscriber.ts index a5cb95e..271563c 100644 --- a/src/core/StoreSubscriber.ts +++ b/src/core/StoreSubscriber.ts @@ -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(); }); } diff --git a/src/core/routes/router.ts b/src/core/routes/Router.ts similarity index 97% rename from src/core/routes/router.ts rename to src/core/routes/Router.ts index 2b6b687..3274245 100644 --- a/src/core/routes/router.ts +++ b/src/core/routes/Router.ts @@ -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; diff --git a/src/core/store/Store.ts b/src/core/store/Store.ts index e229fcf..7849fe5 100644 --- a/src/core/store/Store.ts +++ b/src/core/store/Store.ts @@ -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)); } } diff --git a/src/index.ts b/src/index.ts index 695e536..4ff2833 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/pages/AbstractPage.ts b/src/pages/AbstractPage.ts index 8565c75..3c727e3 100644 --- a/src/pages/AbstractPage.ts +++ b/src/pages/AbstractPage.ts @@ -1,7 +1,7 @@ export abstract class AbstractPage { - params: any; + params: string[]; - constructor(params: any) { + constructor(params: string[]) { this.params = params || Date.now().toString(); } diff --git a/src/pages/DashboardPage.ts b/src/pages/DashboardPage.ts index 50236ad..a29a345 100644 --- a/src/pages/DashboardPage.ts +++ b/src/pages/DashboardPage.ts @@ -1,4 +1,4 @@ -import { $ } from 'core/dom'; +import { $ } from 'core/Dom'; import { AbstractPage } from 'pages/AbstractPage'; import { storage } from 'core/utils'; diff --git a/src/pages/ExcelPage.ts b/src/pages/ExcelPage.ts index e2f163e..03e466b 100644 --- a/src/pages/ExcelPage.ts +++ b/src/pages/ExcelPage.ts @@ -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], diff --git a/src/redux/rootReducer.ts b/src/redux/rootReducer.ts index 99ebbc9..a9f2c32 100644 --- a/src/redux/rootReducer.ts +++ b/src/redux/rootReducer.ts @@ -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; diff --git a/src/redux/types.d.ts b/src/redux/types.d.ts index 5491dda..807a307 100644 --- a/src/redux/types.d.ts +++ b/src/redux/types.d.ts @@ -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; diff --git a/test/Router.spec.js b/test/Router.spec.js index 00ace3b..d955e2a 100644 --- a/test/Router.spec.js +++ b/test/Router.spec.js @@ -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 { diff --git a/test/createStore.spec.js b/test/createStore.spec.js index 8b5551e..edda23b 100644 --- a/test/createStore.spec.js +++ b/test/createStore.spec.js @@ -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' });