add excel state, initial table column size

This commit is contained in:
Sergey Krylov 2022-06-29 10:26:13 +05:00
parent c74c0490c1
commit a8b9cbfb9f
13 changed files with 226 additions and 65 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

40
src/core/createStore.ts Normal file
View File

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

View File

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

View File

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

8
src/redux/actions-types.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
export type ResizePayloadType = {
colState: {
[k in number]: number
},
rowState: {
[k in number]: number
},
};

8
src/redux/actions.ts Normal file
View File

@ -0,0 +1,8 @@
import { TABLE_RESIZE } from './constants';
export function tableResize(resizeData: any) {
return {
type: TABLE_RESIZE,
...resizeData,
};
}

1
src/redux/constants.ts Normal file
View File

@ -0,0 +1 @@
export const TABLE_RESIZE = 'TABLE_RESIZE';

14
src/redux/rootReducer.ts Normal file
View File

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

14
src/redux/types.d.ts vendored Normal file
View File

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