From a8b9cbfb9f7bbf9ef39158b51b823996fd336a9c Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Wed, 29 Jun 2022 10:26:13 +0500 Subject: [PATCH] add excel state, initial table column size --- .eslintrc.js | 4 +- src/components/excel/Excel.ts | 7 +- src/components/table/Table.ts | 30 +++- src/components/table/handlers/table.resize.ts | 132 ++++++++++-------- src/core/ExcelComponent.ts | 15 ++ src/core/createStore.ts | 40 ++++++ src/core/utils.ts | 8 ++ src/index.ts | 10 ++ src/redux/actions-types.d.ts | 8 ++ src/redux/actions.ts | 8 ++ src/redux/constants.ts | 1 + src/redux/rootReducer.ts | 14 ++ src/redux/types.d.ts | 14 ++ 13 files changed, 226 insertions(+), 65 deletions(-) create mode 100644 src/core/createStore.ts create mode 100644 src/redux/actions-types.d.ts create mode 100644 src/redux/actions.ts create mode 100644 src/redux/constants.ts create mode 100644 src/redux/rootReducer.ts create mode 100644 src/redux/types.d.ts diff --git a/.eslintrc.js b/.eslintrc.js index 8ec1a05..b3ae354 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,6 +16,7 @@ module.exports = { "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/lines-between-class-members": "off", "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-floating-promises": "off", "@typescript-eslint/no-unnecessary-type-assertion": "off", "@typescript-eslint/no-unsafe-argument": "off", "@typescript-eslint/no-unsafe-assignment": "off", @@ -28,9 +29,10 @@ module.exports = { "class-methods-use-this": "off", "func-names": "off", "import/prefer-default-export": "off", + "linebreak-style": "off", + "max-len": "off", "no-console": "off", "no-plusplus": "off", - "max-len": "off", }, env: { browser: true, diff --git a/src/components/excel/Excel.ts b/src/components/excel/Excel.ts index 8772e79..92bda52 100644 --- a/src/components/excel/Excel.ts +++ b/src/components/excel/Excel.ts @@ -1,20 +1,24 @@ +import { Store } from '../../core/createStore'; import { $, Dom } from '../../core/dom'; import { Emitter } from '../../core/Emitter'; import { ExcelComponent } from '../../core/ExcelComponent'; interface ExcelOptionsType { - components: any[] + components: any[], + store: any, } export class Excel { $el: HTMLElement | Dom; components: any[]; emitter: Emitter; + store: Store; constructor(selector: string, options: ExcelOptionsType) { this.$el = $(selector); this.components = options.components; this.emitter = new Emitter(); + this.store = options.store; console.log(`Created new Excel class in ${selector} with options: ${options}`); } @@ -24,6 +28,7 @@ export class Excel { const componentOptions = { emitter: this.emitter, + store: this.store, }; this.components = this.components.map(Component => { diff --git a/src/components/table/Table.ts b/src/components/table/Table.ts index 9a434c0..af74766 100644 --- a/src/components/table/Table.ts +++ b/src/components/table/Table.ts @@ -1,9 +1,10 @@ -import { DomClass } from '../../core/dom'; +import * as actions from '../../redux/actions'; +import { $, DomClass } from '../../core/dom'; import { ExcelComponent } from '../../core/ExcelComponent'; -import { selectHandler } from './handlers/table.select.handler'; import { TableSelection } from './TableSelection'; import { createTable } from './table.template'; import { resizeHandler } from './handlers/table.resize'; +import { selectHandler } from './handlers/table.select.handler'; export class Table extends ExcelComponent { static className = 'excel__table'; @@ -41,15 +42,38 @@ export class Table extends ExcelComponent { this.$on('formula:enter-press', () => { this.selection.current.focus(); }); + + // this.$subscribe(state => { + // console.log('State in Table', state); + // }); + + this.initTableSize(); + } + + initTableSize() { + const colSizes = this.store.getState()?.colState; + Object.keys(colSizes).forEach(key => { + const cols = this.$root.findAll(`[data-col="${key}"]`); + cols.forEach(el => $(el as HTMLElement).css({ width: `${colSizes[key]}px` })); + }); } emitSelectCallback() { this.$emit('table:select-cell', this.selection.current.text); } + async resizeTable(event: MouseEvent) { + try { + const resizeData = await resizeHandler(this.$root, event); + this.$dispatch(actions.tableResize({ resizeData })); + } catch (e) { + console.warn('Resize error', e.message); + } + } + onMousedown(event: MouseEvent) { selectHandler(event, this.selection, this.emitSelectCallback.bind(this)); - resizeHandler(this.$root, event); + this.resizeTable(event); } onKeydown(event: KeyboardEvent) { diff --git a/src/components/table/handlers/table.resize.ts b/src/components/table/handlers/table.resize.ts index e23d751..9b6c8f3 100644 --- a/src/components/table/handlers/table.resize.ts +++ b/src/components/table/handlers/table.resize.ts @@ -1,77 +1,89 @@ import { $, Dom } from '../../../core/dom'; type CustomElementType = Element & { css: any }; +type ResizeReturnDataType = { value: number, id: string }; export function resizeHandler($root: Dom, event: MouseEvent) { - const { target } = event; - if (!(target as HTMLElement).dataset.resize) return; + return new Promise(res => { + const { target } = event; + if (!(target as HTMLElement).dataset.resize) return; - const $resizer = $(target as HTMLElement); - const $parent = $resizer.closest('[data-type="resizable"]'); - const coords = $parent.getCoords(); - const type = $resizer.data.resize; + const $resizer = $(target as HTMLElement); + const $parent = $resizer.closest('[data-type="resizable"]'); + const coords = $parent.getCoords(); + const type = $resizer.data.resize; - const allCols = $root.findAll(`[data-col="${$parent.data.col}"]`); + const allCols = $root.findAll(`[data-col="${$parent.data.col}"]`); - let delta: number; + let delta: number; - (Element.prototype as CustomElementType).css = function (styles: any) { - Object.keys(styles).forEach((key: any) => { - this.style[key] = styles[key]; - }); - }; + (Element.prototype as CustomElementType).css = function (styles: any) { + Object.keys(styles).forEach((key: any) => { + this.style[key] = styles[key]; + }); + }; - $resizer.css({ opacity: 1 }); + $resizer.css({ opacity: 1 }); - document.onmousemove = e => { - document.body.style.userSelect = 'none'; + document.onmousemove = e => { + document.body.style.userSelect = 'none'; - switch (type) { - case 'col': { - delta = e.pageX - coords.right; - $resizer.css({ - right: `${-delta}px`, - bottom: '-100vh', - }); + switch (type) { + case 'col': { + delta = e.pageX - coords.right; + $resizer.css({ + right: `${-delta}px`, + bottom: '-100vh', + }); - break; + break; + } + + case 'row': { + delta = e.pageY - coords.bottom; + + $resizer.css({ + bottom: `${-delta}px`, + right: '-100vw', + }); + + break; + } + + default: break; + } + }; + + document.onmouseup = () => { + document.onmousemove = null; + document.onmouseup = null; + document.body.style.userSelect = null; + + let value: number; + + switch (type) { + case 'col': { + value = coords.width + delta; + $parent.css({ width: `${value}px` }); + allCols.forEach((el: CustomElementType) => el.css({ width: `${coords.width + delta}px` })); + break; + } + + case 'row': { + value = coords.height + delta; + $parent.css({ height: `${value}px` }); + break; + } + + default: break; } - case 'row': { - delta = e.pageY - coords.bottom; + res({ + value, + id: type === 'col' ? $parent.data.col : null, + }); - $resizer.css({ - bottom: `${-delta}px`, - right: '-100vw', - }); - - break; - } - - default: break; - } - }; - - document.onmouseup = () => { - document.onmousemove = null; - document.onmouseup = null; - document.body.style.userSelect = null; - - switch (type) { - case 'col': { - $parent.css({ width: `${coords.width + delta}px` }); - allCols.forEach((el: CustomElementType) => el.css({ width: `${coords.width + delta}px` })); - break; - } - - case 'row': { - $parent.css({ height: `${coords.height + (delta)}px` }); - break; - } - - default: break; - } - - $resizer.css({ opacity: 0, bottom: 0, right: 0 }); - }; + $resizer.css({ opacity: 0, bottom: 0, right: 0 }); + }; + }); } diff --git a/src/core/ExcelComponent.ts b/src/core/ExcelComponent.ts index c47552e..3356336 100644 --- a/src/core/ExcelComponent.ts +++ b/src/core/ExcelComponent.ts @@ -1,3 +1,5 @@ +import { ActionType, StateType, SubscribeType } from '../redux/types'; +import { Store } from './createStore'; import { DomListener } from './DomListener'; import { Emitter } from './Emitter'; @@ -10,17 +12,21 @@ type OptionsType = { listeners?: string[]; name: string; emitter?: Emitter; + store: Store; }; export class ExcelComponent extends DomListener implements ExcelComponentClass { name: string; emitter: Emitter; + store: Store; + storeSub: SubscribeType; private unsubscribers: ((args?: any) => any)[]; constructor($root: any, options: OptionsType) { super($root, options?.listeners); this.name = options?.name; this.emitter = options?.emitter; + this.store = options?.store; this.unsubscribers = []; this.prepare(); } @@ -42,6 +48,14 @@ export class ExcelComponent extends DomListener implements ExcelComponentClass { this.unsubscribers.push(unsub); } + $dispatch(action: ActionType) { + this.store.dispatch(action); + } + + $subscribe(fn: (state?: StateType) => void) { + this.storeSub = this.store.subscribe(fn); + } + init() { this.initDOMListeners(); } @@ -49,5 +63,6 @@ export class ExcelComponent extends DomListener implements ExcelComponentClass { destroy() { this.removeDOMListeners(); this.unsubscribers.forEach(unsub => unsub()); + this.storeSub.unsubscribe(); } } diff --git a/src/core/createStore.ts b/src/core/createStore.ts new file mode 100644 index 0000000..2a239e0 --- /dev/null +++ b/src/core/createStore.ts @@ -0,0 +1,40 @@ +import { + ActionType, ReducerType, StateType, SubscribeType, +} from '../redux/types'; +import { storage } from './utils'; + +const initialState: StateType = { + colState: {}, + rowState: {}, + ...storage('excel-state'), +}; + +export class Store { + static initialState = {}; + + state: StateType; + listeners: ((args?: any) => void)[]; + + constructor(private reducer: ReducerType) { + this.state = reducer({ ...initialState }, { type: '__INIT__' }); + this.listeners = []; + } + + subscribe(fn: (state?: StateType) => void): SubscribeType { + this.listeners.push(fn); + return { + unsubscribe() { + this.listeners = this.listeners.filter((l: any) => l !== fn); + }, + }; + } + + dispatch(action: ActionType) { + this.state = this.reducer(this.state, action); + this.listeners.forEach(listener => listener(this.state)); + } + + getState() { + return this.state; + } +} diff --git a/src/core/utils.ts b/src/core/utils.ts index 53c7860..f560953 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -3,3 +3,11 @@ export function capitalize(string: string): string { return string.charAt(0).toUpperCase() + string.slice(1); } + +export function storage(key: string, data: any = null): any { + if (!data) return JSON.parse(localStorage.getItem(key)); + + localStorage.setItem(key, JSON.stringify(data)); + + return true; +} diff --git a/src/index.ts b/src/index.ts index f1e0e26..2660e7f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,9 +4,19 @@ import { Header } from './components/header/Header'; import { Table } from './components/table/Table'; import { Toolbar } from './components/toolbar/Toolbar'; import './styles/index.scss'; +import { Store } from './core/createStore'; +import { storage } from './core/utils'; +import { rootReducer } from './redux/rootReducer'; + +const store = new Store(rootReducer); + +store.subscribe(state => { + storage('excel-state', state); +}); const excel = new Excel('#app', { components: [Header, Toolbar, Formula, Table], + store, }); excel.render(); diff --git a/src/redux/actions-types.d.ts b/src/redux/actions-types.d.ts new file mode 100644 index 0000000..eecb847 --- /dev/null +++ b/src/redux/actions-types.d.ts @@ -0,0 +1,8 @@ +export type ResizePayloadType = { + colState: { + [k in number]: number + }, + rowState: { + [k in number]: number + }, +}; diff --git a/src/redux/actions.ts b/src/redux/actions.ts new file mode 100644 index 0000000..c6b8a4f --- /dev/null +++ b/src/redux/actions.ts @@ -0,0 +1,8 @@ +import { TABLE_RESIZE } from './constants'; + +export function tableResize(resizeData: any) { + return { + type: TABLE_RESIZE, + ...resizeData, + }; +} diff --git a/src/redux/constants.ts b/src/redux/constants.ts new file mode 100644 index 0000000..7f678f9 --- /dev/null +++ b/src/redux/constants.ts @@ -0,0 +1 @@ +export const TABLE_RESIZE = 'TABLE_RESIZE'; diff --git a/src/redux/rootReducer.ts b/src/redux/rootReducer.ts new file mode 100644 index 0000000..3bd2462 --- /dev/null +++ b/src/redux/rootReducer.ts @@ -0,0 +1,14 @@ +import { TABLE_RESIZE } from './constants'; +import { ActionType, StateType } from './types'; + +export function rootReducer(state: StateType, action: ActionType) { + switch (action.type) { + case TABLE_RESIZE: { + const prevState = state.colState || {}; + prevState[action.resizeData?.id] = action.resizeData?.value; + return { ...state, colState: prevState }; + } + + default: return state; + } +} diff --git a/src/redux/types.d.ts b/src/redux/types.d.ts new file mode 100644 index 0000000..5491dda --- /dev/null +++ b/src/redux/types.d.ts @@ -0,0 +1,14 @@ +export type ActionType = { + type: string + [k: string]: any +}; + +export type StateType = { + [k: string]: any +}; + +export type ReducerType = (state: StateType, action: ActionType) => StateType; + +export type SubscribeType = { + unsubscribe: () => void +};