[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/media/system/js/fields/ -> joomla-field-fancy-select.js (source)

   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  });


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer