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>