add style state function
This commit is contained in:
parent
79bf78e1e3
commit
74b8ca6124
@ -33,6 +33,7 @@ module.exports = {
|
||||
"max-len": "off",
|
||||
"no-console": "off",
|
||||
"no-plusplus": "off",
|
||||
"object-curly-newline": "off",
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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] });
|
||||
}
|
||||
}
|
||||
|
||||
6
src/components/toolbar/toolbar-types.ts
Normal file
6
src/components/toolbar/toolbar-types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export type ToolbarStateType = {
|
||||
fontWeight?: 'normal' | 'bold';
|
||||
fontStyle?: 'normal' | 'italic';
|
||||
textDecoration?: 'none' | 'underline';
|
||||
textAlign?: 'left' | 'center' | 'right';
|
||||
};
|
||||
75
src/components/toolbar/toolbar.template.ts
Normal file
75
src/components/toolbar/toolbar.template.ts
Normal 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
20
src/constants.ts
Normal 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'] },
|
||||
};
|
||||
@ -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 => {
|
||||
|
||||
@ -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) {
|
||||
|
||||
27
src/core/ExcelStateComponent.ts
Normal file
27
src/core/ExcelStateComponent.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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)[];
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
// }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user