[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
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 * Fancy select field, which use Choices.js 8 * 9 * Example: 10 * <joomla-field-fancy-select ...attributes> 11 * <select>...</select> 12 * </joomla-field-fancy-select> 13 * 14 * Possible attributes: 15 * 16 * allow-custom Whether allow User to dynamically add a new value. 17 * new-item-prefix="" Prefix for a dynamically added value. 18 * 19 * remote-search Enable remote search. 20 * url="" Url for remote search. 21 * term-key="term" Variable key name for searched term, will be appended to Url. 22 * 23 * min-term-length="1" The minimum length a search value should be before choices are searched. 24 * placeholder="" The value of the inputs placeholder. 25 * search-placeholder="" The value of the search inputs placeholder. 26 * 27 * data-max-results="30" The maximum amount of search results to be displayed. 28 * data-max-render="30" The maximum amount of items to be rendered, critical for large lists. 29 */ 30 window.customElements.define('joomla-field-fancy-select', class extends HTMLElement { 31 // Attributes to monitor 32 get allowCustom() { 33 return this.hasAttribute('allow-custom'); 34 } 35 36 get remoteSearch() { 37 return this.hasAttribute('remote-search'); 38 } 39 40 get url() { 41 return this.getAttribute('url'); 42 } 43 44 get termKey() { 45 return this.getAttribute('term-key') || 'term'; 46 } 47 48 get minTermLength() { 49 return parseInt(this.getAttribute('min-term-length'), 10) || 1; 50 } 51 52 get newItemPrefix() { 53 return this.getAttribute('new-item-prefix') || ''; 54 } 55 56 get placeholder() { 57 return this.getAttribute('placeholder'); 58 } 59 60 get searchPlaceholder() { 61 return this.getAttribute('search-placeholder'); 62 } 63 64 get value() { 65 return this.choicesInstance.getValue(true); 66 } 67 68 set value($val) { 69 this.choicesInstance.setChoiceByValue($val); 70 } 71 /** 72 * Lifecycle 73 */ 74 75 76 constructor() { 77 super(); // Keycodes 78 79 this.keyCode = { 80 ENTER: 13 81 }; 82 83 if (!Joomla) { 84 throw new Error('Joomla API is not properly initiated'); 85 } 86 87 if (!window.Choices) { 88 throw new Error('JoomlaFieldFancySelect requires Choices.js to work'); 89 } 90 91 this.choicesCache = {}; 92 this.activeXHR = null; 93 this.choicesInstance = null; 94 this.isDisconnected = false; 95 } 96 /** 97 * Lifecycle 98 */ 99 100 101 connectedCallback() { 102 // Make sure Choices are loaded 103 if (window.Choices || document.readyState === 'complete') { 104 this.doConnect(); 105 } else { 106 const callback = () => { 107 this.doConnect(); 108 window.removeEventListener('load', callback); 109 }; 110 111 window.addEventListener('load', callback); 112 } 113 } 114 115 doConnect() { 116 // Get a <select> element 117 this.select = this.querySelector('select'); 118 119 if (!this.select) { 120 throw new Error('JoomlaFieldFancySelect requires <select> element to work'); 121 } // The element was already initialised previously and perhaps was detached from DOM 122 123 124 if (this.choicesInstance) { 125 if (this.isDisconnected) { 126 // Re init previous instance 127 this.choicesInstance.init(); 128 this.isDisconnected = false; 129 } 130 131 return; 132 } 133 134 this.isDisconnected = false; // Add placeholder option for multiple mode, 135 // Because it not supported as parameter by Choices for <select> https://github.com/jshjohnson/Choices#placeholder 136 137 if (this.select.multiple && this.placeholder) { 138 const option = document.createElement('option'); 139 option.setAttribute('placeholder', ''); 140 option.textContent = this.placeholder; 141 this.select.appendChild(option); 142 } // Init Choices 143 // eslint-disable-next-line no-undef 144 145 146 this.choicesInstance = new Choices(this.select, { 147 placeholderValue: this.placeholder, 148 searchPlaceholderValue: this.searchPlaceholder, 149 removeItemButton: true, 150 searchFloor: this.minTermLength, 151 searchResultLimit: parseInt(this.select.dataset.maxResults, 10) || 10, 152 renderChoiceLimit: parseInt(this.select.dataset.maxRender, 10) || -1, 153 shouldSort: false, 154 fuseOptions: { 155 threshold: 0.3 // Strict search 156 157 }, 158 noResultsText: Joomla.Text._('JGLOBAL_SELECT_NO_RESULTS_MATCH', 'No results found'), 159 noChoicesText: Joomla.Text._('JGLOBAL_SELECT_NO_RESULTS_MATCH', 'No results found'), 160 itemSelectText: Joomla.Text._('JGLOBAL_SELECT_PRESS_TO_SELECT', 'Press to select'), 161 // Redefine some classes 162 classNames: { 163 button: 'choices__button_joomla' // It is need because an original styling use unavailable Icon.svg file 164 165 } 166 }); // Handle typing of custom Term 167 168 if (this.allowCustom) { 169 // START Work around for issue https://github.com/joomla/joomla-cms/issues/29459 170 // The choices.js always auto-highlights the first element 171 // in the dropdown that not allow to add a custom Term. 172 // 173 // This workaround can be removed when choices.js 174 // will have an option that allow to disable it. 175 // eslint-disable-next-line no-underscore-dangle, prefer-destructuring 176 const _highlightChoice = this.choicesInstance._highlightChoice; // eslint-disable-next-line no-underscore-dangle 177 178 this.choicesInstance._highlightChoice = el => { 179 // Prevent auto-highlight of first element, if nothing actually highlighted 180 if (!el) return; // Call original highlighter 181 182 _highlightChoice.call(this.choicesInstance, el); 183 }; // Unhighlight any highlighted items, when mouse leave the dropdown 184 185 186 this.addEventListener('mouseleave', () => { 187 if (!this.choicesInstance.dropdown.isActive) { 188 return; 189 } 190 191 const highlighted = Array.from(this.choicesInstance.dropdown.element.querySelectorAll(`.$this.choicesInstance.config.classNames.highlightedState}`)); 192 highlighted.forEach(choice => { 193 choice.classList.remove(this.choicesInstance.config.classNames.highlightedState); 194 choice.setAttribute('aria-selected', 'false'); 195 }); // eslint-disable-next-line no-underscore-dangle 196 197 this.choicesInstance._highlightPosition = 0; 198 }); // END workaround for issue #29459 199 // Add custom term on ENTER keydown 200 201 this.addEventListener('keydown', event => { 202 if (event.keyCode !== this.keyCode.ENTER || event.target !== this.choicesInstance.input.element) { 203 return; 204 } 205 206 event.preventDefault(); // eslint-disable-next-line no-underscore-dangle 207 208 if (this.choicesInstance._highlightPosition || !event.target.value) { 209 return; 210 } // Make sure nothing is highlighted 211 212 213 const highlighted = this.choicesInstance.dropdown.element.querySelector(`.$this.choicesInstance.config.classNames.highlightedState}`); 214 215 if (highlighted) { 216 return; 217 } // Check if value already exist 218 219 220 const lowerValue = event.target.value.toLowerCase(); 221 let valueInCache = false; // Check if value in existing choices 222 223 this.choicesInstance.config.choices.some(choiceItem => { 224 if (choiceItem.value.toLowerCase() === lowerValue || choiceItem.label.toLowerCase() === lowerValue) { 225 valueInCache = choiceItem.value; 226 return true; 227 } 228 229 return false; 230 }); 231 232 if (valueInCache === false) { 233 // Check if value in cache 234 Object.keys(this.choicesCache).some(key => { 235 if (key.toLowerCase() === lowerValue || this.choicesCache[key].toLowerCase() === lowerValue) { 236 valueInCache = key; 237 return true; 238 } 239 240 return false; 241 }); 242 } // Make choice based on existing value 243 244 245 if (valueInCache !== false) { 246 this.choicesInstance.setChoiceByValue(valueInCache); 247 event.target.value = null; 248 this.choicesInstance.hideDropdown(); 249 return; 250 } // Create and add new 251 252 253 this.choicesInstance.setChoices([{ 254 value: this.newItemPrefix + event.target.value, 255 label: event.target.value, 256 selected: true, 257 customProperties: { 258 value: event.target.value // Store real value, just in case 259 260 } 261 }], 'value', 'label', false); 262 this.choicesCache[event.target.value] = event.target.value; 263 event.target.value = null; 264 this.choicesInstance.hideDropdown(); 265 }); 266 } // Handle remote search 267 268 269 if (this.remoteSearch && this.url) { 270 // Cache existing 271 this.choicesInstance.config.choices.forEach(choiceItem => { 272 this.choicesCache[choiceItem.value] = choiceItem.label; 273 }); 274 const lookupDelay = 300; 275 let lookupTimeout = null; 276 this.select.addEventListener('search', () => { 277 clearTimeout(lookupTimeout); 278 lookupTimeout = setTimeout(this.requestLookup.bind(this), lookupDelay); 279 }); 280 } 281 } 282 /** 283 * Lifecycle 284 */ 285 286 287 disconnectedCallback() { 288 // Destroy Choices instance, to unbind event listeners 289 if (this.choicesInstance) { 290 this.choicesInstance.destroy(); 291 this.isDisconnected = true; 292 } 293 294 if (this.activeXHR) { 295 this.activeXHR.abort(); 296 this.activeXHR = null; 297 } 298 } 299 300 requestLookup() { 301 let { 302 url 303 } = this; 304 url += url.indexOf('?') === -1 ? '?' : '&'; 305 url += `$encodeURIComponent(this.termKey)}=$encodeURIComponent(this.choicesInstance.input.value)}`; // Stop previous request if any 306 307 if (this.activeXHR) { 308 this.activeXHR.abort(); 309 } 310 311 this.activeXHR = Joomla.request({ 312 url, 313 onSuccess: response => { 314 this.activeXHR = null; 315 const items = response ? JSON.parse(response) : []; 316 317 if (!items.length) { 318 return; 319 } // Remove duplications 320 321 322 let item; // eslint-disable-next-line no-plusplus 323 324 for (let i = items.length - 1; i >= 0; i--) { 325 // The loop must be form the end !!! 326 item = items[i]; // eslint-disable-next-line prefer-template 327 328 item.value = '' + item.value; // Make sure the value is a string, choices.js expect a string. 329 330 if (this.choicesCache[item.value]) { 331 items.splice(i, 1); 332 } else { 333 this.choicesCache[item.value] = item.text; 334 } 335 } // Add new options to field, assume that each item is object, eg {value: "foo", text: "bar"} 336 337 338 if (items.length) { 339 this.choicesInstance.setChoices(items, 'value', 'text', false); 340 } 341 }, 342 onError: () => { 343 this.activeXHR = null; 344 } 345 }); 346 } 347 348 disableAllOptions() { 349 // Choices.js does not offer a public API for accessing the choices 350 // So we have to access the private store => don't eslint 351 // eslint-disable-next-line no-underscore-dangle 352 const { 353 choices 354 } = this.choicesInstance._store; 355 choices.forEach((elem, index) => { 356 choices[index].disabled = true; 357 choices[index].selected = false; 358 }); 359 this.choicesInstance.clearStore(); 360 this.choicesInstance.setChoices(choices, 'value', 'label', true); 361 } 362 363 enableAllOptions() { 364 // Choices.js does not offer a public API for accessing the choices 365 // So we have to access the private store => don't eslint 366 // eslint-disable-next-line no-underscore-dangle 367 const { 368 choices 369 } = this.choicesInstance._store; 370 const values = this.choicesInstance.getValue(true); 371 choices.forEach((elem, index) => { 372 choices[index].disabled = false; 373 }); 374 this.choicesInstance.clearStore(); 375 this.choicesInstance.setChoices(choices, 'value', 'label', true); 376 this.value = values; 377 } 378 379 disableByValue($val) { 380 // Choices.js does not offer a public API for accessing the choices 381 // So we have to access the private store => don't eslint 382 // eslint-disable-next-line no-underscore-dangle 383 const { 384 choices 385 } = this.choicesInstance._store; 386 const values = this.choicesInstance.getValue(true); 387 choices.forEach((elem, index) => { 388 if (elem.value === $val) { 389 choices[index].disabled = true; 390 choices[index].selected = false; 391 } 392 }); 393 const index = values.indexOf($val); 394 395 if (index > -1) { 396 values.slice(index, 1); 397 } 398 399 this.choicesInstance.clearStore(); 400 this.choicesInstance.setChoices(choices, 'value', 'label', true); 401 this.value = values; 402 } 403 404 enableByValue($val) { 405 // Choices.js does not offer a public API for accessing the choices 406 // So we have to access the private store => don't eslint 407 // eslint-disable-next-line no-underscore-dangle 408 const { 409 choices 410 } = this.choicesInstance._store; 411 const values = this.choicesInstance.getValue(true); 412 choices.forEach((elem, index) => { 413 if (elem.value === $val) { 414 choices[index].disabled = false; 415 } 416 }); 417 this.choicesInstance.clearStore(); 418 this.choicesInstance.setChoices(choices, 'value', 'label', true); 419 this.value = values; 420 } 421 422 });
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |