1/** 2 * @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org> 3 * @license GNU General Public License version 2 or later; see LICENSE.txt 4 */ 5 6/** 7 * JField 'showon' class 8 */ 9class Showon { 10 /** 11 * Constructor 12 * 13 * @param {HTMLElement} cont Container element 14 */ 15 constructor(cont) { 16 const self = this; 17 this.container = cont || document; 18 this.fields = { 19 // origin-field-name: { 20 // origin: ['collection of all the trigger nodes'], 21 // targets: ['collection of nodes to be controlled control'] 22 // } 23 }; 24 this.showonFields = this.container.querySelectorAll('[data-showon]'); 25 // Populate the fields data 26 if (this.showonFields.length) { 27 // @todo refactor this, dry 28 this.showonFields.forEach(field => { 29 // Set up only once 30 if (field.hasAttribute('data-showon-initialised')) { 31 return; 32 } 33 field.setAttribute('data-showon-initialised', ''); 34 const jsondata = field.getAttribute('data-showon') || ''; 35 const showonData = JSON.parse(jsondata); 36 let localFields; 37 if (showonData.length) { 38 localFields = self.container.querySelectorAll(`[name="${showonData[0].field}"], [name="${showonData[0].field}[]"]`); 39 if (!this.fields[showonData[0].field]) { 40 this.fields[showonData[0].field] = { 41 origin: [], 42 targets: [] 43 }; 44 } 45 46 // Add trigger elements 47 localFields.forEach(cField => { 48 if (this.fields[showonData[0].field].origin.indexOf(cField) === -1) { 49 this.fields[showonData[0].field].origin.push(cField); 50 } 51 }); 52 53 // Add target elements 54 this.fields[showonData[0].field].targets.push(field); 55 56 // Data showon can have multiple values 57 if (showonData.length > 1) { 58 showonData.forEach((value, index) => { 59 if (index === 0) { 60 return; 61 } 62 localFields = self.container.querySelectorAll(`[name="${value.field}"], [name="${value.field}[]"]`); 63 if (!this.fields[showonData[0].field]) { 64 this.fields[showonData[0].field] = { 65 origin: [], 66 targets: [] 67 }; 68 } 69 70 // Add trigger elements 71 localFields.forEach(cField => { 72 if (this.fields[showonData[0].field].origin.indexOf(cField) === -1) { 73 this.fields[showonData[0].field].origin.push(cField); 74 } 75 }); 76 77 // Add target elements 78 if (this.fields[showonData[0].field].targets.indexOf(field) === -1) { 79 this.fields[showonData[0].field].targets.push(field); 80 } 81 }); 82 } 83 } 84 }); 85 86 // Do some binding 87 this.linkedOptions = this.linkedOptions.bind(this); 88 89 // Attach events to referenced element, to check condition on change and keyup 90 Object.keys(this.fields).forEach(key => { 91 if (this.fields[key].origin.length) { 92 this.fields[key].origin.forEach(elem => { 93 // Initialize the showon behaviour for the given HTMLElement 94 self.linkedOptions(key); 95 96 // Setup listeners 97 elem.addEventListener('change', () => { 98 self.linkedOptions(key); 99 }); 100 elem.addEventListener('keyup', () => { 101 self.linkedOptions(key); 102 }); 103 elem.addEventListener('click', () => { 104 self.linkedOptions(key); 105 }); 106 }); 107 } 108 }); 109 } 110 } 111 112 /** 113 * 114 * @param key 115 */ 116 linkedOptions(key) { 117 // Loop through the elements that need to be either shown or hidden 118 this.fields[key].targets.forEach(field => { 119 const elementShowonDatas = JSON.parse(field.getAttribute('data-showon')) || []; 120 let showfield = true; 121 let itemval; 122 123 // Check if target conditions are satisfied 124 elementShowonDatas.forEach((elementShowonData, index) => { 125 const condition = elementShowonData || {}; 126 condition.valid = 0; 127 128 // Test in each of the elements in the field array if condition is valid 129 this.fields[key].origin.forEach(originField => { 130 if (originField.name.replace('[]', '') !== elementShowonData.field) { 131 return; 132 } 133 const originId = originField.id; 134 135 // If checkbox or radio box the value is read from properties 136 if (originField.getAttribute('type') && ['checkbox', 'radio'].includes(originField.getAttribute('type').toLowerCase())) { 137 if (!originField.checked) { 138 // Unchecked fields will return a blank and so always match 139 // a != condition so we skip them 140 return; 141 } 142 itemval = document.getElementById(originId).value; 143 } else if (originField.nodeName === 'SELECT' && originField.hasAttribute('multiple')) { 144 itemval = Array.from(originField.querySelectorAll('option:checked')).map(el => el.value); 145 } else { 146 // Select lists, text-area etc. Note that multiple-select list returns 147 // an Array here so we can always treat 'itemval' as an array 148 itemval = document.getElementById(originId).value; 149 // Check data attribute data-global instead of value in <select> for use global 150 if (originField.tagName === 'SELECT') { 151 const selectedOption = document.getElementById(originId).selectedOptions[0]; 152 if (selectedOption && 'globalValue' in selectedOption.dataset) { 153 itemval = selectedOption.dataset.globalValue; 154 } 155 } 156 // A multi-select <select> $field will return null when no elements are 157 // selected so we need to define itemval accordingly 158 if (itemval === null && originField.tagName.toLowerCase() === 'select') { 159 itemval = []; 160 } 161 } 162 163 // Convert to array to allow multiple values in the field (e.g. type=list multiple) 164 // and normalize as string 165 if (!(typeof itemval === 'object')) { 166 itemval = JSON.parse(`["${itemval}"]`); 167 } 168 169 // Test if any of the values of the field exists in showon conditions 170 itemval.forEach(val => { 171 // ":" Equal to one or more of the values condition 172 if (condition.sign === '=' && condition.values.indexOf(val) !== -1) { 173 condition.valid = 1; 174 } 175 // "!:" Not equal to one or more of the values condition 176 if (condition.sign === '!=' && condition.values.indexOf(val) === -1) { 177 condition.valid = 1; 178 } 179 }); 180 }); 181 182 // Verify conditions 183 // First condition (no operator): current condition must be valid 184 if (condition.op === '') { 185 if (condition.valid === 0) { 186 showfield = false;
187 } 188 } else { 189 // Other conditions (if exists) 190 // AND operator: both the previous and current conditions must be valid 191 if (condition.op === 'AND' && condition.valid + elementShowonDatas[index - 1].valid < 2) { 192 showfield = false; 193 condition.valid = 0; 194 } 195 // OR operator: one of the previous and current conditions must be valid 196 if (condition.op === 'OR' && condition.valid + elementShowonDatas[index - 1].valid > 0) { 197 showfield = true; 198 condition.valid = 1; 199 } 200 } 201 }); 202 203 // If conditions are satisfied show the target field(s), else hide 204 if (field.tagName !== 'option') { 205 if (showfield) { 206 field.classList.remove('hidden'); 207 field.dispatchEvent(new CustomEvent('joomla:showon-show', { 208 bubbles: true 209 })); 210 } else { 211 field.classList.add('hidden'); 212 field.dispatchEvent(new CustomEvent('joomla:showon-hide', { 213 bubbles: true 214 })); 215 } 216 } else { 217 // @todo: If chosen or choices.js is active we should update them 218 field.disabled = !showfield; 219 } 220 }); 221 } 222} 223if (!window.Joomla) { 224 throw new Error('Joomla API is not properly initialized'); 225} 226// Provide a public API 227if (!Joomla.Showon) { 228 Joomla.Showon = { 229 initialise: container => new Showon(container) 230 }; 231} 232 233/** 234 * Initialize 'showon' feature at an initial page load 235 */ 236Joomla.Showon.initialise(document); 237 238/** 239 * Search for matching parents 240 * 241 * @param {HTMLElement} $child 242 * @param {String} selector 243 * @returns {HTMLElement[]} 244 */ 245const getMatchedParents = ($child, selector) => { 246 let $parent = $child; 247 let $matchingParent; 248 const parents = []; 249 while ($parent) { 250 $matchingParent = $parent.matches && $parent.matches(selector) ? $parent : null; 251 if ($matchingParent) { 252 parents.unshift($matchingParent); 253 } 254 $parent = $parent.parentNode; 255 } 256 return parents; 257}; 258 259/** 260 * Initialize 'showon' feature when part of the page was updated 261 */ 262document.addEventListener('joomla:updated', ({ 263 target 264}) => { 265 // Check is it subform, then wee need to fix some "showon" config 266 if (target.classList.contains('subform-repeatable-group')) { 267 const elements = target.querySelectorAll('[data-showon]'); 268 if (elements.length) { 269 const search = []; 270 const replace = []; 271 272 // Collect all parent groups of changed group 273 getMatchedParents(target, '.subform-repeatable-group').forEach($parent => { 274 search.push(new RegExp(`\\[${$parent.dataset.baseName}X\\]`, 'g')); 275 replace.push(`[${$parent.dataset.group}]`); 276 }); 277 278 // Fix showon field names in a current group 279 elements.forEach(element => { 280 let { 281 showon 282 } = element.dataset; 283 search.forEach((pattern, i) => { 284 showon = showon.replace(pattern, replace[i]); 285 }); 286 element.dataset.showon = showon; 287 }); 288 } 289 } 290 Joomla.Showon.initialise(target); 291});
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.