1/** 2 * Modal jquery code 3 * 11/13/2021 - do not scroll to first focusable field 4 * 8/2/2021 - add code to allow timer modal to optionally display only once via the use of a cookie 5 * 8/17/2021 - do not allow timer modal to display if another modal already open 6 * 11/1/2021 - when setting focus on first element do not scroll 7 * 2/15/2022 - disable close modal on escape if disabled in the frontend 8 * 4/5/2022 - Do not scroll when returning focus on modal close 9 * 5/5/2022 - Allow for modals to call other modals 10 * 6/28/2022 - Allow for checking url for text before triggering modal 11 * 11/22/2022 - custom events for open and close 12 * 1/11/2023 - Provide ability to manually trigger initialization of modals on page. Also only initialize modals when document is ready. 13 */ 14 15 16var bodModalBlock = function(){ 17 "use strict"; 18 19 var $ = jQuery.noConflict(); 20 21 const focusableElements = 22 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; 23 24 var bodModalCount = 0; // global count of modals 25 var bodModalActive = false; 26 var bodModals = []; 27 28 var initElements = function(){ 29 30 bodModalCount = 0; // global count of modals 31 bodModalActive = false; 32 bodModals = []; 33 34 // if we have already inited this type of element 35 $('.wp-block-bod-modal-block').each(function(){ 36 // for the identified modal we add an instance of the class BodModal 37 // to the data element 38 // $(this).data('bod-block-popup', new BodModal(this)); 39 bodModals.push(new BodModal(this)); 40 41 }); 42 }; 43 44 var checkForCookie = function(cookieName){ 45 let name = cookieName + "="; 46 let decodedCookie = decodeURIComponent(document.cookie); 47 let ca = decodedCookie.split(';'); 48 for(let i = 0; i <ca.length; i++) { 49 let c = ca[i]; 50 while (c.charAt(0) == ' ') { 51 c = c.substring(1); 52 } 53 if (c.indexOf(name) == 0) { 54 return c.substring(name.length, c.length); 55 } 56 } 57 return ""; 58 } 59 60 61 // class used to manipulate the modal, there will be a seperate instance of the 62 // class for each modal on the page 63 // container - DOM object for modal 64 var BodModal = function (container) { 65 // convert passed in DOM modal container to jquery object so we can apply jquery methods 66 this.$container = $(container); 67 this.$trigger = this.$container.find('.bod-block-popup-trigger'); 68 this.$overlay = this.$container.find('.bod-block-popup-overlay'); 69 70 // Markup the aria labels 71 72 bodModalCount ++; // get modal number to append to get unique id 73 this.$container.find('.bod-block-popup-wrap').attr('aria-labelledby','bodModalAriaTitle' + bodModalCount); 74 this.$container.find('.bod-modal-title').attr('id' , 'bodModalAriaTitle' + bodModalCount); 75 this.$container.find('.bod-block-popup-wrap').attr('aria-describedby','bodModalAriaContent' + bodModalCount); 76 this.$container.find('.bod-modal-content').attr('id' , 'bodModalAriaContent' + bodModalCount); 77 78 79 // bind our functions to this so we use current rather than event version of this 80 81 this.show = this.show.bind(this); 82 this.afterShow = this.afterShow.bind(this); 83 this.hide = this.hide.bind(this); 84 this.keyPress = this.keyPress.bind(this); 85 86 // we need a timer for fading in and out the modal and for show on page load 87 88 this.timer = null; 89 90 // assign trigger event depending upon trigger type which is 91 // stored has a class 92 93 if (this.$trigger.hasClass('type_selector')) { 94 var triggerSelector = this.$trigger.attr('data-selector'); 95 if (triggerSelector) { 96 $('.' + triggerSelector).css('cursor','pointer').on('click', this.show); // attach click event 97 } 98 } else if (this.$trigger.hasClass('type_load')) { 99 // we have show on page load 100 101 // need to load the data-once attribute which will be either no , or yes 102 103 var loadOnce = this.$trigger.attr('data-once'); 104 var triggerTimer = true; 105 if (loadOnce) { 106 if (loadOnce === 'yes') { 107 let modalId = this.$trigger.attr('data-id'); 108 if (!modalId) modalId= ""; 109 let modalOnce = checkForCookie ("bodModalOnce" + modalId); 110 if (modalOnce) triggerTimer = false; 111 } 112 } 113 114 // check if we need to check for text in url to enable modal 115 var loadurl = this.$trigger.attr('data-urltrig'); 116 if (loadurl) { 117 if (window.location.href.indexOf(loadurl)===-1) triggerTimer = false; 118 } 119 120 if (triggerTimer) { 121 // we need to extract the delay amd set a timeout using it 122 var loadDelay = this.$trigger.attr('data-delay'); 123 isNaN(parseInt(loadDelay)) ? loadDelay = 1000 : loadDelay = parseInt(loadDelay); 124 125 // if we only want to show the modal once we exact the UID provided when the modal created (optional), and the number of days we wait until we show the 126 // modal again (this is added to the cookie) 127 if (loadOnce) { 128 let modalId = this.$trigger.attr('data-id'); 129 if (!modalId) modalId= ""; 130 let noShowDays = this.$trigger.attr('data-days'); 131 isNaN(parseInt(noShowDays)) ? noShowDays = 30 : noShowDays = parseInt(noShowDays); 132 this.timer = setTimeout(this.show, loadDelay, loadOnce, modalId , noShowDays); 133 } else { 134 this.timer = setTimeout(this.show, loadDelay); 135 } 136 } 137 138 } else { // must be button, text, or image trigger 139 this.$trigger.on('click', this.show); // attach click event 140 } 141
142 // setup the overlay and content wrap, put them into a jquery object and 143 // attach the hide event for the click 144 145 // check to see if we have disabled the close on overlay click in the frontend 146 var disableOverlayClose = this.$overlay.attr('data-disabled-overlay-close'); 147 if (!disableOverlayClose) { 148 this.$overlay.on('click' , this.hide); 149 } else { 150 if (disableOverlayClose === 'false') { 151 this.$overlay.on('click' , this.hide); 152 } 153 } 154 155 // check to see if we have disabled the close on escape key press in the frontend 156 var disableEscapeClose = this.$overlay.attr('data-disabled-escape-close'); 157 if (!disableEscapeClose) { 158 this.disableEscapeClose = false; 159 } else { 160 if (disableEscapeClose === 'true') { 161 this.disableEscapeClose = true; 162 } else { 163 this.disableEscapeClose = false; 164 } 165 } 166 167 this.$modalWrap = this.$container.find('.bod-block-popup-wrap'); 168 this.$modal = this.$container.find('.bod-block-popup'); 169 this.$modalcloser = this.$container.find('.bod-block-popup-closer') 170 .on('click' , this.hide); 171 this.$titlecloser = this.$container.find('.bod-block-title-closer') 172 .on('click' , this.hide); 173 this.$btncloser = this.$container.find('.bod-block-close-btn .bod-btn') 174 .on('click' , this.hide); 175 this.$modalToModal = this.$container.find('.bod-modal-to-modal') 176 .on('click' , this.hide); 177 this.$escCloser = $(document).on('keydown' , this.keyPress); 178 179 // Javascript modal wrap 180 181 this.modalWrap = container.querySelector('.bod-block-popup-wrap'); 182 183 // capture first, all and last focusable elements so we can enforce modal focus trap for ADA 184 this.focusableContent = this.modalWrap.querySelectorAll(focusableElements); 185 if (this.focusableContent) { 186 this.firstFocusableElement = this.focusableContent[0]; // get first element to be focused inside modal 187 this.lastFocusableElement = this.focusableContent[this.focusableContent.length - 1]; // get last element to be focused inside modal 188 } 189 this.modalOpen = false; 190 191 } 192 193 BodModal.prototype = { 194 show: function(loadOnce , modalId, noShowDays ){ 195 196 $(document).trigger("bod-modal-before-open",this.$modalWrap); 197 198 // if we try and show a modal but one is already open then return without showing the new one 199 if (this.$trigger.hasClass('type_load') && bodModalActive === true) { 200 return; 201 } 202 203 bodModalActive = true; 204 205 if (loadOnce) { 206 // check if we need to set a cookie to stop modal being shown mmore than once 207 if (loadOnce !== 'no') { 208 // flag set to say we only show once so set the cookie 209 const d = new Date(); 210 d.setTime(d.getTime() + (noShowDays*24*60*60*1000)); 211 let expires = "expires="+ d.toUTCString(); 212 if (modalId) { 213 var cookie = "bodModalOnce" + modalId; 214 } else { 215 var cookie = "bodModalOnce"; 216 } 217 document.cookie = cookie + "=" + loadOnce + ";" + expires + ";path=/"; 218 } 219 } 220 221 // start to clearing any timeouts 222 223 clearTimeout(this.timer); 224 225 // when we show the modal we move it to the body DOM so that 226 // nothing else impacts it 227 this.$overlay.appendTo(document.body).show(); 228 this.$modalWrap.appendTo(document.body).show(); 229 230 // even though we have hit it with a show the opacticy still makes it 231 // invisible. We wait to ensure the modal is ready then then add 232 // an active class with triggers the opacity transition 233 234 this.timer = setTimeout(this.afterShow, 25); 235 }, 236 afterShow: function(){ 237 clearTimeout(this.timer); 238 239 // Lets check what transition we are going to use 240 241 if (this.transition = this.$modal.attr('data-transition')) { 242 if (this.transition == 'left') { 243 this.$modal.addClass('slide-right'); 244 } else if (this.transition == 'right') { 245 this.$modal.addClass('slide-left'); 246 } else if (this.transition == 'bottom') { 247 this.$modal.addClass('slide-up'); 248 } else if (this.transition == 'top') { 249 this.$modal.addClass('slide-down'); 250 } else { 251 this.$modal.addClass('fade'); 252 }; 253 } else { 254 this.$modal.addClass('fade'); 255 }; 256 257 this.$overlay.addClass('active'); 258 this.$modalWrap.addClass('active'); 259 this.$modalWrap.attr('aria-modal','true'); 260 261 this.modalOpen = true; 262 this.triggerElement = document.activeElement; 263 if (this.firstFocusableElement) { 264 this.firstFocusableElement.focus({ 265 preventScroll: true 266 }); 267 } else { 268 document.activeElement.blur(); 269 } 270 $(document).trigger("bod-modal-after-open",this.$modalWrap); 271 272 }, 273 hide: function() { 274 if ($(document).trigger("bod-modal-before-close",this.$modalWrap).data('cancel')){ 275 return; 276 }; 277 278 this.$overlay.removeClass('active'); 279 this.$modalWrap.removeClass('active'); 280 // add the overlay and modal wrap back to the container and hide it 281 this.$overlay.appendTo(this.$container).hide(); 282 this.$modalWrap.appendTo(this.$container).hide(); 283 this.$modalWrap.attr('aria-modal','false'); 284 bodModalActive = false; 285 this.modalOpen = false; 286 $(document).trigger("bod-modal-after-close",this.$modalWrap); 287 288 // return focus to element that was active before modal called 289 // but do not scroll to it 290 if (this.triggerElement) this.triggerElement.focus( 291 { 292 preventScroll: true 293 } 294 ); 295 }, 296 keyPress: function(e) { 297 if ( e.keyCode === 27 && !this.disableEscapeClose) { // ESC 298 this.hide(); 299 } 300 301 // here we do the ADA focus trap but only if the modal is open 302 303 if (this.modalOpen) { 304 305 // code to enforce focus trap for ADA 306 307 let isTabPressed = e.key === 'Tab' || e.keyCode === 9; 308 309 // if keypress is not tab do nothing 310 if (!isTabPressed) { 311 return; 312 } 313 314 // Do we have at least one element in the modal that can receive focus 315 if (this.firstFocusableElement) { 316 if (e.shiftKey) { // if shift key pressed for shift + tab combination 317 if (document.activeElement === this.firstFocusableElement) { 318 this.lastFocusableElement.focus();
318 // add focus for the last focusable element 319 e.preventDefault(); 320 } 321 } else { // if tab key is pressed 322 if (document.activeElement === this.lastFocusableElement) { // if focused has reached to last focusable element then focus first focusable element after pressing tab 323 this.firstFocusableElement.focus(); // add focus for the first focusable element 324 e.preventDefault(); 325 } 326 } 327 } else { 328 // if we are here the modal is open and the tab key has been pressed 329 // but there are no elements in the modal that can receive focus. 330 // Therefore we do not want to provide focus to any element outside 331 // the modal. 332 e.preventDefault(); 333 } 334 } 335 }, 336 } 337 338 return { 339 initModal : initElements 340 } 341 342}; 343 344var bodModal = bodModalBlock(); 345jQuery(document).ready(function( $ ) { 346 347 bodModal.initModal(); 348 349}); 350 351// This is an example for the custom events, this one returns cancel=true so modal will not close 352//(function($){ 353//$(document).on("bod-modal-before-close", function(e, modalData){ 354// $(this).data('cancel', true); 355 356// this.$modalData = $(modalData); 357// this.$modalData.addClass('testbeforeclose'); 358// alert("added before close"); 359//}); 360//})(jQuery); 361 362// This is an example of how to manually trigger initialization 10 secs after page is ready 363//jQuery(document).ready(function( $ ) { 364// setTimeout(bodModal.initModal,10000); 365//});
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.