autocomplete.ftl

 1   <#--
 2   Macro: autocomplete
 3   Description: Generates an autocomplete dropdown
 4   Parameters:
 5   - id (string): the ID of the autocomplete search input element.
 6   - name (string): the name attribute of the hidden input element.
 7   - suggestionsUrl (string): the URL to fetch the suggestions from.
 8   - suggestionsPath (string, optional): the path to the suggestions list in the response object.
 9   - itemValueFieldName (string, optional): the value property of the suggestion object.
 10   - btnColor (string, optional): the color of the dropdown button.
 11   - btnSize (string, optional): the size of the dropdown button.
 12   - itemLabelFieldNames (array, optional): an array of suggestion object property names to display as the title.
 13   - itemTitleFieldNames (array, optional): an array of suggestion object property names to display as the description.
 14   - itemDescriptionFieldNames (array, optional): an array of suggestion object property names to display as the description.
 15   - itemTagsFieldNames (array, optional): an array of suggestion object property names to display as tags.
 16   - currentValue (string, optional): the current value of the autocomplete input.
 17   - currentLabel (string, optional): the current label displayed on the dropdown button.
 18   - required (boolean, optional): whether the input is required or not.
 19   - minimumInputLength (integer, optional): the minimum number of characters needed to trigger the search.
 20   - minimumInputLenghtLabel (string, optional): the label for the minimum input length information.
 21   - formLabel (string, optional): the label displayed on the dropdown button as default.
 22   - searchLabel (string, optional): the label displayed on the dropdown button.
 23   - emptyLabel (string, optional): the label displayed when there are no suggestions.
 24   -->
 25   <#macro autocomplete id name suggestionsUrl suggestionsPath="" itemValueFieldName="value" btnColor="light" btnSize="" itemLabelFieldNames="[]" itemTitleFieldNames=itemLabelFieldNames itemDescriptionFieldNames="[]" itemTagsFieldNames="[]" currentValue="" currentLabel="" required=false minimumInputLength=1 minimumInputLenghtLabel="#i18n{portal.util.labelMinimumSearchLenght}" formLabel=searchLabel searchLabel="#i18n{portal.util.labelSearch}" emptyLabel="#i18n{portal.util.labelNoItem}">
 26       <div class="dropdown">
 27           <input type="text" id="${id}-form-input" name="${name}" style="opacity: 0;width: 0;margin-left:20px;position:absolute;" aria-required="true" value="${currentValue}" <#if required>required=required</#if>>
 28           <button class="btn btn-${btnColor} border <#if btnSize!=''>btn-${btnSize}</#if>" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-expanded="false">
 29               <span id="${id}-dropdown-btn"><#if currentLabel!="">${currentLabel}<#else>${formLabel}</#if></span>
 30               <span id="${id}-remove" class="<#if currentValue=''>d-none</#if> badge bg-indigo ms-2 rounded-5 p-1 py-0"><i class="ti ti-x fs-5"></i></span>
 31               <span><i class="ti ti-chevron-down ps-1"></i></span>
 32           </button>
 33           <ul id="${id}-dropdown"class="dropdown-menu p-0" aria-labelledby="dropdownMenuButton">
 34               <li class="p-3 border-bottom">
 35                   <div class="input-icon mb-0">
 36                       <input type="text" placeholder=${searchLabel} class="form-control" id="${id}-search" <#if currentValue!="">${currentValue}</#if>>
 37                       <span class="input-icon-addon">
 38                           <i id="${id}-search-icon" class="ti ti-search"></i>
 39                       </span>
 40                   </div>
 41                   <#if minimumInputLength gt 1>
 42                       <div class="d-flex justify-content-end">
 43                           <small class="text-muted fst-italic">
 44                               <strong>${minimumInputLength}</strong> ${minimumInputLenghtLabel}
 45                           </small>
 46                       </div>
 47                   </#if>
 48               </li>
 49               <li>
 50                   <ul id="${id}-list-container" class="list-group list-group-flush overflow-auto bg-white" id="suggestions-list" style="max-height:15rem;">
 51                   </ul>
 52               </li>
 53           </ul>
 54       </div>
 55       <script type="module">
 56           import LuteceAutoComplete from './themes/shared/modules/luteceAutoComplete.js';
 57           //util functions
 58           const updateLoader = (add, remove) => (loader.classList.add(...add), loader.classList.remove(...remove));
 59           const createEl = (type, classNames = [], textContent = '') => (el => (el.classList.add(...classNames), el.textContent = textContent, el))(document.createElement(type));
 60   
 61           // Autocomplete
 62           
 63           // elements;
 64           const [formInput, dropdownBtn, removeBtn, loader, dropdown, resultList, searchInput] = ['form-input', 'dropdown-btn', 'remove', 'search-icon','dropdown','list-container','search'].map(suffix => document.getElementById(`${id}-`+suffix));
 65   
 66           // customise the template to display the suggestions
 67           const itemTemplate = suggestion => {
 68               const item = createEl('li', ['list-group-item', 'p-3']);
 69               item.setAttribute('data-value', suggestion.${itemValueFieldName});
 70               item.setAttribute('data-label', ${itemTitleFieldNames}.map(field => suggestion[field]).join(" "));
 71               if (suggestion.${itemValueFieldName} == formInput.value) item.classList.add('active');
 72               item.addEventListener('click', ({ currentTarget }) => {
 73                   dropdown.querySelectorAll(`.list-group-item.active`).forEach(el => el.classList.remove('active'));
 74                   currentTarget.classList.add('active');
 75                   formInput.value = currentTarget.getAttribute('data-value');
 76                   dropdownBtn.textContent = currentTarget.getAttribute('data-label');
 77                   removeBtn.classList.remove('d-none');
 78               });
 79               item.append(
 80                   createEl('h4', ['mb-0', 'fw-bolder'], ${itemTitleFieldNames}.map(field => suggestion[field]).join(" ")),
 81                   createEl('p', ['text-muted', 'mb-0'], ${itemDescriptionFieldNames}.map(field => suggestion[field]).join(" ")),
 82                   ${itemTagsFieldNames}.reduce((tags, field) => (tags.appendChild(createEl('span', ['badge', 'bg-blue-lt', 'me-1'], suggestion[field])), tags), createEl('span'))
 83               );
 84               return item;
 85           };
 86   
 87           // init the autocomplete
 88           const autoComplete = new LuteceAutoComplete(searchInput, resultList, "${suggestionsUrl}", "${suggestionsPath}", itemTemplate, ${minimumInputLength});
 89           // event listeners
 90           removeBtn.addEventListener('click', () => (dropdown.querySelectorAll(`.list-group-item.active`).forEach(el => el.classList.remove('active')), formInput.value = '', dropdownBtn.textContent = '${formLabel}', removeBtn.classList.add('d-none')));
 91           dropdownBtn.addEventListener('click', () => searchInput.focus());
 92           autoComplete.addEventListener('loading-error', () => updateLoader(['ti-zoom-exclamation', 'text-danger'], ['ti-loader-2', 'icon-rotate', 'ti-search']));
 93           autoComplete.addEventListener('loading-start', () => updateLoader(['ti-loader-2', 'icon-rotate'], ['ti-zoom-exclamation', 'text-danger', 'ti-search']));
 94           autoComplete.addEventListener('loading-end', () => {
 95               updateLoader(['ti-search'], ['ti-loader-2', 'icon-rotate']);
 96               if (resultList.childElementCount === 0) {
 97                   const emptyItem = createEl('li', ['list-group-item', 'p-3', 'text-muted','text-center'], `${emptyLabel}`);
 98                   resultList.appendChild(emptyItem);
 99               }
 100           });
 101       </script>
 102   </#macro>