PluginProbe ʕ •ᴥ•ʔ
Advanced Ads – Ad Manager & AdSense / 2.0.25
Advanced Ads – Ad Manager & AdSense v2.0.25
2.0.25 2.0.24 2.0.23 2.0.22 2.0.21 1.38.0 1.39.0 1.39.1 1.39.2 1.39.3 1.39.4 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.40.0 1.40.1 1.40.2 1.41.0 1.42.0 1.42.1 1.42.2 1.43.0 1.44.0 1.44.1 1.45.0 1.45.1 1.46.0 1.47.0 1.47.1 1.47.2 1.47.3 1.47.4 1.47.5 1.48.0 1.48.1 1.49.0 1.5.0 1.5.0.1 1.5.1 1.5.2 1.5.2.1 1.5.4 1.5.4.1 1.5.5 1.50.0 1.51.0 1.51.1 1.51.2 1.51.3 1.52.0 1.52.1 1.52.2 1.52.3 1.52.4 1.53.0 1.53.1 1.53.2 1.54.0 1.54.1 1.55.0 1.56.0 1.56.1 1.56.2 1.56.3 1.56.4 1.6 1.6.1 1.6.10 1.6.10.1 1.6.10.2 1.6.11 1.6.11.1 1.6.12 1.6.13 1.6.14 1.6.15 1.6.16 1.6.17 1.6.17.1 1.6.17.2 1.6.2 1.6.2.1 1.6.3 1.6.4 1.6.4.1 1.6.5 1.6.6 1.6.6.1 1.6.7 1.6.7.1 1.6.8 1.6.8.1 1.6.8.2 1.6.8.3 1.6.9 1.6.9.1 1.6.9.2 1.6.9.3 1.6.9.4 1.7 1.7.0.1 1.7.0.2 1.7.0.3 1.7.1 1.7.1.1 1.7.1.2 1.7.1.3 1.7.1.4 1.7.1.5 1.7.10 trunk 1.7.11 1.0.1 1.7.12 1.0.2 1.7.13 1.0.3 1.7.14 1.1.0 1.7.15 1.1.1 1.7.16 1.1.2 1.7.17 1.1.3 1.7.18 1.10 1.7.19 1.10.1 1.7.2 1.10.10 1.7.2.1 1.10.11 1.7.20 1.10.12 1.7.21 1.10.2 1.7.22 1.10.3 1.7.23 1.10.4 1.7.24 1.10.5 1.7.25 1.10.6 1.7.3 1.10.7 1.7.4 1.10.8 1.7.4.1 1.10.9 1.7.4.2 1.11 1.7.4.3 1.11.1 1.7.4.4 1.11.2 1.7.4.5 1.12 1.7.5 1.13 1.7.5.1 1.13.1 1.7.6 1.13.2 1.7.7 1.13.3 1.7.8 1.13.4 1.7.9 1.13.5 1.7.9.1 1.13.6 1.7.9.2 1.13.7 1.7.9.3 1.13.8 1.8 1.14 1.8.1 1.14.1 1.8.10 1.14.10 1.8.11 1.14.11 1.8.12 1.14.2 1.8.13 1.14.3 1.8.14 1.14.4 1.8.15 1.14.5 1.8.16 1.14.6 1.8.17 1.14.7 1.8.18 1.14.8 1.8.19 1.14.9 1.8.2 1.15 1.8.20 1.16 1.8.21 1.16.1 1.8.22 1.17 1.8.23 1.17.1 1.8.24 1.17.10 1.8.25 1.17.10-rc.1 1.8.26 1.17.11 1.8.27 1.17.12 1.8.28 1.17.12-rc.1 1.8.29 1.17.2 1.8.3 1.17.3 1.8.30 1.17.4 1.8.4 1.17.5 1.8.5 1.17.6 1.8.6 1.17.7 1.8.7 1.17.8 1.8.8 1.17.9 1.8.9 1.17.9-beta.1 1.9 1.18.0 2.0.0 1.19.0 2.0.1 1.19.1 2.0.10 1.2 2.0.11 1.2.1 2.0.12 1.2.2 2.0.13 1.2.3 2.0.14 1.2.4 2.0.15 1.2.5 2.0.16 1.2.6 2.0.17 1.2.7 2.0.18 1.20.0 2.0.19 1.20.0-rc.1 2.0.2 1.20.0-rc.2 2.0.20 1.20.1 2.0.3 1.20.2 2.0.4 1.20.3 2.0.5 1.21.0 2.0.6 1.21.1 2.0.7 1.22.0 2.0.8 1.22.1 2.0.9 1.22.2 1.23.0 1.23.1 1.23.2 1.24.0 1.24.1 1.24.2 1.25.0 1.25.1 1.26.0 1.27.0 1.28.0 1.29.0 1.29.1 1.3 1.3.1 1.3.10 1.3.11 1.3.12 1.3.13 1.3.14 1.3.15 1.3.16 1.3.17 1.3.18 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.30.0 1.30.1 1.30.2 1.30.2-rc.1 1.30.3 1.30.4 1.30.4-rc.1 1.30.5 1.31.0 1.31.1 1.32.0 1.32.0-rc.1 1.33.0 1.33.1 1.33.2 1.34.0 1.35.0 1.35.1 1.36.0 1.36.1 1.36.2 1.36.3 1.37.0 1.37.1 1.37.2
advanced-ads / src / public / frontend-picker.js
advanced-ads / src / public Last commit date
advanced.js 3 months ago frontend-picker.js 2 months ago
frontend-picker.js
408 lines
1 /* eslint-disable @wordpress/no-unused-vars-before-return */
2 // Highlight elements in frontend, if local storage variable is set.
3 /* global advads, localStorage */
4 document.addEventListener( 'DOMContentLoaded', function () {
5 /**
6 * Read all localStorage values once — avoids repeated synchronous DOM API calls.
7 */
8 function isEnabled() {
9 if ( ! advads.supports_localstorage() ) {
10 return false;
11 }
12
13 const picker = localStorage.getItem( 'advads_frontend_picker' );
14 const blogId = localStorage.getItem( 'advads_frontend_blog_id' );
15 const startTime = localStorage.getItem( 'advads_frontend_starttime' );
16 const optBlogId = globalThis.advads_options.blog_id;
17
18 if ( ! picker ) {
19 return false;
20 }
21
22 // Check if the frontend picker was started on the current blog.
23 if ( optBlogId && blogId && optBlogId !== blogId ) {
24 return false;
25 }
26
27 // Deactivate if started more than 45 minutes ago.
28 if (
29 startTime &&
30 parseInt( startTime, 10 ) < Date.now() - 45 * 60 * 1000
31 ) {
32 [
33 'advads_frontend_action',
34 'advads_frontend_element',
35 'advads_frontend_picker',
36 'advads_prev_url',
37 'advads_frontend_pathtype',
38 'advads_frontend_boundary',
39 'advads_frontend_blog_id',
40 'advads_frontend_starttime',
41 ].forEach( ( key ) => localStorage.removeItem( key ) );
42
43 advads.set_cookie( 'advads_frontend_picker', '', -1 );
44 return false;
45 }
46
47 return true;
48 }
49
50 if ( ! isEnabled() ) {
51 return;
52 }
53
54 let pickerCur = null;
55
56 // Build the overlay with plain DOM APIs.
57 const overlay = document.createElement( 'div' );
58 overlay.id = 'advads-picker-overlay';
59 Object.assign( overlay.style, {
60 position: 'absolute',
61 border: 'solid 2px #428bca',
62 backgroundColor: 'rgba(66,139,202,0.5)',
63 boxSizing: 'border-box',
64 zIndex: '1000000',
65 pointerEvents: 'none',
66 display: 'none',
67 } );
68 document.body.prepend( overlay );
69
70 if ( 'true' === localStorage.getItem( 'advads_frontend_boundary' ) ) {
71 document.body.style.cursor = 'not-allowed';
72 }
73
74 // Use a Set for O(1) membership test on every mousemove.
75 const pickerNo = new Set( [
76 document.body,
77 document.documentElement,
78 document,
79 ] );
80
81 /**
82 * Check if we can traverse up the DOM tree.
83 *
84 * @param {HTMLElement} el
85 * @return {boolean} true if the element is a boundary, false otherwise
86 */
87 globalThis.advads.is_boundary_reached = function ( el ) {
88 if ( 'true' !== localStorage.getItem( 'advads_frontend_boundary' ) ) {
89 return false;
90 }
91
92 const helpers = document.querySelectorAll(
93 '.advads-frontend-picker-boundary-helper'
94 );
95 const boundaries = new Set(
96 Array.from( helpers )
97 .map( ( h ) => h.parentElement )
98 .filter( Boolean )
99 );
100
101 boundaries.forEach( ( b ) => {
102 b.style.cursor = 'pointer';
103 } );
104
105 // Is el itself a boundary, or does it have no boundary ancestor?
106 if ( boundaries.has( el ) ) {
107 return true;
108 }
109
110 let node = el.parentElement;
111 while ( node ) {
112 if ( boundaries.has( node ) ) {
113 return false;
114 }
115 node = node.parentElement;
116 }
117 return true;
118 };
119
120 // Determine path function once.
121 const fn =
122 'xpath' === localStorage.getItem( 'advads_frontend_pathtype' )
123 ? getXPath
124 : getPath;
125
126 /**
127 * Throttle mousemove with requestAnimationFrame — fires 60-100x/sec otherwise.
128 * Each move triggers a full DOM traversal via getPath/getXPath.
129 * rAF limits processing to once per render frame (~16ms).
130 */
131 let rafPending = false;
132 let lastMouseEvent = null;
133
134 document.addEventListener( 'mousemove', function ( e ) {
135 lastMouseEvent = e;
136 if ( rafPending ) {
137 return;
138 }
139
140 rafPending = true;
141 globalThis.requestAnimationFrame( function () {
142 rafPending = false;
143
144 const ev = lastMouseEvent;
145 if ( ! ev ) {
146 return;
147 }
148
149 if ( ev.target === pickerCur ) {
150 return;
151 }
152
153 if ( pickerNo.has( ev.target ) ) {
154 pickerCur = null;
155 overlay.style.display = 'none';
156 return;
157 }
158
159 pickerCur = ev.target;
160
161 const path = fn( pickerCur );
162 if ( ! path ) {
163 return;
164 }
165
166 // DEBUG only — remove in production.
167 // console.log( getPath( pickerCur ), 'getPath' );
168 // console.log( getXPath( pickerCur ), 'getXPath' );
169
170 const rect = pickerCur.getBoundingClientRect();
171 const scrollX = window.scrollX || window.pageXOffset;
172 const scrollY = window.scrollY || window.pageYOffset;
173
174 Object.assign( overlay.style, {
175 top: rect.top + scrollY + 'px',
176 left: rect.left + scrollX + 'px',
177 width: rect.width + 'px',
178 height: rect.height + 'px',
179 display: 'block',
180 } );
181 } );
182 } );
183
184 // Save on click.
185 document.addEventListener( 'click', function () {
186 if ( ! pickerCur ) {
187 return;
188 }
189 if ( advads.is_boundary_reached( pickerCur ) ) {
190 return;
191 }
192
193 const path = fn( pickerCur );
194 localStorage.setItem( 'advads_frontend_element', path );
195 globalThis.location = localStorage.getItem( 'advads_prev_url' );
196 } );
197 } );
198
199 /* --------------------------------------------------------------------------
200 * Path helpers — no jQuery dependency.
201 * Both use iterative while loops over raw DOM APIs (el.parentNode, el.tagName,
202 * el.id, el.className) instead of recursive jQuery calls, eliminating per-node
203 * jQuery object allocation, .attr(), .siblings(), .addBack(), .index() overhead.
204 * -------------------------------------------------------------------------- */
205
206 const OVERLAY_ID = 'advads-picker-overlay';
207 const MAX_DEPTH = 3;
208 const HAS_DIGIT = /\d/;
209
210 /**
211 * Returns true if cls contains any digit character.
212 *
213 * @param {string} cls
214 * @return {boolean} true if the class contains any digit character, false otherwise
215 */
216 function hasDigit( cls ) {
217 return HAS_DIGIT.test( cls );
218 }
219
220 /**
221 * Get up to `limit` CSS classes from an element's className,
222 * skipping any class that contains a digit.
223 *
224 * @param {string} className Raw className string.
225 * @param {number} limit Max classes to return.
226 * @return {string[]} up to `limit` CSS classes from an element's className, skipping any class that contains a digit
227 */
228 function getFilteredClasses( className, limit ) {
229 if ( ! className ) {
230 return [];
231 }
232
233 const result = [];
234 for ( const cls of className.split( /[\s\n]+/ ) ) {
235 if ( cls && ! hasDigit( cls ) ) {
236 result.push( cls );
237 if ( result.length === limit ) {
238 break;
239 }
240 }
241 }
242 return result;
243 }
244
245 /**
246 * Count children of `parent` matching `tagName` (and optionally classes),
247 * excluding the overlay element. Returns { total, selfIndex }.
248 *
249 * @param {HTMLElement} parent
250 * @param {HTMLElement} self
251 * @param {string} tagName
252 * @param {string[]} classes Optional class list to match against.
253 * @return {{ total: number, selfIndex: number }} total number of children matching the tagName and classes, and the index of the self element
254 */
255 function getSiblingIndex( parent, self, tagName, classes ) {
256 if ( ! parent ) {
257 return { total: 0, selfIndex: -1 };
258 }
259
260 const tag = tagName.toLowerCase();
261 const hasClasses = classes && classes.length > 0;
262
263 let total = 0;
264 let selfIndex = -1;
265
266 for ( const child of parent.children ) {
267 if ( child.id === OVERLAY_ID ) {
268 continue;
269 }
270 if ( child.nodeName.toLowerCase() !== tag ) {
271 continue;
272 }
273
274 if ( hasClasses ) {
275 // Check all required classes are present.
276 const childClasses = child.className
277 ? child.className.split( /\s+/ )
278 : [];
279 const classSet = new Set( childClasses );
280 if ( ! classes.every( ( c ) => classSet.has( c ) ) ) {
281 continue;
282 }
283 }
284
285 if ( child === self ) {
286 selfIndex = total;
287 }
288 total++;
289 }
290
291 return { total, selfIndex };
292 }
293
294 /**
295 * Get a CSS-selector-style path for the element.
296 * Walks up to MAX_DEPTH levels, stopping at <html>.
297 *
298 * @param {HTMLElement} el
299 * @return {string} a CSS-selector-style path for the element
300 */
301 function getPath( el ) {
302 const parts = [];
303 let node = el;
304
305 while (
306 node &&
307 node.nodeName.toLowerCase() !== 'html' &&
308 parts.length < MAX_DEPTH
309 ) {
310 const tag = node.nodeName.toLowerCase();
311 const elId = node.id;
312 const classes = getFilteredClasses( node.className, 2 );
313
314 let cur = tag;
315
316 if ( elId && ! hasDigit( elId ) ) {
317 cur += '#' + elId;
318 } else if ( classes.length ) {
319 cur += '.' + classes.join( '.' );
320 }
321
322 const { total, selfIndex } = getSiblingIndex(
323 node.parentElement,
324 node,
325 tag,
326 classes
327 );
328
329 if ( total > 1 && selfIndex !== -1 ) {
330 // :eq() is 0-based — matches jQuery's selector behaviour.
331 cur += ':eq(' + selfIndex + ')';
332 }
333
334 parts.unshift( cur );
335 node = node.parentElement;
336 }
337
338 if ( node && node.nodeName.toLowerCase() === 'html' ) {
339 parts.unshift( 'html' );
340 }
341
342 return parts.join( ' > ' );
343 }
344
345 /**
346 * Get an XPath expression for the element.
347 * Walks up to MAX_DEPTH levels, stopping at <body>.
348 * XPath indexes are 1-based — selfIndex + 1 corrects the :eq() off-by-one.
349 *
350 * @param {HTMLElement} el
351 * @return {string} an XPath expression for the element
352 */
353 function getXPath( el ) {
354 const parts = [];
355 let node = el;
356 let depth = 0;
357
358 while (
359 node &&
360 node.nodeName.toLowerCase() !== 'body' &&
361 depth < MAX_DEPTH
362 ) {
363 if ( advads.is_boundary_reached( node ) ) {
364 break;
365 }
366
367 const tag = node.nodeName.toLowerCase();
368 const elId = node.id;
369 const classes = getFilteredClasses( node.className, 2 );
370
371 let cur = tag;
372
373 if ( elId && ! hasDigit( elId ) ) {
374 // @id is unique — return immediately with a rooted //tag[@id="..."] path.
375 parts.unshift( cur + '[@id="' + elId + '"]' );
376 return '//' + parts.join( '/' );
377 }
378
379 if ( classes.length ) {
380 depth++;
381 const xpathClasses = classes.map(
382 ( cls ) =>
383 '(@class and contains(concat(" ", normalize-space(@class), " "), " ' +
384 cls +
385 ' "))'
386 );
387 cur += '[' + xpathClasses.join( ' and ' ) + ']';
388 }
389
390 const { total, selfIndex } = getSiblingIndex(
391 node.parentElement,
392 node,
393 tag,
394 classes
395 );
396
397 if ( total > 1 && selfIndex !== -1 ) {
398 // XPath uses 1-based indexing — +1 corrects the 0-based selfIndex.
399 cur += '[' + ( selfIndex + 1 ) + ']';
400 }
401
402 parts.unshift( cur );
403 node = node.parentElement;
404 }
405
406 return parts.join( '/' );
407 }
408