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 { ExcelComponent } from 'core/ExcelComponent';
import { parse } from 'core/parse';
import { parse } from 'core/utils';
import { changeCurrentStyles } from 'redux/actions';
import * as actions from 'redux/actions';
import { initialStyleState } from '../../constants';

View File

@ -9,8 +9,8 @@ export class Toolbar extends ExcelStateComponent {
constructor($root: Dom, options: OptionsType) {
super($root, {
name: 'Toolbar',
listeners: ['click'],
name: 'Toolbar',
subscribe: ['currentStyles'],
...options,
});
@ -25,7 +25,7 @@ export class Toolbar extends ExcelStateComponent {
get toolbarState() {
return {
...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 {
name: string;
emitter: Emitter;
store: Store;
subscribe: string[];
private name: string | undefined;
private emitter: Emitter | undefined;
private store: Store | undefined;
private subscribe: string[] | undefined;
private unsubscribers: ((args?: any) => any)[];
constructor($root: Dom, options?: OptionsType) {
@ -45,16 +45,16 @@ export abstract class ExcelComponent extends DomListener implements ExcelCompone
}
$emit(event: string, args?: any): void {
this.emitter.emit(event, args);
this.emitter?.emit(event, args);
}
$on(event: string, callback: (args: any) => any) {
const unsub = this.emitter.subscribe(event, callback);
this.unsubscribers.push(unsub);
const unsub = this.emitter?.subscribe(event, callback);
unsub && this.unsubscribers.push(unsub);
}
$dispatch(action: ActionType) {
this.store.dispatch(action);
this.store?.dispatch(action);
}
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 {
$el: HTMLElement;
$el: HTMLElement | null;
constructor(selector: SelectorType) {
this.$el = typeof selector === 'string'
@ -31,8 +31,8 @@ export class Dom implements DomClass {
}
get text() {
if (this.$el.closest('input')) return (this.$el as HTMLInputElement).value;
return this.$el.textContent;
if (this.$el?.closest('input')) return (this.$el as HTMLInputElement).value;
return this.$el?.textContent;
}
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 { ExcelPage } from 'pages/ExcelPage';
import { $, Dom, SelectorType } from 'core/dom';
import { ActiveRoute } from './ActiveRoute';
type RoutesType = {
dashboard: DashboardPage
excel: ExcelPage
dashboard: typeof DashboardPage
excel: typeof ExcelPage
};
export class Router {
private $placeholder: Dom;
private routes: RoutesType;
private page: DashboardPage | ExcelPage | null;
private loader: Dom;
constructor(selector: SelectorType, routes: RoutesType) {
if (!selector) throw new Error('Selector not provided');
@ -19,6 +21,7 @@ export class Router {
this.$placeholder = $(selector);
this.routes = routes;
this.page = null;
this.loader = Loader();
this.changePageHandler = this.changePageHandler.bind(this);
@ -26,12 +29,13 @@ export class Router {
}
init() {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
window.addEventListener('hashchange', this.changePageHandler);
this.changePageHandler();
}
changePageHandler() {
this.$placeholder.clear();
async changePageHandler() {
this.$placeholder.clear().append(this.loader);
this.page?.destroy();
let Page;
@ -49,11 +53,15 @@ export class Router {
// @ts-ignore
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();
}
destroy() {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
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 {
if (!data) {
console.log('RETURN KEY', key, data);
return JSON.parse(localStorage.getItem(key));
const localData = localStorage.getItem(key);
return localData ? JSON.parse(localData) : false;
}
localStorage.setItem(key, JSON.stringify(data));
console.log('SET ITEM', key, data);
return true;
}
@ -27,7 +26,7 @@ export function isEqual(a: any, b: any) {
export function debounce(fn: (fnArgs?: any) => void, wait: number) {
let timeout: NodeJS.Timeout;
return function (...args: any[]) {
return function (...args: any) {
const later = () => {
clearTimeout(timeout);
fn.apply(this, args);
@ -37,3 +36,16 @@ export function debounce(fn: (fnArgs?: any) => void, wait: number) {
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;
constructor(params: any) {
this.params = params;
this.params = params || Date.now().toString();
}
getRoot() {

View File

@ -1,8 +1,8 @@
import { Page } from 'core/Page';
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() {
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 { Table } from 'components/table/Table';
import { Toolbar } from 'components/toolbar/Toolbar';
import { LocalStorageClient } from 'core/Clients';
import { StateProcessor } from 'core/StateProcessor';
import { Store } from 'core/store/createStore';
import { Page } from 'core/Page';
import { debounce, storage } from 'core/utils';
import { AbstractPage } from 'pages/AbstractPage';
import { rootReducer } from 'redux/rootReducer';
import { StateType } from 'redux/types';
import { getNormalizeInitialState } from '../constants';
import { SubscribeType } from 'redux/types';
export function storageName(param: string) {
return `excel:${param}`;
}
export class ExcelPage extends Page {
export class ExcelPage extends AbstractPage {
private excel: Excel;
private storeSub: SubscribeType | null;
private processor: StateProcessor;
getRoot() {
const params = this.params[1] ? this.params[1] : Date.now().toString();
const normalizeState = storage(storageName(params)) || getNormalizeInitialState(params);
constructor(props: any) {
super(props);
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 stateListener = debounce((state: StateType) => {
storage(storageName(params), state);
}, 300);
store.subscribe(stateListener);
this.storeSub = store.subscribe(this.processor.listen);
this.excel = new Excel({
components: [Header, Toolbar, Formula, Table],
@ -42,5 +48,6 @@ export class ExcelPage extends Page {
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;
white-space: nowrap;
outline: none;
&:hover:not(.selected) {
cursor: cell;
}
&.selected {
border: none;
outline: 2px solid $primary-color;

View File

@ -6,6 +6,7 @@
@import './components/formula';
@import './components/table';
@import './components/dashboard';
@import './components/loader';
* {
margin: 0;
@ -24,4 +25,4 @@ body {
height: 100%;
max-width: 100%;
font-size: 0.8rem;
}
}