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