├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE │ ├── --all-other-issues.md │ ├── --bug-report.md │ └── --feature-request.md ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── babel.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── App.vue ├── components │ ├── TBody │ │ ├── TBody.js │ │ ├── TBody.scss │ │ ├── TBody.vue │ │ └── VSelect.vue │ ├── Thead.vue │ └── VueTable.vue ├── data.js ├── helpers.js ├── index.js ├── main.js └── mixins │ └── VueTable │ ├── callback.js │ ├── copyPaste.js │ ├── dragToFill.js │ ├── handleTBody.js │ ├── handleTHead.js │ ├── moveOnTable.js │ ├── scrollOnTable.js │ └── undo.js ├── tests └── unit │ ├── .eslintrc.js │ ├── Tbody │ ├── Computed.spec.js │ ├── Data.spec.js │ └── Methods.spec.js │ ├── Thead │ ├── Computed.spec.js │ ├── Data.spec.js │ └── Methods.spec.js │ ├── VSelect │ ├── Data.spec.js │ └── Methods.spec.js │ └── VueTable │ ├── Computed.spec.js │ ├── Data.spec.js │ └── Methods.spec.js ├── types ├── components │ ├── TBody.d.ts │ ├── Thead.d.ts │ └── VueTable.d.ts └── index.d.ts └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | max_line_length = 100 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: ["plugin:vue/essential", "@vue/airbnb", "@vue/prettier"], 7 | parserOptions: { 8 | parser: "babel-eslint", 9 | }, 10 | rules: { 11 | "no-console": process.env.NODE_ENV === "production" ? "error" : "off", 12 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off", 13 | "padding-line-between-statements": [ 14 | "error", 15 | { blankLine: "always", prev: "*", next: "return" }, 16 | { blankLine: "always", prev: ["const", "let", "var"], next: "*" }, 17 | { 18 | blankLine: "never", 19 | prev: ["const", "let", "var"], 20 | next: ["const", "let", "var"], 21 | }, 22 | { blankLine: "always", prev: "*", next: "block-like" }, 23 | { blankLine: "always", prev: "block-like", next: "*" }, 24 | ], 25 | "vue/order-in-components": [ 26 | "error", 27 | { 28 | order: [ 29 | "el", 30 | "name", 31 | "parent", 32 | "functional", 33 | ["delimiters", "comments"], 34 | ["components", "directives", "filters"], 35 | "extends", 36 | "mixins", 37 | "inheritAttrs", 38 | "model", 39 | ["props", "propsData"], 40 | "data", 41 | "computed", 42 | "watch", 43 | "LIFECYCLE_HOOKS", 44 | "methods", 45 | "head", 46 | ["template", "render"], 47 | "renderError", 48 | ], 49 | }, 50 | ], 51 | }, 52 | overrides: [ 53 | { 54 | files: ["**/__tests__/*.{j,t}s?(x)", "**/tests/unit/**/*.spec.{j,t}s?(x)"], 55 | env: { 56 | jest: true, 57 | }, 58 | }, 59 | ], 60 | }; 61 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/--all-other-issues.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "❗️All other issues" 3 | about: Please create all other issues here. 4 | title: "[QUESTION]" 5 | labels: question 6 | assignees: joffreyBerrier 7 | 8 | --- 9 | 10 | **If you have a question. Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Additional context** 14 | Add any other context or screenshots about the question request here. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/--bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F6A8Bug report" 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: joffreyBerrier 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/--feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F64BFeature request" 3 | about: Suggest an idea for this project 4 | title: "[FEATURE_REQUEST]" 5 | labels: question 6 | assignees: joffreyBerrier 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | 4 | # local env files 5 | .env.local 6 | .env.*.local 7 | 8 | # Log files 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Editor directories and files 14 | .idea 15 | .vscode 16 | *.suo 17 | *.ntvs* 18 | *.njsproj 19 | *.sln 20 | *.sw* 21 | 22 | # Dist folder 23 | dist/ 24 | 25 | .env 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 14.15.4 4 | cache: 5 | yarn: true 6 | directories: 7 | - node_modules 8 | jobs: 9 | include: 10 | - stage: tdd 11 | script: 12 | - yarn build 13 | - yarn test:unit 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 GET SCALIA FRANCE 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue3 version 2 | https://github.com/joffreyBerrier/vue-datepicker 3 | 4 | # :fire: Vue Spreadsheet 2.2.1 :fire: 5 | https://github.com/joffreyBerrier/vue-spreadsheet/releases/tag/2.2.1 6 | 7 | # Medium article (in french) 8 | https://medium.com/scalia/vuejs-spreadsheet-692cab2cb5c8 9 | 10 | # Medium article for publish your own component on npm 11 | https://medium.com/js-dojo/how-to-publish-a-vuejs-component-on-npm-aa703714b512 12 | 13 | # Sandbox example 14 | 15 | *Open this link on a new tab* 16 | 17 | [![Edit vuejs-spreadsheet](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/vue-spreadsheet-wctsv?fontsize=14&hidenavigation=1&theme=dark&view=preview&file=/src/App.vue) 18 | 19 | 20 | ## Description 21 | 22 | :facepunch: An easier Spreadsheet in Vue.js :facepunch: 23 | 24 | Do not hesitate to :star: my repo 25 | 26 | 27 | ## Project setup 28 | 29 | ``` 30 | yarn add vuejs-spreadsheet 31 | 32 | npm i vuejs-spreadsheet 33 | ``` 34 | 35 | ``` 36 | 47 | ``` 48 | 49 | ## Contributing to development 50 | 51 | - First, fork the repo from github. 52 | - Clone your forked repo and run: `yarn` or `npm i` 53 | - You can use the `/example` folder to test out the component, or use `npm link` to another project (_cf. next sub section_). 54 | - Then, make your changes on any branch you want and push it. 55 | - Naming your branch with the gitflow convention: 56 | - Feature branches? [feature/] 57 | - Release branches? [release/] 58 | - Hotfix branches? [hotfix/] 59 | - Support branches? [support/] 60 | - Finally, open a pull request on the official repo, using the source branch from your forked repo. 61 | 62 | ### Debugging and testing from another project 63 | 64 | If you want to link the local project to another project 'B' with access to the sources, follow these intructions: 65 | - go to the root of this project's folder 66 | - update the package.json to point to the source entry point instead of the dist/ `main: 'src/index.js'` 67 | - run `npm link` (or `yarn link`). 68 | - go to the project you import the library 69 | - run `npm link vuejs-spreadsheet` 70 | - Now, in your `node_modules`, the vuejs-spreadsheet dependencies should be a symlink to this local folder! 71 | 72 | In order to make it work, you make change your webpack's configuration by using: 73 | ``` 74 | config: { 75 | resolve: { 76 | symlinks: true, 77 | } 78 | } 79 | ``` 80 | 81 | This will enable your project's B to compile this library using the babel / webpack configuration here, as if it was a real compiled _node_module_. 82 | 83 | _(This configuration may depend on your webpack builder)_ 84 | 85 | ## Wiki :mortar_board: 86 | 87 | Data binding | Type | Description 88 | ---------------------------------------|------------|------------------------- 89 | v-model | Array | That contains data 90 | 91 | Props | Type | Description 92 | ---------------------------------------|------------|------------------------- 93 | :headers | Array | That contains headers 94 | :custom-options | Object | That contains Options 95 | :style-wrap-vue-table | Object | That contains style of the wrapper tableVue 96 | :disable-cells | Array | That contains the headerKey you want to disable 97 | :disable-sort-thead | Array | That contains the disabled th 98 | :loading | Boolean | True => Hidden TbodyData / show slot loader | false => contrary 99 | :parent-scroll-element | Object | That contains the HTML attribute which overflow-y: scroll (by-default is 'html') 100 | :select-position | Object | That contains a top and left position you want to add to the select 101 | :submenu-tbody | Array | That contains the submenu-tbody 102 | :submenu-thead | Array | That contains the submenu-thead 103 | 104 | Options | Type | Description 105 | ---------------------------------------|------------|------------------------- 106 | :fuse-options | Object | That contains an object of fuse configuration look on her website: http://fusejs.io/ 107 | :new-data | Object | That contains the type of data when you have empty cell in a row 108 | :sort-header | Boolean | That activates sort button on header 109 | :tbody-index | Boolean | That displays the index of each row on the left of the table 110 | :trad | Object | That contains an object of translating 111 | 112 | Function | Type | Description 113 | ---------------------------------------|------------|------------------------- 114 | @tbody-all-checked-row | Function | Fired when the checkedAll row has checked 115 | @tbody-checked-row | Function | Fired when row has checked 116 | @tbody-change-data | Function | Fired when data undergo modifications 117 | @tbody-input-change | Function | When the **input changes** 118 | @tbody-input-keydown | Function | Trigger keydown when the **input changes** 119 | @tbody-select-change | Function | When the **select change** 120 | @handle-up-drag-size-header | Function | Fired when the header size changed 121 | @thead-td-sort | Function | When you press the button sort 122 | @tbody-undo-data | Function | When you hit Ctrl / Cmd + Z for undo 123 | @tbody-paste-data | Function | When you paste data to a cell 124 | @tbody-up-dragtofill | Function | Fired when pressed up on dragToFill 125 | @tbody-move-dragtofill | Function | Fired when moved on dragToFill 126 | @tbody-nav-backspace | Function | When you press backspace on cell (event, actualElement, actualCol, rowIndex, colIndex) 127 | @tbody-nav-multiple-backspace | Function | Fired when the multiple cell are delete 128 | @tbody-submenu-click-{#} | Function | {#} - Name of the function declared on **submenu-tbody** 129 | 130 | 131 | ### Example 132 | ``` javascript 133 | 154 | 155 | // if your want to add an specific header 156 |
157 | Specific Header 158 |
159 | 160 | // if your want to add a loader 161 |
162 | Loader 163 |
164 |
165 | ``` 166 | 167 | ### Options :honeybee: 168 | ``` 169 | customOptions: { 170 | dragToFill: true, 171 | tbodyCheckbox: false, 172 | tbodyIndex: true, 173 | sortHeader: true, 174 | trad: { 175 | lang: 'fr', 176 | en: { 177 | select: { 178 | placeholder: 'Search by typing', 179 | }, 180 | }, 181 | fr: { 182 | select: { 183 | placeholder: 'Taper pour chercher', 184 | }, 185 | }, 186 | }, 187 | newData: { 188 | type: 'input', 189 | value: '', 190 | active: false, 191 | style: { 192 | color: '#000', 193 | }, 194 | }, 195 | fuseOptions: { 196 | shouldSort: true, 197 | threshold: 0.2, 198 | location: 0, 199 | distance: 30, 200 | maxPatternLength: 64, 201 | minMatchCharLength: 1, 202 | findAllMatches: false, 203 | tokenize: false, 204 | keys: [ 205 | 'value', 206 | ], 207 | }, 208 | }, 209 | ``` 210 | 211 | ### Comment Box :triangular_ruler: 212 | 213 | If you want to use the commentBox (like excel) 214 | 215 | Create an object ``comment: {} `` on ``styleWrapVueTable`` and on each data 216 | 217 | #### :exclamation: You can choose a global BorderColor for each commentBox 218 | 219 | #### Example 220 | 221 | ``` 222 | styleWrapVueTable: { 223 | ... 224 | comment: { 225 | borderColor: '#696969', 226 | borderSize: '8px', 227 | widthBox: '120px', 228 | heightBox: '80px', 229 | }, 230 | }, 231 | ``` 232 | 233 | #### :exclamation: Or specific color for each commentBox 234 | 235 | CommentBox without content: 236 | 237 | ``` 238 | f: { 239 | ... 240 | comment: { 241 | borderColor: '#eee', 242 | }, 243 | ... 244 | }, 245 | ``` 246 | 247 | CommentBox with content: 248 | 249 | ``` 250 | f: { 251 | ... 252 | comment: { 253 | value: 'comment', 254 | borderColor: '#eee', 255 | }, 256 | ... 257 | }, 258 | ``` 259 | 260 | 261 | ### Checkbox :white_check_mark: 262 | 263 | If you want to use the checkbox 264 | 265 | 1: Create a key ``tbodyCheckbox: true`` on ``customOptions`` 266 | 267 | If you want to get the array of the checked data use ``this.$refs.vueTable.checkedRows`` 268 | 269 | #### Example 270 | 271 | ``` 272 | customOptions: { 273 | ... 274 | tbodyCheckbox: boolean 275 | ... 276 | }, 277 | ``` 278 | 279 | ### Headers :tiger: 280 | 281 | Name | Type | Description 282 | --------------------|---------|------------------- 283 | headerName | String | The chosen header name 284 | headerkey | String | The Slugify version of the headerName 285 | style | Object | The style of the td 286 | - width | String | Indicate the width of ```` 287 | - minWidth | String | minWidth must be equal to width 288 | disabled | Boolean | optional - Disabled cell 289 | 290 | #### Example 291 | 292 | ``` javascript 293 | headers: [ 294 | { 295 | headerName: 'Image', 296 | headerKey: 'img', 297 | style: { 298 | width: '100px' 299 | minWidth: '100px' 300 | }, 301 | }, 302 | { 303 | headerName: 'Nom', 304 | headerKey: 'name', 305 | style: { 306 | width: '100px' 307 | minWidth: '100px' 308 | }, 309 | }, 310 | { 311 | headerName: 'Prénom', 312 | headerKey: 'surname', 313 | style: { 314 | width: '100px' 315 | minWidth: '100px' 316 | }, 317 | }, 318 | { 319 | headerName: 'Age', 320 | headerKey: 'age', 321 | style: { 322 | width: '100px' 323 | minWidth: '100px' 324 | }, 325 | }, 326 | { 327 | headerName: 'Born', 328 | headerKey: 'born', 329 | style: { 330 | width: '100px' 331 | minWidth: '100px' 332 | }, 333 | }, 334 | ], 335 | ``` 336 | 337 | ### Data :honeybee: 338 | 339 | Name | Type | Description 340 | ----------------------|---------|------------------- 341 | key | String | The key of the object written in Slugify 342 | type | String | The type of data rendered (`` 181 | 182 | 183 | 184 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /src/components/TBody/VSelect.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 141 | 142 | 201 | -------------------------------------------------------------------------------- /src/components/Thead.vue: -------------------------------------------------------------------------------- 1 | 134 | 135 | 308 | 309 | 541 | -------------------------------------------------------------------------------- /src/components/VueTable.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 487 | 488 | 527 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | export function cleanProperty(element) { 3 | let { style } = element; 4 | 5 | if (!style) { 6 | style = {}; 7 | } 8 | 9 | style.setProperty("--rectangleWidth", "100%"); 10 | style.setProperty("--rectangleHeight", "40px"); 11 | style.setProperty("--rectangleTop", 0); 12 | style.setProperty("--rectangleBottom", 0); 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import VueTable from "./components/VueTable.vue"; 2 | 3 | export default VueTable; 4 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | 4 | Vue.config.productionTip = false; 5 | 6 | new Vue({ 7 | render: (h) => h(App), 8 | }).$mount("#app"); 9 | -------------------------------------------------------------------------------- /src/mixins/VueTable/callback.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export const callback = { 3 | methods: { 4 | callbackCheckedAll(isChecked) { 5 | this.$emit("tbody-all-checked-row", isChecked); 6 | 7 | if (this.customOptions.tbodyCheckbox) { 8 | this.value.forEach((data) => { 9 | this.$set(data, "vuetable_checked", isChecked); 10 | }); 11 | } 12 | }, 13 | callbackSort(event, header, colIndex) { 14 | this.$emit("thead-td-sort", event, header, colIndex); 15 | }, 16 | callbackSubmenuThead(event, header, colIndex, submenuFunction, selectOptions) { 17 | this.submenuStatusThead = false; 18 | 19 | if (selectOptions) { 20 | this.$emit( 21 | `thead-submenu-click-${submenuFunction}`, 22 | event, 23 | header, 24 | colIndex, 25 | selectOptions 26 | ); 27 | } else { 28 | this.$emit(`thead-submenu-click-${submenuFunction}`, event, header, colIndex); 29 | } 30 | }, 31 | callbackSubmenuTbody(event, header, rowIndex, colIndex, type, submenuFunction) { 32 | this.calculPosition(event, rowIndex, colIndex, "submenu"); 33 | this.$emit( 34 | `tbody-submenu-click-${submenuFunction}`, 35 | event, 36 | header, 37 | rowIndex, 38 | colIndex, 39 | type, 40 | submenuFunction 41 | ); 42 | }, 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /src/mixins/VueTable/copyPaste.js: -------------------------------------------------------------------------------- 1 | const lodashClonedeep = require("lodash.clonedeep"); 2 | 3 | // eslint-disable-next-line import/prefer-default-export 4 | export const copyPaste = { 5 | data() { 6 | return { 7 | storeCopyDatas: [], 8 | storeRectangleSelection: [], 9 | rectangleSelectedCell: null, 10 | selectedCoordCopyCells: null, 11 | selectedCoordCells: null, 12 | }; 13 | }, 14 | mounted() { 15 | document.addEventListener("copy", (event) => { 16 | if (this.actualElement) { 17 | event.preventDefault(); 18 | this.storeCopyDatas = []; 19 | this.copyStoreData("copy"); 20 | } 21 | }); 22 | document.addEventListener("paste", (event) => { 23 | if (this.storeCopyDatas.length > 0) { 24 | event.preventDefault(); 25 | this.pasteReplaceData(); 26 | } 27 | }); 28 | }, 29 | methods: { 30 | disabledEvent(cell, header) { 31 | if (cell.disabled === undefined) { 32 | return this.disableCells.some((x) => x === header); 33 | } 34 | 35 | return cell.disabled; 36 | }, 37 | copyStoreData(params) { 38 | const value = lodashClonedeep(this.value); 39 | 40 | this.removeClass(["stateCopy"]); 41 | 42 | if (this.selectedCoordCells && this.selectedMultipleCell && params === "copy") { 43 | if ( 44 | this.selectedCell.row !== this.selectedCoordCells.rowEnd || 45 | this.selectedCell.col !== this.selectedCoordCells.colEnd 46 | ) { 47 | this.selectedCell.row = this.selectedCoordCells.rowEnd; 48 | this.selectedCell.col = this.selectedCoordCells.colEnd; 49 | } 50 | } 51 | 52 | if ( 53 | this.selectedCoordCells && 54 | this.selectedCell.col === this.selectedCoordCells.colEnd && 55 | this.selectedCell.row === this.selectedCoordCells.rowEnd && 56 | params === "copy" 57 | ) { 58 | this.selectedCoordCopyCells = this.selectedCoordCells; 59 | } else { 60 | this.selectedCoordCopyCells = null; 61 | } 62 | 63 | if (this.selectedMultipleCell && this.selectedCoordCells) { 64 | let rowMin = Math.min(this.selectedCoordCells.rowStart, this.selectedCoordCells.rowEnd); 65 | const rowMax = Math.max(this.selectedCoordCells.rowStart, this.selectedCoordCells.rowEnd); 66 | let colMin = Math.min(this.selectedCoordCells.colStart, this.selectedCoordCells.colEnd); 67 | const colMax = Math.max(this.selectedCoordCells.colStart, this.selectedCoordCells.colEnd); 68 | const header = this.headerKeys[colMin]; 69 | let storeData = {}; 70 | 71 | if (params === "copy") { 72 | this.$set(this.value[rowMin][header], "stateCopy", true); 73 | this.removeClass(["rectangleSelection"]); 74 | this.cleanPropertyOnCell("copy"); 75 | } 76 | 77 | while (rowMin <= rowMax) { 78 | // remove stateCopy if present of storeData 79 | const copyData = value[rowMin][this.headerKeys[colMin]]; 80 | 81 | copyData.active = false; 82 | copyData.selected = false; 83 | copyData.stateCopy = false; 84 | 85 | storeData[this.headerKeys[colMin]] = copyData; 86 | colMin += 1; 87 | 88 | if (colMin > colMax) { 89 | this.storeCopyDatas.push(storeData); 90 | colMin = Math.min(this.selectedCoordCells.colStart, this.selectedCoordCells.colEnd); 91 | rowMin += 1; 92 | storeData = {}; 93 | } 94 | } 95 | 96 | this.copyMultipleCell = true; 97 | } else { 98 | if (params === "copy" && this.selectedCell) { 99 | this.cleanPropertyOnCell("copy"); 100 | this.$set(this.value[this.selectedCell.row][this.selectedCell.header], "stateCopy", true); 101 | } else { 102 | this.storeCopyDatas = []; 103 | } 104 | 105 | // remove stateCopy if present of storeData 106 | const copyData = value[this.selectedCell.row][this.selectedCell.header]; 107 | 108 | copyData.active = false; 109 | copyData.selected = false; 110 | copyData.stateCopy = false; 111 | copyData.rectangleSelection = false; 112 | this.storeCopyDatas.push(copyData); 113 | this.copyMultipleCell = false; 114 | } 115 | }, 116 | pasteReplaceData() { 117 | const maxRow = this.value.length; 118 | const cell = this.value[this.selectedCell.row][this.selectedCell.header]; 119 | 120 | this.cleanPropertyOnCell("paste"); 121 | 122 | // copy / paste one cell || disable on disabled cell 123 | if ( 124 | this.storeCopyDatas[0].value && 125 | !this.copyMultipleCell && 126 | !this.selectedMultipleCell && 127 | !this.eventDrag && 128 | !this.disabledEvent(cell, this.selectedCell.header) 129 | ) { 130 | // get the copied cell as new object 131 | const [copiedData] = lodashClonedeep(this.storeCopyDatas); 132 | 133 | // Keep reference of previous cell object 134 | copiedData.duplicate = cell; 135 | copiedData.active = true; 136 | this.value[this.selectedCell.row][this.selectedCell.header] = copiedData; 137 | // callback changeData 138 | this.$emit("tbody-paste-data", this.selectedCell.row, this.selectedCell.header, copiedData); 139 | this.changeData(this.selectedCell.row, this.selectedCell.header); 140 | // disable on disabled cell 141 | } else if (!this.disabledEvent(cell, this.selectedCell.header) && this.selectedCoordCells) { 142 | // if paste in multiple selection 143 | const conditionPasteToMultipleSelection = 144 | this.selectedCoordCopyCells !== null && 145 | this.selectedCoordCells !== this.selectedCoordCopyCells; 146 | // new paste data 147 | const conditionRowToMultiplePasteRow = 148 | this.storeCopyDatas.length === 1 && 149 | !this.storeCopyDatas[0].type && 150 | this.selectedCoordCopyCells !== null && 151 | Object.values(this.storeCopyDatas[0]).length > 1 && 152 | this.selectedCoordCells.rowStart < this.selectedCoordCells.rowEnd; 153 | // copy / paste multiple cell | drag to fill one / multiple cell 154 | let rowMin = Math.min(this.selectedCoordCells.rowStart, this.selectedCoordCells.rowEnd); 155 | let rowMax = Math.max(this.selectedCoordCells.rowStart, this.selectedCoordCells.rowEnd); 156 | let colMin = Math.min(this.selectedCoordCells.colStart, this.selectedCoordCells.colEnd); 157 | let colMax = Math.max(this.selectedCoordCells.colStart, this.selectedCoordCells.colEnd); 158 | 159 | if (conditionPasteToMultipleSelection) { 160 | rowMin = Math.min( 161 | this.selectedCoordCopyCells.rowStart, 162 | this.selectedCoordCopyCells.rowEnd 163 | ); 164 | rowMax = Math.max( 165 | this.selectedCoordCopyCells.rowStart, 166 | this.selectedCoordCopyCells.rowEnd 167 | ); 168 | } 169 | 170 | if (conditionRowToMultiplePasteRow) { 171 | rowMin = Math.min(this.selectedCoordCells.rowStart, this.selectedCoordCells.rowEnd); 172 | rowMax = Math.max(this.selectedCoordCells.rowStart, this.selectedCoordCells.rowEnd); 173 | } 174 | 175 | if (conditionPasteToMultipleSelection || conditionRowToMultiplePasteRow) { 176 | colMin = Math.min( 177 | this.selectedCoordCopyCells.colStart, 178 | this.selectedCoordCopyCells.colEnd 179 | ); 180 | colMax = Math.max( 181 | this.selectedCoordCopyCells.colStart, 182 | this.selectedCoordCopyCells.colEnd 183 | ); 184 | } 185 | 186 | let row = 0; 187 | let col = 0; 188 | 189 | while (rowMin <= rowMax) { 190 | const header = this.headerKeys[colMin]; 191 | const newCopyData = lodashClonedeep(this.storeCopyDatas); 192 | 193 | if (this.eventDrag) { 194 | // Drag To Fill 195 | const { duplicate } = this.value[rowMin][header]; 196 | 197 | if (newCopyData[0][header]) { 198 | newCopyData[0][header].duplicate = duplicate; 199 | this.value[rowMin][header] = newCopyData[0][header]; // multiple cell 200 | this.$emit("tbody-paste-data", rowMin, header, newCopyData[0][header]); 201 | } else { 202 | newCopyData[0].duplicate = duplicate; 203 | [this.value[rowMin][header]] = newCopyData; // one cell 204 | this.$emit("tbody-paste-data", rowMin, header, newCopyData); 205 | } 206 | 207 | this.changeData(rowMin, header); 208 | } else { 209 | let incrementRow = this.selectedCell.row + row; 210 | let incrementCol = this.selectedCell.col + col; 211 | 212 | if (this.selectedCoordCells !== this.selectedCoordCopyCells) { 213 | incrementRow = this.selectedCoordCells.rowStart + row; 214 | incrementCol = this.selectedCoordCells.colStart + col; 215 | } 216 | 217 | let currentHeader = this.headerKeys[incrementCol]; 218 | // multiple col to multiple col 219 | const colsToCols = Object.values(newCopyData[0]).length === 1; 220 | // one cell to multipleCell 221 | const cellToCells = 222 | newCopyData.length === 1 && 223 | Object.values(newCopyData).length === 1 && 224 | newCopyData[0].type; 225 | // 1 row to 1 row 226 | const rowToRow = 227 | newCopyData.length === 1 && 228 | Object.values(newCopyData[0]).length > 1 && 229 | !newCopyData[0].type && 230 | this.selectedCoordCells.rowStart === this.selectedCoordCells.rowEnd; 231 | // 1 row & multiple cols => to multiple row & cols 232 | const rowColsToRowsCols = 233 | newCopyData.length === 1 && 234 | Object.values(newCopyData[0]).length > 1 && 235 | this.selectedCoordCells.rowStart < this.selectedCoordCells.rowEnd && 236 | this.selectedCoordCells.colStart !== this.selectedCoordCells.colEnd; 237 | // multiple col / row to multiple col / row 238 | const rowsColsToRowsCols = 239 | newCopyData.length > 1 && Object.values(newCopyData[0]).length > 1; 240 | 241 | // ▭ => ▭ / ▭ 242 | // ▭ => / ▭ 243 | if (colsToCols) { 244 | currentHeader = this.headerKeys[this.selectedCell.col]; 245 | 246 | if (incrementRow < maxRow) { 247 | this.replacePasteData(col, header, incrementRow, currentHeader); 248 | col += 1; 249 | } 250 | } 251 | 252 | // ▭ => ▭▭ 253 | // ▭▭ 254 | if (rowColsToRowsCols) { 255 | this.replacePasteData(0, header, incrementRow, currentHeader); 256 | 257 | if (colMin < colMax) { 258 | col += 1; 259 | } else { 260 | col = 0; 261 | } 262 | // ▭ => ▭ 263 | // ▭ 264 | } else if (cellToCells) { 265 | if (this.selectedCoordCells.colStart === this.selectedCoordCells.colEnd) { 266 | currentHeader = this.selectedCell.header; 267 | newCopyData[0].duplicate = this.value[rowMin][currentHeader].duplicate; 268 | 269 | [this.value[rowMin][currentHeader]] = newCopyData; 270 | this.$emit("tbody-paste-data", rowMin, currentHeader, newCopyData[0]); 271 | this.changeData(rowMin, currentHeader); 272 | } else { 273 | // ▭ => ▭ ▭ ▭ 274 | this.replacePasteData(col, header, this.selectedCell.row, header); 275 | } 276 | } 277 | 278 | // ▭▭▭ => ▭ / ▭▭▭ 279 | if (rowToRow) { 280 | this.replacePasteData(0, header, this.selectedCell.row, currentHeader); 281 | col += 1; 282 | } 283 | 284 | // ▭▭▭ => ▭ / ▭▭▭ 285 | // ▭▭▭ => ▭▭▭ 286 | if (rowsColsToRowsCols) { 287 | if (this.value[incrementRow][currentHeader]) { 288 | newCopyData[row][header].duplicate = this.value[incrementRow][ 289 | currentHeader 290 | ].duplicate; 291 | } 292 | 293 | this.replacePasteData(row, header, incrementRow, currentHeader); 294 | 295 | if (colMin < colMax) { 296 | col += 1; 297 | } else { 298 | col = 0; 299 | } 300 | } 301 | 302 | // add active / selected status on firstCell 303 | this.value[this.selectedCell.row][this.selectedCell.header].selected = true; 304 | this.value[this.selectedCell.row][this.selectedCell.header].rectangleSelection = true; 305 | this.value[this.selectedCell.row][this.selectedCell.header].active = true; 306 | } 307 | 308 | colMin += 1; 309 | 310 | if (colMin > colMax) { 311 | if ( 312 | this.selectedCoordCopyCells !== null && 313 | this.selectedCoordCells !== this.selectedCoordCopyCells 314 | ) { 315 | colMin = Math.min( 316 | this.selectedCoordCopyCells.colStart, 317 | this.selectedCoordCopyCells.colEnd 318 | ); 319 | } else { 320 | colMin = Math.min(this.selectedCoordCells.colStart, this.selectedCoordCells.colEnd); 321 | } 322 | 323 | rowMin += 1; 324 | row += 1; 325 | } 326 | } 327 | 328 | this.modifyMultipleCell(); 329 | } 330 | }, 331 | cleanPropertyOnCell(action) { 332 | if (this.storeRectangleSelection.length > 0) { 333 | this.storeRectangleSelection.forEach((cell) => { 334 | if ( 335 | action === "paste" && 336 | !cell.classList.value.includes("rectangleSelection") && 337 | !cell.classList.value.includes("copy") 338 | ) { 339 | this.cleanProperty(cell); 340 | } else if (action === "copy" && !cell.classList.value.includes("selected")) { 341 | this.cleanProperty(cell); 342 | } 343 | }); 344 | } 345 | }, 346 | cleanProperty(element) { 347 | element.style.setProperty("--rectangleWidth", "100%"); 348 | element.style.setProperty("--rectangleHeight", "40px"); 349 | element.style.setProperty("--rectangleTop", 0); 350 | element.style.setProperty("--rectangleBottom", 0); 351 | }, 352 | replacePasteData(col, header, incrementRow, currentHeader) { 353 | const newCopyData = lodashClonedeep(this.storeCopyDatas); 354 | let copyData; 355 | 356 | // If copyMultipleCell, newCopyData => [{header: {}}] 357 | if (this.copyMultipleCell) { 358 | copyData = newCopyData[col][header]; 359 | } else { 360 | // Else, newCopyData => [{}] 361 | copyData = newCopyData[col]; 362 | } 363 | 364 | copyData.duplicate = this.value[incrementRow][currentHeader]; 365 | 366 | this.value[incrementRow][currentHeader] = copyData; 367 | this.$emit("tbody-paste-data", incrementRow, currentHeader, copyData); 368 | this.changeData(incrementRow, currentHeader); 369 | }, 370 | modifyMultipleCell(params) { 371 | let rowMin = Math.min(this.selectedCoordCells.rowStart, this.selectedCoordCells.rowEnd); 372 | const rowMax = Math.max(this.selectedCoordCells.rowStart, this.selectedCoordCells.rowEnd); 373 | let colMin = Math.min(this.selectedCoordCells.colStart, this.selectedCoordCells.colEnd); 374 | const colMax = Math.max(this.selectedCoordCells.colStart, this.selectedCoordCells.colEnd); 375 | 376 | while (rowMin <= rowMax) { 377 | const header = this.headerKeys[colMin]; 378 | const cell = this.value[rowMin][header]; 379 | 380 | // disable on disabled cell 381 | if (params === "removeValue" && !this.disabledEvent(cell, header) && !!cell.value) { 382 | this.changeData(rowMin, header); 383 | this.$set(cell, "value", ""); 384 | this.$set(cell, "selected", false); 385 | this.$emit("tbody-nav-backspace", rowMin, colMin, header, cell); 386 | } 387 | 388 | if (params === "selected") { 389 | this.$set(cell, "selected", true); 390 | this.selectedMultipleCellActive = true; 391 | 392 | if (colMin === colMax && rowMin === rowMax) { 393 | // add active on the last cell 394 | this.removeClass(["active"]); 395 | this.$set(cell, "active", true); 396 | } 397 | } 398 | 399 | colMin += 1; 400 | 401 | if (colMin > colMax) { 402 | colMin = Math.min(this.selectedCoordCells.colStart, this.selectedCoordCells.colEnd); 403 | rowMin += 1; 404 | } 405 | } 406 | 407 | // Set height / width of rectangle 408 | this.setRectangleSelection(colMin, colMax, rowMin, rowMax); 409 | }, 410 | setRectangleSelection(colMin, colMax, rowMin, rowMax) { 411 | let width = 100; 412 | let height = 40; 413 | 414 | // Defined width of rectangle 415 | if (colMin === 0 && colMax === 0) { 416 | width = 100 * (colMin + 1); 417 | } else if (colMin === 0 && colMax > 0) { 418 | width = 100 * (colMax + 1); 419 | } else { 420 | width = 100 * (colMax - colMin + 1); 421 | } 422 | 423 | // Defined height of rectangle 424 | if ((rowMin === 0 && rowMax === 0) || (rowMin === 0 && rowMax > 0)) { 425 | height = 40 * (rowMin + 1); 426 | } else if (this.selectedCoordCells.rowEnd > this.selectedCoordCells.rowStart) { 427 | height = 40 * (this.selectedCoordCells.rowEnd - this.selectedCoordCells.rowStart + 1); 428 | } else { 429 | height = 40 * (this.selectedCoordCells.rowStart - this.selectedCoordCells.rowEnd + 1); 430 | } 431 | 432 | if (this.$refs[`${this.customTable}-vueTbody`]?.$refs) { 433 | [this.rectangleSelectedCell] = this.$refs[`${this.customTable}-vueTbody`].$refs[ 434 | `td-${this.customTable}-${this.selectedCoordCells.colStart}-${this.selectedCoordCells.rowStart}` 435 | ]; 436 | 437 | if (!this.selectedMultipleCellActive) { 438 | [this.rectangleSelectedCell] = this.$refs[`${this.customTable}-vueTbody`].$refs[ 439 | `td-${this.customTable}-${this.selectedCell.col}-${this.selectedCell.row}` 440 | ]; 441 | } 442 | } 443 | 444 | this.rectangleSelectedCell.style.setProperty("--rectangleWidth", `${width + 1}%`); 445 | this.rectangleSelectedCell.style.setProperty("--rectangleHeight", `${height}px`); 446 | 447 | // Position bottom/top of rectangle if rowStart >= rowEnd 448 | if (this.selectedCoordCells.rowStart >= this.selectedCoordCells.rowEnd) { 449 | this.rectangleSelectedCell.style.setProperty("--rectangleTop", "auto"); 450 | this.rectangleSelectedCell.style.setProperty("--rectangleBottom", 0); 451 | } else { 452 | this.rectangleSelectedCell.style.setProperty("--rectangleTop", 0); 453 | this.rectangleSelectedCell.style.setProperty("--rectangleBottom", "auto"); 454 | } 455 | 456 | // Position left/right of rectangle if colStart >= colEnd 457 | if (this.selectedCoordCells.colStart >= this.selectedCoordCells.colEnd) { 458 | this.rectangleSelectedCell.style.setProperty("--rectangleLeft", "auto"); 459 | this.rectangleSelectedCell.style.setProperty("--rectangleRight", 0); 460 | } else { 461 | this.rectangleSelectedCell.style.setProperty("--rectangleLeft", 0); 462 | } 463 | 464 | if (!this.storeRectangleSelection.includes(this.rectangleSelectedCell)) { 465 | this.storeRectangleSelection.push(this.rectangleSelectedCell); 466 | } 467 | }, 468 | }, 469 | }; 470 | -------------------------------------------------------------------------------- /src/mixins/VueTable/dragToFill.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export const dragToFill = { 3 | data() { 4 | return { 5 | eventDrag: false, 6 | }; 7 | }, 8 | methods: { 9 | handleDownDragToFill(event, header, col, rowIndex) { 10 | this.storeCopyDatas = []; 11 | this.$set(this.value[rowIndex][header], "active", true); 12 | this.eventDrag = true; 13 | 14 | if (!this.selectedCoordCells && !this.selectedMultipleCell) { 15 | this.selectedCoordCells = { 16 | rowStart: this.selectedCell.row, 17 | colStart: this.selectedCell.col, 18 | keyStart: this.selectedCell.header, 19 | rowEnd: rowIndex, 20 | colEnd: this.selectedCell.col, 21 | keyEnd: this.selectedCell.header, 22 | }; 23 | } else if (this.selectedMultipleCell) { 24 | // if drag col to col in row to row to row 25 | this.selectedCoordCells.rowStart = rowIndex; 26 | } else { 27 | this.selectedCoordCells = { 28 | rowStart: this.selectedCell.row, 29 | colStart: this.selectedCell.col, 30 | keyStart: this.selectedCell.header, 31 | rowEnd: rowIndex, 32 | colEnd: this.selectedCell.col, 33 | keyEnd: this.selectedCell.header, 34 | }; 35 | } 36 | 37 | this.copyStoreData("drag"); 38 | }, 39 | handleMoveDragToFill(event, header, col, rowIndex, colIndex) { 40 | if ( 41 | this.eventDrag === true && 42 | this.selectedCoordCells && 43 | this.selectedCoordCells.rowEnd !== rowIndex 44 | ) { 45 | this.selectedCoordCells.rowEnd = rowIndex; 46 | this.modifyMultipleCell("selected"); 47 | this.$emit( 48 | "tbody-move-dragtofill", 49 | this.selectedCoordCells, 50 | header, 51 | col, 52 | rowIndex, 53 | colIndex 54 | ); 55 | } 56 | }, 57 | handleUpDragToFill(event, header, rowIndex, colIndex) { 58 | if (this.eventDrag === true && this.selectedCoordCells) { 59 | this.selectedCoordCells.rowEnd = rowIndex; 60 | this.pasteReplaceData(); 61 | this.removeClass(["selected", "rectangleSelection", "active", "show"]); 62 | this.$emit("tbody-up-dragtofill", this.selectedCoordCells, header, rowIndex, colIndex); 63 | this.eventDrag = false; 64 | this.storeCopyDatas = []; 65 | this.selectedCoordCells = null; 66 | } 67 | }, 68 | }, 69 | }; 70 | -------------------------------------------------------------------------------- /src/mixins/VueTable/handleTBody.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export const handleTBody = { 3 | data() { 4 | return { 5 | oldTdActive: null, 6 | oldTdShow: null, 7 | }; 8 | }, 9 | methods: { 10 | bindClassActiveOnTd(header, rowIndex, colIndex) { 11 | this.removeClass(["active", "show"]); 12 | this.value[rowIndex][header].active = true; 13 | // stock oldTdActive in object 14 | this.oldTdActive = { 15 | key: header, 16 | row: rowIndex, 17 | col: colIndex, 18 | }; 19 | }, 20 | handleTBodyContextMenu(event, header, rowIndex, colIndex) { 21 | this.lastSubmenuOpen = { 22 | event, 23 | header, 24 | rowIndex, 25 | colIndex, 26 | }; 27 | }, 28 | handleTbodyTdClick(event, col, header, rowIndex, colIndex, type) { 29 | const column = col; 30 | 31 | if (this.selectedMultipleCell) { 32 | this.selectedMultipleCell = false; 33 | } 34 | 35 | if (!column.active) { 36 | if (!this.keys[16]) { 37 | this.removeClass(["selected", "rectangleSelection"]); 38 | } 39 | 40 | this.removeClass(["search"]); 41 | this.lastSelectOpen = null; 42 | } 43 | 44 | this.bindClassActiveOnTd(header, rowIndex, colIndex); 45 | 46 | this.updateSelectedCell(header, rowIndex, colIndex); 47 | 48 | this.enableSubmenu(); 49 | 50 | if (this.oldTdShow && this.oldTdShow.col !== colIndex) { 51 | this.value[this.oldTdShow.row][this.oldTdShow.key].show = false; 52 | } 53 | 54 | if (type === "select" && column.handleSearch) { 55 | this.activeSelectSearch(event, rowIndex, colIndex, header); 56 | } 57 | }, 58 | handleSearchInputSelect(event, searchValue, col, header, rowIndex, colIndex) { 59 | const disableSearch = !(searchValue === "" && event.keyCode === 8); 60 | 61 | if ( 62 | (!this.keys.cmd || !this.keys.ctrl) && 63 | disableSearch && 64 | ![13, 16, 17, 27, 37, 38, 39, 40, 91].includes(event.keyCode) 65 | ) { 66 | if (this.lastSelectOpen) { 67 | this.$set(this.lastSelectOpen, "searchValue", searchValue); 68 | } else { 69 | this.lastSelectOpen = { 70 | event, 71 | header, 72 | col, 73 | rowIndex, 74 | colIndex, 75 | searchValue, 76 | }; 77 | } 78 | 79 | // active class 80 | if (event.keyCode !== 8) { 81 | const currentData = this.value[rowIndex][header]; 82 | 83 | this.$set(currentData, "search", true); 84 | this.$set(currentData, "show", true); 85 | 86 | this.showDropdown(colIndex, rowIndex); 87 | } 88 | 89 | this.incrementOption = 0; 90 | } 91 | }, 92 | handleSelectMultipleCell(event, header, rowIndex, colIndex) { 93 | if (!this.selectedMultipleCellActive) { 94 | this.selectedMultipleCell = true; 95 | 96 | if (this.selectedCell) { 97 | this.selectedCoordCells = { 98 | rowStart: this.selectedCell.row, 99 | colStart: this.selectedCell.col, 100 | keyStart: this.selectedCell.header, 101 | rowEnd: rowIndex, 102 | colEnd: colIndex, 103 | keyEnd: header, 104 | }; 105 | } 106 | 107 | // Add active on selectedCoordCells selected 108 | this.modifyMultipleCell("selected"); 109 | 110 | // highlight row and column of selected cell 111 | this.highlightTdAndThead(rowIndex, colIndex); 112 | } 113 | }, 114 | handleTbodyInputChange(event, header, rowIndex, colIndex) { 115 | // remove class show on input when it change 116 | if (this.oldTdShow) this.value[this.oldTdShow.row][this.oldTdShow.key].show = false; 117 | this.enableSubmenu(); 118 | 119 | // callback 120 | this.$emit("tbody-input-change", event, header, rowIndex, colIndex); 121 | this.changeData(rowIndex, header); 122 | }, 123 | handleTbodyInputKeydown(event, header, rowIndex, colIndex) { 124 | this.$emit("tbody-input-keydown", event, header, rowIndex, colIndex); 125 | }, 126 | handleTbodyTdDoubleClick(event, header, col, rowIndex, colIndex) { 127 | if (col.handleSearch) return; 128 | 129 | // stock oldTdShow in object 130 | if (this.oldTdShow) { 131 | this.value[this.oldTdShow.row][this.oldTdShow.key].show = false; 132 | } 133 | 134 | // add class show on element 135 | this.$set(this.value[rowIndex][header], "show", true); 136 | event.currentTarget.lastElementChild.focus(); 137 | 138 | this.oldTdShow = { 139 | key: header, 140 | row: rowIndex, 141 | col: colIndex, 142 | }; 143 | 144 | this.enableSubmenu(); 145 | }, 146 | }, 147 | }; 148 | -------------------------------------------------------------------------------- /src/mixins/VueTable/handleTHead.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export const handleTHead = { 3 | methods: { 4 | handleUpDragSizeHeader(event, headers) { 5 | this.$emit("handle-up-drag-size-header", event, headers); 6 | }, 7 | handleTheadContextMenu() { 8 | this.submenuStatusTbody = false; 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/mixins/VueTable/moveOnTable.js: -------------------------------------------------------------------------------- 1 | import { cleanProperty } from "../../helpers"; 2 | 3 | /* eslint-disable-next-line import/prefer-default-export */ 4 | export const moveOnTable = { 5 | data() { 6 | return { 7 | disableKeyTimeout: null, 8 | incrementCol: 0, 9 | incrementRow: null, 10 | pressedShift: 0, 11 | keys: {}, 12 | }; 13 | }, 14 | mounted() { 15 | window.addEventListener("keydown", this.moveKeydown); 16 | window.addEventListener("keyup", this.moveKeyup); 17 | }, 18 | methods: { 19 | moveOnTable(event, colIndex, rowIndex) { 20 | const vueTable = this.$refs[`${this.customTable}-vueTable`]; 21 | const maxCol = Math.max(...this.colHeaderWidths); 22 | 23 | // get the correct height of visible table 24 | if (vueTable) { 25 | const heightTable = 26 | vueTable.clientHeight - 27 | vueTable.firstElementChild.clientHeight - 28 | this.$refs[`${this.customTable}-vueThead`].$el.clientHeight; 29 | const widthTable = vueTable.clientWidth - 40; 30 | const borderBottomCell = Math.round(heightTable / 40); 31 | const borderRightCell = Math.round(widthTable / maxCol); 32 | 33 | // top 34 | if (event.keyCode === 38) { 35 | event.preventDefault(); 36 | 37 | if (borderBottomCell >= rowIndex) { 38 | vueTable.scrollTop -= 40; 39 | } 40 | } 41 | 42 | // bottom 43 | if (event.keyCode === 40) { 44 | event.preventDefault(); 45 | 46 | if (borderBottomCell - 1 <= rowIndex) { 47 | vueTable.scrollTop += 40; 48 | } 49 | } 50 | 51 | // left 52 | if (event.keyCode === 37) { 53 | event.preventDefault(); 54 | 55 | if (borderRightCell + 1 >= colIndex) { 56 | vueTable.scrollLeft -= maxCol; 57 | } 58 | } 59 | 60 | // right 61 | if (event.keyCode === 39) { 62 | event.preventDefault(); 63 | 64 | if (borderRightCell - 1 <= colIndex) { 65 | vueTable.scrollLeft += maxCol; 66 | } 67 | } 68 | } 69 | }, 70 | moveKeydown(event) { 71 | [this.actualElement] = document.getElementsByClassName("active_td"); 72 | 73 | if (event.keyCode === 16) { 74 | this.keys[event.keyCode] = true; 75 | } 76 | 77 | if (event.keyCode === 91 || event.keyCode === 17) { 78 | this.keys.cmd = true; 79 | this.keys.ctrl = true; 80 | } 81 | 82 | if ((this.keys.cmd && event.keyCode === 90) || (this.keys.ctrl && event.keyCode === 90)) { 83 | this.rollBackUndo(); 84 | } 85 | 86 | if (this.lastSelectOpen) { 87 | this.moveOnSelect(event); 88 | } 89 | 90 | if ( 91 | this.actualElement && 92 | this.actualElement.getAttribute("current-table") === this.customTable.toString() && 93 | [37, 39, 40, 38, 13, 27, 8].includes(event.keyCode) 94 | ) { 95 | this.removeClass(["selected"]); 96 | 97 | const colIndex = Number(this.actualElement.getAttribute("data-col-index")); 98 | const rowIndex = Number(this.actualElement.getAttribute("data-row-index")); 99 | const dataType = this.actualElement.getAttribute("data-type"); 100 | const header = this.actualElement.getAttribute("data-header"); 101 | const currentlyEditingCell = this.value[rowIndex][header].show; 102 | 103 | if (!currentlyEditingCell) { 104 | if (!this.setFirstCell) { 105 | this.$set(this.value[rowIndex][header], "rectangleSelection", true); 106 | this.setFirstCell = true; 107 | } 108 | 109 | // set colMax rowMax 110 | const rowMax = this.value.length; 111 | const colMax = this.headers.length; 112 | 113 | this.moveOnTable(event, colIndex, rowIndex); 114 | 115 | // shift 116 | if (this.keys[16]) { 117 | this.pressShiftMultipleCell(event, header, rowMax, rowIndex, colMax, colIndex); 118 | } else if (!this.lastSelectOpen && event.keyCode !== 8) { 119 | if (this.selectedMultipleCell) { 120 | this.selectedMultipleCell = false; 121 | } 122 | 123 | this.$set(this.value[rowIndex][header], "active", false); 124 | this.removeClass(["rectangleSelection"]); 125 | 126 | // left 127 | if (event.keyCode === 37) { 128 | const decrementHeader = Object.values(this.headerKeys)[colIndex - 1]; 129 | 130 | if (decrementHeader) { 131 | this.$set(this.value[rowIndex][decrementHeader], "active", true); 132 | 133 | if (dataType === "select") { 134 | this.activeSelectSearch(event, rowIndex, colIndex, decrementHeader); 135 | } 136 | 137 | this.updateSelectedCell(decrementHeader, rowIndex, colIndex - 1); 138 | } else { 139 | this.$set(this.value[rowIndex][header], "active", true); 140 | 141 | if (dataType === "select") { 142 | this.activeSelectSearch(event, rowIndex, colIndex, header); 143 | } 144 | 145 | this.updateSelectedCell(header, rowIndex, colIndex); 146 | } 147 | } 148 | 149 | // top 150 | if (event.keyCode === 38) { 151 | if (rowIndex !== 0) { 152 | this.$set(this.value[rowIndex - 1][header], "active", true); 153 | 154 | if (dataType === "select") { 155 | this.activeSelectSearch(event, rowIndex - 1, colIndex, header); 156 | } 157 | 158 | this.updateSelectedCell(header, rowIndex - 1, colIndex); 159 | } else { 160 | this.$set(this.value[rowIndex][header], "active", true); 161 | 162 | if (dataType === "select") { 163 | this.activeSelectSearch(event, rowIndex, colIndex, header); 164 | } 165 | 166 | this.updateSelectedCell(header, rowIndex, colIndex); 167 | } 168 | } 169 | 170 | // right 171 | if (event.keyCode === 39) { 172 | const incrementHeader = Object.values(this.headerKeys)[colIndex + 1]; 173 | 174 | if (incrementHeader) { 175 | this.$set(this.value[rowIndex][incrementHeader], "active", true); 176 | 177 | if (dataType === "select") { 178 | this.activeSelectSearch(event, rowIndex, colIndex, incrementHeader); 179 | } 180 | 181 | this.updateSelectedCell(incrementHeader, rowIndex, colIndex + 1); 182 | } else { 183 | this.$set(this.value[rowIndex][header], "active", true); 184 | 185 | if (dataType === "select") { 186 | this.activeSelectSearch(event, rowIndex, colIndex, header); 187 | } 188 | 189 | this.updateSelectedCell(header, rowIndex, colIndex); 190 | } 191 | } 192 | 193 | // bottom 194 | if (event.keyCode === 40) { 195 | if (rowIndex + 1 !== rowMax) { 196 | this.$set(this.value[rowIndex + 1][header], "active", true); 197 | 198 | if (dataType === "select") { 199 | this.activeSelectSearch(event, rowIndex + 1, colIndex, header); 200 | } 201 | 202 | this.updateSelectedCell(header, rowIndex + 1, colIndex); 203 | } else { 204 | this.$set(this.value[rowIndex][header], "active", true); 205 | 206 | if (dataType === "select") { 207 | this.activeSelectSearch(event, rowIndex, colIndex, header); 208 | } 209 | 210 | this.updateSelectedCell(header, rowIndex, colIndex); 211 | } 212 | } 213 | } 214 | 215 | // press backspace 216 | if (event.keyCode === 8 && !this.lastSelectOpen) { 217 | this.handleTbodyNavBackspace(rowIndex, colIndex, header); 218 | } 219 | 220 | // press enter 221 | if (event.keyCode === 13) { 222 | if (this.$refs[`input-${this.customTable}-${colIndex}-${rowIndex}`]) { 223 | this.value[rowIndex][header].show = true; 224 | this.$refs[`input-${this.customTable}-${colIndex}-${rowIndex}`][0].focus(); 225 | } 226 | 227 | this.$emit( 228 | "tbody-nav-enter", 229 | event, 230 | event.keyCode, 231 | this.actualElement, 232 | rowIndex, 233 | colIndex 234 | ); 235 | } 236 | 237 | // press esc 238 | if (event.keyCode === 27) { 239 | this.value[rowIndex][header].active = false; 240 | this.storeCopyDatas = []; 241 | this.removeClass(["stateCopy"]); 242 | } 243 | } 244 | } 245 | }, 246 | moveKeyup(event) { 247 | if (event.keyCode === 16) { 248 | this.keys[event.keyCode] = false; 249 | this.incrementCol = null; 250 | this.incrementRow = null; 251 | this.selectedMultipleCell = true; 252 | this.pressedShift = 0; 253 | } 254 | 255 | if (event.keyCode === 91 || event.keyCode === 17) { 256 | if (!this.disableKeyTimeout === null) { 257 | clearTimeout(this.disableKeyTimeout); 258 | } 259 | 260 | this.disableKeyTimeout = setTimeout(() => { 261 | this.keys.cmd = false; 262 | this.keys.ctrl = false; 263 | this.disableKeyTimeout = null; 264 | }, 400); 265 | } 266 | }, 267 | moveOnSelect(event) { 268 | if (this.incrementOption <= this.filteredList.length) { 269 | const dropdown = this.$refs[`${this.customTable}-vueTbody`].$refs[ 270 | `vsSelect-${this.customTable}-${this.lastSelectOpen.colIndex}-${this.lastSelectOpen.rowIndex}` 271 | ][0].$refs[ 272 | `dropdown-${this.customTable}-${this.lastSelectOpen.colIndex}-${this.lastSelectOpen.rowIndex}` 273 | ]; 274 | const cellHeight = 45; 275 | 276 | // top 277 | if (event.keyCode === 38) { 278 | // The distance between the top border of element with the top viewport border of parent (dropdown) 279 | const topOffsetElementWithinViewport = 280 | dropdown.children[this.incrementOption].offsetTop - dropdown.scrollTop; 281 | // Divided by cellHeight gives the index from *top* of the current element. 282 | const isFirstItemInViewport = topOffsetElementWithinViewport / cellHeight < 1; 283 | 284 | if (this.incrementOption <= this.filteredList.length && this.incrementOption > 0) { 285 | if (this.filteredList[this.incrementOption]) { 286 | this.$set(this.filteredList[this.incrementOption], "active", false); 287 | this.incrementOption -= 1; 288 | this.$set(this.filteredList[this.incrementOption], "active", true); 289 | } else { 290 | this.incrementOption -= 1; 291 | this.$set(this.filteredList[this.incrementOption], "active", false); 292 | this.incrementOption -= 1; 293 | this.$set(this.filteredList[this.incrementOption], "active", true); 294 | } 295 | 296 | if (isFirstItemInViewport) { 297 | dropdown.scrollTop -= cellHeight; 298 | } 299 | } 300 | } 301 | 302 | // bottom 303 | if (event.keyCode === 40) { 304 | /* The distance between the bottom border of element with the bottom viewport border of parent (dropdown) 305 | * The value is always negative, so we invert it with the first minus. 306 | * (dropdown.children[this.incrementOption].offsetTop + cellHeight) => offsetBottom of element 307 | * - dropdown.scrollTop => gives the offsetBottom starting from the top viewport border of dropdown 308 | * - dropdown.offsetHeight => gives the offsetBottom starting from the bottom of viewport dropdown 309 | * You should actually draw a schematic in order to properly understand this. It helped me! 310 | */ 311 | const bottomOffsetElementWithinViewport = -( 312 | dropdown.children[this.incrementOption].offsetTop + 313 | cellHeight - 314 | (dropdown.offsetHeight + dropdown.scrollTop) 315 | ); 316 | // Divided by cellHeight gives the index from *bottom* of the current element. 317 | const isLastItemInViewport = bottomOffsetElementWithinViewport / cellHeight < 1; 318 | 319 | if (this.incrementOption < this.filteredList.length - 1) { 320 | if (this.incrementOption === 0 || this.incrementOption === 1) { 321 | this.$set(this.filteredList[this.incrementOption], "active", true); 322 | this.incrementOption += 1; 323 | this.$set(this.filteredList[this.incrementOption], "active", true); 324 | this.$set(this.filteredList[this.incrementOption - 1], "active", false); 325 | } else if (this.incrementOption > 1) { 326 | this.$set(this.filteredList[this.incrementOption], "active", false); 327 | this.incrementOption += 1; 328 | this.$set(this.filteredList[this.incrementOption], "active", true); 329 | } 330 | } 331 | 332 | if (isLastItemInViewport) { 333 | dropdown.scrollTop += cellHeight; 334 | } 335 | } 336 | } 337 | 338 | // enter 339 | if (event.keyCode === 13) { 340 | const oldSelect = this.lastSelectOpen; 341 | const currentSelect = this.value[oldSelect.rowIndex][oldSelect.header]; 342 | 343 | this.handleTbodySelectChange( 344 | event, 345 | oldSelect.header, 346 | currentSelect, 347 | this.filteredList[this.incrementOption], 348 | oldSelect.rowIndex, 349 | oldSelect.colIndex 350 | ); 351 | } 352 | }, 353 | pressShiftMultipleCell(event, h, rowMax, rowIndex, colMax, colIndex) { 354 | event.preventDefault(); 355 | let header = h; 356 | 357 | this.$set(this.value[rowIndex][header], "active", false); 358 | this.incrementCol = this.incrementCol ? this.incrementCol : colIndex; 359 | this.incrementRow = this.incrementRow ? this.incrementRow : rowIndex; 360 | 361 | if (this.pressedShift >= 0) { 362 | this.pressedShift += 1; 363 | } 364 | 365 | if (this.pressedShift === 0) { 366 | this.selectedCell = { 367 | header, 368 | row: rowIndex, 369 | col: colIndex, 370 | }; 371 | } 372 | 373 | // shift / left 374 | if (event.keyCode === 37) { 375 | this.incrementCol -= 1; 376 | 377 | if (this.incrementCol < 0) { 378 | this.incrementCol = 0; 379 | } 380 | 381 | this.removeClass(["selected"]); 382 | } 383 | 384 | // shift / top 385 | if (event.keyCode === 38) { 386 | this.incrementRow -= 1; 387 | 388 | if (this.incrementRow < 0) { 389 | this.incrementRow = 0; 390 | } 391 | 392 | this.removeClass(["selected"]); 393 | } 394 | 395 | // shift / right 396 | if (event.keyCode === 39) { 397 | if (colMax >= this.incrementCol + 2) { 398 | this.incrementCol += 1; 399 | } else { 400 | this.$set(this.value[rowIndex][header], "active", true); 401 | } 402 | } 403 | 404 | // shift / bottom 405 | if (event.keyCode === 40) { 406 | if (rowMax >= this.incrementRow + 2) { 407 | this.incrementRow += 1; 408 | } else { 409 | this.$set(this.value[rowIndex][header], "active", true); 410 | } 411 | } 412 | 413 | header = Object.values(this.headerKeys)[this.incrementCol]; 414 | this.$set(this.value[this.incrementRow][header], "active", true); 415 | this.handleSelectMultipleCell(event, header, this.incrementRow, this.incrementCol); 416 | }, 417 | handleTbodyNavBackspace(rowIndex, colIndex, header) { 418 | if (this.selectedMultipleCell) { 419 | this.modifyMultipleCell("removeValue"); 420 | } else { 421 | const cell = this.value[rowIndex][header]; 422 | 423 | if (!cell.disabled || !!cell.value) { 424 | cell.value = ""; 425 | this.$emit("tbody-nav-backspace", rowIndex, colIndex, header, cell); 426 | this.changeData(rowIndex, header); 427 | } 428 | } 429 | }, 430 | handleTbodySelectChange(event, header, col, option, rowIndex, colIndex) { 431 | const currentData = this.value[rowIndex][header]; 432 | 433 | currentData.selectOptions.forEach((selectOption) => { 434 | const sOption = selectOption; 435 | 436 | sOption.active = false; 437 | }); 438 | 439 | const value = option.value || option.item.value; 440 | 441 | currentData.selectOptions.find((x) => x.value === value).active = true; 442 | 443 | this.$set(currentData, "search", false); 444 | this.$set(currentData, "show", false); 445 | this.$set(currentData, "value", value); 446 | 447 | this.lastSelectOpen = null; 448 | // remove class show on select when it change 449 | if (this.oldTdShow) this.value[this.oldTdShow.row][this.oldTdShow.key].show = false; 450 | this.enableSubmenu(); 451 | // callback 452 | this.$emit("tbody-select-change", event, header, col, option, rowIndex, colIndex); 453 | this.changeData(rowIndex, header); 454 | }, 455 | updateSelectedCell(header, rowIndex, colIndex) { 456 | const td = this.$refs[`${this.customTable}-vueTbody`].$refs[ 457 | `td-${this.customTable}-${colIndex}-${rowIndex}` 458 | ][0]; 459 | 460 | this.value[rowIndex][header].stateCopy = false; 461 | cleanProperty(td); 462 | 463 | if (!this.setFirstCell) { 464 | this.$set(this.value[rowIndex][header], "rectangleSelection", true); 465 | this.setFirstCell = true; 466 | } 467 | 468 | this.selectedCell = { 469 | header, 470 | row: rowIndex, 471 | col: colIndex, 472 | }; 473 | // highlight selected row and column 474 | this.highlightTdAndThead(rowIndex, colIndex); 475 | }, 476 | }, 477 | }; 478 | -------------------------------------------------------------------------------- /src/mixins/VueTable/scrollOnTable.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export const scrollOnTable = { 3 | data() { 4 | return { 5 | headerTop: 0, 6 | lastSubmenuOpen: null, 7 | }; 8 | }, 9 | mounted() { 10 | document.addEventListener("scroll", (event) => { 11 | this.scrollTopDocument(event); 12 | }); 13 | }, 14 | methods: { 15 | scrollFunction(event) { 16 | this.affixHeader(event, "vueTable"); 17 | 18 | if (this.lastSelectOpen) { 19 | this.calculPosition( 20 | this.lastSelectOpen.event, 21 | this.lastSelectOpen.rowIndex, 22 | this.lastSelectOpen.colIndex, 23 | "dropdown" 24 | ); 25 | } else if (this.lastSubmenuOpen) { 26 | this.calculPosition( 27 | this.lastSubmenuOpen.event, 28 | this.lastSubmenuOpen.rowIndex, 29 | this.lastSubmenuOpen.colIndex, 30 | "contextMenu" 31 | ); 32 | } 33 | }, 34 | scrollTopDocument(event) { 35 | this.affixHeader(event, "document"); 36 | 37 | if (this.lastSelectOpen) { 38 | this.calculPosition( 39 | event, 40 | this.lastSelectOpen.rowIndex, 41 | this.lastSelectOpen.colIndex, 42 | "dropdown" 43 | ); 44 | } else if (this.lastSubmenuOpen) { 45 | this.calculPosition( 46 | event, 47 | this.lastSubmenuOpen.rowIndex, 48 | this.lastSubmenuOpen.colIndex, 49 | "contextMenu" 50 | ); 51 | } 52 | }, 53 | affixHeader(offset, target) { 54 | if ( 55 | this.$refs && 56 | this.$refs[`${this.customTable}-table`] && 57 | this.$refs[`${this.customTable}-table`].offsetTop 58 | ) { 59 | this.scrollDocument = document.querySelector( 60 | `${this.parentScrollElement.attribute}` 61 | ).scrollTop; 62 | const offsetTopVueTable = this.$refs[`${this.customTable}-table`].offsetTop; 63 | const scrollOnDocument = this.scrollDocument || target === "document"; 64 | const offsetEl = scrollOnDocument ? this.scrollDocument : offset.target.scrollTop; 65 | 66 | if (offsetEl > offsetTopVueTable) { 67 | this.headerTop = scrollOnDocument ? offsetEl - offsetTopVueTable : offsetEl - 18; 68 | } else { 69 | this.headerTop = 0; 70 | } 71 | } 72 | }, 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /src/mixins/VueTable/undo.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export const undo = { 3 | data() { 4 | return { 5 | storeUndoData: [], 6 | }; 7 | }, 8 | methods: { 9 | changeData(rowIndex, header) { 10 | const cell = this.value[rowIndex][header]; 11 | 12 | this.storeUndoData.push({ rowIndex, header, cell }); 13 | this.$emit("tbody-change-data", rowIndex, header); 14 | }, 15 | rollBackUndo() { 16 | if (this.storeUndoData.length) { 17 | const lastEdit = this.storeUndoData.pop(); 18 | const previousValue = this.value[lastEdit.rowIndex][lastEdit.header].value; 19 | 20 | this.value[lastEdit.rowIndex][lastEdit.header] = lastEdit.cell.duplicate; 21 | this.$emit("tbody-undo-data", lastEdit.rowIndex, lastEdit.header, previousValue); 22 | } 23 | }, 24 | clearStoreUndo() { 25 | this.storeUndoData = []; 26 | }, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /tests/unit/Tbody/Computed.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils"; 2 | import Tbody from "@/components/TBody/TBody.vue"; 3 | 4 | // data 5 | import exempleData from "@/data"; 6 | 7 | let wrapper; 8 | 9 | beforeEach(() => { 10 | const tbodyData = exempleData.products; 11 | const { headers } = exempleData; 12 | const { tbodyIndex } = exempleData.customOptions; 13 | const { trad } = exempleData.customOptions; 14 | const { disableCells } = exempleData; 15 | const tbodyHighlight = []; 16 | const { tbodyCheckbox } = exempleData; 17 | const { submenuTbody } = exempleData; 18 | const currentTable = Date.now(); 19 | const filteredList = exempleData.products[0].f.selectOptions; 20 | const submenuStatusTbody = true; 21 | 22 | wrapper = mount(Tbody, { 23 | propsData: { 24 | disableCells, 25 | headers, 26 | currentTable, 27 | tbodyCheckbox, 28 | tbodyHighlight, 29 | submenuTbody, 30 | submenuStatusTbody, 31 | filteredList, 32 | tbodyData, 33 | trad, 34 | tbodyIndex, 35 | }, 36 | }); 37 | 38 | return wrapper; 39 | }); 40 | 41 | describe("TBody", () => { 42 | describe("Render component with props", () => { 43 | test("Vue Instance", () => { 44 | expect(wrapper.vm).toBeTruthy(); 45 | }); 46 | }); 47 | 48 | describe("Computed", () => { 49 | test("headerKeys", () => { 50 | const tBody = wrapper.vm; 51 | const headerKeysTest = tBody.headers.map((x) => x.headerKey); 52 | 53 | expect(tBody.headerKeys).toEqual(headerKeysTest); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/unit/Tbody/Data.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils"; 2 | import Tbody from "@/components/TBody/TBody.vue"; 3 | 4 | // data 5 | import exempleData from "@/data"; 6 | 7 | let wrapper; 8 | 9 | beforeEach(() => { 10 | const tbodyData = exempleData.products; 11 | const { headers } = exempleData; 12 | const { tbodyIndex } = exempleData.customOptions; 13 | const { trad } = exempleData.customOptions; 14 | const { disableCells } = exempleData; 15 | const tbodyHighlight = []; 16 | const { tbodyCheckbox } = exempleData; 17 | const { submenuTbody } = exempleData; 18 | const currentTable = Date.now(); 19 | const filteredList = exempleData.products[0].f.selectOptions; 20 | const submenuStatusTbody = true; 21 | 22 | wrapper = mount(Tbody, { 23 | propsData: { 24 | disableCells, 25 | headers, 26 | currentTable, 27 | tbodyCheckbox, 28 | tbodyHighlight, 29 | submenuTbody, 30 | submenuStatusTbody, 31 | filteredList, 32 | tbodyData, 33 | trad, 34 | tbodyIndex, 35 | }, 36 | }); 37 | 38 | return wrapper; 39 | }); 40 | 41 | describe("TBody", () => { 42 | describe("Render component with props", () => { 43 | test("Vue Instance", () => { 44 | expect(wrapper.vm).toBeTruthy(); 45 | }); 46 | }); 47 | 48 | describe("Data", () => { 49 | test("Present Data", () => { 50 | const tBody = wrapper.vm; 51 | 52 | expect(tBody.emptyCell).toEqual(""); 53 | expect(tBody.eventDrag).toBeFalsy(); 54 | // expect(tBody.searchInput).toEqual(""); 55 | expect(tBody.submenuEnableCol).toBeNull(); 56 | expect(tBody.submenuEnableRow).toBeNull(); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/unit/Tbody/Methods.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils"; 2 | import Tbody from "@/components/TBody/TBody.vue"; 3 | 4 | // data 5 | import exempleData from "@/data"; 6 | 7 | let wrapper; 8 | 9 | beforeEach(() => { 10 | const tbodyData = exempleData.products; 11 | const { headers } = exempleData; 12 | const { tbodyIndex } = exempleData.customOptions; 13 | const { trad } = exempleData.customOptions; 14 | const { disableCells } = exempleData; 15 | const tbodyHighlight = []; 16 | const { tbodyCheckbox } = exempleData; 17 | const { submenuTbody } = exempleData; 18 | const currentTable = Date.now(); 19 | const filteredList = exempleData.products[0].f.selectOptions; 20 | const submenuStatusTbody = true; 21 | 22 | wrapper = mount(Tbody, { 23 | propsData: { 24 | disableCells, 25 | headers, 26 | currentTable, 27 | tbodyCheckbox, 28 | tbodyHighlight, 29 | submenuTbody, 30 | submenuStatusTbody, 31 | filteredList, 32 | tbodyData, 33 | trad, 34 | tbodyIndex, 35 | }, 36 | }); 37 | 38 | return wrapper; 39 | }); 40 | 41 | describe("TBody", () => { 42 | describe("Render component with props", () => { 43 | test("Vue Instance", () => { 44 | expect(wrapper.vm).toBeTruthy(); 45 | }); 46 | }); 47 | 48 | describe("Methods", () => { 49 | describe("DisabledEvent", () => { 50 | test("TBODY Disabled Col : false | with disableCells", () => { 51 | const fakeData = { disabled: false }; 52 | 53 | expect(wrapper.vm.disabledEvent(fakeData, "a")).toBeFalsy(); 54 | }); 55 | test("Disabled Col : true | with disableCells", () => { 56 | const fakeData = { disabled: true }; 57 | 58 | expect(wrapper.vm.disabledEvent(fakeData, "a")).toBeTruthy(); 59 | }); 60 | test("Disabled Col : false | without disableCells", () => { 61 | const fakeData = { disabled: false }; 62 | 63 | expect(wrapper.vm.disabledEvent(fakeData, "b")).toBeFalsy(); 64 | }); 65 | test("Disabled Col : true | without disableCells", () => { 66 | const fakeData = { disabled: true }; 67 | 68 | expect(wrapper.vm.disabledEvent(fakeData, "b")).toBeTruthy(); 69 | }); 70 | }); 71 | 72 | describe("handleDownDragToFill", () => { 73 | test("return eventDrag to true", () => { 74 | const tBody = wrapper.vm; 75 | const col = exempleData.products[0].f; 76 | 77 | expect(tBody.eventDrag).toBeFalsy(); 78 | tBody.handleDownDragToFill("", "f", col, 0, 7); 79 | expect(tBody.eventDrag).toBeTruthy(); 80 | }); 81 | 82 | test("emitted tbody-down-dragtofill", () => { 83 | const tBody = wrapper.vm; 84 | const col = exempleData.products[0].f; 85 | 86 | tBody.handleDownDragToFill("", "f", col, 0, 7); 87 | expect(wrapper.emitted("tbody-down-dragtofill")).toBeTruthy(); 88 | }); 89 | 90 | test("not emitted tbody-down-dragtofill", () => { 91 | const tBody = wrapper.vm; 92 | const col = exempleData.products[0].a; 93 | 94 | tBody.handleDownDragToFill("", "a", col, 0, 7); 95 | expect(wrapper.emitted("tbody-down-dragtofill")).toBeFalsy(); 96 | }); 97 | }); 98 | 99 | describe("handleMoveDragToFill", () => { 100 | test("not emitted tbody-move-dragtofill", () => { 101 | const tBody = wrapper.vm; 102 | const col = exempleData.products[0].f; 103 | 104 | tBody.handleMoveDragToFill("", "f", col, 0, 7); 105 | expect(wrapper.emitted("tbody-move-dragtofill")).toBeFalsy(); 106 | }); 107 | 108 | test("emitted tbody-move-dragtofill", () => { 109 | const tBody = wrapper.vm; 110 | const col = exempleData.products[0].f; 111 | 112 | tBody.eventDrag = true; 113 | tBody.handleMoveDragToFill("", "f", col, 0, 7); 114 | expect(wrapper.emitted("tbody-move-dragtofill")).toBeTruthy(); 115 | }); 116 | }); 117 | 118 | describe("handleUpDragToFill", () => { 119 | test("not emitted tbody-up-dragtofill", () => { 120 | const tBody = wrapper.vm; 121 | const col = exempleData.products[0].f; 122 | 123 | tBody.handleUpDragToFill("", "f", col, 0, 7); 124 | expect(wrapper.emitted("tbody-up-dragtofill")).toBeFalsy(); 125 | }); 126 | 127 | test("emitted tbody-up-dragtofill", () => { 128 | const tBody = wrapper.vm; 129 | const col = exempleData.products[0].f; 130 | 131 | tBody.eventDrag = true; 132 | tBody.handleUpDragToFill("", "f", col, 0, 7); 133 | expect(wrapper.emitted("tbody-up-dragtofill")).toBeTruthy(); 134 | expect(Tbody.eventDrag).toBeFalsy(); 135 | }); 136 | }); 137 | 138 | describe("handleClickTd", () => { 139 | test("emitted tbody-td-click", () => { 140 | const tBody = wrapper.vm; 141 | const col = exempleData.products[0].f; 142 | 143 | tBody.handleClickTd("", "f", col, 0, 7); 144 | // expect(tBody.searchInput).toEqual(""); 145 | expect(wrapper.emitted("tbody-td-click")).toBeTruthy(); 146 | }); 147 | }); 148 | 149 | describe("handleDoubleClickTd", () => { 150 | test("not emitted tbody-td-double-click", () => { 151 | const tBody = wrapper.vm; 152 | const col = exempleData.products[0].a; 153 | 154 | tBody.handleDoubleClickTd("", "a", col, 0, 7, "input"); 155 | expect(wrapper.emitted("tbody-td-double-click")).toBeFalsy(); 156 | }); 157 | }); 158 | 159 | describe("handleContextMenuTd", () => { 160 | test("submenuEnableCol / submenuEnableRow", () => { 161 | const tBody = wrapper.vm; 162 | 163 | tBody.handleContextMenuTd("", "a", 0, 7, "input"); 164 | 165 | expect(tBody.submenuEnableCol).toEqual(7); 166 | expect(tBody.submenuEnableRow).toEqual(0); 167 | }); 168 | 169 | test("emitted handle-to-calculate-position", () => { 170 | const tBody = wrapper.vm; 171 | 172 | tBody.handleContextMenuTd("", "a", 0, 7, "input"); 173 | 174 | expect(wrapper.emitted("handle-to-calculate-position")).toBeTruthy(); 175 | }); 176 | 177 | test("emitted submenu-enable", () => { 178 | const tBody = wrapper.vm; 179 | 180 | tBody.handleContextMenuTd("", "a", 0, 7, "input"); 181 | 182 | expect(wrapper.emitted("submenu-enable")).toBeTruthy(); 183 | }); 184 | 185 | test("emitted tbody-td-context-menu", () => { 186 | const tBody = wrapper.vm; 187 | 188 | tBody.handleContextMenuTd("", "a", 0, 7, "input"); 189 | 190 | expect(wrapper.emitted("tbody-td-context-menu")).toBeTruthy(); 191 | }); 192 | }); 193 | 194 | describe("inputHandleChange", () => { 195 | test("emitted tbody-input-change", () => { 196 | const tBody = wrapper.vm; 197 | 198 | tBody.inputHandleChange("", "a", 0, 7, "input"); 199 | 200 | expect(wrapper.emitted("tbody-input-change")).toBeTruthy(); 201 | }); 202 | }); 203 | 204 | describe("handleClickSubmenu", () => { 205 | test("emitted tbody-submenu-click-callback", () => { 206 | const tBody = wrapper.vm; 207 | 208 | tBody.handleClickSubmenu("", "a", 0, 7, "input"); 209 | 210 | expect(wrapper.emitted("tbody-submenu-click-callback")).toBeTruthy(); 211 | }); 212 | }); 213 | 214 | describe("inputHandleKeydow", () => { 215 | test("emitted tbody-input-keydown", () => { 216 | const fakeEvent = { 217 | which: 8, 218 | }; 219 | const tBody = wrapper.vm; 220 | 221 | tBody.inputHandleKeydow(fakeEvent, "a", 0, 7); 222 | 223 | expect(wrapper.emitted("tbody-input-keydown")).toBeTruthy(); 224 | }); 225 | }); 226 | }); 227 | }); 228 | -------------------------------------------------------------------------------- /tests/unit/Thead/Computed.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils"; 2 | import Thead from "@/components/Thead.vue"; 3 | 4 | // data 5 | import exempleData from "@/data"; 6 | 7 | let wrapper; 8 | 9 | beforeEach(() => { 10 | const { headers } = exempleData; 11 | const { tbodyIndex } = exempleData; 12 | const { submenuThead } = exempleData; 13 | const { disableSortThead } = exempleData; 14 | const { sortHeader } = exempleData; 15 | const theadHighlight = []; 16 | const currentTable = Date.now(); 17 | const { tbodyCheckbox } = exempleData; 18 | const submenuStatusThead = true; 19 | const headerTop = 0; 20 | const vueTableHeight = 400; 21 | 22 | wrapper = mount(Thead, { 23 | propsData: { 24 | submenuStatusThead, 25 | theadHighlight, 26 | currentTable, 27 | tbodyCheckbox, 28 | headerTop, 29 | submenuThead, 30 | headers, 31 | disableSortThead, 32 | sortHeader, 33 | tbodyIndex, 34 | vueTableHeight, 35 | }, 36 | }); 37 | 38 | return wrapper; 39 | }); 40 | 41 | describe("THead", () => { 42 | describe("Render component with props", () => { 43 | test("Vue Instance", () => { 44 | expect(wrapper.vm).toBeTruthy(); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/unit/Thead/Data.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils"; 2 | import Thead from "@/components/Thead.vue"; 3 | 4 | // data 5 | import exempleData from "@/data"; 6 | 7 | let wrapper; 8 | 9 | beforeEach(() => { 10 | const { headers } = exempleData; 11 | const { tbodyIndex } = exempleData; 12 | const { submenuThead } = exempleData; 13 | const { disableSortThead } = exempleData; 14 | const { sortHeader } = exempleData; 15 | const theadHighlight = []; 16 | const currentTable = Date.now(); 17 | const { tbodyCheckbox } = exempleData; 18 | const submenuStatusThead = true; 19 | const headerTop = 0; 20 | const vueTableHeight = 400; 21 | 22 | wrapper = mount(Thead, { 23 | propsData: { 24 | submenuStatusThead, 25 | theadHighlight, 26 | currentTable, 27 | tbodyCheckbox, 28 | headerTop, 29 | submenuThead, 30 | headers, 31 | disableSortThead, 32 | sortHeader, 33 | tbodyIndex, 34 | vueTableHeight, 35 | }, 36 | }); 37 | 38 | return wrapper; 39 | }); 40 | 41 | describe("THead", () => { 42 | describe("Render component with props", () => { 43 | test("Vue Instance", () => { 44 | expect(wrapper.vm).toBeTruthy(); 45 | }); 46 | }); 47 | 48 | describe("Data", () => { 49 | test("Present Data", () => { 50 | const tHead = wrapper.vm; 51 | 52 | expect(tHead.eventDrag).toBeFalsy(); 53 | expect(tHead.submenuEnableCol).toBeNull(); 54 | expect(tHead.beforeChangeSize).toEqual({}); 55 | expect(tHead.newSize).toEqual(""); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /tests/unit/Thead/Methods.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils"; 2 | import Thead from "@/components/Thead.vue"; 3 | 4 | // data 5 | import exempleData from "@/data"; 6 | 7 | let wrapper; 8 | 9 | beforeEach(() => { 10 | const { headers } = exempleData; 11 | const { tbodyIndex } = exempleData; 12 | const { submenuThead } = exempleData; 13 | const { disableSortThead } = exempleData; 14 | const { sortHeader } = exempleData; 15 | const theadHighlight = []; 16 | const currentTable = Date.now(); 17 | const { tbodyCheckbox } = exempleData; 18 | const submenuStatusThead = true; 19 | const headerTop = 0; 20 | const vueTableHeight = 400; 21 | 22 | wrapper = mount(Thead, { 23 | propsData: { 24 | submenuStatusThead, 25 | theadHighlight, 26 | currentTable, 27 | tbodyCheckbox, 28 | headerTop, 29 | submenuThead, 30 | headers, 31 | disableSortThead, 32 | sortHeader, 33 | tbodyIndex, 34 | vueTableHeight, 35 | }, 36 | }); 37 | 38 | return wrapper; 39 | }); 40 | 41 | describe("THead", () => { 42 | describe("Render component with props", () => { 43 | test("Vue Instance", () => { 44 | expect(wrapper.vm).toBeTruthy(); 45 | }); 46 | }); 47 | 48 | describe("Methods", () => { 49 | describe("removeClass", () => { 50 | test('return ""', () => { 51 | const tHead = wrapper.vm; 52 | 53 | tHead.headers[0].activeSort = "Z"; 54 | tHead.headers[1].activeSort = "Z"; 55 | tHead.removeClass("activeSort", 1); 56 | expect(tHead.headers[0].activeSort).toEqual(""); 57 | }); 58 | 59 | test("return Z", () => { 60 | const tHead = wrapper.vm; 61 | 62 | tHead.headers[0].activeSort = "Z"; 63 | tHead.headers[1].activeSort = "Z"; 64 | tHead.removeClass("activeSort", 1); 65 | expect(tHead.headers[1].activeSort).toEqual("Z"); 66 | }); 67 | 68 | test("return A", () => { 69 | const tHead = wrapper.vm; 70 | 71 | tHead.headers[0].activeSort = "A"; 72 | tHead.headers[1].activeSort = "A"; 73 | tHead.removeClass("activeSort", 1); 74 | expect(tHead.headers[1].activeSort).toEqual("A"); 75 | }); 76 | }); 77 | 78 | describe("handleDownChangeSize", () => { 79 | test("event drag be true", () => { 80 | const tHead = wrapper.vm; 81 | const head = tHead.headers[1]; 82 | const fakeEvent = { 83 | currentTarget: { 84 | parentElement: { 85 | offsetLeft: 10, 86 | }, 87 | }, 88 | }; 89 | 90 | tHead.handleDownChangeSize(fakeEvent, head, 1); 91 | expect(tHead.eventDrag).toBeTruthy(); 92 | }); 93 | 94 | test("beforeChangeSize exist", () => { 95 | const tHead = wrapper.vm; 96 | const head = tHead.headers[1]; 97 | const fakeEvent = { 98 | currentTarget: { 99 | parentElement: { 100 | offsetLeft: 10, 101 | }, 102 | }, 103 | }; 104 | 105 | tHead.handleDownChangeSize(fakeEvent, head, 1); 106 | 107 | expect(tHead.beforeChangeSize.col).toEqual(1); 108 | expect(tHead.beforeChangeSize.width).toEqual(parseInt(head.style.width, 10)); 109 | }); 110 | 111 | test("current element has style", () => { 112 | const tHead = wrapper.vm; 113 | const head = tHead.headers[1]; 114 | const fakeEvent = { 115 | currentTarget: { 116 | parentElement: { 117 | offsetLeft: 10, 118 | }, 119 | }, 120 | }; 121 | 122 | tHead.handleDownChangeSize(fakeEvent, head, 1); 123 | 124 | expect(tHead.$refs[`resize-${tHead.beforeChangeSize.col}`][0].style.top).toContain("px"); 125 | }); 126 | }); 127 | 128 | describe("handleMoveChangeSize", () => { 129 | test("event drag is true", () => { 130 | const tHead = wrapper.vm; 131 | const head = tHead.headers[1]; 132 | const fakeEvent = { 133 | currentTarget: { 134 | parentElement: { 135 | offsetLeft: 10, 136 | }, 137 | offsetHeight: 0, 138 | offsetParent: { 139 | offsetTop: 0, 140 | }, 141 | }, 142 | }; 143 | 144 | tHead.handleDownChangeSize(fakeEvent, head, 1); 145 | tHead.handleMoveChangeSize(fakeEvent); 146 | expect(tHead.eventDrag).toBeFalsy(); 147 | }); 148 | 149 | test("event drag is false", () => { 150 | const tHead = wrapper.vm; 151 | const fakeEvent = { 152 | currentTarget: { 153 | offsetHeight: 0, 154 | offsetParent: { 155 | offsetTop: 0, 156 | }, 157 | }, 158 | }; 159 | 160 | expect(tHead.handleMoveChangeSize(fakeEvent)).toBeFalsy(); 161 | expect(tHead.eventDrag).toBeFalsy(); 162 | }); 163 | }); 164 | 165 | describe("handleUpDragToFill", () => { 166 | test("event drag are false", () => { 167 | const tHead = wrapper.vm; 168 | const fakeEvent = { 169 | currentTarget: { 170 | parentElement: { 171 | offsetLeft: 10, 172 | }, 173 | }, 174 | }; 175 | 176 | expect(tHead.handleUpDragToFill(fakeEvent)).toBeFalsy(); 177 | expect(tHead.eventDrag).toBeFalsy(); 178 | expect(wrapper.emitted("handle-up-drag-size-header")).toBeFalsy(); 179 | }); 180 | }); 181 | 182 | describe("handleSort", () => { 183 | test('send "" return A', () => { 184 | const tHead = wrapper.vm; 185 | const head = tHead.headers[3]; 186 | 187 | tHead.headers[1].activeSort = "Z"; 188 | tHead.handleSort("", head, 3); 189 | 190 | expect(tHead.headers[3].activeSort).toEqual("A"); 191 | expect(tHead.headers[1].activeSort).toEqual(""); 192 | expect(wrapper.emitted("thead-td-sort")).toBeTruthy(); 193 | }); 194 | 195 | test("send Z return A", () => { 196 | const tHead = wrapper.vm; 197 | const head = tHead.headers[3]; 198 | 199 | head.activeSort = "Z"; 200 | tHead.headers[1].activeSort = "Z"; 201 | tHead.handleSort("", head, 3); 202 | 203 | expect(tHead.headers[3].activeSort).toEqual("A"); 204 | expect(tHead.headers[1].activeSort).toEqual(""); 205 | expect(wrapper.emitted("thead-td-sort")).toBeTruthy(); 206 | }); 207 | 208 | test("send A return Z", () => { 209 | const tHead = wrapper.vm; 210 | const head = tHead.headers[1]; 211 | 212 | head.activeSort = "A"; 213 | tHead.handleSort("", head, 1); 214 | 215 | expect(tHead.headers[1].activeSort).toEqual("Z"); 216 | expect(wrapper.emitted("thead-td-sort")).toBeTruthy(); 217 | }); 218 | }); 219 | 220 | describe("handleContextMenuTd", () => { 221 | test("expect submenuEnableCol", () => { 222 | const tHead = wrapper.vm; 223 | 224 | tHead.handleContextMenuTd("", "e", 3); 225 | 226 | expect(tHead.submenuEnableCol).toEqual(3); 227 | expect(wrapper.emitted("thead-td-context-menu")).toBeTruthy(); 228 | }); 229 | 230 | test("submenuStatusThead = true", () => { 231 | const tHead = wrapper.vm; 232 | 233 | tHead.submenuStatusThead = true; 234 | tHead.handleContextMenuTd("", "e", 3); 235 | 236 | expect(tHead.submenuEnableCol).toEqual(3); 237 | expect(tHead.submenuStatusThead).toBeTruthy(); 238 | expect(wrapper.emitted("thead-td-context-menu")).toBeTruthy(); 239 | expect(wrapper.emitted("submenu-enable")).toEqual([["tbody"]]); 240 | }); 241 | 242 | test("submenuStatusThead = false", () => { 243 | const { headers } = exempleData; 244 | const { tbodyIndex } = exempleData; 245 | const { submenuThead } = exempleData; 246 | const { disableSortThead } = exempleData; 247 | const { sortHeader } = exempleData; 248 | const submenuStatusThead = false; 249 | const vueTableHeight = 400; 250 | const headerTop = 0; 251 | const theadHighlight = []; 252 | const currentTable = Date.now(); 253 | const { tbodyCheckbox } = exempleData; 254 | 255 | wrapper = mount(Thead, { 256 | propsData: { 257 | submenuStatusThead, 258 | theadHighlight, 259 | currentTable, 260 | tbodyCheckbox, 261 | submenuThead, 262 | headers, 263 | disableSortThead, 264 | sortHeader, 265 | tbodyIndex, 266 | vueTableHeight, 267 | headerTop, 268 | }, 269 | }); 270 | 271 | const tHead = wrapper.vm; 272 | 273 | tHead.handleContextMenuTd("", "e", 3); 274 | 275 | expect(tHead.submenuEnableCol).toEqual(3); 276 | expect(tHead.submenuStatusThead).toBeFalsy(); 277 | expect(wrapper.emitted("thead-td-context-menu")).toBeTruthy(); 278 | expect(wrapper.emitted("submenu-enable")).toEqual([["thead"]]); 279 | }); 280 | }); 281 | 282 | describe("handleClickSubmenu", () => { 283 | test("Without selectOptions", () => { 284 | const tHead = wrapper.vm; 285 | 286 | tHead.handleClickSubmenu("", "e", 3, "change-color", undefined); 287 | 288 | expect(wrapper.emitted("thead-submenu-click-callback")).toBeTruthy(); 289 | expect(wrapper.emitted("thead-submenu-click-callback")).toEqual([ 290 | ["", "e", 3, "change-color"], 291 | ]); 292 | }); 293 | 294 | test("With selectOptions", () => { 295 | const tHead = wrapper.vm; 296 | 297 | tHead.handleClickSubmenu("", "e", 3, "change-color", ["e"]); 298 | 299 | expect(wrapper.emitted("thead-submenu-click-callback")).toBeTruthy(); 300 | expect(wrapper.emitted("thead-submenu-click-callback")).toEqual([ 301 | ["", "e", 3, "change-color", ["e"]], 302 | ]); 303 | }); 304 | }); 305 | }); 306 | }); 307 | -------------------------------------------------------------------------------- /tests/unit/VSelect/Data.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils"; 2 | import VSelect from "@/components/TBody/VSelect.vue"; 3 | 4 | // data 5 | import exempleData from "@/data"; 6 | 7 | let wrapper; 8 | 9 | beforeEach(() => { 10 | const colIndex = 1; 11 | const currentTable = Date.now(); 12 | 13 | const disabledEvent = (col, header) => { 14 | if (col.disabled === undefined) { 15 | return this.disableCells.some((x) => x === header); 16 | } 17 | 18 | return col.disabled; 19 | }; 20 | 21 | const filteredList = exempleData.products[0].f.selectOptions; 22 | const header = "f"; 23 | const row = exempleData.products[0]; 24 | const rowIndex = 2; 25 | const { trad } = exempleData.customOptions; 26 | 27 | wrapper = mount(VSelect, { 28 | propsData: { 29 | colIndex, 30 | currentTable, 31 | disabledEvent, 32 | filteredList, 33 | header, 34 | row, 35 | rowIndex, 36 | trad, 37 | }, 38 | }); 39 | 40 | return wrapper; 41 | }); 42 | 43 | describe("VSelect", () => { 44 | describe("Render component with props", () => { 45 | test("Vue Instance", () => { 46 | expect(wrapper.vm).toBeTruthy(); 47 | }); 48 | }); 49 | 50 | describe("Data", () => { 51 | test("Present Data", () => { 52 | const select = wrapper.vm; 53 | 54 | expect(select.searchInput).toEqual(""); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/unit/VSelect/Methods.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils"; 2 | import VSelect from "@/components/TBody/VSelect.vue"; 3 | 4 | // data 5 | import exempleData from "@/data"; 6 | 7 | let wrapper; 8 | const colIndex = 1; 9 | const currentTable = Date.now(); 10 | 11 | let disabledEvent = () => { 12 | return false; 13 | }; 14 | 15 | const header = "f"; 16 | const filteredList = exempleData.products[0].f.selectOptions; 17 | const row = exempleData.products[0]; 18 | const rowIndex = 2; 19 | const { trad } = exempleData.customOptions; 20 | 21 | beforeEach(() => { 22 | wrapper = mount(VSelect, { 23 | propsData: { 24 | colIndex, 25 | currentTable, 26 | disabledEvent, 27 | filteredList, 28 | header, 29 | row, 30 | rowIndex, 31 | trad, 32 | }, 33 | }); 34 | 35 | return wrapper; 36 | }); 37 | 38 | describe("VSelect", () => { 39 | describe("Render component with props", () => { 40 | test("Vue Instance", () => { 41 | expect(wrapper.vm).toBeTruthy(); 42 | }); 43 | }); 44 | 45 | describe("enableSelect", () => { 46 | test("emitted tbody-handle-to-open-select", () => { 47 | const select = wrapper.vm; 48 | 49 | select.enableSelect("", header, row.f, rowIndex, colIndex); 50 | 51 | expect(wrapper.emitted("tbody-handle-to-open-select")).toBeTruthy(); 52 | }); 53 | 54 | test("not emitted tbody-handle-to-open-select", () => { 55 | disabledEvent = () => { 56 | return true; 57 | }; 58 | 59 | const newWrapper = mount(VSelect, { 60 | propsData: { 61 | colIndex, 62 | currentTable, 63 | disabledEvent, 64 | filteredList, 65 | header, 66 | row, 67 | rowIndex, 68 | trad, 69 | }, 70 | }); 71 | const select = newWrapper.vm; 72 | 73 | select.enableSelect("", header, row, rowIndex, colIndex); 74 | 75 | expect(wrapper.emitted("tbody-handle-to-open-select")).toBeFalsy(); 76 | }); 77 | }); 78 | 79 | describe("selectHandleChange", () => { 80 | test("emitted tbody-select-change", () => { 81 | disabledEvent = () => { 82 | return false; 83 | }; 84 | 85 | const fakeEvent = { 86 | target: { 87 | value: "", 88 | }, 89 | }; 90 | const select = wrapper.vm; 91 | 92 | select.selectHandleChange(fakeEvent, header, row, "", rowIndex, colIndex); 93 | 94 | expect(wrapper.emitted("tbody-handle-select-change")).toBeTruthy(); 95 | }); 96 | }); 97 | 98 | describe("handleSearchInputSelect", () => { 99 | test("emitted tbody-handle-search-input-select", () => { 100 | const select = wrapper.vm; 101 | 102 | select.handleSearchInputSelect("", header, row, "f", rowIndex, colIndex); 103 | 104 | expect(wrapper.emitted("tbody-handle-search-input-select")).toBeTruthy(); 105 | }); 106 | 107 | test("not emitted tbody-handle-search-input-select", () => { 108 | disabledEvent = () => { 109 | return true; 110 | }; 111 | 112 | const newWrapper = mount(VSelect, { 113 | propsData: { 114 | colIndex, 115 | currentTable, 116 | disabledEvent, 117 | filteredList, 118 | header, 119 | row, 120 | rowIndex, 121 | trad, 122 | }, 123 | }); 124 | const select = newWrapper.vm; 125 | 126 | select.handleSearchInputSelect("", header, row, "f", rowIndex, colIndex); 127 | 128 | expect(wrapper.emitted("tbody-handle-search-input-select")).toBeFalsy(); 129 | }); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /tests/unit/VueTable/Computed.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils"; 2 | import VueTable from "@/components/VueTable.vue"; 3 | 4 | // data 5 | import exempleData from "@/data"; 6 | 7 | let wrapper; 8 | 9 | beforeEach(() => { 10 | const value = exempleData.products; 11 | const { headers } = exempleData; 12 | const { customOptions } = exempleData; 13 | const { styleWrapVueTable } = exempleData; 14 | const { disableCells } = exempleData; 15 | const { disableSortThead } = exempleData; 16 | const { loading } = exempleData; 17 | const { parentScrollElement } = exempleData; 18 | const { selectPosition } = exempleData; 19 | const { submenuTbody } = exempleData; 20 | const { submenuThead } = exempleData; 21 | 22 | wrapper = mount(VueTable, { 23 | propsData: { 24 | value, 25 | headers, 26 | customOptions, 27 | styleWrapVueTable, 28 | disableCells, 29 | disableSortThead, 30 | loading, 31 | parentScrollElement, 32 | selectPosition, 33 | submenuTbody, 34 | submenuThead, 35 | }, 36 | }); 37 | 38 | return wrapper; 39 | }); 40 | 41 | describe("VueTable", () => { 42 | describe("Render component with props", () => { 43 | test("Vue Instance", () => { 44 | expect(wrapper.vm).toBeTruthy(); 45 | }); 46 | }); 47 | 48 | describe("Computed", () => { 49 | test("colHeaderWidths", () => { 50 | const { colHeaderWidths } = wrapper.vm; 51 | const colHeaderWidthsTest = wrapper.vm.headers.map((x) => parseInt(x.style.width, 10)); 52 | 53 | expect(colHeaderWidths).toEqual(colHeaderWidthsTest); 54 | }); 55 | 56 | test("filteredList empty", () => { 57 | const { filteredList } = wrapper.vm; 58 | 59 | expect(filteredList).toEqual([]); 60 | }); 61 | 62 | test("filteredList not empty", () => { 63 | wrapper.vm.lastSelectOpen = { 64 | col: { 65 | selectOptions: [ 66 | { 67 | value: "abcd efgh", 68 | label: "abcd efgh", 69 | }, 70 | { 71 | value: "ijkl mnop", 72 | label: "ijkl mnop", 73 | }, 74 | ], 75 | }, 76 | searchValue: "abcd", 77 | }; 78 | const { filteredList } = wrapper.vm; 79 | 80 | expect(filteredList[0].item.value).toEqual("abcd efgh"); 81 | }); 82 | 83 | test("headerKeys", () => { 84 | const { headerKeys } = wrapper.vm; 85 | const headerKeysTest = wrapper.vm.headers.map((header) => header.headerKey); 86 | 87 | expect(headerKeys).toEqual(headerKeysTest); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /tests/unit/VueTable/Data.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils"; 2 | import VueTable from "@/components/VueTable.vue"; 3 | 4 | // data 5 | import exempleData from "@/data"; 6 | 7 | let wrapper; 8 | 9 | beforeEach(() => { 10 | const value = exempleData.products; 11 | const { headers } = exempleData; 12 | const { customOptions } = exempleData; 13 | const { styleWrapVueTable } = exempleData; 14 | const { disableCells } = exempleData; 15 | const { disableSortThead } = exempleData; 16 | const { loading } = exempleData; 17 | const { parentScrollElement } = exempleData; 18 | const { selectPosition } = exempleData; 19 | const { submenuTbody } = exempleData; 20 | const { submenuThead } = exempleData; 21 | 22 | wrapper = mount(VueTable, { 23 | propsData: { 24 | value, 25 | headers, 26 | customOptions, 27 | styleWrapVueTable, 28 | disableCells, 29 | disableSortThead, 30 | loading, 31 | parentScrollElement, 32 | selectPosition, 33 | submenuTbody, 34 | submenuThead, 35 | }, 36 | }); 37 | 38 | return wrapper; 39 | }); 40 | 41 | describe("VueTable", () => { 42 | describe("Render component with props", () => { 43 | test("Vue Instance", () => { 44 | expect(wrapper.vm).toBeTruthy(); 45 | }); 46 | }); 47 | 48 | describe("Data", () => { 49 | test("Present Data", () => { 50 | const vueTable = wrapper.vm; 51 | 52 | expect(vueTable.disableKeyTimeout).toBeNull(); 53 | expect(vueTable.eventDrag).toBeFalsy(); 54 | expect(vueTable.incrementCol).toEqual(0); 55 | expect(vueTable.incrementOption).toBeNull(); 56 | expect(vueTable.incrementRow).toBeNull(); 57 | expect(vueTable.keys).toEqual({}); 58 | expect(vueTable.lastSelectOpen).toBeNull(); 59 | expect(vueTable.lastSubmenuOpen).toBeNull(); 60 | expect(vueTable.oldTdActive).toBeNull(); 61 | expect(vueTable.oldTdShow).toBeNull(); 62 | expect(vueTable.pressedShift).toEqual(0); 63 | expect(vueTable.rectangleSelectedCell).toBeNull(); 64 | expect(vueTable.scrollDocument).toBeNull(); 65 | expect(vueTable.scrollToSelectTimeout).toBeNull(); 66 | expect(vueTable.selectedCell).toBeNull(); 67 | expect(vueTable.selectedCoordCells).toBeNull(); 68 | expect(vueTable.selectedCoordCopyCells).toBeNull(); 69 | expect(vueTable.selectedMultipleCell).toBeFalsy(); 70 | expect(vueTable.selectedMultipleCellActive).toBeFalsy(); 71 | expect(vueTable.setFirstCell).toBeFalsy(); 72 | expect(vueTable.storeCopyDatas).toEqual([]); 73 | expect(vueTable.storeRectangleSelection).toEqual([]); 74 | expect(vueTable.submenuStatusTbody).toBeFalsy(); 75 | expect(vueTable.submenuStatusThead).toBeFalsy(); 76 | expect(vueTable.storeUndoData).toEqual([]); 77 | expect(vueTable.headerTop).toEqual(0); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /tests/unit/VueTable/Methods.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils"; 2 | import VueTable from "@/components/VueTable.vue"; 3 | 4 | // data 5 | import exempleData from "@/data"; 6 | 7 | let wrapper; 8 | 9 | beforeEach(() => { 10 | const value = exempleData.products; 11 | const { headers } = exempleData; 12 | const { customOptions } = exempleData; 13 | const { styleWrapVueTable } = exempleData; 14 | const { disableCells } = exempleData; 15 | const { disableSortThead } = exempleData; 16 | const { loading } = exempleData; 17 | const { parentScrollElement } = exempleData; 18 | const { selectPosition } = exempleData; 19 | const { submenuTbody } = exempleData; 20 | const { submenuThead } = exempleData; 21 | 22 | wrapper = mount(VueTable, { 23 | propsData: { 24 | value, 25 | headers, 26 | customOptions, 27 | styleWrapVueTable, 28 | disableCells, 29 | disableSortThead, 30 | loading, 31 | parentScrollElement, 32 | selectPosition, 33 | submenuTbody, 34 | submenuThead, 35 | }, 36 | }); 37 | 38 | return wrapper; 39 | }); 40 | 41 | describe("VueTable", () => { 42 | describe("Render component with props", () => { 43 | test("Vue Instance", () => { 44 | expect(wrapper.vm).toBeTruthy(); 45 | }); 46 | }); 47 | 48 | describe("createdCell", () => { 49 | test("return newData", () => { 50 | const tBody = wrapper.vm; 51 | const { newData } = exempleData.customOptions; 52 | 53 | expect(tBody.customOptions.newData).toEqual(newData); 54 | }); 55 | 56 | test("return newProduct", () => { 57 | const tBody = wrapper.vm; 58 | const { newData } = exempleData.customOptions; 59 | const newHeader = { 60 | headerName: "H", 61 | headerKey: "h", 62 | style: { 63 | width: "200px", 64 | minWidth: "200px", 65 | }, 66 | }; 67 | 68 | tBody.headers.push(newHeader); 69 | tBody.headerKeys.push("h"); 70 | 71 | expect(tBody.headerKeys.find((x) => x === "h")).toBeTruthy(); 72 | expect(tBody.customOptions.newData).toEqual(newData); 73 | expect(tBody.value[0].h).toBeUndefined(); 74 | 75 | tBody.createdCell(); 76 | 77 | expect(tBody.value[0].h).toBeTruthy(); 78 | expect(tBody.value[1].h).toBeTruthy(); 79 | expect(tBody.value[2].h).toBeTruthy(); 80 | expect(tBody.value[3].h).toBeTruthy(); 81 | expect(tBody.value[4].h).toBeTruthy(); 82 | }); 83 | }); 84 | 85 | describe("disabledEvent", () => { 86 | test("Disabled Col : false | with disableCells", () => { 87 | const fakeData = { disabled: false }; 88 | 89 | expect(wrapper.vm.disabledEvent(fakeData, "a")).toBeFalsy(); 90 | }); 91 | test("Disabled Col : true | with disableCells", () => { 92 | const fakeData = { disabled: true }; 93 | 94 | expect(wrapper.vm.disabledEvent(fakeData, "a")).toBeTruthy(); 95 | }); 96 | test("Disabled Col : undefined | with disableCells", () => { 97 | const fakeData = {}; 98 | 99 | expect(wrapper.vm.disabledEvent(fakeData, "a")).toBeTruthy(); 100 | }); 101 | test("Disabled Col : false | without disableCells", () => { 102 | const fakeData = { disabled: false }; 103 | 104 | expect(wrapper.vm.disabledEvent(fakeData, "b")).toBeFalsy(); 105 | }); 106 | test("Disabled Col : true | without disableCells", () => { 107 | const fakeData = { disabled: true }; 108 | 109 | expect(wrapper.vm.disabledEvent(fakeData, "b")).toBeTruthy(); 110 | }); 111 | test("Disabled Col : undefined | without disableCells", () => { 112 | const fakeData = {}; 113 | 114 | expect(wrapper.vm.disabledEvent(fakeData, "b")).toBeFalsy(); 115 | }); 116 | }); 117 | 118 | describe("updateSelectedCell", () => { 119 | const row = 0; 120 | const col = 0; 121 | const header = "a"; 122 | 123 | test("Check selectedCell", () => { 124 | wrapper.vm.updateSelectedCell(header, row, col); 125 | expect(wrapper.vm.selectedCell.header).toEqual(header); 126 | expect(wrapper.vm.selectedCell.row).toEqual(row); 127 | expect(wrapper.vm.selectedCell.col).toEqual(col); 128 | }); 129 | 130 | test("setFirstCell = false", () => { 131 | wrapper.vm.setFirstCell = false; 132 | wrapper.vm.updateSelectedCell(header, row, col); 133 | expect(wrapper.vm.setFirstCell).toBeTruthy(); 134 | expect(wrapper.vm.value[row][header].rectangleSelection).toBeTruthy(); 135 | }); 136 | test("setFirstCell = true", () => { 137 | wrapper.vm.setFirstCell = true; 138 | wrapper.vm.updateSelectedCell(header, row, col); 139 | expect(wrapper.vm.setFirstCell).toBeTruthy(); 140 | expect(wrapper.vm.value[row][header].rectangleSelection).toBeTruthy(); 141 | }); 142 | }); 143 | 144 | describe("enableSelect", () => { 145 | const rowIndex = 0; 146 | const colIndex = 5; 147 | const header = "f"; 148 | 149 | test("search true", () => { 150 | const col = { 151 | search: true, 152 | }; 153 | 154 | wrapper.vm.enableSelect("", header, col, rowIndex, colIndex); 155 | expect(wrapper.vm.value[rowIndex][header].search).toBeFalsy(); 156 | expect(wrapper.vm.value[rowIndex][header].show).toBeFalsy(); 157 | expect(wrapper.vm.lastSelectOpen).toBeNull(); 158 | }); 159 | }); 160 | 161 | describe("showDropdown", () => { 162 | test("search true", () => { 163 | const rowIndex = 0; 164 | const colIndex = 5; 165 | 166 | wrapper.vm.showDropdown(colIndex, rowIndex); 167 | }); 168 | }); 169 | 170 | describe("handleTbodySelectChange", () => { 171 | test("return emit tbody-select-change", () => { 172 | const tBody = wrapper.vm; 173 | const data = tBody.value[0].f; 174 | const fakeEvent = { 175 | keyCode: 99, 176 | }; 177 | const option = { 178 | active: true, 179 | label: "hagrid", 180 | value: "Hagrid", 181 | }; 182 | 183 | tBody.handleTbodySelectChange(fakeEvent, "f", data, option, 0, 5); 184 | expect(wrapper.emitted("tbody-select-change")).toBeTruthy(); 185 | expect(wrapper.emitted("tbody-select-change")).toEqual([ 186 | [fakeEvent, "f", data, option, 0, 5], 187 | ]); 188 | }); 189 | 190 | test("return emit tbody-change-data", () => { 191 | const tBody = wrapper.vm; 192 | const data = tBody.value[1].f; 193 | const fakeEvent = { 194 | keyCode: 99, 195 | }; 196 | const option = { 197 | active: true, 198 | label: "hagrid", 199 | value: "Hagrid", 200 | }; 201 | 202 | tBody.handleTbodySelectChange(fakeEvent, "f", data, option, 0, 5); 203 | expect(wrapper.emitted("tbody-change-data")).toBeTruthy(); 204 | expect(wrapper.emitted("tbody-change-data")).toEqual([[0, "f"]]); 205 | }); 206 | 207 | test("return currentData", () => { 208 | const tBody = wrapper.vm; 209 | const data = tBody.value[0].f; 210 | const fakeEvent = { 211 | keyCode: 99, 212 | }; 213 | const option = { 214 | active: true, 215 | label: "hagrid", 216 | value: "Hagrid", 217 | }; 218 | 219 | tBody.handleTbodySelectChange(fakeEvent, "f", data, option, 0, 5); 220 | 221 | expect(data.search).toBeFalsy(); 222 | expect(data.show).toBeFalsy(); 223 | expect(data.value).toEqual(option.value); 224 | 225 | expect(data.selectOptions.find((x) => x.value === option.value).active).toBeTruthy(); 226 | 227 | expect(wrapper.emitted("tbody-select-change")).toBeTruthy(); 228 | expect(wrapper.emitted("tbody-change-data")).toBeTruthy(); 229 | }); 230 | 231 | test("return currentData.show: false", () => { 232 | const tBody = wrapper.vm; 233 | const data = tBody.value[0].f; 234 | const fakeEvent = { 235 | keyCode: 99, 236 | }; 237 | const option = { 238 | active: true, 239 | label: "hagrid", 240 | value: "Hagrid", 241 | }; 242 | 243 | tBody.oldTdShow = { key: "f", row: 0, col: 6 }; 244 | tBody.handleTbodySelectChange(fakeEvent, "f", data, option, 0, 5); 245 | 246 | expect(data.search).toBeFalsy(); 247 | expect(data.show).toBeFalsy(); 248 | expect(data.value).toEqual(option.value); 249 | expect(tBody.value[tBody.oldTdShow.row][tBody.oldTdShow.key].show).toBeFalsy(); 250 | 251 | expect(data.selectOptions.find((x) => x.value === option.value).active).toBeTruthy(); 252 | 253 | expect(wrapper.emitted("tbody-select-change")).toBeTruthy(); 254 | expect(wrapper.emitted("tbody-change-data")).toBeTruthy(); 255 | }); 256 | }); 257 | 258 | describe("setOldValueOnInputSelect", () => { 259 | test("return false", () => { 260 | const rowIndex = 0; 261 | const colIndex = 7; 262 | const header = "f"; 263 | const data = wrapper.vm.value[rowIndex][header]; 264 | const { type } = data; 265 | 266 | data.show = true; 267 | data.search = true; 268 | wrapper.vm.setOldValueOnInputSelect(data, rowIndex, header, colIndex, type); 269 | expect(data.show).toBeFalsy(); 270 | expect(data.search).toBeFalsy(); 271 | }); 272 | }); 273 | 274 | describe("enableSubmenu", () => { 275 | test("params Thead", () => { 276 | wrapper.vm.submenuStatusThead = false; 277 | wrapper.vm.submenuStatusTbody = true; 278 | wrapper.vm.enableSubmenu("thead"); 279 | expect(wrapper.vm.submenuStatusThead).toBeTruthy(); 280 | expect(wrapper.vm.submenuStatusTbody).toBeFalsy(); 281 | }); 282 | 283 | test("params Tbody", () => { 284 | wrapper.vm.submenuStatusThead = true; 285 | wrapper.vm.submenuStatusTbody = false; 286 | wrapper.vm.enableSubmenu("tbody"); 287 | expect(wrapper.vm.submenuStatusThead).toBeFalsy(); 288 | expect(wrapper.vm.submenuStatusTbody).toBeTruthy(); 289 | }); 290 | 291 | test("no Params", () => { 292 | wrapper.vm.submenuStatusThead = true; 293 | wrapper.vm.submenuStatusTbody = true; 294 | wrapper.vm.enableSubmenu(); 295 | expect(wrapper.vm.submenuStatusThead).toBeFalsy(); 296 | expect(wrapper.vm.submenuStatusTbody).toBeFalsy(); 297 | }); 298 | }); 299 | 300 | describe("bindClassActiveOnTd", () => { 301 | const row = 0; 302 | const col = 0; 303 | const header = "a"; 304 | 305 | test("show => be false | active => be truthly", () => { 306 | wrapper.vm.value[row][header].show = true; 307 | wrapper.vm.value[row][header].active = false; 308 | wrapper.vm.bindClassActiveOnTd(header, row, col); 309 | expect(wrapper.vm.value[row][header].show).toBeFalsy(); 310 | expect(wrapper.vm.value[row][header].active).toBeTruthy(); 311 | }); 312 | test("oldTdActive equal to actualTd", () => { 313 | wrapper.vm.bindClassActiveOnTd(header, row, col); 314 | expect(wrapper.vm.oldTdActive.key).toEqual(header); 315 | expect(wrapper.vm.oldTdActive.row).toEqual(row); 316 | expect(wrapper.vm.oldTdActive.col).toEqual(col); 317 | }); 318 | }); 319 | 320 | describe("RemoveClass", () => { 321 | test("Keys to be falsy", () => { 322 | const fakeParams = [ 323 | "selected", 324 | "rectangleSelection", 325 | "active", 326 | "show", 327 | "search", 328 | "typing", 329 | "stateCopy", 330 | ]; 331 | // Add keys to true 332 | const data = wrapper.vm.value[0].a; 333 | 334 | data.rectangleSelection = false; 335 | data.active = true; 336 | data.show = true; 337 | data.search = true; 338 | data.typing = true; 339 | data.stateCopy = true; 340 | 341 | wrapper.vm.removeClass(fakeParams); 342 | 343 | // Expect keys are false 344 | Object.values(wrapper.vm.value[0]).forEach((value) => { 345 | const dataCompare = value; 346 | 347 | expect(dataCompare.selected).toBeFalsy(); 348 | expect(dataCompare.rectangleSelection).toBeFalsy(); 349 | expect(dataCompare.active).toBeFalsy(); 350 | expect(dataCompare.show).toBeFalsy(); 351 | expect(dataCompare.search).toBeFalsy(); 352 | expect(dataCompare.typing).toBeFalsy(); 353 | expect(dataCompare.stateCopy).toBeFalsy(); 354 | }); 355 | }); 356 | 357 | test("selectedMultipleCellActive to be falsy", () => { 358 | wrapper.vm.selectedMultipleCellActive = true; 359 | wrapper.vm.removeClass(["selected"]); 360 | expect(wrapper.vm.selectedMultipleCellActive).toBeFalsy(); 361 | }); 362 | 363 | test("rectangleSelection to be falsy", () => { 364 | wrapper.vm.setFirstCell = true; 365 | wrapper.vm.removeClass(["rectangleSelection"]); 366 | expect(wrapper.vm.setFirstCell).toBeFalsy(); 367 | }); 368 | }); 369 | 370 | describe("copyStoreData", () => { 371 | // 'drag / 'copy' 372 | test("Copy one cell", () => { 373 | const vueTable = wrapper.vm; 374 | const rowIndex = 0; 375 | const colIndex = 7; 376 | const header = "f"; 377 | const data = vueTable.value[rowIndex][header]; 378 | 379 | vueTable.selectedCell = { 380 | header, 381 | row: rowIndex, 382 | col: colIndex, 383 | }; 384 | 385 | vueTable.copyStoreData("copy"); 386 | expect(vueTable.storeCopyDatas[0].stateCopy).toBeFalsy(); 387 | data.stateCopy = false; 388 | expect(vueTable.copyMultipleCell).toBeFalsy(); 389 | }); 390 | 391 | test("Copy multiple Col One Row", () => { 392 | const vueTable = wrapper.vm; 393 | const rowIndex = 0; 394 | const colIndex = 7; 395 | const header = "f"; 396 | const multipleProduct = { 397 | colEnd: 3, 398 | colStart: 2, 399 | keyEnd: "d", 400 | keyStart: "c", 401 | rowEnd: 4, 402 | rowStart: 4, 403 | }; 404 | 405 | vueTable.selectedCell = { 406 | header, 407 | row: rowIndex, 408 | col: colIndex, 409 | }; 410 | vueTable.selectedCoordCells = multipleProduct; 411 | 412 | const col1 = vueTable.value[multipleProduct.rowStart][multipleProduct.keyStart]; 413 | const col2 = vueTable.value[multipleProduct.rowEnd][multipleProduct.keyEnd]; 414 | 415 | vueTable.selectedMultipleCell = true; 416 | vueTable.copyStoreData("copy"); 417 | expect(vueTable.storeCopyDatas[0][multipleProduct.keyStart].stateCopy).toBeFalsy(); 418 | expect(vueTable.storeCopyDatas[0][multipleProduct.keyEnd].stateCopy).toBeFalsy(); 419 | 420 | vueTable.storeCopyDatas[0][multipleProduct.keyStart].stateCopy = false; 421 | col1.selected = false; 422 | col1.active = false; 423 | col1.stateCopy = false; 424 | col2.selected = false; 425 | col2.active = false; 426 | col2.stateCopy = false; 427 | 428 | expect(vueTable.storeCopyDatas[0][multipleProduct.keyStart]).toEqual(col1); 429 | expect(vueTable.storeCopyDatas[0][multipleProduct.keyEnd]).toEqual(col2); 430 | 431 | expect(vueTable.copyMultipleCell).toBeTruthy(); 432 | }); 433 | 434 | test("Copy multiple Col Multiple Row", () => { 435 | const rowIndex = 0; 436 | const colIndex = 7; 437 | const header = "f"; 438 | const multipleProduct = { 439 | colEnd: 3, 440 | colStart: 2, 441 | keyEnd: "d", 442 | keyStart: "c", 443 | rowEnd: 5, 444 | rowStart: 4, 445 | }; 446 | 447 | wrapper.vm.selectedCell = { 448 | header, 449 | row: rowIndex, 450 | col: colIndex, 451 | }; 452 | wrapper.vm.selectedCoordCells = multipleProduct; 453 | 454 | const product1Col1 = wrapper.vm.value[multipleProduct.rowStart][multipleProduct.keyStart]; 455 | const product1Col2 = wrapper.vm.value[multipleProduct.rowStart][multipleProduct.keyEnd]; 456 | const product2Col1 = wrapper.vm.value[multipleProduct.rowEnd][multipleProduct.keyStart]; 457 | const product2Col2 = wrapper.vm.value[multipleProduct.rowEnd][multipleProduct.keyEnd]; 458 | 459 | wrapper.vm.selectedMultipleCell = true; 460 | wrapper.vm.copyStoreData("copy"); 461 | 462 | expect(wrapper.vm.storeCopyDatas.length).toEqual(2); 463 | expect(Object.values(wrapper.vm.storeCopyDatas).length).toEqual(2); 464 | 465 | product1Col1.stateCopy = false; 466 | product1Col2.stateCopy = false; 467 | 468 | product2Col1.selected = false; 469 | product2Col1.stateCopy = false; 470 | product2Col1.active = false; 471 | 472 | product2Col2.selected = false; 473 | product2Col2.stateCopy = false; 474 | product2Col2.active = false; 475 | 476 | expect(wrapper.vm.storeCopyDatas[0][multipleProduct.keyStart]).toEqual(product1Col1); 477 | expect(wrapper.vm.storeCopyDatas[0][multipleProduct.keyEnd]).toEqual(product1Col2); 478 | 479 | expect(wrapper.vm.storeCopyDatas[1][multipleProduct.keyStart]).toEqual(product2Col1); 480 | expect(wrapper.vm.storeCopyDatas[1][multipleProduct.keyEnd]).toEqual(product2Col2); 481 | 482 | expect(wrapper.vm.copyMultipleCell).toBeTruthy(); 483 | }); 484 | 485 | test("Drag one cell", () => { 486 | const rowIndex = 0; 487 | const colIndex = 7; 488 | const header = "f"; 489 | 490 | wrapper.vm.selectedCell = { 491 | header, 492 | row: rowIndex, 493 | col: colIndex, 494 | }; 495 | 496 | wrapper.vm.copyStoreData("drag"); 497 | expect(wrapper.vm.storeCopyDatas.length).toEqual(1); 498 | }); 499 | }); 500 | 501 | describe("moveOnTable", () => { 502 | test("top", () => { 503 | const tBody = wrapper.vm; 504 | const vueTable = tBody.$refs[`${tBody.customTable}-vueTable`]; 505 | const fakeEvent = { 506 | keyCode: 38, 507 | preventDefault() { 508 | return "preventDefault"; 509 | }, 510 | }; 511 | 512 | tBody.moveOnTable(fakeEvent, 0, 0); 513 | expect(vueTable.scrollTop).toEqual(-40); 514 | expect(vueTable.scrollLeft).toEqual(0); 515 | }); 516 | 517 | test("bottom", () => { 518 | const tBody = wrapper.vm; 519 | const vueTable = tBody.$refs[`${tBody.customTable}-vueTable`]; 520 | const fakeEvent = { 521 | keyCode: 40, 522 | preventDefault() { 523 | return "preventDefault"; 524 | }, 525 | }; 526 | 527 | tBody.moveOnTable(fakeEvent, 0, 0); 528 | expect(vueTable.scrollTop).toEqual(40); 529 | expect(vueTable.scrollLeft).toEqual(0); 530 | }); 531 | 532 | test("left", () => { 533 | const tBody = wrapper.vm; 534 | const vueTable = tBody.$refs[`${tBody.customTable}-vueTable`]; 535 | const fakeEvent = { 536 | keyCode: 37, 537 | preventDefault() { 538 | return "preventDefault"; 539 | }, 540 | }; 541 | 542 | tBody.moveOnTable(fakeEvent, 0, 0); 543 | expect(vueTable.scrollTop).toEqual(0); 544 | expect(vueTable.scrollLeft).toEqual(-200); 545 | }); 546 | 547 | test("right", () => { 548 | const tBody = wrapper.vm; 549 | const vueTable = tBody.$refs[`${tBody.customTable}-vueTable`]; 550 | const fakeEvent = { 551 | keyCode: 39, 552 | preventDefault() { 553 | return "preventDefault"; 554 | }, 555 | }; 556 | 557 | tBody.moveOnTable(fakeEvent, 0, 0); 558 | expect(vueTable.scrollTop).toEqual(0); 559 | expect(vueTable.scrollLeft).toEqual(200); 560 | }); 561 | 562 | test("top left", () => { 563 | const tBody = wrapper.vm; 564 | const vueTable = tBody.$refs[`${tBody.customTable}-vueTable`]; 565 | const fakeEvent = { 566 | keyCode: 38, 567 | preventDefault() { 568 | return "preventDefault"; 569 | }, 570 | }; 571 | const fakeEvent2 = { 572 | keyCode: 37, 573 | preventDefault() { 574 | return "preventDefault"; 575 | }, 576 | }; 577 | 578 | tBody.moveOnTable(fakeEvent, 0, 0); 579 | tBody.moveOnTable(fakeEvent2, 0, 0); 580 | expect(vueTable.scrollTop).toEqual(-40); 581 | expect(vueTable.scrollLeft).toEqual(-200); 582 | }); 583 | 584 | test("top right", () => { 585 | const tBody = wrapper.vm; 586 | const vueTable = tBody.$refs[`${tBody.customTable}-vueTable`]; 587 | const fakeEvent = { 588 | keyCode: 38, 589 | preventDefault() { 590 | return "preventDefault"; 591 | }, 592 | }; 593 | const fakeEvent2 = { 594 | keyCode: 39, 595 | preventDefault() { 596 | return "preventDefault"; 597 | }, 598 | }; 599 | 600 | tBody.moveOnTable(fakeEvent, 0, 0); 601 | tBody.moveOnTable(fakeEvent2, 0, 0); 602 | expect(vueTable.scrollTop).toEqual(-40); 603 | expect(vueTable.scrollLeft).toEqual(200); 604 | }); 605 | 606 | test("bottom left", () => { 607 | const tBody = wrapper.vm; 608 | const vueTable = tBody.$refs[`${tBody.customTable}-vueTable`]; 609 | const fakeEvent = { 610 | keyCode: 40, 611 | preventDefault() { 612 | return "preventDefault"; 613 | }, 614 | }; 615 | const fakeEvent2 = { 616 | keyCode: 37, 617 | preventDefault() { 618 | return "preventDefault"; 619 | }, 620 | }; 621 | 622 | tBody.moveOnTable(fakeEvent, 0, 0); 623 | tBody.moveOnTable(fakeEvent2, 0, 0); 624 | expect(vueTable.scrollTop).toEqual(40); 625 | expect(vueTable.scrollLeft).toEqual(-200); 626 | }); 627 | 628 | test("bottom right", () => { 629 | const tBody = wrapper.vm; 630 | const vueTable = tBody.$refs[`${tBody.customTable}-vueTable`]; 631 | const fakeEvent = { 632 | keyCode: 40, 633 | preventDefault() { 634 | return "preventDefault"; 635 | }, 636 | }; 637 | const fakeEvent2 = { 638 | keyCode: 39, 639 | preventDefault() { 640 | return "preventDefault"; 641 | }, 642 | }; 643 | 644 | tBody.moveOnTable(fakeEvent, 0, 0); 645 | tBody.moveOnTable(fakeEvent2, 0, 0); 646 | expect(vueTable.scrollTop).toEqual(40); 647 | expect(vueTable.scrollLeft).toEqual(200); 648 | }); 649 | }); 650 | 651 | describe("callbackSort", () => { 652 | test("Emitted", () => { 653 | const tBody = wrapper.vm; 654 | 655 | tBody.callbackSort("fakeEvent", "h", 2); 656 | expect(wrapper.emitted("thead-td-sort")).toBeTruthy(); 657 | expect(wrapper.emitted("thead-td-sort")).toEqual([["fakeEvent", "h", 2]]); 658 | }); 659 | }); 660 | 661 | describe("callbackSubmenuTbody", () => { 662 | test("Emitted", () => { 663 | const tBody = wrapper.vm; 664 | 665 | tBody.callbackSubmenuTbody("", "b", 0, 2, "input", "test-function"); 666 | expect(wrapper.emitted("tbody-submenu-click-test-function")).toBeTruthy(); 667 | expect(wrapper.emitted("tbody-submenu-click-test-function")).toEqual([ 668 | ["", "b", 0, 2, "input", "test-function"], 669 | ]); 670 | }); 671 | }); 672 | 673 | describe("callbackSubmenuThead", () => { 674 | test("Emitted without option", () => { 675 | const tBody = wrapper.vm; 676 | 677 | tBody.submenuStatusThead = true; 678 | tBody.callbackSubmenuThead("fakeEvent", "b", 0, "test-function", undefined); 679 | expect(wrapper.emitted("thead-submenu-click-test-function")).toBeTruthy(); 680 | expect(tBody.submenuStatusThead).toBeFalsy(); 681 | expect(wrapper.emitted("thead-submenu-click-test-function")).toEqual([["fakeEvent", "b", 0]]); 682 | }); 683 | 684 | test("Emitted with option", () => { 685 | const tBody = wrapper.vm; 686 | 687 | tBody.submenuStatusThead = true; 688 | tBody.callbackSubmenuThead("fakeEvent", "b", 0, "test-function", ["a"]); 689 | expect(wrapper.emitted("thead-submenu-click-test-function")).toBeTruthy(); 690 | expect(tBody.submenuStatusThead).toBeFalsy(); 691 | expect(wrapper.emitted("thead-submenu-click-test-function")).toEqual([ 692 | ["fakeEvent", "b", 0, ["a"]], 693 | ]); 694 | }); 695 | }); 696 | 697 | describe("handleTheadContextMenu", () => { 698 | test("submenuStatusTbody: false", () => { 699 | const tBody = wrapper.vm; 700 | 701 | tBody.submenuStatusTbody = true; 702 | expect(tBody.submenuStatusTbody).toBeTruthy(); 703 | tBody.handleTheadContextMenu(); 704 | expect(tBody.submenuStatusTbody).toBeFalsy(); 705 | }); 706 | }); 707 | 708 | describe("createdCell", () => { 709 | test("return newData", () => { 710 | const tBody = wrapper.vm; 711 | const { newData } = exempleData.customOptions; 712 | 713 | expect(tBody.customOptions.newData).toEqual(newData); 714 | }); 715 | }); 716 | 717 | describe("changeData", () => { 718 | test("return storeUndoData", () => { 719 | const tBody = wrapper.vm; 720 | 721 | tBody.changeData(1, "a"); 722 | 723 | expect(tBody.storeUndoData.length).toEqual(1); 724 | }); 725 | 726 | test("return emit tbody-change-data", () => { 727 | const tBody = wrapper.vm; 728 | 729 | tBody.changeData(1, "a"); 730 | 731 | expect(wrapper.emitted("tbody-change-data")).toBeTruthy(); 732 | }); 733 | }); 734 | 735 | describe("rollBackUndo", () => { 736 | test("empty stored history", () => { 737 | const tBody = wrapper.vm; 738 | 739 | tBody.value[1].a.value = "fake"; 740 | tBody.changeData(1, "a"); 741 | const store = tBody.storeUndoData[tBody.storeUndoData.length - 1]; 742 | 743 | tBody.rollBackUndo(); 744 | 745 | expect(tBody.value[store.rowIndex][store.header]).toEqual(store.cell.duplicate); 746 | expect(tBody.storeUndoData).toEqual([]); 747 | expect(wrapper.emitted("tbody-undo-data")).toEqual([[store.rowIndex, store.header, "fake"]]); 748 | }); 749 | }); 750 | }); 751 | -------------------------------------------------------------------------------- /types/components/TBody.d.ts: -------------------------------------------------------------------------------- 1 | import { Vue } from "vue-property-decorator"; 2 | /** 3 | * Props 4 | */ 5 | interface FilteredList { 6 | label: string 7 | value: string 8 | } 9 | interface Style { 10 | string: [string] 11 | } 12 | interface Headers { 13 | headerKey: string 14 | headerName: string 15 | style: Style 16 | } 17 | interface CommentData { 18 | borderColor: string 19 | value: string 20 | } 21 | interface SelectOption { 22 | label: string 23 | value: string 24 | } 25 | interface Data { 26 | comment?: CommentData 27 | disabled?: boolean 28 | duplicate?: Data 29 | handleSearch?: boolean 30 | numeric?: boolean 31 | selectOptions?: SelectOption[] 32 | style?: Style 33 | type: string 34 | value: string 35 | } 36 | interface TbodyData { 37 | number: Data 38 | } 39 | interface KeyValue { 40 | string: [string] 41 | } 42 | interface Translation { 43 | en: KeyValue 44 | fr: KeyValue 45 | lang: string 46 | } 47 | interface SubmenuTbody { 48 | disabled: string[] 49 | function: string 50 | type: string 51 | value: string 52 | } 53 | 54 | /** 55 | * Data 56 | */ 57 | interface VuetableTooltip { 58 | number: string 59 | } 60 | interface VueTableComment { 61 | number: string 62 | } 63 | 64 | export default class TBody extends Vue { 65 | // Props 66 | tbodyHighlight: number[]; 67 | filteredList: FilteredList[]; 68 | headers: Headers[]; 69 | currentTable: number; 70 | tbodyData: TbodyData[]; 71 | trad: Translation; 72 | disableCells: string[]; 73 | tbodyIndex: boolean; 74 | tbodyCheckbox: boolean; 75 | submenuStatusTbody: boolean; 76 | submenuTbody: SubmenuTbody[]; 77 | 78 | // Data 79 | emptyCell: string; 80 | eventDrag: boolean; 81 | submenuEnableCol: null | number; 82 | submenuEnableRow: null | number; 83 | vuetableTooltip: VuetableTooltip; 84 | vueTableComment: VueTableComment; 85 | } 86 | -------------------------------------------------------------------------------- /types/components/Thead.d.ts: -------------------------------------------------------------------------------- 1 | import { Vue } from "vue-property-decorator"; 2 | /** 3 | * Props 4 | */ 5 | interface Style { 6 | string: [string] 7 | } 8 | interface Headers { 9 | headerKey: string 10 | headerName: string 11 | style: Style 12 | } 13 | interface SubmenuThead { 14 | disabled: string[] 15 | function: string 16 | type: string 17 | value: string 18 | } 19 | 20 | /** 21 | * Data 22 | */ 23 | interface BeforeChangeSize { 24 | col: number 25 | offset: number 26 | size: number 27 | } 28 | 29 | export default class THead extends Vue { 30 | // Props 31 | theadHighlight: number[]; 32 | headerTop: number; 33 | headers: Headers[] 34 | currentTable: number; 35 | submenuThead: SubmenuThead[]; 36 | disableSortThead: string[]; 37 | sortHeader: boolean; 38 | tbodyIndex: boolean; 39 | tbodyCheckbox: boolean; 40 | submenuStatusThead: boolean; 41 | 42 | // Data 43 | checkedAll: boolean; 44 | beforeChangeSize: BeforeChangeSize; 45 | eventDrag: boolean; 46 | newSize: string; 47 | submenuEnableCol: null | number; 48 | vueTableHeight: number 49 | } 50 | -------------------------------------------------------------------------------- /types/components/VueTable.d.ts: -------------------------------------------------------------------------------- 1 | import { Vue } from "vue-property-decorator"; 2 | /** 3 | * Props 4 | */ 5 | interface FuseOption { 6 | distance: number 7 | findAllMatches: boolean 8 | location: number 9 | maxPatternLength: number 10 | minMatchCharLength: number 11 | shouldSort: boolean 12 | threshold: number 13 | tokenize: boolean 14 | } 15 | interface Style { 16 | string: [string] 17 | } 18 | interface NewData { 19 | style: Style 20 | type: string 21 | value: string 22 | } 23 | interface Headers { 24 | headerKey: string 25 | headerName: string 26 | style: Style 27 | } 28 | interface KeyValue { 29 | string: [string] 30 | } 31 | interface Translation { 32 | en: KeyValue 33 | fr: KeyValue 34 | lang: string 35 | } 36 | interface CustomOptions { 37 | fuseOptions: FuseOption 38 | newData: NewData 39 | sortHeader: boolean 40 | tbodyCheckbox: boolean 41 | tbodyIndex: boolean 42 | trad: Translation 43 | } 44 | interface ParentScrollElement { 45 | attribute: string 46 | positionTop: number 47 | } 48 | interface SelectPosition { 49 | left: number 50 | top: number 51 | } 52 | interface Comment { 53 | borderColor: string 54 | borderSize: string 55 | heightBox: string 56 | widthBox: string 57 | } 58 | interface StyleWrapVueTable { 59 | comment: Comment 60 | fontSize: string 61 | height: string 62 | overflow: string 63 | string: [string] 64 | } 65 | interface Submenu { 66 | disabled: string[] 67 | function: string 68 | type: string 69 | value: string 70 | } 71 | interface Data { 72 | comment?: CommentData 73 | disabled?: boolean 74 | duplicate?: Data 75 | handleSearch?: boolean 76 | numeric?: boolean 77 | selectOptions?: SelectOption[] 78 | style?: Style 79 | type: string 80 | value: string 81 | } 82 | interface Value { 83 | number: Data 84 | } 85 | 86 | /** 87 | * Data 88 | */ 89 | interface Highlight { 90 | tbody: string[] 91 | thead: string[] 92 | } 93 | interface Comment { 94 | borderColor: string 95 | value: string 96 | } 97 | interface SelectOption { 98 | label: string 99 | value: string 100 | } 101 | interface Col { 102 | comment: Comment 103 | duplicate: Col 104 | handleSearch: boolean 105 | search: boolean 106 | selectOptions: SelectOption[] 107 | show: boolean 108 | type: string 109 | value: string 110 | } 111 | interface LastSelectOpen { 112 | col: Col 113 | colIndex: number 114 | event: MouseEvent 115 | header: string 116 | rowIndex: number 117 | } 118 | interface SelectedCell { 119 | col: number 120 | header: string 121 | row: number 122 | } 123 | 124 | export default class VueTable extends Vue { 125 | // Props 126 | customOptions: CustomOptions; 127 | disableCells: string[]; 128 | disableSortThead: string[]; 129 | headers: Headers; 130 | loading: boolean; 131 | parentScrollElement: ParentScrollElement; 132 | selectPosition: SelectPosition; 133 | styleWrapVueTable: StyleWrapVueTable; 134 | submenuTbody: Submenu[]; 135 | submenuThead: Submenu[]; 136 | value: Value[]; 137 | 138 | // Data 139 | customTable: number; 140 | highlight: null | Highlight; 141 | incrementOption: null | number; 142 | lastSelectOpen: null | LastSelectOpen; 143 | scrollDocument: null | number; 144 | scrollToSelectTimeout: null | number; 145 | selectedCell: null | SelectedCell; 146 | selectedMultipleCell: boolean; 147 | selectedMultipleCellActive: boolean; 148 | setFirstCell: boolean; 149 | submenuStatusTbody: boolean; 150 | submenuStatusThead: boolean; 151 | } 152 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for vue-spreadsheet 2.2.1 2 | // Project: vue-spreadsheet 3 | // Definitions by: Joffrey Berrier 4 | 5 | import VueTable from "./components/VueTable"; 6 | import TBody from "./components/Tbody"; 7 | import THead from "./components/THead"; 8 | 9 | export { VueTable, TBody, THead }; 10 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | // vue.config.js 2 | module.exports = { 3 | css: { extract: false }, 4 | }; 5 | --------------------------------------------------------------------------------