PageSourceSearch

https://www.umassglobal.edu/sites/default/files/asset_injector/js/…-0b26ec558296a0c85a7d142b7058da8c.js?tm8iux

js umassglobal.edu collected 2026-10-02 05:03:18 UTC 15,439 bytes, 378 lines download raw bytes

1/**
2 * Generates a clean, isolated interaction_data object where all unused keys
3 * explicitly evaluate to `undefined` to force GTM memory resets and auto-pruning.
4 */
5function createInteractionPayload(eventSpecificData) {
6    const baseSchema = {
7        // Universal Core
8        type: undefined,
9        page_location: document.location.href,
10        page_title: document.title || undefined,
11        interaction_timestamp: Date.now(),
12        interaction_id: (window.gUMG && window.gUMG.interactionToken) || undefined,
13        hashed_ip: (window.gUMG && window.gUMG.clientData?.hashedIP) || undefined,
14
15        // Shared / Overlapping Fields
16        scroll_position: undefined,
17
18        // Click-Specific
19        link_url: undefined,
20        link_text: undefined,
21        outbound: undefined,
22        click_location: undefined,
23        click_type: undefined,
24        target_element: undefined,
25        target_element_meta: undefined,
26        landmark_info: undefined,
27        descendant_path: undefined,
28        click_number: undefined,
29        click_id: undefined,
30        click_on_touch: undefined,
31        pointer_type: undefined,
32        modifier_key: undefined,
33
34        // Scroll-Specific
35        scroll_trigger: undefined,
36        scroll_depth: undefined,
37        scroll_location: undefined,
38        total_scroll_px: undefined,
39        reversal_count: undefined,
40        pause_count: undefined,
41
42        // Type/Input-Specific
43        char_count: undefined,
44        backspace_count: undefined,
45        input_duration_ms: undefined,
46
47        // Timing-Specific
48        engaged_time_sec: undefined,
49        engaged_time_ms: undefined,
50        timing_trigger: undefined
51    };
52
53    // Merging specific event data overwrites defaults while keeping unused keys 'undefined'
54    return Object.assign({}, baseSchema, eventSpecificData);
55}
56
57(function () {
58    'use strict';
59
60    window.dataLayer = window.dataLayer || [];
61    window.clickStack = window.clickStack || [];
62
63    const clickStack = window.clickStack;
64    const bufferQueue = [];
65    const BUFFER_DELAY_MS = 2200;
66
67    /**
68     * Flushes any buffered clicks immediately on page unload.
69     */
70    function flushBuffer() {
71        while (bufferQueue.length > 0) {
72            dispatchBufferedClick(bufferQueue.shift());
73        }
74    }
75
76    window.addEventListener('visibilitychange', function () {
77        if (document.visibilityState === 'hidden') {
78            flushBuffer();
79        }
80    });
81    window.addEventListener('pagehide', flushBuffer);
82
83    /**
84     * Formats and pushes the click payload to window.dataLayer & clickStack.
85     */
86    function dispatchBufferedClick(bufferedEvent) {
87        if (!bufferedEvent) return;
88
89const payload = {
90    event: "user_interaction",
91    "interaction_data": createInteractionPayload({
92        "type": "click",
93
94        // Click-Specific Attributes
95        "link_url": bufferedEvent.targetURL || undefined,
96        "link_text": bufferedEvent.clickText || undefined,
97        "outbound": bufferedEvent.outbound,
98
99        // Page Context (Overrides factory default with buffered location/title)
100        "page_location": bufferedEvent.pageURL,
101        "page_title": bufferedEvent.pageTitle,
102
103        // Location & Coordinates
104        "click_location": bufferedEvent.clickLocation,
105        "scroll_position": bufferedEvent.scrollPosition,
106
107        // Single Categorical Behavioral State
108        "click_type": bufferedEvent.clickType,
109
110        // Target Element Identifiers & Accessibility Meta
111        "target_element": bufferedEvent.targetElement,
112        "target_element_meta": bufferedEvent.targetElementMeta,
113        "landmark_info": bufferedEvent.landmarkInfo,
114        "descendant_path": bufferedEvent.descendantPath || undefined,
115
116        // Interaction Metadata
117        "click_number": bufferedEvent.click,
118        "click_id": bufferedEvent.clickId,
119        "click_on_touch": bufferedEvent.clickOnTouch,
120        "pointer_type": bufferedEvent.pointerType || undefined,
121        "modifier_key": bufferedEvent.modifierKey,
122
123        // Session & Timing (Overrides factory default Date.now() with precise click event time)
124        "interaction_timestamp": bufferedEvent.clickTime,
125        "interaction_id": bufferedEvent.interactionToken || undefined,
126        "hashed_ip": bufferedEvent.hashedIP || undefined
127    })
128};
129
130
131        clickStack.push(payload);
132        window.dataLayer.push(payload);
133    }
134
135    // Main Click Listener
136    window.addEventListener('click', function (event) {
137        let a = new clickEvent(event, event.target);
138        bufferQueue.push(a);
139
140        if (bufferQueue.length >= 5) {
141            const latestClickTime = a.clickTime;
142            const fifthPriorClickTime = bufferQueue[bufferQueue.length - 5].clickTime;
143
144            if (latestClickTime - fifthPriorClickTime < 2000) {
145                bufferQueue.forEach(item => item.rageClick = true);
146            }
147        }
148
149        setTimeout(() => {
150            if (bufferQueue.length > 0) {
151                dispatchBufferedClick(bufferQueue.shift());
152            }
153        }, BUFFER_DELAY_MS);
154    });
155
156    class targetProperties {
157        #landmarks = [
158            { name: "aside", role: "complementary", excludedAncestors: [], requiredAttributes: [], descriptor: "Aside" },
159            { name: "footer", role: "contentinfo", excludedAncestors: ['article', 'aside', 'main', 'nav', 'section'], requiredAttributes: [], descriptor: "Content Info (Footer)" },
160            { name: "form", role: "form", excludedAncestors: [], requiredAttributes: ['aria-label', 'title', 'aria-labelledby'], descriptor: "Form" },
161            { name: "header", role: "banner", excludedAncestors: ['article', 'aside', 'main', 'nav', 'section'], requiredAttributes: [], descriptor: "Banner (Header)" },
162            { name: "section", role: "region", excludedAncestors: [], requiredAttributes: ['aria-label', 'title', 'aria-labelledby'], descriptor: "Region" },
163            { name: "main", role: "main", excludedAncestors: [], requiredAttributes: [], descriptor: "Main" },
164            { name: "nav", role: "navigation", excludedAncestors: [], requiredAttributes: [], descriptor: "Navigation" }
165        ];
166
167        #ancestors = [];
168
169        constructor(target) {
170            this.theTarget = [];
171            this.#getAncestors(target);
172            this.#describeTarget(target);
173            this.landmark = this.#determineLandmark();
174            this.descendantPath = this.#ancestors.map(u => u.name).reverse().join(' > ').substring(0, 500) || undefined;
175            this.isInteractive = this.#isInteractive(target);
176        }
177
178        #describeTarget(target) {
179            this.theTarget.push(this.#describeNode(target));
180        }
181
182        #describeNode(node) {
183            if (!node || !node.tagName) return { name: undefined, role: undefined, title: undefined, "aria-label": undefined, id: undefined, classes: undefined };
184
185            const classList = node.classList && node.classList.length > 0
186                ? Array.from(node.classList).slice(0, 2).join('.')
187                : undefined;
188
189            return {
190                name: node.tagName ? node.tagName.toLowerCase() : undefined,
191                role: node.getAttribute && node.getAttribute('role') ? node.getAttribute('role').toLowerCase() : undefined,
192                title: node.title ? node.title.substring(0, 30) : undefined,
193                "aria-label": node.getAttribute && node.getAttribute('aria-label') ? node.getAttribute('aria-label').substring(0, 30) : undefined,
194                id: node.id ? "#" + node.id : undefined,
195                classes: classList
196            };
197        }
198
199        #getAncestors(target) {
200            let current = target;
201            while (current !== null && typeof current.tagName !== 'undefined' && current.tagName !== "BODY" && current.tagName !== "HTML") {
202                this.#ancestors.push(this.#describeNode(current));
203                current = current.parentElement;
204            }
205        }
206
207        #determineLandmark() {
208            let determinedLandmark = { name: "", role: "", descriptor: "" };
209
210            for (let z = 0; z < this.#ancestors.length; z++) {
211                const currentDescendant = this.#ancestors[z];
212
213                for (let x = 0; x < this.#landmarks.length; x++) {
214                    const landmark = this.#landmarks[x];
215                    const isTagMatch = currentDescendant.name && landmark.name.toLowerCase() === currentDescendant.name.toLowerCase();
216                    const isRoleMatch = currentDescendant.role && landmark.role.toLowerCase() === currentDescendant.role.toLowerCase();
217
218                    if (isTagMatch || isRoleMatch) {
219                        if (!this.#hasExcludedAncestors(x, z) && this.#hasRequiredAttributes(x, z)) {
220                            determinedLandmark.name = landmark.name;
221                            determinedLandmark.role = landmark.role;
222                            determinedLandmark.descriptor = landmark.descriptor;
223                            return determinedLandmark;
224                        }
225                    }
226                }
227            }
228            return determinedLandmark;
229        }
230
231        #hasExcludedAncestors(landmarkIndex, ancestorIndex) {
232            const checkAncestors = this.#ancestors.slice(ancestorIndex + 1).map(u => u.name);
233            const exclusions = this.#landmarks[landmarkIndex].excludedAncestors;
234            return exclusions.some(excluded => checkAncestors.includes(excluded));
235        }
236
237        #hasRequiredAttributes(landmarkIndex, ancestorIndex) {
238            const requiredAttributes = this.#landmarks[landmarkIndex].requiredAttributes;
239            if (requiredAttributes.length === 0) return true;
240
241            const currentDescendant = this.#ancestors[ancestorIndex];
242            return requiredAttributes.some(attr => Boolean(currentDescendant[attr]));
243        }
244
245        #isInteractive(target) {
246            const interactiveTags = ["button", "a", "input", "select", "textarea"];
247            const interactiveRoles = ["button", "link", "checkbox", "menuitem", "tab", "option"];
248
249            return this.#ancestors.some(node =>
250                interactiveTags.includes(node.name) || interactiveRoles.includes(node.role)
251            );
252        }
253    }
254
255    class clickEvent {
256        #tempObj = {};
257
258        #easyRand(len = 16) {
259            const chars = '0123456789abcdef';
260            let res = '';
261            for (let i = 0; i < len; i++) {
262                if (i > 0 && i % 4 === 0 && len >= 12) res += '-';
263                res += chars[Math.floor(Math.random() * 16)];
264            }
265            return res;
266        }
267
268        isTouchDevice() {
269            return ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
270        }
271
272        isOutboundLink(targetURL) {
273            if (!targetURL || typeof targetURL !== 'string') return false;
274            try {
275                const url = new URL(targetURL, window.location.href);
276                if (!url.protocol.startsWith('http')) return false;
277
278                const currentHost = window.location.hostname.replace(/^www\./i, '').toLowerCase();
279                const targetHost = url.hostname.replace(/^www\./i, '').toLowerCase();
280
281                const isSameDomain = targetHost === currentHost;
282                const isSubDomain = targetHost.endsWith('.' + currentHost);
283
284                return !(isSameDomain || isSubDomain);
285            } catch (e) {
286                return false;
287            }
288        }
289
290        getModifierKey(event) {
291            if (event.ctrlKey) return 'ctrl';
292            if (event.metaKey) return 'cmd';
293            if (event.shiftKey) return 'shift';
294            if (event.altKey) return 'alt';
295            return 'none';
296        }
297
298        get clickType() {
299            if (this.rageClick && this.deadClick) return "dead_rage";
300            if (this.rageClick) return "rage";
301            if (this.deadClick) return "dead";
302            return "standard";
303        }
304
305        constructor(event, target) {
306            this.target = target;
307            this.#tempObj = new targetProperties(target);
308
309            // Coordinates & Canvas
310            const clickXY = `${event.clientX}/${event.clientY}`;
311            const pageXY = `${event.pageX}/${event.pageY}`;
312            const canvasWH = `${window.innerWidth}/${window.innerHeight}`;
313            const locParts = [clickXY, pageXY, canvasWH].filter(p => p && p.trim() !== '');
314            this.clickLocation = locParts.length > 0 ? locParts.join(' | ') : undefined;
315
316            this.scrollPosition = `${Math.round(window.scrollX || 0)}/${Math.round(window.scrollY || 0)}`;
317
318            // URLs & Context
319            this.pageURL = document.location.href;
320            this.pageTitle = document.title || undefined;
321            this.clickText = (target.innerText || "").trim().substring(0, 100) || undefined;
322
323            const linkElement = target.closest ? target.closest('a') : null;
324            let rawTargetURL = "";
325
326            if (target.href) {
327                rawTargetURL = typeof target.href === 'string' ? target.href : (target.href.baseVal || "");
328            } else if (linkElement && linkElement.href) {
329                rawTargetURL = typeof linkElement.href === 'string' ? linkElement.href : (linkElement.href.baseVal || "");
330            }
331
332            this.targetURL = rawTargetURL.substring(0, 500) || undefined;
333            this.outbound = this.isOutboundLink(this.targetURL);
334
335            // Behavioral Base States
336            this.interactiveClick = this.#tempObj.isInteractive;
337            this.deadClick = !this.interactiveClick;
338            this.rageClick = false;
339
340            // Target Structural Element
341            const targetObj = this.#tempObj.theTarget[0] || {};
342            const tag = targetObj.name || '';
343            const id = targetObj.id || '';
344            const classes = targetObj.classes ? '.' + targetObj.classes : '';
345            const selector = `${tag}${id}${classes}`.trim();
346            this.targetElement = selector ? selector.substring(0, 100) : undefined;
347
348            // Target Semantic Meta
349            const role = targetObj.role ? `role:${targetObj.role}` : '';
350            const aria = targetObj["aria-label"] ? `aria:${targetObj["aria-label"]}` : '';
351            const title = targetObj.title ? `title:${targetObj.title}` : '';
352            const metaParts = [role, aria, title].filter(p => p && p.trim() !== '');
353            this.targetElementMeta = metaParts.length > 0 ? metaParts.join(' | ').substring(0, 100) : undefined;
354
355            // Landmark Info
356            const lm = this.#tempObj.landmark || {};
357            const lmParts = [lm.name, lm.role, lm.descriptor].filter(p => p && String(p).trim() !== '');
358            this.landmarkInfo = lmParts.length > 0 ? lmParts.join(' | ') : undefined;
359
360            this.descendantPath = this.#tempObj.descendantPath;
361
362            // Metadata
363            this.click = clickStack.length + bufferQueue.length + 1;
364            this.clickTime = Date.now();
365            this.clickId = this.#easyRand(16);
366            this.modifierKey = this.getModifierKey(event);
367
368            const gUMG = window.gUMG || {};
369            this.interactionToken = gUMG.interactionToken || undefined;
370            this.hashedIP = gUMG.clientData?.hashedIP || undefined;
371
372            this.clickOnTouch = this.isTouchDevice();
373            this.pointerType = event.pointerType || undefined;
374        }
375    }
376})();
377
378

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.