| 1 |
/** |
| 2 |
* aBlocks — Query Param Passthrough (frontend). |
| 3 |
* |
| 4 |
* Reads tracked query params (ref, utm_*, …) from the current URL and appends |
| 5 |
* whichever are present to every "tagged" link on the page. A link is tagged |
| 6 |
* when it (or a wrapping block) carries the configured CSS class — so it works |
| 7 |
* both when the class sits on the <a> and when Gutenberg puts it on the block |
| 8 |
* wrapper with the <a> nested inside. |
| 9 |
* |
| 10 |
* Config is injected before this file via wp_add_inline_script as |
| 11 |
* window.ABlocksLinkPassthrough = { keys: string[], linkClass: string }. |
| 12 |
*/ |
| 13 |
( function () { |
| 14 |
'use strict'; |
| 15 |
|
| 16 |
var config = window.ABlocksLinkPassthrough; |
| 17 |
if ( ! config || ! Array.isArray( config.keys ) || ! config.keys.length ) { |
| 18 |
return; |
| 19 |
} |
| 20 |
if ( |
| 21 |
typeof window.URL !== 'function' || |
| 22 |
typeof window.URLSearchParams !== 'function' |
| 23 |
) { |
| 24 |
return; // Very old browser — bail rather than throw. |
| 25 |
} |
| 26 |
|
| 27 |
var mode = config.mode || 'class'; |
| 28 |
var linkClass = ( config.linkClass || 'aff-link' ).trim(); |
| 29 |
var keywords = ( Array.isArray( config.keywords ) ? config.keywords : [] ) |
| 30 |
.map( function ( word ) { |
| 31 |
return String( word ).toLowerCase(); |
| 32 |
} ) |
| 33 |
.filter( Boolean ); |
| 34 |
|
| 35 |
// Nothing can match — bail before touching the DOM. |
| 36 |
if ( mode === 'class' && ! linkClass ) { |
| 37 |
return; |
| 38 |
} |
| 39 |
if ( mode === 'keyword' && ! keywords.length ) { |
| 40 |
return; |
| 41 |
} |
| 42 |
|
| 43 |
var persist = config.persist !== false; |
| 44 |
var cookieDays = parseInt( config.cookieDays, 10 ); |
| 45 |
if ( isNaN( cookieDays ) ) { |
| 46 |
cookieDays = 30; |
| 47 |
} |
| 48 |
var COOKIE = 'ablocks_pt'; |
| 49 |
|
| 50 |
function readCookie( name ) { |
| 51 |
var parts = ( document.cookie || '' ).split( '; ' ); |
| 52 |
for ( var i = 0; i < parts.length; i++ ) { |
| 53 |
var eq = parts[ i ].indexOf( '=' ); |
| 54 |
if ( eq > -1 && parts[ i ].slice( 0, eq ) === name ) { |
| 55 |
return decodeURIComponent( parts[ i ].slice( eq + 1 ) ); |
| 56 |
} |
| 57 |
} |
| 58 |
return ''; |
| 59 |
} |
| 60 |
|
| 61 |
function writeCookie( name, value, days ) { |
| 62 |
var expires = ''; |
| 63 |
if ( days > 0 ) { |
| 64 |
var d = new Date(); |
| 65 |
d.setTime( d.getTime() + days * 24 * 60 * 60 * 1000 ); |
| 66 |
expires = '; expires=' + d.toUTCString(); |
| 67 |
} |
| 68 |
document.cookie = |
| 69 |
name + |
| 70 |
'=' + |
| 71 |
encodeURIComponent( value ) + |
| 72 |
expires + |
| 73 |
'; path=/; SameSite=Lax'; |
| 74 |
} |
| 75 |
|
| 76 |
// Fresh values come from the current URL; remembered values come from the |
| 77 |
// cookie. The URL always wins (a newer ?ref= overrides a stored one). |
| 78 |
var current = new URLSearchParams( window.location.search ); |
| 79 |
var fresh = {}; |
| 80 |
config.keys.forEach( function ( key ) { |
| 81 |
var value = current.get( key ); |
| 82 |
if ( value !== null && value !== '' ) { |
| 83 |
fresh[ key ] = value; |
| 84 |
} |
| 85 |
} ); |
| 86 |
|
| 87 |
var stored = {}; |
| 88 |
if ( persist ) { |
| 89 |
var raw = readCookie( COOKIE ); |
| 90 |
if ( raw ) { |
| 91 |
try { |
| 92 |
new URLSearchParams( raw ).forEach( function ( value, key ) { |
| 93 |
if ( |
| 94 |
config.keys.indexOf( key ) !== -1 && |
| 95 |
value !== '' && |
| 96 |
fresh[ key ] === undefined |
| 97 |
) { |
| 98 |
stored[ key ] = value; |
| 99 |
} |
| 100 |
} ); |
| 101 |
} catch ( e ) {} |
| 102 |
} |
| 103 |
} |
| 104 |
|
| 105 |
// Effective set = remembered values overlaid by anything fresh in the URL. |
| 106 |
var effective = {}; |
| 107 |
config.keys.forEach( function ( key ) { |
| 108 |
if ( fresh[ key ] !== undefined ) { |
| 109 |
effective[ key ] = fresh[ key ]; |
| 110 |
} else if ( stored[ key ] !== undefined ) { |
| 111 |
effective[ key ] = stored[ key ]; |
| 112 |
} |
| 113 |
} ); |
| 114 |
|
| 115 |
var present = Object.keys( effective ).map( function ( key ) { |
| 116 |
return [ key, effective[ key ] ]; |
| 117 |
} ); |
| 118 |
|
| 119 |
if ( ! present.length ) { |
| 120 |
return; |
| 121 |
} |
| 122 |
|
| 123 |
// Persist whenever the URL brought fresh params, so later pages that drop |
| 124 |
// the query string can still recover them from the cookie. |
| 125 |
if ( persist && Object.keys( fresh ).length ) { |
| 126 |
var out = new URLSearchParams(); |
| 127 |
present.forEach( function ( pair ) { |
| 128 |
out.set( pair[ 0 ], pair[ 1 ] ); |
| 129 |
} ); |
| 130 |
writeCookie( COOKIE, out.toString(), cookieDays ); |
| 131 |
} |
| 132 |
|
| 133 |
// In 'class' mode, match the <a> directly or any <a> inside a tagged |
| 134 |
// wrapper; other modes start from every link and filter per anchor. |
| 135 |
var selector = |
| 136 |
mode === 'class' |
| 137 |
? 'a.' + linkClass + ', .' + linkClass + ' a' |
| 138 |
: 'a[href]'; |
| 139 |
|
| 140 |
function decorate( root ) { |
| 141 |
var anchors; |
| 142 |
try { |
| 143 |
anchors = ( root || document ).querySelectorAll( selector ); |
| 144 |
} catch ( e ) { |
| 145 |
return; |
| 146 |
} |
| 147 |
|
| 148 |
anchors.forEach( function ( anchor ) { |
| 149 |
var href = anchor.getAttribute( 'href' ); |
| 150 |
if ( ! href ) { |
| 151 |
return; |
| 152 |
} |
| 153 |
|
| 154 |
var lowered = href.toLowerCase().replace( /^\s+/, '' ); |
| 155 |
if ( |
| 156 |
lowered.charAt( 0 ) === '#' || |
| 157 |
lowered.indexOf( 'mailto:' ) === 0 || |
| 158 |
lowered.indexOf( 'tel:' ) === 0 || |
| 159 |
lowered.indexOf( 'javascript:' ) === 0 || // eslint-disable-line no-script-url |
| 160 |
lowered.indexOf( 'data:' ) === 0 |
| 161 |
) { |
| 162 |
return; |
| 163 |
} |
| 164 |
|
| 165 |
var url; |
| 166 |
try { |
| 167 |
url = new URL( href, window.location.href ); |
| 168 |
} catch ( e ) { |
| 169 |
return; |
| 170 |
} |
| 171 |
|
| 172 |
if ( url.protocol !== 'http:' && url.protocol !== 'https:' ) { |
| 173 |
return; |
| 174 |
} |
| 175 |
|
| 176 |
// 'all' mode is same-site only, so the ref is never leaked to |
| 177 |
// unrelated external domains. |
| 178 |
if ( mode === 'all' && url.origin !== window.location.origin ) { |
| 179 |
return; |
| 180 |
} |
| 181 |
|
| 182 |
// 'keyword' mode only touches links whose URL contains a word. |
| 183 |
if ( mode === 'keyword' ) { |
| 184 |
var haystack = url.href.toLowerCase(); |
| 185 |
var matched = keywords.some( function ( word ) { |
| 186 |
return haystack.indexOf( word ) !== -1; |
| 187 |
} ); |
| 188 |
if ( ! matched ) { |
| 189 |
return; |
| 190 |
} |
| 191 |
} |
| 192 |
|
| 193 |
var changed = false; |
| 194 |
present.forEach( function ( pair ) { |
| 195 |
if ( url.searchParams.get( pair[ 0 ] ) === pair[ 1 ] ) { |
| 196 |
return; // Already carries this exact value. |
| 197 |
} |
| 198 |
url.searchParams.set( pair[ 0 ], pair[ 1 ] ); |
| 199 |
changed = true; |
| 200 |
} ); |
| 201 |
|
| 202 |
if ( changed ) { |
| 203 |
anchor.setAttribute( 'href', url.toString() ); |
| 204 |
} |
| 205 |
} ); |
| 206 |
} |
| 207 |
|
| 208 |
function run() { |
| 209 |
decorate( document ); |
| 210 |
|
| 211 |
// Re-decorate links injected later (interactive blocks, AJAX content…). |
| 212 |
if ( typeof window.MutationObserver === 'function' ) { |
| 213 |
var observer = new MutationObserver( function ( mutations ) { |
| 214 |
for ( var i = 0; i < mutations.length; i++ ) { |
| 215 |
if ( mutations[ i ].addedNodes.length ) { |
| 216 |
decorate( document ); |
| 217 |
return; |
| 218 |
} |
| 219 |
} |
| 220 |
} ); |
| 221 |
observer.observe( document.body, { |
| 222 |
childList: true, |
| 223 |
subtree: true, |
| 224 |
} ); |
| 225 |
} |
| 226 |
} |
| 227 |
|
| 228 |
if ( document.readyState === 'loading' ) { |
| 229 |
document.addEventListener( 'DOMContentLoaded', run ); |
| 230 |
} else { |
| 231 |
run(); |
| 232 |
} |
| 233 |
} )(); |
| 234 |
|