├── .github
├── FUNDING.yml
└── workflows
│ ├── build.yml
│ ├── cypress.yml
│ └── jest.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── __tests__
├── cypress
│ ├── e2e
│ │ ├── appendToBody
│ │ │ └── index.cy.ts
│ │ ├── base
│ │ │ ├── check-uncheck-group.cy.ts
│ │ │ ├── direction.cy.ts
│ │ │ ├── keyboard.cy.ts
│ │ │ ├── open-close-group.cy.ts
│ │ │ ├── open-close-list.cy.ts
│ │ │ ├── remove-values.cy.ts
│ │ │ ├── search.cy.ts
│ │ │ └── tags-values.cy.ts
│ │ ├── disabled
│ │ │ └── index.cy.ts
│ │ ├── experemental
│ │ │ └── is-boost-rendering-list.cy.ts
│ │ ├── large-data
│ │ │ └── save-scroll.cy.ts
│ │ ├── single
│ │ │ └── select-value.cy.ts
│ │ └── slot
│ │ │ └── index.cy.ts
│ ├── helpers
│ │ └── index.ts
│ ├── pages
│ │ ├── cypress-append-to-body.html
│ │ ├── cypress-base.html
│ │ ├── cypress-boost-rendering.html
│ │ ├── cypress-disabled.html
│ │ ├── cypress-large-data.html
│ │ ├── cypress-single.html
│ │ ├── cypress-slot.html
│ │ └── runs.js
│ └── support
│ │ ├── commands.ts
│ │ └── e2e.ts
├── jest
│ ├── helpers
│ │ ├── getElements.ts
│ │ ├── index.ts
│ │ └── renderTreeselect.ts
│ ├── setup.ts
│ └── tests
│ │ ├── core-props
│ │ ├── __snapshots__
│ │ │ ├── disabled.test.ts.snap
│ │ │ ├── isGroupedValue.test.ts.snap
│ │ │ ├── isIndependentNodes.test.ts.snap
│ │ │ ├── isSingleSelect.test.ts.snap
│ │ │ ├── options.test.ts.snap
│ │ │ ├── parentHtmlContainer.test.ts.snap
│ │ │ ├── rtl.test.ts.snap
│ │ │ └── value.test.ts.snap
│ │ ├── ariaLabel.test.ts
│ │ ├── disabled.test.ts
│ │ ├── id.test.ts
│ │ ├── isGroupedValue.test.ts
│ │ ├── isIndependentNodes.test.ts
│ │ ├── isSingleSelect.test.ts
│ │ ├── options.test.ts
│ │ ├── parentHtmlContainer.test.ts
│ │ ├── rtl.test.ts
│ │ └── value.test.ts
│ │ ├── input-props
│ │ ├── __snapshots__
│ │ │ ├── grouped.test.ts.snap
│ │ │ ├── searchable.test.ts.snap
│ │ │ └── showTags.test.ts.snap
│ │ ├── clearable.test.ts
│ │ ├── grouped.test.ts
│ │ ├── placeholder.test.ts
│ │ ├── searchable.test.ts
│ │ ├── showTags.test.ts
│ │ ├── tagsCountText.test.ts
│ │ └── tagsSortFn.test.ts
│ │ ├── list-props
│ │ ├── __snapshots__
│ │ │ ├── alwaysOpen.test.ts.snap
│ │ │ ├── appendToBody.test.ts.snap
│ │ │ ├── direction.test.ts.snap
│ │ │ ├── disabledBranchNode.test.ts.snap
│ │ │ ├── emptyText.test.ts.snap
│ │ │ ├── expandSelected.test.ts.snap
│ │ │ ├── listSlotHtmlComponent.test.ts.snap
│ │ │ ├── openLevel.test.ts.snap
│ │ │ ├── showCount.test.ts.snap
│ │ │ └── stasticList.test.ts.snap
│ │ ├── alwaysOpen.test.ts
│ │ ├── appendToBody.test.ts
│ │ ├── direction.test.ts
│ │ ├── disabledBranchNode.test.ts
│ │ ├── emptyText.test.ts
│ │ ├── expandSelected.test.ts
│ │ ├── listClassName.test.ts
│ │ ├── listSlotHtmlComponent.test.ts
│ │ ├── openLevel.test.ts
│ │ ├── showCount.test.ts
│ │ └── stasticList.test.ts
│ │ └── methods
│ │ ├── __snapshots__
│ │ └── focus.test.ts.snap
│ │ ├── destroy.test.ts
│ │ ├── emits-callbacks.test.ts
│ │ ├── focus.test.ts
│ │ ├── mount.test.ts
│ │ ├── toggleOpenClose.test.ts
│ │ └── updateValue.test.ts
└── testHelpers
│ ├── index.ts
│ ├── options.ts
│ └── selectors.ts
├── app
├── app.css
├── app.js
├── examples
│ ├── default.js
│ ├── disabled.js
│ ├── icons.js
│ ├── independentNodes.js
│ ├── singleSelect.js
│ └── slot.js
├── index.html
├── render
│ └── renderExampleSection.js
└── todo.js
├── cypress.config.ts
├── demo
├── index.css
└── index.js
├── index.html
├── jest.config.ts
├── package-lock.json
├── package.json
├── public
└── tree.png
├── src
├── input
│ ├── index.ts
│ ├── input.css
│ └── inputTypes.ts
├── list
│ ├── helpers
│ │ ├── domHelper.ts
│ │ ├── listCheckStateHelper.ts
│ │ ├── listOptionsHelper.ts
│ │ └── listVisibilityStateHelper.ts
│ ├── index.ts
│ ├── list.css
│ └── listTypes.ts
├── svgIcons.ts
├── treeselectTypes.ts
├── treeselectjs.css
├── treeselectjs.ts
└── vite-env.d.ts
├── treeselectjs.png
├── tsconfig.app.json
├── tsconfig.cypress.json
├── tsconfig.jest.json
├── tsconfig.json
└── vite.config.js
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: ['https://www.buymeacoffee.com/dipson88'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | build-test:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | # Checkout the repository
17 | - name: Checkout code
18 | uses: actions/checkout@v3
19 |
20 | # Set up Node.js
21 | - name: Set up Node.js
22 | uses: actions/setup-node@v3
23 | with:
24 | node-version: '20'
25 |
26 | # Install dependencies
27 | - name: Install dependencies
28 | run: npm install
29 |
30 | # Run build
31 | - name: Run build
32 | run: npm run build
33 |
--------------------------------------------------------------------------------
/.github/workflows/cypress.yml:
--------------------------------------------------------------------------------
1 | name: Cypress Tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | cypress-tests:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | # Checkout the repository
17 | - name: Checkout code
18 | uses: actions/checkout@v3
19 |
20 | # Set up Node.js
21 | - name: Set up Node.js
22 | uses: actions/setup-node@v3
23 | with:
24 | node-version: '20'
25 |
26 | # Install dependencies
27 | - name: Install dependencies
28 | run: npm install
29 |
30 | # Start the dev server in the background
31 | - name: Start dev server
32 | run: npm run dev &
33 | env:
34 | NODE_ENV: development
35 |
36 | # Run Cypress tests
37 | - name: Run Cypress tests
38 | run: npm run cypress:run
39 |
--------------------------------------------------------------------------------
/.github/workflows/jest.yml:
--------------------------------------------------------------------------------
1 | name: Jest Tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'main'
7 | pull_request:
8 | branches:
9 | - 'main'
10 |
11 | jobs:
12 | jest-tests:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | # Step 1: Checkout the repository
17 | - name: Checkout code
18 | uses: actions/checkout@v3
19 |
20 | # Step 2: Set up Node.js environment
21 | - name: Set up Node.js
22 | uses: actions/setup-node@v3
23 | with:
24 | node-version: '20'
25 |
26 | # Step 3: Install dependencies
27 | - name: Install dependencies
28 | run: npm install
29 |
30 | # Step 4: Run Jest tests
31 | - name: Run Jest tests
32 | run: npm run jest:run
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /.vscode
3 | dist
4 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | demo/static
3 | dist
4 | *.yml
5 | README.md
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "trailingComma": "none",
4 | "singleQuote": true,
5 | "semi": false
6 | }
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Dzmitry Zhuraukou
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/__tests__/cypress/e2e/appendToBody/index.cy.ts:
--------------------------------------------------------------------------------
1 | import { classesSelectors, classes } from '../../helpers'
2 |
3 | const { list: listSelectors } = classesSelectors
4 | const { list: litsClasses, input: inputClasses } = classes
5 |
6 | describe('appendToBody', () => {
7 | beforeEach(() => {
8 | cy.visitAppendToBodyPage()
9 | cy.clearClick()
10 | cy.treeselectClick()
11 | })
12 |
13 | it('should render the list in the body', () => {
14 | cy.getTreeselect().find(listSelectors.base).should('not.exist')
15 | cy.get('body').find(listSelectors.base).should('exist')
16 | })
17 |
18 | it('should change direction on change viewport', () => {
19 | cy.outsideClick()
20 | cy.get('body').invoke('css', 'padding-top', '400px').invoke('css', 'padding-bottom', '400px')
21 | cy.treeselectClick()
22 | cy.expandAllGroups()
23 |
24 | cy.scrollTo('top')
25 |
26 | cy.getList().should('have.class', litsClasses.topToBody)
27 | cy.getInput().should('have.class', inputClasses.top)
28 |
29 | cy.scrollTo('bottom')
30 |
31 | cy.getList().should('have.class', litsClasses.bottomToBody)
32 | cy.getInput().should('have.class', inputClasses.bottom)
33 |
34 | cy.getInput().then(($input) => {
35 | const inputRect = $input[0].getBoundingClientRect()
36 |
37 | cy.getList().then(($list) => {
38 | const listRect = $list[0].getBoundingClientRect()
39 |
40 | const distanceY = Math.abs(inputRect.bottom - listRect.top)
41 | const distanceX = Math.abs(inputRect.left - listRect.left)
42 |
43 | cy.log('distanceY', distanceY)
44 | cy.log('distanceX', distanceX)
45 |
46 | expect(distanceY).to.be.lessThan(1)
47 | expect(distanceX).to.be.lessThan(1)
48 | })
49 | })
50 | })
51 | })
52 |
--------------------------------------------------------------------------------
/__tests__/cypress/e2e/base/check-uncheck-group.cy.ts:
--------------------------------------------------------------------------------
1 | import { classes, optionNames } from '../../helpers'
2 |
3 | const { list: listClass } = classes
4 |
5 | describe('check-uncheck-group', () => {
6 | beforeEach(() => {
7 | cy.visitBasePage()
8 | cy.clearClick()
9 | cy.treeselectClick()
10 | cy.expandAllGroups()
11 | })
12 |
13 | // Without sub groups
14 | it('should check group on group check (without sub groups)', () => {
15 | cy.getListItem(optionNames.FranceGroup)
16 | .click()
17 | .should('have.class', listClass.itemChecked)
18 | .should('have.class', listClass.itemFocused)
19 | .should('not.have.class', listClass.itemPartialChecked)
20 |
21 | cy.getListItem(optionNames.ParisItem).should('have.class', listClass.itemChecked)
22 | cy.getListItem(optionNames.LyonItem).should('have.class', listClass.itemChecked)
23 | })
24 |
25 | it('should uncheck group on group uncheck (without sub groups)', () => {
26 | cy.getListItem(optionNames.FranceGroup)
27 | .click()
28 | .should('have.class', listClass.itemChecked)
29 | .click()
30 | .should('not.have.class', listClass.itemChecked)
31 |
32 | cy.getListItem(optionNames.ParisItem).should('not.have.class', listClass.itemChecked)
33 | cy.getListItem(optionNames.LyonItem).should('not.have.class', listClass.itemChecked)
34 | })
35 |
36 | it('should partially check group on group partial check (without sub groups)', () => {
37 | cy.getListItem(optionNames.ParisItem)
38 | .click()
39 | .should('have.class', listClass.itemFocused)
40 | .should('have.class', listClass.itemChecked)
41 |
42 | cy.getListItem(optionNames.FranceGroup).should('have.class', listClass.itemPartialChecked)
43 | })
44 |
45 | it('should be partially checked when one item are unchecked', () => {
46 | cy.getListItem(optionNames.FranceGroup).click().should('have.class', listClass.itemChecked)
47 |
48 | cy.getListItem(optionNames.ParisItem).should('have.class', listClass.itemChecked)
49 | cy.getListItem(optionNames.LyonItem).should('have.class', listClass.itemChecked)
50 |
51 | cy.getListItem(optionNames.ParisItem).click().should('not.have.class', listClass.itemChecked)
52 | cy.getListItem(optionNames.FranceGroup).should('have.class', listClass.itemPartialChecked)
53 | })
54 |
55 | // With sub groups
56 | it('should check group on group check (with sub groups)', () => {
57 | cy.getListItem(optionNames.EnglandGroup)
58 | .click()
59 | .should('have.class', listClass.itemFocused)
60 | .should('have.class', listClass.itemChecked)
61 |
62 | cy.getListItem(optionNames.LondonGroup).should('have.class', listClass.itemChecked)
63 | cy.getListItem(optionNames.ChelseaItem).should('have.class', listClass.itemChecked)
64 | cy.getListItem(optionNames.WestEndItem).should('have.class', listClass.itemChecked)
65 | cy.getListItem(optionNames.BrightonItem).should('have.class', listClass.itemChecked)
66 | })
67 |
68 | it('should uncheck group on group uncheck (with sub groups)', () => {
69 | cy.getListItem(optionNames.EnglandGroup)
70 | .click()
71 | .should('have.class', listClass.itemChecked)
72 | .click()
73 | .should('not.have.class', listClass.itemChecked)
74 |
75 | cy.getListItem(optionNames.LondonGroup).should('not.have.class', listClass.itemChecked)
76 | cy.getListItem(optionNames.ChelseaItem).should('not.have.class', listClass.itemChecked)
77 | cy.getListItem(optionNames.WestEndItem).should('not.have.class', listClass.itemChecked)
78 | cy.getListItem(optionNames.BrightonItem).should('not.have.class', listClass.itemChecked)
79 | })
80 |
81 | it('should partially check group on group partial check (with sub groups)', () => {
82 | cy.getListItem(optionNames.LondonGroup)
83 | .click()
84 | .should('have.class', listClass.itemFocused)
85 | .should('have.class', listClass.itemChecked)
86 |
87 | cy.getListItem(optionNames.EnglandGroup).should('have.class', listClass.itemPartialChecked)
88 | })
89 |
90 | it('should be partially checked when one item are unchecked (with sub groups)', () => {
91 | cy.getListItem(optionNames.EnglandGroup).click().should('have.class', listClass.itemChecked)
92 |
93 | cy.getListItem(optionNames.LondonGroup).click()
94 | cy.getListItem(optionNames.EnglandGroup).should('have.class', listClass.itemPartialChecked)
95 | })
96 |
97 | it('should be partially checked when one sub group checked (with sub groups)', () => {
98 | cy.getListItem(optionNames.EnglandGroup).click().should('have.class', listClass.itemChecked)
99 |
100 | cy.getListItem(optionNames.BrightonItem).click()
101 | cy.getListItem(optionNames.EnglandGroup).should('have.class', listClass.itemPartialChecked)
102 | })
103 | })
104 |
--------------------------------------------------------------------------------
/__tests__/cypress/e2e/base/direction.cy.ts:
--------------------------------------------------------------------------------
1 | import { classes } from '../../helpers'
2 |
3 | const { input: inputClass, list: listClass } = classes
4 |
5 | describe('direction', () => {
6 | beforeEach(() => {
7 | cy.visitBasePage()
8 | cy.clearClick()
9 | cy.treeselectClick()
10 | cy.expandAllGroups()
11 | })
12 |
13 | afterEach(() => {
14 | cy.outsideClick()
15 | cy.get('body').invoke('css', 'padding-top', '0').invoke('css', 'padding-bottom', '0')
16 | })
17 |
18 | it('should open list to the bottom if there is enough space', () => {
19 | cy.getList().should('have.class', listClass.bottom)
20 | })
21 |
22 | it('should open list to the top if there is not enough space', () => {
23 | cy.get('body').invoke('css', 'padding-top', '400px').invoke('css', 'padding-bottom', '400px')
24 | cy.scrollTo('bottom')
25 | cy.getList().should('have.class', listClass.bottom)
26 | cy.getInput().should('have.class', inputClass.bottom)
27 |
28 | cy.scrollTo('top')
29 | cy.getList().should('have.class', listClass.top)
30 | cy.getInput().should('have.class', inputClass.top)
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/__tests__/cypress/e2e/base/keyboard.cy.ts:
--------------------------------------------------------------------------------
1 | import { classes, classesSelectors, optionNames } from '../../helpers'
2 |
3 | const { input: inputClass, list: listClass } = classes
4 | const { input: inputSelector, list: listSelector } = classesSelectors
5 |
6 | describe('keyboard', () => {
7 | beforeEach(() => {
8 | cy.visitBasePage()
9 | cy.treeselectClick()
10 | })
11 |
12 | it('should open/close list on Space key', () => {
13 | cy.get(inputSelector.base).should('have.class', inputClass.opened)
14 | cy.get(listSelector.base).should('exist')
15 |
16 | cy.treeselectType('{ }')
17 | cy.get(inputSelector.base).should('not.have.class', inputClass.opened)
18 | cy.get(listSelector.base).should('not.exist')
19 |
20 | cy.treeselectType('{ }')
21 | cy.get(inputSelector.base).should('have.class', inputClass.opened)
22 | cy.get(listSelector.base).should('exist')
23 | })
24 |
25 | it('should remove values on Backspace key', () => {
26 | cy.getTagsElements().should('have.length', 2)
27 |
28 | cy.treeselectType('{backspace}')
29 | cy.getTagsElements().should('have.length', 1)
30 |
31 | cy.treeselectType('{backspace}')
32 | cy.getTagsElements().should('have.length', 0)
33 |
34 | cy.get(listSelector.itemChecked).should('have.length', 0)
35 | cy.get(listSelector.itemPartialChecked).should('have.length', 0)
36 | })
37 |
38 | it('should select/unselect first item on Enter key', () => {
39 | cy.clearClick()
40 |
41 | cy.getListItem(optionNames.EnglandGroup).should('not.have.class', listClass.itemChecked)
42 | cy.getTagsElements().should('have.length', 0)
43 |
44 | cy.treeselectType('{enter}')
45 | cy.getListItem(optionNames.EnglandGroup).should('have.class', listClass.itemChecked)
46 | cy.getTagsElements().should('have.length', 1)
47 |
48 | cy.treeselectType('{enter}')
49 | cy.getListItem(optionNames.EnglandGroup).should('not.have.class', listClass.itemChecked)
50 | cy.getTagsElements().should('have.length', 0)
51 | })
52 |
53 | it('should change group on ArrowDown/ArrowUp keys', () => {
54 | cy.clearClick()
55 | cy.treeselectType('{downarrow}')
56 | cy.getListItem(optionNames.FranceGroup).should('have.class', listClass.itemFocused)
57 |
58 | cy.treeselectType('{downarrow}')
59 | cy.getListItem(optionNames.EnglandGroup).should('have.class', listClass.itemFocused)
60 |
61 | cy.treeselectType('{uparrow}')
62 | cy.getListItem(optionNames.FranceGroup).should('have.class', listClass.itemFocused)
63 |
64 | cy.treeselectType('{uparrow}')
65 | cy.getListItem(optionNames.EnglandGroup).should('have.class', listClass.itemFocused)
66 | })
67 |
68 | it('should open/close group on ArrowRight/ArrowLeft keys', () => {
69 | cy.clearClick()
70 | cy.getListItem(optionNames.LondonGroup).should('have.class', listClass.itemHidden)
71 |
72 | cy.treeselectType('{rightarrow}')
73 | cy.getListItem(optionNames.LondonGroup).should('not.have.class', listClass.itemHidden)
74 |
75 | cy.treeselectType('{leftarrow}')
76 | cy.getListItem(optionNames.ChelseaItem).should('have.class', listClass.itemHidden)
77 | })
78 |
79 | it('should run full keyboard navigation', () => {
80 | cy.clearClick()
81 |
82 | cy.getListItem(optionNames.EnglandGroup).should('have.class', listClass.itemFocused)
83 |
84 | cy.treeselectType('{rightarrow}')
85 | cy.getListItem(optionNames.LondonGroup).should('not.have.class', listClass.itemHidden)
86 | cy.getListItem(optionNames.BrightonItem).should('not.have.class', listClass.itemHidden)
87 |
88 | cy.treeselectType('{downarrow}')
89 | cy.getListItem(optionNames.LondonGroup).should('have.class', listClass.itemFocused)
90 |
91 | cy.treeselectType('{rightarrow}')
92 | cy.getListItem(optionNames.ChelseaItem).should('not.have.class', listClass.itemHidden)
93 | cy.getListItem(optionNames.WestEndItem).should('not.have.class', listClass.itemHidden)
94 |
95 | cy.treeselectType('{downarrow}')
96 | cy.getListItem(optionNames.ChelseaItem).should('have.class', listClass.itemFocused)
97 |
98 | cy.treeselectType('{downarrow}')
99 | cy.getListItem(optionNames.WestEndItem).should('have.class', listClass.itemFocused)
100 |
101 | cy.treeselectType('{downarrow}')
102 | cy.getListItem(optionNames.BrightonItem).should('have.class', listClass.itemFocused)
103 |
104 | cy.treeselectType('{downarrow}')
105 | cy.getListItem(optionNames.FranceGroup).should('have.class', listClass.itemFocused)
106 |
107 | cy.treeselectType('{rightarrow}')
108 | cy.getListItem(optionNames.ParisItem).should('not.have.class', listClass.itemHidden)
109 | cy.getListItem(optionNames.LyonItem).should('not.have.class', listClass.itemHidden)
110 |
111 | cy.treeselectType('{leftarrow}')
112 | cy.getListItem(optionNames.ParisItem).should('have.class', listClass.itemHidden)
113 | cy.getListItem(optionNames.LyonItem).should('have.class', listClass.itemHidden)
114 |
115 | cy.treeselectType('{uparrow}')
116 | cy.getListItem(optionNames.BrightonItem).should('have.class', listClass.itemFocused)
117 |
118 | cy.treeselectType('{uparrow}')
119 | cy.getListItem(optionNames.WestEndItem).should('have.class', listClass.itemFocused)
120 |
121 | cy.treeselectType('{uparrow}')
122 | cy.getListItem(optionNames.ChelseaItem).should('have.class', listClass.itemFocused)
123 |
124 | cy.treeselectType('{uparrow}')
125 | cy.getListItem(optionNames.LondonGroup).should('have.class', listClass.itemFocused)
126 |
127 | cy.treeselectType('{leftarrow}')
128 | cy.getListItem(optionNames.ChelseaItem).should('have.class', listClass.itemHidden)
129 | cy.getListItem(optionNames.WestEndItem).should('have.class', listClass.itemHidden)
130 |
131 | cy.treeselectType('{uparrow}')
132 | cy.getListItem(optionNames.EnglandGroup).should('have.class', listClass.itemFocused)
133 |
134 | cy.treeselectType('{leftarrow}')
135 | cy.getListItem(optionNames.LondonGroup).should('have.class', listClass.itemHidden)
136 | })
137 | })
138 |
--------------------------------------------------------------------------------
/__tests__/cypress/e2e/base/open-close-group.cy.ts:
--------------------------------------------------------------------------------
1 | import { classes, classesSelectors, optionNames } from '../../helpers'
2 |
3 | const { list: listClass } = classes
4 | const { list: listSelector } = classesSelectors
5 |
6 | describe('open-close-group', () => {
7 | beforeEach(() => {
8 | cy.visitBasePage()
9 | })
10 |
11 | it('should groups can be opened and closed', () => {
12 | // Open list
13 | cy.treeselectClick()
14 |
15 | cy.getListItem(optionNames.EnglandGroup).should('not.have.class', listClass.itemHidden)
16 | cy.getListItem(optionNames.LondonGroup).should('have.class', listClass.itemHidden)
17 | cy.getListItem(optionNames.ChelseaItem).should('have.class', listClass.itemHidden)
18 | cy.getListItem(optionNames.WestEndItem).should('have.class', listClass.itemHidden)
19 | cy.getListItem(optionNames.BrightonItem).should('have.class', listClass.itemHidden)
20 |
21 | cy.getListItem(optionNames.FranceGroup).should('not.have.class', listClass.itemHidden)
22 | cy.getListItem(optionNames.ParisItem).should('have.class', listClass.itemHidden)
23 | cy.getListItem(optionNames.LyonItem).should('have.class', listClass.itemHidden)
24 |
25 | // Open on England group
26 | cy.getListItem(optionNames.EnglandGroup).find(listSelector.itemArrow).click()
27 | cy.getListItem(optionNames.LondonGroup).should('not.have.class', listClass.itemHidden)
28 | cy.getListItem(optionNames.ChelseaItem).should('have.class', listClass.itemHidden)
29 | cy.getListItem(optionNames.WestEndItem).should('have.class', listClass.itemHidden)
30 | cy.getListItem(optionNames.BrightonItem).should('have.not.class', listClass.itemHidden)
31 |
32 | // Open on France group
33 | cy.getListItem(optionNames.FranceGroup).find(listSelector.itemArrow).click()
34 | cy.getListItem(optionNames.ParisItem).should('have.not.class', listClass.itemHidden)
35 | cy.getListItem(optionNames.LyonItem).should('have.not.class', listClass.itemHidden)
36 |
37 | // Open on London group
38 | cy.getListItem(optionNames.LondonGroup).find(listSelector.itemArrow).click()
39 | cy.getListItem(optionNames.ChelseaItem).should('have.not.class', listClass.itemHidden)
40 | cy.getListItem(optionNames.WestEndItem).should('have.not.class', listClass.itemHidden)
41 |
42 | // Close groups
43 |
44 | // Close London group
45 | cy.getListItem(optionNames.LondonGroup).find(listSelector.itemArrow).click()
46 | cy.getListItem(optionNames.ChelseaItem).should('have.class', listClass.itemHidden)
47 | cy.getListItem(optionNames.WestEndItem).should('have.class', listClass.itemHidden)
48 |
49 | // Close France group
50 | cy.getListItem(optionNames.FranceGroup).find(listSelector.itemArrow).click()
51 | cy.getListItem(optionNames.ParisItem).should('have.class', listClass.itemHidden)
52 | cy.getListItem(optionNames.LyonItem).should('have.class', listClass.itemHidden)
53 |
54 | // Close England group
55 | cy.getListItem(optionNames.EnglandGroup).find(listSelector.itemArrow).click()
56 | cy.getListItem(optionNames.LondonGroup).should('have.class', listClass.itemHidden)
57 | cy.getListItem(optionNames.ChelseaItem).should('have.class', listClass.itemHidden)
58 | cy.getListItem(optionNames.WestEndItem).should('have.class', listClass.itemHidden)
59 | cy.getListItem(optionNames.BrightonItem).should('have.class', listClass.itemHidden)
60 | })
61 | })
62 |
--------------------------------------------------------------------------------
/__tests__/cypress/e2e/base/open-close-list.cy.ts:
--------------------------------------------------------------------------------
1 | import { classes, classesSelectors } from '../../helpers'
2 |
3 | const { input: inputClass } = classes
4 | const { input: inputSelector, list: listSelector } = classesSelectors
5 |
6 | describe('open-close-list', () => {
7 | beforeEach(() => {
8 | cy.visitBasePage()
9 | })
10 |
11 | it('should open list on click', () => {
12 | cy.treeselectClick()
13 | cy.get(inputSelector.base).should('have.class', inputClass.opened)
14 | cy.get(listSelector.base).should('exist')
15 | })
16 |
17 | it('should open list on arrow icon click', () => {
18 | cy.inputArrowClick()
19 | cy.get(inputSelector.base).should('have.class', inputClass.opened)
20 | cy.get(listSelector.base).should('exist')
21 | })
22 |
23 | it('should close list on arrow icon click', () => {
24 | cy.inputArrowClick()
25 | cy.get(inputSelector.base).should('have.class', inputClass.opened)
26 | cy.get(listSelector.base).should('exist')
27 |
28 | cy.inputArrowClick()
29 | cy.get(inputSelector.base).should('not.have.class', inputClass.opened)
30 | cy.get(listSelector.base).should('not.exist')
31 | })
32 |
33 | it('should close list on click outside', () => {
34 | cy.treeselectClick()
35 | cy.get(inputSelector.base).should('have.class', inputClass.opened)
36 | cy.get(listSelector.base).should('exist')
37 |
38 | cy.outsideClick()
39 | cy.get(inputSelector.base).should('not.have.class', inputClass.opened)
40 | cy.get(listSelector.base).should('not.exist')
41 | })
42 |
43 | it('should not be opened on focus', () => {
44 | cy.treeselectFocus()
45 | cy.get(inputSelector.base).should('have.class', inputClass.focused)
46 | cy.get(inputSelector.base).should('not.have.class', inputClass.opened)
47 | cy.get(listSelector.base).should('not.exist')
48 | })
49 | })
50 |
--------------------------------------------------------------------------------
/__tests__/cypress/e2e/base/remove-values.cy.ts:
--------------------------------------------------------------------------------
1 | import { classesSelectors } from '../../helpers'
2 |
3 | const { parent: parentSelector, list: listSelector } = classesSelectors
4 |
5 | describe('remove-values', () => {
6 | beforeEach(() => {
7 | cy.visitBasePage()
8 | })
9 |
10 | it('should remove values on clear click', () => {
11 | cy.treeselectClick()
12 | cy.clearClick()
13 |
14 | cy.get(parentSelector).find(listSelector.itemChecked).should('not.exist')
15 | cy.get(parentSelector).find(listSelector.itemPartialChecked).should('not.exist')
16 | })
17 |
18 | it('should remove values on tag click', () => {
19 | cy.treeselectClick()
20 | cy.getTagsElements().should('have.length', 2)
21 |
22 | cy.getTagsElements().first().click()
23 | cy.getTagsElements().should('have.length', 1)
24 | })
25 |
26 | it('should not open list on clear click', () => {
27 | cy.treeselectFocus()
28 | cy.clearClick()
29 |
30 | cy.get(parentSelector).find(listSelector.base).should('not.exist')
31 | })
32 |
33 | it('should not open list on Backspace key', () => {
34 | cy.treeselectFocus()
35 | cy.treeselectType('{backspace}')
36 |
37 | cy.get(parentSelector).find(listSelector.base).should('not.exist')
38 | })
39 | })
40 |
--------------------------------------------------------------------------------
/__tests__/cypress/e2e/base/search.cy.ts:
--------------------------------------------------------------------------------
1 | import { classes, classesSelectors, optionNames } from '../../helpers'
2 |
3 | const { list: listClass } = classes
4 | const { list: listSelectors } = classesSelectors
5 |
6 | describe('check-uncheck-group', () => {
7 | beforeEach(() => {
8 | cy.visitBasePage()
9 | cy.clearClick()
10 | cy.treeselectClick()
11 | })
12 |
13 | it('should open list on search', () => {
14 | cy.outsideClick()
15 | cy.getList().should('not.exist')
16 |
17 | cy.treeselectType('Paris')
18 | cy.getList().should('exist')
19 | })
20 |
21 | it('should filter and show items on search', () => {
22 | cy.treeselectType('Chel')
23 |
24 | cy.getListItem(optionNames.EnglandGroup).should('not.have.class', listClass.itemHidden)
25 | cy.getListItem(optionNames.LondonGroup).should('not.have.class', listClass.itemHidden)
26 | cy.getListItem(optionNames.ChelseaItem).should('not.have.class', listClass.itemHidden)
27 |
28 | cy.getListItem(optionNames.WestEndItem).should('have.class', listClass.itemHidden)
29 | cy.getListItem(optionNames.BrightonItem).should('have.class', listClass.itemHidden)
30 | cy.getListItem(optionNames.FranceGroup).should('have.class', listClass.itemHidden)
31 |
32 | cy.getInput().find('input').should('have.value', 'Chel')
33 | })
34 |
35 | it('should clear search on blur', () => {
36 | cy.treeselectType('Paris')
37 |
38 | cy.getListItem(optionNames.ParisItem).should('not.have.class', listClass.itemHidden)
39 | cy.outsideClick()
40 | cy.getInput().find('input').should('have.value', '')
41 | })
42 |
43 | it('should show no results message', () => {
44 | cy.get(listSelectors.empty).should('not.be.visible')
45 | cy.treeselectType('No results')
46 | cy.get(listSelectors.empty).should('be.visible')
47 | })
48 | })
49 |
--------------------------------------------------------------------------------
/__tests__/cypress/e2e/base/tags-values.cy.ts:
--------------------------------------------------------------------------------
1 | import { optionNames } from '../../helpers'
2 |
3 | describe('input-tags-values', () => {
4 | beforeEach(() => {
5 | cy.visitBasePage()
6 | cy.clearClick()
7 | cy.treeselectClick()
8 | cy.expandAllGroups()
9 | })
10 |
11 | it('should show group tag on group click', () => {
12 | cy.getListItem(optionNames.EnglandGroup).click()
13 | cy.getTagsElements().should('have.length', 1).should('contain', 'England')
14 | })
15 |
16 | it('should show item tag on item click', () => {
17 | cy.getListItem(optionNames.BrightonItem).click()
18 | cy.getTagsElements().should('have.length', 1).should('contain', 'Brighton')
19 | })
20 |
21 | it('should show sub group tag on sub group click', () => {
22 | cy.getListItem(optionNames.LondonGroup).click()
23 | cy.getTagsElements().should('have.length', 1).should('contain', 'London')
24 | })
25 |
26 | it('should show all tags on groups click', () => {
27 | cy.getListItem(optionNames.EnglandGroup).click()
28 | cy.getListItem(optionNames.FranceGroup).click()
29 | cy.getTagsElements().should('have.length', 2).should('contain', 'England').should('contain', 'France')
30 | })
31 | })
32 |
--------------------------------------------------------------------------------
/__tests__/cypress/e2e/disabled/index.cy.ts:
--------------------------------------------------------------------------------
1 | import { classes, optionNames } from '../../helpers'
2 |
3 | const { list: listClasses } = classes
4 |
5 | describe('select-value', () => {
6 | beforeEach(() => {
7 | cy.visitDisabledPage()
8 | cy.clearClick()
9 | cy.treeselectClick()
10 | cy.expandAllGroups()
11 | })
12 |
13 | it('should render disabled elements', () => {
14 | cy.getListItem(optionNames.ChelseaItem).should('have.class', listClasses.itemDisabled)
15 | cy.getListItem(optionNames.FranceGroup).should('have.class', listClasses.itemDisabled)
16 | cy.getListItem(optionNames.ParisItem).should('have.class', listClasses.itemDisabled)
17 | cy.getListItem(optionNames.LyonItem).should('have.class', listClasses.itemDisabled)
18 | })
19 |
20 | it('should not select disabled element on group click', () => {
21 | cy.getListItem(optionNames.EnglandGroup).click()
22 | cy.getInput().should('contain', 'Brighton').should('contain', 'West End').should('not.contain', 'Chelsea')
23 | })
24 |
25 | it('should not select disabled element on item click', () => {
26 | cy.getListItem(optionNames.ChelseaItem).click().should('not.have.class', listClasses.itemChecked)
27 | cy.getInput().should('not.contain', 'Chelsea')
28 | })
29 |
30 | it('should not select disabled element on group click', () => {
31 | cy.getListItem(optionNames.FranceGroup).click()
32 | cy.getInput().should('not.contain', 'France').should('not.contain', 'Paris').should('not.contain', 'Lyon')
33 | })
34 |
35 | it('should not select disabled element on Enter key', () => {
36 | cy.getListItem(optionNames.ChelseaItem).trigger('mouseover').should('have.class', listClasses.itemFocused)
37 |
38 | cy.treeselectType('{enter}')
39 |
40 | cy.getInput().should('not.contain', 'Chelsea')
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/__tests__/cypress/e2e/experemental/is-boost-rendering-list.cy.ts:
--------------------------------------------------------------------------------
1 | import { classes, classesSelectors } from '../../helpers'
2 |
3 | const { list: listClass } = classes
4 | const { list: listSelector } = classesSelectors
5 |
6 | const optionsVisibility = {
7 | visible: ['Option 495', 'Option 504'],
8 | hidden: ['Option 494', 'Option 505']
9 | }
10 |
11 | const visibleSubOptions = ['SubOption 495-0', 'SubOption 495-1', 'SubOption 495-2']
12 |
13 | describe('is-boost-rendering-list', () => {
14 | beforeEach(() => {
15 | cy.visitBoostRenderingPage()
16 | cy.treeselectClick()
17 | })
18 |
19 | it('should render list with boost rendering', () => {
20 | cy.treeselectClick()
21 | cy.getList().scrollTo('center')
22 |
23 | optionsVisibility.visible.forEach((option) => {
24 | cy.getListItem(option).should('not.have.class', listClass.itemScrollNotVisible)
25 | })
26 |
27 | optionsVisibility.hidden.forEach((option) => {
28 | cy.getListItem(option).should('have.class', listClass.itemScrollNotVisible)
29 | })
30 | })
31 |
32 | it('should work correctly with search', () => {
33 | cy.treeselectClick()
34 | cy.getList().scrollTo('center')
35 |
36 | const [firstVisibleOption] = optionsVisibility.visible
37 |
38 | cy.getListItem(firstVisibleOption).find(listSelector.itemArrow).click()
39 |
40 | const lastVisibleOption = optionsVisibility.visible.at(-1) ?? ''
41 | cy.getListItem(lastVisibleOption).should('have.class', listClass.itemScrollNotVisible)
42 |
43 | visibleSubOptions.forEach((subOption) => {
44 | cy.getListItem(subOption).should('not.have.class', listClass.itemScrollNotVisible)
45 | cy.getListItem(subOption).should('not.have.class', listClass.itemHidden)
46 | })
47 |
48 | cy.getListItem(firstVisibleOption).click()
49 | cy.getListItem(firstVisibleOption).should('have.class', listClass.itemChecked)
50 |
51 | visibleSubOptions.forEach((subOption) => {
52 | cy.getListItem(subOption).should('have.class', listClass.itemChecked)
53 | })
54 | })
55 | })
56 |
--------------------------------------------------------------------------------
/__tests__/cypress/e2e/large-data/save-scroll.cy.ts:
--------------------------------------------------------------------------------
1 | describe('save-scroll', () => {
2 | beforeEach(() => {
3 | cy.visitLargeDataPage()
4 | cy.treeselectClick()
5 | })
6 |
7 | it('should render large data', () => {
8 | cy.getList().should('exist')
9 | })
10 |
11 | it('should save scroll position', async () => {
12 | const middleSelector = '[title="Option 25"]'
13 |
14 | cy.getList().scrollTo('center')
15 | cy.get(middleSelector).should('be.visible')
16 |
17 | cy.wait(100)
18 | cy.outsideClick()
19 | cy.wait(100)
20 |
21 | cy.treeselectClick()
22 | cy.get(middleSelector).should('be.visible')
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/__tests__/cypress/e2e/single/select-value.cy.ts:
--------------------------------------------------------------------------------
1 | import { optionNames } from '../../helpers'
2 |
3 | describe('select-value', () => {
4 | beforeEach(() => {
5 | cy.visitSinglePage()
6 | cy.clearClick()
7 | cy.treeselectClick()
8 | cy.expandAllGroups()
9 | })
10 |
11 | it('should select one element on click', () => {
12 | cy.getListItem(optionNames.BrightonItem).click()
13 | cy.getInput().should('contain', 'Brighton')
14 | })
15 |
16 | it('should not unselect selected element on click', () => {
17 | cy.getListItem(optionNames.BrightonItem).click()
18 | cy.getListItem(optionNames.BrightonItem).click()
19 | cy.getInput().should('contain', 'Brighton')
20 | })
21 |
22 | it('should select group on click', () => {
23 | cy.getListItem(optionNames.LondonGroup).click()
24 | cy.getInput().should('contain', 'London')
25 | })
26 |
27 | it('should close list on select', () => {
28 | cy.getListItem(optionNames.BrightonItem).click()
29 | cy.getList().should('not.exist')
30 | })
31 | })
32 |
--------------------------------------------------------------------------------
/__tests__/cypress/e2e/slot/index.cy.ts:
--------------------------------------------------------------------------------
1 | import { classesSelectors } from '../../helpers'
2 |
3 | const { list: listSelectors } = classesSelectors
4 |
5 | describe('slot', () => {
6 | beforeEach(() => {
7 | cy.visitSlotPage()
8 | cy.clearClick()
9 | cy.treeselectClick()
10 | })
11 |
12 | it('should be opened after slot click', () => {
13 | cy.get('.treeselect-demo-slot__slot').should('exist')
14 | cy.get('.treeselect-demo-slot__slot').click()
15 | cy.get(listSelectors.base).should('exist')
16 | })
17 |
18 | it('should be opened after btn slot click', () => {
19 | cy.get('.treeselect-demo-slot__slot-btn').should('exist')
20 | cy.get('.treeselect-demo-slot__slot-btn').click()
21 | cy.get(listSelectors.base).should('exist')
22 | })
23 | })
24 |
--------------------------------------------------------------------------------
/__tests__/cypress/helpers/index.ts:
--------------------------------------------------------------------------------
1 | export * from '../../testHelpers'
2 |
--------------------------------------------------------------------------------
/__tests__/cypress/pages/cypress-append-to-body.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Cypress Append To Body
8 |
9 |
10 |
11 |
12 |
13 |
Cypress Append To Body
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/__tests__/cypress/pages/cypress-base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Cypress Base
8 |
9 |
10 |
11 |
12 |
13 |
Cypress Base
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/__tests__/cypress/pages/cypress-boost-rendering.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Cypress Boost Rendering
8 |
9 |
10 |
11 |
12 |
13 |
Cypress Boost Rendering
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/__tests__/cypress/pages/cypress-disabled.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Cypress Disabled
8 |
9 |
10 |
11 |
12 |
13 |
Cypress Disabled
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/__tests__/cypress/pages/cypress-large-data.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Cypress Large Data
8 |
9 |
10 |
11 |
12 |
13 |
Cypress Large Data
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/__tests__/cypress/pages/cypress-single.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Cypress Single
8 |
9 |
10 |
11 |
12 |
13 |
Cypress Single
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/__tests__/cypress/pages/cypress-slot.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Cypress Single
8 |
9 |
10 |
11 |
12 |
13 |
Cypress Single
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/__tests__/cypress/pages/runs.js:
--------------------------------------------------------------------------------
1 | import '../../../src/treeselectjs.css'
2 | import Treeselect from '../../../src/treeselectjs'
3 | import { defaultOptions, largeOptionsList, largeNestedOptionsList, optionsWithDisabled } from '../../testHelpers'
4 |
5 | export const runBaseTest = () => {
6 | const className = '.treeselect-demo-base'
7 | const domElement = document.querySelector(className)
8 |
9 | if (!domElement) {
10 | return
11 | }
12 |
13 | const treeselect = new Treeselect({
14 | parentHtmlContainer: domElement,
15 | value: [4, 7, 8],
16 | options: defaultOptions
17 | })
18 | }
19 |
20 | export const runSingleTest = () => {
21 | const className = '.treeselect-demo-single-select'
22 | const domElement = document.querySelector(className)
23 |
24 | if (!domElement) {
25 | return
26 | }
27 |
28 | const treeselect = new Treeselect({
29 | parentHtmlContainer: domElement,
30 | value: 4,
31 | options: defaultOptions,
32 | isSingleSelect: true,
33 | showTags: false
34 | })
35 | }
36 |
37 | export const runDisabledTest = () => {
38 | const className = '.treeselect-demo-disabled'
39 | const domElement = document.querySelector(className)
40 |
41 | if (!domElement) {
42 | return
43 | }
44 |
45 | const treeselect = new Treeselect({
46 | parentHtmlContainer: domElement,
47 | value: [],
48 | options: optionsWithDisabled
49 | })
50 | }
51 |
52 | export const runLargeDataTest = () => {
53 | const className = '.treeselect-demo-large-data'
54 | const domElement = document.querySelector(className)
55 |
56 | if (!domElement) {
57 | return
58 | }
59 |
60 | const treeselect = new Treeselect({
61 | parentHtmlContainer: domElement,
62 | value: [],
63 | options: largeOptionsList,
64 | saveScrollPosition: true
65 | })
66 | }
67 |
68 | export const runAppendedToBodyTest = () => {
69 | const className = '.treeselect-demo-appended-to-body'
70 | const domElement = document.querySelector(className)
71 |
72 | if (!domElement) {
73 | return
74 | }
75 |
76 | const treeselect = new Treeselect({
77 | parentHtmlContainer: domElement,
78 | value: [],
79 | options: defaultOptions,
80 | appendToBody: true
81 | })
82 | }
83 |
84 | export const runBoostRenderingTest = () => {
85 | const className = '.treeselect-demo-boost-rendering'
86 | const domElement = document.querySelector(className)
87 |
88 | if (!domElement) {
89 | return
90 | }
91 |
92 | const treeselect = new Treeselect({
93 | parentHtmlContainer: domElement,
94 | value: [],
95 | options: largeNestedOptionsList,
96 | isBoostedRendering: true
97 | })
98 | }
99 |
100 | export const runSlotTest = () => {
101 | const className = '.treeselect-demo-slot'
102 | const domElement = document.querySelector(className)
103 |
104 | if (!domElement) {
105 | return
106 | }
107 |
108 | const slot = document.createElement('div')
109 | slot.classList.add('treeselect-demo-slot__slot')
110 | const btn = document.createElement('button')
111 | btn.className = 'treeselect-demo-slot__slot-btn'
112 | btn.innerText = 'Click!'
113 | slot.appendChild(btn)
114 |
115 | const treeselect = new Treeselect({
116 | parentHtmlContainer: domElement,
117 | value: [],
118 | options: defaultOptions,
119 | listSlotHtmlComponent: slot
120 | })
121 | }
122 |
123 | runBaseTest()
124 | runSingleTest()
125 | runDisabledTest()
126 | runLargeDataTest()
127 | runAppendedToBodyTest()
128 | runBoostRenderingTest()
129 | runSlotTest()
130 |
--------------------------------------------------------------------------------
/__tests__/cypress/support/commands.ts:
--------------------------------------------------------------------------------
1 | import { classesSelectors } from '../../testHelpers'
2 |
3 | const { parent: parentSelector, input: inputSelector, list: listSelector } = classesSelectors
4 | const baseUrl = 'http://localhost:5173/__tests__/cypress/pages'
5 |
6 | declare global {
7 | namespace Cypress {
8 | interface Chainable {
9 | // Visits
10 | visitBasePage(): void
11 | visitSinglePage(): void
12 | visitDisabledPage(): void
13 | visitLargeDataPage(): void
14 | visitAppendToBodyPage(): void
15 | visitBoostRenderingPage(): void
16 | visitSlotPage(): void
17 |
18 | // Actions
19 | treeselectClick(): void
20 | treeselectFocus(): void
21 | treeselectType(query: string): void
22 | inputArrowClick(): void
23 | outsideClick(): void
24 | clearClick(): void
25 | expandAllGroups(): void
26 |
27 | // Getters
28 | getTreeselect(): Chainable>
29 | getList(): Chainable>
30 | getListItem(selector: string): Chainable>
31 | getInput(): Chainable>
32 | getTagsElements(): Chainable>
33 | }
34 | }
35 | }
36 |
37 | // Visits
38 | Cypress.Commands.add('visitBasePage', () => {
39 | cy.visit(`${baseUrl}/cypress-base.html`)
40 | })
41 |
42 | Cypress.Commands.add('visitSinglePage', () => {
43 | cy.visit(`${baseUrl}/cypress-single.html`)
44 | })
45 |
46 | Cypress.Commands.add('visitDisabledPage', () => {
47 | cy.visit(`${baseUrl}/cypress-disabled.html`)
48 | })
49 |
50 | Cypress.Commands.add('visitLargeDataPage', () => {
51 | cy.visit(`${baseUrl}/cypress-large-data.html`)
52 | })
53 |
54 | Cypress.Commands.add('visitAppendToBodyPage', () => {
55 | cy.visit(`${baseUrl}/cypress-append-to-body.html`)
56 | })
57 |
58 | Cypress.Commands.add('visitBoostRenderingPage', () => {
59 | cy.visit(`${baseUrl}/cypress-boost-rendering.html`)
60 | })
61 |
62 | Cypress.Commands.add('visitSlotPage', () => {
63 | cy.visit(`${baseUrl}/cypress-slot.html`)
64 | })
65 |
66 | // Actions
67 | Cypress.Commands.add('treeselectClick', () => {
68 | cy.get(parentSelector).click()
69 | })
70 |
71 | Cypress.Commands.add('treeselectFocus', () => {
72 | cy.get(parentSelector).find('input').focus()
73 | })
74 |
75 | Cypress.Commands.add('treeselectType', (query: string) => {
76 | cy.get(parentSelector).type(query)
77 | })
78 |
79 | Cypress.Commands.add('inputArrowClick', () => {
80 | cy.get(inputSelector.arrow).click()
81 | })
82 |
83 | Cypress.Commands.add('outsideClick', () => {
84 | cy.get('body').click(0, 0)
85 | })
86 |
87 | Cypress.Commands.add('clearClick', () => {
88 | cy.get(inputSelector.clear).first().click()
89 | })
90 |
91 | Cypress.Commands.add('expandAllGroups', () => {
92 | cy.get(listSelector.itemGroup).each(($group) => {
93 | cy.wrap($group).find(listSelector.itemArrow).click()
94 | })
95 | })
96 |
97 | // Getters
98 | Cypress.Commands.add('getTreeselect', () => {
99 | return cy.get(parentSelector)
100 | })
101 |
102 | Cypress.Commands.add('getList', () => {
103 | return cy.get(listSelector.base)
104 | })
105 |
106 | Cypress.Commands.add('getListItem', (optionName: string) => {
107 | return cy.get(`[title="${optionName}"]`)
108 | })
109 |
110 | Cypress.Commands.add('getInput', () => {
111 | return cy.get(inputSelector.base)
112 | })
113 |
114 | Cypress.Commands.add('getTagsElements', () => {
115 | return cy.get(inputSelector.tagsElement)
116 | })
117 |
--------------------------------------------------------------------------------
/__tests__/cypress/support/e2e.ts:
--------------------------------------------------------------------------------
1 | import './commands'
2 |
--------------------------------------------------------------------------------
/__tests__/jest/helpers/getElements.ts:
--------------------------------------------------------------------------------
1 | import { classesSelectors } from '../../testHelpers'
2 |
3 | const { input: inputSelectors, list: listSelectors } = classesSelectors
4 |
5 | // Input
6 | export const getInputElement = (parentHtmlContainer: HTMLElement) =>
7 | parentHtmlContainer.querySelector(inputSelectors.base) as HTMLElement
8 |
9 | export const getTagsElements = (parentHtmlContainer: HTMLElement) =>
10 | parentHtmlContainer.querySelectorAll(inputSelectors.tagsElement) as NodeListOf
11 |
12 | export const getTagsElement = (parentHtmlContainer: HTMLElement) =>
13 | parentHtmlContainer.querySelector(inputSelectors.tags) as HTMLElement
14 |
15 | export const getEditElement = (parentHtmlContainer: HTMLElement) =>
16 | parentHtmlContainer.querySelector(inputSelectors.edit) as HTMLElement
17 |
18 | export const getArrowElement = (parentHtmlContainer: HTMLElement) =>
19 | parentHtmlContainer.querySelector(inputSelectors.arrow) as HTMLElement
20 |
21 | export const getClearElement = (parentHtmlContainer: HTMLElement) =>
22 | parentHtmlContainer.querySelector(inputSelectors.clear) as HTMLElement
23 |
24 | // List
25 | export const getListElement = (parentHtmlContainer: HTMLElement) =>
26 | parentHtmlContainer.querySelector(listSelectors.base) as HTMLElement
27 |
28 | export const getListItems = (parentHtmlContainer: HTMLElement) =>
29 | parentHtmlContainer.querySelectorAll(listSelectors.item) as NodeListOf
30 |
31 | export const getNoResultsElement = (parentHtmlContainer: HTMLElement) =>
32 | parentHtmlContainer.querySelector(listSelectors.empty) as HTMLElement
33 |
34 | export const getListGroupsItems = (parentHtmlContainer: HTMLElement) =>
35 | parentHtmlContainer.querySelectorAll(listSelectors.itemGroup) as NodeListOf
36 |
37 | export const getListSlotElement = (parentHtmlContainer: HTMLElement) =>
38 | parentHtmlContainer.querySelector(listSelectors.slot) as HTMLElement
39 |
40 | export const getGroupArrowIcons = (parentHtmlContainer: HTMLElement) =>
41 | parentHtmlContainer.querySelectorAll(listSelectors.itemArrow) as NodeListOf
42 |
--------------------------------------------------------------------------------
/__tests__/jest/helpers/index.ts:
--------------------------------------------------------------------------------
1 | export * from '../../testHelpers'
2 | export * from './getElements'
3 | export * from './renderTreeselect'
4 |
--------------------------------------------------------------------------------
/__tests__/jest/helpers/renderTreeselect.ts:
--------------------------------------------------------------------------------
1 | import Treeselect, { ITreeselectParams } from '../../../src/treeselectjs'
2 |
3 | export const renderTreeselect = (params: Omit) => {
4 | document.body.innerHTML = ''
5 | const container = document.body.querySelector('#test-container') as HTMLElement
6 |
7 | return new Treeselect({
8 | parentHtmlContainer: container,
9 | ...params
10 | })
11 | }
12 |
--------------------------------------------------------------------------------
/__tests__/jest/setup.ts:
--------------------------------------------------------------------------------
1 | import ResizeObserver from 'resize-observer-polyfill'
2 | import 'intersection-observer'
3 |
4 | global.ResizeObserver = ResizeObserver
5 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/core-props/__snapshots__/disabled.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`disabled prop should render a disabled Treeselect 1`] = `
4 |
81 | `;
82 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/core-props/__snapshots__/isGroupedValue.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`isGroupedValue prop should group values 1`] = `
4 |
118 | `;
119 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/core-props/__snapshots__/rtl.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`rtl prop should render RTL with rtl prop 1`] = `
4 |
80 | `;
81 |
82 | exports[`rtl prop should render RTL with rtl prop and appendToBody 1`] = `
83 |
159 | `;
160 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/core-props/ariaLabel.test.ts:
--------------------------------------------------------------------------------
1 | import { renderTreeselect, getEditElement } from '../../helpers'
2 |
3 | describe('ariaLabel prop', () => {
4 | it('should render a Treeselect with the given aria-label', () => {
5 | const ariaLabel = 'test-aria-label'
6 |
7 | const treeselect = renderTreeselect({
8 | value: [],
9 | options: [],
10 | ariaLabel
11 | })
12 |
13 | const input = getEditElement(treeselect.parentHtmlContainer)
14 | expect(input.getAttribute('aria-label')).toBe(ariaLabel)
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/core-props/disabled.test.ts:
--------------------------------------------------------------------------------
1 | import { fireEvent } from '@testing-library/dom'
2 | import { renderTreeselect, getTagsElement, getTagsElements, getArrowElement, classesSelectors } from '../../helpers'
3 |
4 | const { list: listSelectors } = classesSelectors
5 |
6 | describe('disabled prop', () => {
7 | it('should render a disabled Treeselect', () => {
8 | const treeselect = renderTreeselect({
9 | value: [],
10 | options: [],
11 | disabled: true
12 | })
13 |
14 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
15 | })
16 |
17 | it('should not open the dropdown when Treeselect is disabled', () => {
18 | const treeselect = renderTreeselect({
19 | value: [],
20 | options: [],
21 | disabled: true
22 | })
23 |
24 | const arrow = getArrowElement(treeselect.parentHtmlContainer)
25 | fireEvent.mouseDown(arrow)
26 | expect(document.body.innerHTML).not.toContain(listSelectors.base)
27 | })
28 |
29 | it('should not remove tags when Treeselect is disabled', () => {
30 | const treeselect = renderTreeselect({
31 | value: [1],
32 | options: [{ value: 1, name: 'Option 1', children: [] }],
33 | disabled: true
34 | })
35 |
36 | const tagsElement = getTagsElement(treeselect.parentHtmlContainer)
37 | fireEvent.mouseDown(tagsElement)
38 | const tagsElements = getTagsElements(treeselect.parentHtmlContainer)
39 |
40 | expect(tagsElements.length).toBe(1)
41 | expect(treeselect.value).toEqual([1])
42 | })
43 | })
44 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/core-props/id.test.ts:
--------------------------------------------------------------------------------
1 | import { renderTreeselect, getEditElement } from '../../helpers'
2 |
3 | describe('id prop', () => {
4 | it('should render a Treeselect with the given id', () => {
5 | const id = 'test-id'
6 |
7 | const treeselect = renderTreeselect({
8 | value: [],
9 | options: [],
10 | id
11 | })
12 |
13 | const input = getEditElement(treeselect.parentHtmlContainer)
14 | expect(input.getAttribute('id')).toBe(id)
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/core-props/isGroupedValue.test.ts:
--------------------------------------------------------------------------------
1 | import { renderTreeselect, defaultOptions, optionsValues } from '../../helpers'
2 |
3 | describe('isGroupedValue prop', () => {
4 | it('should not group values by default', () => {
5 | const treeselect = renderTreeselect({
6 | value: [optionsValues.EnglandGroup],
7 | options: defaultOptions
8 | })
9 |
10 | expect(treeselect.value).toEqual([optionsValues.ChelseaItem, optionsValues.WestEndItem, optionsValues.BrightonItem])
11 | })
12 |
13 | it('should group values', () => {
14 | const treeselect = renderTreeselect({
15 | isGroupedValue: true,
16 | value: [optionsValues.EnglandGroup],
17 | options: defaultOptions
18 | })
19 |
20 | expect(treeselect.value).toEqual([optionsValues.EnglandGroup])
21 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
22 | })
23 | })
24 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/core-props/isIndependentNodes.test.ts:
--------------------------------------------------------------------------------
1 | import { renderTreeselect, defaultOptions, optionsValues } from '../../helpers'
2 |
3 | describe('isIndependentNodes prop', () => {
4 | it('should not be independent nodes by default', () => {
5 | const treeselect = renderTreeselect({
6 | value: [optionsValues.EnglandGroup],
7 | options: defaultOptions
8 | })
9 |
10 | treeselect.toggleOpenClose()
11 |
12 | expect(treeselect.value).toEqual([optionsValues.ChelseaItem, optionsValues.WestEndItem, optionsValues.BrightonItem])
13 | })
14 |
15 | it('should be independent nodes', () => {
16 | const treeselect = renderTreeselect({
17 | isIndependentNodes: true,
18 | value: [optionsValues.EnglandGroup, optionsValues.BrightonItem],
19 | options: defaultOptions
20 | })
21 |
22 | treeselect.toggleOpenClose()
23 |
24 | expect(treeselect.value).toEqual([optionsValues.EnglandGroup, optionsValues.BrightonItem])
25 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
26 | })
27 | })
28 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/core-props/isSingleSelect.test.ts:
--------------------------------------------------------------------------------
1 | import { renderTreeselect, defaultOptions, optionsValues } from '../../helpers'
2 |
3 | describe('isSingleSelect prop', () => {
4 | it('should render a Treeselect with single-select mode', () => {
5 | const treeselect = renderTreeselect({
6 | isSingleSelect: true,
7 | value: optionsValues.ChelseaItem,
8 | options: defaultOptions
9 | })
10 |
11 | treeselect.toggleOpenClose()
12 |
13 | expect(treeselect.value).toEqual(optionsValues.ChelseaItem)
14 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
15 | })
16 |
17 | it('should select only one value in single-select mode', () => {
18 | const treeselect = renderTreeselect({
19 | isSingleSelect: true,
20 | value: optionsValues.EnglandGroup,
21 | options: defaultOptions
22 | })
23 |
24 | treeselect.toggleOpenClose()
25 |
26 | expect(treeselect.value).toEqual(optionsValues.EnglandGroup)
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/core-props/parentHtmlContainer.test.ts:
--------------------------------------------------------------------------------
1 | import { renderTreeselect, defaultOptions } from '../../helpers'
2 |
3 | describe('parentHtmlContainer prop', () => {
4 | it('should render a Treeselect component', () => {
5 | const treeselect = renderTreeselect({
6 | value: [],
7 | options: defaultOptions
8 | })
9 |
10 | treeselect.toggleOpenClose()
11 |
12 | expect(treeselect).toBeDefined()
13 | expect(document.body.contains(treeselect.parentHtmlContainer)).toBe(true)
14 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/core-props/rtl.test.ts:
--------------------------------------------------------------------------------
1 | import { renderTreeselect, defaultOptions, classesSelectors } from '../../helpers'
2 |
3 | describe('rtl prop', () => {
4 | it('should render RTL with rtl prop', () => {
5 | const treeselect = renderTreeselect({
6 | value: [],
7 | options: defaultOptions,
8 | rtl: true
9 | })
10 |
11 | expect(treeselect.parentHtmlContainer.getAttribute('dir')).toBe('rtl')
12 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
13 | })
14 |
15 | it('should render RTL with rtl prop and appendToBody', () => {
16 | const treeselect = renderTreeselect({
17 | value: [],
18 | options: defaultOptions,
19 | rtl: true,
20 | appendToBody: true
21 | })
22 |
23 | treeselect.toggleOpenClose()
24 |
25 | const list = document.querySelector(classesSelectors.list.base)!
26 | expect(list.getAttribute('dir')).toBe('rtl')
27 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/core-props/value.test.ts:
--------------------------------------------------------------------------------
1 | import { renderTreeselect, getTagsElements, defaultOptions, optionsValues } from '../../helpers'
2 |
3 | describe('value prop', () => {
4 | it('should render a Treeselect with empty tags', () => {
5 | const treeselect = renderTreeselect({
6 | value: [],
7 | options: defaultOptions
8 | })
9 |
10 | const tagsElements = getTagsElements(treeselect.parentHtmlContainer)
11 | treeselect.toggleOpenClose()
12 |
13 | expect(treeselect.value).toEqual([])
14 | expect(tagsElements.length).toBe(0)
15 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
16 | })
17 |
18 | it('should render a Treeselect with one tag', () => {
19 | const treeselect = renderTreeselect({
20 | value: [optionsValues.BrightonItem],
21 | options: defaultOptions
22 | })
23 |
24 | const tagsElements = getTagsElements(treeselect.parentHtmlContainer)
25 | treeselect.toggleOpenClose()
26 |
27 | expect(treeselect.value).toEqual([optionsValues.BrightonItem])
28 | expect(tagsElements.length).toBe(1)
29 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
30 | })
31 |
32 | it('should render a Treeselect with multiple tags', () => {
33 | const treeselect = renderTreeselect({
34 | value: [optionsValues.ChelseaItem, optionsValues.BrightonItem],
35 | options: defaultOptions
36 | })
37 |
38 | const tagsElements = getTagsElements(treeselect.parentHtmlContainer)
39 | treeselect.toggleOpenClose()
40 |
41 | expect(tagsElements.length).toBe(2)
42 | expect(treeselect.value).toEqual([optionsValues.ChelseaItem, optionsValues.BrightonItem])
43 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
44 | })
45 |
46 | it('should not contain non existent tags', () => {
47 | const treeselect = renderTreeselect({
48 | value: [100],
49 | options: defaultOptions
50 | })
51 |
52 | const tagsElements = getTagsElements(treeselect.parentHtmlContainer)
53 |
54 | expect(tagsElements.length).toBe(0)
55 | expect(treeselect.value).toEqual([])
56 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
57 | })
58 |
59 | it('should not contain duplicate tags', () => {
60 | const treeselect = renderTreeselect({
61 | value: [optionsValues.ChelseaItem, optionsValues.ChelseaItem],
62 | options: defaultOptions
63 | })
64 |
65 | const tagsElements = getTagsElements(treeselect.parentHtmlContainer)
66 | treeselect.toggleOpenClose()
67 |
68 | expect(tagsElements.length).toBe(1)
69 | expect(treeselect.value).toEqual([optionsValues.ChelseaItem])
70 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
71 | })
72 |
73 | // Single mode
74 | it('should render a Treeselect with empty value in single mode', () => {
75 | const treeselect = renderTreeselect({
76 | isSingleSelect: true,
77 | value: null,
78 | options: defaultOptions
79 | })
80 |
81 | const tagsElements = getTagsElements(treeselect.parentHtmlContainer)
82 | treeselect.toggleOpenClose()
83 |
84 | expect(treeselect.value).toEqual(null)
85 | expect(tagsElements.length).toBe(0)
86 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
87 | })
88 |
89 | it('should render a Treeselect with one value in single mode', () => {
90 | const treeselect = renderTreeselect({
91 | isSingleSelect: true,
92 | value: optionsValues.ChelseaItem,
93 | options: defaultOptions
94 | })
95 |
96 | const tagsElements = getTagsElements(treeselect.parentHtmlContainer)
97 | treeselect.toggleOpenClose()
98 |
99 | expect(treeselect.value).toEqual(optionsValues.ChelseaItem)
100 | expect(tagsElements.length).toBe(1)
101 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
102 | })
103 |
104 | it('should not select non existent value in single mode', () => {
105 | const treeselect = renderTreeselect({
106 | isSingleSelect: true,
107 | value: 100,
108 | options: defaultOptions
109 | })
110 |
111 | const tagsElements = getTagsElements(treeselect.parentHtmlContainer)
112 | treeselect.toggleOpenClose()
113 |
114 | expect(treeselect.value).toEqual(null)
115 | expect(tagsElements.length).toBe(0)
116 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
117 | })
118 | })
119 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/input-props/__snapshots__/grouped.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`grouped prop should grouped by default 1`] = `
4 |
118 | `;
119 |
120 | exports[`grouped prop should not be grouped 1`] = `
121 |
315 | `;
316 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/input-props/__snapshots__/showTags.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`showTags prop should show group name if group is selected and showTags is false 1`] = `
4 |
84 | `;
85 |
86 | exports[`showTags prop should show tags by default 1`] = `
87 |
201 | `;
202 |
203 | exports[`showTags prop should show text with count if multiple values are selected and showTags is false 1`] = `
204 |
284 | `;
285 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/input-props/clearable.test.ts:
--------------------------------------------------------------------------------
1 | import { fireEvent } from '@testing-library/dom'
2 | import { renderTreeselect, defaultOptions, getClearElement, optionsValues } from '../../helpers'
3 |
4 | describe('clearable prop', () => {
5 | it('should render a clear button', () => {
6 | const treeselect = renderTreeselect({
7 | value: [optionsValues.LondonGroup, optionsValues.FranceGroup],
8 | options: defaultOptions
9 | })
10 |
11 | const clearElement = getClearElement(treeselect.parentHtmlContainer)
12 | expect(clearElement).toBeDefined()
13 | })
14 |
15 | it('should not render a clear button', () => {
16 | const treeselect = renderTreeselect({
17 | clearable: false,
18 | value: [optionsValues.LondonGroup, optionsValues.FranceGroup],
19 | options: defaultOptions
20 | })
21 |
22 | const clearElement = getClearElement(treeselect.parentHtmlContainer)
23 | expect(clearElement).toBeNull()
24 | })
25 |
26 | it('should clear the value when the clear button is clicked', () => {
27 | const treeselect = renderTreeselect({
28 | value: [optionsValues.LondonGroup, optionsValues.FranceGroup],
29 | options: defaultOptions
30 | })
31 |
32 | const clearElement = getClearElement(treeselect.parentHtmlContainer)
33 | fireEvent.mouseDown(clearElement)
34 | treeselect.toggleOpenClose()
35 |
36 | expect(treeselect.value).toEqual([])
37 | })
38 | })
39 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/input-props/grouped.test.ts:
--------------------------------------------------------------------------------
1 | import { renderTreeselect, defaultOptions, getTagsElements, optionsValues } from '../../helpers'
2 |
3 | describe('grouped prop', () => {
4 | it('should grouped by default', () => {
5 | const treeselect = renderTreeselect({
6 | value: [optionsValues.EnglandGroup],
7 | options: defaultOptions
8 | })
9 |
10 | const tagsElements = getTagsElements(treeselect.parentHtmlContainer)
11 |
12 | expect(tagsElements).toHaveLength(1)
13 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
14 | })
15 |
16 | it('should not be grouped', () => {
17 | const treeselect = renderTreeselect({
18 | grouped: false,
19 | value: [optionsValues.EnglandGroup],
20 | options: defaultOptions
21 | })
22 |
23 | const tagsElements = getTagsElements(treeselect.parentHtmlContainer)
24 |
25 | expect(tagsElements).toHaveLength(3)
26 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/input-props/placeholder.test.ts:
--------------------------------------------------------------------------------
1 | import { renderTreeselect, getEditElement } from '../../helpers'
2 |
3 | describe('placeholder prop', () => {
4 | it('should render default placeholder', () => {
5 | const treeselect = renderTreeselect({
6 | value: [],
7 | options: []
8 | })
9 |
10 | const input = getEditElement(treeselect.parentHtmlContainer)
11 | expect(input.getAttribute('placeholder')).toBe('Search...')
12 | })
13 |
14 | it('should render a Treeselect with the given placeholder', () => {
15 | const placeholder = 'test-placeholder'
16 |
17 | const treeselect = renderTreeselect({
18 | value: [],
19 | options: [],
20 | placeholder
21 | })
22 |
23 | const input = getEditElement(treeselect.parentHtmlContainer)
24 | expect(input.getAttribute('placeholder')).toBe(placeholder)
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/input-props/searchable.test.ts:
--------------------------------------------------------------------------------
1 | import { fireEvent } from '@testing-library/dom'
2 | import {
3 | classes,
4 | defaultOptions,
5 | getEditElement,
6 | getListItems,
7 | getNoResultsElement,
8 | optionNames,
9 | renderTreeselect
10 | } from '../../helpers'
11 |
12 | const { list: listClasses } = classes
13 |
14 | describe('searchable prop', () => {
15 | const awaitInput = async () => new Promise((resolve) => setTimeout(resolve, 400))
16 |
17 | it('should be searchable by default', async () => {
18 | const treeselect = renderTreeselect({
19 | value: [],
20 | options: defaultOptions
21 | })
22 |
23 | const input = getEditElement(treeselect.parentHtmlContainer)
24 | fireEvent.input(input, { target: { value: optionNames.ParisItem } })
25 | await awaitInput()
26 |
27 | const listItems = Array.from(getListItems(treeselect.parentHtmlContainer)).filter(
28 | (item) => !item.classList.contains(listClasses.itemHidden)
29 | )
30 | const visibleItems = [optionNames.FranceGroup, optionNames.ParisItem]
31 |
32 | expect(listItems).toHaveLength(2)
33 | expect(listItems.every((item) => visibleItems.includes(item.textContent!))).toBe(true)
34 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
35 | })
36 |
37 | it('should not be searchable', async () => {
38 | const treeselect = renderTreeselect({
39 | searchable: false,
40 | value: [],
41 | options: defaultOptions
42 | })
43 |
44 | const input = getEditElement(treeselect.parentHtmlContainer)
45 | fireEvent.input(input, { target: { value: optionNames.ParisItem } })
46 | await awaitInput()
47 |
48 | const listItems = Array.from(getListItems(treeselect.parentHtmlContainer)).filter(
49 | (item) => !item.classList.contains(listClasses.itemHidden)
50 | )
51 |
52 | expect(listItems).toHaveLength(0)
53 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
54 | })
55 |
56 | it('should show no results message', async () => {
57 | const treeselect = renderTreeselect({
58 | value: [],
59 | options: defaultOptions
60 | })
61 |
62 | const input = getEditElement(treeselect.parentHtmlContainer)
63 | fireEvent.input(input, { target: { value: 'Not found' } })
64 | await awaitInput()
65 |
66 | const listItems = Array.from(getListItems(treeselect.parentHtmlContainer)).filter(
67 | (item) => !item.classList.contains(listClasses.itemHidden)
68 | )
69 |
70 | const emptyElement = getNoResultsElement(treeselect.parentHtmlContainer)
71 |
72 | expect(listItems).toHaveLength(0)
73 | expect(emptyElement).toBeDefined()
74 | expect(emptyElement.classList.contains(listClasses.itemHidden)).toBe(false)
75 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
76 | })
77 | })
78 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/input-props/showTags.test.ts:
--------------------------------------------------------------------------------
1 | import { renderTreeselect, defaultOptions, optionsValues } from '../../helpers'
2 |
3 | describe('showTags prop', () => {
4 | it('should show tags by default', () => {
5 | const treeselect = renderTreeselect({
6 | value: [optionsValues.EnglandGroup],
7 | options: defaultOptions
8 | })
9 |
10 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
11 | })
12 |
13 | it('should show group name if group is selected and showTags is false', () => {
14 | const treeselect = renderTreeselect({
15 | value: [optionsValues.EnglandGroup],
16 | showTags: false,
17 | options: defaultOptions
18 | })
19 |
20 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
21 | })
22 |
23 | it('should show text with count if multiple values are selected and showTags is false', () => {
24 | const treeselect = renderTreeselect({
25 | value: [optionsValues.EnglandGroup, optionsValues.FranceGroup],
26 | showTags: false,
27 | options: defaultOptions
28 | })
29 |
30 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/input-props/tagsCountText.test.ts:
--------------------------------------------------------------------------------
1 | import { renderTreeselect, getTagsElement, defaultOptions, optionsValues } from '../../helpers'
2 |
3 | describe('tagsCountText prop', () => {
4 | it('should render a Treeselect with the default tagsCountText', () => {
5 | const treeselect = renderTreeselect({
6 | value: [optionsValues.EnglandGroup, optionsValues.FranceGroup],
7 | options: defaultOptions,
8 | showTags: false
9 | })
10 |
11 | const tagsElement = getTagsElement(treeselect.parentHtmlContainer)
12 | expect(tagsElement.outerHTML).toContain('2 elements selected')
13 | })
14 |
15 | it('should render a Treeselect with the given tagsCountText', () => {
16 | const treeselect = renderTreeselect({
17 | value: [optionsValues.EnglandGroup, optionsValues.FranceGroup],
18 | options: defaultOptions,
19 | showTags: false,
20 | tagsCountText: 'test elements'
21 | })
22 |
23 | const tagsElement = getTagsElement(treeselect.parentHtmlContainer)
24 | expect(tagsElement.outerHTML).toContain('2 test elements')
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/input-props/tagsSortFn.test.ts:
--------------------------------------------------------------------------------
1 | import { renderTreeselect, getTagsElements } from '../../helpers'
2 | import { TagsSortItem } from './../../../../src/treeselectTypes'
3 |
4 | const { brighton, chelsea, paris } = {
5 | chelsea: { value: 1, name: 'Chelsea' },
6 | brighton: { value: 2, name: 'Brighton' },
7 | paris: { value: 3, name: 'Paris' }
8 | }
9 |
10 | const mixedOptions = [
11 | { value: paris.value, name: paris.name, children: [] },
12 | { value: brighton.value, name: brighton.name, children: [] },
13 | { value: chelsea.value, name: chelsea.name, children: [] }
14 | ]
15 |
16 | describe('tagsSortFn prop', () => {
17 | it('should use sorting by options list by default', () => {
18 | const treeselect = renderTreeselect({
19 | value: [paris.value, brighton.value, chelsea.value],
20 | options: mixedOptions
21 | })
22 |
23 | const tagsElements = getTagsElements(treeselect.parentHtmlContainer)
24 | const sortedNamesByOptions = mixedOptions.map(({ name }) => name)
25 |
26 | sortedNamesByOptions.forEach((name, index) => {
27 | expect(tagsElements[index].innerHTML).toContain(name)
28 | })
29 | })
30 |
31 | it('should sort tags by name', () => {
32 | const sortNodesByName = (a: TagsSortItem, b: TagsSortItem) => a.name.localeCompare(b.name)
33 |
34 | const treeselect = renderTreeselect({
35 | value: [paris.value, brighton.value, chelsea.value],
36 | tagsSortFn: sortNodesByName,
37 | options: mixedOptions
38 | })
39 |
40 | const tagsElements = getTagsElements(treeselect.parentHtmlContainer)
41 | const sortedNamesByName = mixedOptions.toSorted(sortNodesByName)
42 |
43 | sortedNamesByName.forEach(({ name }, index) => {
44 | expect(tagsElements[index].innerHTML).toContain(name)
45 | })
46 | })
47 |
48 | it('should sort tags by value', () => {
49 | const sortNodesByValue = (a: TagsSortItem, b: TagsSortItem) => {
50 | const aValue = parseFloat(a.value.toString())
51 | const bValue = parseFloat(b.value.toString())
52 |
53 | const isAValueNumber = !isNaN(aValue)
54 | const isBValueNumber = !isNaN(bValue)
55 |
56 | if (isAValueNumber && isBValueNumber) {
57 | return aValue - bValue
58 | }
59 |
60 | if (!isAValueNumber && !isBValueNumber) {
61 | return a.value.toString().localeCompare(b.value.toString())
62 | }
63 |
64 | return isAValueNumber ? -1 : 1
65 | }
66 |
67 | const treeselect = renderTreeselect({
68 | value: [paris.value, brighton.value, chelsea.value],
69 | options: mixedOptions,
70 | tagsSortFn: sortNodesByValue
71 | })
72 |
73 | const tagsElements = getTagsElements(treeselect.parentHtmlContainer)
74 | const sortedNamesByValue = mixedOptions.toSorted(sortNodesByValue)
75 |
76 | sortedNamesByValue.forEach(({ name }, index) => {
77 | expect(tagsElements[index].innerHTML).toContain(name)
78 | })
79 | })
80 |
81 | it('should sort tags by order', () => {
82 | const value: (number | string)[] = [paris.value, chelsea.value, brighton.value]
83 | const sortNodesByOrder = (a: TagsSortItem, b: TagsSortItem) => {
84 | const orderMap = new Map(value.map((id, index) => [id.toString(), index]))
85 | const indexA = orderMap.get(a.value.toString()) ?? Infinity
86 | const indexB = orderMap.get(b.value.toString()) ?? Infinity
87 |
88 | return indexA - indexB
89 | }
90 | const treeselect = renderTreeselect({
91 | value,
92 | tagsSortFn: sortNodesByOrder,
93 | options: mixedOptions
94 | })
95 |
96 | const tagsElements = getTagsElements(treeselect.parentHtmlContainer)
97 | const sortedNamesByOrder = mixedOptions.toSorted(sortNodesByOrder)
98 |
99 | sortedNamesByOrder.forEach(({ name }, index) => {
100 | expect(tagsElements[index].innerHTML).toContain(name)
101 | })
102 | })
103 |
104 | it('should sort tags by order during the update', () => {
105 | let value: (number | string)[] = []
106 | const sortNodesByOrder = (a: TagsSortItem, b: TagsSortItem) => {
107 | const orderMap = new Map(value.map((id, index) => [id.toString(), index]))
108 | const indexA = orderMap.get(a.value.toString()) ?? Infinity
109 | const indexB = orderMap.get(b.value.toString()) ?? Infinity
110 |
111 | return indexA - indexB
112 | }
113 |
114 | const treeselect = renderTreeselect({
115 | value: [],
116 | tagsSortFn: sortNodesByOrder,
117 | options: mixedOptions
118 | })
119 |
120 | value = [paris.value]
121 | treeselect.updateValue(value)
122 | const tagsElements1 = getTagsElements(treeselect.parentHtmlContainer) ?? []
123 | expect(tagsElements1[0].innerHTML).toContain(paris.name)
124 |
125 | value = [paris.value, chelsea.value]
126 | treeselect.updateValue(value)
127 | const tagsElements2 = getTagsElements(treeselect.parentHtmlContainer) ?? []
128 | expect(tagsElements2[0].innerHTML).toContain(paris.name)
129 | expect(tagsElements2[1].innerHTML).toContain(chelsea.name)
130 |
131 | value = [paris.value, chelsea.value, brighton.value]
132 | treeselect.updateValue(value)
133 | const tagsElements3 = getTagsElements(treeselect.parentHtmlContainer) ?? []
134 | expect(tagsElements3[0].innerHTML).toContain(paris.name)
135 | expect(tagsElements3[1].innerHTML).toContain(chelsea.name)
136 | expect(tagsElements3[2].innerHTML).toContain(brighton.name)
137 | })
138 | })
139 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/list-props/__snapshots__/appendToBody.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`appendToBody prop should render the dropdown list in the body 1`] = `
4 |
79 | `;
80 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/list-props/__snapshots__/emptyText.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`emptyText prop should render the custom empty text 1`] = `
4 |
8 |
78 |
82 |
86 |
89 |
116 |
117 |
120 | No data
121 |
122 |
123 |
124 |
125 | `;
126 |
127 | exports[`emptyText prop should render the empty text by default 1`] = `
128 |
132 |
202 |
206 |
210 |
213 |
240 |
241 |
244 | No results found...
245 |
246 |
247 |
248 |
249 | `;
250 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/list-props/alwaysOpen.test.ts:
--------------------------------------------------------------------------------
1 | import { defaultOptions, renderTreeselect, getListElement } from '../../helpers'
2 |
3 | describe('alwaysOpen prop', () => {
4 | it('should be closed by default', () => {
5 | const treeselect = renderTreeselect({
6 | value: [],
7 | options: defaultOptions
8 | })
9 |
10 | const list = getListElement(treeselect.parentHtmlContainer)
11 |
12 | expect(list).toBe(null)
13 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
14 | })
15 |
16 | it('should be opened when alwaysOpen is set', () => {
17 | const treeselect = renderTreeselect({
18 | value: [],
19 | options: defaultOptions,
20 | alwaysOpen: true
21 | })
22 |
23 | const list = getListElement(treeselect.parentHtmlContainer)
24 |
25 | expect(list).not.toBe(null)
26 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/list-props/appendToBody.test.ts:
--------------------------------------------------------------------------------
1 | import { defaultOptions, renderTreeselect, classes } from '../../helpers'
2 |
3 | const { list: listClasses } = classes
4 |
5 | describe('appendToBody prop', () => {
6 | it('should render the dropdown list in the body', () => {
7 | const treeselect = renderTreeselect({
8 | value: [],
9 | options: defaultOptions,
10 | appendToBody: true
11 | })
12 |
13 | treeselect.toggleOpenClose()
14 |
15 | expect(treeselect.parentHtmlContainer.innerHTML).not.toContain(listClasses.base)
16 | expect(document.body.innerHTML).toContain(listClasses.base)
17 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/list-props/direction.test.ts:
--------------------------------------------------------------------------------
1 | import { renderTreeselect, defaultOptions, getListElement } from '../../helpers'
2 |
3 | describe('direction prop', () => {
4 | it('should render the dropdown list with top direction', () => {
5 | const treeselect = renderTreeselect({
6 | value: [],
7 | options: defaultOptions,
8 | direction: 'top'
9 | })
10 |
11 | document.body.style.paddingTop = '300px'
12 |
13 | treeselect.toggleOpenClose()
14 |
15 | const list = getListElement(treeselect.parentHtmlContainer)
16 |
17 | expect(list.getAttribute('direction')).toBe('top')
18 | expect(document.body).toMatchSnapshot()
19 | })
20 |
21 | it('should render the dropdown list with bottom direction', () => {
22 | const treeselect = renderTreeselect({
23 | value: [],
24 | options: defaultOptions,
25 | direction: 'bottom'
26 | })
27 |
28 | document.body.style.paddingBottom = '300px'
29 |
30 | treeselect.toggleOpenClose()
31 |
32 | const list = getListElement(treeselect.parentHtmlContainer)
33 |
34 | expect(list.getAttribute('direction')).toBe('bottom')
35 | expect(document.body).toMatchSnapshot()
36 | })
37 |
38 | it('should render the dropdown list with auto direction', () => {
39 | const treeselect = renderTreeselect({
40 | value: [],
41 | options: defaultOptions,
42 | direction: 'auto'
43 | })
44 |
45 | treeselect.toggleOpenClose()
46 |
47 | const list = getListElement(treeselect.parentHtmlContainer)
48 |
49 | expect(list.getAttribute('direction')).toBe('bottom')
50 | expect(document.body).toMatchSnapshot()
51 | })
52 | })
53 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/list-props/disabledBranchNode.test.ts:
--------------------------------------------------------------------------------
1 | import { fireEvent } from '@testing-library/dom'
2 | import { defaultOptions, renderTreeselect, getListGroupsItems, optionsValues, optionNames } from '../../helpers'
3 |
4 | describe('disabledBranchNode prop', () => {
5 | it('group should be selectable by default', () => {
6 | const treeselect = renderTreeselect({
7 | value: [],
8 | options: defaultOptions,
9 | openLevel: 5
10 | })
11 |
12 | treeselect.toggleOpenClose()
13 |
14 | const [groupItem] = Array.from(getListGroupsItems(treeselect.parentHtmlContainer)).filter(
15 | (item) => item.getAttribute('title') === optionNames.FranceGroup
16 | )
17 |
18 | fireEvent.mouseDown(groupItem)
19 |
20 | const checkboxes = groupItem.querySelectorAll('input') as NodeListOf
21 | const isEveryIsChecked = Array.from(checkboxes).every((checkbox) => checkbox.checked)
22 |
23 | expect(treeselect.value).toEqual([optionsValues.ParisItem, optionsValues.LyonItem])
24 | expect(isEveryIsChecked).toBe(true)
25 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
26 | })
27 |
28 | it('group should not be selectable when disabledBranchNode is true', () => {
29 | const treeselect = renderTreeselect({
30 | value: [],
31 | options: defaultOptions,
32 | openLevel: 5,
33 | disabledBranchNode: true
34 | })
35 |
36 | treeselect.toggleOpenClose()
37 |
38 | const [groupItem] = Array.from(getListGroupsItems(treeselect.parentHtmlContainer)).filter(
39 | (item) => item.getAttribute('title') === optionNames.FranceGroup
40 | )
41 |
42 | fireEvent.mouseDown(groupItem)
43 |
44 | const checkboxes = groupItem.querySelectorAll('input') as NodeListOf
45 | const isEveryIsChecked = Array.from(checkboxes).every((checkbox) => checkbox.checked)
46 |
47 | expect(treeselect.value).toEqual([])
48 | expect(isEveryIsChecked).toBe(false)
49 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
50 | })
51 | })
52 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/list-props/emptyText.test.ts:
--------------------------------------------------------------------------------
1 | import { renderTreeselect, getNoResultsElement, noResultsText } from '../../helpers'
2 |
3 | describe('emptyText prop', () => {
4 | it('should render the empty text by default', () => {
5 | const treeselect = renderTreeselect({
6 | value: [],
7 | options: []
8 | })
9 |
10 | treeselect.toggleOpenClose()
11 |
12 | const emptyItem = getNoResultsElement(treeselect.parentHtmlContainer)
13 |
14 | expect(emptyItem.innerHTML).toContain(noResultsText)
15 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
16 | })
17 |
18 | it('should render the custom empty text', () => {
19 | const newNotResultsText = 'No data'
20 | const treeselect = renderTreeselect({
21 | value: [],
22 | options: [],
23 | emptyText: newNotResultsText
24 | })
25 |
26 | treeselect.toggleOpenClose()
27 |
28 | const emptyItem = getNoResultsElement(treeselect.parentHtmlContainer)
29 |
30 | expect(emptyItem.innerHTML).toContain(newNotResultsText)
31 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/list-props/expandSelected.test.ts:
--------------------------------------------------------------------------------
1 | import { renderTreeselect, defaultOptions, optionsValues, optionNames, getListItems, classes } from '../../helpers'
2 |
3 | const { list: listClasses } = classes
4 |
5 | describe('expandSelected prop', () => {
6 | it('should not expand selected items by default', () => {
7 | const treeselect = renderTreeselect({
8 | value: [optionsValues.BrightonItem, optionsValues.LyonItem],
9 | options: defaultOptions
10 | })
11 |
12 | treeselect.toggleOpenClose()
13 |
14 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
15 | })
16 |
17 | it('should expand selected items when expandSelected is set', () => {
18 | const treeselect = renderTreeselect({
19 | value: [optionsValues.BrightonItem, optionsValues.LyonItem],
20 | options: defaultOptions,
21 | expandSelected: true
22 | })
23 |
24 | treeselect.toggleOpenClose()
25 |
26 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
27 | })
28 |
29 | it('should expand selected item with single select', () => {
30 | const treeselect = renderTreeselect({
31 | value: optionsValues.WestEndItem,
32 | options: defaultOptions,
33 | expandSelected: true,
34 | isSingleSelect: true
35 | })
36 |
37 | treeselect.toggleOpenClose()
38 |
39 | const items = getListItems(treeselect.parentHtmlContainer)
40 | const itemsToShow = [
41 | optionNames.EnglandGroup,
42 | optionNames.LondonGroup,
43 | optionNames.ChelseaItem,
44 | optionNames.WestEndItem,
45 | optionNames.BrightonItem,
46 | optionNames.FranceGroup
47 | ]
48 | const itemsToHide = [optionNames.LyonItem, optionNames.ParisItem]
49 |
50 | items.forEach((item) => {
51 | if (itemsToShow.includes(item.textContent!)) {
52 | expect(item.classList.contains(listClasses.itemHidden)).toBe(false)
53 | } else if (itemsToHide.includes(item.textContent!)) {
54 | expect(item.classList.contains(listClasses.itemHidden)).toBe(true)
55 | }
56 | })
57 |
58 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
59 | })
60 |
61 | it('should expand selected item with isIndependentNodes prop', () => {
62 | const treeselect = renderTreeselect({
63 | value: [optionsValues.BrightonItem],
64 | options: defaultOptions,
65 | expandSelected: true,
66 | isIndependentNodes: true
67 | })
68 |
69 | treeselect.toggleOpenClose()
70 |
71 | const items = getListItems(treeselect.parentHtmlContainer)
72 | const itemsToShow = [
73 | optionNames.EnglandGroup,
74 | optionNames.LondonGroup,
75 | optionNames.BrightonItem,
76 | optionNames.FranceGroup
77 | ]
78 | const itemsToHide = [optionNames.ChelseaItem, optionNames.WestEndItem, optionNames.LyonItem, optionNames.ParisItem]
79 |
80 | items.forEach((item) => {
81 | if (itemsToShow.includes(item.textContent!)) {
82 | expect(item.classList.contains(listClasses.itemHidden)).toBe(false)
83 | } else if (itemsToHide.includes(item.textContent!)) {
84 | expect(item.classList.contains(listClasses.itemHidden)).toBe(true)
85 | }
86 | })
87 |
88 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
89 | })
90 | })
91 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/list-props/listClassName.test.ts:
--------------------------------------------------------------------------------
1 | import { defaultOptions, renderTreeselect, getListElement } from '../../helpers'
2 |
3 | describe('listClassName prop', () => {
4 | it('should add custom class to the dropdown list', () => {
5 | const listClassName = 'custom-list-class'
6 | const treeselect = renderTreeselect({
7 | value: [],
8 | options: defaultOptions,
9 | listClassName
10 | })
11 |
12 | treeselect.toggleOpenClose()
13 |
14 | const list = getListElement(treeselect.parentHtmlContainer)
15 |
16 | expect(list.classList.contains(listClassName)).toBe(true)
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/list-props/listSlotHtmlComponent.test.ts:
--------------------------------------------------------------------------------
1 | import { defaultOptions, renderTreeselect, getListSlotElement } from '../../helpers'
2 |
3 | describe('listSlotHtmlComponent prop', () => {
4 | it('should render the dropdown list with custom slot content', () => {
5 | const slotElement = document.createElement('div')
6 | slotElement.classList.add('custom-list-slot')
7 | slotElement.innerHTML = 'Custom List Slot'
8 |
9 | const treeselect = renderTreeselect({
10 | value: [],
11 | options: defaultOptions,
12 | listSlotHtmlComponent: slotElement
13 | })
14 |
15 | treeselect.toggleOpenClose()
16 |
17 | const listSlotElement = getListSlotElement(treeselect.parentHtmlContainer)
18 |
19 | expect(listSlotElement).toBeTruthy()
20 | expect(listSlotElement.querySelector('.custom-list-slot')).toBeTruthy()
21 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
22 | })
23 | })
24 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/list-props/openLevel.test.ts:
--------------------------------------------------------------------------------
1 | import { defaultOptions, renderTreeselect, getListItems, classes } from '../../helpers'
2 |
3 | const { list: listClasses } = classes
4 |
5 | describe('openLevel prop', () => {
6 | it('groups should be closed by default', () => {
7 | const treeselect = renderTreeselect({
8 | value: [],
9 | options: defaultOptions
10 | })
11 |
12 | treeselect.toggleOpenClose()
13 |
14 | const topGroups = Array.from(getListItems(treeselect.parentHtmlContainer)).filter(
15 | (item) => !item.classList.contains(listClasses.itemHidden)
16 | )
17 |
18 | expect(topGroups).toHaveLength(2)
19 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
20 | })
21 |
22 | it('groups should be opened when openLevel is set', () => {
23 | const treeselect = renderTreeselect({
24 | value: [],
25 | options: defaultOptions,
26 | openLevel: 10
27 | })
28 |
29 | treeselect.toggleOpenClose()
30 |
31 | const hiddenItems = Array.from(getListItems(treeselect.parentHtmlContainer)).filter((item) =>
32 | item.classList.contains(listClasses.itemHidden)
33 | )
34 |
35 | expect(hiddenItems).toHaveLength(0)
36 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
37 | })
38 | })
39 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/list-props/showCount.test.ts:
--------------------------------------------------------------------------------
1 | import { defaultOptions, renderTreeselect, getListGroupsItems } from '../../helpers'
2 |
3 | describe('showCount prop', () => {
4 | it('should not show count by default', () => {
5 | const treeselect = renderTreeselect({
6 | value: [],
7 | options: defaultOptions
8 | })
9 |
10 | treeselect.toggleOpenClose()
11 |
12 | const groupItems = Array.from(getListGroupsItems(treeselect.parentHtmlContainer))
13 | const isEveryItemNotContainCount = groupItems.every((groupItem) => !groupItem.innerHTML.includes('(2)'))
14 |
15 | expect(isEveryItemNotContainCount).toBe(true)
16 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
17 | })
18 |
19 | it('should show count when showCount is true', () => {
20 | const treeselect = renderTreeselect({
21 | value: [],
22 | options: defaultOptions,
23 | showCount: true
24 | })
25 |
26 | treeselect.toggleOpenClose()
27 |
28 | const groupItems = Array.from(getListGroupsItems(treeselect.parentHtmlContainer))
29 | const isEveryItemContainCount = groupItems.every((groupItem) => groupItem.innerHTML.includes('(2)'))
30 |
31 | expect(isEveryItemContainCount).toBe(true)
32 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
33 | })
34 | })
35 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/list-props/stasticList.test.ts:
--------------------------------------------------------------------------------
1 | import { defaultOptions, renderTreeselect, getListElement, classes, getInputElement } from '../../helpers'
2 |
3 | describe('staticList prop', () => {
4 | it('should not be static by default', () => {
5 | const treeselect = renderTreeselect({
6 | value: [],
7 | options: defaultOptions
8 | })
9 |
10 | const list = getListElement(treeselect.parentHtmlContainer)
11 |
12 | expect(list).toBeFalsy()
13 | })
14 |
15 | it('should be static when staticList is set', () => {
16 | const treeselect = renderTreeselect({
17 | value: [],
18 | options: defaultOptions,
19 | staticList: true
20 | })
21 |
22 | treeselect.toggleOpenClose()
23 |
24 | const list = getListElement(treeselect.parentHtmlContainer)
25 |
26 | expect(list).toBeTruthy()
27 | expect(list.classList).toContain(classes.list.static)
28 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
29 | })
30 |
31 | it('should have always bottom direction when staticList is set', () => {
32 | const treeselect = renderTreeselect({
33 | value: [],
34 | options: defaultOptions,
35 | direction: 'top',
36 | alwaysOpen: true,
37 | staticList: true
38 | })
39 |
40 | const list = getListElement(treeselect.parentHtmlContainer)
41 |
42 | expect(list.getAttribute('direction')).toBe('bottom')
43 | expect(list.classList).toContain(classes.list.bottom)
44 |
45 | const input = getInputElement(treeselect.parentHtmlContainer)
46 | expect(input.classList).toContain(classes.input.bottom)
47 | })
48 |
49 | it('should not have static position with appendToBody', () => {
50 | const treeselect = renderTreeselect({
51 | value: [],
52 | options: defaultOptions,
53 | staticList: true,
54 | appendToBody: true
55 | })
56 |
57 | treeselect.toggleOpenClose()
58 | const list = getListElement(treeselect.parentHtmlContainer)
59 | expect(list).toBeFalsy()
60 | })
61 |
62 | it('should correctly render static list with alwaysOpen', () => {
63 | const treeselect = renderTreeselect({
64 | value: [],
65 | options: defaultOptions,
66 | alwaysOpen: true,
67 | staticList: true
68 | })
69 |
70 | const list = getListElement(treeselect.parentHtmlContainer)
71 |
72 | expect(list).toBeTruthy()
73 | expect(list.classList).toContain(classes.list.static)
74 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
75 | })
76 | })
77 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/methods/__snapshots__/focus.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`focus method should focus on the input 1`] = `
4 |
79 | `;
80 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/methods/destroy.test.ts:
--------------------------------------------------------------------------------
1 | import { renderTreeselect, getEditElement, getListElement, defaultOptions } from '../../helpers'
2 |
3 | describe('destroy method', () => {
4 | it('should remove treeselect form the DOM', () => {
5 | const treeselect = renderTreeselect({
6 | value: [],
7 | options: defaultOptions
8 | })
9 |
10 | treeselect.destroy()
11 |
12 | const input = getEditElement(document.body)
13 | const list = getListElement(document.body)
14 |
15 | expect(input).toBeFalsy()
16 | expect(list).toBeFalsy()
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/methods/emits-callbacks.test.ts:
--------------------------------------------------------------------------------
1 | import { fireEvent } from '@testing-library/dom'
2 | import { renderTreeselect, getTagsElements, getArrowElement, getEditElement, getGroupArrowIcons } from '../../helpers'
3 |
4 | describe('emits-callbacks method', () => {
5 | it('should call input callback if change value', () => {
6 | const inputFn = jest.fn()
7 |
8 | const treeselect = renderTreeselect({
9 | value: [1, 2, 3],
10 | options: [
11 | { value: 1, name: '1', children: [] },
12 | { value: 2, name: '2', children: [] },
13 | { value: 3, name: '3', children: [] }
14 | ],
15 | inputCallback: inputFn
16 | })
17 |
18 | treeselect.srcElement?.addEventListener('input', (e: any) => {
19 | inputFn(e.detail)
20 | })
21 |
22 | const tags = getTagsElements(treeselect.parentHtmlContainer)
23 | fireEvent.mouseDown(tags[0])
24 |
25 | expect(inputFn).toHaveBeenNthCalledWith(2, [2, 3])
26 | })
27 |
28 | it('should call open callback if open', () => {
29 | const openFn = jest.fn()
30 |
31 | const treeselect = renderTreeselect({
32 | value: [1],
33 | options: [{ value: 1, name: '1', children: [] }],
34 | openCallback: openFn
35 | })
36 |
37 | treeselect.srcElement?.addEventListener('open', (e: any) => {
38 | openFn(e.detail)
39 | })
40 |
41 | const arrow = getArrowElement(treeselect.parentHtmlContainer)
42 | fireEvent.mouseDown(arrow)
43 |
44 | expect(openFn).toHaveBeenNthCalledWith(2, [1])
45 | })
46 |
47 | it('should call close callback if close', () => {
48 | const closeFn = jest.fn()
49 |
50 | const treeselect = renderTreeselect({
51 | value: [1],
52 | options: [{ value: 1, name: '1', children: [] }],
53 | closeCallback: closeFn
54 | })
55 |
56 | treeselect.srcElement?.addEventListener('close', (e: any) => {
57 | closeFn(e.detail)
58 | })
59 |
60 | const arrow = getArrowElement(treeselect.parentHtmlContainer)
61 | fireEvent.mouseDown(arrow)
62 | fireEvent.mouseDown(arrow)
63 |
64 | expect(closeFn).toHaveBeenNthCalledWith(2, [1])
65 | })
66 |
67 | it('should call search callback if search', () => {
68 | const searchFn = jest.fn()
69 |
70 | const treeselect = renderTreeselect({
71 | value: [1],
72 | options: [{ value: 1, name: '1', children: [] }],
73 | searchCallback: searchFn
74 | })
75 |
76 | treeselect.srcElement?.addEventListener('search', (e: any) => {
77 | searchFn(e.detail)
78 | })
79 |
80 | const input = getEditElement(treeselect.parentHtmlContainer)
81 | fireEvent.input(input, { target: { value: '123' } })
82 |
83 | expect(searchFn).toHaveBeenNthCalledWith(2, '123')
84 | })
85 |
86 | it('should call name-change callback if change name', () => {
87 | const nameChangeFn = jest.fn()
88 |
89 | const treeselect = renderTreeselect({
90 | value: [1, 2, 3],
91 | options: [
92 | { value: 1, name: '1', children: [] },
93 | { value: 2, name: '2', children: [] },
94 | { value: 3, name: '3', children: [] }
95 | ],
96 | nameChangeCallback: nameChangeFn
97 | })
98 |
99 | treeselect.srcElement?.addEventListener('name-change', (e: any) => {
100 | nameChangeFn(e.detail)
101 | })
102 |
103 | const tags = getTagsElements(treeselect.parentHtmlContainer)
104 | fireEvent.mouseDown(tags[0])
105 |
106 | expect(nameChangeFn).toHaveBeenNthCalledWith(2, '2, 3')
107 | })
108 |
109 | it('should call open-close-group callback if open-close group', () => {
110 | const openCloseGroupFn = jest.fn()
111 | const openCloseGroupListener = jest.fn()
112 |
113 | const treeselect = renderTreeselect({
114 | value: [1],
115 | openLevel: 0,
116 | options: [
117 | {
118 | value: 1,
119 | name: '1',
120 | children: [
121 | { value: 2, name: '2', children: [] },
122 | { value: 3, name: '3', children: [] }
123 | ]
124 | }
125 | ],
126 | openCloseGroupCallback: openCloseGroupFn
127 | })
128 |
129 | treeselect.srcElement?.addEventListener('open-close-group', (e: any) => {
130 | openCloseGroupListener(e.detail)
131 | })
132 |
133 | const arrow = getArrowElement(treeselect.parentHtmlContainer)
134 | fireEvent.mouseDown(arrow)
135 | const firstGroupArrow = getGroupArrowIcons(treeselect.parentHtmlContainer)[0]
136 | fireEvent.mouseDown(firstGroupArrow)
137 |
138 | expect(openCloseGroupListener).toHaveBeenNthCalledWith(1, { groupId: 1, isClosed: false })
139 | expect(openCloseGroupFn).toHaveBeenNthCalledWith(1, 1, false)
140 | })
141 | })
142 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/methods/focus.test.ts:
--------------------------------------------------------------------------------
1 | import { renderTreeselect, getEditElement } from '../../helpers'
2 |
3 | describe('focus method', () => {
4 | it('should focus on the input', async () => {
5 | const treeselect = renderTreeselect({
6 | value: [],
7 | options: []
8 | })
9 |
10 | treeselect.focus()
11 | await new Promise((resolve) => setTimeout(resolve, 0))
12 | const input = getEditElement(treeselect.parentHtmlContainer)
13 |
14 | expect(document.activeElement).toBe(input)
15 | expect(treeselect.parentHtmlContainer).toMatchSnapshot()
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/methods/mount.test.ts:
--------------------------------------------------------------------------------
1 | import { renderTreeselect, getTagsElements } from '../../helpers'
2 |
3 | const values = { Option1: 1, Option2: 2, Option3: 3 }
4 | const options = [
5 | { value: values.Option1, name: 'Option 1', children: [] },
6 | { value: values.Option2, name: 'Option 2', children: [] },
7 | { value: values.Option3, name: 'Option 3', children: [] }
8 | ]
9 |
10 | describe('mount method', () => {
11 | it('should update params with mount method', () => {
12 | const newValue = [values.Option1, values.Option2]
13 | const ariaLabel = 'test-aria-label'
14 | const treeselect = renderTreeselect({
15 | value: [],
16 | options: options
17 | })
18 |
19 | treeselect.value = newValue
20 | treeselect.ariaLabel = ariaLabel
21 | treeselect.mount()
22 |
23 | const tags = getTagsElements(treeselect.parentHtmlContainer)
24 |
25 | expect(tags.length).toBe(newValue.length)
26 | expect(treeselect.value).toEqual(newValue)
27 | expect(treeselect.parentHtmlContainer.innerHTML).toContain(ariaLabel)
28 | expect(treeselect.ariaLabel).toBe(ariaLabel)
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/methods/toggleOpenClose.test.ts:
--------------------------------------------------------------------------------
1 | import { renderTreeselect, getListElement } from '../../helpers'
2 |
3 | describe('toggleOpenClose method', () => {
4 | it('should open and close the dropdown', () => {
5 | const treeselect = renderTreeselect({
6 | value: [],
7 | options: []
8 | })
9 |
10 | treeselect.toggleOpenClose()
11 | const listElementOpened = getListElement(treeselect.parentHtmlContainer)
12 |
13 | expect(treeselect.isListOpened).toBe(true)
14 | expect(listElementOpened).toBeTruthy()
15 |
16 | treeselect.toggleOpenClose()
17 | const listElementClosed = getListElement(treeselect.parentHtmlContainer)
18 |
19 | expect(treeselect.isListOpened).toBe(false)
20 | expect(listElementClosed).toBeFalsy()
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/__tests__/jest/tests/methods/updateValue.test.ts:
--------------------------------------------------------------------------------
1 | import { renderTreeselect, getTagsElements } from '../../helpers'
2 |
3 | const values = { Option1: 1, Option2: 2, Option3: 3 }
4 | const options = [
5 | { value: values.Option1, name: 'Option 1', children: [] },
6 | { value: values.Option2, name: 'Option 2', children: [] },
7 | { value: values.Option3, name: 'Option 3', children: [] }
8 | ]
9 |
10 | describe('updateValue method', () => {
11 | it('should update valid value', () => {
12 | const newValues = [values.Option1, values.Option2]
13 | const treeselect = renderTreeselect({
14 | value: [],
15 | options
16 | })
17 |
18 | treeselect.updateValue(newValues)
19 |
20 | const tags = getTagsElements(treeselect.parentHtmlContainer)
21 |
22 | expect(tags.length).toBe(newValues.length)
23 | expect(treeselect.value).toEqual(newValues)
24 | })
25 |
26 | it("shouldn't update invalid value", () => {
27 | const treeselect = renderTreeselect({
28 | value: [],
29 | options
30 | })
31 |
32 | treeselect.updateValue([values.Option1, 4])
33 |
34 | const tags = getTagsElements(treeselect.parentHtmlContainer)
35 |
36 | expect(tags.length).toBe(1)
37 | expect(treeselect.value).toEqual([values.Option1])
38 | })
39 | })
40 |
--------------------------------------------------------------------------------
/__tests__/testHelpers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './options'
2 | export * from './selectors'
3 |
--------------------------------------------------------------------------------
/__tests__/testHelpers/options.ts:
--------------------------------------------------------------------------------
1 | export const optionNames = {
2 | EnglandGroup: 'England',
3 | LondonGroup: 'London',
4 | ChelseaItem: 'Chelsea',
5 | WestEndItem: 'West End',
6 | BrightonItem: 'Brighton',
7 | FranceGroup: 'France',
8 | ParisItem: 'Paris',
9 | LyonItem: 'Lyon'
10 | }
11 |
12 | export const optionsValues = {
13 | EnglandGroup: 1,
14 | LondonGroup: 2,
15 | ChelseaItem: 3,
16 | WestEndItem: 4,
17 | BrightonItem: 5,
18 | FranceGroup: 6,
19 | ParisItem: 7,
20 | LyonItem: 8
21 | }
22 |
23 | export const noResultsText = 'No results found...'
24 |
25 | export const defaultOptions = [
26 | {
27 | name: optionNames.EnglandGroup,
28 | value: optionsValues.EnglandGroup,
29 | children: [
30 | {
31 | name: optionNames.LondonGroup,
32 | value: optionsValues.LondonGroup,
33 | children: [
34 | {
35 | name: optionNames.ChelseaItem,
36 | value: optionsValues.ChelseaItem,
37 | children: []
38 | },
39 | {
40 | name: optionNames.WestEndItem,
41 | value: optionsValues.WestEndItem,
42 | children: []
43 | }
44 | ]
45 | },
46 | {
47 | name: optionNames.BrightonItem,
48 | value: optionsValues.BrightonItem,
49 | children: []
50 | }
51 | ]
52 | },
53 | {
54 | name: optionNames.FranceGroup,
55 | value: optionsValues.FranceGroup,
56 | children: [
57 | {
58 | name: optionNames.ParisItem,
59 | value: optionsValues.ParisItem,
60 | children: []
61 | },
62 | {
63 | name: optionNames.LyonItem,
64 | value: optionsValues.LyonItem,
65 | children: []
66 | }
67 | ]
68 | }
69 | ]
70 |
71 | export const optionsWithDisabled = [
72 | {
73 | name: optionNames.EnglandGroup,
74 | value: optionsValues.EnglandGroup,
75 | children: [
76 | {
77 | name: optionNames.LondonGroup,
78 | value: optionsValues.LondonGroup,
79 | children: [
80 | {
81 | name: optionNames.ChelseaItem,
82 | value: optionsValues.ChelseaItem,
83 | children: [],
84 | disabled: true
85 | },
86 | {
87 | name: optionNames.WestEndItem,
88 | value: optionsValues.WestEndItem,
89 | children: []
90 | }
91 | ]
92 | },
93 | {
94 | name: optionNames.BrightonItem,
95 | value: optionsValues.BrightonItem,
96 | children: []
97 | }
98 | ]
99 | },
100 | {
101 | name: optionNames.FranceGroup,
102 | value: optionsValues.FranceGroup,
103 | disabled: true,
104 | children: [
105 | {
106 | name: optionNames.ParisItem,
107 | value: optionsValues.ParisItem,
108 | children: []
109 | },
110 | {
111 | name: optionNames.LyonItem,
112 | value: optionsValues.LyonItem,
113 | children: []
114 | }
115 | ]
116 | }
117 | ]
118 |
119 | export const largeOptionsList = Array.from({ length: 30 }, (_, index) => ({
120 | name: `Option ${index}`,
121 | value: index,
122 | children: []
123 | }))
124 |
125 | export const largeNestedOptionsList = Array.from({ length: 1000 }, (_, index) => ({
126 | name: `Option ${index}`,
127 | value: index,
128 | children: Array.from({ length: 3 }, (_, subIndex) => ({
129 | name: `SubOption ${index}-${subIndex}`,
130 | value: `${index}-${subIndex}`,
131 | children: Array.from({ length: 3 }, (_, subSubIndex) => ({
132 | name: `SubSubOption ${index}-${subIndex}-${subSubIndex}`,
133 | value: `${index}-${subIndex}-${subSubIndex}`,
134 | children: []
135 | }))
136 | }))
137 | }))
138 |
--------------------------------------------------------------------------------
/__tests__/testHelpers/selectors.ts:
--------------------------------------------------------------------------------
1 | type Classes = {
2 | parent: string
3 | input: {
4 | arrow: string
5 | base: string
6 | bottom: string
7 | clear: string
8 | edit: string
9 | focused: string
10 | opened: string
11 | tags: string
12 | tagsElement: string
13 | top: string
14 | }
15 | list: {
16 | base: string
17 | bottom: string
18 | bottomToBody: string
19 | empty: string
20 | item: string
21 | itemArrow: string
22 | itemChecked: string
23 | itemDisabled: string
24 | itemFocused: string
25 | itemGroup: string
26 | itemHidden: string
27 | itemPartialChecked: string
28 | itemScrollNotVisible: string
29 | itemNonSelectableGroup: string
30 | slot: string
31 | top: string
32 | topToBody: string
33 | static: string
34 | }
35 | }
36 |
37 | export const classes: Classes = {
38 | parent: 'treeselect',
39 | input: {
40 | arrow: 'treeselect-input__arrow',
41 | base: 'treeselect-input',
42 | bottom: 'treeselect-input--bottom',
43 | clear: 'treeselect-input__clear',
44 | edit: 'treeselect-input__edit',
45 | focused: 'treeselect-input--focused',
46 | opened: 'treeselect-input--opened',
47 | tags: 'treeselect-input__tags',
48 | tagsElement: 'treeselect-input__tags-element',
49 | top: 'treeselect-input--top'
50 | },
51 | list: {
52 | base: 'treeselect-list',
53 | bottom: 'treeselect-list--bottom',
54 | bottomToBody: 'treeselect-list--bottom-to-body',
55 | empty: 'treeselect-list__empty',
56 | item: 'treeselect-list__item',
57 | itemArrow: 'treeselect-list__item-icon',
58 | itemChecked: 'treeselect-list__item--checked',
59 | itemDisabled: 'treeselect-list__item--disabled',
60 | itemFocused: 'treeselect-list__item--focused',
61 | itemGroup: 'treeselect-list__item--group',
62 | itemHidden: 'treeselect-list__item--hidden',
63 | itemPartialChecked: 'treeselect-list__item--partial-checked',
64 | itemScrollNotVisible: 'treeselect-list__item--scroll-not-visible',
65 | itemNonSelectableGroup: 'treeselect-list__item--non-selectable-group',
66 | slot: 'treeselect-list__slot',
67 | top: 'treeselect-list--top',
68 | topToBody: 'treeselect-list--top-to-body',
69 | static: 'treeselect-list--static'
70 | }
71 | }
72 |
73 | export const classesSelectors: Classes = {
74 | parent: `.${classes.parent}`,
75 | input: {
76 | arrow: `.${classes.input.arrow}`,
77 | base: `.${classes.input.base}`,
78 | bottom: `.${classes.input.bottom}`,
79 | clear: `.${classes.input.clear}`,
80 | edit: `.${classes.input.edit}`,
81 | focused: `.${classes.input.focused}`,
82 | opened: `.${classes.input.opened}`,
83 | tags: `.${classes.input.tags}`,
84 | tagsElement: `.${classes.input.tagsElement}`,
85 | top: `.${classes.input.top}`
86 | },
87 | list: {
88 | base: `.${classes.list.base}`,
89 | bottom: `.${classes.list.bottom}`,
90 | bottomToBody: `.${classes.list.bottomToBody}`,
91 | empty: `.${classes.list.empty}`,
92 | item: `.${classes.list.item}`,
93 | itemArrow: `.${classes.list.itemArrow}`,
94 | itemChecked: `.${classes.list.itemChecked}`,
95 | itemDisabled: `.${classes.list.itemDisabled}`,
96 | itemFocused: `.${classes.list.itemFocused}`,
97 | itemGroup: `.${classes.list.itemGroup}`,
98 | itemHidden: `.${classes.list.itemHidden}`,
99 | itemPartialChecked: `.${classes.list.itemPartialChecked}`,
100 | itemScrollNotVisible: `.${classes.list.itemScrollNotVisible}`,
101 | itemNonSelectableGroup: `.${classes.list.itemNonSelectableGroup}`,
102 | slot: `.${classes.list.slot}`,
103 | top: `.${classes.list.top}`,
104 | topToBody: `.${classes.list.topToBody}`,
105 | static: `.${classes.list.static}`
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/app/app.css:
--------------------------------------------------------------------------------
1 | .treeselect-demo-slot__slot {
2 | height: 30px;
3 | display: flex;
4 | align-items: center;
5 | justify-content: center;
6 | }
7 |
8 | a.treeselect-demo-slot__slot {
9 | color: #101010;
10 | }
11 |
12 | body {
13 | font-family: 'Open Sans', sans-serif;
14 | background-color: #fbfbfb;
15 | margin: 0;
16 | }
17 |
18 | .loaded .root {
19 | opacity: 1;
20 | }
21 |
22 | .root {
23 | opacity: 0;
24 | transition: opacity 0.3s;
25 | padding: 40px 20px;
26 | }
27 |
28 | .section {
29 | min-height: 445px;
30 | }
31 |
32 | .section__separator {
33 | margin: 30px 0;
34 | height: 1px;
35 | background-color: #e5e5e5;
36 | }
37 |
38 | .root__title {
39 | text-align: center;
40 | margin-top: 0;
41 | }
42 |
43 | .section__title {
44 | text-align: center;
45 | }
46 |
47 | .section__data {
48 | display: flex;
49 | align-items: flex-start;
50 | justify-content: center;
51 | gap: 15px;
52 | flex-wrap: wrap;
53 | }
54 |
55 | .section__select {
56 | width: 50%;
57 | max-width: 400px;
58 | }
59 |
60 | .section__props {
61 | padding-left: 10px;
62 | width: 50%;
63 | max-width: 400px;
64 | border: 1px solid #e5e5e5;
65 | border-radius: 10px 0 0 10px;
66 | margin-top: 0;
67 | font-size: 12px;
68 | white-space: pre-wrap;
69 | max-height: 400px;
70 | overflow: auto;
71 | margin: 0;
72 | background-color: white;
73 | }
74 |
75 | @media screen and (max-width: 850px) {
76 | .section__data {
77 | flex-direction: column;
78 | align-items: center;
79 | }
80 |
81 | .section__select {
82 | width: 100%;
83 | }
84 |
85 | .section__props {
86 | width: 100%;
87 | max-width: 100%;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/app/app.js:
--------------------------------------------------------------------------------
1 | import '../src/treeselectjs.css'
2 | import Treeselect from '../src/treeselectjs'
3 |
4 | import { runDefaultExample } from './examples/default.js'
5 | import { runSlotExample } from './examples/slot.js'
6 | import { runDisabledExample } from './examples/disabled.js'
7 | import { runSingleSelectExample } from './examples/singleSelect.js'
8 | import { runIndependentNodesExample } from './examples/independentNodes.js'
9 | import { runIconsExample } from './examples/icons.js'
10 |
11 | const runApp = (Treeselect) => {
12 | runDefaultExample(Treeselect)
13 | runSlotExample(Treeselect)
14 | runDisabledExample(Treeselect)
15 | runSingleSelectExample(Treeselect)
16 | runIndependentNodesExample(Treeselect)
17 | runIconsExample(Treeselect)
18 |
19 | document.body.classList.add('loaded')
20 | }
21 |
22 | runApp(Treeselect)
23 |
--------------------------------------------------------------------------------
/app/examples/default.js:
--------------------------------------------------------------------------------
1 | import { renderExampleSection } from '../render/renderExampleSection.js'
2 |
3 | const options = [
4 | {
5 | name: 'England',
6 | value: 1,
7 | children: [
8 | {
9 | name: 'London',
10 | value: 2,
11 | children: [
12 | {
13 | name: 'Chelsea',
14 | value: 3,
15 | children: []
16 | },
17 | {
18 | name: 'West End',
19 | value: 4,
20 | children: []
21 | }
22 | ]
23 | },
24 | {
25 | name: 'Brighton',
26 | value: 5,
27 | children: []
28 | }
29 | ]
30 | },
31 | {
32 | name: 'France',
33 | value: 6,
34 | children: [
35 | {
36 | name: 'Paris',
37 | value: 7,
38 | children: []
39 | },
40 | {
41 | name: 'Lyon',
42 | value: 8,
43 | children: []
44 | }
45 | ]
46 | }
47 | ]
48 |
49 | const value = [4, 7, 8]
50 |
51 | const treeselectId = 'treeselect-demo-default'
52 |
53 | export const runDefaultExample = (Treeselect) => {
54 | renderExampleSection({ sectionId: 'default-section', value, options, treeselectId })
55 |
56 | const domElement = document.getElementById(treeselectId)
57 | const treeselect = new Treeselect({
58 | parentHtmlContainer: domElement,
59 | value,
60 | options
61 | })
62 |
63 | treeselect.srcElement.addEventListener('input', (e) => {
64 | console.log('default: Selected value ', e.detail)
65 | })
66 | }
67 |
--------------------------------------------------------------------------------
/app/examples/disabled.js:
--------------------------------------------------------------------------------
1 | import { renderExampleSection } from '../render/renderExampleSection.js'
2 |
3 | const options = [
4 | {
5 | name: 'England',
6 | value: 1,
7 | children: [
8 | {
9 | name: 'London',
10 | value: 2,
11 | children: [
12 | {
13 | name: 'Chelsea',
14 | value: 3,
15 | children: [],
16 | disabled: true
17 | },
18 | {
19 | name: 'West End',
20 | value: 4,
21 | children: []
22 | }
23 | ]
24 | },
25 | {
26 | name: 'Brighton',
27 | value: 5,
28 | children: []
29 | }
30 | ]
31 | },
32 | {
33 | name: 'France',
34 | value: 6,
35 | disabled: true,
36 | children: [
37 | {
38 | name: 'Paris',
39 | value: 7,
40 | children: []
41 | },
42 | {
43 | name: 'Lyon',
44 | value: 8,
45 | children: []
46 | }
47 | ]
48 | }
49 | ]
50 |
51 | const value = []
52 |
53 | const treeselectId = 'treeselect-demo-disabled'
54 |
55 | export const runDisabledExample = (Treeselect) => {
56 | renderExampleSection({ sectionId: 'disabled-section', options, value, treeselectId })
57 |
58 | const domElement = document.getElementById(treeselectId)
59 | const treeselect = new Treeselect({
60 | parentHtmlContainer: domElement,
61 | value,
62 | options
63 | })
64 |
65 | treeselect.srcElement.addEventListener('input', (e) => {
66 | console.log('disabled: Selected value ', e.detail)
67 | })
68 | }
69 |
--------------------------------------------------------------------------------
/app/examples/independentNodes.js:
--------------------------------------------------------------------------------
1 | import { renderExampleSection } from '../render/renderExampleSection.js'
2 |
3 | const options = [
4 | {
5 | name: 'England',
6 | value: 1,
7 | children: [
8 | {
9 | name: 'London',
10 | value: 2,
11 | children: [
12 | {
13 | name: 'Chelsea',
14 | value: 3,
15 | children: []
16 | },
17 | {
18 | name: 'West End',
19 | value: 4,
20 | children: []
21 | }
22 | ]
23 | },
24 | {
25 | name: 'Brighton',
26 | value: 5,
27 | children: []
28 | }
29 | ]
30 | },
31 | {
32 | name: 'France',
33 | value: 6,
34 | children: [
35 | {
36 | name: 'Paris',
37 | value: 7,
38 | children: []
39 | },
40 | {
41 | name: 'Lyon',
42 | value: 8,
43 | children: []
44 | }
45 | ]
46 | }
47 | ]
48 |
49 | const value = [1, 4, 7, 8]
50 |
51 | const treeselectId = 'treeselect-demo-independent-nodes'
52 |
53 | export const runIndependentNodesExample = (Treeselect) => {
54 | renderExampleSection({ sectionId: 'independent-nodes-section', options, value, treeselectId })
55 |
56 | const domElement = document.getElementById(treeselectId)
57 | const treeselect = new Treeselect({
58 | parentHtmlContainer: domElement,
59 | value,
60 | options,
61 | isIndependentNodes: true
62 | })
63 |
64 | treeselect.srcElement.addEventListener('input', (e) => {
65 | console.log('independentNodes: Selected value ', e.detail)
66 | })
67 | }
68 |
--------------------------------------------------------------------------------
/app/examples/singleSelect.js:
--------------------------------------------------------------------------------
1 | import { renderExampleSection } from '../render/renderExampleSection.js'
2 |
3 | const options = [
4 | {
5 | name: 'England',
6 | value: 1,
7 | children: [
8 | {
9 | name: 'London',
10 | value: 2,
11 | children: [
12 | {
13 | name: 'Chelsea',
14 | value: 3,
15 | children: []
16 | },
17 | {
18 | name: 'West End',
19 | value: 4,
20 | children: []
21 | }
22 | ]
23 | },
24 | {
25 | name: 'Brighton',
26 | value: 5,
27 | children: []
28 | }
29 | ]
30 | },
31 | {
32 | name: 'France',
33 | value: 6,
34 | children: [
35 | {
36 | name: 'Paris',
37 | value: 7,
38 | children: []
39 | },
40 | {
41 | name: 'Lyon',
42 | value: 8,
43 | children: []
44 | }
45 | ]
46 | }
47 | ]
48 |
49 | const value = 4
50 |
51 | const treeselectId = 'treeselect-demo-single-select'
52 |
53 | export const runSingleSelectExample = (Treeselect) => {
54 | renderExampleSection({ sectionId: 'single-select-section', options, value, treeselectId })
55 |
56 | const domElement = document.getElementById(treeselectId)
57 | const treeselect = new Treeselect({
58 | parentHtmlContainer: domElement,
59 | value,
60 | options,
61 | isSingleSelect: true,
62 | showTags: false
63 | })
64 |
65 | treeselect.srcElement.addEventListener('input', (e) => {
66 | console.log('singleSelect: Selected value ', e.detail)
67 | })
68 | }
69 |
--------------------------------------------------------------------------------
/app/examples/slot.js:
--------------------------------------------------------------------------------
1 | import { renderExampleSection } from '../render/renderExampleSection.js'
2 |
3 | const options = [
4 | {
5 | name: 'England',
6 | value: 1,
7 | children: [
8 | {
9 | name: 'London',
10 | value: 2,
11 | children: [
12 | {
13 | name: 'Chelsea',
14 | value: 3,
15 | children: []
16 | },
17 | {
18 | name: 'West End',
19 | value: 4,
20 | children: []
21 | }
22 | ]
23 | },
24 | {
25 | name: 'Brighton',
26 | value: 5,
27 | children: []
28 | }
29 | ]
30 | }
31 | ]
32 |
33 | const value = []
34 |
35 | const treeselectId = 'treeselect-demo-slot'
36 |
37 | export const runSlotExample = (Treeselect) => {
38 | renderExampleSection({ sectionId: 'slot-section', options, value, treeselectId })
39 |
40 | const slot = document.createElement('div')
41 | slot.innerHTML = 'Click!'
42 |
43 | const domElement = document.getElementById(treeselectId)
44 | const treeselect = new Treeselect({
45 | parentHtmlContainer: domElement,
46 | value,
47 | options,
48 | listSlotHtmlComponent: slot
49 | })
50 |
51 | treeselect.srcElement.addEventListener('input', (e) => {
52 | console.log('slot: Selected value', e.detail)
53 | })
54 |
55 | slot.addEventListener('click', (e) => {
56 | e.preventDefault()
57 | alert('Slot click!')
58 | })
59 | }
60 |
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Treeselect JS App
12 |
16 |
17 |
18 |
19 |
20 | Treeselect JS Demo
21 |
22 |
23 |
24 | Treeselect with default props
25 |
26 |
27 |
28 |
29 |
30 | Treeselect with slot
31 |
32 |
33 |
34 |
35 |
36 | Treeselect with disabled
37 |
38 |
39 |
40 |
41 |
42 | Treeselect with single select prop
43 |
44 |
45 |
46 |
47 |
48 | Treeselect with independent nodes prop
49 |
50 |
51 |
52 |
53 |
54 | Treeselect with icons
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/app/render/renderExampleSection.js:
--------------------------------------------------------------------------------
1 | export const renderExampleSection = ({ sectionId, value, options, treeselectId, codeSnipped }) => {
2 | const section = document.getElementById(sectionId)
3 | const div = document.createElement('div')
4 | div.classList.add('section__data')
5 |
6 | if (codeSnipped) {
7 | div.innerHTML = `
8 |
9 |
10 |
11 | ${codeSnipped}
12 |
13 |
14 | `
15 | } else {
16 | div.innerHTML = `
17 |
18 |
19 |
20 | parentHtmlContainer: document.querySelector(className),
21 | value: ${JSON.stringify(value, null, 0).replace(/,/g, ', ')},
22 | options: ${JSON.stringify(options, null, 2)}
23 |
24 |
25 | `
26 | }
27 |
28 | section.appendChild(div)
29 | }
30 |
--------------------------------------------------------------------------------
/app/todo.js:
--------------------------------------------------------------------------------
1 | // IMPROVEMENTS (need to review)
2 | // !We need to review focus/blur system, because it is too complicated.
3 | // Slot is not selected by key navigation. if we use appendToBody mode. It is related to the new focus/blur approach.
4 | // Recheck performance
5 | // list scroll overlaps border
6 | // updateDisabled methods to avoid a remount
7 | // create update function for the list to avoid a remount
8 | // disable change focused element on fast scroll appendToBody
9 | // Should we add a list direction if we use staticList prop?
10 |
11 | // FEATURES (TODO)
12 | // opportunity to add empty groups without children
13 | // select input text with mouse (now it is unselectable) (done - experimental)
14 | // Add more e2e tests (cypress, jest)
15 | // Add "+ Count elements" additional text if tags are hidden
16 |
17 | // REQUESTS
18 | // Investigate an ability to add a diff names but the same values, and check them.
19 |
20 | // PERFORMANCE UPDATES TODO (List)
21 | // 1. Do we need uncheck all checkboxes if group is partial checked or otherwise we need to check all checkboxes if group is partial checked?
22 | // 2. Do we need to call disabled nodes and uncheck them? in adjustTreeItemOptions
23 | // 3. Do we need this functionality with isAllDisabled because we uncheck everything in the start and then we can't check disabled node? in updateFlattedOptionStateWithChildren
24 |
--------------------------------------------------------------------------------
/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'cypress'
2 |
3 | export default defineConfig({
4 | e2e: {
5 | downloadsFolder: '__tests__/cypress/downloads',
6 | fileServerFolder: '__tests__/cypress/fixtures',
7 | screenshotsFolder: '__tests__/cypress/screenshots',
8 | specPattern: '__tests__/cypress/**/*.cy.ts',
9 | supportFile: '__tests__/cypress/support/e2e.ts',
10 | supportFolder: '__tests__/cypress/support',
11 | videosFolder: '__tests__/cypress/videos'
12 | }
13 | })
14 |
--------------------------------------------------------------------------------
/demo/index.css:
--------------------------------------------------------------------------------
1 | @import '../app/app.css';
2 |
--------------------------------------------------------------------------------
/demo/index.js:
--------------------------------------------------------------------------------
1 | import { runDefaultExample } from '../app/examples/default.js'
2 | import { runSlotExample } from '../app/examples/slot.js'
3 | import { runDisabledExample } from '../app/examples/disabled.js'
4 | import { runSingleSelectExample } from '../app/examples/singleSelect.js'
5 | import { runIndependentNodesExample } from '../app/examples/independentNodes.js'
6 | import { runIconsExample } from '../app/examples/icons.js'
7 |
8 | // Treeselect is available via the link in the index.html file (treeselectjs.umd.js)
9 | const Treeselect = globalThis.Treeselect
10 |
11 | if (!Treeselect) {
12 | throw new Error('Treeselect is not available. Make sure you have included the script in the index.html file')
13 | }
14 |
15 | const runApp = (Treeselect) => {
16 | runDefaultExample(Treeselect)
17 | runSlotExample(Treeselect)
18 | runDisabledExample(Treeselect)
19 | runSingleSelectExample(Treeselect)
20 | runIndependentNodesExample(Treeselect)
21 | runIconsExample(Treeselect)
22 |
23 | document.body.classList.add('loaded')
24 | }
25 |
26 | runApp(Treeselect)
27 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Treeselect JS Demo
16 |
20 |
21 |
22 |
23 |
24 | Treeselect JS Demo
25 |
26 |
27 |
28 | Treeselect with default props
29 |
30 |
31 |
32 |
33 |
34 | Treeselect with slot
35 |
36 |
37 |
38 |
39 |
40 | Treeselect with disabled
41 |
42 |
43 |
44 |
45 |
46 | Treeselect with single select prop
47 |
48 |
49 |
50 |
51 |
52 | Treeselect with independent nodes prop
53 |
54 |
55 |
56 |
57 |
58 | Treeselect with icons
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | preset: 'ts-jest',
3 | testEnvironment: 'jest-environment-jsdom',
4 | transform: {
5 | '^.+\\.ts?$': 'ts-jest'
6 | },
7 | moduleFileExtensions: ['ts', 'js'],
8 | modulePathIgnorePatterns: ['/__tests__/jest/helpers'],
9 | moduleNameMapper: {
10 | '\\.(css|less|scss|sass)$': 'identity-obj-proxy'
11 | },
12 | testMatch: ['**/__tests__/**/*.test.ts'],
13 | setupFilesAfterEnv: ['/__tests__/jest/setup.ts']
14 | }
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "treeselectjs",
3 | "version": "0.13.1",
4 | "description": "Treeselect JS",
5 | "main": "./dist/treeselectjs.umd.js",
6 | "module": "./dist/treeselectjs.mjs",
7 | "types": "./dist/treeselectjs.d.ts",
8 | "exports": {
9 | ".": {
10 | "import": {
11 | "types": "./dist/treeselectjs.d.ts",
12 | "default": "./dist/treeselectjs.mjs"
13 | },
14 | "require": {
15 | "types": "./dist/treeselectjs.d.ts",
16 | "default": "./dist/treeselectjs.umd.js"
17 | }
18 | },
19 | "./dist/treeselectjs.css": {
20 | "import": "./dist/treeselectjs.css",
21 | "require": "./dist/treeselectjs.css"
22 | }
23 | },
24 | "scripts": {
25 | "dev": "vite --port 5173",
26 | "build": "tsc && vite build",
27 | "typecheck": "tsc --project tsconfig.app.json --noEmit && tsc --project tsconfig.jest.json --noEmit && tsc --project tsconfig.cypress.json --noEmit",
28 | "preview": "vite preview",
29 | "prettier": "prettier --w .",
30 | "jest:watch": "jest --watch",
31 | "jest:run": "jest",
32 | "cypress:open": "cypress open",
33 | "cypress:run": "cypress run",
34 | "test": "npm run typecheck && npm run jest:run && npm run cypress:run"
35 | },
36 | "repository": {
37 | "type": "git",
38 | "url": "https://github.com/dipson88/treeselectjs.git"
39 | },
40 | "keywords": [
41 | "treeselect",
42 | "select",
43 | "groupselect",
44 | "js",
45 | "ts",
46 | "typescript",
47 | "vanilla"
48 | ],
49 | "author": "Dzmitry Zhuraukou",
50 | "license": "MIT",
51 | "bugs": {
52 | "url": "https://github.com/dipson88/treeselectjs/issues"
53 | },
54 | "homepage": "https://github.com/dipson88/treeselectjs#readme",
55 | "devDependencies": {
56 | "@testing-library/dom": "^10.4.0",
57 | "@testing-library/jest-dom": "^6.6.3",
58 | "@types/jest": "^29.5.14",
59 | "cypress": "^13.17.0",
60 | "identity-obj-proxy": "^3.0.0",
61 | "intersection-observer": "^0.12.2",
62 | "jest": "^29.7.0",
63 | "jest-environment-jsdom": "^29.7.0",
64 | "prettier": "^3.5.1",
65 | "resize-observer-polyfill": "^1.5.1",
66 | "ts-jest": "^29.2.5",
67 | "ts-node": "^10.9.2",
68 | "typescript": "^5.7.3",
69 | "vite": "^6.2.0",
70 | "vite-plugin-dts": "^4.5.0"
71 | },
72 | "files": [
73 | "dist"
74 | ]
75 | }
76 |
--------------------------------------------------------------------------------
/public/tree.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dipson88/treeselectjs/298b041a298463d167464f13f76f9d505fb8988c/public/tree.png
--------------------------------------------------------------------------------
/src/input/input.css:
--------------------------------------------------------------------------------
1 | .treeselect-input {
2 | width: 100%;
3 | box-sizing: border-box;
4 | border: 1px solid #d7dde4;
5 | border-radius: 4px;
6 | display: flex;
7 | align-items: center;
8 | flex-wrap: wrap;
9 | padding: 2px 4px;
10 | padding-right: 40px;
11 | position: relative;
12 | min-height: 37px;
13 | background-color: #ffffff;
14 | cursor: text;
15 | }
16 |
17 | .treeselect-input--unsearchable {
18 | cursor: default;
19 | }
20 |
21 | .treeselect-input--unsearchable .treeselect-input__edit {
22 | caret-color: transparent;
23 | cursor: default;
24 | }
25 |
26 | .treeselect-input--unsearchable .treeselect-input__edit:focus {
27 | position: absolute;
28 | z-index: -1;
29 | left: 0;
30 | min-width: 0;
31 | width: 0;
32 | }
33 |
34 | .treeselect-input--value-not-selected .treeselect-input__edit,
35 | .treeselect-input--value-not-selected.treeselect-input--unsearchable .treeselect-input__edit:focus {
36 | z-index: auto;
37 | position: static;
38 | width: 100%;
39 | max-width: 100%;
40 | }
41 |
42 | .treeselect-input--value-not-selected .treeselect-input__tags {
43 | gap: 0;
44 | }
45 |
46 | /* rtl */
47 | [dir='rtl'] .treeselect-input {
48 | padding-right: 4px;
49 | padding-left: 40px;
50 | }
51 |
52 | [dir='rtl'] .treeselect-input__operators {
53 | right: unset;
54 | left: 2px;
55 | }
56 | /* rtl end */
57 |
58 | .treeselect-input__tags {
59 | display: inline-flex;
60 | align-items: center;
61 | flex-wrap: wrap;
62 | gap: 4px;
63 | max-width: 100%;
64 | width: 100%;
65 | box-sizing: border-box;
66 | }
67 |
68 | .treeselect-input__tags-element {
69 | display: inline-flex;
70 | align-items: center;
71 | background-color: #d7dde4;
72 | cursor: pointer;
73 | padding: 2px 5px;
74 | border-radius: 2px;
75 | font-size: 14px;
76 | max-width: 100%;
77 | box-sizing: border-box;
78 | }
79 |
80 | .treeselect-input__tags-element:hover {
81 | background-color: #c5c7cb;
82 | }
83 |
84 | .treeselect-input__tags-element:hover .treeselect-input__tags-cross svg {
85 | stroke: #eb4c42;
86 | }
87 |
88 | .treeselect-input__tags-name {
89 | overflow: hidden;
90 | white-space: nowrap;
91 | text-overflow: ellipsis;
92 | }
93 |
94 | .treeselect-input__tags-cross {
95 | display: flex;
96 | margin-left: 2px;
97 | }
98 |
99 | .treeselect-input__tags-cross svg {
100 | width: 12px;
101 | height: 12px;
102 | }
103 |
104 | .treeselect-input__tags-count {
105 | font-size: 14px;
106 | overflow: hidden;
107 | white-space: nowrap;
108 | text-overflow: ellipsis;
109 | }
110 |
111 | .treeselect-input__edit {
112 | flex: 1;
113 | border: none;
114 | font-size: 14px;
115 | text-overflow: ellipsis;
116 |
117 | width: 100%;
118 | max-width: calc(100% - 45px);
119 | padding: 0;
120 |
121 | /* Hide input for user */
122 | position: absolute;
123 | z-index: -1;
124 | min-width: 0;
125 | }
126 |
127 | .treeselect-input__edit:focus {
128 | outline: none;
129 | min-width: 30px;
130 | max-width: 100%;
131 |
132 | /* Show input for user */
133 | z-index: auto;
134 | position: static;
135 | }
136 |
137 | .treeselect-input__operators {
138 | display: flex;
139 | max-width: 40px;
140 | position: absolute;
141 | right: 2px;
142 | }
143 |
144 | .treeselect-input__clear {
145 | display: flex;
146 | cursor: pointer;
147 | }
148 |
149 | .treeselect-input__clear svg {
150 | stroke: #c5c7cb;
151 | width: 17px;
152 | min-width: 17px;
153 | height: 20px;
154 | }
155 |
156 | .treeselect-input__clear:hover svg {
157 | stroke: #838790;
158 | }
159 |
160 | .treeselect-input__arrow {
161 | display: flex;
162 | cursor: pointer;
163 | }
164 |
165 | .treeselect-input__arrow svg {
166 | stroke: #c5c7cb;
167 | width: 20px;
168 | min-width: 20px;
169 | height: 20px;
170 | }
171 |
172 | .treeselect-input__arrow:hover svg {
173 | stroke: #838790;
174 | }
175 |
--------------------------------------------------------------------------------
/src/input/inputTypes.ts:
--------------------------------------------------------------------------------
1 | import { type InnerOptionType, type IconsType } from '../treeselectTypes'
2 |
3 | export interface ITreeselectInputParams {
4 | value: InnerOptionType[]
5 | showTags: boolean
6 | tagsCountText: string
7 | clearable: boolean
8 | isAlwaysOpened: boolean
9 | searchable: boolean
10 | placeholder: string
11 | disabled: boolean
12 | isSingleSelect: boolean
13 | id: string
14 | ariaLabel: string
15 | iconElements: IconsType
16 | inputCallback: (value: InnerOptionType[]) => void
17 | searchCallback: (value: string) => void
18 | openCallback: () => void
19 | closeCallback: () => void
20 | keydownCallback: (e: KeyboardEvent) => void
21 | focusCallback: () => void
22 | nameChangeCallback: (name: string) => void
23 | }
24 |
25 | export interface ITreeselectInput extends ITreeselectInputParams {
26 | isOpened: boolean
27 | searchText: string
28 | srcElement: HTMLElement | Element
29 | focus: () => void
30 | blur: () => void
31 | updateValue: (newValue: InnerOptionType[]) => void
32 | removeItem: (id: string) => void
33 | clear: () => void
34 | openClose: () => void
35 | clearSearch: () => void
36 | }
37 |
--------------------------------------------------------------------------------
/src/list/helpers/domHelper.ts:
--------------------------------------------------------------------------------
1 | import { type ValueOptionType, type IconsType } from '../../treeselectTypes'
2 | import { type OptionsTreeMap, type TreeItem } from '../listTypes'
3 | import { appendIconToElement } from '../../svgIcons'
4 |
5 | export const updateDOM = ({
6 | optionsTreeMap,
7 | emptyListHtmlElement,
8 | iconElements,
9 | previousSingleSelectedValue,
10 | rtl
11 | }: {
12 | optionsTreeMap: OptionsTreeMap
13 | emptyListHtmlElement: HTMLElement | null
14 | iconElements: IconsType
15 | previousSingleSelectedValue: ValueOptionType[]
16 | rtl: boolean
17 | }) => {
18 | optionsTreeMap.forEach((option) => {
19 | const input = option.checkboxHtmlElement
20 |
21 | if (input) {
22 | input.checked = option.checked
23 | }
24 |
25 | updateCheckedClass({ option, previousSingleSelectedValue })
26 | updatePartialCheckedClass(option)
27 | updateDisabledCheckedClass(option)
28 | updateClosedClass({ option, iconElements })
29 | updateHiddenClass(option)
30 | updateLeftPaddingItems({ option, optionsTreeMap, rtl })
31 | updateCheckboxClass({ option, iconElements })
32 | updateGroupSelectableClass(option)
33 | })
34 |
35 | updateEmptyListClass({ optionsTreeMap, emptyListHtmlElement })
36 | }
37 |
38 | const updateLeftPaddingItems = ({
39 | option,
40 | optionsTreeMap,
41 | rtl
42 | }: {
43 | option: TreeItem
44 | optionsTreeMap: OptionsTreeMap
45 | rtl: boolean
46 | }) => {
47 | const isZeroLevel = option.level === 0
48 | const defaultPadding = 20
49 | const zeroLevelItemPadding = 5
50 | let padding = '0'
51 |
52 | if (isZeroLevel) {
53 | let isGroupsExistOnLevel = false
54 |
55 | for (const [_, item] of optionsTreeMap) {
56 | if (item.isGroup && item.level === option.level) {
57 | isGroupsExistOnLevel = true
58 | break
59 | }
60 | }
61 |
62 | const itemPadding = !option.isGroup && isGroupsExistOnLevel ? `${defaultPadding}px` : `${zeroLevelItemPadding}px`
63 | padding = option.isGroup ? '0' : itemPadding
64 | } else {
65 | padding = option.isGroup
66 | ? `${option.level * defaultPadding}px`
67 | : `${option.level * defaultPadding + defaultPadding}px`
68 | }
69 |
70 | const listItem = option.itemHtmlElement
71 |
72 | if (listItem) {
73 | if (rtl) {
74 | listItem.style.paddingRight = padding
75 | } else {
76 | listItem.style.paddingLeft = padding
77 | }
78 |
79 | // We can use css selectors to reset params with !important
80 | listItem.setAttribute('level', option.level.toString())
81 | listItem.setAttribute('group', option.isGroup.toString())
82 | }
83 | }
84 |
85 | const updateEmptyListClass = ({
86 | optionsTreeMap,
87 | emptyListHtmlElement
88 | }: {
89 | optionsTreeMap: OptionsTreeMap
90 | emptyListHtmlElement: HTMLElement | null
91 | }) => {
92 | let isNotEmpty = false
93 |
94 | for (const [_, option] of optionsTreeMap) {
95 | if (!option.hidden) {
96 | isNotEmpty = true
97 | break
98 | }
99 | }
100 |
101 | emptyListHtmlElement?.classList.toggle('treeselect-list__empty--hidden', isNotEmpty)
102 | }
103 |
104 | export const setAttributesFromHtmlAttr = (itemElement: HTMLDivElement, htmlAttr?: object) => {
105 | if (!htmlAttr) {
106 | return
107 | }
108 |
109 | Object.keys(htmlAttr).forEach((key) => {
110 | const value = htmlAttr[key as keyof object]
111 |
112 | if (typeof value === 'string') {
113 | itemElement.setAttribute(key, value)
114 | }
115 | })
116 | }
117 |
118 | const updateCheckedClass = ({
119 | option,
120 | previousSingleSelectedValue
121 | }: {
122 | option: TreeItem
123 | previousSingleSelectedValue: ValueOptionType[]
124 | }) => {
125 | const listItem = option.itemHtmlElement
126 | listItem?.classList.toggle('treeselect-list__item--checked', option.checked)
127 | const isCheckSingleSelected =
128 | Array.isArray(previousSingleSelectedValue) && previousSingleSelectedValue[0] === option.id && !option.disabled
129 | listItem?.classList.toggle('treeselect-list__item--single-selected', isCheckSingleSelected)
130 | }
131 |
132 | const updatePartialCheckedClass = (option: TreeItem) => {
133 | const listItem = option.itemHtmlElement
134 | listItem?.classList.toggle('treeselect-list__item--partial-checked', option.isPartialChecked)
135 | }
136 |
137 | const updateDisabledCheckedClass = (option: TreeItem) => {
138 | const listItem = option.itemHtmlElement
139 | listItem?.classList.toggle('treeselect-list__item--disabled', option.disabled)
140 | }
141 |
142 | const updateClosedClass = ({ option, iconElements }: { option: TreeItem; iconElements: IconsType }) => {
143 | const arrowIcon = option.arrowItemHtmlElement
144 |
145 | if (option.isGroup && arrowIcon) {
146 | const iconInnerElement = option.isClosed ? iconElements.arrowRight : iconElements.arrowDown
147 | appendIconToElement(iconInnerElement, arrowIcon)
148 |
149 | const listItem = option.itemHtmlElement
150 | listItem?.classList.toggle('treeselect-list__item--closed', option.isClosed)
151 | }
152 | }
153 |
154 | const updateHiddenClass = (option: TreeItem) => {
155 | const listItem = option.itemHtmlElement
156 | listItem?.classList.toggle('treeselect-list__item--hidden', option.hidden)
157 | }
158 |
159 | const updateCheckboxClass = ({ option, iconElements }: { option: TreeItem; iconElements: IconsType }) => {
160 | const icon = option.checkboxIconHtmlElement
161 |
162 | if (icon) {
163 | if (option.checked) {
164 | appendIconToElement(iconElements.check, icon)
165 | } else if (option.isPartialChecked) {
166 | appendIconToElement(iconElements.partialCheck, icon)
167 | } else {
168 | icon.innerHTML = ''
169 | }
170 | }
171 | }
172 |
173 | const updateGroupSelectableClass = (option: TreeItem) => {
174 | const listItem = option.itemHtmlElement
175 | listItem?.classList.toggle('treeselect-list__item--non-selectable-group', !option.isGroupSelectable)
176 | }
177 |
--------------------------------------------------------------------------------
/src/list/helpers/listCheckStateHelper.ts:
--------------------------------------------------------------------------------
1 | import { type ValueOptionType } from '../../treeselectTypes'
2 | import { type TreeItem, type OptionsTreeMap } from '../listTypes'
3 | import { getDirectChildrenOptions } from './listOptionsHelper'
4 |
5 | export const updateOptionsByValue = ({
6 | newValue,
7 | optionsTreeMap,
8 | isSingleSelect,
9 | isIndependentNodes
10 | }: {
11 | newValue: ValueOptionType[]
12 | optionsTreeMap: OptionsTreeMap
13 | isSingleSelect: boolean
14 | isIndependentNodes: boolean
15 | }) => {
16 | uncheckedAllTreeItemOptions(optionsTreeMap)
17 | const optionsToCheck = newValue
18 | .map((id) => optionsTreeMap.get(id) ?? null)
19 | .filter((option) => option !== null && !option.disabled) as TreeItem[]
20 | const [firstItem] = optionsToCheck
21 |
22 | if (isSingleSelect && optionsToCheck.length && firstItem) {
23 | firstItem.checked = true
24 | return
25 | }
26 |
27 | optionsToCheck.forEach((option) => {
28 | option.checked = true
29 | const resultChecked = updateOptionByCheckState({
30 | option,
31 | optionsTreeMap,
32 | isIndependentNodes
33 | })
34 | option.checked = resultChecked
35 | })
36 | }
37 |
38 | export const updateOptionByCheckState = ({
39 | option: { id, checked },
40 | optionsTreeMap,
41 | isIndependentNodes
42 | }: {
43 | option: Pick
44 | optionsTreeMap: OptionsTreeMap
45 | isIndependentNodes: boolean
46 | }) => {
47 | const currentOption = optionsTreeMap.get(id) ?? null
48 |
49 | if (currentOption === null) {
50 | return false
51 | }
52 |
53 | if (isIndependentNodes) {
54 | currentOption.checked = !currentOption.disabled && checked
55 |
56 | return currentOption.checked
57 | }
58 |
59 | const resultCheckedState = updateTreeItemOptionStateWithChildren({
60 | checked,
61 | currentOption,
62 | optionsTreeMap
63 | })
64 | updateParentTreeItemOptions({
65 | childOption: currentOption,
66 | optionsTreeMap
67 | })
68 |
69 | return resultCheckedState
70 | }
71 |
72 | const updateTreeItemOptionStateWithChildren = ({
73 | checked,
74 | currentOption,
75 | optionsTreeMap
76 | }: {
77 | checked: boolean
78 | currentOption: TreeItem
79 | optionsTreeMap: OptionsTreeMap
80 | }) => {
81 | if (!currentOption.isGroup) {
82 | currentOption.checked = !currentOption.disabled && checked
83 | currentOption.isPartialChecked = false
84 |
85 | return currentOption.checked
86 | }
87 |
88 | const childrenOptions = getDirectChildrenOptions({ id: currentOption.id, optionsTreeMap })
89 | const falseOrDisabledOrPartial = !checked || currentOption.disabled || currentOption.isPartialChecked
90 |
91 | if (falseOrDisabledOrPartial) {
92 | currentOption.checked = false
93 | currentOption.isPartialChecked = false
94 | checkUncheckAllChildren({
95 | option: currentOption,
96 | children: childrenOptions,
97 | optionsTreeMap
98 | })
99 |
100 | return currentOption.checked
101 | }
102 |
103 | const canWeCheckAllChildren = !isSomeChildrenDisabled({
104 | children: childrenOptions,
105 | optionsTreeMap
106 | })
107 |
108 | if (canWeCheckAllChildren) {
109 | currentOption.checked = true
110 | currentOption.isPartialChecked = false
111 | checkUncheckAllChildren({
112 | option: currentOption,
113 | children: childrenOptions,
114 | optionsTreeMap
115 | })
116 |
117 | return currentOption.checked
118 | }
119 |
120 | const isAllDisabled = isAllDirectChildrenDisabled(childrenOptions)
121 |
122 | if (isAllDisabled) {
123 | currentOption.checked = false
124 | currentOption.isPartialChecked = false
125 | currentOption.disabled = true
126 |
127 | return currentOption.checked
128 | }
129 |
130 | currentOption.checked = false
131 | currentOption.isPartialChecked = true
132 |
133 | childrenOptions.forEach((options) => {
134 | updateTreeItemOptionStateWithChildren({
135 | checked,
136 | currentOption: options,
137 | optionsTreeMap
138 | })
139 | })
140 |
141 | return currentOption.checked
142 | }
143 |
144 | const updateParentTreeItemOptions = ({
145 | childOption,
146 | optionsTreeMap
147 | }: {
148 | childOption: TreeItem
149 | optionsTreeMap: OptionsTreeMap
150 | }) => {
151 | const parentOption = optionsTreeMap.get(childOption.childOf) ?? null
152 |
153 | if (parentOption === null) {
154 | return
155 | }
156 |
157 | updateParentOption({ parentOption, optionsTreeMap })
158 | updateParentTreeItemOptions({
159 | childOption: parentOption,
160 | optionsTreeMap
161 | })
162 | }
163 |
164 | const updateParentOption = ({
165 | parentOption,
166 | optionsTreeMap
167 | }: {
168 | parentOption: TreeItem
169 | optionsTreeMap: OptionsTreeMap
170 | }) => {
171 | const children = getDirectChildrenOptions({ id: parentOption.id, optionsTreeMap })
172 | const isAllDisabled = isAllDirectChildrenDisabled(children)
173 |
174 | if (isAllDisabled) {
175 | parentOption.checked = false
176 | parentOption.isPartialChecked = false
177 | parentOption.disabled = true
178 |
179 | return
180 | }
181 |
182 | const isAllChecked = isAllDirectChildrenChecked(children)
183 |
184 | if (isAllChecked) {
185 | parentOption.checked = true
186 | parentOption.isPartialChecked = false
187 |
188 | return
189 | }
190 |
191 | const isSomeCheckedOrPartial = isSomeDirectChildrenCheckedOrPartial(children)
192 |
193 | if (isSomeCheckedOrPartial) {
194 | parentOption.checked = false
195 | parentOption.isPartialChecked = true
196 |
197 | return
198 | }
199 |
200 | parentOption.checked = false
201 | parentOption.isPartialChecked = false
202 | }
203 |
204 | const checkUncheckAllChildren = ({
205 | option: { checked, disabled },
206 | children,
207 | optionsTreeMap
208 | }: {
209 | option: Pick
210 | children: TreeItem[]
211 | optionsTreeMap: OptionsTreeMap
212 | }) => {
213 | children.forEach((option) => {
214 | option.disabled = disabled || option.disabled
215 | option.checked = checked && !option.disabled
216 | option.isPartialChecked = false
217 |
218 | const subChildren = getDirectChildrenOptions({ id: option.id, optionsTreeMap })
219 | checkUncheckAllChildren({
220 | option: { checked, disabled },
221 | children: subChildren,
222 | optionsTreeMap
223 | })
224 | })
225 | }
226 |
227 | const isSomeChildrenDisabled = ({
228 | children,
229 | optionsTreeMap
230 | }: {
231 | children: TreeItem[]
232 | optionsTreeMap: OptionsTreeMap
233 | }): boolean => {
234 | const isSomeDisabled = children.some((option) => option.disabled)
235 |
236 | if (isSomeDisabled) {
237 | return true
238 | }
239 |
240 | return children.some((option) => {
241 | if (!option.isGroup) {
242 | return false
243 | }
244 |
245 | const subChildren = getDirectChildrenOptions({ id: option.id, optionsTreeMap })
246 |
247 | return isSomeChildrenDisabled({ children: subChildren, optionsTreeMap })
248 | })
249 | }
250 |
251 | const isAllDirectChildrenDisabled = (children: TreeItem[]) => {
252 | return children.every((option) => !!option.disabled)
253 | }
254 |
255 | const isAllDirectChildrenChecked = (children: TreeItem[]) => {
256 | return children.every((option) => option.checked)
257 | }
258 |
259 | const isSomeDirectChildrenCheckedOrPartial = (children: TreeItem[]) => {
260 | return children.some((option) => option.checked || option.isPartialChecked)
261 | }
262 |
263 | const uncheckedAllTreeItemOptions = (optionsTreeMap: OptionsTreeMap) => {
264 | optionsTreeMap.forEach((option) => {
265 | option.checked = false
266 | option.isPartialChecked = false
267 | })
268 | }
269 |
--------------------------------------------------------------------------------
/src/list/helpers/listOptionsHelper.ts:
--------------------------------------------------------------------------------
1 | import { type OptionType, type ValueOptionType } from '../../treeselectTypes'
2 | import { TreeItem, type OptionsTreeMap } from '../listTypes'
3 | import { updateOptionByCheckState } from './listCheckStateHelper'
4 |
5 | export const getOptionsTreeMap = ({
6 | options,
7 | openLevel,
8 | isIndependentNodes
9 | }: {
10 | options: OptionType[]
11 | openLevel: number
12 | isIndependentNodes: boolean
13 | }) => {
14 | const defaultParams = { level: 0, groupId: '' }
15 | const optionsTreeMap: OptionsTreeMap = new Map()
16 |
17 | getTreeOptions({
18 | optionsTreeMap,
19 | options,
20 | openLevel,
21 | groupId: defaultParams.groupId,
22 | level: defaultParams.level
23 | })
24 |
25 | adjustTreeItemOptions({ optionsTreeMap, isIndependentNodes })
26 |
27 | return optionsTreeMap
28 | }
29 |
30 | const getTreeOptions = ({
31 | optionsTreeMap,
32 | options,
33 | openLevel,
34 | groupId,
35 | level
36 | }: {
37 | optionsTreeMap: OptionsTreeMap
38 | options: OptionType[]
39 | openLevel: number
40 | groupId: ValueOptionType
41 | level: number
42 | }) => {
43 | options.forEach((option) => {
44 | const isGroup = (option.children?.length ?? 0) > 0
45 | const isClosed = level >= openLevel && isGroup
46 | const hidden = level > openLevel
47 |
48 | const children = option.children?.map((child) => child.value) ?? []
49 | const optionId = option.value
50 |
51 | if (optionsTreeMap.has(optionId)) {
52 | console.error(
53 | `Validation: You have duplicated option value: ${optionId}! You should use unique values. Duplicates will lead to unexpected behavior.`
54 | )
55 | }
56 |
57 | optionsTreeMap.set(optionId, {
58 | id: optionId,
59 | name: option.name,
60 | childOf: groupId,
61 | isGroup,
62 | checked: false,
63 | isPartialChecked: false,
64 | level,
65 | isClosed,
66 | hidden,
67 | disabled: option.disabled ?? false,
68 | isGroupSelectable: !isGroup || (option.isGroupSelectable ?? true),
69 | children,
70 | // Html elements will be added during their creation
71 | checkboxHtmlElement: null,
72 | itemHtmlElement: null,
73 | arrowItemHtmlElement: null,
74 | checkboxIconHtmlElement: null
75 | })
76 |
77 | if (isGroup) {
78 | getTreeOptions({
79 | optionsTreeMap,
80 | options: option.children,
81 | openLevel,
82 | groupId: optionId,
83 | level: level + 1
84 | })
85 | }
86 | })
87 | }
88 |
89 | export const getTreeItemOptionByInputId = (inputId: string | null, optionsTreeMap: OptionsTreeMap) => {
90 | if (inputId === null) {
91 | return null
92 | }
93 |
94 | return optionsTreeMap.get(inputId) ?? optionsTreeMap.get(parseInt(inputId)) ?? null
95 | }
96 |
97 | export const getDirectChildrenOptions = ({
98 | id,
99 | optionsTreeMap
100 | }: {
101 | id: ValueOptionType
102 | optionsTreeMap: OptionsTreeMap
103 | }) => {
104 | const option = optionsTreeMap.get(id) ?? null
105 |
106 | if (option === null) {
107 | return []
108 | }
109 |
110 | return option.children.reduce((acc, curr) => {
111 | const child = optionsTreeMap.get(curr) ?? null
112 |
113 | if (child !== null) {
114 | acc.push(child)
115 | }
116 |
117 | return acc
118 | }, [])
119 | }
120 |
121 | export const getCheckedOptions = (optionsTreeMap: OptionsTreeMap) => {
122 | const ungroupedNodes: TreeItem[] = []
123 | const allGroupedNodes: TreeItem[] = []
124 | const allNodes: TreeItem[] = []
125 |
126 | optionsTreeMap.forEach((option) => {
127 | if (!option.checked) {
128 | return
129 | }
130 |
131 | allNodes.push(option)
132 |
133 | if (option.isGroup) {
134 | allGroupedNodes.push(option)
135 | } else {
136 | ungroupedNodes.push(option)
137 | }
138 | })
139 |
140 | const groupedNodes = allNodes.filter((node) => !allGroupedNodes.some(({ id }) => id === node.childOf))
141 |
142 | return { ungroupedNodes, groupedNodes, allNodes }
143 | }
144 |
145 | const adjustTreeItemOptions = ({
146 | optionsTreeMap,
147 | isIndependentNodes
148 | }: {
149 | optionsTreeMap: OptionsTreeMap
150 | isIndependentNodes: boolean
151 | }) => {
152 | // Disabled update
153 | const disabledNodes: TreeItem[] = []
154 |
155 | optionsTreeMap.forEach((option) => {
156 | if (option.disabled) {
157 | disabledNodes.push(option)
158 | }
159 | })
160 |
161 | disabledNodes.forEach(({ id }) =>
162 | updateOptionByCheckState({
163 | option: { id, checked: false },
164 | optionsTreeMap,
165 | isIndependentNodes
166 | })
167 | )
168 | }
169 |
--------------------------------------------------------------------------------
/src/list/helpers/listVisibilityStateHelper.ts:
--------------------------------------------------------------------------------
1 | import { type ValueOptionType } from '../../treeselectTypes'
2 | import { type TreeItem, type OptionsTreeMap, type BeforeSearchStateMap } from '../listTypes'
3 | import { getDirectChildrenOptions } from './listOptionsHelper'
4 |
5 | export const hideShowChildrenOptions = (
6 | optionsTreeMap: OptionsTreeMap,
7 | { id, isClosed }: Pick
8 | ) => {
9 | const allChildrenOptions = getDirectChildrenOptions({ id, optionsTreeMap })
10 |
11 | allChildrenOptions.forEach((option) => {
12 | option.hidden = isClosed ?? false
13 |
14 | if (option.isGroup && !option.isClosed) {
15 | hideShowChildrenOptions(optionsTreeMap, { id: option.id, isClosed })
16 | }
17 | })
18 | }
19 |
20 | export const expandSelectedItems = (optionsTreeMap: OptionsTreeMap, isSingleSelect: boolean) => {
21 | if (isSingleSelect) {
22 | expandSingleSelect(optionsTreeMap)
23 | return
24 | }
25 |
26 | optionsTreeMap.forEach((option) => {
27 | if (option.checked) {
28 | expandAllParents(option.childOf, optionsTreeMap)
29 | }
30 |
31 | if (option.isGroup && !option.disabled && (option.checked || option.isPartialChecked)) {
32 | option.isClosed = false
33 | hideShowChildrenOptions(optionsTreeMap, option)
34 | }
35 | })
36 | }
37 |
38 | const expandSingleSelect = (optionsTreeMap: OptionsTreeMap) => {
39 | let checkedOption: TreeItem | null = null
40 | for (const [_, option] of optionsTreeMap) {
41 | if (option.checked && !option.disabled) {
42 | checkedOption = option
43 | break
44 | }
45 | }
46 |
47 | if (!checkedOption) {
48 | return
49 | }
50 |
51 | if (checkedOption.isGroup) {
52 | checkedOption.isClosed = false
53 | hideShowChildrenOptions(optionsTreeMap, checkedOption)
54 | }
55 |
56 | expandAllParents(checkedOption.childOf, optionsTreeMap)
57 | }
58 |
59 | const expandAllParents = (childOf: ValueOptionType, optionsTreeMap: OptionsTreeMap) => {
60 | const parentNode = optionsTreeMap.get(childOf) ?? null
61 |
62 | if (parentNode) {
63 | parentNode.isClosed = false
64 | hideShowChildrenOptions(optionsTreeMap, parentNode)
65 | expandAllParents(parentNode.childOf, optionsTreeMap)
66 | }
67 | }
68 |
69 | export const updateVisibleBySearchTreeItemOptions = (optionsTreeMap: OptionsTreeMap, searchText: string) => {
70 | optionsTreeMap.forEach((option) => {
71 | const isSearched = option.name.toLowerCase().includes(searchText.toLowerCase())
72 |
73 | if (isSearched) {
74 | if (option.isGroup) {
75 | option.isClosed = true
76 | }
77 |
78 | if (option.childOf) {
79 | openShowAllParents(option.childOf, optionsTreeMap)
80 | }
81 | }
82 |
83 | option.hidden = !isSearched
84 | })
85 | }
86 |
87 | const openShowAllParents = (childOf: ValueOptionType, optionsTreeMap: OptionsTreeMap) => {
88 | const parentNode = optionsTreeMap.get(childOf) ?? null
89 |
90 | if (parentNode) {
91 | parentNode.hidden = false
92 | parentNode.isClosed = false
93 | openShowAllParents(parentNode.childOf, optionsTreeMap)
94 | }
95 | }
96 |
97 | export const updateOptionsMapBySearchState = ({
98 | optionsTreeMap,
99 | beforeSearchStateMap
100 | }: {
101 | optionsTreeMap: OptionsTreeMap
102 | beforeSearchStateMap: BeforeSearchStateMap
103 | }) => {
104 | optionsTreeMap.forEach((option) => {
105 | const beforeSearchState = beforeSearchStateMap.get(option.id)
106 |
107 | if (beforeSearchState) {
108 | option.hidden = beforeSearchState.hidden
109 | option.isClosed = beforeSearchState.isClosed
110 | }
111 | })
112 |
113 | beforeSearchStateMap.clear()
114 | }
115 |
116 | export const updateBeforeSearchStateMap = ({
117 | optionsTreeMap,
118 | beforeSearchStateMap
119 | }: {
120 | optionsTreeMap: OptionsTreeMap
121 | beforeSearchStateMap: BeforeSearchStateMap
122 | }) => {
123 | beforeSearchStateMap.clear()
124 |
125 | optionsTreeMap.forEach((option) => {
126 | beforeSearchStateMap.set(option.id, {
127 | hidden: option.hidden,
128 | isClosed: option.isClosed
129 | })
130 | })
131 | }
132 |
133 | export const createIntersectionScrollObserver = (srcElementList: HTMLElement) =>
134 | new IntersectionObserver(
135 | (entries) => {
136 | entries.forEach((entry) => {
137 | entry.target.classList.toggle('treeselect-list__item--scroll-not-visible', !entry.isIntersecting)
138 | })
139 | },
140 | { root: srcElementList, threshold: 0.5 }
141 | )
142 |
--------------------------------------------------------------------------------
/src/list/list.css:
--------------------------------------------------------------------------------
1 | .treeselect-list {
2 | width: 100%;
3 | box-sizing: border-box;
4 | border: 1px solid #d7dde4;
5 | overflow-y: auto;
6 | background-color: #ffffff;
7 | max-height: 300px;
8 | }
9 |
10 | .treeselect-list__group-container {
11 | box-sizing: border-box;
12 | }
13 |
14 | .treeselect-list__item {
15 | display: flex;
16 | align-items: center;
17 | box-sizing: border-box;
18 | cursor: pointer;
19 | height: 30px;
20 | }
21 |
22 | .treeselect-list__item:focus {
23 | outline: none;
24 | }
25 |
26 | .treeselect-list__item--focused {
27 | background-color: #f0ffff !important;
28 | }
29 |
30 | .treeselect-list__item--hidden {
31 | display: none;
32 | }
33 |
34 | .treeselect-list__item--scroll-not-visible {
35 | visibility: hidden;
36 | }
37 |
38 | .treeselect-list__item-icon {
39 | display: flex;
40 | align-items: center;
41 | cursor: pointer;
42 | height: 20px;
43 | width: 20px;
44 | min-width: 20px;
45 | }
46 |
47 | .treeselect-list__item-icon svg {
48 | pointer-events: none;
49 | width: 100%;
50 | height: 100%;
51 | stroke: #c5c7cb;
52 | }
53 |
54 | .treeselect-list__item-icon * {
55 | pointer-events: none;
56 | }
57 |
58 | .treeselect-list__item-icon:hover svg {
59 | stroke: #838790;
60 | }
61 |
62 | .treeselect-list__item-checkbox-container {
63 | width: 20px;
64 | height: 20px;
65 | min-width: 20px;
66 | border: 1px solid #d7dde4;
67 | border-radius: 3px;
68 | position: relative;
69 | background-color: #ffffff;
70 | pointer-events: none;
71 | box-sizing: border-box;
72 | }
73 |
74 | .treeselect-list__item-checkbox-container svg {
75 | position: absolute;
76 | height: 100%;
77 | width: 100%;
78 | }
79 |
80 | .treeselect-list__item-checkbox {
81 | margin: 0;
82 | width: 0;
83 | height: 0;
84 | pointer-events: none;
85 | position: absolute;
86 | z-index: -1;
87 | }
88 |
89 | .treeselect-list__item-checkbox-icon {
90 | position: absolute;
91 | height: 100%;
92 | width: 100%;
93 | left: 0;
94 | top: 0;
95 | text-align: left;
96 | }
97 |
98 | .treeselect-list__item-label {
99 | width: 100%;
100 | overflow: hidden;
101 | text-overflow: ellipsis;
102 | word-break: keep-all;
103 | white-space: nowrap;
104 | font-size: 14px;
105 | padding-left: 5px;
106 | pointer-events: none;
107 | text-align: left;
108 | }
109 |
110 | .treeselect-list__item-label-counter {
111 | margin-left: 3px;
112 | color: #838790;
113 | font-size: 13px;
114 | }
115 |
116 | .treeselect-list__empty {
117 | display: flex;
118 | align-items: center;
119 | height: 30px;
120 | padding-left: 4px;
121 | }
122 |
123 | .treeselect-list__empty--hidden {
124 | display: none;
125 | }
126 |
127 | .treeselect-list__empty-icon {
128 | display: flex;
129 | align-items: center;
130 | }
131 |
132 | .treeselect-list__empty-text {
133 | font-size: 14px;
134 | padding-left: 5px;
135 | overflow: hidden;
136 | text-overflow: ellipsis;
137 | word-break: keep-all;
138 | white-space: nowrap;
139 | }
140 |
141 | .treeselect-list__slot {
142 | position: sticky;
143 | box-sizing: border-box;
144 | width: 100%;
145 | max-width: 100%;
146 | bottom: 0;
147 | background-color: #ffffff;
148 | }
149 |
150 | /* single-select styles */
151 | .treeselect-list.treeselect-list--single-select .treeselect-list__item-checkbox-container {
152 | display: none;
153 | }
154 |
155 | /* disabled-branch-node styles */
156 | .treeselect-list.treeselect-list--disabled-branch-node
157 | .treeselect-list__item--group
158 | .treeselect-list__item-checkbox-container {
159 | display: none;
160 | }
161 |
162 | .treeselect-list__item--non-selectable-group .treeselect-list__item-checkbox-container {
163 | display: none;
164 | }
165 |
166 | /* checked styles */
167 | .treeselect-list__item--checked {
168 | background-color: #e9f1f1;
169 | }
170 |
171 | .treeselect-list.treeselect-list--single-select .treeselect-list__item--checked {
172 | background-color: transparent;
173 | }
174 |
175 | .treeselect-list.treeselect-list--single-select .treeselect-list__item--single-selected {
176 | background-color: #e9f1f1;
177 | }
178 |
179 | .treeselect-list__item .treeselect-list__item-checkbox-container svg {
180 | stroke: transparent;
181 | }
182 |
183 | .treeselect-list__item--checked .treeselect-list__item-checkbox-container svg,
184 | .treeselect-list__item--partial-checked .treeselect-list__item-checkbox-container svg {
185 | stroke: #ffffff;
186 | }
187 |
188 | .treeselect-list__item--checked .treeselect-list__item-checkbox-container,
189 | .treeselect-list__item--partial-checked .treeselect-list__item-checkbox-container {
190 | background-color: #52c67e;
191 | }
192 |
193 | .treeselect-list__item--disabled .treeselect-list__item-checkbox-container {
194 | background-color: #e9f1f1;
195 | }
196 |
197 | .treeselect-list__item--disabled .treeselect-list__item-label {
198 | color: #c5c7cb;
199 | }
200 |
201 | /* rtl */
202 | [dir='rtl'] .treeselect-list__item-checkbox-icon {
203 | text-align: right;
204 | }
205 |
206 | [dir='rtl'] .treeselect-list__item-label {
207 | text-align: right;
208 | padding-right: 5px;
209 | padding-left: unset;
210 | }
211 |
212 | [dir='rtl'] .treeselect-list__item--closed .treeselect-list__item-icon {
213 | transform: rotate(180deg);
214 | }
215 |
216 | [dir='rtl'] .treeselect-list__empty {
217 | padding-right: 4px;
218 | padding-left: unset;
219 | }
220 |
221 | [dir='rtl'] .treeselect-list__empty-text {
222 | padding-right: 5px;
223 | padding-left: unset;
224 | }
225 | /* rtl end */
226 |
--------------------------------------------------------------------------------
/src/list/listTypes.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type ValueOptionType,
3 | type OptionType,
4 | type IconsType,
5 | type SelectedNodesType,
6 | type TagsSortFnType
7 | } from '../treeselectTypes'
8 |
9 | export interface TreeItem {
10 | id: string | number
11 | name: string
12 | childOf: string | number
13 | isGroup: boolean
14 | checked: boolean
15 | isPartialChecked: boolean
16 | level: number
17 | isClosed: boolean
18 | hidden: boolean
19 | disabled: boolean
20 | isGroupSelectable: boolean
21 | children: ValueOptionType[]
22 | checkboxHtmlElement: HTMLInputElement | null
23 | itemHtmlElement: HTMLElement | null
24 | arrowItemHtmlElement: HTMLElement | null
25 | checkboxIconHtmlElement: HTMLElement | null
26 | }
27 |
28 | export type OptionsTreeMap = Map
29 |
30 | export type BeforeSearchStateMap = Map>
31 |
32 | export interface ITreeselectListParams {
33 | options: OptionType[]
34 | value: ValueOptionType[]
35 | openLevel: number
36 | listSlotHtmlComponent: HTMLElement | null
37 | tagsSortFn: TagsSortFnType
38 | emptyText: string
39 | isSingleSelect: boolean
40 | showCount: boolean
41 | disabledBranchNode: boolean
42 | expandSelected: boolean
43 | isIndependentNodes: boolean
44 | rtl: boolean
45 | listClassName: string
46 | isBoostedRendering: boolean
47 | iconElements: IconsType
48 | inputCallback: (value: SelectedNodesType) => void
49 | arrowClickCallback: (groupId: ValueOptionType, isClosed: boolean) => void
50 | mouseupCallback: () => void
51 | }
52 |
53 | export interface ITreeselectList extends ITreeselectListParams {
54 | searchText: string
55 | selectedNodes: SelectedNodesType
56 | srcElement: HTMLElement
57 | emptyListHtmlElement: HTMLElement | null
58 | optionsTreeMap: OptionsTreeMap
59 | beforeSearchStateMap: BeforeSearchStateMap
60 | intersectionItemsObserver: IntersectionObserver | null
61 | updateValue: (value: ValueOptionType[]) => void
62 | updateSearchValue: (searchText: string) => void
63 | callKeyAction: (e: KeyboardEvent) => void
64 | focusFirstListElement: () => void
65 | isLastFocusedElementExist: () => boolean
66 | destroy: () => void
67 | }
68 |
--------------------------------------------------------------------------------
/src/svgIcons.ts:
--------------------------------------------------------------------------------
1 | import { type IconsType } from './treeselectTypes'
2 |
3 | export const icons: IconsType = {
4 | arrowUp:
5 | '',
6 | arrowDown:
7 | '',
8 | arrowRight:
9 | '',
10 | attention:
11 | '',
12 | clear:
13 | '',
14 | cross:
15 | '',
16 | check:
17 | '',
18 | partialCheck:
19 | ''
20 | }
21 |
22 | export const appendIconToElement = (icon: string | HTMLElement, element: HTMLElement) => {
23 | element.innerHTML = ''
24 |
25 | if (typeof icon === 'string') {
26 | element.innerHTML = icon
27 | } else {
28 | const insertedIcon = icon.cloneNode(true)
29 | element.appendChild(insertedIcon)
30 | }
31 | }
32 |
33 | export const getDefaultIcons = (iconsFromProps?: Partial) => {
34 | const newIcons = iconsFromProps ? { ...iconsFromProps } : {}
35 |
36 | Object.keys(icons).forEach((key) => {
37 | const iconValue = newIcons[key as keyof IconsType]
38 |
39 | if (!iconValue) {
40 | newIcons[key as keyof IconsType] = icons[key as keyof IconsType]
41 | }
42 | })
43 |
44 | return newIcons as IconsType
45 | }
46 |
--------------------------------------------------------------------------------
/src/treeselectTypes.ts:
--------------------------------------------------------------------------------
1 | export type ValueOptionType = string | number
2 |
3 | export type ValueType = ValueOptionType[] | ValueOptionType | null
4 |
5 | export type ValueInputType = ValueOptionType[] | ValueOptionType | null | undefined
6 |
7 | export type OptionType = {
8 | value: ValueOptionType
9 | name: string
10 | disabled?: boolean
11 | isGroupSelectable?: boolean
12 | htmlAttr?: object
13 | children: OptionType[]
14 | }
15 |
16 | export type DirectionType = 'auto' | 'top' | 'bottom'
17 |
18 | export type TagsSortItem = { value: ValueOptionType; name: string }
19 | export type TagsSortFnType = ((itemA: TagsSortItem, itemB: TagsSortItem) => number) | null
20 |
21 | export interface ITreeselect {
22 | parentHtmlContainer: HTMLElement
23 | value: ValueType
24 | options: OptionType[]
25 | openLevel: number
26 | appendToBody: boolean
27 | alwaysOpen: boolean
28 | showTags: boolean
29 | tagsCountText: string
30 | tagsSortFn: TagsSortFnType
31 | clearable: boolean
32 | searchable: boolean
33 | placeholder: string
34 | grouped: boolean
35 | isGroupedValue: boolean
36 | listSlotHtmlComponent: HTMLElement | null
37 | disabled: boolean
38 | emptyText: string
39 | staticList: boolean
40 | id: string
41 | ariaLabel: string
42 | isSingleSelect: boolean
43 | showCount: boolean
44 | disabledBranchNode: boolean
45 | direction: DirectionType
46 | expandSelected: boolean
47 | saveScrollPosition: boolean
48 | isIndependentNodes: boolean
49 | rtl: boolean
50 | iconElements: IconsType
51 | ungroupedValue: ValueOptionType[]
52 | groupedValue: ValueOptionType[]
53 | isListOpened: boolean
54 | selectedName: string
55 | srcElement: HTMLElement | null
56 | inputCallback: ((value: ValueType) => void) | undefined
57 | openCallback: ((value: ValueType) => void) | undefined
58 | closeCallback: ((value: ValueType) => void) | undefined
59 | nameChangeCallback: ((name: string) => void) | undefined
60 | searchCallback: ((value: string) => void) | undefined
61 | openCloseGroupCallback: ((groupId: ValueOptionType, isClosed: boolean) => void) | undefined
62 | mount: () => void
63 | updateValue: (newValue: ValueInputType) => void
64 | destroy: () => void
65 | focus: () => void
66 | toggleOpenClose: () => void
67 | }
68 |
69 | export interface ITreeselectParams {
70 | parentHtmlContainer: HTMLElement
71 | value?: ValueInputType
72 | options?: OptionType[]
73 | openLevel?: number
74 | appendToBody?: boolean
75 | alwaysOpen?: boolean
76 | showTags?: boolean
77 | tagsCountText?: string
78 | tagsSortFn?: TagsSortFnType
79 | clearable?: boolean
80 | searchable?: boolean
81 | placeholder?: string
82 | grouped?: boolean
83 | isGroupedValue?: boolean
84 | listSlotHtmlComponent?: HTMLElement | null
85 | disabled?: boolean
86 | emptyText?: string
87 | staticList?: boolean
88 | id?: string
89 | ariaLabel?: string
90 | isSingleSelect?: boolean
91 | showCount?: boolean
92 | disabledBranchNode?: boolean
93 | direction?: DirectionType
94 | expandSelected?: boolean
95 | saveScrollPosition?: boolean
96 | isIndependentNodes?: boolean
97 | rtl?: boolean
98 | listClassName?: string
99 | isBoostedRendering?: boolean
100 | iconElements?: Partial
101 | inputCallback?: (value: ValueType) => void
102 | openCallback?: (value: ValueType) => void
103 | closeCallback?: (value: ValueType) => void
104 | nameChangeCallback?: (name: string) => void
105 | searchCallback?: (value: string) => void
106 | openCloseGroupCallback?: (groupId: ValueOptionType, isClosed: boolean) => void
107 | }
108 |
109 | export type InnerOptionType = {
110 | id: ValueOptionType
111 | name: string
112 | }
113 |
114 | export type IconsType = {
115 | arrowUp: string | HTMLElement
116 | arrowDown: string | HTMLElement
117 | arrowRight: string | HTMLElement
118 | attention: string | HTMLElement
119 | clear: string | HTMLElement
120 | cross: string | HTMLElement
121 | check: string | HTMLElement
122 | partialCheck: string | HTMLElement
123 | }
124 |
125 | export type SelectedNodesType = {
126 | nodes: InnerOptionType[]
127 | groupedNodes: InnerOptionType[]
128 | allNodes: InnerOptionType[]
129 | }
130 |
--------------------------------------------------------------------------------
/src/treeselectjs.css:
--------------------------------------------------------------------------------
1 | @import './input/input.css';
2 | @import './list/list.css';
3 |
4 | .treeselect {
5 | width: 100%;
6 | position: relative;
7 | box-sizing: border-box;
8 | }
9 |
10 | .treeselect--disabled {
11 | pointer-events: none;
12 | }
13 |
14 | .treeselect-list {
15 | position: absolute;
16 | left: 0;
17 | border-radius: 4px;
18 | box-sizing: border-box;
19 | z-index: 1000;
20 | }
21 |
22 | .treeselect .treeselect-list {
23 | position: absolute;
24 | }
25 |
26 | .treeselect .treeselect-list--static {
27 | position: static;
28 | }
29 |
30 | .treeselect-input--focused {
31 | border-color: #101010;
32 | }
33 |
34 | .treeselect-input--opened.treeselect-input--top {
35 | border-top-color: transparent;
36 | border-top-left-radius: 0;
37 | border-top-right-radius: 0;
38 | }
39 |
40 | .treeselect-input--opened.treeselect-input--bottom {
41 | border-bottom-color: transparent;
42 | border-bottom-left-radius: 0;
43 | border-bottom-right-radius: 0;
44 | }
45 |
46 | .treeselect-list--focused {
47 | border-color: #101010;
48 | }
49 |
50 | .treeselect-list--top,
51 | .treeselect-list--top-to-body {
52 | border-bottom-color: #d7dde4;
53 | border-bottom-left-radius: 0;
54 | border-bottom-right-radius: 0;
55 | }
56 |
57 | .treeselect-list--bottom,
58 | .treeselect-list--bottom-to-body {
59 | border-top-color: #d7dde4;
60 | border-top-left-radius: 0;
61 | border-top-right-radius: 0;
62 | }
63 |
64 | .treeselect-list--top {
65 | left: 0;
66 | bottom: 100%;
67 | }
68 |
69 | .treeselect-list--bottom {
70 | left: 0;
71 | top: 100%;
72 | }
73 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/treeselectjs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dipson88/treeselectjs/298b041a298463d167464f13f76f9d505fb8988c/treeselectjs.png
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["src/**/*.ts", "src/**/*.tsx"]
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "isolatedModules": false,
5 | "types": ["cypress"]
6 | },
7 | "include": ["__tests__/cypress/**/*.ts", "__tests__/testHelpers/**/*.ts"]
8 | }
9 |
--------------------------------------------------------------------------------
/tsconfig.jest.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "isolatedModules": false,
5 | "types": ["jest", "node"]
6 | },
7 | "include": ["__tests__/jest/**/*.test.ts", "__tests__/jest/**/*.spec.ts", "__tests__/testHelpers/**/*.ts"]
8 | }
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ESNext", "DOM"],
7 | "moduleResolution": "Node",
8 | "strict": true,
9 | "resolveJsonModule": true,
10 | "isolatedModules": true,
11 | "esModuleInterop": true,
12 | "noEmit": true,
13 | "noUnusedLocals": true,
14 | "noUnusedParameters": true,
15 | "noImplicitReturns": true,
16 | "skipLibCheck": true
17 | },
18 | "exclude": ["node_modules"],
19 | "files": [],
20 | "references": [
21 | { "path": "./tsconfig.app.json" },
22 | { "path": "./tsconfig.jest.json" },
23 | { "path": "./tsconfig.cypress.json" }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path'
2 | import { defineConfig } from 'vite'
3 | import dts from 'vite-plugin-dts'
4 |
5 | export default defineConfig({
6 | build: {
7 | copyPublicDir: false,
8 | lib: {
9 | entry: resolve(__dirname, 'src/treeselectjs.ts'),
10 | name: 'treeselectjs',
11 | fileName: 'treeselectjs'
12 | },
13 | rollupOptions: {
14 | output: {
15 | assetFileNames: (assetInfo) => {
16 | if (assetInfo.names.includes('style.css')) {
17 | return 'treeselectjs.css'
18 | }
19 |
20 | return assetInfo.names[0]
21 | },
22 | globals: {
23 | treeselectjs: 'Treeselect'
24 | }
25 | }
26 | }
27 | },
28 | server: {
29 | open: './app/index.html'
30 | },
31 | plugins: [
32 | dts({
33 | insertTypesEntry: true,
34 | rollupTypes: true,
35 | tsconfigPath: './tsconfig.app.json'
36 | })
37 | ]
38 | })
39 |
--------------------------------------------------------------------------------