jquery.cascadingdropdown.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. /*! jquery-cascading-dropdown 1.2.9 | (c) 2019 Dzulqarnain Nasir <dzul1983@gmail.com> | MIT */
  2. (function ($, undefined) {
  3. 'use strict';
  4. var defaultOptions = {
  5. usePost: false,
  6. useJson: false
  7. };
  8. // Constructor
  9. function Dropdown(options, parent) {
  10. this.el = $(options.selector, parent.el);
  11. this.parent = parent;
  12. this.options = $.extend({}, defaultOptions, options);
  13. this.name = this.options.paramName || this.el.attr('name');
  14. this.requiredDropdowns = options.requires && options.requires.length ? $(options.requires.join(','), parent.el) : null;
  15. this.isLoadingClassName = this.options.isLoadingClassName || parent.options.isLoadingClassName || 'cascading-dropdown-loading';
  16. }
  17. // Methods
  18. Dropdown.prototype = {
  19. _create: function() {
  20. var self = this;
  21. self.pending = 0;
  22. self.initialised = false;
  23. self.initialState = self.el.clone(true);
  24. self.el.data('plugin_cascadingDropdown', this);
  25. self.originalDropdownItems = self.el.children('option');
  26. // Init event handlers
  27. if(typeof self.options.onChange === 'function') {
  28. self.el.change(function(event) {
  29. var requirementsMet = self._requirementsMet() && self.el[0].value;
  30. self.options.onChange.call(self, event, self.el.val(), self.getRequiredValues(), requirementsMet);
  31. });
  32. }
  33. if(typeof self.options.onEnable === 'function') {
  34. self.el.on('enabled', function(event) {
  35. self.options.onEnable.call(self, event, self.el.val());
  36. });
  37. }
  38. if(typeof self.options.onDisable === 'function') {
  39. self.el.on('disabled', function(event) {
  40. self.options.onDisable.call(self, event, self.el.val());
  41. });
  42. }
  43. if(self.requiredDropdowns) {
  44. self.requiredDropdowns.change(function(event) {
  45. self.update();
  46. });
  47. }
  48. // Init source
  49. self._initSource();
  50. // Call update
  51. self.update();
  52. },
  53. // Destroys the instance and reverts everything back to initial state
  54. _destroy: function() {
  55. this.el.replaceWith(this.initialState).removeData('plugin_cascadingDropdown');
  56. },
  57. // Enables the dropdown
  58. enable: function() {
  59. if(this.el.attr('disabled') === undefined) return;
  60. this.el.removeAttr('disabled').triggerHandler('enabled');
  61. },
  62. // Disables the dropdown
  63. disable: function() {
  64. if(this.el.attr('disabled') !== undefined) return;
  65. this.el.attr('disabled', 'disabled').triggerHandler('disabled');
  66. },
  67. // Checks if required dropdowns have value
  68. _requirementsMet: function() {
  69. var self = this;
  70. if(!self.requiredDropdowns) {
  71. return true;
  72. }
  73. if(self.options.requireAll) { // If requireAll is true, return true if all dropdowns have values
  74. return (self.requiredDropdowns.filter(function() {
  75. return !!$(this).val();
  76. }).length == self.options.requires.length);
  77. } else { // Otherwise, return true if any one of the required dropdowns has value
  78. return (self.requiredDropdowns.filter(function() {
  79. return !!$(this).val();
  80. }).length > 0);
  81. }
  82. },
  83. // Defines dropdown item list source - inspired by jQuery UI Autocomplete
  84. _initSource: function() {
  85. var self = this;
  86. if($.isArray(self.options.source)) {
  87. this.source = function(request, response) {
  88. response($.map(self.options.source, function(item) {
  89. return {
  90. label: item.label || item.value || item,
  91. value: item.value || item.label || item,
  92. selected: item.selected
  93. };
  94. }));
  95. };
  96. } else if ( typeof self.options.source === 'string' ) {
  97. var url = self.options.source;
  98. this.source = function(request, response) {
  99. if ( self.xhr ) {
  100. self.xhr.abort();
  101. }
  102. self.xhr = $.ajax({
  103. url: url,
  104. data: self.options.useJson ? JSON.stringify(request) : request,
  105. dataType: self.options.useJson ? 'json' : undefined,
  106. type: self.options.usePost ? 'post' : 'get',
  107. contentType: 'application/json; charset=utf-8',
  108. success: function(data) {
  109. response(data);
  110. },
  111. error: function() {
  112. response([]);
  113. }
  114. });
  115. };
  116. } else {
  117. this.source = self.options.source;
  118. }
  119. },
  120. getRequiredValues: function() {
  121. var data = {};
  122. if(this.requiredDropdowns) {
  123. $.each(this.requiredDropdowns, function() {
  124. var instance = $(this).data('plugin_cascadingDropdown');
  125. if(instance.name) {
  126. data[instance.name] = instance.el.val();
  127. }
  128. });
  129. }
  130. return data;
  131. },
  132. // Update the dropdown
  133. update: function() {
  134. var self = this;
  135. // Disable it first
  136. self.disable();
  137. // If required dropdowns have no value, return
  138. if(!self._requirementsMet()) {
  139. self.setSelected(0);
  140. self._triggerReady();
  141. return self.el;
  142. }
  143. // If source isn't defined, it's most likely a static dropdown, so just enable it
  144. if(!self.source) {
  145. self.enable();
  146. self._triggerReady();
  147. return self.el;
  148. }
  149. // Reset the dropdown value so we don't trigger a false call
  150. self.el.val('').change();
  151. // Fetch data from required dropdowns
  152. var data = self.getRequiredValues();
  153. // Pass it to defined source for processing
  154. self.pending++;
  155. self.el.addClass(self.isLoadingClassName);
  156. self.source(data, self._response());
  157. return self.el;
  158. },
  159. _response: function(items) {
  160. var self = this;
  161. return function(items) {
  162. self._renderItems(items);
  163. self.pending--;
  164. if(!self.pending) {
  165. self.el.removeClass(self.isLoadingClassName);
  166. }
  167. }
  168. },
  169. // Render the dropdown items
  170. _renderItems: function(items) {
  171. var self = this;
  172. // Remove all dropdown items and restore to initial state
  173. self.el.find('option, optgroup').remove();
  174. self.el.append(self.originalDropdownItems);
  175. if(!items) {
  176. self._triggerReady();
  177. return;
  178. }
  179. var selected = [];
  180. if ($.isArray(items)) {
  181. $.each(items, function(index, item) {
  182. self.el.append(self._renderItem(item));
  183. if (item.selected) selected.push(item.value.toString());
  184. });
  185. } else {
  186. $.each(items, function(key, value) {
  187. var itemData = [];
  188. itemData.push('<optgroup label="' + key + '">');
  189. for (var i = 0; i < value.length; i++) {
  190. var item = value[i];
  191. itemData.push(self._renderItem(item));
  192. if (item.selected) selected.push(item.value.toString());
  193. }
  194. itemData.push('</optgroup>');
  195. self.el.append(itemData.join(''));
  196. });
  197. }
  198. // Enable the dropdown
  199. self.enable();
  200. // If a selected item exists, set it as default
  201. selected.length && self.setSelected(selected);
  202. self._triggerReady();
  203. },
  204. _renderItem: function(item) {
  205. return '<option value="' + item.value + '"' + (item.selected ? ' selected' : '') + '>' + item.label + '</option>';
  206. },
  207. // Trigger the ready event when instance is initialised for the first time
  208. _triggerReady: function() {
  209. if(this.initialised) return;
  210. // Set selected dropdown item if defined
  211. this.options.selected && this.setSelected(this.options.selected);
  212. this.initialised = true;
  213. this.el.triggerHandler('ready');
  214. },
  215. // Sets the selected dropdown item
  216. setSelected: function(indexOrValue, triggerChange) {
  217. var self = this,
  218. dropdownItems = self.el.find('option');
  219. // Trigger change event by default
  220. if(typeof triggerChange === 'undefined') {
  221. triggerChange = true;
  222. }
  223. var selectedItems = [];
  224. // check if indexOrValue is an array
  225. if($.isArray(indexOrValue)) {
  226. selectedItems = indexOrValue;
  227. } else {
  228. selectedItems = selectedItems.concat(indexOrValue);
  229. }
  230. var selectedValue;
  231. if(self.el.is('[multiple]')) {
  232. selectedValue = selectedItems.map(function(item) {
  233. if(typeof item === 'number'
  234. && (item !== undefined
  235. && item > -1
  236. && item < dropdownItems.length)) {
  237. return dropdownItems[item].value
  238. }
  239. return item;
  240. });
  241. } else {
  242. selectedValue = selectedItems[0];
  243. // if selected item is a number, get the value for the item at that index
  244. if(typeof selectedItems[0] === 'number'
  245. && (selectedItems[0] !== undefined
  246. && selectedItems[0] > -1
  247. && selectedItems[0] < dropdownItems.length)) {
  248. selectedValue = dropdownItems[selectedItems[0]].value;
  249. }
  250. }
  251. // Set the dropdown item
  252. self.el.val(selectedValue);
  253. // Trigger change event
  254. if(triggerChange) {
  255. self.el.change();
  256. }
  257. return self.el;
  258. }
  259. };
  260. function CascadingDropdown(element, options) {
  261. this.el = $(element);
  262. this.options = $.extend({ selectBoxes: [] }, options);
  263. this._init();
  264. }
  265. CascadingDropdown.prototype = {
  266. _init: function() {
  267. var self = this;
  268. self.pending = 0;
  269. // Instance array
  270. self.dropdowns = [];
  271. var dropdowns = $($.map(self.options.selectBoxes, function(item) {
  272. return item.selector;
  273. }).join(','), self.el);
  274. // Init event handlers
  275. var counter = 0;
  276. function readyEventHandler(event) {
  277. if(++counter == dropdowns.length) { // Once all dropdowns are ready, unbind the event handler, and execute onReady
  278. dropdowns.unbind('ready', readyEventHandler);
  279. self.options.onReady.call(self, event, self.getValues());
  280. }
  281. }
  282. function changeEventHandler(event) {
  283. self.options.onChange.call(self, event, self.getValues());
  284. }
  285. if(typeof self.options.onReady === 'function') {
  286. dropdowns.bind('ready', readyEventHandler);
  287. }
  288. if(typeof self.options.onChange === 'function') {
  289. dropdowns.bind('change', changeEventHandler);
  290. }
  291. // Init dropdowns
  292. $.each(self.options.selectBoxes, function(index, item) {
  293. // Create the instance
  294. var instance = new Dropdown(this, self);
  295. // Insert it into the dropdown instance array
  296. self.dropdowns.push(instance);
  297. // Call the create method
  298. instance._create();
  299. });
  300. },
  301. // Destroys the instance and reverts everything back to its initial state
  302. destroy: function() {
  303. $.each(this.dropdowns, function(index, item){
  304. item._destroy();
  305. });
  306. this.el.removeData('plugin_cascadingDropdown');
  307. return this.el;
  308. },
  309. // Fetches the values from all dropdowns in this group
  310. getValues: function() {
  311. var values = {};
  312. // Build the object and insert values from instances with name
  313. $.each(this.dropdowns, function(index, instance) {
  314. if(instance.name) {
  315. values[instance.name] = instance.el.val();
  316. }
  317. });
  318. return values;
  319. }
  320. }
  321. // jQuery plugin declaration
  322. $.fn.cascadingDropdown = function(methodOrOptions) {
  323. var $this = $(this),
  324. args = arguments,
  325. instance = $this.data('plugin_cascadingDropdown');
  326. if(typeof methodOrOptions === 'object' || !methodOrOptions) {
  327. return !instance && $this.data('plugin_cascadingDropdown', new CascadingDropdown(this, methodOrOptions));
  328. } else if(typeof methodOrOptions === 'string') {
  329. if(!instance) {
  330. $.error('Cannot call method ' + methodOrOptions + ' before init.');
  331. } else if(instance[methodOrOptions]) {
  332. return instance[methodOrOptions].apply(instance, Array.prototype.slice.call(args, 1))
  333. }
  334. } else {
  335. $.error('Method ' + methodOrOptions + ' does not exist in jQuery.cascadingDropdown');
  336. }
  337. };
  338. })(jQuery);