add loader, local storage client data

This commit is contained in:
Sergey Krylov 2022-07-10 00:08:26 +05:00
parent 75af22029b
commit b897f6a97d
17 changed files with 202 additions and 100 deletions

5
src/components/Loader.ts Normal file
View File

@ -0,0 +1,5 @@
import { $, Dom } from 'core/dom';
export function Loader(): Dom {
return $.create('div', 'loader').html('<div class="loader"><div class="lds-circle"><div></div></div></div>');
}

View File

@ -1,6 +1,6 @@
import { $, Dom } from 'core/dom'; import { $, Dom } from 'core/dom';
import { ExcelComponent } from 'core/ExcelComponent'; import { ExcelComponent } from 'core/ExcelComponent';
import { parse } from 'core/parse'; import { parse } from 'core/utils';
import { changeCurrentStyles } from 'redux/actions'; import { changeCurrentStyles } from 'redux/actions';
import * as actions from 'redux/actions'; import * as actions from 'redux/actions';
import { initialStyleState } from '../../constants'; import { initialStyleState } from '../../constants';

View File

@ -9,8 +9,8 @@ export class Toolbar extends ExcelStateComponent {
constructor($root: Dom, options: OptionsType) { constructor($root: Dom, options: OptionsType) {
super($root, { super($root, {
name: 'Toolbar',
listeners: ['click'], listeners: ['click'],
name: 'Toolbar',
subscribe: ['currentStyles'], subscribe: ['currentStyles'],
...options, ...options,
}); });
@ -25,7 +25,7 @@ export class Toolbar extends ExcelStateComponent {
get toolbarState() { get toolbarState() {
return { return {
...initialStyleState, ...initialStyleState,
...this.store.getState().stylesState['0:0'], ...this.store?.getState()?.stylesState?.['0:0'],
}; };
} }

27
src/core/Clients.ts Normal file
View File

@ -0,0 +1,27 @@
import { storage } from 'core/utils';
import { storageName } from 'pages/ExcelPage';
import { StateType } from 'redux/types';
import { getNormalizeInitialState } from '../constants';
export class LocalStorageClient {
private name: string;
constructor(name: string) {
this.name = storageName(name);
}
save(state: StateType): Promise<void> {
storage(this.name, state);
return Promise.resolve();
}
get() {
const data = storage(this.name) || getNormalizeInitialState(this.name);
return new Promise(resolve => {
setTimeout(() => {
resolve(data);
}, 1500);
});
}
}

View File

@ -19,10 +19,10 @@ export type OptionsType = {
}; };
export abstract class ExcelComponent extends DomListener implements ExcelComponentClass { export abstract class ExcelComponent extends DomListener implements ExcelComponentClass {
name: string; private name: string | undefined;
emitter: Emitter; private emitter: Emitter | undefined;
store: Store; private store: Store | undefined;
subscribe: string[]; private subscribe: string[] | undefined;
private unsubscribers: ((args?: any) => any)[]; private unsubscribers: ((args?: any) => any)[];
constructor($root: Dom, options?: OptionsType) { constructor($root: Dom, options?: OptionsType) {
@ -45,16 +45,16 @@ export abstract class ExcelComponent extends DomListener implements ExcelCompone
} }
$emit(event: string, args?: any): void { $emit(event: string, args?: any): void {
this.emitter.emit(event, args); this.emitter?.emit(event, args);
} }
$on(event: string, callback: (args: any) => any) { $on(event: string, callback: (args: any) => any) {
const unsub = this.emitter.subscribe(event, callback); const unsub = this.emitter?.subscribe(event, callback);
this.unsubscribers.push(unsub); unsub && this.unsubscribers.push(unsub);
} }
$dispatch(action: ActionType) { $dispatch(action: ActionType) {
this.store.dispatch(action); this.store?.dispatch(action);
} }
storeChanged(args?: any) { storeChanged(args?: any) {

View File

@ -0,0 +1,16 @@
import { debounce } from 'core/utils';
export class StateProcessor {
constructor(client, dalay = 300) {
this.client = client;
this.listen = debounce(this.listen.bind(this), dalay);
}
listen(state) {
this.client.save(state);
}
get() {
return this.client.get();
}
}

View File

@ -9,7 +9,7 @@ export interface DomClass {
} }
export class Dom implements DomClass { export class Dom implements DomClass {
$el: HTMLElement; $el: HTMLElement | null;
constructor(selector: SelectorType) { constructor(selector: SelectorType) {
this.$el = typeof selector === 'string' this.$el = typeof selector === 'string'
@ -31,8 +31,8 @@ export class Dom implements DomClass {
} }
get text() { get text() {
if (this.$el.closest('input')) return (this.$el as HTMLInputElement).value; if (this.$el?.closest('input')) return (this.$el as HTMLInputElement).value;
return this.$el.textContent; return this.$el?.textContent;
} }
clear() { clear() {

View File

@ -1,12 +0,0 @@
export function parse(value: string) {
if (value.startsWith('=')) {
try {
// eslint-disable-next-line no-eval
return eval(value.slice(1));
} catch (e) {
return value;
}
}
return value;
}

View File

@ -1,17 +1,19 @@
import { $, Dom, SelectorType } from 'core/dom';
import { ActiveRoute } from 'core/routes/ActiveRoute';
import { Loader } from 'components/Loader';
import { DashboardPage } from 'pages/DashboardPage'; import { DashboardPage } from 'pages/DashboardPage';
import { ExcelPage } from 'pages/ExcelPage'; import { ExcelPage } from 'pages/ExcelPage';
import { $, Dom, SelectorType } from 'core/dom';
import { ActiveRoute } from './ActiveRoute';
type RoutesType = { type RoutesType = {
dashboard: DashboardPage dashboard: typeof DashboardPage
excel: ExcelPage excel: typeof ExcelPage
}; };
export class Router { export class Router {
private $placeholder: Dom; private $placeholder: Dom;
private routes: RoutesType; private routes: RoutesType;
private page: DashboardPage | ExcelPage | null; private page: DashboardPage | ExcelPage | null;
private loader: Dom;
constructor(selector: SelectorType, routes: RoutesType) { constructor(selector: SelectorType, routes: RoutesType) {
if (!selector) throw new Error('Selector not provided'); if (!selector) throw new Error('Selector not provided');
@ -19,6 +21,7 @@ export class Router {
this.$placeholder = $(selector); this.$placeholder = $(selector);
this.routes = routes; this.routes = routes;
this.page = null; this.page = null;
this.loader = Loader();
this.changePageHandler = this.changePageHandler.bind(this); this.changePageHandler = this.changePageHandler.bind(this);
@ -26,12 +29,13 @@ export class Router {
} }
init() { init() {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
window.addEventListener('hashchange', this.changePageHandler); window.addEventListener('hashchange', this.changePageHandler);
this.changePageHandler(); this.changePageHandler();
} }
changePageHandler() { async changePageHandler() {
this.$placeholder.clear(); this.$placeholder.clear().append(this.loader);
this.page?.destroy(); this.page?.destroy();
let Page; let Page;
@ -49,11 +53,15 @@ export class Router {
// @ts-ignore // @ts-ignore
this.page = new Page(ActiveRoute.param); this.page = new Page(ActiveRoute.param);
this.$placeholder.append(this.page?.getRoot()); const root = await this.page?.getRoot();
this.$placeholder.clear().append(root);
this.page?.afterRender(); this.page?.afterRender();
} }
destroy() { destroy() {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
window.removeEventListener('hashchange', this.changePageHandler); window.removeEventListener('hashchange', this.changePageHandler);
} }
} }

View File

@ -6,12 +6,11 @@ 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) { if (!data) {
console.log('RETURN KEY', key, data); const localData = localStorage.getItem(key);
return JSON.parse(localStorage.getItem(key)); return localData ? JSON.parse(localData) : false;
} }
localStorage.setItem(key, JSON.stringify(data)); localStorage.setItem(key, JSON.stringify(data));
console.log('SET ITEM', key, data);
return true; return true;
} }
@ -27,7 +26,7 @@ export function isEqual(a: any, b: any) {
export function debounce(fn: (fnArgs?: any) => void, wait: number) { export function debounce(fn: (fnArgs?: any) => void, wait: number) {
let timeout: NodeJS.Timeout; let timeout: NodeJS.Timeout;
return function (...args: any[]) { return function (...args: any) {
const later = () => { const later = () => {
clearTimeout(timeout); clearTimeout(timeout);
fn.apply(this, args); fn.apply(this, args);
@ -37,3 +36,16 @@ export function debounce(fn: (fnArgs?: any) => void, wait: number) {
timeout = setTimeout(later, wait); timeout = setTimeout(later, wait);
}; };
} }
export function parse(value: string) {
if (value.startsWith('=')) {
try {
// eslint-disable-next-line no-eval
return eval(value.slice(1));
} catch (e) {
return value;
}
}
return value;
}

View File

@ -1,8 +1,8 @@
export abstract class Page { export abstract class AbstractPage {
params: any; params: any;
constructor(params: any) { constructor(params: any) {
this.params = params; this.params = params || Date.now().toString();
} }
getRoot() { getRoot() {

View File

@ -1,8 +1,8 @@
import { Page } from 'core/Page';
import { $ } from 'core/dom'; import { $ } from 'core/dom';
import { createRecordsTable } from 'pages/dashboard.functions'; import { storage } from 'core/utils';
import { AbstractPage } from 'pages/AbstractPage';
export class DashboardPage extends Page { export class DashboardPage extends AbstractPage {
getRoot() { getRoot() {
const id = Date.now().toString(); const id = Date.now().toString();
@ -23,3 +23,46 @@ export class DashboardPage extends Page {
); );
} }
} }
function toHtml(key: string) {
const params = +key.split(':')[1];
const state = storage(key);
const link = `#excel/${params}`;
const date = new Date(+state.openDate);
return `
<li class="db__record">
<a href=${link}>${state.title}</a>
<strong>${date.toLocaleDateString()} ${date.toLocaleTimeString()}</strong>
</li>
`;
}
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

@ -3,30 +3,36 @@ import { Formula } from 'components/formula/Formula';
import { Header } from 'components/header/Header'; import { Header } from 'components/header/Header';
import { Table } from 'components/table/Table'; import { Table } from 'components/table/Table';
import { Toolbar } from 'components/toolbar/Toolbar'; import { Toolbar } from 'components/toolbar/Toolbar';
import { LocalStorageClient } from 'core/Clients';
import { StateProcessor } from 'core/StateProcessor';
import { Store } from 'core/store/createStore'; import { Store } from 'core/store/createStore';
import { Page } from 'core/Page'; import { AbstractPage } from 'pages/AbstractPage';
import { debounce, storage } from 'core/utils';
import { rootReducer } from 'redux/rootReducer'; import { rootReducer } from 'redux/rootReducer';
import { StateType } from 'redux/types'; import { SubscribeType } from 'redux/types';
import { getNormalizeInitialState } from '../constants';
export function storageName(param: string) { export function storageName(param: string) {
return `excel:${param}`; return `excel:${param}`;
} }
export class ExcelPage extends Page { export class ExcelPage extends AbstractPage {
private excel: Excel; private excel: Excel;
private storeSub: SubscribeType | null;
private processor: StateProcessor;
getRoot() { constructor(props: any) {
const params = this.params[1] ? this.params[1] : Date.now().toString(); super(props);
const normalizeState = storage(storageName(params)) || getNormalizeInitialState(params);
this.storeSub = null;
this.processor = new StateProcessor(
new LocalStorageClient(this.params[1]),
);
}
async getRoot() {
const normalizeState = await this.processor.get();
const store = new Store(rootReducer, normalizeState); const store = new Store(rootReducer, normalizeState);
const stateListener = debounce((state: StateType) => { this.storeSub = store.subscribe(this.processor.listen);
storage(storageName(params), state);
}, 300);
store.subscribe(stateListener);
this.excel = new Excel({ this.excel = new Excel({
components: [Header, Toolbar, Formula, Table], components: [Header, Toolbar, Formula, Table],
@ -42,5 +48,6 @@ export class ExcelPage extends Page {
destroy() { destroy() {
this.excel.destroy(); this.excel.destroy();
this.storeSub?.unsubscribe();
} }
} }

View File

@ -1,44 +0,0 @@
import { storage } from 'core/utils';
function toHtml(key: string) {
const params = +key.split(':')[1];
const state = storage(key);
const link = `#excel/${params}`;
const date = new Date(+state.openDate);
return `
<li class="db__record">
<a href=${link}>${state.title}</a>
<strong>${date.toLocaleDateString()} ${date.toLocaleTimeString()}</strong>
</li>
`;
}
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

@ -0,0 +1,36 @@
.loader {
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.lds-circle {
display: inline-block;
transform: translateZ(1px);
}
.lds-circle > div {
display: inline-block;
width: 64px;
height: 64px;
margin: 8px;
border-radius: 50%;
background: #dfc;
animation: lds-circle 2.4s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
@keyframes lds-circle {
0%, 100% {
animation-timing-function: cubic-bezier(0.5, 0, 1, 0.5);
}
0% {
transform: rotateY(0deg);
}
50% {
transform: rotateY(1800deg);
animation-timing-function: cubic-bezier(0, 0.5, 0.5, 1);
}
100% {
transform: rotateY(3600deg);
}
}

View File

@ -48,6 +48,9 @@
border-left: 0; border-left: 0;
white-space: nowrap; white-space: nowrap;
outline: none; outline: none;
&:hover:not(.selected) {
cursor: cell;
}
&.selected { &.selected {
border: none; border: none;
outline: 2px solid $primary-color; outline: 2px solid $primary-color;

View File

@ -6,6 +6,7 @@
@import './components/formula'; @import './components/formula';
@import './components/table'; @import './components/table';
@import './components/dashboard'; @import './components/dashboard';
@import './components/loader';
* { * {
margin: 0; margin: 0;