├── .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 [](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 '};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 \n '+b.dataset.map(function(a,c){return'\n - \n \n '+
17 | (a.content||a.text||a.value)+"\n
"}).join("")+"\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 '};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 \n '+c.dataset.map(function(a,b){return'\n - \n \n '+
17 | (a.content||a.text||a.value)+"\n
"}).join("")+"\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 ``;
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 |
219 | ${ state.dataset.map(function(item, index){
220 | const selected = (item === state.selected || item.value === state.selected) ? "select__item--selected" : "";
221 | const value = item.content || item.text || item.value;
222 | const itemClass = item.class || "";
223 | return `
224 | -
225 |
226 | ${value}
227 |
`;
228 | }).join('') }
229 |
`;
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 |
--------------------------------------------------------------------------------