├── .gitignore
├── LICENSE
├── README.md
├── docs
├── 13b240062f9cf9a7f4131b86a426f140.svg
├── demo.png
├── index.html
├── xspreadsheet.js
└── xspreadsheet.js.map
├── index.html
├── package.json
├── src
├── assets
│ ├── material_common_sprite57.svg
│ └── sprite.svg
├── core
│ ├── alphabet.d.ts
│ ├── alphabet.ts
│ ├── cell.d.ts
│ ├── cell.ts
│ ├── font.d.ts
│ ├── font.ts
│ ├── format.d.ts
│ ├── format.ts
│ ├── formula.d.ts
│ ├── formula.ts
│ ├── index.d.ts
│ ├── index.ts
│ ├── select.d.ts
│ └── select.ts
├── local
│ ├── base
│ │ ├── colorPanel.d.ts
│ │ ├── colorPanel.ts
│ │ ├── dropdown.d.ts
│ │ ├── dropdown.ts
│ │ ├── element.d.ts
│ │ ├── element.ts
│ │ ├── icon.d.ts
│ │ ├── icon.ts
│ │ ├── item.d.ts
│ │ ├── item.ts
│ │ ├── menu.d.ts
│ │ ├── menu.ts
│ │ ├── suggest.d.ts
│ │ └── suggest.ts
│ ├── contextmenu.d.ts
│ ├── contextmenu.ts
│ ├── editor.d.ts
│ ├── editor.ts
│ ├── editorbar.d.ts
│ ├── editorbar.ts
│ ├── event.d.ts
│ ├── event.ts
│ ├── index.d.ts
│ ├── index.ts
│ ├── resizer.d.ts
│ ├── resizer.ts
│ ├── selector.d.ts
│ ├── selector.ts
│ ├── table.d.ts
│ ├── table.ts
│ ├── toolbar.d.ts
│ └── toolbar.ts
├── main.d.ts
├── main.ts
└── style
│ └── index.less
├── tsconfig.json
├── tslint.json
├── webpack.config.dev.js
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .vscode/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 myliang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # XSpreadsheet
2 |
3 | [](https://www.npmjs.org/package/xspreadsheet)
4 | [](https://npmjs.org/package/xspreadsheet)
5 |
6 | > a javascript spreadsheet for web
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | ## Install
15 | ```shell
16 | npm install typescript --save-dev
17 | npm install awesome-typescript-loader --save-dev
18 | npm install xspreadsheet --save-dev
19 | ```
20 |
21 | ## Quick Start
22 |
23 | ``` javascript
24 | import xspreadsheet from 'xspreadsheet'
25 |
26 | const x = xspreadsheet(document.getElementById('#id'))
27 | x.change = (data) => {
28 | console.log('data:', data)
29 | }
30 |
31 | // edit
32 | // data is param in the change method
33 | xspreadsheet(document.getElementById('#id'), {d: data})
34 | ```
35 |
36 | ### in tsconfig.json
37 | ```
38 | {
39 | "compilerOptions": {
40 | ....
41 | "types": ["xspreadsheet"],
42 | ....
43 | }
44 | }
45 |
46 | ```
47 |
48 | ## Browser Support
49 | Modern browsers and Internet Explorer 9+(no test).
50 |
51 | ## LICENSE
52 | MIT
53 |
--------------------------------------------------------------------------------
/docs/13b240062f9cf9a7f4131b86a426f140.svg:
--------------------------------------------------------------------------------
1 |
51 |
--------------------------------------------------------------------------------
/docs/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myliang/xspreadsheet/3075e5007eaaf92ce927362f854e7ed8966b61ee/docs/demo.png
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
16 | XSpreadsheet Demo
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
15 | TypeScript with VSCode
16 |
17 |
18 |
19 |
20 | xxxxxx
21 |
22 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "xspreadsheet",
3 | "version": "1.0.4",
4 | "description": "a javascript spreadsheet",
5 | "author": "myliang ",
6 | "private": false,
7 | "main": "src/main.ts",
8 | "types": "src/main.d.ts",
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/myliang/spreadsheet.git"
12 | },
13 | "scripts": {
14 | "dev": "webpack-dev-server --color --inline --hot --config webpack.config.dev.js --open",
15 | "build": "webpack --config webpack.config.js --progress --color"
16 | },
17 | "keywords": [
18 | "excel",
19 | "js",
20 | "component",
21 | "ui",
22 | "spreadsheet"
23 | ],
24 | "license": "MIT",
25 | "devDependencies": {
26 | "awesome-typescript-loader": "^5.2.0",
27 | "css-loader": "^0.28.11",
28 | "extract-text-webpack-plugin": "^4.0.0-beta.0",
29 | "file-loader": "^1.1.11",
30 | "less": "^3.0.1",
31 | "less-loader": "^4.1.0",
32 | "source-map-loader": "^0.2.3",
33 | "style-loader": "^0.20.3",
34 | "tslint-eslint-rules": "^5.2.0",
35 | "typescript": "^2.9.2",
36 | "webpack": "^4.5.0",
37 | "webpack-cli": "^2.0.14",
38 | "webpack-dev-server": "^3.1.3"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/assets/sprite.svg:
--------------------------------------------------------------------------------
1 |
51 |
--------------------------------------------------------------------------------
/src/core/alphabet.d.ts:
--------------------------------------------------------------------------------
1 | export declare function alphabet(index: number): string;
2 | export declare function alphabetIndex(key: string): number;
3 |
--------------------------------------------------------------------------------
/src/core/alphabet.ts:
--------------------------------------------------------------------------------
1 | const _alphabet = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z']
2 | export function alphabet(index: number): string {
3 | const [a, b] = [parseInt(index / _alphabet.length + ''), index % _alphabet.length]
4 | // console.log('a: ', a, '; b: ', b)
5 | return a > 0 ? `${_alphabet[a - 1]}${_alphabet[b]}` : _alphabet[b]
6 | }
7 |
8 | export function alphabetIndex (key: string): number {
9 | let ret = 0;
10 | for (let i = 0; i < key.length; i++) {
11 | // console.log(key.charCodeAt(i), key[i])
12 | let cindex = key.charCodeAt(i) - 65;
13 | ret += i * _alphabet.length + cindex;
14 | }
15 | return ret;
16 | }
17 |
--------------------------------------------------------------------------------
/src/core/cell.d.ts:
--------------------------------------------------------------------------------
1 | export interface Cell {
2 | font?: string;
3 | format?: string;
4 | fontSize?: number;
5 | bold?: boolean;
6 | italic?: boolean;
7 | underline?: boolean;
8 | color?: string;
9 | backgroundColor?: string;
10 | align?: string;
11 | valign?: string;
12 | wordWrap?: boolean;
13 | visable?: boolean;
14 | rowspan?: number;
15 | colspan?: number;
16 | text?: string;
17 | merge?: [number, number];
18 | [key: string]: any;
19 | }
20 | export declare const defaultCell: Cell;
21 | export declare function getStyleFromCell(cell: Cell | null): {
22 | [key: string]: string;
23 | };
24 |
--------------------------------------------------------------------------------
/src/core/cell.ts:
--------------------------------------------------------------------------------
1 | export interface Cell {
2 | font?: string;
3 | format?: string;
4 | fontSize?: number;
5 | bold?: boolean;
6 | italic?: boolean;
7 | underline?: boolean;
8 | color?: string;
9 | backgroundColor?: string;
10 | align?: string;
11 | valign?: string;
12 | wordWrap?: boolean;
13 | visable?: boolean;
14 | rowspan?: number;
15 | colspan?: number;
16 | text?: string;
17 | merge?: [number, number];
18 | [key: string]: any
19 | }
20 |
21 | export const defaultCell: Cell = {
22 | font: 'Microsoft YaHei',
23 | format: 'normal',
24 | fontSize: 14,
25 | bold: false,
26 | italic: false,
27 | underline: false,
28 | color: '#333',
29 | backgroundColor: '#fff',
30 | align: 'left',
31 | valign: 'middle',
32 | wordWrap: false,
33 | invisible: false,
34 | rowspan: 1,
35 | colspan: 1,
36 | text: '',
37 |
38 | }
39 |
40 | export function getStyleFromCell (cell: Cell | null): {[key: string]: string} {
41 | const map: {[key: string]: string} = {}
42 | if (cell) {
43 | if (cell.font) map['font-family'] = cell.font
44 | if (cell.fontSize) map['font-size'] = `${cell.fontSize}px`
45 | if (cell.bold) map['font-weight'] = 'bold'
46 | if (cell.italic) map['font-style'] = 'italic'
47 | if (cell.underline) map['text-decoration'] = 'underline'
48 | if (cell.color) map['color'] = cell.color
49 | if (cell.backgroundColor) map['background-color'] = cell.backgroundColor
50 | if (cell.align) map['text-align'] = cell.align
51 | if (cell.valign) map['vertical-align'] = cell.valign
52 | if (cell.invisible) {
53 | map['display'] = 'none'
54 | }
55 | if (cell.wordWrap) {
56 | map['word-wrap'] = 'break-word'
57 | map['white-space'] = 'normal'
58 | }
59 | }
60 | return map
61 | }
62 |
--------------------------------------------------------------------------------
/src/core/font.d.ts:
--------------------------------------------------------------------------------
1 | export interface Font {
2 | key: string;
3 | title: string;
4 | }
5 | export declare const fonts: Array;
6 |
--------------------------------------------------------------------------------
/src/core/font.ts:
--------------------------------------------------------------------------------
1 | export interface Font {
2 | key: string;
3 | title: string;
4 | }
5 |
6 | export const fonts: Array = [
7 | {key: 'Microsoft YaHei', title: '微软雅黑'},
8 | {key: 'STFangsong', title: '华文仿宋'},
9 | {key: 'Comic Sans MS', title: 'Comic Sans MS'},
10 | {key: 'Arial', title: 'Arial'},
11 | {key: 'Courier New', title: 'Courier New'},
12 | {key: 'Verdana', title: 'Verdana'}
13 | ]
--------------------------------------------------------------------------------
/src/core/format.d.ts:
--------------------------------------------------------------------------------
1 | export interface Format {
2 | key: string;
3 | title: string;
4 | label?: string;
5 | render(txt: string): string;
6 | }
7 | export declare const formatRenderHtml: (key: string | undefined, txt: string | undefined) => string;
8 | export declare const formats: Array;
9 |
--------------------------------------------------------------------------------
/src/core/format.ts:
--------------------------------------------------------------------------------
1 | export interface Format {
2 | key: string;
3 | title: string;
4 | label?: string;
5 | render(txt: string): string;
6 | }
7 | export const formatRenderHtml = (key: string | undefined, txt: string | undefined) => {
8 | for (let i = 0; i < formats.length; i++) {
9 | if (formats[i].key === key) {
10 | return formats[i].render(txt || '')
11 | }
12 | }
13 | return txt || ''
14 | }
15 |
16 | const formatNumberRender = (v: string) => {
17 | if (/^(-?\d*.?\d*)$/.test(v)) {
18 | v = Number(v).toFixed(2).toString()
19 | const parts = v.split('.')
20 | parts[0] = parts[0].toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1' + ',')
21 | return parts.join('.')
22 | }
23 | return v
24 | }
25 |
26 | const formatRender = (v: string) => v
27 |
28 | export const formats: Array = [
29 | {key: 'normal', title: 'Normal', render: formatRender},
30 | {key: 'text', title: 'Text', render: formatRender},
31 | {key: 'number', title: 'Number', label: '1,000.12', render: formatNumberRender},
32 | {key: 'percent', title: 'Percent', label: '10.12%', render: (v) => `${formatNumberRender(v)}%`},
33 | {key: 'RMB', title: 'RMB', label: '¥10.00', render: (v) => `¥${formatNumberRender(v)}`},
34 | {key: 'USD', title: 'USD', label: '$10.00', render: (v) => `$${formatNumberRender(v)}`}
35 | ]
--------------------------------------------------------------------------------
/src/core/formula.d.ts:
--------------------------------------------------------------------------------
1 | export interface Formula {
2 | key: string;
3 | title: string;
4 | render(ary: Array): number;
5 | }
6 | export declare const formulaFilterKey: (v: string, filter: (formula: Formula, param: string) => string) => string;
7 | export declare const formulaRender: (v: string, renderCell: (rindex: number, cindex: number) => any) => string;
8 | export declare const formulaReplaceParam: (param: string, rowDiff: number, colDiff: number) => string;
9 | export declare const formulas: Array;
10 |
--------------------------------------------------------------------------------
/src/core/formula.ts:
--------------------------------------------------------------------------------
1 | import { alphabetIndex, alphabet } from "./alphabet";
2 |
3 | export interface Formula {
4 | key: string;
5 | title: string;
6 | render(ary: Array): number
7 | }
8 |
9 | export const formulaFilterKey = (v: string, filter: (formula: Formula, param: string) => string) => {
10 | if (v[0] === '=') {
11 | const fx = v.substring(1, v.indexOf('('))
12 | for (let formula of formulas) {
13 | if (formula.key.toLowerCase() === fx.toLowerCase()) {
14 | return filter(formula, v.substring(v.indexOf('(') + 1, v.lastIndexOf(')')))
15 | }
16 | }
17 | }
18 | return v
19 | }
20 |
21 | export const formulaRender = (v: string, renderCell: (rindex: number, cindex: number) => any) => {
22 | return formulaFilterKey(v, (fx, param) => {
23 | return fx.render(formulaParamToArray(param, renderCell)) + '';
24 | })
25 | }
26 |
27 | export const formulaReplaceParam = (param: string, rowDiff: number, colDiff: number): string => {
28 | return formulaFilterKey(param, (fx, params) => {
29 | const replaceFormula = (_v: string):string => {
30 | if (/^[0-9\-\+\*\/()\s]+$/.test(_v.trim())) {
31 | return _v
32 | }
33 | const idx = /\d+/.exec(_v)
34 | if (idx) {
35 | let vc = _v.substring(0, idx.index).trim()
36 | let vr = parseInt(_v.substring(idx.index).trim())
37 | return `${alphabet(alphabetIndex(vc) + colDiff)}${vr + rowDiff}`
38 | }
39 | return _v;
40 | }
41 |
42 | if (params.indexOf(':') !== -1) {
43 | params = params.split(':').map(replaceFormula).join(':')
44 | } else {
45 | params = params.split(',').map(replaceFormula).join(',')
46 | }
47 | return `=${fx.key}(${params})`
48 | })
49 | }
50 |
51 | const formulaParamToArray = (param: string, renderCell: (rindex: number, cindex: number) => any) => {
52 | let paramValues = []
53 | try {
54 | if (param.indexOf(':') !== -1) {
55 | const [min, max] = param.split(':');
56 | const idx = /\d+/.exec(min);
57 | const maxIdx = /\d+/.exec(max);
58 | if (idx && maxIdx) {
59 | // idx = idx.index;
60 | // maxIdx = maxIdx.index;
61 | let minC = min.substring(0, idx.index).trim()
62 | let minR = parseInt(min.substring(idx.index).trim())
63 |
64 | let maxC = max.substring(0, maxIdx.index).trim()
65 | let maxR = parseInt(max.substring(maxIdx.index).trim())
66 | // console.log(min, max, minR, maxR, minC, maxC)
67 | if (maxC === minC) {
68 | for (let i = minR; i <= maxR; i++) {
69 | // console.log('value:::', i-1, alphabetIndex(minC), renderCell(i - 1, alphabetIndex(minC)))
70 | paramValues.push(renderCell(i - 1, alphabetIndex(minC)))
71 | }
72 | } else {
73 | for (let i = alphabetIndex(minC); i <= alphabetIndex(maxC); i++) {
74 | paramValues.push(renderCell(minR - 1, i))
75 | }
76 | }
77 | }
78 | } else {
79 | paramValues = param.split(',').map(p => {
80 | // console.log(/^[0-9\-\+\*\/() ]+$/.test(p), p)
81 | if (/^[0-9\-\+\*\/()\s]+$/.test(p.trim())) {
82 | try {
83 | return eval(p)
84 | } catch (e) {
85 | return 0
86 | }
87 | }
88 | const idx = /\d+/.exec(p)
89 | if (idx) {
90 | const c = p.substring(0, idx.index).trim()
91 | const r = p.substring(idx.index).trim()
92 | return renderCell(parseInt(r) - 1, alphabetIndex(c))
93 | }
94 | return 0
95 | })
96 | }
97 | } catch (e) {
98 | console.log('warning:', e)
99 | }
100 | return paramValues;
101 | }
102 |
103 | export const formulas: Array = [
104 | {key: 'SUM', title: '求和', render: (vv) => vv.reduce((a, b) => Number(a) + Number(b), 0)},
105 | {key: 'AVERAGE', title: '平均值', render: (vv) => vv.reduce((a, b) => Number(a) + Number(b), 0) / vv.length},
106 | {key: 'MAX', title: '最大值', render: (vv) => Math.max(...vv.map(v => Number(v)))},
107 | {key: 'MIN', title: '最小值', render: (vv) => Math.min(...vv.map(v => Number(v)))}
108 | ]
--------------------------------------------------------------------------------
/src/core/index.d.ts:
--------------------------------------------------------------------------------
1 | import { Format } from './format';
2 | import { Font } from './font';
3 | import { Formula } from './formula';
4 | import { Cell } from './cell';
5 | import { Select } from './select';
6 | export interface Row {
7 | height: number;
8 | }
9 | export interface Col {
10 | title: string;
11 | width: number;
12 | }
13 | export interface MapInt {
14 | [key: number]: T;
15 | }
16 | export declare class History {
17 | type: 'rows' | 'cols' | 'cells';
18 | values: Array<[Array, any, any]>;
19 | constructor(type: 'rows' | 'cols' | 'cells');
20 | add(keys: Array, oldValue: any, value: any): void;
21 | }
22 | export declare type StandardCallback = (rindex: number, cindex: number, cell: Cell) => void;
23 | export interface SpreadsheetData {
24 | rowHeight?: number;
25 | colWidth?: number;
26 | rows?: MapInt;
27 | cols?: MapInt;
28 | cell: Cell;
29 | cells?: MapInt>;
30 | [prop: string]: any;
31 | }
32 | export interface SpreadsheetOptions {
33 | formats?: Array;
34 | fonts?: Array;
35 | formulas?: Array;
36 | data?: SpreadsheetData;
37 | }
38 | export declare class Spreadsheet {
39 | formats: Array;
40 | fonts: Array;
41 | formulas: Array;
42 | data: SpreadsheetData;
43 | private histories;
44 | private histories2;
45 | private currentCellIndexes;
46 | select: Select | null;
47 | private copySelect;
48 | private cutSelect;
49 | change: (data: SpreadsheetData) => void;
50 | constructor(options?: SpreadsheetOptions);
51 | buildSelect(startTarget: any, endTarget: any): Select;
52 | defaultRowHeight(): number;
53 | defaultColWidth(): number;
54 | copy(): void;
55 | cut(): void;
56 | paste(cb: StandardCallback, state: 'copy' | 'cut' | 'copyformat', clear: StandardCallback): void;
57 | insert(type: 'row' | 'col', amount: number, cb: StandardCallback): void;
58 | batchPaste(arrow: 'bottom' | 'top' | 'left' | 'right', startRow: number, startCol: number, stopRow: number, stopCol: number, seqCopy: boolean, cb: StandardCallback): void;
59 | private copyCell;
60 | isRedo(): boolean;
61 | redo(cb: StandardCallback): boolean;
62 | isUndo(): boolean;
63 | undo(cb: StandardCallback): boolean;
64 | resetByHistory(v: History, cb: StandardCallback, state: 'undo' | 'redo'): void;
65 | clearformat(cb: StandardCallback): void;
66 | merge(ok: StandardCallback, cancel: StandardCallback, other: StandardCallback): void;
67 | cellAttr(key: keyof Cell, value: any, cb: StandardCallback): void;
68 | cellText(value: any, cb: StandardCallback): Cell | null;
69 | currentCell(indexes?: [number, number]): Cell | null;
70 | cell(rindex: number, cindex: number, v: any, isCopy?: boolean): Cell;
71 | getCell(rindex: number, cindex: number): Cell | null;
72 | getFont(key: string | undefined): Font;
73 | getFormat(key: string | undefined): Format;
74 | row(index: number, v?: number): Row;
75 | rows(isData: boolean): Array;
76 | col(index: number, v?: number): Col;
77 | cols(): Array;
78 | }
79 |
--------------------------------------------------------------------------------
/src/core/index.ts:
--------------------------------------------------------------------------------
1 | import { Format, formats } from './format'
2 | import { Font, fonts } from './font'
3 | import { Formula, formulas, formulaReplaceParam } from './formula'
4 | import { Cell, defaultCell } from './cell'
5 | import { alphabet } from './alphabet'
6 | import { Select } from './select'
7 | import { unbind } from '../local/event';
8 |
9 | export interface Row {
10 | height: number
11 | }
12 | export interface Col {
13 | title: string
14 | width: number
15 | }
16 | export interface MapInt {
17 | [key: number]: T
18 | }
19 | export class History {
20 | values: Array<[Array, any, any]> = [];
21 | constructor (public type: 'rows' | 'cols' | 'cells') {}
22 | add (keys: Array, oldValue: any, value: any) {
23 | this.values.push([keys, oldValue, value])
24 | }
25 | }
26 | // types
27 | export type StandardCallback = (rindex: number, cindex: number, cell: Cell) => void;
28 |
29 | export interface SpreadsheetData {
30 | rowHeight?: number;
31 | colWidth?: number;
32 | rows?: MapInt;
33 | cols?: MapInt;
34 | cell: Cell; // global default cell
35 | cells?: MapInt>;
36 | [prop: string]: any
37 | }
38 |
39 | export interface SpreadsheetOptions {
40 | formats?: Array;
41 | fonts?: Array;
42 | formulas?: Array;
43 | data?: SpreadsheetData;
44 | }
45 |
46 | export class Spreadsheet {
47 | formats: Array;
48 | fonts: Array;
49 | formulas: Array;
50 | data: SpreadsheetData;
51 | private histories: Array = [];
52 | private histories2: Array = [];
53 | private currentCellIndexes: [number, number] = [0, 0];
54 | select: Select | null = null;
55 | private copySelect: Select | null = null;
56 | private cutSelect: Select | null = null;
57 |
58 | change: (data: SpreadsheetData) => void = () => {}
59 |
60 | constructor (options: SpreadsheetOptions = {}) {
61 | this.formats = options.formats || formats
62 | this.fonts = options.fonts || fonts
63 | this.formulas = options.formulas || formulas
64 | // init data
65 | this.data = {rowHeight: 22, colWidth: 100, cell: defaultCell}
66 | if (options.data) {
67 | const { data } = options;
68 | for (let prop of ['rowHeight', 'colWidth', 'rows', 'cols', 'cells']) {
69 | if (data[prop]) {
70 | this.data[prop] = data[prop];
71 | }
72 | }
73 | (Object).assign(this.data.cell, data.cell || {});
74 | }
75 | }
76 |
77 | // build select
78 | buildSelect (startTarget: any, endTarget: any) {
79 | const startAttrs = getElementAttrs(startTarget)
80 | const endAttrs = getElementAttrs(endTarget)
81 | // console.log(':::::::>>>', startAttrs, endAttrs)
82 | let sRow = startAttrs.row
83 | let sCol = startAttrs.col
84 | let eRow = endAttrs.row
85 | let eCol = endAttrs.col
86 | if (sRow > eRow) {
87 | sRow = endAttrs.row
88 | eRow = startAttrs.row
89 | }
90 | if (sCol > eCol) {
91 | sCol = endAttrs.col
92 | eCol = startAttrs.col
93 | }
94 | // calc min, max of row
95 | // console.log('s: ', sRow, sCol, ', e: ', eRow, eCol)
96 | let [minRow, maxRow] = calcMinMaxRow((r: number, c: number) => this.getCell(r, c), sRow, eRow, sCol, eCol)
97 | // console.log('minRow: ', minRow, ', maxRow: ', maxRow)
98 | // calc min, max of col
99 | let [minCol, maxCol] = calcMinMaxCol((r: number, c: number) => this.getCell(r, c), minRow, maxRow, sCol, eCol)
100 | while (true) {
101 | const [minr, maxr] = calcMinMaxRow((r: number, c: number) => this.getCell(r, c), minRow, maxRow, minCol, maxCol)
102 | let [minc, maxc] = calcMinMaxCol((r: number, c: number) => this.getCell(r, c), minRow, maxRow, minCol, maxCol)
103 | if (minRow === minr && maxRow === maxr && minCol === minc && maxCol === maxc) {
104 | break
105 | }
106 | minRow = minr
107 | maxRow = maxr
108 | minCol = minc
109 | maxCol = maxc
110 | }
111 | const firstCell = this.getCell(minRow, minCol)
112 | // console.log('first => rowspan: ', firstCell.rowspan, ', colspan: ', firstCell.colspan)
113 | let canotMerge = minRow + (firstCell && firstCell.rowspan || 1) - 1 === maxRow && minCol + (firstCell && firstCell.colspan || 1) - 1 === maxCol
114 | // console.log('row: ', minRow, maxRow, ', col:', minCol, maxCol, canotMerge)
115 | // 计算是否可以merge
116 | this.select = new Select([minRow, minCol], [maxRow, maxCol], !canotMerge)
117 | return this.select
118 | }
119 |
120 | defaultRowHeight (): number {
121 | return this.data.rowHeight || 22
122 | }
123 |
124 | defaultColWidth (): number {
125 | return this.data.colWidth || 100
126 | }
127 |
128 | copy (): void {
129 | this.copySelect = this.select
130 | }
131 | cut (): void {
132 | this.cutSelect = this.select
133 | }
134 | paste (cb: StandardCallback, state: 'copy' | 'cut' | 'copyformat', clear: StandardCallback): void {
135 | let cselect = this.copySelect
136 | if (this.cutSelect) {
137 | cselect = this.cutSelect
138 | this.cutSelect = null
139 | }
140 | if (cselect && this.select) {
141 | const history = new History('cells')
142 | if (state === 'copyformat') {
143 | this.select.forEach((rindex, cindex, i, j, rowspan, colspan) => {
144 | if (cselect) {
145 | const srcRowIndex = cselect.rowIndex(i)
146 | const srcColIndex = cselect.colIndex(j)
147 | const [oldCell, newCell] = this.copyCell(srcRowIndex, srcColIndex, rindex, cindex, state, cb, clear)
148 | history.add([rindex, cindex], oldCell, newCell)
149 | }
150 | })
151 | } else {
152 | cselect.forEach((rindex, cindex, i, j, rowspan, colspan) => {
153 | if (this.select) {
154 | const destRowIndex = this.select.start[0] + i
155 | const destColIndex = this.select.start[1] + j
156 | const [oldCell, newCell] = this.copyCell(rindex, cindex, destRowIndex, destColIndex, state, cb, clear)
157 | history.add([destRowIndex, destColIndex], oldCell, newCell)
158 | }
159 | })
160 | }
161 | this.histories.push(history)
162 | this.change(this.data)
163 | }
164 | }
165 | insert (type: 'row' | 'col', amount: number, cb: StandardCallback) {
166 | if (this.select) {
167 | const { cells } = this.data
168 | const [srindex, scindex] = this.select.start
169 | if (!cells) return
170 |
171 | // console.log('insert.before.data:', cells)
172 | const history = new History('cells')
173 | if (type === 'row') {
174 | const newCells: MapInt> = {}
175 | Object.keys(cells).forEach(key => {
176 | let rindex = parseInt(key)
177 | let values = cells[rindex]
178 | if (srindex <= rindex) {
179 | Object.keys(values).forEach(key1 => {
180 | let cindex = parseInt(key1)
181 | // clear current cell
182 | cb(rindex, cindex, {})
183 | history.add([rindex, cindex], values[cindex], undefined)
184 |
185 | // set next cell is current celll
186 | cb(rindex + 1, cindex, values[cindex] || {})
187 | history.add([rindex + 1, cindex], this.getCell(rindex + 1, cindex), values[cindex])
188 | })
189 | }
190 | newCells[srindex <= rindex ? rindex + 1 : rindex] = cells[rindex]
191 | })
192 | this.data.cells = newCells
193 | } else if (type === 'col') {
194 | Object.keys(cells).forEach(key => {
195 | let rindex = parseInt(key)
196 | let values = cells[rindex]
197 | let newCell: MapInt = {}
198 | Object.keys(values).forEach(key1 => {
199 | let cindex = parseInt(key1)
200 | if (scindex <= cindex) {
201 | // clear 当前cell
202 | cb(rindex, cindex, {})
203 | history.add([rindex, cindex], values[cindex], undefined)
204 |
205 | // 设置下一个cell 等于当前的cell
206 | cb(rindex, cindex + 1, values[cindex] || {})
207 | history.add([rindex, cindex + 1], this.getCell(rindex, cindex + 1), values[cindex])
208 | }
209 | newCell[scindex <= cindex ? cindex + 1 : cindex] = values[cindex]
210 | })
211 | cells[rindex] = newCell
212 | })
213 | }
214 | this.histories.push(history)
215 | // console.log('insert.after.data:', this.data.cells)
216 | }
217 | }
218 |
219 | batchPaste (arrow: 'bottom' | 'top' | 'left' | 'right',
220 | startRow: number, startCol: number, stopRow: number, stopCol: number,
221 | seqCopy: boolean,
222 | cb: StandardCallback) {
223 | if (this.select) {
224 | const history = new History('cells')
225 | for (let i = startRow; i <= stopRow; i++) {
226 | for (let j = startCol; j <= stopCol; j++) {
227 | const srcRowIndex = this.select.rowIndex(i - startRow)
228 | const srcColIndex = this.select.colIndex(j - startCol)
229 | const [oldDestCell, destCell] = this.copyCell(srcRowIndex, srcColIndex, i, j, seqCopy ? 'seqCopy' : 'copy', cb, () => {})
230 | history.add([i, j], oldDestCell, destCell)
231 | }
232 | }
233 | this.histories.push(history)
234 | this.change(this.data)
235 | }
236 | }
237 | private copyCell (srcRowIndex: number, srcColIndex: number, destRowIndex: number, destColIndex: number,
238 | state: 'seqCopy' | 'copy' | 'cut' | 'copyformat', cb: StandardCallback, clear: StandardCallback): [Cell | null, Cell | null] {
239 | const srcCell = this.getCell(srcRowIndex, srcColIndex)
240 | const rowDiff = destRowIndex - srcRowIndex
241 | const colDiff = destColIndex - srcColIndex
242 | if (srcCell) {
243 | let oldDestCell = this.getCell(destRowIndex, destColIndex)
244 | // let destCell = cellCopy(srcCell, destRowIndex - srcRowIndex, destColIndex - srcColIndex, state === 'seqCopy')
245 | const destCell = Object.assign({}, srcCell)
246 | if (srcCell.merge) {
247 | const [m1, m2] = srcCell.merge
248 | destCell.merge = [m1 + rowDiff, m2 + colDiff];
249 | }
250 |
251 |
252 | if (state === 'cut') {
253 | clear(srcRowIndex, srcColIndex, this.cell(srcRowIndex, srcColIndex, {}))
254 | }
255 | if (state === 'copyformat') {
256 | if (oldDestCell && oldDestCell.text) {
257 | destCell.text = oldDestCell.text
258 | }
259 | } else {
260 | const txt = destCell.text
261 | if (txt && !/^\s*$/.test(txt)) {
262 | if (/^\d*$/.test(txt) && state === 'seqCopy') {
263 | destCell.text = (parseInt(txt) + (destRowIndex - srcRowIndex) + (destColIndex - srcColIndex)) + ''
264 | } else if (txt.indexOf('=') !== -1) {
265 | // 如果text的内容是formula,那么需要需要修改表达式参数
266 | destCell.text = formulaReplaceParam(txt, rowDiff, colDiff)
267 | }
268 | }
269 | }
270 |
271 | cb(destRowIndex, destColIndex, this.cell(destRowIndex, destColIndex, destCell))
272 | return [oldDestCell, destCell];
273 | }
274 | return [null, null];
275 | }
276 |
277 | isRedo (): boolean {
278 | return this.histories2.length > 0
279 | }
280 | redo (cb: StandardCallback): boolean {
281 | const { histories, histories2 } = this
282 | if (histories2.length > 0) {
283 | const history = histories2.pop()
284 | if (history) {
285 | this.resetByHistory(history, cb, 'redo')
286 | histories.push(history)
287 | this.change(this.data)
288 | }
289 | }
290 | return this.isRedo()
291 | }
292 |
293 | isUndo (): boolean {
294 | return this.histories.length > 0
295 | }
296 | undo (cb: StandardCallback): boolean {
297 | const { histories, histories2 } = this
298 | // console.log('histories:', histories, histories2)
299 | if (histories.length > 0) {
300 | const history = histories.pop()
301 | if (history) {
302 | this.resetByHistory(history, cb, 'undo')
303 | histories2.push(history)
304 | this.change(this.data)
305 | }
306 | }
307 | return this.isUndo()
308 | }
309 |
310 | resetByHistory (v: History, cb: StandardCallback, state: 'undo' | 'redo') {
311 | // console.log('history: ', history)
312 | v.values.forEach(([keys, oldValue, value]) => {
313 | if (v.type === 'cells') {
314 | const v = state === 'undo' ? oldValue : value
315 | const oldCell = this.getCell(keys[0], keys[1])
316 | if (!oldCell) {
317 | if (keys.length === 3) {
318 | if (v) {
319 | const nValue: Cell = {}
320 | nValue[keys[2]] = v
321 | cb(keys[0], keys[1], this.cell(keys[0], keys[1], nValue))
322 | }
323 | } else {
324 | cb(keys[0], keys[1], this.cell(keys[0], keys[1], v || {}))
325 | }
326 | } else {
327 | if (keys.length === 3) {
328 | const nValue: Cell = {}
329 | nValue[keys[2]] = v
330 | if (v) {
331 | cb(keys[0], keys[1], this.cell(keys[0], keys[1], nValue, true))
332 | } else {
333 | cb(keys[0], keys[1], this.cell(keys[0], keys[1], mapIntFilter(oldCell, keys[2])))
334 | }
335 | } else {
336 | cb(keys[0], keys[1], this.cell(keys[0], keys[1], v || {}))
337 | }
338 | }
339 | } else {
340 | // cols, rows
341 | // const v = state === 'undo' ? oldValue : value
342 | // if (v !== null) {
343 | // this.data[v.type]
344 | // }
345 | }
346 | // console.log('keys:', keys, ', oldValue:', oldValue, ', value:', value)
347 | })
348 | }
349 |
350 | clearformat (cb: StandardCallback) {
351 | const { select } = this
352 | if (select !== null) {
353 | const history = new History('cells')
354 | select.forEach((rindex, cindex, i, j, rowspan, colspan) => {
355 | let c = this.getCell(rindex, cindex);
356 | if (c) {
357 | history.add([rindex, cindex], c, {text: c.text})
358 | c = this.cell(rindex, cindex, {text: c.text});
359 | cb(rindex, cindex, c);
360 | }
361 | });
362 | this.histories.push(history)
363 | this.change(this.data)
364 | }
365 | }
366 |
367 | /**
368 | *
369 | * @param ok 合并单元格第一个单元格(左上角)的回调函数
370 | * @param cancel 取消合并单元格第一个单元格(左上角)的回调函数
371 | * @param other 其他单元格的回调函数
372 | */
373 | merge (ok: StandardCallback, cancel: StandardCallback, other: StandardCallback): void {
374 | const { select } = this
375 | // console.log('data.before: ', this.data)
376 | if (select !== null && select.cellLen() > 1) {
377 | // merge merge: [rows[0], cols[0]]
378 | const history = new History('cells')
379 | let index = 0
380 | let firstXY: [number, number] = [0, 0]
381 | select.forEach((rindex, cindex, i, j, rowspan, colspan) => {
382 | if (index++ === 0) {
383 | firstXY = [rindex, cindex]
384 | let v: Cell = {}
385 | if (rowspan > 1) v.rowspan = rowspan
386 | if (colspan > 1) v.colspan = colspan
387 | // console.log('rowspan:', rowspan, ', colspan:', colspan, select.canMerge)
388 | if (select.canMerge) {
389 | history.add([rindex, cindex, 'rowspan'], undefined, rowspan)
390 | history.add([rindex, cindex, 'colspan'], undefined, colspan)
391 |
392 | let cell = this.cell(rindex, cindex, v, true)
393 | ok(rindex, cindex, cell)
394 | } else {
395 | const oldCell = this.getCell(rindex, cindex)
396 | if (oldCell !== null) {
397 | history.add([rindex, cindex, 'rowspan'], oldCell.rowspan, undefined)
398 | history.add([rindex, cindex, 'colspan'], oldCell.colspan, undefined)
399 |
400 | let cell = this.cell(rindex, cindex, mapIntFilter(oldCell, 'rowspan', 'colspan', 'merge'))
401 | cancel(rindex, cindex, cell)
402 | }
403 | }
404 | } else {
405 | let v: Cell = {invisible: select.canMerge}
406 | if (select.canMerge) {
407 | history.add([rindex, cindex, 'invisible'], undefined, select.canMerge)
408 |
409 | v.merge = firstXY
410 | let cell = this.cell(rindex, cindex, v, true)
411 | other(rindex, cindex, cell)
412 | } else {
413 | const oldCell = this.getCell(rindex, cindex)
414 | if (oldCell !== null) {
415 | history.add([rindex, cindex, 'invisible'], oldCell.invisible, undefined)
416 | let cell = this.cell(rindex, cindex, mapIntFilter(oldCell, 'rowspan', 'colspan', 'merge', 'invisible'))
417 | other(rindex, cindex, cell)
418 | }
419 | }
420 | }
421 | })
422 | this.histories.push(history)
423 | select.canMerge = !select.canMerge
424 | this.change(this.data)
425 | }
426 | }
427 | cellAttr (key: keyof Cell, value: any, cb: StandardCallback): void {
428 | let v: Cell= {}
429 | v[key] = value
430 | const isDefault = value === this.data.cell[key]
431 | if (this.select !== null) {
432 | const history = new History('cells')
433 | this.select.forEach((rindex, cindex) => {
434 | const oldCell = this.getCell(rindex, cindex)
435 |
436 | history.add([rindex, cindex, key], oldCell !== null ? oldCell[key] : undefined, value)
437 |
438 | let cell = this.cell(rindex, cindex, isDefault ? mapIntFilter(oldCell, key) : v, !isDefault)
439 | cb(rindex, cindex, cell)
440 |
441 | })
442 | this.histories.push(history)
443 | }
444 | this.change(this.data)
445 | }
446 | cellText (value: any, cb: StandardCallback): Cell | null {
447 | if (this.currentCellIndexes) {
448 | // this.addHistoryValues()
449 | const history = new History('cells')
450 | const [rindex, cindex] = this.currentCellIndexes
451 | const oldCell = this.getCell(rindex, cindex)
452 | history.add([rindex, cindex, 'text'], oldCell !== null ? oldCell.text : undefined, value)
453 | const cell = this.cell(rindex, cindex, {text: value}, true)
454 | cb(rindex, cindex, cell)
455 |
456 | this.histories.push(history)
457 | this.change(this.data)
458 | return cell;
459 | }
460 | return null
461 | }
462 | currentCell (indexes?: [number, number]): Cell | null {
463 | if (indexes !== undefined) {
464 | this.currentCellIndexes = indexes
465 | }
466 | const [rindex, cindex] = this.currentCellIndexes
467 | return this.getCell(rindex, cindex)
468 | }
469 |
470 | cell (rindex: number, cindex: number, v: any, isCopy = false): Cell {
471 | this.data.cells = this.data.cells || {}
472 | this.data.cells[rindex] = this.data.cells[rindex] || {}
473 | this.data.cells[rindex][cindex] = this.data.cells[rindex][cindex] || {}
474 | if (isCopy) {
475 | (Object).assign(this.data.cells[rindex][cindex], v)
476 | } else if (v) {
477 | this.data.cells[rindex][cindex] = v
478 | }
479 | return this.data.cells[rindex][cindex]
480 | }
481 |
482 | getCell (rindex: number, cindex: number): Cell | null {
483 | if (this.data.cells && this.data.cells[rindex] && this.data.cells[rindex][cindex]) {
484 | return this.data.cells[rindex][cindex];
485 | }
486 | return null;
487 | }
488 |
489 | getFont (key: string | undefined) {
490 | return this.fonts.filter(it => it.key === key)[0]
491 | }
492 | getFormat (key: string | undefined) {
493 | return this.formats.filter(it => it.key === key)[0]
494 | }
495 |
496 | row (index: number, v?: number): Row {
497 | const { data } = this;
498 | if (v !== undefined) {
499 | const history = new History('rows')
500 | data.rows = data.rows || {}
501 | data.rows[index] = data.rows[index] || {}
502 | data.rows[index].height = v
503 | history.add([index], null, data.rows[index])
504 | this.histories.push(history)
505 | }
506 | return (Object).assign({height: data.rowHeight}, data.rows ? data.rows[index] : {})
507 | }
508 | // isData 是否返回数据的最大行数
509 | rows (isData: boolean): Array {
510 | const { data } = this;
511 | let maxRow;
512 | if (isData) {
513 | maxRow = 10
514 | if (this.data.cells) {
515 | maxRow = mapIntMaxKey(this.data.cells) + 2
516 | }
517 | } else {
518 | maxRow = mapIntMaxKeyWithDefault(100, data.rows)
519 | }
520 | return range(maxRow, (index) => this.row(index))
521 | }
522 |
523 | col (index: number, v?: number): Col {
524 | const { data } = this;
525 | if (v !== undefined) {
526 | const history = new History('cols')
527 | data.cols = data.cols || {}
528 | data.cols[index] = data.cols[index] || {}
529 | data.cols[index].width = v
530 | history.add([index], null, data.cols[index])
531 | this.histories.push(history)
532 | }
533 | const ret:any = {width: data.colWidth, title: alphabet(index)}
534 | if (data.cols && data.cols[index]) {
535 | for (let prop in data.cols[index]) {
536 | const col:any = data.cols[index]
537 | if (col[prop]) {
538 | ret[prop] = col[prop]
539 | }
540 | }
541 | }
542 | return ret
543 | }
544 | cols (): Array {
545 | const { data } = this;
546 | let maxCol = mapIntMaxKeyWithDefault(26 * 2, data.cols);
547 | return range(maxCol, (index) => this.col(index));
548 | }
549 | }
550 |
551 | const mapIntMaxKey = function(mapInt: MapInt): number {
552 | return Math.max(...Object.keys(mapInt).map(s => parseInt(s)))
553 | }
554 | // methods
555 | const mapIntMaxKeyWithDefault = function(max: number, mapInt: MapInt | undefined): number {
556 | if (mapInt) {
557 | const m = mapIntMaxKey(mapInt)
558 | if (m > max) return m;
559 | }
560 | return max;
561 | }
562 | const mapIntFilter = function(obj: any, ...keys: Array): any {
563 | const ret: any = {}
564 | if (obj){
565 | Object.keys(obj).forEach(e => {
566 | if (keys.indexOf(e) === -1) {
567 | ret[e] = obj[e]
568 | }
569 | })
570 | }
571 | return ret
572 | }
573 | const range = function(stop:number, cb: (index: number) => T): Array {
574 | const ret = []
575 | for (let i = 0; i < stop; i++) {
576 | ret.push(cb(i))
577 | }
578 | return ret
579 | }
580 | const getElementAttrs = (target: any) => {
581 | const { offsetTop, offsetLeft, offsetHeight, offsetWidth } = target
582 | return {
583 | row: parseInt(target.getAttribute('row-index')),
584 | col: parseInt(target.getAttribute('col-index')),
585 | rowspan: parseInt(target.getAttribute('rowspan')),
586 | colspan: parseInt(target.getAttribute('colspan')),
587 | left: offsetLeft,
588 | top: offsetTop,
589 | width: offsetWidth,
590 | height: offsetHeight
591 | }
592 | }
593 | const calcMinMaxCol = (cell: any, sRow: number, eRow: number, sCol: number, eCol: number) => {
594 | let minCol = sCol
595 | let maxCol = eCol
596 | // console.log(':::::::;start: ', maxCol, minCol)
597 | for (let j = sRow; j <= eRow; j++) {
598 | let cCol = sCol
599 | let dcell = cell(j, cCol)
600 | if (dcell && dcell.merge) {
601 | cCol += dcell.merge[1] - cCol
602 | }
603 | if (cCol < minCol) minCol = cCol
604 |
605 | cCol = maxCol
606 | dcell = cell(j, cCol)
607 | // console.log(j, cCol, dcell && dcell.colspan || 1)
608 | const cColspan = dcell ? dcell.colspan : 1
609 | if (parseInt(cColspan) > 1) {
610 | cCol += parseInt(cColspan)
611 | } else {
612 | if (dcell && dcell.merge) {
613 | // console.log('merge::', maxCol, dcell.merge)
614 | const [r, c] = dcell.merge
615 | const rc = cell(r, c).colspan
616 | cCol += rc + (c - cCol)
617 | }
618 | }
619 | // console.log('cCol: ', cCol, ', maxCol: ', maxCol)
620 | // console.log(':::::::;end: ', maxCol, minCol)
621 | if (cCol - 1 > maxCol) maxCol = cCol - 1
622 | }
623 | return [minCol, maxCol]
624 | }
625 | const calcMinMaxRow = (cell: any, sRow: number, eRow: number, sCol: number, eCol: number) => {
626 | let minRow = sRow
627 | let maxRow = eRow
628 | for (let j = sCol; j <= eCol; j++) {
629 | let cRow = sRow
630 | let dcell = cell(cRow, j)
631 | if (dcell && dcell.merge) {
632 | cRow += dcell.merge[0] - cRow
633 | }
634 | if (cRow < minRow) minRow = cRow
635 |
636 | cRow = maxRow
637 | dcell = cell(cRow, j)
638 | // console.log('row: ', j, cRow, dcell.rowspan)
639 | const cRowspan = dcell ? dcell.rowspan : 1
640 | if (parseInt(cRowspan) > 1) {
641 | cRow += parseInt(cRowspan)
642 | } else {
643 | if (dcell && dcell.merge) {
644 | const [r, c] = dcell.merge
645 | const rs = cell(r, c).rowspan
646 | cRow += rs + (r - cRow)
647 | }
648 | }
649 | if (cRow - 1 > maxRow) maxRow = cRow - 1
650 | }
651 | return [minRow, maxRow]
652 | }
--------------------------------------------------------------------------------
/src/core/select.d.ts:
--------------------------------------------------------------------------------
1 | export declare class Select {
2 | start: [number, number];
3 | stop: [number, number];
4 | canMerge: boolean;
5 | constructor(start: [number, number], stop: [number, number], canMerge: boolean);
6 | forEach(cb: (r: number, c: number, rindex: number, cindex: number, rowspan: number, colspan: number) => void): void;
7 | rowIndex(index: number): number;
8 | colIndex(index: number): number;
9 | rowLen(): number;
10 | colLen(): number;
11 | cellLen(): number;
12 | contains(rindex: number, cindex: number): boolean;
13 | }
14 |
--------------------------------------------------------------------------------
/src/core/select.ts:
--------------------------------------------------------------------------------
1 | export class Select {
2 | constructor(public start: [number, number], public stop: [number, number], public canMerge: boolean) {}
3 | forEach (cb: (r:number, c: number, rindex: number, cindex: number, rowspan: number, colspan: number) => void): void {
4 | const [sx, sy] = this.start
5 | const [ex, ey] = this.stop
6 | for (let i = sx; i <= ex; i++) {
7 | for (let j = sy; j <= ey; j++) {
8 | cb(i, j, i - sx, j - sy, ex - sx + 1, ey - sy + 1)
9 | }
10 | }
11 | }
12 | rowIndex (index: number) {
13 | return this.start[0] + index % this.rowLen()
14 | }
15 | colIndex (index: number) {
16 | return this.start[1] + index % this.colLen()
17 | }
18 | rowLen () {
19 | return this.stop[0] - this.start[0] + 1
20 | }
21 | colLen () {
22 | return this.stop[1] - this.start[1] + 1
23 | }
24 | cellLen () {
25 | return this.rowLen() * this.colLen()
26 | }
27 | contains (rindex: number, cindex: number) {
28 | const [sx, sy] = this.start
29 | const [ex, ey] = this.stop
30 | return sx <= rindex && ex >= rindex && sy <= cindex && ey >= cindex
31 | }
32 | }
33 |
34 | // export function buildSelect (start: any, end: )
--------------------------------------------------------------------------------
/src/local/base/colorPanel.d.ts:
--------------------------------------------------------------------------------
1 | import { Element } from "./element";
2 | export declare class ColorPanel extends Element {
3 | constructor(click: (color: string) => void);
4 | }
5 | export declare function buildColorPanel(click: (color: string) => void): ColorPanel;
6 |
--------------------------------------------------------------------------------
/src/local/base/colorPanel.ts:
--------------------------------------------------------------------------------
1 | import { Element, h } from "./element";
2 |
3 | const colorss = [
4 | ['#c00000', '#ff0000', '#ffc003', '#ffff00','#91d051', '#00af50', '#00b0f0', '#0070c0', '#002060', '#70309f'],
5 | ['#ffffff', '#000000', '#e7e6e6', '#44546a', '#4472c4', '#ed7d31', '#a5a5a5', '#ffc003', '#5b9bd5', '#70ad47'],
6 | ['#f4f5f8', '#848484', '#d0cece', '#d6dce4', '#d9e2f2', '#fae5d5', '#ededed', '#fff2cc', '#deebf6', '#e2efd9'],
7 | ['#d8d8d8', '#595959', '#afabab', '#adb9ca', '#b4c6e7', '#f7cbac', '#dbdbdb', '#fee598', '#bdd7ee', '#c5e0b3'],
8 | ['#bfbfbf', '#3f3f3f', '#757070', '#8496b0', '#8eaad8', '#f4b183', '#c9c9c9', '#ffd964', '#9dc2e5', '#a8d08d'],
9 | ['#a5a5a5', '#262626', '#3a3838', '#333f4f', '#2f5496', '#c55b11', '#7b7b7b', '#bf9001', '#2e75b5', '#538135'],
10 | ['#7e7e7e', '#0c0c0c', '#171616', '#232a35', '#1e3864', '#833d0b', '#525252', '#7e6000', '#1f4e79', '#375623']
11 | ]
12 |
13 | export class ColorPanel extends Element {
14 |
15 | constructor (click: (color: string) => void) {
16 | super();
17 | this.class('spreadsheet-color-panel')
18 | .child(
19 | h('table').child(
20 | h('tbody').children(
21 | colorss.map(colors => {
22 | return h('tr').children(
23 | colors.map(color => {
24 | return h('td').child(
25 | h()
26 | .class('color-cell')
27 | .on('click', click.bind(null, color))
28 | .style('background-color', color)
29 | )
30 | })
31 | )
32 | })
33 | )));
34 | }
35 |
36 | }
37 |
38 | export function buildColorPanel (click: (color: string) => void) {
39 | return new ColorPanel(click);
40 | }
--------------------------------------------------------------------------------
/src/local/base/dropdown.d.ts:
--------------------------------------------------------------------------------
1 | import { Element } from "./element";
2 | export declare class Dropdown extends Element {
3 | content: Element;
4 | title: Element;
5 | constructor(title: string | Element, width: string, contentChildren: Element[]);
6 | toggleHandler(evt: Event): void;
7 | }
8 | export declare function buildDropdown(title: string | Element, width: string, contentChildren: Element[]): Dropdown;
9 |
--------------------------------------------------------------------------------
/src/local/base/dropdown.ts:
--------------------------------------------------------------------------------
1 | import { Element, h } from "./element";
2 | import { buildIcon } from "./icon";
3 |
4 | export class Dropdown extends Element {
5 | content: Element;
6 | title: Element;
7 |
8 | constructor (title: string | Element, width: string, contentChildren: Element[]) {
9 | super();
10 | this.class('spreadsheet-dropdown spreadsheet-item');
11 |
12 | this.content = h().class('spreadsheet-dropdown-content')
13 | .children(contentChildren)
14 | .onClickOutside(() => this.deactive())
15 | .on('click', (evt) => this.toggleHandler(evt))
16 | .style('width', width).hide();
17 |
18 | this.child(h().class('spreadsheet-dropdown-header').children([
19 | this.title = typeof title === 'string' ? h().class('spreadsheet-dropdown-title').child(title) : title,
20 | h().class('spreadsheet-dropdown-icon').on('click', (evt) => this.toggleHandler(evt)).child(buildIcon('arrow-down'))
21 | ])).child(this.content);
22 | }
23 |
24 | toggleHandler (evt: Event) {
25 | if (this.content.isHide()){
26 | this.content.show()
27 | this.active()
28 | } else {
29 | this.content.hide()
30 | this.deactive()
31 | }
32 | }
33 | }
34 | export function buildDropdown(title: string | Element, width: string, contentChildren: Element[]) {
35 | return new Dropdown(title, width, contentChildren)
36 | }
--------------------------------------------------------------------------------
/src/local/base/element.d.ts:
--------------------------------------------------------------------------------
1 | export declare class Element {
2 | tag: string;
3 | el: HTMLElement;
4 | _data: {
5 | [key: string]: any;
6 | };
7 | _clickOutside: any;
8 | constructor(tag?: string);
9 | data(key: string, value?: any): any;
10 | on(eventName: string, handler: (evt: any) => any): Element;
11 | onClickOutside(cb: () => void): Element;
12 | parent(): any;
13 | class(name: string): Element;
14 | attrs(map?: {
15 | [key: string]: string;
16 | }): Element;
17 | attr(attr: string, value?: any): any;
18 | removeAttr(attr: string): Element;
19 | offset(): any;
20 | clearStyle(): this;
21 | styles(map?: {
22 | [key: string]: string;
23 | }, isClear?: boolean): Element;
24 | style(key: string, value?: any): any;
25 | contains(el: any): boolean;
26 | removeStyle(key: string): void;
27 | children(cs: Array): Element;
28 | child(c: HTMLElement | string | Element): Element;
29 | html(html?: string): string | this;
30 | val(v?: string): any;
31 | clone(): any;
32 | isHide(): boolean;
33 | toggle(): void;
34 | disabled(): Element;
35 | able(): Element;
36 | active(flag?: boolean): Element;
37 | deactive(): Element;
38 | isActive(): boolean;
39 | addClass(cls: string): Element;
40 | removeClass(cls: string): this;
41 | hasClass(cls: string): boolean;
42 | show(isRemove?: boolean): Element;
43 | hide(): Element;
44 | }
45 | export declare function h(tag?: string): Element;
46 |
--------------------------------------------------------------------------------
/src/local/base/element.ts:
--------------------------------------------------------------------------------
1 | import { bind, unbind } from '../event'
2 |
3 | export class Element {
4 | el: HTMLElement;
5 | _data: {[key: string]: any} = {};
6 | _clickOutside: any = null;
7 |
8 | constructor (public tag = 'div') {
9 | this.el = document.createElement(tag)
10 | }
11 |
12 | data (key: string, value?: any) {
13 | if (value !== undefined) {
14 | this._data[key] = value
15 | }
16 | return this._data[key]
17 | }
18 |
19 | on (eventName: string, handler: (evt: any) => any): Element {
20 | const [first, ...others] = eventName.split('.')
21 | // console.log('first:', first, ', others:', others)
22 | this.el.addEventListener(first, (evt: any) => {
23 | // console.log('>>>', others, evt.button)
24 | for (let k of others) {
25 | console.log('::::::::::', k)
26 | if (k === 'left' && evt.button !== 0) {
27 | return
28 | } else if (k === 'right' && evt.button !== 2) {
29 | return
30 | } else if (k === 'stop') {
31 | evt.stopPropagation()
32 | }
33 | }
34 | // console.log('>>>>>>>>>>>>')
35 | handler(evt)
36 | })
37 | return this;
38 | }
39 |
40 | onClickOutside (cb: () => void): Element {
41 | this._clickOutside = cb
42 | return this;
43 | }
44 |
45 | parent(): any {
46 | return this.el.parentNode
47 | }
48 |
49 | class (name: string): Element {
50 | this.el.className = name
51 | return this;
52 | }
53 |
54 | attrs (map: {[key: string]: string} = {}): Element {
55 | for (let key of Object.keys(map))
56 | this.attr(key, map[key]);
57 | return this;
58 | }
59 |
60 | attr (attr: string, value?: any): any {
61 | if (value !== undefined) {
62 | this.el.setAttribute(attr, value);
63 | } else {
64 | return this.el.getAttribute(attr)
65 | }
66 | return this;
67 | }
68 | removeAttr(attr: string): Element {
69 | this.el.removeAttribute(attr);
70 | return this;
71 | }
72 |
73 | offset (): any {
74 | const { offsetTop, offsetLeft, offsetHeight, offsetWidth } = this.el
75 | return {top: offsetTop, left: offsetLeft, height: offsetHeight, width: offsetWidth}
76 | }
77 |
78 | clearStyle () {
79 | (this.el).style = ''
80 | return this;
81 | }
82 |
83 | styles (map: {[key: string]: string} = {}, isClear = false): Element {
84 | if (isClear) {
85 | this.clearStyle()
86 | }
87 | for (let key of Object.keys(map))
88 | this.style(key, map[key]);
89 | return this;
90 | }
91 |
92 | style (key: string, value?: any): any {
93 | if (value !== undefined) {
94 | this.el.style.setProperty(key, value);
95 | } else {
96 | return this.el.style.getPropertyValue(key)
97 | }
98 | return this;
99 | }
100 |
101 | contains (el: any) {
102 | return this.el.contains(el)
103 | }
104 |
105 | removeStyle (key: string) {
106 | this.el.style.removeProperty(key)
107 | return ;
108 | }
109 |
110 | children (cs: Array): Element {
111 | for (let c of cs)
112 | this.child(c);
113 | return this;
114 | }
115 |
116 | child (c: HTMLElement | string | Element): Element {
117 | if (typeof c === 'string') {
118 | this.el.appendChild(document.createTextNode(c))
119 | } else if (c instanceof Element) {
120 | this.el.appendChild(c.el)
121 | } else if (c instanceof HTMLElement) {
122 | this.el.appendChild(c)
123 | }
124 | return this;
125 | }
126 |
127 | html (html?: string) {
128 | if (html !== undefined) {
129 | this.el.innerHTML = html
130 | } else {
131 | return this.el.innerHTML
132 | }
133 | return this;
134 | }
135 |
136 | val (v?: string) {
137 | if (v !== undefined) {
138 | // (this.el).value = v
139 | (this.el).value = v
140 | } else {
141 | return (this.el).value
142 | }
143 | return this;
144 | }
145 |
146 | clone (): any {
147 | return this.el.cloneNode();
148 | }
149 |
150 | isHide () {
151 | return this.style('display') === 'none'
152 | }
153 |
154 | toggle () {
155 | if (this.isHide()) {
156 | this.show()
157 | } else {
158 | this.hide()
159 | }
160 | }
161 |
162 | disabled (): Element {
163 | // this.removeClass('disabled')
164 | this.addClass('disabled')
165 | return this;
166 | }
167 | able (): Element {
168 | this.removeClass('disabled')
169 | return this;
170 | }
171 |
172 | active (flag = true): Element {
173 | // this.el.className = this.el.className.split(' ').filter(c => c !== 'disabled').join(' ') + ' active'
174 | // this.removeClass('disabled')
175 | if (flag)
176 | this.addClass('active')
177 | else
178 | this.deactive()
179 | return this;
180 | }
181 | deactive (): Element {
182 | return this.removeClass('active')
183 | }
184 | isActive (): boolean {
185 | return this.hasClass('active');
186 | }
187 |
188 | addClass (cls: string): Element {
189 | this.el.className = this.el.className.split(' ').concat(cls).join(' ')
190 | return this;
191 | }
192 | removeClass (cls: string) {
193 | // console.log('before.className: ', this.el.className)
194 | this.el.className = this.el.className.split(' ').filter(c => c !== cls).join(' ')
195 | // console.log('after.className: ', this.el.className)
196 | return this;
197 | }
198 | hasClass (cls: string) {
199 | return this.el.className.indexOf(cls) !== -1
200 | }
201 |
202 | show (isRemove = false): Element {
203 | isRemove ? this.removeStyle('display') : this.style('display', 'block');
204 | // clickoutside
205 | if (this._clickOutside) {
206 | this.data('_outsidehandler', (evt: Event) => {
207 | if (this.contains(evt.target)) {
208 | return false
209 | }
210 | this.hide()
211 | unbind('click', this.data('_outsidehandler'))
212 | this._clickOutside && this._clickOutside()
213 | })
214 | setTimeout(() => {
215 | bind('click', this.data('_outsidehandler'))
216 | }, 0)
217 | }
218 | return this;
219 | }
220 |
221 | hide (): Element {
222 | this.style('display', 'none');
223 | if (this._clickOutside) {
224 | unbind('click', this.data('_outsidehandler'))
225 | }
226 | return this;
227 | }
228 | }
229 |
230 | export function h (tag = 'div'): Element {
231 | return new Element(tag)
232 | }
--------------------------------------------------------------------------------
/src/local/base/icon.d.ts:
--------------------------------------------------------------------------------
1 | import { Element } from "./element";
2 | export declare class Icon extends Element {
3 | img: Element;
4 | constructor(name: string);
5 | replace(name: string): void;
6 | }
7 | export declare function buildIcon(name: string): Icon;
8 |
--------------------------------------------------------------------------------
/src/local/base/icon.ts:
--------------------------------------------------------------------------------
1 | import { Element, h } from "./element";
2 |
3 | export class Icon extends Element{
4 |
5 | img: Element;
6 |
7 | constructor (name: string) {
8 | super();
9 | this.class('spreadsheet-icon').child(this.img = h().class(`spreadsheet-icon-img ${name}`));
10 | }
11 |
12 | replace (name: string) {
13 | this.img.class(`spreadsheet-icon-img ${name}`)
14 | }
15 |
16 | }
17 |
18 | export function buildIcon (name: string) {
19 | return new Icon(name);
20 | }
--------------------------------------------------------------------------------
/src/local/base/item.d.ts:
--------------------------------------------------------------------------------
1 | import { Element } from "./element";
2 | import { Icon } from "./icon";
3 | export declare class Item extends Element {
4 | iconEl: Icon | null;
5 | static build(): Item;
6 | constructor();
7 | icon(name: string): this;
8 | replaceIcon(name: string): void;
9 | }
10 | export declare function buildItem(): Item;
11 |
--------------------------------------------------------------------------------
/src/local/base/item.ts:
--------------------------------------------------------------------------------
1 | import { Element } from "./element";
2 | import { Icon, buildIcon } from "./icon";
3 |
4 | export class Item extends Element {
5 |
6 | iconEl: Icon | null = null;
7 |
8 | static build (): Item {
9 | return new Item()
10 | }
11 |
12 | constructor () {
13 | super();
14 | this.class('spreadsheet-item');
15 | }
16 |
17 | icon (name: string) {
18 | this.child(this.iconEl = buildIcon(name))
19 | return this;
20 | }
21 |
22 | replaceIcon (name: string) {
23 | this.iconEl && this.iconEl.replace(name)
24 | }
25 |
26 | }
27 |
28 | export function buildItem (): Item {
29 | return new Item();
30 | }
--------------------------------------------------------------------------------
/src/local/base/menu.d.ts:
--------------------------------------------------------------------------------
1 | import { Element } from "./element";
2 | export declare class Menu extends Element {
3 | constructor(align?: string);
4 | }
5 | export declare function buildMenu(align?: string): Menu;
6 |
--------------------------------------------------------------------------------
/src/local/base/menu.ts:
--------------------------------------------------------------------------------
1 | import { Element } from "./element";
2 |
3 | export class Menu extends Element{
4 |
5 | constructor (align = 'vertical') {
6 | super();
7 | this.class(`spreadsheet-menu ${align}`)
8 | }
9 |
10 | }
11 |
12 | export function buildMenu (align = 'vertical') {
13 | return new Menu(align);
14 | }
--------------------------------------------------------------------------------
/src/local/base/suggest.d.ts:
--------------------------------------------------------------------------------
1 | import { Element } from "./element";
2 | export declare class Suggest extends Element {
3 | list: Array<[string, string]>;
4 | width: number;
5 | filterList: Array;
6 | currentIndex: number;
7 | target: Element | null;
8 | evtTarget: Element | null;
9 | itemClick: (it: [string, string]) => void;
10 | constructor(list: Array<[string, string]>, width: number);
11 | private documentHandler;
12 | private documentKeydownHandler;
13 | private hideAndRemoveEvents;
14 | private removeEvents;
15 | private clickItemHandler;
16 | search(target: Element, input: Element, word: string): void;
17 | }
18 |
--------------------------------------------------------------------------------
/src/local/base/suggest.ts:
--------------------------------------------------------------------------------
1 | import { Element, h } from "./element";
2 | import { buildItem } from "./item";
3 | import { buildMenu } from "./menu";
4 | import { bind, unbind } from "../event";
5 |
6 | export class Suggest extends Element {
7 |
8 | filterList: Array = [];
9 | currentIndex = 0;
10 | target: Element | null = null;
11 | evtTarget: Element | null = null;
12 |
13 | itemClick: (it: [string, string]) => void = (it) => {}
14 |
15 | constructor (public list: Array<[string, string]>, public width: number) {
16 | super();
17 | this.class('spreadsheet-suggest').hide()
18 | }
19 |
20 | private documentHandler (e: any) {
21 | if (this.el.contains(e.target)) {
22 | return false
23 | }
24 | this.hideAndRemoveEvents()
25 | }
26 | private documentKeydownHandler (e: any) {
27 | console.log('keyCode: ', e)
28 | if (this.filterList.length <= 0 && e.target.type !== 'textarea') return ;
29 |
30 | switch (e.keyCode) {
31 | case 37: // left
32 | e.returnValue = false
33 | break;
34 | case 38: // up
35 | this.filterList[this.currentIndex].deactive()
36 | this.currentIndex--
37 | if (this.currentIndex < 0) {
38 | this.currentIndex = this.filterList.length - 1
39 | }
40 | this.filterList[this.currentIndex].active()
41 | e.returnValue = false
42 | e.stopPropagation();
43 | break;
44 | case 39: // right
45 | e.returnValue = false
46 | break;
47 | case 40: // down
48 | this.filterList[this.currentIndex].deactive()
49 | this.currentIndex++
50 | if (this.currentIndex > this.filterList.length - 1) {
51 | this.currentIndex = 0
52 | }
53 | this.filterList[this.currentIndex].active()
54 | e.returnValue = false
55 | break;
56 | case 13: // enter
57 | this.filterList[this.currentIndex].el.click()
58 | e.returnValue = false
59 | break;
60 | }
61 | e.stopPropagation();
62 | }
63 |
64 | private hideAndRemoveEvents () {
65 | this.hide()
66 | this.removeEvents();
67 | }
68 | private removeEvents () {
69 | if (this.evtTarget !== null) {
70 | unbind('click', this.data('_outsidehandler'), this.evtTarget.el)
71 | unbind('keydown', this.data('_keydownhandler'), this.evtTarget.el)
72 | }
73 | }
74 |
75 | private clickItemHandler (it: [string, string]) {
76 | // console.log('click.it: ', it)
77 | this.itemClick(it)
78 | this.hideAndRemoveEvents()
79 | }
80 |
81 |
82 | search (target: Element, input: Element, word: string) {
83 | this.removeEvents()
84 | this.target = target;
85 | this.evtTarget = input;
86 |
87 | const { left, top, width, height } = target.offset()
88 | this.styles({left: `${left}px`, top: `${top + height + 2}px`, width: `${this.width}px`})
89 |
90 | let lis: any = this.list
91 | if (!/^\s*$/.test(word)) {
92 | lis = this.list.filter(it => it[0].startsWith(word.toUpperCase()))
93 | }
94 | lis = lis.map((it: [string, string]) => {
95 | const item = buildItem().on('click', (evt) => this.clickItemHandler(it)).child(it[0])
96 | if (it[1]) {
97 | item.child(h().class('label').html(it[1]))
98 | }
99 | return item
100 | // return `${it[0]}${it[1] ? ' '+it[1]+' ' : ''} `
101 | })
102 |
103 | this.filterList = lis
104 | this.currentIndex = 0
105 |
106 | if (lis.length <= 0) {
107 | lis = [buildItem().child('No Result')] // `No Result `
108 | } else {
109 | lis[0].active()
110 |
111 | // clickoutside
112 | this.data('_outsidehandler', (evt: Event) => {
113 | this.documentHandler(evt)
114 | })
115 | this.data('_keydownhandler', (evt: any) => this.documentKeydownHandler(evt))
116 | setTimeout(() => {
117 | if (this.evtTarget !== null) {
118 | bind('click', this.data('_outsidehandler'), this.evtTarget.el)
119 | bind('keydown', this.data('_keydownhandler'), this.evtTarget.el)
120 | }
121 | }, 0)
122 | }
123 | this.html(``)
124 | this.child(buildMenu().children(lis)).show()
125 | }
126 | }
--------------------------------------------------------------------------------
/src/local/contextmenu.d.ts:
--------------------------------------------------------------------------------
1 | import { Element } from "./base/element";
2 | import { Table } from "./table";
3 | export declare class ContextMenu {
4 | table: Table;
5 | el: Element;
6 | constructor(table: Table);
7 | set(evt: any): void;
8 | }
9 |
--------------------------------------------------------------------------------
/src/local/contextmenu.ts:
--------------------------------------------------------------------------------
1 | import { Element, h } from "./base/element";
2 | import { buildItem } from "./base/item";
3 | import { buildMenu } from "./base/menu";
4 | import { Table } from "./table"
5 |
6 | export class ContextMenu {
7 | el: Element;
8 | constructor (public table: Table) {
9 | this.el = h().class('spreadsheet-contextmenu')
10 | .style('width', '160px')
11 | .on('click', (evt: any) => this.el.hide())
12 | .children([
13 | buildMenu().children([
14 | buildItem().on('click', (evt) => table.copy()).children(['copy', h().class('label').html('ctrl + c')]),
15 | buildItem().on('click', (evt) => table.cut()).children(['cut', h().class('label').html('ctrl + x')]),
16 | buildItem().on('click', (evt) => table.paste()).children(['paste', h().class('label').html('ctrl + v')]),
17 | // h().class('spreadsheet-item-separator'),
18 | // buildItem().on('click', (evt) => table.insert('row', 1)).html('insert row'),
19 | // buildItem().on('click', (evt) => table.insert('col', 1)).html('insert col')
20 | ])
21 | ]).onClickOutside(() => {}).hide()
22 | // clickoutside
23 | }
24 |
25 | set (evt: any) {
26 | const { offsetLeft, offsetTop } = evt.target
27 | const elRect = this.el.el.getBoundingClientRect()
28 | // cal left top
29 | const { clientWidth, clientHeight } = document.documentElement
30 | let top = offsetTop + evt.offsetY
31 | let left = offsetLeft + evt.offsetX
32 |
33 | if (evt.clientY > clientHeight / 1.5) {
34 | top -= elRect.height
35 | }
36 | if (evt.clientX > clientWidth / 1.5) {
37 | left -= elRect.width
38 | }
39 | this.el.style('left', `${left}px`).style('top', `${top}px`).show()
40 | }
41 |
42 | }
--------------------------------------------------------------------------------
/src/local/editor.d.ts:
--------------------------------------------------------------------------------
1 | import { Element } from "./base/element";
2 | import { Suggest } from "./base/suggest";
3 | import { Cell } from "../core/cell";
4 | import { Formula } from "../core/formula";
5 | export declare class Editor {
6 | defaultRowHeight: number;
7 | formulas: Array;
8 | el: Element;
9 | target: HTMLElement | null;
10 | value: Cell | null;
11 | editor: Element;
12 | textarea: Element;
13 | textline: Element;
14 | suggest: Suggest;
15 | change: (v: Cell) => void;
16 | constructor(defaultRowHeight: number, formulas: Array);
17 | onChange(change: (v: Cell) => void): void;
18 | set(target: HTMLElement, value: Cell | null): void;
19 | setValue(value: Cell | null): string;
20 | setStyle(value: Cell | null): void;
21 | clear(): void;
22 | private setTextareaRange;
23 | private inputKeydown;
24 | private inputChange;
25 | private autocomplete;
26 | reload(): void;
27 | }
28 |
--------------------------------------------------------------------------------
/src/local/editor.ts:
--------------------------------------------------------------------------------
1 | import { Element, h } from "./base/element";
2 | import { Suggest } from "./base/suggest";
3 | import { Cell, getStyleFromCell } from "../core/cell"
4 | import { Formula } from "../core/formula";
5 |
6 | export class Editor {
7 | el: Element;
8 | target: HTMLElement | null = null; // 选中的当前的element
9 | value: Cell | null = null; // 选中的当前的cell
10 | editor: Element;
11 | textarea: Element;
12 | textline: Element; // 计算输入文本的宽度用的element
13 | suggest: Suggest; // autocomplete show
14 |
15 | change: (v: Cell) => void = (v) => {};
16 | constructor (public defaultRowHeight: number, public formulas : Array) {
17 | const suggestList: any = formulas.map(it => [it.key, it.title])
18 | this.el = h().children([this.editor = h().class('spreadsheet-editor').children([
19 | this.textarea = h('textarea')
20 | .on('keydown', (evt: any) => this.inputKeydown(evt))
21 | .on('input', (evt: Event) => this.inputChange(evt)),
22 | this.textline = h().styles({visibility: 'hidden', overflow: 'hidden', position: 'fixed', top: '0', left: '0'})
23 | ])
24 | , this.suggest = new Suggest(suggestList, 180)]).hide()
25 |
26 | this.el.on('keydown', (evt: any) => {
27 | if (evt.keyCode !== 13 && evt.keyCode !== 9) {
28 | evt.stopPropagation();
29 | }
30 | })
31 |
32 | this.suggest.itemClick = (it) => {
33 | // console.log('>>>>>>>>>>>>', it)
34 | const text = `=${it[0]}()`;
35 | if (this.value) {
36 | this.value.text = text
37 | }
38 | this.textarea.val(text);
39 | this.textline.html(text);
40 | this.setTextareaRange(text.length - 1)
41 | // (this.textarea.el).setSelectionRange(text.length + 1, text.length + 1);
42 | // setTimeout(() => (this.textarea.el).focus(), 10)
43 | }
44 | }
45 |
46 | onChange (change: (v: Cell) => void) {
47 | this.change = change
48 | }
49 |
50 | set (target: HTMLElement, value: Cell | null) {
51 | // console.log('set::>>')
52 | this.target = target;
53 | const text = this.setValue(value)
54 | this.el.show();
55 | this.setTextareaRange(text.length)
56 | // (this.textarea.el).setSelectionRange(text.length, text.length);
57 | // setTimeout(() => (this.textarea.el).focus(), 10)
58 | this.reload();
59 | }
60 |
61 | setValue (value: Cell | null): string {
62 | this.setStyle(value);
63 | if (value) {
64 | this.value = value;
65 | const text = value.text || '';
66 | this.textarea.val(text);
67 | this.textline.html(text);
68 | return text
69 | } else {
70 | return '';
71 | }
72 | }
73 | setStyle (value: Cell | null): void {
74 | let attrs = {width: this.textarea.style('width'), height: this.textarea.style('height')}
75 | this.textarea.styles(Object.assign(attrs, getStyleFromCell(value)), true)
76 | }
77 |
78 | clear () {
79 | // console.log('clear:>>>')
80 | this.el.hide();
81 | this.target = null;
82 | this.value = null;
83 | this.textarea.val('')
84 | this.textline.html('')
85 | }
86 |
87 | private setTextareaRange (position: number) {
88 | setTimeout(() => {
89 | (this.textarea.el).setSelectionRange(position, position);
90 | (this.textarea.el).focus()
91 | }, 10)
92 | }
93 |
94 | private inputKeydown (evt: any) {
95 | if (evt.keyCode === 13) {
96 | evt.preventDefault()
97 | }
98 | }
99 |
100 | private inputChange (evt: any) {
101 | const v = evt.target.value
102 | if (this.value) {
103 | this.value.text = v
104 | } else {
105 | this.value = {text: v}
106 | }
107 | this.change(this.value)
108 | this.autocomplete(v);
109 |
110 | this.textline.html(v);
111 | this.reload()
112 |
113 | }
114 |
115 | private autocomplete (v: string) {
116 | if (v[0] === '=') {
117 | if (!v.includes('(')) {
118 | const search = v.substring(1)
119 | console.log(':::;search word:', search)
120 | this.suggest.search(this.editor, this.textarea, search);
121 | } else {
122 | this.suggest.hide()
123 | }
124 | } else {
125 | this.suggest.hide()
126 | }
127 | }
128 |
129 | reload () {
130 | // setTimeout(() => {
131 | if (this.target) {
132 | const { offsetTop, offsetLeft, offsetWidth, offsetHeight } = this.target
133 | this.editor.styles({left: `${offsetLeft - 1}px`, top: `${offsetTop - 1}px`})
134 | this.textarea.styles({width: `${offsetWidth - 8}px`, height: `${offsetHeight - 2}px`})
135 | let ow = this.textline.offset().width + 16
136 | // console.log(maxWidth, ow, '>>>>')
137 | if (this.value) {
138 | if (this.value.wordWrap) {
139 | // 如果单元格自动换行,那么宽度固定,高度变化
140 | // this.textarea.style('height', 'auto');
141 | const h = (parseInt(ow / offsetWidth + '') + (ow % offsetWidth > 0 ? 1 : 0)) * this.defaultRowHeight;
142 | if (h > offsetHeight) {
143 | this.textarea.style('height', `${h}px`);
144 | }
145 | } else {
146 | const clientWidth = document.documentElement.clientWidth
147 | const maxWidth = clientWidth - offsetLeft - 24
148 | if (ow > offsetWidth) {
149 | if (ow > maxWidth) {
150 | // console.log(':::::::::', ow, maxWidth)
151 | const h = (parseInt(ow / maxWidth + '') + (ow % maxWidth > 0 ? 1 : 0)) * this.defaultRowHeight;
152 | if (h > offsetHeight) {
153 | this.textarea.style('height', `${h}px`)
154 | } else {
155 | this.textarea.style('height', `${offsetHeight}px`)
156 | }
157 | ow = maxWidth
158 | }
159 | this.textarea.style('width', `${ow}px`)
160 | }
161 | }
162 | }
163 | this.el.show()
164 | }
165 | // }, 0)
166 |
167 | }
168 | }
--------------------------------------------------------------------------------
/src/local/editorbar.d.ts:
--------------------------------------------------------------------------------
1 | import { Element } from "./base/element";
2 | import { Cell } from "../core/cell";
3 | export declare class Editorbar {
4 | el: Element;
5 | value: Cell | null;
6 | textarea: Element;
7 | label: Element;
8 | change: (v: Cell) => void;
9 | constructor();
10 | set(title: string, value: Cell | null): void;
11 | setValue(value: Cell | null): void;
12 | input(evt: any): void;
13 | }
14 |
--------------------------------------------------------------------------------
/src/local/editorbar.ts:
--------------------------------------------------------------------------------
1 | import { Element, h } from "./base/element";
2 | import { Cell } from "../core/cell";
3 | import { mouseMoveUp } from "./event"
4 |
5 | export class Editorbar {
6 | el: Element;
7 | value: Cell | null = null; // 选中的当前的cell
8 | textarea: Element;
9 | label: Element;
10 | change: (v: Cell) => void = (v) => {};
11 | constructor () {
12 | this.el = h().class('spreadsheet-editor-bar').children([
13 | h().class('spreadsheet-formula-bar').children([
14 | this.label = h().class('spreadsheet-formula-label'),
15 | this.textarea = h('textarea').on('input', (evt) => this.input(evt))
16 | ]),
17 | // h().class('spreadsheet-formular-bar-resizer').on('mousedown', this.mousedown)
18 | ])
19 | }
20 |
21 | set (title: string, value: Cell | null) {
22 | this.label.html(title)
23 | this.setValue(value)
24 | }
25 |
26 | setValue (value: Cell | null) {
27 | this.value = value
28 | this.textarea.val(value && value.text || '')
29 | }
30 |
31 | input (evt: any) {
32 | const v = evt.target.value
33 |
34 | if (this.value) {
35 | this.value.text = v
36 | } else {
37 | this.value = {text: v}
38 | }
39 | this.change(this.value)
40 |
41 | }
42 |
43 | }
--------------------------------------------------------------------------------
/src/local/event.d.ts:
--------------------------------------------------------------------------------
1 | export declare function bind(name: string, fn: (evt: T) => void, target?: any): void;
2 | export declare function unbind(name: string, fn: (evt: T) => void, target?: any): void;
3 | export declare function mouseMoveUp(movefunc: (evt: T) => void, upfunc: (evt: T) => void): void;
4 |
--------------------------------------------------------------------------------
/src/local/event.ts:
--------------------------------------------------------------------------------
1 | export function bind(name: string, fn: (evt: T) => void, target: any = window) {
2 | target.addEventListener(name, fn)
3 | }
4 | export function unbind(name: string, fn: (evt: T) => void, target: any = window) {
5 | target.removeEventListener(name, fn)
6 | }
7 | export function mouseMoveUp (movefunc: (evt: T) => void, upfunc: (evt: T) => void) {
8 | bind('mousemove', movefunc)
9 | const up = (evt: T) => {
10 | unbind('mousemove', movefunc)
11 | unbind('mouseup', up)
12 | upfunc(evt)
13 | }
14 | bind('mouseup', up)
15 | }
--------------------------------------------------------------------------------
/src/local/index.d.ts:
--------------------------------------------------------------------------------
1 | import { Spreadsheet, SpreadsheetOptions, SpreadsheetData } from '../core/index';
2 | import '../style/index.less';
3 | import { Table } from './table';
4 | import { Toolbar } from './toolbar';
5 | import { Editorbar } from './editorbar';
6 | export interface Options extends SpreadsheetOptions {
7 | height?: () => number;
8 | mode?: 'design' | 'write' | 'read';
9 | }
10 | export declare class LocalSpreadsheet {
11 | ss: Spreadsheet;
12 | refs: {
13 | [key: string]: HTMLElement;
14 | };
15 | table: Table;
16 | toolbar: Toolbar | null;
17 | editorbar: Editorbar | null;
18 | bindEl: HTMLElement;
19 | options: Options;
20 | _change: (data: SpreadsheetData) => void;
21 | constructor(el: HTMLElement, options?: Options);
22 | loadData(data: SpreadsheetData): LocalSpreadsheet;
23 | change(cb: (data: SpreadsheetData) => void): LocalSpreadsheet;
24 | private render;
25 | private toolbarChange;
26 | private editorbarChange;
27 | private editorChange;
28 | private clickCell;
29 | }
30 |
--------------------------------------------------------------------------------
/src/local/index.ts:
--------------------------------------------------------------------------------
1 | import { Spreadsheet, SpreadsheetOptions, SpreadsheetData } from '../core/index'
2 | import '../style/index.less'
3 | import { Cell, getStyleFromCell } from '../core/cell';
4 | import { Format } from '../core/format';
5 | import { Font } from '../core/font';
6 | import { Editor } from './editor';
7 | import { Selector } from './selector';
8 | import { Table } from './table';
9 | import { Toolbar } from './toolbar';
10 | import { Editorbar } from './editorbar';
11 | import { h, Element } from './base/element'
12 |
13 | export interface Options extends SpreadsheetOptions {
14 | height?: () => number;
15 | mode?: 'design' | 'write' | 'read';
16 | }
17 |
18 | export class LocalSpreadsheet {
19 | ss: Spreadsheet;
20 | refs: {[key: string]: HTMLElement} = {};
21 | table: Table;
22 | toolbar: Toolbar | null = null;
23 | editorbar: Editorbar | null = null;
24 |
25 | bindEl: HTMLElement
26 | options: Options;
27 |
28 | _change: (data: SpreadsheetData) => void = () => {}
29 |
30 | constructor (el: HTMLElement, options: Options = {}) {
31 | this.bindEl = el
32 | this.options = Object.assign({mode: 'design'}, options)
33 |
34 | // clear content in el
35 | this.bindEl && (this.bindEl.innerHTML = '')
36 |
37 | this.ss = new Spreadsheet(options);
38 | // console.log('::::>>>select:', this.ss.select)
39 | if (this.options.mode === 'design') {
40 | this.editorbar = new Editorbar()
41 | this.editorbar.change = (v) => this.editorbarChange(v)
42 |
43 | this.toolbar = new Toolbar(this.ss);
44 | this.toolbar.change = (key, v) => this.toolbarChange(key, v)
45 | this.toolbar.undo = () => {
46 | // console.log('undo::')
47 | return this.table.undo()
48 | }
49 | this.toolbar.redo = () => {
50 | // console.log('redo::')
51 | return this.table.redo()
52 | }
53 | }
54 |
55 | let bodyHeightFn = (): number => {
56 | if (this.options.height) {
57 | return this.options.height()
58 | }
59 | return document.documentElement.clientHeight - 24 - 41 - 26
60 | }
61 | let bodyWidthFn = (): number => {
62 | return this.bindEl.offsetWidth
63 | }
64 | this.table = new Table(this.ss, Object.assign({height: bodyHeightFn, width: bodyWidthFn, mode: this.options.mode}));
65 | this.table.change = (data) => {
66 | this.toolbar && this.toolbar.setRedoAble(this.ss.isRedo())
67 | this.toolbar && this.toolbar.setUndoAble(this.ss.isUndo())
68 | this._change(data)
69 | }
70 | this.table.editorChange = (v) => this.editorChange(v)
71 | this.table.clickCell = (rindex, cindex, cell) => this.clickCell(rindex, cindex, cell)
72 | this.render();
73 | }
74 |
75 | loadData (data: SpreadsheetData): LocalSpreadsheet {
76 | // reload until waiting main thread
77 | setTimeout(() => {
78 | this.ss.data = data
79 | this.table.reload()
80 | }, 1)
81 | return this
82 | }
83 |
84 | change (cb: (data: SpreadsheetData) => void): LocalSpreadsheet {
85 | this._change = cb
86 | return this;
87 | }
88 |
89 | private render (): void {
90 | this.bindEl.appendChild(h().class('spreadsheet').children([
91 | h().class('spreadsheet-bars').children([
92 | this.toolbar && this.toolbar.el || '',
93 | this.editorbar && this.editorbar.el || '',
94 | ]),
95 | this.table.el
96 | ]).el);
97 | }
98 |
99 | private toolbarChange (k: keyof Cell, v: any) {
100 | if (k === 'merge') {
101 | this.table.merge();
102 | return;
103 | } else if (k === 'clearformat') {
104 | this.table.clearformat();
105 | return ;
106 | } else if (k === 'paintformat') {
107 | this.table.copyformat();
108 | return ;
109 | }
110 |
111 | this.table.setCellAttr(k, v);
112 | }
113 |
114 | private editorbarChange (v: Cell) {
115 | this.table.setValueWithText(v)
116 | }
117 |
118 | private editorChange (v: Cell) {
119 | this.editorbar && this.editorbar.setValue(v)
120 | }
121 |
122 | private clickCell (rindex: number, cindex: number, v: Cell | null) {
123 | const cols = this.ss.cols()
124 | this.editorbar && this.editorbar.set(`${cols[cindex].title}${rindex + 1}`, v)
125 | this.toolbar && this.toolbar.set(this.table.td(rindex, cindex), v)
126 | }
127 |
128 | }
129 |
--------------------------------------------------------------------------------
/src/local/resizer.d.ts:
--------------------------------------------------------------------------------
1 | import { Element } from "./base/element";
2 | export declare class Resizer {
3 | vertical: boolean;
4 | change: (index: number, distance: number) => void;
5 | el: Element;
6 | resizer: Element;
7 | resizerLine: Element;
8 | moving: boolean;
9 | index: number;
10 | constructor(vertical: boolean, change: (index: number, distance: number) => void);
11 | set(target: any, index: number, scroll: number): void;
12 | mousedown(evt: any): void;
13 | }
14 |
--------------------------------------------------------------------------------
/src/local/resizer.ts:
--------------------------------------------------------------------------------
1 | import { Element, h } from "./base/element";
2 | import { mouseMoveUp } from './event';
3 |
4 | export class Resizer {
5 | el: Element;
6 | resizer: Element;
7 | resizerLine: Element;
8 | moving: boolean = false;
9 | index: number = 0;
10 | constructor (public vertical: boolean, public change: (index: number, distance: number) => void) {
11 | this.el = h().class('spreadsheet-resizer-wrapper').children([
12 | this.resizer = h().class(`spreadsheet-resizer ${vertical ? 'vertical' : 'horizontal'}`)
13 | .on('mousedown', (evt: Event) => this.mousedown(evt)),
14 | this.resizerLine = h().class(`spreadsheet-resizer-line ${vertical ? 'vertical' : 'horizontal'}`).hide()
15 | ])
16 | }
17 |
18 | set (target: any, index: number, scroll: number) {
19 | if (this.moving) return ;
20 | this.index = index
21 | const { vertical } = this
22 | const { offsetLeft, offsetTop, offsetHeight, offsetWidth, parentNode } = target
23 | this.resizer.styles({
24 | left: `${vertical ? offsetLeft + offsetWidth - 5 - scroll : offsetLeft}px`,
25 | top: `${vertical ? offsetTop : offsetTop + offsetHeight - 5 + 24 - scroll}px`,
26 | width: `${vertical ? 5 : offsetWidth}px`,
27 | height: `${vertical ? offsetHeight : 5}px`
28 | })
29 | this.resizerLine.styles({
30 | left: `${vertical ? offsetLeft + offsetWidth - scroll : offsetLeft}px`,
31 | top: `${vertical ? offsetTop : offsetTop + offsetHeight + 24 - scroll}px`,
32 | width: `${vertical ? 0 : parentNode.parentNode.parentNode.parentNode.parentNode.nextSibling.offsetWidth - 15}px`,
33 | height: `${vertical ? parentNode.parentNode.parentNode.parentNode.nextSibling.offsetHeight + parentNode.offsetHeight : 0}px`
34 | })
35 | // this.el.show()
36 | }
37 |
38 | mousedown (evt: any) {
39 | let startEvt = evt;
40 | let distance = 0;
41 | this.resizerLine.show()
42 | mouseMoveUp((e: any) => {
43 | this.moving = true
44 | if (startEvt !== null && e.buttons === 1) {
45 | if (this.vertical) {
46 | const d = e.x - startEvt.x
47 | distance += d
48 | this.resizer.style('left', `${this.resizer.offset().left + d}px`)
49 | this.resizerLine.style('left', `${this.resizerLine.offset().left + d}px`)
50 | } else {
51 | const d = e.y - startEvt.y
52 | distance += d
53 | this.resizer.style('top', `${this.resizer.offset().top + d}px`)
54 | this.resizerLine.style('top', `${this.resizerLine.offset().top + d}px`)
55 | }
56 | startEvt = e
57 | }
58 | }, (e: any) => {
59 | this.change(this.index, distance)
60 | startEvt = null
61 | this.resizerLine.hide()
62 | distance = 0
63 | this.moving = false
64 | })
65 | }
66 | }
--------------------------------------------------------------------------------
/src/local/selector.d.ts:
--------------------------------------------------------------------------------
1 | import { Element } from "./base/element";
2 | import { Spreadsheet } from "../core/index";
3 | import { Table } from './table';
4 | export declare class Selector {
5 | ss: Spreadsheet;
6 | table: Table;
7 | topEl: Element;
8 | rightEl: Element;
9 | bottomEl: Element;
10 | leftEl: Element;
11 | areaEl: Element;
12 | cornerEl: Element;
13 | copyEl: Element;
14 | el: Element;
15 | _offset: {
16 | left: number;
17 | top: number;
18 | width: number;
19 | height: number;
20 | };
21 | startTarget: any;
22 | endTarget: any;
23 | change: () => void;
24 | changeCopy: (evt: any, arrow: 'bottom' | 'top' | 'left' | 'right', startRow: number, startCol: number, stopRow: number, stopCol: number) => void;
25 | constructor(ss: Spreadsheet, table: Table);
26 | mousedown(evt: any): void;
27 | setCurrentTarget(target: HTMLElement): void;
28 | private cornerMousedown;
29 | reload(): void;
30 | private setOffset;
31 | private rowsHeight;
32 | private colsWidth;
33 | }
34 | export declare class DashedSelector {
35 | el: Element;
36 | constructor();
37 | set(selector: Selector): void;
38 | hide(): void;
39 | }
40 |
--------------------------------------------------------------------------------
/src/local/selector.ts:
--------------------------------------------------------------------------------
1 | import { Element, h } from "./base/element";
2 | import { bind, mouseMoveUp } from './event';
3 | import { Spreadsheet } from "../core/index";
4 | import { Table } from './table';
5 |
6 | export class Selector {
7 | topEl: Element;
8 | rightEl: Element;
9 | bottomEl: Element;
10 | leftEl: Element;
11 | areaEl: Element;
12 | cornerEl: Element;
13 | copyEl: Element;
14 | el: Element;
15 | _offset = {left: 0, top: 0, width: 0, height: 0};
16 |
17 | startTarget: any;
18 | endTarget: any;
19 |
20 | change: () => void = () => {};
21 | changeCopy: (evt: any, arrow: 'bottom' | 'top' | 'left' | 'right', startRow: number, startCol: number, stopRow: number, stopCol: number) => void
22 | = (evt, arrow, startRow, startCol, stopRow, stopCol) => {};
23 |
24 | constructor (public ss: Spreadsheet, public table: Table) {
25 | this.topEl = h().class('top-border');
26 | this.rightEl = h().class('right-border');
27 | this.bottomEl = h().class('bottom-border');
28 | this.leftEl = h().class('left-border');
29 | this.areaEl = h().class('area-border');
30 | this.cornerEl = h().class('corner').on('mousedown', (evt) => this.cornerMousedown(evt));
31 | this.copyEl = h().class('copy-border');
32 | this.el = h().class('spreadsheet-borders').children([
33 | this.topEl,
34 | this.rightEl,
35 | this.bottomEl,
36 | this.leftEl,
37 | this.areaEl,
38 | this.cornerEl,
39 | this.copyEl.hide(),
40 | ]).hide()
41 | }
42 |
43 | mousedown (evt: any) {
44 | // console.log('>>>>>>>>selector>>')
45 | // console.log(this, evt, evt.type, evt.detail, evt.buttons, evt.button)
46 | if (evt.detail === 1 && evt.target.getAttribute('type') === 'cell') {
47 | // console.log(evt.shiftKey)
48 | if (evt.shiftKey) {
49 | this.endTarget = evt.target
50 | this.setOffset()
51 | return
52 | }
53 | // Object.assign(this, {startTarget: evt.target, endTarget: evt.target})
54 | // this.setOffset()
55 | this.setCurrentTarget(evt.target)
56 |
57 | mouseMoveUp((e: any) => {
58 | if (e.buttons === 1 && e.target.getAttribute('type') === 'cell') {
59 | this.endTarget = e.target
60 | this.setOffset()
61 | }
62 | }, (e) => { this.change() })
63 | // show el
64 | this.el.show()
65 | }
66 | }
67 |
68 | setCurrentTarget (target: HTMLElement) {
69 | Object.assign(this, {startTarget: target, endTarget: target})
70 | this.setOffset()
71 | }
72 |
73 | private cornerMousedown (evt: any) {
74 | const { select } = this.ss
75 | if (select === null) {
76 | return ;
77 | }
78 | const [stopRow, stopCol] = select.stop;
79 | const [startRow, startCol] = select.start;
80 |
81 | let boxRange:['bottom' | 'top' | 'left' | 'right', number, number, number, number] | null = null;
82 | mouseMoveUp((e: any) => {
83 | const rowIndex = e.target.getAttribute('row-index')
84 | const colIndex = e.target.getAttribute('col-index')
85 | if (rowIndex && colIndex) {
86 | this.copyEl.show();
87 | let rdiff = stopRow - rowIndex
88 | let cdiff = stopCol - colIndex
89 | let _rdiff = startRow - rowIndex
90 | let _cdiff = startCol - colIndex
91 | const {left, top, height, width} = this._offset;
92 | // console.log(rdiff, cdiff, ',,,', _rdiff, _cdiff)
93 | if (rdiff < 0) {
94 | // bottom
95 | // console.log('FCK=>bottom', this.rowsHeight(stopRow, stopRow + Math.abs(rdiff)), rdiff)
96 | this.copyEl.styles({
97 | left: `${left - 1}px`,
98 | top: `${top - 1}px`,
99 | width: `${width - 1}px`,
100 | height: `${this.rowsHeight(stopRow - select.rowLen() + 1, stopRow + Math.abs(rdiff)) - 1}px`});
101 | boxRange = ['bottom', stopRow + 1, startCol, stopRow + Math.abs(rdiff), stopCol]
102 | } else if (cdiff < 0) {
103 | // right
104 | // console.log('FCK=>right')
105 | this.copyEl.styles({
106 | left: `${left - 1}px`,
107 | top: `${top - 1}px`,
108 | width: `${this.colsWidth(stopCol - select.colLen() + 1, stopCol + Math.abs(cdiff)) - 1}px`,
109 | height: `${height - 1}px`});
110 | boxRange = ['right', startRow, stopCol + 1, stopRow, stopCol + Math.abs(cdiff)]
111 | } else if (_rdiff > 0) {
112 | // top
113 | // console.log('FCK=>top')
114 | const h = this.rowsHeight(startRow - _rdiff, startRow - 1)
115 | this.copyEl.styles({
116 | left: `${left - 1}px`,
117 | top: `${top - h - 1}px`,
118 | width: `${width - 1}px`,
119 | height: `${h - 1}px`});
120 | boxRange = ['top', startRow - _rdiff, startCol, startRow - 1, stopCol]
121 | } else if (_cdiff > 0) {
122 | // left
123 | // console.log('FCK=>left')
124 | const w = this.colsWidth(startCol - _cdiff, startCol - 1)
125 | this.copyEl.styles({
126 | left: `${left - w - 1}px`,
127 | top: `${top - 1}px`,
128 | width: `${w - 1}px`,
129 | height: `${height - 1}px`});
130 | boxRange = ['left', startRow, startCol - _cdiff, stopRow, startCol - 1]
131 | } else {
132 | this.copyEl.styles({
133 | left: `${left - 1}px`,
134 | top: `${top - 1}px`,
135 | width: `${width - 1}px`,
136 | height: `${height - 1}px`});
137 | boxRange = null
138 | }
139 | }
140 | }, (e) => {
141 | this.copyEl.hide()
142 | if (boxRange !== null) {
143 | const [arrow, startRow, startCol, stopRow, stopCol] = boxRange
144 | this.changeCopy(e, arrow, startRow, startCol, stopRow, stopCol)
145 | }
146 | });
147 | }
148 |
149 | reload () {
150 | this.setOffset()
151 | }
152 |
153 | private setOffset () {
154 | if (this.startTarget === undefined) return ;
155 | let { select } = this.ss
156 |
157 | // console.log('select: ', select, this.table)
158 | if (select) {
159 | // console.log('clear>>>>>:::')
160 | // clear
161 | const [minRow, minCol] = select.start
162 | const [maxRow, maxCol] = select.stop
163 | _forEach(minRow, maxRow, this.table.firsttds, (e) => { e.deactive() })
164 | _forEach(minCol, maxCol, this.table.ths, (e) => { e.deactive() })
165 | }
166 |
167 | select = this.ss.buildSelect(this.startTarget, this.endTarget)
168 | const [minRow, minCol] = select.start
169 | const [maxRow, maxCol] = select.stop
170 | // let height = 0, width = 0;
171 | const height = this.rowsHeight(minRow, maxRow, (e) => e.active())
172 | // _forEach(minRow, maxRow, this.table.firsttds, (e) => {
173 | // e.active()
174 | // height += parseInt(e.offset().height)
175 | // })
176 | // height /= 2
177 | const width = this.colsWidth(minCol, maxCol, (e) => e.active())
178 | // _forEach(minCol, maxCol, this.table.ths, (e) => {
179 | // e.active()
180 | // width += parseInt(e.offset().width)
181 | // })
182 |
183 | // console.log('>>', minRow, minCol, maxRow, maxCol, height, width)
184 | const td = this.table.td(minRow, minCol)
185 | if (td) {
186 | // console.log('td:', td)
187 | const {left, top} = td.offset()
188 | this._offset = {left, top, width, height};
189 |
190 | this.topEl.styles({left: `${left - 1}px`, top: `${top - 1}px`, width: `${width + 1}px`, height: '2px'})
191 | this.rightEl.styles({left: `${left + width - 1}px`, top: `${top - 1}px`, width: '2px', height: `${height}px`})
192 | this.bottomEl.styles({left: `${left - 1}px`, top: `${top + height - 1}px`, width: `${width}px`, height: '2px'})
193 | this.leftEl.styles({left: `${left - 1}px`, top: `${top - 1}px`, width: '2px', height: `${height}px`})
194 | this.areaEl.styles({left: `${left}px`, top: `${top}px`, width: `${width - 2}px`, height: `${height - 2}px`})
195 | this.cornerEl.styles({left: `${left + width - 5}px`, top: `${top + height - 5}px`})
196 | }
197 | }
198 | private rowsHeight (minRow:number, maxRow:number, cb: (e: Element) => void = (e) => {}): number {
199 | let height = 0
200 | _forEach(minRow, maxRow, this.table.firsttds, (e) => {
201 | cb(e)
202 | height += parseInt(e.offset().height)
203 | })
204 | height /= 2
205 | return height
206 | }
207 | private colsWidth (minCol: number, maxCol: number, cb: (e: Element) => void = (e) => {}): number {
208 | let width = 0
209 | _forEach(minCol, maxCol, this.table.ths, (e) => {
210 | cb(e)
211 | width += parseInt(e.offset().width)
212 | })
213 | return width
214 | }
215 | }
216 |
217 | const _forEach = (start: number, stop: number, elements: {[key: string]: Array | Element}, cb: (e: Element) => void): void => {
218 | for (let i = start; i <= stop; i++) {
219 | const es = elements[i + ''];
220 | if (es) {
221 | if (es instanceof Element) {
222 | cb(es)
223 | } else {
224 | es.forEach(e => cb(e))
225 | }
226 | }
227 | }
228 | }
229 |
230 | export class DashedSelector {
231 | el: Element;
232 | constructor () {
233 | this.el = h().class('spreadsheet-borders dashed').hide();
234 | }
235 |
236 | set (selector: Selector) {
237 | if (selector._offset) {
238 | const { left, top, width, height } = selector._offset;
239 | this.el
240 | .style('left', `${left - 2}px`)
241 | .style('top', `${top - 2}px`)
242 | .style('width', `${width}px`)
243 | .style('height', `${height}px`)
244 | .show();
245 | }
246 | }
247 |
248 | hide () {
249 | this.el.hide();
250 | }
251 | }
252 |
--------------------------------------------------------------------------------
/src/local/table.d.ts:
--------------------------------------------------------------------------------
1 | import { Element } from "./base/element";
2 | import { Spreadsheet, SpreadsheetData } from '../core/index';
3 | import { Editor } from './editor';
4 | import { Selector, DashedSelector } from './selector';
5 | import { Resizer } from './resizer';
6 | import { ContextMenu } from "./contextmenu";
7 | import { Cell } from "../core/cell";
8 | interface Map {
9 | [key: string]: T;
10 | }
11 | export interface TableOption {
12 | height: () => number;
13 | width: () => number;
14 | mode: 'design' | 'write' | 'read';
15 | }
16 | export declare class Table {
17 | options: TableOption;
18 | cols: Map>;
19 | firsttds: Map>;
20 | tds: Map;
21 | ths: Map;
22 | ss: Spreadsheet;
23 | formulaCellIndexs: Set;
24 | el: Element;
25 | header: Element;
26 | body: Element;
27 | fixedLeftBody: Element | null;
28 | editor: Editor | null;
29 | rowResizer: Resizer | null;
30 | colResizer: Resizer | null;
31 | contextmenu: ContextMenu | null;
32 | selector: Selector;
33 | dashedSelector: DashedSelector;
34 | state: 'copy' | 'cut' | 'copyformat' | null;
35 | currentIndexs: [number, number] | null;
36 | focusing: boolean;
37 | change: (data: SpreadsheetData) => void;
38 | editorChange: (v: Cell) => void;
39 | clickCell: (rindex: number, cindex: number, v: Cell | null) => void;
40 | constructor(ss: Spreadsheet, options: TableOption);
41 | reload(): void;
42 | private moveLeft;
43 | private moveUp;
44 | private moveDown;
45 | private moveRight;
46 | private moveSelector;
47 | setValueWithText(v: Cell): void;
48 | setTdWithCell(rindex: number, cindex: number, cell: Cell, autoWordWrap?: boolean): void;
49 | setCellAttr(k: keyof Cell, v: any): void;
50 | undo(): boolean;
51 | redo(): boolean;
52 | private setTdStylesAndAttrsAndText;
53 | copy(): void;
54 | cut(): void;
55 | copyformat(): void;
56 | paste(): void;
57 | clearformat(): void;
58 | merge(): void;
59 | insert(type: 'row' | 'col', amount: number): void;
60 | td(rindex: number, cindex: number): Element;
61 | private selectorChange;
62 | private selectorChangeCopy;
63 | private renderCell;
64 | private _renderCell;
65 | private reRenderFormulaCells;
66 | private setRowHeight;
67 | private setTdStyles;
68 | private setTdAttrs;
69 | private changeRowHeight;
70 | private changeRowResizer;
71 | private changeColResizer;
72 | private buildColGroup;
73 | private buildFixedLeft;
74 | private buildHeader;
75 | private mousedownCell;
76 | private editCell;
77 | private buildBody;
78 | private addRow;
79 | private firsttdsPush;
80 | }
81 | export {};
82 |
--------------------------------------------------------------------------------
/src/local/table.ts:
--------------------------------------------------------------------------------
1 | import { Element, h } from "./base/element";
2 | import { Spreadsheet, SpreadsheetData } from '../core/index'
3 | import { Editor } from './editor';
4 | import { Selector, DashedSelector } from './selector';
5 | import { Resizer } from './resizer';
6 | import { Editorbar } from "./editorbar";
7 | import { Toolbar } from "./toolbar";
8 | import { ContextMenu } from "./contextmenu";
9 | import { Cell, getStyleFromCell } from "../core/cell";
10 | import { formatRenderHtml } from "../core/format";
11 | import { formulaRender } from "../core/formula";
12 | import { bind } from "./event";
13 |
14 | interface Map {
15 | [key: string]: T
16 | }
17 |
18 | export interface TableOption {
19 | height: () => number,
20 | width: () => number,
21 | mode: 'design' | 'write' | 'read';
22 | }
23 |
24 | export class Table {
25 | cols: Map> = {};
26 | firsttds: Map> = {};
27 | tds: Map = {};
28 | ths: Map = {};
29 | ss: Spreadsheet;
30 | formulaCellIndexs: Set = new Set(); // 表达式单元格set
31 |
32 | el: Element;
33 | header: Element;
34 | body: Element;
35 | fixedLeftBody: Element | null = null;
36 |
37 | editor: Editor | null = null;
38 | rowResizer: Resizer | null = null;
39 | colResizer: Resizer | null = null;
40 |
41 | contextmenu: ContextMenu | null = null;
42 |
43 | selector: Selector;
44 | dashedSelector: DashedSelector;
45 | state: 'copy' | 'cut' | 'copyformat' | null = null;
46 |
47 | currentIndexs: [number, number] | null = null;
48 |
49 | // 当前用户是否焦点再table上
50 | focusing: boolean = false;
51 |
52 | // change
53 | change: (data: SpreadsheetData) => void = () => {}
54 | editorChange: (v: Cell) => void = (v) => {}
55 | clickCell: (rindex: number, cindex: number, v: Cell | null) => void = (rindex, cindex, v) => {}
56 |
57 | constructor (ss: Spreadsheet, public options: TableOption) {
58 | this.ss = ss;
59 | this.ss.change = (data) => {
60 | this.change(data)
61 | }
62 |
63 | if (options.mode !== 'read') {
64 | this.editor = new Editor(ss.defaultRowHeight(), ss.formulas)
65 | this.editor.change = (v: Cell) => this.editorChange(v)
66 | }
67 |
68 | if (options.mode === 'design') {
69 | this.rowResizer = new Resizer(false, (index, distance) => this.changeRowResizer(index, distance))
70 | this.colResizer = new Resizer(true, (index, distance) => this.changeColResizer(index, distance))
71 | this.contextmenu = new ContextMenu(this)
72 | }
73 |
74 | this.selector = new Selector(this.ss, this);
75 | this.selector.change = () => this.selectorChange();
76 | this.selector.changeCopy = (e, arrow, startRow, startCol, stopRow, stopCol) => {
77 | this.selectorChangeCopy(e, arrow, startRow, startCol, stopRow, stopCol);
78 | }
79 | this.dashedSelector = new DashedSelector();
80 |
81 | this.el = h().class('spreadsheet-table').children([
82 | this.colResizer && this.colResizer.el || '',
83 | this.rowResizer && this.rowResizer.el || '',
84 | this.buildFixedLeft(),
85 | this.header = this.buildHeader(),
86 | this.body = this.buildBody()
87 | ]).on('contextmenu', (evt) => {
88 | evt.returnValue = false
89 | evt.preventDefault();
90 | });
91 |
92 | bind('resize', (evt: any) => {
93 | this.header.style('width', `${this.options.width()}px`)
94 | this.body.style('width', `${this.options.width()}px`)
95 | if (this.options.mode !== 'read') {
96 | this.body.style('height', `${this.options.height()}px`)
97 | }
98 | })
99 |
100 | bind('click', (evt: any) => {
101 | // console.log('::::::::', this.el.contains(evt.target))
102 | this.focusing = this.el.parent().contains(evt.target)
103 | })
104 |
105 | // bind ctrl + c, ctrl + x, ctrl + v
106 | bind('keydown', (evt: any) => {
107 |
108 | if (!this.focusing) {
109 | return
110 | }
111 |
112 | // console.log('::::::::', evt)
113 | if (!this.focusing) return;
114 |
115 | // ctrlKey
116 | if (evt.ctrlKey && evt.target.type !== 'textarea' && this.options.mode !== 'read') {
117 | // ctrl + c
118 | if (evt.keyCode === 67) {
119 | this.copy();
120 | evt.returnValue = false
121 | }
122 | // ctrl + x
123 | if (evt.keyCode === 88) {
124 | this.cut();
125 | evt.returnValue = false
126 | }
127 | // ctrl + v
128 | if (evt.keyCode === 86) {
129 | this.paste();
130 | evt.returnValue = false
131 | }
132 | } else {
133 | // console.log('>>>>>>>>>>>>>>', evt)
134 | switch (evt.keyCode) {
135 | case 37: // left
136 | this.moveLeft()
137 | evt.returnValue = false
138 | break;
139 | case 38: // up
140 | this.moveUp()
141 | evt.returnValue = false
142 | break;
143 | case 39: // right
144 | this.moveRight()
145 | evt.returnValue = false
146 | break;
147 | case 40: // down
148 | this.moveDown()
149 | evt.returnValue = false
150 | break;
151 | case 9: // tab
152 | this.moveRight();
153 | evt.returnValue = false
154 | break;
155 | case 13:
156 | this.moveDown();
157 | evt.returnValue = false
158 | break;
159 | }
160 |
161 |
162 | // 输入a-zA-Z1-9
163 | if (this.options.mode !== 'read') {
164 | if (evt.keyCode >= 65 && evt.keyCode <= 90 || evt.keyCode >= 48 && evt.keyCode <= 57 || evt.keyCode >= 96 && evt.keyCode <= 105 || evt.keyCode == 187) {
165 | // if (this.currentIndexs) {
166 | // console.log('::::::::', evt.target.type)
167 | if (evt.target.type !== 'textarea') {
168 | this.ss.cellText(evt.key, (rindex, cindex, cell) => {
169 | if (this.editor) {
170 | const td = this.td(rindex, cindex)
171 | td.html(this.renderCell(rindex, cindex, cell))
172 | this.editor.set(td.el, this.ss.currentCell())
173 | }
174 | })
175 | }
176 | }
177 | }
178 |
179 | }
180 |
181 | });
182 | }
183 |
184 | reload () {
185 | this.firsttds = {}
186 | this.el.html('')
187 | this.el.children([
188 | this.colResizer && this.colResizer.el || '',
189 | this.rowResizer && this.rowResizer.el || '',
190 | this.buildFixedLeft(),
191 | this.header = this.buildHeader(),
192 | this.body = this.buildBody()
193 | ]);
194 | }
195 |
196 | private moveLeft () {
197 | if (this.currentIndexs && this.currentIndexs[1] > 0) {
198 | this.currentIndexs[1] -= 1
199 | this.moveSelector('left')
200 | }
201 | }
202 | private moveUp () {
203 | if (this.currentIndexs && this.currentIndexs[0] > 0) {
204 | this.currentIndexs[0] -= 1
205 | this.moveSelector('up')
206 | }
207 | }
208 | private moveDown () {
209 | if (this.currentIndexs && this.currentIndexs[0] < this.ss.rows(this.options.mode === 'read').length) {
210 | this.currentIndexs[0] += 1
211 | this.moveSelector('down')
212 | }
213 | }
214 | private moveRight () {
215 | if (this.currentIndexs && this.currentIndexs[1] < this.ss.cols().length) {
216 | this.currentIndexs[1] += 1
217 | this.moveSelector('right')
218 | }
219 | }
220 |
221 | // 移动选框
222 | private moveSelector (direction: 'right' | 'left' | 'up' | 'down') {
223 | if (this.currentIndexs) {
224 | const [rindex, cindex] = this.currentIndexs
225 | const td = this.td(rindex, cindex)
226 | // console.log('move.td:', td)
227 | if (td) {
228 | this.selector.setCurrentTarget(td.el)
229 | const bodyWidth = this.options.width()
230 | const bodyHeight = this.options.height()
231 | const {left, top, width, height} = td.offset()
232 | // console.log(this.body.el.scrollLeft, ', body-width:', bodyWidth, ', left:', left, ', width=', width)
233 | const leftDiff = left + width - bodyWidth
234 | if (leftDiff > 0 && direction === 'right') {
235 | this.body.el.scrollLeft = leftDiff + 15
236 | }
237 | if (direction === 'left' && this.body.el.scrollLeft + 60 > left) {
238 | this.body.el.scrollLeft -= (this.body.el.scrollLeft + 60 - left)
239 | }
240 | if (direction === 'up' && this.body.el.scrollTop > top) {
241 | this.body.el.scrollTop -= (this.body.el.scrollTop - top)
242 | }
243 | if (direction === 'down' && top + height - bodyHeight > 0) {
244 | this.body.el.scrollTop = top + height - bodyHeight + 15;
245 | }
246 |
247 | this.mousedownCell(rindex, cindex)
248 | }
249 |
250 | }
251 | }
252 |
253 | setValueWithText (v: Cell) {
254 | // console.log('setValueWithText: v = ', v)
255 | if (this.currentIndexs) {
256 | this.ss.cellText(v.text, (rindex, cindex, cell) => {
257 | this.td(rindex, cindex).html(this.renderCell(rindex, cindex, cell))
258 | })
259 | }
260 | this.editor && this.editor.setValue(v)
261 | }
262 |
263 | setTdWithCell (rindex: number, cindex: number, cell: Cell, autoWordWrap = true) {
264 | this.setTdStyles(rindex, cindex, cell);
265 | this.setRowHeight(rindex, cindex, autoWordWrap);
266 | this.td(rindex, cindex).html(this.renderCell(rindex, cindex, cell));
267 | }
268 |
269 | setCellAttr (k: keyof Cell, v: any) {
270 | // console.log('::k:', k, '::v:', v)
271 | this.ss.cellAttr(k, v, (rindex, cindex, cell) => {
272 | // console.log(':rindex:', rindex, '; cindex:', cindex, '; cell: ', cell)
273 | this.setTdWithCell(rindex, cindex, cell, k === 'wordWrap' && v);
274 | })
275 | this.editor && this.editor.setStyle(this.ss.currentCell())
276 | }
277 |
278 | undo (): boolean {
279 | return this.ss.undo((rindex, cindex, cell) => {
280 | // console.log('>', rindex, ',', cindex, '::', cell)
281 | this.setTdStylesAndAttrsAndText(rindex, cindex, cell)
282 | })
283 | }
284 | redo (): boolean {
285 | return this.ss.redo((rindex, cindex, cell) => {
286 | this.setTdStylesAndAttrsAndText(rindex, cindex, cell)
287 | })
288 | }
289 | private setTdStylesAndAttrsAndText (rindex: number, cindex: number, cell: Cell) {
290 | let td = this.td(rindex, cindex);
291 | this.setTdStyles(rindex, cindex, cell);
292 | this.setTdAttrs(rindex, cindex, cell);
293 | // console.log('txt>>>:', this.renderCell(rindex, cindex, cell))
294 | td.html(this.renderCell(rindex, cindex, cell));
295 | }
296 |
297 | copy () {
298 | this.ss.copy();
299 | this.dashedSelector.set(this.selector);
300 | this.state = 'copy';
301 | }
302 |
303 | cut () {
304 | this.ss.cut();
305 | this.dashedSelector.set(this.selector);
306 | this.state = 'cut';
307 | }
308 |
309 | copyformat () {
310 | this.ss.copy();
311 | this.dashedSelector.set(this.selector);
312 | this.state = 'copyformat';
313 | }
314 |
315 | paste () {
316 | // console.log('state: ', this.state, this.ss.select)
317 | if (this.state !== null && this.ss.select) {
318 | this.ss.paste((rindex, cindex, cell) => {
319 | // console.log('rindex: ', rindex, ', cindex: ', cindex);
320 | let td = this.td(rindex, cindex);
321 | this.setTdStyles(rindex, cindex, cell);
322 | this.setTdAttrs(rindex, cindex, cell);
323 | if (this.state === 'cut' || this.state === 'copy') {
324 | td.html(this.renderCell(rindex, cindex, cell));
325 | }
326 | }, this.state, (rindex, cindex, cell) => {
327 | let td = this.td(rindex, cindex);
328 | this.setTdStyles(rindex, cindex, cell);
329 | this.setTdAttrs(rindex, cindex, cell);
330 | td.html('');
331 | });
332 | this.selector.reload();
333 | }
334 |
335 | if (this.state === 'copyformat') {
336 | this.state = null;
337 | } else if (this.state === 'cut') {
338 | this.state = null;
339 | } else if (this.state === 'copy') {
340 | // this.ss.paste()
341 | }
342 |
343 | this.dashedSelector.hide();
344 | }
345 |
346 | clearformat () {
347 | this.ss.clearformat((rindex, cindex, cell) => {
348 | this.td(rindex, cindex)
349 | .removeAttr('rowspan')
350 | .removeAttr('colspan')
351 | .styles({}, true)
352 | .show(true);
353 | })
354 | }
355 |
356 | merge () {
357 | this.ss.merge((rindex, cindex, cell) => {
358 | // console.log(rindex, cindex, '>>>', this.table.td(rindex, cindex))
359 | this.setTdAttrs(rindex, cindex, cell).show(true)
360 | }, (rindex, cindex, cell) => {
361 | this.setTdAttrs(rindex, cindex, cell).show(true)
362 | }, (rindex, cindex, cell) => {
363 | let td = this.td(rindex, cindex)
364 | !cell.invisible ? td.show(true) : td.hide()
365 | })
366 | }
367 |
368 | // insert
369 | insert (type: 'row' | 'col', amount: number) {
370 | if (type === 'col') {
371 | // insert col
372 | } else if (type === 'row') {
373 | // insert row
374 | }
375 | this.ss.insert(type, amount, (rindex, cindex, cell) => {
376 | this.setTdStylesAndAttrsAndText(rindex, cindex, cell)
377 | })
378 | }
379 |
380 | td (rindex: number, cindex: number): Element {
381 | const td = this.tds[`${rindex}_${cindex}`]
382 | return td
383 | }
384 |
385 | private selectorChange () {
386 | if (this.state === 'copyformat') {
387 | this.paste();
388 | }
389 | }
390 |
391 | private selectorChangeCopy (evt: any, arrow: 'bottom' | 'top' | 'left' | 'right', startRow: number, startCol: number, stopRow: number, stopCol: number) {
392 | this.ss.batchPaste(arrow, startRow, startCol, stopRow, stopCol, evt.ctrlKey, (rindex, cindex, cell) => {
393 | this.setTdStyles(rindex, cindex, cell);
394 | this.setTdAttrs(rindex, cindex, cell);
395 | this.td(rindex, cindex).html(this.renderCell(rindex, cindex, cell));
396 | })
397 | }
398 |
399 | private renderCell (rindex: number, cindex: number, cell: Cell | null): string {
400 | if (cell) {
401 | const setKey = `${rindex}_${cindex}`
402 | // console.log('text:', setKey, cell.text && cell.text)
403 | if (cell.text && cell.text[0] === '=') {
404 | this.formulaCellIndexs.add(setKey)
405 | } else {
406 | if (this.formulaCellIndexs.has(setKey)) {
407 | this.formulaCellIndexs.delete(setKey)
408 | }
409 |
410 | this.reRenderFormulaCells()
411 | }
412 | return formatRenderHtml(cell.format, this._renderCell(cell))
413 | }
414 | return '';
415 | }
416 | private _renderCell (cell: Cell | null): string {
417 | if (cell) {
418 | let text = cell.text || '';
419 | return formulaRender(text, (rindex, cindex) => this._renderCell(this.ss.getCell(rindex, cindex)))
420 | }
421 | return '';
422 | }
423 | private reRenderFormulaCells () {
424 | // console.log('formulaCellIndex: ', this.formulaCellIndexs)
425 | this.formulaCellIndexs.forEach(it => {
426 | let rcindexes = it.split('_')
427 | const rindex = parseInt(rcindexes[0])
428 | const cindex = parseInt(rcindexes[1])
429 | // console.log('>>>', this.ss.data, this.ss.getCell(rindex, cindex))
430 | const text = this.renderCell(rindex, cindex, this.ss.getCell(rindex, cindex))
431 | this.td(rindex, cindex).html(text);
432 | })
433 | }
434 |
435 | private setRowHeight (rindex: number, cindex: number, autoWordWrap: boolean) {
436 | // console.log('rowHeight: ', this.td(rindex, cindex).offset().height, ', autoWordWrap:', autoWordWrap)
437 | // 遍历rindex行的所有单元格,计算最大高度
438 | if (autoWordWrap === false) {
439 | return ;
440 | }
441 | const cols = this.ss.cols()
442 | const td = this.td(rindex, cindex)
443 | let h = td.offset().height
444 | console.log('h:', h)
445 | const tdRowspan = td.attr('rowspan')
446 | if (tdRowspan) {
447 | for (let i = 1; i < parseInt(tdRowspan); i++) {
448 | let firsttds = this.firsttds[(rindex + i) +'']
449 | firsttds && (h -= parseInt(firsttds[0].attr('height') || 0) + 1)
450 | }
451 | }
452 | // console.log('after.h:', h)
453 | this.changeRowHeight(rindex, h - 1);
454 | }
455 |
456 | private setTdStyles (rindex: number, cindex: number, cell: Cell): Element {
457 | return this.td(rindex, cindex).styles(getStyleFromCell(cell), true)
458 | }
459 | private setTdAttrs (rindex: number, cindex: number, cell: Cell): Element {
460 | return this.td(rindex, cindex)
461 | .attr('rowspan', cell.rowspan || 1)
462 | .attr('colspan', cell.colspan || 1);
463 | }
464 |
465 | private changeRowHeight (index: number, h: number) {
466 | if (h <= this.ss.defaultRowHeight()) return
467 | this.ss.row(index, h)
468 | const firstTds = this.firsttds[index+'']
469 | if (firstTds) {
470 | firstTds.forEach(td => td.attr('height', h))
471 | }
472 | this.selector.reload()
473 | this.editor && this.editor.reload()
474 | }
475 | private changeRowResizer (index: number, distance: number) {
476 | const h = this.ss.row(index).height + distance
477 | this.changeRowHeight(index, h);
478 | }
479 | private changeColResizer (index: number, distance: number) {
480 | const w = this.ss.col(index).width + distance
481 | if (w <= 50) return
482 | this.ss.col(index, w)
483 | const cols = this.cols[index+'']
484 | if (cols) {
485 | cols.forEach(col => col.attr('width', w))
486 | }
487 | this.selector.reload()
488 | this.editor && this.editor.reload()
489 | }
490 |
491 | private buildColGroup (lastColWidth: number): Element {
492 | const cols = this.ss.cols();
493 | return h('colgroup').children([
494 | h('col').attr('width', '60'),
495 | ...cols.map((col, index) => {
496 | let c = h('col').attr('width', col.width)
497 | this.cols[index+''] = this.cols[index+''] || []
498 | this.cols[index+''].push(c)
499 | return c;
500 | }),
501 | h('col').attr('width', lastColWidth)
502 | ])
503 | }
504 |
505 | private buildFixedLeft (): Element {
506 | const rows = this.ss.rows(this.options.mode === 'read');
507 | return h().class('spreadsheet-fixed')
508 | .style('width', '60px')
509 | .children([
510 | h().class('spreadsheet-fixed-header').child(h('table').child(
511 | h('thead').child(
512 | h('tr').child(
513 | h('th').child('-')
514 | )
515 | ),
516 | )),
517 | this.fixedLeftBody =
518 | h().class('spreadsheet-fixed-body')
519 | .style('height', `${this.options.mode === 'read' ? 'auto' : this.options.height() - 18}px`)
520 | .children([
521 | h('table').child(
522 | h('tbody').children(
523 | rows.map((row, rindex) => {
524 | let firstTd = h('td').attr('height', `${row.height}`).child(`${rindex + 1}`)
525 | .on('mouseover', (evt: Event) => this.rowResizer && this.rowResizer.set(evt.target, rindex, this.body.el.scrollTop));
526 | this.firsttdsPush(rindex, firstTd)
527 | return h('tr').child(firstTd)
528 | })
529 | )
530 | )
531 | ])
532 | ])
533 | }
534 |
535 | private buildHeader (): Element {
536 | const cols = this.ss.cols();
537 | const thead = h('thead').child(
538 | h('tr').children([
539 | h('th'),
540 | ...cols.map((col, index) => {
541 | let th = h('th').child(col.title).on('mouseover', (evt: Event) => {
542 | console.log(evt)
543 | this.colResizer && this.colResizer.set(evt.target, index, this.body.el.scrollLeft)
544 | });
545 | this.ths[index + ''] = th;
546 | return th;
547 | }),
548 | h('th')
549 | ]
550 | ))
551 | return h().class('spreadsheet-header').style('width', `${this.options.width()}px`).children([
552 | h('table').children([this.buildColGroup(15), thead])
553 | ])
554 | }
555 |
556 | private mousedownCell (rindex: number, cindex: number) {
557 | if (this.editor) {
558 | const editorValue = this.editor.value
559 | if (this.currentIndexs && this.editor.target && editorValue) {
560 | // console.log(':::editorValue:', editorValue)
561 | const oldCell = this.ss.cellText(editorValue.text, (_rindex, _cindex, _cell: Cell) => {
562 | this.td(_rindex, _cindex).html(this.renderCell(_rindex, _cindex, _cell))
563 | });
564 | // const oldTd = this.td(this.currentIndexs[0], this.currentIndexs[1]);
565 | // oldTd.html(this.renderCell(editorValue))
566 | if (oldCell) {
567 | // 设置内容之后,获取高度设置行高
568 | if (oldCell.wordWrap) {
569 | this.setRowHeight(this.currentIndexs[0], this.currentIndexs[1], true)
570 | }
571 | // console.log('old.td.offset:', oldCell)
572 | // this.editorChange(oldCell)
573 | }
574 | }
575 | this.editor.clear()
576 | }
577 |
578 | this.currentIndexs = [rindex, cindex]
579 | const cCell = this.ss.currentCell([rindex, cindex])
580 | this.clickCell(rindex, cindex, cCell)
581 | }
582 |
583 | private editCell(rindex: number, cindex: number) {
584 | const td = this.td(rindex, cindex)
585 | this.editor && this.editor.set(td.el, this.ss.currentCell())
586 | }
587 |
588 | private buildBody () {
589 | const rows = this.ss.rows(this.options.mode === 'read');
590 | const cols = this.ss.cols();
591 |
592 | const mousedown = (rindex: number, cindex: number, evt: any) => {
593 | const {select} = this.ss
594 | if (evt.button === 2) {
595 | // show contextmenu
596 | console.log(':::evt:', evt)
597 | this.contextmenu && this.contextmenu.set(evt)
598 | if (select && select.contains(rindex, cindex)) {
599 | return
600 | }
601 | }
602 | // left key
603 | this.selector.mousedown(evt)
604 | this.mousedownCell(rindex, cindex)
605 | this.focusing = true
606 | }
607 |
608 | const dblclick = (rindex: number, cindex: number) => {
609 | this.editCell(rindex, cindex)
610 | }
611 |
612 | const scrollFn = (evt: any) => {
613 | this.header.el.scrollLeft = evt.target.scrollLeft
614 | this.fixedLeftBody && (this.fixedLeftBody.el.scrollTop = evt.target.scrollTop)
615 | // console.log('>>>>>>>>scroll...', this.header, evt.target.scrollLeft, evt.target.scrollHeight)
616 | }
617 |
618 | const tbody = h('tbody').children(rows.map((row, rindex) => {
619 | let firstTd = h('td').attr('height', `${row.height}`).child(`${rindex + 1}`);
620 | this.firsttdsPush(rindex, firstTd)
621 | return h('tr').children([
622 | firstTd,
623 | ...cols.map((col, cindex) => {
624 | let cell = this.ss.getCell(rindex, cindex)
625 | let td = h('td')
626 | .child(this.renderCell(rindex, cindex, cell))
627 | .attr('type', 'cell')
628 | .attr('row-index', rindex + '')
629 | .attr('col-index', cindex + '')
630 | .attr('rowspan', cell && cell.rowspan || 1)
631 | .attr('colspan', cell && cell.colspan || 1)
632 | .styles(getStyleFromCell(cell), true)
633 | .on('mousedown', (evt: any) => mousedown(rindex, cindex, evt))
634 | .on('dblclick', dblclick.bind(null, rindex, cindex));
635 | this.tds[`${rindex}_${cindex}`] = td
636 | return td;
637 | }),
638 | h('td')
639 | ])
640 | }));
641 |
642 | return h().class('spreadsheet-body')
643 | .on('scroll', scrollFn)
644 | .style('height', `${this.options.mode === 'read' ? 'auto' : this.options.height()}px`)
645 | .style('width', `${this.options.width()}px`)
646 | .children([
647 | h('table').children([this.buildColGroup(0), tbody]),
648 | this.editor && this.editor.el || '',
649 | this.selector.el,
650 | this.contextmenu && this.contextmenu.el || '',
651 | this.dashedSelector.el
652 | ]
653 | )
654 | }
655 |
656 | // 向尾部添加行
657 | private addRow (num = 1) {
658 | if (num > 0) {
659 |
660 | }
661 | }
662 |
663 | private firsttdsPush (index: number, el: Element) {
664 | this.firsttds[`${index}`] = this.firsttds[`${index}`] || []
665 | this.firsttds[`${index}`].push(el)
666 | }
667 |
668 | }
--------------------------------------------------------------------------------
/src/local/toolbar.d.ts:
--------------------------------------------------------------------------------
1 | import { Element } from "./base/element";
2 | import { Spreadsheet } from "../core/index";
3 | import { Cell } from '../core/cell';
4 | import { Dropdown } from './base/dropdown';
5 | export declare class Toolbar {
6 | ss: Spreadsheet;
7 | el: Element;
8 | defaultCell: Cell;
9 | target: Element | null;
10 | currentCell: Cell | null;
11 | elUndo: Element;
12 | elRedo: Element;
13 | elPaintformat: Element;
14 | elClearformat: Element;
15 | elFormat: Dropdown;
16 | elFont: Dropdown;
17 | elFontSize: Dropdown;
18 | elFontWeight: Element;
19 | elFontStyle: Element;
20 | elTextDecoration: Element;
21 | elColor: Dropdown;
22 | elBackgroundColor: Dropdown;
23 | elMerge: Element;
24 | elAlign: Dropdown;
25 | elValign: Dropdown;
26 | elWordWrap: Element;
27 | change: (key: keyof Cell, v: any) => void;
28 | redo: () => boolean;
29 | undo: () => boolean;
30 | constructor(ss: Spreadsheet);
31 | set(target: Element, cell: Cell | null): void;
32 | private setCell;
33 | private setCellStyle;
34 | setRedoAble(flag: boolean): void;
35 | setUndoAble(flag: boolean): void;
36 | private buildSeparator;
37 | private buildAligns;
38 | private buildValigns;
39 | private buildWordWrap;
40 | private buildFontWeight;
41 | private buildFontStyle;
42 | private buildTextDecoration;
43 | private buildMerge;
44 | private buildColor;
45 | private buildBackgroundColor;
46 | private buildUndo;
47 | private buildRedo;
48 | private buildPaintformat;
49 | private buildClearformat;
50 | private buildFormats;
51 | private buildFonts;
52 | private buildFontSizes;
53 | }
54 |
--------------------------------------------------------------------------------
/src/local/toolbar.ts:
--------------------------------------------------------------------------------
1 | import { Element, h } from "./base/element";
2 | import { Spreadsheet } from "../core/index";
3 | import { Cell, getStyleFromCell, defaultCell } from '../core/cell';
4 | import { Table } from './table';
5 | import { buildItem, Item } from './base/item';
6 | import { buildIcon } from './base/icon';
7 | import { buildDropdown, Dropdown } from './base/dropdown';
8 | import { buildMenu } from './base/menu';
9 | import { buildColorPanel } from './base/colorPanel';
10 | import { Font } from "../core/font";
11 | import { Format } from "../core/format";
12 |
13 | export class Toolbar {
14 | el: Element;
15 | defaultCell: Cell;
16 |
17 | target: Element | null = null;
18 | currentCell: Cell | null = null;
19 |
20 | elUndo: Element;
21 | elRedo: Element;
22 | elPaintformat: Element;
23 | elClearformat: Element;
24 | elFormat: Dropdown;
25 | elFont: Dropdown;
26 | elFontSize: Dropdown;
27 | elFontWeight: Element;
28 | elFontStyle: Element;
29 | elTextDecoration: Element;
30 | elColor: Dropdown;
31 | elBackgroundColor: Dropdown;
32 | elMerge: Element;
33 | elAlign: Dropdown;
34 | elValign: Dropdown;
35 | elWordWrap: Element;
36 |
37 | change: (key: keyof Cell, v: any) => void = (key, v) => {}
38 | redo: () => boolean = () => false
39 | undo: () => boolean = () => false
40 |
41 | constructor (public ss: Spreadsheet) {
42 | this.defaultCell = ss.data.cell
43 |
44 | this.el = h().class('spreadsheet-toolbar').child(
45 | buildMenu('horizontal').children([
46 | this.elUndo = this.buildUndo(),
47 | this.elRedo = this.buildRedo(),
48 | this.elPaintformat = this.buildPaintformat(),
49 | this.elClearformat = this.buildClearformat(),
50 | this.elFormat = this.buildFormats(),
51 | this.buildSeparator(),
52 | this.elFont = this.buildFonts(),
53 | this.elFontSize = this.buildFontSizes(),
54 | this.buildSeparator(),
55 | this.elFontWeight = this.buildFontWeight(),
56 | this.elFontStyle = this.buildFontStyle(),
57 | this.elTextDecoration = this.buildTextDecoration(),
58 | this.elColor = this.buildColor(),
59 | this.buildSeparator(),
60 | this.elBackgroundColor = this.buildBackgroundColor(),
61 | this.elMerge = this.buildMerge(),
62 | this.buildSeparator(),
63 | this.elAlign = this.buildAligns(),
64 | this.elValign = this.buildValigns(),
65 | this.elWordWrap = this.buildWordWrap()
66 | ])
67 | )
68 | ;
69 | }
70 |
71 | set (target: Element, cell: Cell | null) {
72 | this.target = target
73 | this.setCell(cell)
74 | }
75 |
76 | private setCell (cell: Cell | null) {
77 | this.currentCell = cell
78 | this.setCellStyle()
79 | }
80 |
81 | private setCellStyle () {
82 | const { target, currentCell, defaultCell, ss } = this
83 | // console.log(':::', currentCell)
84 | if (target) {
85 | // target.clearStyle()
86 | // target.styles(getStyleFromCell(currentCell))
87 | this.elFormat.title.html(ss.getFormat(currentCell !== null && currentCell.format || defaultCell.format).title);
88 | this.elFont.title.html(ss.getFont(currentCell !== null && currentCell.font || defaultCell.font).title);
89 | this.elFontSize.title.html((currentCell !== null && currentCell.fontSize || defaultCell.fontSize) + '');
90 | this.elFontWeight.active(currentCell !== null && currentCell.bold !== undefined && currentCell.bold !== defaultCell.bold);
91 | this.elFontStyle.active(currentCell !== null && currentCell.italic !== undefined && currentCell.italic !== defaultCell.italic);
92 | this.elTextDecoration.active(currentCell !== null && currentCell.underline !== undefined && currentCell.underline !== defaultCell.underline);
93 | this.elColor.title.style('border-bottom-color', currentCell !== null && currentCell.color || defaultCell.color);
94 | this.elBackgroundColor.title.style('border-bottom-color', currentCell !== null && currentCell.backgroundColor || defaultCell.backgroundColor);
95 | (this.elAlign.title).replace(`align-${currentCell !== null && currentCell.align || defaultCell.align}`);
96 | (this.elValign.title).replace(`valign-${currentCell !== null && currentCell.valign || defaultCell.valign}`);
97 | this.elWordWrap.active(currentCell !== null && currentCell.wordWrap !== undefined && currentCell.wordWrap !== defaultCell.wordWrap);
98 | // console.log('select:', currentCell)
99 | if ((currentCell !== null && currentCell.rowspan && currentCell.rowspan > 1)
100 | || (currentCell !== null && currentCell.colspan && currentCell.colspan > 1)) {
101 | this.elMerge.active(true);
102 | } else {
103 | this.elMerge.active(false);
104 | }
105 | }
106 | }
107 |
108 | setRedoAble (flag: boolean) {
109 | flag ? this.elRedo.able() : this.elRedo.disabled()
110 | }
111 |
112 | setUndoAble (flag: boolean) {
113 | flag ? this.elUndo.able() : this.elUndo.disabled()
114 | }
115 |
116 | private buildSeparator (): Element {
117 | return h().class('spreadsheet-item-separator')
118 | }
119 | private buildAligns (): Dropdown {
120 | const titleIcon = buildIcon(`align-${this.defaultCell.align}`)
121 | const clickHandler = (it: string) => {
122 | titleIcon.replace(`align-${it}`)
123 | this.change('align', it)
124 | }
125 | return buildDropdown(titleIcon, '60px', [buildMenu().children(
126 | ['left', 'center', 'right'].map(it =>
127 | buildItem()
128 | .child(buildIcon(`align-${it}`).style('text-align', 'center'))
129 | .on('click', clickHandler.bind(null, it))
130 | )
131 | )])
132 | }
133 | private buildValigns (): Dropdown {
134 | const titleIcon = buildIcon(`valign-${this.defaultCell.valign}`)
135 | const clickHandler = (it: string) => {
136 | titleIcon.replace(`valign-${it}`)
137 | this.change('valign', it)
138 | }
139 | return buildDropdown(titleIcon, '60px', [buildMenu().children(
140 | ['top', 'middle', 'bottom'].map(it =>
141 | buildItem()
142 | .child(buildIcon(`valign-${it}`).style('text-align', 'center'))
143 | .on('click', clickHandler.bind(null, it))
144 | )
145 | )])
146 | }
147 | private buildWordWrap (): Element {
148 | return buildIconItem('textwrap', (is) => this.change('wordWrap', is))
149 | }
150 | private buildFontWeight (): Element {
151 | return buildIconItem('bold', (is) => this.change('bold', is))
152 | }
153 | private buildFontStyle (): Element {
154 | return buildIconItem('italic', (is) => this.change('italic', is))
155 | }
156 | private buildTextDecoration (): Element {
157 | return buildIconItem('underline', (is) => this.change('underline', is))
158 | }
159 | private buildMerge (): Element {
160 | return buildIconItem('merge', (is) => this.change('merge', is))
161 | }
162 | private buildColor (): Dropdown {
163 | const clickHandler = (color: string) => {
164 | this.elColor.title.style('border-bottom-color', color)
165 | this.change('color', color)
166 | }
167 | return buildDropdown(
168 | buildIcon('text-color').styles({'border-bottom': `3px solid ${this.defaultCell.color}`, 'margin-top': '2px', height: '16px'}),
169 | 'auto',
170 | [buildColorPanel(clickHandler)])
171 | }
172 | private buildBackgroundColor (): Dropdown {
173 | const clickHandler = (color: string) => {
174 | this.elBackgroundColor.title.style('border-bottom-color', color)
175 | this.change('backgroundColor', color)
176 | }
177 | return buildDropdown(
178 | buildIcon('cell-color').styles({'border-bottom': `3px solid ${this.defaultCell.backgroundColor}`, 'margin-top': '2px', height: '16px'}),
179 | 'auto',
180 | [buildColorPanel(clickHandler)])
181 | }
182 | private buildUndo (): Element {
183 | return buildItem().child(buildIcon('undo'))
184 | .on('click', (evt) => {
185 | this.undo() ? this.elUndo.able() : this.elUndo.disabled()
186 | })
187 | .disabled()
188 | }
189 | private buildRedo (): Element {
190 | return buildItem().child(buildIcon('redo'))
191 | .on('click', (evt) => {
192 | this.redo() ? this.elRedo.able() : this.elRedo.disabled()
193 | })
194 | .disabled()
195 | }
196 | private buildPaintformat (): Element {
197 | return buildIconItem('paintformat', (is) => {
198 | this.change('paintformat', true);
199 | this.elPaintformat.deactive();
200 | })
201 | }
202 | private buildClearformat (): Element {
203 | return buildIconItem('clearformat', (is) => {
204 | this.change('clearformat', true);
205 | this.elClearformat.deactive();
206 | });
207 | }
208 | private buildFormats (): Dropdown {
209 | const clickHandler = (it: Format) => {
210 | this.elFormat.title.html(this.ss.getFormat(it.key).title);
211 | this.change('format', it.key)
212 | }
213 | return buildDropdown(this.ss.getFormat(this.defaultCell.format).title, '250px', [buildMenu().children(
214 | this.ss.formats.map(it =>
215 | buildItem()
216 | .children([it.title, h().class('label').child(it.label||'')])
217 | .on('click', clickHandler.bind(null, it))
218 | )
219 | )])
220 | }
221 | private buildFonts (): Dropdown {
222 | const clickHandler = (it: Font) => {
223 | this.elFont.title.html(it.title)
224 | this.change('font', it.key)
225 | }
226 | return buildDropdown(this.ss.getFont(this.defaultCell.font).title, '170px', [buildMenu().children(
227 | this.ss.fonts.map(it => {
228 | return buildItem()
229 | .child(it.title)
230 | .on('click', clickHandler.bind(null, it))
231 | })
232 | )])
233 | }
234 | private buildFontSizes (): Dropdown {
235 | const clickHandler = (it: number) => {
236 | this.elFontSize.title.html(`${it}`)
237 | this.change('fontSize', it)
238 | }
239 | return buildDropdown(this.defaultCell.fontSize + '', '70px', [buildMenu().children(
240 | [6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 30, 36].map(it => {
241 | return buildItem()
242 | .child(`${it}`)
243 | .on('click', clickHandler.bind(null, it))
244 | })
245 | )])
246 | }
247 | }
248 |
249 | const buildIconItem = (iconName: string, change: (flag: boolean) => void) => {
250 | const el = buildItem().child(buildIcon(iconName))
251 | el.on('click', (evt) => {
252 | let is = el.isActive()
253 | is ? el.deactive() : el.active()
254 | change(!is)
255 | })
256 | return el;
257 | }
--------------------------------------------------------------------------------
/src/main.d.ts:
--------------------------------------------------------------------------------
1 | import { LocalSpreadsheet, Options } from './local/index';
2 | export default function xspreadsheet(el: HTMLElement, options?: Options): LocalSpreadsheet;
3 | declare global {
4 | interface Window {
5 | xspreadsheet: any;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { LocalSpreadsheet, Options } from './local/index';
2 |
3 | export default function xspreadsheet (el: HTMLElement, options?: Options) {
4 | return new LocalSpreadsheet(el, options)
5 | }
6 |
7 | declare global {
8 | interface Window {
9 | xspreadsheet: any;
10 | }
11 | }
12 |
13 | window.xspreadsheet = xspreadsheet
14 |
--------------------------------------------------------------------------------
/src/style/index.less:
--------------------------------------------------------------------------------
1 | @border-style: 1px solid #e0e2e4;
2 | @icon-size: 18px;
3 |
4 | body {
5 | margin: 0;
6 | }
7 |
8 | .spreadsheet {
9 | font-size: 14px;
10 | line-height: normal;
11 | user-select: none;
12 | -moz-user-select: none;
13 | font-family: Roboto, Helvetica, Arial, sans-serif;
14 | box-sizing: content-box;
15 | background: #fff;
16 |
17 | .spreadsheet-table {
18 | position: relative;
19 | background: #fff;
20 | }
21 |
22 | .spreadsheet-fixed {
23 | position: absolute;
24 | top: 0;
25 | left: 0;
26 | z-index: 10;
27 | background: #fff;
28 |
29 | .spreadsheet-fixed-body {
30 | overflow: hidden;
31 | }
32 | .spreadsheet-fixed-header {
33 | overflow: hidden;
34 | }
35 | }
36 |
37 | .spreadsheet-body {
38 | overflow: scroll;
39 | position: relative;
40 | }
41 | .spreadsheet-header {
42 | overflow: hidden;
43 | width: 100%;
44 | }
45 |
46 | .spreadsheet-header, .spreadsheet-body, .spreadsheet-fixed {
47 |
48 | table {
49 | table-layout: fixed;
50 | text-align: left;
51 | width: 100%;
52 | border-collapse: separate;
53 | border-spacing: 0;
54 | color: #000;
55 |
56 | td, th {
57 | transition: background .1s ease,color .1s ease;
58 | border-bottom: @border-style;
59 | border-right: @border-style;
60 | white-space: nowrap;
61 | text-overflow: ellipsis;
62 | overflow: hidden;
63 | padding: 0 4px;
64 | // height: 22px;
65 | line-height: 22px;
66 | }
67 |
68 | td.active, th.active {
69 | background: rgba(75, 137, 255, .05)!important;
70 | }
71 |
72 | th {
73 | border-top: @border-style;
74 | text-align: center;
75 | }
76 |
77 | th, td:first-child {
78 | font-size: 12px;
79 | background: #f4f5f8;
80 | font-weight: normal;
81 | color: #666;
82 | }
83 |
84 | td:first-child, th:first-child {
85 | border-left: @border-style;
86 | text-align: center;
87 | }
88 | }
89 | }
90 |
91 | }
92 |
93 | .spreadsheet-editor {
94 | position: absolute;
95 | text-align: left;
96 | // z-index: 10;
97 | border: 2px solid rgb(75, 137, 255);
98 | line-height: 0;
99 | z-index: 10;
100 |
101 | textarea {
102 | box-sizing: content-box;
103 | border: none;
104 | padding: 0 3px;
105 | outline-width: 0;
106 | resize: none;
107 | text-align: start;
108 | // max-width: 500px;
109 | overflow-y: hidden;
110 | font-family: inherit;
111 | font-size: inherit;
112 | color: inherit;
113 | white-space: normal;
114 | word-wrap: break-word;
115 | line-height: 22px;
116 | margin: 0;
117 | }
118 | }
119 |
120 | .spreadsheet-suggest, .spreadsheet-contextmenu {
121 | position: absolute;
122 | box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);
123 | background: #fff;
124 | z-index: 100;
125 | }
126 |
127 | .spreadsheet-resizer {
128 | position: absolute;
129 | z-index: 11;
130 | &.horizontal {
131 | cursor: row-resize;
132 | }
133 | &.vertical {
134 | cursor: col-resize;
135 | }
136 | }
137 | .spreadsheet-resizer-line {
138 | position: absolute;
139 | z-index: 100;
140 | &.horizontal {
141 | border-bottom: 2px dashed rgb(75, 137, 255);
142 | }
143 | &.vertical {
144 | border-right: 2px dashed rgb(75, 137, 255);
145 | }
146 | }
147 |
148 | .spreadsheet-borders {
149 | box-sizing: content-box;
150 |
151 | &.dashed {
152 | border: 2px dashed rgb(75, 137, 255);
153 | position: absolute;
154 | background: rgba(75, 137, 255, 0.03);
155 | }
156 |
157 | .left-border, .right-border, .bottom-border, .top-border, .area-border {
158 | position: absolute;
159 | font-size: 0;
160 | pointer-events: none;
161 | background: rgb(75, 137, 255);
162 | }
163 |
164 | .area-border {
165 | background: rgba(75, 137, 255, 0.03);
166 | }
167 |
168 | .corner {
169 | cursor: crosshair;
170 | font-size: 0;
171 | height: 5px;
172 | width: 5px;
173 | border: 2px solid rgb(255, 255, 255);
174 | position: absolute;
175 | bottom: -6px;
176 | right: -6px;
177 | background: rgb(75, 137, 255);
178 | // z-index: 12;
179 | }
180 |
181 | .copy-border {
182 | position: absolute;
183 | pointer-events: none;
184 | border: 1px dashed rgb(75, 137, 255);
185 | background: rgba(75, 137, 255, .03);
186 | }
187 | }
188 |
189 | .spreadsheet-paint-border {
190 | position: absolute;
191 | pointer-events: none;
192 | border: 1px dashed rgb(75, 137, 255);
193 | background: rgba(75, 137, 255, .03);
194 | }
195 |
196 | .spreadsheet-bars {
197 | .spreadsheet-toolbar {
198 | // width: 100%;
199 | height: 40px;
200 | text-align: left;
201 | padding: 0 60px;
202 | border-bottom: @border-style;
203 | background: #f5f6f7;
204 |
205 | >.spreadsheet-menu > .spreadsheet-item {
206 | margin: 7px 1px 0;
207 | }
208 |
209 | }
210 | .spreadsheet-editor-bar {
211 | width: 100%;
212 | height: 26px;
213 | line-height: 26px;
214 | position: relative;
215 | background: #fff;
216 | padding: 0;
217 | }
218 | }
219 |
220 | .spreadsheet-formula-bar {
221 | position: relative;
222 | height: 100%;
223 |
224 | .spreadsheet-formula-label {
225 | width: 60px;
226 | height: 100%;
227 | box-sizing: border-box;
228 | display: inline-block;
229 | text-align: center;
230 | // line-height: inherit;
231 | border-right: 1px solid #e0e2e4;
232 | background-color: #fff;
233 | font-size: 12px;
234 | color: #777;
235 | user-select: none;
236 | float: left;
237 | vertical-align: middle;
238 | }
239 |
240 | textarea {
241 | width: calc(~'100% - 60px');
242 | height: 100%;
243 | box-sizing: border-box;
244 | font-family: inherit;
245 | font-size: inherit;
246 | padding: 4px 10px;
247 | position: relative;
248 | float: left;
249 | resize: none;
250 | overflow-y: hidden;
251 | border: none;
252 | outline-width: 0;
253 | margin: 0;
254 | line-height: 1.2rem;
255 | }
256 | }
257 | .spreadsheet-formula-bar-resizer {
258 | cursor: ns-resize;
259 | // border-bottom: 1px solid #c0c0c0;
260 | height: 4px;
261 | position: absolute;
262 | width: 100%;
263 | left: 0;
264 | bottom: 0;
265 | }
266 |
267 |
268 | .spreadsheet-menu {
269 | &.vertical {
270 | > .spreadsheet-item-separator {
271 | background: #e0e2e4;
272 | height: 1px;
273 | margin: 5px 0;
274 | }
275 | > .spreadsheet-item {
276 | padding: 2px 10px;
277 | border-radius: 0;
278 | // border-top: 1px solid #fafafa;
279 | }
280 | }
281 | &.horizontal {
282 | > .spreadsheet-item {
283 | display: inline-block;
284 | }
285 | > .spreadsheet-item-separator {
286 | display: inline-block;
287 | background: #e0e2e4;
288 | width: 1px;
289 | vertical-align: middle;
290 | height: 18px;
291 | margin: 0 3px;
292 | }
293 | }
294 | > .spreadsheet-item {
295 | border-radius: 2px;
296 | user-select: none;
297 | background: 0;
298 | border: 1px solid transparent;
299 | outline: none;
300 | height: 24px;
301 | color: rgba(0, 0, 0, .8);
302 | line-height: 24px;
303 | list-style: none;
304 | // font-weight: bold;
305 | cursor: default;
306 |
307 | &.disabled {
308 | pointer-events: none;
309 | opacity: 0.5;
310 | }
311 |
312 | &:not(.separator):hover, &.active {
313 | background: rgba(0, 0, 0, .08);
314 | // color: rgba(0, 0, 0, 0.7);
315 | .spreadsheet-icon-img {
316 | opacity: 0.7;
317 | }
318 |
319 | .spreadsheet-dropdown-icon {
320 | background-color: rgba(0, 0, 0, .15);
321 | opacity: 0.6;
322 | }
323 | }
324 |
325 | > .label {
326 | float: right;
327 | opacity: .8;
328 | }
329 | }
330 |
331 | }
332 |
333 | .spreadsheet-dropdown {
334 | position: relative;
335 | display: inline-block;
336 | width: auto!important;
337 |
338 | .spreadsheet-dropdown-content {
339 | position: absolute;
340 | top: calc(~'100% + 5px');
341 | left: 0;
342 | z-index: 200;
343 | background: #fff;
344 | box-shadow: 1px 2px 5px 2px rgba(51,51,51,.15);
345 | width: auto;
346 | }
347 |
348 | .spreadsheet-dropdown-header {
349 | .spreadsheet-dropdown-title {
350 | padding: 0 5px;
351 | display: inline-block;
352 | }
353 | // > .spreadsheet-icon {
354 | // width: 18px;
355 | // height: 16px;
356 | // }
357 |
358 | .spreadsheet-dropdown-icon {
359 | display: inline-block;
360 | vertical-align: top;
361 | // border: 1px solid transparent;
362 | .spreadsheet-icon {
363 | width: 10px;
364 | .arrow-down {
365 | left: -7 * @icon-size - 4px;
366 | }
367 | }
368 | }
369 | }
370 | }
371 |
372 | .spreadsheet-color-panel {
373 | padding: 10px;
374 | width: 100%;
375 |
376 | table {
377 | border-collapse: separate;
378 | border-spacing: 0;
379 | border: none;
380 | }
381 |
382 | table tr td {
383 | padding: 0;
384 | border: none;
385 |
386 | }
387 |
388 | .color-cell {
389 | width: 20px;
390 | height: 20px;
391 | margin: 3px;
392 |
393 | &:hover {
394 | box-shadow: 0 0 2px rgba(0,0,0,.8);
395 | }
396 | }
397 | }
398 |
399 | .spreadsheet-icon {
400 | height: 18px;
401 | width: 18px;
402 | margin: 0px 4px 3px 3px;
403 | direction: ltr;
404 | text-align: left;
405 | user-select: none;
406 | vertical-align: middle;
407 | overflow: hidden;
408 | position: relative;
409 | display: inline-block;
410 | }
411 | .spreadsheet-icon-img {
412 | background-image: url('../assets/sprite.svg');
413 | position: absolute;
414 | width: 262px;
415 | height: 444px;
416 | opacity: 0.55;
417 |
418 | &.undo {
419 | left: 0;
420 | top: 0;
421 | }
422 | &.redo {
423 | left: -1 * @icon-size;
424 | top: 0;
425 | }
426 | &.print {
427 | left: -2 * @icon-size;
428 | top: 0;
429 | }
430 | &.paintformat {
431 | left: -3 * @icon-size;
432 | top: 0;
433 | }
434 | &.clearformat {
435 | left: -4 * @icon-size;
436 | top: 0;
437 | }
438 | &.bold {
439 | left: -5 * @icon-size;
440 | top: 0;
441 | }
442 | &.italic {
443 | left: -6 * @icon-size;
444 | top: 0;
445 | }
446 | &.underline {
447 | left: -7 * @icon-size;
448 | top: 0;
449 | }
450 | &.strikethrough {
451 | left: -8 * @icon-size;
452 | top: 0;
453 | }
454 | &.text-color {
455 | left: -9 * @icon-size;
456 | top: 0;
457 | }
458 | &.cell-color {
459 | left: -10 * @icon-size;
460 | top: 0;
461 | }
462 | &.merge {
463 | left: -11 * @icon-size;
464 | top: 0;
465 | }
466 | &.align-left {
467 | left: -12 * @icon-size;
468 | top: 0;
469 | }
470 | &.align-center {
471 | left: -13 * @icon-size;
472 | top: 0;
473 | }
474 | &.align-right {
475 | left: 0;
476 | top: -1 * @icon-size;
477 | }
478 | &.valign-top {
479 | left: -1 * @icon-size;
480 | top: -1 * @icon-size;
481 | }
482 | &.valign-middle {
483 | left: -2 * @icon-size;
484 | top: -1 * @icon-size;
485 | }
486 | &.valign-bottom {
487 | left: -3 * @icon-size;
488 | top: -1 * @icon-size;
489 | }
490 | &.textwrap {
491 | left: -4 * @icon-size;
492 | top: -1 * @icon-size;
493 | }
494 | &.autofilter {
495 | left: -5 * @icon-size;
496 | top: -1 * @icon-size;
497 | }
498 | &.formula {
499 | left: -6 * @icon-size;
500 | top: -1 * @icon-size;
501 | }
502 | &.arrow-down {
503 | left: -7 * @icon-size;
504 | top: -1 * @icon-size;
505 | }
506 | &.arrow-right {
507 | left: -8 * @icon-size;
508 | top: -1 * @icon-size;
509 | }
510 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | "target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
6 | // "lib": ["es6"], /* Specify library files to be included in the compilation. */
7 | // "allowJs": true, /* Allow javascript files to be compiled. */
8 | // "checkJs": true, /* Report errors in .js files. */
9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
10 | "declaration": true, /* Generates corresponding '.d.ts' file. */
11 | "sourceMap": true, /* Generates corresponding '.map' file. */
12 | // "outFile": "./xpreadsheet", /* Concatenate and emit output to single file. */
13 | // "outDir": "./distjs/", /* Redirect output structure to the directory. */
14 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
15 | "removeComments": true, /* Do not emit comments to output. */
16 | // "noEmit": true, /* Do not emit outputs. */
17 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
18 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
19 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
20 |
21 | /* Strict Type-Checking Options */
22 | "strict": true, /* Enable all strict type-checking options. */
23 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
24 | // "strictNullChecks": true, /* Enable strict null checks. */
25 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
26 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
27 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
28 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
29 |
30 | /* Additional Checks */
31 | // "noUnusedLocals": true, /* Report errors on unused locals. */
32 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
33 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
34 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
35 |
36 | /* Module Resolution Options */
37 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
38 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
39 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
40 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
41 | // "typeRoots": [], /* List of folders to include type definitions from. */
42 | // "types": [], /* Type declaration files to be included in compilation. */
43 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
44 | // "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
45 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
46 |
47 | /* Source Map Options */
48 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
49 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
50 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
51 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
52 |
53 | /* Experimental Options */
54 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
55 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
56 | },
57 | "include": [
58 | "./src/**/*"
59 | ]
60 | }
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": [
4 | "tslint:recommended",
5 | "tslint-eslint-rules"
6 | ],
7 | "jsRules": {},
8 | "rules": {
9 | "ter-indent": [true, 2]
10 | },
11 | "rulesDirectory": []
12 | }
--------------------------------------------------------------------------------
/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
2 | module.exports = {
3 | entry: "./src/main.ts",
4 | output: {
5 | filename: "bundle.js",
6 | path: __dirname + "/dist"
7 | },
8 |
9 | // Enable sourcemaps for debugging webpack's output.
10 | devtool: "source-map",
11 |
12 | devServer: {
13 | clientLogLevel: 'warning',
14 | hot: true,
15 | publicPath: '/'
16 | },
17 |
18 | resolve: {
19 | // Add '.ts' and '.tsx' as resolvable extensions.
20 | extensions: [".ts", ".tsx", ".js", ".json"]
21 | },
22 |
23 | module: {
24 | rules: [
25 | {test: /\.css$/, loader: ExtractTextPlugin.extract("style-loader","css-loader")},
26 | {
27 | test: /\.less$/,
28 | use:ExtractTextPlugin.extract({
29 | fallback:'style-loader',
30 | use:['css-loader','less-loader']
31 | })
32 | },
33 | {test: /\.(eot|woff|woff2|ttf|svg)([\\?]?.*)$/, loader: "file-loader"},
34 | // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
35 | { test: /\.tsx?$/, loader: "awesome-typescript-loader" },
36 |
37 | // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
38 | { enforce: "pre", test: /\.js$/, loader: "source-map-loader" }
39 | ]
40 | },
41 | plugins: [
42 | new ExtractTextPlugin("spreadsheet.css")
43 | ]
44 |
45 | // When importing a module whose path matches one of the following, just
46 | // assume a corresponding global variable exists and use that instead.
47 | // This is important because it allows us to avoid bundling all of our
48 | // dependencies, which allows browsers to cache those libraries between builds.
49 | // externals: {
50 | // "react": "React",
51 | // "react-dom": "ReactDOM"
52 | // },
53 | };
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
2 | module.exports = {
3 | entry: "./src/main.ts",
4 | output: {
5 | filename: "xspreadsheet.js",
6 | path: __dirname + "/docs"
7 | },
8 |
9 | // Enable sourcemaps for debugging webpack's output.
10 | devtool: "source-map",
11 |
12 | resolve: {
13 | // Add '.ts' and '.tsx' as resolvable extensions.
14 | extensions: [".ts", ".tsx", ".js", ".json"]
15 | },
16 |
17 | module: {
18 | rules: [
19 | {test: /\.less$/, loader: 'style-loader!css-loader!less-loader'},
20 | // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
21 | { test: /\.tsx?$/, loader: "awesome-typescript-loader" },
22 | {test: /\.(eot|woff|woff2|ttf|svg)([\\?]?.*)$/, loader: "file-loader"},
23 | // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
24 | { enforce: "pre", test: /\.js$/, loader: "source-map-loader" }
25 | ]
26 | },
27 | plugins: [
28 | new ExtractTextPlugin("xspreadsheet.css")
29 | ]
30 |
31 | // When importing a module whose path matches one of the following, just
32 | // assume a corresponding global variable exists and use that instead.
33 | // This is important because it allows us to avoid bundling all of our
34 | // dependencies, which allows browsers to cache those libraries between builds.
35 | // externals: {
36 | // "react": "React",
37 | // "react-dom": "ReactDOM"
38 | // },
39 | };
--------------------------------------------------------------------------------
|