diff --git a/themes/fid_adlr/js/check_save_statuses_adlr.js b/themes/fid_adlr/js/check_save_statuses_adlr.js index 8a638c5a8bab9967cdd75a685a40e6f3fea5ca21..40a75100135811be01fb397cb6b62368112e829c 100644 --- a/themes/fid_adlr/js/check_save_statuses_adlr.js +++ b/themes/fid_adlr/js/check_save_statuses_adlr.js @@ -1,3 +1,14 @@ +/** + * origin: bootstrap3 - check_save_statuses.js + * + * usage: + * - highlight favorite (saved) records as filled star + * + * modified for adlr: + * - #16706: overrides only one method from check_save_statuses.js + * --> add this js-dependency to search/results.phtml too + * - #21409: adjustments for subrecords on result groups + */ function displaySaveStatus(itemLists, $item) { if (itemLists.length > 0) { // If we got lists back, display them! diff --git a/themes/fid_adlr/js/facets.js b/themes/fid_adlr/js/facets.js index 7898cf42331b125c6c5a85f43ef9f8e372c1b603..02354e50aee0a01a0facc6a9619e69d11d0c56ce 100644 --- a/themes/fid_adlr/js/facets.js +++ b/themes/fid_adlr/js/facets.js @@ -1,5 +1,13 @@ +/** + * origin: bootstrap3 + * + * usage: + * - facet post load, start new filtered searches + * + * modified for adlr: + * - #16514: set spinner instead of loading "..." + */ /*global VuFind */ -// refs 16514: copied from bootstrap3, set spinner instead of loading ..., see below - GG /*exported collapseTopFacets, initFacetTree */ function buildFacetNodes(data, currentPath, allowExclude, excludeTitle, counts) { @@ -27,8 +35,11 @@ function buildFacetNodes(data, currentPath, allowExclude, excludeTitle, counts) $i.appendTo($item); $item.append(' '); } + var $description = $('<span/>') + .addClass('facet-value') + .append(this.displayText); + $item.append($description); - $item.append(this.displayText); $item.appendTo($html); if (!this.isApplied && counts) { @@ -104,25 +115,23 @@ function initFacetTree(treeNode, inSidebar) } treeNode.data('loaded', true); - var source = treeNode.data('source'); - var facet = treeNode.data('facet'); - var operator = treeNode.data('operator'); - var sort = treeNode.data('sort'); - var query = window.location.href.split('?')[1]; - if (inSidebar) { treeNode.prepend('<li class="list-group-item"><i class="fa fa-spinner fa-spin" aria-hidden="true"></i></li>'); } else { treeNode.prepend('<div><i class="fa fa-spinner fa-spin" aria-hidden="true"></i><div>'); } - $.getJSON(VuFind.path + '/AJAX/JSON?' + query, - { - method: "getFacetData", - source: source, - facetName: facet, - facetSort: sort, - facetOperator: operator - }, + var request = { + method: "getFacetData", + source: treeNode.data('source'), + facetName: treeNode.data('facet'), + facetSort: treeNode.data('sort'), + facetOperator: treeNode.data('operator'), + query: treeNode.data('query'), + querySuppressed: treeNode.data('querySuppressed'), + extraFields: treeNode.data('extraFields') + }; + $.getJSON(VuFind.path + '/AJAX/JSON?' + request.query, + request, function getFacetData(response/*, textStatus*/) { buildFacetTree(treeNode, response.data.facets, inSidebar); } @@ -150,7 +159,7 @@ VuFind.register('sideFacets', function SideFacets() { // refs 16514: set spinner instead of loading ... - GG var overlay = '<div class="facet-loading-overlay">' + '<span class="facet-loading-overlay-label"><i class="fa fa-spinner fa-spin fa-3x"></i>' - + '</span></div>'; + + "</span></div>"; // refs 16514: set spinner instead of loading ... END - GG $(this).closest(".collapse").append(overlay); // This callback operates both as a click handler and a JSTree callback; @@ -178,18 +187,18 @@ VuFind.register('sideFacets', function SideFacets() { if (facetList.length === 0) { return; } - var urlParts = window.location.href.split('?'); - var query = urlParts.length > 1 ? urlParts[1] : ''; var request = { method: 'getSideFacets', searchClassId: $container.data('searchClassId'), location: $container.data('location'), configIndex: $container.data('configIndex'), - query: query, + query: $container.data('query'), + querySuppressed: $container.data('querySuppressed'), + extraFields: $container.data('extraFields'), enabledFacets: facetList }; $container.find('.facet-load-indicator').removeClass('hidden'); - $.getJSON(VuFind.path + '/AJAX/JSON?' + query, request) + $.getJSON(VuFind.path + '/AJAX/JSON?' + request.query, request) .done(function onGetSideFacetsDone(response) { $.each(response.data.facets, function initFacet(facet, facetData) { var containerSelector = typeof facetData.checkboxCount !== 'undefined' @@ -251,7 +260,7 @@ VuFind.register('sideFacets', function SideFacets() { $.support.transition = false; if ((' ' + storedItem + ' ').indexOf(' in ') > -1) { $(item).collapse('show'); - } else { + } else if (!$(item).data('forceIn')) { $(item).collapse('hide'); } } finally { @@ -269,6 +278,16 @@ VuFind.register('sideFacets', function SideFacets() { loadAjaxSideFacets(); }); loadAjaxSideFacets(); + + // Keep filter dropdowns on screen + $(".search-filter-dropdown").on("shown.bs.dropdown", function checkFilterDropdownWidth(e) { + var $dropdown = $(e.target).find(".dropdown-menu"); + if ($(e.target).position().left + $dropdown.width() >= window.innerWidth) { + $dropdown.addClass("dropdown-menu-right"); + } else { + $dropdown.removeClass("dropdown-menu-right"); + } + }); } return { init: init, showLoadingOverlay: showLoadingOverlay }; diff --git a/themes/fid_adlr/js/lightbox.js b/themes/fid_adlr/js/lightbox.js index baffb1c183982fe7db303643f8f7424f005d9c99..8478fca4b46968da2876ebb2e8e197d56a66ecfa 100644 --- a/themes/fid_adlr/js/lightbox.js +++ b/themes/fid_adlr/js/lightbox.js @@ -1,13 +1,24 @@ -// refs 16504: copied from bootstrap3 to set an overlay, see below - GG +/** + * origin: finc + * + * usage: + * - lightbox + * + * modified for adlr: + * - #16584: set design for closing button of lightbox + * - #16833 (#20154): remove save search icon by disabled one if save request succeeded + * - 16504: overlay spinner login + */ /*global grecaptcha, recaptchaOnLoad, resetCaptcha, VuFind */ VuFind.register('lightbox', function Lightbox() { // State var _originalUrl = false; var _currentUrl = false; - var _lightboxTitle = ''; + var _lightboxTitle = false; var refreshOnClose = false; var _modalParams = {}; // Elements + // finc: keep _origin - see #17984 var _modal, _modalBody, _clickedButton, _origin = null; // Utilities function _storeClickedStatus() { @@ -16,14 +27,11 @@ VuFind.register('lightbox', function Lightbox() { function _html(content) { _modalBody.html(content); // Set or update title if we have one - if (_lightboxTitle !== '') { - var h2 = _modalBody.find('h2:first-child'); - if (h2.length === 0) { - h2 = $('<h2/>').prependTo(_modalBody); + var $h2 = _modalBody.find("h2:first-of-type"); + if (_lightboxTitle && $h2) { + $h2.text(_lightboxTitle); } - h2.text(_lightboxTitle); - _lightboxTitle = ''; - } + _lightboxTitle = false; _modal.modal('handleUpdate'); } function _emit(msg, _details) { @@ -44,17 +52,20 @@ VuFind.register('lightbox', function Lightbox() { } // Public: Present an alert + // finc: keep role='alert' - see #18988 and #18790 function showAlert(message, _type) { var type = _type || 'info'; - _html('<div class="flash-message alert alert-' + type + '">' + message + '</div>' + // fid_adlr: #16584 set design for closing button of lightbox + _html('<div class="flash-message alert alert-' + type + '" role="alert">' + message + '</div>' + '<button class="btn btn-primary" data-dismiss="modal">' + VuFind.translate('close') + '</button>'); _modal.modal('show'); } + // finc: keep role='alert' - see #18988 and #18790 function flashMessage(message, _type) { var type = _type || 'info'; _modalBody.find('.flash-message,.fa.fa-spinner').remove(); _modalBody.find('h2:first-of-type') - .after('<div class="flash-message alert alert-' + type + '">' + message + '</div>'); + .after('<div class="flash-message alert alert-' + type + '" role="alert">' + message + '</div>'); } function close() { _modal.modal('hide'); @@ -116,6 +127,16 @@ VuFind.register('lightbox', function Lightbox() { $('#modal').find('.checkbox-select-item').change(function lbSelectAllDisable() { $(this).closest('.modal-body').find('.checkbox-select-all').prop('checked', false); }); + + // #19695 accessibility: set id for aria-label + if (_modalBody.find('h1').length) { + _modalBody.find('h1:first').attr('id', 'modal-title'); + $('#modal').attr('aria-labelledby', 'modal-title'); + } else if (_modalBody.find('h2').length) { + _modalBody.find('h2:first').attr('id', 'modal-title'); + $('#modal').attr('aria-labelledby', 'modal-title'); + } + // Recaptcha recaptchaOnLoad(); } @@ -145,9 +166,15 @@ VuFind.register('lightbox', function Lightbox() { _xhr.always(function lbAjaxAlways() { _xhr = false; }) .done(function lbAjaxDone(content, status, jq_xhr) { var errorMsgs = []; - if (jq_xhr.status !== 205) { + var flashMessages = []; + if (jq_xhr.status === 204) { + // No content, close lightbox + close(); + return; + } else if (jq_xhr.status !== 205) { var testDiv = $('<div/>').html(content); errorMsgs = testDiv.find('.flash-message.alert-danger:not([data-lightbox-ignore])'); + flashMessages = testDiv.find('.flash-message:not([data-lightbox-ignore])'); // Place Hold error isolation if (obj.url.match(/\/Record\/.*(Hold|Request)\?/)) { if (errorMsgs.length && testDiv.find('.record').length) { @@ -168,7 +195,7 @@ VuFind.register('lightbox', function Lightbox() { obj.method && ( obj.url.match(/catalogLogin/) || obj.url.match(/MyResearch\/(?!Bulk|Delete|Recover)/) - ) && errorMsgs.length === 0 + ) && flashMessages.length === 0 ) { var eventResult = _emit('VuFind.lightbox.login', { @@ -180,7 +207,8 @@ VuFind.register('lightbox', function Lightbox() { VuFind.refreshPage(); } return false; - } else { + /* FINC-specific: avoid page reload after login if param is given - show account links by javascript */ + } else if (new URL(_originalUrl).searchParams.get('refreshOnClose') !== 'false' || !displayLogin()) { VuFind.lightbox.refreshOnClose = true; } _currentUrl = _originalUrl; // Now that we're logged in, where were we? @@ -190,7 +218,7 @@ VuFind.register('lightbox', function Lightbox() { return; } - /* remove save search icon by disabled one if save request succeeded */ + /* adlr_link: #16833 (#20154), remove save search icon by disabled one if save request succeeded */ if (_originalUrl !== undefined && _originalUrl.match(/SaveSearch\?save=/)) { $("#save-search").toggleClass("hidden"); $("#saved-search").toggleClass("hidden"); @@ -207,9 +235,20 @@ VuFind.register('lightbox', function Lightbox() { ajax({ url: _currentUrl || _originalUrl }); } - // finc: restore focus after deleting records, #20379 #21905 - function setOrigin(origin) { - _origin = origin; + /** + * Finc specific: Login without Page Reload + */ + function displayLogin() { + try { + if ($('#loginOptions').length && $('.logoutOptions').length) { + userIsLoggedIn = true; + $('#loginOptions').hide(); + $('.logoutOptions').removeClass('hidden'); + return true; + } + } catch (e) { /* Empty */ } + + return false; } /** @@ -246,25 +285,31 @@ VuFind.register('lightbox', function Lightbox() { * data-lightbox-title = Lightbox title (overrides any title the page provides) */ _constrainLink = function constrainLink(event) { - if (typeof $(this).data('lightboxIgnore') != 'undefined' - || typeof this.attributes.href === 'undefined' - || this.attributes.href.value.charAt(0) === '#' - || this.href.match(/^[a-zA-Z]+:[^/]/) // ignore resource identifiers (mailto:, tel:, etc.) + var $link = $(this); + if (typeof $link.data("lightboxIgnore") != "undefined" + || typeof $link.attr("href") === "undefined" + || $link.attr("href").charAt(0) === "#" + || $link.attr("href").match(/^[a-zA-Z]+:[^/]/) // ignore resource identifiers (mailto:, tel:, etc.) + || (typeof $link.attr("target") !== "undefined" + && ( + $link.attr("target").toLowerCase() === "_new" + || $link.attr("target").toLowerCase() === "new" + )) ) { return true; } if (this.href.length > 1) { event.preventDefault(); - var obj = {url: $(this).data('lightboxHref') || this.href}; - if ("string" === typeof $(this).data('lightboxPost')) { + var obj = {url: $(this).data('lightbox-href') || this.href}; + if ("string" === typeof $(this).data('lightbox-post')) { obj.type = 'POST'; - obj.data = $(this).data('lightboxPost'); + obj.data = $(this).data('lightbox-post'); } - _lightboxTitle = $(this).data('lightboxTitle') || ''; + _lightboxTitle = $(this).data('lightbox-title') || false; _modalParams = $(this).data(); VuFind.modal('show'); // Copied finc #17984 - _origin = $(this); + setOrigin($(this)); ajax(obj); _currentUrl = this.href; return false; @@ -288,13 +333,6 @@ VuFind.register('lightbox', function Lightbox() { // Gather data var form = event.target; var data = $(form).serializeArray(); - // Check for recaptcha - if (typeof grecaptcha !== 'undefined') { - var recaptcha = $(form).find('.g-recaptcha'); - if (recaptcha.length > 0) { - data.push({ name: 'g-recaptcha-response', value: grecaptcha.getResponse(recaptcha.data('captchaId')) }); - } - } // Force layout data.push({ name: 'layout', value: 'lightbox' }); // Return in lightbox, please // Add submit button information @@ -342,7 +380,7 @@ VuFind.register('lightbox', function Lightbox() { submit.attr('disabled', 'disabled'); } // Store custom title - _lightboxTitle = submit.data('lightboxTitle') || $(form).data('lightboxTitle') || ''; + _lightboxTitle = submit.data('lightbox-title') || $(form).data('lightbox-title') || false; // Get Lightbox content ajax({ url: $(form).attr('action') || _currentUrl, @@ -356,6 +394,99 @@ VuFind.register('lightbox', function Lightbox() { return false; }; + /** + * Keyboard and focus controllers + * Adapted from Micromodal + * - https://github.com/ghosh/Micromodal/blob/master/lib/src/index.js + */ + var FOCUSABLE_ELEMENTS = ['a[href]', 'area[href]', 'input:not([disabled]):not([type="hidden"]):not([aria-hidden])', 'select:not([disabled]):not([aria-hidden])', 'textarea:not([disabled]):not([aria-hidden])', 'button:not([disabled]):not([aria-hidden])', 'iframe', 'object', 'embed', '[contenteditable]', '[tabindex]:not([tabindex^="-"])']; + function getFocusableNodes () { + var nodes = _modal[0].querySelectorAll(FOCUSABLE_ELEMENTS); + return [].slice.apply(nodes); + } + /** + * Tries to set focus on a node which is not a close trigger + * if no other nodes exist then focuses on first close trigger + */ + function setFocusToFirstNode() { + var focusableNodes = getFocusableNodes(); + + // no focusable nodes + if (focusableNodes.length === 0) return; + + // remove nodes on whose click, the modal closes + var nodesWhichAreNotCloseTargets = focusableNodes.filter(function nodeFilter(node) { + return !node.hasAttribute("data-lightbox-close") && ( + !node.hasAttribute("data-dismiss") || + node.getAttribute("data-dismiss") !== "modal" + ); + }); + + if (nodesWhichAreNotCloseTargets.length > 0) { + nodesWhichAreNotCloseTargets[0].focus(); + } + if (nodesWhichAreNotCloseTargets.length === 0) { + focusableNodes[0].focus(); + } + } + + function retainFocus(event) { + var focusableNodes = getFocusableNodes(); + + // no focusable nodes + if (focusableNodes.length === 0) return; + + /** + * Filters nodes which are hidden to prevent + * focus leak outside modal + */ + focusableNodes = focusableNodes.filter(function nodeHiddenFilter(node) { + return (node.offsetParent !== null); + }); + + // if disableFocus is true + if (!_modal[0].contains(document.activeElement)) { + focusableNodes[0].focus(); + } else { + var focusedItemIndex = focusableNodes.indexOf(document.activeElement); + + if (event.shiftKey && focusedItemIndex === 0) { + focusableNodes[focusableNodes.length - 1].focus(); + event.preventDefault(); + } + + if ( + !event.shiftKey && + focusableNodes.length > 0 && + focusedItemIndex === focusableNodes.length - 1 + ) { + focusableNodes[0].focus(); + event.preventDefault(); + } + } + } + + // finc: restore focus after deleting records, #20379 + function setOrigin(origin) { + _origin = origin; + } + + function onKeydown(event) { + if (event.keyCode === 27) { // esc + close(); + } + if (event.keyCode === 9) { // tab + retainFocus(event); + } + } + function bindFocus() { + document.addEventListener('keydown', onKeydown); + setFocusToFirstNode(); + } + function unbindFocus() { + document.removeEventListener('keydown', onKeydown); + } + // Public: Attach listeners to the page function bind(el) { var target = el || document; @@ -402,7 +533,7 @@ VuFind.register('lightbox', function Lightbox() { _html(VuFind.translate('loading') + '...'); _originalUrl = false; _currentUrl = false; - _lightboxTitle = ''; + _lightboxTitle = false; _modalParams = {}; } function init() { @@ -411,23 +542,30 @@ VuFind.register('lightbox', function Lightbox() { _modal.on('hide.bs.modal', function lightboxHide() { if (VuFind.lightbox.refreshOnClose) { VuFind.refreshPage(); - } + } else { + unbindFocus(); this.setAttribute('aria-hidden', true); _emit('VuFind.lightbox.closing'); + } }); _modal.on('hidden.bs.modal', function lightboxHidden() { VuFind.lightbox.reset(); _emit('VuFind.lightbox.closed'); - // copied from finc #17984 - // set focus back on launching element + // finc: set focus back on launching element if (_origin !== null && _origin !== 'undefined') { _origin.focus(); } }); + _modal.on("shown.bs.modal", function lightboxShown() { + bindFocus(); + }); VuFind.modal = function modalShortcut(cmd) { if (cmd === 'show') { _modal.modal($.extend({ show: true }, _modalParams)).attr('aria-hidden', false); + + // Set keyboard focus + setFocusToFirstNode(); } else { _modal.modal(cmd); } @@ -450,8 +588,9 @@ VuFind.register('lightbox', function Lightbox() { render: render, // Reset reset: reset, + // finc: restore focus after deleting records, #20379 + setOrigin: setOrigin, // Init - init: init, - setOrigin: setOrigin + init: init }; }); diff --git a/themes/fid_adlr/js/resultGrouping.js b/themes/fid_adlr/js/resultGrouping.js index 86ec4002d63cdc85d5370cefe2c2ed55f7d10ebe..805bcdd17f2e0deaf37f61464e22d8ebc57c4313 100644 --- a/themes/fid_adlr/js/resultGrouping.js +++ b/themes/fid_adlr/js/resultGrouping.js @@ -1,3 +1,14 @@ +/** + * origin: vufind-results-grouping - resultGrouping.js + * + * usage: + * - en/disabling result grouping via js (see sidebar) + * + * modified for adlr: + * - #21409: + * -- showing spinning wheel while reloading + * -- prevent browser prompt on reload page via JS + */ $(document).ready(function groupItems() { $('.results-group-toggle').click(function(e){ $(this).parent().toggleClass('active'); diff --git a/themes/fid_adlr/js/user_delivery_address.js b/themes/fid_adlr/js/user_delivery_address.js index 3bdcf428e2c93a7abca235f28cfe6ee7bfe57eac..309871cea9ace975921bb7ffc124bc26dfbd2b0c 100644 --- a/themes/fid_adlr/js/user_delivery_address.js +++ b/themes/fid_adlr/js/user_delivery_address.js @@ -1,3 +1,12 @@ +/** + * origin: - + * + * usage: + * - add (on show) / remove (on hide) required attributes on private address inputs + * + * modified for adlr: + * - implemented with #20900 + */ function showHidePrivateAddress() { var requiredInputs = ["line1", "zip", "city", "country"];