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

View File

@ -1,5 +1,6 @@
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>');
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.$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;
}
onInput(event: Event) {
const text = (event.target as HTMLElement).textContent.trim();
onInput(event: InputEvent) {
const { target } = event;
if (!target) return;
const text = (target as HTMLElement).innerText.trim();
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 { $, Dom } from 'core/dom';
import { ExcelComponent } from 'core/ExcelComponent';
@ -33,7 +34,7 @@ export class Table extends ExcelComponent {
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.$emit('table:select-cell', $cell.data.value);
@ -86,7 +87,6 @@ export class Table extends ExcelComponent {
const tableState = this.store.getState();
const tableContent = tableState?.dataState;
const tableStyles = tableState?.stylesState;
Object.keys(tableContent).forEach(cellId => {
const $cell = this.$root.find(`[data-id="${cellId}"]`);
const styles = tableStyles[cellId];
@ -100,7 +100,7 @@ export class Table extends ExcelComponent {
emitSelectCallback() {
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));
}
@ -116,7 +116,7 @@ export class Table extends ExcelComponent {
updateCurrentTextInStore(text: string) {
this.$dispatch(actions.changeText({
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) {
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 { getParamsFromCellId } from 'components/table/table.functions';
import { getParamsFromCellId, startCellId } from 'components/table/table.functions';
export class TableSelection {
static selectedClassName = 'selected';
@ -8,7 +8,6 @@ export class TableSelection {
constructor() {
this.group = [];
this.current = null;
}
get selectedIds() {
@ -42,8 +41,8 @@ export class TableSelection {
}
selectGroup($el: Dom) {
const startCellParams = getParamsFromCellId(this.current.data.id);
const selectedCellParams = getParamsFromCellId($el.data.id);
const startCellParams = getParamsFromCellId(this.current.data.id || startCellId);
const selectedCellParams = getParamsFromCellId($el.data.id || startCellId);
const startCol = Math.min(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';
type CustomElementType = Element & { css: any };
@ -57,17 +58,17 @@ export function resizeHandler($root: Dom, event: MouseEvent) {
document.onmouseup = () => {
document.onmousemove = null;
document.onmouseup = null;
document.body.style.userSelect = null;
document.body.style.userSelect = '';
let value: number;
// let id: string;
let value = 0;
const id = $parent.data[type || ''] || startCellId;
switch (type) {
case 'col': {
value = coords.width + delta;
$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;
}
@ -78,10 +79,10 @@ export function resizeHandler($root: Dom, event: MouseEvent) {
break;
}
default: break;
default: return;
}
res({ value, id: $parent.data[type], type });
res({ value, id, type });
$resizer.css({ opacity: 0, bottom: 0, right: 0 });
};

View File

@ -1,6 +1,6 @@
import { $ } from 'core/dom';
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) {
switch (event.type) {
@ -15,7 +15,8 @@ export function selectHandler(event: MouseEvent | KeyboardEvent, selection: Tabl
default: break;
}
callback();
// Analog callback && callback();
callback?.();
function onMouseDownHandler() {
if (isCell(event)) {
@ -37,7 +38,8 @@ export function selectHandler(event: MouseEvent | KeyboardEvent, selection: Tabl
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);
switch (key) {

View File

@ -10,3 +10,5 @@ export function getParamsFromCellId(cellId: string) {
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 { ExcelStateComponent } from 'core/ExcelStateComponent';
import { OptionsType } from 'core/ExcelComponent';
@ -9,10 +10,10 @@ export class Toolbar extends ExcelStateComponent {
constructor($root: Dom, options: OptionsType) {
super($root, {
...options,
listeners: ['click'],
name: 'Toolbar',
subscribe: ['currentStyles'],
...options,
});
}
@ -25,7 +26,7 @@ export class Toolbar extends ExcelStateComponent {
get toolbarState() {
return {
...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 = {
icon: string;
isActive: boolean;
value: {
[k: string]: string | number
}
value: ToolbarStateType;
};
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 { storage } from 'core/utils';
export const initialStyleState: Partial<CSSStyleDeclaration> = {
export const initialStyleState: ToolbarStateType = {
textAlign: 'left',
fontWeight: 'normal',
textDecoration: 'none',
@ -16,9 +18,9 @@ export function getNormalizeInitialState(params: string): StateType {
currentText: '',
stylesState: {},
title: 'New excel table',
...storage(`excel:${params}`),
currentStyles: { ...storage(`excel:${params}`)?.stylesState?.['0:0'] },
id: params,
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 { 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;
constructor(name: string) {
this.name = storageName(name);
this.name = name;
}
save(state: StateType): Promise<void> {
storage(this.name, state);
storage(storageName(this.name), state);
return Promise.resolve();
}
get() {
const data = storage(this.name) || getNormalizeInitialState(this.name);
const data = storage(storageName(this.name)) || getNormalizeInitialState(this.name);
return new Promise(resolve => {
setTimeout(() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,5 +3,5 @@ import elementClosest from 'element-closest';
try {
elementClosest(window); // this is used to reference window.Element
} 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:
// 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 HtmlWebpackPlugin from 'html-webpack-plugin';
@ -95,6 +95,18 @@ const config = (env: Record<string, any>, argv: Record<string, any>): Configurat
sourceMap: !isProd,
},
},
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
[
'postcss-preset-env',
],
],
},
},
},
{
loader: 'sass-loader',
options: {