PageSourceSearch

https://hi.is/modules/contrib/sitewide_alert/js/init.js?v=10.6.17

js hi.is collected 2026-09-24 08:40:27 UTC 12,582 bytes, 396 lines download raw bytes

1(function (Drupal, drupalSettings, once) {
2  const sitewideAlertsSelector = '[data-sitewide-alert]';
3
4  const shouldShowOnThisPage = (pages = [], negate = true) => {
5    if (pages.length === 0) {
6      return true;
7    }
8
9    let pagePathMatches = false;
10    const currentPath = window.location.pathname;
11
12    for (let i = 0; i < pages.length; i++) {
13      const baseUrl = drupalSettings.path.baseUrl.slice(0, -1);
14      const page = baseUrl + pages[i];
15      // Check if we have to deal with a wild card.
16      if (page.charAt(page.length - 1) === '*') {
17        if (currentPath.startsWith(page.substring(0, page.length - 1))) {
18          pagePathMatches = true;
19          break;
20        }
21      } else if (page === currentPath) {
22        pagePathMatches = true;
23        break;
24      }
25    }
26
27    return negate ? !pagePathMatches : pagePathMatches;
28  };
29
30  const alertWasDismissed = (alert) => {
31    // Only dismissible alerts can be dismissed.
32    if (!alert.dismissible) {
33      return false;
34    }
35
36    if (!(`alert-dismissed-${alert.uuid}` in window.localStorage)) {
37      return false;
38    }
39
40    const dismissedAtTimestamp = Number(
41      window.localStorage.getItem(`alert-dismissed-${alert.uuid}`),
42    );
43
44    // If the visitor has already dismissed the alert, but we are supposed to ignore dismissals before a set date.
45    return dismissedAtTimestamp >= alert.dismissalIgnoreBefore;
46  };
47
48  const updateLandmarkRole = () => {
49    const roots = document.querySelectorAll(sitewideAlertsSelector);
50    roots.forEach((root) => {
51      const hasAlerts = root.querySelector('[data-uuid]') !== null;
52      if (hasAlerts) {
53        root.setAttribute('role', 'region');
54        root.setAttribute('aria-label', Drupal.t('Site alerts'));
55      } else {
56        root.removeAttribute('role');
57        root.removeAttribute('aria-label');
58      }
59    });
60  };
61
62  const updateAlertCount = () => {
63    if (!drupalSettings.sitewideAlert.showCount) {
64      return;
65    }
66    const roots = document.querySelectorAll(sitewideAlertsSelector);
67
68    roots.forEach((root) => {
69      let count = 0;
70      const alerts = root.querySelectorAll('[data-uuid]');
71      const totalCount = alerts.length;
72      alerts.forEach((alert) => {
73        count++;
74        const alertCountContainer = alert.querySelector('.alert-count');
75        if (!alertCountContainer) {
76          return;
77        }
78
79        // @Todo Dispatch custom event here?
80        alertCountContainer.innerHTML = Drupal.t(
81          '@current_count of @total_count alerts',
82          {
83            '@current_count': count,
84            '@total_count': totalCount,
85          },
86        );
87      });
88    });
89  };
90
91  const removeAlert = (alert) => {
92    alert.dispatchEvent(
93      new CustomEvent('sitewide-alert-removed', {
94        bubbles: true,
95        composed: true,
96      }),
97    );
98
99    alert.remove();
100    updateAlertCount();
101    updateLandmarkRole();
102  };
103
104  const dismissAlert = (alert) => {
105    window.localStorage.setItem(
106      `alert-dismissed-${alert.uuid}`,
107      String(Math.round(new Date().getTime() / 1000)),
108    );
109    document
110      .querySelectorAll(`[data-uuid="${alert.uuid}"]`)
111      .forEach((alert) => {
112        alert.dispatchEvent(
113          new CustomEvent('sitewide-alert-dismissed', {
114            bubbles: true,
115            composed: true,
116          }),
117        );
118        removeAlert(alert);
119      });
120  };
121
122  const buildAlertElement = (alert) => {
123    const alertElement = document.createElement('div');
124    alertElement.innerHTML = alert.renderedAlert;
125
126    if (alert.dismissible) {
127      const dismissButtons =
128        alertElement.getElementsByClassName('js-dismiss-button');
129      for (let i = 0; i < dismissButtons.length; i++) {
130        dismissButtons[i].addEventListener('click', () => dismissAlert(alert));
131      }
132    }
133
134    return alertElement.firstElementChild;
135  };
136
137  /**
138   * Process server-side rendered alerts.
139   *
140   * This runs synchronously to:
141   * 1. Check dismissal status in localStorage and remove dismissed alerts
142   * 2. Attach dismiss button handlers to SSR alerts
143   */
144  const processSsrAlerts = () => {
145    const roots = document.querySelectorAll(sitewideAlertsSelector);
146    roots.forEach((root) => {
147      const existingAlerts = root.querySelectorAll('[data-uuid]');
148      existingAlerts.forEach((alertElement) => {
149        const uuid = alertElement.dataset.uuid;
150        const dismissalIgnoreBefore =
151          parseInt(alertElement.dataset.dismissalIgnoreBefore, 10) || 0;
152        const isDismissible = alertElement.dataset.dismissible === 'true';
153
154        // Create alert data object for existing functions.
155        const alertData = {
156          uuid,
157          dismissalIgnoreBefore,
158          dismissible: isDismissible,
159        };
160
161        // Remove dismissed alerts.
162        if (alertWasDismissed(alertData)) {
163          removeAlert(alertElement);
164          return;
165        }
166
167        // Attach dismiss handler if dismissible.
168        if (isDismissible) {
169          const dismissButtons =
170            alertElement.getElementsByClassName('js-dismiss-button');
171          for (let i = 0; i < dismissButtons.length; i++) {
172            dismissButtons[i].addEventListener('click', () =>
173              dismissAlert(alertData),
174            );
175          }
176        }
177      });
178    });
179  };
180
181  const fetchAlerts = () => {
182    const url = new URL(
183      Drupal.url('sitewide_alert/load'),
184      window.location.origin,
185    );
186    // Strip credentials from the URL to avoid "Request cannot be constructed
187    // from a URL that includes credentials" errors with the Fetch API
188    // (e.g. on sites using HTTP Basic Authentication).
189    url.username = '';
190    url.password = '';
191    return fetch(url.toString())
192      .then((res) => res.json())
193      .then(
194        (result) => result.sitewideAlerts,
195        // Note: it's important to handle errors here
196        // instead of a catch() block so that we don't swallow
197        // exceptions from actual bugs in components.
198        (error) => {
199          console.error(error);
200        },
201      );
202  };
203
204  const removeStaleAlerts = (alerts) => {
205    const roots = document.querySelectorAll(sitewideAlertsSelector);
206    roots.forEach((root) => {
207      const existingAlerts = root.querySelectorAll('[data-uuid]');
208
209      // We have to convert and filter existing alerts based on newly fetched alerts.
210      // This can be done by comparing uuids.
211      // If the uuid can't be found in fetched alerts,
212      // the alert with the same uuid should be removed.
213      const alertsToBeRemoved = Array.from(existingAlerts).filter(
214        (alert) => !alerts.includes(alert.getAttribute('data-uuid')),
215      );
216
217      alertsToBeRemoved.forEach((alert) => removeAlert(alert));
218    });
219  };
220
221  const initAlerts = () => {
222    const roots = document.querySelectorAll(sitewideAlertsSelector);
223    // Fetch alerts and prepare rendering.
224    fetchAlerts().then((alerts) => {
225      removeStaleAlerts(alerts.map((alert) => alert.uuid));
226      alerts.forEach((alert) => {
227        // Check if alert should be dismissed.
228        const dismissed = alertWasDismissed(alert);
229        // Check if current page is one of the pages the alert should be shown on or not.
230        const showOnThisPage = shouldShowOnThisPage(
231          alert.showOnPages,
232          alert.negateShowOnPages,
233        );
234        roots.forEach((root) => {
235          // Check for existing alert element.
236          const existingAlertElement = root.querySelector(
237            `[data-uuid="${alert.uuid}"]`,
238          );
239
240          if (showOnThisPage && !dismissed) {
241            if (
242              existingAlertElement &&
243              existingAlertElement.dataset.changed === alert.changed
244            ) {
245              return;
246            }
247            const renderableAlertElement = buildAlertElement(alert);
248            // To prevent an alert from being rendered multiple times
249            // replace the old alert with the new one when new alerts are being fetched.
250            if (existingAlertElement) {
251              root.replaceChild(renderableAlertElement, existingAlertElement);
252            } else {
253              root.appendChild(renderableAlertElement);
254            }
255
256            renderableAlertElement.dispatchEvent(
257              new CustomEvent('sitewide-alert-rendered', {
258                bubbles: true,
259                composed: true,
260              }),
261            );
262
263            Drupal.attachBehaviors(renderableAlertElement);
264            return;
265          }
266
267          // Remove alert if it is on the page and should no longer be.
268          if ((dismissed || !showOnThisPage) && existingAlertElement) {
269            removeAlert(existingAlertElement);
270          }
271        });
272        updateAlertCount();
273      });
274      updateLandmarkRole();
275    });
276  };
277
278  /**
279   * Check if window.history pushstate is available
280   * @returns {boolean}
281   */
282  const supportsHistoryPushState = () => {
283    return 'pushState' in window.history && window.history.pushState !== null;
284  };
285
286  /**
287   * Check if window.history replaceState is available
288   * @returns {boolean}
289   */
290  const supportsHistoryReplaceState = () => {
291    return (
292      'replaceState' in window.history && window.history.replaceState !== null
293    );
294  };
295
296  /**
297   * Trigger CustomEvent sitewidealerts.popstate.
298   *
299   * @param thisArg
300   * @param argArray
301   */
302  const triggerHistoryEvent = (thisArg, argArray) => {
303    const event = new CustomEvent('sitewidealerts.popstate', {
304      detail: { state: thisArg, options: argArray },
305    });
306    window.dispatchEvent(event);
307  };
308
309  /**
310   * Add Proxy to standard pushState function to fire CustomEvent.
311   *
312   * Nor history.pushState either history.replaceState will trigger a popstate event.
313   * Therefor a proxy behavior is added to trigger a CustomEvent whenever the history is changed.
314   *
315   * @see https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
316   * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event
317   */
318  const proxyPushState = () => {
319    if (supportsHistoryPushState()) {
320      window.history.pushState = new Proxy(window.history.pushState, {
321        apply(target, thisArg, argArray) {
322          // triggerEvent
323          triggerHistoryEvent(thisArg, argArray);
324          // execute original
325          return target.apply(thisArg, argArray);
326        },
327      });
328    }
329    if (supportsHistoryReplaceState()) {
330      window.history.replaceState = new Proxy(window.history.replaceState, {
331        apply(target, thisArg, argArray) {
332          // triggerEvent
333          triggerHistoryEvent(thisArg, argArray);
334          // execute original
335          return target.apply(thisArg, argArray);
336        },
337      });
338    }
339  };
340
341  function debounce(func, wait, immediate) {
342    let timeout;
343    return function (...args) {
344      const context = this;
345      const later = function () {
346        timeout = null;
347        if (!immediate) func.apply(context, args);
348      };
349      const callNow = immediate && !timeout;
350      clearTimeout(timeout);
351      timeout = setTimeout(later, wait);
352      if (callNow) func.apply(context, args);
353    };
354  }
355
356  /**
357   * Reinitialize the alters on A: CustomEvent and B: Standard popstate.
358   *
359   * @see shouldShowOnThisPage
360   * @see initAlerts
361   */
362  const historyListener = () => {
363    const debounced = debounce(initAlerts, 2000);
364    window.addEventListener('sitewidealerts.popstate', debounced);
365    window.addEventListener('popstate', debounced);
366  };
367
368  Drupal.behaviors.sitewide_alert_init = {
369    attach: (context) => {
370      once('sitewide_alerts_init', 'html', context).forEach(() => {
371        // Process any SSR alerts first (attach handlers, check dismissals).
372        processSsrAlerts();
373        updateLandmarkRole();
374        // Skip initial fetch if alerts were rendered server-side.
375        if (drupalSettings.sitewideAlert.serverSideRender !== true) {
376          initAlerts();
377        }
378        proxyPushState();
379        historyListener();
380        if (drupalSettings.sitewideAlert.automaticRefresh === true) {
381          const interval = setInterval(
382            () => initAlerts(),
383            drupalSettings.sitewideAlert.refreshInterval < 1000
384              ? 1000
385              : drupalSettings.sitewideAlert.refreshInterval,
386          );
387          // Clear interval if automatic refresh has been turned off.
388          // Only do this if an interval has previously been set.
389          if (!drupalSettings.sitewideAlert.automaticRefresh) {
390            clearInterval(interval);
391          }
392        }
393      });
394    },
395  };
396})(Drupal, drupalSettings, once);

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.