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

View File

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

View File

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

View File

@ -1,14 +1,16 @@
import { $, Dom } from '../../core/dom';
import { ExcelComponent } from '../../core/ExcelComponent';
import * as actions from '../../redux/actions';
import { $, Dom } from 'core/dom';
import { ExcelStateComponent } from 'core/ExcelStateComponent';
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';
constructor($root: Dom, options: any) {
super($root, {
name: 'Header',
listeners: ['input'],
listeners: ['input', 'click'],
subscribe: ['title'],
...options,
});
}
@ -20,10 +22,10 @@ export class Header extends ExcelComponent {
<input type="text" class="input" value="${title}">
<div>
<div class="button">
<div class="button" data-button="delete-table">
<i class="material-icons">delete</i>
</div>
<div class="button">
<div class="button" data-button="exit">
<i class="material-icons">exit_to_app</i>
</div>
`;
@ -34,4 +36,31 @@ export class Header extends ExcelComponent {
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 { $, 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 { selectHandler } from './handlers/table.select.handler';
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';
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 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 { TableSelection } from '../TableSelection';

View File

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

View File

@ -1,8 +1,8 @@
import { initialStyleState } from '../../constants';
import { $, Dom } from '../../core/dom';
import { OptionsType } from '../../core/ExcelComponent';
import { ExcelStateComponent } from '../../core/ExcelStateComponent';
import { $, Dom } from 'core/dom';
import { ExcelStateComponent } from 'core/ExcelStateComponent';
import { OptionsType } from 'core/ExcelComponent';
import { createToolbar } from './toolbar.template';
import { initialStyleState } from '../../constants';
export class Toolbar extends ExcelStateComponent {
static className = 'excel__toolbar';
@ -18,7 +18,6 @@ export class Toolbar extends ExcelStateComponent {
prepare() {
const currentToolbarState = this.toolbarState;
console.log('Current state', currentToolbarState);
this.initState(currentToolbarState);
}
@ -44,7 +43,10 @@ export class Toolbar extends ExcelStateComponent {
onClick(event: MouseEvent) {
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];
this.$emit('toolbar:applyStyle', value);

View File

@ -8,13 +8,16 @@ export const initialStyleState: Partial<CSSStyleDeclaration> = {
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'] },
};
export function getNormalizeInitialState(params: string): StateType {
return {
colState: {},
rowState: {},
dataState: {},
currentText: '',
stylesState: {},
title: 'New excel table',
...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 { Dom } from './dom';
import { DomListener } from './DomListener';
@ -73,6 +73,5 @@ export abstract class ExcelComponent extends DomListener implements ExcelCompone
destroy() {
this.removeDOMListeners();
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 { isEqual } from './utils';

View File

@ -1,11 +1,10 @@
import { initialState } from '../constants';
import { ActionType, ReducerType, StateType, SubscribeType } from '../redux/types';
export class Store {
state: StateType;
listeners: ((args?: any) => void)[];
constructor(private reducer: ReducerType) {
constructor(private reducer: ReducerType, initialState: StateType) {
this.state = reducer({ ...initialState }, { type: '__INIT__' });
this.listeners = [];
}
@ -13,7 +12,7 @@ export class Store {
subscribe(fn: (state?: StateType) => void): SubscribeType {
this.listeners.push(fn);
return {
unsubscribe() {
unsubscribe: () => {
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 {
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));
console.log('SET ITEM', key, data);
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 { Store } from './core/createStore';
import { debounce, storage } from './core/utils';
import { rootReducer } from './redux/rootReducer';
import { StateType } from './redux/types';
import { Router } from 'core/routes/router';
import { DashboardPage } from 'pages/DashboardPage';
import { ExcelPage } from 'pages/ExcelPage';
const store = new Store(rootReducer);
const stateListener = debounce((state: StateType) => {
storage('excel-state', state);
}, 300);
store.subscribe(stateListener);
const excel = new Excel('#app', {
components: [Header, Toolbar, Formula, Table],
store,
new Router('#app', {
dashboard: DashboardPage,
excel: ExcelPage,
});
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) {
return {
@ -34,3 +34,10 @@ export function changeTitle(data: string) {
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 CHANGE_STYLES = 'CURRENT_STYLES';
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';
export function rootReducer(state: StateType, action: ActionType) {
@ -44,6 +45,15 @@ export function rootReducer(state: StateType, action: ActionType) {
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;
}
}