├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── README.md ├── bower.json ├── dist ├── jquery.cascadingdropdown.js ├── jquery.cascadingdropdown.min.js └── jquery.cascadingdropdown.min.js.map ├── gulpfile.js ├── index.html ├── package-lock.json ├── package.json ├── res ├── ajax-loader.gif └── ajax-mocks.js └── src └── jquery.cascadingdropdown.js /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Link to working example** 24 | Provide a link to a working example demonstrating the bug. You can use plnkr.io, jsfiddle.net, etc for this. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **No longer maintained** 2 | 3 | # jQuery Cascading Dropdown Plugin 4 | 5 | A simple and lighweight jQuery plugin for creating cascading dropdowns. 6 | 7 | [View Demo](https://dnasir.github.io/jquery-cascading-dropdown/) 8 | 9 | ## Installation 10 | 11 | ``` 12 | npm install --save jquery-cascading-dropdown 13 | ``` 14 | 15 | Include script after the jQuery library (unless you are packaging scripts somehow else): 16 | 17 | ```html 18 | 19 | ``` 20 | 21 | ## Usage 22 | 23 | To initialize the plugin, simply attach it to the parent group of dropdown elements. 24 | 25 | ```html 26 | 32 | ``` 33 | 34 | ```javascript 35 | $('#dropdowns').cascadingDropdown(options); 36 | ``` 37 | 38 | ## Options 39 | 40 | #### usePost (boolean) 41 | 42 | usePost: false 43 | 44 | Added: 1.1.0 45 | 46 | Tells the plugin to use POST when sending Ajax request. 47 | 48 | #### UseJson (boolean) 49 | 50 | useJson: false 51 | 52 | Added: 1.1.2 53 | 54 | Tells the plugin to stringify (JSON.stringify) dropdown data for Ajax requests. Requires 55 | [json2.js](https://github.com/douglascrockford/JSON-js) if you're planning to support older browsers. 56 | 57 | #### onReady (eventHandler) 58 | 59 | onReady: function(event, allValues) { } 60 | 61 | Added: 1.2.0 62 | 63 | An event that is triggered when the plugin is completely initialised. The event handler will be provided with the event object, and an object containing the current values of all the dropdowns in the group. 64 | 65 | #### onChange (eventHandler) 66 | 67 | onChange: function(event, allValues) { } 68 | 69 | Added: 1.2.0 70 | 71 | An event that is triggered whenever the value of a dropdown in a particular group is changed. The event handler will be provided with the event object, and an object containing the current values of all the dropdowns in the group. 72 | 73 | #### isLoadingClassName (string) 74 | 75 | Added: 1.2.2 76 | 77 | isLoadingClassName: 'cascading-dropdown-loading' 78 | 79 | This overrides the default value for the class name applied to the dropdown element during Ajax calls. 80 | 81 | #### selectBoxes 82 | 83 | selectBoxes: [ 84 | { 85 | selector: '.select1', 86 | ... 87 | } 88 | ] 89 | 90 | Added: 1.0.0 91 | 92 | Array of dropdown objects 93 | 94 | #### Dropdown object properties 95 | 96 | ##### selector (string) 97 | 98 | selector: '.selectbox1' 99 | 100 | Added: 1.0.0 101 | 102 | Selector for select box inside parent container. (Required) 103 | 104 | ##### source (string|function) 105 | 106 | source: '/api/CompanyInfo/GetCountries' 107 | 108 | source: function(request, response) { 109 | $.getJSON('path/to/api', request, function(data) { 110 | response($.map(data, function(item, index) { 111 | return { 112 | label: item.itemLabel, 113 | value: item.itemValue 114 | } 115 | })); 116 | }); 117 | } 118 | 119 | Added: 1.2.0 120 | 121 | Source for dropdown items. This can be a URL pointing to the web service that provides the dropdown items, or a function that manually handles the Ajax request and response. 122 | 123 | If a URL is provided, the web service needs to follow a convention where the object returned must be a JSON object containing an array of objects, each containing at least a key-value property named 'label', or 'value'. 124 | 125 | Example JSON object 126 | 127 | [ 128 | { 129 | "label": "Item 1", 130 | "value": "1" 131 | }, 132 | { 133 | "label": "Item 2", 134 | "value": "2" 135 | } 136 | ] 137 | 138 | It's also possible to include a property named 'selected' in the object to define a selected item. 139 | 140 | It is also possible to create option groups in the select by specifying a key (the group name) in the JSON. 141 | 142 | Example JSON object with groups 143 | 144 | { 145 | 'My Group': 146 | [ 147 | { 148 | "label": "Item 1", 149 | "value": "1" 150 | }, 151 | { 152 | "label": "Item 2", 153 | "value": "2" 154 | } 155 | ], 156 | 'Another Group': 157 | [ 158 | { 159 | "label": "Item 3", 160 | "value": "3" 161 | }, 162 | { 163 | "label": "Item 4", 164 | "value": "4" 165 | } 166 | ] 167 | } 168 | 169 | 170 | If the source parameter is not set, the plugin will simply enable the select box when requirements are met. 171 | 172 | ##### requires (array) 173 | 174 | requires: ['.selectbox1'] 175 | 176 | Added: 1.0.0 177 | 178 | Array of dropdown selectors required to have value before this dropdown is enabled. 179 | 180 | ##### requireAll (boolean) 181 | 182 | requireAll: true 183 | 184 | Added: 1.0.0 185 | 186 | If set to true, all dropdowns defined in the requires array must have a value before this dropdown is enabled. 187 | If this value is set to false, this dropdown will be enabled if any one of the required dropdowns is valid. 188 | 189 | ##### paramName (string) 190 | 191 | paramName: 'countryId' 192 | 193 | Added: 1.0.0 194 | 195 | Required dropdown value parameter name used in Ajax requests. If this value is not set, the plugin will use the dropdown name attribute. If neither this parameter nor the name attribute is set, this dropdown will not be taken into account in any Ajax request. 196 | 197 | ##### selected (string|integer) 198 | 199 | selected: 'red' 200 | 201 | selected: 2 202 | 203 | Added: 1.1.5 204 | 205 | Sets the default dropdown item on initialisation. The value can be a the value of the targeted dropdown item, or its index value. 206 | 207 | ##### onChange (eventHandler) 208 | 209 | onChange: function(event, value, requiredValues, requirementsMet) { } 210 | 211 | Added: 1.0.0
212 | Updated: 1.2.4 213 | 214 | Event handler triggered when the dropdown value is changed. The event handler is passed the event object, the value of the current dropdown, and an object containing the values of all the required dropdowns. A boolean value indicating whether the requirements for a particular dropdown have been met or not is also passed. 215 | 216 | ## Methods 217 | 218 | #### destroy 219 | 220 | Destroys the instance and reverts everything back to their initial state. 221 | 222 | ```javascript 223 | $('#dropdown').cascadingDropdown('destroy'); 224 | ``` 225 | 226 | Added: 1.2.7 227 | 228 | ## Server-side implementation 229 | 230 | By default, this plugin expects the web service to return a JSON object containing an array of objects with properties 'label' and 'value'. The web service may also include a 'selected' property for an object within an array to indicate that that particular object is to be the selected item. 231 | 232 | ```json 233 | // Example server response 234 | [ 235 | { 236 | "label": "Item 1", 237 | "value": "1" 238 | }, 239 | { 240 | "label": "Item 2", 241 | "value": "2", 242 | "selected": true 243 | } 244 | ] 245 | ``` 246 | 247 | If the value property is not defined, the dropdown item will set the label as the value, and vice versa. 248 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-cascading-dropdown", 3 | "version": "1.2.8", 4 | "homepage": "https://github.com/dnasir/jquery-cascading-dropdown", 5 | "authors": [ 6 | "Dzulqarnain Nasir " 7 | ], 8 | "description": "A simple and lighweight jQuery plugin for creating cascading dropdowns.", 9 | "main": [ 10 | "dist/jquery.cascadingdropdown.js", 11 | "dist/jquery.cascadingdropdown.min.js", 12 | "dist/jquery.cascadingdropdown.min.js.map" 13 | ], 14 | "keywords": [ 15 | "jquery", 16 | "cascading", 17 | "dropdown" 18 | ], 19 | "license": "MIT", 20 | "private": true, 21 | "ignore": [ 22 | "**/.*", 23 | "node_modules", 24 | "bower_components" 25 | ], 26 | "dependencies": { 27 | "jquery": "^3.4.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /dist/jquery.cascadingdropdown.js: -------------------------------------------------------------------------------- 1 | /*! jquery-cascading-dropdown 1.2.9 | (c) 2019 Dzulqarnain Nasir | MIT */ 2 | (function ($, undefined) { 3 | 'use strict'; 4 | 5 | var defaultOptions = { 6 | usePost: false, 7 | useJson: false 8 | }; 9 | 10 | // Constructor 11 | function Dropdown(options, parent) { 12 | this.el = $(options.selector, parent.el); 13 | this.parent = parent; 14 | this.options = $.extend({}, defaultOptions, options); 15 | this.name = this.options.paramName || this.el.attr('name'); 16 | this.requiredDropdowns = options.requires && options.requires.length ? $(options.requires.join(','), parent.el) : null; 17 | this.isLoadingClassName = this.options.isLoadingClassName || parent.options.isLoadingClassName || 'cascading-dropdown-loading'; 18 | } 19 | 20 | // Methods 21 | Dropdown.prototype = { 22 | _create: function() { 23 | var self = this; 24 | 25 | self.pending = 0; 26 | self.initialised = false; 27 | self.initialState = self.el.clone(true); 28 | self.el.data('plugin_cascadingDropdown', this); 29 | self.originalDropdownItems = self.el.children('option'); 30 | 31 | // Init event handlers 32 | if(typeof self.options.onChange === 'function') { 33 | self.el.change(function(event) { 34 | var requirementsMet = self._requirementsMet() && self.el[0].value; 35 | self.options.onChange.call(self, event, self.el.val(), self.getRequiredValues(), requirementsMet); 36 | }); 37 | } 38 | 39 | if(typeof self.options.onEnable === 'function') { 40 | self.el.on('enabled', function(event) { 41 | self.options.onEnable.call(self, event, self.el.val()); 42 | }); 43 | } 44 | 45 | if(typeof self.options.onDisable === 'function') { 46 | self.el.on('disabled', function(event) { 47 | self.options.onDisable.call(self, event, self.el.val()); 48 | }); 49 | } 50 | 51 | if(self.requiredDropdowns) { 52 | self.requiredDropdowns.change(function(event) { 53 | self.update(); 54 | }); 55 | } 56 | 57 | // Init source 58 | self._initSource(); 59 | 60 | // Call update 61 | self.update(); 62 | }, 63 | 64 | // Destroys the instance and reverts everything back to initial state 65 | _destroy: function() { 66 | this.el.replaceWith(this.initialState).removeData('plugin_cascadingDropdown'); 67 | }, 68 | 69 | // Enables the dropdown 70 | enable: function() { 71 | if(this.el.attr('disabled') === undefined) return; 72 | this.el.removeAttr('disabled').triggerHandler('enabled'); 73 | }, 74 | 75 | // Disables the dropdown 76 | disable: function() { 77 | if(this.el.attr('disabled') !== undefined) return; 78 | this.el.attr('disabled', 'disabled').triggerHandler('disabled'); 79 | }, 80 | 81 | // Checks if required dropdowns have value 82 | _requirementsMet: function() { 83 | var self = this; 84 | 85 | if(!self.requiredDropdowns) { 86 | return true; 87 | } 88 | 89 | if(self.options.requireAll) { // If requireAll is true, return true if all dropdowns have values 90 | return (self.requiredDropdowns.filter(function() { 91 | return !!$(this).val(); 92 | }).length == self.options.requires.length); 93 | } else { // Otherwise, return true if any one of the required dropdowns has value 94 | return (self.requiredDropdowns.filter(function() { 95 | return !!$(this).val(); 96 | }).length > 0); 97 | } 98 | }, 99 | 100 | // Defines dropdown item list source - inspired by jQuery UI Autocomplete 101 | _initSource: function() { 102 | var self = this; 103 | 104 | if($.isArray(self.options.source)) { 105 | this.source = function(request, response) { 106 | response($.map(self.options.source, function(item) { 107 | return { 108 | label: item.label || item.value || item, 109 | value: item.value || item.label || item, 110 | selected: item.selected 111 | }; 112 | })); 113 | }; 114 | } else if ( typeof self.options.source === 'string' ) { 115 | var url = self.options.source; 116 | 117 | this.source = function(request, response) { 118 | if ( self.xhr ) { 119 | self.xhr.abort(); 120 | } 121 | self.xhr = $.ajax({ 122 | url: url, 123 | data: self.options.useJson ? JSON.stringify(request) : request, 124 | dataType: self.options.useJson ? 'json' : undefined, 125 | type: self.options.usePost ? 'post' : 'get', 126 | contentType: 'application/json; charset=utf-8', 127 | success: function(data) { 128 | response(data); 129 | }, 130 | error: function() { 131 | response([]); 132 | } 133 | }); 134 | }; 135 | } else { 136 | this.source = self.options.source; 137 | } 138 | }, 139 | 140 | getRequiredValues: function() { 141 | var data = {}; 142 | if(this.requiredDropdowns) { 143 | $.each(this.requiredDropdowns, function() { 144 | var instance = $(this).data('plugin_cascadingDropdown'); 145 | if(instance.name) { 146 | data[instance.name] = instance.el.val(); 147 | } 148 | }); 149 | } 150 | 151 | return data; 152 | }, 153 | 154 | // Update the dropdown 155 | update: function() { 156 | var self = this; 157 | 158 | // Disable it first 159 | self.disable(); 160 | 161 | // If required dropdowns have no value, return 162 | if(!self._requirementsMet()) { 163 | self.setSelected(0); 164 | self._triggerReady(); 165 | return self.el; 166 | } 167 | 168 | // If source isn't defined, it's most likely a static dropdown, so just enable it 169 | if(!self.source) { 170 | self.enable(); 171 | self._triggerReady(); 172 | return self.el; 173 | } 174 | 175 | // Reset the dropdown value so we don't trigger a false call 176 | self.el.val('').change(); 177 | 178 | // Fetch data from required dropdowns 179 | var data = self.getRequiredValues(); 180 | 181 | // Pass it to defined source for processing 182 | self.pending++; 183 | self.el.addClass(self.isLoadingClassName); 184 | self.source(data, self._response()); 185 | 186 | return self.el; 187 | }, 188 | 189 | _response: function(items) { 190 | var self = this; 191 | 192 | return function(items) { 193 | self._renderItems(items); 194 | 195 | self.pending--; 196 | if(!self.pending) { 197 | self.el.removeClass(self.isLoadingClassName); 198 | } 199 | } 200 | }, 201 | 202 | // Render the dropdown items 203 | _renderItems: function(items) { 204 | var self = this; 205 | 206 | // Remove all dropdown items and restore to initial state 207 | self.el.find('option, optgroup').remove(); 208 | self.el.append(self.originalDropdownItems); 209 | 210 | if(!items) { 211 | self._triggerReady(); 212 | return; 213 | } 214 | 215 | var selected = []; 216 | 217 | if ($.isArray(items)) { 218 | $.each(items, function(index, item) { 219 | self.el.append(self._renderItem(item)); 220 | if (item.selected) selected.push(item.value.toString()); 221 | }); 222 | } else { 223 | $.each(items, function(key, value) { 224 | var itemData = []; 225 | itemData.push(''); 226 | for (var i = 0; i < value.length; i++) { 227 | var item = value[i]; 228 | itemData.push(self._renderItem(item)); 229 | if (item.selected) selected.push(item.value.toString()); 230 | } 231 | itemData.push(''); 232 | self.el.append(itemData.join('')); 233 | }); 234 | } 235 | 236 | // Enable the dropdown 237 | self.enable(); 238 | 239 | // If a selected item exists, set it as default 240 | selected.length && self.setSelected(selected); 241 | 242 | self._triggerReady(); 243 | }, 244 | 245 | _renderItem: function(item) { 246 | return ''; 247 | }, 248 | 249 | // Trigger the ready event when instance is initialised for the first time 250 | _triggerReady: function() { 251 | if(this.initialised) return; 252 | 253 | // Set selected dropdown item if defined 254 | this.options.selected && this.setSelected(this.options.selected); 255 | 256 | this.initialised = true; 257 | this.el.triggerHandler('ready'); 258 | }, 259 | 260 | // Sets the selected dropdown item 261 | setSelected: function(indexOrValue, triggerChange) { 262 | var self = this, 263 | dropdownItems = self.el.find('option'); 264 | 265 | // Trigger change event by default 266 | if(typeof triggerChange === 'undefined') { 267 | triggerChange = true; 268 | } 269 | 270 | var selectedItems = []; 271 | 272 | // check if indexOrValue is an array 273 | if($.isArray(indexOrValue)) { 274 | selectedItems = indexOrValue; 275 | } else { 276 | selectedItems = selectedItems.concat(indexOrValue); 277 | } 278 | 279 | var selectedValue; 280 | if(self.el.is('[multiple]')) { 281 | selectedValue = selectedItems.map(function(item) { 282 | if(typeof item === 'number' 283 | && (item !== undefined 284 | && item > -1 285 | && item < dropdownItems.length)) { 286 | return dropdownItems[item].value 287 | } 288 | 289 | return item; 290 | }); 291 | 292 | } else { 293 | selectedValue = selectedItems[0]; 294 | 295 | // if selected item is a number, get the value for the item at that index 296 | if(typeof selectedItems[0] === 'number' 297 | && (selectedItems[0] !== undefined 298 | && selectedItems[0] > -1 299 | && selectedItems[0] < dropdownItems.length)) { 300 | selectedValue = dropdownItems[selectedItems[0]].value; 301 | } 302 | } 303 | 304 | // Set the dropdown item 305 | self.el.val(selectedValue); 306 | 307 | // Trigger change event 308 | if(triggerChange) { 309 | self.el.change(); 310 | } 311 | 312 | return self.el; 313 | } 314 | }; 315 | 316 | function CascadingDropdown(element, options) { 317 | this.el = $(element); 318 | this.options = $.extend({ selectBoxes: [] }, options); 319 | this._init(); 320 | } 321 | 322 | CascadingDropdown.prototype = { 323 | _init: function() { 324 | var self = this; 325 | 326 | self.pending = 0; 327 | 328 | // Instance array 329 | self.dropdowns = []; 330 | 331 | var dropdowns = $($.map(self.options.selectBoxes, function(item) { 332 | return item.selector; 333 | }).join(','), self.el); 334 | 335 | // Init event handlers 336 | var counter = 0; 337 | function readyEventHandler(event) { 338 | if(++counter == dropdowns.length) { // Once all dropdowns are ready, unbind the event handler, and execute onReady 339 | dropdowns.unbind('ready', readyEventHandler); 340 | self.options.onReady.call(self, event, self.getValues()); 341 | } 342 | } 343 | 344 | function changeEventHandler(event) { 345 | self.options.onChange.call(self, event, self.getValues()); 346 | } 347 | 348 | if(typeof self.options.onReady === 'function') { 349 | dropdowns.bind('ready', readyEventHandler); 350 | } 351 | 352 | if(typeof self.options.onChange === 'function') { 353 | dropdowns.bind('change', changeEventHandler); 354 | } 355 | 356 | // Init dropdowns 357 | $.each(self.options.selectBoxes, function(index, item) { 358 | // Create the instance 359 | var instance = new Dropdown(this, self); 360 | 361 | // Insert it into the dropdown instance array 362 | self.dropdowns.push(instance); 363 | 364 | // Call the create method 365 | instance._create(); 366 | }); 367 | }, 368 | 369 | // Destroys the instance and reverts everything back to its initial state 370 | destroy: function() { 371 | $.each(this.dropdowns, function(index, item){ 372 | item._destroy(); 373 | }); 374 | this.el.removeData('plugin_cascadingDropdown'); 375 | 376 | return this.el; 377 | }, 378 | 379 | // Fetches the values from all dropdowns in this group 380 | getValues: function() { 381 | var values = {}; 382 | 383 | // Build the object and insert values from instances with name 384 | $.each(this.dropdowns, function(index, instance) { 385 | if(instance.name) { 386 | values[instance.name] = instance.el.val(); 387 | } 388 | }); 389 | 390 | return values; 391 | } 392 | } 393 | 394 | // jQuery plugin declaration 395 | $.fn.cascadingDropdown = function(methodOrOptions) { 396 | var $this = $(this), 397 | args = arguments, 398 | instance = $this.data('plugin_cascadingDropdown'); 399 | 400 | if(typeof methodOrOptions === 'object' || !methodOrOptions) { 401 | return !instance && $this.data('plugin_cascadingDropdown', new CascadingDropdown(this, methodOrOptions)); 402 | } else if(typeof methodOrOptions === 'string') { 403 | if(!instance) { 404 | $.error('Cannot call method ' + methodOrOptions + ' before init.'); 405 | } else if(instance[methodOrOptions]) { 406 | return instance[methodOrOptions].apply(instance, Array.prototype.slice.call(args, 1)) 407 | } 408 | } else { 409 | $.error('Method ' + methodOrOptions + ' does not exist in jQuery.cascadingDropdown'); 410 | } 411 | }; 412 | })(jQuery); 413 | -------------------------------------------------------------------------------- /dist/jquery.cascadingdropdown.min.js: -------------------------------------------------------------------------------- 1 | /*! jquery-cascading-dropdown 1.2.9 | (c) 2019 Dzulqarnain Nasir | MIT */ 2 | !function(a,s){"use strict";var t={usePost:!1,useJson:!1};function n(e,n){this.el=a(e.selector,n.el),this.parent=n,this.options=a.extend({},t,e),this.name=this.options.paramName||this.el.attr("name"),this.requiredDropdowns=e.requires&&e.requires.length?a(e.requires.join(","),n.el):null,this.isLoadingClassName=this.options.isLoadingClassName||n.options.isLoadingClassName||"cascading-dropdown-loading"}function o(e,n){this.el=a(e),this.options=a.extend({selectBoxes:[]},n),this._init()}n.prototype={_create:function(){var t=this;t.pending=0,t.initialised=!1,t.initialState=t.el.clone(!0),t.el.data("plugin_cascadingDropdown",this),t.originalDropdownItems=t.el.children("option"),"function"==typeof t.options.onChange&&t.el.change(function(e){var n=t._requirementsMet()&&t.el[0].value;t.options.onChange.call(t,e,t.el.val(),t.getRequiredValues(),n)}),"function"==typeof t.options.onEnable&&t.el.on("enabled",function(e){t.options.onEnable.call(t,e,t.el.val())}),"function"==typeof t.options.onDisable&&t.el.on("disabled",function(e){t.options.onDisable.call(t,e,t.el.val())}),t.requiredDropdowns&&t.requiredDropdowns.change(function(){t.update()}),t._initSource(),t.update()},_destroy:function(){this.el.replaceWith(this.initialState).removeData("plugin_cascadingDropdown")},enable:function(){this.el.attr("disabled")!==s&&this.el.removeAttr("disabled").triggerHandler("enabled")},disable:function(){this.el.attr("disabled")===s&&this.el.attr("disabled","disabled").triggerHandler("disabled")},_requirementsMet:function(){var e=this;return!e.requiredDropdowns||(e.options.requireAll?e.requiredDropdowns.filter(function(){return!!a(this).val()}).length==e.options.requires.length:0');for(var i=0;i"),r.el.append(t.join(""))}),r.enable(),s.length&&r.setSelected(s),r._triggerReady()}else r._triggerReady()},_renderItem:function(e){return'"},_triggerReady:function(){this.initialised||(this.options.selected&&this.setSelected(this.options.selected),this.initialised=!0,this.el.triggerHandler("ready"))},setSelected:function(e,n){var t=this,i=t.el.find("option");void 0===n&&(n=!0);var o,r=[];return r=a.isArray(e)?e:r.concat(e),t.el.is("[multiple]")?o=r.map(function(e){return"number"==typeof e&&e!==s&&-1 | MIT */\n(function ($, undefined) {\r\n 'use strict';\r\n\r\n var defaultOptions = {\r\n usePost: false,\r\n useJson: false\r\n };\r\n\r\n // Constructor\r\n function Dropdown(options, parent) {\r\n this.el = $(options.selector, parent.el);\r\n this.parent = parent;\r\n this.options = $.extend({}, defaultOptions, options);\r\n this.name = this.options.paramName || this.el.attr('name');\r\n this.requiredDropdowns = options.requires && options.requires.length ? $(options.requires.join(','), parent.el) : null;\r\n this.isLoadingClassName = this.options.isLoadingClassName || parent.options.isLoadingClassName || 'cascading-dropdown-loading';\r\n }\r\n\r\n // Methods\r\n Dropdown.prototype = {\r\n _create: function() {\r\n var self = this;\r\n\r\n self.pending = 0;\r\n self.initialised = false;\r\n self.initialState = self.el.clone(true);\r\n self.el.data('plugin_cascadingDropdown', this);\r\n self.originalDropdownItems = self.el.children('option');\r\n\r\n // Init event handlers\r\n if(typeof self.options.onChange === 'function') {\r\n self.el.change(function(event) {\r\n var requirementsMet = self._requirementsMet() && self.el[0].value;\r\n self.options.onChange.call(self, event, self.el.val(), self.getRequiredValues(), requirementsMet);\r\n });\r\n }\r\n\r\n if(typeof self.options.onEnable === 'function') {\r\n self.el.on('enabled', function(event) {\r\n self.options.onEnable.call(self, event, self.el.val());\r\n });\r\n }\r\n\r\n if(typeof self.options.onDisable === 'function') {\r\n self.el.on('disabled', function(event) {\r\n self.options.onDisable.call(self, event, self.el.val());\r\n });\r\n }\r\n\r\n if(self.requiredDropdowns) {\r\n self.requiredDropdowns.change(function(event) {\r\n self.update();\r\n });\r\n }\r\n\r\n // Init source\r\n self._initSource();\r\n\r\n // Call update\r\n self.update();\r\n },\r\n \r\n // Destroys the instance and reverts everything back to initial state\r\n _destroy: function() {\r\n this.el.replaceWith(this.initialState).removeData('plugin_cascadingDropdown');\r\n },\r\n\r\n // Enables the dropdown\r\n enable: function() {\r\n if(this.el.attr('disabled') === undefined) return;\r\n this.el.removeAttr('disabled').triggerHandler('enabled');\r\n },\r\n\r\n // Disables the dropdown\r\n disable: function() {\r\n if(this.el.attr('disabled') !== undefined) return;\r\n this.el.attr('disabled', 'disabled').triggerHandler('disabled');\r\n },\r\n\r\n // Checks if required dropdowns have value\r\n _requirementsMet: function() {\r\n var self = this;\r\n\r\n if(!self.requiredDropdowns) {\r\n return true;\r\n }\r\n\r\n if(self.options.requireAll) { // If requireAll is true, return true if all dropdowns have values\r\n return (self.requiredDropdowns.filter(function() {\r\n return !!$(this).val();\r\n }).length == self.options.requires.length);\r\n } else { // Otherwise, return true if any one of the required dropdowns has value\r\n return (self.requiredDropdowns.filter(function() {\r\n return !!$(this).val();\r\n }).length > 0);\r\n }\r\n },\r\n\r\n // Defines dropdown item list source - inspired by jQuery UI Autocomplete\r\n _initSource: function() {\r\n var self = this;\r\n\r\n if($.isArray(self.options.source)) {\r\n this.source = function(request, response) {\r\n response($.map(self.options.source, function(item) {\r\n return {\r\n label: item.label || item.value || item,\r\n value: item.value || item.label || item,\r\n selected: item.selected\r\n };\r\n }));\r\n };\r\n } else if ( typeof self.options.source === 'string' ) {\r\n var url = self.options.source;\r\n\r\n this.source = function(request, response) {\r\n if ( self.xhr ) {\r\n self.xhr.abort();\r\n }\r\n self.xhr = $.ajax({\r\n url: url,\r\n data: self.options.useJson ? JSON.stringify(request) : request,\r\n dataType: self.options.useJson ? 'json' : undefined,\r\n type: self.options.usePost ? 'post' : 'get',\r\n contentType: 'application/json; charset=utf-8',\r\n success: function(data) {\r\n response(data);\r\n },\r\n error: function() {\r\n response([]);\r\n }\r\n });\r\n };\r\n } else {\r\n this.source = self.options.source;\r\n }\r\n },\r\n\r\n getRequiredValues: function() {\r\n var data = {};\r\n if(this.requiredDropdowns) {\r\n $.each(this.requiredDropdowns, function() {\r\n var instance = $(this).data('plugin_cascadingDropdown');\r\n if(instance.name) {\r\n data[instance.name] = instance.el.val();\r\n }\r\n });\r\n }\r\n\r\n return data;\r\n },\r\n\r\n // Update the dropdown\r\n update: function() {\r\n var self = this;\r\n\r\n // Disable it first\r\n self.disable();\r\n\r\n // If required dropdowns have no value, return\r\n if(!self._requirementsMet()) {\r\n self.setSelected(0);\r\n self._triggerReady();\r\n return self.el;\r\n }\r\n\r\n // If source isn't defined, it's most likely a static dropdown, so just enable it\r\n if(!self.source) {\r\n self.enable();\r\n self._triggerReady();\r\n return self.el;\r\n }\r\n\r\n // Reset the dropdown value so we don't trigger a false call\r\n self.el.val('').change();\r\n\r\n // Fetch data from required dropdowns\r\n var data = self.getRequiredValues();\r\n\r\n // Pass it to defined source for processing\r\n self.pending++;\r\n self.el.addClass(self.isLoadingClassName);\r\n self.source(data, self._response());\r\n\r\n return self.el;\r\n },\r\n\r\n _response: function(items) {\r\n var self = this;\r\n\r\n return function(items) {\r\n self._renderItems(items);\r\n\r\n self.pending--;\r\n if(!self.pending) {\r\n self.el.removeClass(self.isLoadingClassName);\r\n }\r\n }\r\n },\r\n\r\n // Render the dropdown items\r\n _renderItems: function(items) {\r\n var self = this;\r\n\r\n // Remove all dropdown items and restore to initial state\r\n self.el.find('option, optgroup').remove();\r\n self.el.append(self.originalDropdownItems);\r\n\r\n if(!items) {\r\n self._triggerReady();\r\n return;\r\n }\r\n\r\n var selected = [];\r\n\r\n if ($.isArray(items)) {\r\n $.each(items, function(index, item) {\r\n self.el.append(self._renderItem(item));\r\n if (item.selected) selected.push(item.value.toString());\r\n });\r\n } else {\r\n $.each(items, function(key, value) {\r\n var itemData = [];\r\n itemData.push('');\r\n for (var i = 0; i < value.length; i++) {\r\n var item = value[i];\r\n itemData.push(self._renderItem(item));\r\n if (item.selected) selected.push(item.value.toString());\r\n }\r\n itemData.push('');\r\n self.el.append(itemData.join(''));\r\n });\r\n }\r\n\r\n // Enable the dropdown\r\n self.enable();\r\n\r\n // If a selected item exists, set it as default\r\n selected.length && self.setSelected(selected);\r\n\r\n self._triggerReady();\r\n },\r\n\r\n _renderItem: function(item) {\r\n return '';\r\n },\r\n\r\n // Trigger the ready event when instance is initialised for the first time\r\n _triggerReady: function() {\r\n if(this.initialised) return;\r\n\r\n // Set selected dropdown item if defined\r\n this.options.selected && this.setSelected(this.options.selected);\r\n\r\n this.initialised = true;\r\n this.el.triggerHandler('ready');\r\n },\r\n\r\n // Sets the selected dropdown item\r\n setSelected: function(indexOrValue, triggerChange) {\r\n var self = this,\r\n dropdownItems = self.el.find('option');\r\n\r\n // Trigger change event by default\r\n if(typeof triggerChange === 'undefined') {\r\n triggerChange = true;\r\n }\r\n \r\n var selectedItems = [];\r\n \r\n // check if indexOrValue is an array\r\n if($.isArray(indexOrValue)) {\r\n selectedItems = indexOrValue;\r\n } else {\r\n selectedItems = selectedItems.concat(indexOrValue);\r\n }\r\n\r\n var selectedValue;\r\n if(self.el.is('[multiple]')) {\r\n selectedValue = selectedItems.map(function(item) {\r\n if(typeof item === 'number'\r\n && (item !== undefined \r\n && item > -1 \r\n && item < dropdownItems.length)) {\r\n return dropdownItems[item].value\r\n }\r\n \r\n return item;\r\n });\r\n\r\n } else {\r\n selectedValue = selectedItems[0];\r\n\r\n // if selected item is a number, get the value for the item at that index\r\n if(typeof selectedItems[0] === 'number' \r\n && (selectedItems[0] !== undefined \r\n && selectedItems[0] > -1 \r\n && selectedItems[0] < dropdownItems.length)) {\r\n selectedValue = dropdownItems[selectedItems[0]].value;\r\n }\r\n }\r\n \r\n // Set the dropdown item\r\n self.el.val(selectedValue);\r\n\r\n // Trigger change event\r\n if(triggerChange) {\r\n self.el.change();\r\n }\r\n\r\n return self.el;\r\n }\r\n };\r\n\r\n function CascadingDropdown(element, options) {\r\n this.el = $(element);\r\n this.options = $.extend({ selectBoxes: [] }, options);\r\n this._init();\r\n }\r\n\r\n CascadingDropdown.prototype = {\r\n _init: function() {\r\n var self = this;\r\n\r\n self.pending = 0;\r\n\r\n // Instance array\r\n self.dropdowns = [];\r\n \r\n var dropdowns = $($.map(self.options.selectBoxes, function(item) {\r\n return item.selector;\r\n }).join(','), self.el);\r\n\r\n // Init event handlers\r\n var counter = 0;\r\n function readyEventHandler(event) {\r\n if(++counter == dropdowns.length) { // Once all dropdowns are ready, unbind the event handler, and execute onReady\r\n dropdowns.unbind('ready', readyEventHandler);\r\n self.options.onReady.call(self, event, self.getValues());\r\n }\r\n }\r\n\r\n function changeEventHandler(event) {\r\n self.options.onChange.call(self, event, self.getValues());\r\n }\r\n \r\n if(typeof self.options.onReady === 'function') {\r\n dropdowns.bind('ready', readyEventHandler);\r\n }\r\n\r\n if(typeof self.options.onChange === 'function') {\r\n dropdowns.bind('change', changeEventHandler);\r\n }\r\n\r\n // Init dropdowns\r\n $.each(self.options.selectBoxes, function(index, item) {\r\n // Create the instance\r\n var instance = new Dropdown(this, self);\r\n\r\n // Insert it into the dropdown instance array\r\n self.dropdowns.push(instance);\r\n\r\n // Call the create method\r\n instance._create();\r\n });\r\n },\r\n \r\n // Destroys the instance and reverts everything back to its initial state \r\n destroy: function() {\r\n $.each(this.dropdowns, function(index, item){\r\n item._destroy();\r\n });\r\n this.el.removeData('plugin_cascadingDropdown');\r\n \r\n return this.el;\r\n },\r\n\r\n // Fetches the values from all dropdowns in this group\r\n getValues: function() {\r\n var values = {};\r\n\r\n // Build the object and insert values from instances with name\r\n $.each(this.dropdowns, function(index, instance) {\r\n if(instance.name) {\r\n values[instance.name] = instance.el.val();\r\n }\r\n });\r\n\r\n return values;\r\n }\r\n }\r\n\r\n // jQuery plugin declaration\r\n $.fn.cascadingDropdown = function(methodOrOptions) {\r\n var $this = $(this),\r\n args = arguments,\r\n instance = $this.data('plugin_cascadingDropdown');\r\n\r\n if(typeof methodOrOptions === 'object' || !methodOrOptions) {\r\n return !instance && $this.data('plugin_cascadingDropdown', new CascadingDropdown(this, methodOrOptions));\r\n } else if(typeof methodOrOptions === 'string') {\r\n if(!instance) {\r\n $.error('Cannot call method ' + methodOrOptions + ' before init.');\r\n } else if(instance[methodOrOptions]) {\r\n return instance[methodOrOptions].apply(instance, Array.prototype.slice.call(args, 1))\r\n }\r\n } else {\r\n $.error('Method ' + methodOrOptions + ' does not exist in jQuery.cascadingDropdown');\r\n }\r\n };\r\n})(jQuery);\r\n"]} -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var uglify = require('gulp-uglify'); 3 | var header = require('gulp-header'); 4 | var sourcemaps = require('gulp-sourcemaps'); 5 | var rename = require('gulp-rename'); 6 | var connect = require('gulp-connect'); 7 | var pkg = require('./package.json'); 8 | 9 | var year = new Date().getFullYear(); 10 | var banner = 11 | '/*! <%= pkg.name %> <%= pkg.version %> | (c) ' + 12 | year + 13 | ' <%= pkg.author %> | <%= pkg.license %> */\n'; 14 | 15 | gulp.task('js', function() { 16 | return gulp 17 | .src('src/jquery.cascadingdropdown.js') 18 | .pipe(header(banner, { pkg: pkg })) 19 | .pipe(gulp.dest('dist/')) 20 | .pipe(rename('jquery.cascadingdropdown.min.js')) 21 | .pipe(sourcemaps.init()) 22 | .pipe( 23 | uglify({ 24 | output: { 25 | comments: /^!/ 26 | } 27 | }) 28 | ) 29 | .pipe(sourcemaps.write('./')) 30 | .pipe(gulp.dest('dist/')) 31 | .pipe(connect.reload()); 32 | }); 33 | 34 | gulp.task('webserver', function(done) { 35 | connect.server({ 36 | livereload: true 37 | }); 38 | done(); 39 | }); 40 | 41 | gulp.task('watch', function(done) { 42 | gulp.watch('src/jquery.cascadingdropdown.js', gulp.series(['js'])); 43 | done(); 44 | }); 45 | 46 | gulp.task('default', gulp.series(['js'])); 47 | gulp.task('dev', gulp.parallel(['webserver', 'watch'])); 48 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | jQuery Cascading Dropdown Plugin Examples 7 | 8 | 9 | 10 | 11 | 16 | 49 | 54 | 59 | 60 | 61 |
62 | 65 | 66 |

Basic

67 |

68 | This is a basic dropdown setup where the second dropdown is dependent on 69 | the first dropdown having a value, and the third dropdown is dependent 70 | on either the first or second one having a value. 71 |

72 | 73 |

74 | This example also shows how you can set the selected dropdown item when 75 | you initialise the plugin. This can be done either in code, or by 76 | including the HTML selected attribute on the targeted dropdown item. 77 |

78 | 79 |
80 |

Phone finder

81 | 82 | 89 | 95 | 102 | 103 |

104 | Matches 105 |

106 |
    107 |
  • 108 | 109 | 110 |
  • 111 |
112 |

No matches

113 |
114 | 115 |
Code
116 |
117 |
118 | $('#example1').cascadingDropdown({
119 | 	selectBoxes: [
120 | 		{
121 | 			selector: '.step1',
122 | 			selected: '4.3'
123 | 		},
124 | 		{
125 | 			selector: '.step2',
126 | 			requires: ['.step1']
127 | 		},
128 | 		{
129 | 			selector: '.step3',
130 | 			requires: ['.step1', '.step2'],
131 | 			onChange: function(event, value, requiredValues) {
132 | 				// do stuff
133 | 
134 | 				// event is the change event object for the current dropdown
135 | 				// value is the current dropdown value
136 | 				// requiredValues is an object with required dropdown values
137 | 				// requirementsMet is a boolean value to indicate if all requirements (including current dropdown having a value) have been met
138 | 			}
139 | 		}
140 | 	]
141 | });
143 |
144 | 145 |
146 | 147 |

Dynamic

148 |

149 | This example shows how you can create a completely dynamic group of 150 | dropdowns. Dropdowns with dependencies will react based on the rules 151 | given, and fetch its own list from the server via Ajax. 152 |

153 | 154 |

155 | You can provide an array of strings or objects, or a function as the 156 | dropdown data source. 157 |

158 | 159 |

160 | This example also demonstrates how you can set the selected item for a 161 | dropdown in the source option. For example, you can have it select an 162 | item when it is the only item available. The plugin will select that 163 | item and trigger the change event. 164 |

165 | 166 |
167 |

Phone finder

168 | 169 | 172 | 175 | 178 | 179 |

180 | Matches 181 |

182 |
    183 |
  • 184 | 185 | 186 |
  • 187 |
188 |

No matches

189 |
190 | 191 |
Code
192 |
193 |
194 | $('#example2').cascadingDropdown({
195 | 	selectBoxes: [
196 | 		{
197 | 			selector: '.step1',
198 | 			source: [
199 | 				{ label: '4.0"', value: 4 },
200 | 				{ label: '4.3"', value: 4.3 },
201 | 				{ label: '4.7"', value: 4.7 },
202 | 				{ label: '5.0"', value: 5 }
203 | 			]
204 | 		},
205 | 		{
206 | 			selector: '.step2',
207 | 			requires: ['.step1'],
208 | 			source: function(request, response) {
209 | 				$.getJSON('/api/resolutions', request, function(data) {
210 | 					var selectOnlyOption = data.length <= 1;
211 | 					response($.map(data, function(item, index) {
212 | 						return {
213 | 							label: item + 'p',
214 | 							value: item,
215 | 							selected: selectOnlyOption // Select if only option
216 | 						};
217 | 					}));
218 | 				});
219 | 			}
220 | 		},
221 | 		{
222 | 			selector: '.step3',
223 | 			requires: ['.step1', '.step2'],
224 | 			requireAll: true,
225 | 			source: function(request, response) {
226 | 				$.getJSON('/api/storages', request, function(data) {
227 | 					response($.map(data, function(item, index) {
228 | 						return {
229 | 							label: item + ' GB',
230 | 							value: item,
231 | 							selected: index == 0 // Select first available option
232 | 						};
233 | 					}));
234 | 				});
235 | 			},
236 | 			onChange: function(event, value, requiredValues, requirementsMet){
237 | 				// do stuff
238 | 			}
239 | 		}
240 | 	]
241 | });
243 |
244 | 245 |
246 | 247 |

Combined

248 |

249 | This example demonstrates the plugin's capability to combine both static 250 | and dynamic dropdowns. It also demonstrates how you can set the default 251 | selected dropdown item in a dynamic dropdown scenario. 252 |

253 | 254 |
255 |

Phone finder

256 | 257 | 264 | 267 | 270 | 271 |

272 | Matches 273 |

274 |
    275 |
  • 276 | 277 | 278 |
  • 279 |
280 |

No matches

281 |
282 | 283 |
Code
284 |
285 |
286 | $('#example3').cascadingDropdown({
287 | 	selectBoxes: [
288 | 		{
289 | 			selector: '.step1'
290 | 		},
291 | 		{
292 | 			selector: '.step2'
293 | 		},
294 | 		{
295 | 			selector: '.step3',
296 | 			requires: ['.step1', '.step2'],
297 | 			requireAll: true,
298 | 			source: function(request, response) {
299 | 				$.getJSON('/api/storages', request, function(data) {
300 | 					response($.map(data, function(item, index) {
301 | 						return {
302 | 							label: item + ' GB',
303 | 							value: item,
304 | 							selected: index == 0 // set to true to mark it as the selected item
305 | 						};
306 | 					}));
307 | 				});
308 | 			}
309 | 		}
310 | 	],
311 | 	onChange: function(event, dropdownData) {
312 | 		// do stuff
313 | 		// dropdownData is an object with values from all the dropdowns in this group
314 | 	},
315 | 	onReady: function(event, dropdownData) {
316 | 		// do stuff
317 | 	}
318 | });
320 |
321 | 322 |
323 | 324 |

Option Group

325 |

326 | This example demonstrates the plugin's capability to combine both 327 | standard and grouped (option group) dropdowns. 328 |

329 | 330 |
331 |

Phone finder

332 | 333 | 340 | 343 | 346 | 347 |

348 | Matches 349 |

350 |
    351 |
  • 352 | 353 | 354 |
  • 355 |
356 |

No matches

357 |
358 | 359 |
Code
360 |
361 |
362 | $('#example5').cascadingDropdown({
363 | 	selectBoxes: [
364 | 	{
365 | 		selector: '.step1'
366 | 	},
367 | 	{
368 | 		selector: '.step2',
369 | 		source: function(request, response) {
370 | 			$.getJSON('/api/resolutionsGrouped', request, function(data) {
371 | 				var newData = {};
372 | 				$.each(data, function(key, value) {
373 | 					newData[key] = $.map(value, function(item, index) {
374 | 						return {
375 | 							label: item + 'p',
376 | 							value: item
377 | 						};
378 | 					});
379 | 				});
380 | 
381 | 				response(newData);
382 | 			});
383 | 		}
384 | 	},
385 | 	{
386 | 		selector: '.step3',
387 | 		requires: ['.step1', '.step2'],
388 | 		requireAll: true,
389 | 		source: function(request, response) {
390 | 			$.getJSON('/api/storages', request, function(data) {
391 | 				response($.map(data, function(item, index) {
392 | 					return {
393 | 						label: item + ' GB',
394 | 						value: item,
395 | 						selected: index == 0 // set to true to mark it as the selected item
396 | 					};
397 | 				}));
398 | 			});
399 | 		}
400 | 	}
401 | 	],
402 | 	onChange: function(event, dropdownData) {
403 | 		// do stuff
404 | 		// dropdownData is an object with values from all the dropdowns in this group
405 | 	},
406 | 	onReady: function(event, dropdownData) {
407 | 		// do stuff
408 | 	}
409 | });
411 |
412 | 413 |
414 | 415 |

Multiple select

416 |

417 | You can enable multiple select by including the 418 | multiple attribute. The value for the dropdown will then be 419 | an array of strings instead of a string. However, you will need to 420 | ensure that your backend service supports this type of data. 421 |

422 | 423 |
424 |

Phone finder

425 | 426 | 432 | 433 | 434 | 435 |

436 | Matches 437 |

438 |
    439 |
  • 440 | 441 | 442 |
  • 443 |
444 |

No matches

445 |
446 | 447 |
448 | 449 |

450 | Copyright © 2013 452 | Dzulqarnain Nasir 454 |

455 |
456 | 457 | 458 | 462 | 466 | 470 | 471 | 475 | 763 | 767 | 771 | 774 | 775 | 776 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-cascading-dropdown", 3 | "version": "1.2.9", 4 | "description": "A simple and lighweight jQuery plugin for creating cascading dropdowns.", 5 | "main": "dist/jquery.cascadingdropdown.min.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "gulp", 9 | "dev": "gulp dev", 10 | "example": "gulp webserver" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/dnasir/jquery-cascading-dropdown.git" 15 | }, 16 | "keywords": [ 17 | "jquery", 18 | "cascading", 19 | "dropdown" 20 | ], 21 | "author": "Dzulqarnain Nasir ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/dnasir/jquery-cascading-dropdown/issues" 25 | }, 26 | "homepage": "https://github.com/dnasir/jquery-cascading-dropdown", 27 | "dependencies": { 28 | "jquery": "^3.4.1" 29 | }, 30 | "devDependencies": { 31 | "gulp": "^4.0.2", 32 | "gulp-connect": "^5.7.0", 33 | "gulp-header": "^2.0.7", 34 | "gulp-rename": "^1.2.2", 35 | "gulp-sourcemaps": "^2.6.5", 36 | "gulp-uglify": "^3.0.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /res/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnasir/jquery-cascading-dropdown/e6186d3dff6e987dc57958e1e5c67d6701ae034d/res/ajax-loader.gif -------------------------------------------------------------------------------- /res/ajax-mocks.js: -------------------------------------------------------------------------------- 1 | // Some mockjax code to simulate Ajax calls 2 | var phoneList = [ 3 | { 4 | maker: 'HTC', 5 | model: 'One S', 6 | screen: 4.3, 7 | resolution: 540, 8 | storage: [8, 16] 9 | }, 10 | { 11 | maker: 'Samsung', 12 | model: 'Galaxy S4', 13 | screen: 5, 14 | resolution: 1080, 15 | storage: [16, 32, 64] 16 | }, 17 | { 18 | maker: 'HTC', 19 | model: 'One', 20 | screen: 4.7, 21 | resolution: 1080, 22 | storage: [32, 64] 23 | }, 24 | { 25 | maker: 'Motorola', 26 | model: 'Droid 4', 27 | screen: 4, 28 | resolution: 540, 29 | storage: [8] 30 | }, 31 | { 32 | maker: 'Motorola', 33 | model: 'Droid RAZR HD', 34 | screen: 4.7, 35 | resolution: 720, 36 | storage: [16] 37 | }, 38 | { 39 | maker: 'LG', 40 | model: 'Optimus 4X HD', 41 | screen: 4.7, 42 | resolution: 720, 43 | storage: [16] 44 | }, 45 | { 46 | maker: 'HTC', 47 | model: 'Butterfly', 48 | screen: 5, 49 | resolution: 1080, 50 | storage: [16] 51 | }, 52 | { 53 | maker: 'Motorola', 54 | model: 'Moto X', 55 | screen: 4.7, 56 | resolution: 720, 57 | storage: [16, 32] 58 | } 59 | ]; 60 | 61 | function arrayIntersect(a, b) { 62 | return $.grep(a, function(i) { 63 | return $.inArray(i, b) > -1; 64 | }); 65 | } 66 | 67 | function arrayToInt(array) { 68 | var output = []; 69 | 70 | for(var i=0;i -1; 97 | } 98 | 99 | if(_resolution.length) { 100 | r = $.inArray(item.resolution, _resolution) > -1; 101 | } 102 | 103 | if(_storage.length) { 104 | st = arrayIntersect(item.storage, _storage).length > 0; 105 | } 106 | 107 | return !!(s && r && st); 108 | }); 109 | } 110 | 111 | function getScreens(resolution, storage) { 112 | var phones = getPhones(null, resolution, storage); 113 | 114 | var screens = $.map(phones, function(phone) { return phone.screen; }); 115 | screens.sort(asc); 116 | return arrayUnique(screens); 117 | } 118 | 119 | function getResolutions(screen, storage) { 120 | var phones = getPhones(screen, null, storage); 121 | 122 | var resolutions = $.map(phones, function(phone) { return phone.resolution; }); 123 | 124 | resolutions.sort(asc); 125 | return arrayUnique(resolutions); 126 | } 127 | 128 | function getGroupedResolutions(screen, storage) { 129 | var phones = getPhones(screen, null, storage); 130 | 131 | // Create SD and HD resolution groups 132 | // SD <= 540 133 | // HD >= 720 134 | var groupedResolutions = {'SD': [], 'HD': []}; 135 | $.each(phones, function(index, item) { 136 | if (item.resolution <= 540) { 137 | groupedResolutions.SD.push(item.resolution); 138 | } else { 139 | groupedResolutions.HD.push(item.resolution); 140 | } 141 | }); 142 | 143 | $.each(groupedResolutions, function(key, value) { 144 | value.sort(asc); 145 | groupedResolutions[key] = arrayUnique(value); 146 | }); 147 | 148 | return groupedResolutions; 149 | } 150 | 151 | function getStorages(screen, resolution) { 152 | var phones = getPhones(screen, resolution, null); 153 | 154 | var storages = []; 155 | $.each(phones, function(index, item) { 156 | storages = arrayUnique(storages.concat(item.storage)); 157 | }); 158 | storages.sort(asc); 159 | return storages; 160 | } 161 | 162 | function arrayUnique(array) { 163 | var a = array.concat(); 164 | for(var i=0; i 0); 96 | } 97 | }, 98 | 99 | // Defines dropdown item list source - inspired by jQuery UI Autocomplete 100 | _initSource: function() { 101 | var self = this; 102 | 103 | if($.isArray(self.options.source)) { 104 | this.source = function(request, response) { 105 | response($.map(self.options.source, function(item) { 106 | return { 107 | label: item.label || item.value || item, 108 | value: item.value || item.label || item, 109 | selected: item.selected 110 | }; 111 | })); 112 | }; 113 | } else if ( typeof self.options.source === 'string' ) { 114 | var url = self.options.source; 115 | 116 | this.source = function(request, response) { 117 | if ( self.xhr ) { 118 | self.xhr.abort(); 119 | } 120 | self.xhr = $.ajax({ 121 | url: url, 122 | data: self.options.useJson ? JSON.stringify(request) : request, 123 | dataType: self.options.useJson ? 'json' : undefined, 124 | type: self.options.usePost ? 'post' : 'get', 125 | contentType: 'application/json; charset=utf-8', 126 | success: function(data) { 127 | response(data); 128 | }, 129 | error: function() { 130 | response([]); 131 | } 132 | }); 133 | }; 134 | } else { 135 | this.source = self.options.source; 136 | } 137 | }, 138 | 139 | getRequiredValues: function() { 140 | var data = {}; 141 | if(this.requiredDropdowns) { 142 | $.each(this.requiredDropdowns, function() { 143 | var instance = $(this).data('plugin_cascadingDropdown'); 144 | if(instance.name) { 145 | data[instance.name] = instance.el.val(); 146 | } 147 | }); 148 | } 149 | 150 | return data; 151 | }, 152 | 153 | // Update the dropdown 154 | update: function() { 155 | var self = this; 156 | 157 | // Disable it first 158 | self.disable(); 159 | 160 | // If required dropdowns have no value, return 161 | if(!self._requirementsMet()) { 162 | self.setSelected(0); 163 | self._triggerReady(); 164 | return self.el; 165 | } 166 | 167 | // If source isn't defined, it's most likely a static dropdown, so just enable it 168 | if(!self.source) { 169 | self.enable(); 170 | self._triggerReady(); 171 | return self.el; 172 | } 173 | 174 | // Reset the dropdown value so we don't trigger a false call 175 | self.el.val('').change(); 176 | 177 | // Fetch data from required dropdowns 178 | var data = self.getRequiredValues(); 179 | 180 | // Pass it to defined source for processing 181 | self.pending++; 182 | self.el.addClass(self.isLoadingClassName); 183 | self.source(data, self._response()); 184 | 185 | return self.el; 186 | }, 187 | 188 | _response: function(items) { 189 | var self = this; 190 | 191 | return function(items) { 192 | self._renderItems(items); 193 | 194 | self.pending--; 195 | if(!self.pending) { 196 | self.el.removeClass(self.isLoadingClassName); 197 | } 198 | } 199 | }, 200 | 201 | // Render the dropdown items 202 | _renderItems: function(items) { 203 | var self = this; 204 | 205 | // Remove all dropdown items and restore to initial state 206 | self.el.find('option, optgroup').remove(); 207 | self.el.append(self.originalDropdownItems); 208 | 209 | if(!items) { 210 | self._triggerReady(); 211 | return; 212 | } 213 | 214 | var selected = []; 215 | 216 | if ($.isArray(items)) { 217 | $.each(items, function(index, item) { 218 | self.el.append(self._renderItem(item)); 219 | if (item.selected) selected.push(item.value.toString()); 220 | }); 221 | } else { 222 | $.each(items, function(key, value) { 223 | var itemData = []; 224 | itemData.push(''); 225 | for (var i = 0; i < value.length; i++) { 226 | var item = value[i]; 227 | itemData.push(self._renderItem(item)); 228 | if (item.selected) selected.push(item.value.toString()); 229 | } 230 | itemData.push(''); 231 | self.el.append(itemData.join('')); 232 | }); 233 | } 234 | 235 | // Enable the dropdown 236 | self.enable(); 237 | 238 | // If a selected item exists, set it as default 239 | selected.length && self.setSelected(selected); 240 | 241 | self._triggerReady(); 242 | }, 243 | 244 | _renderItem: function(item) { 245 | return ''; 246 | }, 247 | 248 | // Trigger the ready event when instance is initialised for the first time 249 | _triggerReady: function() { 250 | if(this.initialised) return; 251 | 252 | // Set selected dropdown item if defined 253 | this.options.selected && this.setSelected(this.options.selected); 254 | 255 | this.initialised = true; 256 | this.el.triggerHandler('ready'); 257 | }, 258 | 259 | // Sets the selected dropdown item 260 | setSelected: function(indexOrValue, triggerChange) { 261 | var self = this, 262 | dropdownItems = self.el.find('option'); 263 | 264 | // Trigger change event by default 265 | if(typeof triggerChange === 'undefined') { 266 | triggerChange = true; 267 | } 268 | 269 | var selectedItems = []; 270 | 271 | // check if indexOrValue is an array 272 | if($.isArray(indexOrValue)) { 273 | selectedItems = indexOrValue; 274 | } else { 275 | selectedItems = selectedItems.concat(indexOrValue); 276 | } 277 | 278 | var selectedValue; 279 | if(self.el.is('[multiple]')) { 280 | selectedValue = selectedItems.map(function(item) { 281 | if(typeof item === 'number' 282 | && (item !== undefined 283 | && item > -1 284 | && item < dropdownItems.length)) { 285 | return dropdownItems[item].value 286 | } 287 | 288 | return item; 289 | }); 290 | 291 | } else { 292 | selectedValue = selectedItems[0]; 293 | 294 | // if selected item is a number, get the value for the item at that index 295 | if(typeof selectedItems[0] === 'number' 296 | && (selectedItems[0] !== undefined 297 | && selectedItems[0] > -1 298 | && selectedItems[0] < dropdownItems.length)) { 299 | selectedValue = dropdownItems[selectedItems[0]].value; 300 | } 301 | } 302 | 303 | // Set the dropdown item 304 | self.el.val(selectedValue); 305 | 306 | // Trigger change event 307 | if(triggerChange) { 308 | self.el.change(); 309 | } 310 | 311 | return self.el; 312 | } 313 | }; 314 | 315 | function CascadingDropdown(element, options) { 316 | this.el = $(element); 317 | this.options = $.extend({ selectBoxes: [] }, options); 318 | this._init(); 319 | } 320 | 321 | CascadingDropdown.prototype = { 322 | _init: function() { 323 | var self = this; 324 | 325 | self.pending = 0; 326 | 327 | // Instance array 328 | self.dropdowns = []; 329 | 330 | var dropdowns = $($.map(self.options.selectBoxes, function(item) { 331 | return item.selector; 332 | }).join(','), self.el); 333 | 334 | // Init event handlers 335 | var counter = 0; 336 | function readyEventHandler(event) { 337 | if(++counter == dropdowns.length) { // Once all dropdowns are ready, unbind the event handler, and execute onReady 338 | dropdowns.unbind('ready', readyEventHandler); 339 | self.options.onReady.call(self, event, self.getValues()); 340 | } 341 | } 342 | 343 | function changeEventHandler(event) { 344 | self.options.onChange.call(self, event, self.getValues()); 345 | } 346 | 347 | if(typeof self.options.onReady === 'function') { 348 | dropdowns.bind('ready', readyEventHandler); 349 | } 350 | 351 | if(typeof self.options.onChange === 'function') { 352 | dropdowns.bind('change', changeEventHandler); 353 | } 354 | 355 | // Init dropdowns 356 | $.each(self.options.selectBoxes, function(index, item) { 357 | // Create the instance 358 | var instance = new Dropdown(this, self); 359 | 360 | // Insert it into the dropdown instance array 361 | self.dropdowns.push(instance); 362 | 363 | // Call the create method 364 | instance._create(); 365 | }); 366 | }, 367 | 368 | // Destroys the instance and reverts everything back to its initial state 369 | destroy: function() { 370 | $.each(this.dropdowns, function(index, item){ 371 | item._destroy(); 372 | }); 373 | this.el.removeData('plugin_cascadingDropdown'); 374 | 375 | return this.el; 376 | }, 377 | 378 | // Fetches the values from all dropdowns in this group 379 | getValues: function() { 380 | var values = {}; 381 | 382 | // Build the object and insert values from instances with name 383 | $.each(this.dropdowns, function(index, instance) { 384 | if(instance.name) { 385 | values[instance.name] = instance.el.val(); 386 | } 387 | }); 388 | 389 | return values; 390 | } 391 | } 392 | 393 | // jQuery plugin declaration 394 | $.fn.cascadingDropdown = function(methodOrOptions) { 395 | var $this = $(this), 396 | args = arguments, 397 | instance = $this.data('plugin_cascadingDropdown'); 398 | 399 | if(typeof methodOrOptions === 'object' || !methodOrOptions) { 400 | return !instance && $this.data('plugin_cascadingDropdown', new CascadingDropdown(this, methodOrOptions)); 401 | } else if(typeof methodOrOptions === 'string') { 402 | if(!instance) { 403 | $.error('Cannot call method ' + methodOrOptions + ' before init.'); 404 | } else if(instance[methodOrOptions]) { 405 | return instance[methodOrOptions].apply(instance, Array.prototype.slice.call(args, 1)) 406 | } 407 | } else { 408 | $.error('Method ' + methodOrOptions + ' does not exist in jQuery.cascadingDropdown'); 409 | } 410 | }; 411 | })(jQuery); 412 | --------------------------------------------------------------------------------