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 if (!(`alert-dismissed-${alert.uuid}` in window.localStorage)) { 32 return false; 33 } 34 35 const dismissedAtTimestamp = Number( 36 window.localStorage.getItem(`alert-dismissed-${alert.uuid}`) 37 ); 38 39 // If the visitor has already dismissed the alert, but we are supposed to ignore dismissals before a set date. 40 return dismissedAtTimestamp >= alert.dismissalIgnoreBefore; 41 }; 42 43 const dismissAlert = (alert) => { 44 window.localStorage.setItem( 45 `alert-dismissed-${alert.uuid}`, 46 String(Math.round(new Date().getTime() / 1000)) 47 ); 48 document 49 .querySelectorAll(`[data-uuid="${alert.uuid}"]`) 50 .forEach((alert) => { 51 alert.dispatchEvent( 52 new CustomEvent('sitewide-alert-dismissed', { 53 bubbles: true, 54 composed: true 55 }) 56 ); 57 removeAlert(alert); 58 }); 59 }; 60 61 const buildAlertElement = (alert) => { 62 const alertElement = document.createElement('div'); 63 alertElement.innerHTML = alert.renderedAlert; 64 65 if (alert.dismissible) { 66 const dismissButtons = 67 alertElement.getElementsByClassName('js-dismiss-button'); 68 for (let i = 0; i < dismissButtons.length; i++) { 69 dismissButtons[i].addEventListener('click', () => dismissAlert(alert)); 70 } 71 } 72 73 return alertElement.firstElementChild; 74 }; 75 76 const removeAlert = (alert) => { 77 alert.dispatchEvent( 78 new CustomEvent('sitewide-alert-removed', { 79 bubbles: true, 80 composed: true 81 }) 82 ); 83 84 alert.remove(); 85 }; 86 87 const fetchAlerts = () => { 88 return fetch( 89 `${ 90 window.location.origin + 91 drupalSettings.path.baseUrl + 92 drupalSettings.path.pathPrefix 93 }sitewide_alert/load` 94 ) 95 .then((res) => res.json()) 96 .then( 97 (result) => result.sitewideAlerts, 98 // Note: it's important to handle errors here 99 // instead of a catch() block so that we don't swallow 100 // exceptions from actual bugs in components. 101 (error) => { 102 console.error(error); 103 } 104 ); 105 }; 106 107 const removeStaleAlerts = (alerts) => { 108 const roots = document.querySelectorAll(sitewideAlertsSelector); 109 roots.forEach((root) => { 110 const existingAlerts = root.querySelectorAll('[data-uuid]'); 111 112 // We have to convert and filter existing alerts based on newly fetched alerts. 113 // This can be done by comparing uuids. 114 // If the uuid can't be found in fetched alerts, 115 // the alert with the same uuid should be removed. 116 const alertsToBeRemoved = Array.from(existingAlerts).filter( 117 (alert) => !alerts.includes(alert.getAttribute('data-uuid')) 118 ); 119 120 alertsToBeRemoved.forEach((alert) => removeAlert(alert)); 121 }); 122 }; 123 124 const initAlerts = () => { 125 const roots = document.querySelectorAll(sitewideAlertsSelector); 126 // Fetch alerts and prepare rendering. 127 fetchAlerts().then((alerts) => { 128 removeStaleAlerts(alerts); 129 alerts.forEach((alert) => { 130 // Check if alert has been dismissed. 131 const dismissed = alertWasDismissed(alert); 132 // Check if current page is one of the pages the alert should be shown on or not. 133 const showOnThisPage = shouldShowOnThisPage( 134 alert.showOnPages, 135 alert.negateShowOnPages 136 );
137 roots.forEach((root) => { 138 // Check for existing alert element. 139 const existingAlertElement = root.querySelector( 140 `[data-uuid="${alert.uuid}"]` 141 ); 142 143 if (showOnThisPage && !dismissed) { 144 const renderableAlertElement = buildAlertElement(alert); 145 // To prevent an alert from being rendered multiple times 146 // replace the old alert with the new one when new alerts are being fetched. 147 existingAlertElement 148 ? root.replaceChild(renderableAlertElement, existingAlertElement) 149 : root.appendChild(renderableAlertElement); 150 151 renderableAlertElement.dispatchEvent( 152 new CustomEvent('sitewide-alert-rendered', { 153 bubbles: true, 154 composed: true 155 }) 156 ); 157 158 return; 159 } 160 161 // Remove alert if it is on the page and should no longer be. 162 if ((dismissed || !showOnThisPage) && existingAlertElement) { 163 removeAlert(existingAlertElement); 164 } 165 }); 166 }); 167 }); 168 }; 169 170 /** 171 * Check if window.history pushstate is available 172 * @returns {boolean} 173 */ 174 const supportsHistoryPushState = () => { 175 return ( 176 'pushState' in window.history && window.history['pushState'] !== null 177 ); 178 }; 179 180 /** 181 * Check if window.history replaceState is available 182 * @returns {boolean} 183 */ 184 const supportsHistoryReplaceState = () => { 185 return ( 186 'replaceState' in window.history && 187 window.history['replaceState'] !== null 188 ); 189 }; 190 191 /** 192 * Add Proxy to standard pushState function to fire CustomEvent. 193 * 194 * Nor history.pushState either history.replaceState will trigger a popstate event. 195 * Therefor a proxy behaviour is added to trigger a CustomEvent whenever the history is changed. 196 * 197 * @see https://developer.mozilla.org/en-US/docs/Web/API/History/pushState 198 * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event 199 */ 200 const proxyPushState = () => { 201 if (supportsHistoryPushState()) { 202 window.history.pushState = new Proxy(window.history.pushState, { 203 apply(target, thisArg, argArray) { 204 // triggerEvent 205 triggerHistoryEvent(thisArg, argArray); 206 // execute original 207 return target.apply(thisArg, argArray); 208 } 209 }); 210 } 211 if (supportsHistoryReplaceState()) { 212 window.history.replaceState = new Proxy(window.history.replaceState, { 213 apply(target, thisArg, argArray) { 214 // triggerEvent 215 triggerHistoryEvent(thisArg, argArray); 216 // execute original 217 return target.apply(thisArg, argArray); 218 } 219 }); 220 } 221 }; 222 223 /** 224 * Trigger CustomEvent sitewidealerts.popstate. 225 * 226 * @param thisArg 227 * @param argArray 228 */ 229 const triggerHistoryEvent = (thisArg, argArray) => { 230 const event = new CustomEvent('sitewidealerts.popstate', { 231 detail: { state: thisArg, options: argArray } 232 }); 233 window.dispatchEvent(event); 234 }; 235 236 /** 237 * Reinitialize the alters on A: CustomEvent and B: Standard popstate. 238 * 239 * @see shouldShowOnThisPage 240 * @see initAlerts 241 */ 242 const historyListener = () => { 243 window.addEventListener('sitewidealerts.popstate', () => initAlerts()); 244 window.addEventListener('popstate', () => initAlerts()); 245 }; 246 247 Drupal.behaviors.sitewide_alert_init = { 248 attach: (context) => { 249 once('sitewide_alerts_init', 'html', context).forEach(() => { 250 // On load. 251 initAlerts(); 252 proxyPushState(); 253 historyListener(); 254 if (drupalSettings.sitewideAlert.automaticRefresh === true) { 255 const interval = setInterval( 256 () => initAlerts(), 257 drupalSettings.sitewideAlert.refreshInterval < 1000 258 ? 1000 259 : drupalSettings.sitewideAlert.refreshInterval 260 ); 261 // Clear interval if automatic refresh has been turned off. 262 // Only do this if an interval has previously been set. 263 if (!drupalSettings.sitewideAlert.automaticRefresh) { 264 clearInterval(interval); 265 } 266 } 267 }); 268 } 269 }; 270})(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.