generated from westfarn/web_django_template
Initial commit
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,301 @@
|
||||
/**
|
||||
* Campaign UTM link helper (composer + report).
|
||||
* utm_source from data-utm-source (brand slug), utm_medium=email|sms|postcard,
|
||||
* utm_campaign=slug(name). Short URLs come from the portal JSON endpoint.
|
||||
*/
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
var SLUG_MAX = 80;
|
||||
var SHORTEN_WAIT_MS = 400;
|
||||
var SHORT_URL_RE =
|
||||
/https?:\/\/(?:(?:www\.)?(?:beta\.)?piha\.li|(?:127\.0\.0\.1|localhost):\d+)\/[a-z0-9]{4,8}/i;
|
||||
|
||||
function slug(name) {
|
||||
var s = String(name || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
return (s || "campaign").slice(0, SLUG_MAX);
|
||||
}
|
||||
|
||||
function sourceFrom(panel) {
|
||||
return slug((panel && panel.getAttribute("data-utm-source")) || "campaign");
|
||||
}
|
||||
|
||||
function utmUrlRe(source) {
|
||||
return new RegExp(
|
||||
"https?:\\/\\/[^\\s<>\"]+utm_source=" + source + "[^\\s<>\"]*",
|
||||
"i"
|
||||
);
|
||||
}
|
||||
|
||||
function buildUrl(base, medium, name, source) {
|
||||
var root = String(base || "").replace(/\/+$/, "");
|
||||
var ch = String(medium || "email").toLowerCase();
|
||||
if (ch !== "email" && ch !== "sms" && ch !== "postcard") ch = "email";
|
||||
var q =
|
||||
"utm_source=" +
|
||||
encodeURIComponent(source || "campaign") +
|
||||
"&utm_medium=" +
|
||||
encodeURIComponent(ch) +
|
||||
"&utm_campaign=" +
|
||||
encodeURIComponent(slug(name));
|
||||
return root + "/?" + q;
|
||||
}
|
||||
|
||||
function replaceTextUrl(text, url, source) {
|
||||
var raw = String(text || "");
|
||||
var utmRe = utmUrlRe(source || "campaign");
|
||||
if (utmRe.test(raw)) {
|
||||
return raw.replace(utmRe, url);
|
||||
}
|
||||
if (SHORT_URL_RE.test(raw)) {
|
||||
return raw.replace(SHORT_URL_RE, url);
|
||||
}
|
||||
var trimmed = raw.replace(/\s+$/, "");
|
||||
return trimmed ? trimmed + "\n\n" + url : url;
|
||||
}
|
||||
|
||||
function linkify(text) {
|
||||
var esc = String(text || "").replace(/[&<>"']/g, function (c) {
|
||||
return (
|
||||
{ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[
|
||||
c
|
||||
] || c
|
||||
);
|
||||
});
|
||||
return esc.replace(
|
||||
/(https?:\/\/[^\s]+)/g,
|
||||
'<a href="$1" rel="noopener noreferrer">$1</a>'
|
||||
);
|
||||
}
|
||||
|
||||
function ensureQuillLink(quill, url, label, source) {
|
||||
if (!quill || !quill.root) return;
|
||||
var src = source || "campaign";
|
||||
var existing = quill.root.querySelectorAll(
|
||||
'a[data-campaign-utm], a[href*="utm_source=' +
|
||||
src +
|
||||
'"], a[href*="piha.li/"]'
|
||||
);
|
||||
if (existing.length) {
|
||||
existing.forEach(function (a) {
|
||||
a.setAttribute("href", url);
|
||||
a.setAttribute("data-campaign-utm", "1");
|
||||
});
|
||||
return;
|
||||
}
|
||||
var labelText = label || "Website";
|
||||
var start = Math.max(0, quill.getLength() - 1);
|
||||
quill.insertText(start, "\n" + labelText, "silent");
|
||||
quill.formatText(start + 1, labelText.length, "link", url, "silent");
|
||||
var links = quill.root.querySelectorAll("a");
|
||||
var last = links[links.length - 1];
|
||||
if (last) last.setAttribute("data-campaign-utm", "1");
|
||||
}
|
||||
|
||||
function renderQr(canvas, url) {
|
||||
if (!canvas || !url || typeof QRCode === "undefined") return Promise.resolve();
|
||||
return QRCode.toCanvas(canvas, url, {
|
||||
width: 192,
|
||||
margin: 1,
|
||||
color: { dark: "#00626c", light: "#ffffff" },
|
||||
});
|
||||
}
|
||||
|
||||
function flash(el, okText, errText, ok) {
|
||||
if (!el) return;
|
||||
el.textContent = ok ? okText : errText;
|
||||
el.hidden = false;
|
||||
window.setTimeout(function () {
|
||||
el.hidden = true;
|
||||
}, 2200);
|
||||
}
|
||||
|
||||
function copyText(text) {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
return navigator.clipboard.writeText(text);
|
||||
}
|
||||
return Promise.reject(new Error("clipboard unavailable"));
|
||||
}
|
||||
|
||||
function copyCanvas(canvas) {
|
||||
if (!canvas || !canvas.toBlob) {
|
||||
return Promise.reject(new Error("canvas unavailable"));
|
||||
}
|
||||
return new Promise(function (resolve, reject) {
|
||||
canvas.toBlob(function (blob) {
|
||||
if (!blob) {
|
||||
reject(new Error("qr blob failed"));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!navigator.clipboard ||
|
||||
!navigator.clipboard.write ||
|
||||
typeof ClipboardItem === "undefined"
|
||||
) {
|
||||
reject(new Error("image clipboard unavailable"));
|
||||
return;
|
||||
}
|
||||
navigator.clipboard
|
||||
.write([new ClipboardItem({ "image/png": blob })])
|
||||
.then(resolve, reject);
|
||||
}, "image/png");
|
||||
});
|
||||
}
|
||||
|
||||
function downloadCanvas(canvas, filename) {
|
||||
if (!canvas) return;
|
||||
var a = document.createElement("a");
|
||||
a.href = canvas.toDataURL("image/png");
|
||||
a.download = filename || "campaign-qr.png";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
|
||||
function bindPanel(panel, opts) {
|
||||
if (!panel) return;
|
||||
opts = opts || {};
|
||||
var urlEl = panel.querySelector("[data-utm-url]");
|
||||
var canvas = panel.querySelector("[data-utm-qr]");
|
||||
var qrWrap = panel.querySelector("[data-utm-qr-wrap]");
|
||||
var smsHint = panel.querySelector("[data-utm-sms-hint]");
|
||||
var emailHint = panel.querySelector("[data-utm-email-hint]");
|
||||
var postcardHint = panel.querySelector("[data-utm-postcard-hint]");
|
||||
var statusEl = panel.querySelector("[data-utm-status]");
|
||||
var base = panel.getAttribute("data-base-url") || "";
|
||||
var source = sourceFrom(panel);
|
||||
var label = panel.getAttribute("data-link-label") || "Website";
|
||||
var live = panel.getAttribute("data-live") === "1";
|
||||
var shortenUrl = panel.getAttribute("data-shorten-url") || "";
|
||||
var csrf = (opts.csrfToken || panel.getAttribute("data-csrf") || "").trim();
|
||||
var shortenTimer = null;
|
||||
var shortenSeq = 0;
|
||||
|
||||
function medium() {
|
||||
if (typeof opts.getMedium === "function") return opts.getMedium();
|
||||
return panel.getAttribute("data-medium") || "email";
|
||||
}
|
||||
|
||||
function name() {
|
||||
if (typeof opts.getName === "function") return opts.getName();
|
||||
return panel.getAttribute("data-campaign-name") || "";
|
||||
}
|
||||
|
||||
function campaignId() {
|
||||
return (
|
||||
(typeof opts.getCampaignId === "function" && opts.getCampaignId()) ||
|
||||
panel.getAttribute("data-campaign-id") ||
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
function paint(url, inject) {
|
||||
var ch = medium();
|
||||
if (urlEl) urlEl.textContent = url;
|
||||
if (qrWrap) qrWrap.hidden = ch !== "postcard";
|
||||
if (smsHint) smsHint.hidden = ch !== "sms";
|
||||
if (emailHint) emailHint.hidden = ch !== "email";
|
||||
if (postcardHint) postcardHint.hidden = ch !== "postcard";
|
||||
if (ch === "postcard" && canvas) {
|
||||
renderQr(canvas, url).catch(function () {});
|
||||
}
|
||||
if (inject && typeof opts.onUrlChange === "function") {
|
||||
opts.onUrlChange(url, ch, label);
|
||||
}
|
||||
}
|
||||
|
||||
function apply() {
|
||||
var ch = medium();
|
||||
var longUrl = buildUrl(base, ch, name(), source);
|
||||
if (!shortenUrl) {
|
||||
paint(longUrl, true);
|
||||
return longUrl;
|
||||
}
|
||||
paint(longUrl, false);
|
||||
var seq = ++shortenSeq;
|
||||
window.clearTimeout(shortenTimer);
|
||||
shortenTimer = window.setTimeout(function () {
|
||||
fetch(shortenUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
"X-CSRFToken": csrf,
|
||||
},
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify({
|
||||
name: name(),
|
||||
medium: ch,
|
||||
campaign_id: campaignId(),
|
||||
}),
|
||||
})
|
||||
.then(function (r) {
|
||||
return r.ok ? r.json() : Promise.reject();
|
||||
})
|
||||
.then(function (data) {
|
||||
if (seq !== shortenSeq) return;
|
||||
paint((data && data.display_url) || longUrl, true);
|
||||
})
|
||||
.catch(function () {
|
||||
if (seq !== shortenSeq) return;
|
||||
paint(longUrl, true);
|
||||
});
|
||||
}, SHORTEN_WAIT_MS);
|
||||
return longUrl;
|
||||
}
|
||||
|
||||
panel.querySelectorAll("[data-utm-copy-url]").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
var url = (urlEl && urlEl.textContent) || "";
|
||||
copyText(url).then(
|
||||
function () {
|
||||
flash(statusEl, "Link copied.", "Could not copy.", true);
|
||||
},
|
||||
function () {
|
||||
flash(statusEl, "", "Could not copy link.", false);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
panel.querySelectorAll("[data-utm-copy-qr]").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
copyCanvas(canvas).then(
|
||||
function () {
|
||||
flash(statusEl, "QR image copied.", "Could not copy QR.", true);
|
||||
},
|
||||
function () {
|
||||
flash(statusEl, "", "Copy failed — use Download PNG.", false);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
panel.querySelectorAll("[data-utm-download-qr]").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
downloadCanvas(canvas, "qr-" + slug(name()) + ".png");
|
||||
flash(statusEl, "QR downloaded.", "", true);
|
||||
});
|
||||
});
|
||||
|
||||
if (live) {
|
||||
var nameField = document.getElementById("id_name");
|
||||
if (nameField) nameField.addEventListener("input", apply);
|
||||
}
|
||||
|
||||
panel._utmApply = apply;
|
||||
apply();
|
||||
return { apply: apply, buildUrl: buildUrl };
|
||||
}
|
||||
|
||||
global.CampaignUtm = {
|
||||
slug: slug,
|
||||
buildUrl: buildUrl,
|
||||
replaceTextUrl: replaceTextUrl,
|
||||
linkify: linkify,
|
||||
ensureQuillLink: ensureQuillLink,
|
||||
bindPanel: bindPanel,
|
||||
};
|
||||
})(window);
|
||||
+299
File diff suppressed because one or more lines are too long
Executable
+2092
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,161 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var NOTICE_KEY = 'aiml_analytics_notice';
|
||||
var LEGACY_CONSENT_KEY = 'aiml_analytics_consent';
|
||||
var LEGACY_DISABLED_KEY = 'tianji.disabled';
|
||||
var configEl = document.getElementById('tianji-config');
|
||||
if (!configEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
var trackerUrl = configEl.dataset.trackerUrl;
|
||||
var websiteId = configEl.dataset.websiteId;
|
||||
var userId = configEl.dataset.userId || '';
|
||||
|
||||
function getStorageItem(key) {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function setStorageItem(key, value) {
|
||||
try {
|
||||
localStorage.setItem(key, value);
|
||||
} catch (e) {
|
||||
/* ignore storage errors */
|
||||
}
|
||||
}
|
||||
|
||||
function removeStorageItem(key) {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
} catch (e) {
|
||||
/* ignore storage errors */
|
||||
}
|
||||
}
|
||||
|
||||
function showBanner() {
|
||||
var banner = document.getElementById('cookie-consent-banner');
|
||||
if (banner) {
|
||||
banner.removeAttribute('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function hideBanner() {
|
||||
var banner = document.getElementById('cookie-consent-banner');
|
||||
if (banner) {
|
||||
banner.setAttribute('hidden', '');
|
||||
}
|
||||
}
|
||||
|
||||
function trackEvent(name, data) {
|
||||
if (window.tianji && typeof window.tianji.track === 'function') {
|
||||
window.tianji.track(name, data || {});
|
||||
}
|
||||
}
|
||||
|
||||
function identifyUser() {
|
||||
if (!userId || !window.tianji || typeof window.tianji.identify !== 'function') {
|
||||
return;
|
||||
}
|
||||
window.tianji.identify({ userId: userId });
|
||||
}
|
||||
|
||||
function loadTracker() {
|
||||
if (document.querySelector('script[data-tianji-loaded="true"]')) {
|
||||
identifyUser();
|
||||
return;
|
||||
}
|
||||
|
||||
var script = document.createElement('script');
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.src = trackerUrl;
|
||||
script.setAttribute('data-website-id', websiteId);
|
||||
script.setAttribute('data-do-not-track', 'true');
|
||||
script.setAttribute('data-tianji-loaded', 'true');
|
||||
|
||||
script.onload = function () {
|
||||
identifyUser();
|
||||
document.dispatchEvent(new CustomEvent('tianji:ready'));
|
||||
};
|
||||
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
function migrateLegacyConsent() {
|
||||
var legacyConsent = getStorageItem(LEGACY_CONSENT_KEY);
|
||||
var legacyDisabled = getStorageItem(LEGACY_DISABLED_KEY);
|
||||
|
||||
if (legacyConsent === 'accepted') {
|
||||
setStorageItem(NOTICE_KEY, 'acknowledged');
|
||||
removeStorageItem(LEGACY_CONSENT_KEY);
|
||||
removeStorageItem(LEGACY_DISABLED_KEY);
|
||||
return 'migrated_acknowledged';
|
||||
}
|
||||
|
||||
if (legacyConsent === 'declined' || legacyDisabled === '1') {
|
||||
removeStorageItem(LEGACY_CONSENT_KEY);
|
||||
removeStorageItem(LEGACY_DISABLED_KEY);
|
||||
return 'migrated_declined';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function acknowledgeNotice() {
|
||||
setStorageItem(NOTICE_KEY, 'acknowledged');
|
||||
hideBanner();
|
||||
window.aimlTrackWhenReady('notice_acknowledged');
|
||||
}
|
||||
|
||||
function bindBannerControls() {
|
||||
var acknowledgeBtn = document.getElementById('cookie-consent-acknowledge');
|
||||
var manageLinks = document.querySelectorAll('[data-open-cookie-preferences]');
|
||||
|
||||
if (acknowledgeBtn) {
|
||||
acknowledgeBtn.addEventListener('click', acknowledgeNotice);
|
||||
}
|
||||
manageLinks.forEach(function (link) {
|
||||
link.addEventListener('click', function (event) {
|
||||
event.preventDefault();
|
||||
showBanner();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.aimlTrack = function (name, data) {
|
||||
trackEvent(name, data);
|
||||
};
|
||||
|
||||
window.aimlTrackWhenReady = function (name, data) {
|
||||
if (window.tianji && typeof window.tianji.track === 'function') {
|
||||
trackEvent(name, data);
|
||||
return;
|
||||
}
|
||||
document.addEventListener('tianji:ready', function handler() {
|
||||
document.removeEventListener('tianji:ready', handler);
|
||||
trackEvent(name, data);
|
||||
});
|
||||
};
|
||||
|
||||
function initNotice() {
|
||||
bindBannerControls();
|
||||
loadTracker();
|
||||
|
||||
var migration = migrateLegacyConsent();
|
||||
var notice = getStorageItem(NOTICE_KEY);
|
||||
|
||||
if (notice === 'acknowledged' || migration === 'migrated_acknowledged') {
|
||||
hideBanner();
|
||||
return;
|
||||
}
|
||||
|
||||
showBanner();
|
||||
}
|
||||
|
||||
initNotice();
|
||||
})();
|
||||
Reference in New Issue
Block a user