diff --git a/local/languages/de.ini b/local/languages/de.ini index abea7f9dc4bd7343ef26c63abe8ab4fd8970a3be..6428ea9568274ecf180264ac9f35778dece09cc1 100644 --- a/local/languages/de.ini +++ b/local/languages/de.ini @@ -2078,4 +2078,6 @@ missing_record_exception = "Der aufgerufene Titel (%%id%%) ist nicht vorhanden." Unknown Electronic = "Titel ist beim Resolver-Service nicht bekannt" ; #20826 -title_wrapper = "%%pageTitle%% %%titleSeparator%% %%siteTitle%%" \ No newline at end of file +title_wrapper = "%%pageTitle%% %%titleSeparator%% %%siteTitle%%" + +load_tab_content_hint = "Klicken Sie hier, um den Inhalt der Registerkarte zu laden." diff --git a/local/languages/en.ini b/local/languages/en.ini index ecafdfcc9672f43685fdb55e0c1084f4500d76d4..f1aa7b7cf478120dce8ec464464eec64899cca2b 100644 --- a/local/languages/en.ini +++ b/local/languages/en.ini @@ -1949,7 +1949,6 @@ resolver_link_access_unknown = "Record unknown to resolver" ; message to be shown upon empty resolver response no_resolver_links = "No online links available." - ; reset password reset_password_text = "Please complete the form below to reset your password. You will receive an email after we have completed resetting your password." Reset Password = "Reset Password" @@ -2166,4 +2165,6 @@ missing_record_exception = "Record %%id%% is unavailable." Unknown Electronic = "Record is unknown to the resolver service" ; #20826 -title_wrapper = "%%pageTitle%% %%titleSeparator%% %%siteTitle%%" \ No newline at end of file +title_wrapper = "%%pageTitle%% %%titleSeparator%% %%siteTitle%%" + +load_tab_content_hint = "Click to load tab content." diff --git a/themes/finc-accessibility/js/record.js b/themes/finc-accessibility/js/record.js new file mode 100644 index 0000000000000000000000000000000000000000..9766867212c7524e3ed39a0a6eac7c250fc27d76 --- /dev/null +++ b/themes/finc-accessibility/js/record.js @@ -0,0 +1,323 @@ +/*global deparam, getUrlRoot, grecaptcha, recaptchaOnLoad, resetCaptcha, syn_get_widget, userIsLoggedIn, VuFind, setupJumpMenus */ +/*exported ajaxTagUpdate, recordDocReady, refreshTagListCallback */ + +/** + * Functions and event handlers specific to record pages. + */ +function checkRequestIsValid(element, requestType) { + var recordId = element.href.match(/\/Record\/([^/]+)\//)[1]; + var vars = deparam(element.href); + vars.id = recordId; + + var url = VuFind.path + '/AJAX/JSON?' + $.param({ + method: 'checkRequestIsValid', + id: recordId, + requestType: requestType, + data: vars + }); + $.ajax({ + dataType: 'json', + cache: false, + url: url + }) + .done(function checkValidDone(response) { + if (response.data.status) { + $(element).removeClass('disabled') + .attr('title', response.data.msg) + .html('<i class="fa fa-flag" aria-hidden="true"></i> ' + response.data.msg); + } else { + $(element).remove(); + } + }) + .fail(function checkValidFail(/*response*/) { + $(element).remove(); + }); +} + +function setUpCheckRequest() { + $('.checkRequest').each(function checkRequest() { + checkRequestIsValid(this, 'Hold'); + }); + $('.checkStorageRetrievalRequest').each(function checkStorageRetrievalRequest() { + checkRequestIsValid(this, 'StorageRetrievalRequest'); + }); + $('.checkILLRequest').each(function checkILLRequest() { + checkRequestIsValid(this, 'ILLRequest'); + }); +} + +function deleteRecordComment(element, recordId, recordSource, commentId) { + var url = VuFind.path + '/AJAX/JSON?' + $.param({ method: 'deleteRecordComment', id: commentId }); + $.ajax({ + dataType: 'json', + url: url + }) + .done(function deleteCommentDone(/*response*/) { + $($(element).closest('.comment')[0]).remove(); + }); +} + +function refreshCommentList($target, recordId, recordSource) { + var url = VuFind.path + '/AJAX/JSON?' + $.param({ + method: 'getRecordCommentsAsHTML', + id: recordId, + source: recordSource + }); + $.ajax({ + dataType: 'json', + url: url + }) + .done(function refreshCommentListDone(response) { + // Update HTML + var $commentList = $target.find('.comment-list'); + $commentList.empty(); + $commentList.append(response.data.html); + $commentList.find('.delete').unbind('click').click(function commentRefreshDeleteClick() { + var commentId = $(this).attr('id').substr('recordComment'.length); + deleteRecordComment(this, recordId, recordSource, commentId); + return false; + }); + $target.find('.comment-form input[type="submit"]').button('reset'); + resetCaptcha($target); + }); +} + +function registerAjaxCommentRecord(_context) { + var context = typeof _context === "undefined" ? document : _context; + // Form submission + $(context).find('form.comment-form').unbind('submit').submit(function commentFormSubmit() { + var form = this; + var id = form.id.value; + var recordSource = form.source.value; + var url = VuFind.path + '/AJAX/JSON?' + $.param({ method: 'commentRecord' }); + var data = { + comment: form.comment.value, + id: id, + source: recordSource + }; + if (typeof grecaptcha !== 'undefined') { + var recaptcha = $(form).find('.g-recaptcha'); + if (recaptcha.length > 0) { + data['g-recaptcha-response'] = grecaptcha.getResponse(recaptcha.data('captchaId')); + } + } + $.ajax({ + type: 'POST', + url: url, + data: data, + dataType: 'json' + }) + .done(function addCommentDone(/*response, textStatus*/) { + var $form = $(form); + var $tab = $form.closest('.list-tab-content'); + if (!$tab.length) { + $tab = $form.closest('.tab-pane'); + } + refreshCommentList($tab, id, recordSource); + $form.find('textarea[name="comment"]').val(''); + $form.find('input[type="submit"]').button('loading'); + resetCaptcha($form); + }) + .fail(function addCommentFail(response, textStatus) { + if (textStatus === 'abort' || typeof response.responseJSON === 'undefined') { return; } + VuFind.lightbox.alert(response.responseJSON.data, 'danger'); + }); + return false; + }); + // Delete links + $('.delete').click(function commentDeleteClick() { + var commentId = this.id.substr('recordComment'.length); + deleteRecordComment(this, $('.hiddenId').val(), $('.hiddenSource').val(), commentId); + return false; + }); + // Prevent form submit + return false; +} + +function registerTabEvents() { + // Logged in AJAX + registerAjaxCommentRecord(); + // Render recaptcha + recaptchaOnLoad(); + + setUpCheckRequest(); + + VuFind.lightbox.bind('.tab-pane.active'); +} + +function removeHashFromLocation() { + if (window.history.replaceState) { + var href = window.location.href.split('#'); + window.history.replaceState({}, document.title, href[0]); + } else { + window.location.hash = '#'; + } +} + +function ajaxLoadTab($newTab, tabid, setHash) { + // Request the tab via AJAX: + $.ajax({ + url: VuFind.path + getUrlRoot(document.URL) + '/AjaxTab', + type: 'POST', + data: {tab: tabid} + }) + .always(function ajaxLoadTabDone(data) { + if (typeof data === 'object') { + $newTab.html(data.responseText ? data.responseText : VuFind.translate('error_occurred')); + } else { + $newTab.html(data); + } + registerTabEvents(); + if (typeof syn_get_widget === "function") { + syn_get_widget(); + } + if (typeof setHash == 'undefined' || setHash) { + window.location.hash = tabid; + } else { + removeHashFromLocation(); + } + setupJumpMenus($newTab); + }); + return false; +} + +function refreshTagList(_target, _loggedin) { + var loggedin = !!_loggedin || userIsLoggedIn; + var target = _target || document; + var recordId = $(target).find('.hiddenId').val(); + var recordSource = $(target).find('.hiddenSource').val(); + var $tagList = $(target).find('.tagList'); + if ($tagList.length > 0) { + var url = VuFind.path + '/AJAX/JSON?' + $.param({ + method: 'getRecordTags', + id: recordId, + source: recordSource + }); + $.ajax({ + dataType: 'json', + url: url + }) + .done(function getRecordTagsDone(response) { + $tagList.empty(); + $tagList.replaceWith(response.data.html); + if (loggedin) { + $tagList.addClass('loggedin'); + } else { + $tagList.removeClass('loggedin'); + } + }); + } +} +function refreshTagListCallback() { + refreshTagList(false, true); +} + +function ajaxTagUpdate(_link, tag, _remove) { + var link = _link || document; + var remove = _remove || false; + var $target = $(link).closest('.record'); + var recordId = $target.find('.hiddenId').val(); + var recordSource = $target.find('.hiddenSource').val(); + $.ajax({ + url: VuFind.path + '/AJAX/JSON?method=tagRecord', + method: 'POST', + data: { + tag: '"' + tag.replace(/\+/g, ' ') + '"', + id: recordId, + source: recordSource, + remove: remove + } + }) + .always(function tagRecordAlways() { + refreshTagList($target, false); + }); +} + +function getNewRecordTab(tabid) { + return $('<div class="tab-pane ' + tabid + '-tab" id="' + tabid + '" role="tabpanel" tabindex="-1" aria-labelledby="' + tabid + '-tabselector"><i class="fa fa-spinner fa-spin" aria-hidden="true"></i> ' + VuFind.translate('loading') + '...</div>'); +} + +function backgroundLoadTab(tabid) { + if ($('.' + tabid + '-tab').length > 0) { + return; + } + var newTab = getNewRecordTab(tabid); + $('.nav-tabs a.' + tabid).closest('.result,.record').find('.tab-content').append(newTab); + return ajaxLoadTab(newTab, tabid, false); +} + +function applyRecordTabHash() { + var activeTab = $('.record-tabs li.active').attr('data-tab'); + var $initiallyActiveTab = $('.record-tabs li.initiallyActive a'); + var newTab = typeof window.location.hash !== 'undefined' + ? window.location.hash.toLowerCase() : ''; + + // Open tab in url hash + if (newTab.length <= 1 || newTab === '#tabnav') { + $initiallyActiveTab.click(); + } else if (newTab.length > 1 && '#' + activeTab !== newTab) { + $('.' + newTab.substr(1) + ' a').click(); + } +} + +$(window).on('hashchange', applyRecordTabHash); + +function recordDocReady() { + $('.record-tabs .nav-tabs a').click(function recordTabsClick() { + var $li = $(this).parent(); + // If it's an active tab, click again to follow to a shareable link. + if ($li.hasClass('active')) { + return true; + } + var tabid = $li.attr('data-tab'); + var $top = $(this).closest('.record-tabs'); + + // accessibility: mark tab controls as selected + $top.find('.record-tab.active').find('a').attr('aria-selected', 'false'); + $('#' + tabid + '-tabselector').attr('aria-selected', 'true').attr('aria-controls', tabid); + + // accessibility: set aria-hidden for content panes + $top.find('.tab-pane.active').removeClass('active').attr('aria-hidden', 'true'); + $top.find('.' + tabid + '-tab').addClass('active').attr('aria-hidden', 'false'); + + // if we're flagged to skip AJAX for this tab, we need special behavior: + if ($li.hasClass('noajax')) { + // if this was the initially active tab, we have moved away from it and + // now need to return -- just switch it back on. + if ($li.hasClass('initiallyActive')) { + $(this).tab('show'); + window.location.hash = 'tabnav'; + return false; + } + // otherwise, we need to let the browser follow the link: + return true; + } + $(this).tab('show'); + if ($top.find('.' + tabid + '-tab').length > 0) { + $top.find('.' + tabid + '-tab').addClass('active'); + if ($top.find('#' + tabid ).length) { + $top.find('#' + tabid ).parent().focus(); + } + if ($(this).parent().hasClass('initiallyActive')) { + removeHashFromLocation(); + } else { + window.location.hash = tabid; + } + return false; + } else { + var newTab = getNewRecordTab(tabid).addClass('active'); + $top.find('.tab-content').append(newTab); + if ($top.find('#' + tabid ).length) { + $top.find('#' + tabid ).parent().focus(); + } + return ajaxLoadTab(newTab, tabid, !$(this).parent().hasClass('initiallyActive')); + } + }); + + $('[data-background]').each(function setupBackgroundTabs(index, el) { + backgroundLoadTab(el.className); + }); + + registerTabEvents(); + applyRecordTabHash(); +} diff --git a/themes/finc-accessibility/js/vendor/bootstrap-accessibility-en.min.js b/themes/finc-accessibility/js/vendor/bootstrap-accessibility-en.min.js index 630f112aed7e6a0149687a95215c623908bf0404..fb3ad97f9a4462ee6e30273db5a316df5ee84feb 100644 --- a/themes/finc-accessibility/js/vendor/bootstrap-accessibility-en.min.js +++ b/themes/finc-accessibility/js/vendor/bootstrap-accessibility-en.min.js @@ -3,7 +3,6 @@ * Copyright (c) 2020 PayPal Accessibility Team; Licensed BSD */ !function ($) { "use strict"; - console.log('en'); var uniqueId = function (prefix) { return (prefix || "ui-id") + "-" + Math.floor(1e3 * Math.random() + 1) }, focusable = function (element, isTabIndexNotNaN) { diff --git a/themes/finc-accessibility/scss/compiled.scss b/themes/finc-accessibility/scss/compiled.scss index 766002b57789f053a07aea5a5099e79b2f515732..db481aca78be66dffc9e30ba18c9a77e01996dbd 100644 --- a/themes/finc-accessibility/scss/compiled.scss +++ b/themes/finc-accessibility/scss/compiled.scss @@ -54,4 +54,10 @@ a.remove-filter { display: flex; width: 100%; +} + +.record-tab.active{ + .load-tab-content { + display: none; + } } \ No newline at end of file diff --git a/themes/finc/templates/record/view.phtml b/themes/finc/templates/record/view.phtml index 1f97d387fad4ff410d80c391850a4220a5bfe5a1..3749c2121c6fc81e1f807f1b38c5e40014cb3a48 100644 --- a/themes/finc/templates/record/view.phtml +++ b/themes/finc/templates/record/view.phtml @@ -40,15 +40,17 @@ <?= $this->record($this->driver)->getCoreMetadata() ?> <?php if (count($this->tabs) > 0): ?> - <a name="tabnav"></a> + <?php /* swap deprecated 'name' for 'ID' - CK */ ?> + <a id="tabnav"></a> <div class="record-tabs"> + <?php /* DO NOT add 'role=tablist' for accessibility, see #19938 - CK */ ?> <ul class="nav nav-tabs"> <?php foreach ($this->tabs as $tab => $obj): ?> <?php // add current tab to breadcrumbs if applicable: $desc = $obj->getDescription(); $tabName = preg_replace("/\W/", "-", strtolower($tab)); $tabClasses = ['record-tab', $tabName]; - if (0 === strcasecmp($this->activeTab, $tab)) { + if (($isActiveTab = 0 === strcasecmp($this->activeTab, $tab))) { if (!$this->loadInitialTabWithAjax || !$obj->supportsAjax()) { $tabClasses[] = 'active'; } @@ -63,16 +65,27 @@ $tabClasses[] = 'noajax'; } ?> + <?php /* DO NOT add role="tab" BUT DO ADD aria-controls and ID for accessibility -- + 'aria-selected' (true/false) needs to be set via record.js - CK */ ?> <li class="<?= implode(' ', $tabClasses) ?>" data-tab="<?= $tabName ?>"> - <a - href="<?= $this->recordLink()->getTabUrl($this->driver, $tab) ?>#tabnav"<?php if ($obj->supportsAjax() && in_array($tab, $this->backgroundTabs)): ?> data-background<?php endif ?>><?= $this->transEsc($desc) ?></a> + <a href="<?= $this->recordLink()->getTabUrl($this->driver, $tab) ?>#tabnav" + <?php if ($obj->supportsAjax() && in_array($tab, $this->backgroundTabs)): ?> data-background<?php endif ?> + aria-selected="<?= $isActiveTab ? "true" : "false" ?>" + aria-controls="<?= $tabName ?>" + id="<?= $tabName ?>-tabselector"> + <?= $this->transEsc($desc) ?> + <span class="sr-only load-tab-content"><?= $this->transEsc('load_tab_content_hint') ?></span></a> </li> <?php endforeach; ?> </ul> - <div class="tab-content"> + <div class="tab-content" aria-live="polite" tabindex="-1"> <?php if (!$this->loadInitialTabWithAjax || !isset($activeTabObj) || !$activeTabObj->supportsAjax()): ?> - <div class="tab-pane active <?= $this->escapeHtmlAttr($this->activeTab) ?>-tab"> + <?php /* Add ID, role and aria-labelledby for accessibility - CK */ ?> + <div class="tab-pane active <?= $this->escapeHtmlAttr($this->activeTab) ?>-tab" + role="tabpanel" + id="<?= $this->escapeHtmlAttr($this->activeTab) ?>" + aria-labelledby="<?= $this->activeTab ?>-tabselector"> <?= isset($activeTabObj) ? $this->record($this->driver)->getTab($activeTabObj) : '' ?> </div> <?php endif; ?>