├── .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 |  [](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 | 
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 | [](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 |
154 | ${this.enableFilter ? html`
155 |
156 |
160 |
161 |
162 | ` : null}
163 | ${this.filteredChoices && this.filteredChoices.map((choice) => html`
164 |
165 |
166 |
167 | ${choice.prefix ? html`
${choice.prefix}
` : null}
168 |
169 | ${choice.label}
170 |
171 | ${choice.icon ? html`
172 |
173 | ` : null}
174 |
175 | `)}
176 |
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 |
108 | ` : html`
109 |
110 |
111 | ${this.header}
112 |
113 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------