Template
Extract always-on public/portal/UTM plus optional email_sms, directmail, blog, payments, social, and social_ai so new client sites can be bootstrapped from this seed. Refs #1 Refs #2 Co-authored-by: Cursor <cursoragent@cursor.com>
150 lines
3.9 KiB
JavaScript
150 lines
3.9 KiB
JavaScript
/**
|
|
* 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();
|
|
}
|
|
})();
|