1/* ========================================================= 2 ACCESSIBILITY_SUBPAGES.JS 3 Zayed University - www.zu.ac.ae (sub-page layout) 4 5 Loaded only on inside pages (not the homepage). 6 Homepage continues to use accessibility.js. 7 8 Add sub-page-only fixes here so the main homepage script 9 stays smaller and does not run duplicate logic. 10 11 Dependencies: jQuery (must load before this file). 12 ========================================================= */ 13 14$(document).ready(function () { 15 16 17 /* ##################################################### 18 SECTION 1 19 FOOTER PULSE CONTROLS (#footer-customer-pulse) 20 Two icon-only <a href="javascript:void(0)"> triggers: 21 - .smile.zuicon-customer-pulse -> customerPulse() 22 - #signlanguage.zuicon-sign-language 23 They have no text content, so screen readers cannot 24 announce them. Give each one a button role and an 25 accessible name. No HTML / behaviour changes. 26 ##################################################### */ 27 28 const $footerPulse = $('#footer-customer-pulse'); 29 30 if ($footerPulse.length) { 31 32 $footerPulse 33 .find('a.smile.zuicon-customer-pulse') 34 .attr({ 35 'role': 'button', 36 'aria-label': 'Open customer feedback' 37 }); 38 39 $footerPulse 40 .find('#signlanguage') 41 .attr({ 42 'role': 'button', 43 'aria-label': 'Sign language assistance' 44 }); 45 46 // Keyboard activation: Enter and Space trigger click on both controls. 47 $footerPulse.on( 48 'keydown', 49 'a.smile.zuicon-customer-pulse, #signlanguage', 50 function (e) { 51 if (e.key === 'Enter' || e.key === ' ') { 52 e.preventDefault(); 53 $(this).trigger('click'); 54 } 55 } 56 ); 57 58 } 59 60 61 62 63 /* ##################################################### 64 SECTION 2 65 LEFT-BAR EXPANDABLE ITEMS 66 Source: formats/sub_menu_level1_standard.xslt 67 68 Each <a class="expandable"> is rendered with 69 aria-haspopup="true" and aria-expanded="false". The 70 link navigates to a child page (full page reload), 71 at which point the menu is re-rendered server-side 72 with that folder marked as "expanded". 73 74 This handler flips aria-expanded to "true" on the 75 clicked link just before navigation, so assistive 76 technology can announce the state change in the 77 moment between click and navigation. It's a defensive 78 sync â harmless if the user navigates away 79 immediately, useful if the navigation is delayed 80 (e.g. Ctrl/Cmd+click opens a new tab, or another 81 click handler runs first). 82 ##################################################### */ 83 84 $('#left-bar').on( 85 'click keyup', 86 'a.expandable', 87 function (e) { 88 if (e.type === 'click' 89 || e.key === 'Enter' 90 || e.key === ' ') { 91 $(this).attr('aria-expanded', 'true'); 92 } 93 } 94 ); 95 96 97 98 99 /* ##################################################### 100 SECTION 3 101 MOBILE SECTION-MENU OPEN / CLOSE CONTROLS 102 Source: inline script in inside_content_single*.html 103 104 - #more-options (open) : <a class="btn" href="#more-options"> 105 <span class="fa fa-bars"></span></a> 106 - #submenu-close (close) : injected at runtime as 107 <a class="close submenu-close ..." 108 id="submenu-close">X</a> 109 110 We DO NOT change the tag from <a> to <button>: existing 111 CSS targets these selectors and a tag swap would break 112 layout / icon positioning. 113 114 Instead we layer ARIA + keyboard semantics on top: 115 - role="button" so AT announces them as buttons 116 - aria-label so the icon-only / "X" controls have a name 117 - aria-hidden on the decorative <span class="fa ..."> icon 118 - tabindex="0" on the injected close link (it has no href, 119 so by default it is not in the tab order) 120 - Enter / Space keyboard activation 121 - aria-expanded sync on #more-options reflecting the 122 .left-bar.open state toggled by the inline handlers 123 ##################################################### */ 124 125 const $moreOptions = $('#more-options'); 126 127 if ($moreOptions.length) { 128 129 $moreOptions 130 .attr({ 131 'role': 'button', 132 'aria-label': 'Open section menu', 133 'aria-controls': 'left-bar', 134 'aria-expanded': 'false' 135 }) 136 .find('.fa') 137 .attr('aria-hidden', 'true'); 138 139 // Reflect open state after the inline click handler runs. 140 $moreOptions.on('click keydown', function (e) { 141 if (e.type === 'keydown') { 142 if (e.key !== 'Enter' && e.key !== ' ') { return; } 143 e.preventDefault(); 144 $(this).trigger('click'); 145 return; 146 } 147 // click: defer so .left-bar class is already toggled. 148 const $btn = $(this); 149 setTimeout(function () { 150 $btn.attr( 151 'aria-expanded', 152 $('.left-bar').hasClass('open') ? 'true' : 'false' 153 ); 154 }, 0); 155 }); 156 157 } 158 159 // #submenu-close is appended at runtime by an inline script in 160 // the page template. That inline script is registered earlier 161 // in $(document).ready than this file, so the node exists now; 162 // we still use a delegated keydown for resilience. 163 164 const $submenuClose = $('#submenu-close'); 165 166 if ($submenuClose.length) { 167 $submenuClose.attr({ 168 'role': 'button', 169 'aria-label': 'Close section menu', 170 'tabindex': '0' 171 }); 172 } 173 174 $(document).on('keydown', '#submenu-close', function (e) { 175 if (e.key === 'Enter' || e.key === ' ') { 176 e.preventDefault(); 177 $(this).trigger('click'); 178 } 179 }); 180 181 // When the close link is clicked, mark the opener collapsed. 182 $(document).on('click', '#submenu-close', function () { 183 $('#more-options').attr('aria-expanded', 'false'); 184 }); 185 186 187 188 189 /* ##################################################### 190 SECTION 4 191 MISSING / EMPTY IMAGE ALT TEXT - DISCERNIBLE FALLBACK 192 WCAG 1.1.1 - Non-text content. 193 194 Acts on <img> when alt is absent OR empty / whitespace 195 only (including alt=""). Any non-empty alt string is 196 NEVER overwritten â human-authored descriptions win. 197 198 Resolution order (first match wins): 199 1. aria-hidden="true" OR role="presentation"|"none" 200 on the <img> element itself -> alt="" 201 (Ancestor aria-hidden="true" does NOT skip: alt is 202 still filled for auditing / tooling / consistency.) 203 2. aria-label / aria-labelledby present on the <img> 204 -> alt="" (image already has an accessible name) 205 3. title attribute present and non-empty 206 -> alt = title 207 4. data-original-title / data-title (Bootstrap tooltip) 208 -> alt = that value 209 5. Image is inside <a> or <button> that already has 210 non-empty visible text or aria-label 211 -> alt="" (parent text is the accessible name; 212 adding alt text would duplicate it) 213 6. Image is inside <figure>
213 with non-empty <figcaption> 214 -> alt="" (caption describes the image) 215 7. Derive from filename: 216 strip path / query / extension, drop common prefixes 217 (icon-, ic-, bg-, img-), replace [-_] with spaces, 218 sentence-case. Skip hash-like names. 219 8. Last resort -> alt="" (treat as decorative; this 220 satisfies validators without inventing misleading 221 text). 222 223 The pass runs on document ready, again on window load 224 (catches lazy images and Owl carousel clones), and a 225 MutationObserver watches for late-inserted <img> nodes. 226 ##################################################### */ 227 228 function imgNeedsAltFix(imgEl) { 229 if (!imgEl || imgEl.tagName !== 'IMG') { return false; } 230 if (!imgEl.hasAttribute('alt')) { return true; } 231 var v = imgEl.getAttribute('alt'); 232 return (typeof v === 'string' && v.trim().length === 0); 233 } 234 235 function deriveAltFromContext($img) { 236 237 if ($img.attr('aria-hidden') === 'true' 238 || $img.attr('role') === 'presentation' 239 || $img.attr('role') === 'none') { 240 return ''; 241 } 242 243 if ($img.attr('aria-label') 244 || $img.attr('aria-labelledby')) { 245 return ''; 246 } 247 248 var title = ($img.attr('title') || '').trim(); 249 if (title) { return title; } 250 251 var dataTitle = ( 252 $img.attr('data-original-title') 253 || $img.attr('data-title') 254 || '' 255 ).trim(); 256 if (dataTitle) { return dataTitle; } 257 258 var $interactive = $img.closest('a, button'); 259 if ($interactive.length) { 260 var interactiveLabel = ($interactive.attr('aria-label') || '').trim(); 261 if (interactiveLabel) { return ''; } 262 // Visible text inside the link/button, excluding any nested <img>. 263 var text = $interactive 264 .clone() 265 .find('img').remove().end() 266 .text() 267 .replace(/\s+/g, ' ') 268 .trim(); 269 if (text.length > 0) { return ''; } 270 } 271 272 var $figcaption = $img.closest('figure').find('figcaption').first(); 273 if ($figcaption.length && $figcaption.text().trim().length > 0) { 274 return ''; 275 } 276 277 var src = $img.attr('src') || $img.attr('data-src') || ''; 278 if (src) { 279 var name = src.split('?')[0].split('#')[0].split('/').pop() || ''; 280 name = name.replace(/\.(jpe?g|png|gif|svg|webp|bmp|ico)$/i, ''); 281 // Skip hash-like / opaque filenames (e.g. tracking pixels). 282 if (name && !/^[a-f0-9]{16,}$/i.test(name)) { 283 name = name.replace(/^(icon[-_]|ic[-_]|bg[-_]|img[-_]|image[-_]|pic[-_])/i, ''); 284 name = name.replace(/[-_]+/g, ' ').replace(/\s+/g, ' ').trim(); 285 if (name.length > 1 && /[a-z]/i.test(name)) { 286 return name.charAt(0).toUpperCase() + name.slice(1); 287 } 288 } 289 } 290 291 return ''; 292 } 293 294 function fixMissingAlts(root) { 295 var scope = root && root.nodeType ? root : document; 296 var images = $(scope).find ? $(scope).find('img').filter(function () { 297 return imgNeedsAltFix(this); 298 }) : $(); 299 // If "scope" itself is an <img> needing alt, include it. 300 if (scope.nodeType === 1 && scope.tagName === 'IMG' && imgNeedsAltFix(scope)) { 301 images = images.add(scope); 302 } 303 images.each(function () { 304 var $img = $(this); 305 // Defensive: re-check, another script may have just set alt. 306 if (!imgNeedsAltFix(this)) { return; } 307 $img.attr('alt', deriveAltFromContext($img)); 308 }); 309 } 310 311 fixMissingAlts(document); 312 313 // Catch images that arrive after document ready (lazy loaders, 314 // Owl carousel clones, AJAX content panels, etc.). 315 $(window).on('load', function () { 316 fixMissingAlts(document); 317 }); 318 319 if (typeof window.MutationObserver === 'function') { 320 var imgObserver = new MutationObserver(function (mutations) { 321 for (var i = 0; i < mutations.length; i++) { 322 var added = mutations[i].addedNodes; 323 for (var j = 0; j < added.length; j++) { 324 var node = added[j]; 325 if (node.nodeType !== 1) { continue; } 326 if (node.tagName === 'IMG' && imgNeedsAltFix(node)) { 327 fixMissingAlts(node); 328 } else if (node.querySelector && node.querySelector('img')) { 329 fixMissingAlts(node); 330 } 331 } 332 } 333 }); 334 imgObserver.observe(document.body, { 335 childList: true, 336 subtree: true 337 }); 338 } 339 340 341 342 343 /* ##################################################### 344 SECTION 5 345 MISSING / EMPTY IFRAME TITLE 346 WCAG 4.1.2 / 2.4.1 - iframes need an accessible name. 347 348 Sets title when absent or whitespace-only. Non-empty 349 title values are NEVER overwritten. 350 351 Resolution order (first match wins): 352 1. aria-label on the <iframe> 353 2. aria-labelledby (text of referenced element(s)) 354 3. Nearest preceding h1âh6 in the same column / wrapper 355 4. Parent [aria-label] or <figure> <figcaption> 356 5. Humanized id (e.g. app-form -> Embedded form)
357 6. Last path filename from src (e.g. ZUContactUs/Default.aspx 358 -> "ZU Contact Us"; skips paths with no file extension) 359 7. Last resort -> "Embedded content" 360 ##################################################### */ 361 362 function iframeNeedsTitleFix(iframeEl) { 363 if (!iframeEl || iframeEl.tagName !== 'IFRAME') { return false; } 364 if (!iframeEl.hasAttribute('title')) { return true; } 365 var v = iframeEl.getAttribute('title'); 366 return (typeof v === 'string' && v.trim().length === 0); 367 } 368 369 function humanizeToken(str) { 370 if (!str) { return ''; } 371 str = str.replace(/^(iframe[-_]?|embed[-_]?|frame[-_]?)/i, ''); 372 str = str.replace(/[-_]+/g, ' ').replace(/\s+/g, ' ').trim(); 373 if (!str || !/[a-z]/i.test(str)) { return ''; } 374 return str.charAt(0).toUpperCase() + str.slice(1); 375 } 376 377 function humanizeFileName(name) { 378 if (!name) { return ''; } 379 name = name.replace(/([a-z0-9])([A-Z])/g, '$1 $2'); 380 name = name.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2'); 381 name = name.replace(/[-_]+/g, ' ').replace(/\s+/g, ' ').trim(); 382 return humanizeToken(name); 383 } 384 385 function deriveTitleFromSrc(src) { 386 var s = (src || '').trim(); 387 if (!s) { return ''; } 388 try { 389 var path = s.split('?')[0].split('#')[0]; 390 var segments = path.split('/').filter(Boolean); 391 if (!segments.length) { return ''; } 392 393 var last = segments[segments.length - 1]; 394 if (!/\.(aspx?|html?|php|jsp|htm)$/i.test(last)) { 395 return ''; 396 } 397 398 var file = last.replace(/\.(aspx?|html?|php|jsp|htm)$/i, ''); 399 if (/^(default|index|home|main|view|blank|start)$/i.test(file) 400 && segments.length > 1) { 401 file = segments[segments.length - 2]; 402 file = file.replace(/\.(aspx?|html?|php|jsp|htm)$/i, ''); 403 } 404 405 return humanizeFileName(file); 406 } catch (e) { 407 return ''; 408 } 409 } 410 411 function deriveIframeTitle($iframe) { 412 var ariaLabel = ($iframe.attr('aria-label') || '').trim(); 413 if (ariaLabel) { return ariaLabel; } 414 415 var labelledBy = ($iframe.attr('aria-labelledby') || '').trim(); 416 if (labelledBy) { 417 var parts = labelledBy.split(/\s+/); 418 var text = []; 419 for (var i = 0; i < parts.length; i++) { 420 var el = document.getElementById(parts[i]); 421 if (el) { 422 var t = $(el).text().replace(/\s+/g, ' ').trim(); 423 if (t) { text.push(t); } 424 } 425 } 426 if (text.length) { return text.join(' '); } 427 } 428 429 var $scope = $iframe.closest( 430 '.page-content, .middle-content, .col-lg-8, .col-lg-10, .col-lg-12, .iframe-container, .embed-responsive' 431 ); 432 if (!$scope.length) { $scope = $iframe.parent(); } 433 434 var $heading = $scope.find('h1, h2, h3, h4, h5, h6').first(); 435 if (!$heading.length) { 436 $heading = $iframe.prevAll('h1, h2, h3, h4, h5, h6').first(); 437 } 438 if ($heading.length) { 439 var headingText = $heading.text().replace(/\s+/g, ' ').trim(); 440 if (headingText) { return headingText; } 441 } 442 443 var parentLabel = ($iframe.parent().attr('aria-label') || '').trim(); 444 if (parentLabel) { return parentLabel; } 445 446 var $figcaption = $iframe.closest('figure').find('figcaption').first(); 447 if ($figcaption.length) { 448 var cap = $figcaption.text().replace(/\s+/g, ' ').trim(); 449 if (cap) { return cap; } 450 } 451 452 var idTitle = humanizeToken($iframe.attr('id') || ''); 453 if (idTitle) { return idTitle; } 454 455 var fromSrc = deriveTitleFromSrc($iframe.attr('src') || ''); 456 if (fromSrc) { return fromSrc; } 457 458 return 'Embedded content'; 459 } 460 461 function fixMissingIframeTitles(root) { 462 var scope = root && root.nodeType ? root : document; 463 var iframes = $(scope).find ? $(scope).find('iframe').filter(function () { 464 return iframeNeedsTitleFix(this); 465 }) : $(); 466 if (scope.nodeType === 1 && scope.tagName === 'IFRAME' && iframeNeedsTitleFix(scope)) { 467 iframes = iframes.add(scope); 468 } 469 iframes.each(function () { 470 var $frame = $(this); 471 if (!iframeNeedsTitleFix(this)) { return; } 472 $frame.attr('title', deriveIframeTitle($frame)); 473 }); 474 } 475 476 fixMissingIframeTitles(document); 477 478 $(window).on('load', function () { 479 fixMissingIframeTitles(document); 480 }); 481 482 if (typeof window.MutationObserver === 'function') { 483 var iframeObserver = new MutationObserver(function (mutations) { 484 for (var m = 0; m < mutations.length; m++) { 485 var added = mutations[m].addedNodes; 486 for (var n = 0; n < added.length; n++) { 487 var node = added[n]; 488 if (node.nodeType !== 1) { continue; } 489 if (node.tagName === 'IFRAME' && iframeNeedsTitleFix(node)) { 490 fixMissingIframeTitles(node); 491 } else if (node.querySelector && node.querySelector('iframe')) { 492 fixMissingIframeTitles(node); 493 } 494 } 495 } 496 }); 497 iframeObserver.observe(document.body, { 498 childList: true, 499 subtree: true 500 }); 501 } 502 503 504 505 506 /* ##################################################### 507 SECTION 6 508 IFRAME CONTAINER HEIGHT (.iframe-container) 509 Pages such as make-an-enquiry.html use Bootstrap 510 embed-responsive: the box height comes from 511 padding-bottom on .iframe-container and the iframe 512 is position:absolute with height:100%. A fixed 513 padding % often clips content or leaves inner scroll. 514 515 When .iframe-container exists and contains an 516 <iframe>, read same-origin document height after 517 load and set padding-bottom (%) to match so the 518 iframe fits without a vertical scrollbar. 519 Percentage = (content height / container width) Ã 100, 520 matching the embed-responsive aspect-ratio pattern. 521 522 Cross-origin iframes cannot be measured from the 523 parent page; those containers are left unchanged. 524 ##################################################### */ 525 526 function getIframeContentHeight(iframeEl) { 527 528 if (!iframeEl || iframeEl.tagName !== 'IFRAME') { 529 return 0; 530 } 531 532 try { 533 var doc = iframeEl.contentDocument || iframeEl.contentWindow.document; 534 535 if (!doc || !doc.body) { 536 return 0; 537 } 538 539 return Math.max( 540 doc.body.scrollHeight, 541 doc.documentElement.scrollHeight, 542 doc.body.offsetHeight, 543 doc.documentElement.offsetHeight 544 ); 545 546 } catch (err) { 547 return 0; 548 } 549 550 } 551 552 553 function applyIframeContainerPadding($container) { 554 555 var $iframe = $container.children('iframe').first(); 556 557 if (!$iframe.length) { 558 $iframe = $container.find('iframe').first(); 559 } 560 561 if (!$iframe.length) { 562 return; 563 } 564 565 var height = getIframeContentHeight($iframe[0]); 566 var width = $container.innerWidth() || $container.width(); 567 568 if (height < 1 || width < 1) { 569 return; 570 } 571 572 /* +5% buffer for validation/error UI inside the iframe */ 573 var paddingPercent = ((height / width) * 100) + 5; 574 575 $container.css('padding-bottom', paddingPercent + '%'); 576 577 $iframe.css({ 578 height: '100%', 579 overflow: 'hidden' 580 }); 581 582 } 583 584 585 function bindIframeContainerResize($container) { 586 587 if (!$container.length || !$container.hasClass('iframe-container')) { 588 return; 589 } 590 591 if (!$container.find('iframe').length) { 592 return; 593 } 594 595 var $iframe = $container.find('iframe').first(); 596 597 function scheduleApply() { 598 applyIframeContainerPadding($container); 599 setTimeout(function () { 600 applyIframeContainerPadding($container); 601 }, 100); 602 setTimeout(function () { 603 applyIframeContainerPadding($container); 604 }, 500); 605 setTimeout(function () { 606 applyIframeContainerPadding($container); 607 }, 1500); 608 } 609
610 $iframe.off('load.iframeContainerResize').on('load.iframeContainerResize', scheduleApply); 611 612 try { 613 var doc = $iframe[0].contentDocument || $iframe[0].contentWindow.document; 614 615 if (doc && doc.readyState === 'complete') { 616 scheduleApply(); 617 } 618 619 } catch (err) { 620 /* cross-origin â cannot read iframe document height */ 621 } 622 623 } 624 625 626 function initIframeContainerResize(root) { 627 628 var $containers = $(); 629 630 if (root && root.nodeType === 1) { 631 632 if (root.classList && root.classList.contains('iframe-container') 633 && root.querySelector && root.querySelector('iframe')) { 634 $containers = $containers.add(root); 635 } 636 637 if (root.querySelectorAll) { 638 $(root.querySelectorAll('.iframe-container')).each(function () { 639 if (this.querySelector('iframe')) { 640 $containers = $containers.add(this); 641 } 642 }); 643 } 644 645 } else { 646 647 $('.iframe-container').each(function () { 648 if (this.querySelector('iframe')) { 649 $containers = $containers.add(this); 650 } 651 }); 652 653 } 654 655 $containers.each(function () { 656 bindIframeContainerResize($(this)); 657 }); 658 659 } 660 661 662 initIframeContainerResize(document); 663 664 $(window).on('load', function () { 665 initIframeContainerResize(document); 666 }); 667 668 var iframeContainerResizeTimer; 669 670 $(window).on('resize.iframeContainerResize', function () { 671 672 clearTimeout(iframeContainerResizeTimer); 673 674 iframeContainerResizeTimer = setTimeout(function () { 675 initIframeContainerResize(document); 676 }, 150); 677 678 }); 679 680 681 if (typeof window.MutationObserver === 'function') { 682 683 var iframeContainerObserver = new MutationObserver(function (mutations) { 684 685 for (var m = 0; m < mutations.length; m++) { 686 687 var added = mutations[m].addedNodes; 688 689 for (var n = 0; n < added.length; n++) { 690 691 var node = added[n]; 692 693 if (node.nodeType !== 1) { 694 continue; 695 } 696 697 initIframeContainerResize(node); 698 699 } 700 701 } 702 703 }); 704 705 iframeContainerObserver.observe(document.body, { 706 childList: true, 707 subtree: true 708 }); 709 710 } 711 712 713 /* ##################################################### 714 SECTION 8 715 STRUCTURED DATA (JSON-LD) FOR SEARCH ENGINES 716 Uses only page title and metadata already in the DOM: 717 h1, <title>, meta description, canonical URL, lang, 718 and breadcrumb links. No page-type guessing. 719 ##################################################### */ 720 721 (function initSubpageStructuredData() { 722 723 if (document.getElementById('zu-subpage-schema-webpage')) { 724 return; 725 } 726 727 function trimText(value) { 728 return $.trim(value || ''); 729 } 730 731 function toAbsoluteUrl(href) { 732 733 var url = trimText(href); 734 735 if (!url || url.charAt(0) === '#') { 736 return ''; 737 } 738 739 try { 740 return new URL(url, document.baseURI || window.location.href).href; 741 } catch (e) { 742 return ''; 743 } 744 745 } 746 747 function appendJsonLd(id, data) { 748 749 if (document.getElementById(id) || !data) { 750 return; 751 } 752 753 var script = document.createElement('script'); 754 755 script.type = 'application/ld+json'; 756 script.id = id; 757 script.text = JSON.stringify(data); 758 document.head.appendChild(script); 759 760 } 761 762 var pageName = trimText($('#readable h1').first().text()); 763 764 if (!pageName) { 765 pageName = trimText(document.title); 766 } 767 768 var pageDescription = trimText($('meta[name="description"]').attr('content')); 769 var pageUrl = toAbsoluteUrl($('link[rel="canonical"]').attr('href')); 770 771 if (!pageUrl) { 772 pageUrl = window.location.href.split('#')[0]; 773 } 774 775 var pageLanguage = trimText($('html').attr('lang')); 776 777 if (!pageLanguage && document.body && document.body.className) { 778 779 if (/\blang-ar\b/.test(document.body.className)) { 780 pageLanguage = 'ar'; 781 } else if (/\blang-en\b/.test(document.body.className)) { 782 pageLanguage = 'en'; 783 } 784 785 } 786 787 appendJsonLd('zu-subpage-schema-organization', { 788 '@context': 'https://schema.org', 789 '@type': 'Organization', 790 '@id': 'https://www.zu.ac.ae/#organization', 791 'name': 'Zayed University', 792 'url': 'https://www.zu.ac.ae/', 793 'logo': 'https://www.zu.ac.ae/main/files/images/v22/header-icons/zulogo-header.svg' 794 }); 795 796 var webPageSchema = { 797 '@context': 'https://schema.org', 798 '@type': 'WebPage', 799 'name': pageName, 800 'url': pageUrl, 801 'isPartOf': { 802 '@id': 'https://www.zu.ac.ae/#organization' 803 }, 804 'publisher': { 805 '@id': 'https://www.zu.ac.ae/#organization' 806 } 807 }; 808 809 if (pageDescription) { 810 webPageSchema.description = pageDescription; 811 } 812 813 if (pageLanguage) { 814 webPageSchema.inLanguage = pageLanguage; 815 } 816 817 appendJsonLd('zu-subpage-schema-webpage', webPageSchema); 818 819 var breadcrumbItems = []; 820 var position = 1; 821 822 $('.breadcrumbs-container a').each(function () { 823 824 var $link = $(this); 825 var name = trimText($link.attr('aria-label')) || trimText($link.text()); 826 var itemUrl = toAbsoluteUrl($link.attr('href')); 827 828 if (!name) { 829 return; 830 } 831 832 breadcrumbItems.push({ 833 '@type': 'ListItem', 834 'position': position, 835 'name': name, 836 'item': itemUrl || pageUrl 837 }); 838 839 position++; 840 841 }); 842 843 if (breadcrumbItems.length >= 2) { 844 845 appendJsonLd('zu-subpage-schema-breadcrumbs', { 846 '@context': 'https://schema.org', 847 '@type': 'BreadcrumbList', 848 'itemListElement': breadcrumbItems 849 }); 850 851 } 852 853 }()); 854 855 /* ===================================================== 856 SKIP LINK VISIBILITY 857 Keep the skip link visually hidden until it receives
858 keyboard focus. Restore sr-only when focus leaves or 859 when Escape is pressed. Focus-in removes it again, 860 including when reached with Shift+Tab. 861 ===================================================== */ 862 863 var skipLinkSelector = 'a.skip-link[href="#readable"]'; 864 865 $(skipLinkSelector).addClass('sr-only'); 866 867 $(document) 868 .on('keydown.skipLink', function (e) { 869 if (e.key === 'Tab' 870 && !e.shiftKey 871 && document.activeElement === document.body) { 872 var $skipLink = $(skipLinkSelector).first(); 873 if ($skipLink.length) { 874 e.preventDefault(); 875 $skipLink.trigger('focus'); 876 } 877 } 878 }) 879 .on('focusin.skipLink', skipLinkSelector, function () { 880 $(this).removeClass('sr-only'); 881 }) 882 .on('keydown.skipLink', skipLinkSelector, function (e) { 883 if (e.key === 'Escape') { 884 $(this).addClass('sr-only').blur(); 885 } 886 }); 887 888});
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.