add loader, local storage client data
This commit is contained in:
parent
75af22029b
commit
b897f6a97d
5
src/components/Loader.ts
Normal file
5
src/components/Loader.ts
Normal 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>');
|
||||
}
|
||||
@ -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';
|
||||
|
||||
@ -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
27
src/core/Clients.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
16
src/core/StateProcessor.ts
Normal file
16
src/core/StateProcessor.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
36
src/styles/components/loader.scss
Normal file
36
src/styles/components/loader.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
@import './components/formula';
|
||||
@import './components/table';
|
||||
@import './components/dashboard';
|
||||
@import './components/loader';
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user