diff --git a/src/components/Loader.ts b/src/components/Loader.ts
new file mode 100644
index 0000000..fef58f0
--- /dev/null
+++ b/src/components/Loader.ts
@@ -0,0 +1,5 @@
+import { $, Dom } from 'core/dom';
+
+export function Loader(): Dom {
+ return $.create('div', 'loader').html('
');
+}
diff --git a/src/components/table/Table.ts b/src/components/table/Table.ts
index 48074d4..af18097 100644
--- a/src/components/table/Table.ts
+++ b/src/components/table/Table.ts
@@ -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';
diff --git a/src/components/toolbar/Toolbar.ts b/src/components/toolbar/Toolbar.ts
index a009771..1126053 100644
--- a/src/components/toolbar/Toolbar.ts
+++ b/src/components/toolbar/Toolbar.ts
@@ -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'],
};
}
diff --git a/src/core/Clients.ts b/src/core/Clients.ts
new file mode 100644
index 0000000..0ccb6c5
--- /dev/null
+++ b/src/core/Clients.ts
@@ -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 {
+ storage(this.name, state);
+ return Promise.resolve();
+ }
+
+ get() {
+ const data = storage(this.name) || getNormalizeInitialState(this.name);
+
+ return new Promise(resolve => {
+ setTimeout(() => {
+ resolve(data);
+ }, 1500);
+ });
+ }
+}
diff --git a/src/core/ExcelComponent.ts b/src/core/ExcelComponent.ts
index 8f9deac..9934005 100644
--- a/src/core/ExcelComponent.ts
+++ b/src/core/ExcelComponent.ts
@@ -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) {
diff --git a/src/core/StateProcessor.ts b/src/core/StateProcessor.ts
new file mode 100644
index 0000000..64a859d
--- /dev/null
+++ b/src/core/StateProcessor.ts
@@ -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();
+ }
+}
diff --git a/src/core/dom.ts b/src/core/dom.ts
index 9eeddae..224825f 100644
--- a/src/core/dom.ts
+++ b/src/core/dom.ts
@@ -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() {
diff --git a/src/core/parse.ts b/src/core/parse.ts
deleted file mode 100644
index 3b95b08..0000000
--- a/src/core/parse.ts
+++ /dev/null
@@ -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;
-}
diff --git a/src/core/routes/router.ts b/src/core/routes/router.ts
index 8260d59..641bdbb 100644
--- a/src/core/routes/router.ts
+++ b/src/core/routes/router.ts
@@ -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);
}
}
diff --git a/src/core/utils.ts b/src/core/utils.ts
index 402e7bc..e9aefd6 100644
--- a/src/core/utils.ts
+++ b/src/core/utils.ts
@@ -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;
+}
diff --git a/src/core/Page.ts b/src/pages/AbstractPage.ts
similarity index 56%
rename from src/core/Page.ts
rename to src/pages/AbstractPage.ts
index 2ad73fd..8565c75 100644
--- a/src/core/Page.ts
+++ b/src/pages/AbstractPage.ts
@@ -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() {
diff --git a/src/pages/DashboardPage.ts b/src/pages/DashboardPage.ts
index 847e1a9..7ee81e3 100644
--- a/src/pages/DashboardPage.ts
+++ b/src/pages/DashboardPage.ts
@@ -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 `
+
+ ${state.title}
+ ${date.toLocaleDateString()} ${date.toLocaleTimeString()}
+
+ `;
+}
+
+export function createRecordsTable() {
+ const keys = getAllKeys();
+ if (!keys.length) return 'Пока не создали ни одной таблицы
';
+
+ return `
+
+
+
+ ${keys.map((key) => toHtml(key)).join('')}
+
+ `;
+}
+
+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;
+}
diff --git a/src/pages/ExcelPage.ts b/src/pages/ExcelPage.ts
index 838d098..07a34ae 100644
--- a/src/pages/ExcelPage.ts
+++ b/src/pages/ExcelPage.ts
@@ -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();
}
}
diff --git a/src/pages/dashboard.functions.ts b/src/pages/dashboard.functions.ts
deleted file mode 100644
index 3e00e21..0000000
--- a/src/pages/dashboard.functions.ts
+++ /dev/null
@@ -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 `
-
- ${state.title}
- ${date.toLocaleDateString()} ${date.toLocaleTimeString()}
-
- `;
-}
-
-export function createRecordsTable() {
- const keys = getAllKeys();
- if (!keys.length) return 'Пока не создали ни одной таблицы
';
-
- return `
-
-
-
- ${keys.map((key) => toHtml(key)).join('')}
-
- `;
-}
-
-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;
-}
diff --git a/src/styles/components/loader.scss b/src/styles/components/loader.scss
new file mode 100644
index 0000000..03b1fe7
--- /dev/null
+++ b/src/styles/components/loader.scss
@@ -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);
+ }
+}
diff --git a/src/styles/components/table.scss b/src/styles/components/table.scss
index dce13ee..1c35c7e 100644
--- a/src/styles/components/table.scss
+++ b/src/styles/components/table.scss
@@ -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;
diff --git a/src/styles/index.scss b/src/styles/index.scss
index b65a60c..f6a19c9 100644
--- a/src/styles/index.scss
+++ b/src/styles/index.scss
@@ -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;
-}
\ No newline at end of file
+}