Template
Keep mint/UTM in always-on core so email_sms and directmail stay optional, and brand utm_source from SITE_NAME (or UTM_SOURCE) instead of a hardcoded client name. Closes #3
302 lines
9.1 KiB
JavaScript
302 lines
9.1 KiB
JavaScript
/**
|
|
* 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);
|