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 { $, 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';
|
||||||
|
|||||||
@ -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
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 {
|
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) {
|
||||||
|
|||||||
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 {
|
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() {
|
||||||
|
|||||||
@ -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 { 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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() {
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
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;
|
||||||
|
|||||||
@ -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;
|
||||||
@ -24,4 +25,4 @@ body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user