1/** 2 * @file 3 * Attaches behaviors for the Chosen module. 4 */ 5(function(Drupal, drupalSettings, once) { 6 'use strict'; 7 8 // Update Chosen elements when state has changed. 9 document.addEventListener('state:disabled', function(e) { 10 if (e.target && e.target.matches('select')) { 11 const event = new Event('chosen:updated', { bubbles: true, cancelable: true }); 12 e.target.dispatchEvent(event); 13 } 14 }); 15 16 Drupal.behaviors.chosen = { 17 18 settings: { 19 20 /** 21 * Completely ignores elements that match one of these selectors. 22 * 23 * Disabled on: 24 * - Field UI 25 * - WYSIWYG elements 26 * - Tabledrag weights 27 * - Elements that have opted-out of Chosen 28 * - Elements already processed by Chosen. 29 * 30 * @type {string} 31 */ 32 ignoreSelector: '#field-ui-field-storage-add-form select, #entity-form-display-edit-form select, #entity-view-display-edit-form select, .wysiwyg, .draggable select[name$="[weight]"], .draggable select[name$="[position]"], .locale-translate-filter-form select, .chosen-disable, .chosen-processed', 33 34 /** 35 * Explicit "opt-in" selector. 36 * 37 * @type {string} 38 */ 39 optedInSelector: 'select.chosen-enable', 40 41 /** 42 * The default selector, overridden by drupalSettings. 43 * 44 * @type {string} 45 */ 46 selector: 'select:visible', 47 48 /** 49 * Minimum options for single and multiple selects. 50 * These may be defined in drupalSettings.chosen. 51 */ 52 minimum_single: 0, 53 minimum_multiple: 0, 54 55 /** 56 * Other default options for Chosen. 57 */ 58 options: {}, 59 minimum_width: 0, 60 use_relative_width: false, 61 }, 62 63 /** 64 * Drupal attach behavior. 65 */ 66 attach: function(context, settings) { 67 this.settings = this.getSettings(settings); 68 const elements = this.getElements(context); 69 // Use Drupal's once function to process elements only once 70 once('chosen', elements).forEach(function(element) { 71 // If inside a Drupal dialog, resize the dialog on open & close event. 72 if (element.closest('#drupal-modal')) { 73 element.addEventListener('chosen:showing_dropdown', (e) => { 74 const nextElement = element.nextElementSibling; 75 if (nextElement) { 76 const chosenDrop = nextElement.querySelector('.chosen-drop'); 77 if (chosenDrop) { 78 chosenDrop.style.position = 'static'; 79 const event = new Event('dialogContentResize', { bubbles: true, cancelable: true }); 80 element.dispatchEvent(event); 81 } 82 } 83 }); 84 element.addEventListener('chosen:hiding_dropdown', (e) => { 85 const nextElement = element.nextElementSibling; 86 if (nextElement) { 87 const chosenDrop = nextElement.querySelector('.chosen-drop'); 88 if (chosenDrop) { 89 chosenDrop.style.position = ''; 90 const event = new Event('dialogContentResize', { bubbles: true, cancelable: true }); 91 element.dispatchEvent(event); 92 } 93 } 94 }); 95 } 96 this.createChosen(element); 97 }.bind(this)); 98 }, 99 100 /** 101 * Creates a Chosen instance for a specific element. 102 * 103 * @param {HTMLElement} element 104 * The element. 105 */ 106 createChosen: function(element) { 107 const options = this.getElementOptions(element); 108 element.chosen(options); 109 110 if (options.add_helper_buttons || element.hasAttribute('chosen_add_helper_buttons')) { 111 if (element.hasAttribute('multiple')) { 112 const allButton = document.createElement('button'); 113 allButton.type = 'button'; 114 allButton.className = 'button chosen-helper-btn'; 115 allButton.textContent = 'All'; 116 allButton.addEventListener('click', function() { 117 Array.from(element.options).forEach(function(option) { 118 option.selected = true; 119 }); 120 const event = new Event('chosen:updated', { bubbles: true, cancelable: true }); 121 element.dispatchEvent(event); 122 }); 123 124 const noneButton = document.createElement('button'); 125 noneButton.type = 'button'; 126 noneButton.className = 'button chosen-helper-btn'; 127 noneButton.textContent = 'None'; 128 noneButton.addEventListener('click', function() {
129 Array.from(element.options).forEach(function(option) { 130 option.selected = false; 131 }); 132 const event = new Event('chosen:updated', { bubbles: true, cancelable: true }); 133 element.dispatchEvent(event); 134 }); 135 136 element.parentNode.appendChild(allButton); 137 element.parentNode.appendChild(noneButton); 138 } 139 } 140 }, 141 142 /** 143 * Filter out elements that should not be converted into Chosen. 144 * 145 * @param {HTMLElement} element 146 * The element. 147 * 148 * @return {boolean} 149 * TRUE if the element should stay, FALSE otherwise. 150 */ 151 filterElements: function(element) { 152 // Remove elements that should be ignored completely. 153 if (element.matches(this.settings.ignoreSelector)) { 154 return false; 155 } 156 157 // Zero value means no minimum. 158 const minOptions = element.hasAttribute('multiple') ? this.settings.minimum_multiple : this.settings.minimum_single; 159 return !minOptions || element.querySelectorAll('option').length >= minOptions; 160 }, 161 162 /** 163 * Checks if the element is visible. 164 * 165 * @param {HTMLElement} el 166 * A DOM element to check visibility for. 167 * @return {boolean} 168 * True if the element is visible, false otherwise. 169 */ 170 isVisible: function(el) { 171 return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length); 172 }, 173 174 /** 175 * Parses the selector to handle :visible manually. 176 * 177 * @param {string} selector 178 * A CSS selector string that may contain :visible. 179 * @return {Array} 180 * An array of elements that match the selector and are visible (if :visible is present). 181 */ 182 getVisibleElements: function(context, selector) { 183 const selectors = selector.split(',').map(s => s.trim()); 184 let elements = []; 185 186 selectors.forEach(s => { 187 const hasVisibleFilter = s.includes(':visible'); 188 const cleanSelector = s.replace(':visible', '').trim(); 189 190 if (!cleanSelector) { 191 return; 192 } 193 194 let selectedElements; 195 try { 196 selectedElements = Array.from(context.querySelectorAll(cleanSelector)); 197 } catch (error) { 198 return; 199 } 200 201 if (hasVisibleFilter) { 202 selectedElements = selectedElements.filter(this.isVisible); 203 } 204 205 elements = elements.concat(selectedElements); 206 }); 207 208 return elements; 209 }, 210 211 /** 212 * Retrieves the elements that should be converted into Chosen instances. 213 * 214 * @param {HTMLElement|Document} context 215 * A DOM Element or Document to use as context. 216 * @param {string} [selector] 217 * A selector to use, defaults to the default selector in the settings. 218 */ 219 getElements: function(context, selector) { 220 context = context || document; 221 let elements = this.getVisibleElements(context, selector || this.settings.selector); 222 223 // Remove elements that should not be converted into Chosen. 224 elements = elements.filter(function(element) { 225 return this.filterElements(element); 226 }.bind(this)); 227 228 // Add elements that have explicitly opted in to Chosen. 229 const optedInElements = Array.from(context.querySelectorAll(this.settings.optedInSelector)); 230 231 // Combine elements and opted-in elements, avoiding duplicates. 232 optedInElements.forEach(function(element) { 233 if (!elements.includes(element)) { 234 elements.push(element); 235 } 236 }); 237 238 return elements; 239 }, 240 241 /** 242 * Retrieves options used to create a Chosen instance based on an element. 243 * 244 * @param {HTMLElement} element 245 * The element to process. 246 * 247 * @return {Object} 248 * The options object used to instantiate a Chosen instance with. 249 */ 250 getElementOptions: function(element) { 251 const options = Object.assign({}, this.settings.options); 252 let dimension; 253 let width; 254 255 // The width default option is considered the minimum width, so this 256 // must be evaluated for every option. 257 if (this.settings.minimum_width > 0) { 258 // Given we need to manage settings as both percentage and pixel widths, 259 // we need to handle width calculations separately. 260 if (this.settings.use_relative_width) { 261 dimension = '%'; 262 const elementWidth = element.getBoundingClientRect().width; 263 const parentWidth = element.parentNode.getBoundingClientRect().width; 264 width = ((elementWidth / parentWidth) * 100).toPrecision(5); 265 } else { 266 dimension = 'px'; 267 width = element.getBoundingClientRect().width; 268 } 269 270 if (width < this.settings.minimum_width) { 271 options.width = this.settings.minimum_width + dimension; 272 } else { 273 options.width = width + dimension; 274 } 275 } 276 277 // Some field widgets have cardinality, so we must respect that. 278 // @see \Drupal\chosen\ChosenFormRender::preRenderSelect() 279 const cardinality = element.dataset.cardinality; 280 if (element.hasAttribute('multiple') && cardinality) { 281 options.max_selected_options = parseInt(cardinality); 282 } 283 284 return options; 285 }, 286 287 /** 288 * Retrieves the settings passed from Drupal. 289 * 290 * @param {Object} [settings] 291 * Passed Drupal settings object, if any. 292 */ 293 getSettings: function(settings) { 294 // Deep merge the settings 295 return this.deepMerge({}
295, this.settings, (settings && settings.chosen) || drupalSettings.chosen || {}); 296 }, 297 298 /** 299 * Deep merges multiple objects into the first object. 300 * This function is used instead of Object.assign to handle nested objects. 301 * 302 * @param {Object} target 303 * The target object to merge properties into. 304 * @param {...Object} sources 305 * The source objects from which to copy properties. 306 * @return {Object} 307 * The target object. 308 */ 309 deepMerge: function(target, ...sources) { 310 if (!sources.length) return target; 311 const source = sources.shift(); 312 313 if (typeof target !== 'object' || typeof source !== 'object') { 314 return target; 315 } 316 317 for (const key in source) { 318 if (source.hasOwnProperty(key)) { 319 if (typeof source[key] === 'object') { 320 if (!target[key]) Object.assign(target, { [key]: {} }); 321 this.deepMerge(target[key], source[key]); 322 } else { 323 Object.assign(target, { [key]: source[key] }); 324 } 325 } 326 } 327 328 return this.deepMerge(target, ...sources); 329 }, 330 331 }; 332 333})(Drupal, drupalSettings, once);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.