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 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 %}
+
+{% 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" %}