diff --git a/docs/developments.md b/docs/developments.md index 7f20e19..5eea555 100644 --- a/docs/developments.md +++ b/docs/developments.md @@ -49,3 +49,11 @@ - Реализовано добавление стилей для ячеек в соответствии с тулбаром - Реализовано изменение заголовка таблицы - Реализован [debounce](/src/helpers/debounce.ts) для оптимизации + +#### Routing + +- Реализован [роутинг](../structures/routing.dio) приложения: + - Создание новых таблиц с добавлением query-params + - Переход на существующие таблицы + - Выход в главное меню + - Удаление таблицы diff --git a/src/components/dashboard/dashboard.functions.ts b/src/components/dashboard/dashboard.functions.ts new file mode 100644 index 0000000..85f7a86 --- /dev/null +++ b/src/components/dashboard/dashboard.functions.ts @@ -0,0 +1,52 @@ +import localStorageFn from '@src/helpers/localStorage'; +import { IRootState } from '@src/store/store.types'; + +export const toHTML = (key: string) => { + const model = localStorageFn(key); + const id = key.split(':')[1]; + + if (!model) { + return ''; + } + + return /* html */ ` +
  • + ${model.title} + + ${new Date(model.dateTable).toLocaleDateString()} ${new Date(model.dateTable).toLocaleTimeString()} + +
  • + `; +}; + +export const getAllKeys = () => { + const keys = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key?.includes('excel')) { + keys.push(key); + } + } + return keys; +}; + +export const createTable = () => { + const keys = getAllKeys(); + + if (!keys.length) { + return ` +

    Таблицы отсутствуют

    + `; + } + + return ` +
    + Название + Дата открытия +
    + + + `; +}; diff --git a/src/components/excel/Excel.ts b/src/components/excel/Excel.ts index c1ec99a..478c54c 100644 --- a/src/components/excel/Excel.ts +++ b/src/components/excel/Excel.ts @@ -3,6 +3,7 @@ import { IExcelComponent } from '@src/core/excelComponent/ExcelComponent'; import Observer from '@src/core/observer/Observer'; import StoreSubscriber from '@src/core/storeSubscriber/StoreSubscriber'; import { TActions } from '@src/store/action.types'; +import { updateDate } from '@src/store/actions'; import { IReturnCreateStore, IRootState } from '@src/store/store.types'; interface IExcelOptions { @@ -11,8 +12,6 @@ interface IExcelOptions { } class Excel { - public $el; - public components: (new (...arg: any[]) => T)[]; public objectComponents: T[]; @@ -23,8 +22,7 @@ class Excel { public subscriber: StoreSubscriber; - constructor(selector: string, options: IExcelOptions) { - this.$el = $(selector); + constructor(options: IExcelOptions) { this.components = options.components || []; this.objectComponents = []; this.observer = new Observer(); @@ -51,8 +49,8 @@ class Excel { return $root; } - render() { - this.$el?.append(this.getRoot()); + init() { + this.store.dispatch(updateDate()); this.subscriber.subscribeComponents(this.objectComponents); this.objectComponents.forEach((component) => { component.init(); diff --git a/src/components/formula/Formula.ts b/src/components/formula/Formula.ts index 3b77e26..a3f9243 100644 --- a/src/components/formula/Formula.ts +++ b/src/components/formula/Formula.ts @@ -35,7 +35,6 @@ class Formula extends ExcelComponent implements IFormula { this.$subscribe('table:select', ($cell: Dom) => { const id = $cell.getId(); const dataValue = $cell.attr('data-value'); - console.log('dataValue', dataValue); if (id) { this.$dispatch(actions.changeTextActionCreator({ text: $cell.text(), id })); } diff --git a/src/components/header/Header.ts b/src/components/header/Header.ts index a94231b..d1baac2 100644 --- a/src/components/header/Header.ts +++ b/src/components/header/Header.ts @@ -1,10 +1,11 @@ -import { defaultTitle } from '@src/consts/consts'; +import { DEFAULT_TITLE } from '@src/consts/consts'; import $, { Dom } from '@src/core/dom/dom'; import ExcelComponent from '@src/core/excelComponent/ExcelComponent'; +import ActiveRoute from '@src/core/routes/ActiveRoute'; import debounce from '@src/helpers/debounce'; import { changeTitle } from '@src/store/actions'; import { IComponentOptions } from '@src/types/components'; -import { IInputEvent } from '@src/types/general'; +import { IButtonEvent, IInputEvent } from '@src/types/general'; interface IHeader {} @@ -14,7 +15,7 @@ class Header extends ExcelComponent implements IHeader { constructor($root: Dom, options: IComponentOptions) { super($root, { name: 'Header', - listeners: ['input'], + listeners: ['input', 'click'], ...options, }); } @@ -24,17 +25,17 @@ class Header extends ExcelComponent implements IHeader { } toHTML(): string { - const { title = defaultTitle } = this.store.getState(); + const { title = DEFAULT_TITLE } = this.store.getState(); return `
    - -
    @@ -45,6 +46,24 @@ class Header extends ExcelComponent implements IHeader { const $target = $(event.target); this.$dispatch(changeTitle($target.text())); } + + onClick(event: IButtonEvent) { + const $target = $(event.target); + + if ($target.data?.button === 'remove') { + // eslint-disable-next-line + const decision = confirm('Вы действительно хотите удалить таблицу?'); + + if (decision) { + localStorage.removeItem(`excel:${ActiveRoute.param}`); + ActiveRoute.navigate(''); + } + } + + if ($target.data?.button === 'exit') { + ActiveRoute.navigate(''); + } + } } export default Header; diff --git a/src/consts/consts.ts b/src/consts/consts.ts index 9900710..a5d07cf 100644 --- a/src/consts/consts.ts +++ b/src/consts/consts.ts @@ -7,4 +7,4 @@ export const initialToolbarState: IToolbarState = { fontStyle: 'normal', }; -export const defaultTitle = 'Новая таблица'; +export const DEFAULT_TITLE = 'Новая таблица'; diff --git a/src/core/dom/dom.ts b/src/core/dom/dom.ts index 66db3e8..c9e957e 100644 --- a/src/core/dom/dom.ts +++ b/src/core/dom/dom.ts @@ -239,9 +239,7 @@ export class Dom implements IDom { attr(name: string, value: string): this; attr(name: string): string | undefined | null; attr(name: string, value?: string): string | this | undefined | null { - console.log('value', value); - if (value) { - console.log('name', name); + if (typeof value === 'string') { this.$el?.setAttribute(name, value); return this; } diff --git a/src/core/routes/ActiveRoute.ts b/src/core/routes/ActiveRoute.ts new file mode 100644 index 0000000..ef1953c --- /dev/null +++ b/src/core/routes/ActiveRoute.ts @@ -0,0 +1,29 @@ +class ActiveRoute { + /** + * Геттер получения текущего адреса + * + * @static + * @readonly + * @type {string} + */ + static get path() { + return window.location.hash.slice(1); + } + + /** + * Геттер получения параметра таблицы + * + * @static + * @readonly + * @type {string} + */ + static get param() { + return ActiveRoute.path.split('/')[1]; + } + + static navigate(path: string) { + window.location.hash = path; + } +} + +export default ActiveRoute; diff --git a/src/core/routes/Page.ts b/src/core/routes/Page.ts new file mode 100644 index 0000000..16f73be --- /dev/null +++ b/src/core/routes/Page.ts @@ -0,0 +1,30 @@ +import { Dom } from '../dom/dom'; + +interface IPage { + getRoot(): Dom; + afterRender(): void; + destroy(): void; +} + +class Page implements IPage { + protected params: string | undefined; + + constructor(params?: string) { + this.params = params; + } + + /** + * Метод выбрасывает ошибку, если не реализован в классах наследников + * + * @returns {Element} + */ + getRoot(): Dom { + throw new Error('error page'); + } + + afterRender(): void {} + + destroy(): void {} +} + +export default Page; diff --git a/src/core/routes/Router.ts b/src/core/routes/Router.ts new file mode 100644 index 0000000..39417aa --- /dev/null +++ b/src/core/routes/Router.ts @@ -0,0 +1,72 @@ +import DashboardPage from '@src/pages/DashboardPage'; +import ExcelPage from '@src/pages/ExcelPage'; +import $, { Dom } from '../dom/dom'; +import ActiveRoute from './ActiveRoute'; +import Page from './Page'; + +interface IRoutesParams { + dashboard: new (...arg: any[]) => DashboardPage; + excel: new (...arg: any[]) => ExcelPage; +} + +interface IRouter { + /** + * Инициализация роутинга + * Добавление слушателя события на изменения hash + */ + init(): void; + + /** + * Метод рендеринга страницы (компонента) + */ + changePageHandler(): void; + /** + * Метод при размонтировании + * Удаляет слушатели событий + */ + destroy(): void; +} + +class Router implements IRouter { + private $placeholder: Dom; + + private routes: IRoutesParams; + + private page: null | Page; + + constructor(selector: string, routes: IRoutesParams) { + if (!selector) { + throw new Error('Selector is not provided in Router'); + } + + this.$placeholder = $(selector); + this.routes = routes; + this.page = null; + + this.changePageHandler = this.changePageHandler.bind(this); + + this.init(); + } + + init() { + window.addEventListener('hashchange', this.changePageHandler); + this.changePageHandler(); + } + + changePageHandler() { + this.$placeholder.clear(); + if (this.page) { + this.page.destroy(); + } + const PageClass = ActiveRoute.path.includes('excel') ? this.routes.excel : this.routes.dashboard; + this.page = new PageClass(ActiveRoute.param); + this.$placeholder.append(this.page.getRoot()); + this.page.afterRender(); + } + + destroy() { + window.removeEventListener('hashchange', this.changePageHandler); + } +} + +export default Router; diff --git a/src/helpers/localStorage.ts b/src/helpers/localStorage.ts index f2b20b2..934a60d 100644 --- a/src/helpers/localStorage.ts +++ b/src/helpers/localStorage.ts @@ -7,7 +7,7 @@ const localStorageFn = (key: string, data?: unknown) => { if (!data) { const localStorageData = localStorage.getItem(key); - return localStorageData && (JSON.parse(localStorageData) as R); + return localStorageData ? (JSON.parse(localStorageData) as R) : undefined; } localStorage.setItem(key, JSON.stringify(data)); diff --git a/src/index.ts b/src/index.ts index 31da3ac..cfe95d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,28 +1,10 @@ -import Excel from './components/excel/Excel'; -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 { EXCEL_STATE } from './consts/localStorage'; -import debounce from './helpers/debounce'; -import localStorageFn from './helpers/localStorage'; +import Router from './core/routes/Router'; +import DashboardPage from './pages/DashboardPage'; +import ExcelPage from './pages/ExcelPage'; import './scss/index.scss'; -import createStore from './store/createStore'; -import { initialState } from './store/initialState'; -import rootReducer from './store/rootReducer'; -const store = createStore(rootReducer, initialState); - -const stateListener = debounce((state: S) => { - console.info(state); - localStorageFn(EXCEL_STATE, state); -}, 500); - -store.subscribe(stateListener); - -const excel = new Excel
    ('#app', { - components: [Header, Toolbar, Formula, Table], - store, +// eslint-disable-next-line +new Router('#app', { + dashboard: DashboardPage, + excel: ExcelPage, }); - -excel.render(); diff --git a/src/pages/DashboardPage.ts b/src/pages/DashboardPage.ts new file mode 100644 index 0000000..fb2af36 --- /dev/null +++ b/src/pages/DashboardPage.ts @@ -0,0 +1,26 @@ +import { createTable } from '@src/components/dashboard/dashboard.functions'; +import $, { Dom } from '@src/core/dom/dom'; +import Page from '@src/core/routes/Page'; + +class DashboardPage extends Page { + getRoot(): Dom { + const newId = Date.now().toString(); + const nodeElement = $.create('div', 'dashboard').html(/* html */ ` +
    +

    Excel Dashboard

    +
    + + +
    + ${createTable()} +
    + `); + return nodeElement as Dom; + } +} + +export default DashboardPage; diff --git a/src/pages/ExcelPage.ts b/src/pages/ExcelPage.ts new file mode 100644 index 0000000..b9d82ea --- /dev/null +++ b/src/pages/ExcelPage.ts @@ -0,0 +1,55 @@ +import Excel from '@src/components/excel/Excel'; +import Formula from '@src/components/formula/Formula'; +import Header from '@src/components/header/Header'; +import Table from '@src/components/table/Table'; +import Toolbar from '@src/components/toolbar/Toolbar'; +import { Dom } from '@src/core/dom/dom'; +import Page from '@src/core/routes/Page'; +import debounce from '@src/helpers/debounce'; +import localStorageFn from '@src/helpers/localStorage'; +import createStore from '@src/store/createStore'; +import { normalizeInitialState } from '@src/store/initialState'; +import rootReducer from '@src/store/rootReducer'; +import { IRootState } from '@src/store/store.types'; + +type TExcelGeneric = Header | Toolbar | Formula | Table; + +const storageName = (param: string | undefined) => `excel:${param}`; + +class ExcelPage extends Page { + private excel: Excel
    | null; + + constructor(params: string) { + super(params); + this.excel = null; + } + + getRoot(): Dom { + const params = this.params ?? Date.now().toString(); + const localState = localStorageFn(storageName(params)); + const store = createStore(rootReducer, normalizeInitialState(localState)); + + const stateListener = debounce((state: S) => { + localStorageFn(storageName(params), state); + }, 500); + + store.subscribe(stateListener); + + this.excel = new Excel({ + components: [Header, Toolbar, Formula, Table], + store, + }); + + return this.excel.getRoot(); + } + + afterRender(): void { + this.excel?.init(); + } + + destroy(): void { + this.excel?.destroy(); + } +} + +export default ExcelPage; diff --git a/src/store/action.types.ts b/src/store/action.types.ts index 498b9fe..de1ed5d 100644 --- a/src/store/action.types.ts +++ b/src/store/action.types.ts @@ -23,12 +23,17 @@ export interface IChangeTitle { payload: string; } +export interface IUpdateDate { + readonly type: 'UPDATE_DATE'; +} + export type TTableActions = | ITableResizeActionCreator | IChangeTextActionCreator | IApplyStyle | ICurrentStyles | IChangeTitle + | IUpdateDate | { type: '__INIT__'; payload: any }; export type TActions = TTableActions; diff --git a/src/store/actions.ts b/src/store/actions.ts index c189861..5812fbc 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -6,6 +6,7 @@ import { IChangeTitle, ICurrentStyles, ITableResizeActionCreator, + IUpdateDate, } from './action.types'; export const tableResizeActionCreator = (data: ITableResize): ITableResizeActionCreator => ({ @@ -32,3 +33,7 @@ export const changeTitle = (data: string): IChangeTitle => ({ type: 'CHANGE_TITLE', payload: data, }); + +export const updateDate = (): IUpdateDate => ({ + type: 'UPDATE_DATE', +}); diff --git a/src/store/initialState.ts b/src/store/initialState.ts index f3e6a3f..cddf983 100644 --- a/src/store/initialState.ts +++ b/src/store/initialState.ts @@ -1,4 +1,4 @@ -import { initialToolbarState } from '@src/consts/consts'; +import { DEFAULT_TITLE, initialToolbarState } from '@src/consts/consts'; import { EXCEL_STATE } from '@src/consts/localStorage'; import localStorageFn from '@src/helpers/localStorage'; import { IRootState } from './store.types'; @@ -10,11 +10,18 @@ export const defaultState: IRootState = { stylesState: {}, currentText: '', currentStyles: initialToolbarState, - title: 'Новая таблица', + title: DEFAULT_TITLE, + dateTable: new Date().toJSON(), }; const localStorageState = localStorageFn(EXCEL_STATE); -export const initialState = localStorageState || defaultState; +export const initialState = localStorageState || structuredClone(defaultState); -export default initialState; +const normalize = (state: S) => ({ + ...state, + currentStyle: initialToolbarState, + currentText: '', +}); + +export const normalizeInitialState = (state?: S) => (state ? normalize(state) : structuredClone(defaultState)); diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.ts index 21e5b0f..e15c8e8 100644 --- a/src/store/rootReducer.ts +++ b/src/store/rootReducer.ts @@ -47,6 +47,9 @@ const rootReducer = (state: IRootState, action: TActions): IRootState => { case 'CHANGE_TITLE': return { ...state, title: action.payload }; + case 'UPDATE_DATE': + return { ...state, dateTable: new Date().toJSON() }; + default: return state; } diff --git a/src/store/store.types.ts b/src/store/store.types.ts index 68367b3..04c2a2b 100644 --- a/src/store/store.types.ts +++ b/src/store/store.types.ts @@ -18,4 +18,5 @@ export interface IRootState { currentText: string; currentStyles: IToolbarState; title: string; + dateTable: string; } diff --git a/src/types/general.ts b/src/types/general.ts index 2c02d17..8ae9619 100644 --- a/src/types/general.ts +++ b/src/types/general.ts @@ -4,6 +4,10 @@ export interface IDivClickEvent extends MouseEvent { target: HTMLDivElement; } -export interface IInputEvent extends InputEvent { +export interface IInputEvent extends MouseEvent { target: HTMLInputElement; } + +export interface IButtonEvent extends MouseEvent { + target: HTMLButtonElement; +} diff --git a/structures/routing.dio b/structures/routing.dio new file mode 100644 index 0000000..7704a65 --- /dev/null +++ b/structures/routing.dio @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +