├── .changeset ├── README.md └── config.json ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── saucelabs.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── PUBLISH.MD ├── README.md ├── demo ├── demo.js └── index.html ├── helpers ├── ld-header-with-choices.ts ├── ld-header-with-date-and-sort.ts ├── ld-header-with-filter-and-sort.ts ├── ld-header-with-filter.ts └── ld-header-with-sort.ts ├── index.html ├── iron-flex-import.ts ├── lit-datatable-column.ts ├── lit-datatable-footer.ts ├── lit-datatable.ts ├── localize.ts ├── package.json ├── screen.png ├── test ├── integration │ ├── component-use-lit-datatable.ts │ └── lit-datatable-integration.test.ts └── unit │ ├── lit-datatable-with-column.test.ts │ └── lit-datatable.test.ts ├── tsconfig.json ├── web-test-runner.config.js └── yarn.lock /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/master/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.4.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "linked": [], 6 | "access": "restricted", 7 | "baseBranch": "master", 8 | "updateInternalDependencies": "patch", 9 | "ignore": [] 10 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | *.log 4 | reports/ 5 | .DS_Store 6 | build 7 | .tmp 8 | npm-debug.log 9 | .project 10 | *.out 11 | .sonar 12 | package-lock.json 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb-base", 4 | "plugin:@typescript-eslint/recommended", 5 | "plugin:import/typescript" 6 | ], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "project": "./tsconfig.json", 10 | "ecmaVersion": 6, 11 | "sourceType": "module", 12 | "ecmaFeatures": { 13 | "modules": true 14 | } 15 | }, 16 | "rules": { 17 | "@typescript-eslint/explicit-function-return-type": "off", 18 | "@typescript-eslint/explicit-module-boundary-types": "off", 19 | "no-param-reassign": "off", 20 | "import/prefer-default-export": "off", 21 | "max-len": [ 22 | "warn", 23 | 200, 24 | 2, 25 | { 26 | "ignoreUrls": true, 27 | "ignoreComments": false, 28 | "ignoreRegExpLiterals": true, 29 | "ignoreStrings": true, 30 | "ignoreTemplateLiterals": true 31 | } 32 | ], 33 | "no-underscore-dangle": "off", 34 | "new-cap": "off", 35 | "import/no-extraneous-dependencies": "off", 36 | "class-methods-use-this": "off", 37 | "comma-dangle": [ 38 | "error", 39 | { 40 | "arrays": "always-multiline", 41 | "objects": "always-multiline", 42 | "imports": "never", 43 | "exports": "never", 44 | "functions": "never" 45 | } 46 | ], 47 | "import/extensions": "off", 48 | "eol-last": 2, 49 | "max-classes-per-file": "off" 50 | }, 51 | "env": { 52 | "mocha": true, 53 | "browser": true 54 | }, 55 | "globals": { 56 | "vis": "readonly", 57 | "saveAs": "readonly", 58 | "SnapEngage": "readonly", 59 | "jsPDF": "readonly" 60 | }, 61 | "settings": { 62 | "import/resolver": { 63 | "node": { 64 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 65 | } 66 | }, 67 | "import/extensions": [ 68 | ".ts", 69 | ".tsx" 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /.github/workflows/saucelabs.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | # This workflow contains a single job called "build" 16 | build: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | 20 | # Steps represent a sequence of tasks that will be executed as part of the job 21 | steps: 22 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 23 | - uses: actions/checkout@v2 24 | 25 | # Runs a single command using the runners shell 26 | - name: Use Node.js 27 | uses: actions/setup-node@v1 28 | with: 29 | always-auth: true 30 | node-version: '14.x' 31 | registry-url: https://registry.npmjs.org 32 | scope: '@octocat' 33 | 34 | # Runs a set of commands using the runners shell 35 | - name: Install dependencies and run tests 36 | run: npm install && npm run build && npm run lint && npm test 37 | env: 38 | SAUCE_USERNAME: ${{secrets.SAUCE_USERNAME}} 39 | SAUCE_ACCESS_KEY: ${{secrets.SAUCE_ACCESS_KEY}} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | *.log 4 | reports/ 5 | .DS_Store 6 | build 7 | .tmp 8 | npm-debug.log 9 | .project 10 | *.out 11 | .sonar 12 | *.js 13 | *.js.map 14 | *.d.ts 15 | *.d.ts.map 16 | !demo/index.js 17 | !web-test-runner.config.js 18 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/* 2 | *.ts 3 | !*.d.ts 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @doubletrade/lit-datatable 2 | 3 | ## 1.22.0 4 | 5 | ### Minor Changes 6 | 7 | - update lit-datepicker version 8 | 9 | ## 1.21.0 10 | 11 | ### Minor Changes 12 | 13 | - better compute class of current key 14 | 15 | ## 1.20.0 16 | 17 | ### Minor Changes 18 | 19 | - add way to scroll to a row 20 | 21 | ## 1.19.0 22 | 23 | ### Minor Changes 24 | 25 | - include sourcemaps in publicated sources 26 | 27 | ## 1.18.0 28 | 29 | ### Minor Changes 30 | 31 | - use type for ld-header-with-choices 32 | 33 | ## 1.17.0 34 | 35 | ### Minor Changes 36 | 37 | - some typing fix 38 | 39 | ## 1.16.0 40 | 41 | ### Minor Changes 42 | 43 | - Some fixes and graphical fixes 44 | 45 | ## 1.15.0 46 | 47 | ### Minor Changes 48 | 49 | - fix deps version and fix an issue with date column 50 | 51 | ## 1.14.0 52 | 53 | ### Minor Changes 54 | 55 | - bump lit-datepicker 56 | 57 | ## 1.13.0 58 | 59 | ### Minor Changes 60 | 61 | - add some mixins for choice helper 62 | 63 | ## 1.12.0 64 | 65 | ### Minor Changes 66 | 67 | - add some mixins 68 | 69 | ## 1.11.1 70 | 71 | ### Patch Changes 72 | 73 | - update lit-date-picker version 74 | 75 | ## 1.11.0 76 | 77 | ### Minor Changes 78 | 79 | - fix checkbox display on FF 80 | 81 | ## 1.10.0 82 | 83 | ### Minor Changes 84 | 85 | - avoid crash in search in case of choices are not set yet 86 | 87 | ## 1.9.0 88 | 89 | ### Minor Changes 90 | 91 | - add search on choice filter and fix padding 92 | 93 | ## 1.8.0 94 | 95 | ### Minor Changes 96 | 97 | - use yarn, add feature on choice helper 98 | 99 | ## 1.6.0 100 | 101 | ### Minor Changes 102 | 103 | - fix type of choice 104 | 105 | ## 1.5.0 106 | 107 | ### Minor Changes 108 | 109 | - use boolean for sticky header property 110 | 111 | ## 1.4.0 112 | 113 | ### Minor Changes 114 | 115 | - add some mixin on th and td element 116 | 117 | ## 1.3.0 118 | 119 | ### Minor Changes 120 | 121 | - Add mixin on td border-top 122 | - Add saucelabs testing 123 | 124 | ## 1.2.0 125 | 126 | ### Minor Changes 127 | 128 | - Some types fixing, fix event listners 129 | 130 | ## 1.1.0 131 | 132 | ### Minor Changes 133 | 134 | - Add tests and HTMLElementTagNameMap 135 | 136 | ## 1.0.0 137 | 138 | ### Major Changes 139 | 140 | - First major version 141 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Google, Inc 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /PUBLISH.MD: -------------------------------------------------------------------------------- 1 | # PUBLISH 2 | 3 | ``` 4 | npx changeset 5 | npx changeset version (MINOR) 6 | git add . && git commit -m"Package Version" 7 | npx changeset publish 8 | ``` 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![CI](https://github.com/DoubleTrade/lit-datatable/workflows/CI/badge.svg) [![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://www.webcomponents.org/element/@doubletrade/lit-datatable) 2 | 3 | # lit-datatable 4 | 5 | `lit-datatable` is a material design implementation of a data table. 6 | 7 | ![screen](screen.png) 8 | 9 | 10 | ## Roadmap 11 | Add some helpers. 12 | 13 | ## Install 14 | ``` 15 | npm install @doubletrade/lit-datatable 16 | ``` 17 | 18 | ## Launch demo 19 | ``` 20 | npm install 21 | npm run build 22 | npm run serve 23 | ``` 24 | 25 | ## Lint 26 | ``` 27 | npm run lint:javascript 28 | ``` 29 | 30 | ## Data 31 | ```js 32 | // Data from api 33 | const data = [ 34 | { fruit: 'apple', color: 'green', weight: '100gr' }, 35 | { fruit: 'banana', color: 'yellow', weight: '140gr' } 36 | ]; 37 | // Conf to set order of column and visibility (can be dynamically) 38 | const conf = [ 39 | { property: 'fruit', header: 'Fruit', hidden: false }, 40 | { property: 'color', header: 'Color', hidden: true }, 41 | { property: 'weight', header: 'Weight', hidden: false } 42 | ]; 43 | ``` 44 | 45 | ## Simple example 46 | ```html 47 | 48 | ``` 49 | ## Simple example with sticky header 50 | ```html 51 | 52 | ``` 53 | ## With HTML header 54 | Use native html from lit-html to render a custom header. 55 | Header can use value from data and property from conf. 56 | ```js 57 | const headerOfFruit = (value, property) => html`
${value}
`; 58 | ``` 59 | ```html 60 | 61 | 62 | 63 | ``` 64 | ## With HTML data 65 | As header, use native html from lit-html to render a custom body. 66 | ```js 67 | const bodyOfFruit = (value, property) => html`
${value}
`; 68 | ``` 69 | ```html 70 | 71 | 72 | 73 | ``` 74 | ## With HTML data and footer 75 | A footer is available to catch size and page changed in order to relaunch the request to the backend. 76 | ```html 77 | 78 | 79 | 80 | 89 | 90 | ``` 91 | ## With HTML data and sorter 92 | A default sorter is available, set a header column without html and type `sort`. 93 | The sort must be of the following form : `property,direction`, ex: `fruit,asc`. 94 | ```html 95 | 96 | 97 | 98 | 99 | ``` 100 | ## With HTML data and custom sorter 101 | You can use a specific sorter is available in helpers. 102 | ```js 103 | const sort = key => (value, property) => html` 104 | 109 | ${value} 110 | `; 111 | ``` 112 | ```html 113 | 114 | 115 | 116 | 117 | ``` 118 | ## Custom style on a td 119 | ```html 120 | 121 | ``` 122 | 123 | ## With HTML data and filter 124 | ```html 125 | 126 | 127 | 128 | 129 | ``` 130 | 131 | ## With HTML data, sort and filter 132 | ```html 133 | 134 | 135 | 136 | 137 | ``` 138 | 139 | ## With HTML data and choices filter 140 | ```html 141 | 142 | 143 | 144 | 145 | ``` 146 | 147 | ## With HTML data and choices filter with value filtering 148 | ```html 149 | 150 | 151 | 152 | 153 | ``` 154 | 155 | ## With HTML data and date filter 156 | The format of startDate and endDate is a timestamp. 157 | ```html 158 | 159 | 160 | 161 | 162 | ``` 163 | 164 | --- 165 | 166 | [![Testing Powered By SauceLabs](https://opensource.saucelabs.com/images/opensauce/powered-by-saucelabs-badge-white.png?sanitize=true 'Testing Powered By SauceLabs')](https://saucelabs.com) 167 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: off */ 2 | 3 | import { LitElement, html, css } from 'lit-element'; 4 | 5 | import '../lit-datatable'; 6 | import '../lit-datatable-column'; 7 | import '../lit-datatable-footer'; 8 | import '../helpers/ld-header-with-sort'; 9 | 10 | class LitDatatableDemo extends LitElement { 11 | static get styles() { 12 | const mainStyle = css` 13 | :host { 14 | display: block; 15 | } 16 | 17 | .content > lit-datatable:not(:last-of-type) { 18 | margin-bottom: 24px; 19 | } 20 | `; 21 | return [mainStyle]; 22 | } 23 | 24 | render() { 25 | // Data from api 26 | const data = [{ fruit: 'apple', color: 'green', weight: '100gr' }, { fruit: 'banana', color: 'yellow', weight: '140gr' }]; 27 | // Conf to set order of column 28 | const conf = [{ property: 'fruit', header: 'Fruit', hidden: false }, { property: 'color', header: 'Color', hidden: true }, { property: 'weight', header: 'Weight', hidden: false }]; 29 | 30 | // Customized header (get value and property fields) 31 | const headerOfFruit = (value, property) => html`
${value}
`; 32 | 33 | // Customized body (get value and property fields) 34 | const bodyOfFruit = (value, property) => html`
${value}
`; 35 | 36 | // Sorter on column 37 | const sort = (value, property) => html` 38 | 42 | ${value} 43 | 44 | `; 45 | 46 | return html` 47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 86 | 87 |
`; 88 | } 89 | } 90 | 91 | window.customElements.define('lit-datatable-demo', LitDatatableDemo); 92 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | range-datepicker demo 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /helpers/ld-header-with-choices.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LitElement, css, html, PropertyValues, TemplateResult 3 | } from 'lit'; 4 | import { property, customElement, query } from 'lit/decorators.js'; 5 | import '@polymer/paper-icon-button/paper-icon-button'; 6 | import '@polymer/paper-item/paper-icon-item'; 7 | import '@polymer/paper-item/paper-item-body'; 8 | import '@polymer/iron-icons/iron-icons'; 9 | 10 | import { ironFlexLayoutAlignTheme, ironFlexLayoutTheme } from '../iron-flex-import'; 11 | import './ld-header-with-sort'; 12 | 13 | export interface Choice { 14 | key: string; 15 | label: string; 16 | style?: string; 17 | icon?: string; 18 | iconStyle?: string; 19 | prefix?: TemplateResult; 20 | } 21 | 22 | @customElement('ld-header-with-choices') 23 | export class LdHeaderWithChoices extends LitElement { 24 | @property({ type: Array }) choices: Array = []; 25 | 26 | @property({ type: Boolean }) enableFilter = false; 27 | 28 | @property({ type: String }) filterValue = ''; 29 | 30 | @query('#filterInput') filterInput!: HTMLInputElement; 31 | 32 | @property({ type: Array }) filteredChoices: Array = []; 33 | 34 | @property({ type: Array }) selectedChoices: Array = []; 35 | 36 | @property({ type: String }) property = ''; 37 | 38 | @property({ type: Boolean }) opened = false; 39 | 40 | @query('.dropdown') dropdown!: HTMLDivElement; 41 | 42 | static get styles() { 43 | const main = css` 44 | :host { 45 | display: block; 46 | } 47 | 48 | .dropdown { 49 | position: fixed; 50 | background: var(--ld-header-with-choices-background-color, white); 51 | transform-origin: 50% 0; 52 | transition: transform 0.1s; 53 | transform: scaleY(1); 54 | box-shadow: var(--ld-header-with-choices-box-shadown, 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)); 55 | width: var(--dt-dropdown-choice-dropdown-width, max-content); 56 | z-index: 99; 57 | max-height: 300px; 58 | overflow: auto; 59 | margin: var(--dt-dropdown-choice-dropdown-margin, 0); 60 | color: var(--primary-text-color, black); 61 | border-radius: var(--dt-dropdown-choice-dropdown-border-radius, 0); 62 | box-shadow: var(--dt-dropdown-choice-dropdown-box-shadow, 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)); 63 | } 64 | 65 | .dropdown.hide { 66 | transform: scaleY(0); 67 | } 68 | 69 | paper-icon-button[icon="check-box"] { 70 | min-width: 40px; 71 | color: var(--paper-datatable-api-checked-checkbox-color, --primary-color); 72 | } 73 | 74 | paper-icon-button[icon="check-box-outline-blank"] { 75 | min-width: 40px; 76 | color: var(--paper-datatable-api-unchecked-checkbox-color, --primary-text-color); 77 | } 78 | 79 | .selected { 80 | color: var(--primary-color, #1E73BE); 81 | font-style: italic; 82 | margin-left: 4px; 83 | } 84 | 85 | .choice-icon { 86 | margin-left: 24px; 87 | } 88 | 89 | .label { 90 | font-size: 13px; 91 | font-family: Roboto; 92 | font-weight: 400; 93 | margin-right: 16px; 94 | } 95 | 96 | .prefix { 97 | margin-right: 10px; 98 | } 99 | 100 | paper-icon-button { 101 | --paper-icon-button: { 102 | color: var(--paper-icon-button-color); 103 | transition: color 0.3s; 104 | } 105 | 106 | --paper-icon-button-hover: { 107 | color: var(--paper-icon-button-color-hover); 108 | } 109 | } 110 | 111 | #search-container { 112 | padding: 6px 6px 6px 10px; 113 | border-bottom: 1px solid #E0E0E0; 114 | } 115 | 116 | #search-container input{ 117 | border: none; 118 | font-size: var(--header-filter-input-font-size, 16px); 119 | width: calc(100% - 30px); 120 | outline: none; 121 | background: transparent; 122 | height: 24px; 123 | padding: 0; 124 | color: var(--dt-input-text-color, black); 125 | box-shadow: none; 126 | min-width: 0; 127 | } 128 | #search-container input:-webkit-autofill, 129 | input:-webkit-autofill:hover, 130 | input:-webkit-autofill:focus, 131 | input:-webkit-autofill:active { 132 | -webkit-box-shadow: 0 0 0 30px white inset !important; 133 | } 134 | `; 135 | return [main, ironFlexLayoutTheme, ironFlexLayoutAlignTheme]; 136 | } 137 | 138 | render() { 139 | return html` 140 |
141 | 142 |
143 | 144 | 145 | ${this.selectedChoices && this.selectedChoices.length > 0 ? html` 146 |
147 | ${this.countSelected(this.selectedChoices)} 148 |
` : null} 149 |
150 | 151 |
152 | 153 | 177 | 178 |
179 | `; 180 | } 181 | 182 | static get properties() { 183 | return { 184 | }; 185 | } 186 | 187 | constructor() { 188 | super(); 189 | this.selectedChoices = []; 190 | this.opened = false; 191 | } 192 | 193 | computeIconName(choice: string, selectedChoices: Array) { 194 | if (selectedChoices.indexOf(choice) === -1) { 195 | return 'check-box-outline-blank'; 196 | } 197 | return 'check-box'; 198 | } 199 | 200 | countSelected(selectedChoices: Array) { 201 | return selectedChoices.length > 0 ? ` (${selectedChoices.length})` : ''; 202 | } 203 | 204 | tapChoice(name: string) { 205 | const selectedChoices = [...this.selectedChoices]; 206 | const indexOfChoice = selectedChoices.indexOf(name); 207 | if (indexOfChoice === -1) { 208 | selectedChoices.push(name); 209 | } else { 210 | selectedChoices.splice(indexOfChoice, 1); 211 | } 212 | this.dispatchEvent(new CustomEvent( 213 | 'selected-choices-changed', 214 | { detail: { value: selectedChoices, property: this.property } } 215 | )); 216 | } 217 | 218 | updated(properties: PropertyValues) { 219 | if (properties.has('opened')) { 220 | if (this.opened) { 221 | this.dropdown.classList.remove('hide'); 222 | if (this.enableFilter) { 223 | this.filterInput.focus(); 224 | this.filterValue = ''; 225 | } 226 | } else { 227 | this.dropdown.classList.add('hide'); 228 | } 229 | this.fitToBorder(); 230 | } 231 | if (properties.has('enableFilter') || properties.has('choices') || properties.has('filterValue')) { 232 | this.updateFilteredChoices(); 233 | } 234 | } 235 | 236 | openDropdown() { 237 | this.opened = !this.opened; 238 | } 239 | 240 | fitToBorder() { 241 | if (this.shadowRoot) { 242 | if (this.dropdown) { 243 | this.dropdown.style.left = '0'; 244 | const dropdownWidth = this.dropdown.offsetWidth; 245 | const viewPortWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); 246 | const thisX = this.getBoundingClientRect().x; 247 | const thisY = this.getBoundingClientRect().y; 248 | if ((dropdownWidth + thisX) > viewPortWidth) { 249 | this.dropdown.style.left = `${viewPortWidth - dropdownWidth}px`; 250 | } else { 251 | this.dropdown.style.left = `${thisX}px`; 252 | } 253 | this.dropdown.style.top = `${thisY + this.offsetHeight + 9}px`; 254 | } 255 | } 256 | } 257 | 258 | firstUpdated() { 259 | window.addEventListener('resize', () => { 260 | this.fitToBorder(); 261 | }); 262 | window.addEventListener('keyup', (event) => { 263 | if (this.opened && event.key === 'Escape') { 264 | this.opened = false; 265 | } 266 | }); 267 | window.addEventListener('click', (event) => { 268 | const path = event.composedPath && event.composedPath(); 269 | if (path.includes(this)) { 270 | event.preventDefault(); 271 | } else if (this.opened) { 272 | this.opened = false; 273 | } 274 | }); 275 | } 276 | 277 | filterValueChanged(event: InputEvent) { 278 | event.stopPropagation(); 279 | const target = event.target as HTMLInputElement; 280 | this.filterValue = target.value; 281 | } 282 | 283 | updateFilteredChoices() { 284 | this.filteredChoices = (this.enableFilter && this.choices) 285 | ? this.choices.filter((c) => c?.label?.toLowerCase().includes(this.filterValue?.toLowerCase())) 286 | : this.choices; 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /helpers/ld-header-with-date-and-sort.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LitElement, css, html, PropertyValues 3 | } from 'lit'; 4 | import { property, customElement } from 'lit/decorators.js'; 5 | import '@polymer/paper-input/paper-input'; 6 | import '@polymer/paper-icon-button/paper-icon-button'; 7 | import '@polymer/iron-flex-layout/iron-flex-layout-classes'; 8 | import '@doubletrade/lit-datepicker/lit-datepicker-input'; 9 | import { ironFlexLayoutAlignTheme, ironFlexLayoutTheme } from '../iron-flex-import'; 10 | import './ld-header-with-sort'; 11 | import { Language } from '../localize'; 12 | 13 | @customElement('ld-header-with-date-and-sort') 14 | export class LdHeaderWithDateAndSort extends LitElement { 15 | @property({ type: String }) header = ''; 16 | 17 | language: Language | null = 'en'; 18 | 19 | @property({ type: String }) dateFormat = ''; 20 | 21 | @property({ type: String }) property = ''; 22 | 23 | @property({ type: String }) direction: '' | 'asc' | 'desc' = ''; 24 | 25 | @property({ type: Boolean }) active = false; 26 | 27 | @property({ type: String }) dateFrom: string | null = null; 28 | 29 | @property({ type: String }) dateTo: string | null = null; 30 | 31 | @property({ type: Boolean }) noRange = false; 32 | 33 | @property({ type: String }) horizontalAlign: 'left' | 'right' = 'right'; 34 | 35 | static get styles() { 36 | const mainStyle = css` 37 | :host { 38 | display: block; 39 | } 40 | .actions { 41 | padding-left: 8px; 42 | } 43 | paper-icon-button { 44 | padding: 0; 45 | min-width: 24px; 46 | min-height: 24px; 47 | width: 24px; 48 | height: 24px; 49 | --paper-icon-button: { 50 | color: var(--paper-icon-button-color); 51 | } 52 | 53 | --paper-icon-button-hover: { 54 | color: var(--paper-icon-button-color-hover); 55 | } 56 | }`; 57 | return [mainStyle, ironFlexLayoutTheme, ironFlexLayoutAlignTheme]; 58 | } 59 | 60 | render() { 61 | const input = (dateFrom: string, dateTo: string, noRange: boolean) => html` 62 | 83 | 84 | 89 | `; 90 | return html` 91 | ${this.active ? html` 92 | 93 |
94 | 105 | 106 | 107 |
108 |
` : html` 109 | 110 |
111 | ${this.header} 112 |
113 |
114 | 115 |
116 |
`}`; 117 | } 118 | 119 | updated(properties: PropertyValues) { 120 | if (properties.has('direction')) { 121 | this.dispatchEvent(new CustomEvent('direction-changed', { detail: { value: this.direction } })); 122 | } 123 | 124 | if (properties.has('active')) { 125 | this.dispatchEvent(new CustomEvent('active-changed', { detail: { value: this.active } })); 126 | } 127 | } 128 | 129 | dateToChanged({ detail }: CustomEvent<{value: string}>) { 130 | if (this.dateFrom && detail.value) { 131 | this.dateTo = detail.value; 132 | this.dispatchEvent(new CustomEvent('filter', { 133 | detail: { 134 | dateFrom: parseInt(this.dateFrom, 10), 135 | dateTo: parseInt(detail.value, 10), 136 | property: this.property, 137 | }, 138 | })); 139 | } 140 | } 141 | 142 | dateFromChanged({ detail }: CustomEvent<{value: string}>) { 143 | if (detail.value) { 144 | this.dateFrom = detail.value; 145 | if (this.noRange) { 146 | this.dispatchEvent(new CustomEvent('filter', { 147 | detail: { 148 | dateFrom: parseInt(detail.value, 10), 149 | property: this.property, 150 | }, 151 | })); 152 | } 153 | } 154 | } 155 | 156 | async toggleActive() { 157 | this.active = !this.active; 158 | if (!this.active) { 159 | this.dateFrom = null; 160 | this.dateTo = null; 161 | } 162 | await this.updateComplete; 163 | if (this.shadowRoot) { 164 | const paperInput = this.shadowRoot.querySelector('paper-input'); 165 | if (paperInput) { 166 | paperInput.setAttribute('tabindex', '1'); 167 | await this.updateComplete; 168 | paperInput.focus(); 169 | } 170 | } 171 | } 172 | 173 | computeDate(dateFrom: string | null, dateTo: string | null, noRange: boolean) { 174 | if (dateFrom && dateTo) { 175 | return `${dateFrom} ${dateTo}`; 176 | } 177 | if (noRange && dateFrom) { 178 | return dateFrom; 179 | } 180 | return ''; 181 | } 182 | 183 | clearDate() { 184 | this.toggleActive(); 185 | this.dispatchEvent(new CustomEvent('filter', { 186 | detail: { 187 | dateFrom: null, 188 | dateTo: null, 189 | property: this.property, 190 | }, 191 | })); 192 | } 193 | 194 | directionChanged({ detail }: CustomEvent<{value: '' | 'asc' | 'desc'}>) { 195 | this.direction = detail.value; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /helpers/ld-header-with-filter-and-sort.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, css, html } from 'lit'; 2 | import { property, customElement } from 'lit/decorators.js'; 3 | import '@polymer/paper-input/paper-input'; 4 | import '@polymer/paper-icon-button/paper-icon-button'; 5 | import Localize from '../localize'; 6 | import './ld-header-with-sort'; 7 | 8 | @customElement('ld-header-with-filter-and-sort') 9 | export class LdHeaderWithFilterAndSort extends Localize(LitElement) { 10 | @property({ type: String }) header = ''; 11 | 12 | @property({ type: String }) direction: '' | 'asc' | 'desc' = ''; 13 | 14 | @property({ type: Boolean }) active = false; 15 | 16 | @property({ type: String }) filterValue: string | null = null; 17 | 18 | @property({ type: String }) property = ''; 19 | 20 | resources = { 21 | en: { 22 | search: 'Search', 23 | clear: 'Clear', 24 | }, 25 | 'en-en': { 26 | search: 'Search', 27 | clear: 'Clear', 28 | }, 29 | 'en-US': { 30 | search: 'Search', 31 | clear: 'Clear', 32 | }, 33 | 'en-us': { 34 | search: 'Search', 35 | clear: 'Clear', 36 | }, 37 | fr: { 38 | search: 'Rechercher', 39 | clear: 'Effacer', 40 | }, 41 | 'fr-fr': { 42 | search: 'Rechercher', 43 | clear: 'Effacer', 44 | }, 45 | }; 46 | 47 | static get styles() { 48 | const mainStyle = css` 49 | :host { 50 | display: block; 51 | } 52 | 53 | paper-input { 54 | min-width: var(--paper-datatable-api-min-width-input-filter, 120px); 55 | --paper-input-container-underline-focus: { 56 | display: block; 57 | } 58 | ; 59 | --paper-input-container-label: { 60 | position: initial; 61 | } 62 | ; 63 | --paper-input-container: { 64 | padding: 0; 65 | } 66 | ; 67 | --paper-input-container-input: { 68 | font-size: 12px; 69 | } 70 | ; 71 | } 72 | 73 | paper-icon-button { 74 | --paper-icon-button: { 75 | color: var(--paper-icon-button-color); 76 | } 77 | 78 | --paper-icon-button-hover: { 79 | color: var(--paper-icon-button-color-hover); 80 | } 81 | } 82 | 83 | .header { 84 | margin-right: 16px; 85 | }`; 86 | return [mainStyle]; 87 | } 88 | 89 | render() { 90 | let content = html` 91 |
92 | ${this.header} 93 |
94 | 95 | ${this.localize('search')} 96 | `; 97 | if (this.active) { 98 | content = html` 99 | 104 | 109 | 112 | ${this.localize('clear')} 113 | 114 | `; 115 | } 116 | return html` 117 | 121 | ${content} 122 | `; 123 | } 124 | 125 | async toggleActive() { 126 | this.active = !this.active; 127 | this.dispatchEvent(new CustomEvent('active-changed', { detail: { value: this.active } })); 128 | if (!this.active && this.filterValue) { 129 | this.filterValue = null; 130 | this.dispatchFilterEvent(); 131 | } else { 132 | await this.updateComplete; 133 | if (this.shadowRoot) { 134 | const paperInput = this.shadowRoot.querySelector('paper-input'); 135 | if (paperInput) { 136 | paperInput.setAttribute('tabindex', '1'); 137 | paperInput.focus(); 138 | } 139 | } 140 | } 141 | } 142 | 143 | directionChanged({ detail }: CustomEvent<{value: '' | 'asc' | 'desc'}>) { 144 | if (this.direction !== detail.value) { 145 | this.direction = detail.value; 146 | this.dispatchEvent(new CustomEvent('direction-changed', { detail: { value: this.direction } })); 147 | } 148 | } 149 | 150 | valueChanged({ detail }: CustomEvent<{value: string}>) { 151 | if (this.filterValue !== detail.value) { 152 | this.filterValue = detail.value; 153 | this.dispatchFilterEvent(); 154 | } 155 | } 156 | 157 | dispatchFilterEvent() { 158 | this.dispatchEvent(new CustomEvent('filter-value-changed', { detail: { value: this.filterValue, property: this.property } })); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /helpers/ld-header-with-filter.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, css, html } from 'lit'; 2 | import { property, customElement } from 'lit/decorators.js'; 3 | import '@polymer/paper-input/paper-input'; 4 | import '@polymer/paper-icon-button/paper-icon-button'; 5 | import type { Language, Resources } from '../localize'; 6 | import Localize from '../localize'; 7 | 8 | import { ironFlexLayoutAlignTheme, ironFlexLayoutTheme } from '../iron-flex-import'; 9 | 10 | @customElement('ld-header-with-filter') 11 | export default class LdHeaderWithFilter extends Localize(LitElement) { 12 | @property({ type: String }) header = ''; 13 | 14 | @property({ type: String }) direction: '' | 'asc' | 'desc' = ''; 15 | 16 | @property({ type: Boolean }) active = false; 17 | 18 | @property({ type: String }) filterValue: string | null = null; 19 | 20 | @property({ type: String }) property = ''; 21 | 22 | language: Language | null = 'en'; 23 | 24 | resources: Resources | null = { 25 | en: { 26 | search: 'Search', 27 | clear: 'Clear', 28 | }, 29 | 'en-en': { 30 | search: 'Search', 31 | clear: 'Clear', 32 | }, 33 | 'en-US': { 34 | search: 'Search', 35 | clear: 'Clear', 36 | }, 37 | 'en-us': { 38 | search: 'Search', 39 | clear: 'Clear', 40 | }, 41 | fr: { 42 | search: 'Rechercher', 43 | clear: 'Effacer', 44 | }, 45 | 'fr-fr': { 46 | search: 'Rechercher', 47 | clear: 'Effacer', 48 | }, 49 | }; 50 | 51 | static get styles() { 52 | const mainStyle = css` 53 | :host { 54 | display: block; 55 | } 56 | 57 | paper-input { 58 | min-width: var(--paper-datatable-api-min-width-input-filter, 120px); 59 | } 60 | 61 | paper-icon-button { 62 | padding: 0; 63 | width: 24px; 64 | height: 24px; 65 | } 66 | 67 | .header { 68 | margin-right: 16px; 69 | }`; 70 | return [mainStyle, ironFlexLayoutTheme, ironFlexLayoutAlignTheme]; 71 | } 72 | 73 | render() { 74 | let content = html` 75 |
76 |
77 | ${this.header} 78 |
79 | 80 | ${this.localize('search')} 81 |
82 | `; 83 | if (this.active) { 84 | content = html` 85 | 111 | 116 | 121 | 124 | ${this.localize('clear')} 125 | 126 | `; 127 | } 128 | return content; 129 | } 130 | 131 | async toggleActive() { 132 | this.active = !this.active; 133 | this.dispatchEvent(new CustomEvent('active-changed', { detail: { value: this.active } })); 134 | if (!this.active && this.filterValue) { 135 | this.filterValue = null; 136 | this.dispatchFilterEvent(); 137 | } else { 138 | await this.updateComplete; 139 | if (this.shadowRoot) { 140 | const paperInput = this.shadowRoot.querySelector('paper-input'); 141 | if (paperInput) { 142 | paperInput.setAttribute('tabindex', '1'); 143 | paperInput.focus(); 144 | } 145 | } 146 | } 147 | } 148 | 149 | directionChanged({ detail }: CustomEvent<{value: 'asc' | 'desc' | ''}>) { 150 | if (this.direction !== detail.value) { 151 | this.direction = detail.value; 152 | this.dispatchEvent(new CustomEvent('direction-changed', { detail: { value: this.direction } })); 153 | } 154 | } 155 | 156 | valueChanged({ detail }: CustomEvent<{value: string}>) { 157 | if (this.filterValue !== detail.value) { 158 | this.filterValue = detail.value; 159 | this.dispatchFilterEvent(); 160 | } 161 | } 162 | 163 | dispatchFilterEvent() { 164 | this.dispatchEvent(new CustomEvent('filter-value-changed', { detail: { value: this.filterValue, property: this.property } })); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /helpers/ld-header-with-sort.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, css, html } from 'lit'; 2 | import { property, customElement } from 'lit/decorators.js'; 3 | import '@polymer/paper-tooltip/paper-tooltip'; 4 | import '@polymer/paper-icon-button/paper-icon-button'; 5 | import '@polymer/iron-icons/iron-icons'; 6 | import type { Language, Resources } from '../localize'; 7 | import Localize from '../localize'; 8 | 9 | import { ironFlexLayoutAlignTheme, ironFlexLayoutTheme } from '../iron-flex-import'; 10 | 11 | @customElement('ld-header-with-sort') 12 | export class LdHeaderWithSort extends Localize(LitElement) { 13 | @property({ type: String }) direction: '' | 'asc' | 'desc' = ''; 14 | 15 | language: Language | null = 'en'; 16 | 17 | resources: Resources | null = { 18 | en: { 19 | sortAZ: 'Sort A-Z', 20 | sortZA: 'Sort Z-A', 21 | sortCancel: 'Cancel sort', 22 | }, 23 | 'en-en': { 24 | sortAZ: 'Sort A-Z', 25 | sortZA: 'Sort Z-A', 26 | sortCancel: 'Cancel sort', 27 | }, 28 | 'en-US': { 29 | sortAZ: 'Sort A-Z', 30 | sortZA: 'Sort Z-A', 31 | sortCancel: 'Cancel sort', 32 | }, 33 | 'en-us': { 34 | sortAZ: 'Sort A-Z', 35 | sortZA: 'Sort Z-A', 36 | sortCancel: 'Cancel sort', 37 | }, 38 | fr: { 39 | sortAZ: 'Trier de A à Z', 40 | sortZA: 'Trier de Z à A', 41 | sortCancel: 'Annuler le tri', 42 | }, 43 | 'fr-fr': { 44 | sortAZ: 'Trier de A à Z', 45 | sortZA: 'Trier de Z à A', 46 | sortCancel: 'Annuler le tri', 47 | }, 48 | }; 49 | 50 | static get styles() { 51 | const main = css` 52 | :host { 53 | display: block; 54 | } 55 | 56 | .desc, .asc { 57 | transition: transform 0.2s; 58 | } 59 | 60 | .desc { 61 | color: var(--lit-datatable-api-arrow-color, var(--paper-light-green-600)); 62 | transform: rotate(0deg); 63 | } 64 | 65 | .asc { 66 | color: var(--lit-datatable-api-arrow-color, var(--paper-light-green-600)); 67 | transform: rotate(180deg); 68 | } 69 | `; 70 | return [main, ironFlexLayoutTheme, ironFlexLayoutAlignTheme]; 71 | } 72 | 73 | render() { 74 | return html` 75 |
76 |
77 | 78 |
79 | 80 | 81 | ${this.getTooltipText(this.direction)} 82 |
83 | `; 84 | } 85 | 86 | handleSort() { 87 | switch (this.direction) { 88 | case '': 89 | this.direction = 'desc'; 90 | break; 91 | case 'desc': 92 | this.direction = 'asc'; 93 | break; 94 | default: 95 | this.direction = ''; 96 | break; 97 | } 98 | 99 | this.dispatchEvent(new CustomEvent('direction-changed', { detail: { value: this.direction } })); 100 | } 101 | 102 | getTooltipText(direction: 'asc' | 'desc' | '') { 103 | if (direction === 'asc') { 104 | return this.localize('sortCancel'); 105 | } if (direction === 'desc') { 106 | return this.localize('sortAZ'); 107 | } 108 | return this.localize('sortZA'); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /iron-flex-import.ts: -------------------------------------------------------------------------------- 1 | import { unsafeCSS } from 'lit'; 2 | import '@polymer/iron-flex-layout/iron-flex-layout-classes'; 3 | 4 | const ironFlexLayoutThemeTemplate = customElements.get('dom-module').import('iron-flex', 'template'); 5 | const ironFlexLayoutAlignThemeTemplate = customElements.get('dom-module').import('iron-flex-alignment', 'template'); 6 | 7 | export const ironFlexLayoutTheme = unsafeCSS( 8 | ironFlexLayoutThemeTemplate.content.firstElementChild.textContent 9 | ); 10 | export const ironFlexLayoutAlignTheme = unsafeCSS( 11 | ironFlexLayoutAlignThemeTemplate.content.firstElementChild.textContent 12 | ); 13 | -------------------------------------------------------------------------------- /lit-datatable-column.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LitElement, css, html, PropertyValues 3 | } from 'lit'; 4 | import { property, customElement } from 'lit/decorators.js'; 5 | import './helpers/ld-header-with-sort'; 6 | import './helpers/ld-header-with-filter'; 7 | import './helpers/ld-header-with-date-and-sort'; 8 | import './helpers/ld-header-with-filter-and-sort'; 9 | import './helpers/ld-header-with-choices'; 10 | import { Language } from './localize'; 11 | import type { Choice } from './helpers/ld-header-with-choices'; 12 | 13 | type TypeOfColumn = 'sort' | 'filter' | 'choices' | 'dateSortNoRange' | 'dateSort' | 'filterSort'; 14 | 15 | @customElement('lit-datatable-column') 16 | export class LitDatatableColumn extends LitElement { 17 | @property({ type: String }) property = ''; 18 | 19 | @property({ type: Array }) otherProperties: Array = []; 20 | 21 | @property({ attribute: false }) html: ((value: any, otherValues?: any) => any) | null = null; 22 | 23 | @property({ type: Array }) eventsForDom: Array = []; 24 | 25 | @property({ type: String }) sort = ''; 26 | 27 | @property({ type: Boolean }) enableFilter = false; 28 | 29 | @property({ type: String }) type?: TypeOfColumn = undefined; 30 | 31 | @property({ type: String }) language: Language = 'en'; 32 | 33 | @property({ attribute: false }) sortEvent: EventListener | null = null; 34 | 35 | @property({ attribute: false }) choicesEvent: EventListener | null = null; 36 | 37 | @property({ attribute: false }) dateSortEvent: EventListener | null = null; 38 | 39 | @property({ attribute: false }) filterEvent: EventListener | null = null; 40 | 41 | @property({ type: String }) filterValue = ''; 42 | 43 | @property({ type: Array }) choices: Array = []; 44 | 45 | @property({ type: Array }) selectedChoices: Array = []; 46 | 47 | @property({ type: String }) start = ''; 48 | 49 | @property({ type: String }) end = ''; 50 | 51 | @property({ type: String }) horizontalAlign: 'left' | 'right' = 'left'; 52 | 53 | @property({ type: Boolean }) column = false; 54 | 55 | @property({ type: Boolean }) header = false; 56 | 57 | @property({ type: String }) columnStyle = ''; 58 | 59 | timeoutFilterText = 0; 60 | 61 | static get styles() { 62 | const mainStyle = css` 63 | :host { 64 | display: block; 65 | } 66 | `; 67 | 68 | return [mainStyle]; 69 | } 70 | 71 | render() { 72 | return null; 73 | } 74 | 75 | updated(properties: PropertyValues) { 76 | if (properties.has('html')) { 77 | this.dispatchEvent(new CustomEvent('html-changed')); 78 | } 79 | 80 | if (properties.has('type') || properties.has('sort')) { 81 | if (this.type === 'sort') { 82 | this.html = (value: string, p: string) => html` 83 | 88 | ${value} 89 | `; 90 | } 91 | } 92 | 93 | if (properties.has('type') || properties.has('filterValue')) { 94 | if (this.type === 'filter') { 95 | this.html = (value: string, p: string) => html` 96 | 103 | ${value} 104 | `; 105 | } 106 | } 107 | 108 | if (properties.has('type') || properties.has('choices') || properties.has('selectedChoices')) { 109 | if (this.type === 'choices') { 110 | this.html = (value: string, p: string) => html` 111 | 117 | ${value} 118 | `; 119 | } 120 | } 121 | 122 | if (properties.has('type') || properties.has('start') || properties.has('stop') || properties.has('sort')) { 123 | if (this.type === 'dateSort') { 124 | this.html = (value: string, p: string) => html` 125 | 139 | `; 140 | } else if (this.type === 'dateSortNoRange') { 141 | this.html = (value: string, p: string) => html` 142 | 156 | `; 157 | } 158 | } 159 | if (properties.has('type') || properties.has('sort') || properties.has('filterValue')) { 160 | if (this.type === 'filterSort') { 161 | this.html = (value: string, p: string) => html` 162 | 172 | `; 173 | } 174 | } 175 | } 176 | 177 | getSortDirection(sort: string, p: string) { 178 | if (sort) { 179 | const splittedSort = this.sort.split(','); 180 | if (splittedSort) { 181 | if (splittedSort[0] === p) { 182 | return splittedSort[1]; 183 | } 184 | } 185 | } 186 | return ''; 187 | } 188 | 189 | handleSortDirectionChanged(p: string, { detail }: CustomEvent<{value: string}>) { 190 | const splittedSort = this.sort.split(','); 191 | if (detail.value) { 192 | this.sort = `${p},${detail.value}`; 193 | this.dispatchEvent(new CustomEvent('sort', { detail: { value: this.sort } })); 194 | } else if (splittedSort && splittedSort[0] === p) { 195 | this.sort = ''; 196 | this.dispatchEvent(new CustomEvent('sort', { detail: { value: this.sort } })); 197 | } 198 | } 199 | 200 | handleFilterTextChanged({ detail }: CustomEvent) { 201 | if (this.timeoutFilterText) { 202 | clearTimeout(this.timeoutFilterText); 203 | } 204 | 205 | this.timeoutFilterText = window.setTimeout( 206 | () => this.dispatchEvent(new CustomEvent('filter', { detail })), 207 | 1000 208 | ); 209 | } 210 | 211 | handleFilterChoiceChanged({ detail }: CustomEvent) { 212 | this.dispatchEvent(new CustomEvent('choices', { detail })); 213 | } 214 | 215 | dateChanged({ detail }: CustomEvent) { 216 | this.dispatchEvent(new CustomEvent('dates', { detail })); 217 | } 218 | } 219 | 220 | declare global { 221 | interface HTMLElementTagNameMap { 222 | 'lit-datatable-column': LitDatatableColumn; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /lit-datatable-footer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LitElement, css, html 3 | } from 'lit'; 4 | import { property, customElement } from 'lit/decorators.js'; 5 | import '@polymer/paper-dropdown-menu/paper-dropdown-menu'; 6 | import type { PaperDropdownMenuElement } from '@polymer/paper-dropdown-menu/paper-dropdown-menu'; 7 | import type { PaperListboxElement } from '@polymer/paper-listbox/paper-listbox'; 8 | import '@polymer/paper-listbox/paper-listbox'; 9 | import '@polymer/paper-item/paper-item'; 10 | import '@polymer/paper-icon-button/paper-icon-button'; 11 | import '@polymer/paper-tooltip/paper-tooltip'; 12 | import type { Language, Resources } from './localize'; 13 | import Localize from './localize'; 14 | 15 | import { ironFlexLayoutAlignTheme, ironFlexLayoutTheme } from './iron-flex-import'; 16 | 17 | type FooterPosition = 'right' | 'left'; 18 | 19 | @customElement('lit-datatable-footer') 20 | export class LitDatatableFooter extends Localize(LitElement) { 21 | language: Language | null = 'en'; 22 | 23 | resources: Resources | null = { 24 | en: { 25 | nextPage: 'Next page', 26 | previousPage: 'Previous page', 27 | linesPerPage: 'Lines per page', 28 | of: 'of', 29 | 30 | }, 31 | 'en-en': { 32 | nextPage: 'Next page', 33 | previousPage: 'Previous page', 34 | linesPerPage: 'Lines per page', 35 | of: 'of', 36 | }, 37 | 'en-US': { 38 | nextPage: 'Next page', 39 | previousPage: 'Previous page', 40 | linesPerPage: 'Lines per page', 41 | of: 'of', 42 | }, 43 | 'en-us': { 44 | nextPage: 'Next page', 45 | previousPage: 'Previous page', 46 | linesPerPage: 'Lines per page', 47 | of: 'of', 48 | }, 49 | fr: { 50 | nextPage: 'Page suivante', 51 | previousPage: 'Page précédente', 52 | linesPerPage: 'Lignes par page', 53 | of: 'sur', 54 | }, 55 | 'fr-fr': { 56 | nextPage: 'Page suivante', 57 | previousPage: 'Page précédente', 58 | linesPerPage: 'Lignes par page', 59 | of: 'sur', 60 | }, 61 | }; 62 | 63 | @property({ type: String }) footerPosition: FooterPosition = 'left'; 64 | 65 | @property({ type: Number }) size = 0; 66 | 67 | @property({ type: Number }) page = 0; 68 | 69 | @property({ type: Number }) totalElements = 0; 70 | 71 | @property({ type: Number }) totalPages = 0; 72 | 73 | @property({ type: Array }) availableSize: Array = []; 74 | 75 | static get styles() { 76 | const mainStyle = css` 77 | :host { 78 | display: block; 79 | } 80 | 81 | .foot { 82 | font-size: 12px; 83 | font-weight: normal; 84 | height: 55px; 85 | border-top: 1px solid; 86 | border-color: var(--lit-datatable-divider-color, rgba(0, 0, 0, var(--dark-divider-opacity))); 87 | padding: 0 14px 0 0; 88 | color: var(--lit-datatable-footer-color, rgba(0, 0, 0, var(--dark-secondary-opacity))); 89 | } 90 | 91 | .foot .left { 92 | padding: 0 0 0 14px; 93 | } 94 | 95 | .foot paper-icon-button { 96 | width: 24px; 97 | height: 24px; 98 | padding: 0; 99 | margin-left: 24px; 100 | } 101 | 102 | .foot .status { 103 | margin: 0 8px 0 32px; 104 | } 105 | 106 | .foot .size { 107 | width: 64px; 108 | text-align: right; 109 | } 110 | 111 | paper-dropdown-menu { 112 | color: var(--lit-datatable-footer-color, rgba(0, 0, 0, var(--dark-secondary-opacity))); 113 | } 114 | `; 115 | return [mainStyle, ironFlexLayoutAlignTheme, ironFlexLayoutTheme]; 116 | } 117 | 118 | render() { 119 | return html` 120 | 139 |
140 |
141 |
142 |
143 |
144 | ${this.localize('linesPerPage')} 145 |
146 |
147 | 148 | 149 | ${this.availableSize && this.availableSize.map((size) => html` 150 | ${size} 151 | `)} 152 | 153 | 154 |
155 |
156 |
157 | ${this.computeCurrentSize(this.page, this.size, this.totalElements)}-${this.computeCurrentMaxSize(this.page, this.size, this.totalElements)} 158 | ${this.localize('of')} 159 | ${this.totalElements} 160 |
161 | 162 | ${this.localize('previousPage')} 163 | 164 | ${this.localize('nextPage')} 165 |
166 |
167 |
168 | `; 169 | } 170 | 171 | computeCurrentSize(page: number, size: number, totalElements: number) { 172 | if (totalElements) { 173 | return (page * size) + 1; 174 | } 175 | return 0; 176 | } 177 | 178 | computeCurrentMaxSize(page: number, size: number, totalElements: number) { 179 | const maxSize = size * (page + 1); 180 | return maxSize > totalElements ? totalElements : maxSize; 181 | } 182 | 183 | launchEvent() { 184 | this.dispatchEvent(new CustomEvent('page-or-size-changed', { detail: { page: this.page, size: this.size } })); 185 | } 186 | 187 | nextPage() { 188 | if (this.page + 1 < this.totalPages) { 189 | this.page += 1; 190 | this.launchEvent(); 191 | } 192 | } 193 | 194 | prevPage() { 195 | if (this.page > 0) { 196 | this.page -= 1; 197 | this.launchEvent(); 198 | } 199 | } 200 | 201 | nextButtonDisabled(page: number, totalPages: number) { 202 | return totalPages === 0 || page + 1 === totalPages; 203 | } 204 | 205 | prevButtonDisabled(page: number) { 206 | return page === 0; 207 | } 208 | 209 | newSizeIsSelected({ currentTarget }: CustomEvent) { 210 | const paperListBox = currentTarget as PaperListboxElement; 211 | let newSize = paperListBox.selected; 212 | if (newSize) { 213 | if (typeof newSize === 'string') { 214 | newSize = parseInt(newSize, 10); 215 | } 216 | if (newSize !== this.size) { 217 | this.page = 0; 218 | this.size = newSize; 219 | this.launchEvent(); 220 | } 221 | } 222 | } 223 | 224 | computePosition(position: FooterPosition) { 225 | if (position === 'right') { 226 | return 'end-justified'; 227 | } 228 | return ''; 229 | } 230 | 231 | async firstUpdated() { 232 | await this.updateComplete; 233 | if (this.shadowRoot) { 234 | const paperDropdownMenu = this.shadowRoot.querySelector('paper-dropdown-menu'); 235 | if (paperDropdownMenu) { 236 | paperDropdownMenu.updateStyles({ 237 | '--paper-input-container-underline_-_display': 'none', 238 | '--paper-input-container-shared-input-style_-_font-weight': '500', 239 | '--paper-input-container-shared-input-style_-_text-align': 'right', 240 | '--paper-input-container-shared-input-style_-_font-size': '12px', 241 | '--paper-input-container-shared-input-style_-_color': 'var(--paper-datatable-navigation-bar-text-color, rgba(0, 0, 0, .54))', 242 | '--paper-input-container-input-color': 'var(--paper-datatable-navigation-bar-text-color, rgba(0, 0, 0, .54))', 243 | '--disabled-text-color': 'var(--paper-datatable-navigation-bar-text-color, rgba(0, 0, 0, .54))', 244 | }); 245 | } 246 | } 247 | } 248 | } 249 | 250 | declare global { 251 | interface HTMLElementTagNameMap { 252 | 'lit-datatable-footer': LitDatatableFooter; 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /lit-datatable.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LitElement, css, html, PropertyValues, render 3 | } from 'lit'; 4 | import { property, customElement } from 'lit/decorators.js'; 5 | import { deepEqual } from 'fast-equals'; 6 | 7 | import type { LitDatatableColumn } from './lit-datatable-column'; 8 | 9 | export interface Conf { 10 | header: string; 11 | property: string; 12 | hidden?: boolean; 13 | } 14 | 15 | interface EventOfTr { 16 | type: string; 17 | event: (item: any) => void; 18 | } 19 | 20 | interface TableElement { 21 | element: HTMLTableRowElement; 22 | columns: Array; 23 | events: Array; 24 | } 25 | 26 | @customElement('lit-datatable') 27 | export class LitDatatable extends LitElement { 28 | @property({ type: Array }) data: Array = []; 29 | 30 | @property({ type: Array }) conf: Array = []; 31 | 32 | @property({ type: Array }) table: Array = []; 33 | 34 | @property({ type: String }) sort = ''; 35 | 36 | @property({ type: Array }) headers: Array = []; 37 | 38 | @property({ type: Boolean, attribute: 'sticky-header' }) stickyHeader = false; 39 | 40 | @property({ type: Object }) datatableColumns: Map = new Map(); 41 | 42 | @property({ type: Object }) datatableHeaders: Map = new Map(); 43 | 44 | @property({ type: Number }) lastConfSize = 0; 45 | 46 | @property({ type: Number }) lastDataSize = 0; 47 | 48 | /** 49 | * The property's name that is a unique key for each element in "data" 50 | * (e.g. "productId" or "id") 51 | * 52 | */ 53 | @property({ type: String }) key?:string; 54 | 55 | debounceGenerate = 0; 56 | 57 | static get styles() { 58 | const mainStyle = css` 59 | :host { 60 | display: block; 61 | } 62 | 63 | slot { 64 | display: none; 65 | } 66 | 67 | table { 68 | width: 100%; 69 | border-spacing: 0px; 70 | border-collapse: seperate; 71 | } 72 | 73 | th { 74 | background: var(--lit-datatable-th-background, white); 75 | color: var(--lit-datatable-th-color, rgba(0, 0, 0, var(--dark-secondary-opacity))); 76 | text-align: left; 77 | white-space: nowrap; 78 | 79 | font-weight: var(--lit-datatable-api-header-weight, 500); 80 | font-size: var(--lit-datatable-api-header-font-size, 12px); 81 | padding: var(--lit-datatable-api-header-padding, 6px 26px); 82 | 83 | border-bottom: 1px solid; 84 | border-color: var(--lit-datatable-divider-color, rgba(0, 0, 0, var(--dark-divider-opacity))) 85 | } 86 | 87 | th.sticky { 88 | position: sticky; 89 | background: var(--lit-datatable-th-background, white); 90 | top: 0; 91 | z-index: 1; 92 | } 93 | 94 | tbody td { 95 | height: var(--lit-datatable-api-body-td-height, 43px); 96 | } 97 | 98 | tbody tr { 99 | height: var(--lit-datatable-api-body-tr-height, 43px); 100 | } 101 | 102 | thead tr { 103 | height: var(--lit-datatable-api-header-tr-height, 43px); 104 | } 105 | 106 | thead th { 107 | height: var(--lit-datatable-api-header-th-height, 43px); 108 | } 109 | 110 | tbody tr:nth-child(even) { 111 | background-color: var(--lit-datatable-api-tr-even-background-color, none); 112 | } 113 | 114 | tbody tr:nth-child(odd) { 115 | background-color: var(--lit-datatable-api-tr-odd-background-color, none); 116 | } 117 | 118 | tbody tr:hover { 119 | background: var(--lit-datatable-api-tr-hover-background-color, none); 120 | } 121 | 122 | tbody tr.is-currently-highlight { 123 | background: var(--lit-datatable-api-tr-highlight-background-color, none); 124 | } 125 | 126 | tbody tr.selected { 127 | background-color: var(--lit-datatable-api-tr-selected-background, var(--paper-grey-100)); 128 | } 129 | 130 | td { 131 | font-size: var(--lit-datatable-td-font-size, 13px); 132 | font-weight: normal; 133 | color: var(--lit-datatable-td-color, rgba(0, 0, 0, var(--dark-primary-opacity))); 134 | padding: var(--lit-datatable-api-td-padding, 6px var(--lit-datatable-api-horizontal-padding, 26px)); 135 | cursor: var(--lit-datatable-api-td-cursor, inherit); 136 | height: 36px; 137 | } 138 | 139 | tbody tr:not(:first-child) td { 140 | border-top: var(--lit-datatable-api-td-border-top, 1px solid); 141 | border-color: var(--lit-datatable-divider-color, rgba(0, 0, 0, var(--dark-divider-opacity))) 142 | } 143 | `; 144 | 145 | return [mainStyle]; 146 | } 147 | 148 | render() { 149 | return html` 150 | 151 | 152 | 153 | 154 |
155 | `; 156 | } 157 | 158 | updated(properties: PropertyValues<{ data: Array; conf: Array; sort: string, stickyHeader: boolean }>) { 159 | // Data or conf change we have to generate the table 160 | if ((properties.has('data') && !deepEqual(properties.get('data'), this.data)) 161 | || (properties.has('conf') && !deepEqual(properties.get('conf'), this.conf))) { 162 | this.deleteAllEvents(); 163 | this.generateData(); 164 | } 165 | 166 | if (properties.has('conf') || properties.has('stickyHeader')) { 167 | const confs = [...this.conf].filter((c) => !c.hidden); 168 | this.updateHeaders(confs); 169 | } 170 | 171 | if (properties.has('sort')) { 172 | this.updateSortHeaders(); 173 | } 174 | } 175 | 176 | updateSortHeaders() { 177 | if (this.sort !== undefined && this.sort !== null) { 178 | this.datatableHeaders.forEach((d) => { d.sort = this.sort; }); 179 | } 180 | } 181 | 182 | firstUpdated() { 183 | if (this.shadowRoot) { 184 | const slot = this.shadowRoot.querySelector('slot'); 185 | if (slot) { 186 | const assignedNodes = slot.assignedNodes() as Array; 187 | this.datatableColumns = new Map(assignedNodes 188 | .filter((a) => a.tagName === 'LIT-DATATABLE-COLUMN' && a.column) 189 | .map((a) => [a.property, a])); 190 | this.datatableHeaders = new Map(assignedNodes 191 | .filter((a) => a.tagName === 'LIT-DATATABLE-COLUMN' && a.header) 192 | .map((a) => [a.property, a])); 193 | } 194 | } 195 | } 196 | 197 | deleteAllEvents() { 198 | this.datatableColumns.forEach((datatableColumn) => { 199 | datatableColumn.eventsForDom.forEach((renderer) => { 200 | datatableColumn.removeEventListener('html-changed', renderer); 201 | }); 202 | }); 203 | } 204 | 205 | renderCell(item: any, td: HTMLTableCellElement, confProperty: string, event?: Event, litDatatableColumn?: LitDatatableColumn) { 206 | if (event) { 207 | litDatatableColumn = event.currentTarget as LitDatatableColumn; 208 | } 209 | if (litDatatableColumn) { 210 | const otherProperties = this.getOtherValues(litDatatableColumn, item); 211 | if (litDatatableColumn?.html) { 212 | render(litDatatableColumn.html( 213 | this.extractData(item, litDatatableColumn.property), otherProperties 214 | ), td); 215 | } else if (litDatatableColumn) { 216 | render(this.extractData(item, litDatatableColumn.property), td); 217 | } 218 | } else if (confProperty) { 219 | render(this.extractData(item, confProperty), td); 220 | } 221 | } 222 | 223 | setEventListener(datatableColumn: LitDatatableColumn, lineIndex: number, renderer: EventListener) { 224 | if (datatableColumn) { 225 | if (datatableColumn.eventsForDom[lineIndex]) { 226 | datatableColumn.removeEventListener('html-changed', datatableColumn.eventsForDom[lineIndex]); 227 | } 228 | datatableColumn.eventsForDom[lineIndex] = renderer; 229 | datatableColumn.addEventListener('html-changed', datatableColumn.eventsForDom[lineIndex]); 230 | } 231 | } 232 | 233 | getOtherValues(datatableColumn: LitDatatableColumn, item: any) { 234 | let otherProperties = {}; 235 | if (datatableColumn && datatableColumn.otherProperties) { 236 | otherProperties = datatableColumn.otherProperties.reduce((obj: any, key: string) => { 237 | obj[key] = item[key]; 238 | return obj; 239 | }, {}); 240 | } 241 | return otherProperties; 242 | } 243 | 244 | renderHtml(conf: Conf, lineIndex: number, item: any, td: HTMLTableCellElement, tr: HTMLTableRowElement) { 245 | const p = conf.property; 246 | const datatableColumn = this.datatableColumns.get(p); 247 | if (datatableColumn) { 248 | this.setEventListener(datatableColumn, lineIndex, this.renderCell.bind(this, item, td, p)); 249 | } 250 | this.renderCell(item, td, p, undefined, datatableColumn); 251 | tr.appendChild(td); 252 | } 253 | 254 | cleanEventsOfTr(item: any) { 255 | item.events.forEach((event: EventOfTr) => item.element.removeEventListener(event.type, event.event)); 256 | } 257 | 258 | createEventsOfTr(tr: HTMLTableRowElement, item: any): Array { 259 | const trTapEvent = this.trTap.bind(this, item); 260 | const trOverEvent = this.trHover.bind(this, item); 261 | const trOutEvent = this.trOut.bind(this, item); 262 | tr.addEventListener('tap', trTapEvent); 263 | tr.addEventListener('mouseover', trOverEvent); 264 | tr.addEventListener('mouseout', trOutEvent); 265 | return [{ type: 'mouseover', event: trOverEvent }, { type: 'mouseout', event: trOutEvent }, { type: 'tap', event: trTapEvent }]; 266 | } 267 | 268 | cleanTrElements() { 269 | const splices = this.table.splice(this.data.length); 270 | 271 | splices.forEach((line: TableElement) => { 272 | this.cleanEventsOfTr(line); 273 | if (line?.element?.parentNode) { 274 | line.element.parentNode.removeChild(line.element); 275 | } 276 | }); 277 | } 278 | 279 | cleanTdElements(confs: Array) { 280 | [...this.table].forEach((line) => { 281 | const splicedColumns = line.columns.splice(confs.length); 282 | 283 | splicedColumns.forEach((column) => { 284 | line.element.removeChild(column); 285 | }); 286 | }); 287 | } 288 | 289 | updateHeaders(confs: Array) { 290 | if (this.shadowRoot) { 291 | let tr = this.shadowRoot.querySelector('table thead tr'); 292 | if (!tr) { 293 | tr = document.createElement('tr'); 294 | } 295 | if (this.lastConfSize > confs.length) { 296 | [...this.headers].forEach((header, i) => { 297 | if (i <= (this.lastConfSize - 1)) { 298 | if (tr) { 299 | tr.removeChild(header); 300 | } 301 | this.headers.splice(i, 1); 302 | } 303 | }); 304 | } 305 | confs.forEach((conf: Conf, i: number) => { 306 | const p = conf.property; 307 | const datatableHeader = this.datatableHeaders.get(p); 308 | let th: HTMLTableHeaderCellElement; 309 | if (this.headers[i]) { 310 | th = this.headers[i]; 311 | } else { 312 | th = document.createElement('th'); 313 | this.headers.push(th); 314 | } 315 | th.classList.toggle('sticky', this.stickyHeader); 316 | if (datatableHeader && datatableHeader.columnStyle) { 317 | th.setAttribute('style', datatableHeader.columnStyle); 318 | } else { 319 | th.setAttribute('style', ''); 320 | } 321 | if (this.stickyHeader) { 322 | th.style.zIndex = `${confs.length - i}`; 323 | } 324 | if (datatableHeader) { 325 | th.dataset.property = p; 326 | this.setEventListener(datatableHeader, 0, 327 | () => { 328 | if (th.dataset.property === datatableHeader.property) { 329 | render(datatableHeader.html ? datatableHeader.html(conf.header, datatableHeader.property) : null, th); 330 | } 331 | }); 332 | if (datatableHeader.type === 'sort' || datatableHeader.type === 'filterSort') { 333 | if (datatableHeader.sortEvent) { 334 | datatableHeader.removeEventListener('sort', datatableHeader.sortEvent as EventListener); 335 | } 336 | datatableHeader.sortEvent = this.dispatchCustomEvent.bind(this, 'sort') as EventListener; 337 | datatableHeader.addEventListener('sort', datatableHeader.sortEvent as EventListener); 338 | } 339 | if (datatableHeader.type === 'filter' || datatableHeader.type === 'filterSort') { 340 | if (datatableHeader.filterEvent) { 341 | datatableHeader.removeEventListener('filter', datatableHeader.filterEvent as EventListener); 342 | } 343 | datatableHeader.filterEvent = this.dispatchCustomEvent.bind(this, 'filter') as EventListener; 344 | datatableHeader.addEventListener('filter', datatableHeader.filterEvent as EventListener); 345 | } 346 | if (datatableHeader.type === 'choices') { 347 | if (datatableHeader.choicesEvent) { 348 | datatableHeader.removeEventListener('choices', datatableHeader.choicesEvent as EventListener); 349 | } 350 | datatableHeader.choicesEvent = this.dispatchCustomEvent.bind(this, 'choices') as EventListener; 351 | datatableHeader.addEventListener('choices', datatableHeader.choicesEvent as EventListener); 352 | } 353 | if (datatableHeader.type === 'dateSort' || datatableHeader.type === 'dateSortNoRange') { 354 | if (datatableHeader.dateSortEvent) { 355 | datatableHeader.removeEventListener('dates', datatableHeader.dateSortEvent as EventListener); 356 | } 357 | datatableHeader.dateSortEvent = this.dispatchCustomEvent.bind(this, 'dates') as EventListener; 358 | datatableHeader.addEventListener('dates', datatableHeader.dateSortEvent as EventListener); 359 | if (datatableHeader.sortEvent) { 360 | datatableHeader.removeEventListener('sort', datatableHeader.sortEvent as EventListener); 361 | } 362 | datatableHeader.sortEvent = this.dispatchCustomEvent.bind(this, 'sort') as EventListener; 363 | datatableHeader.addEventListener('sort', datatableHeader.sortEvent as EventListener); 364 | } 365 | } 366 | if (datatableHeader && datatableHeader.html) { 367 | render(datatableHeader.html(conf.header, datatableHeader.property), th); 368 | } else { 369 | render(conf.header, th); 370 | } 371 | if (tr) { 372 | tr.appendChild(th); 373 | } 374 | }); 375 | if (this.shadowRoot) { 376 | const thead = this.shadowRoot.querySelector('thead'); 377 | if (thead) { 378 | thead.appendChild(tr); 379 | } 380 | } 381 | } 382 | } 383 | 384 | dispatchCustomEvent(key: string, { detail }: CustomEvent): any { 385 | this.dispatchEvent(new CustomEvent(key, { detail })); 386 | } 387 | 388 | trCreated(tr: HTMLTableRowElement, lineIndex: number, item: any) { 389 | this.dispatchEvent(new CustomEvent('tr-create', { detail: { tr, lineIndex, item } })); 390 | } 391 | 392 | trTap(item: any) { 393 | this.dispatchEvent(new CustomEvent('tap-tr', { detail: item })); 394 | } 395 | 396 | trHover(item: any) { 397 | this.dispatchEvent(new CustomEvent('tr-mouseover', { detail: item })); 398 | } 399 | 400 | trOut(item: any) { 401 | this.dispatchEvent(new CustomEvent('tr-mouseout', { detail: item })); 402 | } 403 | 404 | createTr(lineIndex: number, item: any) { 405 | const tr = this.setKeyToTr(document.createElement('tr'), item); 406 | if (!this.table[lineIndex]) { 407 | this.table[lineIndex] = { element: tr, columns: [], events: this.createEventsOfTr(tr, item) }; 408 | } 409 | return tr; 410 | } 411 | 412 | createTd(lineIndex: number) { 413 | const td = document.createElement('td') as HTMLTableCellElement; 414 | this.table[lineIndex].columns.push(td); 415 | return td; 416 | } 417 | 418 | setKeyToTr(tr: HTMLTableRowElement, item: any) { 419 | if (this.key && Object.prototype.hasOwnProperty.call(item, this.key)) { 420 | const data = this.extractData(item, this.key); 421 | tr.classList.add(`key-${data}`); 422 | } 423 | return tr; 424 | } 425 | 426 | updateBody(confs: Array) { 427 | if (this.data !== undefined) { 428 | if (this.lastConfSize > confs.length) { 429 | this.cleanTdElements(confs); 430 | } 431 | if (this.lastDataSize > this.data.length) { 432 | this.cleanTrElements(); 433 | } 434 | this.data.forEach((item, lineIndex: number) => { 435 | let tr: HTMLTableRowElement; 436 | if (this.table[lineIndex]) { 437 | this.cleanEventsOfTr(this.table[lineIndex]); 438 | tr = this.table[lineIndex].element; 439 | tr.className = ''; 440 | tr = this.setKeyToTr(tr, item); 441 | this.table[lineIndex].events = this.createEventsOfTr(tr, item); 442 | } else { 443 | tr = this.createTr(lineIndex, item); 444 | } 445 | 446 | this.trCreated(tr, lineIndex, item); 447 | 448 | confs.forEach((conf, columnIndex) => { 449 | let td; 450 | if (this.table[lineIndex].columns[columnIndex]) { 451 | td = this.table[lineIndex].columns[columnIndex]; 452 | } else { 453 | td = this.createTd(lineIndex); 454 | } 455 | 456 | const datatableColumn = this.datatableColumns.get(conf.property); 457 | if (datatableColumn && datatableColumn.columnStyle) { 458 | td.setAttribute('style', datatableColumn.columnStyle); 459 | } else { 460 | td.setAttribute('style', ''); 461 | } 462 | 463 | this.renderHtml(conf, lineIndex, item, td, tr); 464 | }); 465 | if (this.shadowRoot) { 466 | const tbody = this.shadowRoot.querySelector('tbody'); 467 | if (tbody) { 468 | tbody.appendChild(tr); 469 | } 470 | } 471 | }); 472 | } 473 | } 474 | 475 | setLoading(loading: boolean) { 476 | this.dispatchEvent(new CustomEvent('loading', { detail: { value: loading } })); 477 | } 478 | 479 | async generateData() { 480 | this.setLoading(true); 481 | await this.updateComplete; 482 | const confs = [...this.conf].filter((c) => !c.hidden); 483 | this.updateBody(confs); 484 | if (this.data !== undefined) { 485 | this.lastDataSize = this.data.length; 486 | this.lastConfSize = confs.length; 487 | } 488 | this.setLoading(false); 489 | } 490 | 491 | extractData(item: any, columnProperty: string) { 492 | if (columnProperty) { 493 | const splittedProperties = columnProperty.split('.'); 494 | if (splittedProperties.length > 1) { 495 | return splittedProperties.reduce((prevRow: any, p: string) => { 496 | if (typeof prevRow === 'string' && item[prevRow] !== undefined && item[prevRow][p] !== undefined) { 497 | return item[prevRow][p]; 498 | } 499 | 500 | return prevRow[p] || ''; 501 | }); 502 | } 503 | return item[columnProperty]; 504 | } 505 | return null; 506 | } 507 | 508 | /** 509 | * Scroll to a tr with the key 510 | * The key property have to be set 511 | * 512 | */ 513 | async scrollOnTr(key: string) { 514 | if (this.shadowRoot && key) { 515 | await this.updateComplete; 516 | const classPrimaryDisplayed = 'is-currently-highlight'; 517 | this.shadowRoot.querySelectorAll(`.${classPrimaryDisplayed}`).forEach((tr) => { 518 | tr.classList.remove(classPrimaryDisplayed); 519 | }); 520 | const trToScroll = this.shadowRoot.querySelector(`tr.key-${key}`); 521 | if (trToScroll) { 522 | trToScroll.scrollIntoView({ block: 'center', inline: 'nearest' }); 523 | trToScroll.classList.add(classPrimaryDisplayed); 524 | } 525 | } 526 | } 527 | } 528 | 529 | declare global { 530 | interface HTMLElementTagNameMap { 531 | 'lit-datatable': LitDatatable; 532 | } 533 | } 534 | -------------------------------------------------------------------------------- /localize.ts: -------------------------------------------------------------------------------- 1 | export type Language = 'en' | 'fr' | 'en-en' | 'en-US' | 'en-us' | 'fr-fr'; 2 | 3 | interface Resource { 4 | [key: string]: string; 5 | } 6 | 7 | type Constructor = { 8 | new (...args: any[]): T; 9 | }; 10 | 11 | export interface Resources { 12 | en: Resource; 13 | 'en-en': Resource; 14 | 'en-US': Resource; 15 | 'en-us': Resource; 16 | fr: Resource; 17 | 'fr-fr': Resource; 18 | } 19 | 20 | export default >(subclass: C) => class extends subclass { 21 | language: Language | null = null; 22 | 23 | resources: Resources | null = null; 24 | 25 | static get properties() { 26 | return { 27 | language: { type: String }, 28 | resources: { type: Object }, 29 | }; 30 | } 31 | 32 | localize(key: string) { 33 | if (this.resources && this.language 34 | && this.resources[this.language] && this.resources[this.language][key]) { 35 | return this.resources[this.language][key]; 36 | } 37 | return ''; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@doubletrade/lit-datatable", 3 | "version": "1.22.0", 4 | "description": "lit-datatable is a material design implementation of a data table, powered by lit-element.", 5 | "main": "lit-datatable.js", 6 | "scripts": { 7 | "lint": "eslint . --ext js,ts --ignore-path .gitignore --quiet", 8 | "prepublishOnly": "npm run lint && npm run build", 9 | "serve": "wds --node-resolve --app-index demo/index.html", 10 | "build": "tsc", 11 | "build:watch": "tsc --watch", 12 | "test": "wtr", 13 | "test:watch": "wtr --watch" 14 | }, 15 | "files": [ 16 | "*.js", 17 | "**/*.js", 18 | "*.d.ts", 19 | "**/*.d.ts", 20 | "**/*.js.map" 21 | ], 22 | "author": "", 23 | "license": "ISC", 24 | "dependencies": { 25 | "@doubletrade/lit-datepicker": "1.0.0", 26 | "@polymer/iron-demo-helpers": "^3.1.0", 27 | "@polymer/iron-flex-layout": "^3.0.1", 28 | "@polymer/iron-icon": "^3.0.1", 29 | "@polymer/iron-icons": "^3.0.1", 30 | "@polymer/paper-dropdown-menu": "^3.2.0", 31 | "@polymer/paper-icon-button": "^3.0.2", 32 | "@polymer/paper-input": "^3.2.1", 33 | "@polymer/paper-item": "^3.0.1", 34 | "@polymer/paper-listbox": "^3.0.1", 35 | "@polymer/paper-tooltip": "^3.0.1", 36 | "@polymer/polymer": "^3.4.1", 37 | "fast-equals": "^2.0.0", 38 | "lit": "2.0.0-rc.2" 39 | }, 40 | "devDependencies": { 41 | "@changesets/cli": "2.14.1", 42 | "@open-wc/testing": "^2.5.32", 43 | "@typescript-eslint/eslint-plugin": "4.17.0", 44 | "@typescript-eslint/parser": "4.17.0", 45 | "@web/dev-server": "0.1.8", 46 | "@web/test-runner": "^0.12.17", 47 | "@web/test-runner-saucelabs": "^0.5.0", 48 | "eslint": "7.22.0", 49 | "eslint-config-airbnb-base": "14.2.1", 50 | "eslint-import-resolver-typescript": "2.4.0", 51 | "eslint-plugin-html": "6.1.2", 52 | "eslint-plugin-import": "2.22.1", 53 | "npm-check-updates": "^11.3.0", 54 | "sinon": "^9.2.4", 55 | "typescript": "^4.2.3" 56 | }, 57 | "repository": { 58 | "type": "git", 59 | "url": "https://github.com/DoubleTrade/lit-datatable" 60 | }, 61 | "type": "module", 62 | "publishConfig": { 63 | "access": "public" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DoubleTrade/lit-datatable/03357189e4de4552cadca1abe1fa0dc22e3ceced/screen.png -------------------------------------------------------------------------------- /test/integration/component-use-lit-datatable.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LitElement, html 3 | } from 'lit'; 4 | 5 | import { property, customElement } from 'lit/decorators.js'; 6 | import type { Conf } from '../../lit-datatable'; 7 | import '../../lit-datatable'; 8 | import '../../lit-datatable-column'; 9 | 10 | @customElement('component-use-lit-datatable') 11 | export class ComponentUseLitDatatable extends LitElement { 12 | @property({ type: Array }) testString = 'test'; 13 | 14 | @property({ type: Array }) data: Array = [ 15 | { fruit: 'apple', color: 'green', weight: '100gr' }, 16 | { fruit: 'banana', color: 'yellow', weight: '140gr' }, 17 | ]; 18 | 19 | @property({ type: Array }) conf: Array = [ 20 | { property: 'fruit', header: 'Fruit', hidden: false }, 21 | { property: 'color', header: 'Color', hidden: false }, 22 | { property: 'weight', header: 'Weight', hidden: false }, 23 | ]; 24 | 25 | render() { 26 | const fruitRenderer = (value: string) => html` 27 | ${value}
${this.testString}
28 | `; 29 | 30 | return html` 31 | 32 | 33 | 34 | `; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/integration/lit-datatable-integration.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | fixture, html, expect, elementUpdated 3 | } from '@open-wc/testing'; 4 | import sinon from 'sinon'; 5 | import './component-use-lit-datatable'; 6 | import type { ComponentUseLitDatatable } from './component-use-lit-datatable'; 7 | import type { LitDatatableColumn } from '../../lit-datatable-column'; 8 | 9 | class LitDatatableIntegrationTest { 10 | el!: ComponentUseLitDatatable; 11 | 12 | async init() { 13 | const litDatatable = html` 14 | 15 | `; 16 | this.el = await fixture(litDatatable); 17 | return this.elementUpdated(); 18 | } 19 | 20 | elementUpdated() { 21 | return elementUpdated(this.el); 22 | } 23 | 24 | getDatatableColumn(type: 'header' | 'column') { 25 | if (this?.el?.shadowRoot) { 26 | return this.el.shadowRoot.querySelectorAll(`lit-datatable-column[${type}]`); 27 | } 28 | return null; 29 | } 30 | } 31 | 32 | describe('lit-datatable', () => { 33 | it('check events on columns', async () => { 34 | const litDatatableWithColumn = new LitDatatableIntegrationTest(); 35 | await litDatatableWithColumn.init(); 36 | await litDatatableWithColumn.elementUpdated(); 37 | const datatableColumns = litDatatableWithColumn.getDatatableColumn('column'); 38 | 39 | expect(datatableColumns).to.be.not.equal(null); 40 | if (datatableColumns && datatableColumns[0]) { 41 | expect(datatableColumns.length).to.be.not.equal(0); 42 | expect(datatableColumns[0]).to.be.not.equal(null); 43 | const htmlChangedEvent = sinon.spy(); 44 | datatableColumns[0].addEventListener('html-changed', htmlChangedEvent); 45 | expect(htmlChangedEvent.callCount).to.be.equal(0); 46 | litDatatableWithColumn.el.testString = 'newTest'; 47 | await litDatatableWithColumn.elementUpdated(); 48 | expect(htmlChangedEvent.callCount).to.be.equal(1); 49 | litDatatableWithColumn.el.testString = 'test'; 50 | await litDatatableWithColumn.elementUpdated(); 51 | expect(htmlChangedEvent.callCount).to.be.equal(2); 52 | } 53 | }); 54 | 55 | it('check events on columns on change data', async () => { 56 | const litDatatableWithColumn = new LitDatatableIntegrationTest(); 57 | await litDatatableWithColumn.init(); 58 | await litDatatableWithColumn.elementUpdated(); 59 | const datatableColumns = litDatatableWithColumn.getDatatableColumn('column'); 60 | 61 | expect(datatableColumns).to.be.not.equal(null); 62 | if (datatableColumns && datatableColumns[0]) { 63 | expect(datatableColumns.length).to.be.not.equal(0); 64 | expect(datatableColumns[0]).to.be.not.equal(null); 65 | const htmlChangedEvent = sinon.spy(); 66 | datatableColumns[0].addEventListener('html-changed', htmlChangedEvent); 67 | expect(htmlChangedEvent.callCount).to.be.equal(0); 68 | litDatatableWithColumn.el.data = [ 69 | { fruit: 'apple', color: 'green', weight: '100gr' }, 70 | { fruit: 'banana', color: 'yellow', weight: '140gr' }, 71 | ]; 72 | litDatatableWithColumn.el.conf = [ 73 | { property: 'weight', header: 'Weight', hidden: false }, 74 | { property: 'color', header: 'Color', hidden: false }, 75 | { property: 'fruit', header: 'Fruit', hidden: false }, 76 | ]; 77 | await litDatatableWithColumn.elementUpdated(); 78 | expect(htmlChangedEvent.callCount).to.be.equal(1); 79 | litDatatableWithColumn.el.testString = 'newtest'; 80 | await litDatatableWithColumn.elementUpdated(); 81 | expect(htmlChangedEvent.callCount).to.be.equal(2); 82 | } 83 | }); 84 | 85 | it('check events on columns on change conf', async () => { 86 | const litDatatableWithColumn = new LitDatatableIntegrationTest(); 87 | await litDatatableWithColumn.init(); 88 | await litDatatableWithColumn.elementUpdated(); 89 | const datatableColumns = litDatatableWithColumn.getDatatableColumn('column'); 90 | 91 | expect(datatableColumns).to.be.not.equal(null); 92 | if (datatableColumns && datatableColumns[0]) { 93 | expect(datatableColumns.length).to.be.not.equal(0); 94 | expect(datatableColumns[0]).to.be.not.equal(null); 95 | const htmlChangedEvent = sinon.spy(); 96 | datatableColumns[0].addEventListener('html-changed', htmlChangedEvent); 97 | expect(htmlChangedEvent.callCount).to.be.equal(0); 98 | litDatatableWithColumn.el.conf = [ 99 | { property: 'weight', header: 'Weight', hidden: false }, 100 | { property: 'color', header: 'Color', hidden: false }, 101 | { property: 'fruit', header: 'Fruit', hidden: false }, 102 | ]; 103 | await litDatatableWithColumn.elementUpdated(); 104 | expect(htmlChangedEvent.callCount).to.be.equal(1); 105 | litDatatableWithColumn.el.testString = 'newtest'; 106 | await litDatatableWithColumn.elementUpdated(); 107 | expect(htmlChangedEvent.callCount).to.be.equal(2); 108 | } 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /test/unit/lit-datatable-with-column.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | fixture, html, expect, elementUpdated 3 | } from '@open-wc/testing'; 4 | import { TemplateResult, html as litHtml } from 'lit'; 5 | import type { LitDatatable, Conf } from '../../lit-datatable'; 6 | import '../../lit-datatable'; 7 | import '../../lit-datatable-column'; 8 | 9 | interface PropertyColumn { 10 | property: string; 11 | html: ((value: string, otherValues?: any) => TemplateResult) | null; 12 | otherProperties: Array; 13 | columnStyle?: string; 14 | } 15 | 16 | class LitDatatableWithColumnTest { 17 | el!: LitDatatable; 18 | 19 | async init(conf: Array, data: Array, columns: Array, headers: Array): Promise { 20 | const litDatatable = html` 21 | 22 | ${columns.map((column) => html` 23 | 30 | `)} 31 | ${headers.map((header) => html` 32 | 39 | `)} 40 | 41 | `; 42 | this.el = await fixture(litDatatable); 43 | return this.elementUpdated(); 44 | } 45 | 46 | elementUpdated(): Promise { 47 | return elementUpdated(this.el); 48 | } 49 | 50 | get bodyTrs() { 51 | if (this?.el?.shadowRoot) { 52 | return this.el.shadowRoot.querySelectorAll('tbody tr'); 53 | } 54 | return null; 55 | } 56 | 57 | get bodyTds() { 58 | if (this?.el?.shadowRoot) { 59 | return this.el.shadowRoot.querySelectorAll('tbody td'); 60 | } 61 | return null; 62 | } 63 | 64 | get headTrs() { 65 | if (this?.el?.shadowRoot) { 66 | return this.el.shadowRoot.querySelectorAll('thead tr'); 67 | } 68 | return null; 69 | } 70 | 71 | get headThs() { 72 | if (this?.el?.shadowRoot) { 73 | return this.el.shadowRoot.querySelectorAll('thead th'); 74 | } 75 | return null; 76 | } 77 | } 78 | 79 | const basicData = [ 80 | { fruit: 'apple', color: 'green', weight: '100gr' }, 81 | { fruit: 'banana', color: 'yellow', weight: '140gr' }, 82 | ]; 83 | 84 | const basicConf: Array = [ 85 | { property: 'fruit', header: 'Fruit', hidden: false }, 86 | { property: 'color', header: 'Color', hidden: false }, 87 | { property: 'weight', header: 'Weight', hidden: false }, 88 | ]; 89 | 90 | describe('lit-datatable', () => { 91 | it('counts', async () => { 92 | const columns: Array = [ 93 | { 94 | html: (value) => litHtml`${value} test`, 95 | property: 'fruit', 96 | otherProperties: [], 97 | }, 98 | ]; 99 | const litDatatableWithColumn = new LitDatatableWithColumnTest(); 100 | await litDatatableWithColumn.init(basicConf, basicData, columns, []); 101 | await litDatatableWithColumn.elementUpdated(); 102 | const { 103 | bodyTrs, bodyTds, headTrs, headThs, 104 | } = litDatatableWithColumn; 105 | expect(headTrs?.length).to.be.equal(1); 106 | expect(headThs?.length).to.be.equal(3); 107 | expect(bodyTrs?.length).to.be.equal(2); 108 | expect(bodyTds?.length).to.be.equal(6); 109 | }); 110 | 111 | it('header values', async () => { 112 | const columns: Array = [ 113 | { 114 | html: (value) => litHtml`${value}
test
`, 115 | property: 'fruit', 116 | otherProperties: [], 117 | }, 118 | ]; 119 | const litDatatableWithColumn = new LitDatatableWithColumnTest(); 120 | await litDatatableWithColumn.init(basicConf, basicData, [], columns); 121 | await litDatatableWithColumn.elementUpdated(); 122 | const { bodyTds, headThs } = litDatatableWithColumn; 123 | expect(bodyTds).to.be.not.equal(null); 124 | if (bodyTds) { 125 | expect(bodyTds[0]?.textContent).to.be.equal('apple'); 126 | expect(bodyTds[1]?.textContent).to.be.equal('green'); 127 | expect(bodyTds[2]?.textContent).to.be.equal('100gr'); 128 | expect(bodyTds[3]?.textContent).to.be.equal('banana'); 129 | expect(bodyTds[4]?.textContent).to.be.equal('yellow'); 130 | expect(bodyTds[5]?.textContent).to.be.equal('140gr'); 131 | } 132 | expect(headThs).to.be.not.equal(null); 133 | if (headThs) { 134 | expect(headThs[0]?.textContent).to.be.equal('Fruit test'); 135 | expect(headThs[1]?.textContent).to.be.equal('Color'); 136 | expect(headThs[2]?.textContent).to.be.equal('Weight'); 137 | } 138 | }); 139 | 140 | it('body values', async () => { 141 | const columns: Array = [ 142 | { 143 | html: (value) => litHtml`${value}
test
`, 144 | property: 'fruit', 145 | otherProperties: [], 146 | }, 147 | ]; 148 | const litDatatableWithColumn = new LitDatatableWithColumnTest(); 149 | await litDatatableWithColumn.init(basicConf, basicData, columns, []); 150 | await litDatatableWithColumn.elementUpdated(); 151 | const { bodyTds, headThs } = litDatatableWithColumn; 152 | expect(bodyTds).to.be.not.equal(null); 153 | if (bodyTds) { 154 | expect(bodyTds[0]?.textContent).to.be.equal('apple test'); 155 | expect(bodyTds[1]?.textContent).to.be.equal('green'); 156 | expect(bodyTds[2]?.textContent).to.be.equal('100gr'); 157 | expect(bodyTds[3]?.textContent).to.be.equal('banana test'); 158 | expect(bodyTds[4]?.textContent).to.be.equal('yellow'); 159 | expect(bodyTds[5]?.textContent).to.be.equal('140gr'); 160 | } 161 | expect(headThs).to.be.not.equal(null); 162 | if (headThs) { 163 | expect(headThs[0]?.textContent).to.be.equal('Fruit'); 164 | expect(headThs[1]?.textContent).to.be.equal('Color'); 165 | expect(headThs[2]?.textContent).to.be.equal('Weight'); 166 | } 167 | }); 168 | 169 | it('body other values', async () => { 170 | const columns: Array = [ 171 | { 172 | html: (value, otherValues) => litHtml`${value}
${otherValues.color}
`, 173 | property: 'fruit', 174 | otherProperties: ['color'], 175 | }, 176 | ]; 177 | const litDatatableWithColumn = new LitDatatableWithColumnTest(); 178 | await litDatatableWithColumn.init(basicConf, basicData, columns, []); 179 | await litDatatableWithColumn.elementUpdated(); 180 | const { bodyTds, headThs } = litDatatableWithColumn; 181 | expect(bodyTds).to.be.not.equal(null); 182 | if (bodyTds) { 183 | expect(bodyTds[0]?.textContent).to.be.equal('apple green'); 184 | expect(bodyTds[1]?.textContent).to.be.equal('green'); 185 | expect(bodyTds[2]?.textContent).to.be.equal('100gr'); 186 | expect(bodyTds[3]?.textContent).to.be.equal('banana yellow'); 187 | expect(bodyTds[4]?.textContent).to.be.equal('yellow'); 188 | expect(bodyTds[5]?.textContent).to.be.equal('140gr'); 189 | } 190 | expect(headThs).to.be.not.equal(null); 191 | if (headThs) { 192 | expect(headThs[0]?.textContent).to.be.equal('Fruit'); 193 | expect(headThs[1]?.textContent).to.be.equal('Color'); 194 | expect(headThs[2]?.textContent).to.be.equal('Weight'); 195 | } 196 | }); 197 | 198 | it('body values change conf', async () => { 199 | const columns: Array = [ 200 | { 201 | html: (value) => litHtml`${value}
test
`, 202 | property: 'fruit', 203 | otherProperties: [], 204 | }, 205 | ]; 206 | const litDatatableWithColumn = new LitDatatableWithColumnTest(); 207 | await litDatatableWithColumn.init(basicConf, basicData, columns, []); 208 | await litDatatableWithColumn.elementUpdated(); 209 | let bodyTds; 210 | let headThs; 211 | ({ bodyTds, headThs } = litDatatableWithColumn); 212 | expect(bodyTds).to.be.not.equal(null); 213 | if (bodyTds) { 214 | expect(bodyTds[0]?.textContent).to.be.equal('apple test'); 215 | expect(bodyTds[1]?.textContent).to.be.equal('green'); 216 | expect(bodyTds[2]?.textContent).to.be.equal('100gr'); 217 | expect(bodyTds[3]?.textContent).to.be.equal('banana test'); 218 | expect(bodyTds[4]?.textContent).to.be.equal('yellow'); 219 | expect(bodyTds[5]?.textContent).to.be.equal('140gr'); 220 | } 221 | expect(headThs).to.be.not.equal(null); 222 | if (headThs) { 223 | expect(headThs[0]?.textContent).to.be.equal('Fruit'); 224 | expect(headThs[1]?.textContent).to.be.equal('Color'); 225 | expect(headThs[2]?.textContent).to.be.equal('Weight'); 226 | } 227 | 228 | const newConf: Array = [ 229 | { property: 'fruit', header: 'Fruit', hidden: false }, 230 | { property: 'weight', header: 'Weight', hidden: false }, 231 | { property: 'color', header: 'Color', hidden: false }, 232 | ]; 233 | litDatatableWithColumn.el.conf = newConf; 234 | await litDatatableWithColumn.elementUpdated(); 235 | 236 | ({ bodyTds, headThs } = litDatatableWithColumn); 237 | expect(bodyTds).to.be.not.equal(null); 238 | if (bodyTds) { 239 | expect(bodyTds[0]?.textContent).to.be.equal('apple test'); 240 | expect(bodyTds[1]?.textContent).to.be.equal('100gr'); 241 | expect(bodyTds[2]?.textContent).to.be.equal('green'); 242 | expect(bodyTds[3]?.textContent).to.be.equal('banana test'); 243 | expect(bodyTds[4]?.textContent).to.be.equal('140gr'); 244 | expect(bodyTds[5]?.textContent).to.be.equal('yellow'); 245 | } 246 | expect(headThs).to.be.not.equal(null); 247 | if (headThs) { 248 | expect(headThs[0]?.textContent).to.be.equal('Fruit'); 249 | expect(headThs[1]?.textContent).to.be.equal('Weight'); 250 | expect(headThs[2]?.textContent).to.be.equal('Color'); 251 | } 252 | }); 253 | 254 | it('header styles', async () => { 255 | const columns: Array = [ 256 | { 257 | html: (value) => litHtml`${value}
test
`, 258 | property: 'fruit', 259 | otherProperties: [], 260 | columnStyle: 'background: red;', 261 | }, 262 | ]; 263 | const litDatatableWithColumn = new LitDatatableWithColumnTest(); 264 | await litDatatableWithColumn.init(basicConf, basicData, [], columns); 265 | await litDatatableWithColumn.elementUpdated(); 266 | const { headThs } = litDatatableWithColumn; 267 | expect(headThs).to.be.not.equal(null); 268 | if (headThs) { 269 | expect(/.*red.*/.test(headThs[0]?.style.background)).to.be.equal(true); 270 | } 271 | }); 272 | 273 | it('body styles', async () => { 274 | const columns: Array = [ 275 | { 276 | html: (value) => litHtml`${value}
test
`, 277 | property: 'fruit', 278 | otherProperties: [], 279 | columnStyle: 'background: red;', 280 | }, 281 | ]; 282 | const litDatatableWithColumn = new LitDatatableWithColumnTest(); 283 | await litDatatableWithColumn.init(basicConf, basicData, columns, []); 284 | await litDatatableWithColumn.elementUpdated(); 285 | const { bodyTds } = litDatatableWithColumn; 286 | expect(bodyTds).to.be.not.equal(null); 287 | if (bodyTds) { 288 | expect(/.*red.*/.test(bodyTds[0]?.style.background)).to.be.equal(true); 289 | expect(/.*red.*/.test(bodyTds[3]?.style.background)).to.be.equal(true); 290 | } 291 | }); 292 | 293 | it('body values', async () => { 294 | const columns: Array = [ 295 | { 296 | html: null, 297 | property: 'fruit', 298 | otherProperties: [], 299 | }, 300 | ]; 301 | const litDatatableWithColumn = new LitDatatableWithColumnTest(); 302 | await litDatatableWithColumn.init(basicConf, basicData, columns, []); 303 | await litDatatableWithColumn.elementUpdated(); 304 | const { bodyTds, headThs } = litDatatableWithColumn; 305 | expect(bodyTds).to.be.not.equal(null); 306 | if (bodyTds) { 307 | expect(bodyTds[0]?.textContent).to.be.equal('apple'); 308 | expect(bodyTds[1]?.textContent).to.be.equal('green'); 309 | expect(bodyTds[2]?.textContent).to.be.equal('100gr'); 310 | expect(bodyTds[3]?.textContent).to.be.equal('banana'); 311 | expect(bodyTds[4]?.textContent).to.be.equal('yellow'); 312 | expect(bodyTds[5]?.textContent).to.be.equal('140gr'); 313 | } 314 | expect(headThs).to.be.not.equal(null); 315 | if (headThs) { 316 | expect(headThs[0]?.textContent).to.be.equal('Fruit'); 317 | expect(headThs[1]?.textContent).to.be.equal('Color'); 318 | expect(headThs[2]?.textContent).to.be.equal('Weight'); 319 | } 320 | }); 321 | }); 322 | -------------------------------------------------------------------------------- /test/unit/lit-datatable.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | fixture, html, expect, elementUpdated 3 | } from '@open-wc/testing'; 4 | import sinon from 'sinon'; 5 | import type { LitDatatable, Conf } from '../../lit-datatable'; 6 | import '../../lit-datatable'; 7 | 8 | class LitDatatableTest { 9 | el!: LitDatatable; 10 | 11 | async init(conf: Array, data: Array, stickyHeader?: boolean): Promise { 12 | const litDatatable = html` 13 | 14 | `; 15 | this.el = await fixture(litDatatable); 16 | return this.elementUpdated(); 17 | } 18 | 19 | elementUpdated(): Promise { 20 | return elementUpdated(this.el); 21 | } 22 | 23 | get bodyTrs() { 24 | if (this?.el?.shadowRoot) { 25 | return this.el.shadowRoot.querySelectorAll('tbody tr'); 26 | } 27 | return null; 28 | } 29 | 30 | get bodyTds() { 31 | if (this?.el?.shadowRoot) { 32 | return this.el.shadowRoot.querySelectorAll('tbody td'); 33 | } 34 | return null; 35 | } 36 | 37 | get headTrs() { 38 | if (this?.el?.shadowRoot) { 39 | return this.el.shadowRoot.querySelectorAll('thead tr'); 40 | } 41 | return null; 42 | } 43 | 44 | get headThs() { 45 | if (this?.el?.shadowRoot) { 46 | return this.el.shadowRoot.querySelectorAll('thead th'); 47 | } 48 | return null; 49 | } 50 | } 51 | 52 | const basicData = [ 53 | { fruit: 'apple', color: 'green', weight: '100gr' }, 54 | { fruit: 'banana', color: 'yellow', weight: '140gr' }, 55 | ]; 56 | 57 | const basicDataWithSubObject = [ 58 | { fruit: 'apple', color: 'green', weight: { value: '100gr' } }, 59 | { fruit: 'banana', color: 'yellow', weight: { value: '140gr' } }, 60 | ]; 61 | 62 | const basicConf: Array = [ 63 | { property: 'fruit', header: 'Fruit', hidden: false }, 64 | { property: 'color', header: 'Color', hidden: false }, 65 | { property: 'weight', header: 'Weight', hidden: false }, 66 | ]; 67 | 68 | const basicConfWithSubObject: Array = [ 69 | { property: 'fruit', header: 'Fruit', hidden: false }, 70 | { property: 'color', header: 'Color', hidden: false }, 71 | { property: 'weight.value', header: 'Weight', hidden: false }, 72 | ]; 73 | 74 | describe('lit-datatable', () => { 75 | it('counts', async () => { 76 | const litDatatable = new LitDatatableTest(); 77 | await litDatatable.init(basicConf, basicData); 78 | await litDatatable.elementUpdated(); 79 | const { 80 | bodyTrs, bodyTds, headTrs, headThs, 81 | } = litDatatable; 82 | expect(headTrs?.length).to.be.equal(1); 83 | expect(headThs?.length).to.be.equal(3); 84 | expect(bodyTrs?.length).to.be.equal(2); 85 | expect(bodyTds?.length).to.be.equal(6); 86 | }); 87 | 88 | it('header values', async () => { 89 | const litDatatable = new LitDatatableTest(); 90 | await litDatatable.init(basicConf, basicData); 91 | await litDatatable.elementUpdated(); 92 | const { headThs } = litDatatable; 93 | expect(headThs).to.be.not.equal(null); 94 | if (headThs) { 95 | expect(headThs[0]?.textContent).to.be.equal('Fruit'); 96 | expect(headThs[1]?.textContent).to.be.equal('Color'); 97 | expect(headThs[2]?.textContent).to.be.equal('Weight'); 98 | } 99 | }); 100 | 101 | it('body values', async () => { 102 | const litDatatable = new LitDatatableTest(); 103 | await litDatatable.init(basicConf, basicData); 104 | await litDatatable.elementUpdated(); 105 | const { bodyTds } = litDatatable; 106 | expect(bodyTds).to.be.not.equal(null); 107 | if (bodyTds) { 108 | expect(bodyTds[0]?.textContent).to.be.equal('apple'); 109 | expect(bodyTds[1]?.textContent).to.be.equal('green'); 110 | expect(bodyTds[2]?.textContent).to.be.equal('100gr'); 111 | expect(bodyTds[3]?.textContent).to.be.equal('banana'); 112 | expect(bodyTds[4]?.textContent).to.be.equal('yellow'); 113 | expect(bodyTds[5]?.textContent).to.be.equal('140gr'); 114 | } 115 | }); 116 | 117 | it('body values with sub object', async () => { 118 | const litDatatable = new LitDatatableTest(); 119 | await litDatatable.init(basicConfWithSubObject, basicDataWithSubObject); 120 | await litDatatable.elementUpdated(); 121 | const { bodyTds } = litDatatable; 122 | expect(bodyTds).to.be.not.equal(null); 123 | if (bodyTds) { 124 | expect(bodyTds[0]?.textContent).to.be.equal('apple'); 125 | expect(bodyTds[1]?.textContent).to.be.equal('green'); 126 | expect(bodyTds[2]?.textContent).to.be.equal('100gr'); 127 | expect(bodyTds[3]?.textContent).to.be.equal('banana'); 128 | expect(bodyTds[4]?.textContent).to.be.equal('yellow'); 129 | expect(bodyTds[5]?.textContent).to.be.equal('140gr'); 130 | } 131 | }); 132 | 133 | it('change column position', async () => { 134 | const litDatatable = new LitDatatableTest(); 135 | await litDatatable.init(basicConf, basicData); 136 | await litDatatable.elementUpdated(); 137 | const { bodyTds } = litDatatable; 138 | expect(bodyTds).to.be.not.equal(null); 139 | if (bodyTds) { 140 | expect(bodyTds[0]?.textContent).to.be.equal('apple'); 141 | expect(bodyTds[1]?.textContent).to.be.equal('green'); 142 | expect(bodyTds[2]?.textContent).to.be.equal('100gr'); 143 | expect(bodyTds[3]?.textContent).to.be.equal('banana'); 144 | expect(bodyTds[4]?.textContent).to.be.equal('yellow'); 145 | expect(bodyTds[5]?.textContent).to.be.equal('140gr'); 146 | } 147 | 148 | const newConfPosition: Array = [ 149 | { property: 'weight', header: 'Weight', hidden: false }, 150 | { property: 'color', header: 'Color', hidden: false }, 151 | { property: 'fruit', header: 'Fruit', hidden: false }, 152 | ]; 153 | litDatatable.el.conf = newConfPosition; 154 | await litDatatable.elementUpdated(); 155 | if (bodyTds) { 156 | expect(bodyTds[0]?.textContent).to.be.equal('100gr'); 157 | expect(bodyTds[1]?.textContent).to.be.equal('green'); 158 | expect(bodyTds[2]?.textContent).to.be.equal('apple'); 159 | expect(bodyTds[3]?.textContent).to.be.equal('140gr'); 160 | expect(bodyTds[4]?.textContent).to.be.equal('yellow'); 161 | expect(bodyTds[5]?.textContent).to.be.equal('banana'); 162 | } 163 | }); 164 | 165 | it('hide column', async () => { 166 | const litDatatable = new LitDatatableTest(); 167 | await litDatatable.init(basicConf, basicData); 168 | await litDatatable.elementUpdated(); 169 | let bodyTrs; 170 | let bodyTds; 171 | let headTrs; 172 | let headThs; 173 | ({ 174 | bodyTrs, bodyTds, headTrs, headThs, 175 | } = litDatatable); 176 | expect(headTrs?.length).to.be.equal(1); 177 | expect(headThs?.length).to.be.equal(3); 178 | expect(bodyTrs?.length).to.be.equal(2); 179 | expect(bodyTds?.length).to.be.equal(6); 180 | expect(bodyTds).to.be.not.equal(null); 181 | if (bodyTds) { 182 | expect(bodyTds[0]?.textContent).to.be.equal('apple'); 183 | expect(bodyTds[1]?.textContent).to.be.equal('green'); 184 | expect(bodyTds[2]?.textContent).to.be.equal('100gr'); 185 | expect(bodyTds[3]?.textContent).to.be.equal('banana'); 186 | expect(bodyTds[4]?.textContent).to.be.equal('yellow'); 187 | expect(bodyTds[5]?.textContent).to.be.equal('140gr'); 188 | } 189 | 190 | const newConf: Array = [ 191 | { property: 'weight', header: 'Weight', hidden: false }, 192 | { property: 'color', header: 'Color', hidden: true }, 193 | { property: 'fruit', header: 'Fruit', hidden: false }, 194 | ]; 195 | litDatatable.el.conf = newConf; 196 | await litDatatable.elementUpdated(); 197 | ({ 198 | bodyTrs, bodyTds, headTrs, headThs, 199 | } = litDatatable); 200 | expect(headTrs?.length).to.be.equal(1); 201 | expect(headThs?.length).to.be.equal(2); 202 | expect(bodyTrs?.length).to.be.equal(2); 203 | expect(bodyTds?.length).to.be.equal(4); 204 | expect(bodyTds).to.be.not.equal(null); 205 | if (bodyTds) { 206 | expect(bodyTds[0]?.textContent).to.be.equal('100gr'); 207 | expect(bodyTds[1]?.textContent).to.be.equal('apple'); 208 | expect(bodyTds[2]?.textContent).to.be.equal('140gr'); 209 | expect(bodyTds[3]?.textContent).to.be.equal('banana'); 210 | } 211 | }); 212 | 213 | it('change data length', async () => { 214 | const litDatatable = new LitDatatableTest(); 215 | await litDatatable.init(basicConf, basicData); 216 | await litDatatable.elementUpdated(); 217 | let bodyTrs; 218 | let bodyTds; 219 | let headTrs; 220 | let headThs; 221 | ({ 222 | bodyTrs, bodyTds, headTrs, headThs, 223 | } = litDatatable); 224 | expect(headTrs?.length).to.be.equal(1); 225 | expect(headThs?.length).to.be.equal(3); 226 | expect(bodyTrs?.length).to.be.equal(2); 227 | expect(bodyTds?.length).to.be.equal(6); 228 | 229 | // Add row 230 | const addRow = [ 231 | { fruit: 'apple', color: 'green', weight: '100gr' }, 232 | { fruit: 'banana', color: 'yellow', weight: '140gr' }, 233 | { fruit: 'cherry', color: 'red', weight: '40gr' }, 234 | ]; 235 | litDatatable.el.data = addRow; 236 | await litDatatable.elementUpdated(); 237 | 238 | ({ 239 | bodyTrs, bodyTds, headTrs, headThs, 240 | } = litDatatable); 241 | expect(headTrs?.length).to.be.equal(1); 242 | expect(headThs?.length).to.be.equal(3); 243 | expect(bodyTrs?.length).to.be.equal(3); 244 | expect(bodyTds?.length).to.be.equal(9); 245 | 246 | // Delete row 247 | const deleteRow = [ 248 | { fruit: 'apple', color: 'green', weight: '100gr' }, 249 | ]; 250 | litDatatable.el.data = deleteRow; 251 | await litDatatable.elementUpdated(); 252 | 253 | ({ 254 | bodyTrs, bodyTds, headTrs, headThs, 255 | } = litDatatable); 256 | expect(headTrs?.length).to.be.equal(1); 257 | expect(headThs?.length).to.be.equal(3); 258 | expect(bodyTrs?.length).to.be.equal(1); 259 | expect(bodyTds?.length).to.be.equal(3); 260 | }); 261 | 262 | it('tr tap event', async () => { 263 | const litDatatable = new LitDatatableTest(); 264 | await litDatatable.init(basicConf, basicData); 265 | await litDatatable.elementUpdated(); 266 | const { bodyTrs } = litDatatable; 267 | expect(bodyTrs).to.be.not.equal(null); 268 | if (bodyTrs) { 269 | const tapEventSpy = sinon.spy(); 270 | const tapEvent = new Event('tap'); 271 | litDatatable.el.addEventListener('tap-tr', tapEventSpy); 272 | bodyTrs[0].dispatchEvent(tapEvent); 273 | expect(tapEventSpy.callCount).to.be.equal(1); 274 | } 275 | }); 276 | 277 | it('tr mouseover event', async () => { 278 | const litDatatable = new LitDatatableTest(); 279 | await litDatatable.init(basicConf, basicData); 280 | await litDatatable.elementUpdated(); 281 | const { bodyTrs } = litDatatable; 282 | expect(bodyTrs).to.be.not.equal(null); 283 | if (bodyTrs) { 284 | const mouseEventSpy = sinon.spy(); 285 | const mouseEvent = new Event('mouseover'); 286 | litDatatable.el.addEventListener('tr-mouseover', mouseEventSpy); 287 | bodyTrs[0].dispatchEvent(mouseEvent); 288 | expect(mouseEventSpy.callCount).to.be.equal(1); 289 | } 290 | }); 291 | 292 | it('tr mouseout event', async () => { 293 | const litDatatable = new LitDatatableTest(); 294 | await litDatatable.init(basicConf, basicData); 295 | await litDatatable.elementUpdated(); 296 | const { bodyTrs } = litDatatable; 297 | expect(bodyTrs).to.be.not.equal(null); 298 | if (bodyTrs) { 299 | const mouseEventSpy = sinon.spy(); 300 | const mouseEvent = new Event('mouseout'); 301 | litDatatable.el.addEventListener('tr-mouseout', mouseEventSpy); 302 | bodyTrs[0].dispatchEvent(mouseEvent); 303 | expect(mouseEventSpy.callCount).to.be.equal(1); 304 | } 305 | }); 306 | 307 | it('sticky header', async () => { 308 | const litDatatable = new LitDatatableTest(); 309 | await litDatatable.init(basicConf, basicData, true); 310 | await litDatatable.elementUpdated(); 311 | const { headThs } = litDatatable; 312 | expect(headThs).to.be.not.equal(null); 313 | if (headThs) { 314 | const eachThIsSticky = Array.from(headThs).every((th) => th.classList.contains('sticky')); 315 | expect(eachThIsSticky).to.be.equal(true); 316 | } 317 | }); 318 | 319 | it('non sticky header', async () => { 320 | const litDatatable = new LitDatatableTest(); 321 | await litDatatable.init(basicConf, basicData, false); 322 | await litDatatable.elementUpdated(); 323 | const { headThs } = litDatatable; 324 | expect(headThs).to.be.not.equal(null); 325 | if (headThs) { 326 | const eachThIsSticky = Array.from(headThs).every((th) => th.classList.contains('sticky')); 327 | expect(eachThIsSticky).to.be.equal(false); 328 | } 329 | }); 330 | }); 331 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "lib": [ 7 | "esnext.array", 8 | "esnext", 9 | "es2017", 10 | "dom" 11 | ], 12 | "allowSyntheticDefaultImports": true, 13 | "declaration": true, 14 | "declarationMap": true, 15 | "sourceMap": true, 16 | "inlineSources": true, 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noImplicitReturns": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noImplicitAny": true, 23 | "noImplicitThis": true, 24 | "skipLibCheck": true, 25 | "experimentalDecorators": true, 26 | "esModuleInterop": true 27 | } 28 | } -------------------------------------------------------------------------------- /web-test-runner.config.js: -------------------------------------------------------------------------------- 1 | import { createSauceLabsLauncher } from '@web/test-runner-saucelabs'; 2 | 3 | const sauceLabsLauncher = createSauceLabsLauncher({ 4 | user: process.env.SAUCE_USERNAME, 5 | key: process.env.SAUCE_ACCESS_KEY, 6 | region: 'us-west-1', 7 | }); 8 | 9 | const sharedCapabilities = { 10 | 'sauce:options': { 11 | name: 'lit-datatable', 12 | build: `lit-datatable ${process.env.GITHUB_REF ?? 'local'} build ${ 13 | process.env.GITHUB_RUN_NUMBER ?? '' 14 | }`, 15 | }, 16 | }; 17 | 18 | export default { 19 | files: ['test/**/*.test.js'], 20 | nodeResolve: true, 21 | coverage: true, 22 | dedupe: true, 23 | browsers: [ 24 | sauceLabsLauncher({ 25 | ...sharedCapabilities, 26 | browserName: 'chrome', 27 | browserVersion: 'latest', 28 | platformName: 'Windows 10', 29 | }), 30 | sauceLabsLauncher({ 31 | ...sharedCapabilities, 32 | browserName: 'firefox', 33 | browserVersion: 'latest', 34 | platformName: 'Windows 10', 35 | }), 36 | sauceLabsLauncher({ 37 | ...sharedCapabilities, 38 | browserName: 'safari', 39 | browserVersion: 'latest', 40 | platformName: 'macOS 10.15', 41 | }), 42 | ], 43 | }; 44 | --------------------------------------------------------------------------------