add routing, dashboard page, excel table pages

This commit is contained in:
Sergey Krylov 2022-07-06 17:53:15 +05:00
parent 1456c256e3
commit 6f1b9803bd
25 changed files with 315 additions and 78 deletions

View File

@ -26,6 +26,7 @@ module.exports = {
"@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/restrict-template-expressions": "off", "@typescript-eslint/restrict-template-expressions": "off",
"@typescript-eslint/unbound-method": "off", "@typescript-eslint/unbound-method": "off",
"@typescript-eslint/quotes": "warn",
"arrow-parens": "off", "arrow-parens": "off",
"class-methods-use-this": "off", "class-methods-use-this": "off",
"func-names": "off", "func-names": "off",
@ -34,6 +35,7 @@ module.exports = {
"max-len": "off", "max-len": "off",
"no-console": "off", "no-console": "off",
"no-new": "off", "no-new": "off",
"no-continue": "off",
"no-plusplus": "off", "no-plusplus": "off",
"object-curly-newline": "off", "object-curly-newline": "off",
}, },

View File

@ -1,8 +1,8 @@
import { Store } from '../../core/createStore'; import { Store } from 'core/createStore';
import { $, Dom } from '../../core/dom'; import { $, Dom } from 'core/dom';
import { Emitter } from '../../core/Emitter'; import { Emitter } from 'core/Emitter';
import { ExcelComponent } from '../../core/ExcelComponent'; import { ExcelComponent } from 'core/ExcelComponent';
import { StoreSubscriber } from '../../core/StoreSubscriber'; import { StoreSubscriber } from 'core/StoreSubscriber';
interface ExcelOptionsType { interface ExcelOptionsType {
components: any[], components: any[],
@ -16,14 +16,11 @@ export class Excel {
store: Store; store: Store;
subscriber: StoreSubscriber; subscriber: StoreSubscriber;
constructor(selector: string, options: ExcelOptionsType) { constructor(options: ExcelOptionsType) {
this.$el = $(selector);
this.components = options.components; this.components = options.components;
this.emitter = new Emitter(); this.emitter = new Emitter();
this.store = options.store; this.store = options.store;
this.subscriber = new StoreSubscriber(this.store); this.subscriber = new StoreSubscriber(this.store);
console.log(`Created new Excel class in ${selector} with options: ${options}`);
} }
getRoot() { getRoot() {
@ -44,12 +41,10 @@ export class Excel {
return component; return component;
}); });
return $root.$el; return $root;
} }
render() { init() {
this.$el.append(this.getRoot());
this.subscriber.subscribeComponents(this.components); this.subscriber.subscribeComponents(this.components);
this.components.forEach(component => component.init()); this.components.forEach(component => component.init());
} }

View File

@ -1,5 +1,5 @@
import { Dom } from '../../core/dom'; import { Dom } from 'core/dom';
import { ExcelComponent } from '../../core/ExcelComponent'; import { ExcelComponent } from 'core/ExcelComponent';
export class Formula extends ExcelComponent { export class Formula extends ExcelComponent {
static className = 'excel__formula'; static className = 'excel__formula';

View File

@ -1,14 +1,16 @@
import { $, Dom } from '../../core/dom'; import { $, Dom } from 'core/dom';
import { ExcelComponent } from '../../core/ExcelComponent'; import { ExcelStateComponent } from 'core/ExcelStateComponent';
import * as actions from '../../redux/actions'; import { deleteTable } from 'redux/actions';
import * as actions from 'redux/actions';
export class Header extends ExcelComponent { export class Header extends ExcelStateComponent {
static className = 'excel__header'; static className = 'excel__header';
constructor($root: Dom, options: any) { constructor($root: Dom, options: any) {
super($root, { super($root, {
name: 'Header', name: 'Header',
listeners: ['input'], listeners: ['input', 'click'],
subscribe: ['title'],
...options, ...options,
}); });
} }
@ -20,10 +22,10 @@ export class Header extends ExcelComponent {
<input type="text" class="input" value="${title}"> <input type="text" class="input" value="${title}">
<div> <div>
<div class="button"> <div class="button" data-button="delete-table">
<i class="material-icons">delete</i> <i class="material-icons">delete</i>
</div> </div>
<div class="button"> <div class="button" data-button="exit">
<i class="material-icons">exit_to_app</i> <i class="material-icons">exit_to_app</i>
</div> </div>
`; `;
@ -34,4 +36,31 @@ export class Header extends ExcelComponent {
this.$dispatch(actions.changeTitle($target.text)); this.$dispatch(actions.changeTitle($target.text));
} }
onClick(event: MouseEvent) {
const target = event.target as HTMLElement;
if (!target) return;
const $btn = $(target).closest('[data-button]');
if (!$btn.$el) return;
const btnEvent = $btn?.data?.button;
switch (btnEvent) {
case 'exit': {
window.location.hash = '';
break;
}
case 'delete-table': {
this.$dispatch(deleteTable(this.store.getState().id));
console.log('THIS', this);
this.destroy();
break;
}
default:
break;
}
}
} }

View File

@ -1,9 +1,9 @@
import { $, Dom } from 'core/dom';
import { ExcelComponent } from 'core/ExcelComponent';
import { parse } from 'core/parse';
import { changeCurrentStyles } from 'redux/actions';
import * as actions from 'redux/actions';
import { initialStyleState } from '../../constants'; import { initialStyleState } from '../../constants';
import { $, Dom } from '../../core/dom';
import { ExcelComponent } from '../../core/ExcelComponent';
import { parse } from '../../core/parse';
import { changeCurrentStyles } from '../../redux/actions';
import * as actions from '../../redux/actions';
import { resizeHandler } from './handlers/table.resize'; import { resizeHandler } from './handlers/table.resize';
import { selectHandler } from './handlers/table.select.handler'; import { selectHandler } from './handlers/table.select.handler';
import { createTable } from './table.template'; import { createTable } from './table.template';

View File

@ -1,4 +1,4 @@
import { $, Dom } from '../../core/dom'; import { $, Dom } from 'core/dom';
import { getParamsFromCellId } from './table.functions'; import { getParamsFromCellId } from './table.functions';
export class TableSelection { export class TableSelection {

View File

@ -1,4 +1,4 @@
import { $, Dom } from '../../../core/dom'; import { $, Dom } from 'core/dom';
type CustomElementType = Element & { css: any }; type CustomElementType = Element & { css: any };
type ResizeReturnDataType = { value: number, id: string, type: string }; type ResizeReturnDataType = { value: number, id: string, type: string };

View File

@ -1,4 +1,4 @@
import { $ } from '../../../core/dom'; import { $ } from 'core/dom';
import { getParamsFromCellId, isCell } from '../table.functions'; import { getParamsFromCellId, isCell } from '../table.functions';
import { TableSelection } from '../TableSelection'; import { TableSelection } from '../TableSelection';

View File

@ -1,4 +1,4 @@
import { $ } from '../../core/dom'; import { $ } from 'core/dom';
export function isCell(event: Event): boolean { export function isCell(event: Event): boolean {
return $(event.target as HTMLElement).data.type === 'cell'; return $(event.target as HTMLElement).data.type === 'cell';

View File

@ -1,8 +1,8 @@
import { initialStyleState } from '../../constants'; import { $, Dom } from 'core/dom';
import { $, Dom } from '../../core/dom'; import { ExcelStateComponent } from 'core/ExcelStateComponent';
import { OptionsType } from '../../core/ExcelComponent'; import { OptionsType } from 'core/ExcelComponent';
import { ExcelStateComponent } from '../../core/ExcelStateComponent';
import { createToolbar } from './toolbar.template'; import { createToolbar } from './toolbar.template';
import { initialStyleState } from '../../constants';
export class Toolbar extends ExcelStateComponent { export class Toolbar extends ExcelStateComponent {
static className = 'excel__toolbar'; static className = 'excel__toolbar';
@ -18,7 +18,6 @@ export class Toolbar extends ExcelStateComponent {
prepare() { prepare() {
const currentToolbarState = this.toolbarState; const currentToolbarState = this.toolbarState;
console.log('Current state', currentToolbarState);
this.initState(currentToolbarState); this.initState(currentToolbarState);
} }
@ -44,7 +43,10 @@ export class Toolbar extends ExcelStateComponent {
onClick(event: MouseEvent) { onClick(event: MouseEvent) {
const target = $(event.target as HTMLElement); const target = $(event.target as HTMLElement);
const value = JSON.parse(target.data.value); const stringValue = target?.data?.value;
if (!stringValue) return;
const value = JSON.parse(stringValue);
const key = Object.keys(value)[0]; const key = Object.keys(value)[0];
this.$emit('toolbar:applyStyle', value); this.$emit('toolbar:applyStyle', value);

View File

@ -8,13 +8,16 @@ export const initialStyleState: Partial<CSSStyleDeclaration> = {
fontStyle: 'normal', fontStyle: 'normal',
}; };
export const initialState: StateType = { export function getNormalizeInitialState(params: string): StateType {
colState: {}, return {
rowState: {}, colState: {},
dataState: {}, rowState: {},
currentText: '', dataState: {},
stylesState: {}, currentText: '',
title: 'New excel table', stylesState: {},
...storage('excel-state'), title: 'New excel table',
currentStyles: { ...storage('excel-state')?.stylesState?.['0:0'] }, ...storage(`excel:${params}`),
}; currentStyles: { ...storage(`excel:${params}`)?.stylesState?.['0:0'] },
id: params,
};
}

View File

@ -1,4 +1,4 @@
import { ActionType, SubscribeType } from '../redux/types'; import { ActionType, SubscribeType } from 'redux/types';
import { Store } from './createStore'; import { Store } from './createStore';
import { Dom } from './dom'; import { Dom } from './dom';
import { DomListener } from './DomListener'; import { DomListener } from './DomListener';
@ -73,6 +73,5 @@ export abstract class ExcelComponent extends DomListener implements ExcelCompone
destroy() { destroy() {
this.removeDOMListeners(); this.removeDOMListeners();
this.unsubscribers.forEach(unsub => unsub()); this.unsubscribers.forEach(unsub => unsub());
this.storeSub.unsubscribe();
} }
} }

19
src/core/Page.ts Normal file
View File

@ -0,0 +1,19 @@
export abstract class Page {
params: any;
constructor(params: any) {
this.params = params;
}
getRoot() {
}
afterRender() {
}
destroy() {
}
}

View File

@ -1,4 +1,4 @@
import { StateType } from '../redux/types'; import { StateType } from 'redux/types';
import { Store } from './createStore'; import { Store } from './createStore';
import { isEqual } from './utils'; import { isEqual } from './utils';

View File

@ -1,11 +1,10 @@
import { initialState } from '../constants';
import { ActionType, ReducerType, StateType, SubscribeType } from '../redux/types'; import { ActionType, ReducerType, StateType, SubscribeType } from '../redux/types';
export class Store { export class Store {
state: StateType; state: StateType;
listeners: ((args?: any) => void)[]; listeners: ((args?: any) => void)[];
constructor(private reducer: ReducerType) { constructor(private reducer: ReducerType, initialState: StateType) {
this.state = reducer({ ...initialState }, { type: '__INIT__' }); this.state = reducer({ ...initialState }, { type: '__INIT__' });
this.listeners = []; this.listeners = [];
} }
@ -13,7 +12,7 @@ export class Store {
subscribe(fn: (state?: StateType) => void): SubscribeType { subscribe(fn: (state?: StateType) => void): SubscribeType {
this.listeners.push(fn); this.listeners.push(fn);
return { return {
unsubscribe() { unsubscribe: () => {
this.listeners = this.listeners.filter((l: any) => l !== fn); this.listeners = this.listeners.filter((l: any) => l !== fn);
}, },
}; };

View File

@ -0,0 +1,9 @@
export class ActiveRoute {
static get path() {
return window.location.hash.slice(1);
}
static get param() {
return window.location.hash.split('/');
}
}

57
src/core/routes/router.ts Normal file
View File

@ -0,0 +1,57 @@
import { DashboardPage } from 'pages/DashboardPage';
import { ExcelPage } from 'pages/ExcelPage';
import { $, Dom } from 'core/dom';
import { ActiveRoute } from './ActiveRoute';
export class Router {
private $placeholder: Dom;
private routes: {
dashboard: DashboardPage
excel: ExcelPage
};
private page: DashboardPage | ExcelPage;
constructor(selector: string, routes: any) {
if (!selector) throw new Error('Selector not provided');
this.$placeholder = $(selector);
this.routes = routes;
this.page = null;
this.changePageHandler = this.changePageHandler.bind(this);
this.init();
}
init() {
window.addEventListener('hashchange', this.changePageHandler);
this.changePageHandler();
}
changePageHandler() {
this.$placeholder.clear();
this.page?.destroy();
let Page;
switch (true) {
case ActiveRoute.path.includes('excel'):
Page = this.routes.excel;
break;
case ActiveRoute.path.includes('dashboard'):
default:
Page = this.routes.dashboard;
break;
}
// @ts-ignore
this.page = new Page(ActiveRoute.param);
this.$placeholder.append(this.page.getRoot());
this.page.afterRender();
}
destroy() {
window.removeEventListener('hashchange', this.changePageHandler);
}
}

View File

@ -5,9 +5,13 @@ export function capitalize(string: string): string {
} }
export function storage(key: string, data: any = null): any { export function storage(key: string, data: any = null): any {
if (!data) return JSON.parse(localStorage.getItem(key)); if (!data) {
console.log('RETURN KEY', key, data);
return JSON.parse(localStorage.getItem(key));
}
localStorage.setItem(key, JSON.stringify(data)); localStorage.setItem(key, JSON.stringify(data));
console.log('SET ITEM', key, data);
return true; return true;
} }

View File

@ -1,25 +1,9 @@
import { Excel } from './components/excel/Excel';
import { Formula } from './components/formula/Formula';
import { Header } from './components/header/Header';
import { Table } from './components/table/Table';
import { Toolbar } from './components/toolbar/Toolbar';
import './styles/index.scss'; import './styles/index.scss';
import { Store } from './core/createStore'; import { Router } from 'core/routes/router';
import { debounce, storage } from './core/utils'; import { DashboardPage } from 'pages/DashboardPage';
import { rootReducer } from './redux/rootReducer'; import { ExcelPage } from 'pages/ExcelPage';
import { StateType } from './redux/types';
const store = new Store(rootReducer); new Router('#app', {
dashboard: DashboardPage,
const stateListener = debounce((state: StateType) => { excel: ExcelPage,
storage('excel-state', state);
}, 300);
store.subscribe(stateListener);
const excel = new Excel('#app', {
components: [Header, Toolbar, Formula, Table],
store,
}); });
excel.render();

View File

@ -0,0 +1,25 @@
import { Page } from 'core/Page';
import { $ } from 'core/dom';
import { createRecordsTable } from 'pages/dashboard.functions';
export class DashboardPage extends Page {
getRoot() {
const id = Date.now().toString();
return $.create('div', 'db').html(
`
<div class="db__header">
<h1>Excel Панель управыления</h1>
</div>
<div class="db__new">
<div class="db__view">
<a href="#excel/${id}" class="db__create">Новая таблица</a>
</div>
</div>
<div class="db__table db__view">
${createRecordsTable()}
</div>
`,
);
}
}

46
src/pages/ExcelPage.ts Normal file
View File

@ -0,0 +1,46 @@
import { Excel } from 'components/excel/Excel';
import { Formula } from 'components/formula/Formula';
import { Header } from 'components/header/Header';
import { Table } from 'components/table/Table';
import { Toolbar } from 'components/toolbar/Toolbar';
import { Store } from 'core/createStore';
import { Page } from 'core/Page';
import { debounce, storage } from 'core/utils';
import { rootReducer } from 'redux/rootReducer';
import { StateType } from 'redux/types';
import { getNormalizeInitialState } from '../constants';
export function storageName(param: string) {
return `excel:${param}`;
}
export class ExcelPage extends Page {
private excel: Excel;
getRoot() {
const params = this.params[1] ? this.params[1] : Date.now().toString();
const normalizeState = storage(storageName(params)) || getNormalizeInitialState(params);
const store = new Store(rootReducer, normalizeState);
const stateListener = debounce((state: StateType) => {
storage(storageName(params), state);
}, 300);
store.subscribe(stateListener);
this.excel = new Excel({
components: [Header, Toolbar, Formula, Table],
store,
});
return this.excel.getRoot();
}
afterRender() {
this.excel.init();
}
destroy() {
this.excel.destroy();
}
}

View File

@ -0,0 +1,46 @@
import { storage } from 'core/utils';
function toHtml(key: string) {
const date = +key.split(':')[1];
const params = storage(key);
const link = `#excel/${date}`;
return `
<li class="db__record">
<a href=${link}>${params.title}</a>
<strong>${new Date(date).toLocaleDateString()}</strong>
</li>
`;
}
export function getAllRecords() {
}
export function createRecordsTable() {
const keys = getAllKeys();
if (!keys.length) return '<p>Пока не создали ни одной таблицы</p>';
return `
<div class="db__list-header">
<span>Название</span>
<span>Дата открытия</span>
</div>
<ul class="db__list">
${keys.map((key) => toHtml(key)).join('')}
</ul>
`;
}
function getAllKeys() {
const keys = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!key.includes('excel')) continue;
keys.push(key);
}
return keys;
}

View File

@ -1,4 +1,4 @@
import { CHANGE_TEXT, CHANGE_STYLES, TABLE_RESIZE, APPLY_STYLES, CHANGE_TITLE } from './constants'; import { CHANGE_TEXT, CHANGE_STYLES, TABLE_RESIZE, APPLY_STYLES, CHANGE_TITLE, DELETE_TABLE } from './constants';
export function tableResize(resizeData: any) { export function tableResize(resizeData: any) {
return { return {
@ -34,3 +34,10 @@ export function changeTitle(data: string) {
data, data,
}; };
} }
export function deleteTable(data: string) {
return {
type: DELETE_TABLE,
data,
};
}

View File

@ -3,3 +3,4 @@ export const CHANGE_TEXT = 'CHANGE_TEXT';
export const APPLY_STYLES = 'APPLY_STYLES'; export const APPLY_STYLES = 'APPLY_STYLES';
export const CHANGE_STYLES = 'CURRENT_STYLES'; export const CHANGE_STYLES = 'CURRENT_STYLES';
export const CHANGE_TITLE = 'CHANGE_TITLE'; export const CHANGE_TITLE = 'CHANGE_TITLE';
export const DELETE_TABLE = 'DELETE_TABLE';

View File

@ -1,4 +1,5 @@
import { CHANGE_TEXT, CHANGE_STYLES, TABLE_RESIZE, APPLY_STYLES, CHANGE_TITLE } from './constants'; import { storageName } from 'pages/ExcelPage';
import { CHANGE_TEXT, CHANGE_STYLES, TABLE_RESIZE, APPLY_STYLES, CHANGE_TITLE, DELETE_TABLE } from './constants';
import { ActionType, StateType } from './types'; import { ActionType, StateType } from './types';
export function rootReducer(state: StateType, action: ActionType) { export function rootReducer(state: StateType, action: ActionType) {
@ -44,6 +45,15 @@ export function rootReducer(state: StateType, action: ActionType) {
return { ...state, title: action.data }; return { ...state, title: action.data };
} }
case DELETE_TABLE: {
const name = storageName(action.data);
console.log('delete table item', name);
localStorage.removeItem(name);
window.location.hash = '';
return { ...state };
}
default: return state; default: return state;
} }
} }