diff --git a/local/config/vufind/FincILS.ini b/local/config/vufind/FincILS.ini index f288a15ef6a89018c6e24217a42ae9db35309f06..d255b92b531bdf33863c64144edbdbd21e2ce8a3 100644 --- a/local/config/vufind/FincILS.ini +++ b/local/config/vufind/FincILS.ini @@ -95,4 +95,11 @@ queryIls[] = 'getFacetAvail:Local' ; through user input. E.g. to select an email profile according to the selected ; pickUpLocation you have to set the value to "pickUpLocation". ; By default it is left empty using the default profile "EmailHold". -;emailProfileSelector = +;emailProfileSelector = + +; the ILLRequests section contains regex patterns for inter library loan item detection +; itemPattern matches the item ID, labelPattern matches the item's label +; as used in a PAIA regex filter +;[ILLRequests] +;itemPattern = "/^(?:(?!DE-15)).*$/" +;labelPattern = "/^ILL Status String$" \ No newline at end of file diff --git a/local/config/vufind/FincLibero.ini b/local/config/vufind/FincLibero.ini new file mode 100644 index 0000000000000000000000000000000000000000..26fb4a666a515219f64e3131dccad0fea6de5e75 --- /dev/null +++ b/local/config/vufind/FincLibero.ini @@ -0,0 +1,52 @@ +; General Settings for all FincLibero instances +[Parent_Config] +;inherits FincILS settings from same directory +relative_path=FincILS.ini + +; entries in [RequiredConfig] denote settings that are tested during initialization +; of FincLibero. Syntax is +; <INI Section>[<key within section>] = true for required, false for not required +; e.g. General[departmentLocationBase] = true makes FincLibero::init() +; throw an InitException in case [General]->departmentLocationBase is not set +; in any of FincLibero.ini or FincILS.ini +;[RequiredConfig] +;General[departmentLocationBase] = true +;General[titleHoldLimitations] = true +;General[pickUpLocationPatterns] = true +;General[requestableLimitations] = true +;General[holdableLimitations] = true +;General[recallableLimitations] = true +;General[awlLimitations] = true +;General[stackURIs] = true +;General[readingRoomURIs] = true + +; the following section is here to list info and examples for the settings +; that may be defined as required in [RequiredConfig] +; examples collected in DE-15 and DE-Zwi2 +;[General] +; Pattern that will extend uris for department +;departmentLocationBase = "http://data.ub.uni-leipzig.de/resource/DE-Zwi2/department/zw" + +; Limitations that will trigger an action. +;titleHoldLimitations[] = "DE-Zwi2:OrderTitle:[0-9]+X?" + +; Patterns that will identify limitations as pickUpLocations +;pickUpLocationPatterns[] = "/^http:\/\/data\.ub\.uni-leipzig\.de\/resource\//" + +; Limitations that will trigger the placeStorageRetrievalRequest action +;requestableLimitations[] = DE-15:OrderRequired + +; Limitations that will trigger the placeHold action +;holdableLimitations[] = DE-15:URI + +; Limitations that will trigger a recall action (currently not used). +;recallableLimitations[] = DE-15:ReservationPossible + +; Limitations that will identify the item for being bound to another item. +;awlLimitations[] = DE-15:OrderViaBoundItem + +; URIs that will be used for stack views +;stackURIs[] = "http://data.ub.uni-leipzig.de/resource/DE-15/pickup/zw01thek" + +; URIs that will be used for reading room views +;readingRoomURIs[] = "http://data.ub.uni-leipzig.de/resource/DE-15/pickup/zw01thekfo" diff --git a/local/languages/de.ini b/local/languages/de.ini index fc2852d71200a869d5c844346094cca46eb94636..6428ea9568274ecf180264ac9f35778dece09cc1 100644 --- a/local/languages/de.ini +++ b/local/languages/de.ini @@ -2054,7 +2054,6 @@ DE-Kn38 = "Hochschule für Musik und Tanz Köln" Range-from-to = "Bereich von/bis" ; #17833 -form-legend = "Bitte füllen Sie alle Felder aus" form-button-submit = "Ausgefülltes Formular abschicken" ; #17601 @@ -2079,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 48a6d2dbab0cae35c423264ffc0f5f85d83fe4fb..f1aa7b7cf478120dce8ec464464eec64899cca2b 100644 --- a/local/languages/en.ini +++ b/local/languages/en.ini @@ -975,7 +975,7 @@ DE-15-292 = "Leipzig University Library, Central Library Medicine" DE-105 = "Technische Universität Bergakademie Freiberg" DE-197 = "City Library Leipzig" DE-520 = "Dresden University of Applied Sciences" -DE-540 = "Dresden Academy of Fine Arts" +DE-540 = "Dresden University of Fine Arts" DE-1156 = "Folkwang University of Arts" DE-1972 = "Robert Schumann Academy Düsseldorf" DE-Ch1 = "Technische Universität Chemnitz" @@ -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" @@ -2136,7 +2135,6 @@ DE-Kn38 = "Hochschule für Musik und Tanz Köln" Range-from-to = "Range from/to" ; #17833 -form-legend = "Please fill in all fields to create an account" form-button-submit = "Submit the completed form" ; #17601 @@ -2167,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/module/finc/src/finc/ILS/Connection.php b/module/finc/src/finc/ILS/Connection.php index 4ddbdddb7def752e7bd4613feefb22c14e280a71..a7f05faf35fdc579c430ccbc362c746858fe0187 100644 --- a/module/finc/src/finc/ILS/Connection.php +++ b/module/finc/src/finc/ILS/Connection.php @@ -91,4 +91,87 @@ class Connection extends \VuFind\ILS\Connection implements TranslatorAwareInterf } return $response; } + + /** + * Check ILLRequests + * + * A support method for checkFunction(). This is responsible for checking + * the driver configuration to determine if the system supports ILL requests. + * + * @param array $functionConfig The ILL request configuration values + * @param array $params An array of function-specific params (or null) + * + * @return mixed On success, an associative array with specific function keys + * and values either for placing requests via a form; on failure, false. + */ + protected function checkMethodILLRequests($functionConfig, $params) + { + if ( + method_exists($this, 'getMyILLRequests') + && + isset($functionConfig['daiaILLpattern']) + ) { + return ['daiaILLpattern' => $functionConfig['daiaILLpattern']]; + } + + return false; + } + + + /** + * Get access to the driver object. + * Extends parent function with a bypass of failover on InitException + * + * @param bool $init Should we initialize the driver (if necessary), or load it + * "as-is"? + * + * @throws \Exception + * @throws InitException + * @return object + */ + public function getDriver($init = true) + { + if (null === $this->driver) { + $this->setDriver($this->driverManager->get($this->config->driver)); + } + if (!$this->driverInitialized && $init) { + try { + $this->initializeDriver(); + } catch (InitException $e) { + throw $e; + } catch (\Exception $e) { + if (!$this->failOverToNoILS()) { + throw $e; + } + } + } + return $this->driver; + } + + /** + * Get Hidden Login Mode + * + * This is responsible for indicating whether login should be hidden. + * + * Extends parent function with a bypass of failover on InitException + * + * @throws \Exception + * @throws InitException + * @return bool true if the login should be hidden, false if not + */ + public function loginIsHidden() + { + // Graceful degradation -- return false if no method supported. + try { + return $this->checkCapability('loginIsHidden') + ? $this->getDriver()->loginIsHidden() : false; + } catch (InitException $e) { + throw $e; + } catch (\Exception $e) { + if ($this->failOverToNoILS()) { + return call_user_func_array([$this, __METHOD__], func_get_args()); + } + throw $e; + } + } } diff --git a/module/finc/src/finc/ILS/Driver/FincILS.php b/module/finc/src/finc/ILS/Driver/FincILS.php index 6a204440bdc41f2122b145e283d04b7763521c23..7f8f971642d890cfab5d37eb0ae675cc449a01a1 100644 --- a/module/finc/src/finc/ILS/Driver/FincILS.php +++ b/module/finc/src/finc/ILS/Driver/FincILS.php @@ -108,6 +108,22 @@ class FincILS extends PAIA implements LoggerAwareInterface */ protected $isil; + /** + * Regex to be used in getMyILLRequests, + * finds item IDs denoting inter library loan requests + * + * @var string + */ + protected $illItemPattern; + + /** + * Regex to be used in getMyILLRequests, + * finds item labels denoting inter library loan requests + * + * @var string + */ + protected $illLabelPattern; + /** * Connection timeout in seconds used for _testILSConnection() * @@ -223,6 +239,16 @@ class FincILS extends PAIA implements LoggerAwareInterface $this->ilsTestTimeout = isset($this->config['General']) && isset($this->config['General']['ilsTestTimeout']) ? $this->config['General']['ilsTestTimeout'] : 90; + + // set filter for reserved item IDs at ILL request + $this->illItemPattern = + (isset($this->config['ILLRequests']['itemPattern'])) + ? $this->config['ILLRequests']['itemPattern'] : null; + + // set filter for reserved item labels at ILL request + $this->illLabelPattern = + (isset($this->config['ILLRequests']['labelPattern'])) + ? $this->config['ILLRequests']['labelPattern'] : null; } /** @@ -1704,4 +1730,34 @@ class FincILS extends PAIA implements LoggerAwareInterface // overriden in FincLibero return $limitations; } + + /** + * Customized getMyILLRequests, relies on PAIA-URI pattern + * @param array $patron + * @return array + */ + public function getMyILLRequests($patron) + { + // filters for getMyILLRequests are: + // document.item = URI has to match config pattern + + if (!empty($this->illItemPattern)) { + $filter['regex']['item'] = $this->illItemPattern; + } + + if (!empty($this->illLabelPattern)) { + // filter out some item according their status label + // cf. #15214 + $filter['regex']['label'] = $this->illLabelPattern; + } + + if (!empty($filter)) { + // get items-docs for given filters + $items = $this->paiaGetItems($patron, $filter); + + return $this->mapPaiaItems($items, 'myHoldsMapping'); + } + + return []; + } } diff --git a/module/finc/src/finc/ILS/Driver/FincLibero.php b/module/finc/src/finc/ILS/Driver/FincLibero.php index 116bcf3881f4989ecc7f945dc03a3c83dad32a86..91d958f1bf604b53a71c8e72ecc1e3c9763c7219 100644 --- a/module/finc/src/finc/ILS/Driver/FincLibero.php +++ b/module/finc/src/finc/ILS/Driver/FincLibero.php @@ -27,6 +27,7 @@ * @link http://vufind.org/wiki/vufind2:building_an_ils_driver Wiki */ namespace finc\ILS\Driver; +use finc\ILS\InitException; use VuFind\I18n\Translator\TranslatorAwareTrait; use VuFind\I18n\Translator\TranslatorAwareInterface, VuFind\Exception\ILS as ILSException; @@ -122,8 +123,136 @@ class FincLibero extends FincILS implements TranslatorAwareInterface public function init() { parent::init(); - $this->boundItemIdPattern = $this->config['General']['bound_item_id_pattern'] ?? null; - $this->boundItemLabelPattern = $this->config['General']['bound_item_label_pattern'] ?? null; + + $this->setMemberFromConfig('boundItemPattern', 'General', 'bound_item_id_pattern'); + $this->setMemberFromConfig('boundItemLabelPattern', 'General', + 'bound_item_label_pattern'); + + // Get the base URI to extend departments. + $this->setMemberFromConfig( + 'departmentLocationBase', 'General', 'departmentLocationBase', + "No departmentLocationBase defined in FincLibero.ini." + ); + + // get the URIs for the limitations that are marking an item that a hold can + // be placed on + $this->setMemberFromConfig( + 'titleHoldLimitations', + 'General', + 'titleHoldLimitations', + "No Limitations defined for action titleHold.", + 'titleHoldLimitations' + ); + + // get the pickUpLocationPatterns configured in ILS ini and being used for + // filtering limitations used for pickuplocations + $this->setMemberFromConfig( + 'pickUpLocationPatterns', + 'General', + 'pickUpLocationPatterns', + "No pickUpLocationPatterns defined in FincLibero.ini." + ); + + // get the URIs for the limitations that are marking an item that a storage + // retrieval request can be placed on + $this->setMemberFromConfig( + 'requestableLimitations', + 'General', + 'requestableLimitations', + "No Limitations defined for action Storage Retrieval Request.", + 'requestableLimitations' + ); + + // get the URIs for the limitations that are marking an item that a hold can + // be placed on + $this->setMemberFromConfig( + 'holdableLimitations', + 'General', + 'holdableLimitations', + "No Limitations defined for action Hold.", + 'holdableLimitations' + ); + + // get the URIs for the limitations that are marking an item that a recall can + // be placed on + $this->setMemberFromConfig( + 'recallableLimitations', + 'General', + 'recallableLimitations', + "No Limitations defined for action Recall.", + 'recallableLimitations' + ); + + // get URIs of limitations that will identify the item for being bound to + // another item + $this->setMemberFromConfig( + 'awlLimitations', + 'General', + 'awlLimitations', + "No awlLimitations defined.", + 'awlLimitations' + ); + + // get the URIs identifying records for stack views + $this->setMemberFromConfig( + 'stackURIs', + 'General', + 'stackURIs', + "No stack URIs defined." + ); + + // get the URIs identifying records for reading room views + $this->setMemberFromConfig( + 'readingRoomURIs', + 'General', + 'readingRoomURIs', + "No reading room URIs defined." + ); + } + + /** + * Helper function for @see FincLibero::init() + * Set some member variables based on Config entries from FincILS.ini or + * FincLibero.ini. + * If the given config key (in given section) is set, the member variable will be + * initialized with the value(s) from that key. + * Otherwise (not set): + * When the setting is required via the [RequiredConfig] section AND a debug message is + * given, this message will be added to the debug log. + * With the last parameter $configuredLimitationName, the config entry will be added + * to the set of configured limitations under the given name + * + * @param string $memberName name of class member to be set + * @param string $configSection section from INI file to look up (e.g. 'General' for section [General]) + * @param string $configKey config key within that section + * @param string $debugMessage message to add to debug log on fail if the config is set required + * @param string $configuredLimitationName name of limitation type to be used in setConfiguredLimitation() + */ + protected function setMemberFromConfig( + $memberName, + $configSection, + $configKey, + $debugMessage = '', + $configuredLimitationName = '' + ) { + if ( + !isset($this->config[$configSection]) + || + !isset($this->config[$configSection][$configKey]) + ) { + if ( + !empty($debugMessage) + && + ($this->config['RequiredConfig'][$configSection][$configKey] ?? false) + ) { + throw new InitException($debugMessage); + } + } else { + $this->{$memberName} = $this->config[$configSection][$configKey]; + if (!empty($configuredLimitationName)) { + $this->setConfiguredLimitation($configuredLimitationName); + } + } } /** @@ -408,6 +537,28 @@ class FincLibero extends FincILS implements TranslatorAwareInterface } } + /** + * FincLibero specific override: we need to manually update the cat_password in + * the session as we do not save passwords in the database and therefore the + * ILSAuthenticator will use the session data instead. + * + * @param array $details Array with patron information, newPassword and + * oldPassword. + * + * @return array An array with patron information. + */ + public function changePassword($details) + { + $retval = parent::changePassword($details); + + if ($retval == ['success' => true, 'status' => 'Successfully changed']) { + $session = $this->getSession(); + $session->cat_password = $details['newPassword']; + } + + return $retval; + } + /** * PAIA helper function to map session data to return value of patronLogin() * @@ -483,17 +634,7 @@ class FincLibero extends FincILS implements TranslatorAwareInterface { $return = parent::getItemStatus($item); - // add all item specific information from DAIA field about to item_notes - // (https://intern.finc.info/issues/7863) - $about = (isset($item['about'])) ? [$item['about']] : []; - - $return['item_notes'] = array_unique( - array_merge( - (array) $return['status'], - $return['item_notes'], - $about - ) - ); + $return['item_notes'] = $this->getItemNotes($return, $item); $return['awlRecordId'] = $this->getBoundItemId($item); // is this item bound with another item? @@ -516,9 +657,49 @@ class FincLibero extends FincILS implements TranslatorAwareInterface } } + $return['service_type'] = $this->_reduceServices($return['services']); + return $return; } + /** + * Helper function for getItemStatus(). + * Gather all item notes from current (parent) result and DAIA item + * + * @param array $return intermediate result from parent::getItemStatus() + * @param array $item DAIA item + * @return array 'item_notes' part for result + */ + protected function getItemNotes($return, $item) + { + // add all item specific information from DAIA field about to item_notes + // (https://intern.finc.info/issues/7863) + return array_unique( + array_merge( + (array)$return['status'], + $return['item_notes'], + $item['about'] ?? [] + ) + ); + } + + /** + * Helper function for 'service_type' entry in getItemStatus + * + * @param array $services actually present services in DAIA item + * @return false|int|mixed preferred service if present or 0 + */ + protected function _reduceServices($services) + { + $prio = [ + 'loan', + 'presentation', + ]; + $res = array_intersect($prio, $services); + if (empty($res)) return 0; + else return current($res); + } + /** * Helper function to return an appropriate status string for current item * @@ -731,6 +912,31 @@ class FincLibero extends FincILS implements TranslatorAwareInterface return $this->mapPaiaItems($items, 'myHoldsMapping'); } + /** + * This method queries the ILS for a patron's current storage retrieval requests. + * Returns items with properties: + * - document.status = 2 (ordered) + * - document.storage and document.storageid do have a certain location (Magazin, but not Lesesaal) + * + * note: stackURIs musst be configured in FincLibero.ini as required + * + * @param array $patron Array returned from patronLogin() + * + * @return array + */ + public function getMyStorageRetrievalRequests($patron) + { + $filter = [ + 'status' => [2], + 'storageid' => $this->stackURIs, + 'regex' => ['item' => "/^(" . $this->getDaiaIdPrefixNamespace() . ").*$/"] + ]; + // get items-docs for given filters + $items = $this->paiaGetItems($patron, $filter); + + return $this->mapPaiaItems($items, 'myStorageRetrievalRequestsMapping'); + } + /** * Customized getMyTransactions for FincLibero to return items with properties: * - document.status = 3 (held) diff --git a/module/finc/src/finc/ILS/InitException.php b/module/finc/src/finc/ILS/InitException.php new file mode 100644 index 0000000000000000000000000000000000000000..7e7a692cc70717ea3e4b107c33806966381ced22 --- /dev/null +++ b/module/finc/src/finc/ILS/InitException.php @@ -0,0 +1,40 @@ +<?php +/** + * Exception thrown in FincLibero::init() + * PHP version 7 + * + * Copyright (C) Leipzig University Library 2021. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package ILS_Drivers + * @author Dorian Merz <merz@ub.uni-leipzig.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:ils_drivers Wiki + */ +namespace finc\ILS; + +/** + * Exception thrown in FincLibero::init() + * + * @category VuFind + * @package ILS_Drivers + * @author Dorian Merz <merz@ub.uni-leipzig.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:ils_drivers Wiki + */ +class InitException extends \Exception +{ +} diff --git a/module/finc/src/finc/View/Helper/Root/ExternalLink.php b/module/finc/src/finc/View/Helper/Root/ExternalLink.php index 91b1df9feaadbd1b7b2d51291c99fbd5a88d46d9..6bfbb6de3ccd846cfaef56c46476b148458683ec 100644 --- a/module/finc/src/finc/View/Helper/Root/ExternalLink.php +++ b/module/finc/src/finc/View/Helper/Root/ExternalLink.php @@ -27,6 +27,9 @@ */ namespace finc\View\Helper\Root; +use Zend\View\Helper\EscapeHtml; +use Zend\View\Helper\EscapeHtmlAttr; + /** * External link view helper * @@ -38,13 +41,6 @@ namespace finc\View\Helper\Root; */ class ExternalLink extends \Zend\View\Helper\AbstractHelper { - /** - * Context view helper - * - * @var \VuFind\View\Helper\Root\Translate - */ - protected $translator; - /** * Default html attributes for external links * @@ -55,6 +51,13 @@ class ExternalLink extends \Zend\View\Helper\AbstractHelper 'rel' => 'noopener' ]; + /** + * Escape manuelly flag + * + * @var bool + */ + protected $manualEscape; + /** * Renders an anchor element (hyperlink) to an external website * @@ -62,7 +65,8 @@ class ExternalLink extends \Zend\View\Helper\AbstractHelper * @param string $label desired label or translation token * @param array $attributes more attributes to apply to the a element, * key-value-pairs - * @param bool $translateLabel true if label shall be translated + * @param bool $manualEscape true if $label conatins html to display and + * $attributes are escped before calling method * * @return string */ @@ -70,36 +74,55 @@ class ExternalLink extends \Zend\View\Helper\AbstractHelper $href, $label = '', $attributes = [], - $translateLabel = false + $manualEscape = false ) { + $this->manualEscape = $manualEscape; + $link = '<a href="' . $href . '"'; $attributes = array_merge($this->defaultAttributes, $attributes); foreach ($attributes as $key => $value) { - $link .= ' ' . $key . '="' . $value . '"'; + $link .= ' ' . $key . '="' . $this->escapeHtmlAttr($value) . '"'; } $link .= '>'; if (empty($label)) { $label = $href; - } elseif ($translateLabel) { - $label = $this->translate($label); } - $link .= $label . '</a>'; + $link .= $this->escapeHtml($label) . '</a>'; return $link; } /** - * Helper function to provide access to Translate ViewHelper + * Helper function to provide access to EscapeHTML ViewHelper + * and esacpe lable if not disabled + * + * @param string $html html to escape + * + * @return string + */ + protected function escapeHtml($html) + { + if ($this->manualEscape) { + return $html; + } + $htmlEscaper = $this->getView()->plugin('escapeHtml'); + return $htmlEscaper($html); + } + + /** + * Helper function to provide access to EscaperHTML Attribute ViewHelper + * and esacpe HTML attributes if not disabled * * @param string $label translation token * * @return string */ - protected function translate($label) + protected function escapeHtmlAttr($htmlAttr) { - if (!isset($this->translator)) { - $this->translator = $this->getView()->plugin('translate'); + if ($this->manualEscape) { + return $htmlAttr; } - return $this->translator->translate($label); + $htmlAttrEscaper = $this->getView()->plugin('escapeHtmlAttr'); + return $htmlAttrEscaper($htmlAttr); } } 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-de.min.js b/themes/finc-accessibility/js/vendor/bootstrap-accessibility-de.min.js index 43f5fe0175e2f59b7456f6ae7d5439be1dc7a39e..0b23dca9f44e502f5c348a865c348630a554e075 100644 --- a/themes/finc-accessibility/js/vendor/bootstrap-accessibility-de.min.js +++ b/themes/finc-accessibility/js/vendor/bootstrap-accessibility-de.min.js @@ -310,4 +310,21 @@ k = e.which || e.keyCode; /(37|38|39|40)/.test(k) && (index = $tabs.index($tabs.filter(".active")), (37 == k || 38 == k) && (index--, selectTab(index)), (39 == k || 40 == k) && (index++, selectTab(index)), e.preventDefault(), e.stopPropagation()) }, $(document).on("keydown.carousel.data-api", "li[role=tab]", $.fn.carousel.Constructor.prototype.keydown) -}(jQuery); \ No newline at end of file +}(jQuery); + +$(document).on('keydown', '.dropdown-abort', function(e) { + if(e.which == 13) { + let toggle = $(this).parent().parent().parent().find('.dropdown-toggle'); + if(toggle.not(this).length){ + toggle.focus(); + } + } +}); +$(document).on('keyup', '.dropdown-abort', function(e) { + if(e.which == 13) { + let toggle = $(this).parent().parent().parent().find('.dropdown-toggle'); + if(toggle.not(this).length){ + toggle.dropdown("toggle"); + } + } +}); \ No newline at end of file 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..11bef8e1ba5fde3b9db5fe1c6b18a7239e3b23a1 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) { @@ -311,4 +310,21 @@ k = e.which || e.keyCode; /(37|38|39|40)/.test(k) && (index = $tabs.index($tabs.filter(".active")), (37 == k || 38 == k) && (index--, selectTab(index)), (39 == k || 40 == k) && (index++, selectTab(index)), e.preventDefault(), e.stopPropagation()) }, $(document).on("keydown.carousel.data-api", "li[role=tab]", $.fn.carousel.Constructor.prototype.keydown) -}(jQuery); \ No newline at end of file +}(jQuery); + +$(document).on('keydown', '.dropdown-abort', function(e) { + if(e.which == 13) { + let toggle = $(this).parent().parent().parent().find('.dropdown-toggle'); + if(toggle.not(this).length){ + toggle.focus(); + } + } +}); +$(document).on('keyup', '.dropdown-abort', function(e) { + if(e.which == 13) { + let toggle = $(this).parent().parent().parent().find('.dropdown-toggle'); + if(toggle.not(this).length){ + toggle.dropdown("toggle"); + } + } +}); \ No newline at end of file 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-accessibility/templates/Recommend/SideFacets/cluster-list.phtml b/themes/finc-accessibility/templates/Recommend/SideFacets/cluster-list.phtml index 293af05732827251527ee22b281cc69fe25b3cae..1b1f8adcbba51d11ecdae3e1785993b1e8cfff98 100644 --- a/themes/finc-accessibility/templates/Recommend/SideFacets/cluster-list.phtml +++ b/themes/finc-accessibility/templates/Recommend/SideFacets/cluster-list.phtml @@ -30,7 +30,7 @@ ]) ?> <?php endforeach; ?> <?php if (empty($this->cluster['list'])): ?> - <div class="facet"><?=$this->transEsc('facet_list_empty')?></div> + <span class="facet"><?=$this->transEsc('facet_list_empty')?></span> <?php endif; ?> <?php /* LESS and SEE MORE links */ ?> diff --git a/themes/finc-accessibility/templates/Recommend/SideFacets/filter-list.phtml b/themes/finc-accessibility/templates/Recommend/SideFacets/filter-list.phtml index 3957f6ba12e6ac722e65ea77266a15df592e7e73..3d2c763fce3e185c1e9f000fd9d06f4856a12c75 100644 --- a/themes/finc-accessibility/templates/Recommend/SideFacets/filter-list.phtml +++ b/themes/finc-accessibility/templates/Recommend/SideFacets/filter-list.phtml @@ -1,7 +1,7 @@ <!-- finc-accessibility - Recommend - SideFacets - filter-list.phtml --> <?php /* #18509 copied from bootstrap for adding language tags to displayText */ ?> <div class="facet-group active-filters"> - <div class="title"><?=$this->transEsc('Remove Filters')?> <span class="sr-only"><?=$this->transEsc('facet_deselect_hint') ?></div> + <div class="title"><?=$this->transEsc('Remove Filters')?> <span class="sr-only"><?=$this->transEsc('facet_deselect_hint') ?></span></div> <ul> <?php foreach ($filterList as $field => $filters): ?> <?php foreach ($filters as $i => $filter): ?> diff --git a/themes/finc/js/lightbox.js b/themes/finc/js/lightbox.js index 4d183e226b88d770ccea6bb32f1779abf431f7cd..8cdfbe87fe9d0cec2d198dd47dc4231c84df65ca 100644 --- a/themes/finc/js/lightbox.js +++ b/themes/finc/js/lightbox.js @@ -114,6 +114,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(); } @@ -426,6 +436,9 @@ VuFind.register('lightbox', function Lightbox() { } } } + function setOrigin(origin) { + _origin = origin; + } function onKeydown(e) { if (event.keyCode === 27) { // esc close(); @@ -483,7 +496,6 @@ VuFind.register('lightbox', function Lightbox() { }); }); } - function reset() { _html(VuFind.translate('loading') + '...'); _originalUrl = false; @@ -540,6 +552,7 @@ VuFind.register('lightbox', function Lightbox() { render: render, // Reset reset: reset, + setOrigin: setOrigin, // Init init: init }; diff --git a/themes/finc/templates/Recommend/EbscoResults.phtml b/themes/finc/templates/Recommend/EbscoResults.phtml index 2801644566d2ab48ed2af231dea8c7941f3daf61..253b18944ffacebbbb4811444c50dd5308a4b9f8 100644 --- a/themes/finc/templates/Recommend/EbscoResults.phtml +++ b/themes/finc/templates/Recommend/EbscoResults.phtml @@ -13,15 +13,17 @@ if (is_array($data['results']) && count($data['results']) > 0): ?> <span class="badge"> <?= $this->escapeHtml($result['hits']) ?> </span> - <a href="<?= $this->escapeHtmlAttr($result['url']) ?>" target="_blank"> - <?= $this->escapeHtml($this->truncate($this->transEsc('Ebsco::'.$result['database']), 90)) ?> - </a> + <?= $this->externalLink( + $this->escapeHtmlAttr($result['url']), + $this->truncate($this->translate('Ebsco::'.$result['database']), 90) + ) ?> </div> <?php endforeach; ?> - <a class="facet narrow-toggle" href="<?=$this->escapeHtmlAttr($data['hits_total_url'])?>" target="_blank" rel="nofollow"> - <?=$this->transEsc('more')?> ... - </a> - + <?= $this->externalLink( + $data['hits_total_url'], + $this->translate('more'), + ['class' => 'facet narrow-toggle'] + ) ?> </div> </div> <?php endif; ?> diff --git a/themes/finc/templates/Recommend/SideFacets.phtml b/themes/finc/templates/Recommend/SideFacets.phtml index 8c7fff5acfa902339f2eaf28c3f5376178d00a96..3e524b476825eedf817ba8fa2c8b110c3aadf186 100644 --- a/themes/finc/templates/Recommend/SideFacets.phtml +++ b/themes/finc/templates/Recommend/SideFacets.phtml @@ -55,9 +55,9 @@ if ($hierarchicalFacets) { <?php if (!empty($sideFacetSet) && $results->getResultTotal() > 0): ?> <?php foreach ($sideFacetSet as $title => $cluster): ?> <div class="facet-group" id="side-panel-<?=$this->escapeHtmlAttr($title)?>"> - <button <?php if(in_array($title, $collapsedFacets)): ?>class="title collapsed" aria-expanded="false"<?php else: ?>class="title" aria-expanded="true"<?php endif ?> data-toggle="collapse" href="#side-collapse-<?=$this->escapeHtmlAttr($title) ?>" > + <a <?php if(in_array($title, $collapsedFacets)): ?>class="title collapsed" aria-expanded="false"<?php else: ?>class="title" aria-expanded="true"<?php endif ?> data-toggle="collapse" href="#side-collapse-<?=$this->escapeHtmlAttr($title) ?>" > <?=$this->transEsc($cluster['label'])?> <span class="sr-only"><?=$this->transEsc('facet_select_hint') ?></span> - </button> + </a> <ul id="side-collapse-<?=$this->escapeHtmlAttr($title)?>" class="collapse<?php if (!in_array($title, $collapsedFacets)): ?> in<?php endif ?>"> <?=$this->context($this)->renderInContext( 'Recommend/SideFacets/facet.phtml', diff --git a/themes/finc/templates/Recommend/SideFacets/range-slider.phtml b/themes/finc/templates/Recommend/SideFacets/range-slider.phtml index 430be23580f50ce2ba0fccf96c7bb22bdf494db6..31a94bfef9261342ea71876881dca013bad5985f 100644 --- a/themes/finc/templates/Recommend/SideFacets/range-slider.phtml +++ b/themes/finc/templates/Recommend/SideFacets/range-slider.phtml @@ -4,7 +4,7 @@ * slider-container element */ ?> -<div class="facet"> +<li class="facet"> <?php if (!empty($this->facet['values'][0])): ?> <?php $this->sideFacet()->setAppliedFacet($this->transEsc('Skip to facet', ['%%filter_name%%' => $this->transEsc('Year of Publication')]), $this->escapeHtmlAttr($this->title) . 'from')?> <?php elseif (!empty($this->facet['values'][1])): ?> @@ -34,7 +34,6 @@ <?php endif; ?> <input class="btn btn-default" type="submit" value="<?=$this->transEsc('Set')?>"/> </form> -</div> <?php if ($this->facet['type'] == 'date'): ?> <?php $this->headScript()->appendFile('vendor/bootstrap-slider.min.js'); ?> <?php $this->headLink()->appendStylesheet('vendor/bootstrap-slider.min.css'); ?> @@ -83,4 +82,5 @@ JS; ?> <?=$this->inlineScript(\Zend\View\Helper\HeadScript::SCRIPT, $script, 'SET'); ?> <?php endif; ?> +</li> <!-- finc - recommend - sidefacets - rangeslider - END --> diff --git a/themes/finc/templates/RecordDriver/DefaultRecord/link-isn.phtml b/themes/finc/templates/RecordDriver/DefaultRecord/link-isn.phtml index 77bed61f58f3b6a7a2cfc3113ec6a2fbb194be64..30507b95f9bba7a18fa8cdd5d980856dfd166994 100644 --- a/themes/finc/templates/RecordDriver/DefaultRecord/link-isn.phtml +++ b/themes/finc/templates/RecordDriver/DefaultRecord/link-isn.phtml @@ -1,9 +1,9 @@ <?php /* use advanced search if we have multiple issns */ if (is_array($this->lookfor) && count($this->lookfor) > 1) { - $query = '?join=AND&bool0[]=OR'; + $query = '?join=AND&bool0%5B%5D=OR'; foreach ($this->lookfor as $issn) { - $query .= '&lookfor0[]=' . urlencode($issn) . '&type0[]=ISN'; + $query .= '&lookfor0%5B%5D=' . urlencode($issn) . '&type0%5B%5D=ISN'; } } elseif (count($this->lookfor) == 1) { $query = '?lookfor=%22' . urlencode($this->lookfor[0]) . '%22&type=ISN'; diff --git a/themes/finc/templates/RecordDriver/DefaultRecord/list-entry.phtml b/themes/finc/templates/RecordDriver/DefaultRecord/list-entry.phtml index 377e7ddf6a32d81a0bc92b694099ffb1713e1234..a816ef7b34e942e2e74fc35ea7393c004298a462 100644 --- a/themes/finc/templates/RecordDriver/DefaultRecord/list-entry.phtml +++ b/themes/finc/templates/RecordDriver/DefaultRecord/list-entry.phtml @@ -224,7 +224,13 @@ $thumbnailAlignment = $this->record($this->driver)->getThumbnailAlignment('list' <ul class="dropdown-menu" role="menu" aria-labelledby="<?= $dLabel ?>"> <li> <?php /* #17711 give user feedback and dont reload page after deleting */ ?> - <a href="javascript:document.getElementById('<?=$dLabel?>').focus();" title="<?= $this->transEsc('confirm_delete_brief') ?>" onClick="$.post( + <a href="javascript:document.getElementById('<?=$dLabel?>').focus();" title="<?= $this->transEsc('confirm_delete_brief') ?>" onClick=" + let next = $(this).closest('.result.ajaxItem').next('.result.ajaxItem').find('.del-button'); + if (next.length === 0) { + next = $('[id^=delete_list_items_]').first(); + } + VuFind.lightbox.setOrigin(next); + $.post( '<?= $deleteUrl ?>', { 'delete':'<?= $this->escapeJs($id) ?>', diff --git a/themes/finc/templates/RecordDriver/SolrAI/result-list.phtml b/themes/finc/templates/RecordDriver/SolrAI/result-list.phtml index e21b80019c88987fa4b300e1455720c988b77584..63c4ed5c15e04155f7917b395ef565e2dd4f658b 100644 --- a/themes/finc/templates/RecordDriver/SolrAI/result-list.phtml +++ b/themes/finc/templates/RecordDriver/SolrAI/result-list.phtml @@ -4,6 +4,7 @@ $coverDetails = $this->record($this->driver)->getCoverDetails('result-list', 'me $cover = $coverDetails['html']; $thumbnail = false; $thumbnailAlignment = $this->record($this->driver)->getThumbnailAlignment('result'); +$describedById = $driver->getSourceIdentifier() . '|' . $driver->getUniqueId(); if ($cover): ob_start(); ?> <div class="media-<?=$thumbnailAlignment?> <?=$this->escapeHtmlAttr($coverDetails['size'])?>" aria-hidden="true"> @@ -31,7 +32,7 @@ if ($cover): <div class="media-body"> <div class="result-body"> <div> - <a href="<?=$this->recordLink()->getUrl($this->driver)?>" class="title getFull" data-view="<?=$this->params->getOptions()->getListViewOption()?>" lang=""> + <a id="<?=$describedById?>" href="<?=$this->recordLink()->getUrl($this->driver)?>" class="title getFull" data-view="<?=$this->params->getOptions()->getListViewOption()?>" lang=""> <?=$this->record($this->driver)->getTitleHtml()?> </a> </div> diff --git a/themes/finc/templates/RecordTab/acquisitionpda.phtml b/themes/finc/templates/RecordTab/acquisitionpda.phtml index 91296ba15fa006a007013d4545a6cbfcb675aa6f..e069017a899ee0ec5596fda3bb820e5e9df2dbb3 100644 --- a/themes/finc/templates/RecordTab/acquisitionpda.phtml +++ b/themes/finc/templates/RecordTab/acquisitionpda.phtml @@ -15,9 +15,13 @@ $controllerClass = 'controller:SolrMarcFincPDA'; <p class="alert alert-info"><?=$this->transEsc('PDA::pda_restriction_text')?></p> <div class="btn-group"> <?php /* Leave title in here - it is used for the tooltip! - CK */ ?> - <a class="btn btn-primary" data-toggle="tooltip" title="<?=$this->transEsc('PDA::pda_open_new_window')?>" href="<?=$this->interlibraryloan()->getSwbLink($this->driver)?>" target="_blank"> - <?=$this->transEsc('PDA::pda_tab_interlibrary_button')?> - </a> + <?= $this->externalLink( + $this->interlibraryloan()->getSwbLink($this->driver), + $this->translate('PDA::pda_tab_interlibrary_button'), + [ 'class' => 'btn btn-primary', + 'data-toggle' => 'tooltip', + 'title' => $this->translate('PDA::pda_open_new_window')] + ) ?> <a class="btn btn-primary pda-button <?=$controllerClass?>" data-lightbox href="<?=$this->url('record-pda', array('id' => $id))?>" rel="nofollow"> <?=$this->transEsc('PDA::pda_tab_order_button')?> </a> diff --git a/themes/finc/templates/RecordTab/holdingsils.phtml b/themes/finc/templates/RecordTab/holdingsils.phtml index d929e6b59b9855a214f6df8fa0c0852f307a6d1e..ed99f581cf84e3bd1f396fba374371e32315551f 100644 --- a/themes/finc/templates/RecordTab/holdingsils.phtml +++ b/themes/finc/templates/RecordTab/holdingsils.phtml @@ -65,7 +65,7 @@ if (!empty($holdingTitleHold)): ?> <h2><?=$this->transEsc("Internet")?></h2> <?php if (!empty($urls)): ?> <?php foreach ($urls as $current): ?> - <a href="<?=$this->escapeHtmlAttr($this->proxyUrl($current['url']))?>"><?=$this->escapeHtml($current['desc'])?></a><br/> + <?= $this->externalLink($this->escapeHtmlAttr($this->proxyUrl($current['url'])), $current['desc']) ?><br/> <?php endforeach; ?> <?php endif; ?> <?php /* finc-specific snippet - #9274 - replaces if ($openUrlActive): ... - CK */ ?> @@ -75,7 +75,7 @@ if (!empty($holdingTitleHold)): ?> if (!empty($fallbackUrls)): ?> <span id="urlsHideable" style="display: none"> <?php foreach ($fallbackUrls as $current): ?> - <a href="<?=$this->escapeHtmlAttr($this->proxyUrl($current['url']))?>" target="_blank"><?=$this->escapeHtml($current['desc'] ?? $current['url'])?></a><br/> + <?= $this->externalLink($this->escapeHtmlAttr($this->proxyUrl($current['url'])), $current['desc'] ?? $current['url']) ?><br/> <?php endforeach; ?> </span> <?php endif; ?> @@ -93,7 +93,7 @@ if (!empty($holdingTitleHold)): ?> <h2> <?php $locationText = $this->transEsc('location_' . $holding['location'], [], $holding['location']); ?> <?php if (isset($holding['locationhref']) && $holding['locationhref']): ?> - <a href="<?=$holding['locationhref']?>" target="_blank"><?=$locationText?></a> + <?= $this->externalLink($holding['locationhref'], $locationText) ?> <?php else: ?> <?=$locationText?> <?php endif; ?> diff --git a/themes/finc/templates/ajax/resolverLinks.phtml b/themes/finc/templates/ajax/resolverLinks.phtml index d9b82875c17cd7e6184f08cc5721db62b1dcad85..cdc5ac85facdc8c80928de7ad8b60646db73edb8 100644 --- a/themes/finc/templates/ajax/resolverLinks.phtml +++ b/themes/finc/templates/ajax/resolverLinks.phtml @@ -22,7 +22,12 @@ <?php /* finc-specific change #7986 - END */ ?> <a href="<?=$this->escapeHtmlAttr($link['href'])?>" title="<?=isset($link['service_type'])?$this->escapeHtmlAttr($link['service_type']):''?>"<?=!empty($link['access'])?' class="access-'.$link['access'].'"':''?>><?=isset($link['title'])?$this->escapeHtml($link['title']):''?></a> <br /> <?php /* finc-specific change #5334 - CK */ ?> - <small><?=isset($link['coverage'])?$this->escapeHtml($link['coverage']):''?><?=isset($link['coverageHref'])?' <a href="'.$link['coverageHref'].'" target="_blank">'.$this->translate('Readme').'</a>':''?></small> + <small> + <?= isset($link['coverage']) ? $this->escapeHtml($link['coverage']) : '' ?> + <?= isset($link['coverageHref']) + ? $this->externalLink($link['coverageHref'], $this->translate('Readme')) + : '' ?> + </small> <?php /* finc-specific change #5334 - END */ ?> <?php else: ?> <?=isset($link['title'])?$this->escapeHtml($link['title']):''?> <?=isset($link['coverage'])?$this->transEsc($link['coverage']):''?> diff --git a/themes/finc/templates/ajax/status-full.phtml b/themes/finc/templates/ajax/status-full.phtml index 303067afd3f54d49ba871af84d7028fc758dcb9f..f48411b6a7613839ee6b963ee2510646bc2053be 100644 --- a/themes/finc/templates/ajax/status-full.phtml +++ b/themes/finc/templates/ajax/status-full.phtml @@ -12,7 +12,7 @@ <td data-title="<?= $this->transEsc('Location') ?>:" class="fullLocation"> <?php $locationText = $this->transEsc('location_' . $item['location'], [], $item['location']); ?> <?php if (isset($item['locationhref']) && $item['locationhref']): ?> - <a href="<?=$item['locationhref']?>" target="_blank"><?=$locationText?></a> + <?= $this->externalLink($item['locationhref'], $locationText) ?> <?php else: ?> <?=$locationText?> <?php endif; ?> diff --git a/themes/finc/templates/amsl/sources-list.phtml b/themes/finc/templates/amsl/sources-list.phtml index 371f906684de2b40f1af54bc0857935c9662358e..8970d0d041d4de5d3bbc0838e7a59a97c667e406 100644 --- a/themes/finc/templates/amsl/sources-list.phtml +++ b/themes/finc/templates/amsl/sources-list.phtml @@ -49,9 +49,11 @@ $this->layout()->breadcrumbs .= '</li> <li class="active">' . $this->transEsc('L <?php foreach ($source as $sub_label => $collection): ?> <li> <?php if (!empty($collection['href'])): ?> - <a title="<?=$this->transEsc("Search For")?> <?=$sub_label?>" href='<?=$collection["href"]?>' target="_blank"> - <?=$sub_label?> - </a> + <?= $this->externalLink( + $collection["href"], + $sub_label, + ['title' => "{$this->transEsc("Search For")} {$sub_label}"] + ) ?> <?php else: ?> <div tabindex="0" aria-label="<?=$this->transEsc("Source Title")?>"> <?=$sub_label?> @@ -77,8 +79,12 @@ $this->layout()->breadcrumbs .= '</li> <li class="active">' . $this->transEsc('L <span> <?=$this->transEsc('support_by_dfg');?> </span> - <a href='http://www.dfg.de' target='_blank'> - <img src='<?=$this->imageLink('dfg_logo_text.png')?>' alt='Deutsche Forschungsgemeinschaft, DFG'> + <?= $this->externalLink( + 'http://www.dfg.de', + "<img src='{$this->imageLink('dfg_logo_text.png')}' alt='Deutsche Forschungsgemeinschaft, DFG'>", + [], + true + ) ?> </a> </div> </div> diff --git a/themes/finc/templates/footer.phtml b/themes/finc/templates/footer.phtml index 301b47e88a484d27332358c9a3aea934023687ec..719d70099b343d088d6db736236e067b6fa92110 100644 --- a/themes/finc/templates/footer.phtml +++ b/themes/finc/templates/footer.phtml @@ -37,8 +37,8 @@ <span> <?= $this->transEsc("Footer-Powered-By-Text") ?> </span> - <?= $this->externalLink("https://vufind.org", '<img src="' . $this->imageLink('vufind_logo.png') . '" alt="' . $this->translate('vufind-logo_alt') . '" aria-hidden="true"/>', ['title' => $this->translate('vufind-logo_title')]) ?> - <?= $this->externalLink("https://finc.info", '<img src="' . $this->imageLink('finc_logo.png') . '" alt="' . $this->translate('finc-logo_alt') . '" aria-hidden="true"/>', ['title' => $this->translate('finc-logo_title')]) ?> + <?= $this->externalLink("https://vufind.org", '<img src="' . $this->imageLink('vufind_logo.png') . '" alt="' . $this->translate('vufind-logo_alt') . '" aria-hidden="true"/>', ['title' => $this->translate('vufind-logo_title')], true) ?> + <?= $this->externalLink("https://finc.info", '<img src="' . $this->imageLink('finc_logo.png') . '" alt="' . $this->translate('finc-logo_alt') . '" aria-hidden="true"/>', ['title' => $this->translate('finc-logo_title')], true) ?> </div> </div> </footer> diff --git a/themes/finc/templates/header.phtml b/themes/finc/templates/header.phtml index 50d239b701eb65722cd1115f6bdc0c405fae1650..67b8f3acd01568715ac098fdb5c6741fe52f82d9 100644 --- a/themes/finc/templates/header.phtml +++ b/themes/finc/templates/header.phtml @@ -109,7 +109,7 @@ <?php foreach ($this->layout()->allLangs as $langCode => $langName): ?> <?php if ($langCode !== $this->layout()->userLang) : ?> <li> - <button type="submit" class="btn <?=(count($this->layout()->allLangs) == 2) ? ' btn-secondary' : ''?>" href="#" onClick="document.langForm.mylang.value='<?=$langCode?>';document.langForm.submit()"> + <button type="submit" class="btn <?=(count($this->layout()->allLangs) == 2) ? ' btn-secondary' : ''?>" data-href="#" onClick="document.langForm.mylang.value='<?=$langCode?>';document.langForm.submit()"> <span class="visible-sm-md-only"><?=$langCode?></span> <span class="hidden-sm-md"><?=$this->displayLanguageOption($langName)?></span> </button> @@ -127,8 +127,8 @@ <?php /* finc searchbox: we use searchbox here so it becomes part of the sticky header, we need to place this after the navbar-right for anything but mobile - see flex-container in SCSS:*/ ?> <?php if ($this->layout()->searchbox !== false): ?> - <div class="search container"> - <nav class="nav searchbox hidden-print" role="search"> + <div class="search container" role="search"> + <nav class="nav searchbox hidden-print"> <?=$this->layout()->searchbox?> </nav> </div> diff --git a/themes/finc/templates/layout/layout.phtml b/themes/finc/templates/layout/layout.phtml index a269a91e3a942567214f3c2f97c6b620544988bb..8f4a34ab4c74a4cf5262f19e35e5843b65ff16ab 100644 --- a/themes/finc/templates/layout/layout.phtml +++ b/themes/finc/templates/layout/layout.phtml @@ -257,7 +257,7 @@ if (!isset($this->layout()->searchbox)) { <!-- MODAL IN CASE WE NEED ONE --> <?php /* move X button to logical pos. in structure + make accessible via tab - CK */ ?> -<div id="modal" class="modal fade hidden-print" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="modal-title" aria-hidden="true" aria-describedby="modal-description"> +<div id="modal" class="modal fade hidden-print" tabindex="-1" role="dialog" aria-modal="true" aria-hidden="true" aria-describedby="modal-description"> <div class="modal-dialog"> <div class="modal-content"> <button type="button" class="close" data-dismiss="modal" tabindex="0" aria-label="<?= $this->transEsc('CloseModal') ?>"> diff --git a/themes/finc/templates/myresearch/account.phtml b/themes/finc/templates/myresearch/account.phtml index b1055a8c55baea6e4f76428bbad1ef93d11cedae..f4e40c2f045ce098ba788dd2cc29355671bfa88a 100644 --- a/themes/finc/templates/myresearch/account.phtml +++ b/themes/finc/templates/myresearch/account.phtml @@ -11,7 +11,6 @@ <?=$this->flashmessages()?> <form method="post" name="accountForm" id="accountForm" class="form-user-create" data-toggle="validator" role="form"> - <legend class="sr-only"><?=$this->transEsc('form-legend')?></legend> <?=$this->auth()->getCreateFields()?> <?=$this->recaptcha()->html($this->useRecaptcha) ?> <div class="form-group"> diff --git a/themes/finc/templates/myresearch/mylist.phtml b/themes/finc/templates/myresearch/mylist.phtml index 6354ae6aa1c7962981d456fc50ba0388082b6014..ed7bf00413d20dec9fd6795100a4b9f99b4e0d0c 100644 --- a/themes/finc/templates/myresearch/mylist.phtml +++ b/themes/finc/templates/myresearch/mylist.phtml @@ -57,7 +57,7 @@ $user = $this->auth()->isLoggedIn(); </a> <ul class="dropdown-menu"> <li><a href="<?=$this->url('myresearch-deletelist')?>?listID=<?=urlencode($list->id)?>&confirm=1"><?=$this->transEsc('confirm_dialog_yes')?></a></li> - <li><a href="#"><?=$this->transEsc('confirm_dialog_no')?></a></li> + <li><a href="#" class="dropdown-abort"><?=$this->transEsc('confirm_dialog_no')?></a></li> </ul> </div> <?php endif; ?> @@ -95,5 +95,4 @@ $user = $this->auth()->isLoggedIn(); <br/><?=$this->recommend($current)?> <?php endforeach; ?> </div> - -<!-- finc: myresearch - mylist - END --> \ No newline at end of file +<!-- finc: myresearch - mylist - END --> diff --git a/themes/finc/templates/myresearch/newpassword.phtml b/themes/finc/templates/myresearch/newpassword.phtml index b2102e7315a77aac2af970fed31039ebc427488a..e493278a4bcba753d7aed4bbb79edfc44bffe16a 100644 --- a/themes/finc/templates/myresearch/newpassword.phtml +++ b/themes/finc/templates/myresearch/newpassword.phtml @@ -23,7 +23,6 @@ <div class="error"><?=$this->transEsc('recovery_user_not_found') ?></div> <?php else: ?> <form id="newpassword" class="form-new-password" action="<?=$this->url('myresearch-newpassword') ?>" method="post" data-toggle="validator" role="form"> - <legend class="sr-only"><?=$this->transEsc('form-legend')?></legend> <input type="hidden" value="<?=$this->escapeHtmlAttr($this->auth()->getManager()->getCsrfHash())?>" name="csrf"/> <input type="hidden" value="<?=$this->escapeHtmlAttr($this->hash) ?>" name="hash"/> <input type="hidden" value="<?=$this->escapeHtmlAttr($this->username) ?>" name="username"/> diff --git a/themes/finc/templates/record/cart-buttons.phtml b/themes/finc/templates/record/cart-buttons.phtml index edb7d83ea32988fbf3970619cf617a6174abcc5b..beaa7cbe11028a7347ce015293c8cd923e53f0b9 100644 --- a/themes/finc/templates/record/cart-buttons.phtml +++ b/themes/finc/templates/record/cart-buttons.phtml @@ -3,7 +3,7 @@ <?php if ($cart->isActive()): ?> <?php $cartId = $this->source . '|' . $this->id; ?> - <span class="btn-bookbag-toggle" data-cart-id="<?=$this->escapeHtmlAttr($this->id)?>" data-cart-source="<?=$this->escapeHtmlAttr($this->source)?>"> + <div class="btn-bookbag-toggle" data-cart-id="<?=$this->escapeHtmlAttr($this->id)?>" data-cart-source="<?=$this->escapeHtmlAttr($this->source)?>"> <?php /* Make add-to/remove-from bookbag accessible for keyboard navigation - CK */ ?> <a class="cart-add hidden<?php if (!$cart->contains($cartId)): ?> correct<?php endif ?>" href="javascript:" tabindex="0"> <i class="cart-link-icon fa fa-plus" aria-hidden="true"></i><span class="cart-link-label"><?=$this->transEsc('Add to Book Bag')?></span> @@ -11,16 +11,17 @@ <a class="cart-remove hidden<?php if ($cart->contains($cartId)): ?> correct<?php endif ?>" href="javascript:" tabindex="0"> <i class="cart-link-icon fa fa-minus-circle" aria-hidden="true"></i> <span class="cart-link-label"><?=$this->transEsc('Remove from Book Bag')?></span> </a> - <noscript> - <form method="post" name="addForm" action="<?=$this->url('cart-processor')?>"> + <form class="cartProcessorNoJs" method="post" name="addForm" action="<?=$this->url('cart-processor')?>"> + <noscript> <input type="hidden" name="ids[]" value="<?=$this->escapeHtmlAttr($cartId)?>"/> <?php if ($cart->contains($cartId)): ?> <input class="btn btn-default" type="submit" name="delete" value="<?=$this->transEsc('Remove from Book Bag')?>"/> <?php else: ?> <input class="btn btn-default" type="submit" name="add" value="<?=$this->transEsc('Add to Book Bag')?>"/> <?php endif; ?> - </form> - </noscript> - </span> + </noscript> + </form> + <script>$(document).ready(function() { $(".cartProcessorNoJs").css('display', 'none'); });</script> + </div> <?php endif; ?> <!-- finc: record - cart-buttons END --> diff --git a/themes/finc/templates/record/pdaform.phtml b/themes/finc/templates/record/pdaform.phtml index a7faa1f04c5d4fdb60d1075833acab13f2d9eb4f..ab7cb4438655e44e234fa26104d70738ca39a50a 100644 --- a/themes/finc/templates/record/pdaform.phtml +++ b/themes/finc/templates/record/pdaform.phtml @@ -36,8 +36,14 @@ $this->layout()->breadcrumbs = '<li>' . $this->searchMemory()->getLastSearchLink <?=$this->recaptcha()->html($this->useRecaptcha)?> </span> <input type="submit" class="btn btn-primary" role="button" name="submit" value="<?=$this->transEsc('Submit')?>"/> - <a class="btn btn-primary" data-lightbox-ignore data-toggle="tooltip" title="<?=$this->transEsc('PDA::pda_open_new_window')?>" href="<?=$this->interlibraryloan()->getSwbLink($this->driver)?>" target="_blank"><?=$this->transEsc('PDA::pda_tab_interlibrary_button')?> - </a> + <?= $this->externalLink( + $this->interlibraryloan()->getSwbLink($this->driver), + $this->translate('PDA::pda_tab_interlibrary_button'), + ['class' => 'btn btn-primary', + 'data-lightbox-ignore' => '', + 'data-toggle' => 'tooltip', + 'title' => $this->translate('PDA::pda_open_new_window')] + ) ?> <button class="btn btn-transparent" type="button" data-dismiss="modal" href="#"><?=$this->transEsc('Reset')?></button> </div> 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; ?> diff --git a/themes/finc/templates/search/results.phtml b/themes/finc/templates/search/results.phtml index de9ade6fd08919a365af15829592376107b95f24..8bac2e91e997dee739e6556a1349061de5c306b5 100644 --- a/themes/finc/templates/search/results.phtml +++ b/themes/finc/templates/search/results.phtml @@ -85,15 +85,15 @@ $this->headScript()->appendFile("check_save_statuses.js"); <?php if ($recordTotal > 0): ?> <?php /* finc: use spans for easier to show/hide choices - CK */ ?> <div class="search-controls"> - <span class="limit"> + <div class="limit"> <?=$this->render('search/controls/limit.phtml')?> - </span> - <span class="sort right"> + </div> + <div class="sort right"> <?=$this->render('search/controls/sort.phtml')?> - </span> - <span class="view"> + </div> + <div class="view"> <?=$this->render('search/controls/view.phtml')?> - </span> + </div> </div> <?php endif; ?> </div>