├── .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 | [![npm package](https://img.shields.io/npm/v/xspreadsheet.svg)](https://www.npmjs.org/package/xspreadsheet) 4 | [![NPM downloads](http://img.shields.io/npm/dm/xspreadsheet.svg)](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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 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 |
23 |
24 |
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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 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 | }; --------------------------------------------------------------------------------