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.