PageSourceSearch

https://www.parker.edu/wp-content/plugins/parker-salesforce-integr…rker-elementor-visibility.js?ver=1790090288

js parker.edu collected 2026-09-24 09:46:53 UTC 17,241 bytes, 448 lines download raw bytes

1/**
2 * Resolve Elementor lead-source visibility rules, in the browser.
3 *
4 * ============================================================================
5 * WHY THE BROWSER AND NOT PHP. Pages are cached by WP Engine for 180 days and
6 * again by Cloudflare, so a server-side decision would be baked into the cached
7 * HTML: the first visitor's attribution would pick a version and everybody
8 * after them would be served it. See ElementorVisibility.php for the headers.
9 *
10 * ============================================================================
11 * THIS HIDES CONTENT, IT DOES NOT PROTECT IT. The markup is always sent. Hidden
12 * elements are in view-source and in the DOM. Nothing confidential belongs
13 * behind one of these rules.
14 *
15 * ============================================================================
16 * THE ORDER OF EVENTS MATTERS MORE THAN ANYTHING ELSE HERE.
17 *
18 *   "show only if"  is hidden by CSS printed in the head, so it is never
19 *                   painted. This script reveals it on a match.
20 *
21 *   "hide if"       renders visible, because the page must be usable without
22 *                   JavaScript. This script removes it on a match, and runs as
23 *                   early as it can - every frame it is visible is a frame
24 *                   somebody sees the wrong copy.
25 *
26 * So this runs at parse time in the head, resolves whatever is already in the
27 * DOM, and then watches for the rest. It does NOT wait for DOMContentLoaded:
28 * waiting would guarantee the flash it exists to avoid.
29 */
30(function () {
31    'use strict';
32
33    var ATTR = 'data-pmip-vis';
34
35    // THE TRACKER MAY NOT HAVE LOADED YET, and this deliberately does not
36    // depend on it - a dependency would order this after it and cost the
37    // early paint. So: try, and if its API is not there yet, try again on
38    // DOMContentLoaded and once more on load. Three attempts covers a deferred
39    // tracker without polling forever.
40    var tiers = null;
41    var resolved = false;
42
43    function readTiers() {
44        try {
45            if (window.ParkerUtmTracking && typeof window.ParkerUtmTracking.derive === 'function') {
46                return window.ParkerUtmTracking.derive() || null;
47            }
48        } catch (e) { /* a broken tracker must not take the page with it */ }
49
50        return null;
51    }
52
53    /**
54     * Does this element's rule match the visitor?
55     *
56     * TIER 4 MATCHES ON CONTAINS, the rest on equals. Tier 4 is free text
57     * holding the campaign - real values look like "spring-dc-campaign |
58     * edu-chat-bot", and nobody is going to type that into a rule. Tiers 1-3
59     * are restricted picklists, where an exact value is the only correct test.
60     */
61    function matches(tier, wanted, actual) {
62        if (!actual) { return false; }
63
64        var i;
65
66        if (tier === 'tier4') {
67            var haystack = String(actual).toLowerCase();
68            for (i = 0; i < wanted.length; i++) {
69                if (haystack.indexOf(String(wanted[i]).toLowerCase()) !== -1) {
70                    return true;
71                }
72            }
73            return false;
74        }
75
76        for (i = 0; i < wanted.length; i++) {
77            if (String(wanted[i]) === String(actual)) {
78                return true;
79            }
80        }
81
82        return false;
83    }
84
85    /**
86     * Do ALL of an element's conditions hold?
87     *
88     * TIERS NARROW, THEY DO NOT WIDEN. Tier 1 "Paid" with Tier 2 "Paid Search"
89     * means paid search - so every condition must hold, and one that fails
90     * settles it.
91     *
92     * VALUES WITHIN A TIER ARE THE OTHER WAY ROUND: any one counts, because a
93     * visitor has exactly one value per tier and requiring two would describe
94     * nobody. That asymmetry is the whole grammar, and it is not arbitrary - it
95     * follows from a tier holding one value at a time.
96     *
97     * NO CONDITIONS IS NOT A MATCH. An element that reached here with an empty
98     * rule set has no opinion, and callers treat that as "no rule" rather than
99     * as a rule that matches everybody.
100     */
101    function matchesAll(rules) {
102        if (!rules || !rules.length) { return false; }
103
104        for (var i = 0; i < rules.length; i++) {
105            var rule = rules[i];
106            var vals = rule && rule.values;
107
108            if (!vals || !vals.length) { return false; }
109
110            if (!matches(rule.tier, vals, tiers ? tiers[rule.tier] : '')) {
111                return false;
112            }
113        }
114
115        return true;
116    }
117
118    /** An element's conditions, parsed once. */
119    function rulesOf(el) {
120        try {
121            var raw = el.getAttribute(ATTR + '-rules');
122            var out = raw ? JSON.parse(raw) : [];
123            return Array.isArray(out) ? out : [];
124        } catch (e) {
125            return [];
126        }
127    }
128
129    function resolve(el) {
130        // ONCE PER ELEMENT. The observer can see the same node twice - once
131        // when it is added and again if an ancestor is replaced - and a second
132        // pass on an already-revealed element would re-hide it.
133        if (el.getAttribute('data-pmip-vis-done') === '1') { return; }
134
135        // GROUP MEMBERS ARE NOT RESOLVED HERE. Their visibility is decided by
136        // the draw, which needs the whole group; revealing one early on its
137        // tier rule alone would show two variants at once.
138        if (el.hasAttribute(AB)) { return; }
139
140        var mode = el.getAttribute(ATTR);
141        if (mode !== 'show' && mode !== 'hide') { return; }
142
143        var rules = rulesOf(el);
144
145        // A RULE WITH NO CONDITIONS IS NOT A RULE. "Show only if [nothing]"
146        // would hide the element from everybody; PHP already refuses to emit
147        // one, and this refuses to act on one that arrived anyway.
148        if (!rules.length) {
149            reveal(el);
150            return;
151        }
152
153        var hit = matchesAll(rules);
154
155        if (mode === 'show') {
156            if (hit) { reveal(el); }
157            // No match: it is already hidden by the head CSS. Left alone
158            // rather than hidden again - and deliberately NOT marked done, so
159            // a later attempt can still reveal it if the tracker was slow.
160            else if (resolved) { el.setAttribute('data-pmip-vis-done', '1'); }
161            return;
162        }
163
164        // hide
165        if (hit) {
166            el.style.setProperty('display', 'none', 'important');
167        }
168        el.setAttribute('data-pmip-vis-done', '1');
169    }
170
171    function reveal(el) {
172        el.removeAttribute(ATTR);
173        el.setAttribute('data-pmip-vis-done', '1');
174    }
175
176    // ================================================================
177    // A/B GROUPS.
178    //
179    // TARGETING DECIDES WHO IS ELIGIBLE, THIS DECIDES WHICH OF THEM SEES WHAT.
180    // Two variants carrying the same tier rule split that audience between
181    // them - a real test, because both groups are drawn at random from one
182    // population rather than being different people to begin with.
183    //
184    // A GROUP CANNOT BE RESOLVED ELEMENT BY ELEMENT, which is why it does not
185    // go through resolve(). The draw has to see every member before choosing
186    // one, so it waits for the document rather than running as the parser
187    // arrives. That costs nothing visually: group members are hidden by the
188    // head CSS regardless of their rule, so nothing is on screen to flash.
189    // ================================================================
190    var AB = 'data-pmip-ab';
191    var AB_KEY = 'parker_ab';
192
193    function readAssignments() {
194        try {
195            return JSON.parse(window.localStorage.getItem(AB_KEY) || '{}') || {};
196        } catch (e) { return {}; }
197    }
198
199    function writeAssignments(map) {
200        try {
201            window.localStorage.setItem(AB_KEY, JSON.stringify(map));
202        } catch (e) { /* private browsing - the draw still works, just per page */ }
203    }
204
205    /** Is this element eligible - that is, does its tier rule pass? */
206    function eligible(el) {
207        var mode = el.getAttribute(ATTR);
208
209        // NO TIER RULE MEANS ELIGIBLE FOR EVERYBODY. That is how a default
210        // variant is expressed: it wins whenever the targeted ones do not
211        // apply, so a targeted group still shows something to everyone else.
212        if (mode !== 'show' && mode !== 'hide') { return true; }
213
214        var rules = rulesOf(el);
215
216        if (!rules.length) { return true; }
217
218        var hit = matchesAll(rules);
219
220        return mode === 'show' ? hit : !hit;
221    }
222
223    /**
224     * Pick one member by weight.
225     *
226     * WEIGHTS ARE RELATIVE, NOT PERCENTAGES - 50/50 and 1/1 are the same split,
227     * and nothing has to add up to 100. A total of zero means every member was
228     * retired, which is a real state (somebody set every weight to 0) and must
229     * not divide by zero: it returns nothing and the group shows nothing, which
230     * is what "no variant is running" should look like.
231     */
232    function draw(members) {
233        var total = 0, i;
234
235        for (i = 0; i < members.length; i++) {
236            total += Math.max(0, parseInt(members[i].getAttribute(AB + '-weight'), 10) || 0);
237        }
238
239        if (total <= 0) { return null; }
240
241        var roll = Math.random() * total;
242
243        for (i = 0; i < members.length; i++) {
244            roll -= Math.max(0, parseInt(members[i].getAttribute(AB + '-weight'), 10) || 0);
245            if (roll < 0) { return members[i]; }
246        }
247
248        return members[members.length - 1];
249    }
250
251    /**
252     * The sticky decision, as a function so it can be exercised rather than
253     * read.
254     *
255     * WHY STICKINESS IS THE WHOLE TEST. A visitor who saw variant B on the
256     * landing page and C on the next has seen neither, and the numbers measure
257     * the coin rather than the content. Everything else here is plumbing; this
258     * is the property that makes the result mean anything.
259     *
260     * RE-DRAWN ONLY WHEN THE STORED CHOICE IS NO LONGER ELIGIBLE, which happens
261     * when somebody returns through a different campaign. Keeping it would show
262     * paid-search copy to an organic visit; re-drawing every time would destroy
263     * the stickiness above.
264     *
265     * @param  {Array}  open  Eligible members, in page order.
266     * @param  {string} want  The variant name stored for this visitor, if any.
267     * @return {{pick: (Object|null), fresh: boolean}} fresh = newly drawn, so
268     *         the caller knows whether anything needs storing.
269     */
270    function pickVariant(open, want) {
271        for (var i = 0; i < open.length; i++) {
272            if (want && open[i].getAttribute(AB + '-variant') === want) {
273                return { pick: open[i], fresh: false };
274            }
275        }
276
277        var drawn = draw(open);
278
279        return { pick: drawn, fresh: !!drawn };
280    }
281
282    /**
283     * Resolve every group on the page.
284     *
285     * STICKY, AND THAT IS NOT A NICETY. A visitor who saw variant B on the
286     * landing page and variant C on the next one has seen neither, and the
287     * data says nothing - the test measures the coin rather than the content.
288     * So the choice is stored and reused.
289     *
290     * RE-DRAWN ONLY IF THE STORED CHOICE HAS BECOME INELIGIBLE, which happens
291     * when somebody returns through a different campaign. Keeping a now-
292     * ineligible variant would show paid-search copy to an organic visit;
293     * re-drawing every time would destroy the stickiness that makes the test
294     * readable.
295     */
296    function resolveGroups() {
297        var nodes = document.querySelectorAll('[' + AB + ']');
298        if (!nodes.length) { return; }
299
300        var groups = {};
301        var i;
302
303        for (i = 0; i < nodes.length; i++) {
304            var name = nodes[i].getAttribute(AB);
305            if (!name) { continue; }
306            if (!groups[name]) { groups[name] = []; }
307            groups[name].push(nodes[i]);
308        }
309
310        var stored = readAssignments();
311        var shown  = [];
312        var dirty  = false;
313
314        Object.keys(groups).forEach(function (name) {
315            var members = groups[name];
316            var open    = [];
317
318            for (var j = 0; j < members.length; j++) {
319                if (eligible(members[j])) { open.push(members[j]); }
320            }
321
322            // NOTHING ELIGIBLE IS A REAL ANSWER, not an error. Every member was
323            // targeted and this visitor matched none of them, so the group
324            // shows nothing - which is why the editor panel says to leave one
325            // variant ruleless if something should always appear.
326            if (!open.length) { return; }
327
328            var chosen = pickVariant(open, stored[name]);
329            if (!chosen.pick) { return; }
330
331            var pick = chosen.pick;
332
333            if (chosen.fresh) {
334                stored[name] = pick.getAttribute(AB + '-variant');
335                dirty = true;
336            }
337
338            // REVEALED BY REMOVING THE ATTRIBUTE THE CSS KEYS ON, so a losing
339            // member keeps it and stays hidden. Nothing is ever set to
340            // display:block - the element's own layout is whatever Elementor
341            // gave it, and forcing a display value would break flex and grid
342            // containers.
343            pick.removeAttribute(AB);
344            pick.setAttribute('data-pmip-ab-won', '1');
345
346            shown.push(name + ':' + stored[name]);
347        });
348
349        if (dirty) { writeAssignments(stored); }
350
351        if (shown.length) { record(shown); }
352    }
353
354    /**
355     * Put the assignment where a lead will carry it.
356     *
357     * WITHOUT THIS THE SPLIT IS THEATRE. Randomising which section somebody
358     * sees and then not recording which one they saw produces a test that can
359     * never be read - and the question "did B convert better than C" is asked
360     * of the leads table, not of this page.
361     *
362     * WRITTEN INTO THE TRACKING STORE rather than posted anywhere: it then
363     * rides into the form on the next fill, exactly as the UTM values do, and
364     * lands on the lead row in tracking_json.
365     */
366    function record(shown) {
367        var value = shown.sort().join(',');
368
369        try {
370            var raw   = window.localStorage.getItem('parker_utm_data');
371            var store = raw ? JSON.parse(raw) : null;
372
373            if (store && typeof store === 'object') {
374                if (store.ab_variant === value) { return; }
375                store.ab_variant = value;
376                window.localStorage.setItem('parker_utm_data', JSON.stringify(store));
377            }
378        } catch (e) { /* never worth breaking the page for */ }
379
380        // Re-fill any form already on the page, so a visitor who opens one
381        // without navigating still carries the assignment.
382        try {
383            if (window.ParkerUtmTracking && typeof window.ParkerUtmTracking.refill === 'function') {
384                window.ParkerUtmTracking.refill();
385            }
386        } catch (e) { /* as above */ }
387    }
388
389    function sweep() {
390        tiers = readTiers();
391
392        var nodes = document.querySelectorAll('[' + ATTR + ']');
393        for (var i = 0; i < nodes.length; i++) {
394            resolve(nodes[i]);
395        }
396    }
397
398    // FIRST PASS IMMEDIATELY, on whatever has been parsed so far. In the head
399    // that is nothing, which is fine - the observer below catches every element
400    // as the parser reaches it, which is the earliest any of them can be acted
401    // on.
402    sweep();
403
404    if (window.MutationObserver) {
405        var observer = new MutationObserver(function (records) {
406            for (var i = 0; i < records.length; i++) {
407                var added = records[i].addedNodes;
408                for (var j = 0; j < added.length; j++) {
409                    var node = added[j];
410                    if (node.nodeType !== 1) { continue; }
411
412                    if (node.hasAttribute && node.hasAttribute(ATTR)) {
413                        resolve(node);
414                    }
415                    if (node.querySelectorAll) {
416                        var inner = node.querySelectorAll('[' + ATTR + ']');
417                        for (var k = 0; k < inner.length; k++) {
418                            resolve(inner[k]);
419                        }
420                    }
421                }
422            }
423        });
424
425        observer.observe(document.documentElement, { childList: true, subtree: true });
426
427        // STOPPED ONCE THE PAGE IS DONE. An observer left watching the whole
428        // document for the life of the page is a real cost on a builder page
429        // that animates, and there is nothing left for it to find.
430        window.addEventListener('load', function () {
431            sweep();
432            observer.disconnect();
433        });
434    }
435
436    // THE LAST WORD. By DOMContentLoaded the tracker has certainly defined its
437    // API, so this pass is the one that resolves anything the early passes saw
438    // before the tracker was ready. 'resolved' tells resolve() it may now mark
439    // a non-matching "show" element as finished.
440    document.addEventListener('DOMContentLoaded', function () {
441        resolved = true;
442        sweep();
443
444        // GROUPS LAST, and only here. The draw has to see every member before
445        // choosing one, so it cannot run while the parser is still arriving.
446        resolveGroups();
447    });
448})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.