├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bower.json ├── dist ├── vanilla-select-ie.min.js ├── vanilla-select.css ├── vanilla-select.min.js └── vanilla-select.min.js.gz ├── example.html ├── gulpfile.js ├── package-lock.json ├── package.json ├── src ├── vanilla-select.js └── vanilla-select.scss └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | ## 1.0.18 (2020-10-27) 7 | 8 | 9 | 10 | ## 1.0.13 (2018-07-03) 11 | 12 | 13 | 14 | 15 | ## 1.0.12 (2018-07-03) 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Kateryna Vorotina 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vanilla-select [![npm version](https://badge.fury.io/js/vanilla-select.svg)](https://www.npmjs.com/package/vanilla-select) 2 | A vanilla, lightweight (~2.5kb gzipped), configurable select box component. 3 | 4 | [Demo Page](https://vorotina.github.io/vanilla-select/) 5 | 6 | ## Advantages 7 | * Lightweight 8 | * No Dependencies 9 | * Elegant API - inspiration taken from [React.Component](https://facebook.github.io/react/docs/react-component.html) 10 | * Fast search 11 | 12 | 13 | ## Installation 14 | With [NPM](https://www.npmjs.com/package/vanilla-select): 15 | ```zsh 16 | npm install vanilla-select --save-dev 17 | ``` 18 | 19 | With [Bower](https://bower.io/): 20 | ```zsh 21 | bower install vanilla-select --save-dev 22 | ``` 23 | 24 | Or include directly: 25 | 26 | ```html 27 | 28 | 29 | 30 | 31 | ``` 32 | ## Setup 33 | 34 | ```js 35 | const source = [{ 36 | icon: 'fa-font', 37 | value: 'Amatic SC' 38 | }]; 39 | 40 | const select = new Select({ 41 | placeholder: 'Select Font', 42 | dataset: source, 43 | search: true, 44 | noResults: 'No results found', 45 | onSelected: item => callback(item) 46 | }).componentMount({ 47 | el: document.querySelector('[ref="select"]') 48 | }); 49 | ``` 50 | 51 | ## Configuration 52 | | Option | Definition | 53 | | ------------ | ---------- | 54 | | placeholder | Type: String
Default: ''
Placeholder text | 55 | | dataset | Type: Array
Default: []
Equivelant to the element within a select | 56 | | search | Type: Boolean
Default: false
Whether a user should be allowed to search | 57 | | noResults | Type: String
Default: ''
The text that is shown when search has returned no results | 58 | | selected | Type: Object
Default: null
Default selected option 59 | | onSelected | Arguments: item
Callback, invoked each time the item is selected, regardless if it changes the value 60 | 61 | ## Development 62 | To setup a local environment: clone this repo, navigate into it's directory in a terminal window and run the following command: 63 | 64 | ```npm install``` 65 | 66 | ## Browser compatibility 67 | vanilla-select is compiled using [Closure Compiler](https://developers.google.com/closure/compiler/) to enable support for [ES5 browsers](http://caniuse.com/#feat=es5). 68 | 69 | ### Browsers 70 | Edge 15+ 71 | Chrome 41+ 72 | FireFox 35+ 73 | Opera 28+ 74 | Safari 9+ 75 | 76 | If you need to support IE11 and IE Edge14 - use vanilla-select-ie.min.js bundle. 77 | It includes [element-closest](https://github.com/jonathantneal/closest/blob/master/element-closest.js) polyfill. 78 | 79 | 80 | ### Gulp tasks 81 | | Task | Usage | 82 | | ------------------- | ------------------------------------------------------------ | 83 | | `gulp build` | Build JS an CSS | 84 | | `gulp serve` | Fire up local server for development | 85 | 86 | ### Roadmap 87 | * Keyboard navigation 88 | 89 | 90 | ## License 91 | MIT License 92 | 93 | 94 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vanilla-select", 3 | "description": "Standalone replacement for select boxes", 4 | "main": [ 5 | "./dist/vanilla-select.min.js", 6 | "./dist/vanilla-select.min.css" 7 | ], 8 | "authors": [ 9 | "Kateryna Vorotina" 10 | ], 11 | "ignore": [ 12 | "node_modules" 13 | ], 14 | "keywords": [ 15 | "vanilla", 16 | "select", 17 | "dropdown", 18 | "js" 19 | ], 20 | "license": "MIT", 21 | "homepage": "https://github.com/vorotina/vanilla-select", 22 | "version": "1.0.13" 23 | } 24 | -------------------------------------------------------------------------------- /dist/vanilla-select-ie.min.js: -------------------------------------------------------------------------------- 1 | var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.inherits=function(a,c){function e(){}e.prototype=c.prototype;a.superClass_=c.prototype;a.prototype=new e;a.prototype.constructor=a;for(var d in c)if(Object.defineProperties){var b=Object.getOwnPropertyDescriptor(c,d);b&&Object.defineProperty(a,d,b)}else a[d]=c[d]};$jscomp.owns=function(a,c){return Object.prototype.hasOwnProperty.call(a,c)};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1; 2 | $jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(a,c,e){a!=Array.prototype&&a!=Object.prototype&&(a[c]=e.value)};$jscomp.getGlobal=function(a){return"undefined"!=typeof window&&window===a?a:"undefined"!=typeof global&&null!=global?global:a};$jscomp.global=$jscomp.getGlobal(this); 3 | $jscomp.polyfill=function(a,c,e,d){if(c){e=$jscomp.global;a=a.split(".");for(d=0;d\n
\n \n
'+b.placeholder+'
\n
\n
\n '};c.prototype.componentDidMount=function(){var a=this;document.body.addEventListener("click",this.onDocumentClick);this.$defer("mounted",function(){return document.body.removeEventListener("click",a.onDocumentClick)})};c.prototype.componentDidUnmount=function(){this.dropdown.componentUnmount()};c.prototype.componentDidUpdate=function(){var a=this,b=this.refs.toolbox;b.addEventListener("click",this.onToolboxClick);this.$defer("updated",function(){return b.removeEventListener("click", 14 | a.onToolboxClick)});this.dropdown.componentMount({el:this.refs.dropdown})};var e=function(d){d=a.call(this,d)||this;d.state={query:"",dataset:d.props.dataset||[],selected:d.props.selected,openDownwards:!0};d.onQueryChanged=d.onQueryChanged.bind(d);d.onItemClicked=d.onItemClicked.bind(d);return d};$jscomp.inherits(e,a);e.prototype.onQueryChanged=function(a){var b=a.target.value,c=new RegExp(b,"gi"),d=(this.props.dataset||[]).filter(function(a){return c.test(a.text||a.value||"")});this.setState(function(a){return{query:b, 15 | dataset:d}})};e.prototype.onItemClicked=function(a){if((a=a.target.closest("li"))&&this.state){var b=this.state.dataset[a.dataset.index];this.props.onSelected&&this.props.onSelected(b);this.setState(function(a){return{selected:b}})}};e.prototype.render=function(a,b){if(0===b.dataset.length)return'\n '+(a.noResults||"")+"";var c="";a.search&&(c='');return c+'\n "};e.prototype.componentDidUpdate=function(){var a=this,b=this.$el,c=b.getBoundingClientRect();c.top+c.height>window.innerHeight&&(b.style.marginTop=-b.clientHeight-b.parentElement.clientHeight+"px");if(this.props.search){var e=this.refs.query;e.addEventListener("change",this.onQueryChanged);this.$defer("updated",function(){return e.removeEventListener("change",a.onQueryChanged)})}var f= 18 | this.refs.list;f&&(f.addEventListener("click",this.onItemClicked),this.$defer("updated",function(){return f.removeEventListener("click",a.onItemClicked)}))};return c}); 19 | -------------------------------------------------------------------------------- /dist/vanilla-select.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8";.select__box{position:relative}.select__toolbox{cursor:pointer;padding:.25em .5em;border-radius:5px;border:1px solid #2d8682}.select__label{height:25px;line-height:25px;margin-right:25px}.select__arrow{position:absolute;font-style:normal;cursor:pointer;right:0;bottom:0;text-align:center;width:30px;border-radius:0 5px 5px 0;color:#2d2d2d}.select__arrow:before{content:'›';transform:rotate(90deg);font-size:30px;position:relative;margin:5px 0 0 13px;float:left}.select__dropdown{position:absolute;min-width:100%;border-radius:5px;border:1px solid #2d8682;transition:all .3s;background:#000;color:#fff;z-index:1;visibility:hidden;opacity:0}.select__dropdown .select__query{width:calc(100% - 12px);padding:8px;margin:6px;border-radius:5px;background:#fff;border:1px solid #2d8682}.select__dropdown .select__query_noresult{padding:0 10px 10px;display:block;font-size:14px;color:#666}.select__dropdown .select__list{width:100%;padding-left:0;list-style:none;overflow-y:auto}.select__dropdown .select__list .select__item{cursor:pointer;padding:.5em}.select__dropdown .select__list .select__item:hover{background:#397271}.select__dropdown .select__list .select__item .select__item_icon{display:inline-block;margin-right:.25em}.select__dropdown .select__list .select__item .select__item_text{font-size:14px;font-weight:300;letter-spacing:1.2px}.select__dropdown .select__list .select__item--selected{background:#46b6b4}.select__dropdown--show{visibility:visible;opacity:1} -------------------------------------------------------------------------------- /dist/vanilla-select.min.js: -------------------------------------------------------------------------------- 1 | var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.inherits=function(a,d){function e(){}e.prototype=d.prototype;a.superClass_=d.prototype;a.prototype=new e;a.prototype.constructor=a;for(var b in d)if(Object.defineProperties){var c=Object.getOwnPropertyDescriptor(d,b);c&&Object.defineProperty(a,b,c)}else a[b]=d[b]};$jscomp.owns=function(a,d){return Object.prototype.hasOwnProperty.call(a,d)};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1; 2 | $jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(a,d,e){a!=Array.prototype&&a!=Object.prototype&&(a[d]=e.value)};$jscomp.getGlobal=function(a){return"undefined"!=typeof window&&window===a?a:"undefined"!=typeof global&&null!=global?global:a};$jscomp.global=$jscomp.getGlobal(this); 3 | $jscomp.polyfill=function(a,d,e,b){if(d){e=$jscomp.global;a=a.split(".");for(b=0;b\n
\n \n
'+c.placeholder+'
\n
\n
\n '};d.prototype.componentDidMount=function(){var a=this;document.body.addEventListener("click",this.onDocumentClick);this.$defer("mounted",function(){return document.body.removeEventListener("click",a.onDocumentClick)})};d.prototype.componentDidUnmount=function(){this.dropdown.componentUnmount()};d.prototype.componentDidUpdate=function(){var a=this,c=this.refs.toolbox;c.addEventListener("click",this.onToolboxClick);this.$defer("updated",function(){return c.removeEventListener("click", 14 | a.onToolboxClick)});this.dropdown.componentMount({el:this.refs.dropdown})};var e=function(b){b=a.call(this,b)||this;b.state={query:"",dataset:b.props.dataset||[],selected:b.props.selected,openDownwards:!0};b.onQueryChanged=b.onQueryChanged.bind(b);b.onItemClicked=b.onItemClicked.bind(b);return b};$jscomp.inherits(e,a);e.prototype.onQueryChanged=function(a){var b=a.target.value,d=new RegExp(b,"gi"),e=(this.props.dataset||[]).filter(function(a){return d.test(a.text||a.value||"")});this.setState(function(a){return{query:b, 15 | dataset:e}})};e.prototype.onItemClicked=function(a){if((a=a.target.closest("li"))&&this.state){var b=this.state.dataset[a.dataset.index];this.props.onSelected&&this.props.onSelected(b);this.setState(function(a){return{selected:b}})}};e.prototype.render=function(a,c){if(0===c.dataset.length)return'\n '+(a.noResults||"")+"";var b="";a.search&&(b='');return b+'\n "};e.prototype.componentDidUpdate=function(){var a=this,c=this.$el,d=c.getBoundingClientRect();d.top+d.height>window.innerHeight&&(c.style.marginTop=-c.clientHeight-c.parentElement.clientHeight+"px");if(this.props.search){var e=this.refs.query;e.addEventListener("change",this.onQueryChanged);this.$defer("updated",function(){return e.removeEventListener("change",a.onQueryChanged)})}var f= 18 | this.refs.list;f&&(f.addEventListener("click",this.onItemClicked),this.$defer("updated",function(){return f.removeEventListener("click",a.onItemClicked)}))};return d}); 19 | -------------------------------------------------------------------------------- /dist/vanilla-select.min.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vorotina/vanilla-select/dd840979a6d7b9d24f7203c27152f5b919f1a2e9/dist/vanilla-select.min.js.gz -------------------------------------------------------------------------------- /example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | VanillaSelect - standalone replacement for select elements 5 | 6 | 7 | 8 | 9 | 34 | 35 | 36 |
37 |

EXAMPLE:

38 | 39 |

Support search:

40 |
41 |
42 | 43 |

And default selected option:

44 |
45 |
46 | 47 | 48 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var clean = require('gulp-clean'); 3 | var sass = require('gulp-sass'); 4 | var csslint = require('gulp-csslint'); 5 | var cssmin = require('gulp-cssmin'); 6 | var concat = require('gulp-concat'); 7 | var jshint = require('gulp-jshint'); 8 | var stripDebug = require('gulp-strip-debug'); 9 | var closureCompiler = require('gulp-closure-compiler'); 10 | var gzip = require('gulp-gzip'); 11 | var server = require('gulp-server-livereload'); 12 | 13 | gulp.task('clean', function(){ 14 | return gulp.src('./dist/*') 15 | .pipe(clean()); 16 | }); 17 | 18 | gulp.task('sass', function () { 19 | return gulp.src('./src/**.scss') 20 | .pipe(sass().on('error', sass.logError)) 21 | .pipe(gulp.dest('./dist/')); 22 | }); 23 | 24 | gulp.task('csslint', function(){ 25 | return gulp.src(['./dist/*.css']) 26 | .pipe(csslint({ 27 | 'adjoining-classes' : false 28 | })) 29 | .pipe(csslint.reporter()); 30 | }); 31 | 32 | gulp.task('style', ['sass', 'csslint'], function() { 33 | return gulp.src('./dist/*.css') 34 | .pipe(cssmin()) 35 | .pipe(gulp.dest('./dist/')); 36 | }); 37 | 38 | gulp.task('jshint', function(){ 39 | return gulp.src('./src/*.js') 40 | .pipe(jshint({ "esversion": 6 })) 41 | .pipe(jshint.reporter('default')); 42 | }); 43 | 44 | gulp.task('script', function() { 45 | return gulp.src('./src/*.js') 46 | .pipe(stripDebug()) 47 | .pipe(closureCompiler({ 48 | compilerPath: 'node_modules/google-closure-compiler/compiler.jar', 49 | fileName: 'vanilla-select.min.js' 50 | })) 51 | .pipe(gulp.dest('dist')) 52 | .pipe(gzip()) 53 | .pipe(gulp.dest('dist')) 54 | }); 55 | 56 | gulp.task('script-ie', function() { 57 | return gulp.src(['./node_modules/element-closest/element-closest.js', './src/*.js']) 58 | .pipe(stripDebug()) 59 | .pipe(closureCompiler({ 60 | compilerPath: 'node_modules/google-closure-compiler/compiler.jar', 61 | fileName: 'vanilla-select-ie.min.js' 62 | })) 63 | .pipe(gulp.dest('dist')) 64 | }); 65 | 66 | gulp.task('build', ['clean', 'style', 'script']); 67 | 68 | gulp.task('serve', function() { 69 | gulp.src('.') 70 | .pipe(server({ 71 | livereload: true, 72 | directoryListing: true, 73 | open: true 74 | })); 75 | }); 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vanilla-select", 3 | "version": "1.0.25", 4 | "description": "Standalone replacement for select boxes", 5 | "main": "./dist/vanilla-select.min.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/vorotina/vanilla-select" 9 | }, 10 | "keywords": [ 11 | "vanilla", 12 | "select", 13 | "dropdown", 14 | "js" 15 | ], 16 | "author": { 17 | "name": "Kateryna Vorotina", 18 | "email": "vorotina@gmail.com" 19 | }, 20 | "scripts": { 21 | "build": "gulp build", 22 | "build-ie": "gulp script-ie", 23 | "serve": "gulp serve" 24 | }, 25 | "license": "MIT", 26 | "homepage": "https://github.com/vorotina/vanilla-select#readme", 27 | "devDependencies": { 28 | "google-closure-compiler": "^20170521.0.0", 29 | "gulp": "^3.9.1", 30 | "gulp-clean": "^0.3.2", 31 | "gulp-cli": "^1.2.1", 32 | "gulp-closure-compiler": "^0.4.0", 33 | "gulp-concat": "^2.6.0", 34 | "gulp-csslint": "^0.3.0", 35 | "gulp-cssmin": "^0.1.7", 36 | "gulp-gzip": "^1.4.0", 37 | "gulp-jshint": "^2.0.4", 38 | "gulp-sass": "^4.1.0", 39 | "gulp-server-livereload": "^1.9.2", 40 | "gulp-strip-debug": "^1.1.0", 41 | "jshint": "^2.9.4", 42 | "standard-version": "^4.4.0" 43 | }, 44 | "dependencies": { 45 | "element-closest": "^2.0.2" 46 | }, 47 | "resolutions": { 48 | "graceful-fs": "^4.2.4" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/vanilla-select.js: -------------------------------------------------------------------------------- 1 | (function (name, definition) { 2 | if (typeof define === 'function') { 3 | define(name, definition); 4 | } else if (typeof module !== 'undefined' && module.exports) { 5 | module.exports = definition(); 6 | } else { 7 | this[name] = definition(); 8 | } 9 | }('Select', function () { 10 | 'use strict'; 11 | 12 | class Component { 13 | 14 | constructor(props) { 15 | this.props = props || {}; 16 | this.state = {}; 17 | this.refs = {}; 18 | this.$disposables = { 19 | mounted: [], 20 | updated: [] 21 | }; 22 | } 23 | 24 | $defer(stage, dispose) { 25 | this.$disposables[stage].push(dispose); 26 | } 27 | 28 | $dispose(stage) { 29 | this.$disposables[stage].reverse().forEach(dispose => dispose()); 30 | this.$disposables[stage].length = 0; 31 | } 32 | 33 | componentMount(ctx) { 34 | this.$el && this.componentUnmount(); 35 | this.componentWillMount(); 36 | this.$el = (ctx && ctx.el) || document.createElement('div'); 37 | this.$defer('mounted', () => { 38 | this.$el.innerHTML = ''; 39 | this.$el = null; 40 | }); 41 | this.forceUpdate(); 42 | this.componentDidMount(); 43 | return this; 44 | } 45 | 46 | componentWillMount() {} 47 | 48 | componentDidMount() {} 49 | 50 | componentUnmount() { 51 | this.componentWillUnmount(); 52 | this.$dispose('mounted'); 53 | this.componentDidUnmount(); 54 | return this; 55 | } 56 | 57 | componentWillUnmount() {} 58 | 59 | componentDidUnmount() {} 60 | 61 | setState(updater, callback) { 62 | let prevState = this.state; 63 | let nextState = typeof updater == 'function' ? updater(prevState, this.props) : updater; 64 | this.state = Object.assign(prevState, nextState); 65 | if (this.shouldComponentUpdate(prevState, nextState)) { 66 | this.forceUpdate(); 67 | } 68 | callback && callback.call(this); 69 | return this; 70 | } 71 | 72 | forceUpdate() { 73 | this.componentUpdate(this.props, this.state); 74 | return this; 75 | } 76 | 77 | shouldComponentUpdate(prevState, nextState) { 78 | return prevState !== nextState; 79 | } 80 | 81 | componentUpdate(props, state) { 82 | if (this.$el) { 83 | this.componentWillUpdate(props, state); 84 | this.$dispose('updated'); 85 | this.$el.innerHTML = this.render(props, state); 86 | this.refs = Array.from(this.$el.querySelectorAll('[ref]')).reduce(function (refs, node) { 87 | return (refs[node.getAttribute('ref')] = node), refs; 88 | }, {}); 89 | this.componentDidUpdate(props, state); 90 | } 91 | } 92 | 93 | componentWillUpdate(props, state) {} 94 | 95 | render(props, state) { 96 | return ''; 97 | } 98 | 99 | componentDidUpdate(props, state) {} 100 | } 101 | 102 | class Select extends Component { 103 | constructor(props) { 104 | super(props); 105 | const selected = this.props.selected; 106 | this.state = { 107 | placeholder: selected && selected.text ? selected.text : selected || this.props.placeholder || '', 108 | selected: selected || null, 109 | expanded: false 110 | }; 111 | this.onDocumentClick = this.onDocumentClick.bind(this); 112 | this.onToolboxClick = this.onToolboxClick.bind(this); 113 | this.dropdown = new Dropdown(Object.assign({}, this.props, { 114 | onSelected: selected => { 115 | this.props.onSelected && this.props.onSelected(selected); 116 | this.setState(prevState => ({ 117 | placeholder: selected.text || selected.value, 118 | selected: selected, 119 | expanded: false 120 | })); 121 | } 122 | })); 123 | } 124 | 125 | onDocumentClick(event) { 126 | if (event.$owner !== this) { 127 | this.setState(prevState => ({ 128 | expanded: false 129 | })); 130 | } 131 | } 132 | 133 | onToolboxClick(event) { 134 | event.$owner = this; 135 | this.setState(prevState => ({ 136 | expanded: !prevState.expanded 137 | }), () => { 138 | if (this.props.search && this.state.expanded) { 139 | this.dropdown.refs.query.focus(); 140 | } 141 | }); 142 | } 143 | 144 | render(props, state) { 145 | return `
146 |
147 | 148 |
${state.placeholder}
149 |
150 |
151 |
`; 152 | } 153 | 154 | componentDidMount() { 155 | document.body.addEventListener('click', this.onDocumentClick); 156 | this.$defer('mounted', () => document.body.removeEventListener('click', this.onDocumentClick)); 157 | } 158 | 159 | componentDidUnmount() { 160 | this.dropdown.componentUnmount(); 161 | } 162 | 163 | componentDidUpdate() { 164 | const $toolbox = this.refs.toolbox; 165 | $toolbox.addEventListener('click', this.onToolboxClick); 166 | this.$defer('updated', () => $toolbox.removeEventListener('click', this.onToolboxClick)); 167 | this.dropdown.componentMount({ 168 | el: this.refs.dropdown 169 | }); 170 | } 171 | } 172 | 173 | class Dropdown extends Component { 174 | constructor(props) { 175 | super(props); 176 | this.state = { 177 | query: "", 178 | dataset: this.props.dataset || [], 179 | selected: this.props.selected, 180 | openDownwards: true 181 | }; 182 | this.onQueryChanged = this.onQueryChanged.bind(this); 183 | this.onItemClicked = this.onItemClicked.bind(this); 184 | } 185 | 186 | onQueryChanged(event) { 187 | const query = event.target.value; 188 | const match = new RegExp(query, 'gi'); 189 | const dataset = (this.props.dataset || []).filter(item => match.test(item.text || item.value || '')); 190 | this.setState(prevState => ({ 191 | query: query, 192 | dataset: dataset 193 | })); 194 | } 195 | 196 | onItemClicked(event) { 197 | const target = event.target.closest('li'); 198 | if (target && this.state) { 199 | const index = target.dataset.index; 200 | const selected = this.state.dataset[index]; 201 | this.props.onSelected && this.props.onSelected(selected); 202 | this.setState(prevState => ({ 203 | selected: selected 204 | })); 205 | } 206 | } 207 | 208 | render(props, state) { 209 | if (state.dataset.length === 0) { 210 | return ` 211 | ${props.noResults || ''}`; 212 | } 213 | let search = ''; 214 | if (props.search) { 215 | search = ``; 216 | } 217 | return `${search} 218 | `; 230 | } 231 | 232 | 233 | 234 | componentDidUpdate() { 235 | const el = this.$el; 236 | const bounds = el.getBoundingClientRect(); 237 | if (bounds.top + bounds.height > window.innerHeight) { 238 | el.style.marginTop = - el.clientHeight - el.parentElement.clientHeight + 'px'; 239 | } 240 | if (this.props.search) { 241 | const $query = this.refs.query; 242 | $query.addEventListener('change', this.onQueryChanged); 243 | this.$defer('updated', () => $query.removeEventListener('change', this.onQueryChanged)); 244 | } 245 | const $list = this.refs.list; 246 | if ($list) { 247 | $list.addEventListener('click', this.onItemClicked); 248 | this.$defer('updated', () => $list.removeEventListener('click', this.onItemClicked)); 249 | } 250 | } 251 | } 252 | 253 | return Select; 254 | })); 255 | -------------------------------------------------------------------------------- /src/vanilla-select.scss: -------------------------------------------------------------------------------- 1 | .select__box { 2 | position: relative; 3 | } 4 | 5 | .select__toolbox { 6 | cursor: pointer; 7 | padding: 0.25em .5em; 8 | border-radius: 5px; 9 | border: 1px solid #2d8682; 10 | } 11 | 12 | .select__label { 13 | height: 25px; 14 | line-height: 25px; 15 | margin-right: 25px; 16 | } 17 | 18 | .select__arrow { 19 | position: absolute; 20 | font-style: normal; 21 | cursor: pointer; 22 | right: 0; 23 | bottom: 0; 24 | text-align: center; 25 | width: 30px; 26 | border-radius: 0 5px 5px 0; 27 | color: #2d2d2d; 28 | 29 | &:before{ 30 | content: '›'; 31 | transform: rotate(90deg); 32 | font-size: 30px; 33 | position: relative; 34 | margin: 5px 0 0 13px; 35 | float: left; 36 | } 37 | } 38 | 39 | 40 | .select__dropdown { 41 | position: absolute; 42 | min-width: 100%; 43 | border-radius: 5px; 44 | border: 1px solid #2d8682; 45 | transition: all .3s; 46 | background: black; 47 | color: #fff; 48 | z-index: 1; 49 | visibility: hidden; 50 | opacity: 0; 51 | 52 | .select__query { 53 | width: calc(100% - 12px); 54 | padding: 8px; 55 | margin: 6px; 56 | border-radius: 5px; 57 | background: #fff; 58 | border: 1px solid #2d8682; 59 | } 60 | 61 | .select__query_noresult{ 62 | padding: 0 10px 10px; 63 | display: block; 64 | font-size: 14px; 65 | color: #666; 66 | } 67 | 68 | .select__list { 69 | width: 100%; 70 | padding-left: 0; 71 | list-style: none; 72 | overflow-y: auto; 73 | 74 | .select__item { 75 | cursor: pointer; 76 | padding: .5em; 77 | 78 | &:hover { 79 | background: #397271; 80 | } 81 | 82 | .select__item_icon { 83 | display: inline-block; 84 | margin-right: .25em; 85 | } 86 | 87 | .select__item_text { 88 | font-size: 14px; 89 | font-weight: 300; 90 | letter-spacing: 1.2px; 91 | } 92 | } 93 | 94 | .select__item--selected { 95 | background: #46b6b4; 96 | } 97 | } 98 | } 99 | 100 | .select__dropdown--show { 101 | visibility: visible; 102 | opacity: 1; 103 | } 104 | --------------------------------------------------------------------------------