├── .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 | [![npm package](https://img.shields.io/npm/v/x-data-spreadsheet.svg)](https://www.npmjs.org/package/x-data-spreadsheet) 4 | [![NPM downloads](http://img.shields.io/npm/dm/x-data-spreadsheet.svg)](https://npmjs.org/package/x-data-spreadsheet) 5 | [![NPM downloads](http://img.shields.io/npm/dt/x-data-spreadsheet.svg)](https://npmjs.org/package/x-data-spreadsheet) 6 | [![Build passing](https://travis-ci.org/myliang/x-spreadsheet.svg?branch=master)](https://travis-ci.org/myliang/x-spreadsheet) 7 | [![codecov](https://codecov.io/gh/myliang/x-spreadsheet/branch/master/graph/badge.svg)](https://codecov.io/gh/myliang/x-spreadsheet) 8 | ![GitHub](https://img.shields.io/github/license/myliang/x-spreadsheet.svg) 9 | ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/myliang/x-spreadsheet.svg) 10 | [![Join the chat at https://gitter.im/x-datav/spreadsheet](https://badges.gitter.im/x-datav/spreadsheet.svg)](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 | --------------------------------------------------------------------------------