From 74b8ca6124ece08a808a2adf6f9e8e2dad91dee5 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Mon, 4 Jul 2022 10:21:36 +0500 Subject: [PATCH] add style state function --- .eslintrc.js | 1 + src/components/formula/Formula.ts | 4 +- src/components/header/Header.ts | 16 ++++- src/components/table/Table.ts | 42 ++++++++---- src/components/table/TableSelection.ts | 8 +++ src/components/toolbar/Toolbar.ts | 67 +++++++++++-------- src/components/toolbar/toolbar-types.ts | 6 ++ src/components/toolbar/toolbar.template.ts | 75 ++++++++++++++++++++++ src/constants.ts | 20 ++++++ src/core/Emitter.ts | 2 +- src/core/ExcelComponent.ts | 9 +-- src/core/ExcelStateComponent.ts | 27 ++++++++ src/core/createStore.ts | 16 +---- src/core/dom.ts | 14 +++- src/redux/actions.ts | 23 ++++++- src/redux/constants.ts | 3 + src/redux/rootReducer.ts | 34 +++++++++- 17 files changed, 300 insertions(+), 67 deletions(-) create mode 100644 src/components/toolbar/toolbar-types.ts create mode 100644 src/components/toolbar/toolbar.template.ts create mode 100644 src/constants.ts create mode 100644 src/core/ExcelStateComponent.ts diff --git a/.eslintrc.js b/.eslintrc.js index 2a9801b..8b7c8ac 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -33,6 +33,7 @@ module.exports = { "max-len": "off", "no-console": "off", "no-plusplus": "off", + "object-curly-newline": "off", }, env: { browser: true, diff --git a/src/components/formula/Formula.ts b/src/components/formula/Formula.ts index 215ab89..ea137aa 100644 --- a/src/components/formula/Formula.ts +++ b/src/components/formula/Formula.ts @@ -1,4 +1,4 @@ -import { Dom, DomClass } from '../../core/dom'; +import { Dom } from '../../core/dom'; import { ExcelComponent } from '../../core/ExcelComponent'; export class Formula extends ExcelComponent { @@ -6,7 +6,7 @@ export class Formula extends ExcelComponent { private formulaInput: Dom; - constructor($root: DomClass, options: any) { + constructor($root: Dom, options: any) { super($root, { listeners: ['input', 'keydown'], name: 'Formula', diff --git a/src/components/header/Header.ts b/src/components/header/Header.ts index 8632005..143266c 100644 --- a/src/components/header/Header.ts +++ b/src/components/header/Header.ts @@ -1,19 +1,23 @@ -import { DomClass } from '../../core/dom'; +import { $, Dom } from '../../core/dom'; import { ExcelComponent } from '../../core/ExcelComponent'; +import * as actions from '../../redux/actions'; export class Header extends ExcelComponent { static className = 'excel__header'; - constructor($root: DomClass, options: any) { + constructor($root: Dom, options: any) { super($root, { name: 'Header', + listeners: ['input'], ...options, }); } toHTML(): string { + const { title } = this.store.getState(); + return ` - +
@@ -24,4 +28,10 @@ export class Header extends ExcelComponent {
`; } + + onInput(event: InputEvent) { + const $target = $(event.target as HTMLInputElement); + + this.$dispatch(actions.changeTitle($target.text)); + } } diff --git a/src/components/table/Table.ts b/src/components/table/Table.ts index 4594945..40653c1 100644 --- a/src/components/table/Table.ts +++ b/src/components/table/Table.ts @@ -1,17 +1,19 @@ -import * as actions from '../../redux/actions'; -import { $, DomClass } from '../../core/dom'; +import { initialStyleState } from '../../constants'; +import { $, Dom } from '../../core/dom'; import { ExcelComponent } from '../../core/ExcelComponent'; -import { TableSelection } from './TableSelection'; -import { createTable } from './table.template'; +import { changeCurrentStyles } from '../../redux/actions'; +import * as actions from '../../redux/actions'; import { resizeHandler } from './handlers/table.resize'; import { selectHandler } from './handlers/table.select.handler'; +import { createTable } from './table.template'; +import { TableSelection } from './TableSelection'; export class Table extends ExcelComponent { static className = 'excel__table'; private selection: TableSelection; - constructor($root: DomClass, options: any) { + constructor($root: Dom, options: any) { super($root, { name: 'Table', listeners: ['mousedown', 'keydown', 'input'], @@ -44,15 +46,21 @@ export class Table extends ExcelComponent { this.selection.current.focus(); }); - // this.$subscribe(state => { - // console.log('State in Table', state); - // }); + this.$on('toolbar:applyStyle', (value) => { + this.selection.applyStyle(value); + + this.$dispatch(actions.applyStyle({ + value, + ids: this.selection.selectedIds, + })); + }); + this.initTable(); } initTable() { this.initTableSize(); - this.initTableContent(); + this.initTableContentAndStyles(); } initTableSize() { @@ -71,15 +79,25 @@ export class Table extends ExcelComponent { }); } - initTableContent() { - const tableContent = this.store.getState().dataState; + initTableContentAndStyles() { + const tableState = this.store.getState(); + const tableContent = tableState?.dataState; + const tableStyles = tableState?.stylesState; + Object.keys(tableContent).forEach(cellId => { - this.$root.find(`[data-id="${cellId}"]`).text = tableContent[cellId] || ''; + const $cell = this.$root.find(`[data-id="${cellId}"]`); + const styles = tableStyles[cellId]; + + $cell.text = tableContent[cellId] || ''; + $cell.css(styles); }); } emitSelectCallback() { this.$emit('table:select-cell', this.selection.current.text); + + const styles = this.selection.current?.getStyles(Object.keys(initialStyleState) as (keyof Partial)[]); + this.$dispatch(changeCurrentStyles(styles)); } async resizeTable(event: MouseEvent) { diff --git a/src/components/table/TableSelection.ts b/src/components/table/TableSelection.ts index edefab8..1619a71 100644 --- a/src/components/table/TableSelection.ts +++ b/src/components/table/TableSelection.ts @@ -11,6 +11,10 @@ export class TableSelection { this.current = null; } + get selectedIds() { + return this.group.map(el => el.data.id); + } + select($el: Dom) { this.clearSelection(); this.group = [$el]; @@ -57,4 +61,8 @@ export class TableSelection { } } } + + applyStyle(style: CSSStyleRule) { + this.group.forEach(el => el.css(style)); + } } diff --git a/src/components/toolbar/Toolbar.ts b/src/components/toolbar/Toolbar.ts index f4350d2..bf8ee19 100644 --- a/src/components/toolbar/Toolbar.ts +++ b/src/components/toolbar/Toolbar.ts @@ -1,41 +1,54 @@ -import { DomClass } from '../../core/dom'; -import { ExcelComponent } from '../../core/ExcelComponent'; +import { initialStyleState } from '../../constants'; +import { $, Dom } from '../../core/dom'; +import { OptionsType } from '../../core/ExcelComponent'; +import { ExcelStateComponent } from '../../core/ExcelStateComponent'; +import { createToolbar } from './toolbar.template'; -export class Toolbar extends ExcelComponent { +export class Toolbar extends ExcelStateComponent { static className = 'excel__toolbar'; - constructor($root: DomClass, options: any) { + constructor($root: Dom, options: OptionsType) { super($root, { name: 'Toolbar', listeners: ['click'], + subscribe: ['currentStyles'], ...options, }); } - toHTML(): string { - return ` -
- format_bold -
-
- format_italic -
-
- format_underline -
-
- format_align_left -
-
- format_align_center -
-
- format_align_right -
- `; + prepare() { + const currentToolbarState = this.toolbarState; + console.log('Current state', currentToolbarState); + + this.initState(currentToolbarState); } - onClick() { - console.log('Toolbar click'); + get toolbarState() { + return { + ...initialStyleState, + ...this.store.getState().stylesState['0:0'], + }; + } + + get template(): string { + return createToolbar(this.state); + } + + toHTML(): string { + return this.template; + } + + storeChanged(args?: any) { + this.setState(args.currentStyles); + } + + onClick(event: MouseEvent) { + const target = $(event.target as HTMLElement); + const value = JSON.parse(target.data.value); + const key = Object.keys(value)[0]; + + this.$emit('toolbar:applyStyle', value); + + this.setState({ [key]: value[key] }); } } diff --git a/src/components/toolbar/toolbar-types.ts b/src/components/toolbar/toolbar-types.ts new file mode 100644 index 0000000..c0f75c8 --- /dev/null +++ b/src/components/toolbar/toolbar-types.ts @@ -0,0 +1,6 @@ +export type ToolbarStateType = { + fontWeight?: 'normal' | 'bold'; + fontStyle?: 'normal' | 'italic'; + textDecoration?: 'none' | 'underline'; + textAlign?: 'left' | 'center' | 'right'; +}; diff --git a/src/components/toolbar/toolbar.template.ts b/src/components/toolbar/toolbar.template.ts new file mode 100644 index 0000000..b81e056 --- /dev/null +++ b/src/components/toolbar/toolbar.template.ts @@ -0,0 +1,75 @@ +import { initialStyleState } from '../../constants'; +import { ToolbarStateType } from './toolbar-types'; + +type ButtonConfigType = { + icon: string; + isActive: boolean; + value: { + [k: string]: string | number + } +}; + +export function createToolbar(state: ToolbarStateType): string { + 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_underline', + isActive: state.textDecoration === 'underline', + value: { + textDecoration: state.textDecoration === 'underline' ? initialStyleState.textDecoration : 'underline', + }, + }, + { + icon: 'format_align_left', + isActive: state.textAlign === 'left', + value: { + textAlign: 'left', + }, + }, + { + icon: 'format_align_center', + isActive: state.textAlign === 'center', + value: { + textAlign: state.textAlign === 'center' ? initialStyleState.textAlign : 'center', + }, + }, + { + icon: 'format_align_right', + isActive: state.textAlign === 'right', + value: { + textAlign: state.textAlign === 'right' ? initialStyleState.textAlign : 'right', + }, + }, + ]; + + return btns.map(btn => toButton(btn)).join(' '); +} + +function toButton(button: ButtonConfigType) { + const meta = ` + data-type="button" + data-value='${JSON.stringify(button.value)}' + `; + + return ` +
+ ${button.icon} +
+ `; +} diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..b28c754 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,20 @@ +import { storage } from './core/utils'; +import { StateType } from './redux/types'; + +export const initialStyleState: Partial = { + textAlign: 'left', + fontWeight: 'normal', + textDecoration: 'none', + fontStyle: 'normal', +}; + +export const initialState: StateType = { + colState: {}, + rowState: {}, + dataState: {}, + currentText: '', + stylesState: {}, + title: 'New excel table', + ...storage('excel-state'), + currentStyles: { ...storage('excel-state').stylesState['0:0'] }, +}; diff --git a/src/core/Emitter.ts b/src/core/Emitter.ts index e38338f..dfbe507 100644 --- a/src/core/Emitter.ts +++ b/src/core/Emitter.ts @@ -7,7 +7,7 @@ export class Emitter { this.listeners = {}; } - emit(eventName: string, ...args: any[]) { + emit(eventName: string, args: any[]) { if (!Array.isArray(this.listeners[eventName])) return false; this.listeners[eventName].forEach(listener => { diff --git a/src/core/ExcelComponent.ts b/src/core/ExcelComponent.ts index bce3712..f092a71 100644 --- a/src/core/ExcelComponent.ts +++ b/src/core/ExcelComponent.ts @@ -1,5 +1,6 @@ import { ActionType, SubscribeType } from '../redux/types'; import { Store } from './createStore'; +import { Dom } from './dom'; import { DomListener } from './DomListener'; import { Emitter } from './Emitter'; @@ -9,7 +10,7 @@ interface ExcelComponentClass { storeChanged?: (args: any) => void; } -type OptionsType = { +export type OptionsType = { listeners?: string[]; name: string; emitter?: Emitter; @@ -25,7 +26,7 @@ export abstract class ExcelComponent extends DomListener implements ExcelCompone subscribe: string[]; private unsubscribers: ((args?: any) => any)[]; - constructor($root: any, options: OptionsType) { + constructor($root: Dom, options?: OptionsType) { super($root, options?.listeners); this.name = options?.name; this.emitter = options?.emitter; @@ -44,8 +45,8 @@ export abstract class ExcelComponent extends DomListener implements ExcelCompone return ''; } - $emit(event: string, ...args: any): void { - this.emitter.emit(event, ...args); + $emit(event: string, args?: any): void { + this.emitter.emit(event, args); } $on(event: string, callback: (args: any) => any) { diff --git a/src/core/ExcelStateComponent.ts b/src/core/ExcelStateComponent.ts new file mode 100644 index 0000000..f65f7e6 --- /dev/null +++ b/src/core/ExcelStateComponent.ts @@ -0,0 +1,27 @@ +import { Dom } from './dom'; +import { ExcelComponent, OptionsType } from './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); + } +} diff --git a/src/core/createStore.ts b/src/core/createStore.ts index f8de14e..cde045e 100644 --- a/src/core/createStore.ts +++ b/src/core/createStore.ts @@ -1,19 +1,7 @@ -import { - ActionType, ReducerType, StateType, SubscribeType, -} from '../redux/types'; -import { storage } from './utils'; - -const initialState: StateType = { - colState: {}, - rowState: {}, - dataState: {}, - currentText: '', - ...storage('excel-state'), -}; +import { initialState } from '../constants'; +import { ActionType, ReducerType, StateType, SubscribeType } from '../redux/types'; export class Store { - static initialState = {}; - state: StateType; listeners: ((args?: any) => void)[]; diff --git a/src/core/dom.ts b/src/core/dom.ts index e046d1d..cda9284 100644 --- a/src/core/dom.ts +++ b/src/core/dom.ts @@ -1,3 +1,5 @@ +import { initialStyleState } from '../constants'; + type SelectorType = string | HTMLElement; export interface DomClass { @@ -29,6 +31,7 @@ export class Dom implements DomClass { } get text() { + if (this.$el.closest('input')) return (this.$el as HTMLInputElement).value; return this.$el.textContent; } @@ -86,7 +89,9 @@ export class Dom implements DomClass { } css(styles: any) { - Object.keys(styles).forEach((key: any) => { + if (!styles) return; + + Object.keys(styles)?.forEach((key: any) => { this.$el.style[key] = styles[key]; }); } @@ -98,6 +103,13 @@ export class Dom implements DomClass { removeClass(className: string) { this.$el?.classList.remove(className); } + + getStyles(styles: (keyof Partial)[]) { + return styles.reduce((res: Partial, s: any) => { + res[s] = this.$el.style[s] || initialStyleState[s]; + return res; + }, {}); + } } export function $(selector: SelectorType) { diff --git a/src/redux/actions.ts b/src/redux/actions.ts index 69235b4..7b3b6dd 100644 --- a/src/redux/actions.ts +++ b/src/redux/actions.ts @@ -1,4 +1,4 @@ -import { CHANGE_TEXT, TABLE_RESIZE } from './constants'; +import { CHANGE_TEXT, CHANGE_STYLES, TABLE_RESIZE, APPLY_STYLES, CHANGE_TITLE } from './constants'; export function tableResize(resizeData: any) { return { @@ -13,3 +13,24 @@ export function changeText(data: { text: string, id: string }) { 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, + }; +} diff --git a/src/redux/constants.ts b/src/redux/constants.ts index 3ca1267..5cb2615 100644 --- a/src/redux/constants.ts +++ b/src/redux/constants.ts @@ -1,2 +1,5 @@ export const TABLE_RESIZE = 'TABLE_RESIZE'; export const CHANGE_TEXT = 'CHANGE_TEXT'; +export const APPLY_STYLES = 'APPLY_STYLES'; +export const CHANGE_STYLES = 'CURRENT_STYLES'; +export const CHANGE_TITLE = 'CHANGE_TITLE'; diff --git a/src/redux/rootReducer.ts b/src/redux/rootReducer.ts index 0402d0a..3842298 100644 --- a/src/redux/rootReducer.ts +++ b/src/redux/rootReducer.ts @@ -1,4 +1,4 @@ -import { CHANGE_TEXT, TABLE_RESIZE } from './constants'; +import { CHANGE_TEXT, CHANGE_STYLES, TABLE_RESIZE, APPLY_STYLES, CHANGE_TITLE } from './constants'; import { ActionType, StateType } from './types'; export function rootReducer(state: StateType, action: ActionType) { @@ -14,12 +14,42 @@ export function rootReducer(state: StateType, action: ActionType) { case CHANGE_TEXT: { const newState: StateType = state.dataState || {}; + const fieldName = action.data.id; - newState[action.data.id] = action.data.text; + newState[fieldName] = action.data.text; return { ...state, currentText: action?.data?.text, dataState: newState }; } + case CHANGE_STYLES: { + return { ...state, currentStyles: action.data }; + } + + case APPLY_STYLES: { + const fieldName = 'stylesState'; + const val = this.state[fieldName] || {}; + + action.data.ids.forEach((id: string) => { + val[id] = { ...val[id], ...action.data.value }; + }); + + return { + ...state, + [fieldName]: val, + currentStyles: { ...state.currentStyles, ...action.data.value }, + }; + } + + case CHANGE_TITLE: { + return { ...state, title: action.data }; + } + default: return state; } } + +// function value(state, field, action) { +// const val = state[field] || {}; +// val[action.data.id] = action.data.value; +// return val; +// }