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;
+// }