├── .gitignore ├── .jshintrc ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── index.html ├── vanilla-js-dropdown.css └── vanilla-js-dropdown.min.js ├── docs ├── images │ └── vanilla-js-logo.png ├── index.html ├── javascript │ ├── prism.js │ └── vanilla-js-dropdown.min.js └── styles │ ├── docs-page.css │ ├── docs-page.less │ ├── prism.css │ └── vanilla-js-dropdown.css ├── gulpfile.js ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── index.pug ├── javascript │ └── vanilla-js-dropdown.js └── styles │ └── vanilla-js-dropdown.less └── test ├── main.js └── spec ├── dropdown.spec.js └── fixtures ├── select_disabled.fixture.html ├── select_id.fixture.html ├── select_no_id.fixture.html └── select_optgroup.fixture.html /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /coverage -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // Define globals exposed by modern browsers. 3 | "browser": true, 4 | 5 | // Define globals exposed by Node.js. 6 | "node": true, 7 | 8 | // Force all variable names to use either camelCase style or UPPER_CASE. 9 | "camelcase": true, 10 | 11 | // Prohibit use of == and != in favor of === and !==. 12 | "eqeqeq": true, 13 | 14 | // Enforce tab width of 2 spaces. 15 | "indent": 2, 16 | 17 | // Prohibit use of a variable before it is defined. 18 | "latedef": false, 19 | 20 | // Enforce line length to 100 characters 21 | "maxlen": 100, 22 | 23 | // Require capitalized names for constructor functions. 24 | "newcap": true, 25 | 26 | // Enforce use of single quotation marks for strings. 27 | "quotmark": "single", 28 | 29 | // Prohibit use of explicitly undeclared variables. 30 | "undef": true, 31 | 32 | // Warn when variables are defined but never used. 33 | "unused": true, 34 | 35 | // Suppress warnings about == null comparisons. 36 | "eqnull": true 37 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "bracketSpacing": true, 6 | "printWidth": 100 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "11.1.0" 5 | 6 | branches: 7 | except: 8 | - npm 9 | 10 | branches: 11 | only: 12 | - master 13 | 14 | script: 15 | - npm start 16 | - npm test 17 | 18 | after_script: 19 | - cat ./coverage/**/lcov.info | ./node_modules/coveralls/bin/coveralls.js 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | --- 4 | 5 | ## [2.3.0] - 2022-07-23 6 | 7 | ### Added 8 | 9 | 1. Support for disabled list items 10 | 11 | ## [2.2.0] - 2019-04-13 12 | 13 | ### Added 14 | 15 | 1. Support for `` 16 | 17 | ## [2.1.2] - 2019-03-11 18 | 19 | ### Fixed 20 | 21 | 1. Original selected option shown selected in dropdown on page refresh or navigating to page 22 | 23 | ## [2.1.1] - 2018-04-28 24 | 25 | ### Fixed 26 | 27 | 1. Invalid _id_ attribute assigned when a DOM element passed 28 | 29 | ## [2.1.0] - 2017-10-21 30 | 31 | ### Added 32 | 33 | 1. Ability to provide a direct DOM element (as an alternative to providing an _id_) 34 | 35 | ## [2.0.0] - 2017-04-19 36 | 37 | ### Added 38 | 39 | 1. Ability to create the pseudo-select based on existing selects 40 | 2. [SUIT](https://suitcss.github.io/) naming convention for CSS 41 | 42 | ### Removed 43 | 44 | - [ BREAKING ] - ability to modify CSS classes through options 45 | - [ BREAKING ] - ability to set the pseudo-select opened through options (can be done with existing _.open()_ method instead) 46 | - [ BREAKING ] - the hidden input to store the pseudo-select value (unnecessary with setting and using the value of original select element) 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vanilla JavaScript Dropdown - replacement for `` that makes styling easy and consistent. 6 | 7 | _— Inspired by the blazing fast, lightweight, cross-platform and crazy popular [Vanilla JS](http://vanilla-js.com/) framework._ 8 | 9 | ## How it works 10 | 11 | Reads the original ``, so when you submit your form the value will be there. 14 | 15 | JavaScript disabled? No problem! Nicely degrades to original ` 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 35 | 36 | -------------------------------------------------------------------------------- /dist/vanilla-js-dropdown.css: -------------------------------------------------------------------------------- 1 | .js-Dropdown { 2 | display: inline-block; 3 | font: 400 14px sans-serif; 4 | position: relative; 5 | width: 20em; 6 | } 7 | .js-Dropdown-title { 8 | background: #ffffff; 9 | border: 1px groove #a5a5a5; 10 | box-sizing: border-box; 11 | cursor: pointer; 12 | font: 400 14px sans-serif; 13 | height: 3em; 14 | padding: 0.5em; 15 | position: relative; 16 | text-align: left; 17 | width: 100%; 18 | } 19 | .js-Dropdown-title:after { 20 | border-color: #a5a5a5 transparent transparent transparent; 21 | border-style: solid; 22 | border-width: 10px 12px; 23 | content: ''; 24 | display: block; 25 | height: 0; 26 | position: absolute; 27 | right: 1em; 28 | top: 45%; 29 | width: 0; 30 | } 31 | .js-Dropdown-list { 32 | background: #ffffff; 33 | border-left: 1px solid #a5a5a5; 34 | border-right: 1px solid #a5a5a5; 35 | box-sizing: border-box; 36 | display: none; 37 | height: 0; 38 | list-style: none; 39 | margin: 0; 40 | opacity: 0; 41 | padding: 0; 42 | position: absolute; 43 | transition: 0.2s linear; 44 | width: 100%; 45 | z-index: 999; 46 | } 47 | .js-Dropdown-list.is-open { 48 | display: block; 49 | height: auto; 50 | opacity: 1; 51 | } 52 | .js-Dropdown-list li { 53 | border-bottom: 1px solid #a5a5a5; 54 | cursor: pointer; 55 | padding: 1em 0.5em; 56 | } 57 | .js-Dropdown-list li:hover { 58 | background-color: #fff5e9; 59 | } 60 | .js-Dropdown-list li.is-selected { 61 | background-color: #ffdfb6; 62 | } 63 | .js-Dropdown-list li.is-disabled { 64 | background-color: #f5f5f5; 65 | color: #a5a5a5; 66 | cursor: not-allowed; 67 | } 68 | .js-Dropdown-optgroup { 69 | border-bottom: 1px solid #a5a5a5; 70 | color: #a5a5a5; 71 | cursor: default; 72 | padding: 1em 0.5em; 73 | text-align: center; 74 | } 75 | -------------------------------------------------------------------------------- /dist/vanilla-js-dropdown.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Vanilla JavaScript Dropdown v2.3.0 3 | * https://zoltantothcom.github.io/vanilla-js-dropdown 4 | */ 5 | var CustomSelect=function(e){var s="string"==typeof e.elem?document.getElementById(e.elem):e.elem,a="boolean"==typeof e.bubbles,o="js-Dropdown-title",l="is-selected",i="is-disabled",t="is-open",n=s.getElementsByTagName("optgroup"),d=s.options,c=d.length,r=0,u=document.createElement("div"),m=(u.className="js-Dropdown",s.id&&(u.id="custom-"+s.id),document.createElement("button")),p=(m.className=o,m.textContent=d[0].textContent,document.createElement("ul"));if(p.className="js-Dropdown-list",n.length)for(var g=0;g 2 | 3 | 4 | 5 | 6 | Vanilla JavaScript dropdown - a tiny select tag replacement. 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 |
17 | Vanilla JavaScript 18 | 19 |

Vanilla JavaScript Dropdown

20 | 21 |

Vanilla JavaScript replacement for <select> element that makes the styling easy and consistent.

22 |
23 | 24 |
25 |

How it works

26 | 27 |

Reads the original <select> element's options (with respect of «selected», if any), creates and attaches the pseudo-select just before the original one, and hides the original.

28 | 29 |

Upon selection it updates the original <select>, so when you submit your form the value will be there.

30 | 31 |

JavaScript disabled? No problem! Nicely degrades to original <select>.

32 | 33 |

— Inspired by the blazing fast, lightweight, cross-platform Vanilla JS framework.

34 |
35 | 36 |
37 |

Demo

38 | 39 |

40 | Click Submit to see the value of the original select changing. 41 |
42 | Feel free to look at the source (Ctrl + U) or open the console (F12) to make sure the submitted data retrieved from the original select element. 43 |

44 | 45 |
46 | 63 | 64 | 65 |
66 |
67 | 68 |
69 |

Download

70 | 71 |

Available on GitHub and npm

72 |
73 | 74 |
75 |

Installation

76 | 77 |
    78 |
  1. 79 | Include the script 80 |
    <script src="path/to/vanilla-js-dropdown.min.js"></script>
    81 | or install it from npm 82 |
    npm i vanilla-js-dropdown
    83 | A simple usage example can be found in this code playground. 84 |
  2. 85 |
  3. 86 | Include the CSS (feel free to edit it or write your own) 87 |
    <link rel="stylesheet" href="path/to/vanilla-js-dropdown.css">
    88 |
  4. 89 |
  5. 90 | Write your select 91 | 92 |
    <select id="color-select" name="color">
     93 |     <optgroup label="Red colors">
     94 |         <option value="indianRed">IndianRed</option>
     95 |         <option value="salmon">Salmon</option>
     96 |         <option value="crimson">Crimson</option>
     97 |     </optgroup>
     98 |     <optgroup label="Green colors">
     99 |         <option value="chartreuse">Chartreuse</option>
    100 |         <option value="seagreen">SeaGreen</option>
    101 |         <option value="olive">Olive</option>
    102 |     </optgroup>
    103 |     <optgroup label="Blue colors">
    104 |         <option value="aqua" disabled>Aqua</option>
    105 |         <option value="steelblue">SteelBlue</option>
    106 |         <option value="royalblue">RoyalBlue</option>
    107 |     </optgroup>
    108 | </select>
    109 | 
    110 |
  6. 111 |
  7. 112 | Initialize the select 113 |
    var select = new CustomSelect({
    114 |     elem: "color-select",  // id of the original select element
    115 | });
    116 | 
    117 | // Open the select
    118 | select.open();
    119 |
  8. 120 |
121 |
122 | 123 |
124 |

Options

125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 |
OptionRequiredDescription
elemyesid of the original select element or a direct DOM element
138 |
139 | 140 |
141 |

Methods

142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 |
MethodDescription
.open()Opens the select
.close()Closes the select
.toggle()Opens the select if closed and vice-versa
161 |
162 | 163 |
164 |

Licence

165 | 166 |
167 |

This is free and unencumbered software released into the public domain.

168 | 169 |

Anyone is free to copy, modify, publish, use, compile, sell, or 170 | distribute this software, either in source code form or as a compiled 171 | binary, for any purpose, commercial or non-commercial, and by any 172 | means.

173 | 174 |

In jurisdictions that recognize copyright laws, the author or authors 175 | of this software dedicate any and all copyright interest in the 176 | software to the public domain. We make this dedication for the benefit 177 | of the public at large and to the detriment of our heirs and 178 | successors. We intend this dedication to be an overt act of 179 | relinquishment in perpetuity of all present and future rights to this 180 | software under copyright law.

181 | 182 |

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 183 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 184 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 185 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 186 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 187 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 188 | OTHER DEALINGS IN THE SOFTWARE.

189 | 190 |

For more information, please refer to http://unlicense.org

191 |
192 |
193 | 194 | Fork me on GitHub 195 | 196 | 197 | 198 | 222 | 223 | 224 | -------------------------------------------------------------------------------- /docs/javascript/prism.js: -------------------------------------------------------------------------------- 1 | /* http://prismjs.com/download.html?themes=prism-okaidia&languages=markup+css+clike+javascript+scss */ 2 | var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(){var e=/\blang(?:uage)?-(?!\*)(\w+)\b/i,t=_self.Prism={util:{encode:function(e){return e instanceof n?new n(e.type,t.util.encode(e.content),e.alias):"Array"===t.util.type(e)?e.map(t.util.encode):e.replace(/&/g,"&").replace(/e.length)break e;if(!(d instanceof a)){u.lastIndex=0;var m=u.exec(d);if(m){c&&(f=m[1].length);var y=m.index-1+f,m=m[0].slice(f),v=m.length,k=y+v,b=d.slice(0,y+1),w=d.slice(k+1),P=[p,1];b&&P.push(b);var A=new a(i,g?t.tokenize(m,g):m,h);P.push(A),w&&P.push(w),Array.prototype.splice.apply(r,P)}}}}}return r},hooks:{all:{},add:function(e,n){var a=t.hooks.all;a[e]=a[e]||[],a[e].push(n)},run:function(e,n){var a=t.hooks.all[e];if(a&&a.length)for(var r,l=0;r=a[l++];)r(n)}}},n=t.Token=function(e,t,n){this.type=e,this.content=t,this.alias=n};if(n.stringify=function(e,a,r){if("string"==typeof e)return e;if("Array"===t.util.type(e))return e.map(function(t){return n.stringify(t,a,e)}).join("");var l={type:e.type,content:n.stringify(e.content,a,r),tag:"span",classes:["token",e.type],attributes:{},language:a,parent:r};if("comment"==l.type&&(l.attributes.spellcheck="true"),e.alias){var i="Array"===t.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(l.classes,i)}t.hooks.run("wrap",l);var o="";for(var s in l.attributes)o+=(o?" ":"")+s+'="'+(l.attributes[s]||"")+'"';return"<"+l.tag+' class="'+l.classes.join(" ")+'" '+o+">"+l.content+""},!_self.document)return _self.addEventListener?(_self.addEventListener("message",function(e){var n=JSON.parse(e.data),a=n.language,r=n.code,l=n.immediateClose;_self.postMessage(t.highlight(r,t.languages[a],a)),l&&_self.close()},!1),_self.Prism):_self.Prism;var a=document.getElementsByTagName("script");return a=a[a.length-1],a&&(t.filename=a.src,document.addEventListener&&!a.hasAttribute("data-manual")&&document.addEventListener("DOMContentLoaded",t.highlightAll)),_self.Prism}();"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); 3 | Prism.languages.markup={comment://,prolog:/<\?[\w\W]+?\?>/,doctype://,cdata://i,tag:{pattern:/<\/?(?!\d)[^\s>\/=.$<]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\\1|\\?(?!\1)[\w\W])*\1|[^\s'">=]+))?)*\s*\/?>/i,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=(?:('|")[\w\W]*?(\1)|[^\s>]+)/i,inside:{punctuation:/[=>"']/}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/&#?[\da-z]{1,8};/i},Prism.hooks.add("wrap",function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))}),Prism.languages.xml=Prism.languages.markup,Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup; 4 | Prism.languages.css={comment:/\/\*[\w\W]*?\*\//,atrule:{pattern:/@[\w-]+?.*?(;|(?=\s*\{))/i,inside:{rule:/@[\w-]+/}},url:/url\((?:(["'])(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,selector:/[^\{\}\s][^\{\};]*?(?=\s*\{)/,string:/("|')(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1/,property:/(\b|\B)[\w-]+(?=\s*:)/i,important:/\B!important\b/i,"function":/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:]/},Prism.languages.css.atrule.inside.rest=Prism.util.clone(Prism.languages.css),Prism.languages.markup&&(Prism.languages.insertBefore("markup","tag",{style:{pattern:/()[\w\W]*?(?=<\/style>)/i,lookbehind:!0,inside:Prism.languages.css,alias:"language-css"}}),Prism.languages.insertBefore("inside","attr-value",{"style-attr":{pattern:/\s*style=("|').*?\1/i,inside:{"attr-name":{pattern:/^\s*style/i,inside:Prism.languages.markup.tag.inside},punctuation:/^\s*=\s*['"]|['"]\s*$/,"attr-value":{pattern:/.+/i,inside:Prism.languages.css}},alias:"language-css"}},Prism.languages.markup.tag)); 5 | Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\w\W]*?\*\//,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0}],string:/(["'])(\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,"boolean":/\b(true|false)\b/,"function":/[a-z0-9_]+(?=\()/i,number:/\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)\b/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/}; 6 | Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|var|void|while|with|yield)\b/,number:/\b-?(0x[\dA-Fa-f]+|0b[01]+|0o[0-7]+|\d*\.?\d+([Ee][+-]?\d+)?|NaN|Infinity)\b/,"function":/[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*(?=\()/i}),Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^\/])\/(?!\/)(\[.+?]|\\.|[^\/\\\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0}}),Prism.languages.insertBefore("javascript","class-name",{"template-string":{pattern:/`(?:\\`|\\?[^`])*`/,inside:{interpolation:{pattern:/\$\{[^}]+\}/,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}}}),Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/()[\w\W]*?(?=<\/script>)/i,lookbehind:!0,inside:Prism.languages.javascript,alias:"language-javascript"}}),Prism.languages.js=Prism.languages.javascript; 7 | Prism.languages.scss=Prism.languages.extend("css",{comment:{pattern:/(^|[^\\])(?:\/\*[\w\W]*?\*\/|\/\/.*)/,lookbehind:!0},atrule:{pattern:/@[\w-]+(?:\([^()]+\)|[^(])*?(?=\s+[{;])/,inside:{rule:/@[\w-]+/}},url:/(?:[-a-z]+-)*url(?=\()/i,selector:{pattern:/(?=\S)[^@;\{\}\(\)]?([^@;\{\}\(\)]|&|#\{\$[-_\w]+\})+(?=\s*\{(\}|\s|[^\}]+(:|\{)[^\}]+))/m,inside:{placeholder:/%[-_\w]+/}}}),Prism.languages.insertBefore("scss","atrule",{keyword:[/@(?:if|else(?: if)?|for|each|while|import|extend|debug|warn|mixin|include|function|return|content)/i,{pattern:/( +)(?:from|through)(?= )/,lookbehind:!0}]}),Prism.languages.insertBefore("scss","property",{variable:/\$[-_\w]+|#\{\$[-_\w]+\}/}),Prism.languages.insertBefore("scss","function",{placeholder:{pattern:/%[-_\w]+/,alias:"selector"},statement:/\B!(?:default|optional)\b/i,"boolean":/\b(?:true|false)\b/,"null":/\bnull\b/,operator:{pattern:/(\s)(?:[-+*\/%]|[=!]=|<=?|>=?|and|or|not)(?=\s)/,lookbehind:!0}}),Prism.languages.scss.atrule.inside.rest=Prism.util.clone(Prism.languages.scss); 8 | -------------------------------------------------------------------------------- /docs/javascript/vanilla-js-dropdown.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Vanilla JavaScript Dropdown v2.3.0 3 | * https://zoltantothcom.github.io/vanilla-js-dropdown 4 | */ 5 | var CustomSelect=function(e){var s="string"==typeof e.elem?document.getElementById(e.elem):e.elem,a="boolean"==typeof e.bubbles,o="js-Dropdown-title",l="is-selected",i="is-disabled",t="is-open",n=s.getElementsByTagName("optgroup"),d=s.options,c=d.length,r=0,u=document.createElement("div"),m=(u.className="js-Dropdown",s.id&&(u.id="custom-"+s.id),document.createElement("button")),p=(m.className=o,m.textContent=d[0].textContent,document.createElement("ul"));if(p.className="js-Dropdown-list",n.length)for(var g=0;g code[class*="language-"], 40 | pre[class*="language-"] { 41 | background: #272822; 42 | } 43 | 44 | /* Inline code */ 45 | :not(pre) > code[class*="language-"] { 46 | padding: .1em; 47 | border-radius: .3em; 48 | white-space: normal; 49 | } 50 | 51 | .token.comment, 52 | .token.prolog, 53 | .token.doctype, 54 | .token.cdata { 55 | color: slategray; 56 | } 57 | 58 | .token.punctuation { 59 | color: #f8f8f2; 60 | } 61 | 62 | .namespace { 63 | opacity: .7; 64 | } 65 | 66 | .token.property, 67 | .token.tag, 68 | .token.constant, 69 | .token.symbol, 70 | .token.deleted { 71 | color: #f92672; 72 | } 73 | 74 | .token.boolean, 75 | .token.number { 76 | color: #ae81ff; 77 | } 78 | 79 | .token.selector, 80 | .token.attr-name, 81 | .token.string, 82 | .token.char, 83 | .token.builtin, 84 | .token.inserted { 85 | color: #a6e22e; 86 | } 87 | 88 | .token.operator, 89 | .token.entity, 90 | .token.url, 91 | .language-css .token.string, 92 | .style .token.string, 93 | .token.variable { 94 | color: #f8f8f2; 95 | } 96 | 97 | .token.atrule, 98 | .token.attr-value, 99 | .token.function { 100 | color: #e6db74; 101 | } 102 | 103 | .token.keyword { 104 | color: #66d9ef; 105 | } 106 | 107 | .token.regex, 108 | .token.important { 109 | color: #fd971f; 110 | } 111 | 112 | .token.important, 113 | .token.bold { 114 | font-weight: bold; 115 | } 116 | .token.italic { 117 | font-style: italic; 118 | } 119 | 120 | .token.entity { 121 | cursor: help; 122 | } 123 | 124 | -------------------------------------------------------------------------------- /docs/styles/vanilla-js-dropdown.css: -------------------------------------------------------------------------------- 1 | .js-Dropdown { 2 | display: inline-block; 3 | font: 400 14px sans-serif; 4 | position: relative; 5 | width: 20em; 6 | } 7 | .js-Dropdown-title { 8 | background: #ffffff; 9 | border: 1px groove #a5a5a5; 10 | box-sizing: border-box; 11 | cursor: pointer; 12 | font: 400 14px sans-serif; 13 | height: 3em; 14 | padding: 0.5em; 15 | position: relative; 16 | text-align: left; 17 | width: 100%; 18 | } 19 | .js-Dropdown-title:after { 20 | border-color: #a5a5a5 transparent transparent transparent; 21 | border-style: solid; 22 | border-width: 10px 12px; 23 | content: ''; 24 | display: block; 25 | height: 0; 26 | position: absolute; 27 | right: 1em; 28 | top: 45%; 29 | width: 0; 30 | } 31 | .js-Dropdown-list { 32 | background: #ffffff; 33 | border-left: 1px solid #a5a5a5; 34 | border-right: 1px solid #a5a5a5; 35 | box-sizing: border-box; 36 | display: none; 37 | height: 0; 38 | list-style: none; 39 | margin: 0; 40 | opacity: 0; 41 | padding: 0; 42 | position: absolute; 43 | transition: 0.2s linear; 44 | width: 100%; 45 | z-index: 999; 46 | } 47 | .js-Dropdown-list.is-open { 48 | display: block; 49 | height: auto; 50 | opacity: 1; 51 | } 52 | .js-Dropdown-list li { 53 | border-bottom: 1px solid #a5a5a5; 54 | cursor: pointer; 55 | padding: 1em 0.5em; 56 | } 57 | .js-Dropdown-list li:hover { 58 | background-color: #fff5e9; 59 | } 60 | .js-Dropdown-list li.is-selected { 61 | background-color: #ffdfb6; 62 | } 63 | .js-Dropdown-list li.is-disabled { 64 | background-color: #f5f5f5; 65 | color: #a5a5a5; 66 | cursor: not-allowed; 67 | } 68 | .js-Dropdown-optgroup { 69 | border-bottom: 1px solid #a5a5a5; 70 | color: #a5a5a5; 71 | cursor: default; 72 | padding: 1em 0.5em; 73 | text-align: center; 74 | } 75 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var pkg = require('./package.json'), 2 | pug = require('gulp-pug'), 3 | gulp = require('gulp'), 4 | less = require('gulp-less'), 5 | clean = require('gulp-clean-css'), 6 | uglify = require('gulp-uglify'), 7 | rename = require('gulp-rename'), 8 | header = require('gulp-header'), 9 | jshint = require('gulp-jshint'), 10 | stylish = require('jshint-stylish'); 11 | 12 | var banner = ['/**', 13 | ' * Vanilla JavaScript Dropdown v<%= pkg.version %>', 14 | ' * <%= pkg.homepage %>', 15 | ' */', 16 | ''].join('\n'); 17 | 18 | gulp.task('script', function(done) { 19 | gulp.src(['./src/javascript/vanilla-js-dropdown.js']) 20 | .pipe(uglify()) 21 | .pipe(header(banner, { 22 | pkg: pkg 23 | })) 24 | .pipe(rename({ 25 | suffix: '.min' 26 | })) 27 | .pipe(gulp.dest('./dist')) 28 | .pipe(gulp.dest('./docs/javascript')); 29 | 30 | done(); 31 | }); 32 | 33 | gulp.task('markup', function(done) { 34 | gulp.src('./src/index.pug') 35 | .pipe(pug({ 36 | pretty: true 37 | })) 38 | .pipe(gulp.dest('./dist')); 39 | 40 | done(); 41 | }); 42 | 43 | gulp.task('styles', function(done) { 44 | gulp.src('./src/styles/*.less') 45 | .pipe(less()) 46 | .pipe(gulp.dest('./dist')) 47 | .pipe(gulp.dest('./docs/styles')); 48 | 49 | done(); 50 | }); 51 | 52 | gulp.task('docs-styles', function(done) { 53 | gulp.src('./docs/styles/*.less') 54 | .pipe(less()) 55 | .pipe(clean({ 56 | compatibility: 'ie9' 57 | })) 58 | .pipe(gulp.dest('./docs/styles')); 59 | 60 | done(); 61 | }); 62 | 63 | gulp.task('lint', function() { 64 | return gulp.src('./src/javascript/*.js') 65 | .pipe(jshint('.jshintrc')) 66 | .pipe(jshint.reporter(stylish)); 67 | }); 68 | 69 | gulp.task('default', gulp.series('script', 'markup', 'styles', 'docs-styles', 'lint')); 70 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Tue Feb 21 2017 14:28:05 GMT-0500 (Eastern Standard Time) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['jasmine-jquery', 'jasmine'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'src/*/*.js', 19 | 'test/*js', 20 | 'test/spec/*.js', 21 | 'node_modules/jquery/dist/jquery.min.js', 22 | 'test/spec/fixtures/*.html', 23 | { 24 | pattern: 'img/*.jpg', 25 | watched: false, 26 | included: false, 27 | served: true, 28 | nocache: false 29 | } 30 | ], 31 | 32 | 33 | // list of files to exclude 34 | exclude: [], 35 | 36 | 37 | // preprocess matching files before serving them to the browser 38 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 39 | preprocessors: { 40 | 'src/*/*.js': 'coverage' 41 | }, 42 | 43 | 44 | // test results reporter to use 45 | // possible values: 'dots', 'progress' 46 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 47 | reporters: ['spec', 'coverage'], 48 | coverageReporter: { 49 | type: 'lcov', 50 | dir: 'coverage' 51 | }, 52 | 53 | 54 | // web server port 55 | port: 9876, 56 | 57 | 58 | // enable / disable colors in the output (reporters and logs) 59 | colors: true, 60 | 61 | 62 | // level of logging 63 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 64 | logLevel: config.LOG_INFO, 65 | 66 | 67 | // enable / disable watching file and executing tests whenever any file changes 68 | autoWatch: true, 69 | 70 | 71 | // start these browsers 72 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 73 | browsers: ['PhantomJS'], 74 | 75 | 76 | // Continuous Integration mode 77 | // if true, Karma captures browsers, runs the tests and exits 78 | singleRun: true 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vanilla-js-dropdown", 3 | "version": "2.3.0", 4 | "description": "Vanilla JavaScript Dropdown - a tiny (~700 bytes gzipped) select tag replacement with optgroups support.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "gulp", 8 | "test": "karma start karma.conf.js" 9 | }, 10 | "devDependencies": { 11 | "coveralls": "^3.0.3", 12 | "gulp": "^4.0.0", 13 | "gulp-clean-css": "^4.0.0", 14 | "gulp-cli": "^2.0.1", 15 | "gulp-header": "^2.0.7", 16 | "gulp-jshint": "^2.1.0", 17 | "gulp-less": "^4.0.1", 18 | "gulp-pug": "^4.0.1", 19 | "gulp-rename": "^1.4.0", 20 | "gulp-strip-code": "^0.1.4", 21 | "gulp-uglify": "^3.0.2", 22 | "jasmine-core": "^3.3.0", 23 | "jasmine-jquery": "^2.1.1", 24 | "jquery": "^3.1.1", 25 | "jshint": "^2.10.1", 26 | "jshint-stylish": "^2.2.1", 27 | "karma": "^6.3.16", 28 | "karma-chrome-launcher": "^2.2.0", 29 | "karma-cli": "^2.0.0", 30 | "karma-coverage": "^1.1.2", 31 | "karma-jasmine": "^2.0.1", 32 | "karma-jasmine-jquery": "^0.1.1", 33 | "karma-phantomjs-launcher": "^1.0.2", 34 | "karma-spec-reporter": "0.0.32", 35 | "phantom": "^6.0.3", 36 | "pug": "^3.0.1" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "https://github.com/zoltantothcom/vanilla-js-dropdown.git" 41 | }, 42 | "author": "Zoltan Toth", 43 | "license": "Unlicense", 44 | "bugs": { 45 | "url": "https://github.com/zoltantothcom/vanilla-js-dropdown/issues" 46 | }, 47 | "homepage": "https://zoltantothcom.github.io/vanilla-js-dropdown" 48 | } 49 | -------------------------------------------------------------------------------- /src/index.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | meta(charset="utf-8") 5 | 6 | title="Vanilla JavaScript Dropdown" 7 | link(rel="stylesheet" href="vanilla-js-dropdown.css") 8 | 9 | body 10 | 11 | div 12 | label(for="select") Select a color 13 | select#select 14 | optgroup(label="Red colors") 15 | option(value="indianred") IndianRed 16 | option(value="salmon") Salmon 17 | option(value="crimson") Crimson 18 | optgroup(label="Green colors") 19 | option(value="chartreuse" disabled) Chartreuse 20 | option(value="seagreen") SeaGreen 21 | option(value="olive") Olive 22 | optgroup(label="Blue colors") 23 | option(value="aqua") Aqua 24 | option(value="steelblue" selected) SteelBlue 25 | option(value="royalblue") RoyalBlue 26 | 27 | script(src="vanilla-js-dropdown.min.js") 28 | script. 29 | var colorSelect = new CustomSelect({ 30 | elem: 'select' 31 | }); 32 | -------------------------------------------------------------------------------- /src/javascript/vanilla-js-dropdown.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author Zoltan Toth 4 | * @version 2.3.0 5 | */ 6 | 7 | /** 8 | * @description 9 | * Vanilla JavaScript dropdown - a tiny (~600 bytes gzipped) select tag replacement. 10 | * 11 | * @class 12 | * @param {(string|Object)} options.elem - HTML id of the select or the DOM element. 13 | */ 14 | var CustomSelect = function(options) { 15 | var elem = typeof options.elem === 'string' ? document.getElementById(options.elem) : options.elem; 16 | var bubbles = typeof options.bubbles === 'boolean' ? true : false, 17 | mainClass = 'js-Dropdown', 18 | titleClass = 'js-Dropdown-title', 19 | listClass = 'js-Dropdown-list', 20 | optgroupClass = 'js-Dropdown-optgroup', 21 | selectedClass = 'is-selected', 22 | disabledClass = 'is-disabled', 23 | openClass = 'is-open', 24 | selectOpgroups = elem.getElementsByTagName('optgroup'), 25 | selectOptions = elem.options, 26 | optionsLength = selectOptions.length, 27 | index = 0; 28 | 29 | // creating the pseudo-select container 30 | var selectContainer = document.createElement('div'); 31 | 32 | selectContainer.className = mainClass; 33 | 34 | if (elem.id) { 35 | selectContainer.id = 'custom-' + elem.id; 36 | } 37 | 38 | // creating the always visible main button 39 | var button = document.createElement('button'); 40 | 41 | button.className = titleClass; 42 | button.textContent = selectOptions[0].textContent; 43 | 44 | // creating the UL 45 | var ul = document.createElement('ul'); 46 | ul.className = listClass; 47 | 48 | // dealing with optgroups 49 | if (selectOpgroups.length) { 50 | for (var i = 0; i < selectOpgroups.length; i++) { 51 | var div = document.createElement('div'); 52 | div.innerText = selectOpgroups[i].label; 53 | div.classList.add(optgroupClass); 54 | 55 | ul.appendChild(div); 56 | generateOptions(selectOpgroups[i].getElementsByTagName('option')); 57 | } 58 | } else { 59 | generateOptions(selectOptions); 60 | } 61 | 62 | // appending the button and the list 63 | selectContainer.appendChild(button); 64 | selectContainer.appendChild(ul); 65 | 66 | selectContainer.addEventListener('click', onClick); 67 | 68 | // pseudo-select is ready - append it and hide the original 69 | elem.parentNode.insertBefore(selectContainer, elem); 70 | elem.style.display = 'none'; 71 | 72 | /** 73 | * Generates a list from passed options. 74 | * 75 | * @param {object} options - options for the whole select or for an optgroup. 76 | */ 77 | function generateOptions(options) { 78 | for (var i = 0; i < options.length; i++) { 79 | var li = document.createElement('li'); 80 | 81 | li.innerText = options[i].textContent; 82 | li.setAttribute('data-value', options[i].value); 83 | li.setAttribute('data-index', index++); 84 | 85 | if (selectOptions[elem.selectedIndex].textContent === options[i].textContent) { 86 | li.classList.add(selectedClass); 87 | button.textContent = options[i].textContent; 88 | } 89 | if (options[i].disabled) { 90 | li.classList.add(disabledClass); 91 | } 92 | 93 | ul.appendChild(li); 94 | } 95 | } 96 | 97 | /** 98 | * Closes the current select on any click outside of it. 99 | * 100 | */ 101 | document.addEventListener('click', function(e) { 102 | if (!selectContainer.contains(e.target)) close(); 103 | }); 104 | 105 | /** 106 | * Handles the clicks on current select. 107 | * 108 | * @param {object} e - The item the click occured on. 109 | */ 110 | function onClick(e) { 111 | e.preventDefault(); 112 | 113 | var t = e.target; // || e.srcElement; - uncomment for IE8 114 | 115 | if (t.className === titleClass) { 116 | toggle(); 117 | } 118 | 119 | if (t.tagName === 'LI' && !t.classList.contains(disabledClass)) { 120 | selectContainer.querySelector('.' + titleClass).innerText = t.innerText; 121 | elem.options.selectedIndex = t.getAttribute('data-index'); 122 | 123 | //trigger 'change' event 124 | var evt = bubbles ? new CustomEvent('change', {bubbles: true}) : new CustomEvent('change'); 125 | elem.dispatchEvent(evt); 126 | 127 | // highlight the selected 128 | for (var i = 0; i < optionsLength; i++) { 129 | ul.querySelectorAll('li')[i].classList.remove(selectedClass); 130 | } 131 | t.classList.add(selectedClass); 132 | 133 | close(); 134 | } 135 | } 136 | 137 | /** 138 | * Toggles the open/close state of the select on title's clicks. 139 | * 140 | * @public 141 | */ 142 | function toggle() { 143 | ul.classList.toggle(openClass); 144 | } 145 | 146 | /** 147 | * Opens the select. 148 | * 149 | * @public 150 | */ 151 | function open() { 152 | ul.classList.add(openClass); 153 | } 154 | 155 | /** 156 | * Closes the select. 157 | * 158 | * @public 159 | */ 160 | function close() { 161 | ul.classList.remove(openClass); 162 | } 163 | 164 | return { 165 | toggle: toggle, 166 | close: close, 167 | open: open, 168 | }; 169 | }; 170 | -------------------------------------------------------------------------------- /src/styles/vanilla-js-dropdown.less: -------------------------------------------------------------------------------- 1 | @js-dropdown-back: #ffffff; 2 | @js-dropdown-border: #a5a5a5; 3 | @js-dropdown-selected: #ffdfb6; 4 | @js-dropdown-disabled: #f5f5f5; 5 | 6 | .js-Dropdown { 7 | display: inline-block; 8 | font: 400 14px sans-serif; 9 | position: relative; 10 | width: 20em; 11 | } 12 | 13 | .js-Dropdown-title { 14 | background: @js-dropdown-back; 15 | border: 1px groove @js-dropdown-border; 16 | box-sizing: border-box; 17 | cursor: pointer; 18 | font: 400 14px sans-serif; 19 | height: 3em; 20 | padding: 0.5em; 21 | position: relative; 22 | text-align: left; 23 | width: 100%; 24 | 25 | &:after { 26 | border-color: @js-dropdown-border transparent transparent transparent; 27 | border-style: solid; 28 | border-width: 10px 12px; 29 | content: ''; 30 | display: block; 31 | height: 0; 32 | position: absolute; 33 | right: 1em; 34 | top: 45%; 35 | width: 0; 36 | } 37 | } 38 | 39 | .js-Dropdown-list { 40 | background: @js-dropdown-back; 41 | border-left: 1px solid @js-dropdown-border; 42 | border-right: 1px solid @js-dropdown-border; 43 | box-sizing: border-box; 44 | display: none; 45 | height: 0; 46 | list-style: none; 47 | margin: 0; 48 | opacity: 0; 49 | padding: 0; 50 | position: absolute; 51 | transition: 0.2s linear; 52 | width: 100%; 53 | z-index: 999; 54 | 55 | &.is-open { 56 | display: block; 57 | height: auto; 58 | opacity: 1; 59 | } 60 | 61 | li { 62 | border-bottom: 1px solid @js-dropdown-border; 63 | cursor: pointer; 64 | padding: 1em 0.5em; 65 | 66 | &:hover { 67 | background-color: lighten(@js-dropdown-selected, 10%); 68 | } 69 | 70 | &.is-selected { 71 | background-color: @js-dropdown-selected; 72 | } 73 | 74 | &.is-disabled { 75 | background-color: @js-dropdown-disabled; 76 | color: @js-dropdown-border; 77 | cursor: not-allowed; 78 | } 79 | } 80 | } 81 | 82 | .js-Dropdown-optgroup { 83 | border-bottom: 1px solid @js-dropdown-border; 84 | color: @js-dropdown-border; 85 | cursor: default; 86 | padding: 1em 0.5em; 87 | text-align: center; 88 | } 89 | -------------------------------------------------------------------------------- /test/main.js: -------------------------------------------------------------------------------- 1 | var fixturePath = 'base/test/spec/fixtures', 2 | selectFixtureWithId = 'select_id.fixture.html', 3 | selectFixtureWithoutId = 'select_no_id.fixture.html', 4 | selectFixtureWithOptgroup = 'select_optgroup.fixture.html'; 5 | selectFixtureWithDisabled = 'select_disabled.fixture.html'; 6 | -------------------------------------------------------------------------------- /test/spec/dropdown.spec.js: -------------------------------------------------------------------------------- 1 | describe('SELECT - DOM element passed with ID', function() { 2 | beforeEach(function() { 3 | jasmine.getFixtures().fixturesPath = fixturePath; 4 | loadFixtures(selectFixtureWithId); 5 | 6 | this.original = document.getElementById('select'); 7 | 8 | this.select = new CustomSelect({ 9 | elem: this.original, 10 | }); 11 | }); 12 | 13 | it("should have an _id_ if it's present on original select", function() { 14 | var custom = document.querySelector('.js-Dropdown'); 15 | 16 | if (this.original.id) { 17 | expect(custom.hasAttribute('id')).toBeTruthy(); 18 | expect(custom.getAttribute('id')).toBe('custom-select'); 19 | } else { 20 | expect(custom.hasAttribute('id')).toBeFalsy(); 21 | } 22 | }); 23 | 24 | sharedTests(); 25 | }); 26 | 27 | describe('SELECT - DOM element passed _without_ ID', function() { 28 | beforeEach(function() { 29 | jasmine.getFixtures().fixturesPath = fixturePath; 30 | loadFixtures(selectFixtureWithoutId); 31 | 32 | this.original = document.getElementsByTagName('select')[0]; 33 | 34 | this.select = new CustomSelect({ 35 | elem: this.original, 36 | }); 37 | }); 38 | 39 | it('should have no id on custom select', function() { 40 | var custom = document.querySelector('.js-Dropdown'); 41 | 42 | expect(custom.hasAttribute('id')).toBeFalsy(); 43 | }); 44 | }); 45 | 46 | describe('SELECT - ID passed', function() { 47 | beforeEach(function() { 48 | jasmine.getFixtures().fixturesPath = fixturePath; 49 | loadFixtures(selectFixtureWithId); 50 | 51 | this.select = new CustomSelect({ 52 | elem: 'select', 53 | }); 54 | }); 55 | 56 | it('should have a custom id', function() { 57 | var select = document.querySelector('.js-Dropdown'); 58 | 59 | expect(select.hasAttribute('id')).toBeTruthy(); 60 | expect(select.getAttribute('id')).toBe('custom-select'); 61 | }); 62 | 63 | sharedTests(); 64 | }); 65 | 66 | describe('SELECT - contains OPTGROUP', function() { 67 | beforeEach(function() { 68 | jasmine.getFixtures().fixturesPath = fixturePath; 69 | loadFixtures(selectFixtureWithOptgroup); 70 | 71 | this.original = document.getElementById('select'); 72 | 73 | this.select = new CustomSelect({ 74 | elem: this.original, 75 | }); 76 | }); 77 | 78 | it("should have an _id_ if it's present on original select", function() { 79 | var custom = document.querySelector('.js-Dropdown'); 80 | 81 | if (this.original.id) { 82 | expect(custom.hasAttribute('id')).toBeTruthy(); 83 | expect(custom.getAttribute('id')).toBe('custom-select'); 84 | } else { 85 | expect(custom.hasAttribute('id')).toBeFalsy(); 86 | } 87 | }); 88 | 89 | it('should have optgroup titles', function() { 90 | expect($('#custom-select div.js-Dropdown-optgroup').length).toBeGreaterThan(0); 91 | expect($('#custom-select div.js-Dropdown-optgroup').length).toBe(2); 92 | }); 93 | 94 | sharedTests(); 95 | }); 96 | 97 | describe('SELECT - supports event bubbling', function() { 98 | beforeEach(function() { 99 | jasmine.getFixtures().fixturesPath = fixturePath; 100 | loadFixtures(selectFixtureWithId); 101 | 102 | this.original = document.getElementById('select'); 103 | 104 | this.select = new CustomSelect({ 105 | elem: this.original, 106 | bubbles: true, 107 | }); 108 | }); 109 | 110 | it("should create an event with the bubbles attribute if the option is passed", function() { 111 | this.original.addEventListener('change', function(event) { 112 | expect(event.bubbles).toBe(true); 113 | }); 114 | 115 | var spyEvent = spyOnEvent('.js-Dropdown-list li:eq(3)', 'click'); 116 | $('.js-Dropdown-list li:eq(3)').click(); 117 | 118 | expect('click').toHaveBeenTriggeredOn('.js-Dropdown-list li:eq(3)'); 119 | expect(spyEvent).toHaveBeenTriggered(); 120 | }); 121 | }); 122 | 123 | describe('SELECT - contains disabled item', function() { 124 | beforeEach(function() { 125 | jasmine.getFixtures().fixturesPath = fixturePath; 126 | loadFixtures(selectFixtureWithDisabled); 127 | 128 | this.original = document.getElementById('select'); 129 | 130 | this.select = new CustomSelect({ 131 | elem: this.original, 132 | }); 133 | }); 134 | 135 | it('should have a disabled class', function() { 136 | expect($('ul.js-Dropdown-list li:first-child')).toHaveClass('is-disabled'); 137 | }); 138 | }); 139 | 140 | function sharedTests() { 141 | describe('original select', function() { 142 | it('markup should be present', function() { 143 | expect($('#select')).toBeDefined(); 144 | }); 145 | 146 | it('should have more than 0 options', function() { 147 | expect($('#select option').length).toBeGreaterThan(0); 148 | }); 149 | }); 150 | 151 | describe('generated select', function() { 152 | it('should be defined', function() { 153 | expect(this.select).toBeDefined(); 154 | }); 155 | 156 | it('should have the container', function() { 157 | expect($('#custom-select')).toBeDefined(); 158 | }); 159 | 160 | it('should have the main button', function() { 161 | expect($('button.js-Dropdown-title')).toBeDefined(); 162 | }); 163 | 164 | it('should have the list', function() { 165 | expect($('ul.js-Dropdown-list')).toBeDefined(); 166 | }); 167 | 168 | it('should have the same number of options as the original', function() { 169 | var count = $('#select option').length; 170 | expect($('ul.js-Dropdown-list li').length).toBe(count); 171 | }); 172 | }); 173 | 174 | describe('methods', function() { 175 | it('.open() should open the select', function() { 176 | expect($('.js-Dropdown-list')).not.toHaveClass('is-open'); 177 | this.select.open(); 178 | expect($('.js-Dropdown-list')).toHaveClass('is-open'); 179 | }); 180 | 181 | it('.close() should close the select', function() { 182 | this.select.open(); 183 | expect($('.js-Dropdown-list')).toHaveClass('is-open'); 184 | this.select.close(); 185 | expect($('.js-Dropdown-list')).not.toHaveClass('is-open'); 186 | }); 187 | 188 | it('.toggle() should toggle the select', function() { 189 | expect($('.js-Dropdown-list')).not.toHaveClass('is-open'); 190 | this.select.toggle(); 191 | expect($('.js-Dropdown-list')).toHaveClass('is-open'); 192 | this.select.toggle(); 193 | expect($('.js-Dropdown-list')).not.toHaveClass('is-open'); 194 | }); 195 | }); 196 | 197 | describe('behavior', function() { 198 | it('should open on main button click', function() { 199 | expect($('.js-Dropdown-list')).not.toHaveClass('is-open'); 200 | 201 | var spyEvent = spyOnEvent('.js-Dropdown-title', 'click'); 202 | $('.js-Dropdown-title').click(); 203 | 204 | expect('click').toHaveBeenTriggeredOn('.js-Dropdown-title'); 205 | expect(spyEvent).toHaveBeenTriggered(); 206 | 207 | expect($('.js-Dropdown-list')).toHaveClass('is-open'); 208 | }); 209 | 210 | it('should close on any click outside the select', function() { 211 | this.select.open(); 212 | expect($('.js-Dropdown-list')).toHaveClass('is-open'); 213 | 214 | var spyEvent = spyOnEvent('body', 'click'); 215 | $('body').click(); 216 | 217 | expect('click').toHaveBeenTriggeredOn('body'); 218 | expect(spyEvent).toHaveBeenTriggered(); 219 | 220 | expect($('.js-Dropdown-list')).not.toHaveClass('is-open'); 221 | }); 222 | 223 | it('should highlight only the clicked item', function() { 224 | this.select.open(); 225 | 226 | var spyEvent = spyOnEvent('.js-Dropdown-list li:eq(3)', 'click'); 227 | $('.js-Dropdown-list li:eq(3)').click(); 228 | 229 | expect('click').toHaveBeenTriggeredOn('.js-Dropdown-list li:eq(3)'); 230 | expect(spyEvent).toHaveBeenTriggered(); 231 | 232 | expect($('.js-Dropdown-list')).not.toHaveClass('is-open'); 233 | expect($('.js-Dropdown-list li:eq(0)')).not.toHaveClass('is-selected'); 234 | expect($('.js-Dropdown-list li:eq(1)')).not.toHaveClass('is-selected'); 235 | expect($('.js-Dropdown-list li:eq(2)')).not.toHaveClass('is-selected'); 236 | expect($('.js-Dropdown-list li:eq(3)')).toHaveClass('is-selected'); 237 | expect($('.js-Dropdown-list li:eq(4)')).not.toHaveClass('is-selected'); 238 | }); 239 | 240 | it('should set the selected on original', function() { 241 | this.select.open(); 242 | 243 | var spyEvent = spyOnEvent('.js-Dropdown-list li:eq(4)', 'click'); 244 | $('.js-Dropdown-list li:eq(4)').click(); 245 | 246 | expect('click').toHaveBeenTriggeredOn('.js-Dropdown-list li:eq(4)'); 247 | expect(spyEvent).toHaveBeenTriggered(); 248 | 249 | expect($('.js-Dropdown-list')).not.toHaveClass('is-open'); 250 | expect($('.js-Dropdown-list li:eq(4)')).toHaveClass('is-selected'); 251 | 252 | expect($('#select option:eq(0)').prop('selected')).not.toBeTruthy(); 253 | expect($('#select option:eq(4)').prop('selected')).toBeTruthy(); 254 | }); 255 | }); 256 | } 257 | -------------------------------------------------------------------------------- /test/spec/fixtures/select_disabled.fixture.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/spec/fixtures/select_id.fixture.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/spec/fixtures/select_no_id.fixture.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /test/spec/fixtures/select_optgroup.fixture.html: -------------------------------------------------------------------------------- 1 | 14 | --------------------------------------------------------------------------------