diff --git a/.eslintrc.js b/.eslintrc.js index 75d8cd0..4ff7eb6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -14,9 +14,20 @@ module.exports = { }, rules: { "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unsafe-argument": "off", "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/restrict-template-expressions": "off", + "@typescript-eslint/lines-between-class-members": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/ban-ts-comment": "off", + "arrow-parens": "off", + "class-methods-use-this": "off", + "import/prefer-default-export": "off", "no-console": "off", + "no-plusplus": "off", }, env: { browser: true, diff --git a/src/components/excel/Excel.ts b/src/components/excel/Excel.ts new file mode 100644 index 0000000..e7761f8 --- /dev/null +++ b/src/components/excel/Excel.ts @@ -0,0 +1,38 @@ +import { $, Dom } from '../../core/dom'; + +interface ExcelOptionsType { + components: any[] +} + +export class Excel { + $el: HTMLElement | Dom; + components: any[]; + + constructor(selector: string, options: ExcelOptionsType) { + this.$el = $(selector); + this.components = options.components; + + console.log(`Created new Excel class in ${selector} with options: ${options}`); + } + + getRoot() { + const $root = $.create('div', 'excel'); + + this.components = this.components.map(Component => { + const $el = $.create('div', Component.className); + const component: any = new Component($el); + + $el.html(component.toHTML()); + $root.append($el.$el); + + return component; + }); + + return $root.$el; + } + + render() { + this.$el.append(this.getRoot()); + this.components.forEach(component => component.init()); + } +} diff --git a/src/components/formula/Formula.ts b/src/components/formula/Formula.ts new file mode 100644 index 0000000..eb224a7 --- /dev/null +++ b/src/components/formula/Formula.ts @@ -0,0 +1,28 @@ +import { DomClass } from '../../core/dom'; +import { ExcelComponent } from '../../core/ExcelComponent'; + +export class Formula extends ExcelComponent { + static className = 'excel__formula'; + + constructor($root: DomClass) { + super($root, { + listeners: ['input', 'click'], + name: 'Formula', + }); + } + + toHTML(): string { + return ` +
fx
+
+ `; + } + + onInput(event: Event) { + console.log('Formula on input listeners', event); + } + + onClick() { + + } +} diff --git a/src/components/header/Header.ts b/src/components/header/Header.ts new file mode 100644 index 0000000..e420c30 --- /dev/null +++ b/src/components/header/Header.ts @@ -0,0 +1,19 @@ +import { ExcelComponent } from '../../core/ExcelComponent'; + +export class Header extends ExcelComponent { + static className = 'excel__header'; + + toHTML(): string { + return ` + + +
+
+ delete +
+
+ exit_to_app +
+ `; + } +} diff --git a/src/components/table/Table.ts b/src/components/table/Table.ts new file mode 100644 index 0000000..78a0e17 --- /dev/null +++ b/src/components/table/Table.ts @@ -0,0 +1,10 @@ +import { ExcelComponent } from '../../core/ExcelComponent'; +import { createTable } from './table.template'; + +export class Table extends ExcelComponent { + static className = 'excel__table'; + + toHTML(): string { + return createTable(50, 24); + } +} diff --git a/src/components/table/table.template.ts b/src/components/table/table.template.ts new file mode 100644 index 0000000..153f713 --- /dev/null +++ b/src/components/table/table.template.ts @@ -0,0 +1,45 @@ +const CODES = { + A: 65, + Z: 90, +}; + +function createCell(cellContent = '') { + return ` +
${cellContent}
+ `; +} + +function createCol(columnContent = '') { + return ` +
${columnContent}
+ `; +} + +function createRow(dataContent = '', infoContent = '') { + return ` +
+
${infoContent}
+
${dataContent}
+
+ `; +} + +export function createTable(rowsCount = 10, columnCount = 10) { + const colsCount = Math.min(CODES.Z - CODES.A + 1, columnCount); + const rows: string[] = []; + + const cols = new Array(colsCount) + .fill('') + .map((el, index) => String.fromCharCode(CODES.A + index)) + .map((el) => createCol(el)) + .join(''); + + rows.push(createRow(cols)); + + for (let i = 0; i < rowsCount; i++) { + const cells = new Array(colsCount).fill(createCell()).join(''); + rows.push(createRow(cells, `${i + 1}`)); + } + + return rows.join(''); +} diff --git a/src/components/toolbar/Toolbar.ts b/src/components/toolbar/Toolbar.ts new file mode 100644 index 0000000..a60f7c4 --- /dev/null +++ b/src/components/toolbar/Toolbar.ts @@ -0,0 +1,40 @@ +import { Dom } from '../../core/dom'; +import { ExcelComponent } from '../../core/ExcelComponent'; + +export class Toolbar extends ExcelComponent { + static className = 'excel__toolbar'; + + constructor($root: Dom) { + super($root, { + name: 'Toolbar', + listeners: ['click'], + }); + } + + toHTML(): string { + return ` +
+ format_bold +
+
+ format_italic +
+
+ format_underline +
+
+ format_align_left +
+
+ format_align_center +
+
+ format_align_right +
+ `; + } + + onClick() { + console.log('Toolbar click'); + } +} diff --git a/src/core/DomListener.ts b/src/core/DomListener.ts new file mode 100644 index 0000000..125130e --- /dev/null +++ b/src/core/DomListener.ts @@ -0,0 +1,41 @@ +import { Dom } from './dom'; +import { capitalize } from './utils'; + +export class DomListener { + $root: Dom; + listeners: string[]; + + constructor($root: Dom, listeners?: string[]) { + if (!$root) throw new Error('Не передали корневой элемент'); + + this.$root = $root; + this.listeners = listeners; + } + + initDOMListeners() { + if (!this.listeners) return; + + this.listeners.forEach((listener: string) => { + const method: any = getMethodName(listener); + // @ts-ignore FIXME: + this[method] = this[method]?.bind(this); + // @ts-ignore FIXME: + if (!this[method]) throw new Error(`Отсутствует метод ${method} в компоненте ${this?.name}`); + // @ts-ignore FIXME: + this.$root.on(listener, this[method]); + }); + } + + removeDOMListeners() { + this.listeners.forEach(listener => { + // @ts-ignore FIXME: + const method: any = getMethodName(listener); + // @ts-ignore FIXME: + this.$root.off(listener, this[method]); + }); + } +} + +function getMethodName(eventName: string): string { + return `on${capitalize(eventName)}`; +} diff --git a/src/core/ExcelComponent.ts b/src/core/ExcelComponent.ts new file mode 100644 index 0000000..9c9a987 --- /dev/null +++ b/src/core/ExcelComponent.ts @@ -0,0 +1,31 @@ +import { DomListener } from './DomListener'; + +interface ExcelComponentClass { + toHTML: () => string; +} + +type OptionsType = { + listeners?: string[]; + name: string; +}; + +export class ExcelComponent extends DomListener implements ExcelComponentClass { + name: string; + + constructor($root: any, options: OptionsType) { + super($root, options?.listeners); + this.name = options?.name; + } + + toHTML() { + return ''; + } + + init() { + this.initDOMListeners(); + } + + destroy() { + this.removeDOMListeners(); + } +} diff --git a/src/core/dom.ts b/src/core/dom.ts new file mode 100644 index 0000000..c1499a8 --- /dev/null +++ b/src/core/dom.ts @@ -0,0 +1,66 @@ +type SelectorType = string | HTMLElement; + +export interface DomClass { + html(html?: string): string | DomClass; + clear(): DomClass; + append(node: Node | string | DomClass): DomClass +} + +export class Dom implements DomClass { + $el: HTMLElement; + + constructor(selector: SelectorType) { + this.$el = typeof selector === 'string' + ? document.querySelector(selector) + : selector; + } + + html(html = '') { + if (typeof html === 'string') { + this.$el.innerHTML = html; + return this; + } + + return this.$el.outerHTML.trim(); + } + + clear() { + this.html(''); + + return this; + } + + // FIXME: any + append(node: any) { + let child = node; + + if (node instanceof Dom) child = node.$el; + + if (Element.prototype.append) { + this.$el.append(child); + } else { + this.$el.appendChild(child); + } + + return this; + } + + on(eventType: string, callback: any) { + this.$el.addEventListener(eventType, callback); + } + + off(eventType: string, callback: any) { + this.$el.removeEventListener(eventType, callback); + } +} + +export function $(selector: SelectorType) { + return new Dom(selector); +} + +$.create = (tagName: string, classes = '') => { + const el: HTMLElement = document.createElement(tagName); + if (classes) el.classList.add(classes); + + return $(el); +}; diff --git a/src/core/utils.ts b/src/core/utils.ts new file mode 100644 index 0000000..53c7860 --- /dev/null +++ b/src/core/utils.ts @@ -0,0 +1,5 @@ +export function capitalize(string: string): string { + if (!string) return ''; + + return string.charAt(0).toUpperCase() + string.slice(1); +} diff --git a/src/index.ts b/src/index.ts index 8579212..f1e0e26 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,12 @@ +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 './styles/index.scss'; + +const excel = new Excel('#app', { + components: [Header, Toolbar, Formula, Table], +}); + +excel.render(); diff --git a/tsconfig.json b/tsconfig.json index 44d292b..61ee365 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,8 @@ "target": "es3", "allowJs": true, "moduleResolution": "node", - "esModuleInterop": true + "esModuleInterop": true, + "sourceMap": true }, "ts-node": { "compilerOptions": { @@ -13,5 +14,5 @@ "strictPropertyInitialization": true } }, - "exclude": ["node_modules"] + "exclude": ["node_modules"], } \ No newline at end of file diff --git a/webpack.config.ts b/webpack.config.ts index a1ee3dc..e031ab5 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -17,7 +17,7 @@ const config = (env: Record, argv: Record): Configurat const isProd = argv.mode === 'production'; const getFilename = (ext: string): string => `[name]${isProd ? '-[hash]' : ''}.${ext}`; - const getSourceMap = () => (isProd ? false : 'source-map'); + const getSourceMap = () => (isProd ? false : 'inline-source-map'); const getPluginList = (): any[] => { const basePluginList = [ @@ -81,11 +81,11 @@ const config = (env: Record, argv: Record): Configurat clean: true, }, resolve: { - extensions: ['.ts', '.tsx', '.js', '.jsx', '.json', '.scss'], alias: { - '@': path.resolve(__dirname, 'src'), - '@core': path.resolve(__dirname, 'src', '@core'), + '@': path.resolve(__dirname, 'src/'), + '@core': path.resolve(__dirname, 'src/core/'), }, + extensions: ['.ts', '.tsx', '.js', '.jsx', '.json', '.scss'], }, plugins: getPluginList(), module: { @@ -110,7 +110,7 @@ const config = (env: Record, argv: Record): Configurat directory: path.join(__dirname, 'static'), }, compress: true, - port: 8080, + port: 80, watchFiles: './src', }, performance: {