├── _config.yml ├── screenshots ├── select1.png └── select2.png ├── CONTRIBUTERS ├── .editorconfig ├── .gitignore ├── .github └── workflows │ ├── linting.yml │ ├── release.yml │ └── main.yml ├── src ├── .eslintrc.json ├── dual-listbox.scss └── dual-listbox.js ├── vitest.config.js ├── CONTRIBUTING.md ├── LICENSE ├── style.css ├── dist ├── dual-listbox.css ├── dual-listbox.css.map ├── dual-listbox.js └── dual-listbox.js.map ├── package.json ├── CODE_OF_CONDUCT.md ├── README.md ├── doc └── dual-listbox.md ├── tests └── dual-listbox.test.js └── index.html /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-architect -------------------------------------------------------------------------------- /screenshots/select1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maykinmedia/dual-listbox/HEAD/screenshots/select1.png -------------------------------------------------------------------------------- /screenshots/select2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maykinmedia/dual-listbox/HEAD/screenshots/select2.png -------------------------------------------------------------------------------- /CONTRIBUTERS: -------------------------------------------------------------------------------- 1 | Jorik Kraaikamp (https://github.com/JostCrow) 2 | Piotr Gawron (https://github.com/piotr-gawron) 3 | Sven van de Scheur (https://github.com/svenvandescheur) 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [{package.json}] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Code Coverage ### 2 | build/reports/ 3 | 4 | ### Dependencies ### 5 | node_modules/ 6 | 7 | ### IDE ### 8 | .idea 9 | .vscode 10 | 11 | npm-debug\.log 12 | .parcel-cache/ 13 | coverage/ 14 | # dist/ 15 | dist/dual-listbox.test.js 16 | dist/dual-listbox.test.js.map 17 | -------------------------------------------------------------------------------- /.github/workflows/linting.yml: -------------------------------------------------------------------------------- 1 | name: Prettier 2 | on: 3 | push: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | build: 9 | name: Prettier 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - uses: bahmutov/npm-install@v1 14 | 15 | - run: npm run lint 16 | - run: git status 17 | -------------------------------------------------------------------------------- /src/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "parserOptions": { 7 | "sourceType": "module" 8 | }, 9 | "rules": { 10 | "no-const-assign": "warn", 11 | "no-this-before-super": "warn", 12 | "no-undef": "warn", 13 | "no-unreachable": "warn", 14 | "no-unused-vars": "warn", 15 | "constructor-super": "warn", 16 | "valid-typeof": "warn" 17 | }, 18 | "root": true 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | # Setup .npmrc file to publish to npm 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: "16.x" 14 | registry-url: "https://registry.npmjs.org" 15 | - run: npm ci 16 | - run: npm run prepare-release 17 | - run: npm publish 18 | env: 19 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: "Dual Listbox" 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: "16" 16 | - run: npm ci 17 | - run: npm run build 18 | test: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions/setup-node@v2 23 | with: 24 | node-version: "16" 25 | - run: npm ci 26 | - run: npm test 27 | -------------------------------------------------------------------------------- /vitest.config.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { defineConfig } from "vite"; 4 | 5 | export default defineConfig({ 6 | test: { 7 | globals: true, 8 | environment: "jsdom", 9 | coverage: { 10 | reporter: ["text", "clover"], 11 | all: true, 12 | exclude: [ 13 | "tests", 14 | "dist", 15 | "coverage/**", 16 | "src/*.test.js", 17 | "**/*.d.ts", 18 | "*.config.*", 19 | "**/{ava,babel,nyc}.config.{js,cjs,mjs}", 20 | "**/jest.config.{js,cjs,mjs,ts}", 21 | "**/{karma,rollup,webpack}.config.js", 22 | "**/.{eslint,mocha}rc.{js,cjs}", 23 | ], 24 | }, 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Maykin Media 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 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto+Condensed:300|Roboto+Mono'); 2 | 3 | html, body { 4 | font-weight: 300; 5 | } 6 | 7 | .source { 8 | font-size: 60%; 9 | cursor: pointer; 10 | color: rgb(50, 115, 220); 11 | } 12 | 13 | .selected-element { 14 | margin: 20px 0; 15 | font-weight: 300; 16 | } 17 | 18 | .selected-element p { 19 | margin-bottom: 0; 20 | } 21 | 22 | .changed-element li { 23 | list-style: none; 24 | } 25 | 26 | li.L0, li.L1, li.L2, li.L3, 27 | li.L5, li.L6, li.L7, li.L8 { 28 | list-style-type: decimal !important; 29 | } 30 | 31 | pre { 32 | display: none; 33 | } 34 | 35 | pre.open { 36 | display: block; 37 | } 38 | 39 | code[class*="language-"], pre[class*="language-"] { 40 | font-family: 'Roboto Mono', monospace; 41 | } 42 | 43 | .token { 44 | font-family: 'Roboto Mono', monospace; 45 | font-size: .75rem; 46 | height: 1rem; 47 | line-height: 1.5; 48 | } 49 | 50 | .tag:not(body) { 51 | background-color: transparent; 52 | display: inline-block; 53 | font-size: .75rem; 54 | height: 1rem; 55 | line-height: 1.5; 56 | padding-left: 0; 57 | padding-right: 0; 58 | white-space: nowrap; 59 | } 60 | -------------------------------------------------------------------------------- /dist/dual-listbox.css: -------------------------------------------------------------------------------- 1 | .dual-listbox{display:flex;flex-direction:column}.dual-listbox .dual-listbox__container{display:flex;align-items:center;flex-direction:row;flex-wrap:wrap}.dual-listbox .dual-listbox__search{border:1px solid #ddd;padding:10px;width:100%}.dual-listbox .dual-listbox__available,.dual-listbox .dual-listbox__selected{border:1px solid #ddd;height:300px;overflow-y:auto;padding:0;width:300px;margin-top:0;-webkit-margin-before:0}.dual-listbox .dual-listbox__buttons{display:flex;flex-direction:column;margin:0 10px}.dual-listbox .dual-listbox__button{margin-bottom:5px;border:0;background-color:#ddd;padding:10px;color:#fff;cursor:pointer}.dual-listbox .dual-listbox__button:hover{background-color:#ccc}.dual-listbox .dual-listbox__title{padding:15px 10px;font-size:120%;font-weight:700;border-left:1px solid #efefef;border-right:1px solid #efefef;border-top:1px solid #efefef;margin-top:1rem;-webkit-margin-before:1rem}.dual-listbox .dual-listbox__item{display:block;padding:10px;cursor:pointer;user-select:none;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;border-bottom:1px solid #efefef;transition:background .2s ease}.dual-listbox .dual-listbox__item.dual-listbox__item--selected{background-color:#089de3b3}.dual-listbox .dragging{opacity:.5;background-color:#ddd}.dual-listbox .drop-in{border:1px solid #aaa}.dual-listbox .drop-above{border:0;border-top:1px solid #7c90ff} 2 | /*# sourceMappingURL=dual-listbox.css.map */ 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dual-listbox", 3 | "version": "2.0.0", 4 | "description": "Dual listbox for multi-select elements", 5 | "browserslist": "> 0.05%", 6 | "devDependencies": { 7 | "c8": "^7.11.2", 8 | "esbuild": "^0.14.38", 9 | "esbuild-plugin-sass": "^1.0.1", 10 | "jsdom": "^19.0.0", 11 | "serve": "^14.0.1", 12 | "vite": "^2.9.13", 13 | "vitest": "^0.9.4" 14 | }, 15 | "scripts": { 16 | "test": "vitest run --coverage", 17 | "build": "node build.js", 18 | "serve": "serve", 19 | "watch": "nodemon -w ./src/ -e js,scss ./build.js", 20 | "lint": "prettier src/ -c", 21 | "format": "prettier src/ -w", 22 | "prepare-release": "npm run format && npm test && npm run build" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/maykinmedia/dual-listbox.git" 27 | }, 28 | "keywords": [ 29 | "Select", 30 | "Dual", 31 | "Listbox", 32 | "Multiselect", 33 | "Multi" 34 | ], 35 | "author": "Maykin Media ", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/maykinmedia/dual-listbox/issues" 39 | }, 40 | "homepage": "https://github.com/maykinmedia/dual-listbox#readme", 41 | "jest": { 42 | "collectCoverage": true, 43 | "collectCoverageFrom": [ 44 | "src/**/*.{js,jsx}", 45 | "!**/node_modules/**", 46 | "!**/vendor/**" 47 | ], 48 | "transform": { 49 | "^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": "/babelJest.js" 50 | } 51 | }, 52 | "dependencies": { 53 | "nodemon": "^2.0.20", 54 | "prettier": "^2.6.2" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /dist/dual-listbox.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": ["../../../../../tmp/tmp-74295-sUie0C8NQBmy/dual-listbox/src/dual-listbox.css"], 4 | "sourcesContent": [".dual-listbox{display:flex;flex-direction:column}.dual-listbox .dual-listbox__container{display:flex;align-items:center;flex-direction:row;flex-wrap:wrap}.dual-listbox .dual-listbox__search{border:1px solid #ddd;padding:10px;width:100%}.dual-listbox .dual-listbox__available,.dual-listbox .dual-listbox__selected{border:1px solid #ddd;height:300px;overflow-y:auto;padding:0;width:300px;margin-top:0;-webkit-margin-before:0}.dual-listbox .dual-listbox__buttons{display:flex;flex-direction:column;margin:0 10px}.dual-listbox .dual-listbox__button{margin-bottom:5px;border:0;background-color:#ddd;padding:10px;color:#fff;cursor:pointer}.dual-listbox .dual-listbox__button:hover{background-color:#ccc}.dual-listbox .dual-listbox__title{padding:15px 10px;font-size:120%;font-weight:bold;border-left:1px solid #efefef;border-right:1px solid #efefef;border-top:1px solid #efefef;margin-top:1rem;-webkit-margin-before:1rem}.dual-listbox .dual-listbox__item{display:block;padding:10px;cursor:pointer;user-select:none;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;border-bottom:1px solid #efefef;transition:background 0.2s ease}.dual-listbox .dual-listbox__item.dual-listbox__item--selected{background-color:rgba(8,157,227,0.7)}.dual-listbox .dragging{opacity:0.5;background-color:#ddd}.dual-listbox .drop-in{border:1px solid #aaa}.dual-listbox .drop-above{border:0;border-top:1px solid #7c90ff}"], 5 | "mappings": "AAAA,cAAc,aAAa,sBAAsB,uCAAuC,aAAa,mBAAmB,mBAAmB,eAAe,oCAAoC,sBAA9L,aAAiO,WAAW,6EAA6E,sBAAsB,aAAa,gBAA5V,UAAsX,YAAY,aAAa,wBAAwB,qCAAqC,aAAa,sBAAzd,cAA6f,oCAAoC,kBAAkB,SAAS,sBAA5jB,aAA+lB,WAAW,eAAe,0CAA0C,sBAAsB,mCAAzrB,kBAA8uB,eAAe,gBAAiB,8BAA8B,+BAA+B,6BAA6B,gBAAgB,2BAA2B,kCAAkC,cAAr7B,aAAg9B,eAAe,iBAAiB,sBAAsB,yBAAyB,qBAAqB,gCAAgC,+BAAgC,+DAA+D,2BAAqC,wBAAwB,WAAY,sBAAsB,uBAAuB,sBAAsB,0BAA0B,SAAS", 6 | "names": [] 7 | } 8 | -------------------------------------------------------------------------------- /src/dual-listbox.scss: -------------------------------------------------------------------------------- 1 | .dual-listbox { 2 | display: flex; 3 | flex-direction: column; 4 | 5 | .dual-listbox__container { 6 | display: flex; 7 | align-items: center; 8 | flex-direction: row; 9 | flex-wrap: wrap; 10 | } 11 | 12 | .dual-listbox__search { 13 | border: 1px solid #ddd; 14 | padding: 10px; 15 | width: 100%; 16 | } 17 | 18 | .dual-listbox__available, 19 | .dual-listbox__selected { 20 | border: 1px solid #ddd; 21 | height: 300px; 22 | overflow-y: auto; 23 | padding: 0; 24 | width: 300px; 25 | margin-top: 0; 26 | -webkit-margin-before: 0; 27 | } 28 | 29 | .dual-listbox__buttons { 30 | display: flex; 31 | flex-direction: column; 32 | margin: 0 10px; 33 | } 34 | 35 | .dual-listbox__button { 36 | margin-bottom: 5px; 37 | border: 0; 38 | background-color: #ddd; 39 | padding: 10px; 40 | color: #fff; 41 | cursor: pointer; 42 | 43 | &:hover { 44 | background-color: #ccc; 45 | } 46 | } 47 | 48 | .dual-listbox__title { 49 | padding: 15px 10px; 50 | font-size: 120%; 51 | font-weight: bold; 52 | border-left: 1px solid #efefef; 53 | border-right: 1px solid #efefef; 54 | border-top: 1px solid #efefef; 55 | margin-top: 1rem; 56 | -webkit-margin-before: 1rem; 57 | } 58 | 59 | .dual-listbox__item { 60 | display: block; 61 | padding: 10px; 62 | cursor: pointer; 63 | user-select: none; 64 | -moz-user-select: none; 65 | -webkit-user-select: none; 66 | -ms-user-select: none; 67 | border-bottom: 1px solid #efefef; 68 | transition: background 0.2s ease; 69 | 70 | &.dual-listbox__item--selected { 71 | background-color: transparentize(#089de3, 0.3); 72 | } 73 | } 74 | 75 | .dragging { 76 | opacity: 0.5; 77 | background-color: #ddd; 78 | } 79 | 80 | .drop-in { 81 | border: 1px solid #aaa; 82 | } 83 | 84 | .drop-above { 85 | border: 0; 86 | border-top: 1px solid rgb(124, 144, 255); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at jorik@maykinmedia.nl. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/maykinmedia/dual-listbox.svg?branch=master)](https://travis-ci.org/maykinmedia/dual-listbox) 2 | [![Coverage Status](https://coveralls.io/repos/github/maykinmedia/dual-listbox/badge.svg?branch=master)](https://coveralls.io/github/maykinmedia/dual-listbox?branch=master) 3 | [![Code Climate](https://codeclimate.com/github/maykinmedia/dual-listbox/badges/gpa.svg)](https://codeclimate.com/github/maykinmedia/dual-listbox) 4 | [![Lintly](https://lintly.com/gh/maykinmedia/dual-listbox/badge.svg)](https://lintly.com/gh/maykinmedia/dual-listbox/) 5 | [![npm](https://img.shields.io/npm/dw/dual-listbox.svg)](https://github.com/maykinmedia/dual-listbox) 6 | [![Known Vulnerabilities](https://snyk.io/test/github/maykinmedia/dual-listbox/badge.svg)](https://snyk.io/test/github/maykinmedia/dual-listbox) 7 | [![BCH compliance](https://bettercodehub.com/edge/badge/maykinmedia/dual-listbox?branch=master)](https://bettercodehub.com/) 8 | 9 | [![NPM](https://nodei.co/npm/dual-listbox.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/dual-listbox/) 10 | 11 | # Changes 12 | > Solves the duplicate option rendering problem, as mentioned in this issue https://github.com/maykinmedia/dual-listbox/issues/63 13 | 14 | # Dual Listbox 15 | 16 | > Make your multi select pretty and easy to use with only javascript. No other frameworks/libraries required. 17 | 18 | [Try the demo](https://maykinmedia.github.io/dual-listbox/) 19 | 20 | Styling. (From the stylesheet that can be found in the dist folder) 21 | 22 | ![Default](screenshots/select1.png) 23 | 24 | with selected options and one option highlighted. 25 | 26 | ![selected](screenshots/select2.png) 27 | 28 | ## Install 29 | 30 | Install with [npm](https://www.npmjs.com/) 31 | 32 | ```sh 33 | $ npm i dual-listbox --save 34 | ``` 35 | 36 | CDN 37 | 38 | ```html 39 | 40 | 44 | 45 | 46 | 47 | 51 | ``` 52 | 53 | ## Usage 54 | 55 | ```javascript 56 | let dualListbox = new DualListbox("select"); // Selects the first selectbox on the page. 57 | let dualListbox = new DualListbox(".select"); // Selects the first element with the class 'select' 58 | let dualListbox = new DualListbox("#select"); // Selects the first element with the id 'select' 59 | 60 | let select = document.querySelector("#select"); 61 | let dualListbox = new DualListbox(select); // Add a HTMLElement 62 | ``` 63 | 64 | You can also pass some options to the DualListbox 65 | 66 | ```javascript 67 | let dualListbox = new DualListbox("#select", { 68 | addEvent: function (value) { 69 | // Should use the event listeners 70 | console.log(value); 71 | }, 72 | removeEvent: function (value) { 73 | // Should use the event listeners 74 | console.log(value); 75 | }, 76 | availableTitle: "Different title", 77 | selectedTitle: "Different title", 78 | addButtonText: ">", 79 | removeButtonText: "<", 80 | addAllButtonText: ">>", 81 | removeAllButtonText: "<<", 82 | 83 | sortable: true, 84 | upButtonText: "ᐱ", 85 | downButtonText: "ᐯ", 86 | 87 | draggable: true, 88 | 89 | options: [ 90 | { text: "Option 1", value: "OPTION1" }, 91 | { text: "Option 2", value: "OPTION2" }, 92 | { text: "Selected option", value: "OPTION3", selected: true }, 93 | ], 94 | }); 95 | 96 | dualListbox.addEventListener("added", function (event) { 97 | console.log(event); 98 | console.log(event.addedElement); 99 | }); 100 | dualListbox.addEventListener("removed", function (event) { 101 | console.log(event); 102 | console.log(event.removedElement); 103 | }); 104 | ``` 105 | 106 | Try it online on [JSFiddle](https://jsfiddle.net/pn2zcwre/3/). 107 | 108 | ## Exposed elements 109 | 110 | All the elements should be exposed. This way it should be possible to add custom attributes to the element of choice. 111 | 112 | ```javascript 113 | let dualListbox = new DualListbox("#select"); 114 | 115 | // Access the buttons: 116 | dualListbox.add_button.setAttribute("a", "a"); 117 | dualListbox.add_all_button.setAttribute("a", "a"); 118 | dualListbox.remove_button.setAttribute("a", "a"); 119 | dualListbox.remove_all_button.setAttribute("a", "a"); 120 | 121 | // Access the search field: 122 | dualListbox.search_left.classList.add("some_class"); 123 | dualListbox.search_right.classList.add("some_class"); 124 | 125 | // Access the list containers: 126 | dualListbox.selectedList.setAttribute("a", "a"); 127 | dualListbox.availableList.setAttribute("a", "a"); 128 | ``` 129 | 130 | ## Contributing 131 | 132 | Pull requests and stars are always welcome. For bugs and feature requests, [please create an issue](https://github.com/maykinmedia/dual-listbox/issues). 133 | 134 | ## Author 135 | 136 | **Maykin Media** 137 | 138 | - [maykinmedia.nl](https://www.maykinmedia.nl/) 139 | - [github/maykinmedia](https://github.com/maykinmedia) 140 | - [twitter/maykinmedia](http://twitter.com/maykinmedia) 141 | 142 | ## License 143 | 144 | Copyright © 2019 [Maykin Media](https://www.maykinmedia.nl/) 145 | Licensed under the MIT license. 146 | -------------------------------------------------------------------------------- /doc/dual-listbox.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## dual-listbox 4 | Formset module 5 | Contains logic for generating django formsets 6 | 7 | 8 | * [dual-listbox](#module_dual-listbox) 9 | * [~DualListbox](#module_dual-listbox..DualListbox) 10 | * [.setDefaults()](#module_dual-listbox..DualListbox+setDefaults) 11 | * [.addEventListener(eventName, callback)](#module_dual-listbox..DualListbox+addEventListener) 12 | * [.addSelected(listItem)](#module_dual-listbox..DualListbox+addSelected) 13 | * [.redraw()](#module_dual-listbox..DualListbox+redraw) 14 | * [.removeSelected(listItem)](#module_dual-listbox..DualListbox+removeSelected) 15 | * [.searchLists(searchString, dualListbox)](#module_dual-listbox..DualListbox+searchLists) 16 | * [.updateAvailableListbox()](#module_dual-listbox..DualListbox+updateAvailableListbox) 17 | * [.updateSelectedListbox()](#module_dual-listbox..DualListbox+updateSelectedListbox) 18 | * [._actionAllSelected()](#module_dual-listbox..DualListbox+_actionAllSelected) 19 | * [._updateListbox()](#module_dual-listbox..DualListbox+_updateListbox) 20 | * [._actionItemSelected()](#module_dual-listbox..DualListbox+_actionItemSelected) 21 | * [._actionAllDeselected()](#module_dual-listbox..DualListbox+_actionAllDeselected) 22 | * [._actionItemDeselected()](#module_dual-listbox..DualListbox+_actionItemDeselected) 23 | * [._actionItemDoubleClick()](#module_dual-listbox..DualListbox+_actionItemDoubleClick) 24 | * [._actionItemClick()](#module_dual-listbox..DualListbox+_actionItemClick) 25 | * [._addButtonActions()](#module_dual-listbox..DualListbox+_addButtonActions) 26 | * [._addClickActions(listItem)](#module_dual-listbox..DualListbox+_addClickActions) 27 | * [._createList()](#module_dual-listbox..DualListbox+_createList) 28 | * [._createButtons()](#module_dual-listbox..DualListbox+_createButtons) 29 | 30 | 31 | 32 | ### dual-listbox~DualListbox 33 | Dual select interface allowing the user to select items from a list of provided options. 34 | 35 | **Kind**: inner class of [dual-listbox](#module_dual-listbox) 36 | 37 | * [~DualListbox](#module_dual-listbox..DualListbox) 38 | * [.setDefaults()](#module_dual-listbox..DualListbox+setDefaults) 39 | * [.addEventListener(eventName, callback)](#module_dual-listbox..DualListbox+addEventListener) 40 | * [.addSelected(listItem)](#module_dual-listbox..DualListbox+addSelected) 41 | * [.redraw()](#module_dual-listbox..DualListbox+redraw) 42 | * [.removeSelected(listItem)](#module_dual-listbox..DualListbox+removeSelected) 43 | * [.searchLists(searchString, dualListbox)](#module_dual-listbox..DualListbox+searchLists) 44 | * [.updateAvailableListbox()](#module_dual-listbox..DualListbox+updateAvailableListbox) 45 | * [.updateSelectedListbox()](#module_dual-listbox..DualListbox+updateSelectedListbox) 46 | * [._actionAllSelected()](#module_dual-listbox..DualListbox+_actionAllSelected) 47 | * [._updateListbox()](#module_dual-listbox..DualListbox+_updateListbox) 48 | * [._actionItemSelected()](#module_dual-listbox..DualListbox+_actionItemSelected) 49 | * [._actionAllDeselected()](#module_dual-listbox..DualListbox+_actionAllDeselected) 50 | * [._actionItemDeselected()](#module_dual-listbox..DualListbox+_actionItemDeselected) 51 | * [._actionItemDoubleClick()](#module_dual-listbox..DualListbox+_actionItemDoubleClick) 52 | * [._actionItemClick()](#module_dual-listbox..DualListbox+_actionItemClick) 53 | * [._addButtonActions()](#module_dual-listbox..DualListbox+_addButtonActions) 54 | * [._addClickActions(listItem)](#module_dual-listbox..DualListbox+_addClickActions) 55 | * [._createList()](#module_dual-listbox..DualListbox+_createList) 56 | * [._createButtons()](#module_dual-listbox..DualListbox+_createButtons) 57 | 58 | 59 | 60 | #### dualListbox.setDefaults() 61 | Sets the default values that can be overwritten. 62 | 63 | **Kind**: instance method of [DualListbox](#module_dual-listbox..DualListbox) 64 | 65 | 66 | #### dualListbox.addEventListener(eventName, callback) 67 | Add eventListener to the dualListbox element. 68 | 69 | **Kind**: instance method of [DualListbox](#module_dual-listbox..DualListbox) 70 | 71 | | Param | Type | 72 | | --- | --- | 73 | | eventName | String | 74 | | callback | function | 75 | 76 | 77 | 78 | #### dualListbox.addSelected(listItem) 79 | Add the listItem to the selected list. 80 | 81 | **Kind**: instance method of [DualListbox](#module_dual-listbox..DualListbox) 82 | 83 | | Param | Type | 84 | | --- | --- | 85 | | listItem | NodeElement | 86 | 87 | 88 | 89 | #### dualListbox.redraw() 90 | Redraws the Dual listbox content 91 | 92 | **Kind**: instance method of [DualListbox](#module_dual-listbox..DualListbox) 93 | 94 | 95 | #### dualListbox.removeSelected(listItem) 96 | Removes the listItem from the selected list. 97 | 98 | **Kind**: instance method of [DualListbox](#module_dual-listbox..DualListbox) 99 | 100 | | Param | Type | 101 | | --- | --- | 102 | | listItem | NodeElement | 103 | 104 | 105 | 106 | #### dualListbox.searchLists(searchString, dualListbox) 107 | Filters the listboxes with the given searchString. 108 | 109 | **Kind**: instance method of [DualListbox](#module_dual-listbox..DualListbox) 110 | 111 | | Param | Type | 112 | | --- | --- | 113 | | searchString | Object | 114 | | dualListbox | | 115 | 116 | 117 | 118 | #### dualListbox.updateAvailableListbox() 119 | Update the elements in the available listbox; 120 | 121 | **Kind**: instance method of [DualListbox](#module_dual-listbox..DualListbox) 122 | 123 | 124 | #### dualListbox.updateSelectedListbox() 125 | Update the elements in the selected listbox; 126 | 127 | **Kind**: instance method of [DualListbox](#module_dual-listbox..DualListbox) 128 | 129 | 130 | #### dualListbox.\_actionAllSelected() 131 | Action to set all listItems to selected. 132 | 133 | **Kind**: instance method of [DualListbox](#module_dual-listbox..DualListbox) 134 | 135 | 136 | #### dualListbox.\_updateListbox() 137 | Update the elements in the listbox; 138 | 139 | **Kind**: instance method of [DualListbox](#module_dual-listbox..DualListbox) 140 | 141 | 142 | #### dualListbox.\_actionItemSelected() 143 | Action to set one listItem to selected. 144 | 145 | **Kind**: instance method of [DualListbox](#module_dual-listbox..DualListbox) 146 | 147 | 148 | #### dualListbox.\_actionAllDeselected() 149 | Action to set all listItems to available. 150 | 151 | **Kind**: instance method of [DualListbox](#module_dual-listbox..DualListbox) 152 | 153 | 154 | #### dualListbox.\_actionItemDeselected() 155 | Action to set one listItem to available. 156 | 157 | **Kind**: instance method of [DualListbox](#module_dual-listbox..DualListbox) 158 | 159 | 160 | #### dualListbox.\_actionItemDoubleClick() 161 | Action when double clicked on a listItem. 162 | 163 | **Kind**: instance method of [DualListbox](#module_dual-listbox..DualListbox) 164 | 165 | 166 | #### dualListbox.\_actionItemClick() 167 | Action when single clicked on a listItem. 168 | 169 | **Kind**: instance method of [DualListbox](#module_dual-listbox..DualListbox) 170 | 171 | 172 | #### dualListbox.\_addButtonActions() 173 | Adds the actions to the buttons that are created. 174 | 175 | **Kind**: instance method of [DualListbox](#module_dual-listbox..DualListbox) 176 | 177 | 178 | #### dualListbox.\_addClickActions(listItem) 179 | Adds the click items to the listItem. 180 | 181 | **Kind**: instance method of [DualListbox](#module_dual-listbox..DualListbox) 182 | 183 | | Param | Type | 184 | | --- | --- | 185 | | listItem | Object | 186 | 187 | 188 | 189 | #### dualListbox.\_createList() 190 | Creates list with the header. 191 | 192 | **Kind**: instance method of [DualListbox](#module_dual-listbox..DualListbox) 193 | 194 | 195 | #### dualListbox.\_createButtons() 196 | Creates the buttons to add/remove the selected item. 197 | 198 | **Kind**: instance method of [DualListbox](#module_dual-listbox..DualListbox) 199 | -------------------------------------------------------------------------------- /dist/dual-listbox.js: -------------------------------------------------------------------------------- 1 | (()=>{var L="dual-listbox",_="dual-listbox__container",p="dual-listbox__available",b="dual-listbox__selected",r="dual-listbox__title",o="dual-listbox__item",v="dual-listbox__buttons",E="dual-listbox__button",c="dual-listbox__search",n="dual-listbox__item--selected",h="up",u="down",d=class{constructor(t,e={}){this.setDefaults(),this.dragged=null,this.options=[],d.isDomElement(t)?this.select=t:this.select=document.querySelector(t),this._initOptions(e),this._initReusableElements(),e.options!==void 0?this.options=e.options:this._splitOptions(this.select.options),this._buildDualListbox(this.select.parentNode),this._addActions(),this.showSortButtons&&this._initializeSortButtons(),this.redraw()}setDefaults(){this.availableTitle="Available options",this.selectedTitle="Selected options",this.showAddButton=!0,this.addButtonText="add",this.showRemoveButton=!0,this.removeButtonText="remove",this.showAddAllButton=!0,this.addAllButtonText="add all",this.showRemoveAllButton=!0,this.removeAllButtonText="remove all",this.searchPlaceholder="Search",this.showSortButtons=!1,this.sortFunction=(t,e)=>t.selected?-1:e.selected?1:t.ordere.order?1:0,this.upButtonText="up",this.downButtonText="down",this.enableDoubleClick=!0,this.draggable=!0}changeOrder(t,e){console.log(t);let s=this.options.findIndex(a=>(console.log(a,t.dataset.id),a.value===t.dataset.id));console.log(s);let i=this.options.splice(s,1);console.log(i),this.options.splice(e,0,i[0])}addOptions(t){t.forEach(e=>{this.addOption(e)})}addOption(t,e=null){e?this.options.splice(e,0,t):this.options.push(t)}addEventListener(t,e){this.dualListbox.addEventListener(t,e)}changeSelected(t){let e=this.options.find(s=>s.value===t.dataset.id);e.selected=!e.selected,this.redraw(),setTimeout(()=>{let s=document.createEvent("HTMLEvents");e.selected?(s.initEvent("added",!1,!0),s.addedElement=t):(s.initEvent("removed",!1,!0),s.removedElement=t),this.dualListbox.dispatchEvent(s)},0)}actionAllSelected(t){t&&t.preventDefault(),this.options.forEach(e=>e.selected=!0),this.redraw()}actionAllDeselected(t){t&&t.preventDefault(),this.options.forEach(e=>e.selected=!1),this.redraw()}redraw(){this.options.sort(this.sortFunction),this.updateAvailableListbox(),this.updateSelectedListbox(),this.syncSelect()}searchLists(t,e){let s=e.querySelectorAll(`.${o}`),i=t.toLowerCase();for(let a=0;a!t.selected))}updateSelectedListbox(){this._updateListbox(this.selectedList,this.options.filter(t=>t.selected))}syncSelect(){for(;this.select.firstChild;)this.select.removeChild(this.select.lastChild);this.options.forEach(t=>{let e=document.createElement("option");e.value=t.value,e.innerText=t.text,t.selected&&e.setAttribute("selected","selected"),this.select.appendChild(e)})}_updateListbox(t,e){for(;t.firstChild;)t.removeChild(t.firstChild);e.forEach(s=>{t.appendChild(this._createListItem(s))})}actionItemSelected(t){t.preventDefault();let e=this.availableList.querySelector(`.${n}`);e&&this.changeSelected(e)}actionItemDeselected(t){t.preventDefault();let e=this.selectedList.querySelector(`.${n}`);e&&this.changeSelected(e)}_actionItemDoubleClick(t,e=null){e&&(e.preventDefault(),e.stopPropagation()),this.enableDoubleClick&&this.changeSelected(t)}_actionItemClick(t,e,s=null){s&&s.preventDefault();let i=e.querySelectorAll(`.${o}`);for(let a=0;athis.actionAllSelected(t)),this.add_button.addEventListener("click",t=>this.actionItemSelected(t)),this.remove_button.addEventListener("click",t=>this.actionItemDeselected(t)),this.remove_all_button.addEventListener("click",t=>this.actionAllDeselected(t))}_addClickActions(t){return t.addEventListener("dblclick",e=>this._actionItemDoubleClick(t,e)),t.addEventListener("click",e=>this._actionItemClick(t,this.dualListbox,e)),t}_addSearchActions(){this.search_left.addEventListener("change",t=>this.searchLists(t.target.value,this.availableList)),this.search_left.addEventListener("keyup",t=>this.searchLists(t.target.value,this.availableList)),this.search_right.addEventListener("change",t=>this.searchLists(t.target.value,this.selectedList)),this.search_right.addEventListener("keyup",t=>this.searchLists(t.target.value,this.selectedList))}_buildDualListbox(t){this.select.style.display="none",this.dualListBoxContainer.appendChild(this._createList(this.search_left,this.availableListTitle,this.availableList)),this.dualListBoxContainer.appendChild(this.buttons),this.dualListBoxContainer.appendChild(this._createList(this.search_right,this.selectedListTitle,this.selectedList)),this.dualListbox.appendChild(this.dualListBoxContainer),t.insertBefore(this.dualListbox,this.select)}_createList(t,e,s){let i=document.createElement("div");return i.appendChild(t),i.appendChild(e),i.appendChild(s),i}_createButtons(){this.buttons=document.createElement("div"),this.buttons.classList.add(v),this.add_all_button=document.createElement("button"),this.add_all_button.innerHTML=this.addAllButtonText,this.add_button=document.createElement("button"),this.add_button.innerHTML=this.addButtonText,this.remove_button=document.createElement("button"),this.remove_button.innerHTML=this.removeButtonText,this.remove_all_button=document.createElement("button"),this.remove_all_button.innerHTML=this.removeAllButtonText;let t={showAddAllButton:this.add_all_button,showAddButton:this.add_button,showRemoveButton:this.remove_button,showRemoveAllButton:this.remove_all_button};for(let e in t)if(e){let s=this[e],i=t[e];i.setAttribute("type","button"),i.classList.add(E),s&&this.buttons.appendChild(i)}}_createListItem(t){let e=document.createElement("li");return e.classList.add(o),e.innerHTML=t.text,e.dataset.id=t.value,this._liListeners(e),this._addClickActions(e),this.draggable&&e.setAttribute("draggable","true"),e}_liListeners(t){t.addEventListener("dragstart",e=>{console.log("drag start",e),this.dragged=e.currentTarget,e.currentTarget.classList.add("dragging")}),t.addEventListener("dragend",e=>{e.currentTarget.classList.remove("dragging")}),t.addEventListener("dragover",e=>{e.preventDefault()},!1),t.addEventListener("dragenter",e=>{e.target.classList.add("drop-above")}),t.addEventListener("dragleave",e=>{e.target.classList.remove("drop-above")}),t.addEventListener("drop",e=>{e.preventDefault(),e.stopPropagation(),e.target.classList.remove("drop-above");let s=this.options.findIndex(i=>i.value===e.target.dataset.id);e.target.parentElement===this.dragged.parentElement?(this.changeOrder(this.dragged,s),this.redraw()):(this.changeSelected(this.dragged),this.changeOrder(this.dragged,s),this.redraw())})}_createSearchLeft(){this.search_left=document.createElement("input"),this.search_left.classList.add(c),this.search_left.placeholder=this.searchPlaceholder}_createSearchRight(){this.search_right=document.createElement("input"),this.search_right.classList.add(c),this.search_right.placeholder=this.searchPlaceholder}_createDragListeners(){[this.availableList,this.selectedList].forEach(t=>{t.addEventListener("dragover",e=>{e.preventDefault()},!1),t.addEventListener("dragenter",e=>{e.target.classList.add("drop-in")}),t.addEventListener("dragleave",e=>{e.target.classList.remove("drop-in")}),t.addEventListener("drop",e=>{e.preventDefault(),e.target.classList.remove("drop-in"),(t.classList.contains("dual-listbox__selected")||t.classList.contains("dual-listbox__available"))&&this.changeSelected(this.dragged)})})}_initOptions(t){for(let e in t)t.hasOwnProperty(e)&&(this[e]=t[e])}_initReusableElements(){this.dualListbox=document.createElement("div"),this.dualListbox.classList.add(L),this.select.id&&this.dualListbox.classList.add(this.select.id),this.dualListBoxContainer=document.createElement("div"),this.dualListBoxContainer.classList.add(_),this.availableList=document.createElement("ul"),this.availableList.classList.add(p),this.selectedList=document.createElement("ul"),this.selectedList.classList.add(b),this.availableListTitle=document.createElement("div"),this.availableListTitle.classList.add(r),this.availableListTitle.innerText=this.availableTitle,this.selectedListTitle=document.createElement("div"),this.selectedListTitle.classList.add(r),this.selectedListTitle.innerText=this.selectedTitle,this._createButtons(),this._createSearchLeft(),this._createSearchRight(),this.draggable&&setTimeout(()=>{this._createDragListeners()},10)}_splitOptions(t){[...t].forEach((e,s)=>{this.addOption({text:e.innerHTML,value:e.value,selected:e.attributes.selected||!1,order:s})})}_initializeSortButtons(){let t=document.createElement("button");t.classList.add("dual-listbox__button"),t.innerText=this.upButtonText,t.addEventListener("click",i=>this._onSortButtonClick(i,h));let e=document.createElement("button");e.classList.add("dual-listbox__button"),e.innerText=this.downButtonText,e.addEventListener("click",i=>this._onSortButtonClick(i,u));let s=document.createElement("div");s.classList.add("dual-listbox__buttons"),s.appendChild(t),s.appendChild(e),this.dualListBoxContainer.appendChild(s)}_onSortButtonClick(t,e){t.preventDefault();let s=this.dualListbox.querySelector(".dual-listbox__item--selected"),i=this.options.find(a=>a.value===s.dataset.id);if(s){let a=this._getNewIndex(s,e);a>=0&&(this.changeOrder(s,a),this.redraw())}}_getNewIndex(t,e){let s=this.options.findIndex(a=>a.value===t.dataset.id),i=s;return h===e?i-=1:u===e&&s 13 | `; 14 | 15 | const FIXTURE_FILLED_SELECT = ` 16 | 28 | `; 29 | 30 | const FIXTURE_FILLED_SELECT_PRESELECTED = ` 31 | 43 | `; 44 | 45 | const FIXTURE_FILLED_SELECT_WITH_ID = ` 46 | 58 | `; 59 | 60 | const FIXTURE_FILLED_SELECT_PRESELECTED_MULTIPLE = ` 61 | 73 | `; 74 | 75 | const OPTIONS_WITH_SELECTED_VALUE = [ 76 | { 77 | text: "option 1", 78 | value: "VAL1", 79 | }, 80 | { 81 | text: "option 2", 82 | value: "VAL2", 83 | }, 84 | { 85 | text: "option 3", 86 | value: "VAL3", 87 | selected: true, 88 | }, 89 | ]; 90 | 91 | test("should export a default", () => { 92 | expect(DualListbox).toBeTruthy(); 93 | }); 94 | 95 | test("should export a name", () => { 96 | expect(DualListbox2).toBeTruthy(); 97 | }); 98 | 99 | test("should be able to initialize an empty select", () => { 100 | document.body.innerHTML = FIXTURE_EMPTY_SELECT; 101 | let dlb = new DualListbox(`.${SELECT_CLASS}`); 102 | expect(dlb.options.length).toBe(0); 103 | expect(dlb.options.filter((option) => option.selected).length).toBe(0); 104 | }); 105 | 106 | test("should be able to initialize an empty select with additional items", () => { 107 | document.body.innerHTML = FIXTURE_EMPTY_SELECT; 108 | let dlb = new DualListbox(`.${SELECT_CLASS}`, { 109 | options: OPTIONS_WITH_SELECTED_VALUE, 110 | }); 111 | expect(dlb.options.length).toBe(3); 112 | expect(dlb.options.filter((option) => option.selected).length).toBe(1); 113 | }); 114 | 115 | test("should be able to initialize a filled select", () => { 116 | document.body.innerHTML = FIXTURE_FILLED_SELECT; 117 | let dlb = new DualListbox(`.${SELECT_CLASS}`); 118 | expect(dlb.options.length).toBe(10); 119 | expect(dlb.options.filter((option) => option.selected).length).toBe(0); 120 | }); 121 | 122 | test("should be able to initialize a filled select with preselected items", () => { 123 | document.body.innerHTML = FIXTURE_FILLED_SELECT_PRESELECTED; 124 | let dlb = new DualListbox(`.${SELECT_CLASS}`); 125 | expect(dlb.options.length).toBe(10); 126 | expect(dlb.options.filter((option) => option.selected).length).toBe(1); 127 | }); 128 | 129 | test("should be able to initialize a filled select with additional items", () => { 130 | document.body.innerHTML = FIXTURE_FILLED_SELECT_PRESELECTED; 131 | let dlb = new DualListbox(`.${SELECT_CLASS}`, { 132 | options: OPTIONS_WITH_SELECTED_VALUE, 133 | }); 134 | expect(dlb.options.length).toBe(3); 135 | expect(dlb.options.filter((option) => option.selected).length).toBe(1); 136 | }); 137 | 138 | test("should be able to initialize a filled select with id", () => { 139 | document.body.innerHTML = FIXTURE_FILLED_SELECT_WITH_ID; 140 | let dlb = new DualListbox(`#select`); 141 | expect(dlb.options.length).toBe(10); 142 | expect(dlb.options.filter((option) => option.selected).length).toBe(1); 143 | }); 144 | 145 | test("should be able to add a list item to selected", () => { 146 | document.body.innerHTML = FIXTURE_FILLED_SELECT; 147 | let dlb = new DualListbox(`.${SELECT_CLASS}`); 148 | let listItem = document.querySelector('[data-id="1"]'); 149 | dlb.changeSelected(listItem); 150 | expect(dlb.options.length).toBe(10); 151 | expect(dlb.options.filter((option) => option.selected).length).toBe(1); 152 | }); 153 | 154 | test("should be able to remove a list item from selected", () => { 155 | document.body.innerHTML = FIXTURE_FILLED_SELECT; 156 | let dlb = new DualListbox(`.${SELECT_CLASS}`); 157 | let listItem = document.querySelector('[data-id="1"]'); 158 | dlb.changeSelected(listItem); 159 | expect(dlb.options.length).toBe(10); 160 | expect(dlb.options.filter((option) => option.selected).length).toBe(1); 161 | 162 | dlb.changeSelected(listItem); 163 | expect(dlb.options.length).toBe(10); 164 | expect(dlb.options.filter((option) => option.selected).length).toBe(0); 165 | }); 166 | 167 | test("should be able to remove a list item from selected that is not selected", () => { 168 | document.body.innerHTML = FIXTURE_FILLED_SELECT; 169 | let dlb = new DualListbox(`.${SELECT_CLASS}`); 170 | let listItem = document.querySelector('[data-id="1"]'); 171 | dlb.changeSelected(listItem); 172 | expect(dlb.options.length).toBe(10); 173 | expect(dlb.options.filter((option) => option.selected).length).toBe(1); 174 | }); 175 | 176 | test("should be able to search the items", () => { 177 | document.body.innerHTML = FIXTURE_FILLED_SELECT; 178 | let query = "One"; 179 | let dlb = new DualListbox(`.${SELECT_CLASS}`); 180 | dlb.searchLists(query, dlb.dualListbox); 181 | expect(dlb.options.length).toBe(10); 182 | expect(dlb.options.filter((option) => option.selected).length).toBe(0); 183 | [...dlb.availableList.querySelectorAll(".dual-listbox__item")].forEach( 184 | (element) => { 185 | expect(element.style.display !== "none").toBe( 186 | element.innerHTML.toLowerCase().indexOf(query.toLowerCase()) >= 187 | 0 188 | ); 189 | } 190 | ); 191 | }); 192 | 193 | test("should be able to perform case insensitive search", () => { 194 | document.body.innerHTML = FIXTURE_FILLED_SELECT; 195 | let query = "tWO"; 196 | let dlb = new DualListbox(`.${SELECT_CLASS}`); 197 | dlb.searchLists(query, dlb.dualListbox); 198 | expect(dlb.options.length).toBe(10); 199 | expect(dlb.options.filter((option) => option.selected).length).toBe(0); 200 | [...dlb.availableList.querySelectorAll(".dual-listbox__item")].forEach( 201 | (element) => { 202 | expect(element.style.display !== "none").toBe( 203 | element.innerHTML.toLowerCase().indexOf(query.toLowerCase()) >= 204 | 0 205 | ); 206 | } 207 | ); 208 | }); 209 | 210 | test("should be able to search the items with no text", () => { 211 | document.body.innerHTML = FIXTURE_FILLED_SELECT; 212 | let dlb = new DualListbox(`.${SELECT_CLASS}`); 213 | dlb.searchLists("", dlb.dualListbox); 214 | expect(dlb.options.length).toBe(10); 215 | expect(dlb.options.filter((option) => option.selected).length).toBe(0); 216 | }); 217 | 218 | test("should be able to hit the addEvent callback", () => { 219 | document.body.innerHTML = FIXTURE_FILLED_SELECT; 220 | 221 | function addCallback(value) { 222 | expect(value).toBe("1"); 223 | } 224 | 225 | let dlb = new DualListbox(`.${SELECT_CLASS}`, { 226 | addEvent: addCallback, 227 | }); 228 | let listItem = document.querySelector('[data-id="1"]'); 229 | dlb.changeSelected(listItem); 230 | }); 231 | 232 | test("should be able to hit the removeEvent callback", () => { 233 | document.body.innerHTML = FIXTURE_FILLED_SELECT_PRESELECTED; 234 | 235 | function addCallback(value) { 236 | expect(value).toBe("2"); 237 | } 238 | 239 | let dlb = new DualListbox(`.${SELECT_CLASS}`, { 240 | removeEvent: addCallback, 241 | }); 242 | let listItem = document.querySelector('[data-id="2"]'); 243 | dlb.changeSelected(listItem); 244 | }); 245 | 246 | test("should be able to click on one of the elements", () => { 247 | document.body.innerHTML = FIXTURE_FILLED_SELECT; 248 | 249 | new DualListbox(`.${SELECT_CLASS}`); 250 | let listItem = document.querySelector('[data-id="2"]'); 251 | let clickEvent = document.createEvent("MouseEvents"); 252 | clickEvent.initEvent("click", true, true); 253 | listItem.dispatchEvent(clickEvent); 254 | 255 | let selectedItem = document.querySelector(".dual-listbox__item--selected"); 256 | expect(selectedItem).toBeTruthy(); 257 | }); 258 | 259 | test("should be able to click on one of the elements to remove the class", () => { 260 | document.body.innerHTML = FIXTURE_FILLED_SELECT; 261 | 262 | new DualListbox(`.${SELECT_CLASS}`); 263 | let listItem = document.querySelector('[data-id="2"]'); 264 | let clickEvent = document.createEvent("MouseEvents"); 265 | clickEvent.initEvent("click", true, true); 266 | listItem.dispatchEvent(clickEvent); 267 | 268 | let selectedItem = document.querySelector(".dual-listbox__item--selected"); 269 | expect(selectedItem).toBeTruthy(); 270 | 271 | listItem.dispatchEvent(clickEvent); 272 | 273 | let selectedItem2 = document.querySelector(".dual-listbox__item--selected"); 274 | expect(selectedItem2).toBeFalsy(); 275 | }); 276 | 277 | test("should be able to doubleclick on one of the elements to select", () => { 278 | document.body.innerHTML = FIXTURE_FILLED_SELECT; 279 | 280 | let dlb = new DualListbox(`.${SELECT_CLASS}`); 281 | expect(dlb.options.length).toBe(10); 282 | expect(dlb.options.filter((option) => option.selected).length).toBe(0); 283 | 284 | let listItem = document.querySelector('[data-id="2"]'); 285 | let clickEvent = document.createEvent("MouseEvents"); 286 | clickEvent.initEvent("dblclick", true, true); 287 | listItem.dispatchEvent(clickEvent); 288 | 289 | expect(dlb.options.length).toBe(10); 290 | expect(dlb.options.filter((option) => option.selected).length).toBe(1); 291 | }); 292 | 293 | test("should be able to doubleclick on one of the elements to deselect", () => { 294 | document.body.innerHTML = FIXTURE_FILLED_SELECT_PRESELECTED; 295 | 296 | let dlb = new DualListbox(`.${SELECT_CLASS}`); 297 | expect(dlb.options.length).toBe(10); 298 | expect(dlb.options.filter((option) => option.selected).length).toBe(1); 299 | 300 | let listItem = document.querySelector('[data-id="2"]'); 301 | let clickEvent = document.createEvent("MouseEvents"); 302 | clickEvent.initEvent("dblclick", true, true); 303 | listItem.dispatchEvent(clickEvent); 304 | 305 | expect(dlb.options.length).toBe(10); 306 | expect(dlb.options.filter((option) => option.selected).length).toBe(0); 307 | }); 308 | 309 | test("should be able fire search on change", () => { 310 | document.body.innerHTML = FIXTURE_FILLED_SELECT; 311 | 312 | let dlb = new DualListbox(`.${SELECT_CLASS}`); 313 | let search = document.querySelector(".dual-listbox__search"); 314 | let changeEvent = document.createEvent("MouseEvents"); 315 | changeEvent.initEvent("change", true, true); 316 | search.dispatchEvent(changeEvent); 317 | 318 | expect(dlb.options.length).toBe(10); 319 | expect(dlb.options.filter((option) => option.selected).length).toBe(0); 320 | }); 321 | 322 | test("should be able fire search with the keyboard", () => { 323 | document.body.innerHTML = FIXTURE_FILLED_SELECT; 324 | 325 | let dlb = new DualListbox(`.${SELECT_CLASS}`); 326 | let search = document.querySelector(".dual-listbox__search"); 327 | let keyupEvent = document.createEvent("MouseEvents"); 328 | keyupEvent.initEvent("keyup", true, true); 329 | search.dispatchEvent(keyupEvent); 330 | 331 | expect(dlb.options.length).toBe(10); 332 | expect(dlb.options.filter((option) => option.selected).length).toBe(0); 333 | }); 334 | 335 | test("should be able to create object from DOM element", () => { 336 | document.body.innerHTML = FIXTURE_FILLED_SELECT; 337 | let dlb = new DualListbox(document.body.getElementsByTagName("select")[0]); 338 | 339 | expect(dlb.options.length).toBe(10); 340 | expect(dlb.options.filter((option) => option.selected).length).toBe(0); 341 | }); 342 | 343 | test("should set the item to availeble.", () => { 344 | document.body.innerHTML = FIXTURE_FILLED_SELECT_PRESELECTED; 345 | 346 | let dlb = new DualListbox(document.body.getElementsByTagName("select")[0]); 347 | expect(dlb.options.length).toBe(10); 348 | expect(dlb.options.filter((option) => option.selected).length).toBe(1); 349 | 350 | dlb.selectedList 351 | .querySelector(".dual-listbox__item") 352 | .classList.add("dual-listbox__item--selected"); 353 | let event = {}; 354 | event.preventDefault = () => {}; 355 | dlb.actionItemDeselected(event); 356 | expect(dlb.options.length).toBe(10); 357 | expect(dlb.options.filter((option) => option.selected).length).toBe(0); 358 | }); 359 | 360 | test("should set the item to selected.", () => { 361 | document.body.innerHTML = FIXTURE_FILLED_SELECT_PRESELECTED; 362 | 363 | let dlb = new DualListbox(document.body.getElementsByTagName("select")[0]); 364 | expect(dlb.options.length).toBe(10); 365 | expect(dlb.options.filter((option) => option.selected).length).toBe(1); 366 | 367 | dlb.availableList 368 | .querySelector(".dual-listbox__item") 369 | .classList.add("dual-listbox__item--selected"); 370 | let event = {}; 371 | event.preventDefault = () => {}; 372 | dlb.actionItemSelected(event); 373 | expect(dlb.options.length).toBe(10); 374 | expect(dlb.options.filter((option) => option.selected).length).toBe(2); 375 | }); 376 | 377 | test("should only set the searched results to selected.", () => { 378 | document.body.innerHTML = FIXTURE_FILLED_SELECT; 379 | 380 | let dlb = new DualListbox(document.body.getElementsByTagName("select")[0]); 381 | expect(dlb.options.length).toBe(10); 382 | expect(dlb.options.filter((option) => option.selected).length).toBe(0); 383 | 384 | let event = {}; 385 | event.preventDefault = () => {}; 386 | dlb.searchLists("Four", dlb.dualListbox); 387 | dlb.actionAllSelected(event); 388 | expect(dlb.options.length).toBe(10); 389 | expect(dlb.options.filter((option) => option.selected).length).toBe(10); 390 | }); 391 | 392 | test("should only set the searched results to available.", () => { 393 | let domParent = document.createElement("div"); 394 | domParent.innerHTML = FIXTURE_FILLED_SELECT_PRESELECTED_MULTIPLE; 395 | 396 | let dlb = new DualListbox(domParent.getElementsByTagName("select")[0]); 397 | expect(dlb.options.length).toBe(10); 398 | expect(dlb.options.filter((option) => option.selected).length).toBe(3); 399 | 400 | let event = {}; 401 | event.preventDefault = () => {}; 402 | dlb.searchLists("Four", dlb.dualListbox); 403 | dlb.actionAllDeselected(event); 404 | expect(dlb.options.length).toBe(10); 405 | expect(dlb.options.filter((option) => option.selected).length).toBe(0); 406 | }); 407 | 408 | test("should be able to add the removed eventListener", (done) => { 409 | let domParent = document.createElement("div"); 410 | domParent.innerHTML = FIXTURE_FILLED_SELECT_PRESELECTED_MULTIPLE; 411 | 412 | let dlb = new DualListbox(domParent.getElementsByTagName("select")[0]); 413 | expect(dlb.options.length).toBe(10); 414 | expect(dlb.options.filter((option) => option.selected).length).toBe(3); 415 | 416 | dlb.addEventListener("removed", (event) => { 417 | expect(event.removedElement).toBeTruthy(); 418 | expect(event.removedElement.textContent).toBe("Two"); 419 | done(); 420 | }); 421 | 422 | let listItem = document.querySelector('[data-id="2"]'); 423 | dlb.changeSelected(listItem); 424 | }); 425 | 426 | // test("should be able to add the added eventListener", (done) => { 427 | // let domParent = document.createElement("div"); 428 | // domParent.innerHTML = FIXTURE_FILLED_SELECT_PRESELECTED_MULTIPLE; 429 | 430 | // let dlb = new DualListbox(domParent.getElementsByTagName("select")[0]); 431 | // expect(dlb.options.length).toBe(10); 432 | // expect(dlb.options.filter((option) => option.selected).length).toBe(3); 433 | 434 | // dlb.addEventListener("added", (event) => { 435 | // expect(event.addedElement).toBeTruthy(); 436 | // expect(event.addedElement.outerHTML).toBe( 437 | // '
  • One
  • ' 438 | // ); 439 | // done(); 440 | // }); 441 | 442 | // let listItem = document.querySelector('[data-id="1"]'); 443 | // dlb.changeSelected(listItem); 444 | // }); 445 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dual listbox example 5 | 6 | 7 | 8 | 12 | 16 | 17 | 18 | 19 | 20 |
    21 |
    22 |
    23 |

    Dual Listbox

    24 |

    25 | A better way to manage your multi select element. 26 |

    27 |
    28 |
    29 |
    30 | 31 |
    32 |
    33 |

    34 | Select by class 35 | source 36 |

    37 | 38 |
    <select class="select1" multiple>
     41 |     <option value="1">One</option>
     42 |     <option value="2">Two</option>
     43 |     <option value="3">Three</option>
     44 | </select>
     45 | 
     46 | <script>
     47 |     let dlb1 = new DualListbox('.select1');
     48 | </script>
    49 | 50 | 55 |
    56 |
    57 | 58 |
    59 |
    60 |

    61 | Add options and add eventListeners 62 | source 63 |

    64 | 65 |
    <select class="select2" multiple>
     68 |     <option value="1">One</option>
     69 |     <option value="2">Two</option>
     70 |     <option value="3">Three</option>
     71 | </select>
     72 | 
     73 | <script>
     74 |     let dlb2 = new DualListbox('.select2', {
     75 |         availableTitle:'Available numbers',
     76 |         selectedTitle: 'Selected numbers',
     77 |         addButtonText: '>',
     78 |         removeButtonText: '<',
     79 |         addAllButtonText: '>>',
     80 |         removeAllButtonText: '<<',
     81 |         searchPlaceholder: 'search numbers'
     82 |     });
     83 |     dlb2.addEventListener('added', function(event){
     84 |         console.log(event);
     85 |     });
     86 |     dlb2.addEventListener('removed', function(event){
     87 |         console.log(event);
     88 |     });
     89 | </script>
    90 | 91 |
    92 |

    Selected element:

    93 |
      94 |
    • Nothing yet
    • 95 |
    96 |
    97 | 98 | 103 |
    104 |
    105 | 106 |
    107 |
    108 |

    109 | Remove all the buttons from being rendered. 110 | source 111 |

    112 | 113 |
    <select class="select3" multiple>
    116 |     <option value="1">One</option>
    117 |     <option value="2">Two</option>
    118 |     <option value="3">Three</option>
    119 | </select>
    120 | 
    121 | <script>
    122 |     let dlb3 = new DualListbox('.select3', {
    123 |         showAddButton: false,
    124 |         showAddAllButton: false,
    125 |         showRemoveButton: false,
    126 |         showRemoveAllButton: false
    127 |     });
    128 | </script>
    129 | 130 | 135 |
    136 |
    137 | 138 |
    139 |
    140 |

    141 | Show the sort buttons 142 | source 143 |

    144 | 145 |
    <select class="select4" multiple>
    148 |     <option value="1">One</option>
    149 |     <option value="2">Two</option>
    150 |     <option value="3">Three</option>
    151 | </select>
    152 | 
    153 | <script>
    154 |     let dlb4 = new DualListbox('.select4', {
    155 |         showSortButtons: true,
    156 |     });
    157 | </script>
    158 | 159 | 170 |
    171 |
    172 | 173 |
    174 |
    175 |

    All options

    176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 200 | 201 | 202 | 203 | 204 | 205 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 243 | 244 | 245 | 246 | 247 | 248 | 252 | 253 | 254 | 255 | 256 | 257 | 261 | 262 | 263 | 264 | 265 | 266 | 270 | 271 | 272 | 273 | 274 | 275 | 279 | 280 | 281 | 282 | 283 | 284 | 288 | 289 | 290 | 291 | 292 | 293 | 297 | 298 | 299 | 300 | 301 | 302 | 306 | 307 | 308 | 309 | 310 | 311 | 315 | 316 | 317 | 318 | 321 | 328 | 332 | 333 | 334 | 335 | 338 | 339 | 343 | 344 | 345 |
    OptionDefaultExcepted valuesExplanation
    draggabletruebooleanIf the list items should be draggable.
    showSortButtonsfalseboolean 197 | If the sort buttons should be shown. (up and 198 | down) 199 |
    enableDoubleClicktrueboolean 206 | If double clicking a list items should change 207 | column. 208 |
    showAddButtontruebooleanIf the "add" button should be shown.
    showRemoveButtontruebooleanIf the "remove" button should be shown.
    showAddAllButtontruebooleanIf the "add all" button should be shown.
    showRemoveAllButtontruebooleanIf the "remove all" button should be shown.
    availableTitle"Available options"string 240 | The title that should be shown above the 241 | "Available options" 242 |
    selectedTitle"Selected options"string 249 | The title that should be shown above the 250 | "Selected options" 251 |
    addButtonText"add"string 258 | The text that should be displayed in the "add" 259 | button. 260 |
    removeButtonText"remove"string 267 | The text that should be displayed in the 268 | "remove" button. 269 |
    addAllButtonText"add all"string 276 | The text that should be displayed in the "add 277 | all" button. 278 |
    removeAllButtonText"remove all"string 285 | The text that should be displayed in the "remove 286 | all" button. 287 |
    searchPlaceholder"Search"string 294 | The text that should be displayed in the 295 | "search" fields. 296 |
    upButtonText"up"string 303 | The text that should be displayed in the "up" 304 | button. (only when sorting is enabled) 305 |
    downButtonText"down"string 312 | The text that should be displayed in the "down" 313 | button. (only when sorting is enabled) 314 |
    options 319 | undefined 320 | 322 | Array<{text:"text to display", value: "what 324 | the select value should be", selected: 325 | false, order: 1}> 327 | 329 | A list of options that should be added. This 330 | will overwrite the select options 331 |
    sortFunction 336 | Function 337 | Function 340 | A function to overwrite the default sorting that 341 | will be applied. 342 |
    346 |
    347 |
    348 | 349 |
    350 |
    351 |

    Public functions

    352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 |
    Function nameArgumentsExplanation
    changeOrderliItem, newPosition 365 | Change the order of the given list Element and 366 | the new position 367 |
    addOptionsoptionsAdd a single option to the options list.
    addOptionoption, index (optional) 378 | Add a single option to the options list. 379 | Optionally add the index. 380 |
    addEventListenereventName, callbackAdd an eventListener
    changeSelectedlistItem 391 | Change the selected state of the list element. 392 |
    actionAllSelectedevent (optional)Change all items to be selected.
    actionAllDeselectedevent (optional)Change all items to not be selected.
    redrawRedraw the lists.
    412 |
    413 |
    414 | 415 | 433 | 434 | 435 | 436 | 480 | 481 | 482 | -------------------------------------------------------------------------------- /src/dual-listbox.js: -------------------------------------------------------------------------------- 1 | const MAIN_BLOCK = "dual-listbox"; 2 | 3 | const CONTAINER_ELEMENT = "dual-listbox__container"; 4 | const AVAILABLE_ELEMENT = "dual-listbox__available"; 5 | const SELECTED_ELEMENT = "dual-listbox__selected"; 6 | const TITLE_ELEMENT = "dual-listbox__title"; 7 | const ITEM_ELEMENT = "dual-listbox__item"; 8 | const BUTTONS_ELEMENT = "dual-listbox__buttons"; 9 | const BUTTON_ELEMENT = "dual-listbox__button"; 10 | const SEARCH_ELEMENT = "dual-listbox__search"; 11 | 12 | const SELECTED_MODIFIER = "dual-listbox__item--selected"; 13 | 14 | const DIRECTION_UP = "up"; 15 | const DIRECTION_DOWN = "down"; 16 | 17 | /** 18 | * Dual select interface allowing the user to select items from a list of provided options. 19 | * @class 20 | */ 21 | class DualListbox { 22 | constructor(selector, options = {}) { 23 | this.setDefaults(); 24 | this.dragged = null; 25 | this.options = []; 26 | 27 | if (DualListbox.isDomElement(selector)) { 28 | this.select = selector; 29 | } else { 30 | this.select = document.querySelector(selector); 31 | } 32 | 33 | this._initOptions(options); 34 | this._initReusableElements(); 35 | if (options.options !== undefined) { 36 | this.options = options.options; 37 | } else { 38 | this._splitOptions(this.select.options); 39 | } 40 | 41 | this._buildDualListbox(this.select.parentNode); 42 | this._addActions(); 43 | 44 | if (this.showSortButtons) { 45 | this._initializeSortButtons(); 46 | } 47 | 48 | this.redraw(); 49 | } 50 | 51 | /** 52 | * Sets the default values that can be overwritten. 53 | */ 54 | setDefaults() { 55 | this.availableTitle = "Available options"; 56 | this.selectedTitle = "Selected options"; 57 | 58 | this.showAddButton = true; 59 | this.addButtonText = "add"; 60 | 61 | this.showRemoveButton = true; 62 | this.removeButtonText = "remove"; 63 | 64 | this.showAddAllButton = true; 65 | this.addAllButtonText = "add all"; 66 | 67 | this.showRemoveAllButton = true; 68 | this.removeAllButtonText = "remove all"; 69 | 70 | this.searchPlaceholder = "Search"; 71 | 72 | this.showSortButtons = false; 73 | this.sortFunction = (a, b) => { 74 | if (a.selected) { 75 | return -1; 76 | } 77 | if (b.selected) { 78 | return 1; 79 | } 80 | if (a.order < b.order) { 81 | return -1; 82 | } 83 | if (a.order > b.order) { 84 | return 1; 85 | } 86 | return 0; 87 | }; 88 | this.upButtonText = "up"; 89 | this.downButtonText = "down"; 90 | 91 | this.enableDoubleClick = true; 92 | this.draggable = true; 93 | } 94 | 95 | changeOrder(liItem, newPosition) { 96 | console.log(liItem); 97 | const index = this.options.findIndex((option) => { 98 | console.log(option, liItem.dataset.id); 99 | return option.value === liItem.dataset.id; 100 | }); 101 | console.log(index); 102 | const cutOptions = this.options.splice(index, 1); 103 | console.log(cutOptions); 104 | this.options.splice(newPosition, 0, cutOptions[0]); 105 | } 106 | 107 | addOptions(options) { 108 | options.forEach((option) => { 109 | this.addOption(option); 110 | }); 111 | } 112 | 113 | addOption(option, index = null) { 114 | if (index) { 115 | this.options.splice(index, 0, option); 116 | } else { 117 | this.options.push(option); 118 | } 119 | } 120 | 121 | /** 122 | * Add eventListener to the dualListbox element. 123 | * 124 | * @param {String} eventName 125 | * @param {function} callback 126 | */ 127 | addEventListener(eventName, callback) { 128 | this.dualListbox.addEventListener(eventName, callback); 129 | } 130 | 131 | /** 132 | * Add the listItem to the selected list. 133 | * 134 | * @param {NodeElement} listItem 135 | */ 136 | changeSelected(listItem) { 137 | const changeOption = this.options.find( 138 | (option) => option.value === listItem.dataset.id 139 | ); 140 | changeOption.selected = !changeOption.selected; 141 | this.redraw(); 142 | 143 | setTimeout(() => { 144 | let event = document.createEvent("HTMLEvents"); 145 | if (changeOption.selected) { 146 | event.initEvent("added", false, true); 147 | event.addedElement = listItem; 148 | } else { 149 | event.initEvent("removed", false, true); 150 | event.removedElement = listItem; 151 | } 152 | 153 | this.dualListbox.dispatchEvent(event); 154 | }, 0); 155 | } 156 | 157 | actionAllSelected(event) { 158 | if (event) { 159 | event.preventDefault(); 160 | } 161 | this.options.forEach((option) => (option.selected = true)); 162 | this.redraw(); 163 | } 164 | 165 | actionAllDeselected(event) { 166 | if (event) { 167 | event.preventDefault(); 168 | } 169 | this.options.forEach((option) => (option.selected = false)); 170 | this.redraw(); 171 | } 172 | 173 | /** 174 | * Redraws the Dual listbox content 175 | */ 176 | redraw() { 177 | this.options.sort(this.sortFunction); 178 | 179 | this.updateAvailableListbox(); 180 | this.updateSelectedListbox(); 181 | this.syncSelect(); 182 | } 183 | 184 | /** 185 | * Filters the listboxes with the given searchString. 186 | * 187 | * @param {Object} searchString 188 | * @param dualListbox 189 | */ 190 | searchLists(searchString, dualListbox) { 191 | let items = dualListbox.querySelectorAll(`.${ITEM_ELEMENT}`); 192 | let lowerCaseSearchString = searchString.toLowerCase(); 193 | 194 | for (let i = 0; i < items.length; i++) { 195 | let item = items[i]; 196 | if ( 197 | item.textContent 198 | .toLowerCase() 199 | .indexOf(lowerCaseSearchString) === -1 200 | ) { 201 | item.style.display = "none"; 202 | } else { 203 | item.style.display = "list-item"; 204 | } 205 | } 206 | } 207 | 208 | /** 209 | * Update the elements in the available listbox; 210 | */ 211 | updateAvailableListbox() { 212 | this._updateListbox( 213 | this.availableList, 214 | this.options.filter((option) => !option.selected) 215 | ); 216 | } 217 | 218 | /** 219 | * Update the elements in the selected listbox; 220 | */ 221 | updateSelectedListbox() { 222 | this._updateListbox( 223 | this.selectedList, 224 | this.options.filter((option) => option.selected) 225 | ); 226 | } 227 | 228 | syncSelect() { 229 | while (this.select.firstChild) { 230 | this.select.removeChild(this.select.lastChild); 231 | } 232 | 233 | this.options.forEach((option) => { 234 | let optionElement = document.createElement("option"); 235 | optionElement.value = option.value; 236 | optionElement.innerText = option.text; 237 | if (option.selected) { 238 | optionElement.setAttribute("selected", "selected"); 239 | } 240 | this.select.appendChild(optionElement); 241 | }); 242 | } 243 | 244 | // 245 | // 246 | // PRIVATE FUNCTIONS 247 | // 248 | // 249 | 250 | /** 251 | * Update the elements in the listbox; 252 | */ 253 | _updateListbox(list, options) { 254 | while (list.firstChild) { 255 | list.removeChild(list.firstChild); 256 | } 257 | 258 | options.forEach((option) => { 259 | list.appendChild(this._createListItem(option)); 260 | }); 261 | } 262 | 263 | /** 264 | * Action to set one listItem to selected. 265 | */ 266 | actionItemSelected(event) { 267 | event.preventDefault(); 268 | 269 | let selected = this.availableList.querySelector( 270 | `.${SELECTED_MODIFIER}` 271 | ); 272 | if (selected) { 273 | this.changeSelected(selected); 274 | } 275 | } 276 | 277 | /** 278 | * Action to set one listItem to available. 279 | */ 280 | actionItemDeselected(event) { 281 | event.preventDefault(); 282 | 283 | let selected = this.selectedList.querySelector(`.${SELECTED_MODIFIER}`); 284 | if (selected) { 285 | this.changeSelected(selected); 286 | } 287 | } 288 | 289 | /** 290 | * Action when double clicked on a listItem. 291 | */ 292 | _actionItemDoubleClick(listItem, event = null) { 293 | if (event) { 294 | event.preventDefault(); 295 | event.stopPropagation(); 296 | } 297 | if (this.enableDoubleClick) this.changeSelected(listItem); 298 | } 299 | 300 | /** 301 | * Action when single clicked on a listItem. 302 | */ 303 | _actionItemClick(listItem, dualListbox, event = null) { 304 | if (event) { 305 | event.preventDefault(); 306 | } 307 | 308 | let items = dualListbox.querySelectorAll(`.${ITEM_ELEMENT}`); 309 | 310 | for (let i = 0; i < items.length; i++) { 311 | let value = items[i]; 312 | if (value !== listItem) { 313 | value.classList.remove(SELECTED_MODIFIER); 314 | } 315 | } 316 | 317 | if (listItem.classList.contains(SELECTED_MODIFIER)) { 318 | listItem.classList.remove(SELECTED_MODIFIER); 319 | } else { 320 | listItem.classList.add(SELECTED_MODIFIER); 321 | } 322 | } 323 | 324 | /** 325 | * @Private 326 | * Adds the needed actions to the elements. 327 | */ 328 | _addActions() { 329 | this._addButtonActions(); 330 | this._addSearchActions(); 331 | } 332 | 333 | /** 334 | * Adds the actions to the buttons that are created. 335 | */ 336 | _addButtonActions() { 337 | this.add_all_button.addEventListener("click", (event) => 338 | this.actionAllSelected(event) 339 | ); 340 | this.add_button.addEventListener("click", (event) => 341 | this.actionItemSelected(event) 342 | ); 343 | this.remove_button.addEventListener("click", (event) => 344 | this.actionItemDeselected(event) 345 | ); 346 | this.remove_all_button.addEventListener("click", (event) => 347 | this.actionAllDeselected(event) 348 | ); 349 | } 350 | 351 | /** 352 | * Adds the click items to the listItem. 353 | * 354 | * @param {Object} listItem 355 | */ 356 | _addClickActions(listItem) { 357 | listItem.addEventListener("dblclick", (event) => 358 | this._actionItemDoubleClick(listItem, event) 359 | ); 360 | listItem.addEventListener("click", (event) => 361 | this._actionItemClick(listItem, this.dualListbox, event) 362 | ); 363 | return listItem; 364 | } 365 | 366 | /** 367 | * @Private 368 | * Adds the actions to the search input. 369 | */ 370 | _addSearchActions() { 371 | this.search_left.addEventListener("change", (event) => 372 | this.searchLists(event.target.value, this.availableList) 373 | ); 374 | this.search_left.addEventListener("keyup", (event) => 375 | this.searchLists(event.target.value, this.availableList) 376 | ); 377 | this.search_right.addEventListener("change", (event) => 378 | this.searchLists(event.target.value, this.selectedList) 379 | ); 380 | this.search_right.addEventListener("keyup", (event) => 381 | this.searchLists(event.target.value, this.selectedList) 382 | ); 383 | } 384 | 385 | /** 386 | * @Private 387 | * Builds the Dual listbox and makes it visible to the user. 388 | */ 389 | _buildDualListbox(container) { 390 | this.select.style.display = "none"; 391 | 392 | this.dualListBoxContainer.appendChild( 393 | this._createList( 394 | this.search_left, 395 | this.availableListTitle, 396 | this.availableList 397 | ) 398 | ); 399 | this.dualListBoxContainer.appendChild(this.buttons); 400 | this.dualListBoxContainer.appendChild( 401 | this._createList( 402 | this.search_right, 403 | this.selectedListTitle, 404 | this.selectedList 405 | ) 406 | ); 407 | 408 | this.dualListbox.appendChild(this.dualListBoxContainer); 409 | 410 | container.insertBefore(this.dualListbox, this.select); 411 | } 412 | 413 | /** 414 | * Creates list with the header. 415 | */ 416 | _createList(search, header, list) { 417 | let result = document.createElement("div"); 418 | result.appendChild(search); 419 | result.appendChild(header); 420 | result.appendChild(list); 421 | return result; 422 | } 423 | 424 | /** 425 | * Creates the buttons to add/remove the selected item. 426 | */ 427 | _createButtons() { 428 | this.buttons = document.createElement("div"); 429 | this.buttons.classList.add(BUTTONS_ELEMENT); 430 | 431 | this.add_all_button = document.createElement("button"); 432 | this.add_all_button.innerHTML = this.addAllButtonText; 433 | 434 | this.add_button = document.createElement("button"); 435 | this.add_button.innerHTML = this.addButtonText; 436 | 437 | this.remove_button = document.createElement("button"); 438 | this.remove_button.innerHTML = this.removeButtonText; 439 | 440 | this.remove_all_button = document.createElement("button"); 441 | this.remove_all_button.innerHTML = this.removeAllButtonText; 442 | 443 | const options = { 444 | showAddAllButton: this.add_all_button, 445 | showAddButton: this.add_button, 446 | showRemoveButton: this.remove_button, 447 | showRemoveAllButton: this.remove_all_button, 448 | }; 449 | 450 | for (let optionName in options) { 451 | if (optionName) { 452 | const option = this[optionName]; 453 | const button = options[optionName]; 454 | 455 | button.setAttribute("type", "button"); 456 | button.classList.add(BUTTON_ELEMENT); 457 | 458 | if (option) { 459 | this.buttons.appendChild(button); 460 | } 461 | } 462 | } 463 | } 464 | 465 | /** 466 | * @Private 467 | * Creates the listItem out of the option. 468 | */ 469 | _createListItem(option) { 470 | let listItem = document.createElement("li"); 471 | 472 | listItem.classList.add(ITEM_ELEMENT); 473 | listItem.innerHTML = option.text; 474 | listItem.dataset.id = option.value; 475 | 476 | this._liListeners(listItem); 477 | this._addClickActions(listItem); 478 | 479 | if (this.draggable) { 480 | listItem.setAttribute("draggable", "true"); 481 | } 482 | 483 | return listItem; 484 | } 485 | 486 | _liListeners(li) { 487 | li.addEventListener("dragstart", (event) => { 488 | // store a ref. on the dragged elem 489 | console.log("drag start", event); 490 | this.dragged = event.currentTarget; 491 | event.currentTarget.classList.add("dragging"); 492 | }); 493 | li.addEventListener("dragend", (event) => { 494 | event.currentTarget.classList.remove("dragging"); 495 | }); 496 | 497 | // For changing the order 498 | li.addEventListener( 499 | "dragover", 500 | (event) => { 501 | // Allow the drop event to be emitted for the dropzone. 502 | event.preventDefault(); 503 | }, 504 | false 505 | ); 506 | 507 | li.addEventListener("dragenter", (event) => { 508 | event.target.classList.add("drop-above"); 509 | }); 510 | 511 | li.addEventListener("dragleave", (event) => { 512 | event.target.classList.remove("drop-above"); 513 | }); 514 | 515 | li.addEventListener("drop", (event) => { 516 | event.preventDefault(); 517 | event.stopPropagation(); 518 | event.target.classList.remove("drop-above"); 519 | let newIndex = this.options.findIndex( 520 | (option) => option.value === event.target.dataset.id 521 | ); 522 | if (event.target.parentElement === this.dragged.parentElement) { 523 | this.changeOrder(this.dragged, newIndex); 524 | this.redraw(); 525 | } else { 526 | this.changeSelected(this.dragged); 527 | this.changeOrder(this.dragged, newIndex); 528 | this.redraw(); 529 | } 530 | }); 531 | } 532 | 533 | /** 534 | * @Private 535 | * Creates the search input. 536 | */ 537 | _createSearchLeft() { 538 | this.search_left = document.createElement("input"); 539 | this.search_left.classList.add(SEARCH_ELEMENT); 540 | this.search_left.placeholder = this.searchPlaceholder; 541 | } 542 | 543 | /** 544 | * @Private 545 | * Creates the search input. 546 | */ 547 | _createSearchRight() { 548 | this.search_right = document.createElement("input"); 549 | this.search_right.classList.add(SEARCH_ELEMENT); 550 | this.search_right.placeholder = this.searchPlaceholder; 551 | } 552 | 553 | /** 554 | * @Private 555 | * Create drag and drop listeners 556 | */ 557 | _createDragListeners() { 558 | [this.availableList, this.selectedList].forEach((dropzone) => { 559 | dropzone.addEventListener( 560 | "dragover", 561 | (event) => { 562 | // Allow the drop event to be emitted for the dropzone. 563 | event.preventDefault(); 564 | }, 565 | false 566 | ); 567 | 568 | dropzone.addEventListener("dragenter", (event) => { 569 | event.target.classList.add("drop-in"); 570 | }); 571 | 572 | dropzone.addEventListener("dragleave", (event) => { 573 | event.target.classList.remove("drop-in"); 574 | }); 575 | 576 | dropzone.addEventListener("drop", (event) => { 577 | event.preventDefault(); 578 | 579 | event.target.classList.remove("drop-in"); 580 | if ( 581 | dropzone.classList.contains("dual-listbox__selected") || 582 | dropzone.classList.contains("dual-listbox__available") 583 | ) { 584 | this.changeSelected(this.dragged); 585 | } 586 | }); 587 | }); 588 | } 589 | 590 | /** 591 | * @Private 592 | * Set the option variables to this. 593 | */ 594 | _initOptions(options) { 595 | for (let key in options) { 596 | if (options.hasOwnProperty(key)) { 597 | this[key] = options[key]; 598 | } 599 | } 600 | } 601 | 602 | /** 603 | * @Private 604 | * Creates all the static elements for the Dual listbox. 605 | */ 606 | _initReusableElements() { 607 | this.dualListbox = document.createElement("div"); 608 | this.dualListbox.classList.add(MAIN_BLOCK); 609 | if (this.select.id) { 610 | this.dualListbox.classList.add(this.select.id); 611 | } 612 | 613 | this.dualListBoxContainer = document.createElement("div"); 614 | this.dualListBoxContainer.classList.add(CONTAINER_ELEMENT); 615 | 616 | this.availableList = document.createElement("ul"); 617 | this.availableList.classList.add(AVAILABLE_ELEMENT); 618 | 619 | this.selectedList = document.createElement("ul"); 620 | this.selectedList.classList.add(SELECTED_ELEMENT); 621 | 622 | this.availableListTitle = document.createElement("div"); 623 | this.availableListTitle.classList.add(TITLE_ELEMENT); 624 | this.availableListTitle.innerText = this.availableTitle; 625 | 626 | this.selectedListTitle = document.createElement("div"); 627 | this.selectedListTitle.classList.add(TITLE_ELEMENT); 628 | this.selectedListTitle.innerText = this.selectedTitle; 629 | 630 | this._createButtons(); 631 | this._createSearchLeft(); 632 | this._createSearchRight(); 633 | if (this.draggable) { 634 | setTimeout(() => { 635 | this._createDragListeners(); 636 | }, 10); 637 | } 638 | } 639 | 640 | /** 641 | * @Private 642 | * Splits the options and places them in the correct list. 643 | */ 644 | _splitOptions(options) { 645 | [...options].forEach((option, index) => { 646 | this.addOption({ 647 | text: option.innerHTML, 648 | value: option.value, 649 | selected: option.attributes.selected || false, 650 | order: index, 651 | }); 652 | }); 653 | } 654 | 655 | /** 656 | * @private 657 | * @return {void} 658 | */ 659 | _initializeSortButtons() { 660 | const sortUpButton = document.createElement("button"); 661 | sortUpButton.classList.add("dual-listbox__button"); 662 | sortUpButton.innerText = this.upButtonText; 663 | sortUpButton.addEventListener("click", (event) => 664 | this._onSortButtonClick(event, DIRECTION_UP) 665 | ); 666 | 667 | const sortDownButton = document.createElement("button"); 668 | sortDownButton.classList.add("dual-listbox__button"); 669 | sortDownButton.innerText = this.downButtonText; 670 | sortDownButton.addEventListener("click", (event) => 671 | this._onSortButtonClick(event, DIRECTION_DOWN) 672 | ); 673 | 674 | const buttonContainer = document.createElement("div"); 675 | buttonContainer.classList.add("dual-listbox__buttons"); 676 | buttonContainer.appendChild(sortUpButton); 677 | buttonContainer.appendChild(sortDownButton); 678 | 679 | this.dualListBoxContainer.appendChild(buttonContainer); 680 | } 681 | 682 | /** 683 | * @private 684 | * @param {MouseEvent} event 685 | * @param {string} direction 686 | * @return {void} 687 | */ 688 | _onSortButtonClick(event, direction) { 689 | event.preventDefault(); 690 | 691 | const selected = this.dualListbox.querySelector( 692 | ".dual-listbox__item--selected" 693 | ); 694 | const option = this.options.find( 695 | (option) => option.value === selected.dataset.id 696 | ); 697 | if (selected) { 698 | const newIndex = this._getNewIndex(selected, direction); 699 | if (newIndex >= 0) { 700 | this.changeOrder(selected, newIndex); 701 | this.redraw(); 702 | } 703 | } 704 | } 705 | 706 | /** 707 | * Returns an array where the first element is the old index of the currently 708 | * selected item in the right box and the second element is the new index. 709 | * 710 | * @private 711 | * @param {string} direction 712 | * @return {int[]} 713 | */ 714 | _getNewIndex(selected, direction) { 715 | const oldIndex = this.options.findIndex( 716 | (option) => option.value === selected.dataset.id 717 | ); 718 | 719 | let newIndex = oldIndex; 720 | if (DIRECTION_UP === direction) { 721 | newIndex -= 1; 722 | } else if ( 723 | DIRECTION_DOWN === direction && 724 | oldIndex < selected.length - 1 725 | ) { 726 | newIndex += 1; 727 | } 728 | 729 | return newIndex; 730 | } 731 | 732 | /** 733 | * @Private 734 | * Returns true if argument is a DOM element 735 | */ 736 | static isDomElement(o) { 737 | return typeof HTMLElement === "object" 738 | ? o instanceof HTMLElement //DOM2 739 | : o && 740 | typeof o === "object" && 741 | o !== null && 742 | o.nodeType === 1 && 743 | typeof o.nodeName === "string"; 744 | } 745 | } 746 | 747 | window.DualListbox = DualListbox; 748 | export default DualListbox; 749 | export { DualListbox }; 750 | -------------------------------------------------------------------------------- /dist/dual-listbox.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": ["../src/dual-listbox.js"], 4 | "sourcesContent": ["const MAIN_BLOCK = \"dual-listbox\";\n\nconst CONTAINER_ELEMENT = \"dual-listbox__container\";\nconst AVAILABLE_ELEMENT = \"dual-listbox__available\";\nconst SELECTED_ELEMENT = \"dual-listbox__selected\";\nconst TITLE_ELEMENT = \"dual-listbox__title\";\nconst ITEM_ELEMENT = \"dual-listbox__item\";\nconst BUTTONS_ELEMENT = \"dual-listbox__buttons\";\nconst BUTTON_ELEMENT = \"dual-listbox__button\";\nconst SEARCH_ELEMENT = \"dual-listbox__search\";\n\nconst SELECTED_MODIFIER = \"dual-listbox__item--selected\";\n\nconst DIRECTION_UP = \"up\";\nconst DIRECTION_DOWN = \"down\";\n\n/**\n * Dual select interface allowing the user to select items from a list of provided options.\n * @class\n */\nclass DualListbox {\n constructor(selector, options = {}) {\n this.setDefaults();\n this.dragged = null;\n this.options = [];\n\n if (DualListbox.isDomElement(selector)) {\n this.select = selector;\n } else {\n this.select = document.querySelector(selector);\n }\n\n this._initOptions(options);\n this._initReusableElements();\n if (options.options !== undefined) {\n this.options = options.options;\n } else {\n this._splitOptions(this.select.options);\n }\n\n this._buildDualListbox(this.select.parentNode);\n this._addActions();\n\n if (this.showSortButtons) {\n this._initializeSortButtons();\n }\n\n this.redraw();\n }\n\n /**\n * Sets the default values that can be overwritten.\n */\n setDefaults() {\n this.availableTitle = \"Available options\";\n this.selectedTitle = \"Selected options\";\n\n this.showAddButton = true;\n this.addButtonText = \"add\";\n\n this.showRemoveButton = true;\n this.removeButtonText = \"remove\";\n\n this.showAddAllButton = true;\n this.addAllButtonText = \"add all\";\n\n this.showRemoveAllButton = true;\n this.removeAllButtonText = \"remove all\";\n\n this.searchPlaceholder = \"Search\";\n\n this.showSortButtons = false;\n this.sortFunction = (a, b) => {\n if (a.selected) {\n return -1;\n }\n if (b.selected) {\n return 1;\n }\n if (a.order < b.order) {\n return -1;\n }\n if (a.order > b.order) {\n return 1;\n }\n return 0;\n };\n this.upButtonText = \"up\";\n this.downButtonText = \"down\";\n\n this.enableDoubleClick = true;\n this.draggable = true;\n }\n\n changeOrder(liItem, newPosition) {\n console.log(liItem);\n const index = this.options.findIndex((option) => {\n console.log(option, liItem.dataset.id);\n return option.value === liItem.dataset.id;\n });\n console.log(index);\n const cutOptions = this.options.splice(index, 1);\n console.log(cutOptions);\n this.options.splice(newPosition, 0, cutOptions[0]);\n }\n\n addOptions(options) {\n options.forEach((option) => {\n this.addOption(option);\n });\n }\n\n addOption(option, index = null) {\n if (index) {\n this.options.splice(index, 0, option);\n } else {\n this.options.push(option);\n }\n }\n\n /**\n * Add eventListener to the dualListbox element.\n *\n * @param {String} eventName\n * @param {function} callback\n */\n addEventListener(eventName, callback) {\n this.dualListbox.addEventListener(eventName, callback);\n }\n\n /**\n * Add the listItem to the selected list.\n *\n * @param {NodeElement} listItem\n */\n changeSelected(listItem) {\n const changeOption = this.options.find(\n (option) => option.value === listItem.dataset.id\n );\n changeOption.selected = !changeOption.selected;\n this.redraw();\n\n setTimeout(() => {\n let event = document.createEvent(\"HTMLEvents\");\n if (changeOption.selected) {\n event.initEvent(\"added\", false, true);\n event.addedElement = listItem;\n } else {\n event.initEvent(\"removed\", false, true);\n event.removedElement = listItem;\n }\n\n this.dualListbox.dispatchEvent(event);\n }, 0);\n }\n\n actionAllSelected(event) {\n if (event) {\n event.preventDefault();\n }\n this.options.forEach((option) => (option.selected = true));\n this.redraw();\n }\n\n actionAllDeselected(event) {\n if (event) {\n event.preventDefault();\n }\n this.options.forEach((option) => (option.selected = false));\n this.redraw();\n }\n\n /**\n * Redraws the Dual listbox content\n */\n redraw() {\n this.options.sort(this.sortFunction);\n\n this.updateAvailableListbox();\n this.updateSelectedListbox();\n this.syncSelect();\n }\n\n /**\n * Filters the listboxes with the given searchString.\n *\n * @param {Object} searchString\n * @param dualListbox\n */\n searchLists(searchString, dualListbox) {\n let items = dualListbox.querySelectorAll(`.${ITEM_ELEMENT}`);\n let lowerCaseSearchString = searchString.toLowerCase();\n\n for (let i = 0; i < items.length; i++) {\n let item = items[i];\n if (\n item.textContent\n .toLowerCase()\n .indexOf(lowerCaseSearchString) === -1\n ) {\n item.style.display = \"none\";\n } else {\n item.style.display = \"list-item\";\n }\n }\n }\n\n /**\n * Update the elements in the available listbox;\n */\n updateAvailableListbox() {\n this._updateListbox(\n this.availableList,\n this.options.filter((option) => !option.selected)\n );\n }\n\n /**\n * Update the elements in the selected listbox;\n */\n updateSelectedListbox() {\n this._updateListbox(\n this.selectedList,\n this.options.filter((option) => option.selected)\n );\n }\n\n syncSelect() {\n while (this.select.firstChild) {\n this.select.removeChild(this.select.lastChild);\n }\n\n this.options.forEach((option) => {\n let optionElement = document.createElement(\"option\");\n optionElement.value = option.value;\n optionElement.innerText = option.text;\n if (option.selected) {\n optionElement.setAttribute(\"selected\", \"selected\");\n }\n this.select.appendChild(optionElement);\n });\n }\n\n //\n //\n // PRIVATE FUNCTIONS\n //\n //\n\n /**\n * Update the elements in the listbox;\n */\n _updateListbox(list, options) {\n while (list.firstChild) {\n list.removeChild(list.firstChild);\n }\n\n options.forEach((option) => {\n list.appendChild(this._createListItem(option));\n });\n }\n\n /**\n * Action to set one listItem to selected.\n */\n actionItemSelected(event) {\n event.preventDefault();\n\n let selected = this.availableList.querySelector(\n `.${SELECTED_MODIFIER}`\n );\n if (selected) {\n this.changeSelected(selected);\n }\n }\n\n /**\n * Action to set one listItem to available.\n */\n actionItemDeselected(event) {\n event.preventDefault();\n\n let selected = this.selectedList.querySelector(`.${SELECTED_MODIFIER}`);\n if (selected) {\n this.changeSelected(selected);\n }\n }\n\n /**\n * Action when double clicked on a listItem.\n */\n _actionItemDoubleClick(listItem, event = null) {\n if (event) {\n event.preventDefault();\n event.stopPropagation();\n }\n if (this.enableDoubleClick) this.changeSelected(listItem);\n }\n\n /**\n * Action when single clicked on a listItem.\n */\n _actionItemClick(listItem, dualListbox, event = null) {\n if (event) {\n event.preventDefault();\n }\n\n let items = dualListbox.querySelectorAll(`.${ITEM_ELEMENT}`);\n\n for (let i = 0; i < items.length; i++) {\n let value = items[i];\n if (value !== listItem) {\n value.classList.remove(SELECTED_MODIFIER);\n }\n }\n\n if (listItem.classList.contains(SELECTED_MODIFIER)) {\n listItem.classList.remove(SELECTED_MODIFIER);\n } else {\n listItem.classList.add(SELECTED_MODIFIER);\n }\n }\n\n /**\n * @Private\n * Adds the needed actions to the elements.\n */\n _addActions() {\n this._addButtonActions();\n this._addSearchActions();\n }\n\n /**\n * Adds the actions to the buttons that are created.\n */\n _addButtonActions() {\n this.add_all_button.addEventListener(\"click\", (event) =>\n this.actionAllSelected(event)\n );\n this.add_button.addEventListener(\"click\", (event) =>\n this.actionItemSelected(event)\n );\n this.remove_button.addEventListener(\"click\", (event) =>\n this.actionItemDeselected(event)\n );\n this.remove_all_button.addEventListener(\"click\", (event) =>\n this.actionAllDeselected(event)\n );\n }\n\n /**\n * Adds the click items to the listItem.\n *\n * @param {Object} listItem\n */\n _addClickActions(listItem) {\n listItem.addEventListener(\"dblclick\", (event) =>\n this._actionItemDoubleClick(listItem, event)\n );\n listItem.addEventListener(\"click\", (event) =>\n this._actionItemClick(listItem, this.dualListbox, event)\n );\n return listItem;\n }\n\n /**\n * @Private\n * Adds the actions to the search input.\n */\n _addSearchActions() {\n this.search_left.addEventListener(\"change\", (event) =>\n this.searchLists(event.target.value, this.availableList)\n );\n this.search_left.addEventListener(\"keyup\", (event) =>\n this.searchLists(event.target.value, this.availableList)\n );\n this.search_right.addEventListener(\"change\", (event) =>\n this.searchLists(event.target.value, this.selectedList)\n );\n this.search_right.addEventListener(\"keyup\", (event) =>\n this.searchLists(event.target.value, this.selectedList)\n );\n }\n\n /**\n * @Private\n * Builds the Dual listbox and makes it visible to the user.\n */\n _buildDualListbox(container) {\n this.select.style.display = \"none\";\n\n this.dualListBoxContainer.appendChild(\n this._createList(\n this.search_left,\n this.availableListTitle,\n this.availableList\n )\n );\n this.dualListBoxContainer.appendChild(this.buttons);\n this.dualListBoxContainer.appendChild(\n this._createList(\n this.search_right,\n this.selectedListTitle,\n this.selectedList\n )\n );\n\n this.dualListbox.appendChild(this.dualListBoxContainer);\n\n container.insertBefore(this.dualListbox, this.select);\n }\n\n /**\n * Creates list with the header.\n */\n _createList(search, header, list) {\n let result = document.createElement(\"div\");\n result.appendChild(search);\n result.appendChild(header);\n result.appendChild(list);\n return result;\n }\n\n /**\n * Creates the buttons to add/remove the selected item.\n */\n _createButtons() {\n this.buttons = document.createElement(\"div\");\n this.buttons.classList.add(BUTTONS_ELEMENT);\n\n this.add_all_button = document.createElement(\"button\");\n this.add_all_button.innerHTML = this.addAllButtonText;\n\n this.add_button = document.createElement(\"button\");\n this.add_button.innerHTML = this.addButtonText;\n\n this.remove_button = document.createElement(\"button\");\n this.remove_button.innerHTML = this.removeButtonText;\n\n this.remove_all_button = document.createElement(\"button\");\n this.remove_all_button.innerHTML = this.removeAllButtonText;\n\n const options = {\n showAddAllButton: this.add_all_button,\n showAddButton: this.add_button,\n showRemoveButton: this.remove_button,\n showRemoveAllButton: this.remove_all_button,\n };\n\n for (let optionName in options) {\n if (optionName) {\n const option = this[optionName];\n const button = options[optionName];\n\n button.setAttribute(\"type\", \"button\");\n button.classList.add(BUTTON_ELEMENT);\n\n if (option) {\n this.buttons.appendChild(button);\n }\n }\n }\n }\n\n /**\n * @Private\n * Creates the listItem out of the option.\n */\n _createListItem(option) {\n let listItem = document.createElement(\"li\");\n\n listItem.classList.add(ITEM_ELEMENT);\n listItem.innerHTML = option.text;\n listItem.dataset.id = option.value;\n\n this._liListeners(listItem);\n this._addClickActions(listItem);\n\n if (this.draggable) {\n listItem.setAttribute(\"draggable\", \"true\");\n }\n\n return listItem;\n }\n\n _liListeners(li) {\n li.addEventListener(\"dragstart\", (event) => {\n // store a ref. on the dragged elem\n console.log(\"drag start\", event);\n this.dragged = event.currentTarget;\n event.currentTarget.classList.add(\"dragging\");\n });\n li.addEventListener(\"dragend\", (event) => {\n event.currentTarget.classList.remove(\"dragging\");\n });\n\n // For changing the order\n li.addEventListener(\n \"dragover\",\n (event) => {\n // Allow the drop event to be emitted for the dropzone.\n event.preventDefault();\n },\n false\n );\n\n li.addEventListener(\"dragenter\", (event) => {\n event.target.classList.add(\"drop-above\");\n });\n\n li.addEventListener(\"dragleave\", (event) => {\n event.target.classList.remove(\"drop-above\");\n });\n\n li.addEventListener(\"drop\", (event) => {\n event.preventDefault();\n event.stopPropagation();\n event.target.classList.remove(\"drop-above\");\n let newIndex = this.options.findIndex(\n (option) => option.value === event.target.dataset.id\n );\n if (event.target.parentElement === this.dragged.parentElement) {\n this.changeOrder(this.dragged, newIndex);\n this.redraw();\n } else {\n this.changeSelected(this.dragged);\n this.changeOrder(this.dragged, newIndex);\n this.redraw();\n }\n });\n }\n\n /**\n * @Private\n * Creates the search input.\n */\n _createSearchLeft() {\n this.search_left = document.createElement(\"input\");\n this.search_left.classList.add(SEARCH_ELEMENT);\n this.search_left.placeholder = this.searchPlaceholder;\n }\n\n /**\n * @Private\n * Creates the search input.\n */\n _createSearchRight() {\n this.search_right = document.createElement(\"input\");\n this.search_right.classList.add(SEARCH_ELEMENT);\n this.search_right.placeholder = this.searchPlaceholder;\n }\n\n /**\n * @Private\n * Create drag and drop listeners\n */\n _createDragListeners() {\n [this.availableList, this.selectedList].forEach((dropzone) => {\n dropzone.addEventListener(\n \"dragover\",\n (event) => {\n // Allow the drop event to be emitted for the dropzone.\n event.preventDefault();\n },\n false\n );\n\n dropzone.addEventListener(\"dragenter\", (event) => {\n event.target.classList.add(\"drop-in\");\n });\n\n dropzone.addEventListener(\"dragleave\", (event) => {\n event.target.classList.remove(\"drop-in\");\n });\n\n dropzone.addEventListener(\"drop\", (event) => {\n event.preventDefault();\n\n event.target.classList.remove(\"drop-in\");\n if (\n dropzone.classList.contains(\"dual-listbox__selected\") ||\n dropzone.classList.contains(\"dual-listbox__available\")\n ) {\n this.changeSelected(this.dragged);\n }\n });\n });\n }\n\n /**\n * @Private\n * Set the option variables to this.\n */\n _initOptions(options) {\n for (let key in options) {\n if (options.hasOwnProperty(key)) {\n this[key] = options[key];\n }\n }\n }\n\n /**\n * @Private\n * Creates all the static elements for the Dual listbox.\n */\n _initReusableElements() {\n this.dualListbox = document.createElement(\"div\");\n this.dualListbox.classList.add(MAIN_BLOCK);\n if (this.select.id) {\n this.dualListbox.classList.add(this.select.id);\n }\n\n this.dualListBoxContainer = document.createElement(\"div\");\n this.dualListBoxContainer.classList.add(CONTAINER_ELEMENT);\n\n this.availableList = document.createElement(\"ul\");\n this.availableList.classList.add(AVAILABLE_ELEMENT);\n\n this.selectedList = document.createElement(\"ul\");\n this.selectedList.classList.add(SELECTED_ELEMENT);\n\n this.availableListTitle = document.createElement(\"div\");\n this.availableListTitle.classList.add(TITLE_ELEMENT);\n this.availableListTitle.innerText = this.availableTitle;\n\n this.selectedListTitle = document.createElement(\"div\");\n this.selectedListTitle.classList.add(TITLE_ELEMENT);\n this.selectedListTitle.innerText = this.selectedTitle;\n\n this._createButtons();\n this._createSearchLeft();\n this._createSearchRight();\n if (this.draggable) {\n setTimeout(() => {\n this._createDragListeners();\n }, 10);\n }\n }\n\n /**\n * @Private\n * Splits the options and places them in the correct list.\n */\n _splitOptions(options) {\n [...options].forEach((option, index) => {\n this.addOption({\n text: option.innerHTML,\n value: option.value,\n selected: option.attributes.selected || false,\n order: index,\n });\n });\n }\n\n /**\n * @private\n * @return {void}\n */\n _initializeSortButtons() {\n const sortUpButton = document.createElement(\"button\");\n sortUpButton.classList.add(\"dual-listbox__button\");\n sortUpButton.innerText = this.upButtonText;\n sortUpButton.addEventListener(\"click\", (event) =>\n this._onSortButtonClick(event, DIRECTION_UP)\n );\n\n const sortDownButton = document.createElement(\"button\");\n sortDownButton.classList.add(\"dual-listbox__button\");\n sortDownButton.innerText = this.downButtonText;\n sortDownButton.addEventListener(\"click\", (event) =>\n this._onSortButtonClick(event, DIRECTION_DOWN)\n );\n\n const buttonContainer = document.createElement(\"div\");\n buttonContainer.classList.add(\"dual-listbox__buttons\");\n buttonContainer.appendChild(sortUpButton);\n buttonContainer.appendChild(sortDownButton);\n\n this.dualListBoxContainer.appendChild(buttonContainer);\n }\n\n /**\n * @private\n * @param {MouseEvent} event\n * @param {string} direction\n * @return {void}\n */\n _onSortButtonClick(event, direction) {\n event.preventDefault();\n\n const selected = this.dualListbox.querySelector(\n \".dual-listbox__item--selected\"\n );\n const option = this.options.find(\n (option) => option.value === selected.dataset.id\n );\n if (selected) {\n const newIndex = this._getNewIndex(selected, direction);\n if (newIndex >= 0) {\n this.changeOrder(selected, newIndex);\n this.redraw();\n }\n }\n }\n\n /**\n * Returns an array where the first element is the old index of the currently\n * selected item in the right box and the second element is the new index.\n *\n * @private\n * @param {string} direction\n * @return {int[]}\n */\n _getNewIndex(selected, direction) {\n const oldIndex = this.options.findIndex(\n (option) => option.value === selected.dataset.id\n );\n\n let newIndex = oldIndex;\n if (DIRECTION_UP === direction) {\n newIndex -= 1;\n } else if (\n DIRECTION_DOWN === direction &&\n oldIndex < selected.length - 1\n ) {\n newIndex += 1;\n }\n\n return newIndex;\n }\n\n /**\n * @Private\n * Returns true if argument is a DOM element\n */\n static isDomElement(o) {\n return typeof HTMLElement === \"object\"\n ? o instanceof HTMLElement //DOM2\n : o &&\n typeof o === \"object\" &&\n o !== null &&\n o.nodeType === 1 &&\n typeof o.nodeName === \"string\";\n }\n}\n\nwindow.DualListbox = DualListbox;\nexport default DualListbox;\nexport { DualListbox };\n"], 5 | "mappings": "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", 6 | "names": [] 7 | } 8 | --------------------------------------------------------------------------------