Created
October 1, 2025 21:28
-
-
Save joshuafredrickson/285a3b3da8218529e296e7052cc49a11 to your computer and use it in GitHub Desktop.
Revisions
-
joshuafredrickson created this gist
Oct 1, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,363 @@ /* CONFIGURE HERE - TARGET_TEXTS: all text variants of the phone number on your site to replace. - TARGET_TEL: digits-only version of the phone number (for tel: links). - CAMPAIGN_NUMBERS: map of se_campaign value => replacement number display format (tel is auto-generated). - STORAGE_KEY/STORAGE_DAYS: storage settings (uses localStorage if available, cookie fallback). */ (function () { var TARGET_TEXTS = [ "(555) 555-1212", ]; var TARGET_TEL = "5555551212"; var CAMPAIGN_NUMBERS = { // Example mappings — edit to yours (case insensitive) // se_campaign=google => number below yelp: "555-123-4567", google: "555-456-7890", }; var STORAGE_KEY = "se_campaign"; var STORAGE_DAYS = 30; // ---------------- no edits needed below ---------------- /** * Extracts a query parameter value from the current URL * @param {string} name - The parameter name to look for * @returns {string|null} The parameter value or null if not found */ function getQueryParam(name) { var params = window.location.search.substring(1).split("&"); for (var i = 0; i < params.length; i++) { var pair = params[i].split("="); if (decodeURIComponent(pair[0]) === name) { return typeof pair[1] === "undefined" ? "" : decodeURIComponent(pair[1] || ""); } } return null; } /** * Tests if localStorage is available and working * @returns {boolean} True if localStorage is supported */ function supportsLocalStorage() { try { var k = "__test__"; window.localStorage.setItem(k, "1"); window.localStorage.removeItem(k); return true; } catch (e) { return false; } } /** * Sets a cookie with an expiration date * @param {string} name - Cookie name * @param {string} value - Cookie value * @param {number} days - Number of days until expiration */ function setCookie(name, value, days) { var expires = ""; if (days) { var d = new Date(); d.setTime(d.getTime() + days * 24 * 60 * 60 * 1000); expires = "; expires=" + d.toUTCString(); } document.cookie = name + "=" + encodeURIComponent(value) + expires + "; path=/; SameSite=Lax"; } /** * Retrieves a cookie value by name * @param {string} name - Cookie name to retrieve * @returns {string|null} The cookie value or null if not found */ function getCookie(name) { var nameEQ = name + "="; var ca = document.cookie.split(";"); for (var i = 0; i < ca.length; i++) { var c = ca[i].trim(); if (c.indexOf(nameEQ) === 0) return decodeURIComponent(c.substring(nameEQ.length)); } return null; } /** * Saves the phone number configuration to storage (localStorage or cookie fallback) * Includes expiry timestamp for automatic cleanup * @param {Object} obj - Object with display and tel properties */ function saveNumberConfig(obj) { if (supportsLocalStorage()) { try { var dataWithExpiry = { data: obj, expiry: new Date().getTime() + (STORAGE_DAYS * 24 * 60 * 60 * 1000) }; localStorage.setItem(STORAGE_KEY, JSON.stringify(dataWithExpiry)); } catch (e) {} } else { setCookie(STORAGE_KEY, JSON.stringify(obj), STORAGE_DAYS); } } /** * Loads the phone number configuration from storage * Automatically removes expired entries * @returns {Object|null} The stored config object or null if not found/expired */ function loadNumberConfig() { var raw = null; if (supportsLocalStorage()) { try { raw = localStorage.getItem(STORAGE_KEY); if (raw) { var parsed = JSON.parse(raw); // Check if it has expiry field (new format) if (parsed && parsed.expiry) { if (new Date().getTime() > parsed.expiry) { // Expired, remove it localStorage.removeItem(STORAGE_KEY); return null; } return parsed.data; } // Old format without expiry, return as-is return parsed; } } catch (e) { return null; } } // Fallback to cookie raw = getCookie(STORAGE_KEY); if (!raw) return null; try { return JSON.parse(raw); } catch (e) { return null; } } /** * Escapes special regex characters in a string for safe use in RegExp * @param {string} str - String to escape * @returns {string} Escaped string */ function escapeForRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } /** * Strips all non-digit characters from a string * @param {string} str - String to normalize * @returns {string} String containing only digits */ function normalizeDigits(str) { return String(str || "").replace(/\D+/g, ""); } var targetTextRegex = new RegExp("(" + TARGET_TEXTS.map(escapeForRegex).join("|") + ")", "g"); var targetTelDigits = normalizeDigits(TARGET_TEL); /** * Checks if a text node contains any target phone numbers to replace * @param {string} text - Text content to check * @returns {boolean} True if text contains a target phone number */ function shouldReplaceTextNode(text) { targetTextRegex.lastIndex = 0; // Reset regex state for global flag return targetTextRegex.test(text); } /** * Replaces target phone numbers in a text node with the replacement number * @param {Node} node - Text node to modify * @param {string} replacementDisplay - The replacement phone number to display */ function replaceInTextNode(node, replacementDisplay) { targetTextRegex.lastIndex = 0; // Reset regex state for global flag node.nodeValue = node.nodeValue.replace(targetTextRegex, replacementDisplay); } /** * Finds and replaces tel: links that match the target phone number * Updates both href attribute and visible text * @param {string} replacementTel - Digits-only replacement number for href * @param {string} replacementDisplay - Formatted replacement number for display */ function replaceTelAnchors(replacementTel, replacementDisplay) { var anchors = document.querySelectorAll('a[href^="tel:"]'); for (var i = 0; i < anchors.length; i++) { var a = anchors[i]; var href = a.getAttribute("href") || ""; var digits = normalizeDigits(href); if (digits === targetTelDigits) { a.setAttribute("href", "tel:" + replacementTel); // If the visible text equals one of the target variants, update it var text = (a.textContent || "").trim(); if (TARGET_TEXTS.indexOf(text) > -1 || normalizeDigits(text) === targetTelDigits) { a.textContent = replacementDisplay; } } } } // Shared filter for TreeWalker to skip script/style/noscript tags var SKIP_TAGS = { script: 1, style: 1, noscript: 1 }; var textNodeFilter = { acceptNode: function (node) { var p = node.parentNode; if (!p || SKIP_TAGS[p.nodeName.toLowerCase()]) { return NodeFilter.FILTER_REJECT; } return shouldReplaceTextNode(node.nodeValue) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; } }; /** * Performs initial replacement of all phone numbers on the page * Replaces both text nodes and tel: links * @param {Object} replacement - Object with display and tel properties */ function replaceEverywhere(replacement) { if (!replacement || !replacement.display || !replacement.tel) return; // Text nodes var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, textNodeFilter); var current; var nodesToUpdate = []; while ((current = walker.nextNode())) { nodesToUpdate.push(current); } for (var i = 0; i < nodesToUpdate.length; i++) { replaceInTextNode(nodesToUpdate[i], replacement.display); } // tel: anchors replaceTelAnchors(replacement.tel, replacement.display); } /** * Sets up a MutationObserver to watch for dynamically added content * Automatically replaces phone numbers in new DOM nodes * @param {Object} replacement - Object with display and tel properties */ function setupObserver(replacement) { if (!("MutationObserver" in window)) return; var observer = new MutationObserver(function (mutations) { for (var i = 0; i < mutations.length; i++) { var m = mutations[i]; for (var j = 0; j < m.addedNodes.length; j++) { var n = m.addedNodes[j]; if (n.nodeType === 3) { // text node if (shouldReplaceTextNode(n.nodeValue)) { replaceInTextNode(n, replacement.display); } } else if (n.nodeType === 1) { // element: scan its subtree quickly for text matches and tel anchors try { // Text nodes under this element var walker = document.createTreeWalker(n, NodeFilter.SHOW_TEXT, textNodeFilter); var node; while ((node = walker.nextNode())) { replaceInTextNode(node, replacement.display); } // tel anchors under this element if (n.querySelectorAll) { var anchors = n.querySelectorAll('a[href^="tel:"]'); for (var k = 0; k < anchors.length; k++) { var a = anchors[k]; var href = a.getAttribute("href") || ""; var digits = normalizeDigits(href); if (digits === targetTelDigits) { a.setAttribute("href", "tel:" + replacement.tel); var t = (a.textContent || "").trim(); if (TARGET_TEXTS.indexOf(t) > -1 || normalizeDigits(t) === targetTelDigits) { a.textContent = replacement.display; } } } } } catch (e) {} } } } }); observer.observe(document.documentElement || document.body, { childList: true, subtree: true }); } /** * Builds a case-insensitive lookup map for campaign numbers (cached) * @returns {Object} Lowercase campaign key to display number map */ var campaignLookup = (function() { var lookup = {}; for (var key in CAMPAIGN_NUMBERS) { if (CAMPAIGN_NUMBERS.hasOwnProperty(key)) { lookup[key.toLowerCase()] = CAMPAIGN_NUMBERS[key]; } } return lookup; })(); /** * Looks up a campaign number by name (case insensitive) * @param {string} campaign - Campaign name to look up * @returns {Object|null} Object with display and tel properties, or null if not found */ function getCampaignNumber(campaign) { if (!campaign) return null; var display = campaignLookup[campaign.toLowerCase()]; if (!display) return null; return { display: display, tel: normalizeDigits(display) }; } /** * Main initialization function * Checks for se_campaign parameter, loads stored config, and performs replacements * Priority: stored config > new campaign parameter > no replacement */ function init() { var campaign = getQueryParam("se_campaign"); var fromStorage = loadNumberConfig(); var chosen = null; // If we already have a stored campaign, use it (don't override) if (fromStorage && fromStorage.display && fromStorage.tel) { chosen = fromStorage; } else if (campaign) { // Only set new campaign if nothing is stored chosen = getCampaignNumber(campaign); if (chosen) { saveNumberConfig(chosen); } } if (!chosen) return; // nothing to do // Perform initial replacement and observe for future dynamic content if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", function () { replaceEverywhere(chosen); setupObserver(chosen); }); } else { replaceEverywhere(chosen); setupObserver(chosen); } } init(); })();