├── .gitignore ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── README.md ├── dist ├── hierarchy-select.min.css └── hierarchy-select.min.js ├── docs ├── assets │ ├── highlight.css │ └── pygments.css ├── dist │ ├── hierarchy-select.min.css │ └── hierarchy-select.min.js └── index.html ├── e2e ├── addons.js ├── docs.po.js └── hierarchy-with-search.spec.js ├── package.json ├── protractor.conf.js └── src ├── hierarchy-select.js └── hierarchy-select.scss /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /node_modules 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | 3 | language: node_js 4 | 5 | node_js: "12" 6 | 7 | addons: 8 | chrome: stable 9 | 10 | before_script: 11 | - npm install 12 | - npm run webdriver-update 13 | 14 | script: npm test 15 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | const sass = require('node-sass'); 2 | module.exports = function (grunt) { 3 | grunt.initConfig({ 4 | pkg: grunt.file.readJSON('package.json'), 5 | clean: { 6 | dist: 'dist', 7 | docs: 'docs/dist' 8 | }, 9 | uglify: { 10 | build: { 11 | files: { 12 | 'dist/hierarchy-select.min.js': 'src/hierarchy-select.js' 13 | } 14 | } 15 | }, 16 | sass: { 17 | options: { 18 | implementation: sass, 19 | outputStyle: 'compressed' 20 | }, 21 | build: { 22 | files: { 23 | 'dist/hierarchy-select.min.css': 'src/hierarchy-select.scss' 24 | } 25 | } 26 | }, 27 | copy: { 28 | docs: { 29 | expand: true, 30 | cwd: 'dist', 31 | src: '**', 32 | dest: 'docs/dist/' 33 | } 34 | } 35 | }); 36 | grunt.loadNpmTasks('grunt-contrib-clean'); 37 | grunt.loadNpmTasks('grunt-contrib-copy'); 38 | grunt.loadNpmTasks('grunt-contrib-uglify'); 39 | grunt.loadNpmTasks('grunt-sass'); 40 | grunt.registerTask('default', ['clean', 'uglify', 'sass', 'copy']); 41 | }; 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015 Evgeniy NeoFusion 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 | # Hierarchy Select jQuery Plugin for Twitter Bootstrap 4 2 | 3 | [![Build Status](https://travis-ci.com/NeoFusion/hierarchy-select.svg?branch=v2)](https://travis-ci.com/NeoFusion/hierarchy-select) 4 | 5 | For Bootstrap 3 use [version 1.x](https://github.com/NeoFusion/hierarchy-select/tree/v1). 6 | -------------------------------------------------------------------------------- /dist/hierarchy-select.min.css: -------------------------------------------------------------------------------- 1 | .hierarchy-select.dropdown .hs-searchbox{padding:0 5px 4px}.hierarchy-select.dropdown .dropdown-menu a[data-level='2']{padding-left:40px}.hierarchy-select.dropdown .dropdown-menu a[data-level='3']{padding-left:60px}.hierarchy-select.dropdown .dropdown-menu a[data-level='4']{padding-left:80px}.hierarchy-select.dropdown .dropdown-menu a[data-level='5']{padding-left:100px}.hierarchy-select.dropdown .dropdown-menu a[data-level='6']{padding-left:120px}.hierarchy-select.dropdown .dropdown-menu a[data-level='7']{padding-left:140px}.hierarchy-select.dropdown .dropdown-menu a[data-level='8']{padding-left:160px}.hierarchy-select.dropdown .dropdown-menu a[data-level='9']{padding-left:180px}.hierarchy-select.dropdown .dropdown-menu a[data-level='10']{padding-left:200px} 2 | -------------------------------------------------------------------------------- /dist/hierarchy-select.min.js: -------------------------------------------------------------------------------- 1 | !function(a){"use strict";function h(e,t){this.$element=a(e),this.options=a.extend({},a.fn.hierarchySelect.defaults,t),this.$button=this.$element.children("button"),this.$menu=this.$element.children(".dropdown-menu"),this.$menuInner=this.$menu.children(".hs-menu-inner"),this.$searchbox=this.$menu.find("input"),this.$hiddenField=this.$element.children("input"),this.previouslySelected=null,this.init()}h.prototype={constructor:h,init:function(){this.setWidth(),this.setHeight(),this.initSelect(),this.clickListener(),this.buttonListener(),this.searchListener()},initSelect:function(){var e,t,n=this.$hiddenField.val();this.options.initialValueSet&&n&&0e.offsetTop+e.clientHeight||(e.parentNode.scrollTop=e.offsetTop-e.parentNode.offsetTop)},0)}),this.$element.on("hide.bs.dropdown",function(){s.previouslySelected&&s.setSelected(s.previouslySelected)}),this.$element.on("shown.bs.dropdown",function(){s.previouslySelected=s.$menuInner.find(".active"),s.$searchbox.focus()}),this.$menuInner.on("click","a",function(e){e.preventDefault();var t=a(this);t.hasClass("disabled")?e.stopPropagation():s.setSelected(t)})},buttonListener:function(){var t=this;this.options.search||this.$button.on("keydown",function(e){switch(e.keyCode){case 9:t.$element.hasClass("show")&&e.preventDefault();break;case 13:t.$element.hasClass("show")&&(e.preventDefault(),t.selectItem());break;case 27:t.$element.hasClass("show")&&(e.preventDefault(),e.stopPropagation(),t.$button.focus(),t.previouslySelected&&t.setSelected(t.previouslySelected),t.$button.dropdown("toggle"));break;case 38:t.$element.hasClass("show")&&(e.preventDefault(),e.stopPropagation(),t.moveUp());break;case 40:t.$element.hasClass("show")&&(e.preventDefault(),e.stopPropagation(),t.moveDown())}})},searchListener:function(){var s=this;this.options.search?(this.$searchbox.on("keydown",function(e){switch(e.keyCode){case 9:e.preventDefault(),e.stopPropagation(),s.$menuInner.click(),s.$button.focus();break;case 13:s.selectItem();break;case 27:e.preventDefault(),e.stopPropagation(),s.$button.focus(),s.previouslySelected&&s.setSelected(s.previouslySelected),s.$button.dropdown("toggle");break;case 38:e.preventDefault(),s.moveUp();break;case 40:e.preventDefault(),s.moveDown()}}),this.$searchbox.on("input propertychange",function(e){e.preventDefault();var t=s.$searchbox.val().toLowerCase(),n=s.$menuInner.find("a");0===t.length?n.each(function(){var e=a(this);e.toggleClass("disabled",!1),e.toggleClass("d-none",!1)}):n.each(function(){var e=a(this);-1!==e.text().toLowerCase().indexOf(t)?(e.toggleClass("disabled",!1),e.toggleClass("d-none",!1),s.options.hierarchy&&function(e){for(var t=e,n=t.data("level");"object"==typeof t&&0=t.offsetTop-e.offsetTop&&(e.scrollTop=t.offsetTop-e.offsetTop)}a.fn.hierarchySelect=function(s){var i,o=Array.prototype.slice.call(arguments,1),e=this.each(function(){var e=a(this),t=e.data("HierarchySelect"),n="object"==typeof s&&s;t||e.data("HierarchySelect",t=new h(this,n)),"string"==typeof s&&(i=t[s].apply(t,o))});return void 0===i?e:i},a.fn.hierarchySelect.defaults={width:"auto",height:"256px",hierarchy:!0,search:!0,initialValueSet:!1,resetSearchOnSelection:!1},a.fn.hierarchySelect.Constructor=h,a.fn.hierarchySelect.noConflict=function(){return a.fn.hierarchySelect=e,this}}(jQuery); -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | .highlight { 2 | padding: 1rem; 3 | margin-top: 1rem; 4 | margin-bottom: 1rem; 5 | -ms-overflow-style: -ms-autohiding-scrollbar; 6 | background-color: #f8f9fa 7 | } 8 | 9 | .highlight pre { 10 | padding: 0; 11 | margin-top: 0; 12 | margin-bottom: 0; 13 | background-color: transparent; 14 | border: 0 15 | } 16 | 17 | .highlight pre code { 18 | font-size: inherit 19 | } 20 | -------------------------------------------------------------------------------- /docs/assets/pygments.css: -------------------------------------------------------------------------------- 1 | .hll { background-color: #ffffcc } 2 | .c { color: #999; } /* Comment */ 3 | .err { color: #aa0000; background-color: #ffaaaa } /* Error */ 4 | .k { color: #006699; } /* Keyword */ 5 | .o { color: #555555 } /* Operator */ 6 | .cm { color: #999; } /* Comment.Multiline */ 7 | .cp { color: #009999 } /* Comment.Preproc */ 8 | .c1 { color: #999; } /* Comment.Single */ 9 | .cs { color: #999; } /* Comment.Special */ 10 | .gd { background-color: #ffcccc; border: 1px solid #cc0000 } /* Generic.Deleted */ 11 | .ge { font-style: italic } /* Generic.Emph */ 12 | .gr { color: #ff0000 } /* Generic.Error */ 13 | .gh { color: #003300; } /* Generic.Heading */ 14 | .gi { background-color: #ccffcc; border: 1px solid #00cc00 } /* Generic.Inserted */ 15 | .go { color: #aaaaaa } /* Generic.Output */ 16 | .gp { color: #000099; } /* Generic.Prompt */ 17 | .gs { } /* Generic.Strong */ 18 | .gu { color: #003300; } /* Generic.Subheading */ 19 | .gt { color: #99cc66 } /* Generic.Traceback */ 20 | .kc { color: #006699; } /* Keyword.Constant */ 21 | .kd { color: #006699; } /* Keyword.Declaration */ 22 | .kn { color: #006699; } /* Keyword.Namespace */ 23 | .kp { color: #006699 } /* Keyword.Pseudo */ 24 | .kr { color: #006699; } /* Keyword.Reserved */ 25 | .kt { color: #007788; } /* Keyword.Type */ 26 | .m { color: #ff6600 } /* Literal.Number */ 27 | .s { color: #d44950 } /* Literal.String */ 28 | .na { color: #4f9fcf } /* Name.Attribute */ 29 | .nb { color: #336666 } /* Name.Builtin */ 30 | .nc { color: #00aa88; } /* Name.Class */ 31 | .no { color: #336600 } /* Name.Constant */ 32 | .nd { color: #9999ff } /* Name.Decorator */ 33 | .ni { color: #999999; } /* Name.Entity */ 34 | .ne { color: #cc0000; } /* Name.Exception */ 35 | .nf { color: #cc00ff } /* Name.Function */ 36 | .nl { color: #9999ff } /* Name.Label */ 37 | .nn { color: #00ccff; } /* Name.Namespace */ 38 | .nt { color: #2f6f9f; } /* Name.Tag */ 39 | .nv { color: #003333 } /* Name.Variable */ 40 | .ow { color: #000000; } /* Operator.Word */ 41 | .w { color: #bbbbbb } /* Text.Whitespace */ 42 | .mf { color: #ff6600 } /* Literal.Number.Float */ 43 | .mh { color: #ff6600 } /* Literal.Number.Hex */ 44 | .mi { color: #ff6600 } /* Literal.Number.Integer */ 45 | .mo { color: #ff6600 } /* Literal.Number.Oct */ 46 | .sb { color: #cc3300 } /* Literal.String.Backtick */ 47 | .sc { color: #cc3300 } /* Literal.String.Char */ 48 | .sd { color: #cc3300; font-style: italic } /* Literal.String.Doc */ 49 | .s2 { color: #cc3300 } /* Literal.String.Double */ 50 | .se { color: #cc3300; } /* Literal.String.Escape */ 51 | .sh { color: #cc3300 } /* Literal.String.Heredoc */ 52 | .si { color: #aa0000 } /* Literal.String.Interpol */ 53 | .sx { color: #cc3300 } /* Literal.String.Other */ 54 | .sr { color: #33aaaa } /* Literal.String.Regex */ 55 | .s1 { color: #cc3300 } /* Literal.String.Single */ 56 | .ss { color: #ffcc33 } /* Literal.String.Symbol */ 57 | .bp { color: #336666 } /* Name.Builtin.Pseudo */ 58 | .vc { color: #003333 } /* Name.Variable.Class */ 59 | .vg { color: #003333 } /* Name.Variable.Global */ 60 | .vi { color: #003333 } /* Name.Variable.Instance */ 61 | .il { color: #ff6600 } /* Literal.Number.Integer.Long */ 62 | -------------------------------------------------------------------------------- /docs/dist/hierarchy-select.min.css: -------------------------------------------------------------------------------- 1 | .hierarchy-select.dropdown .hs-searchbox{padding:0 5px 4px}.hierarchy-select.dropdown .dropdown-menu a[data-level='2']{padding-left:40px}.hierarchy-select.dropdown .dropdown-menu a[data-level='3']{padding-left:60px}.hierarchy-select.dropdown .dropdown-menu a[data-level='4']{padding-left:80px}.hierarchy-select.dropdown .dropdown-menu a[data-level='5']{padding-left:100px}.hierarchy-select.dropdown .dropdown-menu a[data-level='6']{padding-left:120px}.hierarchy-select.dropdown .dropdown-menu a[data-level='7']{padding-left:140px}.hierarchy-select.dropdown .dropdown-menu a[data-level='8']{padding-left:160px}.hierarchy-select.dropdown .dropdown-menu a[data-level='9']{padding-left:180px}.hierarchy-select.dropdown .dropdown-menu a[data-level='10']{padding-left:200px} 2 | -------------------------------------------------------------------------------- /docs/dist/hierarchy-select.min.js: -------------------------------------------------------------------------------- 1 | !function(a){"use strict";function h(e,t){this.$element=a(e),this.options=a.extend({},a.fn.hierarchySelect.defaults,t),this.$button=this.$element.children("button"),this.$menu=this.$element.children(".dropdown-menu"),this.$menuInner=this.$menu.children(".hs-menu-inner"),this.$searchbox=this.$menu.find("input"),this.$hiddenField=this.$element.children("input"),this.previouslySelected=null,this.init()}h.prototype={constructor:h,init:function(){this.setWidth(),this.setHeight(),this.initSelect(),this.clickListener(),this.buttonListener(),this.searchListener()},initSelect:function(){var e,t,n=this.$hiddenField.val();this.options.initialValueSet&&n&&0e.offsetTop+e.clientHeight||(e.parentNode.scrollTop=e.offsetTop-e.parentNode.offsetTop)},0)}),this.$element.on("hide.bs.dropdown",function(){s.previouslySelected&&s.setSelected(s.previouslySelected)}),this.$element.on("shown.bs.dropdown",function(){s.previouslySelected=s.$menuInner.find(".active"),s.$searchbox.focus()}),this.$menuInner.on("click","a",function(e){e.preventDefault();var t=a(this);t.hasClass("disabled")?e.stopPropagation():s.setSelected(t)})},buttonListener:function(){var t=this;this.options.search||this.$button.on("keydown",function(e){switch(e.keyCode){case 9:t.$element.hasClass("show")&&e.preventDefault();break;case 13:t.$element.hasClass("show")&&(e.preventDefault(),t.selectItem());break;case 27:t.$element.hasClass("show")&&(e.preventDefault(),e.stopPropagation(),t.$button.focus(),t.previouslySelected&&t.setSelected(t.previouslySelected),t.$button.dropdown("toggle"));break;case 38:t.$element.hasClass("show")&&(e.preventDefault(),e.stopPropagation(),t.moveUp());break;case 40:t.$element.hasClass("show")&&(e.preventDefault(),e.stopPropagation(),t.moveDown())}})},searchListener:function(){var s=this;this.options.search?(this.$searchbox.on("keydown",function(e){switch(e.keyCode){case 9:e.preventDefault(),e.stopPropagation(),s.$menuInner.click(),s.$button.focus();break;case 13:s.selectItem();break;case 27:e.preventDefault(),e.stopPropagation(),s.$button.focus(),s.previouslySelected&&s.setSelected(s.previouslySelected),s.$button.dropdown("toggle");break;case 38:e.preventDefault(),s.moveUp();break;case 40:e.preventDefault(),s.moveDown()}}),this.$searchbox.on("input propertychange",function(e){e.preventDefault();var t=s.$searchbox.val().toLowerCase(),n=s.$menuInner.find("a");0===t.length?n.each(function(){var e=a(this);e.toggleClass("disabled",!1),e.toggleClass("d-none",!1)}):n.each(function(){var e=a(this);-1!==e.text().toLowerCase().indexOf(t)?(e.toggleClass("disabled",!1),e.toggleClass("d-none",!1),s.options.hierarchy&&function(e){for(var t=e,n=t.data("level");"object"==typeof t&&0=t.offsetTop-e.offsetTop&&(e.scrollTop=t.offsetTop-e.offsetTop)}a.fn.hierarchySelect=function(s){var i,o=Array.prototype.slice.call(arguments,1),e=this.each(function(){var e=a(this),t=e.data("HierarchySelect"),n="object"==typeof s&&s;t||e.data("HierarchySelect",t=new h(this,n)),"string"==typeof s&&(i=t[s].apply(t,o))});return void 0===i?e:i},a.fn.hierarchySelect.defaults={width:"auto",height:"256px",hierarchy:!0,search:!0,initialValueSet:!1,resetSearchOnSelection:!1},a.fn.hierarchySelect.Constructor=h,a.fn.hierarchySelect.noConflict=function(){return a.fn.hierarchySelect=e,this}}(jQuery); -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Hierarchy Select jQuery Plugin for Twitter Bootstrap 4 7 | 8 | 9 | 10 | 11 | 12 | 13 | 28 |
29 |

Live search with hierarchy

30 |
31 |
32 |
33 | 66 |
67 |
68 |
69 |
70 |
<div class="dropdown hierarchy-select" id="example-one">
 71 |     <button type="button" class="btn btn-secondary dropdown-toggle" id="example-one-button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
 72 |     <div class="dropdown-menu" aria-labelledby="example-one-button">
 73 |         <div class="hs-searchbox">
 74 |             <input type="text" class="form-control" autocomplete="off">
 75 |         </div>
 76 |         <div class="hs-menu-inner">
 77 |             <a class="dropdown-item" data-value="" data-level="1" data-default-selected="" href="#">All categories</a>
 78 |             <a class="dropdown-item" data-value="1" data-level="1" href="#">Wine</a>
 79 |             <a class="dropdown-item" data-value="2" data-level="2" href="#">Color</a>
 80 |             <a class="dropdown-item" data-value="3" data-level="3" href="#">Red</a>
 81 |             <a class="dropdown-item" data-value="4" data-level="3" href="#">White</a>
 82 |             <a class="dropdown-item" data-value="5" data-level="3" href="#">Rose</a>
 83 |             <a class="dropdown-item" data-value="6" data-level="2" href="#">Country</a>
 84 |             <a class="dropdown-item" data-value="7" data-level="3" href="#">Marokko</a>
 85 |             <a class="dropdown-item" data-value="8" data-level="3" href="#">Russia</a>
 86 |             <a class="dropdown-item" data-value="9" data-level="2" href="#">Sugar Content</a>
 87 |             <a class="dropdown-item" data-value="10" data-level="3" href="#">Semi Sweet</a>
 88 |             <a class="dropdown-item" data-value="11" data-level="3" href="#">Brut</a>
 89 |             <a class="dropdown-item" data-value="12" data-level="2" href="#">Rating</a>
 90 |             <a class="dropdown-item" data-value="13" data-level="2" href="#">Grape Sort</a>
 91 |             <a class="dropdown-item" data-value="14" data-level="3" href="#">Riesling</a>
 92 |             <a class="dropdown-item" data-value="15" data-level="3" href="#">Aleatico</a>
 93 |             <a class="dropdown-item" data-value="16" data-level="3" href="#">Bouchet</a>
 94 |             <a class="dropdown-item" data-value="17" data-level="1" href="#">Whiskey</a>
 95 |             <a class="dropdown-item" data-value="18" data-level="2" href="#">Country</a>
 96 |             <a class="dropdown-item" data-value="19" data-level="3" href="#">Ireland</a>
 97 |             <a class="dropdown-item" data-value="20" data-level="3" href="#">Kanada</a>
 98 |             <a class="dropdown-item" data-value="21" data-level="3" href="#">Scotland</a>
 99 |         </div>
100 |     </div>
101 |     <input class="d-none" name="example_one" readonly="readonly" aria-hidden="true" type="text"/>
102 | </div>
103 |
104 |
105 |
$('#example-one').hierarchySelect({
106 |     width: 'auto'
107 | });
108 |
109 |

Live search without hierarchy

110 |
111 |
112 |
113 | 136 |
137 |
138 |
139 |
140 |
<div class="dropdown hierarchy-select" id="example-two">
141 |     <button type="button" class="btn btn-secondary dropdown-toggle" id="example-two-button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
142 |     <div class="dropdown-menu" aria-labelledby="example-two-button">
143 |         <div class="hs-searchbox">
144 |             <input type="text" class="form-control" autocomplete="off">
145 |         </div>
146 |         <div class="hs-menu-inner">
147 |             <a class="dropdown-item" data-value="" data-default-selected="" href="#">All colors</a>
148 |             <a class="dropdown-item" data-value="1" href="#">Red</a>
149 |             <a class="dropdown-item" data-value="2" href="#">Orange</a>
150 |             <a class="dropdown-item" data-value="3" href="#">Yellow</a>
151 |             <a class="dropdown-item" data-value="4" href="#">Green</a>
152 |             <a class="dropdown-item" data-value="5" href="#">Blue</a>
153 |             <a class="dropdown-item" data-value="6" href="#">Purple</a>
154 |             <a class="dropdown-item" data-value="7" href="#">Pink</a>
155 |             <a class="dropdown-item" data-value="8" href="#">Brown</a>
156 |             <a class="dropdown-item" data-value="9" href="#">Black</a>
157 |             <a class="dropdown-item" data-value="10" href="#">Grey</a>
158 |             <a class="dropdown-item" data-value="11" href="#">White</a>
159 |         </div>
160 |     </div>
161 |     <input class="d-none" name="example_two" readonly="readonly" aria-hidden="true" type="text"/>
162 | </div>
163 |
164 |
165 |
$('#example-two').hierarchySelect({
166 |     hierarchy: false,
167 |     width: 'auto',
168 |     resetSearchOnSelection: true
169 | });
170 |
171 |

Simple select

172 |
173 |
174 |
175 | 198 |
199 |
200 |
201 |
202 |
<div class="dropdown hierarchy-select" id="example-three">
203 |     <button type="button" class="btn btn-secondary dropdown-toggle" id="example-three-button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
204 |     <div class="dropdown-menu" aria-labelledby="example-three-button">
205 |         <div class="hs-searchbox">
206 |             <input type="text" class="form-control" autocomplete="off">
207 |         </div>
208 |         <div class="hs-menu-inner">
209 |             <a class="dropdown-item" data-value="" data-default-selected="" href="#">All colors</a>
210 |             <a class="dropdown-item" data-value="1" href="#">Red</a>
211 |             <a class="dropdown-item" data-value="2" href="#">Orange</a>
212 |             <a class="dropdown-item" data-value="3" href="#">Yellow</a>
213 |             <a class="dropdown-item" data-value="4" href="#">Green</a>
214 |             <a class="dropdown-item" data-value="5" href="#">Blue</a>
215 |             <a class="dropdown-item" data-value="6" href="#">Purple</a>
216 |             <a class="dropdown-item" data-value="7" href="#">Pink</a>
217 |             <a class="dropdown-item" data-value="8" href="#">Brown</a>
218 |             <a class="dropdown-item" data-value="9" href="#">Black</a>
219 |             <a class="dropdown-item" data-value="10" href="#">Grey</a>
220 |             <a class="dropdown-item" data-value="11" href="#">White</a>
221 |         </div>
222 |     </div>
223 |     <input class="d-none" name="example_three" readonly="readonly" aria-hidden="true" type="text" value="4"/>
224 | </div>
225 |
226 |
227 |
$('#example-three').hierarchySelect({
228 |     hierarchy: false,
229 |     search: false,
230 |     width: 200,
231 |     initialValueSet: true,
232 |     onChange: function (value) {
233 |         console.log('[Three] value: "' + value + '"');
234 |     }
235 | });
236 |
237 |
238 | 239 | 240 | 241 | 242 | 263 | 264 | 265 | -------------------------------------------------------------------------------- /e2e/addons.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | beforeEach(function () { 3 | // Add custom matchers 4 | jasmine.addMatchers({ 5 | // Test if element is current active element 6 | toBeActiveElement: function () { 7 | return { 8 | compare: function (actual) { 9 | var result = {}; 10 | 11 | result.pass = Promise.all([ 12 | actual.equals(browser.driver.switchTo().activeElement()), 13 | actual.getTagName(), 14 | ]) 15 | .then(function (resolved) { 16 | var equals = resolved[0]; 17 | var tagName = resolved[1]; 18 | 19 | if (equals) { 20 | result.message = 'Expected "' + tagName + '" not to be current active element'; 21 | } else { 22 | result.message = 'Expected "' + tagName + '" to be current active element'; 23 | } 24 | 25 | return equals; 26 | }); 27 | 28 | return result; 29 | }, 30 | }; 31 | }, 32 | // Test if element has class 33 | toHaveClass: function () { 34 | return { 35 | compare: function (actual, expected) { 36 | var result = {}; 37 | 38 | result.pass = actual.getAttribute("class") 39 | .then(function (classes) { 40 | var pass = classes.split(" ").indexOf(expected) !== -1; 41 | 42 | if (pass) { 43 | result.message = 'Expected "' + classes + '" not to have class ' + expected; 44 | } else { 45 | result.message = 'Expected "' + classes + '" to have class ' + expected; 46 | } 47 | 48 | return pass; 49 | }); 50 | 51 | return result; 52 | } 53 | }; 54 | }, 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /e2e/docs.po.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports.ExamplePage = function (_section) { 3 | var section = '#' + _section; 4 | var self = this; 5 | 6 | // Set a reference to the first example root element 7 | this.root = element(by.css(section)); 8 | 9 | // Set references to main elements 10 | this.defaultSelected = this.root.element(by.css('[data-default-selected]')); 11 | this.dropDownButton = this.root.element(by.css('.dropdown-toggle')); 12 | this.dropDownList = this.root.element(by.css('.dropdown-menu .hs-menu-inner')); 13 | this.dropDownListElements = this.root.all(by.css('a')); 14 | this.filterInput = this.root.element(by.css('.hs-searchbox input')); 15 | this.valueHolder = element(by.css(section + ' > input')); 16 | 17 | this.getCurrentValue = function () { 18 | return self.valueHolder.getAttribute('value'); 19 | }; 20 | 21 | this.getListElement = function (index) { 22 | return self.dropDownListElements.get(index); 23 | }; 24 | 25 | this.openDropDown = function () { 26 | return self.dropDownButton.click(); 27 | }; 28 | 29 | this.setFilter = function (value) { 30 | return self.filterInput.sendKeys(value); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /e2e/hierarchy-with-search.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // Get page object 3 | var ExamplePage = require('./docs.po').ExamplePage; 4 | 5 | // Turn off angular support 6 | browser.ignoreSynchronization = true; 7 | 8 | describe('Hierarchy select with search and hierarchy', function () { 9 | var examplePage; 10 | 11 | beforeEach(function () { 12 | browser.get('/'); 13 | examplePage = new ExamplePage('example-one'); 14 | }); 15 | 16 | it('should open a drop-down and set focus on filter input field on initial click', function () { 17 | examplePage.openDropDown(); 18 | 19 | expect(examplePage.filterInput).toBeActiveElement(); 20 | expect(examplePage.root).toHaveClass('show'); 21 | }); 22 | 23 | it('should close drop-down on ESC or TAB key and set focus on drop-down button', function () { 24 | Promise.all([ 25 | examplePage.openDropDown(), 26 | examplePage.setFilter(protractor.Key.ESCAPE), 27 | ]) 28 | .then(function () { 29 | expect(examplePage.root).not.toHaveClass('open'); 30 | expect(examplePage.dropDownButton).toBeActiveElement(); 31 | 32 | return Promise.all([ 33 | examplePage.openDropDown(), 34 | 35 | examplePage.setFilter(protractor.Key.TAB), 36 | ]); 37 | }) 38 | .then(function () { 39 | expect(examplePage.root).not.toHaveClass('open'); 40 | expect(examplePage.dropDownButton).toBeActiveElement(); 41 | }); 42 | }); 43 | 44 | it('should set first element in drop-down list active on initial click', function () { 45 | examplePage.openDropDown(); 46 | 47 | expect(examplePage.getListElement(0)).toHaveClass('active'); 48 | }); 49 | 50 | it('should navigate through elements on arrow keys up/down', function () { 51 | examplePage.openDropDown(); 52 | 53 | expect(examplePage.getListElement(0)).toHaveClass('active'); 54 | 55 | examplePage.setFilter(protractor.Key.ARROW_DOWN) 56 | .then(function () { 57 | expect(examplePage.getListElement(0)).not.toHaveClass('active'); 58 | expect(examplePage.getListElement(1)).toHaveClass('active'); 59 | 60 | return examplePage.setFilter(protractor.Key.ARROW_UP); 61 | }) 62 | .then(function () { 63 | expect(examplePage.getListElement(0)).toHaveClass('active'); 64 | expect(examplePage.getListElement(1)).not.toHaveClass('active'); 65 | }); 66 | }); 67 | 68 | it('should select currently active value after pressing enter', function () { 69 | var selectedValue = ''; 70 | 71 | examplePage.openDropDown(); 72 | 73 | expect(examplePage.getListElement(0)).toHaveClass('active'); 74 | 75 | Promise.all([ 76 | examplePage.setFilter(protractor.Key.ARROW_DOWN), 77 | examplePage.getListElement(1).getAttribute('data-value'), 78 | ]) 79 | .then(function (resolved) { 80 | selectedValue = resolved[1]; 81 | return examplePage.setFilter(protractor.Key.ENTER); 82 | }) 83 | .then(function () { 84 | expect(examplePage.getCurrentValue()).toBe(selectedValue); 85 | }); 86 | }); 87 | 88 | it('should have the value of an element with the attribute `data-default-selected` be the default value', function () { 89 | examplePage.defaultSelected.isPresent() 90 | .then(function (isPresent) { 91 | if (isPresent) { 92 | expect(examplePage.defaultSelected.getAttribute('data-value')).toBe(examplePage.getCurrentValue()); 93 | } 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hierarchy-select", 3 | "version": "2.3.0", 4 | "description": "Hierarchy Select jQuery Plugin for Twitter Bootstrap 4", 5 | "author": { 6 | "name": "Evgeniy NeoFusion", 7 | "email": "evgeniy@neofusion.ru" 8 | }, 9 | "contributors": [ 10 | { 11 | "name": "Sergey Palikhov", 12 | "email": "sergeypalihov@gmail.com" 13 | } 14 | ], 15 | "license": "MIT", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/NeoFusion/hierarchy-select.git" 19 | }, 20 | "homepage": "https://github.com/NeoFusion/hierarchy-select", 21 | "bugs": { 22 | "url": "https://github.com/NeoFusion/hierarchy-select/issues" 23 | }, 24 | "main": "dist/hierarchy-select.min.js", 25 | "files": [ 26 | "dist", 27 | "src" 28 | ], 29 | "keywords": [ 30 | "bootstrap", 31 | "form", 32 | "jquery", 33 | "jquery-plugin" 34 | ], 35 | "scripts": { 36 | "server": "http-server ./docs", 37 | "server-silent": "http-server ./docs -s -p8081", 38 | "webdriver-update": "./node_modules/.bin/webdriver-manager update", 39 | "start": "npm run server", 40 | "test": "npm run e2e", 41 | "pree2e": "webdriver-manager update --standalone false --gecko false", 42 | "e2e": "concurrently --kill-others --success first \"npm run server-silent\" \"protractor\"" 43 | }, 44 | "peerDependencies": { 45 | "jquery": "1.9.1 - 3" 46 | }, 47 | "devDependencies": { 48 | "concurrently": "^5.2.0", 49 | "grunt": "^1.1.0", 50 | "grunt-contrib-clean": "^2.0.0", 51 | "grunt-contrib-copy": "^1.0.0", 52 | "grunt-contrib-uglify": "^4.0.1", 53 | "grunt-sass": "^3.1.0", 54 | "http-server": "^0.12.1", 55 | "jasmine-core": "^3.5.0", 56 | "jasmine-spec-reporter": "^5.0.2", 57 | "node-sass": "^4.14.1", 58 | "protractor": "^5.4.4" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | var SpecReporter = require('jasmine-spec-reporter').SpecReporter; 2 | 3 | exports.config = { 4 | allScriptsTimeout: 11000, 5 | specs: [ 6 | './e2e/**/*.spec.js' 7 | ], 8 | capabilities: { 9 | 'browserName': 'chrome', 10 | chromeOptions: { 11 | args: [ "--headless", "--disable-gpu", "--no-sandbox", "--window-size=1920x1080" ] 12 | } 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:8081/demo', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare: function() { 23 | jasmine.getEnv().addReporter(new SpecReporter()); 24 | require('./e2e/addons'); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/hierarchy-select.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | 'use strict'; 3 | 4 | var HierarchySelect = function(element, options) { 5 | this.$element = $(element); 6 | this.options = $.extend({}, $.fn.hierarchySelect.defaults, options); 7 | this.$button = this.$element.children('button'); 8 | this.$menu = this.$element.children('.dropdown-menu'); 9 | this.$menuInner = this.$menu.children('.hs-menu-inner'); 10 | this.$searchbox = this.$menu.find('input'); 11 | this.$hiddenField = this.$element.children('input'); 12 | this.previouslySelected = null; 13 | this.init(); 14 | }; 15 | 16 | HierarchySelect.prototype = { 17 | constructor: HierarchySelect, 18 | init: function() { 19 | this.setWidth(); 20 | this.setHeight(); 21 | this.initSelect(); 22 | this.clickListener(); 23 | this.buttonListener(); 24 | this.searchListener(); 25 | }, 26 | initSelect: function() { 27 | var hiddenFieldValue = this.$hiddenField.val(); 28 | if (this.options.initialValueSet && hiddenFieldValue && hiddenFieldValue.length > 0) { 29 | this.setValue(hiddenFieldValue); 30 | } else { 31 | var item = this.$menuInner.find('a[data-default-selected]:first'); 32 | if (item.length) { 33 | this.setValue(item.data('value')); 34 | } else { 35 | var firstItem = this.$menuInner.find('a:first'); 36 | this.setValue(firstItem.data('value')); 37 | } 38 | } 39 | }, 40 | setWidth: function() { 41 | this.$searchbox.attr('size', 1); // Fix min-width 42 | if (this.options.width === 'auto') { 43 | var width = this.$menu.width(); 44 | this.$element.css('min-width', width + 2 + 'px'); 45 | } else if (this.options.width) { 46 | this.$element.css('width', this.options.width); 47 | this.$menu.css('min-width', this.options.width); 48 | this.$button.css('width', '100%'); 49 | } else { 50 | this.$element.css('min-width', '42px'); 51 | } 52 | }, 53 | setHeight: function() { 54 | if (this.options.height) { 55 | this.$menu.css('overflow', 'hidden'); 56 | this.$menuInner.css({ 57 | 'max-height': this.options.height, 58 | 'overflow-y': 'auto' 59 | }); 60 | } 61 | }, 62 | getText: function() { 63 | return this.$button.text(); 64 | }, 65 | getValue: function() { 66 | return this.$hiddenField.val(); 67 | }, 68 | setValue: function(value) { 69 | var a = this.$menuInner.children('a[data-value="' + value + '"]:first'); 70 | this.setSelected(a); 71 | }, 72 | enable: function() { 73 | this.$button.removeAttr('disabled'); 74 | }, 75 | disable: function() { 76 | this.$button.attr('disabled', 'disabled'); 77 | }, 78 | setSelected: function(a) { 79 | if (a.length && this.previouslySelected !== a) { 80 | var text = a.text(); 81 | var value = a.data('value'); 82 | this.previouslySelected = a; 83 | this.$button.html(text); 84 | this.$hiddenField.val(value); 85 | this.$menu.find('.active').removeClass('active'); 86 | if (this.options.onChange) this.options.onChange(value, text); 87 | if (this.options.resetSearchOnSelection) this.resetSearch(); 88 | a.addClass('active'); 89 | } 90 | }, 91 | moveUp: function () { 92 | var items = this.$menuInner.find('a:not(.d-none,.disabled)'); 93 | var active = this.$menuInner.find('.active'); 94 | var index = items.index(active); 95 | if (typeof items[index - 1] !== 'undefined') { 96 | this.$menuInner.find('.active').removeClass('active'); 97 | items[index - 1].classList.add('active'); 98 | processElementOffset(this.$menuInner[0], items[index - 1]); 99 | } 100 | }, 101 | moveDown: function () { 102 | var items = this.$menuInner.find('a:not(.d-none,.disabled)'); 103 | var active = this.$menuInner.find('.active'); 104 | var index = items.index(active); 105 | if (typeof items[index + 1] !== 'undefined') { 106 | this.$menuInner.find('.active').removeClass('active'); 107 | if (items[index + 1]) { 108 | items[index + 1].classList.add('active'); 109 | processElementOffset(this.$menuInner[0], items[index + 1]); 110 | } 111 | } 112 | }, 113 | resetSearch: function() { 114 | this.$searchbox.val('').trigger('propertychange'); 115 | }, 116 | selectItem: function () { 117 | var that = this; 118 | var selected = this.$menuInner.find('.active'); 119 | if (selected.hasClass('d-none') || selected.hasClass('disabled')) { 120 | return; 121 | } 122 | setTimeout(function() { 123 | that.$button.focus(); 124 | }, 5); 125 | selected && this.setSelected(selected); 126 | this.$button.dropdown('toggle'); 127 | }, 128 | clickListener: function() { 129 | var that = this; 130 | this.$element.on('show.bs.dropdown', function() { 131 | var selected = that.$menuInner.find('.active'); 132 | selected && setTimeout(function() { 133 | var el = selected[0]; 134 | var p = selected[0].parentNode; 135 | if (!(p.scrollTop <= el.offsetTop - p.offsetTop && (p.scrollTop + p.clientHeight) > el.offsetTop + el.clientHeight)) { 136 | el.parentNode.scrollTop = el.offsetTop - el.parentNode.offsetTop; 137 | } 138 | }, 0); 139 | }); 140 | this.$element.on('hide.bs.dropdown', function() { 141 | that.previouslySelected && that.setSelected(that.previouslySelected); 142 | }); 143 | this.$element.on('shown.bs.dropdown', function() { 144 | that.previouslySelected = that.$menuInner.find('.active'); 145 | that.$searchbox.focus(); 146 | }); 147 | this.$menuInner.on('click', 'a', function (e) { 148 | e.preventDefault(); 149 | var $this = $(this); 150 | if ($this.hasClass('disabled')) { 151 | e.stopPropagation(); 152 | } else { 153 | that.setSelected($this); 154 | } 155 | }); 156 | }, 157 | buttonListener: function () { 158 | var that = this; 159 | if (this.options.search) { 160 | return; 161 | } 162 | this.$button.on('keydown', function (e) { 163 | switch (e.keyCode) { 164 | case 9: // Tab 165 | if (that.$element.hasClass('show')) { 166 | e.preventDefault(); 167 | } 168 | break; 169 | case 13: // Enter 170 | if (that.$element.hasClass('show')) { 171 | e.preventDefault(); 172 | that.selectItem(); 173 | } 174 | break; 175 | case 27: // Esc 176 | if (that.$element.hasClass('show')) { 177 | e.preventDefault(); 178 | e.stopPropagation(); 179 | that.$button.focus(); 180 | that.previouslySelected && that.setSelected(that.previouslySelected); 181 | that.$button.dropdown('toggle'); 182 | } 183 | break; 184 | case 38: // Up 185 | if (that.$element.hasClass('show')) { 186 | e.preventDefault(); 187 | e.stopPropagation(); 188 | that.moveUp(); 189 | } 190 | break; 191 | case 40: // Down 192 | if (that.$element.hasClass('show')) { 193 | e.preventDefault(); 194 | e.stopPropagation(); 195 | that.moveDown(); 196 | } 197 | break; 198 | default: 199 | break; 200 | } 201 | }); 202 | }, 203 | searchListener: function() { 204 | var that = this; 205 | if (!this.options.search) { 206 | this.$searchbox.parent().toggleClass('d-none', true); 207 | return; 208 | } 209 | function disableParents(element) { 210 | var item = element; 211 | var level = item.data('level'); 212 | while (typeof item === 'object' && item.length > 0 && level > 1) { 213 | level--; 214 | item = item.prevAll('a[data-level="' + level + '"]:first'); 215 | if (item.hasClass('d-none')) { 216 | item.toggleClass('disabled', true); 217 | item.removeClass('d-none'); 218 | } 219 | } 220 | } 221 | this.$searchbox.on('keydown', function (e) { 222 | switch (e.keyCode) { 223 | case 9: // Tab 224 | e.preventDefault(); 225 | e.stopPropagation(); 226 | that.$menuInner.click(); 227 | that.$button.focus(); 228 | break; 229 | case 13: // Enter 230 | that.selectItem(); 231 | break; 232 | case 27: // Esc 233 | e.preventDefault(); 234 | e.stopPropagation(); 235 | that.$button.focus(); 236 | that.previouslySelected && that.setSelected(that.previouslySelected); 237 | that.$button.dropdown('toggle'); 238 | break; 239 | case 38: // Up 240 | e.preventDefault(); 241 | that.moveUp(); 242 | break; 243 | case 40: // Down 244 | e.preventDefault(); 245 | that.moveDown(); 246 | break; 247 | default: 248 | break; 249 | } 250 | }); 251 | this.$searchbox.on('input propertychange', function (e) { 252 | e.preventDefault(); 253 | var searchString = that.$searchbox.val().toLowerCase(); 254 | var items = that.$menuInner.find('a'); 255 | if (searchString.length === 0) { 256 | items.each(function() { 257 | var item = $(this); 258 | item.toggleClass('disabled', false); 259 | item.toggleClass('d-none', false); 260 | }); 261 | } else { 262 | items.each(function() { 263 | var item = $(this); 264 | var text = item.text().toLowerCase(); 265 | if (text.indexOf(searchString) !== -1) { 266 | item.toggleClass('disabled', false); 267 | item.toggleClass('d-none', false); 268 | if (that.options.hierarchy) { 269 | disableParents(item); 270 | } 271 | } else { 272 | item.toggleClass('disabled', false); 273 | item.toggleClass('d-none', true); 274 | } 275 | }); 276 | } 277 | }); 278 | } 279 | }; 280 | 281 | var Plugin = function(option) { 282 | var args = Array.prototype.slice.call(arguments, 1); 283 | var method; 284 | var chain = this.each(function() { 285 | var $this = $(this); 286 | var data = $this.data('HierarchySelect'); 287 | var options = typeof option === 'object' && option; 288 | if (!data) { 289 | $this.data('HierarchySelect', (data = new HierarchySelect(this, options))); 290 | } 291 | if (typeof option === 'string') { 292 | method = data[option].apply(data, args); 293 | } 294 | }); 295 | 296 | return (method === undefined) ? chain : method; 297 | }; 298 | 299 | var old = $.fn.hierarchySelect; 300 | 301 | $.fn.hierarchySelect = Plugin; 302 | $.fn.hierarchySelect.defaults = { 303 | width: 'auto', 304 | height: '256px', 305 | hierarchy: true, 306 | search: true, 307 | initialValueSet: false, 308 | resetSearchOnSelection: false 309 | }; 310 | $.fn.hierarchySelect.Constructor = HierarchySelect; 311 | 312 | $.fn.hierarchySelect.noConflict = function () { 313 | $.fn.hierarchySelect = old; 314 | return this; 315 | }; 316 | 317 | function processElementOffset(parent, element) { 318 | if (parent.offsetHeight + parent.scrollTop < element.offsetTop + element.offsetHeight) { 319 | parent.scrollTop = element.offsetTop + element.offsetHeight - parent.offsetHeight; 320 | } else if (parent.scrollTop >= element.offsetTop - parent.offsetTop) { 321 | parent.scrollTop = element.offsetTop - parent.offsetTop; 322 | } 323 | } 324 | })(jQuery); 325 | -------------------------------------------------------------------------------- /src/hierarchy-select.scss: -------------------------------------------------------------------------------- 1 | .hierarchy-select.dropdown { 2 | .hs-searchbox { 3 | padding: 0 5px 4px; 4 | } 5 | @mixin padding-left($level) { 6 | a[data-level='#{$level}'] { 7 | padding-left: $level * 20px; 8 | } 9 | } 10 | .dropdown-menu { 11 | @for $i from 2 through 10 { 12 | @include padding-left($i); 13 | } 14 | } 15 | } 16 | --------------------------------------------------------------------------------