PageSourceSearch

https://espial.naf.no/web/espialTracking.js

js naf.no collected 2026-09-24 17:46:24 UTC 12,164 bytes, 404 lines download raw bytes

1espial = (function () {
2  "use strict";
3
4  var location = window.location;
5  var document = window.document;
6  var scriptEl = document.currentScript;
7  var endpoint = scriptEl.getAttribute("data-api"); // Construct analytics endpoint
8  var lastPage; // Keeps track of the location.pathname
9  var scrollDepthSteps = [0, 25, 50, 75, 100];
10  var currentScrollDepth = 0; // Keeps track of the scroll depts
11
12  // Check for proper setup
13  if (!scriptEl.hasAttribute("data-domain"))
14    console.error("Analytics configuration: domain not set.");
15  if (!scriptEl.hasAttribute("data-api"))
16    console.error("Analytics configuration: endpoint not set.");
17
18  /**
19   * Helper for logging when events are ignored
20   * @param {string} reason Text stating reason for ignoring event
21   * @param {object} options
22   * @param {function} [options.callback] Function to call after logging
23   */
24  function onIgnoredEvent(reason, options) {
25    if (reason) console.warn("Ignoring Event: " + reason);
26    options && options.callback && options.callback();
27  }
28
29  /**
30   * Returns matches based on wildcardPath
31   * @param {string} wildcardPath
32   * @returns {array}
33   */
34  function pathMatches(wildcardPath) {
35    var actualPath = location.pathname;
36    return actualPath.match(
37      new RegExp(
38        "^" +
39          wildcardPath
40            .trim()
41            .replace(/\*\*/g, ".*")
42            .replace(/([^\.])\*/g, "$1[^\\s/]*") +
43          "/?$",
44      ),
45    );
46  }
47
48  /**
49   * Throttles a mainFunction
50   * @returns {function} A throttled version of the function
51   */
52  function throttle(mainFunction, delay) {
53    let timerFlag = null; // Variable to keep track of the timer
54
55    // Returning a throttled version
56    return (...args) => {
57      if (timerFlag === null) {
58        // If there is no timer currently running
59        mainFunction(...args); // Execute the main function
60        timerFlag = setTimeout(() => {
61          // Set a timer to clear the timerFlag after the specified delay
62          timerFlag = null; // Clear the timerFlag to allow the main function to be executed again
63        }, delay);
64      }
65    };
66  }
67
68  /**
69   * Check if the href of a link is outbound
70   * @param {HTMLElement} link
71   * @returns {boolean}
72   */
73  function isOutboundLink(link) {
74    return link && link.href && link.host && link.host !== location.host;
75  }
76
77  /**
78   * Returns the current scroll depth
79   * @returns {int}
80   */
81  function getScrollPercent() {
82    var h = document.documentElement,
83      b = document.body,
84      st = "scrollTop",
85      sh = "scrollHeight";
86    let scrollDepth = Math.ceil(
87      ((h[st] || b[st]) / ((h[sh] || b[sh]) - h.clientHeight)) * 100,
88    );
89    if (scrollDepth > 95) scrollDepth = 100;
90    return scrollDepth;
91  }
92
93  /**
94   * Returns the first link-element in the DOM that is a parent of the cliked element
95   * @param {HTMLElement} link
96   * @returns {HTMLElement|null}
97   */
98  function getLinkEl(link) {
99    while (
100      link &&
101      (typeof link.tagName === "undefined" || !isLink(link) || !link.href)
102    ) {
103      link = link.parentNode;
104    }
105    return link;
106  }
107
108  /**
109   * Returns the first button-element in the DOM that is a parent of the cliked element
110   * @param {HTMLElement} link
111   * @returns {HTMLElement|null}
112   */
113  function getButtonEl(el) {
114    while (el && (typeof el.tagName === "undefined" || !isButton(el))) {
115      el = el.parentNode;
116    }
117    return el;
118  }
119
120  /**
121   * Check if a element is a link
122   * @param {HTMLElement} element
123   * @returns {boolean}
124   */
125  function isLink(element) {
126    return element && element.tagName && element.tagName.toLowerCase() === "a";
127  }
128
129  /**
130   * Check if a element is a button
131   * @param {HTMLElement} element
132   * @returns {boolean}
133   */
134  function isButton(element) {
135    return (
136      element && element.tagName && element.tagName.toLowerCase() === "button"
137    );
138  }
139
140  /**
141   * Handle clicks
142   * @param {event} event
143   * @returns {function|null}
144   */
145  function handleLinkClickEvent(event) {
146    if (event.type === "auxclick" && event.button !== "MIDDLE_MOUSE_BUTTON") {
147      return;
148    }
149
150    var link = getLinkEl(event.target);
151    var button = getButtonEl(event.target);
152
153    if (isOutboundLink(link)) {
154      return sendLinkClickEvent(event, link, {
155        name: "outbound link: click",
156        props: { url: link.href },
157      });
158    }
159
160    const buttonClassRegex = new RegExp("button__StyledButton-sc");
161    if (button && buttonClassRegex.test(button.getAttribute("class"))) {
162      if (isButton(button))
163        trigger("cta: click", { props: { str_value: button.textContent } });
164      if (isLink(button))
165        trigger("cta: click", {
166          props: { url: link.href, str_value: button.textContent },
167        });
168    }
169
170    const linkClassRegex = new RegExp("button__StyledAnchor-sc");
171    if (link && linkClassRegex.test(link.getAttribute("class"))) {
172      return sendLinkClickEvent(event, link, {
173        name: "cta: click",
174        props: { url: link.href, str_value: link.textContent },
175      });
176    }
177
178    // TODO: Implement sendLinkClickEvent for download links
179    // TODO: Implement sendLinkClickEvent for mailto: and tel:
180  }
181
182  /**
183   * Check if we should follow the link
184   * @param {event} event
185   * @param {HTMLElement} link
186   * @returns {boolean}
187   */
188  function shouldFollowLink(event, link) {
189    // If default has been prevented by an external script, we should not intercept navigation.
190    if (event.defaultPrevented) {
191      return false;
192    }
193
194    var targetsCurrentWindow =
195      !link.target || link.target.match(/^_(self|parent|top)$/i);
196    var isRegularClick =
197      !(event.ctrlKey || event.metaKey || event.shiftKey) &&
198      event.type === "click";
199    return targetsCurrentWindow && isRegularClick;
200  }
201
202  /**
203   * Track click event
204   * @param {Event} event
205   * @param {HTMLElement} link
206   * @param {Object} eventAttrs
207   */
208  function sendLinkClickEvent(event, link, eventAttrs) {
209    var followedLink = false;
210
211    function followLink() {
212      if (!followedLink) {
213        followedLink = true;
214        window.location = link.href;
215      }
216    }
217
218    if (shouldFollowLink(event, link)) {
219      var attrs = { props: eventAttrs.props, callback: followLink };
220      attrs.revenue = eventAttrs.revenue;
221      trigger(eventAttrs.name, attrs);
222      setTimeout(followLink, 5000);
223      event.preventDefault();
224    } else {
225      var attrs = { props: eventAttrs.props };
226      attrs.revenue = eventAttrs.revenue;
227      trigger(eventAttrs.name, attrs);
228    }
229  }
230
231  /**
232   * Handles tracking scroll depth
233   */
234  function handleScrollEvent() {
235    // Hold the scrollDepth as we enter the function
236    const tempScrollDepth = currentScrollDepth;
237    // Find new the closest scroll step
238    let newScrollDepth = Math.max(
239      ...scrollDepthSteps.filter((num) => num <= getScrollPercent()),
240    );
241
242    // If the scrollDepth has increased
243    if (newScrollDepth > tempScrollDepth) {
244      // Uppdate current scroll depth
245      currentScrollDepth = newScrollDepth;
246      // Since the function is throttled, the user might scroll so fast some steps are skipped.
247      let newScrollDepthIndex = scrollDepthSteps.indexOf(newScrollDepth);
248      let tempScrollDepthIndex = scrollDepthSteps.indexOf(tempScrollDepth);
249
250      // If the scrolldepth when entering the function is not the same as
251      // the one before the current we know we have skipped one or more steps
252      if (newScrollDepthIndex - tempScrollDepthIndex > 1) {
253        for (let i = tempScrollDepthIndex + 1; i < newScrollDepthIndex; i++) {
254          trigger("scroll", { props: { int_value: scrollDepthSteps[i] } });
255        }
256      }
257      trigger("scroll", { props: { int_value: currentScrollDepth } });
258    }
259  }
260
261  /**
262   * Log event
263   * @param {string} eventName Name of the event
264   * @param {object} options
265   * @param {object} [options.meta]
266   * @param {object} [options.props] Attach custom properties (also known as custom dimensions in Google Analytics) when sending pageviews or custom events in order to create custom metrics.
267   * @param {object} [options.revenue] Used for ecommerce and assigning dynamic monetary values to goals and custom events to track revenue attribution.
268   */
269  function trigger(eventName, options) {
270    // Ignore localhost and headless browsers
271    if (
272      /^localhost$|^127(\.[0-9]+){0,2}\.[0-9]+$|^\[::1?\]$/.test(
273        location.hostname,
274      ) ||
275      location.protocol === "file:"
276    ) {
277      return onIgnoredEvent("localhost", options);
278    }
279    if (
280      window._phantom ||
281      window.__nightmare ||
282      window.navigator.webdriver ||
283      window.Cypress
284    ) {
285      return onIgnoredEvent(null, options);
286    }
287    // Ignore if flagged in localStorage
288    // Wrapped in try to prevent throwing error if localStorage is not accessible
289    try {
290      if (window.localStorage.analytics_ignore === "true") {
291        return onIgnoredEvent("localStorage flag", options);
292      }
293    } catch (e) {
294      // Do nothing
295    }
296
297    // Check if url is in include or exclude list
298    var dataIncludeAttr = scriptEl && scriptEl.getAttribute("data-include");
299    var dataExcludeAttr = scriptEl && scriptEl.getAttribute("data-exclude");
300
301    if (eventName === "pageview") {
302      var isIncluded =
303        !dataIncludeAttr ||
304        (dataIncludeAttr && dataIncludeAttr.split(",").some(pathMatches));
305      var isExcluded =
306        dataExcludeAttr && dataExcludeAttr.split(",").some(pathMatches);
307      if (!isIncluded || isExcluded)
308        return onIgnoredEvent("exclusion rule", options);
309    }
310
311    // Build the payload
312    var payload = {};
313    payload.n = eventName;
314    payload.u = location.href;
315    payload.d = scriptEl.getAttribute("data-domain");
316    payload.r = options && options.r ? options.r : document.referrer || null;
317
318    if (options && options.meta) {
319      payload.m = options.meta;
320    }
321    if (options && options.props) {
322      payload.p = options.props;
323    }
324    if (options && options.revenue) {
325      payload.$ = options.revenue;
326    }
327
328    // Attach attributes from script-tag to payload properties
329    var propAttributes = scriptEl.getAttributeNames().filter(function (name) {
330      return name.substring(0, 6) === "event-";
331    });
332
333    var props = payload.p || {};
334
335    propAttributes.forEach(function (attribute) {
336      var propKey = attribute.replace("event-", "");
337      var propValue = scriptEl.getAttribute(attribute);
338      props[propKey] = props[propKey] || propValue;
339    });
340
341    payload.p = props;
342
343    // Send request
344    var request = new XMLHttpRequest();
345    request.open("POST", endpoint, true);
346    request.setRequestHeader("Content-Type", "application/json");
347
348    request.send(JSON.stringify(payload));
349
350    request.onreadystatechange = function () {
351      if (request.readyState === 4) {
352        options && options.callback && options.callback();
353      }
354    };
355  }
356
357  /**
358   * Trigger a new pageview if the location has changed
359   */
360  function pageview() {
361    if (lastPage === location.pathname) return;
362    lastPage = location.pathname;
363    trigger("pageview");
364  }
365
366  /**
367   * Initializes the component
368   * @public
369   */
370  function init() {
371    // If pushState is used we inject a function track changes to pushState
372    var his = window.history;
373    if (his.pushState) {
374      var originalPushState = his["pushState"];
375      his.pushState = function () {
376        originalPushState.apply(this, arguments);
377        pageview();
378      };
379      window.addEventListener("popstate", pageview);
380    }
381
382    // Trigger a pageview when the page once the document is ready
383    if (document.visibilityState === "prerender") {
384      document.addEventListener("visibilitychange", function () {
385        if (!lastPage && document.visibilityState === "visible") {
386          pageview();
387        }
388      });
389    } else {
390      pageview();
391    }
392
393    document.addEventListener("click", handleLinkClickEvent);
394    document.addEventListener("auxclick", handleLinkClickEvent);
395    document.addEventListener("scroll", throttle(handleScrollEvent, 500));
396  }
397
398  // Initialize the component
399  init();
400
401  return {
402    trigger: trigger,
403  };
404})();

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.