/** * Address autocomplete via Django /api/address-suggest/ (Nominatim proxy). * Never calls Nominatim from the browser. * * Markup: wrap fields in [data-address-autocomplete][data-suggest-url="..."]. * Mark inputs with data-ac="line1|line2|city|state|zip|country". * The line1 input is the typeahead trigger. */ (function () { "use strict"; var DEBOUNCE_MS = 280; var MIN_CHARS = 3; function debounce(fn, wait) { var t; return function () { var ctx = this; var args = arguments; clearTimeout(t); t = setTimeout(function () { fn.apply(ctx, args); }, wait); }; } function field(root, key) { return root.querySelector('[data-ac="' + key + '"]'); } function ensureList(root) { var list = root.querySelector(".address-ac-list"); if (list) { return list; } list = document.createElement("ul"); list.className = "address-ac-list"; list.hidden = true; list.setAttribute("role", "listbox"); var line1 = field(root, "line1"); var wrap = line1 && line1.closest(".form-wrap, .field"); (wrap || root).appendChild(list); if (wrap) { wrap.classList.add("address-ac-wrap"); } else { root.classList.add("address-ac-wrap"); } return list; } function hide(list) { list.hidden = true; list.innerHTML = ""; } function fill(root, item) { var map = { line1: item.line1 || "", line2: item.line2 || "", city: item.city || "", state: item.state || "", zip: item.zip || "", country: item.country || "US", }; Object.keys(map).forEach(function (key) { var el = field(root, key); if (el) { el.value = map[key]; el.dispatchEvent(new Event("input", { bubbles: true })); el.dispatchEvent(new Event("change", { bubbles: true })); } }); } function render(root, list, results) { list.innerHTML = ""; if (!results.length) { hide(list); return; } results.forEach(function (item, index) { var li = document.createElement("li"); li.className = "address-ac-item"; li.setAttribute("role", "option"); li.setAttribute("tabindex", "-1"); li.dataset.index = String(index); li.textContent = item.label || item.line1 || "Address"; li.addEventListener("mousedown", function (ev) { ev.preventDefault(); fill(root, item); hide(list); }); list.appendChild(li); }); list.hidden = false; } function bind(root) { var url = root.getAttribute("data-suggest-url"); var input = field(root, "line1"); if (!url || !input) { return; } var list = ensureList(root); input.setAttribute("autocomplete", "off"); input.setAttribute("aria-autocomplete", "list"); var fetchSuggest = debounce(function () { var q = (input.value || "").trim(); if (q.length < MIN_CHARS) { hide(list); return; } var req = url + (url.indexOf("?") >= 0 ? "&" : "?") + "q=" + encodeURIComponent(q) + "&limit=5"; fetch(req, { headers: { Accept: "application/json" }, credentials: "same-origin" }) .then(function (res) { return res.json(); }) .then(function (data) { render(root, list, (data && data.results) || []); }) .catch(function () { hide(list); }); }, DEBOUNCE_MS); input.addEventListener("input", fetchSuggest); input.addEventListener("keydown", function (ev) { if (ev.key === "Escape") { hide(list); } }); document.addEventListener("click", function (ev) { if (!root.contains(ev.target)) { hide(list); } }); } function init() { document.querySelectorAll("[data-address-autocomplete]").forEach(bind); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();