From 82b5cb0aa1defc42e94dc017364e699e618c0e95 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Fri, 8 Nov 2024 17:15:07 +0100 Subject: [PATCH] [CHANGE] Doctrine dropdown to autocomplete input field --- afat/forms.py | 45 +- .../afat/javascript/afat-fatlink-add.js | 25 + .../afat/javascript/afat-fatlink-add.min.js | 2 + .../javascript/afat-fatlink-add.min.js.map | 1 + .../bootstrap5-autocomplete/1.1.25/LICENSE | 21 + .../1.1.25/autocomplete.js | 1252 +++++++++++++++++ .../1.1.25/autocomplete.min.js | 4 + .../1.1.25/autocomplete.min.js.map | 7 + .../bootstrap5-autocomplete/1.1.25/readme.md | 167 +++ .../afat/bundles/afat-fatlink-add-js.html | 3 + .../afat/partials/form/fleet-doctrine.html | 7 + .../view/fatlinks/fatlinks-add-fatlink.html | 2 + afat/views/fatlinks.py | 3 +- 13 files changed, 1507 insertions(+), 32 deletions(-) create mode 100644 afat/static/afat/javascript/afat-fatlink-add.js create mode 100644 afat/static/afat/javascript/afat-fatlink-add.min.js create mode 100644 afat/static/afat/javascript/afat-fatlink-add.min.js.map create mode 100644 afat/static/afat/libs/bootstrap5-autocomplete/1.1.25/LICENSE create mode 100644 afat/static/afat/libs/bootstrap5-autocomplete/1.1.25/autocomplete.js create mode 100644 afat/static/afat/libs/bootstrap5-autocomplete/1.1.25/autocomplete.min.js create mode 100644 afat/static/afat/libs/bootstrap5-autocomplete/1.1.25/autocomplete.min.js.map create mode 100644 afat/static/afat/libs/bootstrap5-autocomplete/1.1.25/readme.md create mode 100644 afat/templates/afat/bundles/afat-fatlink-add-js.html create mode 100644 afat/templates/afat/partials/form/fleet-doctrine.html diff --git a/afat/forms.py b/afat/forms.py index 24daff08..4c68d21b 100644 --- a/afat/forms.py +++ b/afat/forms.py @@ -9,13 +9,8 @@ # Alliance Auth AFAT from afat.app_settings import AFAT_DEFAULT_FATLINK_EXPIRY_TIME -from afat.helper.fatlinks import get_doctrines from afat.models import Doctrine, FleetType, Setting -# The first line dropdown is blank -# Usage example: dropdown = form_choices_blank + [(o.id, o.name) for o in objects] -form_choices_blank = [("", "---------")] - def get_mandatory_form_label_text(text): """ @@ -54,23 +49,17 @@ class AFatEsiFatForm(forms.Form): label=_("Fleet type (optional)"), queryset=FleetType.objects.filter(is_enabled=True), ) - doctrine_esi = forms.ChoiceField( + doctrine_esi = forms.CharField( required=False, label=_("Doctrine (optional)"), - choices=(), + widget=forms.TextInput( + attrs={ + "data-datalist": "afat-fleet-doctrine-list", + "data-full-width": "true", + } + ), ) - def __init__(self, *args, **kwargs): - self.request = kwargs.pop("request", None) - super().__init__(*args, **kwargs) - - # Due to the dynamic nature of this dropdown field, we need to initialize it here. - # This is a workaround to avoid settings being cached and not updated when changed. - # This is also the reason we cannot use a ModelChoiceField here. - self.fields["doctrine_esi"].choices = form_choices_blank + [ - (str(o), str(o)) for o in get_doctrines() - ] - class AFatManualFatForm(forms.Form): """ @@ -111,10 +100,15 @@ class AFatClickFatForm(forms.Form): label=_("Fleet type (optional)"), queryset=FleetType.objects.filter(is_enabled=True), ) - doctrine = forms.ChoiceField( + doctrine = forms.CharField( required=False, label=_("Doctrine (optional)"), - choices=(), + widget=forms.TextInput( + attrs={ + "data-datalist": "afat-fleet-doctrine-list", + "data-full-width": "true", + } + ), ) duration = forms.IntegerField( required=True, @@ -124,17 +118,6 @@ class AFatClickFatForm(forms.Form): widget=forms.TextInput(attrs={"placeholder": _("Expiry time in minutes")}), ) - def __init__(self, *args, **kwargs): - self.request = kwargs.pop("request", None) - super().__init__(*args, **kwargs) - - # Due to the dynamic nature of this dropdown field, we need to initialize it here. - # This is a workaround to avoid settings being cached and not updated when changed. - # This is also the reason we cannot use a ModelChoiceField here. - self.fields["doctrine"].choices = form_choices_blank + [ - (str(o), str(o)) for o in get_doctrines() - ] - class FatLinkEditForm(forms.Form): """ diff --git a/afat/static/afat/javascript/afat-fatlink-add.js b/afat/static/afat/javascript/afat-fatlink-add.js new file mode 100644 index 00000000..f6410f0f --- /dev/null +++ b/afat/static/afat/javascript/afat-fatlink-add.js @@ -0,0 +1,25 @@ +import Autocomplete from '/static/afat/libs/bootstrap5-autocomplete/1.1.25/autocomplete.min.js'; + +$(document).ready(() => { + 'use strict'; + + const autoCompleteDropdown = (element) => { + const autoCompleteDoctrine = new Autocomplete( // eslint-disable-line no-unused-vars + element, + Object.assign( + {}, + { + onSelectItem: console.log, + }, + { + onRenderItem: (item, label) => { + return ` ${label}`; + }, + } + ) + ); + }; + + autoCompleteDropdown(document.getElementById('id_doctrine_esi')); + autoCompleteDropdown(document.getElementById('id_doctrine')); +}); diff --git a/afat/static/afat/javascript/afat-fatlink-add.min.js b/afat/static/afat/javascript/afat-fatlink-add.min.js new file mode 100644 index 00000000..59229421 --- /dev/null +++ b/afat/static/afat/javascript/afat-fatlink-add.min.js @@ -0,0 +1,2 @@ +import Autocomplete from"/static/afat/libs/bootstrap5-autocomplete/1.1.25/autocomplete.min.js";$(document).ready(()=>{const e=e=>{new Autocomplete(e,Object.assign({},{onSelectItem:console.log},{onRenderItem:(e,t)=>` ${t}`}))};e(document.getElementById("id_doctrine_esi")),e(document.getElementById("id_doctrine"))}); +//# sourceMappingURL=afat-fatlink-add.min.js.map \ No newline at end of file diff --git a/afat/static/afat/javascript/afat-fatlink-add.min.js.map b/afat/static/afat/javascript/afat-fatlink-add.min.js.map new file mode 100644 index 00000000..a94f1155 --- /dev/null +++ b/afat/static/afat/javascript/afat-fatlink-add.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["afat-fatlink-add.js"],"names":["Autocomplete","$","document","ready","autoCompleteDropdown","element","Object","assign","onSelectItem","console","log","onRenderItem","item","label","value","toLowerCase","getElementById"],"mappings":"OAAOA,iBAAkB,uEAEzBC,EAAEC,QAAQ,EAAEC,MAAM,KAGd,MAAMC,EAAuB,IACI,IAAIJ,aAC7BK,EACAC,OAAOC,OACH,GACA,CACIC,aAAcC,QAAQC,GAC1B,EACA,CACIC,aAAc,CAACC,EAAMC,2BACaD,EAAKE,MAAMC,YAAY,uBAAuBF,GAEpF,CACJ,CACJ,CACJ,EAEAT,EAAqBF,SAASc,eAAe,iBAAiB,CAAC,EAC/DZ,EAAqBF,SAASc,eAAe,aAAa,CAAC,CAC/D,CAAC"} \ No newline at end of file diff --git a/afat/static/afat/libs/bootstrap5-autocomplete/1.1.25/LICENSE b/afat/static/afat/libs/bootstrap5-autocomplete/1.1.25/LICENSE new file mode 100644 index 00000000..1017728a --- /dev/null +++ b/afat/static/afat/libs/bootstrap5-autocomplete/1.1.25/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Thomas Portelange + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/afat/static/afat/libs/bootstrap5-autocomplete/1.1.25/autocomplete.js b/afat/static/afat/libs/bootstrap5-autocomplete/1.1.25/autocomplete.js new file mode 100644 index 00000000..b1140e6a --- /dev/null +++ b/afat/static/afat/libs/bootstrap5-autocomplete/1.1.25/autocomplete.js @@ -0,0 +1,1252 @@ +/** + * Bootstrap 5 autocomplete + */ + +// #region config + +/** + * @callback RenderCallback + * @param {Object} item + * @param {String} label + * @param {Autocomplete} inst + * @returns {string} + */ + +/** + * @callback ItemCallback + * @param {Object} item + * @param {Autocomplete} inst + * @returns {void} + */ + +/** + * @callback ServerCallback + * @param {Response} response + * @param {Autocomplete} inst + * @returns {Promise} + */ + +/** + * @callback FetchCallback + * @param {Autocomplete} inst + * @returns {void} + */ + +/** + * @typedef Config + * @property {Boolean} showAllSuggestions Show all suggestions even if they don't match + * @property {Number} suggestionsThreshold Number of chars required to show suggestions + * @property {Number} maximumItems Maximum number of items to display + * @property {Boolean} autoselectFirst Always select the first item + * @property {Boolean} ignoreEnter Ignore enter if no items are selected (play nicely with autoselectFirst=0) + * @property {Boolean} updateOnSelect Update input value on selection (doesn't play nice with autoselectFirst) + * @property {Boolean} highlightTyped Highlight matched part of the label + * @property {String} highlightClass Class added to the mark label + * @property {Boolean} fullWidth Match the width on the input field + * @property {Boolean} fixed Use fixed positioning (solve overflow issues) + * @property {Boolean} fuzzy Fuzzy search + * @property {Boolean} startsWith Must start with the string. Defaults to false (it matches any position). + * @property {Boolean} fillIn Show fill in icon. + * @property {Boolean} preventBrowserAutocomplete Additional measures to prevent browser autocomplete + * @property {String} itemClass Applied to the dropdown item. Accepts space separated classes. + * @property {Array} activeClasses By default: ["bg-primary", "text-white"] + * @property {String} labelField Key for the label + * @property {String} valueField Key for the value + * @property {Array} searchFields Key for the search + * @property {String} queryParam Key for the query parameter for server + * @property {Array|Object} items An array of label/value objects or an object with key/values + * @property {Function} source A function that provides the list of items + * @property {Boolean} hiddenInput Create an hidden input which stores the valueField + * @property {String} hiddenValue Populate the initial hidden value. Mostly useful with liveServer. + * @property {String} clearControl Selector that will clear the input on click. + * @property {String} datalist The id of the source datalist + * @property {String} server Endpoint for data provider + * @property {String} serverMethod HTTP request method for data provider, default is GET + * @property {String|Object} serverParams Parameters to pass along to the server. You can specify a "related" key with the id of a related field. + * @property {String} serverDataKey By default: data + * @property {Object} fetchOptions Any other fetch options (https://developer.mozilla.org/en-US/docs/Web/API/fetch#syntax) + * @property {Boolean} liveServer Should the endpoint be called each time on input + * @property {Boolean} noCache Prevent caching by appending a timestamp + * @property {Number} debounceTime Debounce time for live server + * @property {String} notFoundMessage Display a no suggestions found message. Leave empty to disable + * @property {RenderCallback} onRenderItem Callback function that returns the label + * @property {ItemCallback} onSelectItem Callback function to call on selection + * @property {ServerCallback} onServerResponse Callback function to process server response. Must return a Promise + * @property {ItemCallback} onChange Callback function to call on change-event. Returns currently selected item if any + * @property {FetchCallback} onBeforeFetch Callback function before fetch + * @property {FetchCallback} onAfterFetch Callback function after fetch + */ + +/** + * @type {Config} + */ +const DEFAULTS = { + showAllSuggestions: false, + suggestionsThreshold: 1, + maximumItems: 0, + autoselectFirst: true, + ignoreEnter: false, + updateOnSelect: false, + highlightTyped: false, + highlightClass: "", + fullWidth: false, + fixed: false, + fuzzy: false, + startsWith: false, + fillIn: false, + preventBrowserAutocomplete: false, + itemClass: "", + activeClasses: ["bg-primary", "text-white"], + labelField: "label", + valueField: "value", + searchFields: ["label"], + queryParam: "query", + items: [], + source: null, + hiddenInput: false, + hiddenValue: "", + clearControl: "", + datalist: "", + server: "", + serverMethod: "GET", + serverParams: {}, + serverDataKey: "data", + fetchOptions: {}, + liveServer: false, + noCache: true, + debounceTime: 300, + notFoundMessage: "", + onRenderItem: (item, label, inst) => { + return label; + }, + onSelectItem: (item, inst) => {}, + onServerResponse: (response, inst) => { + return response.json(); + }, + onChange: (item, inst) => {}, + onBeforeFetch: (inst) => {}, + onAfterFetch: (inst) => {}, +}; + +// #endregion + +// #region constants + +const LOADING_CLASS = "is-loading"; +const ACTIVE_CLASS = "is-active"; +const SHOW_CLASS = "show"; +const NEXT = "next"; +const PREV = "prev"; + +const INSTANCE_MAP = new WeakMap(); +let counter = 0; +let activeCounter = 0; + +// #endregion + +// #region functions + +/** + * @param {Function} func + * @param {number} timeout + * @returns {Function} + */ +function debounce(func, timeout = 300) { + let timer; + return (...args) => { + clearTimeout(timer); + timer = setTimeout(() => { + //@ts-ignore + func.apply(this, args); + }, timeout); + }; +} + +/** + * @param {String} str + * @returns {String} + */ +function removeDiacritics(str) { + return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); +} + +/** + * @param {String|Number} str + * @returns {String} + */ +function normalize(str) { + if (!str) { + return ""; + } + return removeDiacritics(str.toString()).toLowerCase(); +} + +/** + * A simple fuzzy match algorithm that checks if chars are matched + * in order in the target string + * + * @param {String} str + * @param {String} lookup + * @returns {Boolean} + */ +function fuzzyMatch(str, lookup) { + if (str.indexOf(lookup) >= 0) { + return true; + } + let pos = 0; + for (let i = 0; i < lookup.length; i++) { + const c = lookup[i]; + if (c == " ") continue; + pos = str.indexOf(c, pos) + 1; + if (pos <= 0) { + return false; + } + } + return true; +} + +/** + * @param {HTMLElement} el + * @param {HTMLElement} newEl + * @returns {HTMLElement} + */ +function insertAfter(el, newEl) { + return el.parentNode.insertBefore(newEl, el.nextSibling); +} + +/** + * @param {string} html + * @returns {string} + */ +function decodeHtml(html) { + var txt = document.createElement("textarea"); + txt.innerHTML = html; + return txt.value; +} + +/** + * @param {HTMLElement} el + * @param {Object} attrs + */ +function attrs(el, attrs) { + for (const [k, v] of Object.entries(attrs)) { + el.setAttribute(k, v); + } +} + +/** + * Add a zero width join between chars + * @param {HTMLElement|Element} el + */ +function zwijit(el) { + //@ts-ignore + el.ariaLabel = el.innerText; + //@ts-ignore + el.innerHTML = el.innerText + .split("") + .map((char) => char + "‍") + .join(""); +} + +function nested(str, obj = "window") { + return str.split(".").reduce((r, p) => r[p], obj); +} + +// #endregion + +class Autocomplete { + /** + * @param {HTMLInputElement} el + * @param {Config|Object} config + */ + constructor(el, config = {}) { + if (!(el instanceof HTMLElement)) { + console.error("Invalid element", el); + return; + } + INSTANCE_MAP.set(el, this); + counter++; + activeCounter++; + this._searchInput = el; + + this._configure(config); + + // Private vars + this._preventInput = false; + this._keyboardNavigation = false; + this._searchFunc = debounce(() => { + this._loadFromServer(true); + }, this._config.debounceTime); + + // Create html + this._configureSearchInput(); + this._configureDropElement(); + + if (this._config.fixed) { + document.addEventListener("scroll", this, true); + window.addEventListener("resize", this); + } + + const clearControl = this._getClearControl(); + if (clearControl) { + clearControl.addEventListener("click", this); + } + + // Add listeners (remove then on dispose()). See handleEvent. + ["focus", "change", "blur", "input", "keydown"].forEach((type) => { + this._searchInput.addEventListener(type, this); + }); + ["mousemove", "mouseleave"].forEach((type) => { + this._dropElement.addEventListener(type, this); + }); + + this._fetchData(); + } + + // #region Core + + /** + * Attach to all elements matched by the selector + * @param {string} selector + * @param {Config|Object} config + */ + static init(selector = "input.autocomplete", config = {}) { + /** + * @type {NodeListOf} + */ + const nodes = document.querySelectorAll(selector); + nodes.forEach((el) => { + this.getOrCreateInstance(el, config); + }); + } + + /** + * @param {HTMLInputElement} el + */ + static getInstance(el) { + return INSTANCE_MAP.has(el) ? INSTANCE_MAP.get(el) : null; + } + + /** + * @param {HTMLInputElement} el + * @param {Object} config + */ + static getOrCreateInstance(el, config = {}) { + return this.getInstance(el) || new this(el, config); + } + + dispose() { + activeCounter--; + + ["focus", "change", "blur", "input", "keydown"].forEach((type) => { + this._searchInput.removeEventListener(type, this); + }); + ["mousemove", "mouseleave"].forEach((type) => { + this._dropElement.removeEventListener(type, this); + }); + + const clearControl = this._getClearControl(); + if (clearControl) { + clearControl.removeEventListener("click", this); + } + + // only remove if there are no more active elements + if (this._config.fixed && activeCounter <= 0) { + document.removeEventListener("scroll", this, true); + window.removeEventListener("resize", this); + } + + this._dropElement.parentElement.removeChild(this._dropElement); + + INSTANCE_MAP.delete(this._searchInput); + } + + _getClearControl() { + if (this._config.clearControl) { + return document.querySelector(this._config.clearControl); + } + } + + /** + * @link https://github.com/lifaon74/events-polyfill/issues/10 + * @link https://gist.github.com/WebReflection/ec9f6687842aa385477c4afca625bbf4#handling-events + * @param {Event} event + */ + handleEvent = (event) => { + // debounce scroll and resize + const debounced = ["scroll", "resize"]; + if (debounced.includes(event.type)) { + if (this._timer) window.cancelAnimationFrame(this._timer); + this._timer = window.requestAnimationFrame(() => { + this[`on${event.type}`](event); + }); + } else { + this[`on${event.type}`](event); + } + }; + + /** + * @param {Config|Object} config + */ + _configure(config = {}) { + this._config = Object.assign({}, DEFAULTS); + + // Handle options, using arguments first and data attr as override + const o = { ...config, ...this._searchInput.dataset }; + + // Allow 1/0, true/false as strings + const parseBool = (value) => ["true", "false", "1", "0", true, false].includes(value) && !!JSON.parse(value); + + // Typecast provided options based on defaults types + for (const [key, defaultValue] of Object.entries(DEFAULTS)) { + // Check for undefined keys + if (o[key] === void 0) { + continue; + } + const value = o[key]; + switch (typeof defaultValue) { + case "number": + this._config[key] = parseInt(value); + break; + case "boolean": + this._config[key] = parseBool(value); + break; + case "string": + this._config[key] = value.toString(); + break; + case "object": + // Arrays have a type object in js + if (Array.isArray(defaultValue)) { + if (typeof value === "string") { + const separator = value.includes("|") ? "|" : ","; + this._config[key] = value.split(separator); + } else { + this._config[key] = value; + } + } else { + this._config[key] = typeof value === "string" ? JSON.parse(value) : value; + } + break; + case "function": + this._config[key] = typeof value === "string" ? window[value] : value; + break; + default: + this._config[key] = value; + break; + } + } + } + + // #endregion + + // #region Html + + _configureSearchInput() { + this._searchInput.autocomplete = "off"; + this._searchInput.spellcheck = false; + // note: firefox doesn't support the properties so we use attributes + // @link https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-autocomplete + // @link https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-expanded + // use the aria-expanded state on the element with role combobox to communicate that the list is displayed. + attrs(this._searchInput, { + "aria-autocomplete": "list", + "aria-haspopup": "menu", + "aria-expanded": "false", + role: "combobox", + }); + + // Even with autocomplete "off" we can get suggestion from browser due to label + if (this._searchInput.id && this._config.preventBrowserAutocomplete) { + const label = document.querySelector(`[for="${this._searchInput.id}"]`); + if (label) { + zwijit(label); + } + } + + // Hidden input? + this._hiddenInput = null; + if (this._config.hiddenInput) { + this._hiddenInput = document.createElement("input"); + this._hiddenInput.type = "hidden"; + this._hiddenInput.value = this._config.hiddenValue; + this._hiddenInput.name = this._searchInput.name; + this._searchInput.name = "_" + this._searchInput.name; + insertAfter(this._searchInput, this._hiddenInput); + } + } + + _configureDropElement() { + this._dropElement = document.createElement("ul"); + this._dropElement.id = "ac-menu-" + counter; + this._dropElement.classList.add(...["dropdown-menu", "autocomplete-menu", "p-0"]); + this._dropElement.style.maxHeight = "280px"; + if (!this._config.fullWidth) { + this._dropElement.style.maxWidth = "360px"; + } + if (this._config.fixed) { + this._dropElement.style.position = "fixed"; + } + this._dropElement.style.overflowY = "auto"; + // Prevent scrolling the menu from scrolling the page + // @link https://developer.mozilla.org/en-US/docs/Web/CSS/overscroll-behavior + this._dropElement.style.overscrollBehavior = "contain"; + this._dropElement.style.textAlign = "unset"; // otherwise RTL is not good + + insertAfter(this._searchInput, this._dropElement); + // include aria-controls with the value of the id of the suggested list of values. + this._searchInput.setAttribute("aria-controls", this._dropElement.id); + } + + // #endregion + + // #region Events + + onclick(e) { + if (e.target.matches(this._config.clearControl)) { + this.clear(); + } + } + + oninput(e) { + if (this._preventInput) { + return; + } + // Input has changed, clear value + if (this._hiddenInput) { + this._hiddenInput.value = null; + } + this.showOrSearch(); + } + + onchange(e) { + const search = this._searchInput.value; + const item = Object.values(this._items).find((item) => item.label === search); + this._config.onChange(item, this); + } + + onblur(e) { + // Clicking on the scrollbar will trigger a blur in modals... + if (e.relatedTarget && e.relatedTarget.classList.contains("modal")) { + // Set focus back in + this._searchInput.focus(); + return; + } + setTimeout(() => { + this.hideSuggestions(); + }, 100); + } + + onfocus(e) { + this.showOrSearch(); + } + + /** + * keypress doesn't send arrow keys, so we use keydown + * @param {KeyboardEvent} e + */ + onkeydown(e) { + const key = e.keyCode || e.key; + switch (key) { + case 13: + case "Enter": + if (this.isDropdownVisible()) { + const selection = this.getSelection(); + if (selection) { + selection.click(); + } + if (selection || !this._config.ignoreEnter) { + e.preventDefault(); + } + } + break; + case 38: + case "ArrowUp": + e.preventDefault(); + this._keyboardNavigation = true; + this._moveSelection(PREV); + break; + case 40: + case "ArrowDown": + e.preventDefault(); + this._keyboardNavigation = true; + if (this.isDropdownVisible()) { + this._moveSelection(NEXT); + } else { + // show menu regardless of input length + this.showOrSearch(false); + } + break; + case 27: + case "Escape": + if (this.isDropdownVisible()) { + this._searchInput.focus(); + this.hideSuggestions(); + } + break; + } + } + + onmousemove(e) { + // Moving the mouse means no longer using keyboard + this._keyboardNavigation = false; + } + + onmouseleave(e) { + // Remove selection + this.removeSelection(); + } + + onscroll(e) { + this._positionMenu(); + } + + onresize(e) { + this._positionMenu(); + } + + // #endregion + + // #region Api + + /** + * @param {String} k + * @returns {Config} + */ + getConfig(k = null) { + if (k !== null) { + return this._config[k]; + } + return this._config; + } + + /** + * @param {String} k + * @param {*} v + */ + setConfig(k, v) { + this._config[k] = v; + } + + setData(src) { + this._items = {}; + this._addItems(src); + } + + enable() { + this._searchInput.setAttribute("disabled", ""); + } + + disable() { + this._searchInput.removeAttribute("disabled"); + } + + /** + * @returns {boolean} + */ + isDisabled() { + return this._searchInput.hasAttribute("disabled") || this._searchInput.disabled || this._searchInput.hasAttribute("readonly"); + } + + /** + * @returns {boolean} + */ + isDropdownVisible() { + return this._dropElement.classList.contains(SHOW_CLASS); + } + + clear() { + this._searchInput.value = ""; + if (this._hiddenInput) { + this._hiddenInput.value = ""; + } + } + + // #endregion + + // #region Selection management + + /** + * @returns {HTMLElement} + */ + getSelection() { + return this._dropElement.querySelector("a." + ACTIVE_CLASS); + } + + removeSelection() { + const selection = this.getSelection(); + if (selection) { + selection.classList.remove(...this._activeClasses()); + } + } + + /** + * @returns {Array} + */ + _activeClasses() { + return [...this._config.activeClasses, ...[ACTIVE_CLASS]]; + } + + /** + * @param {HTMLElement} li + * @returns {Boolean} + */ + _isItemEnabled(li) { + if (li.style.display === "none") { + return false; + } + const fc = li.firstElementChild; + return fc.tagName === "A" && !fc.classList.contains("disabled"); + } + + /** + * @param {String} dir + * @param {*|HTMLElement} sel + * @returns {HTMLElement} + */ + _moveSelection(dir = NEXT, sel = null) { + const active = this.getSelection(); + + // select first li + if (!active) { + // no active selection, cannot go back + if (dir === PREV) { + return sel; + } + // find first enabled item + if (!sel) { + sel = this._dropElement.firstChild; + while (sel && !this._isItemEnabled(sel)) { + sel = sel["nextSibling"]; + } + } + } else { + const sibling = dir === NEXT ? "nextSibling" : "previousSibling"; + + // Iterate over enabled li + sel = active.parentNode; + do { + sel = sel[sibling]; + } while (sel && !this._isItemEnabled(sel)); + + // We have a new selection + if (sel) { + // Change classes + active.classList.remove(...this._activeClasses()); + + // Scroll if necessary + if (dir === PREV) { + // Don't use scrollIntoView as it scrolls the whole window + sel.parentNode.scrollTop = sel.offsetTop - sel.parentNode.offsetTop; + } else { + // This is the equivalent of scrollIntoView(false) but only for parent node + if (sel.offsetTop > sel.parentNode.offsetHeight - sel.offsetHeight) { + sel.parentNode.scrollTop += sel.offsetHeight; + } + } + } else if (active) { + sel = active.parentElement; + } + } + + if (sel) { + const a = sel.querySelector("a"); + a.classList.add(...this._activeClasses()); + this._searchInput.setAttribute("aria-activedescendant", a.id); + if (this._config.updateOnSelect) { + this._searchInput.value = a.dataset.label; + } + } else { + this._searchInput.setAttribute("aria-activedescendant", ""); + } + return sel; + } + + // #endregion + + // #region Implementation + + /** + * Do we have enough input to show suggestions ? + * @returns {Boolean} + */ + _shouldShow() { + if (this.isDisabled()) { + return false; + } + return this._searchInput.value.length >= this._config.suggestionsThreshold; + } + + /** + * Show suggestions or load them + * @param {Boolean} check + */ + showOrSearch(check = true) { + if (check && !this._shouldShow()) { + this.hideSuggestions(); + return; + } + if (this._config.liveServer) { + this._searchFunc(); + } else if (this._config.source) { + this._config.source(this._searchInput.value, (items) => { + this.setData(items); + this._showSuggestions(); + }); + } else { + this._showSuggestions(); + } + } + + /** + * @param {String} name + * @returns {HTMLElement} + */ + _createGroup(name) { + const newChild = this._createLi(); + const newChildSpan = document.createElement("span"); + newChild.append(newChildSpan); + newChildSpan.classList.add(...["dropdown-header", "text-truncate"]); + newChildSpan.innerHTML = name; + return newChild; + } + + /** + * @param {String} lookup + * @param {Object} item + * @returns {HTMLElement} + */ + _createItem(lookup, item) { + let label = item.label; + + if (this._config.highlightTyped) { + const idx = normalize(label).indexOf(lookup); + if (idx !== -1) { + label = + label.substring(0, idx) + + `${label.substring(idx, idx + lookup.length)}` + + label.substring(idx + lookup.length, label.length); + } + } + + label = this._config.onRenderItem(item, label, this); + + const newChild = this._createLi(); + const newChildLink = document.createElement("a"); + newChild.append(newChildLink); + newChildLink.id = this._dropElement.id + "-" + this._dropElement.children.length; + newChildLink.classList.add(...["dropdown-item", "text-truncate"]); + if (this._config.itemClass) { + newChildLink.classList.add(...this._config.itemClass.split(" ")); + } + newChildLink.setAttribute("data-value", item.value); + newChildLink.setAttribute("data-label", item.label); + // Behave like a datalist (tab doesn't allow item selection) + // @link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist + newChildLink.setAttribute("tabindex", "-1"); + newChildLink.setAttribute("role", "menuitem"); + newChildLink.setAttribute("href", "#"); + newChildLink.innerHTML = label; + if (item.data) { + for (const [key, value] of Object.entries(item.data)) { + newChildLink.dataset[key] = value; + } + } + + if (this._config.fillIn) { + const fillIn = document.createElement("button"); + fillIn.type = "button"; // prevent submit + fillIn.classList.add(...["btn", "btn-link", "border-0"]); + fillIn.innerHTML = ` + + `; + newChild.append(fillIn); + newChild.classList.add(...["d-flex", "justify-content-between"]); + fillIn.addEventListener("click", (event) => { + this._searchInput.value = item.label; + this._searchInput.focus(); // focus back to keep editing + }); + } + + // Hover sets active item + newChildLink.addEventListener("mouseenter", (event) => { + // Don't trigger enter if using arrows + if (this._keyboardNavigation) { + return; + } + this.removeSelection(); + newChild.querySelector("a").classList.add(...this._activeClasses()); + }); + // Prevent searchInput losing focus and close the menu + newChildLink.addEventListener("mousedown", (event) => { + event.preventDefault(); + }); + // Apply value + newChildLink.addEventListener("click", (event) => { + event.preventDefault(); + + // Prevent input otherwise it might trigger autocomplete due to value change + this._preventInput = true; + this._searchInput.value = decodeHtml(item.label); + // Populate value in hidden input + if (this._hiddenInput) { + this._hiddenInput.value = item.value; + } + this._config.onSelectItem(item, this); + this.hideSuggestions(); + this._preventInput = false; + }); + + return newChild; + } + + /** + * Show drop menu with suggestions + */ + _showSuggestions() { + // It's not focused anymore + if (document.activeElement != this._searchInput) { + return; + } + const lookup = normalize(this._searchInput.value); + this._dropElement.innerHTML = ""; + + const keys = Object.keys(this._items); + let count = 0; + let firstItem = null; + + const groups = []; + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const entry = this._items[key]; + + // Check search length since we can trigger dropdown with arrow + const showAllSuggestions = this._config.showAllSuggestions || lookup.length === 0; + // Do we find a matching string or do we display immediately ? + let isMatched = lookup.length == 0 && this._config.suggestionsThreshold === 0; + if (!showAllSuggestions && lookup.length > 0) { + // match on any field + this._config.searchFields.forEach((sf) => { + const text = normalize(entry[sf]); + let found = false; + if (this._config.fuzzy) { + found = fuzzyMatch(text, lookup); + } else { + const idx = text.indexOf(lookup); + found = this._config.startsWith ? idx === 0 : idx >= 0; + } + if (found) { + isMatched = true; + } + }); + } + const selectFirst = isMatched || lookup.length === 0; + if (showAllSuggestions || isMatched) { + count++; + + // Group + if (entry.group && !groups.includes(entry.group)) { + const newItem = this._createGroup(entry.group); + this._dropElement.appendChild(newItem); + groups.push(entry.group); + } + + const newItem = this._createItem(lookup, entry); + // Only select as first item if its matching or no lookup + if (!firstItem && selectFirst) { + firstItem = newItem; + } + this._dropElement.appendChild(newItem); + if (this._config.maximumItems > 0 && count >= this._config.maximumItems) { + break; + } + } + } + + if (firstItem && this._config.autoselectFirst) { + this.removeSelection(); + this._moveSelection(NEXT, firstItem); + } + + if (count === 0) { + if (this._config.notFoundMessage) { + const newChild = this._createLi(); + newChild.innerHTML = `${this._config.notFoundMessage}`; + this._dropElement.appendChild(newChild); + this._showDropdown(); + } else { + // Remove dropdown if not found + this.hideSuggestions(); + } + } else { + // Or show it if necessary + this._showDropdown(); + } + } + + /** + * @returns {HTMLLIElement} + */ + _createLi() { + const newChild = document.createElement("li"); + newChild.setAttribute("role", "presentation"); + return newChild; + } + + /** + * Show and position dropdown + */ + _showDropdown() { + this._dropElement.classList.add(SHOW_CLASS); + // Register role when shown to avoid empty children issues + this._dropElement.setAttribute("role", "menu"); + attrs(this._searchInput, { + "aria-expanded": "true", + }); + this._positionMenu(); + } + + /** + * Show or hide suggestions + * @param {Boolean} check + */ + toggleSuggestions(check = true) { + if (this._dropElement.classList.contains(SHOW_CLASS)) { + this.hideSuggestions(); + } else { + this.showOrSearch(check); + } + } + + /** + * Hide the dropdown menu + */ + hideSuggestions() { + this._dropElement.classList.remove(SHOW_CLASS); + attrs(this._searchInput, { + "aria-expanded": "false", + }); + this.removeSelection(); + } + + /** + * @returns {HTMLInputElement} + */ + getInput() { + return this._searchInput; + } + + /** + * @returns {HTMLUListElement} + */ + getDropMenu() { + return this._dropElement; + } + + /** + * Position the dropdown menu + */ + _positionMenu() { + const styles = window.getComputedStyle(this._searchInput); + const bounds = this._searchInput.getBoundingClientRect(); + const isRTL = styles.direction === "rtl"; + const fullWidth = this._config.fullWidth; + const fixed = this._config.fixed; + + // Don't position left if not fixed since it may not work in all situations + // due to offsetParent margin or in tables + let left = null; + let top = null; + + if (fixed) { + left = bounds.x; + top = bounds.y + bounds.height; + + // Align end + if (isRTL && !fullWidth) { + left -= this._dropElement.offsetWidth - bounds.width; + } + } + + // Reset any height overflow adjustement + this._dropElement.style.transform = "unset"; + + // Use full holder width + if (fullWidth) { + this._dropElement.style.width = this._searchInput.offsetWidth + "px"; + } + + // Position element + if (left !== null) { + this._dropElement.style.left = left + "px"; + } + if (top !== null) { + this._dropElement.style.top = top + "px"; + } + + // Overflow height + const dropBounds = this._dropElement.getBoundingClientRect(); + const h = window.innerHeight; + + // We display above input if it overflows + if (dropBounds.y + dropBounds.height > h) { + // We need to add the offset twice + const topOffset = fullWidth ? bounds.height + 4 : bounds.height; + // In chrome, we need 100.1% to avoid blurry text + // @link https://stackoverflow.com/questions/32034574/font-looks-blurry-after-translate-in-chrome + this._dropElement.style.transform = "translateY(calc(-100.1% - " + topOffset + "px))"; + } + } + + _fetchData() { + this._items = {}; + + // From an array of items or an object + this._addItems(this._config.items); + + // From a datalist + const dl = this._config.datalist; + if (dl) { + const datalist = document.querySelector(`#${dl}`); + if (datalist) { + const items = Array.from(datalist.children).map((o) => { + const value = o.getAttribute("value") ?? o.innerHTML.toLowerCase(); + const label = o.innerHTML; + + return { + value, + label, + }; + }); + this._addItems(items); + } else { + console.error(`Datalist not found ${dl}`); + } + } + this._setHiddenVal(); + + // From an external source + if (this._config.server && !this._config.liveServer) { + this._loadFromServer(); + } + } + + _setHiddenVal() { + if (this._config.hiddenInput && !this._config.hiddenValue) { + for (const [value, entry] of Object.entries(this._items)) { + if (entry.label == this._searchInput.value) { + this._hiddenInput.value = value; + } + } + } + } + + /** + * @param {Array|Object} src An array of items or a value:label object + */ + _addItems(src) { + const keys = Object.keys(src); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const entry = src[key]; + + if (entry.group && entry.items) { + entry.items.forEach((e) => (e.group = entry.group)); + this._addItems(entry.items); + continue; + } + + const label = typeof entry === "string" ? entry : entry.label; + const item = typeof entry !== "object" ? {} : entry; + + // Normalize entry + item.label = entry[this._config.labelField] ?? label; + item.value = entry[this._config.valueField] ?? key; + + // Make sure we have a label + if (item.label) { + this._items[item.value] = item; + } + } + } + + /** + * @param {boolean} show + */ + _loadFromServer(show = false) { + if (this._abortController) { + this._abortController.abort(); + } + this._abortController = new AbortController(); + + // Read data params dynamically as well + let extraParams = this._searchInput.dataset.serverParams || {}; + if (typeof extraParams == "string") { + extraParams = JSON.parse(extraParams); + } + const params = Object.assign({}, this._config.serverParams, extraParams); + // Pass current value + params[this._config.queryParam] = this._searchInput.value; + // Prevent caching + if (this._config.noCache) { + params.t = Date.now(); + } + // We have a related field + if (params.related) { + /** + * @type {HTMLInputElement} + */ + //@ts-ignore + const input = document.getElementById(params.related); + if (input) { + params.related = input.value; + const inputName = input.getAttribute("name"); + if (inputName) { + params[inputName] = input.value; + } + } + } + + const urlParams = new URLSearchParams(params); + let url = this._config.server; + let fetchOptions = Object.assign(this._config.fetchOptions, { + method: this._config.serverMethod || "GET", + signal: this._abortController.signal, + }); + + if (fetchOptions.method === "POST") { + fetchOptions.body = urlParams; + } else { + url += "?" + urlParams.toString(); + } + + this._searchInput.classList.add(LOADING_CLASS); + this._config.onBeforeFetch(this); + + fetch(url, fetchOptions) + .then((r) => this._config.onServerResponse(r, this)) + .then((suggestions) => { + const data = nested(this._config.serverDataKey, suggestions) || suggestions; + this.setData(data); + this._setHiddenVal(); + this._abortController = null; + if (show) { + this._showSuggestions(); + } + }) + .catch((e) => { + // Current version of Firefox rejects the promise with a DOMException + if (e.name === "AbortError" || this._abortController.signal.aborted) { + return; + } + console.error(e); + }) + .finally((e) => { + this._searchInput.classList.remove(LOADING_CLASS); + this._config.onAfterFetch(this); + }); + } + + // #endregion +} + +export default Autocomplete; diff --git a/afat/static/afat/libs/bootstrap5-autocomplete/1.1.25/autocomplete.min.js b/afat/static/afat/libs/bootstrap5-autocomplete/1.1.25/autocomplete.min.js new file mode 100644 index 00000000..0d8e9ff0 --- /dev/null +++ b/afat/static/afat/libs/bootstrap5-autocomplete/1.1.25/autocomplete.min.js @@ -0,0 +1,4 @@ +var x={showAllSuggestions:!1,suggestionsThreshold:1,maximumItems:0,autoselectFirst:!0,ignoreEnter:!1,updateOnSelect:!1,highlightTyped:!1,highlightClass:"",fullWidth:!1,fixed:!1,fuzzy:!1,startsWith:!1,fillIn:!1,preventBrowserAutocomplete:!1,itemClass:"",activeClasses:["bg-primary","text-white"],labelField:"label",valueField:"value",searchFields:["label"],queryParam:"query",items:[],source:null,hiddenInput:!1,hiddenValue:"",clearControl:"",datalist:"",server:"",serverMethod:"GET",serverParams:{},serverDataKey:"data",fetchOptions:{},liveServer:!1,noCache:!0,debounceTime:300,notFoundMessage:"",onRenderItem:(o,t,e)=>t,onSelectItem:(o,t)=>{},onServerResponse:(o,t)=>o.json(),onChange:(o,t)=>{},onBeforeFetch:o=>{},onAfterFetch:o=>{}},C="is-loading",E="is-active",f="show",m="next",v="prev",p=new WeakMap,A=0,b=0;function O(o,t=300){let e;return(...s)=>{clearTimeout(e),e=setTimeout(()=>{o.apply(this,s)},t)}}function I(o){return o.normalize("NFD").replace(/[\u0300-\u036f]/g,"")}function y(o){return o?I(o.toString()).toLowerCase():""}function D(o,t){if(o.indexOf(t)>=0)return!0;let e=0;for(let s=0;st+"‍").join("")}function H(o,t="window"){return o.split(".").reduce((e,s)=>e[s],t)}var S=class{constructor(t,e={}){if(!(t instanceof HTMLElement)){console.error("Invalid element",t);return}p.set(t,this),A++,b++,this.s=t,this.L(e),this.c=!1,this.a=!1,this.C=O(()=>{this.p(!0)},this.e.debounceTime),this.E(),this.A(),this.e.fixed&&(document.addEventListener("scroll",this,!0),window.addEventListener("resize",this));let s=this.g();s&&s.addEventListener("click",this),["focus","change","blur","input","keydown"].forEach(i=>{this.s.addEventListener(i,this)}),["mousemove","mouseleave"].forEach(i=>{this.i.addEventListener(i,this)}),this.T()}static init(t="input.autocomplete",e={}){document.querySelectorAll(t).forEach(i=>{this.getOrCreateInstance(i,e)})}static getInstance(t){return p.has(t)?p.get(t):null}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,e)}dispose(){b--,["focus","change","blur","input","keydown"].forEach(e=>{this.s.removeEventListener(e,this)}),["mousemove","mouseleave"].forEach(e=>{this.i.removeEventListener(e,this)});let t=this.g();t&&t.removeEventListener("click",this),this.e.fixed&&b<=0&&(document.removeEventListener("scroll",this,!0),window.removeEventListener("resize",this)),this.i.parentElement.removeChild(this.i),p.delete(this.s)}g(){if(this.e.clearControl)return document.querySelector(this.e.clearControl)}handleEvent=t=>{["scroll","resize"].includes(t.type)?(this.v&&window.cancelAnimationFrame(this.v),this.v=window.requestAnimationFrame(()=>{this[`on${t.type}`](t)})):this[`on${t.type}`](t)};L(t={}){this.e=Object.assign({},x);let e={...t,...this.s.dataset},s=i=>["true","false","1","0",!0,!1].includes(i)&&!!JSON.parse(i);for(let[i,r]of Object.entries(x)){if(e[i]===void 0)continue;let n=e[i];switch(typeof r){case"number":this.e[i]=parseInt(n);break;case"boolean":this.e[i]=s(n);break;case"string":this.e[i]=n.toString();break;case"object":if(Array.isArray(r))if(typeof n=="string"){let a=n.includes("|")?"|":",";this.e[i]=n.split(a)}else this.e[i]=n;else this.e[i]=typeof n=="string"?JSON.parse(n):n;break;case"function":this.e[i]=typeof n=="string"?window[n]:n;break;default:this.e[i]=n;break}}}E(){if(this.s.autocomplete="off",this.s.spellcheck=!1,w(this.s,{"aria-autocomplete":"list","aria-haspopup":"menu","aria-expanded":"false",role:"combobox"}),this.s.id&&this.e.preventBrowserAutocomplete){let t=document.querySelector(`[for="${this.s.id}"]`);t&&F(t)}this.n=null,this.e.hiddenInput&&(this.n=document.createElement("input"),this.n.type="hidden",this.n.value=this.e.hiddenValue,this.n.name=this.s.name,this.s.name="_"+this.s.name,T(this.s,this.n))}A(){this.i=document.createElement("ul"),this.i.id="ac-menu-"+A,this.i.classList.add("dropdown-menu","autocomplete-menu","p-0"),this.i.style.maxHeight="280px",this.e.fullWidth||(this.i.style.maxWidth="360px"),this.e.fixed&&(this.i.style.position="fixed"),this.i.style.overflowY="auto",this.i.style.overscrollBehavior="contain",this.i.style.textAlign="unset",T(this.s,this.i),this.s.setAttribute("aria-controls",this.i.id)}onclick(t){t.target.matches(this.e.clearControl)&&this.clear()}oninput(t){this.c||(this.n&&(this.n.value=null),this.showOrSearch())}onchange(t){let e=this.s.value,s=Object.values(this.r).find(i=>i.label===e);this.e.onChange(s,this)}onblur(t){if(t.relatedTarget&&t.relatedTarget.classList.contains("modal")){this.s.focus();return}setTimeout(()=>{this.hideSuggestions()},100)}onfocus(t){this.showOrSearch()}onkeydown(t){switch(t.keyCode||t.key){case 13:case"Enter":if(this.isDropdownVisible()){let s=this.getSelection();s&&s.click(),(s||!this.e.ignoreEnter)&&t.preventDefault()}break;case 38:case"ArrowUp":t.preventDefault(),this.a=!0,this.u(v);break;case 40:case"ArrowDown":t.preventDefault(),this.a=!0,this.isDropdownVisible()?this.u(m):this.showOrSearch(!1);break;case 27:case"Escape":this.isDropdownVisible()&&(this.s.focus(),this.hideSuggestions());break}}onmousemove(t){this.a=!1}onmouseleave(t){this.removeSelection()}onscroll(t){this.d()}onresize(t){this.d()}getConfig(t=null){return t!==null?this.e[t]:this.e}setConfig(t,e){this.e[t]=e}setData(t){this.r={},this.h(t)}enable(){this.s.setAttribute("disabled","")}disable(){this.s.removeAttribute("disabled")}isDisabled(){return this.s.hasAttribute("disabled")||this.s.disabled||this.s.hasAttribute("readonly")}isDropdownVisible(){return this.i.classList.contains(f)}clear(){this.s.value="",this.n&&(this.n.value="")}getSelection(){return this.i.querySelector("a."+E)}removeSelection(){let t=this.getSelection();t&&t.classList.remove(...this.l())}l(){return[...this.e.activeClasses,E]}b(t){if(t.style.display==="none")return!1;let e=t.firstElementChild;return e.tagName==="A"&&!e.classList.contains("disabled")}u(t=m,e=null){let s=this.getSelection();if(s){let i=t===m?"nextSibling":"previousSibling";e=s.parentNode;do e=e[i];while(e&&!this.b(e));e?(s.classList.remove(...this.l()),t===v?e.parentNode.scrollTop=e.offsetTop-e.parentNode.offsetTop:e.offsetTop>e.parentNode.offsetHeight-e.offsetHeight&&(e.parentNode.scrollTop+=e.offsetHeight)):s&&(e=s.parentElement)}else{if(t===v)return e;if(!e)for(e=this.i.firstChild;e&&!this.b(e);)e=e.nextSibling}if(e){let i=e.querySelector("a");i.classList.add(...this.l()),this.s.setAttribute("aria-activedescendant",i.id),this.e.updateOnSelect&&(this.s.value=i.dataset.label)}else this.s.setAttribute("aria-activedescendant","");return e}k(){return this.isDisabled()?!1:this.s.value.length>=this.e.suggestionsThreshold}showOrSearch(t=!0){if(t&&!this.k()){this.hideSuggestions();return}this.e.liveServer?this.C():this.e.source?this.e.source(this.s.value,e=>{this.setData(e),this.f()}):this.f()}O(t){let e=this.m(),s=document.createElement("span");return e.append(s),s.classList.add("dropdown-header","text-truncate"),s.innerHTML=t,e}I(t,e){let s=e.label;if(this.e.highlightTyped){let n=y(s).indexOf(t);n!==-1&&(s=s.substring(0,n)+`${s.substring(n,n+t.length)}`+s.substring(n+t.length,s.length))}s=this.e.onRenderItem(e,s,this);let i=this.m(),r=document.createElement("a");if(i.append(r),r.id=this.i.id+"-"+this.i.children.length,r.classList.add("dropdown-item","text-truncate"),this.e.itemClass&&r.classList.add(...this.e.itemClass.split(" ")),r.setAttribute("data-value",e.value),r.setAttribute("data-label",e.label),r.setAttribute("tabindex","-1"),r.setAttribute("role","menuitem"),r.setAttribute("href","#"),r.innerHTML=s,e.data)for(let[n,a]of Object.entries(e.data))r.dataset[n]=a;if(this.e.fillIn){let n=document.createElement("button");n.type="button",n.classList.add("btn","btn-link","border-0"),n.innerHTML=` + + `,i.append(n),i.classList.add("d-flex","justify-content-between"),n.addEventListener("click",a=>{this.s.value=e.label,this.s.focus()})}return r.addEventListener("mouseenter",n=>{this.a||(this.removeSelection(),i.querySelector("a").classList.add(...this.l()))}),r.addEventListener("mousedown",n=>{n.preventDefault()}),r.addEventListener("click",n=>{n.preventDefault(),this.c=!0,this.s.value=M(e.label),this.n&&(this.n.value=e.value),this.e.onSelectItem(e,this),this.hideSuggestions(),this.c=!1}),i}f(){if(document.activeElement!=this.s)return;let t=y(this.s.value);this.i.innerHTML="";let e=Object.keys(this.r),s=0,i=null,r=[];for(let n=0;n0&&this.e.searchFields.forEach(u=>{let d=y(h[u]),g=!1;if(this.e.fuzzy)g=D(d,t);else{let L=d.indexOf(t);g=this.e.startsWith?L===0:L>=0}g&&(l=!0)});let k=l||t.length===0;if(c||l){if(s++,h.group&&!r.includes(h.group)){let d=this.O(h.group);this.i.appendChild(d),r.push(h.group)}let u=this.I(t,h);if(!i&&k&&(i=u),this.i.appendChild(u),this.e.maximumItems>0&&s>=this.e.maximumItems)break}}if(i&&this.e.autoselectFirst&&(this.removeSelection(),this.u(m,i)),s===0)if(this.e.notFoundMessage){let n=this.m();n.innerHTML=`${this.e.notFoundMessage}`,this.i.appendChild(n),this.w()}else this.hideSuggestions();else this.w()}m(){let t=document.createElement("li");return t.setAttribute("role","presentation"),t}w(){this.i.classList.add(f),this.i.setAttribute("role","menu"),w(this.s,{"aria-expanded":"true"}),this.d()}toggleSuggestions(t=!0){this.i.classList.contains(f)?this.hideSuggestions():this.showOrSearch(t)}hideSuggestions(){this.i.classList.remove(f),w(this.s,{"aria-expanded":"false"}),this.removeSelection()}getInput(){return this.s}getDropMenu(){return this.i}d(){let t=window.getComputedStyle(this.s),e=this.s.getBoundingClientRect(),s=t.direction==="rtl",i=this.e.fullWidth,r=this.e.fixed,n=null,a=null;r&&(n=e.x,a=e.y+e.height,s&&!i&&(n-=this.i.offsetWidth-e.width)),this.i.style.transform="unset",i&&(this.i.style.width=this.s.offsetWidth+"px"),n!==null&&(this.i.style.left=n+"px"),a!==null&&(this.i.style.top=a+"px");let h=this.i.getBoundingClientRect(),c=window.innerHeight;if(h.y+h.height>c){let l=i?e.height+4:e.height;this.i.style.transform="translateY(calc(-100.1% - "+l+"px))"}}T(){this.r={},this.h(this.e.items);let t=this.e.datalist;if(t){let e=document.querySelector(`#${t}`);if(e){let s=Array.from(e.children).map(i=>{let r=i.getAttribute("value")??i.innerHTML.toLowerCase(),n=i.innerHTML;return{value:r,label:n}});this.h(s)}else console.error(`Datalist not found ${t}`)}this.S(),this.e.server&&!this.e.liveServer&&this.p()}S(){if(this.e.hiddenInput&&!this.e.hiddenValue)for(let[t,e]of Object.entries(this.r))e.label==this.s.value&&(this.n.value=t)}h(t){let e=Object.keys(t);for(let s=0;sh.group=r.group),this.h(r.items);continue}let n=typeof r=="string"?r:r.label,a=typeof r!="object"?{}:r;a.label=r[this.e.labelField]??n,a.value=r[this.e.valueField]??i,a.label&&(this.r[a.value]=a)}}p(t=!1){this.o&&this.o.abort(),this.o=new AbortController;let e=this.s.dataset.serverParams||{};typeof e=="string"&&(e=JSON.parse(e));let s=Object.assign({},this.e.serverParams,e);if(s[this.e.queryParam]=this.s.value,this.e.noCache&&(s.t=Date.now()),s.related){let a=document.getElementById(s.related);if(a){s.related=a.value;let h=a.getAttribute("name");h&&(s[h]=a.value)}}let i=new URLSearchParams(s),r=this.e.server,n=Object.assign(this.e.fetchOptions,{method:this.e.serverMethod||"GET",signal:this.o.signal});n.method==="POST"?n.body=i:r+="?"+i.toString(),this.s.classList.add(C),this.e.onBeforeFetch(this),fetch(r,n).then(a=>this.e.onServerResponse(a,this)).then(a=>{let h=H(this.e.serverDataKey,a)||a;this.setData(h),this.S(),this.o=null,t&&this.f()}).catch(a=>{a.name==="AbortError"||this.o.signal.aborted||console.error(a)}).finally(a=>{this.s.classList.remove(C),this.e.onAfterFetch(this)})}},j=S;export{j as default}; +//# sourceMappingURL=autocomplete.min.js.map diff --git a/afat/static/afat/libs/bootstrap5-autocomplete/1.1.25/autocomplete.min.js.map b/afat/static/afat/libs/bootstrap5-autocomplete/1.1.25/autocomplete.min.js.map new file mode 100644 index 00000000..0b3a5d0d --- /dev/null +++ b/afat/static/afat/libs/bootstrap5-autocomplete/1.1.25/autocomplete.min.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["autocomplete.js"], + "sourcesContent": ["/**\n * Bootstrap 5 autocomplete\n */\n\n// #region config\n\n/**\n * @callback RenderCallback\n * @param {Object} item\n * @param {String} label\n * @param {Autocomplete} inst\n * @returns {string}\n */\n\n/**\n * @callback ItemCallback\n * @param {Object} item\n * @param {Autocomplete} inst\n * @returns {void}\n */\n\n/**\n * @callback ServerCallback\n * @param {Response} response\n * @param {Autocomplete} inst\n * @returns {Promise}\n */\n\n/**\n * @callback FetchCallback\n * @param {Autocomplete} inst\n * @returns {void}\n */\n\n/**\n * @typedef Config\n * @property {Boolean} showAllSuggestions Show all suggestions even if they don't match\n * @property {Number} suggestionsThreshold Number of chars required to show suggestions\n * @property {Number} maximumItems Maximum number of items to display\n * @property {Boolean} autoselectFirst Always select the first item\n * @property {Boolean} ignoreEnter Ignore enter if no items are selected (play nicely with autoselectFirst=0)\n * @property {Boolean} updateOnSelect Update input value on selection (doesn't play nice with autoselectFirst)\n * @property {Boolean} highlightTyped Highlight matched part of the label\n * @property {String} highlightClass Class added to the mark label\n * @property {Boolean} fullWidth Match the width on the input field\n * @property {Boolean} fixed Use fixed positioning (solve overflow issues)\n * @property {Boolean} fuzzy Fuzzy search\n * @property {Boolean} startsWith Must start with the string. Defaults to false (it matches any position).\n * @property {Boolean} fillIn Show fill in icon.\n * @property {Boolean} preventBrowserAutocomplete Additional measures to prevent browser autocomplete\n * @property {String} itemClass Applied to the dropdown item. Accepts space separated classes.\n * @property {Array} activeClasses By default: [\"bg-primary\", \"text-white\"]\n * @property {String} labelField Key for the label\n * @property {String} valueField Key for the value\n * @property {Array} searchFields Key for the search\n * @property {String} queryParam Key for the query parameter for server\n * @property {Array|Object} items An array of label/value objects or an object with key/values\n * @property {Function} source A function that provides the list of items\n * @property {Boolean} hiddenInput Create an hidden input which stores the valueField\n * @property {String} hiddenValue Populate the initial hidden value. Mostly useful with liveServer.\n * @property {String} clearControl Selector that will clear the input on click.\n * @property {String} datalist The id of the source datalist\n * @property {String} server Endpoint for data provider\n * @property {String} serverMethod HTTP request method for data provider, default is GET\n * @property {String|Object} serverParams Parameters to pass along to the server. You can specify a \"related\" key with the id of a related field.\n * @property {String} serverDataKey By default: data\n * @property {Object} fetchOptions Any other fetch options (https://developer.mozilla.org/en-US/docs/Web/API/fetch#syntax)\n * @property {Boolean} liveServer Should the endpoint be called each time on input\n * @property {Boolean} noCache Prevent caching by appending a timestamp\n * @property {Number} debounceTime Debounce time for live server\n * @property {String} notFoundMessage Display a no suggestions found message. Leave empty to disable\n * @property {RenderCallback} onRenderItem Callback function that returns the label\n * @property {ItemCallback} onSelectItem Callback function to call on selection\n * @property {ServerCallback} onServerResponse Callback function to process server response. Must return a Promise\n * @property {ItemCallback} onChange Callback function to call on change-event. Returns currently selected item if any\n * @property {FetchCallback} onBeforeFetch Callback function before fetch\n * @property {FetchCallback} onAfterFetch Callback function after fetch\n */\n\n/**\n * @type {Config}\n */\nconst DEFAULTS = {\n showAllSuggestions: false,\n suggestionsThreshold: 1,\n maximumItems: 0,\n autoselectFirst: true,\n ignoreEnter: false,\n updateOnSelect: false,\n highlightTyped: false,\n highlightClass: \"\",\n fullWidth: false,\n fixed: false,\n fuzzy: false,\n startsWith: false,\n fillIn: false,\n preventBrowserAutocomplete: false,\n itemClass: \"\",\n activeClasses: [\"bg-primary\", \"text-white\"],\n labelField: \"label\",\n valueField: \"value\",\n searchFields: [\"label\"],\n queryParam: \"query\",\n items: [],\n source: null,\n hiddenInput: false,\n hiddenValue: \"\",\n clearControl: \"\",\n datalist: \"\",\n server: \"\",\n serverMethod: \"GET\",\n serverParams: {},\n serverDataKey: \"data\",\n fetchOptions: {},\n liveServer: false,\n noCache: true,\n debounceTime: 300,\n notFoundMessage: \"\",\n onRenderItem: (item, label, inst) => {\n return label;\n },\n onSelectItem: (item, inst) => {},\n onServerResponse: (response, inst) => {\n return response.json();\n },\n onChange: (item, inst) => {},\n onBeforeFetch: (inst) => {},\n onAfterFetch: (inst) => {},\n};\n\n// #endregion\n\n// #region constants\n\nconst LOADING_CLASS = \"is-loading\";\nconst ACTIVE_CLASS = \"is-active\";\nconst SHOW_CLASS = \"show\";\nconst NEXT = \"next\";\nconst PREV = \"prev\";\n\nconst INSTANCE_MAP = new WeakMap();\nlet counter = 0;\nlet activeCounter = 0;\n\n// #endregion\n\n// #region functions\n\n/**\n * @param {Function} func\n * @param {number} timeout\n * @returns {Function}\n */\nfunction debounce(func, timeout = 300) {\n let timer;\n return (...args) => {\n clearTimeout(timer);\n timer = setTimeout(() => {\n //@ts-ignore\n func.apply(this, args);\n }, timeout);\n };\n}\n\n/**\n * @param {String} str\n * @returns {String}\n */\nfunction removeDiacritics(str) {\n return str.normalize(\"NFD\").replace(/[\\u0300-\\u036f]/g, \"\");\n}\n\n/**\n * @param {String|Number} str\n * @returns {String}\n */\nfunction normalize(str) {\n if (!str) {\n return \"\";\n }\n return removeDiacritics(str.toString()).toLowerCase();\n}\n\n/**\n * A simple fuzzy match algorithm that checks if chars are matched\n * in order in the target string\n *\n * @param {String} str\n * @param {String} lookup\n * @returns {Boolean}\n */\nfunction fuzzyMatch(str, lookup) {\n if (str.indexOf(lookup) >= 0) {\n return true;\n }\n let pos = 0;\n for (let i = 0; i < lookup.length; i++) {\n const c = lookup[i];\n if (c == \" \") continue;\n pos = str.indexOf(c, pos) + 1;\n if (pos <= 0) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * @param {HTMLElement} el\n * @param {HTMLElement} newEl\n * @returns {HTMLElement}\n */\nfunction insertAfter(el, newEl) {\n return el.parentNode.insertBefore(newEl, el.nextSibling);\n}\n\n/**\n * @param {string} html\n * @returns {string}\n */\nfunction decodeHtml(html) {\n var txt = document.createElement(\"textarea\");\n txt.innerHTML = html;\n return txt.value;\n}\n\n/**\n * @param {HTMLElement} el\n * @param {Object} attrs\n */\nfunction attrs(el, attrs) {\n for (const [k, v] of Object.entries(attrs)) {\n el.setAttribute(k, v);\n }\n}\n\n/**\n * Add a zero width join between chars\n * @param {HTMLElement|Element} el\n */\nfunction zwijit(el) {\n //@ts-ignore\n el.ariaLabel = el.innerText;\n //@ts-ignore\n el.innerHTML = el.innerText\n .split(\"\")\n .map((char) => char + \"‍\")\n .join(\"\");\n}\n\nfunction nested(str, obj = \"window\") {\n return str.split(\".\").reduce((r, p) => r[p], obj);\n}\n\n// #endregion\n\nclass Autocomplete {\n /**\n * @param {HTMLInputElement} el\n * @param {Config|Object} config\n */\n constructor(el, config = {}) {\n if (!(el instanceof HTMLElement)) {\n console.error(\"Invalid element\", el);\n return;\n }\n INSTANCE_MAP.set(el, this);\n counter++;\n activeCounter++;\n this._searchInput = el;\n\n this._configure(config);\n\n // Private vars\n this._preventInput = false;\n this._keyboardNavigation = false;\n this._searchFunc = debounce(() => {\n this._loadFromServer(true);\n }, this._config.debounceTime);\n\n // Create html\n this._configureSearchInput();\n this._configureDropElement();\n\n if (this._config.fixed) {\n document.addEventListener(\"scroll\", this, true);\n window.addEventListener(\"resize\", this);\n }\n\n const clearControl = this._getClearControl();\n if (clearControl) {\n clearControl.addEventListener(\"click\", this);\n }\n\n // Add listeners (remove then on dispose()). See handleEvent.\n [\"focus\", \"change\", \"blur\", \"input\", \"keydown\"].forEach((type) => {\n this._searchInput.addEventListener(type, this);\n });\n [\"mousemove\", \"mouseleave\"].forEach((type) => {\n this._dropElement.addEventListener(type, this);\n });\n\n this._fetchData();\n }\n\n // #region Core\n\n /**\n * Attach to all elements matched by the selector\n * @param {string} selector\n * @param {Config|Object} config\n */\n static init(selector = \"input.autocomplete\", config = {}) {\n /**\n * @type {NodeListOf}\n */\n const nodes = document.querySelectorAll(selector);\n nodes.forEach((el) => {\n this.getOrCreateInstance(el, config);\n });\n }\n\n /**\n * @param {HTMLInputElement} el\n */\n static getInstance(el) {\n return INSTANCE_MAP.has(el) ? INSTANCE_MAP.get(el) : null;\n }\n\n /**\n * @param {HTMLInputElement} el\n * @param {Object} config\n */\n static getOrCreateInstance(el, config = {}) {\n return this.getInstance(el) || new this(el, config);\n }\n\n dispose() {\n activeCounter--;\n\n [\"focus\", \"change\", \"blur\", \"input\", \"keydown\"].forEach((type) => {\n this._searchInput.removeEventListener(type, this);\n });\n [\"mousemove\", \"mouseleave\"].forEach((type) => {\n this._dropElement.removeEventListener(type, this);\n });\n\n const clearControl = this._getClearControl();\n if (clearControl) {\n clearControl.removeEventListener(\"click\", this);\n }\n\n // only remove if there are no more active elements\n if (this._config.fixed && activeCounter <= 0) {\n document.removeEventListener(\"scroll\", this, true);\n window.removeEventListener(\"resize\", this);\n }\n\n this._dropElement.parentElement.removeChild(this._dropElement);\n\n INSTANCE_MAP.delete(this._searchInput);\n }\n\n _getClearControl() {\n if (this._config.clearControl) {\n return document.querySelector(this._config.clearControl);\n }\n }\n\n /**\n * @link https://github.com/lifaon74/events-polyfill/issues/10\n * @link https://gist.github.com/WebReflection/ec9f6687842aa385477c4afca625bbf4#handling-events\n * @param {Event} event\n */\n handleEvent = (event) => {\n // debounce scroll and resize\n const debounced = [\"scroll\", \"resize\"];\n if (debounced.includes(event.type)) {\n if (this._timer) window.cancelAnimationFrame(this._timer);\n this._timer = window.requestAnimationFrame(() => {\n this[`on${event.type}`](event);\n });\n } else {\n this[`on${event.type}`](event);\n }\n };\n\n /**\n * @param {Config|Object} config\n */\n _configure(config = {}) {\n this._config = Object.assign({}, DEFAULTS);\n\n // Handle options, using arguments first and data attr as override\n const o = { ...config, ...this._searchInput.dataset };\n\n // Allow 1/0, true/false as strings\n const parseBool = (value) => [\"true\", \"false\", \"1\", \"0\", true, false].includes(value) && !!JSON.parse(value);\n\n // Typecast provided options based on defaults types\n for (const [key, defaultValue] of Object.entries(DEFAULTS)) {\n // Check for undefined keys\n if (o[key] === void 0) {\n continue;\n }\n const value = o[key];\n switch (typeof defaultValue) {\n case \"number\":\n this._config[key] = parseInt(value);\n break;\n case \"boolean\":\n this._config[key] = parseBool(value);\n break;\n case \"string\":\n this._config[key] = value.toString();\n break;\n case \"object\":\n // Arrays have a type object in js\n if (Array.isArray(defaultValue)) {\n if (typeof value === \"string\") {\n const separator = value.includes(\"|\") ? \"|\" : \",\";\n this._config[key] = value.split(separator);\n } else {\n this._config[key] = value;\n }\n } else {\n this._config[key] = typeof value === \"string\" ? JSON.parse(value) : value;\n }\n break;\n case \"function\":\n this._config[key] = typeof value === \"string\" ? window[value] : value;\n break;\n default:\n this._config[key] = value;\n break;\n }\n }\n }\n\n // #endregion\n\n // #region Html\n\n _configureSearchInput() {\n this._searchInput.autocomplete = \"off\";\n this._searchInput.spellcheck = false;\n // note: firefox doesn't support the properties so we use attributes\n // @link https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-autocomplete\n // @link https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-expanded\n // use the aria-expanded state on the element with role combobox to communicate that the list is displayed.\n attrs(this._searchInput, {\n \"aria-autocomplete\": \"list\",\n \"aria-haspopup\": \"menu\",\n \"aria-expanded\": \"false\",\n role: \"combobox\",\n });\n\n // Even with autocomplete \"off\" we can get suggestion from browser due to label\n if (this._searchInput.id && this._config.preventBrowserAutocomplete) {\n const label = document.querySelector(`[for=\"${this._searchInput.id}\"]`);\n if (label) {\n zwijit(label);\n }\n }\n\n // Hidden input?\n this._hiddenInput = null;\n if (this._config.hiddenInput) {\n this._hiddenInput = document.createElement(\"input\");\n this._hiddenInput.type = \"hidden\";\n this._hiddenInput.value = this._config.hiddenValue;\n this._hiddenInput.name = this._searchInput.name;\n this._searchInput.name = \"_\" + this._searchInput.name;\n insertAfter(this._searchInput, this._hiddenInput);\n }\n }\n\n _configureDropElement() {\n this._dropElement = document.createElement(\"ul\");\n this._dropElement.id = \"ac-menu-\" + counter;\n this._dropElement.classList.add(...[\"dropdown-menu\", \"autocomplete-menu\", \"p-0\"]);\n this._dropElement.style.maxHeight = \"280px\";\n if (!this._config.fullWidth) {\n this._dropElement.style.maxWidth = \"360px\";\n }\n if (this._config.fixed) {\n this._dropElement.style.position = \"fixed\";\n }\n this._dropElement.style.overflowY = \"auto\";\n // Prevent scrolling the menu from scrolling the page\n // @link https://developer.mozilla.org/en-US/docs/Web/CSS/overscroll-behavior\n this._dropElement.style.overscrollBehavior = \"contain\";\n this._dropElement.style.textAlign = \"unset\"; // otherwise RTL is not good\n\n insertAfter(this._searchInput, this._dropElement);\n // include aria-controls with the value of the id of the suggested list of values.\n this._searchInput.setAttribute(\"aria-controls\", this._dropElement.id);\n }\n\n // #endregion\n\n // #region Events\n\n onclick(e) {\n if (e.target.matches(this._config.clearControl)) {\n this.clear();\n }\n }\n\n oninput(e) {\n if (this._preventInput) {\n return;\n }\n // Input has changed, clear value\n if (this._hiddenInput) {\n this._hiddenInput.value = null;\n }\n this.showOrSearch();\n }\n\n onchange(e) {\n const search = this._searchInput.value;\n const item = Object.values(this._items).find((item) => item.label === search);\n this._config.onChange(item, this);\n }\n\n onblur(e) {\n // Clicking on the scrollbar will trigger a blur in modals...\n if (e.relatedTarget && e.relatedTarget.classList.contains(\"modal\")) {\n // Set focus back in\n this._searchInput.focus();\n return;\n }\n setTimeout(() => {\n this.hideSuggestions();\n }, 100);\n }\n\n onfocus(e) {\n this.showOrSearch();\n }\n\n /**\n * keypress doesn't send arrow keys, so we use keydown\n * @param {KeyboardEvent} e\n */\n onkeydown(e) {\n const key = e.keyCode || e.key;\n switch (key) {\n case 13:\n case \"Enter\":\n if (this.isDropdownVisible()) {\n const selection = this.getSelection();\n if (selection) {\n selection.click();\n }\n if (selection || !this._config.ignoreEnter) {\n e.preventDefault();\n }\n }\n break;\n case 38:\n case \"ArrowUp\":\n e.preventDefault();\n this._keyboardNavigation = true;\n this._moveSelection(PREV);\n break;\n case 40:\n case \"ArrowDown\":\n e.preventDefault();\n this._keyboardNavigation = true;\n if (this.isDropdownVisible()) {\n this._moveSelection(NEXT);\n } else {\n // show menu regardless of input length\n this.showOrSearch(false);\n }\n break;\n case 27:\n case \"Escape\":\n if (this.isDropdownVisible()) {\n this._searchInput.focus();\n this.hideSuggestions();\n }\n break;\n }\n }\n\n onmousemove(e) {\n // Moving the mouse means no longer using keyboard\n this._keyboardNavigation = false;\n }\n\n onmouseleave(e) {\n // Remove selection\n this.removeSelection();\n }\n\n onscroll(e) {\n this._positionMenu();\n }\n\n onresize(e) {\n this._positionMenu();\n }\n\n // #endregion\n\n // #region Api\n\n /**\n * @param {String} k\n * @returns {Config}\n */\n getConfig(k = null) {\n if (k !== null) {\n return this._config[k];\n }\n return this._config;\n }\n\n /**\n * @param {String} k\n * @param {*} v\n */\n setConfig(k, v) {\n this._config[k] = v;\n }\n\n setData(src) {\n this._items = {};\n this._addItems(src);\n }\n\n enable() {\n this._searchInput.setAttribute(\"disabled\", \"\");\n }\n\n disable() {\n this._searchInput.removeAttribute(\"disabled\");\n }\n\n /**\n * @returns {boolean}\n */\n isDisabled() {\n return this._searchInput.hasAttribute(\"disabled\") || this._searchInput.disabled || this._searchInput.hasAttribute(\"readonly\");\n }\n\n /**\n * @returns {boolean}\n */\n isDropdownVisible() {\n return this._dropElement.classList.contains(SHOW_CLASS);\n }\n\n clear() {\n this._searchInput.value = \"\";\n if (this._hiddenInput) {\n this._hiddenInput.value = \"\";\n }\n }\n\n // #endregion\n\n // #region Selection management\n\n /**\n * @returns {HTMLElement}\n */\n getSelection() {\n return this._dropElement.querySelector(\"a.\" + ACTIVE_CLASS);\n }\n\n removeSelection() {\n const selection = this.getSelection();\n if (selection) {\n selection.classList.remove(...this._activeClasses());\n }\n }\n\n /**\n * @returns {Array}\n */\n _activeClasses() {\n return [...this._config.activeClasses, ...[ACTIVE_CLASS]];\n }\n\n /**\n * @param {HTMLElement} li\n * @returns {Boolean}\n */\n _isItemEnabled(li) {\n if (li.style.display === \"none\") {\n return false;\n }\n const fc = li.firstElementChild;\n return fc.tagName === \"A\" && !fc.classList.contains(\"disabled\");\n }\n\n /**\n * @param {String} dir\n * @param {*|HTMLElement} sel\n * @returns {HTMLElement}\n */\n _moveSelection(dir = NEXT, sel = null) {\n const active = this.getSelection();\n\n // select first li\n if (!active) {\n // no active selection, cannot go back\n if (dir === PREV) {\n return sel;\n }\n // find first enabled item\n if (!sel) {\n sel = this._dropElement.firstChild;\n while (sel && !this._isItemEnabled(sel)) {\n sel = sel[\"nextSibling\"];\n }\n }\n } else {\n const sibling = dir === NEXT ? \"nextSibling\" : \"previousSibling\";\n\n // Iterate over enabled li\n sel = active.parentNode;\n do {\n sel = sel[sibling];\n } while (sel && !this._isItemEnabled(sel));\n\n // We have a new selection\n if (sel) {\n // Change classes\n active.classList.remove(...this._activeClasses());\n\n // Scroll if necessary\n if (dir === PREV) {\n // Don't use scrollIntoView as it scrolls the whole window\n sel.parentNode.scrollTop = sel.offsetTop - sel.parentNode.offsetTop;\n } else {\n // This is the equivalent of scrollIntoView(false) but only for parent node\n if (sel.offsetTop > sel.parentNode.offsetHeight - sel.offsetHeight) {\n sel.parentNode.scrollTop += sel.offsetHeight;\n }\n }\n } else if (active) {\n sel = active.parentElement;\n }\n }\n\n if (sel) {\n const a = sel.querySelector(\"a\");\n a.classList.add(...this._activeClasses());\n this._searchInput.setAttribute(\"aria-activedescendant\", a.id);\n if (this._config.updateOnSelect) {\n this._searchInput.value = a.dataset.label;\n }\n } else {\n this._searchInput.setAttribute(\"aria-activedescendant\", \"\");\n }\n return sel;\n }\n\n // #endregion\n\n // #region Implementation\n\n /**\n * Do we have enough input to show suggestions ?\n * @returns {Boolean}\n */\n _shouldShow() {\n if (this.isDisabled()) {\n return false;\n }\n return this._searchInput.value.length >= this._config.suggestionsThreshold;\n }\n\n /**\n * Show suggestions or load them\n * @param {Boolean} check\n */\n showOrSearch(check = true) {\n if (check && !this._shouldShow()) {\n this.hideSuggestions();\n return;\n }\n if (this._config.liveServer) {\n this._searchFunc();\n } else if (this._config.source) {\n this._config.source(this._searchInput.value, (items) => {\n this.setData(items);\n this._showSuggestions();\n });\n } else {\n this._showSuggestions();\n }\n }\n\n /**\n * @param {String} name\n * @returns {HTMLElement}\n */\n _createGroup(name) {\n const newChild = this._createLi();\n const newChildSpan = document.createElement(\"span\");\n newChild.append(newChildSpan);\n newChildSpan.classList.add(...[\"dropdown-header\", \"text-truncate\"]);\n newChildSpan.innerHTML = name;\n return newChild;\n }\n\n /**\n * @param {String} lookup\n * @param {Object} item\n * @returns {HTMLElement}\n */\n _createItem(lookup, item) {\n let label = item.label;\n\n if (this._config.highlightTyped) {\n const idx = normalize(label).indexOf(lookup);\n if (idx !== -1) {\n label =\n label.substring(0, idx) +\n `${label.substring(idx, idx + lookup.length)}` +\n label.substring(idx + lookup.length, label.length);\n }\n }\n\n label = this._config.onRenderItem(item, label, this);\n\n const newChild = this._createLi();\n const newChildLink = document.createElement(\"a\");\n newChild.append(newChildLink);\n newChildLink.id = this._dropElement.id + \"-\" + this._dropElement.children.length;\n newChildLink.classList.add(...[\"dropdown-item\", \"text-truncate\"]);\n if (this._config.itemClass) {\n newChildLink.classList.add(...this._config.itemClass.split(\" \"));\n }\n newChildLink.setAttribute(\"data-value\", item.value);\n newChildLink.setAttribute(\"data-label\", item.label);\n // Behave like a datalist (tab doesn't allow item selection)\n // @link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist\n newChildLink.setAttribute(\"tabindex\", \"-1\");\n newChildLink.setAttribute(\"role\", \"menuitem\");\n newChildLink.setAttribute(\"href\", \"#\");\n newChildLink.innerHTML = label;\n if (item.data) {\n for (const [key, value] of Object.entries(item.data)) {\n newChildLink.dataset[key] = value;\n }\n }\n\n if (this._config.fillIn) {\n const fillIn = document.createElement(\"button\");\n fillIn.type = \"button\"; // prevent submit\n fillIn.classList.add(...[\"btn\", \"btn-link\", \"border-0\"]);\n fillIn.innerHTML = `\n \n `;\n newChild.append(fillIn);\n newChild.classList.add(...[\"d-flex\", \"justify-content-between\"]);\n fillIn.addEventListener(\"click\", (event) => {\n this._searchInput.value = item.label;\n this._searchInput.focus(); // focus back to keep editing\n });\n }\n\n // Hover sets active item\n newChildLink.addEventListener(\"mouseenter\", (event) => {\n // Don't trigger enter if using arrows\n if (this._keyboardNavigation) {\n return;\n }\n this.removeSelection();\n newChild.querySelector(\"a\").classList.add(...this._activeClasses());\n });\n // Prevent searchInput losing focus and close the menu\n newChildLink.addEventListener(\"mousedown\", (event) => {\n event.preventDefault();\n });\n // Apply value\n newChildLink.addEventListener(\"click\", (event) => {\n event.preventDefault();\n\n // Prevent input otherwise it might trigger autocomplete due to value change\n this._preventInput = true;\n this._searchInput.value = decodeHtml(item.label);\n // Populate value in hidden input\n if (this._hiddenInput) {\n this._hiddenInput.value = item.value;\n }\n this._config.onSelectItem(item, this);\n this.hideSuggestions();\n this._preventInput = false;\n });\n\n return newChild;\n }\n\n /**\n * Show drop menu with suggestions\n */\n _showSuggestions() {\n // It's not focused anymore\n if (document.activeElement != this._searchInput) {\n return;\n }\n const lookup = normalize(this._searchInput.value);\n this._dropElement.innerHTML = \"\";\n\n const keys = Object.keys(this._items);\n let count = 0;\n let firstItem = null;\n\n const groups = [];\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n const entry = this._items[key];\n\n // Check search length since we can trigger dropdown with arrow\n const showAllSuggestions = this._config.showAllSuggestions || lookup.length === 0;\n // Do we find a matching string or do we display immediately ?\n let isMatched = lookup.length == 0 && this._config.suggestionsThreshold === 0;\n if (!showAllSuggestions && lookup.length > 0) {\n // match on any field\n this._config.searchFields.forEach((sf) => {\n const text = normalize(entry[sf]);\n let found = false;\n if (this._config.fuzzy) {\n found = fuzzyMatch(text, lookup);\n } else {\n const idx = text.indexOf(lookup);\n found = this._config.startsWith ? idx === 0 : idx >= 0;\n }\n if (found) {\n isMatched = true;\n }\n });\n }\n const selectFirst = isMatched || lookup.length === 0;\n if (showAllSuggestions || isMatched) {\n count++;\n\n // Group\n if (entry.group && !groups.includes(entry.group)) {\n const newItem = this._createGroup(entry.group);\n this._dropElement.appendChild(newItem);\n groups.push(entry.group);\n }\n\n const newItem = this._createItem(lookup, entry);\n // Only select as first item if its matching or no lookup\n if (!firstItem && selectFirst) {\n firstItem = newItem;\n }\n this._dropElement.appendChild(newItem);\n if (this._config.maximumItems > 0 && count >= this._config.maximumItems) {\n break;\n }\n }\n }\n\n if (firstItem && this._config.autoselectFirst) {\n this.removeSelection();\n this._moveSelection(NEXT, firstItem);\n }\n\n if (count === 0) {\n if (this._config.notFoundMessage) {\n const newChild = this._createLi();\n newChild.innerHTML = `${this._config.notFoundMessage}`;\n this._dropElement.appendChild(newChild);\n this._showDropdown();\n } else {\n // Remove dropdown if not found\n this.hideSuggestions();\n }\n } else {\n // Or show it if necessary\n this._showDropdown();\n }\n }\n\n /**\n * @returns {HTMLLIElement}\n */\n _createLi() {\n const newChild = document.createElement(\"li\");\n newChild.setAttribute(\"role\", \"presentation\");\n return newChild;\n }\n\n /**\n * Show and position dropdown\n */\n _showDropdown() {\n this._dropElement.classList.add(SHOW_CLASS);\n // Register role when shown to avoid empty children issues\n this._dropElement.setAttribute(\"role\", \"menu\");\n attrs(this._searchInput, {\n \"aria-expanded\": \"true\",\n });\n this._positionMenu();\n }\n\n /**\n * Show or hide suggestions\n * @param {Boolean} check\n */\n toggleSuggestions(check = true) {\n if (this._dropElement.classList.contains(SHOW_CLASS)) {\n this.hideSuggestions();\n } else {\n this.showOrSearch(check);\n }\n }\n\n /**\n * Hide the dropdown menu\n */\n hideSuggestions() {\n this._dropElement.classList.remove(SHOW_CLASS);\n attrs(this._searchInput, {\n \"aria-expanded\": \"false\",\n });\n this.removeSelection();\n }\n\n /**\n * @returns {HTMLInputElement}\n */\n getInput() {\n return this._searchInput;\n }\n\n /**\n * @returns {HTMLUListElement}\n */\n getDropMenu() {\n return this._dropElement;\n }\n\n /**\n * Position the dropdown menu\n */\n _positionMenu() {\n const styles = window.getComputedStyle(this._searchInput);\n const bounds = this._searchInput.getBoundingClientRect();\n const isRTL = styles.direction === \"rtl\";\n const fullWidth = this._config.fullWidth;\n const fixed = this._config.fixed;\n\n // Don't position left if not fixed since it may not work in all situations\n // due to offsetParent margin or in tables\n let left = null;\n let top = null;\n\n if (fixed) {\n left = bounds.x;\n top = bounds.y + bounds.height;\n\n // Align end\n if (isRTL && !fullWidth) {\n left -= this._dropElement.offsetWidth - bounds.width;\n }\n }\n\n // Reset any height overflow adjustement\n this._dropElement.style.transform = \"unset\";\n\n // Use full holder width\n if (fullWidth) {\n this._dropElement.style.width = this._searchInput.offsetWidth + \"px\";\n }\n\n // Position element\n if (left !== null) {\n this._dropElement.style.left = left + \"px\";\n }\n if (top !== null) {\n this._dropElement.style.top = top + \"px\";\n }\n\n // Overflow height\n const dropBounds = this._dropElement.getBoundingClientRect();\n const h = window.innerHeight;\n\n // We display above input if it overflows\n if (dropBounds.y + dropBounds.height > h) {\n // We need to add the offset twice\n const topOffset = fullWidth ? bounds.height + 4 : bounds.height;\n // In chrome, we need 100.1% to avoid blurry text\n // @link https://stackoverflow.com/questions/32034574/font-looks-blurry-after-translate-in-chrome\n this._dropElement.style.transform = \"translateY(calc(-100.1% - \" + topOffset + \"px))\";\n }\n }\n\n _fetchData() {\n this._items = {};\n\n // From an array of items or an object\n this._addItems(this._config.items);\n\n // From a datalist\n const dl = this._config.datalist;\n if (dl) {\n const datalist = document.querySelector(`#${dl}`);\n if (datalist) {\n const items = Array.from(datalist.children).map((o) => {\n const value = o.getAttribute(\"value\") ?? o.innerHTML.toLowerCase();\n const label = o.innerHTML;\n\n return {\n value,\n label,\n };\n });\n this._addItems(items);\n } else {\n console.error(`Datalist not found ${dl}`);\n }\n }\n this._setHiddenVal();\n\n // From an external source\n if (this._config.server && !this._config.liveServer) {\n this._loadFromServer();\n }\n }\n\n _setHiddenVal() {\n if (this._config.hiddenInput && !this._config.hiddenValue) {\n for (const [value, entry] of Object.entries(this._items)) {\n if (entry.label == this._searchInput.value) {\n this._hiddenInput.value = value;\n }\n }\n }\n }\n\n /**\n * @param {Array|Object} src An array of items or a value:label object\n */\n _addItems(src) {\n const keys = Object.keys(src);\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n const entry = src[key];\n\n if (entry.group && entry.items) {\n entry.items.forEach((e) => (e.group = entry.group));\n this._addItems(entry.items);\n continue;\n }\n\n const label = typeof entry === \"string\" ? entry : entry.label;\n const item = typeof entry !== \"object\" ? {} : entry;\n\n // Normalize entry\n item.label = entry[this._config.labelField] ?? label;\n item.value = entry[this._config.valueField] ?? key;\n\n // Make sure we have a label\n if (item.label) {\n this._items[item.value] = item;\n }\n }\n }\n\n /**\n * @param {boolean} show\n */\n _loadFromServer(show = false) {\n if (this._abortController) {\n this._abortController.abort();\n }\n this._abortController = new AbortController();\n\n // Read data params dynamically as well\n let extraParams = this._searchInput.dataset.serverParams || {};\n if (typeof extraParams == \"string\") {\n extraParams = JSON.parse(extraParams);\n }\n const params = Object.assign({}, this._config.serverParams, extraParams);\n // Pass current value\n params[this._config.queryParam] = this._searchInput.value;\n // Prevent caching\n if (this._config.noCache) {\n params.t = Date.now();\n }\n // We have a related field\n if (params.related) {\n /**\n * @type {HTMLInputElement}\n */\n //@ts-ignore\n const input = document.getElementById(params.related);\n if (input) {\n params.related = input.value;\n const inputName = input.getAttribute(\"name\");\n if (inputName) {\n params[inputName] = input.value;\n }\n }\n }\n\n const urlParams = new URLSearchParams(params);\n let url = this._config.server;\n let fetchOptions = Object.assign(this._config.fetchOptions, {\n method: this._config.serverMethod || \"GET\",\n signal: this._abortController.signal,\n });\n\n if (fetchOptions.method === \"POST\") {\n fetchOptions.body = urlParams;\n } else {\n url += \"?\" + urlParams.toString();\n }\n\n this._searchInput.classList.add(LOADING_CLASS);\n this._config.onBeforeFetch(this);\n\n fetch(url, fetchOptions)\n .then((r) => this._config.onServerResponse(r, this))\n .then((suggestions) => {\n const data = nested(this._config.serverDataKey, suggestions) || suggestions;\n this.setData(data);\n this._setHiddenVal();\n this._abortController = null;\n if (show) {\n this._showSuggestions();\n }\n })\n .catch((e) => {\n // Current version of Firefox rejects the promise with a DOMException\n if (e.name === \"AbortError\" || this._abortController.signal.aborted) {\n return;\n }\n console.error(e);\n })\n .finally((e) => {\n this._searchInput.classList.remove(LOADING_CLASS);\n this._config.onAfterFetch(this);\n });\n }\n\n // #endregion\n}\n\nexport default Autocomplete;\n"], + "mappings": "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", + "names": ["DEFAULTS", "item", "label", "inst", "response", "LOADING_CLASS", "ACTIVE_CLASS", "SHOW_CLASS", "NEXT", "PREV", "INSTANCE_MAP", "counter", "activeCounter", "debounce", "func", "timeout", "timer", "args", "removeDiacritics", "str", "normalize", "fuzzyMatch", "lookup", "pos", "i", "c", "insertAfter", "el", "newEl", "decodeHtml", "html", "txt", "attrs", "k", "v", "zwijit", "char", "nested", "obj", "r", "p", "Autocomplete", "config", "_searchInput", "_configure", "_preventInput", "_keyboardNavigation", "_searchFunc", "_loadFromServer", "_config", "_configureSearchInput", "_configureDropElement", "clearControl", "_getClearControl", "type", "_dropElement", "_fetchData", "selector", "event", "_timer", "o", "parseBool", "value", "key", "defaultValue", "separator", "_hiddenInput", "e", "search", "_items", "selection", "_moveSelection", "_positionMenu", "src", "_addItems", "_activeClasses", "_isItemEnabled", "li", "fc", "dir", "sel", "active", "sibling", "a", "_shouldShow", "check", "items", "_showSuggestions", "_createGroup", "name", "newChild", "_createLi", "newChildSpan", "_createItem", "idx", "newChildLink", "fillIn", "keys", "count", "firstItem", "groups", "entry", "showAllSuggestions", "isMatched", "sf", "text", "found", "selectFirst", "newItem", "_showDropdown", "styles", "bounds", "isRTL", "fullWidth", "fixed", "left", "top", "dropBounds", "h", "topOffset", "dl", "datalist", "_setHiddenVal", "show", "_abortController", "extraParams", "params", "input", "inputName", "urlParams", "url", "fetchOptions", "suggestions", "data", "autocomplete_default"] +} diff --git a/afat/static/afat/libs/bootstrap5-autocomplete/1.1.25/readme.md b/afat/static/afat/libs/bootstrap5-autocomplete/1.1.25/readme.md new file mode 100644 index 00000000..b87ba3b7 --- /dev/null +++ b/afat/static/afat/libs/bootstrap5-autocomplete/1.1.25/readme.md @@ -0,0 +1,167 @@ +# Autocomplete for Bootstrap 4/5 + +[![NPM](https://nodei.co/npm/bootstrap5-autocomplete.png?mini=true)](https://nodei.co/npm/bootstrap5-autocomplete/) +[![Downloads](https://img.shields.io/npm/dt/bootstrap5-autocomplete.svg)](https://www.npmjs.com/package/bootstrap5-autocomplete) + +## How to use + +An ES6 autocomplete for your `input` using standards Bootstrap 5 (and 4) styles. + +No additional CSS needed! + +```js +import Autocomplete from "./autocomplete.js"; +Autocomplete.init(); +``` + +## Server side support + +You can also use options provided by the server. This script expects a JSON response with the following structure: + +```json +{ + "optionValue1":"optionLabel1", + "optionValue2":"optionLabel2", + ... +} +``` + +or + +```json +[ + { + "value": "server1", + "label": "Server 1" + }, + ... +] +``` + +Simply set `data-server` where your endpoint is located. The suggestions will be populated upon init except if `data-live-server` is set, in which case, it will be populated on type. A ?query= parameter is passed along with the current value of the searchInput. + +Data can be nested in the response under the data key (configurable with serverDataKey). + +## Options + +Options can be either passed to the constructor (eg: optionName) or in data-option-name format. + +| Name | Type | Description | +| -------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| showAllSuggestions | Boolean | Show all suggestions even if they don't match | +| suggestionsThreshold | Number | Number of chars required to show suggestions | +| maximumItems | Number | Maximum number of items to display | +| autoselectFirst | Boolean | Always select the first item | +| ignoreEnter | Boolean | Ignore enter if no items are selected (play nicely with autoselectFirst=0) | +| updateOnSelect | Boolean | Update input value on selection (doesn't play nice with autoselectFirst) | +| highlightTyped | Boolean | Highlight matched part of the label | +| highlightClass | String | Class added to the mark label | +| fullWidth | Boolean | Match the width on the input field | +| fixed | Boolean | Use fixed positioning (solve overflow issues) | +| fuzzy | Boolean | Fuzzy search | +| startsWith | Boolean | Must start with the string. Defaults to false (it matches any position). | +| fillIn | Boolean | Show fill in icon. | +| preventBrowserAutocomplete | Boolean | Additional measures to prevent browser autocomplete | +| itemClass | String | Applied to the dropdown item. Accepts space separated classes. | +| activeClasses | Array | By default: ["bg-primary", "text-white"] | +| labelField | String | Key for the label | +| valueField | String | Key for the value | +| searchFields | Array | Key for the search | +| queryParam | String | Key for the query parameter for server | +| items | Array \| Object | An array of label/value objects or an object with key/values | +| source | function | A function that provides the list of items | +| hiddenInput | Boolean | Create an hidden input which stores the valueField | +| hiddenValue | String | Populate the initial hidden value. Mostly useful with liveServer. | +| clearControl | String | Selector that will clear the input on click. | +| datalist | String | The id of the source datalist | +| server | String | Endpoint for data provider | +| serverMethod | String | HTTP request method for data provider, default is GET | +| serverParams | String \| Object | Parameters to pass along to the server. You can specify a "related" key with the id of a related field. | +| serverDataKey | String | By default: data | +| fetchOptions | Object | Any other fetch options (https://developer.mozilla.org/en-US/docs/Web/API/fetch#syntax) | +| liveServer | Boolean | Should the endpoint be called each time on input | +| noCache | Boolean | Prevent caching by appending a timestamp | +| debounceTime | Number | Debounce time for live server | +| notFoundMessage | String | Display a no suggestions found message. Leave empty to disable | +| onRenderItem | [RenderCallback](#RenderCallback) | Callback function that returns the label | +| onSelectItem | [ItemCallback](#ItemCallback) | Callback function to call on selection | +| onServerResponse | [ServerCallback](#ServerCallback) | Callback function to process server response. Must return a Promise | +| onChange | [ItemCallback](#ItemCallback) | Callback function to call on change-event. Returns currently selected item if any | +| onBeforeFetch | [FetchCallback](#FetchCallback) | Callback function before fetch | +| onAfterFetch | [FetchCallback](#FetchCallback) | Callback function after fetch | + +## Callbacks + +### RenderCallback ⇒ string + +| Param | Type | +| ----- | ------------------------------------------ | +| item | Object | +| label | String | +| inst | [Autocomplete](#Autocomplete) | + + + +### ItemCallback ⇒ void + +| Param | Type | +| ----- | ------------------------------------------ | +| item | Object | +| inst | [Autocomplete](#Autocomplete) | + + + +### ServerCallback ⇒ Promise + +| Param | Type | +| -------- | ------------------------------------------ | +| response | Response | +| inst | [Autocomplete](#Autocomplete) | + +## Tips + +- Use arrow down to show dropdown (and arrow up to hide it) +- If you have a really long list of options, a scrollbar will be used +- Access instance on a given element with Autocomplete.getInstance(myEl) +- Use type="search" for your inputs to get a clear icon + +## Groups + +You can have your items grouped by using the following syntax: + +```js +const src = [ + { + group: "My Group Name", + items: [ + { + value: "...", + label: "...", + }, + ], + }, +]; +``` + +## Not using Bootstrap ? + +This class does NOT depends on Bootstrap JS. If you are not using Bootstrap, you can simply implement the css +the way you like it :-) + +## Demo + +https://codepen.io/lekoalabe/pen/MWXydNQ or see demo.html + +## Custom element + +You can now use this as a custom element as part of my [Formidable Elements](https://github.com/lekoala/formidable-elements) collection. + +## Browser supports + +Modern browsers (edge, chrome, firefox, safari... not IE11). [Add a warning if necessary](https://github.com/lekoala/nomodule-browser-warning.js/). + +## Also check out + +- [Bootstrap 5 Tags](https://github.com/lekoala/bootstrap5-tags): tags input for bootstrap +- [BS Companion](https://github.com/lekoala/bs-companion): the perfect bootstrap companion +- [Admini](https://github.com/lekoala/admini): the minimalistic bootstrap 5 admin panel diff --git a/afat/templates/afat/bundles/afat-fatlink-add-js.html b/afat/templates/afat/bundles/afat-fatlink-add-js.html new file mode 100644 index 00000000..4843ea84 --- /dev/null +++ b/afat/templates/afat/bundles/afat-fatlink-add-js.html @@ -0,0 +1,3 @@ +{% load afat %} + + diff --git a/afat/templates/afat/partials/form/fleet-doctrine.html b/afat/templates/afat/partials/form/fleet-doctrine.html new file mode 100644 index 00000000..43bfb9e9 --- /dev/null +++ b/afat/templates/afat/partials/form/fleet-doctrine.html @@ -0,0 +1,7 @@ +{% if doctrines|length > 0 %} + + {% for doctrine in doctrines %} + + {% endfor %} + +{% endif %} diff --git a/afat/templates/afat/view/fatlinks/fatlinks-add-fatlink.html b/afat/templates/afat/view/fatlinks/fatlinks-add-fatlink.html index 65a5ee1e..2ba1b22f 100644 --- a/afat/templates/afat/view/fatlinks/fatlinks-add-fatlink.html +++ b/afat/templates/afat/view/fatlinks/fatlinks-add-fatlink.html @@ -16,6 +16,7 @@
{% include "afat/partials/fatlinks/add/esi-link.html" %} {% include "afat/partials/fatlinks/add/clickable-link.html" %} + {% include "afat/partials/form/fleet-doctrine.html" %}
{% endblock %} @@ -25,6 +26,7 @@ {% block extra_javascript %} {% include "afat/bundles/afat-js.html" %} + {% include "afat/bundles/afat-fatlink-add-js.html" %}