add style state function

This commit is contained in:
Sergey Krylov 2022-07-04 10:21:36 +05:00
parent 79bf78e1e3
commit 74b8ca6124
17 changed files with 300 additions and 67 deletions

View File

@ -33,6 +33,7 @@ module.exports = {
"max-len": "off",
"no-console": "off",
"no-plusplus": "off",
"object-curly-newline": "off",
},
env: {
browser: true,

View File

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

View File

@ -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 `
<input type="text" class="input" value="Новая таблица">
<input type="text" class="input" value="${title}">
<div>
<div class="button">
@ -24,4 +28,10 @@ export class Header extends ExcelComponent {
</div>
`;
}
onInput(event: InputEvent) {
const $target = $(event.target as HTMLInputElement);
this.$dispatch(actions.changeTitle($target.text));
}
}

View File

@ -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<CSSStyleDeclaration>)[]);
this.$dispatch(changeCurrentStyles(styles));
}
async resizeTable(event: MouseEvent) {

View File

@ -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));
}
}

View File

@ -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 `
<div class="button">
<i class="material-icons">format_bold</i>
</div>
<div class="button">
<i class="material-icons">format_italic</i>
</div>
<div class="button">
<i class="material-icons">format_underline</i>
</div>
<div class="button">
<i class="material-icons">format_align_left</i>
</div>
<div class="button">
<i class="material-icons">format_align_center</i>
</div>
<div class="button">
<i class="material-icons">format_align_right</i>
</div>
`;
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] });
}
}

View File

@ -0,0 +1,6 @@
export type ToolbarStateType = {
fontWeight?: 'normal' | 'bold';
fontStyle?: 'normal' | 'italic';
textDecoration?: 'none' | 'underline';
textAlign?: 'left' | 'center' | 'right';
};

View File

@ -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 `
<div
class="button ${button.isActive && 'active'}"
${meta}
>
<i class="material-icons" ${meta}>${button.icon}</i>
</div>
`;
}

20
src/constants.ts Normal file
View File

@ -0,0 +1,20 @@
import { storage } from './core/utils';
import { StateType } from './redux/types';
export const initialStyleState: Partial<CSSStyleDeclaration> = {
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'] },
};

View File

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

View File

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

View File

@ -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);
}
}

View File

@ -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)[];

View File

@ -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<CSSStyleDeclaration>)[]) {
return styles.reduce((res: Partial<CSSStyleDeclaration>, s: any) => {
res[s] = this.$el.style[s] || initialStyleState[s];
return res;
}, {});
}
}
export function $(selector: SelectorType) {

View File

@ -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,
};
}

View File

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

View File

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