PageSourceSearch

https://www.zu.ac.ae/main/files/js/accessibility_subpages.js

js zu.ac.ae collected 2026-09-24 08:50:41 UTC 29,409 bytes, 888 lines download raw bytes

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.