├── .gitignore ├── .hsdoc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bower.json ├── dist ├── css │ ├── select-theme-chosen.css │ ├── select-theme-dark.css │ └── select-theme-default.css └── js │ ├── select.js │ └── select.min.js ├── docs ├── examples │ └── simple │ │ └── index.html ├── intro.md └── welcome │ ├── coffee │ └── welcome.coffee │ ├── css │ ├── prism.css │ └── welcome.css │ ├── index.html │ ├── js │ ├── jquery.min.js │ └── welcome.js │ └── sass │ └── welcome.sass ├── gulpfile.js ├── package.json └── src ├── css ├── _checkmark.scss ├── _checkmark.svg ├── _no-mobile-tap-highlight.sass ├── _select.sass ├── _user-select.sass ├── select-theme-chosen.sass ├── select-theme-dark.sass └── select-theme-default.sass └── js └── select.js /.gitignore: -------------------------------------------------------------------------------- 1 | .sass-cache 2 | node_modules/ 3 | bower_components/ 4 | -------------------------------------------------------------------------------- /.hsdoc: -------------------------------------------------------------------------------- 1 | title: "select" 2 | description: "Styleable select elements built on Tether." 3 | source: "coffee/select.coffee" 4 | examples: "**/*.md" 5 | assets: "{bower_components/*,dist/js/*.js,dist/css/*.css,docs/css/*.css,docs/js/*,js,docs/welcome/*,examples/*}" 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.0.0 2 | - Bump `Tether` to v1 3 | 4 | ## v1.0.0 5 | - Add proper UMD to `Select` 6 | - Convert from `Coffeescript` to `ES6 (Babel)` 7 | - Fix `*.json` files to include `main` 8 | - Remove bundled `select.js` 9 | - Restructure directory layout 10 | - Update `gulp` builds 11 | - Update `tether` dependency to `v0.7.2` 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | You will need: 4 | 5 | - Node.js/io.js & npm 6 | - Bower 7 | - Gulp 8 | 9 | 10 | ## Getting started 11 | 12 | 1. Fork the project 13 | 2. Clone your forked project by running `git clone git@github.com:{ 14 | YOUR_USERNAME }/select.git` 15 | 3. Run `npm install` to install both node modules and bower components 16 | 4. Test that you can build the source by moving/renaming the existing `dist` 17 | directory and running `npm run build` 18 | 5. Assuming everything went well, you should now have a `dist` directory that 19 | matches the one you moved in step 4. 20 | 21 | 22 | ## Writing code! 23 | 24 | We use `gulp` to facilitate things like transpilation, minification, etc. so 25 | can focus on writing relevant code. If there is a fix or feature you would like 26 | to contribute, we ask that you take the following steps: 27 | 28 | 1. Most of the _editable_ code lives in the `src` directory while built code 29 | will end up in the `dist` directory upon running `npm run build`. 30 | 31 | 2. Depending on how big your changes are, bump the version numbers appropriately 32 | in `bower.json` and `package.json`. We try to follow semver, so a good rule 33 | of thumb for how to bump the version is: 34 | - A fix to existing code, perform a patch bump e.g. x.x.0 -> x.x.1 35 | - New feature, perform a minor bump e.g. x.0.x -> x.1.x 36 | - Breaking changes such a rewrite, perform a major bump e.g. 37 | 1.x.x -> 2.x.x 38 | 39 | Versioning is hard, so just use good judgement and we'll be more than happy 40 | to help out. 41 | 42 | __NOTE__: There is a `gulp` task that will automate some of the versioning. 43 | You can run `gulp version:{type}` where type is `patch|minor|major` to 44 | update both `bower.json` and `package.json` as well as add the appropriate 45 | git tag. 46 | 47 | 3. Provide a thoughtful commit message and push your changes to your fork using 48 | `git push origin master` (assuming your forked project is using `origin` for 49 | the remote name and you are on the `master` branch). 50 | 51 | 4. Open a Pull Request on GitHub with a description of your changes. 52 | 53 | 54 | ## Testing 55 | 56 | Work in progress. We are hoping to add some tests, so if you would like to help 57 | us get started, feel free to contact us through the Issues or open a Pull 58 | Request. 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 HubSpot, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Select 2 | 3 | [![GitHub 4 | version](https://badge.fury.io/gh/HubSpot%2Fselect.svg)](http://badge.fury.io/gh/HubSpot%2Fselect) 5 | 6 | Select.js is a Javascript and CSS library for creating styleable select elements. It aims to reproduce the behavior of native controls as much as is possible, while allowing for complete styling with CSS. 7 | 8 | 9 | ## Install 10 | 11 | __Dependencies__ 12 | 13 | * __[Tether](https://github.com/HubSpot/tether)__ 14 | 15 | Installing via `npm` and `bower` will bring in the above dependencies as well. 16 | 17 | 18 | __npm__ 19 | ```sh 20 | $ npm install tether-select 21 | ``` 22 | 23 | __bower__ 24 | ```sh 25 | $ bower install tether-select 26 | ``` 27 | 28 | ## Usage 29 | 30 | ```javascript 31 | let selectInstance = new Select({ 32 | el: document.querySelector('select.select-target'), 33 | className: 'select-theme-default' 34 | }) 35 | ``` 36 | 37 | [API Documentation](http://github.hubspot.com/select) 38 | 39 | [Demo](http://github.hubspot.com/select/docs/welcome) 40 | 41 | 42 | ## Contributing 43 | 44 | We encourage contributions of all kinds. If you would like to contribute in some way, please review our [guidelines for contributing](CONTRIBUTING.md). 45 | 46 | 47 | ## License 48 | Copyright © 2015 HubSpot - [MIT License](LICENSE) 49 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tether-select", 3 | "version": "1.1.1", 4 | "homepage": "https://github.com/HubSpot/select", 5 | "authors": [ 6 | "Zack Bloom ", 7 | "Adam Schwartz " 8 | ], 9 | "maintainers": [ 10 | "Nicholas Hwang " 11 | ], 12 | "description": "Styleable select elements built on Tether", 13 | "keywords": [ 14 | "select", 15 | "options", 16 | "javascript", 17 | "css" 18 | ], 19 | "license": "MIT", 20 | "main": "dist/js/select.js", 21 | "ignore": [ 22 | "**/.*", 23 | "node_modules", 24 | "bower_components", 25 | "test", 26 | "tests" 27 | ], 28 | "dependencies": { 29 | "tether": "~1.0.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /dist/css/select-theme-chosen.css: -------------------------------------------------------------------------------- 1 | .select-select { 2 | display: none; 3 | /* For when we are on a small touch device and want to use native controls */ 4 | pointer-events: none; 5 | position: absolute; 6 | opacity: 0; } 7 | 8 | .select-element, .select-element:after, .select-element:before, .select-element *, .select-element *:after, .select-element *:before { 9 | box-sizing: border-box; } 10 | 11 | .select-element { 12 | position: absolute; 13 | display: none; } 14 | .select-element.select-open { 15 | display: block; } 16 | 17 | .select-theme-chosen { 18 | font-family: "Helvetica Neue", sans-serif; 19 | font-size: 13px; } 20 | .select-theme-chosen, .select-theme-chosen *, .select-theme-chosen *:after, .select-theme-chosen *:before { 21 | box-sizing: border-box; } 22 | 23 | .select.select-theme-chosen { 24 | -webkit-user-select: none; 25 | -moz-user-select: none; 26 | -ms-user-select: none; 27 | -o-user-select: none; 28 | user-select: none; } 29 | .select.select-theme-chosen .select-content { 30 | border-radius: 5px; 31 | box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), 0 2px 8px rgba(0, 0, 0, 0.2); 32 | background: #fff; 33 | color: #444; 34 | overflow: auto; 35 | max-width: 248px; 36 | max-height: 248px; 37 | -webkit-overflow-scrolling: touch; } 38 | @media (max-width: 372px), (max-height: 372px) { 39 | .select.select-theme-chosen .select-content { 40 | max-width: 155px; 41 | max-height: 155px; } } 42 | .select.select-theme-chosen .select-options { 43 | -webkit-tap-highlight-color: transparent; 44 | -webkit-touch-callout: none; 45 | margin: 0; 46 | padding: 0; } 47 | .select.select-theme-chosen .select-options .select-option { 48 | -webkit-tap-highlight-color: transparent; 49 | -webkit-touch-callout: none; 50 | position: relative; 51 | list-style: none; 52 | margin: 0; 53 | line-height: 19px; 54 | padding: 6px 11px 6px 30px; 55 | display: block; 56 | cursor: pointer; 57 | white-space: nowrap; 58 | overflow: hidden; 59 | text-overflow: ellipsis; } 60 | .select.select-theme-chosen .select-options .select-option.select-option-selected:before { 61 | content: url("data:image/svg+xml;utf8,"); 62 | position: absolute; 63 | left: 13px; 64 | top: 0; 65 | top: 5px; 66 | height: 11px; 67 | width: 11px; 68 | margin: auto; } 69 | .select.select-theme-chosen .select-options .select-option:hover, .select.select-theme-chosen .select-options .select-option.select-option-highlight { 70 | background-image: -webkit-linear-gradient(#3875D7 20%, #2A62BC 90%); 71 | background-image: linear-gradient(#3875D7 20%, #2A62BC 90%); 72 | background-color: #3875D7; 73 | color: #fff; } 74 | .select.select-theme-chosen .select-options .select-option:hover.select-option-selected:before, .select.select-theme-chosen .select-options .select-option.select-option-highlight.select-option-selected:before { 75 | content: url("data:image/svg+xml;utf8,"); } 76 | .select.select-theme-chosen .select-options .select-option:first-child { 77 | border-radius: 5px 5px 0 0; } 78 | .select.select-theme-chosen .select-options .select-option:last-child { 79 | border-radius: 0 0 5px 5px; } 80 | 81 | .select-target.select-theme-chosen { 82 | display: inline-block; 83 | vertical-align: middle; 84 | *vertical-align: auto; 85 | *zoom: 1; 86 | *display: inline; 87 | -webkit-user-select: none; 88 | -moz-user-select: none; 89 | -ms-user-select: none; 90 | -o-user-select: none; 91 | user-select: none; 92 | -webkit-tap-highlight-color: transparent; 93 | -webkit-touch-callout: none; 94 | border-radius: 5px; 95 | box-shadow: 0 0 3px #FFF inset, 0 1px 1px rgba(0, 0, 0, 0.1); 96 | background-image: -webkit-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%); 97 | background-image: linear-gradient(to bottom, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%); 98 | position: relative; 99 | padding: 3px 30px 2px 11px; 100 | background: #f6f6f6; 101 | border: 1px solid #aaa; 102 | cursor: pointer; 103 | color: #444; 104 | text-decoration: none; 105 | white-space: nowrap; 106 | max-width: 100%; 107 | overflow: hidden; 108 | text-overflow: ellipsis; 109 | line-height: 24px; } 110 | .select-target.select-theme-chosen.select-target-focused, .select-target.select-theme-chosen.select-target-focused:focus { 111 | border-color: #5897FB; 112 | outline: none; } 113 | .select-target.select-theme-chosen b { 114 | position: absolute; 115 | right: 13px; 116 | top: 0; 117 | bottom: 1px; 118 | margin: auto; 119 | height: 16px; 120 | width: 26px; } 121 | .select-target.select-theme-chosen b:before, .select-target.select-theme-chosen b:after { 122 | content: ""; 123 | display: block; 124 | position: absolute; 125 | margin: auto; 126 | right: 0; 127 | height: 0; 128 | width: 0; 129 | border: 3px solid transparent; } 130 | .select-target.select-theme-chosen b:before { 131 | top: 0; 132 | border-bottom-color: inherit; } 133 | .select-target.select-theme-chosen b:after { 134 | bottom: 0; 135 | border-top-color: inherit; } 136 | -------------------------------------------------------------------------------- /dist/css/select-theme-dark.css: -------------------------------------------------------------------------------- 1 | .select-select { 2 | display: none; 3 | /* For when we are on a small touch device and want to use native controls */ 4 | pointer-events: none; 5 | position: absolute; 6 | opacity: 0; } 7 | 8 | .select-element, .select-element:after, .select-element:before, .select-element *, .select-element *:after, .select-element *:before { 9 | box-sizing: border-box; } 10 | 11 | .select-element { 12 | position: absolute; 13 | display: none; } 14 | .select-element.select-open { 15 | display: block; } 16 | 17 | .select-theme-dark, .select-theme-dark *, .select-theme-dark *:after, .select-theme-dark *:before { 18 | box-sizing: border-box; } 19 | 20 | .select.select-theme-dark { 21 | -webkit-user-select: none; 22 | -moz-user-select: none; 23 | -ms-user-select: none; 24 | -o-user-select: none; 25 | user-select: none; } 26 | .select.select-theme-dark .select-content { 27 | border-radius: .25em; 28 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); 29 | background: #252525; 30 | color: #b9b9b9; 31 | font-family: inherit; 32 | overflow: auto; 33 | max-width: 18rem; 34 | max-height: 18rem; 35 | -webkit-overflow-scrolling: touch; } 36 | @media (max-width: 27rem), (max-height: 27rem) { 37 | .select.select-theme-dark .select-content { 38 | max-width: 11.25rem; 39 | max-height: 11.25rem; } } 40 | .select.select-theme-dark .select-options { 41 | -webkit-tap-highlight-color: transparent; 42 | -webkit-touch-callout: none; 43 | margin: 0; 44 | padding: 0; } 45 | .select.select-theme-dark .select-options .select-option { 46 | -webkit-tap-highlight-color: transparent; 47 | -webkit-touch-callout: none; 48 | position: relative; 49 | list-style: none; 50 | margin: 0; 51 | line-height: 1.25rem; 52 | padding: 0.5rem 1em 0.5rem 2.5em; 53 | display: block; 54 | cursor: pointer; 55 | white-space: nowrap; 56 | overflow: hidden; 57 | text-overflow: ellipsis; } 58 | .select.select-theme-dark .select-options .select-option.select-option-selected:before { 59 | content: url("data:image/svg+xml;utf8,"); 60 | position: absolute; 61 | left: 1em; 62 | top: 0; 63 | bottom: .2em; 64 | height: 1em; 65 | width: 1em; 66 | margin: auto; } 67 | .select.select-theme-dark .select-options .select-option:hover, .select.select-theme-dark .select-options .select-option.select-option-highlight { 68 | background: #63a2f1; 69 | color: #fff; } 70 | .select.select-theme-dark .select-options .select-option:hover.select-option-selected:before, .select.select-theme-dark .select-options .select-option.select-option-highlight.select-option-selected:before { 71 | content: url("data:image/svg+xml;utf8,"); } 72 | .select.select-theme-dark .select-options .select-option:first-child { 73 | border-radius: 0.25em 0.25em 0 0; } 74 | .select.select-theme-dark .select-options .select-option:last-child { 75 | border-radius: 0 0 0.25em 0.25em; } 76 | 77 | .select-target.select-theme-dark { 78 | display: inline-block; 79 | vertical-align: middle; 80 | *vertical-align: auto; 81 | *zoom: 1; 82 | *display: inline; 83 | -webkit-user-select: none; 84 | -moz-user-select: none; 85 | -ms-user-select: none; 86 | -o-user-select: none; 87 | user-select: none; 88 | -webkit-tap-highlight-color: transparent; 89 | -webkit-touch-callout: none; 90 | border-radius: .25em; 91 | position: relative; 92 | padding: 0.5rem 3em 0.5rem 1em; 93 | background: #252525; 94 | border: .18em solid #151515; 95 | cursor: pointer; 96 | color: #b9b9b9; 97 | text-decoration: none; 98 | white-space: nowrap; 99 | max-width: 100%; 100 | overflow: hidden; 101 | text-overflow: ellipsis; } 102 | .select-target.select-theme-dark:hover { 103 | border-color: #000; 104 | color: #fff; } 105 | .select-target.select-theme-dark.select-target-focused, .select-target.select-theme-dark.select-target-focused:focus { 106 | border-color: #63a2f1; 107 | outline: none; } 108 | .select-target.select-theme-dark b { 109 | position: absolute; 110 | right: 1em; 111 | top: 0; 112 | bottom: 0; 113 | margin: auto; 114 | height: 1.25rem; 115 | width: 2em; } 116 | .select-target.select-theme-dark b:before, .select-target.select-theme-dark b:after { 117 | content: ""; 118 | display: block; 119 | position: absolute; 120 | margin: auto; 121 | right: 0; 122 | height: 0; 123 | width: 0; 124 | border: .263em solid transparent; } 125 | .select-target.select-theme-dark b:before { 126 | top: 0; 127 | border-bottom-color: inherit; } 128 | .select-target.select-theme-dark b:after { 129 | bottom: 0; 130 | border-top-color: inherit; } 131 | -------------------------------------------------------------------------------- /dist/css/select-theme-default.css: -------------------------------------------------------------------------------- 1 | .select-select { 2 | display: none; 3 | /* For when we are on a small touch device and want to use native controls */ 4 | pointer-events: none; 5 | position: absolute; 6 | opacity: 0; } 7 | 8 | .select-element, .select-element:after, .select-element:before, .select-element *, .select-element *:after, .select-element *:before { 9 | box-sizing: border-box; } 10 | 11 | .select-element { 12 | position: absolute; 13 | display: none; } 14 | .select-element.select-open { 15 | display: block; } 16 | 17 | .select-theme-default, .select-theme-default *, .select-theme-default *:after, .select-theme-default *:before { 18 | box-sizing: border-box; } 19 | 20 | .select.select-theme-default { 21 | -webkit-user-select: none; 22 | -moz-user-select: none; 23 | -ms-user-select: none; 24 | -o-user-select: none; 25 | user-select: none; } 26 | .select.select-theme-default .select-content { 27 | border-radius: .25em; 28 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); 29 | background: #fff; 30 | font-family: inherit; 31 | color: inherit; 32 | overflow: auto; 33 | max-width: 18rem; 34 | max-height: 18rem; 35 | -webkit-overflow-scrolling: touch; } 36 | @media (max-width: 27rem), (max-height: 27rem) { 37 | .select.select-theme-default .select-content { 38 | max-width: 11.25rem; 39 | max-height: 11.25rem; } } 40 | .select.select-theme-default .select-options { 41 | -webkit-tap-highlight-color: transparent; 42 | -webkit-touch-callout: none; 43 | margin: 0; 44 | padding: 0; } 45 | .select.select-theme-default .select-options .select-option { 46 | -webkit-tap-highlight-color: transparent; 47 | -webkit-touch-callout: none; 48 | position: relative; 49 | list-style: none; 50 | margin: 0; 51 | line-height: 1.25rem; 52 | padding: 0.5rem 1em 0.5rem 2.5em; 53 | display: block; 54 | cursor: pointer; 55 | white-space: nowrap; 56 | overflow: hidden; 57 | text-overflow: ellipsis; } 58 | .select.select-theme-default .select-options .select-option.select-option-selected:before { 59 | content: url("data:image/svg+xml;utf8,"); 60 | position: absolute; 61 | left: 1em; 62 | top: 0; 63 | bottom: .2em; 64 | height: 1em; 65 | width: 1em; 66 | margin: auto; } 67 | .select.select-theme-default .select-options .select-option:hover, .select.select-theme-default .select-options .select-option.select-option-highlight { 68 | background: #63a2f1; 69 | color: #fff; } 70 | .select.select-theme-default .select-options .select-option:hover.select-option-selected:before, .select.select-theme-default .select-options .select-option.select-option-highlight.select-option-selected:before { 71 | content: url("data:image/svg+xml;utf8,"); } 72 | .select.select-theme-default .select-options .select-option:first-child { 73 | border-radius: 0.25em 0.25em 0 0; } 74 | .select.select-theme-default .select-options .select-option:last-child { 75 | border-radius: 0 0 0.25em 0.25em; } 76 | 77 | .select-target.select-theme-default { 78 | display: inline-block; 79 | vertical-align: middle; 80 | *vertical-align: auto; 81 | *zoom: 1; 82 | *display: inline; 83 | -webkit-user-select: none; 84 | -moz-user-select: none; 85 | -ms-user-select: none; 86 | -o-user-select: none; 87 | user-select: none; 88 | -webkit-tap-highlight-color: transparent; 89 | -webkit-touch-callout: none; 90 | border-radius: .25em; 91 | position: relative; 92 | padding: 0.5rem 3em 0.5rem 1em; 93 | background: #f6f6f6; 94 | border: .18em solid #ddd; 95 | cursor: pointer; 96 | color: #444; 97 | text-decoration: none; 98 | white-space: nowrap; 99 | max-width: 100%; 100 | overflow: hidden; 101 | text-overflow: ellipsis; } 102 | .select-target.select-theme-default:hover { 103 | border-color: #aaa; 104 | color: #000; } 105 | .select-target.select-theme-default.select-target-focused, .select-target.select-theme-default.select-target-focused:focus { 106 | border-color: #63a2f1; 107 | outline: none; } 108 | .select-target.select-theme-default b { 109 | position: absolute; 110 | right: 1em; 111 | top: 0; 112 | bottom: 0; 113 | margin: auto; 114 | height: 1.25rem; 115 | width: 2em; } 116 | .select-target.select-theme-default b:before, .select-target.select-theme-default b:after { 117 | content: ""; 118 | display: block; 119 | position: absolute; 120 | margin: auto; 121 | right: 0; 122 | height: 0; 123 | width: 0; 124 | border: .263em solid transparent; } 125 | .select-target.select-theme-default b:before { 126 | top: 0; 127 | border-bottom-color: inherit; } 128 | .select-target.select-theme-default b:after { 129 | bottom: 0; 130 | border-top-color: inherit; } 131 | -------------------------------------------------------------------------------- /dist/js/select.js: -------------------------------------------------------------------------------- 1 | /*! tether-select 1.1.1 */ 2 | 3 | (function(root, factory) { 4 | if (typeof define === 'function' && define.amd) { 5 | define(["tether"], factory); 6 | } else if (typeof exports === 'object') { 7 | module.exports = factory(require('tether')); 8 | } else { 9 | root.Select = factory(root.Tether); 10 | } 11 | }(this, function(Tether) { 12 | 13 | /* global Tether */ 14 | 15 | 'use strict'; 16 | 17 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 18 | 19 | var _get = function get(_x3, _x4, _x5) { var _again = true; _function: while (_again) { var object = _x3, property = _x4, receiver = _x5; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x3 = parent; _x4 = property; _x5 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; 20 | 21 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 22 | 23 | function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 24 | 25 | var _Tether$Utils = Tether.Utils; 26 | var extend = _Tether$Utils.extend; 27 | var addClass = _Tether$Utils.addClass; 28 | var removeClass = _Tether$Utils.removeClass; 29 | var hasClass = _Tether$Utils.hasClass; 30 | var getBounds = _Tether$Utils.getBounds; 31 | var Evented = _Tether$Utils.Evented; 32 | 33 | var ENTER = 13; 34 | var ESCAPE = 27; 35 | var SPACE = 32; 36 | var UP = 38; 37 | var DOWN = 40; 38 | 39 | var touchDevice = ('ontouchstart' in document.documentElement); 40 | var clickEvent = touchDevice ? 'touchstart' : 'click'; 41 | 42 | function _useNative() { 43 | var innerWidth = window.innerWidth; 44 | var innerHeight = window.innerHeight; 45 | 46 | return touchDevice && (innerWidth <= 640 || innerHeight <= 640); 47 | } 48 | 49 | function isRepeatedChar(str) { 50 | return Array.prototype.reduce.call(str, function (a, b) { 51 | return a === b ? b : false; 52 | }); 53 | } 54 | 55 | function getFocusedSelect() { 56 | var focusedTarget = document.querySelector('.select-target-focused'); 57 | return focusedTarget ? focusedTarget.selectInstance : null; 58 | } 59 | 60 | var searchText = ''; 61 | var searchTextTimeout = undefined; 62 | 63 | document.addEventListener('keypress', function (e) { 64 | var select = getFocusedSelect(); 65 | if (!select || e.charCode === 0) { 66 | return; 67 | } 68 | 69 | if (e.keyCode === SPACE) { 70 | e.preventDefault(); 71 | } 72 | 73 | clearTimeout(searchTextTimeout); 74 | searchTextTimeout = setTimeout(function () { 75 | searchText = ''; 76 | }, 500); 77 | 78 | searchText += String.fromCharCode(e.charCode); 79 | 80 | var options = select.findOptionsByPrefix(searchText); 81 | 82 | if (options.length === 1) { 83 | // We have an exact match, choose it 84 | select.selectOption(options[0]); 85 | } 86 | 87 | if (searchText.length > 1 && isRepeatedChar(searchText)) { 88 | // They hit the same char over and over, maybe they want to cycle through 89 | // the options that start with that char 90 | var repeatedOptions = select.findOptionsByPrefix(searchText[0]); 91 | 92 | if (repeatedOptions.length) { 93 | var selected = repeatedOptions.indexOf(select.getChosen()); 94 | 95 | // Pick the next thing (if something with this prefix wasen't selected 96 | // we'll end up with the first option) 97 | selected += 1; 98 | selected = selected % repeatedOptions.length; 99 | 100 | select.selectOption(repeatedOptions[selected]); 101 | return; 102 | } 103 | } 104 | 105 | if (options.length) { 106 | // We have multiple things that start with this prefix. Based on the 107 | // behavior of native select, this is considered after the repeated case. 108 | select.selectOption(options[0]); 109 | return; 110 | } 111 | 112 | // No match at all, do nothing 113 | }); 114 | 115 | document.addEventListener('keydown', function (e) { 116 | // We consider this independently of the keypress handler so we can intercept 117 | // keys that have built-in functions. 118 | var select = getFocusedSelect(); 119 | if (!select) { 120 | return; 121 | } 122 | 123 | if ([UP, DOWN, ESCAPE].indexOf(e.keyCode) >= 0) { 124 | e.preventDefault(); 125 | } 126 | 127 | if (select.isOpen()) { 128 | switch (e.keyCode) { 129 | case UP: 130 | case DOWN: 131 | select.moveHighlight(e.keyCode); 132 | break; 133 | case ENTER: 134 | select.selectHighlightedOption(); 135 | break; 136 | case ESCAPE: 137 | select.close(); 138 | select.target.focus(); 139 | } 140 | } else { 141 | if ([UP, DOWN, SPACE].indexOf(e.keyCode) >= 0) { 142 | select.open(); 143 | } 144 | } 145 | }); 146 | 147 | var Select = (function (_Evented) { 148 | _inherits(Select, _Evented); 149 | 150 | function Select(options) { 151 | _classCallCheck(this, Select); 152 | 153 | _get(Object.getPrototypeOf(Select.prototype), 'constructor', this).call(this, options); 154 | this.options = extend({}, Select.defaults, options); 155 | this.select = this.options.el; 156 | 157 | if (typeof this.select.selectInstance !== 'undefined') { 158 | throw new Error('This element has already been turned into a Select'); 159 | } 160 | 161 | this.update = this.update.bind(this); 162 | 163 | this.setupTarget(); 164 | this.renderTarget(); 165 | 166 | this.setupDrop(); 167 | this.renderDrop(); 168 | 169 | this.setupSelect(); 170 | 171 | this.setupTether(); 172 | this.bindClick(); 173 | 174 | this.bindMutationEvents(); 175 | 176 | this.value = this.select.value; 177 | } 178 | 179 | _createClass(Select, [{ 180 | key: 'useNative', 181 | value: function useNative() { 182 | var native = this.options.useNative; 183 | return native === true || _useNative() && native !== false; 184 | } 185 | }, { 186 | key: 'setupTarget', 187 | value: function setupTarget() { 188 | var _this = this; 189 | 190 | this.target = document.createElement('a'); 191 | this.target.href = 'javascript:;'; 192 | 193 | addClass(this.target, 'select-target'); 194 | 195 | var tabIndex = this.select.getAttribute('tabindex') || 0; 196 | this.target.setAttribute('tabindex', tabIndex); 197 | 198 | if (this.options.className) { 199 | addClass(this.target, this.options.className); 200 | } 201 | 202 | this.target.selectInstance = this; 203 | 204 | this.target.addEventListener('click', function () { 205 | if (!_this.isOpen()) { 206 | _this.target.focus(); 207 | } else { 208 | _this.target.blur(); 209 | } 210 | }); 211 | 212 | this.target.addEventListener('focus', function () { 213 | addClass(_this.target, 'select-target-focused'); 214 | }); 215 | 216 | this.target.addEventListener('blur', function (_ref) { 217 | var relatedTarget = _ref.relatedTarget; 218 | 219 | if (_this.isOpen()) { 220 | if (relatedTarget && !_this.drop.contains(relatedTarget)) { 221 | _this.close(); 222 | } 223 | } 224 | 225 | removeClass(_this.target, 'select-target-focused'); 226 | }); 227 | 228 | this.select.parentNode.insertBefore(this.target, this.select.nextSibling); 229 | } 230 | }, { 231 | key: 'setupDrop', 232 | value: function setupDrop() { 233 | var _this2 = this; 234 | 235 | this.drop = document.createElement('div'); 236 | addClass(this.drop, 'select'); 237 | 238 | if (this.options.className) { 239 | addClass(this.drop, this.options.className); 240 | } 241 | 242 | document.body.appendChild(this.drop); 243 | 244 | this.drop.addEventListener('click', function (e) { 245 | if (hasClass(e.target, 'select-option')) { 246 | _this2.pickOption(e.target); 247 | } 248 | 249 | // Built-in selects don't propagate click events in their drop directly 250 | // to the body, so we don't want to either. 251 | e.stopPropagation(); 252 | }); 253 | 254 | this.drop.addEventListener('mousemove', function (e) { 255 | if (hasClass(e.target, 'select-option')) { 256 | _this2.highlightOption(e.target); 257 | } 258 | }); 259 | 260 | this.content = document.createElement('div'); 261 | addClass(this.content, 'select-content'); 262 | this.drop.appendChild(this.content); 263 | } 264 | }, { 265 | key: 'open', 266 | value: function open() { 267 | var _this3 = this; 268 | 269 | addClass(this.target, 'select-open'); 270 | 271 | if (this.useNative()) { 272 | var _event = document.createEvent("MouseEvents"); 273 | _event.initEvent("mousedown", true, true); 274 | this.select.dispatchEvent(_event); 275 | 276 | return; 277 | } 278 | 279 | addClass(this.drop, 'select-open'); 280 | 281 | setTimeout(function () { 282 | _this3.tether.enable(); 283 | }); 284 | 285 | var selectedOption = this.drop.querySelector('.select-option-selected'); 286 | 287 | if (!selectedOption) { 288 | return; 289 | } 290 | 291 | this.highlightOption(selectedOption); 292 | this.scrollDropContentToOption(selectedOption); 293 | 294 | var positionSelectStyle = function positionSelectStyle() { 295 | if (hasClass(_this3.drop, 'tether-abutted-left') || hasClass(_this3.drop, 'tether-abutted-bottom')) { 296 | var dropBounds = getBounds(_this3.drop); 297 | var optionBounds = getBounds(selectedOption); 298 | 299 | var offset = dropBounds.top - (optionBounds.top + optionBounds.height); 300 | 301 | _this3.drop.style.top = (parseFloat(_this3.drop.style.top) || 0) + offset + 'px'; 302 | } 303 | }; 304 | 305 | var alignToHighlighted = this.options.alignToHighlighted; 306 | var _content = this.content; 307 | var scrollHeight = _content.scrollHeight; 308 | var clientHeight = _content.clientHeight; 309 | 310 | if (alignToHighlighted === 'always' || alignToHighlighted === 'auto' && scrollHeight <= clientHeight) { 311 | setTimeout(function () { 312 | positionSelectStyle(); 313 | }); 314 | } 315 | 316 | this.trigger('open'); 317 | } 318 | }, { 319 | key: 'close', 320 | value: function close() { 321 | removeClass(this.target, 'select-open'); 322 | 323 | if (this.useNative()) { 324 | this.select.blur(); 325 | } 326 | 327 | this.tether.disable(); 328 | 329 | removeClass(this.drop, 'select-open'); 330 | 331 | this.trigger('close'); 332 | } 333 | }, { 334 | key: 'toggle', 335 | value: function toggle() { 336 | if (this.isOpen()) { 337 | this.close(); 338 | } else { 339 | this.open(); 340 | } 341 | } 342 | }, { 343 | key: 'isOpen', 344 | value: function isOpen() { 345 | return hasClass(this.drop, 'select-open'); 346 | } 347 | }, { 348 | key: 'bindClick', 349 | value: function bindClick() { 350 | var _this4 = this; 351 | 352 | this.target.addEventListener(clickEvent, function (e) { 353 | e.preventDefault(); 354 | _this4.toggle(); 355 | }); 356 | 357 | document.addEventListener(clickEvent, function (event) { 358 | if (!_this4.isOpen()) { 359 | return; 360 | } 361 | 362 | // Clicking inside dropdown 363 | if (event.target === _this4.drop || _this4.drop.contains(event.target)) { 364 | return; 365 | } 366 | 367 | // Clicking target 368 | if (event.target === _this4.target || _this4.target.contains(event.target)) { 369 | return; 370 | } 371 | 372 | _this4.close(); 373 | }); 374 | } 375 | }, { 376 | key: 'setupTether', 377 | value: function setupTether() { 378 | this.tether = new Tether(extend({ 379 | element: this.drop, 380 | target: this.target, 381 | attachment: 'top left', 382 | targetAttachment: 'bottom left', 383 | classPrefix: 'select', 384 | constraints: [{ 385 | to: 'window', 386 | attachment: 'together' 387 | }] 388 | }, this.options.tetherOptions)); 389 | } 390 | }, { 391 | key: 'renderTarget', 392 | value: function renderTarget() { 393 | this.target.innerHTML = ''; 394 | 395 | var options = this.select.querySelectorAll('option'); 396 | for (var i = 0; i < options.length; ++i) { 397 | var option = options[i]; 398 | if (option.selected) { 399 | this.target.innerHTML = option.innerHTML; 400 | break; 401 | } 402 | } 403 | 404 | this.target.appendChild(document.createElement('b')); 405 | } 406 | }, { 407 | key: 'renderDrop', 408 | value: function renderDrop() { 409 | var optionList = document.createElement('ul'); 410 | addClass(optionList, 'select-options'); 411 | 412 | var options = this.select.querySelectorAll('option'); 413 | for (var i = 0; i < options.length; ++i) { 414 | var el = options[i]; 415 | var option = document.createElement('li'); 416 | addClass(option, 'select-option'); 417 | 418 | option.setAttribute('data-value', el.value); 419 | option.innerHTML = el.innerHTML; 420 | 421 | if (el.selected) { 422 | addClass(option, 'select-option-selected'); 423 | } 424 | 425 | optionList.appendChild(option); 426 | } 427 | 428 | this.content.innerHTML = ''; 429 | this.content.appendChild(optionList); 430 | } 431 | }, { 432 | key: 'update', 433 | value: function update() { 434 | this.renderDrop(); 435 | this.renderTarget(); 436 | } 437 | }, { 438 | key: 'setupSelect', 439 | value: function setupSelect() { 440 | this.select.selectInstance = this; 441 | 442 | addClass(this.select, 'select-select'); 443 | 444 | this.select.addEventListener('change', this.update); 445 | } 446 | }, { 447 | key: 'bindMutationEvents', 448 | value: function bindMutationEvents() { 449 | if (typeof window.MutationObserver !== 'undefined') { 450 | this.observer = new MutationObserver(this.update); 451 | this.observer.observe(this.select, { 452 | childList: true, 453 | attributes: true, 454 | characterData: true, 455 | subtree: true 456 | }); 457 | } else { 458 | this.select.addEventListener('DOMSubtreeModified', this.update); 459 | } 460 | } 461 | }, { 462 | key: 'findOptionsByPrefix', 463 | value: function findOptionsByPrefix(text) { 464 | var options = this.drop.querySelectorAll('.select-option'); 465 | 466 | text = text.toLowerCase(); 467 | 468 | return Array.prototype.filter.call(options, function (option) { 469 | return option.innerHTML.toLowerCase().substr(0, text.length) === text; 470 | }); 471 | } 472 | }, { 473 | key: 'findOptionsByValue', 474 | value: function findOptionsByValue(val) { 475 | var options = this.drop.querySelectorAll('.select-option'); 476 | 477 | return Array.prototype.filter.call(options, function (option) { 478 | return option.getAttribute('data-value') === val; 479 | }); 480 | } 481 | }, { 482 | key: 'getChosen', 483 | value: function getChosen() { 484 | if (this.isOpen()) { 485 | return this.drop.querySelector('.select-option-highlight'); 486 | } 487 | return this.drop.querySelector('.select-option-selected'); 488 | } 489 | }, { 490 | key: 'selectOption', 491 | value: function selectOption(option) { 492 | if (this.isOpen()) { 493 | this.highlightOption(option); 494 | this.scrollDropContentToOption(option); 495 | } else { 496 | this.pickOption(option, false); 497 | } 498 | } 499 | }, { 500 | key: 'resetSelection', 501 | value: function resetSelection() { 502 | this.selectOption(this.drop.querySelector('.select-option')); 503 | } 504 | }, { 505 | key: 'highlightOption', 506 | value: function highlightOption(option) { 507 | var highlighted = this.drop.querySelector('.select-option-highlight'); 508 | if (highlighted) { 509 | removeClass(highlighted, 'select-option-highlight'); 510 | } 511 | 512 | addClass(option, 'select-option-highlight'); 513 | 514 | this.trigger('highlight', { option: option }); 515 | } 516 | }, { 517 | key: 'moveHighlight', 518 | value: function moveHighlight(directionKeyCode) { 519 | var highlighted = this.drop.querySelector('.select-option-highlight'); 520 | if (!highlighted) { 521 | this.highlightOption(this.drop.querySelector('.select-option')); 522 | return; 523 | } 524 | 525 | var options = this.drop.querySelectorAll('.select-option'); 526 | 527 | var highlightedIndex = Array.prototype.indexOf.call(options, highlighted); 528 | if (!(highlightedIndex >= 0)) { 529 | return; 530 | } 531 | 532 | if (directionKeyCode === UP) { 533 | highlightedIndex -= 1; 534 | } else { 535 | highlightedIndex += 1; 536 | } 537 | 538 | if (highlightedIndex < 0 || highlightedIndex >= options.length) { 539 | return; 540 | } 541 | 542 | var newHighlight = options[highlightedIndex]; 543 | 544 | this.highlightOption(newHighlight); 545 | this.scrollDropContentToOption(newHighlight); 546 | } 547 | }, { 548 | key: 'scrollDropContentToOption', 549 | value: function scrollDropContentToOption(option) { 550 | var _content2 = this.content; 551 | var scrollHeight = _content2.scrollHeight; 552 | var clientHeight = _content2.clientHeight; 553 | var scrollTop = _content2.scrollTop; 554 | 555 | if (scrollHeight > clientHeight) { 556 | var contentBounds = getBounds(this.content); 557 | var optionBounds = getBounds(option); 558 | 559 | this.content.scrollTop = optionBounds.top - (contentBounds.top - scrollTop); 560 | } 561 | } 562 | }, { 563 | key: 'selectHighlightedOption', 564 | value: function selectHighlightedOption() { 565 | this.pickOption(this.drop.querySelector('.select-option-highlight')); 566 | } 567 | }, { 568 | key: 'pickOption', 569 | value: function pickOption(option) { 570 | var _this5 = this; 571 | 572 | var close = arguments.length <= 1 || arguments[1] === undefined ? true : arguments[1]; 573 | 574 | this.value = this.select.value = option.getAttribute('data-value'); 575 | this.triggerChange(); 576 | 577 | if (close) { 578 | setTimeout(function () { 579 | _this5.close(); 580 | _this5.target.focus(); 581 | }); 582 | } 583 | } 584 | }, { 585 | key: 'triggerChange', 586 | value: function triggerChange() { 587 | var event = document.createEvent("HTMLEvents"); 588 | event.initEvent("change", true, false); 589 | this.select.dispatchEvent(event); 590 | 591 | this.trigger('change', { value: this.select.value }); 592 | } 593 | }, { 594 | key: 'change', 595 | value: function change(val) { 596 | var options = this.findOptionsByValue(val); 597 | 598 | if (!options.length) { 599 | throw new Error('Select Error: An option with the value "' + val + '" doesn\'t exist'); 600 | } 601 | 602 | this.pickOption(options[0], false); 603 | } 604 | }]); 605 | 606 | return Select; 607 | })(Evented); 608 | 609 | Select.defaults = { 610 | alignToHighlighed: 'auto', 611 | className: 'select-theme-default' 612 | }; 613 | 614 | Select.init = function () { 615 | var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; 616 | 617 | if (document.readyState === 'loading') { 618 | document.addEventListener('DOMContentLoaded', function () { 619 | return Select.init(options); 620 | }); 621 | return; 622 | } 623 | 624 | if (typeof options.selector === 'undefined') { 625 | options.selector = 'select'; 626 | } 627 | 628 | var selectors = document.querySelectorAll(options.selector); 629 | for (var i = 0; i < selectors.length; ++i) { 630 | var el = selectors[i]; 631 | if (!el.selectInstance) { 632 | new Select(extend({ el: el }, options)); 633 | } 634 | } 635 | }; 636 | return Select; 637 | 638 | })); 639 | -------------------------------------------------------------------------------- /dist/js/select.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"function"==typeof define&&define.amd?define(["tether"],e):"object"==typeof exports?module.exports=e(require("tether")):t.Select=e(t.Tether)}(this,function(t){"use strict";function e(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function n(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}function i(){var t=window.innerWidth,e=window.innerHeight;return b&&(640>=t||640>=e)}function o(t){return Array.prototype.reduce.call(t,function(t,e){return t===e?e:!1})}function r(){var t=document.querySelector(".select-target-focused");return t?t.selectInstance:null}var s=function(){function t(t,e){for(var n=0;n1&&o(E)){var i=e.findOptionsByPrefix(E[0]);if(i.length){var s=i.indexOf(e.getChosen());return s+=1,s%=i.length,void e.selectOption(i[s])}}return n.length?void e.selectOption(n[0]):void 0}}),document.addEventListener("keydown",function(t){var e=r();if(e)if([m,O,v].indexOf(t.keyCode)>=0&&t.preventDefault(),e.isOpen())switch(t.keyCode){case m:case O:e.moveHighlight(t.keyCode);break;case f:e.selectHighlightedOption();break;case v:e.close(),e.target.focus()}else[m,O,y].indexOf(t.keyCode)>=0&&e.open()});var C=function(o){function r(t){if(e(this,r),a(Object.getPrototypeOf(r.prototype),"constructor",this).call(this,t),this.options=c({},r.defaults,t),this.select=this.options.el,"undefined"!=typeof this.select.selectInstance)throw new Error("This element has already been turned into a Select");this.update=this.update.bind(this),this.setupTarget(),this.renderTarget(),this.setupDrop(),this.renderDrop(),this.setupSelect(),this.setupTether(),this.bindClick(),this.bindMutationEvents(),this.value=this.select.value}return n(r,o),s(r,[{key:"useNative",value:function(){var t=this.options.useNative;return t===!0||i()&&t!==!1}},{key:"setupTarget",value:function(){var t=this;this.target=document.createElement("a"),this.target.href="javascript:;",h(this.target,"select-target");var e=this.select.getAttribute("tabindex")||0;this.target.setAttribute("tabindex",e),this.options.className&&h(this.target,this.options.className),this.target.selectInstance=this,this.target.addEventListener("click",function(){t.isOpen()?t.target.blur():t.target.focus()}),this.target.addEventListener("focus",function(){h(t.target,"select-target-focused")}),this.target.addEventListener("blur",function(e){var n=e.relatedTarget;t.isOpen()&&n&&!t.drop.contains(n)&&t.close(),u(t.target,"select-target-focused")}),this.select.parentNode.insertBefore(this.target,this.select.nextSibling)}},{key:"setupDrop",value:function(){var t=this;this.drop=document.createElement("div"),h(this.drop,"select"),this.options.className&&h(this.drop,this.options.className),document.body.appendChild(this.drop),this.drop.addEventListener("click",function(e){p(e.target,"select-option")&&t.pickOption(e.target),e.stopPropagation()}),this.drop.addEventListener("mousemove",function(e){p(e.target,"select-option")&&t.highlightOption(e.target)}),this.content=document.createElement("div"),h(this.content,"select-content"),this.drop.appendChild(this.content)}},{key:"open",value:function(){var t=this;if(h(this.target,"select-open"),this.useNative()){var e=document.createEvent("MouseEvents");return e.initEvent("mousedown",!0,!0),void this.select.dispatchEvent(e)}h(this.drop,"select-open"),setTimeout(function(){t.tether.enable()});var n=this.drop.querySelector(".select-option-selected");if(n){this.highlightOption(n),this.scrollDropContentToOption(n);var i=function(){if(p(t.drop,"tether-abutted-left")||p(t.drop,"tether-abutted-bottom")){var e=d(t.drop),i=d(n),o=e.top-(i.top+i.height);t.drop.style.top=(parseFloat(t.drop.style.top)||0)+o+"px"}},o=this.options.alignToHighlighted,r=this.content,s=r.scrollHeight,a=r.clientHeight;("always"===o||"auto"===o&&a>=s)&&setTimeout(function(){i()}),this.trigger("open")}}},{key:"close",value:function(){u(this.target,"select-open"),this.useNative()&&this.select.blur(),this.tether.disable(),u(this.drop,"select-open"),this.trigger("close")}},{key:"toggle",value:function(){this.isOpen()?this.close():this.open()}},{key:"isOpen",value:function(){return p(this.drop,"select-open")}},{key:"bindClick",value:function(){var t=this;this.target.addEventListener(k,function(e){e.preventDefault(),t.toggle()}),document.addEventListener(k,function(e){t.isOpen()&&(e.target===t.drop||t.drop.contains(e.target)||e.target===t.target||t.target.contains(e.target)||t.close())})}},{key:"setupTether",value:function(){this.tether=new t(c({element:this.drop,target:this.target,attachment:"top left",targetAttachment:"bottom left",classPrefix:"select",constraints:[{to:"window",attachment:"together"}]},this.options.tetherOptions))}},{key:"renderTarget",value:function(){this.target.innerHTML="";for(var t=this.select.querySelectorAll("option"),e=0;e=0&&(t===m?i-=1:i+=1,!(0>i||i>=n.length))){var o=n[i];this.highlightOption(o),this.scrollDropContentToOption(o)}}},{key:"scrollDropContentToOption",value:function(t){var e=this.content,n=e.scrollHeight,i=e.clientHeight,o=e.scrollTop;if(n>i){var r=d(this.content),s=d(t);this.content.scrollTop=s.top-(r.top-o)}}},{key:"selectHighlightedOption",value:function(){this.pickOption(this.drop.querySelector(".select-option-highlight"))}},{key:"pickOption",value:function(t){var e=this,n=arguments.length<=1||void 0===arguments[1]?!0:arguments[1];this.value=this.select.value=t.getAttribute("data-value"),this.triggerChange(),n&&setTimeout(function(){e.close(),e.target.focus()})}},{key:"triggerChange",value:function(){var t=document.createEvent("HTMLEvents");t.initEvent("change",!0,!1),this.select.dispatchEvent(t),this.trigger("change",{value:this.select.value})}},{key:"change",value:function(t){var e=this.findOptionsByValue(t);if(!e.length)throw new Error('Select Error: An option with the value "'+t+"\" doesn't exist");this.pickOption(e[0],!1)}}]),r}(g);return C.defaults={alignToHighlighed:"auto",className:"select-theme-default"},C.init=function(){var t=arguments.length<=0||void 0===arguments[0]?{}:arguments[0];if("loading"===document.readyState)return void document.addEventListener("DOMContentLoaded",function(){return C.init(t)});"undefined"==typeof t.selector&&(t.selector="select");for(var e=document.querySelectorAll(t.selector),n=0;n 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 34 | 35 | 36 |
37 | 291 |
292 | 293 |
294 | 548 |
549 | 550 |
551 |
552 |
553 | 554 |
555 | 609 |
610 | 611 |
612 | 666 |
667 | 668 |
669 |
670 |
671 | 672 |
673 | 681 |
682 | 683 |
684 | 692 |
693 | 694 |
695 |
696 |
697 | 698 |
699 | 707 |
708 | 709 |
710 | Align dropdown items to target on open (like a real select):
711 | 719 |
720 | 721 |
722 |
723 |
724 | 725 |
726 | 735 |
736 | 737 |
738 | 747 |
748 | 749 | 750 | 751 | 759 | 760 | 761 | -------------------------------------------------------------------------------- /docs/intro.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ## Select 9 | 10 | Select is a Javascript and CSS library for creating styleable select elements. Unlike many other select-replacements, Select is designed 11 | from the ground up to replicate the behavior of native select controls as much as is possible, providing a seemless experience for users. 12 | That means it works properly when you type characters, use tab for focus, etc. 13 | 14 | Use Select where you would use a native select. It doesn't, and will never, provide any sort of autocomplete functionality, so you probably 15 | don't want to use it on a list of more than a few hundred elements. 16 | 17 | Select uses [Tether](http://github.hubspot.com/tether/docs/welcome) to efficiently position its element container. 18 | 19 | ### [Demo](http://github.hubspot.com/select/docs/welcome) 20 | 21 | ### Dependencies 22 | 23 | Tether 24 | 25 | ### Browser Support 26 | 27 | IE9+ and all modern browsers 28 | 29 | ### Usage 30 | 31 | #### Initialization 32 | 33 | To initialize a single `selectElement`, simply create a `new Select` object. 34 | 35 | ```coffeescript 36 | new Select 37 | el: selectElement 38 | ``` 39 | 40 | To initialize all selects on a page, you can use the `Select.init` method: 41 | 42 | ```coffeescript 43 | Select.init() 44 | ``` 45 | 46 | By default, that will init all `select` elements, pass a `selector` to be more specific: 47 | 48 | ```coffeescript 49 | Select.init({selector: '.my-select'}) 50 | ``` 51 | 52 | You can pass any options you'd like to init your select's with into `init`: 53 | 54 | ```coffeescript 55 | Select.init({className: 'select-theme-dark'}) 56 | ``` 57 | 58 | #### The Select Object 59 | 60 | The `Select` constructor returns a `Select` object. You can also get the select instance by reading the `.selectInstance` property off of the 61 | original `select` element: 62 | 63 | ```coffeescript 64 | MySelect = new Select 65 | el: myElement 66 | 67 | # OR 68 | 69 | new Select 70 | el: myElement 71 | 72 | MySelect = el.selectInstance 73 | ``` 74 | 75 | The `Select` object has the following properties: 76 | 77 | - `.close()`: Close the dropdown, if it's open 78 | - `.open()`: Open the dropdown, if it's closed 79 | - `.toggle()`: Toggle between open and closed 80 | - `.isOpen()`: Returns true if the dropdown is open 81 | - `.change(val)`: Change the select to the option with the value provided 82 | - `.update()`: Update the dropdown with new options. This happens automatically when you change the underlying select, so you should never have to call it. 83 | - `.value`: The current value of the select 84 | 85 | You can also bind events on the select object: 86 | 87 | - `.on(event, handler, [context])`: When `event` happens, call `handler`, with `context` 88 | - `.off(event, [handler])`: Unbind the provided `event` - `handler` combination 89 | - `.once(event, handler, [context])`: The next time `event` happens, call `handler`, with `context` 90 | 91 | Events: 92 | 93 | - `open` 94 | - `close` 95 | - `change` 96 | - `highlight` 97 | 98 | When the select's value changes, the value of the original `select` element it's based on will change 99 | as well, so feel free to read the value from that element, or listen to it's `change` event. 100 | 101 | #### Changing the theme 102 | 103 | To change from the default theme, change the `className` property. 104 | 105 | ```coffeescript 106 | new Select 107 | el: selectElement 108 | className: 'select-theme-dark' 109 | ``` 110 | 111 | #### Changing the positioning 112 | 113 | Select has an option called `alignToHighlighted` which allows you to change whether 114 | the drop is positioned like a real 115 | select element (with the currently selected option over the element) or like a canonical dropdown menu. 116 | 117 | By default, this property is set to `"auto"`, meaning it will align the drop like a select only when the number of items in the options chooser does not cause it to scroll. The other options are `"always"` and never `"never"`. 118 | 119 | In this example, we Select to always open the drop like a dropdown menu. 120 | 121 | ```coffeescript 122 | new Select 123 | el: selectElement 124 | alignToHighlighted: 'never' 125 | ``` 126 | 127 | #### Native controls 128 | 129 | By default, select fallsback to opening the native browser select on touch devices which are less than or equal to 640px in either height or width. 130 | 131 | To disable this: 132 | 133 | ```coffeescript 134 | new Select 135 | el: selectElement 136 | useNative: false 137 | ``` 138 | 139 | ### Themes 140 | 141 | Currently there are three themes for Select.js. 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 |
ThemeClass name
Default(No class name necessary)
Darkselect-theme-dark
Chosenselect-theme-chosen
157 | -------------------------------------------------------------------------------- /docs/welcome/coffee/welcome.coffee: -------------------------------------------------------------------------------- 1 | init = -> 2 | setupHeroSelect() 3 | setupThemeSelect() 4 | 5 | setupHeroSelect = -> 6 | new Select 7 | el: $('.hero-select')[0] 8 | alignToHighlighted: 'always' 9 | 10 | currentThemeClassName = undefined 11 | setupThemeSelect = -> 12 | $showcase = $ '#themeShowcase' 13 | $select = $ '.themes-select' 14 | currentThemeClassName = $select.val() 15 | select = new Select 16 | el: $select[0] 17 | className: currentThemeClassName 18 | alignToHighlighted: 'always' 19 | 20 | $select.on 'change', -> 21 | newClassName = $select.val() 22 | $([select.drop, select.target, $showcase[0]]).removeClass(currentThemeClassName).addClass(newClassName) 23 | currentThemeClassName = newClassName 24 | 25 | $ init 26 | -------------------------------------------------------------------------------- /docs/welcome/css/prism.css: -------------------------------------------------------------------------------- 1 | /* Prism.js */ 2 | code[class*="language-"], pre[class*="language-"] {color: black; font-family: Consolas, Monaco, 'Andale Mono', monospace; direction: ltr; text-align: left; white-space: pre; word-spacing: normal; -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; -webkit-hyphens: none; -moz-hyphens: none; -ms-hyphens: none; hyphens: none; } /* Code blocks */ pre[class*="language-"] {padding: 1em; margin: .5em 0; overflow: auto; font-size: 14px; } :not(pre) > code[class*="language-"], pre[class*="language-"] {background: rgba(0, 0, 0, .05); } /* Inline code */ :not(pre) > code[class*="language-"] {padding: .1em; border-radius: .3em; } .token.comment, .token.prolog, .token.doctype, .token.cdata {color: slategray; } .token.punctuation {color: #999; } .namespace {opacity: .7; } .token.property, .token.tag, .token.boolean, .token.number, .token.constant, .token.symbol {color: #905; } .token.selector, .token.attr-name, .token.string, .token.builtin {color: #690; } .token.operator, .token.entity, .token.url, .language-css .token.string, .style .token.string, .token.variable {color: #a67f59; } .token.atrule, .token.attr-value, .token.keyword {color: #07a; } .token.regex, .token.important {color: #e90; } .token.important {font-weight: bold; } .token.entity {cursor: help; } -------------------------------------------------------------------------------- /docs/welcome/css/welcome.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; } 3 | 4 | body { 5 | margin: 0; 6 | font-family: "proxima-nova", "Helvetica Neue", sans-serif; } 7 | 8 | table.showcase { 9 | height: 100%; 10 | width: 100%; 11 | position: relative; } 12 | table.showcase .showcase-inner { 13 | margin: 40px auto 60px; 14 | padding: 10px; } 15 | table.showcase:after { 16 | content: ""; 17 | display: block; 18 | position: absolute; 19 | opacity: 0.5; 20 | left: 0; 21 | right: 0; 22 | bottom: 20px; 23 | margin: auto; 24 | height: 0; 25 | width: 0; 26 | border-width: 18px; 27 | border-style: solid; 28 | border-color: transparent; 29 | border-top-color: inherit; } 30 | table.showcase.last-showcase:after { 31 | display: none; } 32 | table.showcase .showcase-inner h1 { 33 | font-size: 50px; 34 | text-align: center; 35 | font-weight: 300; } 36 | table.showcase .showcase-inner h2 { 37 | font-size: 24px; 38 | text-align: center; 39 | font-weight: 300; 40 | margin: 1em 0 1em; 41 | padding: 0 10%; } 42 | table.showcase .showcase-inner h2 a { 43 | color: inherit; } 44 | table.showcase .showcase-inner p { 45 | text-align: center; } 46 | 47 | .select-target { 48 | width: 200px; 49 | text-align: left; } 50 | 51 | .button { 52 | display: inline-block; 53 | border: 2px solid #333; 54 | color: #333; 55 | padding: 1em 1.25em; 56 | font-weight: 500; 57 | text-transform: uppercase; 58 | letter-spacing: 3px; 59 | text-decoration: none; 60 | cursor: pointer; 61 | width: 140px; 62 | font-size: .8em; 63 | line-height: 1.3em; } 64 | .button.dark { 65 | background: #333; 66 | color: #fff; } 67 | 68 | table.showcase.hero .hero-container { 69 | background-image: -webkit-linear-gradient(left, #fff, #f0f0f0, #fff); 70 | background-image: linear-gradient(left, #fff, #f0f0f0, #fff); 71 | margin: 0 auto; 72 | text-align: center; } 73 | 74 | table.showcase.about { 75 | background: #fff1dd; } 76 | table.showcase.about a { 77 | color: #c96c24; } 78 | table.showcase.about p { 79 | box-sizing: border-box; 80 | text-align: left; 81 | width: 500px; 82 | max-width: 100%; 83 | margin-left: auto; 84 | margin-right: auto; } 85 | table.showcase.about p > code { 86 | background: rgba(0, 0, 0, 0.05); } 87 | table.showcase.about pre { 88 | box-sizing: border-box; 89 | text-align: left; 90 | width: 500px; 91 | max-width: 100%; 92 | margin-left: auto; 93 | margin-right: auto; } 94 | 95 | table.showcase.themes { 96 | background: #eee; } 97 | table.showcase.themes.select-theme-dark { 98 | background: #323232; } 99 | table.showcase.themes.select-theme-dark h1, table.showcase.themes.select-theme-dark h2 { 100 | color: #e6e6e6; } 101 | table.showcase.themes.select-theme-chosen { 102 | background: #fff; } 103 | table.showcase.themes .themes-list { 104 | margin: 4em auto; 105 | padding: 0; 106 | list-style: none; 107 | text-align: center; 108 | font-size: 18px; } 109 | table.showcase.themes .themes-list li { 110 | padding: 0; 111 | margin: 0; 112 | display: inline; } 113 | table.showcase.themes .themes-list li a { 114 | cursor: pointer; 115 | opacity: 0.3; 116 | padding: .5em; 117 | display: inline-block; } 118 | table.showcase.themes .themes-list li a.selected { 119 | opacity: 1; } 120 | 121 | @media (max-width: 786px) { 122 | table.showcase .showcase-inner table[data-sortable] tr *:nth-child(4), table.showcase .button.dark { 123 | display: none; } } 124 | 125 | @media (max-width: 480px) { 126 | table.showcase .showcase-inner table[data-sortable] tr *:nth-child(3) { 127 | display: none; } } 128 | 129 | @media (max-width: 568px) { 130 | table.showcase.about p, table.showcase.about pre { 131 | width: 400px; } } 132 | 133 | @media (max-width: 420px) { 134 | table.showcase.about p, table.showcase.about pre { 135 | width: 280px; } } 136 | -------------------------------------------------------------------------------- /docs/welcome/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Select.js – Styleable select elements built on Tether.js 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 |

Select

28 |
29 | 83 |
84 |

Styleable select elements built on Tether

85 |

86 | ★ On Github    87 | Download 88 |

89 |
90 |
91 | 92 |
93 |
94 |

How to Use

95 |

Select is an open-source javascript and CSS library which makes <select> elements stylable. It's powered by Tether.js.

96 |

To use, first download the latest tether and select releases. 97 |

Then simply add this to your page:

98 |
<link rel="stylesheet" href="select-theme-default.css" />
 99 | <script src="tether.min.js"></script>
100 | <script src="select.min.js"></script>
101 |

And that's it! Learn more by visiting the documentation. 102 |

103 |
104 |
105 |
106 |

Themes

107 |

Selects for every occasion.

108 |

109 | 115 |

116 |
117 |
118 | 119 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 188 | 189 | 190 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /docs/welcome/js/welcome.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var currentThemeClassName, init, setupHeroSelect, setupThemeSelect; 3 | 4 | init = function() { 5 | setupHeroSelect(); 6 | return setupThemeSelect(); 7 | }; 8 | 9 | setupHeroSelect = function() { 10 | return new Select({ 11 | el: $('.hero-select')[0], 12 | alignToHighlighted: 'always' 13 | }); 14 | }; 15 | 16 | currentThemeClassName = void 0; 17 | 18 | setupThemeSelect = function() { 19 | var $select, $showcase, select; 20 | $showcase = $('#themeShowcase'); 21 | $select = $('.themes-select'); 22 | currentThemeClassName = $select.val(); 23 | select = new Select({ 24 | el: $select[0], 25 | className: currentThemeClassName, 26 | alignToHighlighted: 'always' 27 | }); 28 | return $select.on('change', function() { 29 | var newClassName; 30 | newClassName = $select.val(); 31 | $([select.drop, select.target, $showcase[0]]).removeClass(currentThemeClassName).addClass(newClassName); 32 | return currentThemeClassName = newClassName; 33 | }); 34 | }; 35 | 36 | $(init); 37 | 38 | }).call(this); 39 | -------------------------------------------------------------------------------- /docs/welcome/sass/welcome.sass: -------------------------------------------------------------------------------- 1 | html, body 2 | height: 100% 3 | 4 | body 5 | margin: 0 6 | font-family: "proxima-nova", "Helvetica Neue", sans-serif 7 | 8 | table.showcase 9 | height: 100% 10 | width: 100% 11 | position: relative 12 | 13 | .showcase-inner 14 | margin: 40px auto 60px 15 | padding: 10px 16 | 17 | &:after 18 | content: "" 19 | display: block 20 | position: absolute 21 | opacity: 0.5 22 | left: 0 23 | right: 0 24 | bottom: 20px 25 | margin: auto 26 | height: 0 27 | width: 0 28 | border-width: 18px 29 | border-style: solid 30 | border-color: transparent 31 | border-top-color: inherit 32 | 33 | &.last-showcase:after 34 | display: none 35 | 36 | .showcase-inner 37 | 38 | h1 39 | font-size: 50px 40 | text-align: center 41 | font-weight: 300 42 | 43 | h2 44 | font-size: 24px 45 | text-align: center 46 | font-weight: 300 47 | margin: 1em 0 1em 48 | padding: 0 10% 49 | 50 | a 51 | color: inherit 52 | 53 | p 54 | text-align: center 55 | 56 | .select-target 57 | width: 200px 58 | text-align: left 59 | 60 | .button 61 | display: inline-block 62 | border: 2px solid #333 63 | color: #333 64 | padding: 1em 1.25em 65 | font-weight: 500 66 | text-transform: uppercase 67 | letter-spacing: 3px 68 | text-decoration: none 69 | cursor: pointer 70 | width: 140px 71 | font-size: .8em 72 | line-height: 1.3em 73 | 74 | &.dark 75 | background: #333 76 | color: #fff 77 | 78 | table.showcase 79 | 80 | &.hero 81 | 82 | .hero-container 83 | background-image: linear-gradient(left, #fff, #f0f0f0, #fff) 84 | margin: 0 auto 85 | text-align: center 86 | 87 | &.about 88 | background: #fff1dd 89 | 90 | a 91 | color: #c96c24 92 | 93 | p 94 | box-sizing: border-box 95 | text-align: left 96 | width: 500px 97 | max-width: 100% 98 | margin-left: auto 99 | margin-right: auto 100 | 101 | > code 102 | background: rgba(0, 0, 0, 0.05) 103 | 104 | pre 105 | box-sizing: border-box 106 | text-align: left 107 | width: 500px 108 | max-width: 100% 109 | margin-left: auto 110 | margin-right: auto 111 | 112 | &.themes 113 | background: #eee 114 | 115 | &.select-theme-dark 116 | background: #323232 117 | 118 | h1, h2 119 | color: #e6e6e6 120 | 121 | &.select-theme-chosen 122 | background: #fff 123 | 124 | .themes-list 125 | margin: 4em auto 126 | padding: 0 127 | list-style: none 128 | text-align: center 129 | font-size: 18px 130 | 131 | li 132 | padding: 0 133 | margin: 0 134 | display: inline 135 | 136 | a 137 | cursor: pointer 138 | opacity: 0.3 139 | padding: .5em 140 | display: inline-block 141 | 142 | &.selected 143 | opacity: 1 144 | 145 | @media (max-width: 786px) 146 | table.showcase 147 | .showcase-inner table[data-sortable] tr *:nth-child(4), .button.dark 148 | display: none 149 | 150 | @media (max-width: 480px) 151 | table.showcase .showcase-inner table[data-sortable] tr *:nth-child(3) 152 | display: none 153 | 154 | @media (max-width: 568px) 155 | table.showcase.about 156 | p, pre 157 | width: 400px 158 | 159 | @media (max-width: 420px) 160 | table.showcase.about 161 | p, pre 162 | width: 280px 163 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var del = require('del'); 2 | var gulp = require('gulp'); 3 | var babel = require('gulp-babel'); 4 | var bump = require('gulp-bump'); 5 | var filter = require('gulp-filter'); 6 | var header = require('gulp-header'); 7 | var prefixer = require('gulp-autoprefixer'); 8 | var rename = require('gulp-rename'); 9 | var uglify = require('gulp-uglify'); 10 | var sass = require('gulp-sass'); 11 | var tagVersion = require('gulp-tag-version'); 12 | var umd = require('gulp-wrap-umd'); 13 | 14 | // Variables 15 | var distDir = './dist'; 16 | var pkg = require('./package.json'); 17 | var banner = ['/*!', pkg.name, pkg.version, '*/\n'].join(' '); 18 | var umdOptions = { 19 | exports: 'Select', 20 | namespace: 'Select', 21 | deps: [{ 22 | name: 'Tether', 23 | globalName: 'Tether', 24 | paramName: 'Tether', 25 | amdName: 'tether', 26 | cjsName: 'tether' 27 | }] 28 | }; 29 | 30 | 31 | // Clean 32 | gulp.task('clean', function() { 33 | del.sync([distDir]); 34 | }); 35 | 36 | 37 | // Javascript 38 | gulp.task('js', function() { 39 | gulp.src('./src/js/select.js') 40 | .pipe(babel()) 41 | .pipe(umd(umdOptions)) 42 | .pipe(header(banner)) 43 | 44 | // Original 45 | .pipe(gulp.dest(distDir + '/js')) 46 | 47 | // Minified 48 | .pipe(uglify()) 49 | .pipe(rename({suffix: '.min'})) 50 | .pipe(gulp.dest(distDir + '/js')); 51 | }); 52 | 53 | 54 | // CSS 55 | gulp.task('css', function() { 56 | gulp.src('./src/css/**/*.sass') 57 | .pipe(sass({ 58 | includePaths: ['./bower_components'] 59 | })) 60 | .pipe(prefixer()) 61 | .pipe(gulp.dest(distDir + '/css')); 62 | }); 63 | 64 | 65 | // Version bump 66 | var VERSIONS = ['patch', 'minor', 'major']; 67 | for (var i = 0; i < VERSIONS.length; ++i){ 68 | (function(version) { 69 | var pkgFilter = filter('package.json'); 70 | gulp.task('version:' + version, function() { 71 | gulp.src(['package.json', 'bower.json']) 72 | .pipe(bump({type: version})) 73 | .pipe(pkgFilter) 74 | .pipe(tagVersion()) 75 | .pipe(pkgFilter.restore()) 76 | .pipe(gulp.dest('.')) 77 | }); 78 | })(VERSIONS[i]); 79 | } 80 | 81 | 82 | // Watch 83 | gulp.task('watch', ['js', 'css'], function() { 84 | gulp.watch('./src/js/**/*', ['js']); 85 | gulp.watch('./src/css/**/*', ['css']); 86 | }); 87 | 88 | 89 | // Defaults 90 | gulp.task('build', ['js', 'css']); 91 | gulp.task('default', ['build']); 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tether-select", 3 | "version": "1.1.1", 4 | "description": "Styleable select elements built on Tether", 5 | "authors": [ 6 | "Adam Schwartz ", 7 | "Zack Bloom " 8 | ], 9 | "maintainers": [ 10 | "Nicholas Hwang " 11 | ], 12 | "scripts": { 13 | "install": "bower install", 14 | "watch": "gulp watch", 15 | "build": "gulp build" 16 | }, 17 | "license": "MIT", 18 | "main": "dist/js/select.js", 19 | "devDependencies": { 20 | "bower": "^1.4.1", 21 | "del": "^1.2.0", 22 | "gulp": "^3.9.0", 23 | "gulp-autoprefixer": "^2.3.1", 24 | "gulp-babel": "^5.1.0", 25 | "gulp-bump": "^0.3.0", 26 | "gulp-filter": "^2.0.2", 27 | "gulp-header": "^1.2.2", 28 | "gulp-rename": "^1.2.2", 29 | "gulp-sass": "^2.0.1", 30 | "gulp-tag-version": "^1.2.1", 31 | "gulp-uglify": "^1.2.0", 32 | "gulp-wrap-umd": "^0.2.1" 33 | }, 34 | "dependencies": { 35 | "tether": "~1.0.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/css/_checkmark.scss: -------------------------------------------------------------------------------- 1 | @mixin checkmark($color: #000) { 2 | content: #{ 3 | "url(\"data:image/svg+xml;utf8," + 4 | "" + 5 | "" + 6 | "" + 7 | "\"" + 8 | ")" 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/css/_checkmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/css/_no-mobile-tap-highlight.sass: -------------------------------------------------------------------------------- 1 | =no-mobile-tap-highlight 2 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0) 3 | -webkit-touch-callout: none 4 | -------------------------------------------------------------------------------- /src/css/_select.sass: -------------------------------------------------------------------------------- 1 | .select-select 2 | display: none 3 | 4 | /* For when we are on a small touch device and want to use native controls */ 5 | pointer-events: none 6 | position: absolute 7 | opacity: 0 8 | -------------------------------------------------------------------------------- /src/css/_user-select.sass: -------------------------------------------------------------------------------- 1 | =user-select($value) 2 | -webkit-user-select: $value 3 | -moz-user-select: $value 4 | -ms-user-select: $value 5 | -o-user-select: $value 6 | user-select: $value 7 | -------------------------------------------------------------------------------- /src/css/select-theme-chosen.sass: -------------------------------------------------------------------------------- 1 | @import no-mobile-tap-highlight 2 | @import user-select 3 | @import checkmark 4 | 5 | @import ../bower_components/tether/src/css/mixins/inline-block 6 | @import ../bower_components/tether/src/css/helpers/tether 7 | 8 | @import select 9 | 10 | +tether($themePrefix: "select") 11 | 12 | $optionLineHeight: 19px 13 | $paddingTopBottom: 6px 14 | 15 | $maxRows: 8 16 | $maxRowsMobile: 5 17 | $optionHeight: $optionLineHeight + (2 * $paddingTopBottom) 18 | $maxContentSize: $maxRows * $optionHeight 19 | $maxContentSizeMobile: $maxRowsMobile * $optionHeight 20 | $mobileBreakPointSize: ($maxRows + 4) * $optionHeight 21 | 22 | .select-theme-chosen 23 | font-family: "Helvetica Neue", sans-serif 24 | font-size: 13px 25 | 26 | &, & *, & *:after, & *:before 27 | box-sizing: border-box 28 | 29 | .select.select-theme-chosen 30 | +user-select(none) 31 | 32 | .select-content 33 | border-radius: 5px 34 | box-shadow: 0 0 0 1px rgba(0, 0, 0, .1), 0 2px 8px rgba(0, 0, 0, .2) 35 | background: #fff 36 | color: #444 37 | overflow: auto 38 | max-width: $maxContentSize 39 | max-height: $maxContentSize 40 | 41 | -webkit-overflow-scrolling: touch 42 | 43 | @media (max-width: $mobileBreakPointSize), (max-height: $mobileBreakPointSize) 44 | max-width: $maxContentSizeMobile 45 | max-height: $maxContentSizeMobile 46 | 47 | .select-options 48 | +no-mobile-tap-highlight 49 | margin: 0 50 | padding: 0 51 | 52 | .select-option 53 | +no-mobile-tap-highlight 54 | position: relative 55 | list-style: none 56 | margin: 0 57 | line-height: $optionLineHeight 58 | padding: $paddingTopBottom 11px $paddingTopBottom 30px 59 | display: block 60 | cursor: pointer 61 | white-space: nowrap 62 | overflow: hidden 63 | text-overflow: ellipsis 64 | 65 | &.select-option-selected 66 | 67 | &:before 68 | +checkmark(#444) 69 | position: absolute 70 | left: 13px 71 | top: 0 72 | top: 5px 73 | height: 11px 74 | width: 11px 75 | margin: auto 76 | 77 | &:hover, &.select-option-highlight 78 | background-image: linear-gradient(#3875D7 20%, #2A62BC 90%) 79 | background-color: #3875D7 80 | color: #fff 81 | 82 | &.select-option-selected:before 83 | +checkmark(#fff) 84 | 85 | &:first-child 86 | border-radius: 5px 5px 0 0 87 | 88 | &:last-child 89 | border-radius: 0 0 5px 5px 90 | 91 | .select-target.select-theme-chosen 92 | +inline-block 93 | +user-select(none) 94 | +no-mobile-tap-highlight 95 | border-radius: 5px 96 | box-shadow: 0 0 3px #FFF inset, 0 1px 1px rgba(0, 0, 0, 0.1) 97 | background-image: linear-gradient(to bottom, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%) 98 | position: relative 99 | padding: 3px 30px 2px 11px 100 | background: #f6f6f6 101 | border: 1px solid #aaa 102 | cursor: pointer 103 | color: #444 104 | text-decoration: none 105 | white-space: nowrap 106 | max-width: 100% 107 | overflow: hidden 108 | text-overflow: ellipsis 109 | line-height: 24px 110 | 111 | &.select-target-focused, &.select-target-focused:focus 112 | border-color: #5897FB 113 | outline: none 114 | 115 | b 116 | position: absolute 117 | right: 13px 118 | top: 0 119 | bottom: 1px 120 | margin: auto 121 | height: 16px 122 | width: 26px 123 | 124 | &:before, &:after 125 | content: "" 126 | display: block 127 | position: absolute 128 | margin: auto 129 | right: 0 130 | height: 0 131 | width: 0 132 | border: 3px solid transparent 133 | 134 | &:before 135 | top: 0 136 | border-bottom-color: inherit 137 | 138 | &:after 139 | bottom: 0 140 | border-top-color: inherit 141 | -------------------------------------------------------------------------------- /src/css/select-theme-dark.sass: -------------------------------------------------------------------------------- 1 | @import no-mobile-tap-highlight 2 | @import user-select 3 | @import checkmark 4 | 5 | @import ../bower_components/tether/src/css/mixins/inline-block 6 | @import ../bower_components/tether/src/css/helpers/tether 7 | 8 | @import select 9 | 10 | +tether($themePrefix: "select") 11 | 12 | $optionLineHeight: 1.25rem 13 | $paddingTopBottom: .5rem 14 | 15 | $maxRows: 8 16 | $maxRowsMobile: 5 17 | $optionHeight: $optionLineHeight + (2 * $paddingTopBottom) 18 | $maxContentSize: $maxRows * $optionHeight 19 | $maxContentSizeMobile: $maxRowsMobile * $optionHeight 20 | $mobileBreakPointSize: ($maxRows + 4) * $optionHeight 21 | 22 | .select-theme-dark 23 | 24 | &, & *, & *:after, & *:before 25 | box-sizing: border-box 26 | 27 | .select.select-theme-dark 28 | +user-select(none) 29 | 30 | .select-content 31 | border-radius: .25em 32 | box-shadow: 0 2px 8px rgba(0, 0, 0, .2) 33 | background: #252525 34 | color: #b9b9b9 35 | font-family: inherit 36 | overflow: auto 37 | max-width: $maxContentSize 38 | max-height: $maxContentSize 39 | 40 | -webkit-overflow-scrolling: touch 41 | 42 | @media (max-width: $mobileBreakPointSize), (max-height: $mobileBreakPointSize) 43 | max-width: $maxContentSizeMobile 44 | max-height: $maxContentSizeMobile 45 | 46 | .select-options 47 | +no-mobile-tap-highlight 48 | margin: 0 49 | padding: 0 50 | 51 | .select-option 52 | +no-mobile-tap-highlight 53 | position: relative 54 | list-style: none 55 | margin: 0 56 | line-height: $optionLineHeight 57 | padding: $paddingTopBottom 1em $paddingTopBottom 2.5em 58 | display: block 59 | cursor: pointer 60 | white-space: nowrap 61 | overflow: hidden 62 | text-overflow: ellipsis 63 | 64 | &.select-option-selected 65 | 66 | &:before 67 | +checkmark(#b9b9b9) 68 | position: absolute 69 | left: 1em 70 | top: 0 71 | bottom: .2em 72 | height: 1em 73 | width: 1em 74 | margin: auto 75 | 76 | &:hover, &.select-option-highlight 77 | background: #63a2f1 78 | color: #fff 79 | 80 | &.select-option-selected:before 81 | +checkmark(#fff) 82 | 83 | &:first-child 84 | border-radius: .25em .25em 0 0 85 | 86 | &:last-child 87 | border-radius: 0 0 .25em .25em 88 | 89 | .select-target.select-theme-dark 90 | +inline-block 91 | +user-select(none) 92 | +no-mobile-tap-highlight 93 | border-radius: .25em 94 | position: relative 95 | padding: $paddingTopBottom 3em $paddingTopBottom 1em 96 | background: #252525 97 | border: .18em solid #151515 98 | cursor: pointer 99 | color: #b9b9b9 100 | text-decoration: none 101 | white-space: nowrap 102 | max-width: 100% 103 | overflow: hidden 104 | text-overflow: ellipsis 105 | 106 | &:hover 107 | border-color: #000 108 | color: #fff 109 | 110 | &.select-target-focused, &.select-target-focused:focus 111 | border-color: #63a2f1 112 | outline: none 113 | 114 | b 115 | position: absolute 116 | right: 1em 117 | top: 0 118 | bottom: 0 119 | margin: auto 120 | height: $optionLineHeight 121 | width: 2em 122 | 123 | &:before, &:after 124 | content: "" 125 | display: block 126 | position: absolute 127 | margin: auto 128 | right: 0 129 | height: 0 130 | width: 0 131 | border: .263em solid transparent 132 | 133 | &:before 134 | top: 0 135 | border-bottom-color: inherit 136 | 137 | &:after 138 | bottom: 0 139 | border-top-color: inherit 140 | -------------------------------------------------------------------------------- /src/css/select-theme-default.sass: -------------------------------------------------------------------------------- 1 | @import no-mobile-tap-highlight 2 | @import user-select 3 | @import checkmark 4 | 5 | @import ../bower_components/tether/src/css/mixins/inline-block 6 | @import ../bower_components/tether/src/css/helpers/tether 7 | 8 | @import select 9 | 10 | +tether($themePrefix: "select") 11 | 12 | $optionLineHeight: 1.25rem 13 | $paddingTopBottom: .5rem 14 | 15 | $maxRows: 8 16 | $maxRowsMobile: 5 17 | $optionHeight: $optionLineHeight + (2 * $paddingTopBottom) 18 | $maxContentSize: $maxRows * $optionHeight 19 | $maxContentSizeMobile: $maxRowsMobile * $optionHeight 20 | $mobileBreakPointSize: ($maxRows + 4) * $optionHeight 21 | 22 | .select-theme-default 23 | 24 | &, & *, & *:after, & *:before 25 | box-sizing: border-box 26 | 27 | .select.select-theme-default 28 | +user-select(none) 29 | 30 | .select-content 31 | border-radius: .25em 32 | box-shadow: 0 2px 8px rgba(0, 0, 0, .2) 33 | background: #fff 34 | font-family: inherit 35 | color: inherit 36 | overflow: auto 37 | max-width: $maxContentSize 38 | max-height: $maxContentSize 39 | 40 | -webkit-overflow-scrolling: touch 41 | 42 | @media (max-width: $mobileBreakPointSize), (max-height: $mobileBreakPointSize) 43 | max-width: $maxContentSizeMobile 44 | max-height: $maxContentSizeMobile 45 | 46 | .select-options 47 | +no-mobile-tap-highlight 48 | margin: 0 49 | padding: 0 50 | 51 | .select-option 52 | +no-mobile-tap-highlight 53 | position: relative 54 | list-style: none 55 | margin: 0 56 | line-height: $optionLineHeight 57 | padding: $paddingTopBottom 1em $paddingTopBottom 2.5em 58 | display: block 59 | cursor: pointer 60 | white-space: nowrap 61 | overflow: hidden 62 | text-overflow: ellipsis 63 | 64 | &.select-option-selected 65 | 66 | &:before 67 | +checkmark(#444) 68 | position: absolute 69 | left: 1em 70 | top: 0 71 | bottom: .2em 72 | height: 1em 73 | width: 1em 74 | margin: auto 75 | 76 | &:hover, &.select-option-highlight 77 | background: #63a2f1 78 | color: #fff 79 | 80 | &.select-option-selected:before 81 | +checkmark(#fff) 82 | 83 | &:first-child 84 | border-radius: .25em .25em 0 0 85 | 86 | &:last-child 87 | border-radius: 0 0 .25em .25em 88 | 89 | .select-target.select-theme-default 90 | +inline-block 91 | +user-select(none) 92 | +no-mobile-tap-highlight 93 | border-radius: .25em 94 | position: relative 95 | padding: $paddingTopBottom 3em $paddingTopBottom 1em 96 | background: #f6f6f6 97 | border: .18em solid #ddd 98 | cursor: pointer 99 | color: #444 100 | text-decoration: none 101 | white-space: nowrap 102 | max-width: 100% 103 | overflow: hidden 104 | text-overflow: ellipsis 105 | 106 | &:hover 107 | border-color: #aaa 108 | color: #000 109 | 110 | &.select-target-focused, &.select-target-focused:focus 111 | border-color: #63a2f1 112 | outline: none 113 | 114 | b 115 | position: absolute 116 | right: 1em 117 | top: 0 118 | bottom: 0 119 | margin: auto 120 | height: $optionLineHeight 121 | width: 2em 122 | 123 | &:before, &:after 124 | content: "" 125 | display: block 126 | position: absolute 127 | margin: auto 128 | right: 0 129 | height: 0 130 | width: 0 131 | border: .263em solid transparent 132 | 133 | &:before 134 | top: 0 135 | border-bottom-color: inherit 136 | 137 | &:after 138 | bottom: 0 139 | border-top-color: inherit 140 | -------------------------------------------------------------------------------- /src/js/select.js: -------------------------------------------------------------------------------- 1 | /* global Tether */ 2 | 3 | const { 4 | extend, 5 | addClass, 6 | removeClass, 7 | hasClass, 8 | getBounds, 9 | Evented 10 | } = Tether.Utils; 11 | 12 | const ENTER = 13; 13 | const ESCAPE = 27; 14 | const SPACE = 32; 15 | const UP = 38; 16 | const DOWN = 40; 17 | 18 | const touchDevice = 'ontouchstart' in document.documentElement; 19 | const clickEvent = touchDevice ? 'touchstart' : 'click'; 20 | 21 | function useNative() { 22 | const {innerWidth, innerHeight} = window; 23 | return touchDevice && (innerWidth <= 640 || innerHeight <= 640); 24 | } 25 | 26 | function isRepeatedChar (str) { 27 | return Array.prototype.reduce.call(str, (a, b) => { 28 | return a === b ? b : false; 29 | }); 30 | } 31 | 32 | function getFocusedSelect () { 33 | const focusedTarget = document.querySelector('.select-target-focused'); 34 | return focusedTarget ? focusedTarget.selectInstance : null; 35 | } 36 | 37 | let searchText = ''; 38 | let searchTextTimeout; 39 | 40 | document.addEventListener('keypress', (e) => { 41 | const select = getFocusedSelect(); 42 | if (!select || e.charCode === 0) { 43 | return; 44 | } 45 | 46 | if (e.keyCode === SPACE) { 47 | e.preventDefault(); 48 | } 49 | 50 | clearTimeout(searchTextTimeout); 51 | searchTextTimeout = setTimeout(() => { 52 | searchText = ''; 53 | }, 500); 54 | 55 | searchText += String.fromCharCode(e.charCode); 56 | 57 | const options = select.findOptionsByPrefix(searchText); 58 | 59 | if (options.length === 1) { 60 | // We have an exact match, choose it 61 | select.selectOption(options[0]); 62 | } 63 | 64 | if (searchText.length > 1 && isRepeatedChar(searchText)) { 65 | // They hit the same char over and over, maybe they want to cycle through 66 | // the options that start with that char 67 | const repeatedOptions = select.findOptionsByPrefix(searchText[0]); 68 | 69 | if (repeatedOptions.length) { 70 | let selected = repeatedOptions.indexOf(select.getChosen()); 71 | 72 | // Pick the next thing (if something with this prefix wasen't selected 73 | // we'll end up with the first option) 74 | selected += 1; 75 | selected = selected % repeatedOptions.length; 76 | 77 | select.selectOption(repeatedOptions[selected]); 78 | return; 79 | } 80 | } 81 | 82 | if (options.length) { 83 | // We have multiple things that start with this prefix. Based on the 84 | // behavior of native select, this is considered after the repeated case. 85 | select.selectOption(options[0]); 86 | return; 87 | } 88 | 89 | // No match at all, do nothing 90 | }) 91 | 92 | document.addEventListener('keydown', (e) => { 93 | // We consider this independently of the keypress handler so we can intercept 94 | // keys that have built-in functions. 95 | const select = getFocusedSelect(); 96 | if (!select) { 97 | return; 98 | } 99 | 100 | if ([UP, DOWN, ESCAPE].indexOf(e.keyCode) >= 0) { 101 | e.preventDefault(); 102 | } 103 | 104 | if (select.isOpen()) { 105 | switch(e.keyCode) { 106 | case UP: 107 | case DOWN: 108 | select.moveHighlight(e.keyCode); 109 | break; 110 | case ENTER: 111 | select.selectHighlightedOption(); 112 | break; 113 | case ESCAPE: 114 | select.close(); 115 | select.target.focus(); 116 | } 117 | } else { 118 | if ([UP, DOWN, SPACE].indexOf(e.keyCode) >= 0) { 119 | select.open(); 120 | } 121 | } 122 | }); 123 | 124 | 125 | 126 | class Select extends Evented { 127 | constructor(options) { 128 | super(options); 129 | this.options = extend({}, Select.defaults, options); 130 | this.select = this.options.el; 131 | 132 | if (typeof this.select.selectInstance !== 'undefined') { 133 | throw new Error('This element has already been turned into a Select'); 134 | } 135 | 136 | this.update = this.update.bind(this); 137 | 138 | this.setupTarget(); 139 | this.renderTarget(); 140 | 141 | this.setupDrop(); 142 | this.renderDrop(); 143 | 144 | this.setupSelect(); 145 | 146 | this.setupTether(); 147 | this.bindClick(); 148 | 149 | this.bindMutationEvents(); 150 | 151 | this.value = this.select.value; 152 | } 153 | 154 | useNative() { 155 | const native = this.options.useNative; 156 | return native === true || (useNative() && native !== false); 157 | } 158 | 159 | setupTarget() { 160 | this.target = document.createElement('a'); 161 | this.target.href = 'javascript:;'; 162 | 163 | addClass(this.target, 'select-target'); 164 | 165 | const tabIndex = this.select.getAttribute('tabindex') || 0; 166 | this.target.setAttribute('tabindex', tabIndex); 167 | 168 | if (this.options.className) { 169 | addClass(this.target, this.options.className); 170 | } 171 | 172 | this.target.selectInstance = this; 173 | 174 | this.target.addEventListener('click', () => { 175 | if (!this.isOpen()) { 176 | this.target.focus(); 177 | } else { 178 | this.target.blur(); 179 | } 180 | }); 181 | 182 | this.target.addEventListener('focus', () => { 183 | addClass(this.target, 'select-target-focused'); 184 | }); 185 | 186 | this.target.addEventListener('blur', ({relatedTarget}) => { 187 | if (this.isOpen()) { 188 | if (relatedTarget && !this.drop.contains(relatedTarget)) { 189 | this.close(); 190 | } 191 | } 192 | 193 | removeClass(this.target, 'select-target-focused'); 194 | }); 195 | 196 | this.select.parentNode.insertBefore(this.target, this.select.nextSibling); 197 | } 198 | 199 | setupDrop() { 200 | this.drop = document.createElement('div'); 201 | addClass(this.drop, 'select'); 202 | 203 | if (this.options.className) { 204 | addClass(this.drop, this.options.className); 205 | } 206 | 207 | document.body.appendChild(this.drop); 208 | 209 | this.drop.addEventListener('click', (e) => { 210 | if (hasClass(e.target, 'select-option')) { 211 | this.pickOption(e.target); 212 | } 213 | 214 | // Built-in selects don't propagate click events in their drop directly 215 | // to the body, so we don't want to either. 216 | e.stopPropagation(); 217 | }); 218 | 219 | this.drop.addEventListener('mousemove', (e) => { 220 | if (hasClass(e.target, 'select-option')) { 221 | this.highlightOption(e.target); 222 | } 223 | }); 224 | 225 | this.content = document.createElement('div'); 226 | addClass(this.content, 'select-content'); 227 | this.drop.appendChild(this.content); 228 | } 229 | 230 | open() { 231 | addClass(this.target, 'select-open'); 232 | 233 | if (this.useNative()) { 234 | let event = document.createEvent("MouseEvents"); 235 | event.initEvent("mousedown", true, true); 236 | this.select.dispatchEvent(event); 237 | 238 | return; 239 | } 240 | 241 | addClass(this.drop, 'select-open'); 242 | 243 | setTimeout(() => { 244 | this.tether.enable(); 245 | }); 246 | 247 | const selectedOption = this.drop.querySelector('.select-option-selected'); 248 | 249 | if (!selectedOption) { 250 | return; 251 | } 252 | 253 | this.highlightOption(selectedOption); 254 | this.scrollDropContentToOption(selectedOption); 255 | 256 | const positionSelectStyle = () => { 257 | if (hasClass(this.drop, 'tether-abutted-left') || 258 | hasClass(this.drop, 'tether-abutted-bottom')) { 259 | const dropBounds = getBounds(this.drop); 260 | const optionBounds = getBounds(selectedOption); 261 | 262 | const offset = dropBounds.top - (optionBounds.top + optionBounds.height); 263 | 264 | this.drop.style.top = `${(parseFloat(this.drop.style.top) || 0) + offset}px`; 265 | } 266 | }; 267 | 268 | const alignToHighlighted = this.options.alignToHighlighted; 269 | const {scrollHeight, clientHeight} = this.content; 270 | if (alignToHighlighted === 'always' || (alignToHighlighted === 'auto' && scrollHeight <= clientHeight)) { 271 | setTimeout(() => { 272 | positionSelectStyle(); 273 | }); 274 | } 275 | 276 | this.trigger('open'); 277 | } 278 | 279 | close() { 280 | removeClass(this.target, 'select-open'); 281 | 282 | if (this.useNative()) { 283 | this.select.blur(); 284 | } 285 | 286 | this.tether.disable(); 287 | 288 | removeClass(this.drop, 'select-open'); 289 | 290 | this.trigger('close'); 291 | } 292 | 293 | toggle() { 294 | if (this.isOpen()) { 295 | this.close(); 296 | } else { 297 | this.open(); 298 | } 299 | } 300 | 301 | isOpen() { 302 | return hasClass(this.drop, 'select-open'); 303 | } 304 | 305 | bindClick() { 306 | this.target.addEventListener(clickEvent, (e) => { 307 | e.preventDefault(); 308 | this.toggle(); 309 | }); 310 | 311 | document.addEventListener(clickEvent, (event) => { 312 | if (!this.isOpen()) { 313 | return; 314 | } 315 | 316 | // Clicking inside dropdown 317 | if (event.target === this.drop || 318 | this.drop.contains(event.target)) { 319 | return; 320 | } 321 | 322 | // Clicking target 323 | if (event.target === this.target || 324 | this.target.contains(event.target)) { 325 | return; 326 | } 327 | 328 | this.close(); 329 | }); 330 | } 331 | 332 | setupTether() { 333 | this.tether = new Tether(extend({ 334 | element: this.drop, 335 | target: this.target, 336 | attachment: 'top left', 337 | targetAttachment: 'bottom left', 338 | classPrefix: 'select', 339 | constraints: [{ 340 | to: 'window', 341 | attachment: 'together' 342 | }] 343 | }, this.options.tetherOptions)); 344 | } 345 | 346 | renderTarget() { 347 | this.target.innerHTML = ''; 348 | 349 | const options = this.select.querySelectorAll('option'); 350 | for (let i = 0; i < options.length; ++i) { 351 | const option = options[i]; 352 | if (option.selected) { 353 | this.target.innerHTML = option.innerHTML; 354 | break; 355 | } 356 | } 357 | 358 | this.target.appendChild(document.createElement('b')); 359 | } 360 | 361 | renderDrop() { 362 | let optionList = document.createElement('ul'); 363 | addClass(optionList, 'select-options'); 364 | 365 | const options = this.select.querySelectorAll('option'); 366 | for (let i = 0; i < options.length; ++i) { 367 | let el = options[i]; 368 | let option = document.createElement('li'); 369 | addClass(option, 'select-option'); 370 | 371 | option.setAttribute('data-value', el.value); 372 | option.innerHTML = el.innerHTML; 373 | 374 | if (el.selected) { 375 | addClass(option, 'select-option-selected'); 376 | } 377 | 378 | optionList.appendChild(option); 379 | } 380 | 381 | this.content.innerHTML = ''; 382 | this.content.appendChild(optionList); 383 | } 384 | 385 | update() { 386 | this.renderDrop(); 387 | this.renderTarget(); 388 | } 389 | 390 | setupSelect() { 391 | this.select.selectInstance = this; 392 | 393 | addClass(this.select, 'select-select'); 394 | 395 | this.select.addEventListener('change', this.update); 396 | } 397 | 398 | bindMutationEvents() { 399 | if (typeof window.MutationObserver !== 'undefined') { 400 | this.observer = new MutationObserver(this.update); 401 | this.observer.observe(this.select, { 402 | childList: true, 403 | attributes: true, 404 | characterData: true, 405 | subtree: true 406 | }); 407 | } else { 408 | this.select.addEventListener('DOMSubtreeModified', this.update); 409 | } 410 | } 411 | 412 | findOptionsByPrefix(text) { 413 | let options = this.drop.querySelectorAll('.select-option'); 414 | 415 | text = text.toLowerCase(); 416 | 417 | return Array.prototype.filter.call(options, (option) => { 418 | return option.innerHTML.toLowerCase().substr(0, text.length) === text; 419 | }); 420 | } 421 | 422 | findOptionsByValue(val) { 423 | let options = this.drop.querySelectorAll('.select-option'); 424 | 425 | return Array.prototype.filter.call(options, (option) => { 426 | return option.getAttribute('data-value') === val; 427 | }); 428 | } 429 | 430 | getChosen() { 431 | if (this.isOpen()) { 432 | return this.drop.querySelector('.select-option-highlight'); 433 | } 434 | return this.drop.querySelector('.select-option-selected'); 435 | } 436 | 437 | selectOption(option) { 438 | if (this.isOpen()) { 439 | this.highlightOption(option); 440 | this.scrollDropContentToOption(option); 441 | } else { 442 | this.pickOption(option, false); 443 | } 444 | } 445 | 446 | resetSelection() { 447 | this.selectOption(this.drop.querySelector('.select-option')); 448 | } 449 | 450 | highlightOption(option) { 451 | let highlighted = this.drop.querySelector('.select-option-highlight'); 452 | if (highlighted) { 453 | removeClass(highlighted, 'select-option-highlight'); 454 | } 455 | 456 | addClass(option, 'select-option-highlight'); 457 | 458 | this.trigger('highlight', {option}); 459 | } 460 | 461 | moveHighlight(directionKeyCode) { 462 | const highlighted = this.drop.querySelector('.select-option-highlight'); 463 | if (!highlighted) { 464 | this.highlightOption(this.drop.querySelector('.select-option')); 465 | return; 466 | } 467 | 468 | const options = this.drop.querySelectorAll('.select-option'); 469 | 470 | let highlightedIndex = Array.prototype.indexOf.call(options, highlighted); 471 | if (!(highlightedIndex >= 0)) { 472 | return; 473 | } 474 | 475 | if (directionKeyCode === UP) { 476 | highlightedIndex -= 1; 477 | } else { 478 | highlightedIndex += 1; 479 | } 480 | 481 | if (highlightedIndex < 0 || highlightedIndex >= options.length) { 482 | return; 483 | } 484 | 485 | const newHighlight = options[highlightedIndex]; 486 | 487 | this.highlightOption(newHighlight); 488 | this.scrollDropContentToOption(newHighlight); 489 | } 490 | 491 | scrollDropContentToOption(option) { 492 | const {scrollHeight, clientHeight, scrollTop} = this.content; 493 | if (scrollHeight > clientHeight) { 494 | const contentBounds = getBounds(this.content); 495 | const optionBounds = getBounds(option); 496 | 497 | this.content.scrollTop = optionBounds.top - (contentBounds.top - scrollTop); 498 | } 499 | } 500 | 501 | selectHighlightedOption() { 502 | this.pickOption(this.drop.querySelector('.select-option-highlight')); 503 | } 504 | 505 | pickOption(option, close=true) { 506 | this.value = this.select.value = option.getAttribute('data-value'); 507 | this.triggerChange(); 508 | 509 | if (close) { 510 | setTimeout(() => { 511 | this.close(); 512 | this.target.focus(); 513 | }); 514 | } 515 | } 516 | 517 | triggerChange() { 518 | let event = document.createEvent("HTMLEvents"); 519 | event.initEvent("change", true, false); 520 | this.select.dispatchEvent(event); 521 | 522 | this.trigger('change', {value: this.select.value}); 523 | } 524 | 525 | change(val) { 526 | const options = this.findOptionsByValue(val); 527 | 528 | if (!options.length) { 529 | throw new Error(`Select Error: An option with the value "${ val }" doesn't exist`); 530 | } 531 | 532 | this.pickOption(options[0], false); 533 | } 534 | } 535 | 536 | Select.defaults = { 537 | alignToHighlighed: 'auto', 538 | className: 'select-theme-default' 539 | }; 540 | 541 | Select.init = (options={}) => { 542 | if (document.readyState === 'loading') { 543 | document.addEventListener('DOMContentLoaded', () => Select.init(options)); 544 | return; 545 | } 546 | 547 | if (typeof options.selector === 'undefined') { 548 | options.selector = 'select'; 549 | } 550 | 551 | const selectors = document.querySelectorAll(options.selector); 552 | for (let i = 0; i < selectors.length; ++i) { 553 | const el = selectors[i]; 554 | if (!el.selectInstance) { 555 | new Select(extend({el}, options)); 556 | } 557 | } 558 | }; 559 | --------------------------------------------------------------------------------