finish version 1.0.0

This commit is contained in:
Sergey Krylov 2022-07-10 12:24:12 +05:00
parent 38b8834de8
commit 72b559b941
20 changed files with 1480 additions and 172 deletions

1461
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -31,6 +31,8 @@
"jest": "^28.1.2", "jest": "^28.1.2",
"jest-environment-jsdom": "^28.1.2", "jest-environment-jsdom": "^28.1.2",
"mini-css-extract-plugin": "^2.6.0", "mini-css-extract-plugin": "^2.6.0",
"postcss": "^8.4.14",
"postcss-loader": "^7.0.0",
"sass": "^1.52.2", "sass": "^1.52.2",
"sass-loader": "^13.0.0", "sass-loader": "^13.0.0",
"ssh2-sftp-client": "^5.3.2", "ssh2-sftp-client": "^5.3.2",
@ -63,11 +65,17 @@
}, },
"homepage": "https://bitbucket.org/ksv741/excel-course#readme", "homepage": "https://bitbucket.org/ksv741/excel-course#readme",
"description": "", "description": "",
"browserslist": "> 0.25%, not dead", "browserslist": [
"last 2 version",
"> 1%",
"IE 9"
],
"module": "true", "module": "true",
"dependencies": { "dependencies": {
"autoprefixer": "^10.4.7",
"element-closest": "^3.0.2", "element-closest": "^3.0.2",
"normalize.css": "^8.0.1" "normalize.css": "^8.0.1",
"postcss-preset-env": "^7.7.2"
}, },
"jest": { "jest": {
"transform": { "transform": {

View File

@ -1,5 +1,6 @@
import { $, Dom } from 'core/dom'; import { $, Dom } from 'core/dom';
export function Loader(): Dom { export function Loader(): Dom {
return $.create('div', 'loader').html('<div class="loader"><div class="lds-circle"><div></div></div></div>'); return $.create('div', 'loader')
.html('<div class="loader"><div class="lds-circle"><div></div></div></div>');
} }

View File

@ -28,7 +28,7 @@ export class Formula extends ExcelComponent {
this.formulaInput = this.$root.find('#formula-input'); this.formulaInput = this.$root.find('#formula-input');
this.$on('table:select-cell', text => { this.$on('table:select-cell', text => {
this.formulaInput.text = text; this.formulaInput.text = text || '';
}); });
} }
@ -36,8 +36,11 @@ export class Formula extends ExcelComponent {
this.formulaInput.text = currentText; this.formulaInput.text = currentText;
} }
onInput(event: Event) { onInput(event: InputEvent) {
const text = (event.target as HTMLElement).textContent.trim(); const { target } = event;
if (!target) return;
const text = (target as HTMLElement).innerText.trim();
this.$emit('formula:input', text); this.$emit('formula:input', text);
} }

View File

@ -1,3 +1,4 @@
import { startCellId } from 'components/table/table.functions';
import * as actions from 'redux/actions'; import * as actions from 'redux/actions';
import { $, Dom } from 'core/dom'; import { $, Dom } from 'core/dom';
import { ExcelComponent } from 'core/ExcelComponent'; import { ExcelComponent } from 'core/ExcelComponent';
@ -33,7 +34,7 @@ export class Table extends ExcelComponent {
init() { init() {
super.init(); super.init();
const $cell = this.$root.find('[data-id="0:0"]'); const $cell = this.$root.find(`[data-id="${startCellId}"]`);
this.selection.select($cell); this.selection.select($cell);
this.$emit('table:select-cell', $cell.data.value); this.$emit('table:select-cell', $cell.data.value);
@ -86,7 +87,6 @@ export class Table extends ExcelComponent {
const tableState = this.store.getState(); const tableState = this.store.getState();
const tableContent = tableState?.dataState; const tableContent = tableState?.dataState;
const tableStyles = tableState?.stylesState; const tableStyles = tableState?.stylesState;
Object.keys(tableContent).forEach(cellId => { Object.keys(tableContent).forEach(cellId => {
const $cell = this.$root.find(`[data-id="${cellId}"]`); const $cell = this.$root.find(`[data-id="${cellId}"]`);
const styles = tableStyles[cellId]; const styles = tableStyles[cellId];
@ -100,7 +100,7 @@ export class Table extends ExcelComponent {
emitSelectCallback() { emitSelectCallback() {
this.$emit('table:select-cell', this.selection.current.data.value); this.$emit('table:select-cell', this.selection.current.data.value);
const styles = this.selection.current?.getStyles(Object.keys(initialStyleState) as (keyof Partial<CSSStyleDeclaration>)[]); const styles = this.selection.current?.getStyles(Object.keys(initialStyleState));
this.$dispatch(changeCurrentStyles(styles)); this.$dispatch(changeCurrentStyles(styles));
} }
@ -116,7 +116,7 @@ export class Table extends ExcelComponent {
updateCurrentTextInStore(text: string) { updateCurrentTextInStore(text: string) {
this.$dispatch(actions.changeText({ this.$dispatch(actions.changeText({
text, text,
id: this.selection.current.data.id, id: this.selection.current.data.id || startCellId,
})); }));
} }
@ -130,6 +130,6 @@ export class Table extends ExcelComponent {
} }
onInput(event: InputEvent) { onInput(event: InputEvent) {
this.updateCurrentTextInStore((event.target as HTMLElement).textContent.trim()); this.updateCurrentTextInStore((event.target as HTMLElement).innerText.trim());
} }
} }

View File

@ -1,5 +1,5 @@
import { $, Dom } from 'core/dom'; import { $, Dom } from 'core/dom';
import { getParamsFromCellId } from 'components/table/table.functions'; import { getParamsFromCellId, startCellId } from 'components/table/table.functions';
export class TableSelection { export class TableSelection {
static selectedClassName = 'selected'; static selectedClassName = 'selected';
@ -8,7 +8,6 @@ export class TableSelection {
constructor() { constructor() {
this.group = []; this.group = [];
this.current = null;
} }
get selectedIds() { get selectedIds() {
@ -42,8 +41,8 @@ export class TableSelection {
} }
selectGroup($el: Dom) { selectGroup($el: Dom) {
const startCellParams = getParamsFromCellId(this.current.data.id); const startCellParams = getParamsFromCellId(this.current.data.id || startCellId);
const selectedCellParams = getParamsFromCellId($el.data.id); const selectedCellParams = getParamsFromCellId($el.data.id || startCellId);
const startCol = Math.min(startCellParams.col, selectedCellParams.col); const startCol = Math.min(startCellParams.col, selectedCellParams.col);
const endCol = Math.max(startCellParams.col, selectedCellParams.col); const endCol = Math.max(startCellParams.col, selectedCellParams.col);

View File

@ -1,3 +1,4 @@
import { startCellId } from 'components/table/table.functions';
import { $, Dom } from 'core/dom'; import { $, Dom } from 'core/dom';
type CustomElementType = Element & { css: any }; type CustomElementType = Element & { css: any };
@ -57,17 +58,17 @@ export function resizeHandler($root: Dom, event: MouseEvent) {
document.onmouseup = () => { document.onmouseup = () => {
document.onmousemove = null; document.onmousemove = null;
document.onmouseup = null; document.onmouseup = null;
document.body.style.userSelect = null; document.body.style.userSelect = '';
let value: number; let value = 0;
// let id: string; const id = $parent.data[type || ''] || startCellId;
switch (type) { switch (type) {
case 'col': { case 'col': {
value = coords.width + delta; value = coords.width + delta;
$parent.css({ width: `${value}px` }); $parent.css({ width: `${value}px` });
allCols.forEach((el: CustomElementType) => el.css({ width: `${coords.width + delta}px` })); allCols.forEach(el => $(el as HTMLElement).css({ width: `${coords.width + delta}px` }));
break; break;
} }
@ -78,10 +79,10 @@ export function resizeHandler($root: Dom, event: MouseEvent) {
break; break;
} }
default: break; default: return;
} }
res({ value, id: $parent.data[type], type }); res({ value, id, type });
$resizer.css({ opacity: 0, bottom: 0, right: 0 }); $resizer.css({ opacity: 0, bottom: 0, right: 0 });
}; };

View File

@ -1,6 +1,6 @@
import { $ } from 'core/dom'; import { $ } from 'core/dom';
import { TableSelection } from 'components/table/TableSelection'; import { TableSelection } from 'components/table/TableSelection';
import { getParamsFromCellId, isCell } from 'components/table/table.functions'; import { getParamsFromCellId, isCell, startCellId } from 'components/table/table.functions';
export function selectHandler(event: MouseEvent | KeyboardEvent, selection: TableSelection, callback?: () => void) { export function selectHandler(event: MouseEvent | KeyboardEvent, selection: TableSelection, callback?: () => void) {
switch (event.type) { switch (event.type) {
@ -15,7 +15,8 @@ export function selectHandler(event: MouseEvent | KeyboardEvent, selection: Tabl
default: break; default: break;
} }
callback(); // Analog callback && callback();
callback?.();
function onMouseDownHandler() { function onMouseDownHandler() {
if (isCell(event)) { if (isCell(event)) {
@ -37,7 +38,8 @@ export function selectHandler(event: MouseEvent | KeyboardEvent, selection: Tabl
if (!selection?.current || !handleKeys.includes(key)) return; if (!selection?.current || !handleKeys.includes(key)) return;
const currentCellId = selection.current?.data?.id; // If something goes wrong, go to start line
const currentCellId = selection.current.data.id || startCellId;
let { row, col } = getParamsFromCellId(currentCellId); let { row, col } = getParamsFromCellId(currentCellId);
switch (key) { switch (key) {

View File

@ -10,3 +10,5 @@ export function getParamsFromCellId(cellId: string) {
return { col, row }; return { col, row };
} }
export const startCellId = '0:0';

View File

@ -1,3 +1,4 @@
import { startCellId } from 'components/table/table.functions';
import { $, Dom } from 'core/dom'; import { $, Dom } from 'core/dom';
import { ExcelStateComponent } from 'core/ExcelStateComponent'; import { ExcelStateComponent } from 'core/ExcelStateComponent';
import { OptionsType } from 'core/ExcelComponent'; import { OptionsType } from 'core/ExcelComponent';
@ -9,10 +10,10 @@ export class Toolbar extends ExcelStateComponent {
constructor($root: Dom, options: OptionsType) { constructor($root: Dom, options: OptionsType) {
super($root, { super($root, {
...options,
listeners: ['click'], listeners: ['click'],
name: 'Toolbar', name: 'Toolbar',
subscribe: ['currentStyles'], subscribe: ['currentStyles'],
...options,
}); });
} }
@ -25,7 +26,7 @@ export class Toolbar extends ExcelStateComponent {
get toolbarState() { get toolbarState() {
return { return {
...initialStyleState, ...initialStyleState,
...this.store?.getState()?.stylesState?.['0:0'], ...this.store?.getState()?.stylesState?.[startCellId],
}; };
} }

View File

@ -4,9 +4,7 @@ import { initialStyleState } from 'src/constants';
type ButtonConfigType = { type ButtonConfigType = {
icon: string; icon: string;
isActive: boolean; isActive: boolean;
value: { value: ToolbarStateType;
[k: string]: string | number
}
}; };
export function createToolbar(state: ToolbarStateType): string { export function createToolbar(state: ToolbarStateType): string {

View File

@ -1,7 +1,9 @@
import { startCellId } from 'components/table/table.functions';
import { ToolbarStateType } from 'components/toolbar/toolbar-types';
import { StateType } from 'redux/types'; import { StateType } from 'redux/types';
import { storage } from 'core/utils'; import { storage } from 'core/utils';
export const initialStyleState: Partial<CSSStyleDeclaration> = { export const initialStyleState: ToolbarStateType = {
textAlign: 'left', textAlign: 'left',
fontWeight: 'normal', fontWeight: 'normal',
textDecoration: 'none', textDecoration: 'none',
@ -16,9 +18,9 @@ export function getNormalizeInitialState(params: string): StateType {
currentText: '', currentText: '',
stylesState: {}, stylesState: {},
title: 'New excel table', title: 'New excel table',
...storage(`excel:${params}`),
currentStyles: { ...storage(`excel:${params}`)?.stylesState?.['0:0'] },
id: params, id: params,
openDate: Date.now(), openDate: Date.now(),
...storage(`excel:${params}`),
currentStyles: { ...storage(`excel:${params}`)?.stylesState?.[startCellId] },
}; };
} }

View File

@ -3,20 +3,25 @@ import { storageName } from 'pages/ExcelPage';
import { StateType } from 'redux/types'; import { StateType } from 'redux/types';
import { getNormalizeInitialState } from 'src/constants'; import { getNormalizeInitialState } from 'src/constants';
export class LocalStorageClient { export interface ClientDataType {
save: (state: StateType) => Promise<any>;
get: () => Promise<any>;
}
export class LocalStorageClient implements ClientDataType {
private name: string; private name: string;
constructor(name: string) { constructor(name: string) {
this.name = storageName(name); this.name = name;
} }
save(state: StateType): Promise<void> { save(state: StateType): Promise<void> {
storage(this.name, state); storage(storageName(this.name), state);
return Promise.resolve(); return Promise.resolve();
} }
get() { get() {
const data = storage(this.name) || getNormalizeInitialState(this.name); const data = storage(storageName(this.name)) || getNormalizeInitialState(this.name);
return new Promise(resolve => { return new Promise(resolve => {
setTimeout(() => { setTimeout(() => {

View File

@ -5,7 +5,7 @@ export class DomListener {
$root: Dom; $root: Dom;
listeners: string[]; listeners: string[];
constructor($root: Dom, listeners?: string[]) { constructor($root: Dom, listeners: string[]) {
if (!$root) throw new Error('Не передали корневой элемент'); if (!$root) throw new Error('Не передали корневой элемент');
this.$root = $root; this.$root = $root;

View File

@ -11,26 +11,26 @@ interface ExcelComponentClass {
} }
export type OptionsType = { export type OptionsType = {
listeners?: string[]; listeners: string[];
name: string; name: string;
emitter?: Emitter; emitter: Emitter;
store: Store; store: Store;
subscribe: string[], subscribe: string[],
}; };
export abstract class ExcelComponent extends DomListener implements ExcelComponentClass { export abstract class ExcelComponent extends DomListener implements ExcelComponentClass {
private name: string | undefined; private name: string;
private emitter: Emitter | undefined; private emitter: Emitter;
private store: Store | undefined; public store: Store;
private subscribe: string[] | undefined; private subscribe: string[];
private unsubscribers: ((args?: any) => any)[]; private unsubscribers: ((args?: any) => any)[];
constructor($root: Dom, options?: OptionsType) { constructor($root: Dom, options: OptionsType) {
super($root, options?.listeners); super($root, options.listeners);
this.name = options?.name; this.name = options.name;
this.emitter = options?.emitter; this.emitter = options.emitter;
this.store = options?.store; this.store = options.store;
this.subscribe = options?.subscribe; this.subscribe = options.subscribe;
this.unsubscribers = []; this.unsubscribers = [];
this.prepare(); this.prepare();

View File

@ -1,12 +1,16 @@
import { ClientDataType } from 'core/Clients';
import { debounce } from 'core/utils'; import { debounce } from 'core/utils';
import { StateType } from 'redux/types';
export class StateProcessor { export class StateProcessor {
constructor(client, dalay = 300) { private client: ClientDataType;
constructor(client: ClientDataType, dalay = 300) {
this.client = client; this.client = client;
this.listen = debounce(this.listen.bind(this), dalay); this.listen = debounce(this.listen.bind(this), dalay);
} }
listen(state) { listen(state: StateType) {
this.client.save(state); this.client.save(state);
} }

View File

@ -1,3 +1,4 @@
import { ToolbarStateType } from 'components/toolbar/toolbar-types';
import { initialStyleState } from 'src/constants'; import { initialStyleState } from 'src/constants';
export type SelectorType = string | HTMLElement; export type SelectorType = string | HTMLElement;
@ -9,30 +10,33 @@ export interface DomClass {
} }
export class Dom implements DomClass { export class Dom implements DomClass {
$el: HTMLElement | null; $el: HTMLElement;
constructor(selector: SelectorType) { constructor(selector: SelectorType) {
this.$el = typeof selector === 'string' // Could not find element with selector in DOM, need a check
? document.querySelector(selector) if (typeof selector === 'string') {
: selector; const elementFromDOM = document.querySelector(selector);
if (!elementFromDOM) throw new Error(`Can't find element with "${selector}" selector`);
else this.$el = elementFromDOM as HTMLElement;
} else {
this.$el = selector;
}
} }
html(html = '') { html(html = '') {
if (typeof html === 'string') {
this.$el.innerHTML = html; this.$el.innerHTML = html;
return this; return this;
} }
return this.$el.outerHTML.trim();
}
set text(text: string) { set text(text: string) {
if (!text) this.$el.textContent = '';
this.$el.textContent = text; this.$el.textContent = text;
} }
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 as HTMLElement).innerText;
} }
clear() { clear() {
@ -47,11 +51,8 @@ export class Dom implements DomClass {
if (node instanceof Dom) child = node.$el; if (node instanceof Dom) child = node.$el;
if (Element.prototype.append) { if (this.$el.append) this.$el.append(child);
this.$el.append(child); else this.$el.appendChild(child);
} else {
this.$el.appendChild(child);
}
return this; return this;
} }
@ -69,7 +70,7 @@ export class Dom implements DomClass {
} }
get data() { get data() {
return this.$el.dataset; return this.$el.dataset || '';
} }
setData(name: string, value: string) { setData(name: string, value: string) {
@ -108,9 +109,9 @@ export class Dom implements DomClass {
this.$el?.classList.remove(className); this.$el?.classList.remove(className);
} }
getStyles(styles: (keyof Partial<CSSStyleDeclaration>)[]) { getStyles(styles: any[]) {
return styles.reduce((res: Partial<CSSStyleDeclaration>, s: any) => { return styles.reduce((res, s) => {
res[s] = this.$el.style[s] || initialStyleState[s]; res[s] = this.$el.style[s] || initialStyleState[s as keyof ToolbarStateType];
return res; return res;
}, {}); }, {});
} }

View File

@ -9,7 +9,7 @@ export class Store {
this.listeners = []; this.listeners = [];
} }
subscribe(fn: (state?: StateType) => void): SubscribeType { subscribe(fn: (state: StateType) => void): SubscribeType {
this.listeners.push(fn); this.listeners.push(fn);
return { return {
unsubscribe: () => { unsubscribe: () => {

View File

@ -3,5 +3,5 @@ import elementClosest from 'element-closest';
try { try {
elementClosest(window); // this is used to reference window.Element elementClosest(window); // this is used to reference window.Element
} catch (e) { } catch (e) {
console.log('Error in polyfills', e); console.error('Error in polyfills', e);
} }

View File

@ -2,7 +2,7 @@ import path from 'path';
// FIXME: // FIXME:
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { Configuration as DevServerConfiguration } from 'webpack-dev-server'; import 'webpack-dev-server';
import type { Configuration } from 'webpack'; import type { Configuration } from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin';
@ -95,6 +95,18 @@ const config = (env: Record<string, any>, argv: Record<string, any>): Configurat
sourceMap: !isProd, sourceMap: !isProd,
}, },
}, },
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
[
'postcss-preset-env',
],
],
},
},
},
{ {
loader: 'sass-loader', loader: 'sass-loader',
options: { options: {