1/** 2 * ES2015 accessible modal window system, using ARIA 3 * 4 * Forked from van11y.net 5 * 6 * Website: https://van11y.net/accessible-modal/ 7 * License MIT: https://github.com/nico3333fr/van11y-accessible-modal-window-aria/blob/master/LICENSE 8 */ 9'use strict'; 10 11(function (doc) { 12 13 'use strict'; 14 15 var MODAL_ID_PREFIX = 'label_modal_'; 16 var MODAL_CLASS_SUFFIX = 'modal'; 17 var MODAL_PREFIX_CLASS_ATTR = 'data-modal-prefix-class'; 18 var MODAL_TEXT_ATTR = 'data-modal-text'; 19 var MODAL_CONTENT_ID_ATTR = 'data-modal-content-id'; 20 var MODAL_DESCRIBEDBY_ID_ATTR = 'data-modal-describedby-id'; 21 var MODAL_TITLE_ATTR = 'data-modal-title'; 22 var MODAL_FOCUS_TO_ATTR = 'data-modal-focus-toid'; 23 var MODAL_CLOSE_TEXT_ATTR = 'data-modal-close-text'; 24 25 var MODAL_BUTTON_CLASS_SUFFIX = 'modal-close'; 26 var MODAL_BUTTON_JS_ID = 'mc-modal-close'; 27 var MODAL_BUTTON_JS_CLASS = 'mc-modal-close'; 28 var MODAL_BUTTON_CONTENT_BACK_ID = 'data-content-back-id'; 29 var MODAL_BUTTON_FOCUS_BACK_ID = 'data-focus-back'; 30 31 var MODAL_WRAPPER_CLASS_SUFFIX = 'modal__wrapper'; 32 var MODAL_CONTENT_CLASS_SUFFIX = 'modal__content'; 33 var MODAL_CONTENT_JS_ID = 'mc-modal-content'; 34 35 var MODAL_CLOSE_TEXT_CLASS_SUFFIX = 'modal-close__text'; 36 37 var MODAL_TITLE_ID = 'modal-title'; 38 var MODAL_TITLE_CLASS_SUFFIX = 'modal-title'; 39 40 var FOCUSABLE_ELEMENTS_STRING = "a[href], area[href], input:not([type='hidden']):not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]"; 41 var WRAPPER_PAGE_JS = 'mc-modal-page'; 42 var MODAL_JS_ID = 'mc-modal'; 43 var MODAL_OVERLAY_ID = 'mc-modal-overlay'; 44 var MODAL_OVERLAY_CLASS_SUFFIX = 'modal-overlay'; 45 var MODAL_OVERLAY_TXT = 'Close modal'; 46 var MODAL_OVERLAY_BG_ENABLED_ATTR = 'data-background-click'; 47 var NO_SCROLL_CLASS = 'mc-no-scroll'; 48 var ATTR_OPEN = 'open'; 49 50 var findById = function findById(id) { 51 return doc.getElementById(id); 52 }; 53 54 var addClass = function addClass(el, className) { 55 if (el.classList) { 56 el.classList.add(className); 57 } 58 }; 59 60 var removeClass = function removeClass(el, className) { 61 if (el.classList) { 62 el.classList.remove(className); 63 } 64 }; 65 66 var hasClass = function hasClass(el, className) { 67 if (el.classList) { 68 return el.classList.contains(className); 69 } else { 70 return false; 71 } 72 }; 73 74 function wrapInner(parent, wrapper) { 75 if (typeof wrapper === "string") wrapper = document.createElement(wrapper); 76 77 parent.appendChild(wrapper); 78 79 while (parent.firstChild !== wrapper) wrapper.appendChild(parent.firstChild); 80 } 81 82 function remove(el) { 83 /* node.remove() is too modern for IEâ¤11 */ 84 el.parentNode.removeChild(el); 85 } 86 87 /* gets an element el, search if it is child of parent class, returns id of the parent */ 88 var searchParent = function searchParent(el, parentClass) { 89 var found = false; 90 var parentElement = el.parentNode; 91 while (parentElement && found === false) { 92 if (hasClass(parentElement, parentClass) === true) { 93 found = true; 94 } else { 95 parentElement = parentElement.parentNode; 96 } 97 } 98 if (found === true) { 99 return parentElement.getAttribute('id'); 100 } else { 101 return ''; 102 } 103 }; 104 105 /** 106 * Create the template for an overlay 107 * @param {Object} config 108 * @return {String} 109 */ 110 var createOverlay = function createOverlay(config) { 111 112 var id = MODAL_OVERLAY_ID; 113 var overlayText = config.text || MODAL_OVERLAY_TXT; 114 var overlayClass = config.prefixClass + MODAL_OVERLAY_CLASS_SUFFIX; 115 var overlayBackgroundEnabled = config.backgroundEnabled === 'disabled' ? 'disabled' : 'enabled'; 116 117 return '<span\n id="' + id + '"\n class="' + overlayClass + '"\n ' + MODAL_OVERLAY_BG_ENABLED_ATTR + '="' + overlayBackgroundEnabled + '"\n title="' + overlayText + '"\n >
117\n <span class="screen-reader-text">' + overlayText + '</span>\n </span>'; 118 }; 119 120 /** 121 * Create the template for a modal 122 * @param {Object} config 123 * @return {String} 124 */ 125 var createModal = function createModal(config) { 126 var dialog = document.createElement( 'dialog' ); 127 var outerContent = document.createElement( 'div' ); 128 var innerContent = document.createElement( 'div' ); 129 var contentContainer = document.createElement( 'div' ); 130 var titleContainer = document.createElement( 'div' ); 131 contentContainer.setAttribute( 'id', MODAL_CONTENT_JS_ID ); 132 titleContainer.classList.add( 'mc-modal-title-container' ); 133 134 var id = MODAL_JS_ID; 135 var modalClassName = config.modalPrefixClass + MODAL_CLASS_SUFFIX; 136 var modalClassWrapper = config.modalPrefixClass + MODAL_WRAPPER_CLASS_SUFFIX; 137 var buttonCloseClassName = config.modalPrefixClass + MODAL_BUTTON_CLASS_SUFFIX; 138 var buttonCloseInner = '<span class="' + config.modalPrefixClass + MODAL_CLOSE_TEXT_CLASS_SUFFIX + '">\n ' + config.modalCloseText + '\n </span>'; 139 var contentClassName = config.modalPrefixClass + MODAL_CONTENT_CLASS_SUFFIX; 140 var titleClassName = config.modalPrefixClass + MODAL_TITLE_CLASS_SUFFIX; 141 var title = config.modalTitle !== '' ? '<h1 id="' + MODAL_TITLE_ID + '" class="' + titleClassName + '">\n ' + config.modalTitle + '\n </h1>' : ''; 142 var button_close = '<button type="button" class="' + MODAL_BUTTON_JS_CLASS + ' ' + buttonCloseClassName + '" id="' + MODAL_BUTTON_JS_ID + '" ' + MODAL_BUTTON_CONTENT_BACK_ID + '="' + config.modalContentId + '" ' + MODAL_BUTTON_FOCUS_BACK_ID + '="' + config.modalFocusBackId + '"><span class="dashicons dashicons-no" aria-hidden="true"></span>\n ' + buttonCloseInner + '\n </button>'; 143 var content = config.modalText; 144 145 // If there is no content but an id we try to fetch content id 146 if (content === '' && config.modalContentId) { 147 var contentFromId = findById(config.modalContentId); 148 var hasMultipleEvents = contentFromId.classList.contains( 'calendar-events' ); 149 if ( ! hasMultipleEvents && contentFromId ) { 150 // Legacy templates don't have this class inside the content. 151 var event = contentFromId.querySelector( '.mc-event' ); 152 var classes; 153 if ( event ) { 154 classes = event.getAttribute( 'class' ); 155 } 156 if ( classes ) { 157 dialog.setAttribute('class', classes); 158 } 159 } 160 if (contentFromId) { 161 contentContainer.insertAdjacentElement( 'beforeEnd', contentFromId ); 162 } 163 } else { 164 contentContainer.insertAdjacentHTML( 'beforeEnd', content ); 165 } 166 dialog.setAttribute( 'id', id ); 167 dialog.classList.add( modalClassName ); 168 dialog.setAttribute( 'role', 'dialog' ); 169 dialog.setAttribute( 'aria-describedby', config.modalDescribedById + ' ' + ATTR_OPEN ); 170 dialog.setAttribute( 'aria-labelledby', MODAL_TITLE_ID ); 171 outerContent.setAttribute( 'role', 'document' ); 172 outerContent.classList.add( modalClassWrapper ); 173 titleContainer.insertAdjacentHTML( 'afterBegin', title ); 174 titleContainer.insertAdjacentHTML( 'afterBegin', button_close ); 175 innerContent.classList.add( contentClassName ); 176 innerContent.insertAdjacentElement( 'afterbegin', titleContainer ); 177 innerContent.insertAdjacentElement( 'beforeEnd', contentContainer ); 178 outerContent.insertAdjacentElement( 'afterBegin', innerContent ); 179 dialog.insertAdjacentElement( 'afterBegin', outerContent ); 180 181 return dialog; 182 }; 183 184 var closeModal = function closeModal(config) { 185 if (config.modalFocusBackId !== '') { 186 var modalReturn = findById( config.modalFocusBackId ); 187 var modalReturnContainer = modalReturn.closest( '.mc-events' ); // only works when control is inside container. 188 if ( modalReturnContainer && modalReturnContainer.tagName === 'ARTICLE' ) { 189 modalReturnContainer.insertAdjacentElement( 'beforeEnd', config.modalContent.firstChild ); 190 } else { 191 modalReturnContainer = modalReturn.closest( '.mc-main' ); 192 modalReturnContainer.insertAdjacentElement( 'beforeEnd', config.modalContent.firstChild ); 193 } 194 } 195 196 remove(config.modal); 197 remove(config.overlay); 198 }; 199 200 /** Find all modals inside a container 201 * @param {Node} node Default document 202 * @return {Array} 203 */ 204 var $listModals = function $listModals() { 205 var node = arguments.length <= 0 || arguments[0] === undefined ? doc : arguments[0]; 206 return [].slice.call(node.querySelectorAll('.mc-modal')); 207 }; 208 209 /** 210 * Build modals for a container 211 * @param {Node} node 212 */ 213 var attach = function attach(node) { 214 var addListeners = arguments.length <= 1 || arguments[1] === undefined ? true : arguments[1]; 215 216 $listModals(node).forEach(function (modal_node) { 217 218 var iLisible = Math.random().toString(32).slice(2, 12); 219 var wrapperBody = findById(WRAPPER_PAGE_JS); 220 var body = doc.querySelector('body'); 221 222 // Only generate an ID if node does not already have one. 223 if ( modal_node.hasAttribute('id') === false ) { 224 modal_node.setAttribute('id', MODAL_ID_PREFIX + iLisible); 225 } 226 modal_node.setAttribute('aria-haspopup', 'dialog'); 227 228 if (wrapperBody === null || wrapperBody.length === 0) { 229 var wrapper = doc.createElement('DIV'); 230 wrapper.setAttribute('id', WRAPPER_PAGE_JS); 231 wrapInner(body, wrapper); 232 wrapperBody = wrapper; 233 } 234 });
235 236 if (addListeners) { 237 238 /* listeners */ 239 ['click', 'keydown'].forEach(function (eventName) { 240 241 doc.body.addEventListener(eventName, function (e) { 242 243 // click on link modal 244 var parentModalLauncher = searchParent(e.target, 'mc-modal'); 245 if ((hasClass(e.target, 'mc-modal') === true || parentModalLauncher !== '') && eventName === 'click') { 246 var body = doc.querySelector('body'); 247 var modalLauncher = parentModalLauncher !== '' ? findById(parentModalLauncher) : e.target; 248 var modalPrefixClass = modalLauncher.hasAttribute(MODAL_PREFIX_CLASS_ATTR) === true ? modalLauncher.getAttribute(MODAL_PREFIX_CLASS_ATTR) + '-' : ''; 249 var modalText = modalLauncher.hasAttribute(MODAL_TEXT_ATTR) === true ? modalLauncher.getAttribute(MODAL_TEXT_ATTR) : ''; 250 var modalContentId = modalLauncher.hasAttribute(MODAL_CONTENT_ID_ATTR) === true ? modalLauncher.getAttribute(MODAL_CONTENT_ID_ATTR) : ''; 251 var modalDescribedById = modalLauncher.hasAttribute(MODAL_DESCRIBEDBY_ID_ATTR) === true ? modalLauncher.getAttribute(MODAL_DESCRIBEDBY_ID_ATTR) : ''; 252 var modalTitle = modalLauncher.hasAttribute(MODAL_TITLE_ATTR) === true ? modalLauncher.getAttribute(MODAL_TITLE_ATTR) : ''; 253 var modalCloseText = modalLauncher.hasAttribute(MODAL_CLOSE_TEXT_ATTR) === true ? modalLauncher.getAttribute(MODAL_CLOSE_TEXT_ATTR) : MODAL_OVERLAY_TXT; 254 var modalGiveFocusToId = modalLauncher.hasAttribute(MODAL_FOCUS_TO_ATTR) === true ? modalLauncher.getAttribute(MODAL_FOCUS_TO_ATTR) : ''; 255 256 var wrapperBody = findById(WRAPPER_PAGE_JS); 257 258 // insert overlay 259 body.insertAdjacentHTML('beforeEnd', createOverlay({ 260 text: modalCloseText, 261 backgroundEnabled: '', 262 prefixClass: modalPrefixClass 263 })); 264 265 // insert modal 266 body.insertAdjacentElement('beforeEnd', createModal({ 267 modalText: modalText, 268 modalPrefixClass: modalPrefixClass, 269 backgroundEnabled: modalContentId, 270 modalTitle: modalTitle, 271 modalCloseText: modalCloseText, 272 modalCloseTitle: modalCloseText, 273 modalContentId: modalContentId, 274 modalDescribedById: modalDescribedById, 275 modalFocusBackId: modalLauncher.getAttribute('id') 276 })); 277 278 // hide page 279 wrapperBody.setAttribute('inert', ''); 280 281 // add class noscroll to body 282 addClass(body, NO_SCROLL_CLASS); 283 284 // give focus to close button or specified element 285 var closeButton = findById(MODAL_BUTTON_JS_ID); 286 if (modalGiveFocusToId !== '') { 287 var focusTo = findById(modalGiveFocusToId); 288 if (focusTo) { 289 focusTo.focus(); 290 } else { 291 closeButton.focus(); 292 } 293 } else { 294 closeButton.focus(); 295 } 296 297 e.preventDefault(); 298 } 299 300 // click on close button or on overlay not blocked 301 var parentButton = searchParent(e.target, MODAL_BUTTON_JS_CLASS); 302 if ((e.target.getAttribute('id') === MODAL_BUTTON_JS_ID || parentButton !== '' || e.target.getAttribute('id') === MODAL_OVERLAY_ID || hasClass(e.target, MODAL_BUTTON_JS_CLASS) === true) && eventName === 'click') { 303 var body = doc.querySelector('body'); 304 var wrapperBody = findById(WRAPPER_PAGE_JS); 305 var modal = findById(MODAL_JS_ID); 306 var modalContent = findById(MODAL_CONTENT_JS_ID) ? findById(MODAL_CONTENT_JS_ID) : ''; 307 var overlay = findById(MODAL_OVERLAY_ID); 308 var modalButtonClose = findById(MODAL_BUTTON_JS_ID); 309 var modalFocusBackId = modalButtonClose.getAttribute(MODAL_BUTTON_FOCUS_BACK_ID); 310 var contentBackId = modalButtonClose.getAttribute(MODAL_BUTTON_CONTENT_BACK_ID); 311 var backgroundEnabled = overlay.getAttribute(MODAL_OVERLAY_BG_ENABLED_ATTR); 312 313 if (!(e.target.getAttribute('id') === MODAL_OVERLAY_ID && backgroundEnabled === 'disabled')) { 314 315 closeModal({ 316 modal: modal, 317 modalContent: modalContent, 318 overlay: overlay, 319 modalFocusBackId: modalFocusBackId, 320 contentBackId: contentBackId, 321 backgroundEnabled: backgroundEnabled, 322 fromId: e.target.getAttribute('id') 323 }); 324 325 // show back page 326 wrapperBody.removeAttribute('inert'); 327 328 // remove class noscroll to body 329 removeClass(body, NO_SCROLL_CLASS); 330 331 if (modalFocusBackId) { 332 var contentFocus = findById(modalFocusBackId); 333 if (contentFocus) { 334 contentFocus.focus(); 335 } 336 } 337 } 338 } 339 340 // strike a key when modal opened 341 if (findById(MODAL_JS_ID) && eventName === 'keydown') { 342 var body = doc.querySelector('body'); 343 var wrapperBody = findById(WRAPPER_PAGE_JS); 344 var modal = findById(MODAL_JS_ID); 345 var modalContent = findById(MODAL_CONTENT_JS_ID) ? findById(MODAL_CONTENT_JS_ID) : '';
346 var overlay = findById(MODAL_OVERLAY_ID); 347 var modalButtonClose = findById(MODAL_BUTTON_JS_ID); 348 var modalFocusBackId = modalButtonClose.getAttribute(MODAL_BUTTON_FOCUS_BACK_ID); 349 var contentBackId = modalButtonClose.getAttribute(MODAL_BUTTON_CONTENT_BACK_ID); 350 var listFocusables = [].slice.call(modal.querySelectorAll(FOCUSABLE_ELEMENTS_STRING)); 351 listFocusables = listFocusables.filter( checkElementVisibility ); 352 function checkElementVisibility(el) { 353 return el.checkVisibility(); 354 } 355 356 // esc 357 if (e.keyCode === 27) { 358 359 closeModal({ 360 modal: modal, 361 modalContent: modalContent, 362 overlay: overlay, 363 modalFocusBackId: modalFocusBackId, 364 contentBackId: contentBackId 365 }); 366 367 // show back page 368 wrapperBody.removeAttribute('inert'); 369 370 // remove class noscroll to body 371 removeClass(body, NO_SCROLL_CLASS); 372 373 if (modalFocusBackId) { 374 var contentFocus = findById(modalFocusBackId); 375 if (contentFocus) { 376 contentFocus.focus(); 377 } 378 } 379 } 380 381 // tab or shift tab in modal => capture focus 382 if (e.key === 'Tab' && listFocusables.indexOf(e.target) >= 0) { 383 // shift-tab on first element focusable => focus on last 384 if (e.shiftKey) { 385 if (e.target === listFocusables[0]) { 386 listFocusables.at(-1).focus(); 387 e.preventDefault(); 388 } 389 } else { 390 // tab on last element focusable => focus on first 391 if (e.target === listFocusables.at(-1)) { 392 listFocusables[0].focus(); 393 e.preventDefault(); 394 } 395 } 396 } 397 398 // tab outside modal => put it in focus 399 if (e.keyCode === 9 && listFocusables.indexOf(e.target) === -1) { 400 e.preventDefault(); 401 listFocusables[0].focus(); 402 } 403 } 404 }, true); 405 }); 406 } 407 }; 408 409 var onLoad = function onLoad() { 410 attach(); 411 document.removeEventListener('DOMContentLoaded', onLoad); 412 }; 413 414 document.addEventListener('DOMContentLoaded', onLoad); 415 416 window.accessibleModalWindowAria = attach; 417})(document);
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.