├── .babelrc
├── .eslintignore
├── .eslintrc.js
├── .github
└── FUNDING.yml
├── .gitignore
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── LICENSE
├── assets
├── material_common_sprite82.svg
└── sprite.svg
├── build
├── locale_loader.js
├── webpack.config.js
├── webpack.dev.js
├── webpack.locale.js
└── webpack.prod.js
├── dist
├── 58eaeb4e52248a5c75936c6f4c33a370.svg
├── ece3e4fa05d4292823fdef970eaf1233.svg
├── index.html
├── locale
│ ├── de.js
│ ├── en.js
│ ├── nl.js
│ └── zh-cn.js
├── xspreadsheet.css
├── xspreadsheet.css.map
├── xspreadsheet.js
└── xspreadsheet.js.map
├── docs
├── 58eaeb4e52248a5c75936c6f4c33a370.svg
├── demo.png
├── dist
│ ├── 58eaeb4e52248a5c75936c6f4c33a370.svg
│ ├── ece3e4fa05d4292823fdef970eaf1233.svg
│ ├── index.html
│ ├── locale
│ │ ├── de.js
│ │ ├── en.js
│ │ ├── nl.js
│ │ └── zh-cn.js
│ ├── xspreadsheet.css
│ ├── xspreadsheet.css.map
│ ├── xspreadsheet.js
│ └── xspreadsheet.js.map
├── ece3e4fa05d4292823fdef970eaf1233.svg
├── index.html
├── locale
│ ├── de.js
│ ├── en.js
│ ├── nl.js
│ └── zh-cn.js
├── xspreadsheet.css
├── xspreadsheet.css.map
├── xspreadsheet.js
└── xspreadsheet.js.map
├── index.html
├── npmx.txt
├── package-lock.json
├── package.json
├── readme.md
├── src
├── algorithm
│ ├── bitmap.js
│ └── expression.js
├── canvas
│ ├── draw.js
│ └── draw2.js
├── component
│ ├── border_palette.js
│ ├── bottombar.js
│ ├── button.js
│ ├── calendar.js
│ ├── color_palette.js
│ ├── contextmenu.js
│ ├── datepicker.js
│ ├── dropdown.js
│ ├── dropdown_align.js
│ ├── dropdown_border.js
│ ├── dropdown_color.js
│ ├── dropdown_font.js
│ ├── dropdown_fontsize.js
│ ├── dropdown_format.js
│ ├── dropdown_formula.js
│ ├── dropdown_linetype.js
│ ├── editor.js
│ ├── element.js
│ ├── event.js
│ ├── form_field.js
│ ├── form_input.js
│ ├── form_select.js
│ ├── icon.js
│ ├── message.js
│ ├── modal.js
│ ├── modal_validation.js
│ ├── resizer.js
│ ├── scrollbar.js
│ ├── selector.js
│ ├── sheet.js
│ ├── sort_filter.js
│ ├── suggest.js
│ ├── table.js
│ ├── toolbar.js
│ ├── toolbar
│ │ ├── align.js
│ │ ├── autofilter.js
│ │ ├── bold.js
│ │ ├── border.js
│ │ ├── clearformat.js
│ │ ├── dropdown_item.js
│ │ ├── fill_color.js
│ │ ├── font.js
│ │ ├── font_size.js
│ │ ├── format.js
│ │ ├── formula.js
│ │ ├── freeze.js
│ │ ├── icon_item.js
│ │ ├── index.js
│ │ ├── italic.js
│ │ ├── item.js
│ │ ├── merge.js
│ │ ├── more.js
│ │ ├── paintformat.js
│ │ ├── redo.js
│ │ ├── strike.js
│ │ ├── text_color.js
│ │ ├── textwrap.js
│ │ ├── toggle_item.js
│ │ ├── underline.js
│ │ ├── undo.js
│ │ └── valign.js
│ └── tooltip.js
├── config.js
├── core
│ ├── _.prototypes.js
│ ├── alphabet.js
│ ├── auto_filter.js
│ ├── cell.js
│ ├── cell_range.js
│ ├── clipboard.js
│ ├── col.js
│ ├── data_proxy.js
│ ├── font.js
│ ├── format.js
│ ├── formula.js
│ ├── helper.js
│ ├── history.js
│ ├── merge.js
│ ├── row.js
│ ├── scroll.js
│ ├── selector.js
│ ├── validation.js
│ └── validator.js
├── index.js
├── index.less
└── locale
│ ├── de.js
│ ├── en.js
│ ├── locale.js
│ ├── nl.js
│ └── zh-cn.js
└── test
├── core
├── alphabet_test.js
├── cell_range_test.js
├── cell_test.js
├── font_test.js
├── format_test.js
└── formula_test.js
├── helper_test.js
└── index_test.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env"],
3 | "plugins": ["@babel/plugin-proposal-class-properties"],
4 | }
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | build
2 | dist
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "extends": "airbnb-base",
3 | "rules": {
4 | "no-param-reassign": ["error", { "props": false }],
5 | "class-methods-use-this": "off",
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: x-spreadsheet # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | yarn.lock
3 |
4 | .nyc_output/*
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - 10.12.0
5 |
6 | branches:
7 | only:
8 | - master
9 |
10 | install:
11 | - npm install -g istanbul
12 | - npm install
13 |
14 | before_script:
15 |
16 | script:
17 | - npm run build
18 | - npm run test
19 | - npm run coverage
20 |
21 | after_script:
22 | - cp ./dist/* ./docs/ -r
23 |
24 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at liangyuliang0335@126.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/build/locale_loader.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | function getLocaleCode(name, code) {
4 | return `${code.replace('export default', 'const message =')}
5 | if (window && window.x && window.x.spreadsheet) {
6 | window.x.spreadsheet.$messages = window.x.spreadsheet.$messages || {};
7 | window.x.spreadsheet.$messages['${name}'] = message;
8 | }
9 | export default message;
10 | `;
11 | }
12 |
13 | module.exports = require('babel-loader').custom(babel => {
14 | return {
15 | result(result, { options }) {
16 | // console.log('options:', options);
17 | const lang = path.basename(options.filename, '.js');
18 | result.code = getLocaleCode(lang, result.code);
19 | return result;
20 | },
21 | };
22 | });
--------------------------------------------------------------------------------
/build/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
3 |
4 | const resolve = dir => path.join(__dirname, '..', dir);
5 |
6 | module.exports = {
7 | entry: {
8 | xspreadsheet: './src/index.js',
9 | },
10 | module: {
11 | rules: [
12 | {
13 | test: /\.js$/,
14 | use: {
15 | loader: 'babel-loader',
16 | options: {
17 | presets: ['@babel/preset-env'],
18 | }
19 | },
20 | include: [resolve('src'), resolve('test')],
21 | },
22 | {
23 | test: /\.css$/,
24 | use: [
25 | MiniCssExtractPlugin.loader,
26 | 'style-loader',
27 | 'css-loader',
28 | ],
29 | },
30 | {
31 | test: /\.less$/,
32 | use: [
33 | MiniCssExtractPlugin.loader,
34 | 'css-loader',
35 | 'less-loader',
36 | ],
37 | },
38 | {
39 | test: /\.(png|svg|jpg|gif)$/,
40 | use: [
41 | 'file-loader',
42 | ],
43 | },
44 | {
45 | test: /\.(woff|woff2|eot|ttf|otf)$/,
46 | use: [
47 | 'file-loader',
48 | ],
49 | },
50 | ],
51 | },
52 | };
53 |
--------------------------------------------------------------------------------
/build/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const merge = require('webpack-merge');
2 | const common = require('./webpack.config.js');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const CleanWebpackPlugin = require('clean-webpack-plugin');
5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
6 |
7 | module.exports = merge(common, {
8 | mode: 'development',
9 | plugins: [
10 | new CleanWebpackPlugin(['dist']),
11 | // you should know that the HtmlWebpackPlugin by default will generate its own index.html
12 | new HtmlWebpackPlugin({
13 | template: './index.html',
14 | title: 'x-spreadsheet',
15 | }),
16 | new MiniCssExtractPlugin({
17 | // Options similar to the same options in webpackOptions.output
18 | // both options are optional
19 | filename: '[name].[contenthash].css',
20 | // chunkFilename: devMode ? '[id].[hash].css' : '[id].css',
21 | }),
22 | ],
23 | output: {
24 | filename: '[name].[contenthash].js',
25 | },
26 | devtool: 'inline-source-map',
27 | devServer: {
28 | host: '0.0.0.0',
29 | contentBase: '../dist',
30 | },
31 | });
32 |
--------------------------------------------------------------------------------
/build/webpack.locale.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 |
4 | const localeFiles = fs.readdirSync(path.resolve(__dirname, '../src/locale'));
5 | const entry = {};
6 | localeFiles.forEach((file) => {
7 | const name = file.split('.')[0];
8 |
9 | if (name !== 'locale') {
10 | entry[name] = `./src/locale/${file}`;
11 | }
12 | });
13 |
14 | module.exports = {
15 | entry,
16 | output: {
17 | filename: '[name].js',
18 | path: path.resolve(__dirname, '../dist/locale'),
19 | },
20 | module: {
21 | rules: [
22 | {
23 | test: /\.js$/,
24 | loader: path.resolve(__dirname, 'locale_loader.js'),
25 | }
26 | ]
27 | },
28 | plugins: [
29 | ],
30 | };
31 |
--------------------------------------------------------------------------------
/build/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const merge = require('webpack-merge');
3 | const common = require('./webpack.config.js');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 | const CleanWebpackPlugin = require('clean-webpack-plugin');
6 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
7 |
8 | module.exports = merge(common, {
9 | mode: 'production',
10 | devtool: 'source-map',
11 | plugins: [
12 | new CleanWebpackPlugin(['dist']),
13 | // you should know that the HtmlWebpackPlugin by default will generate its own index.html
14 | new HtmlWebpackPlugin({
15 | template: './index.html',
16 | title: 'x-spreadsheet',
17 | }),
18 | new MiniCssExtractPlugin({
19 | // Options similar to the same options in webpackOptions.output
20 | // both options are optional
21 | filename: '[name].css',
22 | // chunkFilename: devMode ? '[id].[hash].css' : '[id].css',
23 | }),
24 | ],
25 | output: {
26 | filename: '[name].js',
27 | path: path.resolve(__dirname, '../dist'),
28 | },
29 | });
30 |
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | x-spreadsheet
7 |
8 |
9 |
10 |
11 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/dist/locale/de.js:
--------------------------------------------------------------------------------
1 | !function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){"use strict";n.r(t);const r={toolbar:{undo:"Rückgängig machen",redo:"Wiederherstellen",paintformat:"Format kopieren/einfügen",clearformat:"Format löschen",format:"Format",font:"Schriftart",fontSize:"Schriftgrad",fontBold:"Fett",fontItalic:"Kursiv",underline:"Betonen",strike:"Streichen",textColor:"Text Farbe",fillColor:"Füllung Farbe",border:"Umrandung",merge:"Zellen verbinden",align:"Waagrechte Ausrichtung",valign:"Vertikale uitlijning",textwrap:"Textumbruch",freeze:"Zelle sperren",formula:"Funktionen",more:"Mehr"},contextmenu:{copy:"Kopieren",cut:"Ausschneiden",paste:"Einfügen",pasteValue:"Nur Werte einfügen",pasteFormat:"Nur Format einfügen",insertRow:"Zeile einfügen",insertColumn:"Spalte einfügen",deleteRow:"Zeile löschen",deleteColumn:"Spalte löschen",deleteCell:"Zelle löschen",deleteCellText:"Zellentext löschen"},format:{normal:"Regulär",text:"Text",number:"Nummer",percent:"Prozent",rmb:"RMB",usd:"USD",date:"Datum",time:"Termin",datetime:"Datum Termin",duration:"Dauer"},formula:{sum:"Summe",average:"Durchschnittliche",max:"Max",min:"Min",concat:"Concat"}};window&&window.x&&window.x.spreadsheet&&(window.x.spreadsheet.$messages=window.x.spreadsheet.$messages||{},window.x.spreadsheet.$messages.de=r),t.default=r}]);
--------------------------------------------------------------------------------
/dist/locale/en.js:
--------------------------------------------------------------------------------
1 | !function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=1)}([,function(e,t,n){"use strict";n.r(t);const r={toolbar:{undo:"Undo",redo:"Redo",paintformat:"Paint format",clearformat:"Clear format",format:"Format",font:"Font",fontSize:"Font size",fontBold:"Font bold",fontItalic:"Font italic",underline:"Underline",strike:"Strike",textColor:"Text color",fillColor:"Fill color",border:"Borders",merge:"Merge cells",align:"Horizontal align",valign:"Vertical align",textwrap:"Text wrapping",freeze:"Freeze cell",formula:"Functions",more:"More"},contextmenu:{copy:"Copy",cut:"Cut",paste:"Paste",pasteValue:"Paste values only",pasteFormat:"Paste format only",insertRow:"Insert row",insertColumn:"Insert column",deleteRow:"Delete row",deleteColumn:"Delete column",deleteCell:"Delete cell",deleteCellText:"Delete cell text",validation:"Data validations"},format:{normal:"Normal",text:"Plain Text",number:"Number",percent:"Percent",rmb:"RMB",usd:"USD",date:"Date",time:"Time",datetime:"Date time",duration:"Duration"},formula:{sum:"Sum",average:"Average",max:"Max",min:"Min",concat:"Concat"},validation:{required:"it must be required",notMatch:"it not match its validation rule",between:"it is between {} and {}",notBetween:"it is not between {} and {}",notIn:"it is not in list",equal:"it equal to {}",notEqual:"it not equal to {}",lessThan:"it less than {}",lessThanEqual:"it less than or equal to {}",greaterThan:"it greater than {}",greaterThanEqual:"it greater than or equal to {}"},error:{pasteForMergedCell:"Unable to do this for merged cells"},calendar:{weeks:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"]},button:{cancel:"Cancel",remove:"Remove",save:"Save"},dataValidation:{mode:"Mode",range:"Cell Range",criteria:"Criteria",modeType:{cell:"Cell",column:"Colun",row:"Row"},type:{list:"List",number:"Number",date:"Date",phone:"Phone",email:"Email"},operator:{be:"between",nbe:"not betwwen",lt:"less than",lte:"less than or equal to",gt:"greater than",gte:"greater than or equal to",eq:"equal to",neq:"not equal to"}}};window&&window.x&&window.x.spreadsheet&&(window.x.spreadsheet.$messages=window.x.spreadsheet.$messages||{},window.x.spreadsheet.$messages.en=r),t.default=r}]);
--------------------------------------------------------------------------------
/dist/locale/nl.js:
--------------------------------------------------------------------------------
1 | !function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=2)}({2:function(e,t,n){"use strict";n.r(t);const r={toolbar:{undo:"Ongedaan maken",redo:"Opnieuw uitvoeren",paintformat:"Opmaak kopiëren/plakken",clearformat:"Opmaak wissen",format:"Opmaak",font:"Lettertype",fontSize:"Tekengrootte",fontBold:"Vet",fontItalic:"Cursief",underline:"Onderstrepen",strike:"Doorstrepen",textColor:"Tekstkleur",fillColor:"Opvulkleur",border:"Randen",merge:"Cellen samenvoegen",align:"Horizontale uitlijning",valign:"Verticale uitlijning",textwrap:"Terugloop",freeze:"Cel bevriezen",formula:"Functies",more:"Meer"},contextmenu:{copy:"Kopiëren",cut:"Knippen",paste:"Plakken",pasteValue:"Alleen waarden plakken",pasteFormat:"Alleen opmaak plakken",insertRow:"Rij invoegen",insertColumn:"Kolom invoegen",deleteRow:"Rij verwijderen",deleteColumn:"Kolom verwijderen",deleteCell:"Cel verwijderen",deleteCellText:"Celtekst verwijderen"},format:{normal:"Standaard",text:"Tekst",number:"Nummer",percent:"Percentage",rmb:"RMB",usd:"USD",date:"Datum",time:"Tijdstip",datetime:"Datum tijd",duration:"Duratie"},formula:{sum:"Som",average:"Gemiddelde",max:"Max",min:"Min",concat:"Concat"}};window&&window.x&&window.x.spreadsheet&&(window.x.spreadsheet.$messages=window.x.spreadsheet.$messages||{},window.x.spreadsheet.$messages.nl=r),t.default=r}});
--------------------------------------------------------------------------------
/dist/locale/zh-cn.js:
--------------------------------------------------------------------------------
1 | !function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=3)}({3:function(e,t,r){"use strict";r.r(t);const n={toolbar:{undo:"撤销",redo:"恢复",paintformat:"格式刷",clearformat:"清除格式",format:"数据格式",font:"字体",fontSize:"字号",fontBold:"加粗",fontItalic:"倾斜",underline:"下划线",strike:"删除线",textColor:"字体颜色",fillColor:"填充颜色",border:"边框",merge:"合并单元格",align:"水平对齐",valign:"垂直对齐",textwrap:"自动换行",freeze:"冻结",formula:"函数",more:"更多"},contextmenu:{copy:"复制",cut:"剪切",paste:"粘贴",pasteValue:"粘贴数据",pasteFormat:"粘贴格式",insertRow:"插入行",insertColumn:"插入列",deleteRow:"删除行",deleteColumn:"删除列",deleteCell:"删除",deleteCellText:"删除数据",validation:"数据验证"},format:{normal:"正常",text:"文本",number:"数值",percent:"百分比",rmb:"人民币",usd:"美元",date:"短日期",time:"时间",datetime:"长日期",duration:"持续时间"},formula:{sum:"求和",average:"求平均值",max:"求最大值",min:"求最小值",concat:"字符拼接"},validation:{required:"此值必填",notMatch:"此值不匹配验证规则",between:"此值应在 {} 和 {} 之间",notBetween:"此值不应在 {} 和 {} 之间",notIn:"此值不在列表中",equal:"此值应该等于 {}",notEqual:"此值不应该等于 {}",lessThan:"此值应该小于 {}",lessThanEqual:"此值应该小于等于 {}",greaterThan:"此值应该大于 {}",greaterThanEqual:"此值应该大于等于 {}"},error:{pasteForMergedCell:"无法对合并的单元格执行此操作"},calendar:{weeks:["日","一","二","三","四","五","六"],months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"]},button:{cancel:"取消",remove:"删除",save:"保存"},dataValidation:{mode:"模式",range:"单元区间",criteria:"条件",modeType:{cell:"单元格",column:"列模式",row:"行模式"},type:{list:"列表",number:"数字",date:"日期",phone:"手机号",email:"电子邮件"},operator:{be:"在区间",nbe:"不在区间",lt:"小于",lte:"小于等于",gt:"大于",gte:"大于等于",eq:"等于",neq:"不等于"}}};window&&window.x&&window.x.spreadsheet&&(window.x.spreadsheet.$messages=window.x.spreadsheet.$messages||{},window.x.spreadsheet.$messages["zh-cn"]=n),t.default=n}});
--------------------------------------------------------------------------------
/docs/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/odoo/x-spreadsheet/6e24e2e4174e2bbdd5fa67eb766eede4f39a01f2/docs/demo.png
--------------------------------------------------------------------------------
/docs/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | x-spreadsheet
7 |
8 |
9 |
10 |
11 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/docs/dist/locale/de.js:
--------------------------------------------------------------------------------
1 | !function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){"use strict";n.r(t);const r={toolbar:{undo:"Rückgängig machen",redo:"Wiederherstellen",paintformat:"Format kopieren/einfügen",clearformat:"Format löschen",format:"Format",font:"Schriftart",fontSize:"Schriftgrad",fontBold:"Fett",fontItalic:"Kursiv",underline:"Betonen",strike:"Streichen",textColor:"Text Farbe",fillColor:"Füllung Farbe",border:"Umrandung",merge:"Zellen verbinden",align:"Waagrechte Ausrichtung",valign:"Vertikale uitlijning",textwrap:"Textumbruch",freeze:"Zelle sperren",formula:"Funktionen",more:"Mehr"},contextmenu:{copy:"Kopieren",cut:"Ausschneiden",paste:"Einfügen",pasteValue:"Nur Werte einfügen",pasteFormat:"Nur Format einfügen",insertRow:"Zeile einfügen",insertColumn:"Spalte einfügen",deleteRow:"Zeile löschen",deleteColumn:"Spalte löschen",deleteCell:"Zelle löschen",deleteCellText:"Zellentext löschen"},format:{normal:"Regulär",text:"Text",number:"Nummer",percent:"Prozent",rmb:"RMB",usd:"USD",date:"Datum",time:"Termin",datetime:"Datum Termin",duration:"Dauer"},formula:{sum:"Summe",average:"Durchschnittliche",max:"Max",min:"Min",concat:"Concat"}};window&&window.x&&window.x.spreadsheet&&(window.x.spreadsheet.$messages=window.x.spreadsheet.$messages||{},window.x.spreadsheet.$messages.de=r),t.default=r}]);
--------------------------------------------------------------------------------
/docs/dist/locale/en.js:
--------------------------------------------------------------------------------
1 | !function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=1)}([,function(e,t,n){"use strict";n.r(t);const r={toolbar:{undo:"Undo",redo:"Redo",paintformat:"Paint format",clearformat:"Clear format",format:"Format",font:"Font",fontSize:"Font size",fontBold:"Font bold",fontItalic:"Font italic",underline:"Underline",strike:"Strike",textColor:"Text color",fillColor:"Fill color",border:"Borders",merge:"Merge cells",align:"Horizontal align",valign:"Vertical align",textwrap:"Text wrapping",freeze:"Freeze cell",formula:"Functions",more:"More"},contextmenu:{copy:"Copy",cut:"Cut",paste:"Paste",pasteValue:"Paste values only",pasteFormat:"Paste format only",insertRow:"Insert row",insertColumn:"Insert column",deleteRow:"Delete row",deleteColumn:"Delete column",deleteCell:"Delete cell",deleteCellText:"Delete cell text",validation:"Data validations"},format:{normal:"Normal",text:"Plain Text",number:"Number",percent:"Percent",rmb:"RMB",usd:"USD",date:"Date",time:"Time",datetime:"Date time",duration:"Duration"},formula:{sum:"Sum",average:"Average",max:"Max",min:"Min",concat:"Concat"},validation:{required:"it must be required",notMatch:"it not match its validation rule",between:"it is between {} and {}",notBetween:"it is not between {} and {}",notIn:"it is not in list",equal:"it equal to {}",notEqual:"it not equal to {}",lessThan:"it less than {}",lessThanEqual:"it less than or equal to {}",greaterThan:"it greater than {}",greaterThanEqual:"it greater than or equal to {}"},error:{pasteForMergedCell:"Unable to do this for merged cells"},calendar:{weeks:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"]},button:{cancel:"Cancel",remove:"Remove",save:"Save"},dataValidation:{mode:"Mode",range:"Cell Range",criteria:"Criteria",modeType:{cell:"Cell",column:"Colun",row:"Row"},type:{list:"List",number:"Number",date:"Date",phone:"Phone",email:"Email"},operator:{be:"between",nbe:"not betwwen",lt:"less than",lte:"less than or equal to",gt:"greater than",gte:"greater than or equal to",eq:"equal to",neq:"not equal to"}}};window&&window.x&&window.x.spreadsheet&&(window.x.spreadsheet.$messages=window.x.spreadsheet.$messages||{},window.x.spreadsheet.$messages.en=r),t.default=r}]);
--------------------------------------------------------------------------------
/docs/dist/locale/nl.js:
--------------------------------------------------------------------------------
1 | !function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=2)}({2:function(e,t,n){"use strict";n.r(t);const r={toolbar:{undo:"Ongedaan maken",redo:"Opnieuw uitvoeren",paintformat:"Opmaak kopiëren/plakken",clearformat:"Opmaak wissen",format:"Opmaak",font:"Lettertype",fontSize:"Tekengrootte",fontBold:"Vet",fontItalic:"Cursief",underline:"Onderstrepen",strike:"Doorstrepen",textColor:"Tekstkleur",fillColor:"Opvulkleur",border:"Randen",merge:"Cellen samenvoegen",align:"Horizontale uitlijning",valign:"Verticale uitlijning",textwrap:"Terugloop",freeze:"Cel bevriezen",formula:"Functies",more:"Meer"},contextmenu:{copy:"Kopiëren",cut:"Knippen",paste:"Plakken",pasteValue:"Alleen waarden plakken",pasteFormat:"Alleen opmaak plakken",insertRow:"Rij invoegen",insertColumn:"Kolom invoegen",deleteRow:"Rij verwijderen",deleteColumn:"Kolom verwijderen",deleteCell:"Cel verwijderen",deleteCellText:"Celtekst verwijderen"},format:{normal:"Standaard",text:"Tekst",number:"Nummer",percent:"Percentage",rmb:"RMB",usd:"USD",date:"Datum",time:"Tijdstip",datetime:"Datum tijd",duration:"Duratie"},formula:{sum:"Som",average:"Gemiddelde",max:"Max",min:"Min",concat:"Concat"}};window&&window.x&&window.x.spreadsheet&&(window.x.spreadsheet.$messages=window.x.spreadsheet.$messages||{},window.x.spreadsheet.$messages.nl=r),t.default=r}});
--------------------------------------------------------------------------------
/docs/dist/locale/zh-cn.js:
--------------------------------------------------------------------------------
1 | !function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=3)}({3:function(e,t,r){"use strict";r.r(t);const n={toolbar:{undo:"撤销",redo:"恢复",paintformat:"格式刷",clearformat:"清除格式",format:"数据格式",font:"字体",fontSize:"字号",fontBold:"加粗",fontItalic:"倾斜",underline:"下划线",strike:"删除线",textColor:"字体颜色",fillColor:"填充颜色",border:"边框",merge:"合并单元格",align:"水平对齐",valign:"垂直对齐",textwrap:"自动换行",freeze:"冻结",formula:"函数",more:"更多"},contextmenu:{copy:"复制",cut:"剪切",paste:"粘贴",pasteValue:"粘贴数据",pasteFormat:"粘贴格式",insertRow:"插入行",insertColumn:"插入列",deleteRow:"删除行",deleteColumn:"删除列",deleteCell:"删除",deleteCellText:"删除数据",validation:"数据验证"},format:{normal:"正常",text:"文本",number:"数值",percent:"百分比",rmb:"人民币",usd:"美元",date:"短日期",time:"时间",datetime:"长日期",duration:"持续时间"},formula:{sum:"求和",average:"求平均值",max:"求最大值",min:"求最小值",concat:"字符拼接"},validation:{required:"此值必填",notMatch:"此值不匹配验证规则",between:"此值应在 {} 和 {} 之间",notBetween:"此值不应在 {} 和 {} 之间",notIn:"此值不在列表中",equal:"此值应该等于 {}",notEqual:"此值不应该等于 {}",lessThan:"此值应该小于 {}",lessThanEqual:"此值应该小于等于 {}",greaterThan:"此值应该大于 {}",greaterThanEqual:"此值应该大于等于 {}"},error:{pasteForMergedCell:"无法对合并的单元格执行此操作"},calendar:{weeks:["日","一","二","三","四","五","六"],months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"]},button:{cancel:"取消",remove:"删除",save:"保存"},dataValidation:{mode:"模式",range:"单元区间",criteria:"条件",modeType:{cell:"单元格",column:"列模式",row:"行模式"},type:{list:"列表",number:"数字",date:"日期",phone:"手机号",email:"电子邮件"},operator:{be:"在区间",nbe:"不在区间",lt:"小于",lte:"小于等于",gt:"大于",gte:"大于等于",eq:"等于",neq:"不等于"}}};window&&window.x&&window.x.spreadsheet&&(window.x.spreadsheet.$messages=window.x.spreadsheet.$messages||{},window.x.spreadsheet.$messages["zh-cn"]=n),t.default=n}});
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | x-spreadsheet
7 |
8 |
9 |
10 |
11 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/docs/locale/de.js:
--------------------------------------------------------------------------------
1 | !function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){"use strict";n.r(t);const r={toolbar:{undo:"Rückgängig machen",redo:"Wiederherstellen",paintformat:"Format kopieren/einfügen",clearformat:"Format löschen",format:"Format",font:"Schriftart",fontSize:"Schriftgrad",fontBold:"Fett",fontItalic:"Kursiv",underline:"Betonen",strike:"Streichen",textColor:"Text Farbe",fillColor:"Füllung Farbe",border:"Umrandung",merge:"Zellen verbinden",align:"Waagrechte Ausrichtung",valign:"Vertikale uitlijning",textwrap:"Textumbruch",freeze:"Zelle sperren",formula:"Funktionen",more:"Mehr"},contextmenu:{copy:"Kopieren",cut:"Ausschneiden",paste:"Einfügen",pasteValue:"Nur Werte einfügen",pasteFormat:"Nur Format einfügen",insertRow:"Zeile einfügen",insertColumn:"Spalte einfügen",deleteRow:"Zeile löschen",deleteColumn:"Spalte löschen",deleteCell:"Zelle löschen",deleteCellText:"Zellentext löschen"},format:{normal:"Regulär",text:"Text",number:"Nummer",percent:"Prozent",rmb:"RMB",usd:"USD",date:"Datum",time:"Termin",datetime:"Datum Termin",duration:"Dauer"},formula:{sum:"Summe",average:"Durchschnittliche",max:"Max",min:"Min",concat:"Concat"}};window&&window.x&&window.x.spreadsheet&&(window.x.spreadsheet.$messages=window.x.spreadsheet.$messages||{},window.x.spreadsheet.$messages.de=r),t.default=r}]);
--------------------------------------------------------------------------------
/docs/locale/en.js:
--------------------------------------------------------------------------------
1 | !function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=1)}([,function(e,t,n){"use strict";n.r(t);const r={toolbar:{undo:"Undo",redo:"Redo",paintformat:"Paint format",clearformat:"Clear format",format:"Format",font:"Font",fontSize:"Font size",fontBold:"Font bold",fontItalic:"Font italic",underline:"Underline",strike:"Strike",textColor:"Text color",fillColor:"Fill color",border:"Borders",merge:"Merge cells",align:"Horizontal align",valign:"Vertical align",textwrap:"Text wrapping",freeze:"Freeze cell",formula:"Functions",more:"More"},contextmenu:{copy:"Copy",cut:"Cut",paste:"Paste",pasteValue:"Paste values only",pasteFormat:"Paste format only",insertRow:"Insert row",insertColumn:"Insert column",deleteRow:"Delete row",deleteColumn:"Delete column",deleteCell:"Delete cell",deleteCellText:"Delete cell text",validation:"Data validations"},format:{normal:"Normal",text:"Plain Text",number:"Number",percent:"Percent",rmb:"RMB",usd:"USD",date:"Date",time:"Time",datetime:"Date time",duration:"Duration"},formula:{sum:"Sum",average:"Average",max:"Max",min:"Min",concat:"Concat"},validation:{required:"it must be required",notMatch:"it not match its validation rule",between:"it is between {} and {}",notBetween:"it is not between {} and {}",notIn:"it is not in list",equal:"it equal to {}",notEqual:"it not equal to {}",lessThan:"it less than {}",lessThanEqual:"it less than or equal to {}",greaterThan:"it greater than {}",greaterThanEqual:"it greater than or equal to {}"},error:{pasteForMergedCell:"Unable to do this for merged cells"},calendar:{weeks:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"]},button:{cancel:"Cancel",remove:"Remove",save:"Save"},dataValidation:{mode:"Mode",range:"Cell Range",criteria:"Criteria",modeType:{cell:"Cell",column:"Colun",row:"Row"},type:{list:"List",number:"Number",date:"Date",phone:"Phone",email:"Email"},operator:{be:"between",nbe:"not betwwen",lt:"less than",lte:"less than or equal to",gt:"greater than",gte:"greater than or equal to",eq:"equal to",neq:"not equal to"}}};window&&window.x&&window.x.spreadsheet&&(window.x.spreadsheet.$messages=window.x.spreadsheet.$messages||{},window.x.spreadsheet.$messages.en=r),t.default=r}]);
--------------------------------------------------------------------------------
/docs/locale/nl.js:
--------------------------------------------------------------------------------
1 | !function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=2)}({2:function(e,t,n){"use strict";n.r(t);const r={toolbar:{undo:"Ongedaan maken",redo:"Opnieuw uitvoeren",paintformat:"Opmaak kopiëren/plakken",clearformat:"Opmaak wissen",format:"Opmaak",font:"Lettertype",fontSize:"Tekengrootte",fontBold:"Vet",fontItalic:"Cursief",underline:"Onderstrepen",strike:"Doorstrepen",textColor:"Tekstkleur",fillColor:"Opvulkleur",border:"Randen",merge:"Cellen samenvoegen",align:"Horizontale uitlijning",valign:"Verticale uitlijning",textwrap:"Terugloop",freeze:"Cel bevriezen",formula:"Functies",more:"Meer"},contextmenu:{copy:"Kopiëren",cut:"Knippen",paste:"Plakken",pasteValue:"Alleen waarden plakken",pasteFormat:"Alleen opmaak plakken",insertRow:"Rij invoegen",insertColumn:"Kolom invoegen",deleteRow:"Rij verwijderen",deleteColumn:"Kolom verwijderen",deleteCell:"Cel verwijderen",deleteCellText:"Celtekst verwijderen"},format:{normal:"Standaard",text:"Tekst",number:"Nummer",percent:"Percentage",rmb:"RMB",usd:"USD",date:"Datum",time:"Tijdstip",datetime:"Datum tijd",duration:"Duratie"},formula:{sum:"Som",average:"Gemiddelde",max:"Max",min:"Min",concat:"Concat"}};window&&window.x&&window.x.spreadsheet&&(window.x.spreadsheet.$messages=window.x.spreadsheet.$messages||{},window.x.spreadsheet.$messages.nl=r),t.default=r}});
--------------------------------------------------------------------------------
/docs/locale/zh-cn.js:
--------------------------------------------------------------------------------
1 | !function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=3)}({3:function(e,t,r){"use strict";r.r(t);const n={toolbar:{undo:"撤销",redo:"恢复",paintformat:"格式刷",clearformat:"清除格式",format:"数据格式",font:"字体",fontSize:"字号",fontBold:"加粗",fontItalic:"倾斜",underline:"下划线",strike:"删除线",textColor:"字体颜色",fillColor:"填充颜色",border:"边框",merge:"合并单元格",align:"水平对齐",valign:"垂直对齐",textwrap:"自动换行",freeze:"冻结",formula:"函数",more:"更多"},contextmenu:{copy:"复制",cut:"剪切",paste:"粘贴",pasteValue:"粘贴数据",pasteFormat:"粘贴格式",insertRow:"插入行",insertColumn:"插入列",deleteRow:"删除行",deleteColumn:"删除列",deleteCell:"删除",deleteCellText:"删除数据",validation:"数据验证"},format:{normal:"正常",text:"文本",number:"数值",percent:"百分比",rmb:"人民币",usd:"美元",date:"短日期",time:"时间",datetime:"长日期",duration:"持续时间"},formula:{sum:"求和",average:"求平均值",max:"求最大值",min:"求最小值",concat:"字符拼接"},validation:{required:"此值必填",notMatch:"此值不匹配验证规则",between:"此值应在 {} 和 {} 之间",notBetween:"此值不应在 {} 和 {} 之间",notIn:"此值不在列表中",equal:"此值应该等于 {}",notEqual:"此值不应该等于 {}",lessThan:"此值应该小于 {}",lessThanEqual:"此值应该小于等于 {}",greaterThan:"此值应该大于 {}",greaterThanEqual:"此值应该大于等于 {}"},error:{pasteForMergedCell:"无法对合并的单元格执行此操作"},calendar:{weeks:["日","一","二","三","四","五","六"],months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"]},button:{cancel:"取消",remove:"删除",save:"保存"},dataValidation:{mode:"模式",range:"单元区间",criteria:"条件",modeType:{cell:"单元格",column:"列模式",row:"行模式"},type:{list:"列表",number:"数字",date:"日期",phone:"手机号",email:"电子邮件"},operator:{be:"在区间",nbe:"不在区间",lt:"小于",lte:"小于等于",gt:"大于",gte:"大于等于",eq:"等于",neq:"不等于"}}};window&&window.x&&window.x.spreadsheet&&(window.x.spreadsheet.$messages=window.x.spreadsheet.$messages||{},window.x.spreadsheet.$messages["zh-cn"]=n),t.default=n}});
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | <%= htmlWebpackPlugin.options.title %>
7 |
8 |
9 |
10 |
11 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/npmx.txt:
--------------------------------------------------------------------------------
1 | mkdir x-spreadsheet && cd x-spreadsheet
2 | npm init -y
3 | npm install webpack webpack-cli --save-dev
4 |
5 | mkdir dist src
6 | touch webpack.config.js
7 |
8 |
9 | npm install --save-dev file-loader css-loader file-loader
10 | npm install --save-dev html-webpack-plugin
11 | npm install --save-dev clean-webpack-plugin
12 | npm install --save-dev webpack-dev-server
13 | npm install --save-dev webpack-merge
14 |
15 | # less
16 | npm install less --save-dev
17 | npm install less-loader --save-dev
18 |
19 | npm install eslint --save-dev
20 | ./node_modules/.bin/eslint --init # airbnb
21 |
22 |
23 | # test mocha
24 | npm install --save-dev mocha
25 |
26 | # babel
27 | npm install --save-dev babel-loader babel-core babel-preset-env
28 | # for macha
29 | npm install --save-dev babel-register
30 | # npm install --save-dev babel-plugin-transform-runtime
31 | # npm install --save babel-runtime
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "x-data-spreadsheet",
3 | "version": "1.0.27",
4 | "description": "a javascript xpreadsheet",
5 | "main": "src/index.js",
6 | "files": [
7 | "assets",
8 | "dist",
9 | "src"
10 | ],
11 | "author": "myliang ",
12 | "license": "MIT",
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/myliang/x-spreadsheet.git"
16 | },
17 | "nyc": {
18 | "all": true,
19 | "include": [
20 | "src/core/*.js"
21 | ],
22 | "exclude": [
23 | "**/*.spec.js"
24 | ]
25 | },
26 | "scripts": {
27 | "dev": "webpack-dev-server --open --config build/webpack.dev.js",
28 | "build": "webpack --config build/webpack.prod.js",
29 | "build-locale": "webpack --config build/webpack.locale.js",
30 | "lint": "./node_modules/eslint/bin/eslint.js src",
31 | "test": "nyc ./node_modules/mocha/bin/mocha --require @babel/register test/*",
32 | "coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov -t 31ecdb12-8ecb-46f7-a486-65c2516307dd",
33 | "postinstall": "opencollective-postinstall"
34 | },
35 | "keywords": [
36 | "javascript",
37 | "spreadsheet",
38 | "canvas"
39 | ],
40 | "devDependencies": {
41 | "@babel/core": "^7.3.4",
42 | "@babel/plugin-proposal-class-properties": "^7.4.4",
43 | "@babel/preset-env": "^7.3.4",
44 | "@babel/register": "^7.0.0",
45 | "babel-loader": "^8.0.5",
46 | "clean-webpack-plugin": "^0.1.19",
47 | "codecov": "^3.3.0",
48 | "css-loader": "^1.0.0",
49 | "eslint": "^5.5.0",
50 | "eslint-config-airbnb-base": "^13.1.0",
51 | "eslint-plugin-import": "^2.14.0",
52 | "file-loader": "^2.0.0",
53 | "html-webpack-plugin": "^3.2.0",
54 | "less": "^3.8.1",
55 | "less-loader": "^4.1.0",
56 | "mini-css-extract-plugin": "^0.4.4",
57 | "mocha": "^5.2.0",
58 | "nyc": "^13.3.0",
59 | "style-loader": "^0.23.0",
60 | "webpack": "^4.29.6",
61 | "webpack-cli": "^3.1.0",
62 | "webpack-dev-server": "^3.1.14",
63 | "webpack-merge": "^4.1.4"
64 | },
65 | "dependencies": {
66 | "opencollective-postinstall": "^2.0.2",
67 | "opencollective": "^1.0.3"
68 | },
69 | "collective": {
70 | "type": "opencollective",
71 | "url": "https://opencollective.com/x-spreadsheet"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # x-spreadsheet
2 |
3 | [](https://www.npmjs.org/package/x-data-spreadsheet)
4 | [](https://npmjs.org/package/x-data-spreadsheet)
5 | [](https://npmjs.org/package/x-data-spreadsheet)
6 | [](https://travis-ci.org/myliang/x-spreadsheet)
7 | [](https://codecov.io/gh/myliang/x-spreadsheet)
8 | 
9 | 
10 | [](https://gitter.im/x-datav/spreadsheet?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
11 |
12 | > A web-based JavaScript spreadsheet
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ## CDN
21 | ```html
22 |
23 |
24 |
25 |
28 | ```
29 |
30 | ## NPM
31 |
32 | ```shell
33 | npm install x-data-spreadsheet
34 | ```
35 |
36 | ```html
37 |
38 | ```
39 |
40 | ```javascript
41 | import Spreadsheet from "x-data-spreadsheet";
42 | // If you need to override the default options, you can set the override
43 | // const options = {};
44 | // new Spreadsheet('#x-spreadsheet-demo', options);
45 | const s = new Spreadsheet("#x-spreadsheet-demo")
46 | .loadData({}) // load data
47 | .change(data => {
48 | // save data to db
49 | });
50 |
51 | // data validation
52 | s.validate()
53 | ```
54 |
55 | ```javascript
56 | // default options
57 | {
58 | showToolbar: true,
59 | showGrid: true,
60 | showContextmenu: true,
61 | view: {
62 | height: () => document.documentElement.clientHeight,
63 | width: () => document.documentElement.clientWidth,
64 | },
65 | row: {
66 | len: 100,
67 | height: 25,
68 | },
69 | col: {
70 | len: 26,
71 | width: 100,
72 | indexWidth: 60,
73 | minWidth: 60,
74 | },
75 | style: {
76 | bgcolor: '#ffffff',
77 | align: 'left',
78 | valign: 'middle',
79 | textwrap: false,
80 | strike: false,
81 | underline: false,
82 | color: '#0a0a0a',
83 | font: {
84 | name: 'Helvetica',
85 | size: 10,
86 | bold: false,
87 | italic: false,
88 | },
89 | },
90 | }
91 | ```
92 |
93 | ## Internationalization
94 | ```javascript
95 | // npm
96 | import Spreadsheet from 'x-data-spreadsheet';
97 | import zhCN from 'x-data-spreadsheet/dist/locale/zh-cn';
98 |
99 | Spreadsheet.locale('zh-cn', zhCN);
100 | new Spreadsheet(document.getElementById('xss-demo'));
101 | ```
102 | ```html
103 |
104 |
105 |
106 |
107 |
108 |
111 | ```
112 |
113 | ## Features
114 | - Undo & Redo
115 | - Paint format
116 | - Clear format
117 | - Format
118 | - Font
119 | - Font size
120 | - Font bold
121 | - Font italic
122 | - Underline
123 | - Strike
124 | - Text color
125 | - Fill color
126 | - Borders
127 | - Merge cells
128 | - Align
129 | - Text wrapping
130 | - Freeze cell
131 | - Functions
132 | - Resize row-height, col-width
133 | - Copy, Cut, Paste
134 | - Autofill
135 | - Insert row, column
136 | - Delete row, column
137 | - Data validations
138 |
139 | ## Development
140 |
141 | ```sheel
142 | git clone https://github.com/myliang/x-spreadsheet.git
143 | cd x-spreadsheet
144 | npm install
145 | npm run dev
146 | ```
147 |
148 | Open your browser and visit http://127.0.0.1:8080.
149 |
150 | ## Browser Support
151 |
152 | Modern browsers(chrome, firefox, Safari).
153 |
154 | ## LICENSE
155 |
156 | MIT
157 |
--------------------------------------------------------------------------------
/src/algorithm/bitmap.js:
--------------------------------------------------------------------------------
1 | /* eslint no-bitwise: "off" */
2 | /*
3 | v: int value
4 | digit: bit len of v
5 | flag: true or false
6 | */
7 | const bitmap = (v, digit, flag) => {
8 | const b = 1 << digit;
9 | return flag ? (v | b) : (v ^ b);
10 | };
11 | export default bitmap;
12 |
--------------------------------------------------------------------------------
/src/algorithm/expression.js:
--------------------------------------------------------------------------------
1 | // src: include chars: [0-9], +, -, *, /
2 | // // 9+(3-1)*3+10/2 => 9 3 1-3*+ 10 2/+
3 | const infix2suffix = (src) => {
4 | const operatorStack = [];
5 | const stack = [];
6 | for (let i = 0; i < src.length; i += 1) {
7 | const c = src.charAt(i);
8 | if (c !== ' ') {
9 | if (c >= '0' && c <= '9') {
10 | stack.push(c);
11 | } else if (c === ')') {
12 | let c1 = operatorStack.pop();
13 | while (c1 !== '(') {
14 | stack.push(c1);
15 | c1 = operatorStack.pop();
16 | }
17 | } else {
18 | // priority: */ > +-
19 | if (operatorStack.length > 0 && (c === '+' || c === '-')) {
20 | const last = operatorStack[operatorStack.length - 1];
21 | if (last === '*' || last === '/') {
22 | while (operatorStack.length > 0) {
23 | stack.push(operatorStack.pop());
24 | }
25 | }
26 | }
27 | operatorStack.push(c);
28 | }
29 | }
30 | }
31 | while (operatorStack.length > 0) {
32 | stack.push(operatorStack.pop());
33 | }
34 | return stack;
35 | };
36 |
37 | export default {
38 | infix2suffix,
39 | };
40 |
--------------------------------------------------------------------------------
/src/canvas/draw2.js:
--------------------------------------------------------------------------------
1 | class Draw {
2 | constructor(el) {
3 | this.el = el;
4 | this.ctx = el.getContext('2d');
5 | }
6 |
7 | clear() {
8 | const { width, height } = this.el;
9 | this.ctx.clearRect(0, 0, width, height);
10 | return this;
11 | }
12 |
13 | attr(m) {
14 | Object.assign(this.ctx, m);
15 | return this;
16 | }
17 |
18 | save() {
19 | this.ctx.save();
20 | this.ctx.beginPath();
21 | return this;
22 | }
23 |
24 | restore() {
25 | this.ctx.restore();
26 | return this;
27 | }
28 |
29 | beginPath() {
30 | this.ctx.beginPath();
31 | return this;
32 | }
33 |
34 | closePath() {
35 | this.ctx.closePath();
36 | return this;
37 | }
38 |
39 | measureText(text) {
40 | return this.ctx.measureText(text);
41 | }
42 |
43 | rect(x, y, width, height) {
44 | this.ctx.rect(x, y, width, height);
45 | return this;
46 | }
47 |
48 | scale(x, y) {
49 | this.ctx.scale(x, y);
50 | return this;
51 | }
52 |
53 | rotate(angle) {
54 | this.ctx.rotate(angle);
55 | return this;
56 | }
57 |
58 | translate(x, y) {
59 | this.ctx.translate(x, y);
60 | return this;
61 | }
62 |
63 | transform(a, b, c, d, e) {
64 | this.ctx.transform(a, b, c, d, e);
65 | return this;
66 | }
67 |
68 | fillRect(x, y, w, h) {
69 | this.ctx.fillRect(x, y, w, h);
70 | return this;
71 | }
72 |
73 | strokeRect(x, y, w, h) {
74 | this.ctx.strokeRect(x, y, w, h);
75 | return this;
76 | }
77 |
78 | fillText(text, x, y, maxWidth) {
79 | this.ctx.fillText(text, x, y, maxWidth);
80 | return this;
81 | }
82 |
83 | strokeText(text, x, y, maxWidth) {
84 | this.ctx.strokeText(text, x, y, maxWidth);
85 | return this;
86 | }
87 | }
88 |
89 | export default {};
90 | export {
91 | Draw,
92 | };
93 |
--------------------------------------------------------------------------------
/src/component/border_palette.js:
--------------------------------------------------------------------------------
1 | import { h } from './element';
2 | import Icon from './icon';
3 | import DropdownColor from './dropdown_color';
4 | import DropdownLineType from './dropdown_linetype';
5 | import { cssPrefix } from '../config';
6 |
7 | function buildTable(...trs) {
8 | return h('table', '').child(
9 | h('tbody', '').children(...trs),
10 | );
11 | }
12 |
13 | function buildTd(iconName) {
14 | return h('td', '').child(
15 | h('div', `${cssPrefix}-border-palette-cell`).child(
16 | new Icon(`border-${iconName}`),
17 | ).on('click', () => {
18 | this.mode = iconName;
19 | const { mode, style, color } = this;
20 | this.change({ mode, style, color });
21 | }),
22 | );
23 | }
24 |
25 | export default class BorderPalette {
26 | constructor() {
27 | this.color = '#000';
28 | this.style = 'thin';
29 | this.mode = 'all';
30 | this.change = () => {};
31 | this.ddColor = new DropdownColor('line-color', this.color);
32 | this.ddColor.change = (color) => {
33 | this.color = color;
34 | };
35 | this.ddType = new DropdownLineType(this.style);
36 | this.ddType.change = ([s]) => {
37 | this.style = s;
38 | };
39 | this.el = h('div', `${cssPrefix}-border-palette`);
40 | const table = buildTable(
41 | h('tr', '').children(
42 | h('td', `${cssPrefix}-border-palette-left`).child(
43 | buildTable(
44 | h('tr', '').children(
45 | ...['all', 'inside', 'horizontal', 'vertical', 'outside'].map(it => buildTd.call(this, it)),
46 | ),
47 | h('tr', '').children(
48 | ...['left', 'top', 'right', 'bottom', 'none'].map(it => buildTd.call(this, it)),
49 | ),
50 | ),
51 | ),
52 | h('td', `${cssPrefix}-border-palette-right`).children(
53 | h('div', `${cssPrefix}-toolbar-btn`).child(this.ddColor.el),
54 | h('div', `${cssPrefix}-toolbar-btn`).child(this.ddType.el),
55 | ),
56 | ),
57 | );
58 | this.el.child(table);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/component/bottombar.js:
--------------------------------------------------------------------------------
1 | import { h } from './element';
2 | import { cssPrefix } from '../config';
3 |
4 | export default class Bottombar {
5 | constructor(datas) {
6 | this.datas = datas;
7 | this.el = h('div', `${cssPrefix}-bottombar`);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/component/button.js:
--------------------------------------------------------------------------------
1 | import { Element } from './element';
2 | import { cssPrefix } from '../config';
3 | import { t } from '../locale/locale';
4 |
5 | export default class Button extends Element {
6 | // type: primary
7 | constructor(title, type = '') {
8 | super('div', `${cssPrefix}-button ${type}`);
9 | this.child(t(`button.${title}`));
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/component/calendar.js:
--------------------------------------------------------------------------------
1 | import { h } from './element';
2 | import Icon from './icon';
3 | import { t } from '../locale/locale';
4 |
5 | function addMonth(date, step) {
6 | date.setMonth(date.getMonth() + step);
7 | }
8 |
9 | function weekday(date, index) {
10 | const d = new Date(date);
11 | d.setDate(index - date.getDay() + 1);
12 | return d;
13 | }
14 |
15 | function monthDays(year, month, cdate) {
16 | // the first day of month
17 | const startDate = new Date(year, month, 1, 23, 59, 59);
18 | const datess = [[], [], [], [], [], []];
19 | for (let i = 0; i < 6; i += 1) {
20 | for (let j = 0; j < 7; j += 1) {
21 | const index = i * 7 + j;
22 | const d = weekday(startDate, index);
23 | const disabled = d.getMonth() !== month;
24 | // console.log('d:', d, ', cdate:', cdate);
25 | const active = d.getMonth() === cdate.getMonth() && d.getDate() === cdate.getDate();
26 | datess[i][j] = { d, disabled, active };
27 | }
28 | }
29 | return datess;
30 | }
31 |
32 | export default class Calendar {
33 | constructor(value) {
34 | this.value = value;
35 | this.cvalue = new Date(value);
36 |
37 | this.headerLeftEl = h('div', 'calendar-header-left');
38 | this.bodyEl = h('tbody', '');
39 | this.buildAll();
40 | this.el = h('div', 'x-spreadsheet-calendar')
41 | .children(
42 | h('div', 'calendar-header').children(
43 | this.headerLeftEl,
44 | h('div', 'calendar-header-right').children(
45 | h('a', 'calendar-prev')
46 | .on('click.stop', () => this.prev())
47 | .child(new Icon('chevron-left')),
48 | h('a', 'calendar-next')
49 | .on('click.stop', () => this.next())
50 | .child(new Icon('chevron-right')),
51 | ),
52 | ),
53 | h('table', 'calendar-body').children(
54 | h('thead', '').child(
55 | h('tr', '').children(
56 | ...t('calendar.weeks').map(week => h('th', 'cell').child(week)),
57 | ),
58 | ),
59 | this.bodyEl,
60 | ),
61 | );
62 | this.selectChange = () => {};
63 | }
64 |
65 | setValue(value) {
66 | this.value = value;
67 | this.cvalue = new Date(value);
68 | this.buildAll();
69 | }
70 |
71 | prev() {
72 | const { value } = this;
73 | addMonth(value, -1);
74 | this.buildAll();
75 | }
76 |
77 | next() {
78 | const { value } = this;
79 | addMonth(value, 1);
80 | this.buildAll();
81 | }
82 |
83 | buildAll() {
84 | this.buildHeaderLeft();
85 | this.buildBody();
86 | }
87 |
88 | buildHeaderLeft() {
89 | const { value } = this;
90 | this.headerLeftEl.html(`${t('calendar.months')[value.getMonth()]} ${value.getFullYear()}`);
91 | }
92 |
93 | buildBody() {
94 | const { value, cvalue, bodyEl } = this;
95 | const mDays = monthDays(value.getFullYear(), value.getMonth(), cvalue);
96 | const trs = mDays.map((it) => {
97 | const tds = it.map((it1) => {
98 | let cls = 'cell';
99 | if (it1.disabled) cls += ' disabled';
100 | if (it1.active) cls += ' active';
101 | return h('td', '').child(
102 | h('div', cls)
103 | .on('click.stop', () => {
104 | this.selectChange(it1.d);
105 | })
106 | .child(it1.d.getDate().toString()),
107 | );
108 | });
109 | return h('tr', '').children(...tds);
110 | });
111 | bodyEl.html('').children(...trs);
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/component/color_palette.js:
--------------------------------------------------------------------------------
1 | import { h } from './element';
2 | import { cssPrefix } from '../config';
3 |
4 | const themeColorPlaceHolders = ['#ffffff', '#000100', '#e7e5e6', '#445569', '#5b9cd6', '#ed7d31', '#a5a5a5', '#ffc001', '#4371c6', '#71ae47'];
5 |
6 | const themeColors = [
7 | ['#f2f2f2', '#7f7f7f', '#d0cecf', '#d5dce4', '#deeaf6', '#fce5d5', '#ededed', '#fff2cd', '#d9e2f3', '#e3efd9'],
8 | ['#d8d8d8', '#595959', '#afabac', '#adb8ca', '#bdd7ee', '#f7ccac', '#dbdbdb', '#ffe59a', '#b3c6e7', '#c5e0b3'],
9 | ['#bfbfbf', '#3f3f3f', '#756f6f', '#8596b0', '#9cc2e6', '#f4b184', '#c9c9c9', '#fed964', '#8eaada', '#a7d08c'],
10 | ['#a5a5a5', '#262626', '#3a3839', '#333f4f', '#2e75b5', '#c45a10', '#7b7b7b', '#bf8e01', '#2f5596', '#538136'],
11 | ['#7f7f7f', '#0c0c0c', '#171516', '#222a35', '#1f4e7a', '#843c0a', '#525252', '#7e6000', '#203864', '#365624'],
12 | ];
13 |
14 | const standardColors = ['#c00000', '#fe0000', '#fdc101', '#ffff01', '#93d051', '#00b04e', '#01b0f1', '#0170c1', '#012060', '#7030a0'];
15 |
16 | function buildTd(bgcolor) {
17 | return h('td', '').child(
18 | h('div', `${cssPrefix}-color-palette-cell`)
19 | .on('click.stop', () => this.change(bgcolor))
20 | .css('background-color', bgcolor),
21 | );
22 | }
23 |
24 | export default class ColorPalette {
25 | constructor() {
26 | this.el = h('div', `${cssPrefix}-color-palette`);
27 | this.change = () => {};
28 | const table = h('table', '').children(
29 | h('tbody', '').children(
30 | h('tr', `${cssPrefix}-theme-color-placeholders`).children(
31 | ...themeColorPlaceHolders.map(color => buildTd.call(this, color)),
32 | ),
33 | ...themeColors.map(it => h('tr', `${cssPrefix}-theme-colors`).children(
34 | ...it.map(color => buildTd.call(this, color)),
35 | )),
36 | h('tr', `${cssPrefix}-standard-colors`).children(
37 | ...standardColors.map(color => buildTd.call(this, color)),
38 | ),
39 | ),
40 | );
41 | this.el.child(table);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/component/contextmenu.js:
--------------------------------------------------------------------------------
1 | import { h } from './element';
2 | import { bindClickoutside, unbindClickoutside } from './event';
3 | import { cssPrefix } from '../config';
4 | import { tf } from '../locale/locale';
5 |
6 | const menuItems = [
7 | { key: 'copy', title: tf('contextmenu.copy'), label: 'Ctrl+C' },
8 | { key: 'cut', title: tf('contextmenu.cut'), label: 'Ctrl+X' },
9 | { key: 'paste', title: tf('contextmenu.paste'), label: 'Ctrl+V' },
10 | { key: 'paste-value', title: tf('contextmenu.pasteValue'), label: 'Ctrl+Shift+V' },
11 | { key: 'paste-format', title: tf('contextmenu.pasteFormat'), label: 'Ctrl+Alt+V' },
12 | { key: 'divider' },
13 | { key: 'insert-row', title: tf('contextmenu.insertRow') },
14 | { key: 'insert-column', title: tf('contextmenu.insertColumn') },
15 | { key: 'divider' },
16 | { key: 'delete-row', title: tf('contextmenu.deleteRow') },
17 | { key: 'delete-column', title: tf('contextmenu.deleteColumn') },
18 | { key: 'delete-cell-text', title: tf('contextmenu.deleteCellText') },
19 | { key: 'divider' },
20 | { key: 'validation', title: tf('contextmenu.validation') },
21 | { key: 'divider' },
22 | { key: 'cell-printable', title: tf('contextmenu.cellprintable') },
23 | { key: 'cell-non-printable', title: tf('contextmenu.cellnonprintable') },
24 | { key: 'divider' },
25 | { key: 'cell-editable', title: tf('contextmenu.celleditable') },
26 | { key: 'cell-non-editable', title: tf('contextmenu.cellnoneditable') },
27 | ];
28 |
29 | function buildMenuItem(item) {
30 | if (item.key === 'divider') {
31 | return h('div', `${cssPrefix}-item divider`);
32 | }
33 | return h('div', `${cssPrefix}-item`)
34 | .on('click', () => {
35 | this.itemClick(item.key);
36 | this.hide();
37 | })
38 | .children(
39 | item.title(),
40 | h('div', 'label').child(item.label || ''),
41 | );
42 | }
43 |
44 | function buildMenu() {
45 |
46 | return menuItems.map(it => buildMenuItem.call(this, it));
47 | }
48 |
49 | export default class ContextMenu {
50 | constructor(viewFn, isHide = false) {
51 | this.el = h('div', `${cssPrefix}-contextmenu`)
52 | .children(...buildMenu.call(this))
53 | .hide();
54 | this.viewFn = viewFn;
55 | this.itemClick = () => {};
56 | this.isHide = isHide;
57 | }
58 |
59 | hide() {
60 | const { el } = this;
61 | el.hide();
62 | unbindClickoutside(el);
63 | }
64 |
65 | setPosition(x, y) {
66 | if (this.isHide) return;
67 | const { el } = this;
68 | const { height, width } = el.show().offset();
69 | const view = this.viewFn();
70 | let top = y;
71 | let left = x;
72 | if (view.height - y <= height) {
73 | top -= height;
74 | }
75 | if (view.width - x <= width) {
76 | left -= width;
77 | }
78 | el.offset({ left, top });
79 | bindClickoutside(el);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/component/datepicker.js:
--------------------------------------------------------------------------------
1 | import Calendar from './calendar';
2 | import { h } from './element';
3 | import { cssPrefix } from '../config';
4 |
5 | export default class Datepicker {
6 | constructor() {
7 | this.calendar = new Calendar(new Date());
8 | this.el = h('div', `${cssPrefix}-datepicker`).child(
9 | this.calendar.el,
10 | ).hide();
11 | }
12 |
13 | setValue(date) {
14 | // console.log(':::::::', date, typeof date, date instanceof string);
15 | const { calendar } = this;
16 | if (typeof date === 'string') {
17 | // console.log(/^\d{4}-\d{1,2}-\d{1,2}$/.test(date));
18 | if (/^\d{4}-\d{1,2}-\d{1,2}$/.test(date)) {
19 | calendar.setValue(new Date(date.replace(new RegExp('-', 'g'), '/')));
20 | }
21 | } else if (date instanceof Date) {
22 | calendar.setValue(date);
23 | }
24 | return this;
25 | }
26 |
27 | change(cb) {
28 | this.calendar.selectChange = (d) => {
29 | cb(d);
30 | this.hide();
31 | };
32 | }
33 |
34 | show() {
35 | this.el.show();
36 | }
37 |
38 | hide() {
39 | this.el.hide();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/component/dropdown.js:
--------------------------------------------------------------------------------
1 | import { Element, h } from './element';
2 | import { bindClickoutside, unbindClickoutside } from './event';
3 | import { cssPrefix } from '../config';
4 |
5 | export default class Dropdown extends Element {
6 | constructor(title, width, showArrow, placement, ...children) {
7 | super('div', `${cssPrefix}-dropdown ${placement}`);
8 | this.title = title;
9 | this.change = () => {};
10 | if (typeof title === 'string') {
11 | this.title = h('div', `${cssPrefix}-dropdown-title`).child(title);
12 | } else if (showArrow) {
13 | this.title.addClass('arrow-left');
14 | }
15 | this.contentEl = h('div', `${cssPrefix}-dropdown-content`)
16 | .children(...children)
17 | .css('width', width)
18 | .hide();
19 |
20 | this.headerEl = h('div', `${cssPrefix}-dropdown-header`);
21 | this.headerEl.on('click', () => {
22 | if (this.contentEl.css('display') !== 'block') {
23 | this.show();
24 | } else {
25 | this.hide();
26 | }
27 | }).children(
28 | this.title,
29 | showArrow ? h('div', `${cssPrefix}-icon arrow-right`).child(
30 | h('div', `${cssPrefix}-icon-img arrow-down`),
31 | ) : '',
32 | );
33 | this.children(this.headerEl, this.contentEl);
34 | }
35 |
36 | setTitle(title) {
37 | this.title.html(title);
38 | this.hide();
39 | }
40 |
41 | show() {
42 | const { contentEl } = this;
43 | contentEl.show();
44 | this.parent().active();
45 | bindClickoutside(this.parent(), () => {
46 | this.hide();
47 | });
48 | }
49 |
50 | hide() {
51 | this.parent().active(false);
52 | this.contentEl.hide();
53 | unbindClickoutside(this.parent());
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/component/dropdown_align.js:
--------------------------------------------------------------------------------
1 | import Dropdown from './dropdown';
2 | import { h } from './element';
3 | import Icon from './icon';
4 | import { cssPrefix } from '../config';
5 |
6 | function buildItemWithIcon(iconName) {
7 | return h('div', `${cssPrefix}-item`).child(new Icon(iconName));
8 | }
9 |
10 | export default class DropdownAlign extends Dropdown {
11 | constructor(aligns, align) {
12 | const icon = new Icon(`align-${align}`);
13 | const naligns = aligns.map(it => buildItemWithIcon(`align-${it}`)
14 | .on('click', () => {
15 | this.setTitle(it);
16 | this.change(it);
17 | }));
18 | super(icon, 'auto', true, 'bottom-left', ...naligns);
19 | }
20 |
21 | setTitle(align) {
22 | this.title.setName(`align-${align}`);
23 | this.hide();
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/component/dropdown_border.js:
--------------------------------------------------------------------------------
1 | import Dropdown from './dropdown';
2 | import Icon from './icon';
3 | import BorderPalette from './border_palette';
4 |
5 | export default class DropdownBorder extends Dropdown {
6 | constructor() {
7 | const icon = new Icon('border-all');
8 | const borderPalette = new BorderPalette();
9 | borderPalette.change = (v) => {
10 | this.change(v);
11 | this.hide();
12 | };
13 | super(icon, 'auto', false, 'bottom-left', borderPalette.el);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/component/dropdown_color.js:
--------------------------------------------------------------------------------
1 | import Dropdown from './dropdown';
2 | import Icon from './icon';
3 | import ColorPalette from './color_palette';
4 |
5 | export default class DropdownColor extends Dropdown {
6 | constructor(iconName, color) {
7 | const icon = new Icon(iconName)
8 | .css('height', '16px')
9 | .css('border-bottom', `3px solid ${color}`);
10 | const colorPalette = new ColorPalette();
11 | colorPalette.change = (v) => {
12 | this.setTitle(v);
13 | this.change(v);
14 | };
15 | super(icon, 'auto', false, 'bottom-left', colorPalette.el);
16 | }
17 |
18 | setTitle(color) {
19 | this.title.css('border-color', color);
20 | this.hide();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/component/dropdown_font.js:
--------------------------------------------------------------------------------
1 | import Dropdown from './dropdown';
2 | import { h } from './element';
3 | import { baseFonts } from '../core/font';
4 | import { cssPrefix } from '../config';
5 |
6 | export default class DropdownFont extends Dropdown {
7 | constructor() {
8 | const nfonts = baseFonts.map(it => h('div', `${cssPrefix}-item`)
9 | .on('click', () => {
10 | this.setTitle(it.title);
11 | this.change(it);
12 | })
13 | .child(it.title));
14 | super(baseFonts[0].title, '160px', true, 'bottom-left', ...nfonts);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/component/dropdown_fontsize.js:
--------------------------------------------------------------------------------
1 | import Dropdown from './dropdown';
2 | import { h } from './element';
3 | import { fontSizes } from '../core/font';
4 | import { cssPrefix } from '../config';
5 |
6 | export default class DropdownFontSize extends Dropdown {
7 | constructor() {
8 | const nfontSizes = fontSizes.map(it => h('div', `${cssPrefix}-item`)
9 | .on('click', () => {
10 | this.setTitle(`${it.pt}`);
11 | this.change(it);
12 | })
13 | .child(`${it.pt}`));
14 | super('10', '60px', true, 'bottom-left', ...nfontSizes);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/component/dropdown_format.js:
--------------------------------------------------------------------------------
1 | import Dropdown from './dropdown';
2 | import { h } from './element';
3 | import { baseFormats } from '../core/format';
4 | import { cssPrefix } from '../config';
5 |
6 | export default class DropdownFormat extends Dropdown {
7 | constructor() {
8 | let nformats = baseFormats.slice(0);
9 | nformats.splice(2, 0, { key: 'divider' });
10 | nformats.splice(8, 0, { key: 'divider' });
11 | nformats = nformats.map((it) => {
12 | const item = h('div', `${cssPrefix}-item`);
13 | if (it.key === 'divider') {
14 | item.addClass('divider');
15 | } else {
16 | item.child(it.title())
17 | .on('click', () => {
18 | this.setTitle(it.title());
19 | this.change(it);
20 | });
21 | if (it.label) item.child(h('div', 'label').html(it.label));
22 | }
23 | return item;
24 | });
25 | super('Normal', '220px', true, 'bottom-left', ...nformats);
26 | }
27 |
28 | setTitle(key) {
29 | for (let i = 0; i < baseFormats.length; i += 1) {
30 | if (baseFormats[i].key === key) {
31 | this.title.html(baseFormats[i].title);
32 | }
33 | }
34 | this.hide();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/component/dropdown_formula.js:
--------------------------------------------------------------------------------
1 | import Dropdown from './dropdown';
2 | import Icon from './icon';
3 | import { h } from './element';
4 | import { baseFormulas } from '../core/formula';
5 | import { cssPrefix } from '../config';
6 |
7 | export default class DropdownFormula extends Dropdown {
8 | constructor() {
9 | const nformulas = baseFormulas.map(it => h('div', `${cssPrefix}-item`)
10 | .on('click', () => {
11 | this.hide();
12 | this.change(it);
13 | })
14 | .child(it.key));
15 | super(new Icon('formula'), '180px', true, 'bottom-left', ...nformulas);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/component/dropdown_linetype.js:
--------------------------------------------------------------------------------
1 | import Dropdown from './dropdown';
2 | import { h } from './element';
3 | import Icon from './icon';
4 | import { cssPrefix } from '../config';
5 |
6 | const lineTypes = [
7 | ['thin', ''],
8 | ['medium', ''],
9 | ['thick', ''],
10 | ['dashed', ''],
11 | ['dotted', ''],
12 | // ['double', ''],
13 | ];
14 |
15 | export default class DropdownLineType extends Dropdown {
16 | constructor(type) {
17 | const icon = new Icon('line-type');
18 | let beforei = 0;
19 | const lineTypeEls = lineTypes.map((it, iti) => h('div', `${cssPrefix}-item state ${type === it[0] ? 'checked' : ''}`)
20 | .on('click', () => {
21 | lineTypeEls[beforei].toggle('checked');
22 | lineTypeEls[iti].toggle('checked');
23 | beforei = iti;
24 | this.hide();
25 | this.change(it);
26 | })
27 | .child(
28 | h('div', `${cssPrefix}-line-type`).html(it[1]),
29 | ));
30 |
31 | super(icon, 'auto', false, 'bottom-left', ...lineTypeEls);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/component/editor.js:
--------------------------------------------------------------------------------
1 | //* global window */
2 | import { h } from './element';
3 | import Suggest from './suggest';
4 | import Datepicker from './datepicker';
5 | import { cssPrefix } from '../config';
6 | // import { mouseMoveUp } from '../event';
7 |
8 | function resetTextareaSize() {
9 | if (!/^\s*$/.test(this.inputText)) {
10 | const {
11 | textlineEl, textEl, areaOffset,
12 | } = this;
13 | const tlineWidth = textlineEl.offset().width + 9;
14 | const maxWidth = this.viewFn().width - areaOffset.left - 9;
15 | // console.log('tlineWidth:', tlineWidth, ':', maxWidth);
16 | if (tlineWidth > areaOffset.width) {
17 | let twidth = tlineWidth;
18 | if (tlineWidth > maxWidth) {
19 | twidth = maxWidth;
20 | let h1 = parseInt(tlineWidth / maxWidth, 10);
21 | h1 += (tlineWidth % maxWidth) > 0 ? 1 : 0;
22 | h1 *= this.rowHeight;
23 | if (h1 > areaOffset.height) {
24 | textEl.css('height', `${h1}px`);
25 | }
26 | }
27 | textEl.css('width', `${twidth}px`);
28 | }
29 | }
30 | }
31 |
32 | function inputEventHandler(evt) {
33 | const v = evt.target.value;
34 | // console.log(evt, 'v:', v);
35 | const { suggest, textlineEl, validator } = this;
36 | const cell = this.cell;
37 | if(cell !== null){
38 | if(("editable" in cell && cell.editable == true) || (cell['editable'] === undefined)) {
39 | this.inputText = v;
40 | if (validator) {
41 | if (validator.type === 'list') {
42 | suggest.search(v);
43 | } else {
44 | suggest.hide();
45 | }
46 | } else {
47 | const start = v.lastIndexOf('=');
48 | if (start !== -1) {
49 | suggest.search(v.substring(start + 1));
50 | } else {
51 | suggest.hide();
52 | }
53 | }
54 | textlineEl.html(v);
55 | resetTextareaSize.call(this);
56 | this.change('input', v);
57 | }
58 | else {
59 | evt.target.value = "";
60 | }
61 | }
62 | else {
63 | this.inputText = v;
64 | if (validator) {
65 | if (validator.type === 'list') {
66 | suggest.search(v);
67 | } else {
68 | suggest.hide();
69 | }
70 | } else {
71 | const start = v.lastIndexOf('=');
72 | if (start !== -1) {
73 | suggest.search(v.substring(start + 1));
74 | } else {
75 | suggest.hide();
76 | }
77 | }
78 | textlineEl.html(v);
79 | resetTextareaSize.call(this);
80 | this.change('input', v);
81 | }
82 | }
83 |
84 | function setTextareaRange(position) {
85 | const { el } = this.textEl;
86 | setTimeout(() => {
87 | el.focus();
88 | el.setSelectionRange(position, position);
89 | }, 0);
90 | }
91 |
92 | function setText(text, position) {
93 | const { textEl, textlineEl } = this;
94 | // firefox bug
95 | textEl.el.blur();
96 |
97 | textEl.val(text);
98 | textlineEl.html(text);
99 | setTextareaRange.call(this, position);
100 | }
101 |
102 | function suggestItemClick(it) {
103 | const { inputText, validator } = this;
104 | let position = 0;
105 | if (validator && validator.type === 'list') {
106 | this.inputText = it;
107 | position = this.inputText.length;
108 | } else {
109 | const start = inputText.lastIndexOf('=');
110 | const sit = inputText.substring(0, start + 1);
111 | let eit = inputText.substring(start + 1);
112 | if (eit.indexOf(')') !== -1) {
113 | eit = eit.substring(eit.indexOf(')'));
114 | } else {
115 | eit = '';
116 | }
117 | this.inputText = `${sit + it.key}(`;
118 | // console.log('inputText:', this.inputText);
119 | position = this.inputText.length;
120 | this.inputText += `)${eit}`;
121 | }
122 | setText.call(this, this.inputText, position);
123 | }
124 |
125 | function resetSuggestItems() {
126 | this.suggest.setItems(this.formulas);
127 | }
128 |
129 | function dateFormat(d) {
130 | let month = d.getMonth() + 1;
131 | let date = d.getDate();
132 | if (month < 10) month = `0${month}`;
133 | if (date < 10) date = `0${date}`;
134 | return `${d.getFullYear()}-${month}-${date}`;
135 | }
136 |
137 | export default class Editor {
138 | constructor(formulas, viewFn, rowHeight) {
139 | this.viewFn = viewFn;
140 | this.rowHeight = rowHeight;
141 | this.formulas = formulas;
142 | this.suggest = new Suggest(formulas, (it) => {
143 | suggestItemClick.call(this, it);
144 | });
145 | this.datepicker = new Datepicker();
146 | this.datepicker.change((d) => {
147 | // console.log('d:', d);
148 | this.setText(dateFormat(d));
149 | this.clear();
150 | });
151 | this.areaEl = h('div', `${cssPrefix}-editor-area`)
152 | .children(
153 | this.textEl = h('textarea', '')
154 | .on('input', evt => inputEventHandler.call(this, evt)),
155 | this.textlineEl = h('div', 'textline'),
156 | this.suggest.el,
157 | this.datepicker.el,
158 | )
159 | .on('mousemove.stop', () => {})
160 | .on('mousedown.stop', () => {});
161 | this.el = h('div', `${cssPrefix}-editor`)
162 | .child(this.areaEl).hide();
163 | this.suggest.bindInputEvents(this.textEl);
164 |
165 | this.areaOffset = null;
166 | this.freeze = { w: 0, h: 0 };
167 | this.cell = null;
168 | this.inputText = '';
169 | this.change = () => {};
170 | }
171 |
172 | setFreezeLengths(width, height) {
173 | this.freeze.w = width;
174 | this.freeze.h = height;
175 | }
176 |
177 | clear() {
178 | // const { cell } = this;
179 | // const cellText = (cell && cell.text) || '';
180 | if (this.inputText !== '') {
181 | this.change('finished', this.inputText);
182 | }
183 | this.cell = null;
184 | this.areaOffset = null;
185 | this.inputText = '';
186 | this.el.hide();
187 | this.textEl.val('');
188 | this.textlineEl.html('');
189 | resetSuggestItems.call(this);
190 | this.datepicker.hide();
191 | }
192 |
193 | setOffset(offset, suggestPosition = 'top') {
194 | const {
195 | textEl, areaEl, suggest, freeze, el,
196 | } = this;
197 | if (offset) {
198 | this.areaOffset = offset;
199 | const {
200 | left, top, width, height, l, t,
201 | } = offset;
202 | // console.log('left:', left, ',top:', top, ', freeze:', freeze);
203 | const elOffset = { left: 0, top: 0 };
204 | // top left
205 | if (freeze.w > l && freeze.h > t) {
206 | //
207 | } else if (freeze.w < l && freeze.h < t) {
208 | elOffset.left = freeze.w;
209 | elOffset.top = freeze.h;
210 | } else if (freeze.w > l) {
211 | elOffset.top = freeze.h;
212 | } else if (freeze.h > t) {
213 | elOffset.left = freeze.w;
214 | }
215 | el.offset(elOffset);
216 | areaEl.offset({ left: left - elOffset.left - 0.8, top: top - elOffset.top - 0.8 });
217 | textEl.offset({ width: width - 9 + 0.8, height: height - 3 + 0.8 });
218 | const sOffset = { left: 0 };
219 | sOffset[suggestPosition] = height;
220 | suggest.setOffset(sOffset);
221 | suggest.hide();
222 | }
223 | }
224 |
225 | setCell(cell, validator) {
226 | // console.log('::', validator);
227 | const { el, datepicker, suggest } = this;
228 | el.show();
229 | this.cell = cell;
230 | const text = (cell && cell.text) || '';
231 | this.setText(text);
232 |
233 | this.validator = validator;
234 | if (validator) {
235 | const { type } = validator;
236 | if (type === 'date') {
237 | datepicker.show();
238 | if (!/^\s*$/.test(text)) {
239 | datepicker.setValue(text);
240 | }
241 | }
242 | if (type === 'list') {
243 | suggest.setItems(validator.values());
244 | suggest.search('');
245 | }
246 | }
247 | }
248 |
249 | setText(text) {
250 | this.inputText = text;
251 | // console.log('text>>:', text);
252 | setText.call(this, text, text.length);
253 | resetTextareaSize.call(this);
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/src/component/element.js:
--------------------------------------------------------------------------------
1 | /* global document */
2 | /* global window */
3 | class Element {
4 | constructor(tag, className = '') {
5 | if (typeof tag === 'string') {
6 | this.el = document.createElement(tag);
7 | this.el.className = className;
8 | } else {
9 | this.el = tag;
10 | }
11 | this.data = {};
12 | }
13 |
14 | data(key, value) {
15 | if (value !== undefined) {
16 | this.data[key] = value;
17 | return this;
18 | }
19 | return this.data[key];
20 | }
21 |
22 | on(eventNames, handler) {
23 | const [fen, ...oen] = eventNames.split('.');
24 | let eventName = fen;
25 | if (eventName === 'mousewheel' && /Firefox/i.test(window.navigator.userAgent)) {
26 | eventName = 'DOMMouseScroll';
27 | }
28 | this.el.addEventListener(eventName, (evt) => {
29 | handler(evt);
30 | for (let i = 0; i < oen.length; i += 1) {
31 | const k = oen[i];
32 | if (k === 'left' && evt.button !== 0) {
33 | return;
34 | }
35 | if (k === 'right' && evt.button !== 2) {
36 | return;
37 | }
38 | if (k === 'stop') {
39 | evt.stopPropagation();
40 | }
41 | }
42 | });
43 | return this;
44 | }
45 |
46 | offset(value) {
47 | if (value !== undefined) {
48 | Object.keys(value).forEach((k) => {
49 | this.css(k, `${value[k]}px`);
50 | });
51 | return this;
52 | }
53 | const {
54 | offsetTop, offsetLeft, offsetHeight, offsetWidth,
55 | } = this.el;
56 | return {
57 | top: offsetTop,
58 | left: offsetLeft,
59 | height: offsetHeight,
60 | width: offsetWidth,
61 | };
62 | }
63 |
64 | scroll(v) {
65 | const { el } = this;
66 | if (v !== undefined) {
67 | if (v.left !== undefined) {
68 | el.scrollLeft = v.left;
69 | }
70 | if (v.top !== undefined) {
71 | el.scrollTop = v.top;
72 | }
73 | }
74 | return { left: el.scrollLeft, top: el.scrollTop };
75 | }
76 |
77 | box() {
78 | return this.el.getBoundingClientRect();
79 | }
80 |
81 | parent() {
82 | return new Element(this.el.parentNode);
83 | }
84 |
85 | children(...eles) {
86 | if (arguments.length === 0) {
87 | return this.el.childNodes;
88 | }
89 | eles.forEach(ele => this.child(ele));
90 | return this;
91 | }
92 |
93 | /*
94 | first() {
95 | return this.el.firstChild;
96 | }
97 |
98 | last() {
99 | return this.el.lastChild;
100 | }
101 |
102 | remove(ele) {
103 | return this.el.removeChild(ele);
104 | }
105 |
106 | prepend(ele) {
107 | const { el } = this;
108 | if (el.children.length > 0) {
109 | el.insertBefore(ele, el.firstChild);
110 | } else {
111 | el.appendChild(ele);
112 | }
113 | return this;
114 | }
115 |
116 | prev() {
117 | return this.el.previousSibling;
118 | }
119 |
120 | next() {
121 | return this.el.nextSibling;
122 | }
123 | */
124 |
125 | child(arg) {
126 | let ele = arg;
127 | if (typeof arg === 'string') {
128 | ele = document.createTextNode(arg);
129 | } else if (arg instanceof Element) {
130 | ele = arg.el;
131 | }
132 | this.el.appendChild(ele);
133 | return this;
134 | }
135 |
136 | contains(ele) {
137 | return this.el.contains(ele);
138 | }
139 |
140 | className(v) {
141 | if (v !== undefined) {
142 | this.el.className = v;
143 | return this;
144 | }
145 | return this.el.className;
146 | }
147 |
148 | addClass(name) {
149 | this.el.classList.add(name);
150 | return this;
151 | }
152 |
153 | hasClass(name) {
154 | return this.el.classList.contains(name);
155 | }
156 |
157 | removeClass(name) {
158 | this.el.classList.remove(name);
159 | return this;
160 | }
161 |
162 | toggle(cls = 'active') {
163 | return this.toggleClass(cls);
164 | }
165 |
166 | toggleClass(name) {
167 | return this.el.classList.toggle(name);
168 | }
169 |
170 | active(flag = true, cls = 'active') {
171 | if (flag) this.addClass(cls);
172 | else this.removeClass(cls);
173 | return this;
174 | }
175 |
176 | checked(flag = true) {
177 | this.active(flag, 'checked');
178 | return this;
179 | }
180 |
181 | disabled(flag = true) {
182 | if (flag) this.addClass('disabled');
183 | else this.removeClass('disabled');
184 | return this;
185 | }
186 |
187 | // key, value
188 | // key
189 | // {k, v}...
190 | attr(key, value) {
191 | if (value !== undefined) {
192 | this.el.setAttribute(key, value);
193 | } else {
194 | if (typeof key === 'string') {
195 | return this.el.getAttribute(key);
196 | }
197 | Object.keys(key).forEach((k) => {
198 | this.el.setAttribute(k, key[k]);
199 | });
200 | }
201 | return this;
202 | }
203 |
204 | removeAttr(key) {
205 | this.el.removeAttribute(key);
206 | return this;
207 | }
208 |
209 | html(content) {
210 | if (content !== undefined) {
211 | this.el.innerHTML = content;
212 | return this;
213 | }
214 | return this.el.innerHTML;
215 | }
216 |
217 | val(v) {
218 | if (v !== undefined) {
219 | this.el.value = v;
220 | return this;
221 | }
222 | return this.el.value;
223 | }
224 |
225 | cssRemoveKeys(...keys) {
226 | keys.forEach(k => this.el.style.removeProperty(k));
227 | return this;
228 | }
229 |
230 | // css( propertyName )
231 | // css( propertyName, value )
232 | // css( properties )
233 | css(name, value) {
234 | if (value === undefined && typeof name !== 'string') {
235 | Object.keys(name).forEach((k) => {
236 | this.el.style[k] = name[k];
237 | });
238 | return this;
239 | }
240 | if (value !== undefined) {
241 | this.el.style[name] = value;
242 | return this;
243 | }
244 | return this.el.style[name];
245 | }
246 |
247 | computedStyle() {
248 | return window.getComputedStyle(this.el, null);
249 | }
250 |
251 | show() {
252 | this.css('display', 'block');
253 | return this;
254 | }
255 |
256 | hide() {
257 | this.css('display', 'none');
258 | return this;
259 | }
260 | }
261 |
262 | const h = (tag, className = '') => new Element(tag, className);
263 |
264 | export {
265 | Element,
266 | h,
267 | };
268 |
--------------------------------------------------------------------------------
/src/component/event.js:
--------------------------------------------------------------------------------
1 | /* global window */
2 | export function bind(target, name, fn) {
3 | target.addEventListener(name, fn);
4 | }
5 | export function unbind(target, name, fn) {
6 | target.removeEventListener(name, fn);
7 | }
8 | export function unbindClickoutside(el) {
9 | if (el.xclickoutside) {
10 | unbind(window.document.body, 'click', el.xclickoutside);
11 | delete el.xclickoutside;
12 | }
13 | }
14 |
15 | // the left mouse button: mousedown → mouseup → click
16 | // the right mouse button: mousedown → contenxtmenu → mouseup
17 | // the right mouse button in firefox(>65.0): mousedown → contenxtmenu → mouseup → click on window
18 | export function bindClickoutside(el, cb) {
19 | el.xclickoutside = (evt) => {
20 | // ignore double click
21 | // console.log('evt:', evt);
22 | if (evt.detail === 2 || el.contains(evt.target)) return;
23 | if (cb) cb(el);
24 | else {
25 | el.hide();
26 | unbindClickoutside(el);
27 | }
28 | };
29 | bind(window.document.body, 'click', el.xclickoutside);
30 | }
31 | export function mouseMoveUp(target, movefunc, upfunc) {
32 | bind(target, 'mousemove', movefunc);
33 | const t = target;
34 | t.xEvtUp = (evt) => {
35 | // console.log('mouseup>>>');
36 | unbind(target, 'mousemove', movefunc);
37 | unbind(target, 'mouseup', target.xEvtUp);
38 | upfunc(evt);
39 | };
40 | bind(target, 'mouseup', target.xEvtUp);
41 | }
42 |
43 | function calTouchDirection(spanx, spany, evt, cb) {
44 | let direction = '';
45 | // console.log('spanx:', spanx, ', spany:', spany);
46 | if (Math.abs(spanx) > Math.abs(spany)) {
47 | // horizontal
48 | direction = spanx > 0 ? 'right' : 'left';
49 | cb(direction, spanx, evt);
50 | } else {
51 | // vertical
52 | direction = spany > 0 ? 'down' : 'up';
53 | cb(direction, spany, evt);
54 | }
55 | }
56 | // cb = (direction, distance) => {}
57 | export function bindTouch(target, { move, end }) {
58 | let startx = 0;
59 | let starty = 0;
60 | bind(target, 'touchstart', (evt) => {
61 | const { pageX, pageY } = evt.touches[0];
62 | startx = pageX;
63 | starty = pageY;
64 | });
65 | bind(target, 'touchmove', (evt) => {
66 | if (!move) return;
67 | const { pageX, pageY } = evt.changedTouches[0];
68 | const spanx = pageX - startx;
69 | const spany = pageY - starty;
70 | if (Math.abs(spanx) > 10 || Math.abs(spany) > 10) {
71 | // console.log('spanx:', spanx, ', spany:', spany);
72 | calTouchDirection(spanx, spany, evt, move);
73 | startx = pageX;
74 | starty = pageY;
75 | }
76 | evt.preventDefault();
77 | });
78 | bind(target, 'touchend', (evt) => {
79 | if (!end) return;
80 | const { pageX, pageY } = evt.changedTouches[0];
81 | const spanx = pageX - startx;
82 | const spany = pageY - starty;
83 | calTouchDirection(spanx, spany, evt, end);
84 | });
85 | }
86 |
--------------------------------------------------------------------------------
/src/component/form_field.js:
--------------------------------------------------------------------------------
1 | import { h } from './element';
2 | import { cssPrefix } from '../config';
3 | import { t } from '../locale/locale';
4 |
5 | const patterns = {
6 | number: /(^\d+$)|(^\d+(\.\d{0,4})?$)/,
7 | date: /^\d{4}-\d{1,2}-\d{1,2}$/,
8 | };
9 |
10 | // rule: { required: false, type, pattern: // }
11 | export default class FormField {
12 | constructor(input, rule, label, labelWidth) {
13 | this.label = '';
14 | this.rule = rule;
15 | if (label) {
16 | this.label = h('label', 'label').css('width', `${labelWidth}px`).html(label);
17 | }
18 | this.tip = h('div', 'tip').child('tip').hide();
19 | this.input = input;
20 | this.input.vchange = () => this.validate();
21 | this.el = h('div', `${cssPrefix}-form-field`)
22 | .children(this.label, input.el, this.tip);
23 | }
24 |
25 | isShow() {
26 | return this.el.css('display') !== 'none';
27 | }
28 |
29 | show() {
30 | this.el.show();
31 | }
32 |
33 | hide() {
34 | this.el.hide();
35 | return this;
36 | }
37 |
38 | val(v) {
39 | return this.input.val(v);
40 | }
41 |
42 | hint(hint) {
43 | this.input.hint(hint);
44 | }
45 |
46 | validate() {
47 | const {
48 | input, rule, tip, el,
49 | } = this;
50 | const v = input.val();
51 | if (rule.required) {
52 | if (/^\s*$/.test(v)) {
53 | tip.html(t('validation.required'));
54 | el.addClass('error');
55 | return false;
56 | }
57 | }
58 | if (rule.type || rule.pattern) {
59 | const pattern = rule.pattern || patterns[rule.type];
60 | if (!pattern.test(v)) {
61 | tip.html(t('validation.notMatch'));
62 | el.addClass('error');
63 | return false;
64 | }
65 | }
66 | el.removeClass('error');
67 | return true;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/component/form_input.js:
--------------------------------------------------------------------------------
1 | import { h } from './element';
2 | import { cssPrefix } from '../config';
3 |
4 | export default class FormInput {
5 | constructor(width, hint) {
6 | this.vchange = () => {};
7 | this.el = h('div', `${cssPrefix}-form-input`);
8 | this.input = h('input', '').css('width', width)
9 | .on('input', evt => this.vchange(evt))
10 | .attr('placeholder', hint);
11 | this.el.child(this.input);
12 | }
13 |
14 | hint(v) {
15 | this.input.attr('placeholder', v);
16 | }
17 |
18 | val(v) {
19 | return this.input.val(v);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/component/form_select.js:
--------------------------------------------------------------------------------
1 | import { h } from './element';
2 | import Suggest from './suggest';
3 | import { cssPrefix } from '../config';
4 |
5 | export default class FormSelect {
6 | constructor(key, items, width, getTitle = it => it, change = () => {}) {
7 | this.key = key;
8 | this.getTitle = getTitle;
9 | this.vchange = () => {};
10 | this.el = h('div', `${cssPrefix}-form-select`);
11 | this.suggest = new Suggest(items.map(it => ({ key: it, title: this.getTitle(it) })), (it) => {
12 | this.itemClick(it.key);
13 | change(it.key);
14 | this.vchange(it.key);
15 | }, width, this.el);
16 | this.el.children(
17 | this.itemEl = h('div', 'input-text').html(this.getTitle(key)),
18 | this.suggest.el,
19 | ).on('click', () => this.show());
20 | }
21 |
22 | show() {
23 | this.suggest.search('');
24 | }
25 |
26 | itemClick(it) {
27 | this.key = it;
28 | this.itemEl.html(this.getTitle(it));
29 | }
30 |
31 | val(v) {
32 | if (v !== undefined) {
33 | this.key = v;
34 | this.itemEl.html(this.getTitle(v));
35 | return this;
36 | }
37 | return this.key;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/component/icon.js:
--------------------------------------------------------------------------------
1 | import { Element, h } from './element';
2 | import { cssPrefix } from '../config';
3 |
4 | export default class Icon extends Element {
5 | constructor(name) {
6 | super('div', `${cssPrefix}-icon`);
7 | this.iconNameEl = h('div', `${cssPrefix}-icon-img ${name}`);
8 | this.child(this.iconNameEl);
9 | }
10 |
11 | setName(name) {
12 | this.iconNameEl.className(`${cssPrefix}-icon-img ${name}`);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/component/message.js:
--------------------------------------------------------------------------------
1 | /* global document */
2 | import { h } from './element';
3 | import Icon from './icon';
4 | import { cssPrefix } from '../config';
5 |
6 | export function xtoast(title, content) {
7 | const el = h('div', `${cssPrefix}-toast`);
8 | const dimmer = h('div', `${cssPrefix}-dimmer active`);
9 | const remove = () => {
10 | document.body.removeChild(el.el);
11 | document.body.removeChild(dimmer.el);
12 | };
13 |
14 | el.children(
15 | h('div', `${cssPrefix}-toast-header`).children(
16 | new Icon('close').on('click.stop', () => remove()),
17 | title,
18 | ),
19 | h('div', `${cssPrefix}-toast-content`).html(content),
20 | );
21 | document.body.appendChild(el.el);
22 | document.body.appendChild(dimmer.el);
23 | // set offset
24 | const { width, height } = el.box();
25 | const { clientHeight, clientWidth } = document.documentElement;
26 | el.offset({
27 | left: (clientWidth - width) / 2,
28 | top: (clientHeight - height) / 3,
29 | });
30 | }
31 |
32 | export default {};
33 |
--------------------------------------------------------------------------------
/src/component/modal.js:
--------------------------------------------------------------------------------
1 | /* global document */
2 | /* global window */
3 | import { h } from './element';
4 | import Icon from './icon';
5 | import { cssPrefix } from '../config';
6 | import { bind, unbind } from './event';
7 |
8 | export default class Modal {
9 | constructor(title, content, width = '600px') {
10 | this.title = title;
11 | this.el = h('div', `${cssPrefix}-modal`).css('width', width).children(
12 | h('div', `${cssPrefix}-modal-header`).children(
13 | new Icon('close').on('click.stop', () => this.hide()),
14 | this.title,
15 | ),
16 | h('div', `${cssPrefix}-modal-content`).children(...content),
17 | ).hide();
18 | }
19 |
20 | show() {
21 | // dimmer
22 | this.dimmer = h('div', `${cssPrefix}-dimmer active`);
23 | document.body.appendChild(this.dimmer.el);
24 | const { width, height } = this.el.show().box();
25 | const { clientHeight, clientWidth } = document.documentElement;
26 | this.el.offset({
27 | left: (clientWidth - width) / 2,
28 | top: (clientHeight - height) / 3,
29 | });
30 | window.xkeydownEsc = (evt) => {
31 | if (evt.keyCode === 27) {
32 | this.hide();
33 | }
34 | };
35 | bind(window, 'keydown', window.xkeydownEsc);
36 | }
37 |
38 | hide() {
39 | this.el.hide();
40 | document.body.removeChild(this.dimmer.el);
41 | unbind(window, 'keydown', window.xkeydownEsc);
42 | delete window.xkeydownEsc;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/component/modal_validation.js:
--------------------------------------------------------------------------------
1 | import Modal from './modal';
2 | import FormInput from './form_input';
3 | import FormSelect from './form_select';
4 | import FormField from './form_field';
5 | import Button from './button';
6 | import { t } from '../locale/locale';
7 | import { h } from './element';
8 | import { cssPrefix } from '../config';
9 |
10 | const fieldLabelWidth = 100;
11 |
12 | export default class ModalValidation extends Modal {
13 | constructor() {
14 | const mf = new FormField(
15 | new FormSelect('cell',
16 | ['cell'], // cell|row|column
17 | '100%',
18 | it => t(`dataValidation.modeType.${it}`)),
19 | { required: true },
20 | `${t('dataValidation.range')}:`,
21 | fieldLabelWidth,
22 | );
23 | const rf = new FormField(
24 | new FormInput('120px', 'E3 or E3:F12'),
25 | { required: true, pattern: /^([A-Z]{1,2}[1-9]\d*)(:[A-Z]{1,2}[1-9]\d*)?$/ },
26 | );
27 | const cf = new FormField(
28 | new FormSelect('list',
29 | ['list', 'number', 'date', 'phone', 'email'],
30 | '100%',
31 | it => t(`dataValidation.type.${it}`),
32 | it => this.criteriaSelected(it)),
33 | { required: true },
34 | `${t('dataValidation.criteria')}:`,
35 | fieldLabelWidth,
36 | );
37 |
38 | // operator
39 | const of = new FormField(
40 | new FormSelect('be',
41 | ['be', 'nbe', 'eq', 'neq', 'lt', 'lte', 'gt', 'gte'],
42 | '160px',
43 | it => t(`dataValidation.operator.${it}`),
44 | it => this.criteriaOperatorSelected(it)),
45 | { required: true },
46 | ).hide();
47 | // min, max
48 | const minvf = new FormField(
49 | new FormInput('70px', '10'),
50 | { required: true },
51 | ).hide();
52 | const maxvf = new FormField(
53 | new FormInput('70px', '100'),
54 | { required: true, type: 'number' },
55 | ).hide();
56 | // value
57 | const svf = new FormField(
58 | new FormInput('120px', 'a,b,c'),
59 | { required: true },
60 | );
61 | const vf = new FormField(
62 | new FormInput('70px', '10'),
63 | { required: true, type: 'number' },
64 | ).hide();
65 |
66 | super(t('contextmenu.validation'), [
67 | h('div', `${cssPrefix}-form-fields`).children(
68 | mf.el,
69 | rf.el,
70 | ),
71 | h('div', `${cssPrefix}-form-fields`).children(
72 | cf.el,
73 | of.el,
74 | minvf.el,
75 | maxvf.el,
76 | vf.el,
77 | svf.el,
78 | ),
79 | h('div', `${cssPrefix}-buttons`).children(
80 | new Button('cancel').on('click', () => this.btnClick('cancel')),
81 | new Button('remove').on('click', () => this.btnClick('remove')),
82 | new Button('save', 'primary').on('click', () => this.btnClick('save')),
83 | ),
84 | ]);
85 | this.mf = mf;
86 | this.rf = rf;
87 | this.cf = cf;
88 | this.of = of;
89 | this.minvf = minvf;
90 | this.maxvf = maxvf;
91 | this.vf = vf;
92 | this.svf = svf;
93 | this.change = () => {};
94 | }
95 |
96 | showVf(it) {
97 | const hint = it === 'date' ? '2018-11-12' : '10';
98 | const { vf } = this;
99 | vf.input.hint(hint);
100 | vf.show();
101 | }
102 |
103 | criteriaSelected(it) {
104 | const {
105 | of, minvf, maxvf, vf, svf,
106 | } = this;
107 | if (it === 'date' || it === 'number') {
108 | of.show();
109 | minvf.rule.type = it;
110 | maxvf.rule.type = it;
111 | if (it === 'date') {
112 | minvf.hint('2018-11-12');
113 | maxvf.hint('2019-11-12');
114 | } else {
115 | minvf.hint('10');
116 | maxvf.hint('100');
117 | }
118 | minvf.show();
119 | maxvf.show();
120 | vf.hide();
121 | svf.hide();
122 | } else {
123 | if (it === 'list') {
124 | svf.show();
125 | } else {
126 | svf.hide();
127 | }
128 | vf.hide();
129 | of.hide();
130 | minvf.hide();
131 | maxvf.hide();
132 | }
133 | }
134 |
135 | criteriaOperatorSelected(it) {
136 | if (!it) return;
137 | const {
138 | minvf, maxvf, vf,
139 | } = this;
140 | if (it === 'be' || it === 'nbe') {
141 | minvf.show();
142 | maxvf.show();
143 | vf.hide();
144 | } else {
145 | const type = this.cf.val();
146 | vf.rule.type = type;
147 | if (type === 'date') {
148 | vf.hint('2018-11-12');
149 | } else {
150 | vf.hint('10');
151 | }
152 | vf.show();
153 | minvf.hide();
154 | maxvf.hide();
155 | }
156 | }
157 |
158 | btnClick(action) {
159 | if (action === 'cancel') {
160 | this.hide();
161 | } else if (action === 'remove') {
162 | this.change('remove');
163 | this.hide();
164 | } else if (action === 'save') {
165 | // validate
166 | const attrs = ['mf', 'rf', 'cf', 'of', 'svf', 'vf', 'minvf', 'maxvf'];
167 | for (let i = 0; i < attrs.length; i += 1) {
168 | const field = this[attrs[i]];
169 | // console.log('field:', field);
170 | if (field.isShow()) {
171 | // console.log('it:', it);
172 | if (!field.validate()) return;
173 | }
174 | }
175 |
176 | const mode = this.mf.val();
177 | const ref = this.rf.val();
178 | const type = this.cf.val();
179 | const operator = this.of.val();
180 | let value = this.svf.val();
181 | if (type === 'number' || type === 'date') {
182 | if (operator === 'be' || operator === 'nbe') {
183 | value = [this.minvf.val(), this.maxvf.val()];
184 | } else {
185 | value = this.vf.val();
186 | }
187 | }
188 | // console.log(mode, ref, type, operator, value);
189 | this.change('save',
190 | mode,
191 | ref,
192 | {
193 | type, operator, required: false, value,
194 | });
195 | this.hide();
196 | }
197 | }
198 |
199 | // validation: { mode, ref, validator }
200 | setValue(v) {
201 | if (v) {
202 | const {
203 | mf, rf, cf, of, svf, vf, minvf, maxvf,
204 | } = this;
205 | const {
206 | mode, ref, validator,
207 | } = v;
208 | const {
209 | type, operator, value,
210 | } = validator || { type: 'list' };
211 | mf.val(mode || 'cell');
212 | rf.val(ref);
213 | cf.val(type);
214 | of.val(operator);
215 | if (Array.isArray(value)) {
216 | minvf.val(value[0]);
217 | maxvf.val(value[1]);
218 | } else {
219 | svf.val(value || '');
220 | vf.val(value || '');
221 | }
222 | this.criteriaSelected(type);
223 | this.criteriaOperatorSelected(operator);
224 | }
225 | this.show();
226 | }
227 | }
228 |
--------------------------------------------------------------------------------
/src/component/resizer.js:
--------------------------------------------------------------------------------
1 | /* global window */
2 | import { h } from './element';
3 | import { mouseMoveUp } from './event';
4 | import { cssPrefix } from '../config';
5 |
6 | export default class Resizer {
7 | constructor(vertical = false, minDistance) {
8 | this.moving = false;
9 | this.vertical = vertical;
10 | this.el = h('div', `${cssPrefix}-resizer ${vertical ? 'vertical' : 'horizontal'}`).children(
11 | this.hoverEl = h('div', `${cssPrefix}-resizer-hover`)
12 | .on('mousedown.stop', evt => this.mousedownHandler(evt)),
13 | this.lineEl = h('div', `${cssPrefix}-resizer-line`).hide(),
14 | ).hide();
15 | // cell rect
16 | this.cRect = null;
17 | this.finishedFn = null;
18 | this.minDistance = minDistance;
19 | }
20 |
21 | // rect : {top, left, width, height}
22 | // line : {width, height}
23 | show(rect, line) {
24 | const {
25 | moving, vertical, hoverEl, lineEl, el,
26 | } = this;
27 | if (moving) return;
28 | this.cRect = rect;
29 | const {
30 | left, top, width, height,
31 | } = rect;
32 | el.offset({
33 | left: vertical ? left + width - 5 : left,
34 | top: vertical ? top : top + height - 5,
35 | }).show();
36 | hoverEl.offset({
37 | width: vertical ? 5 : width,
38 | height: vertical ? height : 5,
39 | });
40 | lineEl.offset({
41 | width: vertical ? 0 : line.width,
42 | height: vertical ? line.height : 0,
43 | });
44 | }
45 |
46 | hide() {
47 | this.el.offset({
48 | left: 0,
49 | top: 0,
50 | }).hide();
51 | }
52 |
53 | mousedownHandler(evt) {
54 | let startEvt = evt;
55 | const {
56 | el, lineEl, cRect, vertical, minDistance,
57 | } = this;
58 | let distance = vertical ? cRect.width : cRect.height;
59 | // console.log('distance:', distance);
60 | lineEl.show();
61 | mouseMoveUp(window, (e) => {
62 | this.moving = true;
63 | if (startEvt !== null && e.buttons === 1) {
64 | // console.log('top:', top, ', left:', top, ', cRect:', cRect);
65 | if (vertical) {
66 | distance += e.movementX;
67 | if (distance > minDistance) {
68 | el.css('left', `${cRect.left + distance}px`);
69 | }
70 | } else {
71 | distance += e.movementY;
72 | if (distance > minDistance) {
73 | el.css('top', `${cRect.top + distance}px`);
74 | }
75 | }
76 | startEvt = e;
77 | }
78 | }, () => {
79 | startEvt = null;
80 | lineEl.hide();
81 | this.moving = false;
82 | this.hide();
83 | if (this.finishedFn) {
84 | if (distance < minDistance) distance = minDistance;
85 | this.finishedFn(cRect, distance);
86 | }
87 | });
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/component/scrollbar.js:
--------------------------------------------------------------------------------
1 | import { h } from './element';
2 | import { cssPrefix } from '../config';
3 |
4 | export default class Scrollbar {
5 | constructor(vertical) {
6 | this.vertical = vertical;
7 | this.moveFn = null;
8 | this.el = h('div', `${cssPrefix}-scrollbar ${vertical ? 'vertical' : 'horizontal'}`)
9 | .child(this.contentEl = h('div', ''))
10 | .on('mousemove.stop', () => {})
11 | .on('scroll.stop', (evt) => {
12 | const { scrollTop, scrollLeft } = evt.target;
13 | // console.log('scrollTop:', scrollTop);
14 | if (this.moveFn) {
15 | this.moveFn(this.vertical ? scrollTop : scrollLeft, evt);
16 | }
17 | // console.log('evt:::', evt);
18 | });
19 | }
20 |
21 | move(v) {
22 | this.el.scroll(v);
23 | return this;
24 | }
25 |
26 | scroll() {
27 | return this.el.scroll();
28 | }
29 |
30 | set(distance, contentDistance) {
31 | const d = distance - 1;
32 | // console.log('distance:', distance, ', contentDistance:', contentDistance);
33 | if (contentDistance > d) {
34 | const cssKey = this.vertical ? 'height' : 'width';
35 | // console.log('d:', d);
36 | this.el.css(cssKey, `${d - 15}px`).show();
37 | this.contentEl
38 | .css(this.vertical ? 'width' : 'height', '1px')
39 | .css(cssKey, `${contentDistance}px`);
40 | } else {
41 | this.el.hide();
42 | }
43 | return this;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/component/sort_filter.js:
--------------------------------------------------------------------------------
1 | import { h } from './element';
2 | import Button from './button';
3 | import { bindClickoutside, unbindClickoutside } from './event';
4 | import { cssPrefix } from '../config';
5 | import { t } from '../locale/locale';
6 |
7 | function buildMenu(clsName) {
8 | return h('div', `${cssPrefix}-item ${clsName}`);
9 | }
10 |
11 | function buildSortItem(it) {
12 | return buildMenu('state').child(t(`sort.${it}`))
13 | .on('click.stop', () => this.itemClick(it));
14 | }
15 |
16 | function buildFilterBody(items) {
17 | const { filterbEl, filterValues } = this;
18 | filterbEl.html('');
19 | const itemKeys = Object.keys(items);
20 | itemKeys.forEach((it, index) => {
21 | const cnt = items[it];
22 | const active = filterValues.includes(it) ? 'checked' : '';
23 | filterbEl.child(h('div', `${cssPrefix}-item state ${active}`)
24 | .on('click.stop', () => this.filterClick(index, it))
25 | .children(it === '' ? t('filter.empty') : it, h('div', 'label').html(`(${cnt})`)));
26 | });
27 | }
28 |
29 | function resetFilterHeader() {
30 | const { filterhEl, filterValues, values } = this;
31 | filterhEl.html(`${filterValues.length} / ${values.length}`);
32 | filterhEl.checked(filterValues.length === values.length);
33 | }
34 |
35 | export default class SortFilter {
36 | constructor() {
37 | this.filterbEl = h('div', `${cssPrefix}-body`);
38 | this.filterhEl = h('div', `${cssPrefix}-header state`).on('click.stop', () => this.filterClick(0, 'all'));
39 | this.el = h('div', `${cssPrefix}-sort-filter`).children(
40 | this.sortAscEl = buildSortItem.call(this, 'asc'),
41 | this.sortDescEl = buildSortItem.call(this, 'desc'),
42 | buildMenu('divider'),
43 | h('div', `${cssPrefix}-filter`).children(
44 | this.filterhEl,
45 | this.filterbEl,
46 | ),
47 | h('div', `${cssPrefix}-buttons`).children(
48 | new Button('cancel').on('click', () => this.btnClick('cancel')),
49 | new Button('ok', 'primary').on('click', () => this.btnClick('ok')),
50 | ),
51 | ).hide();
52 | // this.setFilters(['test1', 'test2', 'text3']);
53 | this.ci = null;
54 | this.sortDesc = null;
55 | this.values = null;
56 | this.filterValues = [];
57 | }
58 |
59 | btnClick(it) {
60 | if (it === 'ok') {
61 | const { ci, sort, filterValues } = this;
62 | if (this.ok) {
63 | this.ok(ci, sort, 'in', filterValues);
64 | }
65 | }
66 | this.hide();
67 | }
68 |
69 | itemClick(it) {
70 | // console.log('it:', it);
71 | this.sort = it;
72 | const { sortAscEl, sortDescEl } = this;
73 | sortAscEl.checked(it === 'asc');
74 | sortDescEl.checked(it === 'desc');
75 | }
76 |
77 | filterClick(index, it) {
78 | // console.log('index:', index, it);
79 | const { filterbEl, filterValues, values } = this;
80 | const children = filterbEl.children();
81 | if (it === 'all') {
82 | if (children.length === filterValues.length) {
83 | this.filterValues = [];
84 | children.forEach(i => h(i).checked(false));
85 | } else {
86 | this.filterValues = Array.from(values);
87 | children.forEach(i => h(i).checked(true));
88 | }
89 | } else {
90 | const checked = h(children[index]).toggle('checked');
91 | if (checked) {
92 | filterValues.push(it);
93 | } else {
94 | filterValues.splice(filterValues.findIndex(i => i === it), 1);
95 | }
96 | }
97 | resetFilterHeader.call(this);
98 | }
99 |
100 | // v: autoFilter
101 | // items: {value: cnt}
102 | // sort { ci, order }
103 | set(ci, items, filter, sort) {
104 | this.ci = ci;
105 | const { sortAscEl, sortDescEl } = this;
106 | if (sort !== null) {
107 | this.sort = sort.order;
108 | sortAscEl.checked(sort.asc());
109 | sortDescEl.checked(sort.desc());
110 | } else {
111 | this.sortDesc = null;
112 | sortAscEl.checked(false);
113 | sortDescEl.checked(false);
114 | }
115 | // this.setFilters(items, filter);
116 | this.values = Object.keys(items);
117 | this.filterValues = filter ? Array.from(filter.value) : Object.keys(items);
118 | buildFilterBody.call(this, items, filter);
119 | resetFilterHeader.call(this);
120 | }
121 |
122 | setOffset(v) {
123 | this.el.offset(v).show();
124 | let tindex = 1;
125 | bindClickoutside(this.el, () => {
126 | if (tindex <= 0) {
127 | this.hide();
128 | }
129 | tindex -= 1;
130 | });
131 | }
132 |
133 | show() {
134 | this.el.show();
135 | }
136 |
137 | hide() {
138 | this.el.hide();
139 | unbindClickoutside(this.el);
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/component/suggest.js:
--------------------------------------------------------------------------------
1 | import { h } from './element';
2 | import { bindClickoutside, unbindClickoutside } from './event';
3 | import { cssPrefix } from '../config';
4 |
5 | function inputMovePrev(evt) {
6 | evt.preventDefault();
7 | evt.stopPropagation();
8 | const { filterItems } = this;
9 | if (filterItems.length <= 0) return;
10 | if (this.itemIndex >= 0) filterItems[this.itemIndex].toggle();
11 | this.itemIndex -= 1;
12 | if (this.itemIndex < 0) {
13 | this.itemIndex = filterItems.length - 1;
14 | }
15 | filterItems[this.itemIndex].toggle();
16 | }
17 |
18 | function inputMoveNext(evt) {
19 | evt.stopPropagation();
20 | const { filterItems } = this;
21 | if (filterItems.length <= 0) return;
22 | if (this.itemIndex >= 0) filterItems[this.itemIndex].toggle();
23 | this.itemIndex += 1;
24 | if (this.itemIndex > filterItems.length - 1) {
25 | this.itemIndex = 0;
26 | }
27 | filterItems[this.itemIndex].toggle();
28 | }
29 |
30 | function inputEnter(evt) {
31 | evt.preventDefault();
32 | const { filterItems } = this;
33 | if (filterItems.length <= 0) return;
34 | evt.stopPropagation();
35 | if (this.itemIndex < 0) this.itemIndex = 0;
36 | filterItems[this.itemIndex].el.click();
37 | this.hide();
38 | }
39 |
40 | function inputKeydownHandler(evt) {
41 | const { keyCode } = evt;
42 | if (evt.ctrlKey) {
43 | evt.stopPropagation();
44 | }
45 | switch (keyCode) {
46 | case 37: // left
47 | evt.stopPropagation();
48 | break;
49 | case 38: // up
50 | inputMovePrev.call(this, evt);
51 | break;
52 | case 39: // right
53 | evt.stopPropagation();
54 | break;
55 | case 40: // down
56 | inputMoveNext.call(this, evt);
57 | break;
58 | case 13: // enter
59 | inputEnter.call(this, evt);
60 | break;
61 | case 9:
62 | inputEnter.call(this, evt);
63 | break;
64 | default:
65 | evt.stopPropagation();
66 | break;
67 | }
68 | }
69 |
70 | export default class Suggest {
71 | constructor(items, itemClick, width = '200px') {
72 | this.filterItems = [];
73 | this.items = items;
74 | this.el = h('div', `${cssPrefix}-suggest`).css('width', width).hide();
75 | this.itemClick = itemClick;
76 | this.itemIndex = -1;
77 | }
78 |
79 | setOffset(v) {
80 | this.el.cssRemoveKeys('top', 'bottom')
81 | .offset(v);
82 | }
83 |
84 | hide() {
85 | const { el } = this;
86 | this.filterItems = [];
87 | this.itemIndex = -1;
88 | el.hide();
89 | unbindClickoutside(this.el.parent());
90 | }
91 |
92 | setItems(items) {
93 | this.items = items;
94 | // this.search('');
95 | }
96 |
97 | search(word) {
98 | let { items } = this;
99 | if (!/^\s*$/.test(word)) {
100 | items = items.filter(it => (it.key || it).startsWith(word.toUpperCase()));
101 | }
102 | items = items.map((it) => {
103 | let { title } = it;
104 | if (title) {
105 | if (typeof title === 'function') {
106 | title = title();
107 | }
108 | } else {
109 | title = it;
110 | }
111 | const item = h('div', `${cssPrefix}-item`)
112 | .child(title)
113 | .on('click.stop', () => {
114 | this.itemClick(it);
115 | this.hide();
116 | });
117 | if (it.label) {
118 | item.child(h('div', 'label').html(it.label));
119 | }
120 | return item;
121 | });
122 | this.filterItems = items;
123 | if (items.length <= 0) {
124 | return;
125 | }
126 | const { el } = this;
127 | // items[0].toggle();
128 | el.html('').children(...items).show();
129 | bindClickoutside(el.parent(), () => { this.hide(); });
130 | }
131 |
132 | bindInputEvents(input) {
133 | input.on('keydown', evt => inputKeydownHandler.call(this, evt));
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/component/toolbar/align.js:
--------------------------------------------------------------------------------
1 | import DropdownItem from './dropdown_item';
2 | import DropdownAlign from '../dropdown_align';
3 |
4 | export default class Align extends DropdownItem {
5 | constructor(value) {
6 | super('align', '', value);
7 | }
8 |
9 | dropdown() {
10 | const { value } = this;
11 | return new DropdownAlign(['left', 'center', 'right'], value);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/component/toolbar/autofilter.js:
--------------------------------------------------------------------------------
1 | import ToggleItem from './toggle_item';
2 |
3 | export default class Autofilter extends ToggleItem {
4 | constructor() {
5 | super('autofilter');
6 | }
7 |
8 | setState() {}
9 | }
10 |
--------------------------------------------------------------------------------
/src/component/toolbar/bold.js:
--------------------------------------------------------------------------------
1 | import ToggleItem from './toggle_item';
2 |
3 | export default class Bold extends ToggleItem {
4 | constructor() {
5 | super('font-bold', 'Ctrl+B');
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/component/toolbar/border.js:
--------------------------------------------------------------------------------
1 | import DropdownItem from './dropdown_item';
2 | import DropdownBorder from '../dropdown_border';
3 |
4 | export default class Border extends DropdownItem {
5 | constructor() {
6 | super('border');
7 | }
8 |
9 | dropdown() {
10 | return new DropdownBorder();
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/component/toolbar/clearformat.js:
--------------------------------------------------------------------------------
1 | import IconItem from './icon_item';
2 |
3 | export default class Clearformat extends IconItem {
4 | constructor() {
5 | super('clearformat');
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/component/toolbar/dropdown_item.js:
--------------------------------------------------------------------------------
1 | import Item from './item';
2 |
3 | export default class DropdownItem extends Item {
4 | dropdown() {}
5 |
6 | getValue(v) {
7 | return v;
8 | }
9 |
10 | element() {
11 | const { tag } = this;
12 | this.dd = this.dropdown();
13 | this.dd.change = it => this.change(tag, this.getValue(it));
14 | return super.element().child(
15 | this.dd,
16 | );
17 | }
18 |
19 | setState(v) {
20 | if (v) {
21 | this.value = v;
22 | this.dd.setTitle(v);
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/component/toolbar/fill_color.js:
--------------------------------------------------------------------------------
1 | import DropdownItem from './dropdown_item';
2 | import DropdownColor from '../dropdown_color';
3 |
4 | export default class FillColor extends DropdownItem {
5 | constructor(color) {
6 | super('bgcolor', undefined, color);
7 | }
8 |
9 | dropdown() {
10 | const { tag, value } = this;
11 | return new DropdownColor(tag, value);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/component/toolbar/font.js:
--------------------------------------------------------------------------------
1 | import DropdownItem from './dropdown_item';
2 | import DropdownFont from '../dropdown_font';
3 |
4 | export default class Font extends DropdownItem {
5 | constructor() {
6 | super('font-name');
7 | }
8 |
9 | getValue(it) {
10 | return it.key;
11 | }
12 |
13 | dropdown() {
14 | return new DropdownFont();
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/component/toolbar/font_size.js:
--------------------------------------------------------------------------------
1 | import DropdownItem from './dropdown_item';
2 | import DropdownFontsize from '../dropdown_fontsize';
3 |
4 | export default class Format extends DropdownItem {
5 | constructor() {
6 | super('font-size');
7 | }
8 |
9 | getValue(it) {
10 | return it.pt;
11 | }
12 |
13 | dropdown() {
14 | return new DropdownFontsize();
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/component/toolbar/format.js:
--------------------------------------------------------------------------------
1 | import DropdownItem from './dropdown_item';
2 | import DropdownFormat from '../dropdown_format';
3 |
4 | export default class Format extends DropdownItem {
5 | constructor() {
6 | super('format');
7 | }
8 |
9 | getValue(it) {
10 | return it.key;
11 | }
12 |
13 | dropdown() {
14 | return new DropdownFormat();
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/component/toolbar/formula.js:
--------------------------------------------------------------------------------
1 | import DropdownItem from './dropdown_item';
2 | import DropdownFormula from '../dropdown_formula';
3 |
4 | export default class Format extends DropdownItem {
5 | constructor() {
6 | super('formula');
7 | }
8 |
9 | getValue(it) {
10 | return it.key;
11 | }
12 |
13 | dropdown() {
14 | return new DropdownFormula();
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/component/toolbar/freeze.js:
--------------------------------------------------------------------------------
1 | import ToggleItem from './toggle_item';
2 |
3 | export default class Freeze extends ToggleItem {
4 | constructor() {
5 | super('freeze');
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/component/toolbar/icon_item.js:
--------------------------------------------------------------------------------
1 | import Item from './item';
2 | import Icon from '../icon';
3 |
4 | export default class IconItem extends Item {
5 | element() {
6 | return super.element()
7 | .child(new Icon(this.tag))
8 | .on('click', () => this.change(this.tag));
9 | }
10 |
11 | setState(disabled) {
12 | this.el.disabled(disabled);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/component/toolbar/index.js:
--------------------------------------------------------------------------------
1 | /* global window */
2 |
3 | import Align from './align';
4 | import Valign from './valign';
5 | import Autofilter from './autofilter';
6 | import Bold from './bold';
7 | import Italic from './italic';
8 | import Strike from './strike';
9 | import Underline from './underline';
10 | import Border from './border';
11 | import Clearformat from './clearformat';
12 | import Paintformat from './paintformat';
13 | import TextColor from './text_color';
14 | import FillColor from './fill_color';
15 | import FontSize from './font_size';
16 | import Font from './font';
17 | import Format from './format';
18 | import Formula from './formula';
19 | import Freeze from './freeze';
20 | import Merge from './merge';
21 | import Redo from './redo';
22 | import Undo from './undo';
23 | import Textwrap from './textwrap';
24 | import More from './more';
25 |
26 | import { h } from '../element';
27 | import { cssPrefix } from '../../config';
28 | import { bind } from '../event';
29 |
30 | function buildDivider() {
31 | return h('div', `${cssPrefix}-toolbar-divider`);
32 | }
33 |
34 | function initBtns2() {
35 | this.btns2 = [];
36 | this.items.forEach((it) => {
37 | if (Array.isArray(it)) {
38 | it.forEach(({ el }) => {
39 | const rect = el.box();
40 | const { marginLeft, marginRight } = el.computedStyle();
41 | this.btns2.push([el, rect.width + parseInt(marginLeft, 10) + parseInt(marginRight, 10)]);
42 | });
43 | } else {
44 | const rect = it.box();
45 | const { marginLeft, marginRight } = it.computedStyle();
46 | this.btns2.push([it, rect.width + parseInt(marginLeft, 10) + parseInt(marginRight, 10)]);
47 | }
48 | });
49 | }
50 |
51 | function moreResize() {
52 | const {
53 | el, btns, moreEl, btns2,
54 | } = this;
55 | const { moreBtns, contentEl } = moreEl.dd;
56 | el.css('width', `${this.widthFn() - 60}px`);
57 | const elBox = el.box();
58 |
59 | let sumWidth = 160;
60 | let sumWidth2 = 12;
61 | const list1 = [];
62 | const list2 = [];
63 | btns2.forEach(([it, w], index) => {
64 | sumWidth += w;
65 | if (index === btns2.length - 1 || sumWidth < elBox.width) {
66 | list1.push(it);
67 | } else {
68 | sumWidth2 += w;
69 | list2.push(it);
70 | }
71 | });
72 | btns.html('').children(...list1);
73 | moreBtns.html('').children(...list2);
74 | contentEl.css('width', `${sumWidth2}px`);
75 | if (list2.length > 0) {
76 | moreEl.show();
77 | } else {
78 | moreEl.hide();
79 | }
80 | }
81 |
82 | export default class Toolbar {
83 | constructor(data, widthFn, isHide = false) {
84 | this.data = data;
85 | this.change = () => {};
86 | this.widthFn = widthFn;
87 | this.isHide = isHide;
88 | const style = data.defaultStyle();
89 | this.items = [
90 | [
91 | this.undoEl = new Undo(),
92 | this.redoEl = new Redo(),
93 | this.paintformatEl = new Paintformat(),
94 | this.clearformatEl = new Clearformat(),
95 | ],
96 | buildDivider(),
97 | [
98 | this.formatEl = new Format(),
99 | ],
100 | buildDivider(),
101 | [
102 | this.fontEl = new Font(),
103 | this.fontSizeEl = new FontSize(),
104 | ],
105 | buildDivider(),
106 | [
107 | this.boldEl = new Bold(),
108 | this.italicEl = new Italic(),
109 | this.underlineEl = new Underline(),
110 | this.strikeEl = new Strike(),
111 | this.textColorEl = new TextColor(style.color),
112 | ],
113 | buildDivider(),
114 | [
115 | this.fillColorEl = new FillColor(style.bgcolor),
116 | this.borderEl = new Border(),
117 | this.mergeEl = new Merge(),
118 | ],
119 | buildDivider(),
120 | [
121 | this.alignEl = new Align(style.align),
122 | this.valignEl = new Valign(style.valign),
123 | this.textwrapEl = new Textwrap(),
124 | ],
125 | buildDivider(),
126 | [
127 | this.freezeEl = new Freeze(),
128 | this.autofilterEl = new Autofilter(),
129 | this.formulaEl = new Formula(),
130 | this.moreEl = new More(),
131 | ],
132 | ];
133 |
134 | this.el = h('div', `${cssPrefix}-toolbar`);
135 | this.btns = h('div', `${cssPrefix}-toolbar-btns`);
136 |
137 | this.items.forEach((it) => {
138 | if (Array.isArray(it)) {
139 | it.forEach((i) => {
140 | this.btns.child(i.el);
141 | i.change = (...args) => {
142 | this.change(...args);
143 | };
144 | });
145 | } else {
146 | this.btns.child(it.el);
147 | }
148 | });
149 |
150 | this.el.child(this.btns);
151 | if (isHide) {
152 | this.el.hide();
153 | } else {
154 | this.reset();
155 | setTimeout(() => {
156 | initBtns2.call(this);
157 | moreResize.call(this);
158 | }, 0);
159 | bind(window, 'resize', () => {
160 | moreResize.call(this);
161 | });
162 | }
163 | }
164 |
165 | paintformatActive() {
166 | return this.paintformatEl.active();
167 | }
168 |
169 | paintformatToggle() {
170 | this.paintformatEl.toggle();
171 | }
172 |
173 | trigger(type) {
174 | this[`${type}El`].click();
175 | }
176 |
177 | reset() {
178 | if (this.isHide) return;
179 | const { data } = this;
180 | const style = data.getSelectedCellStyle();
181 | const cell = data.getSelectedCell();
182 | // console.log('canUndo:', data.canUndo());
183 | this.undoEl.setState(!data.canUndo());
184 | this.redoEl.setState(!data.canRedo());
185 | this.mergeEl.setState(data.canUnmerge(), !data.selector.multiple());
186 | this.autofilterEl.setState(!data.canAutofilter());
187 | // this.mergeEl.disabled();
188 | // console.log('selectedCell:', style, cell);
189 | const { font } = style;
190 | this.fontEl.setState(font.name);
191 | this.fontSizeEl.setState(font.size);
192 | this.boldEl.setState(font.bold);
193 | this.italicEl.setState(font.italic);
194 | this.underlineEl.setState(style.underline);
195 | this.strikeEl.setState(style.strike);
196 | this.textColorEl.setState(style.color);
197 | this.fillColorEl.setState(style.bgcolor);
198 | this.alignEl.setState(style.align);
199 | this.valignEl.setState(style.valign);
200 | this.textwrapEl.setState(style.textwrap);
201 | // console.log('freeze is Active:', data.freezeIsActive());
202 | this.freezeEl.setState(data.freezeIsActive());
203 | if (cell) {
204 | if (cell.format) {
205 | this.formatEl.setState(cell.format);
206 | }
207 | }
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/src/component/toolbar/italic.js:
--------------------------------------------------------------------------------
1 | import ToggleItem from './toggle_item';
2 |
3 | export default class Italic extends ToggleItem {
4 | constructor() {
5 | super('font-italic', 'Ctrl+I');
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/component/toolbar/item.js:
--------------------------------------------------------------------------------
1 | import { cssPrefix } from '../../config';
2 | import tooltip from '../tooltip';
3 | import { h } from '../element';
4 | import { t } from '../../locale/locale';
5 |
6 | export default class Item {
7 | // tooltip
8 | // tag: the subclass type
9 | // shortcut: shortcut key
10 | constructor(tag, shortcut, value) {
11 | this.tip = t(`toolbar.${tag.replace(/-[a-z]/g, c => c[1].toUpperCase())}`);
12 | this.tag = tag;
13 | this.shortcut = shortcut;
14 | this.value = value;
15 | this.el = this.element();
16 | this.change = () => {};
17 | }
18 |
19 | element() {
20 | const { tip } = this;
21 | return h('div', `${cssPrefix}-toolbar-btn`)
22 | .on('mouseenter', (evt) => {
23 | tooltip(tip, evt.target);
24 | })
25 | .attr('data-tooltip', tip);
26 | }
27 |
28 | setState() {}
29 | }
30 |
--------------------------------------------------------------------------------
/src/component/toolbar/merge.js:
--------------------------------------------------------------------------------
1 | import ToggleItem from './toggle_item';
2 |
3 | export default class Merge extends ToggleItem {
4 | constructor() {
5 | super('merge');
6 | }
7 |
8 | setState(active, disabled) {
9 | this.el.active(active).disabled(disabled);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/component/toolbar/more.js:
--------------------------------------------------------------------------------
1 | import Dropdown from '../dropdown';
2 | import DropdownItem from './dropdown_item';
3 |
4 | import { cssPrefix } from '../../config';
5 | import { h } from '../element';
6 | import Icon from '../icon';
7 |
8 | class DropdownMore extends Dropdown {
9 | constructor() {
10 | const icon = new Icon('ellipsis');
11 | const moreBtns = h('div', `${cssPrefix}-toolbar-more`);
12 | super(icon, 'auto', false, 'bottom-right', moreBtns);
13 | this.moreBtns = moreBtns;
14 | this.contentEl.css('max-width', '420px');
15 | }
16 | }
17 |
18 | export default class More extends DropdownItem {
19 | constructor() {
20 | super('more');
21 | this.el.hide();
22 | }
23 |
24 | dropdown() {
25 | return new DropdownMore();
26 | }
27 |
28 | show() {
29 | this.el.show();
30 | }
31 |
32 | hide() {
33 | this.el.hide();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/component/toolbar/paintformat.js:
--------------------------------------------------------------------------------
1 | import ToggleItem from './toggle_item';
2 |
3 | export default class Paintformat extends ToggleItem {
4 | constructor() {
5 | super('paintformat');
6 | }
7 |
8 | setState() {}
9 | }
10 |
--------------------------------------------------------------------------------
/src/component/toolbar/redo.js:
--------------------------------------------------------------------------------
1 | import IconItem from './icon_item';
2 |
3 | export default class Redo extends IconItem {
4 | constructor() {
5 | super('redo', 'Ctrl+Y');
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/component/toolbar/strike.js:
--------------------------------------------------------------------------------
1 | import ToggleItem from './toggle_item';
2 |
3 | export default class Strike extends ToggleItem {
4 | constructor() {
5 | super('strike', 'Ctrl+U');
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/component/toolbar/text_color.js:
--------------------------------------------------------------------------------
1 | import DropdownItem from './dropdown_item';
2 | import DropdownColor from '../dropdown_color';
3 |
4 | export default class TextColor extends DropdownItem {
5 | constructor(color) {
6 | super('color', undefined, color);
7 | }
8 |
9 | dropdown() {
10 | const { tag, value } = this;
11 | return new DropdownColor(tag, value);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/component/toolbar/textwrap.js:
--------------------------------------------------------------------------------
1 | import ToggleItem from './toggle_item';
2 |
3 | export default class Textwrap extends ToggleItem {
4 | constructor() {
5 | super('textwrap');
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/component/toolbar/toggle_item.js:
--------------------------------------------------------------------------------
1 | import Item from './item';
2 | import Icon from '../icon';
3 |
4 | export default class ToggleItem extends Item {
5 | element() {
6 | const { tag } = this;
7 | return super.element()
8 | .child(new Icon(tag))
9 | .on('click', () => this.click());
10 | }
11 |
12 | click() {
13 | this.change(this.tag, this.toggle());
14 | }
15 |
16 | setState(active) {
17 | this.el.active(active);
18 | }
19 |
20 | toggle() {
21 | return this.el.toggle();
22 | }
23 |
24 | active() {
25 | return this.el.hasClass('active');
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/component/toolbar/underline.js:
--------------------------------------------------------------------------------
1 | import ToggleItem from './toggle_item';
2 |
3 | export default class Underline extends ToggleItem {
4 | constructor() {
5 | super('underline', 'Ctrl+U');
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/component/toolbar/undo.js:
--------------------------------------------------------------------------------
1 | import IconItem from './icon_item';
2 |
3 | export default class Undo extends IconItem {
4 | constructor() {
5 | super('undo', 'Ctrl+Z');
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/component/toolbar/valign.js:
--------------------------------------------------------------------------------
1 | import DropdownItem from './dropdown_item';
2 | import DropdownAlign from '../dropdown_align';
3 |
4 | export default class Valign extends DropdownItem {
5 | constructor(value) {
6 | super('valign', '', value);
7 | }
8 |
9 | dropdown() {
10 | const { value } = this;
11 | return new DropdownAlign(['top', 'middle', 'bottom'], value);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/component/tooltip.js:
--------------------------------------------------------------------------------
1 | /* global document */
2 | import { h } from './element';
3 | import { bind } from './event';
4 | import { cssPrefix } from '../config';
5 |
6 | export default function tooltip(html, target) {
7 | if (target.classList.contains('active')) {
8 | return;
9 | }
10 | const {
11 | left, top, width, height,
12 | } = target.getBoundingClientRect();
13 | const el = h('div', `${cssPrefix}-tooltip`).html(html).show();
14 | document.body.appendChild(el.el);
15 | const elBox = el.box();
16 | // console.log('elBox:', elBox);
17 | el.css('left', `${left + (width / 2) - (elBox.width / 2)}px`)
18 | .css('top', `${top + height + 2}px`);
19 |
20 | bind(target, 'mouseleave', () => {
21 | if (document.body.contains(el.el)) {
22 | document.body.removeChild(el.el);
23 | }
24 | });
25 |
26 | bind(target, 'click', () => {
27 | if (document.body.contains(el.el)) {
28 | document.body.removeChild(el.el);
29 | }
30 | });
31 | }
32 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | /* global window */
2 | export const cssPrefix = 'x-spreadsheet';
3 | export const dpr = window.devicePixelRatio || 1;
4 | export default {
5 | cssPrefix,
6 | dpr,
7 | };
8 |
--------------------------------------------------------------------------------
/src/core/_.prototypes.js:
--------------------------------------------------------------------------------
1 | // font.js
2 | /**
3 | * @typedef {number} fontsizePX px for fontSize
4 | */
5 | /**
6 | * @typedef {number} fontsizePT pt for fontSize
7 | */
8 | /**
9 | * @typedef {object} BaseFont
10 | * @property {string} key inner key
11 | * @property {string} title title for display
12 | */
13 |
14 | /**
15 | * @typedef {object} FontSize
16 | * @property {fontsizePT} pt
17 | * @property {fontsizePX} px
18 | */
19 |
20 | // alphabet.js
21 | /**
22 | * @typedef {string} tagA1 A1 tag for XY-tag (0, 0)
23 | * @example "A1"
24 | */
25 | /**
26 | * @typedef {[number, number]} tagXY
27 | * @example [0, 0]
28 | */
29 |
--------------------------------------------------------------------------------
/src/core/alphabet.js:
--------------------------------------------------------------------------------
1 | import './_.prototypes';
2 |
3 | const alphabets = ['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'];
4 |
5 | /** index number 2 letters
6 | * @example stringAt(26) ==> 'AA'
7 | * @date 2019-10-10
8 | * @export
9 | * @param {number} index
10 | * @returns {string}
11 | */
12 | export function stringAt(index) {
13 | let str = '';
14 | let cindex = index;
15 | while (cindex >= alphabets.length) {
16 | cindex /= alphabets.length;
17 | cindex -= 1;
18 | str += alphabets[parseInt(cindex, 10) % alphabets.length];
19 | }
20 | const last = index % alphabets.length;
21 | str += alphabets[last];
22 | return str;
23 | }
24 |
25 | /** translate letter in A1-tag to number
26 | * @date 2019-10-10
27 | * @export
28 | * @param {string} str "AA" in A1-tag "AA1"
29 | * @returns {number}
30 | */
31 | export function indexAt(str) {
32 | let ret = 0;
33 | for (let i = 0; i < str.length - 1; i += 1) {
34 | const cindex = str.charCodeAt(i) - 65;
35 | const exponet = str.length - 1 - i;
36 | ret += (alphabets.length ** exponet) + (alphabets.length * cindex);
37 | }
38 | ret += str.charCodeAt(str.length - 1) - 65;
39 | return ret;
40 | }
41 |
42 | // B10 => x,y
43 | /** translate A1-tag to XY-tag
44 | * @date 2019-10-10
45 | * @export
46 | * @param {tagA1} src
47 | * @returns {tagXY}
48 | */
49 | export function expr2xy(src) {
50 | let x = '';
51 | let y = '';
52 | for (let i = 0; i < src.length; i += 1) {
53 | if (src.charAt(i) >= '0' && src.charAt(i) <= '9') {
54 | y += src.charAt(i);
55 | } else {
56 | x += src.charAt(i);
57 | }
58 | }
59 | return [indexAt(x), parseInt(y, 10) - 1];
60 | }
61 |
62 | /** translate XY-tag to A1-tag
63 | * @example x,y => B10
64 | * @date 2019-10-10
65 | * @export
66 | * @param {number} x
67 | * @param {number} y
68 | * @returns {tagA1}
69 | */
70 | export function xy2expr(x, y) {
71 | return `${stringAt(x)}${y + 1}`;
72 | }
73 |
74 | /** translate A1-tag src by (xn, yn)
75 | * @date 2019-10-10
76 | * @export
77 | * @param {tagA1} src
78 | * @param {number} xn
79 | * @param {number} yn
80 | * @returns {tagA1}
81 | */
82 | export function expr2expr(src, xn, yn) {
83 | const [x, y] = expr2xy(src);
84 | return xy2expr(x + xn, y + yn);
85 | }
86 |
87 | export default {
88 | stringAt,
89 | indexAt,
90 | expr2xy,
91 | xy2expr,
92 | expr2expr,
93 | };
94 |
--------------------------------------------------------------------------------
/src/core/auto_filter.js:
--------------------------------------------------------------------------------
1 | import { CellRange } from './cell_range';
2 | // operator: all|eq|neq|gt|gte|lt|lte|in|be
3 | // value:
4 | // in => []
5 | // be => [min, max]
6 | class Filter {
7 | constructor(ci, operator, value) {
8 | this.ci = ci;
9 | this.operator = operator;
10 | this.value = value;
11 | }
12 |
13 | set(operator, value) {
14 | this.operator = operator;
15 | this.value = value;
16 | }
17 |
18 | includes(v) {
19 | const { operator, value } = this;
20 | if (operator === 'all') {
21 | return true;
22 | }
23 | if (operator === 'in') {
24 | return value.includes(v);
25 | }
26 | return false;
27 | }
28 |
29 | vlength() {
30 | const { operator, value } = this;
31 | if (operator === 'in') {
32 | return value.length;
33 | }
34 | return 0;
35 | }
36 |
37 | getData() {
38 | const { ci, operator, value } = this;
39 | return { ci, operator, value };
40 | }
41 | }
42 |
43 | class Sort {
44 | constructor(ci, order) {
45 | this.ci = ci;
46 | this.order = order;
47 | }
48 |
49 | asc() {
50 | return this.order === 'asc';
51 | }
52 |
53 | desc() {
54 | return this.order === 'desc';
55 | }
56 | }
57 |
58 | export default class AutoFilter {
59 | constructor() {
60 | this.ref = null;
61 | this.filters = [];
62 | this.sort = null;
63 | }
64 |
65 | setData({ ref, filters, sort }) {
66 | if (ref != null) {
67 | this.ref = ref;
68 | this.fitlers = filters.map(it => new Filter(it.ci, it.operator, it.value));
69 | if (sort) {
70 | this.sort = new Sort(sort.ci, sort.order);
71 | }
72 | }
73 | }
74 |
75 | getData() {
76 | if (this.active()) {
77 | const { ref, filters, sort } = this;
78 | return { ref, filters: filters.map(it => it.getData()), sort };
79 | }
80 | return {};
81 | }
82 |
83 | addFilter(ci, operator, value) {
84 | const filter = this.getFilter(ci);
85 | if (filter == null) {
86 | this.filters.push(new Filter(ci, operator, value));
87 | } else {
88 | filter.set(operator, value);
89 | }
90 | }
91 |
92 | setSort(ci, order) {
93 | this.sort = order ? new Sort(ci, order) : null;
94 | }
95 |
96 | includes(ri, ci) {
97 | if (this.active()) {
98 | return this.hrange().includes(ri, ci);
99 | }
100 | return false;
101 | }
102 |
103 | getSort(ci) {
104 | const { sort } = this;
105 | if (sort && sort.ci === ci) {
106 | return sort;
107 | }
108 | return null;
109 | }
110 |
111 | getFilter(ci) {
112 | const { filters } = this;
113 | for (let i = 0; i < filters.length; i += 1) {
114 | if (filters[i].ci === ci) {
115 | return filters[i];
116 | }
117 | }
118 | return null;
119 | }
120 |
121 | filteredRows(getCell) {
122 | // const ary = [];
123 | // let lastri = 0;
124 | const rset = new Set();
125 | const fset = new Set();
126 | if (this.active()) {
127 | const { sri, eri } = this.range();
128 | const { filters } = this;
129 | for (let ri = sri + 1; ri <= eri; ri += 1) {
130 | for (let i = 0; i < filters.length; i += 1) {
131 | const filter = filters[i];
132 | const cell = getCell(ri, filter.ci);
133 | const ctext = cell ? cell.text : '';
134 | if (!filter.includes(ctext)) {
135 | rset.add(ri);
136 | break;
137 | } else {
138 | fset.add(ri);
139 | }
140 | }
141 | }
142 | }
143 | return { rset, fset };
144 | }
145 |
146 | items(ci, getCell) {
147 | const m = {};
148 | if (this.active()) {
149 | const { sri, eri } = this.range();
150 | for (let ri = sri + 1; ri <= eri; ri += 1) {
151 | const cell = getCell(ri, ci);
152 | if (cell !== null && !/^\s*$/.test(cell.text)) {
153 | const key = cell.text;
154 | const cnt = (m[key] || 0) + 1;
155 | m[key] = cnt;
156 | } else {
157 | m[''] = (m[''] || 0) + 1;
158 | }
159 | }
160 | }
161 | return m;
162 | }
163 |
164 | range() {
165 | return CellRange.valueOf(this.ref);
166 | }
167 |
168 | hrange() {
169 | const r = this.range();
170 | r.eri = r.sri;
171 | return r;
172 | }
173 |
174 | clear() {
175 | this.ref = null;
176 | this.filters = [];
177 | this.sort = null;
178 | }
179 |
180 | active() {
181 | return this.ref !== null;
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/src/core/cell.js:
--------------------------------------------------------------------------------
1 | import { expr2xy, xy2expr } from './alphabet';
2 |
3 | // Converting infix expression to a suffix expression
4 | // src: AVERAGE(SUM(A1,A2), B1) + 50 + B20
5 | // return: [A1, A2], SUM[, B1],AVERAGE,50,+,B20,+
6 | const infixExprToSuffixExpr = (src) => {
7 | const operatorStack = [];
8 | const stack = [];
9 | let subStrs = []; // SUM, A1, B2, 50 ...
10 | let fnArgType = 0; // 1 => , 2 => :
11 | let fnArgOperator = '';
12 | let fnArgsLen = 1; // A1,A2,A3...
13 | for (let i = 0; i < src.length; i += 1) {
14 | const c = src.charAt(i);
15 | // console.log('c:', c);
16 | if (c !== ' ') {
17 | if (c >= 'a' && c <= 'z') {
18 | subStrs.push(c.toUpperCase());
19 | } else if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || c === '.') {
20 | subStrs.push(c);
21 | } else if (c === '"') {
22 | i += 1;
23 | while (src.charAt(i) !== '"') {
24 | subStrs.push(src.charAt(i));
25 | i += 1;
26 | }
27 | stack.push(`"${subStrs.join('')}`);
28 | subStrs = [];
29 | } else {
30 | // console.log('subStrs:', subStrs.join(''), stack);
31 | if (c !== '(' && subStrs.length > 0) {
32 | stack.push(subStrs.join(''));
33 | }
34 | if (c === ')') {
35 | let c1 = operatorStack.pop();
36 | if (fnArgType === 2) {
37 | // fn argument range => A1:B5
38 | try {
39 | const [ex, ey] = expr2xy(stack.pop());
40 | const [sx, sy] = expr2xy(stack.pop());
41 | // console.log('::', sx, sy, ex, ey);
42 | let rangelen = 0;
43 | for (let x = sx; x <= ex; x += 1) {
44 | for (let y = sy; y <= ey; y += 1) {
45 | stack.push(xy2expr(x, y));
46 | rangelen += 1;
47 | }
48 | }
49 | stack.push([c1, rangelen]);
50 | } catch (e) {
51 | // console.log(e);
52 | }
53 | } else if (fnArgType === 1 || fnArgType === 3) {
54 | if (fnArgType === 3) stack.push(fnArgOperator);
55 | // fn argument => A1,A2,B5
56 | stack.push([c1, fnArgsLen]);
57 | fnArgsLen = 1;
58 | } else {
59 | // console.log('c1:', c1, fnArgType, stack, operatorStack);
60 | while (c1 !== '(') {
61 | stack.push(c1);
62 | if (operatorStack.length <= 0) break;
63 | c1 = operatorStack.pop();
64 | }
65 | }
66 | fnArgType = 0;
67 | } else if (c === '=' || c === '>' || c === '<') {
68 | const nc = src.charAt(i + 1);
69 | fnArgOperator = c;
70 | if (nc === '=' || nc === '-') {
71 | fnArgOperator += nc;
72 | i += 1;
73 | }
74 | fnArgType = 3;
75 | } else if (c === ':') {
76 | fnArgType = 2;
77 | } else if (c === ',') {
78 | if (fnArgType === 3) {
79 | stack.push(fnArgOperator);
80 | }
81 | fnArgType = 1;
82 | fnArgsLen += 1;
83 | } else if (c === '(' && subStrs.length > 0) {
84 | // function
85 | operatorStack.push(subStrs.join(''));
86 | } else {
87 | // priority: */ > +-
88 | // console.log(operatorStack, c, stack);
89 | if (operatorStack.length > 0 && (c === '+' || c === '-')) {
90 | let top = operatorStack[operatorStack.length - 1];
91 | if (top !== '(') stack.push(operatorStack.pop());
92 | if (top === '*' || top === '/') {
93 | while (operatorStack.length > 0) {
94 | top = operatorStack[operatorStack.length - 1];
95 | if (top !== '(') stack.push(operatorStack.pop());
96 | else break;
97 | }
98 | }
99 | }
100 | operatorStack.push(c);
101 | }
102 | subStrs = [];
103 | }
104 | }
105 | }
106 | if (subStrs.length > 0) {
107 | stack.push(subStrs.join(''));
108 | }
109 | while (operatorStack.length > 0) {
110 | stack.push(operatorStack.pop());
111 | }
112 | return stack;
113 | };
114 |
115 | const evalSubExpr = (subExpr, cellRender) => {
116 | if (subExpr[0] >= '0' && subExpr[0] <= '9') {
117 | return Number(subExpr);
118 | }
119 | if (subExpr[0] === '"') {
120 | return subExpr.substring(1);
121 | }
122 | const [x, y] = expr2xy(subExpr);
123 | return cellRender(x, y);
124 | };
125 |
126 | // evaluate the suffix expression
127 | // srcStack: <= infixExprToSufixExpr
128 | // formulaMap: {'SUM': {}, ...}
129 | // cellRender: (x, y) => {}
130 | const evalSuffixExpr = (srcStack, formulaMap, cellRender, cellList) => {
131 | const stack = [];
132 | // console.log(':::::formulaMap:', formulaMap);
133 | for (let i = 0; i < srcStack.length; i += 1) {
134 | // console.log(':::>>>', srcStack[i]);
135 | const expr = srcStack[i];
136 | const fc = expr[0];
137 | if (expr === '+') {
138 | const top = stack.pop();
139 | stack.push(Number(stack.pop()) + Number(top));
140 | } else if (expr === '-') {
141 | if (stack.length === 1) {
142 | const top = stack.pop();
143 | stack.push(Number(top) * -1);
144 | } else {
145 | const top = stack.pop();
146 | stack.push(Number(stack.pop()) - Number(top));
147 | }
148 | } else if (expr === '*') {
149 | stack.push(Number(stack.pop()) * Number(stack.pop()));
150 | } else if (expr === '/') {
151 | const top = stack.pop();
152 | stack.push(Number(stack.pop()) / Number(top));
153 | } else if (fc === '=' || fc === '>' || fc === '<') {
154 | const top = stack.pop();
155 | const Fn = Function;
156 | stack.push(new Fn(`return ${stack.pop()} ${expr === '=' ? '==' : expr} ${top}`)());
157 | } else if (Array.isArray(expr)) {
158 | const [formula, len] = expr;
159 | const params = [];
160 | for (let j = 0; j < len; j += 1) {
161 | params.push(stack.pop());
162 | }
163 | // console.log('::::params:', formulaMap, expr, formula, params);
164 | stack.push(formulaMap[formula].render(params.reverse()));
165 | } else {
166 | // console.log('cellList:', cellList, expr);
167 | if (cellList.includes(expr)) {
168 | return 0;
169 | }
170 | if ((fc >= 'a' && fc <= 'z') || (fc >= 'A' && fc <= 'Z')) {
171 | cellList.push(expr);
172 | }
173 | stack.push(evalSubExpr(expr, cellRender));
174 | }
175 | // console.log('stack:', stack);
176 | }
177 | return stack[0];
178 | };
179 |
180 | const cellRender = (src, formulaMap, getCellText, cellList = []) => {
181 | if (src[0] === '=') {
182 | const stack = infixExprToSuffixExpr(src.substring(1));
183 | if (stack.length <= 0) return src;
184 | return evalSuffixExpr(
185 | stack,
186 | formulaMap,
187 | (x, y) => cellRender(getCellText(x, y), formulaMap, getCellText, cellList),
188 | cellList,
189 | );
190 | }
191 | return src;
192 | };
193 |
194 | export default {
195 | render: cellRender,
196 | };
197 | export {
198 | infixExprToSuffixExpr,
199 | };
200 |
--------------------------------------------------------------------------------
/src/core/cell_range.js:
--------------------------------------------------------------------------------
1 | import { xy2expr, expr2xy } from './alphabet';
2 |
3 | class CellRange {
4 | constructor(sri, sci, eri, eci, w = 0, h = 0) {
5 | this.sri = sri;
6 | this.sci = sci;
7 | this.eri = eri;
8 | this.eci = eci;
9 | this.w = w;
10 | this.h = h;
11 | }
12 |
13 | set(sri, sci, eri, eci) {
14 | this.sri = sri;
15 | this.sci = sci;
16 | this.eri = eri;
17 | this.eci = eci;
18 | }
19 |
20 | multiple() {
21 | return this.eri - this.sri > 0 || this.eci - this.sci > 0;
22 | }
23 |
24 | // cell-index: ri, ci
25 | // cell-ref: A10
26 | includes(...args) {
27 | let [ri, ci] = [0, 0];
28 | if (args.length === 1) {
29 | [ci, ri] = expr2xy(args[0]);
30 | } else if (args.length === 2) {
31 | [ri, ci] = args;
32 | }
33 | const {
34 | sri, sci, eri, eci,
35 | } = this;
36 | return sri <= ri && ri <= eri && sci <= ci && ci <= eci;
37 | }
38 |
39 | each(cb, rowFilter = () => true) {
40 | const {
41 | sri, sci, eri, eci,
42 | } = this;
43 | for (let i = sri; i <= eri; i += 1) {
44 | if (rowFilter(i)) {
45 | for (let j = sci; j <= eci; j += 1) {
46 | cb(i, j);
47 | }
48 | }
49 | }
50 | }
51 |
52 | contains(other) {
53 | return this.sri <= other.sri
54 | && this.sci <= other.sci
55 | && this.eri >= other.eri
56 | && this.eci >= other.eci;
57 | }
58 |
59 | // within
60 | within(other) {
61 | return this.sri >= other.sri
62 | && this.sci >= other.sci
63 | && this.eri <= other.eri
64 | && this.eci <= other.eci;
65 | }
66 |
67 | // disjoint
68 | disjoint(other) {
69 | return this.sri > other.eri
70 | || this.sci > other.eci
71 | || other.sri > this.eri
72 | || other.sci > this.eci;
73 | }
74 |
75 | // intersects
76 | intersects(other) {
77 | return this.sri <= other.eri
78 | && this.sci <= other.eci
79 | && other.sri <= this.eri
80 | && other.sci <= this.eci;
81 | }
82 |
83 | // union
84 | union(other) {
85 | const {
86 | sri, sci, eri, eci,
87 | } = this;
88 | return new CellRange(
89 | other.sri < sri ? other.sri : sri,
90 | other.sci < sci ? other.sci : sci,
91 | other.eri > eri ? other.eri : eri,
92 | other.eci > eci ? other.eci : eci,
93 | );
94 | }
95 |
96 | // intersection
97 | // intersection(other) {}
98 |
99 | // Returns Array that represents that part of this that does not intersect with other
100 | // difference
101 | difference(other) {
102 | const ret = [];
103 | const addRet = (sri, sci, eri, eci) => {
104 | ret.push(new CellRange(sri, sci, eri, eci));
105 | };
106 | const {
107 | sri, sci, eri, eci,
108 | } = this;
109 | const dsr = other.sri - sri;
110 | const dsc = other.sci - sci;
111 | const der = eri - other.eri;
112 | const dec = eci - other.eci;
113 | if (dsr > 0) {
114 | addRet(sri, sci, other.sri - 1, eci);
115 | if (der > 0) {
116 | addRet(other.eri + 1, sci, eri, eci);
117 | if (dsc > 0) {
118 | addRet(other.sri, sci, other.eri, other.sci - 1);
119 | }
120 | if (dec > 0) {
121 | addRet(other.sri, other.eci + 1, other.eri, eci);
122 | }
123 | } else {
124 | if (dsc > 0) {
125 | addRet(other.sri, sci, eri, other.sci - 1);
126 | }
127 | if (dec > 0) {
128 | addRet(other.sri, other.eci + 1, eri, eci);
129 | }
130 | }
131 | } else if (der > 0) {
132 | addRet(other.eri + 1, sci, eri, eci);
133 | if (dsc > 0) {
134 | addRet(sri, sci, other.eri, other.sci - 1);
135 | }
136 | if (dec > 0) {
137 | addRet(sri, other.eci + 1, other.eri, eci);
138 | }
139 | }
140 | if (dsc > 0) {
141 | addRet(sri, sci, eri, other.sci - 1);
142 | if (dec > 0) {
143 | addRet(sri, other.eri + 1, eri, eci);
144 | if (dsr > 0) {
145 | addRet(sri, other.sci, other.sri - 1, other.eci);
146 | }
147 | if (der > 0) {
148 | addRet(other.sri + 1, other.sci, eri, other.eci);
149 | }
150 | } else {
151 | if (dsr > 0) {
152 | addRet(sri, other.sci, other.sri - 1, eci);
153 | }
154 | if (der > 0) {
155 | addRet(other.sri + 1, other.sci, eri, eci);
156 | }
157 | }
158 | } else if (dec > 0) {
159 | addRet(eri, other.eci + 1, eri, eci);
160 | if (dsr > 0) {
161 | addRet(sri, sci, other.sri - 1, other.eci);
162 | }
163 | if (der > 0) {
164 | addRet(other.eri + 1, sci, eri, other.eci);
165 | }
166 | }
167 | return ret;
168 | }
169 |
170 | size() {
171 | return [
172 | this.eri - this.sri + 1,
173 | this.eci - this.sci + 1,
174 | ];
175 | }
176 |
177 | toString() {
178 | const {
179 | sri, sci, eri, eci,
180 | } = this;
181 | let ref = xy2expr(sci, sri);
182 | if (this.multiple()) {
183 | ref = `${ref}:${xy2expr(eci, eri)}`;
184 | }
185 | return ref;
186 | }
187 |
188 | clone() {
189 | const {
190 | sri, sci, eri, eci, w, h,
191 | } = this;
192 | return new CellRange(sri, sci, eri, eci, w, h);
193 | }
194 |
195 | /*
196 | toJSON() {
197 | return this.toString();
198 | }
199 | */
200 |
201 | equals(other) {
202 | return this.eri === other.eri
203 | && this.eci === other.eci
204 | && this.sri === other.sri
205 | && this.sci === other.sci;
206 | }
207 |
208 | static valueOf(ref) {
209 | // B1:B8, B1 => 1 x 1 cell range
210 | const refs = ref.split(':');
211 | const [sci, sri] = expr2xy(refs[0]);
212 | let [eri, eci] = [sri, sci];
213 | if (refs.length > 1) {
214 | [eci, eri] = expr2xy(refs[1]);
215 | }
216 | return new CellRange(sri, sci, eri, eci);
217 | }
218 | }
219 |
220 | export default CellRange;
221 |
222 | export {
223 | CellRange,
224 | };
225 |
--------------------------------------------------------------------------------
/src/core/clipboard.js:
--------------------------------------------------------------------------------
1 | export default class Clipboard {
2 | constructor() {
3 | this.range = null; // CellRange
4 | this.state = 'clear';
5 | }
6 |
7 | copy(cellRange) {
8 | this.range = cellRange;
9 | this.state = 'copy';
10 | return this;
11 | }
12 |
13 | cut(cellRange) {
14 | this.range = cellRange;
15 | this.state = 'cut';
16 | return this;
17 | }
18 |
19 | isCopy() {
20 | return this.state === 'copy';
21 | }
22 |
23 | isCut() {
24 | return this.state === 'cut';
25 | }
26 |
27 | isClear() {
28 | return this.state === 'clear';
29 | }
30 |
31 | clear() {
32 | this.range = null;
33 | this.state = 'clear';
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/core/col.js:
--------------------------------------------------------------------------------
1 | import helper from './helper';
2 |
3 | class Cols {
4 | constructor({
5 | len, width, indexWidth, minWidth,
6 | }) {
7 | this._ = {};
8 | this.len = len;
9 | this.width = width;
10 | this.indexWidth = indexWidth;
11 | this.minWidth = minWidth;
12 | }
13 |
14 | setData(d) {
15 | if (d.len) {
16 | this.len = d.len;
17 | delete d.len;
18 | }
19 | this._ = d;
20 | }
21 |
22 | getData() {
23 | const { len } = this;
24 | return Object.assign({ len }, this._);
25 | }
26 |
27 | getWidth(i) {
28 | const col = this._[i];
29 | if (col && col.width) {
30 | return col.width;
31 | }
32 | return this.width;
33 | }
34 |
35 | getOrNew(ci) {
36 | this._[ci] = this._[ci] || {};
37 | return this._[ci];
38 | }
39 |
40 | setWidth(ci, width) {
41 | const col = this.getOrNew(ci);
42 | col.width = width;
43 | }
44 |
45 | setStyle(ci, style) {
46 | const col = this.getOrNew(ci);
47 | col.style = style;
48 | }
49 |
50 | sumWidth(min, max) {
51 | return helper.rangeSum(min, max, i => this.getWidth(i));
52 | }
53 |
54 | totalWidth() {
55 | return this.sumWidth(0, this.len);
56 | }
57 | }
58 |
59 | export default {};
60 | export {
61 | Cols,
62 | };
63 |
--------------------------------------------------------------------------------
/src/core/font.js:
--------------------------------------------------------------------------------
1 | // docs
2 | import './_.prototypes';
3 |
4 | /** default font list
5 | * @type {BaseFont[]}
6 | */
7 | const baseFonts = [
8 | { key: 'Arial', title: 'Arial' },
9 | { key: 'Helvetica', title: 'Helvetica' },
10 | { key: 'Source Sans Pro', title: 'Source Sans Pro' },
11 | { key: 'Comic Sans MS', title: 'Comic Sans MS' },
12 | { key: 'Courier New', title: 'Courier New' },
13 | { key: 'Verdana', title: 'Verdana' },
14 | { key: 'Lato', title: 'Lato' },
15 | ];
16 |
17 | /** default fontSize list
18 | * @type {FontSize[]}
19 | */
20 | const fontSizes = [
21 | { pt: 7.5, px: 10 },
22 | { pt: 8, px: 11 },
23 | { pt: 9, px: 12 },
24 | { pt: 10, px: 13 },
25 | { pt: 10.5, px: 14 },
26 | { pt: 11, px: 15 },
27 | { pt: 12, px: 16 },
28 | { pt: 14, px: 18.7 },
29 | { pt: 15, px: 20 },
30 | { pt: 16, px: 21.3 },
31 | { pt: 18, px: 24 },
32 | { pt: 22, px: 29.3 },
33 | { pt: 24, px: 32 },
34 | { pt: 26, px: 34.7 },
35 | { pt: 36, px: 48 },
36 | { pt: 42, px: 56 },
37 | // { pt: 54, px: 71.7 },
38 | // { pt: 63, px: 83.7 },
39 | // { pt: 72, px: 95.6 },
40 | ];
41 |
42 | /** map pt to px
43 | * @date 2019-10-10
44 | * @param {fontsizePT} pt
45 | * @returns {fontsizePX}
46 | */
47 | function getFontSizePxByPt(pt) {
48 | for (let i = 0; i < fontSizes.length; i += 1) {
49 | const fontSize = fontSizes[i];
50 | if (fontSize.pt === pt) {
51 | return fontSize.px;
52 | }
53 | }
54 | return pt;
55 | }
56 |
57 | /** transform baseFonts to map
58 | * @date 2019-10-10
59 | * @param {BaseFont[]} [ary=[]]
60 | * @returns {object}
61 | */
62 | function fonts(ary = []) {
63 | const map = {};
64 | baseFonts.concat(ary).forEach((f) => {
65 | map[f.key] = f;
66 | });
67 | return map;
68 | }
69 |
70 | export default {};
71 | export {
72 | fontSizes,
73 | fonts,
74 | baseFonts,
75 | getFontSizePxByPt,
76 | };
77 |
--------------------------------------------------------------------------------
/src/core/format.js:
--------------------------------------------------------------------------------
1 | import { tf } from '../locale/locale';
2 |
3 | const formatStringRender = v => v;
4 |
5 | const formatNumberRender = (v) => {
6 | // match "-12.1" or "12" or "12.1"
7 | if (/^(-?\d*.?\d*)$/.test(v)) {
8 | const v1 = Number(v).toFixed(2).toString();
9 | const [first, ...parts] = v1.split('\\.');
10 | return [first.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'), ...parts];
11 | }
12 | return v;
13 | };
14 |
15 | const baseFormats = [
16 | {
17 | key: 'normal',
18 | title: tf('format.normal'),
19 | type: 'string',
20 | render: formatStringRender,
21 | },
22 | {
23 | key: 'text',
24 | title: tf('format.text'),
25 | type: 'string',
26 | render: formatStringRender,
27 | },
28 | {
29 | key: 'number',
30 | title: tf('format.number'),
31 | type: 'number',
32 | label: '1,000.12',
33 | render: formatNumberRender,
34 | },
35 | {
36 | key: 'percent',
37 | title: tf('format.percent'),
38 | type: 'number',
39 | label: '10.12%',
40 | render: v => `${v}%`,
41 | },
42 | {
43 | key: 'rmb',
44 | title: tf('format.rmb'),
45 | type: 'number',
46 | label: '¥10.00',
47 | render: v => `¥${formatNumberRender(v)}`,
48 | },
49 | {
50 | key: 'usd',
51 | title: tf('format.usd'),
52 | type: 'number',
53 | label: '$10.00',
54 | render: v => `$${formatNumberRender(v)}`,
55 | },
56 | {
57 | key: 'eur',
58 | title: tf('format.eur'),
59 | type: 'number',
60 | label: '€10.00',
61 | render: v => `€${formatNumberRender(v)}`,
62 | },
63 | {
64 | key: 'date',
65 | title: tf('format.date'),
66 | type: 'date',
67 | label: '26/09/2008',
68 | render: formatStringRender,
69 | },
70 | {
71 | key: 'time',
72 | title: tf('format.time'),
73 | type: 'date',
74 | label: '15:59:00',
75 | render: formatStringRender,
76 | },
77 | {
78 | key: 'datetime',
79 | title: tf('format.datetime'),
80 | type: 'date',
81 | label: '26/09/2008 15:59:00',
82 | render: formatStringRender,
83 | },
84 | {
85 | key: 'duration',
86 | title: tf('format.duration'),
87 | type: 'date',
88 | label: '24:01:00',
89 | render: formatStringRender,
90 | },
91 | ];
92 |
93 | // const formats = (ary = []) => {
94 | // const map = {};
95 | // baseFormats.concat(ary).forEach((f) => {
96 | // map[f.key] = f;
97 | // });
98 | // return map;
99 | // };
100 | const formatm = {};
101 | baseFormats.forEach((f) => {
102 | formatm[f.key] = f;
103 | });
104 |
105 | export default {
106 | };
107 | export {
108 | formatm,
109 | baseFormats,
110 | };
111 |
--------------------------------------------------------------------------------
/src/core/formula.js:
--------------------------------------------------------------------------------
1 | /**
2 | formula:
3 | key
4 | title
5 | render
6 | */
7 | /**
8 | * @typedef {object} Formula
9 | * @property {string} key
10 | * @property {function} title
11 | * @property {function} render
12 | */
13 | import { tf } from '../locale/locale';
14 |
15 | /** @type {Formula[]} */
16 | const baseFormulas = [
17 | {
18 | key: 'SUM',
19 | title: tf('formula.sum'),
20 | render: ary => ary.reduce((a, b) => Number(a) + Number(b), 0),
21 | },
22 | {
23 | key: 'AVERAGE',
24 | title: tf('formula.average'),
25 | render: ary => ary.reduce((a, b) => Number(a) + Number(b), 0) / ary.length,
26 | },
27 | {
28 | key: 'MAX',
29 | title: tf('formula.max'),
30 | render: ary => Math.max(...ary.map(v => Number(v))),
31 | },
32 | {
33 | key: 'MIN',
34 | title: tf('formula.min'),
35 | render: ary => Math.min(...ary.map(v => Number(v))),
36 | },
37 | {
38 | key: 'IF',
39 | title: tf('formula._if'),
40 | render: ([b, t, f]) => (b ? t : f),
41 | },
42 | {
43 | key: 'AND',
44 | title: tf('formula.and'),
45 | render: ary => ary.every(it => it),
46 | },
47 | {
48 | key: 'OR',
49 | title: tf('formula.or'),
50 | render: ary => ary.some(it => it),
51 | },
52 | {
53 | key: 'CONCAT',
54 | title: tf('formula.concat'),
55 | render: ary => ary.join(''),
56 | },
57 | /* support: 1 + A1 + B2 * 3
58 | {
59 | key: 'DIVIDE',
60 | title: tf('formula.divide'),
61 | render: ary => ary.reduce((a, b) => Number(a) / Number(b)),
62 | },
63 | {
64 | key: 'PRODUCT',
65 | title: tf('formula.product'),
66 | render: ary => ary.reduce((a, b) => Number(a) * Number(b),1),
67 | },
68 | {
69 | key: 'SUBTRACT',
70 | title: tf('formula.subtract'),
71 | render: ary => ary.reduce((a, b) => Number(a) - Number(b)),
72 | },
73 | */
74 | ];
75 |
76 | const formulas = baseFormulas;
77 |
78 | // const formulas = (formulaAry = []) => {
79 | // const formulaMap = {};
80 | // baseFormulas.concat(formulaAry).forEach((f) => {
81 | // formulaMap[f.key] = f;
82 | // });
83 | // return formulaMap;
84 | // };
85 | const formulam = {};
86 | baseFormulas.forEach((f) => {
87 | formulam[f.key] = f;
88 | });
89 |
90 | export default {
91 | };
92 |
93 | export {
94 | formulam,
95 | formulas,
96 | baseFormulas,
97 | };
98 |
--------------------------------------------------------------------------------
/src/core/helper.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | function cloneDeep(obj) {
3 | return JSON.parse(JSON.stringify(obj));
4 | }
5 |
6 | const mergeDeep = (object = {}, ...sources) => {
7 | sources.forEach((source) => {
8 | Object.keys(source).forEach((key) => {
9 | const v = source[key];
10 | // console.log('k:', key, ', v:', source[key], typeof v, v instanceof Object);
11 | if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
12 | object[key] = v;
13 | } else if (typeof v !== 'function' && !Array.isArray(v) && v instanceof Object) {
14 | object[key] = object[key] || {};
15 | mergeDeep(object[key], v);
16 | } else {
17 | object[key] = v;
18 | }
19 | });
20 | });
21 | // console.log('::', object);
22 | return object;
23 | };
24 |
25 | function equals(obj1, obj2) {
26 | const keys = Object.keys(obj1);
27 | if (keys.length !== Object.keys(obj2).length) return false;
28 | for (let i = 0; i < keys.length; i += 1) {
29 | const k = keys[i];
30 | const v1 = obj1[k];
31 | const v2 = obj2[k];
32 | if (v2 === undefined) return false;
33 | if (typeof v1 === 'string' || typeof v1 === 'number' || typeof v1 === 'boolean') {
34 | if (v1 !== v2) return false;
35 | } else if (Array.isArray(v1)) {
36 | if (v1.length !== v2.length) return false;
37 | for (let ai = 0; ai < v1.length; ai += 1) {
38 | if (!equals(v1[ai], v2[ai])) return false;
39 | }
40 | } else if (typeof v1 !== 'function' && !Array.isArray(v1) && v1 instanceof Object) {
41 | if (!equals(v1, v2)) return false;
42 | }
43 | }
44 | return true;
45 | }
46 |
47 | /*
48 | objOrAry: obejct or Array
49 | cb: (value, index | key) => { return value }
50 | */
51 | const sum = (objOrAry, cb = value => value) => {
52 | let total = 0;
53 | let size = 0;
54 | Object.keys(objOrAry).forEach((key) => {
55 | total += cb(objOrAry[key], key);
56 | size += 1;
57 | });
58 | return [total, size];
59 | };
60 |
61 | function deleteProperty(obj, property) {
62 | const oldv = obj[`${property}`];
63 | delete obj[`${property}`];
64 | return oldv;
65 | }
66 |
67 | function rangeReduceIf(min, max, inits, initv, ifv, getv) {
68 | let s = inits;
69 | let v = initv;
70 | let i = min;
71 | for (; i < max; i += 1) {
72 | if (s > ifv) break;
73 | v = getv(i);
74 | s += v;
75 | }
76 | return [i, s - v, v];
77 | }
78 |
79 | function rangeSum(min, max, getv) {
80 | let s = 0;
81 | for (let i = min; i < max; i += 1) {
82 | s += getv(i);
83 | }
84 | return s;
85 | }
86 |
87 | function rangeEach(min, max, cb) {
88 | for (let i = min; i < max; i += 1) {
89 | cb(i);
90 | }
91 | }
92 |
93 | function arrayEquals(a1, a2) {
94 | if (a1.length === a2.length) {
95 | for (let i = 0; i < a1.length; i += 1) {
96 | if (a1[i] !== a2[i]) return false;
97 | }
98 | } else return false;
99 | return true;
100 | }
101 |
102 | export default {
103 | cloneDeep,
104 | merge: (...sources) => mergeDeep({}, ...sources),
105 | equals,
106 | arrayEquals,
107 | sum,
108 | rangeEach,
109 | rangeSum,
110 | rangeReduceIf,
111 | deleteProperty,
112 | };
113 |
--------------------------------------------------------------------------------
/src/core/history.js:
--------------------------------------------------------------------------------
1 | // import helper from '../helper';
2 |
3 | export default class History {
4 | constructor() {
5 | this.undoItems = [];
6 | this.redoItems = [];
7 | }
8 |
9 | add(data) {
10 | this.undoItems.push(JSON.stringify(data));
11 | this.redoItems = [];
12 | }
13 |
14 | canUndo() {
15 | return this.undoItems.length > 0;
16 | }
17 |
18 | canRedo() {
19 | return this.redoItems.length > 0;
20 | }
21 |
22 | undo(currentd, cb) {
23 | const { undoItems, redoItems } = this;
24 | if (this.canUndo()) {
25 | redoItems.push(JSON.stringify(currentd));
26 | cb(JSON.parse(undoItems.pop()));
27 | }
28 | }
29 |
30 | redo(currentd, cb) {
31 | const { undoItems, redoItems } = this;
32 | if (this.canRedo()) {
33 | undoItems.push(JSON.stringify(currentd));
34 | cb(JSON.parse(redoItems.pop()));
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/core/merge.js:
--------------------------------------------------------------------------------
1 | import { CellRange } from './cell_range';
2 |
3 | class Merges {
4 | constructor(d = []) {
5 | this._ = d;
6 | }
7 |
8 | forEach(cb) {
9 | this._.forEach(cb);
10 | }
11 |
12 | deleteWithin(cr) {
13 | this._ = this._.filter(it => !it.within(cr));
14 | }
15 |
16 | getFirstIncludes(ri, ci) {
17 | for (let i = 0; i < this._.length; i += 1) {
18 | const it = this._[i];
19 | if (it.includes(ri, ci)) {
20 | return it;
21 | }
22 | }
23 | return null;
24 | }
25 |
26 | filterIntersects(cellRange) {
27 | return new Merges(this._.filter(it => it.intersects(cellRange)));
28 | }
29 |
30 | intersects(cellRange) {
31 | for (let i = 0; i < this._.length; i += 1) {
32 | const it = this._[i];
33 | if (it.intersects(cellRange)) {
34 | // console.log('intersects');
35 | return true;
36 | }
37 | }
38 | return false;
39 | }
40 |
41 | union(cellRange) {
42 | let cr = cellRange;
43 | this._.forEach((it) => {
44 | if (it.intersects(cr)) {
45 | cr = it.union(cr);
46 | }
47 | });
48 | return cr;
49 | }
50 |
51 | add(cr) {
52 | this.deleteWithin(cr);
53 | this._.push(cr);
54 | }
55 |
56 | // type: row | column
57 | shift(type, index, n, cbWithin) {
58 | this._.forEach((cellRange) => {
59 | const {
60 | sri, sci, eri, eci,
61 | } = cellRange;
62 | const range = cellRange;
63 | if (type === 'row') {
64 | if (sri >= index) {
65 | range.sri += n;
66 | range.eri += n;
67 | } else if (sri < index && index <= eri) {
68 | range.eri += n;
69 | cbWithin(sri, sci, n, 0);
70 | }
71 | } else if (type === 'column') {
72 | if (sci >= index) {
73 | range.sci += n;
74 | range.eci += n;
75 | } else if (sci < index && index <= eci) {
76 | range.eci += n;
77 | cbWithin(sri, sci, 0, n);
78 | }
79 | }
80 | });
81 | }
82 |
83 | move(cellRange, rn, cn) {
84 | this._.forEach((it1) => {
85 | const it = it1;
86 | if (it.within(cellRange)) {
87 | it.eri += rn;
88 | it.sri += rn;
89 | it.sci += cn;
90 | it.eci += cn;
91 | }
92 | });
93 | }
94 |
95 | setData(merges) {
96 | this._ = merges.map(merge => CellRange.valueOf(merge));
97 | return this;
98 | }
99 |
100 | getData() {
101 | return this._.map(merge => merge.toString());
102 | }
103 | }
104 |
105 | export default {};
106 | export {
107 | Merges,
108 | };
109 |
--------------------------------------------------------------------------------
/src/core/scroll.js:
--------------------------------------------------------------------------------
1 | export default class Scroll {
2 | constructor() {
3 | this.x = 0; // left
4 | this.y = 0; // top
5 | this.ri = 0; // cell row-index
6 | this.ci = 0; // cell col-index
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/core/selector.js:
--------------------------------------------------------------------------------
1 | import { CellRange } from './cell_range';
2 |
3 | export default class Selector {
4 | constructor() {
5 | this.range = new CellRange(0, 0, 0, 0);
6 | this.ri = 0;
7 | this.ci = 0;
8 | }
9 |
10 | multiple() {
11 | return this.range.multiple();
12 | }
13 |
14 | setIndexes(ri, ci) {
15 | this.ri = ri;
16 | this.ci = ci;
17 | }
18 |
19 | size() {
20 | return this.range.size();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/core/validation.js:
--------------------------------------------------------------------------------
1 | import Validator from './validator';
2 | import { CellRange } from './cell_range';
3 |
4 | class Validation {
5 | constructor(mode, refs, validator) {
6 | this.refs = refs;
7 | this.mode = mode; // cell
8 | this.validator = validator;
9 | }
10 |
11 | includes(ri, ci) {
12 | const { refs } = this;
13 | for (let i = 0; i < refs.length; i += 1) {
14 | const cr = CellRange.valueOf(refs[i]);
15 | if (cr.includes(ri, ci)) return true;
16 | }
17 | return false;
18 | }
19 |
20 | addRef(ref) {
21 | this.remove(CellRange.valueOf(ref));
22 | this.refs.push(ref);
23 | }
24 |
25 | remove(cellRange) {
26 | const nrefs = [];
27 | this.refs.forEach((it) => {
28 | const cr = CellRange.valueOf(it);
29 | if (cr.intersects(cellRange)) {
30 | const crs = cr.difference(cellRange);
31 | crs.forEach(it1 => nrefs.push(it1.toString()));
32 | } else {
33 | nrefs.push(it);
34 | }
35 | });
36 | this.refs = nrefs;
37 | }
38 |
39 | getData() {
40 | const { refs, mode, validator } = this;
41 | const {
42 | type, required, operator, value,
43 | } = validator;
44 | return {
45 | refs, mode, type, required, operator, value,
46 | };
47 | }
48 |
49 | static valueOf({
50 | refs, mode, type, required, operator, value,
51 | }) {
52 | return new Validation(mode, refs, new Validator(type, required, value, operator));
53 | }
54 | }
55 | class Validations {
56 | constructor() {
57 | this._ = [];
58 | // ri_ci: errMessage
59 | this.errors = new Map();
60 | }
61 |
62 | getError(ri, ci) {
63 | return this.errors.get(`${ri}_${ci}`);
64 | }
65 |
66 | validate(ri, ci, text) {
67 | const v = this.get(ri, ci);
68 | const key = `${ri}_${ci}`;
69 | const { errors } = this;
70 | if (v !== null) {
71 | const [flag, message] = v.validator.validate(text);
72 | if (!flag) {
73 | errors.set(key, message);
74 | } else {
75 | errors.delete(key);
76 | }
77 | } else {
78 | errors.delete(key);
79 | }
80 | return true;
81 | }
82 |
83 | // type: date|number|phone|email|list
84 | // validator: { required, value, operator }
85 | add(mode, ref, {
86 | type, required, value, operator,
87 | }) {
88 | const validator = new Validator(
89 | type, required, value, operator,
90 | );
91 | const v = this.getByValidator(validator);
92 | if (v !== null) {
93 | v.addRef(ref);
94 | } else {
95 | this._.push(new Validation(mode, [ref], validator));
96 | }
97 | }
98 |
99 | getByValidator(validator) {
100 | for (let i = 0; i < this._.length; i += 1) {
101 | const v = this._[i];
102 | if (v.validator.equals(validator)) {
103 | return v;
104 | }
105 | }
106 | return null;
107 | }
108 |
109 | get(ri, ci) {
110 | for (let i = 0; i < this._.length; i += 1) {
111 | const v = this._[i];
112 | if (v.includes(ri, ci)) return v;
113 | }
114 | return null;
115 | }
116 |
117 | remove(cellRange) {
118 | this.each((it) => {
119 | it.remove(cellRange);
120 | });
121 | }
122 |
123 | each(cb) {
124 | this._.forEach(it => cb(it));
125 | }
126 |
127 | getData() {
128 | return this._.filter(it => it.refs.length > 0).map(it => it.getData());
129 | }
130 |
131 | setData(d) {
132 | this._ = d.map(it => Validation.valueOf(it));
133 | }
134 | }
135 |
136 | export default {};
137 | export {
138 | Validations,
139 | };
140 |
--------------------------------------------------------------------------------
/src/core/validator.js:
--------------------------------------------------------------------------------
1 | import { t } from '../locale/locale';
2 | import helper from './helper';
3 |
4 | const rules = {
5 | phone: /^[1-9]\d{10}$/,
6 | email: /w+([-+.]w+)*@w+([-.]w+)*.w+([-.]w+)*/,
7 | };
8 |
9 | function returnMessage(flag, key, ...arg) {
10 | let message = '';
11 | if (!flag) {
12 | message = t(`validation.${key}`, ...arg);
13 | }
14 | return [flag, message];
15 | }
16 |
17 | export default class Validator {
18 | // operator: b|nb|eq|neq|lt|lte|gt|gte
19 | // type: date|number|list|phone|email
20 | constructor(type, required, value, operator) {
21 | this.required = required;
22 | this.value = value;
23 | this.type = type;
24 | this.operator = operator;
25 | this.message = '';
26 | }
27 |
28 | parseValue(v) {
29 | const { type } = this;
30 | if (type === 'date') {
31 | return new Date(v);
32 | }
33 | if (type === 'number') {
34 | return Number(v);
35 | }
36 | return v;
37 | }
38 |
39 | equals(other) {
40 | let flag = this.type === other.type
41 | && this.required === other.required
42 | && this.operator === other.operator;
43 | if (flag) {
44 | if (Array.isArray(this.value)) {
45 | flag = helper.arrayEquals(this.value, other.value);
46 | } else {
47 | flag = this.value === other.value;
48 | }
49 | }
50 | return flag;
51 | }
52 |
53 | values() {
54 | return this.value.split(',');
55 | }
56 |
57 | validate(v) {
58 | const {
59 | required, operator, value, type,
60 | } = this;
61 | if (required && /^\s*$/.test(v)) {
62 | return returnMessage(false, 'required');
63 | }
64 | if (/^\s*$/.test(v)) return [true];
65 | if (rules[type] && !rules[type].test(v)) {
66 | return returnMessage(false, 'notMatch');
67 | }
68 | if (type === 'list') {
69 | return returnMessage(this.values().includes(v), 'notIn');
70 | }
71 | if (operator) {
72 | const v1 = this.parseValue(v);
73 | if (operator === 'be') {
74 | const [min, max] = value;
75 | return returnMessage(
76 | v1 >= this.parseValue(min) && v1 <= this.parseValue(max),
77 | 'between',
78 | min,
79 | max,
80 | );
81 | }
82 | if (operator === 'nbe') {
83 | const [min, max] = value;
84 | return returnMessage(
85 | v1 < this.parseValue(min) || v1 > this.parseValue(max),
86 | 'notBetween',
87 | min,
88 | max,
89 | );
90 | }
91 | if (operator === 'eq') {
92 | return returnMessage(
93 | v1 === this.parseValue(value),
94 | 'equal',
95 | value,
96 | );
97 | }
98 | if (operator === 'neq') {
99 | return returnMessage(
100 | v1 !== this.parseValue(value),
101 | 'notEqual',
102 | value,
103 | );
104 | }
105 | if (operator === 'lt') {
106 | return returnMessage(
107 | v1 < this.parseValue(value),
108 | 'lessThan',
109 | value,
110 | );
111 | }
112 | if (operator === 'lte') {
113 | return returnMessage(
114 | v1 <= this.parseValue(value),
115 | 'lessThanEqual',
116 | value,
117 | );
118 | }
119 | if (operator === 'gt') {
120 | return returnMessage(
121 | v1 > this.parseValue(value),
122 | 'greaterThan',
123 | value,
124 | );
125 | }
126 | if (operator === 'gte') {
127 | return returnMessage(
128 | v1 >= this.parseValue(value),
129 | 'greaterThanEqual',
130 | value,
131 | );
132 | }
133 | }
134 | return [true];
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /* global window, document */
2 | import { h } from './component/element';
3 | import DataProxy from './core/data_proxy';
4 | import Sheet from './component/sheet';
5 | import { cssPrefix } from './config';
6 | import { locale } from './locale/locale';
7 | import './index.less';
8 |
9 |
10 | class Spreadsheet {
11 | constructor(selectors, options = {}) {
12 | let targetEl = selectors;
13 | if (typeof selectors === 'string') {
14 | targetEl = document.querySelector(selectors);
15 | }
16 | this.data = new DataProxy('sheet1', options);
17 | const rootEl = h('div', `${cssPrefix}`)
18 | .on('contextmenu', evt => evt.preventDefault());
19 | // create canvas element
20 | targetEl.appendChild(rootEl.el);
21 | this.sheet = new Sheet(rootEl, this.data);
22 | }
23 |
24 | loadData(data) {
25 | this.sheet.loadData(data);
26 | return this;
27 | }
28 |
29 | getData() {
30 | return this.data.getData();
31 | }
32 |
33 | validate() {
34 | const { validations } = this.data;
35 | return validations.errors.size <= 0;
36 | }
37 |
38 | change(cb) {
39 | this.data.change = cb;
40 | return this;
41 | }
42 |
43 | static locale(lang, message) {
44 | locale(lang, message);
45 | }
46 | }
47 |
48 | const spreadsheet = (el, options = {}) => new Spreadsheet(el, options);
49 |
50 | if (window) {
51 | window.x = window.x || {};
52 | window.x.spreadsheet = spreadsheet;
53 | window.x.spreadsheet.locale = (lang, message) => locale(lang, message);
54 | }
55 |
56 | export default Spreadsheet;
57 | export {
58 | spreadsheet,
59 | };
60 |
--------------------------------------------------------------------------------
/src/locale/de.js:
--------------------------------------------------------------------------------
1 | export default {
2 | toolbar: {
3 | undo: 'Rückgängig machen',
4 | redo: 'Wiederherstellen',
5 | paintformat: 'Format kopieren/einfügen',
6 | clearformat: 'Format löschen',
7 | format: 'Format',
8 | font: 'Schriftart',
9 | fontSize: 'Schriftgrad',
10 | fontBold: 'Fett',
11 | fontItalic: 'Kursiv',
12 | underline: 'Betonen',
13 | strike: 'Streichen',
14 | textColor: 'Text Farbe',
15 | fillColor: 'Füllung Farbe',
16 | border: 'Umrandung',
17 | merge: 'Zellen verbinden',
18 | align: 'Waagrechte Ausrichtung',
19 | valign: 'Vertikale uitlijning',
20 | textwrap: 'Textumbruch',
21 | freeze: 'Zelle sperren',
22 | formula: 'Funktionen',
23 | more: 'Mehr',
24 | },
25 | contextmenu: {
26 | copy: 'Kopieren',
27 | cut: 'Ausschneiden',
28 | paste: 'Einfügen',
29 | pasteValue: 'Nur Werte einfügen',
30 | pasteFormat: 'Nur Format einfügen',
31 | insertRow: 'Zeile einfügen',
32 | insertColumn: 'Spalte einfügen',
33 | deleteRow: 'Zeile löschen',
34 | deleteColumn: 'Spalte löschen',
35 | deleteCell: 'Zelle löschen',
36 | deleteCellText: 'Zellentext löschen',
37 | },
38 | format: {
39 | normal: 'Regulär',
40 | text: 'Text',
41 | number: 'Nummer',
42 | percent: 'Prozent',
43 | rmb: 'RMB',
44 | usd: 'USD',
45 | date: 'Datum',
46 | time: 'Termin',
47 | datetime: 'Datum Termin',
48 | duration: 'Dauer',
49 | },
50 | formula: {
51 | sum: 'Summe',
52 | average: 'Durchschnittliche',
53 | max: 'Max',
54 | min: 'Min',
55 | concat: 'Concat',
56 | },
57 | };
58 |
--------------------------------------------------------------------------------
/src/locale/en.js:
--------------------------------------------------------------------------------
1 | export default {
2 | toolbar: {
3 | undo: 'Undo',
4 | redo: 'Redo',
5 | paintformat: 'Paint format',
6 | clearformat: 'Clear format',
7 | format: 'Format',
8 | fontName: 'Font',
9 | fontSize: 'Font size',
10 | fontBold: 'Font bold',
11 | fontItalic: 'Font italic',
12 | underline: 'Underline',
13 | strike: 'Strike',
14 | color: 'Text color',
15 | bgcolor: 'Fill color',
16 | border: 'Borders',
17 | merge: 'Merge cells',
18 | align: 'Horizontal align',
19 | valign: 'Vertical align',
20 | textwrap: 'Text wrapping',
21 | freeze: 'Freeze cell',
22 | autofilter: 'Filter',
23 | formula: 'Functions',
24 | more: 'More',
25 | },
26 | contextmenu: {
27 | copy: 'Copy',
28 | cut: 'Cut',
29 | paste: 'Paste',
30 | pasteValue: 'Paste values only',
31 | pasteFormat: 'Paste format only',
32 | insertRow: 'Insert row',
33 | insertColumn: 'Insert column',
34 | deleteRow: 'Delete row',
35 | deleteColumn: 'Delete column',
36 | deleteCell: 'Delete cell',
37 | deleteCellText: 'Delete cell text',
38 | validation: 'Data validations',
39 | cellprintable : 'Enable export',
40 | cellnonprintable :'Disable export',
41 | celleditable : 'Enable editing',
42 | cellnoneditable :'Disable editing',
43 | },
44 | format: {
45 | normal: 'Normal',
46 | text: 'Plain Text',
47 | number: 'Number',
48 | percent: 'Percent',
49 | rmb: 'RMB',
50 | usd: 'USD',
51 | eur: 'EUR',
52 | date: 'Date',
53 | time: 'Time',
54 | datetime: 'Date time',
55 | duration: 'Duration',
56 | },
57 | formula: {
58 | sum: 'Sum',
59 | average: 'Average',
60 | max: 'Max',
61 | min: 'Min',
62 | _if: 'IF',
63 | and: 'AND',
64 | or: 'OR',
65 | concat: 'Concat',
66 | },
67 | validation: {
68 | required: 'it must be required',
69 | notMatch: 'it not match its validation rule',
70 | between: 'it is between {} and {}',
71 | notBetween: 'it is not between {} and {}',
72 | notIn: 'it is not in list',
73 | equal: 'it equal to {}',
74 | notEqual: 'it not equal to {}',
75 | lessThan: 'it less than {}',
76 | lessThanEqual: 'it less than or equal to {}',
77 | greaterThan: 'it greater than {}',
78 | greaterThanEqual: 'it greater than or equal to {}',
79 | },
80 | error: {
81 | pasteForMergedCell: 'Unable to do this for merged cells',
82 | },
83 | calendar: {
84 | weeks: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
85 | months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
86 | },
87 | button: {
88 | cancel: 'Cancel',
89 | remove: 'Remove',
90 | save: 'Save',
91 | ok: 'OK',
92 | },
93 | sort: {
94 | desc: 'Sort Z -> A',
95 | asc: 'Sort A -> Z',
96 | },
97 | filter: {
98 | empty: 'empty',
99 | },
100 | dataValidation: {
101 | mode: 'Mode',
102 | range: 'Cell Range',
103 | criteria: 'Criteria',
104 | modeType: {
105 | cell: 'Cell',
106 | column: 'Colun',
107 | row: 'Row',
108 | },
109 | type: {
110 | list: 'List',
111 | number: 'Number',
112 | date: 'Date',
113 | phone: 'Phone',
114 | email: 'Email',
115 | },
116 | operator: {
117 | be: 'between',
118 | nbe: 'not betwwen',
119 | lt: 'less than',
120 | lte: 'less than or equal to',
121 | gt: 'greater than',
122 | gte: 'greater than or equal to',
123 | eq: 'equal to',
124 | neq: 'not equal to',
125 | },
126 | },
127 | };
128 |
--------------------------------------------------------------------------------
/src/locale/locale.js:
--------------------------------------------------------------------------------
1 | /* global window */
2 | import en from './en';
3 |
4 | let $lang = 'en';
5 | const $messages = {
6 | en,
7 | };
8 |
9 | function translate(key, messages) {
10 | if (messages && messages[$lang]) {
11 | let message = messages[$lang];
12 | const keys = key.split('.');
13 | for (let i = 0; i < keys.length; i += 1) {
14 | const property = keys[i];
15 | const value = message[property];
16 | if (i === keys.length - 1) return value;
17 | if (!value) return undefined;
18 | message = value;
19 | }
20 | }
21 | return undefined;
22 | }
23 |
24 | function t(key) {
25 | let v = translate(key, $messages);
26 | if (!v && window && window.x && window.x.spreadsheet && window.x.spreadsheet.$messages) {
27 | v = translate(key, window.x.spreadsheet.$messages);
28 | }
29 | return v || '';
30 | }
31 |
32 | function tf(key) {
33 | return () => t(key);
34 | }
35 |
36 | function locale(lang, message) {
37 | $lang = lang;
38 | if (message) {
39 | $messages[lang] = message;
40 | }
41 | }
42 |
43 | export default {
44 | t,
45 | };
46 |
47 | export {
48 | locale,
49 | t,
50 | tf,
51 | };
52 |
--------------------------------------------------------------------------------
/src/locale/nl.js:
--------------------------------------------------------------------------------
1 | export default {
2 | toolbar: {
3 | undo: 'Ongedaan maken',
4 | redo: 'Opnieuw uitvoeren',
5 | paintformat: 'Opmaak kopiëren/plakken',
6 | clearformat: 'Opmaak wissen',
7 | format: 'Opmaak',
8 | font: 'Lettertype',
9 | fontSize: 'Tekengrootte',
10 | fontBold: 'Vet',
11 | fontItalic: 'Cursief',
12 | underline: 'Onderstrepen',
13 | strike: 'Doorstrepen',
14 | textColor: 'Tekstkleur',
15 | fillColor: 'Opvulkleur',
16 | border: 'Randen',
17 | merge: 'Cellen samenvoegen',
18 | align: 'Horizontale uitlijning',
19 | valign: 'Verticale uitlijning',
20 | textwrap: 'Terugloop',
21 | freeze: 'Cel bevriezen',
22 | formula: 'Functies',
23 | more: 'Meer',
24 | },
25 | contextmenu: {
26 | copy: 'Kopiëren',
27 | cut: 'Knippen',
28 | paste: 'Plakken',
29 | pasteValue: 'Alleen waarden plakken',
30 | pasteFormat: 'Alleen opmaak plakken',
31 | insertRow: 'Rij invoegen',
32 | insertColumn: 'Kolom invoegen',
33 | deleteRow: 'Rij verwijderen',
34 | deleteColumn: 'Kolom verwijderen',
35 | deleteCell: 'Cel verwijderen',
36 | deleteCellText: 'Celtekst verwijderen',
37 | },
38 | format: {
39 | normal: 'Standaard',
40 | text: 'Tekst',
41 | number: 'Nummer',
42 | percent: 'Percentage',
43 | rmb: 'RMB',
44 | usd: 'USD',
45 | date: 'Datum',
46 | time: 'Tijdstip',
47 | datetime: 'Datum tijd',
48 | duration: 'Duratie',
49 | },
50 | formula: {
51 | sum: 'Som',
52 | average: 'Gemiddelde',
53 | max: 'Max',
54 | min: 'Min',
55 | concat: 'Concat',
56 | },
57 | };
58 |
--------------------------------------------------------------------------------
/src/locale/zh-cn.js:
--------------------------------------------------------------------------------
1 | export default {
2 | toolbar: {
3 | undo: '撤销',
4 | redo: '恢复',
5 | paintformat: '格式刷',
6 | clearformat: '清除格式',
7 | format: '数据格式',
8 | fontName: '字体',
9 | fontSize: '字号',
10 | fontBold: '加粗',
11 | fontItalic: '倾斜',
12 | underline: '下划线',
13 | strike: '删除线',
14 | color: '字体颜色',
15 | bgcolor: '填充颜色',
16 | border: '边框',
17 | merge: '合并单元格',
18 | align: '水平对齐',
19 | valign: '垂直对齐',
20 | textwrap: '自动换行',
21 | freeze: '冻结',
22 | autofilter: '自动筛选',
23 | formula: '函数',
24 | more: '更多',
25 | },
26 | contextmenu: {
27 | copy: '复制',
28 | cut: '剪切',
29 | paste: '粘贴',
30 | pasteValue: '粘贴数据',
31 | pasteFormat: '粘贴格式',
32 | insertRow: '插入行',
33 | insertColumn: '插入列',
34 | deleteRow: '删除行',
35 | deleteColumn: '删除列',
36 | deleteCell: '删除',
37 | deleteCellText: '删除数据',
38 | validation: '数据验证',
39 | },
40 | format: {
41 | normal: '正常',
42 | text: '文本',
43 | number: '数值',
44 | percent: '百分比',
45 | rmb: '人民币',
46 | usd: '美元',
47 | date: '短日期',
48 | time: '时间',
49 | datetime: '长日期',
50 | duration: '持续时间',
51 | },
52 | formula: {
53 | sum: '求和',
54 | average: '求平均值',
55 | max: '求最大值',
56 | min: '求最小值',
57 | concat: '字符拼接',
58 | _if: '条件判断',
59 | and: '和',
60 | or: '或',
61 | },
62 | validation: {
63 | required: '此值必填',
64 | notMatch: '此值不匹配验证规则',
65 | between: '此值应在 {} 和 {} 之间',
66 | notBetween: '此值不应在 {} 和 {} 之间',
67 | notIn: '此值不在列表中',
68 | equal: '此值应该等于 {}',
69 | notEqual: '此值不应该等于 {}',
70 | lessThan: '此值应该小于 {}',
71 | lessThanEqual: '此值应该小于等于 {}',
72 | greaterThan: '此值应该大于 {}',
73 | greaterThanEqual: '此值应该大于等于 {}',
74 | },
75 | error: {
76 | pasteForMergedCell: '无法对合并的单元格执行此操作',
77 | },
78 | calendar: {
79 | weeks: ['日', '一', '二', '三', '四', '五', '六'],
80 | months: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
81 | },
82 | button: {
83 | cancel: '取消',
84 | remove: '删除',
85 | save: '保存',
86 | ok: '确认',
87 | },
88 | sort: {
89 | desc: '降序',
90 | asc: '升序',
91 | },
92 | filter: {
93 | empty: '空白',
94 | },
95 | dataValidation: {
96 | mode: '模式',
97 | range: '单元区间',
98 | criteria: '条件',
99 | modeType: {
100 | cell: '单元格',
101 | column: '列模式',
102 | row: '行模式',
103 | },
104 | type: {
105 | list: '列表',
106 | number: '数字',
107 | date: '日期',
108 | phone: '手机号',
109 | email: '电子邮件',
110 | },
111 | operator: {
112 | be: '在区间',
113 | nbe: '不在区间',
114 | lt: '小于',
115 | lte: '小于等于',
116 | gt: '大于',
117 | gte: '大于等于',
118 | eq: '等于',
119 | neq: '不等于',
120 | },
121 | },
122 | };
123 |
--------------------------------------------------------------------------------
/test/core/alphabet_test.js:
--------------------------------------------------------------------------------
1 | // const = require('../../src/data/);
2 | import assert from 'assert';
3 | import { describe, it } from 'mocha';
4 | import {
5 | indexAt,
6 | stringAt,
7 | expr2xy,
8 | expr2expr,
9 | } from '../../src/core/alphabet';
10 |
11 | describe('alphabet', () => {
12 | describe('.indexAt()', () => {
13 | it('should return 0 when the value is A', () => {
14 | assert.equal(indexAt('A'), 0);
15 | });
16 | it('should return 25 when the value is Z', () => {
17 | assert.equal(indexAt('Z'), 25);
18 | });
19 | it('should return 26 when the value is AA', () => {
20 | assert.equal(indexAt('AA'), 26);
21 | });
22 | it('should return 52 when the value is BA', () => {
23 | assert.equal(indexAt('BA'), 52);
24 | });
25 | it('should return 54 when the value is BC', () => {
26 | assert.equal(indexAt('BC'), 54);
27 | });
28 | it('should return 78 when the value is CA', () => {
29 | assert.equal(indexAt('CA'), 78);
30 | });
31 | it('should return 26 * 26 when the value is ZA', () => {
32 | assert.equal(indexAt('ZA'), 26 * 26);
33 | });
34 | it('should return 26 * 26 + 26 when the value is AAA', () => {
35 | assert.equal(indexAt('AAA'), (26 * 26) + 26);
36 | });
37 | });
38 | describe('.stringAt()', () => {
39 | it('should return A when the value is 0', () => {
40 | assert.equal(stringAt(0), 'A');
41 | });
42 | it('should return Z when the value is 25', () => {
43 | assert.equal(stringAt(25), 'Z');
44 | });
45 | it('should return AA when the value is 26', () => {
46 | assert.equal(stringAt(26), 'AA');
47 | });
48 | it('should return BC when the value is 54', () => {
49 | assert.equal(stringAt(54), 'BC');
50 | });
51 | it('should return CB when the value is 78', () => {
52 | assert.equal(stringAt(78), 'CA');
53 | });
54 | it('should return ZA when the value is 26 * 26', () => {
55 | assert.equal(stringAt(26 * 26), 'ZA');
56 | });
57 | it('should return Z when the value is 26 * 26 + 1', () => {
58 | assert.equal(stringAt((26 * 26) + 1), 'ZB');
59 | });
60 | it('should return AAA when the value is 26 * 26 + 26', () => {
61 | assert.equal(stringAt((26 * 26) + 26), 'AAA');
62 | });
63 | });
64 | describe('.expr2xy()', () => {
65 | it('should return 0 when the value is A1', () => {
66 | assert.equal(expr2xy('A1')[0], 0);
67 | assert.equal(expr2xy('A1')[1], 0);
68 | });
69 | });
70 | describe('.expr2expr()', () => {
71 | it('should return B2 when the value is A1, 1, 1', () => {
72 | assert.equal(expr2expr('A1', 1, 1), 'B2');
73 | });
74 | it('should return C4 when the value is A1, 2, 3', () => {
75 | assert.equal(expr2expr('A1', 2, 3), 'C4');
76 | });
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/test/core/cell_test.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import { describe, it } from 'mocha';
3 | import cell, { infixExprToSuffixExpr } from '../../src/core/cell';
4 | import { formulam } from '../../src/core/formula';
5 |
6 | describe('infixExprToSuffixExpr', () => {
7 | it('should return myname:A1 score:50 when the value is CONCAT("my name:", A1, " score:", 50)', () => {
8 | assert.equal(infixExprToSuffixExpr('CONCAT("my name:", A1, " score:", 50)').join(''), '"my name:A1" score:50CONCAT,4');
9 | });
10 | it('should return A1B2SUM,2C1C5AVERAGE,350B20++ when the value is AVERAGE(SUM(A1,B2), C1, C5) + 50 + B20', () => {
11 | assert.equal(infixExprToSuffixExpr('AVERAGE(SUM(A1,B2), C1, C5) + 50 + B20').join(''), 'A1B2SUM,2C1C5AVERAGE,350+B20+');
12 | });
13 | it('should return A1B2B3SUM,3C1C5AVERAGE,350+B20+ when the value is ((AVERAGE(SUM(A1,B2, B3), C1, C5) + 50) + B20)', () => {
14 | assert.equal(infixExprToSuffixExpr('((AVERAGE(SUM(A1,B2, B3), C1, C5) + 50) + B20)').join(''), 'A1B2B3SUM,3C1C5AVERAGE,350+B20+');
15 | });
16 | it('should return 11==tfIF,3 when the value is IF(1==1, "t", "f")', () => {
17 | assert.equal(infixExprToSuffixExpr('IF(1==1, "t", "f")').join(''), '11=="t"fIF,3');
18 | });
19 | it('should return 11=tfIF,3 when the value is IF(1=1, "t", "f")', () => {
20 | assert.equal(infixExprToSuffixExpr('IF(1=1, "t", "f")').join(''), '11="t"fIF,3');
21 | });
22 | it('should return 21>21IF,3 when the value is IF(2>1, 2, 1)', () => {
23 | assert.equal(infixExprToSuffixExpr('IF(2>1, 2, 1)').join(''), '21>21IF,3');
24 | });
25 | it('should return 11=AND,121IF,3 when the value is IF(AND(1=1), 2, 1)', () => {
26 | assert.equal(infixExprToSuffixExpr('IF(AND(1=1), 2, 1)').join(''), '11=AND,121IF,3');
27 | });
28 | it('should return 11=21>AND,221IF,3 when the value is IF(AND(1=1, 2>1), 2, 1)', () => {
29 | assert.equal(infixExprToSuffixExpr('IF(AND(1=1, 2>1), 2, 1)').join(''), '11=21>AND,221IF,3');
30 | });
31 | it('should return 105-20- when the value is 10-5-20', () => {
32 | assert.equal(infixExprToSuffixExpr('10-5-20').join(''), '105-20-');
33 | });
34 | it('should return 105-2010*- when the value is 10-5-20*10', () => {
35 | assert.equal(infixExprToSuffixExpr('10-5-20*10').join(''), '105-2010*-');
36 | });
37 | it('should return 10520*- when the value is 10-5*20', () => {
38 | assert.equal(infixExprToSuffixExpr('10-5*20').join(''), '10520*-');
39 | });
40 | it('should return 105-20+ when the value is 10-5+20', () => {
41 | assert.equal(infixExprToSuffixExpr('10-5+20').join(''), '105-20+');
42 | });
43 | it('should return 123*+45*6+7*+ when the value is 1 + 2*3 + (4 * 5 + 6) * 7', () => {
44 | assert.equal(infixExprToSuffixExpr('1+2*3+(4*5+6)*7').join(''), '123*+45*6+7*+');
45 | });
46 | it('should return 9312*-3*+42/+ when the value is 9+(3-1*2)*3+4/2', () => {
47 | assert.equal(infixExprToSuffixExpr('9+(3-1*2)*3+4/2').join(''), '9312*-3*+42/+');
48 | });
49 | it('should return 931-+23+*42/+ when the value is (9+(3-1))*(2+3)+4/2', () => {
50 | assert.equal(infixExprToSuffixExpr('(9+(3-1))*(2+3)+4/2').join(''), '931-+23+*42/+');
51 | });
52 | it('should return SUM(1) when the value is 1SUM,1', () => {
53 | assert.equal(infixExprToSuffixExpr('SUM(1)').join(''), '1SUM');
54 | });
55 | it('should return SUM() when the value is ""', () => {
56 | assert.equal(infixExprToSuffixExpr('SUM()').join(''), 'SUM');
57 | });
58 | it('should return SUM( when the value is SUM', () => {
59 | assert.equal(infixExprToSuffixExpr('SUM(').join(''), 'SUM');
60 | });
61 | });
62 |
63 | describe('cell', () => {
64 | describe('.render()', () => {
65 | it('should return 0 + 2 + 2 + 6 + 49 + 20 when the value is =SUM(A1,B2, C1, C5) + 50 + B20', () => {
66 | assert.equal(cell.render('=SUM(A1,B2, C1, C5) + 50 + B20', formulam, (x, y) => x + y), 0 + 2 + 2 + 6 + 50 + 20);
67 | });
68 | it('should return 50 + 20 when the value is =50 + B20', () => {
69 | assert.equal(cell.render('=50 + B20', formulam, (x, y) => x + y), 50 + 20);
70 | });
71 | it('should return 2 when the value is =IF(2>1, 2, 1)', () => {
72 | assert.equal(cell.render('=IF(2>1, 2, 1)', formulam, (x, y) => x + y), 2);
73 | });
74 | it('should return 1 + 500 - 20 when the value is =AVERAGE(A1:A3) + 50 * 10 - B20', () => {
75 | assert.equal(cell.render('=AVERAGE(A1:A3) + 50 * 10 - B20', formulam, (x, y) => {
76 | // console.log('x:', x, ', y:', y);
77 | return x + y;
78 | }), 1 + 500 - 20);
79 | });
80 | });
81 | });
82 |
--------------------------------------------------------------------------------
/test/core/font_test.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import { describe, it } from 'mocha';
3 | import {
4 | fontSizes,
5 | fonts,
6 | baseFonts,
7 | getFontSizePxByPt,
8 | } from '../../src/core/font';
9 |
10 | describe('baseFonts', () => {
11 | it('should be Array of "{ key: string, key: string }"', () => {
12 | const result = baseFonts.find((i) => {
13 | const keyType = typeof i.key;
14 | const titleType = typeof i.title;
15 | return keyType !== 'string' || titleType !== 'string';
16 | });
17 | assert.equal(result, undefined);
18 | });
19 | });
20 |
21 | describe('fontSizes', () => {
22 | it('should be Array of "{ pt: number, px: number }"', () => {
23 | const result = fontSizes.find((i) => {
24 | const ptType = typeof i.pt;
25 | const pxType = typeof i.px;
26 | return ptType !== 'number' || pxType !== 'number';
27 | });
28 | assert.equal(result, undefined);
29 | });
30 | });
31 |
32 | describe('getFontSizePxByPt()', () => {
33 | const fontsizeItem = { pt: 7.5, px: 10 };
34 | // not include pt
35 | const notIncludePT = 6.5;
36 |
37 | it(`should be return ${fontsizeItem.px} when the value is ${fontsizeItem.pt}`, () => {
38 | assert.equal(getFontSizePxByPt(fontsizeItem.pt), fontsizeItem.px);
39 | });
40 | it(`should be return ${notIncludePT} when the value is ${notIncludePT} (same as input arg)`, () => {
41 | assert.equal(getFontSizePxByPt(notIncludePT), notIncludePT);
42 | });
43 | });
44 |
45 | describe('fonts()', () => {
46 | const fontItem = baseFonts[0];
47 | it(`should include { ${fontItem.key}: ${JSON.stringify(fontItem)} } when the value is not provide.`, () => {
48 | const f = fonts();
49 | assert.equal(f[fontItem.key], fontItem);
50 | });
51 |
52 | /** @type {BaseFont} */
53 | const appendItem = [{
54 | key: 'test',
55 | title: 'test title',
56 | }];
57 | const appendItems = [appendItem];
58 | it(`should include { ${appendItems[0].key}: ${JSON.stringify(appendItems[0])} } when the value is ${JSON.stringify(appendItems)}`, () => {
59 | const f = fonts(appendItems);
60 | assert.equal(f[appendItem.key], appendItem);
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/test/core/format_test.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import { describe, it } from 'mocha';
3 | import {
4 | formatm,
5 | baseFormats,
6 | } from '../../src/core/format';
7 |
8 | const gformats = formatm;
9 | describe('formatm', () => {
10 | describe('#render()', () => {
11 | it('normal: should return AC when the value is AC', () => {
12 | assert.equal(gformats.normal.render('AC'), 'AC');
13 | });
14 | it('text: should return abc when the value is abc', () => {
15 | assert.equal(gformats.text.render('abc'), 'abc');
16 | });
17 | it('number: should return 11,000.20 when the value is 11000.20', () => {
18 | assert.equal(gformats.number.render('11000.20'), '11,000.20');
19 | });
20 | it('number: should return 110,00.20 (NOT MODIFIED when encounter ileagal input) when the value is 110,00.20', () => {
21 | assert.equal(gformats.number.render('110,00.20'), '110,00.20');
22 | });
23 | it('percent: should return 50.456% when the value is 50.456', () => {
24 | assert.equal(gformats.percent.render('50.456'), '50.456%');
25 | });
26 | it('RMB: should return ¥1,200.33 when the value is 1200.333', () => {
27 | assert.equal(gformats.rmb.render('1200.333'), '¥1,200.33');
28 | });
29 | it('USD: should return $1,200.33 when the value is 1200.333', () => {
30 | assert.equal(gformats.usd.render('1200.333'), '$1,200.33');
31 | });
32 | it('EUR: should return €1,200.33 when the value is 1200.333', () => {
33 | assert.equal(gformats.eur.render('1200.333'), '€1,200.33');
34 | });
35 | });
36 | });
37 |
38 | describe('baseFormats', () => {
39 | // item.key
40 | it('typeof item.key should be "string"',
41 | () => {
42 | const KEY = 'key';
43 | assert.equal(baseFormats.find(i => typeof i[KEY] !== 'string'), undefined);
44 | });
45 | // item.title
46 | it('typeof item.title should be "function"',
47 | () => {
48 | const KEY = 'title';
49 | assert.equal(baseFormats.find(i => typeof i[KEY] !== 'function'), undefined);
50 | });
51 | // item.type
52 | it('typeof item.type should be "string"',
53 | () => {
54 | const KEY = 'type';
55 | assert.equal(baseFormats.find(i => typeof i[KEY] !== 'string'), undefined);
56 | });
57 | // item.render
58 | it('typeof item.render should be "function"',
59 | () => {
60 | const KEY = 'render';
61 | assert.equal(baseFormats.find(i => typeof i[KEY] !== 'function'), undefined);
62 | });
63 | // item.label
64 | it('typeof item.label should be "string" or "undefined"',
65 | () => {
66 | const KEY = 'label';
67 | assert.equal(baseFormats.find(i => typeof i[KEY] !== 'string' && typeof i[KEY] !== 'undefined'), undefined);
68 | });
69 | });
70 |
--------------------------------------------------------------------------------
/test/core/formula_test.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import { describe, it } from 'mocha';
3 | import { formulam } from '../../src/core/formula';
4 |
5 | const gformulas = formulam;
6 | describe('formula', () => {
7 | describe('#render()', () => {
8 | it('SUM: should return 36 when the value is [\'12\', \'12\', 12]', () => {
9 | assert.equal(gformulas.SUM.render(['12', '12', 12]), 36);
10 | });
11 | it('AVERAGE: should return 13 when the value is [\'12\', \'13\', 14]', () => {
12 | assert.equal(gformulas.AVERAGE.render(['12', '13', 14]), 13);
13 | });
14 | it('MAX: should return 14 when the value is [\'12\', \'13\', 14]', () => {
15 | assert.equal(gformulas.MAX.render(['12', '13', 14]), 14);
16 | });
17 | it('MIN: should return 12 when the value is [\'12\', \'13\', 14]', () => {
18 | assert.equal(gformulas.MIN.render(['12', '13', 14]), 12);
19 | });
20 | it('IF: should return 12 when the value is [12 > 11, 12, 11]', () => {
21 | assert.equal(gformulas.IF.render([12 > 11, 12, 11]), 12);
22 | });
23 | it('AND: should return true when the value is ["a", true, "ok"]', () => {
24 | assert.equal(gformulas.AND.render(['a', true, 'ok']), true);
25 | });
26 | it('AND: should return false when the value is ["a", false, "ok"]', () => {
27 | assert.equal(gformulas.AND.render(['a', false, 'ok']), false);
28 | });
29 | it('OR: should return true when the value is ["a", true]', () => {
30 | assert.equal(gformulas.OR.render(['a', true]), true);
31 | });
32 | it('OR: should return true when the value is ["a", false]', () => {
33 | assert.equal(gformulas.OR.render(['a', false]), true);
34 | });
35 | it('OR: should return false when the value is [0, false]', () => {
36 | assert.equal(gformulas.OR.render([0, false]), false);
37 | });
38 | it('CONCAT: should return 1200USD when the value is [\'1200\', \'USD\']', () => {
39 | assert.equal(gformulas.CONCAT.render(['1200', 'USD']), '1200USD');
40 | });
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/test/helper_test.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import { describe, it } from 'mocha';
3 | import helper from '../src/core/helper';
4 |
5 | describe('helper', () => {
6 | describe('.cloneDeep()', () => {
7 | it('The modification of the returned value does not affect the original value', () => {
8 | const obj = { k: { k1: 'v' } };
9 | const obj1 = helper.cloneDeep(obj);
10 | obj1.k.k1 = 'v1';
11 | assert.equal(obj.k.k1, 'v');
12 | });
13 | });
14 | describe('.merge()', () => {
15 | it('should return { a: \'a\' } where the value is { a: \'a\' }', () => {
16 | const merge = helper.merge({ a: 'a' });
17 | assert.equal(merge.a, 'a');
18 | });
19 | it('should return {a: \'a\', b: \'b\'} where the value is {a: \'a\'}, {b: \'b\'}', () => {
20 | const merge = helper.merge({ a: 'a' }, { b: 'b' });
21 | assert.equal(merge.a, 'a');
22 | assert.equal(merge.b, 'b');
23 | });
24 | it('should return { a: { a1: \'a2\' }, b: \'b\' } where the value is {a: {a1: \'a1\'}, b: \'b\'}, {a: {a1: \'b\'}}', () => {
25 | const obj = { a: { a1: 'a1' }, b: 'b' };
26 | const merge = helper.merge(obj, { a: { a1: 'a2' } });
27 | assert.equal(obj.a.a1, 'a1');
28 | assert.equal(merge.a.a1, 'a2');
29 | assert.equal(merge.b, 'b');
30 | });
31 | });
32 | // sum
33 | describe('.sum()', () => {
34 | it('should return [50, 3] where the value is [10, 20, 20]', () => {
35 | const [total, size] = helper.sum([10, 20, 20]);
36 | assert.equal(total, 50);
37 | assert.equal(size, 3);
38 | });
39 | it('should return [50, 3] where the value is {k1: 10, k2: 20, k3: 20}', () => {
40 | const [total, size] = helper.sum({ k1: 10, k2: 20, k3: 20 });
41 | assert.equal(total, 50);
42 | assert.equal(size, 3);
43 | });
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/test/index_test.js:
--------------------------------------------------------------------------------
1 | // import assert from 'assert';
2 | // import { describe, it } from 'mocha';
3 | // import alphabet from '../../src/index';
4 | //
5 | //
6 |
--------------------------------------------------------------------------------