1/*! University of Maine - v0.0.1 2 * http://10up.com/ 3 * Copyright (c) 2022 4 * Licensed GPLv2+ 5 */ 6// jshint ignore: start 7/** 8 * Name: Accessible Modal Plugin 9 * Contributors: Tim Wright ([email protected], @csskarma) 10 * License: MIT, https://opensource.org/licenses/MIT 11 * Converted from a jQuery plugin originally written by @scottohara: https://github.com/scottaohara/accessible-components 12 */ 13 14( function() { 15 16 'use strict'; 17 18 // Define global TenUp object if it doesn't exist 19 if ( 'object' !== typeof window.TenUp ) { 20 window.TenUp = {}; 21 } 22 23 24 // Polyfill for el.matches 25 if (!Element.prototype.matches) { 26 Element.prototype.matches = 27 Element.prototype.matchesSelector || 28 Element.prototype.mozMatchesSelector || 29 Element.prototype.msMatchesSelector || 30 Element.prototype.oMatchesSelector || 31 Element.prototype.webkitMatchesSelector || 32 function(s) { 33 var matches = (this.document || this.ownerDocument).querySelectorAll(s), 34 i = matches.length; 35 while (--i >= 0 && matches.item(i) !== this) {} 36 return i > -1; 37 }; 38 } 39 40 /* 41 * Cross-browser way to deal with class management 42 */ 43 44 TenUp.hasClass = function ( el, cls ) { 45 return el.className && new RegExp("(\\s|^)" + cls + "(\\s|$)").test( el.className ); 46 }; 47 48 /* 49 * Cross-browser way to add a class 50 */ 51 52 TenUp.addClass = function ( el, cls ) { 53 54 if ( el.classList ) { 55 el.classList.add(cls); 56 } else if (!TenUp.hasClass(el, cls)) { 57 el.className += " " + cls; 58 } 59 60 }; 61 62 /* 63 * Cross-browser way to remove a class 64 */ 65 66 TenUp.removeClass = function ( el, cls ) { 67 if ( el.classList ) { 68 el.classList.remove( cls ); 69 } else if( TenUp.hasClass( el, cls ) ) { 70 var reg = new RegExp( '(\\s|^)' + cls + '(\\s|$)' ); 71 el.className = el.className.replace( reg, ' ' ); 72 } 73 }; 74 75 // Caching and setting up some variables 76 77 var i; 78 79 // use this defualt title if the attr isn't set 80 var safetyModalTitle = "Dialog Window"; 81 82 83 84 85 86 87 // initialize all the modals 88 TenUp.modal = function ( options, callback ) { 89 90 // Define the default values 91 var defaults = { 92 'target': '.a11y-modal' 93 }; 94 var defaults_count = defaults.length; 95 var opt; 96 97 // Map all default settings to user defined options 98 for ( opt = 0; opt < defaults_count; opt = opt + 1) { 99 if( typeof options[opt] === "undefined" ) { 100 options[opt] = defaults[opt]; 101 } 102 } 103 104 var el = document.querySelector( options.target ); 105 106 if ( ! el ) { 107 return; 108 } 109 110 var id = el.id; 111 var self = document.getElementById( id ); 112 113 var modalTrigger = options.trigger; 114 var modal = '.a11y-modal'; 115 var modalDoc = '.modal'; 116 var modalTitle = '[data-modal-title]'; 117 var modalClose = '[data-modal-close]'; 118 var bodyElements = 'a11y-hide-if-modal-open'; 119 var genModalClose = document.createElement( 'button' ); 120 var html = document.body; 121 var modallisting = document.querySelectorAll( modal ); 122 var modallistingCount = modallisting.length; 123 124 // build out the fallback button 125 genModalClose.setAttribute( 'type', 'button' ); 126 genModalClose.setAttribute( 'data-modal-close', 'true' ); 127 TenUp.addClass( genModalClose, 'modal__outro__close' ); 128 129 genModalClose.innerHTML = '<span umaine-modal-aria-hidden="true">x</span>'; 130 // setup modals properly 131 var setup_a11y_modal = function () { 132 133 // setup each modal instance to have the 134 // appropriate attributes. These attributes 135 // are applied to what would be considered the 136 // modal container, or 'overlay' 137 138 var findTitle = self.querySelector( modalTitle ); 139 var findHeading = self.querySelector( '[data-modal-title]' ); 140 var modalDocVar = self.querySelectorAll( modalDoc ); 141 var modalDocVarCount = modalDocVar.length; 142 var modalObj; 143 var thisLabel; 144 var j; 145 146 // first check to see what sort of dialog this should be 147 // if a data-modal-alert attribute is set to true, then 148 // this is meant to be an alert dialog, so set the role 149 // to 'alertdialog'. If it's not set, it's mean to be 150 // a normal dialog. So set the role to just 'dialog' 151 152 if ( self.getAttribute( 'data-modal-alert' ) === 'true' ) { 153 self.setAttribute( 'role', 'alertdialog' ); 154 } else { 155 self.setAttribute( 'role', 'dialog' ); 156 } 157 158 // we will need to set focus to the modal content 159 // container for focus trapping reasons, so we 160 // need this to have a tabindex 161 162 self.setAttribute( 'tabindex', '-1' ); 163 self.querySelector( modalDoc ).setAttribute( 'tabindex', '-1' ); 164 165 // check to see if an aria-label was set on the modal 166 // if not, then start running checks to apply an aria-labelledby 167 168 if ( !self.getAttribute( 'aria-label' ) ) { 169 170 // if the modal window has a child modalTitle set, 171 // then add an aria-labelledby attribute to the dialog, 172 // pointing to that element. 173 174 if ( findTitle ) { 175 176 thisLabel = findTitle.getAttribute( 'id' ); 177 178 } //if 179 180 // in the event that a modalTitle wasn't manually set, 181 // then we should look to see if there's a heading element 182 // present at all, and then make THAT the source for the 183 // aria-labelledby 184 185 else if ( findHeading ) { 186 187 // does the heading we found have an id already? 188 // let's check 189 190 if ( findHeading.setAttribute( 'id' ) ) { 191 192 thisLabel = findHeading.setAttribute( 'id' ); 193 194 } else { // if it doesn't, then generate one 195 196 thisLabel = self.setAttribute( 'id' ) + '_title'; 197 198 findHeading.setAttribute( 'id', thisLabel ); 199 200 } // else 201 202 } // else/if 203 204 self.setAttribute( 'aria-labelledby', thisLabel ); 205 206 } // if 207 208 // setup each modal content area (the component that 209 // contains the actual content) 210 211 for ( j = 0; j < modalDocVarCount; j = j + 1 ) { 212 213 modalObj = modalDocVar[j]; 214 215 // important for older versions of NVDA to accurately 216 // understand a modal's content 217 218 modalObj.setAttribute( 'role', 'document' ); 219 220 // Modals need a close button, and it should be the last 221 // element in the modal. 222 223 // If a modal doesn't have a close button, create it. 224 225 if ( typeof modalObj.querySelector( modalClose ) === 'undefined' ) { 226 227 if ( typeof modalObj.querySelector( '.modal__outro' ) === 'undefined' ) { 228 229 modalObj.querySelector( '.modal__outro' ).appendChild( genModalClose ); 230 231 } else { 232 233 modalObj.appendChild( genModalClose ); 234 235 } // if/else 236 237 } // if 238 239 // Set aria-label and control attributes to the close trigger. 240 241 modalObj.querySelector( modalClose ).setAttribute( 'aria-label', 'Close Mod
241al' ); 242 modalObj.querySelector( modalClose ).setAttribute( 'aria-controls', modalObj.parentNode.getAttribute( 'id' ) ); 243 244 } // end for loop 245 246 }; 247 248 // setup modal triggers 249 // the following applies needed aria-attributes 250 // to the modal triggers, as well as doing a 251 // final check to ensure that the modal window 252 // has appropriate labeling 253 254 var setup_a11y_modal_triggers = function () { 255 256 var modalTriggerEl = document.querySelectorAll( modalTrigger ); 257 var modalTriggerCount = modalTriggerEl.length; 258 var grabTarget; 259 var modalTarget; 260 var modalObj; 261 var m; 262 263 for ( m = 0; m < modalTriggerCount; m = m + 1 ) { 264 265 modalObj = modalTriggerEl[m]; 266 267 // if the trigger is a link, we need to give it a 268 // button role. 269 270 if ( modalObj.getAttribute( 'href' ) ) { 271 272 modalObj.setAttribute( 'role', 'button' ); 273 274 } 275 276 // The triggers need to point to the modals they control via 277 // the aria-controls attribute. So run a check to see if the 278 // attribute exists on the button. 279 // 280 // It's likely that it WON'T exist, as the optimal method for 281 // the minimum mark-up is to use a data-modal-open attribute 282 // instead. The reason for this is that in situations without 283 // JavaScript, we don't want partial ARIA hooks, as that can 284 // create confusion for ATs that would expect certain 285 // functionality that wouldn't be available due to lack of JS. 286 287 if ( !modalObj.getAttribute( 'aria-controls' ) ) { 288 289 // make sure that the trigger actually triggers something. 290 // if it there's no data-modal-open attribute set, then 291 // pull the target from the href 292 293 if ( modalObj.getAttribute( 'data-modal-open' ) ) { 294 295 grabTarget = modalObj.getAttribute( 'data-modal-open' ); 296 modalObj.setAttribute( 'aria-controls', grabTarget ); 297 298 } 299 // if there's no data-modal-open, pull the target from 300 // from the href 301 302 else if ( modalObj.getAttribute( 'href' ) ) { 303 304 grabTarget = modalObj.getAttribute( 'href' ).split( '#' )[1]; 305 modalObj.setAttribute( 'aria-controls', grabTarget ); 306 307 } 308 309 // if neither of the above are set, then this just won't work 310 311 else { 312 // No target set. A target is set by setting the value of an aria-controls attribute, which if absent, can be generated by the trigger's href URI, or a data-modal-open attribute to the value of the modal window ID you are attempting to open. 313 return false; 314 } 315 316 } // end if aria-controls 317 318 // now that the aria-controls is set, point to the modal's target 319 // so we can run the next if 320 321 modalTarget = document.querySelector( '#' + modalObj.getAttribute( 'aria-controls' ) ); 322 323 // finally a last check to see if the trigger is meant to launch 324 // an alert dialog modal. If the alertdialog role wasn't set during 325 // the initial setup function, then look to see if the 'data-modal-alert' 326 // attribute is present on the trigger, and if so, apply the alertdialog 327 // role to the modal on trigger activation. 328 329 if ( modalObj.getAttribute( 'data-modal-alert' ) === 'true' && modalTarget.getAttribute( 'role' ) !== 'alertdialog' ) { 330 modalTarget.setAttribute( 'role', 'alertdialog' ); 331 } 332 333 } // for loop 334 }; 335 336 // Place modal window(s) as the first child(ren) 337 // of the body element so tabbing backwards can 338 // move focus into the browser's address bar 339 340 var organize_dom = function () { 341 342 var body = document.body; 343 var modalEl = document.querySelectorAll( modal ); 344 var modalElCount = modalEl.length; 345 var k; 346 347 // place all the modal dialogs at the top of the DOM, as the 348 // first children of BODY. This will allow for backwards tabbing 349 // into the browser's address bar, where as if the modals were 350 // not located at the top of the DOM, keyboard focus would be 351 // completely trapped within the modal window. 352 353 for( k = 0; k < modalElCount; k = k + 1 ) { 354 body.insertBefore( modalEl[k], body.firstChild ); 355 } 356 357 // for all direct children of the BODY element, add a class 358 // to target during open/close 359 TenUp.addClass( body.querySelector( '*:not(.a11y-modal)' ), bodyElements ); 360 361 }; 362 363 var open_a11y_modal = function ( e ) { 364 365 // setup vars 366 367 var openTarget = e.target; 368 var modalTarget = document.getElementById( openTarget.getAttribute( 'aria-controls' ) ); 369 370 // Check to see if the modal has either an aria-label or labelledby attribute 371 // if not, that means that the modal didn't have a manually set aria-label, 372 // nor does the modal have any sort of heading element to draw a title from. 373 // In this instance, pull the safetyModalTitle var in as an aria-label 374 375 if ( !modalTarget.getAttribute( 'aria-labelledby' ) && !modalTarget.getAttribute( 'aria-label' ) ) { 376 377 // Last ditch effort to allow control over what the aria-label will be. 378 // If the data-set-modal-title attribute is set to the modal trigger,
379 // its value will be set as the modal's aria-label 380 381 if ( openTarget.getAttribute( 'data-set-modal-title' ) ) { 382 safetyModalTitle = openTarget.getAttribute( 'data-set-modal-title' ); 383 } 384 385 // set an aria-label to the modal 386 modalTarget.setAttribute( 'aria-label', safetyModalTitle ); 387 388 } // if 389 390 // traps focus while the modal is open 391 392 trap_focus(); 393 394 // if modal trigger is an <a>, make sure that URI isn't 395 // updated and more importantly that the document doesn't 396 // auto-jump to the DOM location of the modal window. 397 398 e.preventDefault(); 399 400 // set that modal be visible, controlled by the 401 // umaine-modal-aria-hidden attribute and CSS 402 // then shift focus to it 403 404 modalTarget.setAttribute( 'umaine-modal-aria-hidden', 'false' ); 405 406 // add a class to the HTML, to allow for a CSS hook 407 // to help restrict document scroll while the modal 408 // is open 409 410 TenUp.addClass( html, 'modal-is-open' ); 411 412 // Hide main document content from screen readers by 413 // applying an umaine-modal-aria-hidden attribute to all direct 414 // siblings of the modal windows. (var bodyElements) 415 416 document.querySelector( '.' + bodyElements ).setAttribute( 'umaine-modal-aria-hidden', 'true' ); 417 418 // finally, apply focus to the newly opened modal window 419 420 modalTarget.querySelector( modalDoc ).focus(); 421 422 }; 423 424 // Bind to both the button click and the escape key to 425 // close the modal window but only if isModalOpen is set to true 426 427 var close_a11y_modal = function ( e ) { 428 429 e.preventDefault(); 430 431 var returnFocus = document.querySelectorAll( '[aria-controls="' + self.getAttribute( 'id' ) + '"]'); 432 var returnFocusCount = returnFocus.length; 433 434 returnFocus = returnFocus[returnFocusCount - 1]; 435 436 TenUp.removeClass( html, 'modal-is-open' ); 437 self.setAttribute( 'umaine-modal-aria-hidden', 'true' ); 438 439 // remove the umaine-modal-aria-hidden that was applied during modal open 440 441 document.querySelector( '.' + bodyElements ).removeAttribute( 'umaine-modal-aria-hidden' ); 442 443 returnFocus.focus(); 444 445 }; 446 447 // keyboard controls specific to the modal dialog windows 448 449 var keytrolls_a11y_modal_trigger = function ( e ) { 450 451 var keyCode = e.keyCode || e.which; 452 453 switch ( keyCode ) { 454 455 // space & enter 456 457 case 32: 458 case 13: 459 e.stopPropagation(); 460 e.target.click(); 461 break; 462 463 } // switch 464 465 }; 466 467 // trap focus within the modal window, because otherwise 468 // users can tab to obscured elements, and that's just 469 // bad UX. 470 471 var trap_focus = function () { 472 473 // I'm open to better solutions for trapping focus within the modal with it's open 474 var all_nodes = document.querySelectorAll( "*" ); 475 var trapArea = self.querySelector( modalDoc ); 476 var nodeCount = all_nodes.length; 477 var j; 478 479 480 for ( j = 0; j < nodeCount; j = j + 1 ) { 481 all_nodes.item( j ).addEventListener( "focus", function( e ) { 482 483 if ( TenUp.hasClass( html, 'modal-is-open' ) && !trapArea.contains( e.target ) ) { 484 485 e.stopPropagation(); 486 trapArea.focus(); 487 488 } 489 }, false ); 490 } 491 492 }; // end trap_focus 493 494 /* 495 * SETUP FUNCTIONS 496 */ 497 498 organize_dom(); 499 setup_a11y_modal(); 500 setup_a11y_modal_triggers(); 501 502 /* 503 * EVENT BINDINGS 504 */ 505 506 // close the modal is the overlay is clicked 507 508 self.addEventListener( "click", function( e ) { 509 510 if ( e.target === self.querySelector( modalDoc ).parentNode ) { 511 e.stopPropagation(); 512 close_a11y_modal( e ); 513 } 514 }, false ); 515 516 // close the modal on ESC 517 518 self.addEventListener("keydown", function( e ) { 519 520 if( e.keyCode == 27 && TenUp.hasClass( html, 'modal-is-open' ) ) { 521 close_a11y_modal( e ); 522 } 523 }, false); 524 525 // close the modal if you click the close button 526 527 self.querySelector( modalClose ).addEventListener('click', function( e ) { 528 close_a11y_modal( e ); 529 }, false ); 530 531 // open the modal when the trigger is clicked 532 533 document.addEventListener( "click", function( e ) { 534 535 if ( e.target.matches( modalTrigger ) ) { 536 e.stopPropagation(); 537 open_a11y_modal( e ); 538 } 539 540 }, false ); 541 542 // making sure the modal triggers open with <enter> and <space> (making it act like a button, if it's a link) 543 544 document.addEventListener( "keydown", function( e ) { 545 if (e.target.matches( modalTrigger )) { 546 e.stopPropagation(); 547 keytrolls_a11y_modal_trigger( e ); 548 } 549 }, false ); 550 551 // accept callback functions, because why not? 552 553 if ( typeof callback === 'function' ) { 554 callback.call(); 555 } 556 557 }; // end: a11y_modal: function 558 559 } )(); 560 561;(function ( w, doc ) { 562 563 // Enable strict mode 564 'use strict'; 565 566 ( function hashClick() { 567 568 // wait for everything to be loaded 569 w.onload = function() { 570 571 // is there a #hash in the url? 572 if ( w.location.hash ) { 573 574 var hashValue = w.location.hash; 575 var element = doc.querySelector("a[href='" + hashValue + "']"); 576 var data = doc.querySelector("[data-href='" + hashValue + "']"); 577 578 if( element ) { element.click(); } 579 if( data ) { data.click(); } 580 581 } // End if 582 583 }; // End window.onload 584 585 })(); 586 587 })( this, this.document ); 588 589// jshint ignore: start 590/** 591 * University of Maine 592 * http://10up.com/ 593 * 594 * Copyright (c) 2017 10up 595 * Licensed under the GPLv2+ license. 596 */ 597 598var UMaine = UMaine || {}; 599 600UMaine.modal = ( function( window, TenUp, undefined ) { 601 var cache = { 602 modal: document.getElementById( 'gallery-modal' ),
603 modalCaption: document.getElementById( 'modal-caption' ) 604 }; 605 606 /** 607 * Attach events 608 */ 609 var init = function() { 610 /** 611 * Bail if TenUp is not defined 612 */ 613 614 if ( 'undefined' === typeof TenUp ) { 615 return; 616 } 617 618 TenUp.modal({ 619 'target': '#gallery-modal', 620 'trigger': '.gallery a img' 621 }, function() { 622 setUpGalleries(); 623 }); 624 }; 625 626 /** 627 * Prepares functions in case multiple galleries are set 628 */ 629 var setUpGalleries = function() { 630 var galleries = document.querySelectorAll( '.gallery' ); 631 632 if ( 0 === galleries.length ) { 633 return; 634 } 635 636 Array.prototype.forEach.call( galleries, setUpGallery ); 637 }; 638 639 /** 640 * Updates links and image attributes to make modal open. 641 * @param {*} gallery 642 * @param {*} index 643 */ 644 var setUpGallery = function( gallery, index ) { 645 if ( 'undefined' === typeof gallery ) { 646 return; 647 } 648 649 cache.modal.setAttribute( 'aria-labelledby', 'Image Lightbox' ); 650 651 var links = gallery.querySelectorAll( 'a' ); 652 653 if ( 0 === links.length ) { 654 return; 655 } 656 657 Array.prototype.forEach.call( links, changeLinkTarget ); 658 } 659 660 /** 661 * Changes gallery link target to point to the modal 662 * Saves image url in a data attr 663 * @param {*} link 664 * @param {*} index 665 */ 666 var changeLinkTarget = function( link, index ) { 667 if ( 'undefined' === typeof link ) { 668 return; 669 } 670 671 var imageUrl = link.getAttribute( 'href' ), 672 image = link.querySelector( 'img' ); 673 674 /** 675 * Change link only if it points to an image 676 */ 677 var regex = /\.(gif|jpe?g|tiff|png|webp)$/; 678 679 if ( ! regex.test( imageUrl ) ) { 680 return; 681 } 682 683 link.setAttribute( 'href', '#gallery-modal' ); 684 image.setAttribute( 'data-image', imageUrl ); 685 image.setAttribute( 'aria-controls', 'gallery-modal' ); 686 687 link.addEventListener( 'click', populateModal ); 688 }; 689 690 /** 691 * Loads image inside modal. 692 * @param {*} event 693 */ 694 var populateModal = function( event ) { 695 if ( ! event.target.matches( '.gallery a img' ) ) { 696 return; 697 } 698 699 var image = event.target, 700 imageUrl = image.getAttribute( 'data-image' ), 701 link = image.parentElement, 702 modalContent, 703 newImage, 704 closeButton; 705 706 if ( 'undefined' === typeof cache.modal ) { 707 return; 708 } 709 710 setUpCaption( link ); 711 712 modalContent = cache.modal.querySelector( '.modal__content' ); 713 closeButton = cache.modal.querySelector( '.modal__outro__close' ); 714 715 if ( 'undefined' === typeof modalContent ) { 716 return; 717 } 718 719 newImage = new Image(); 720 newImage.src = imageUrl;
721 newImage.classList.add( 'modal-image' ); 722 723 modalContent.appendChild( newImage ); 724 closeButton.addEventListener( 'click', emptyModal ); 725 // close the modal on ESC 726 document.addEventListener( 'keydown', function( event ) { 727 if( event.keyCode == 27 ) { 728 event.stopPropagation(); 729 emptyModal(); 730 } 731 }, false); 732 733 cache.modal.addEventListener( "click", function( event ) { 734 if ( ! event.target.matches( '#gallery-modal' ) ) { 735 return; 736 } 737 738 event.stopPropagation(); 739 emptyModal(); 740 }, false ); 741 }; 742 743 /** 744 * Remove image to prevent duplicates inside modal. 745 */ 746 var emptyModal = function() { 747 var images = document.querySelectorAll( '.modal-image' ); 748 749 if ( 0 === images.length ) { 750 return; 751 } 752 753 Array.prototype.forEach.call( images, function( image ) { 754 image.parentNode.removeChild( image ); 755 } ); 756 757 cache.modalCaption.innerText = ''; 758 }; 759 760 /** 761 * Looks for a caption and adds it to the bottom of the modal 762 * @param {*} link 763 */ 764 var setUpCaption = function( link ) { 765 if ( 'undefined' === typeof link ) { 766 return; 767 } 768 769 var parent = link.parentElement, 770 captionEl = parent.nextElementSibling; 771 772 if ( ! captionEl ) { 773 return; 774 } 775 776 cache.modalCaption.innerText = captionEl.innerText; 777 }; 778 779 return { 780 init: init 781 }; 782} )( window, TenUp ); 783 784( function( window, undefined ) { 785 "use strict"; 786 UMaine.modal.init(); 787} )( this );
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.