diff --git a/www/css/app.css b/www/css/app.css index 4ab414f0..22be35a7 100644 --- a/www/css/app.css +++ b/www/css/app.css @@ -97,6 +97,10 @@ div:not(.panel-success, .alert-message) { background: lightblue; } +#articleList a:hover, #articleList a.hover { + background: lightblue; +} + .dark #articleListWithHeader { background: darkslategrey; } diff --git a/www/js/app.js b/www/js/app.js index 35ba9d4d..af2d66da 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -121,24 +121,59 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module' document.getElementById('formArticleSearch').addEventListener('submit', function () { document.getElementById("searchArticles").click(); }); - document.getElementById('prefix').addEventListener('keyup', function (e) { - if (selectedArchive !== null && selectedArchive.isReady()) { - if (e.which == 40) { - var articleResults = document.querySelectorAll('.list-group-item'); - if (articleResults && articleResults.length) { - articleResults[0].focus(); + var keyPressHandled = false; + $('#prefix').on('keydown', function (e) { + if (/^Esc/.test(e.key)) { + // Hide the article list + e.preventDefault(); + e.stopPropagation(); + $('#articleListWithHeader').hide(); + document.getElementById('articleContent').style.position = 'fixed'; + $('#articleContent').focus(); + $("#myModal").modal('hide'); // This is in case the modal box is showing with an index search + keyPressHandled = true; + } + // Keyboard selection code adapted from https://stackoverflow.com/a/14747926/9727685 + if (/^((Arrow)?Down|(Arrow)?Up|Enter)$/.test(e.key)) { + // User pressed Down arrow or Up arrow or Enter + e.preventDefault(); + e.stopPropagation(); + // This is needed to prevent processing in the keyup event : https://stackoverflow.com/questions/9951274 + keyPressHandled = true; + var activeElement = document.querySelector("#articleList .hover") || document.querySelector("#articleList a"); + if (!activeElement) return; + if (/Enter/.test(e.key)) { + if (activeElement.classList.contains('hover')) { + var dirEntryId = activeElement.getAttribute('dirEntryId'); + findDirEntryFromDirEntryIdAndLaunchArticleRead(dirEntryId); + return; } } - if (e.which == 27) { - //User pressed Esc, so hide the article list - $('#articleListWithHeader').hide(); - document.getElementById('articleContent').style.position = 'fixed'; - $('#articleContent').focus(); - - $("#myModal").modal('hide'); // This is in case the modal box is showing with an index search - return; + if (/(Arrow)?Down/.test(e.key)) { + if (activeElement.classList.contains('hover')) { + activeElement.classList.remove('hover'); + activeElement = activeElement.nextElementSibling || activeElement; + var nextElement = activeElement.nextElementSibling || activeElement; + if (!uiUtil.isElementInView(nextElement, true)) nextElement.scrollIntoView(false); + } } - onKeyUpPrefix(e); + if (/(Arrow)?Up/.test(e.key)) { + activeElement.classList.remove('hover'); + activeElement = activeElement.previousElementSibling || activeElement; + var previousElement = activeElement.previousElementSibling || activeElement; + if (!uiUtil.isElementInView(previousElement, true)) previousElement.scrollIntoView(); + if (previousElement === activeElement) document.getElementById('top').scrollIntoView(); + } + activeElement.classList.add('hover'); + + } + }); + $('#prefix').on('keyup', function (e) { + if (selectedArchive !== null && selectedArchive.isReady()) { + if (keyPressHandled) + keyPressHandled = false; + else + onKeyUpPrefix(e); } }); $('#prefix').on('focus', function (e) { @@ -2019,7 +2054,6 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies', 'q', 'module' function handleTitleClick(event) { var dirEntryId = event.target.getAttribute("dirEntryId"); findDirEntryFromDirEntryIdAndLaunchArticleRead(dirEntryId); - var dirEntry = selectedArchive.parseDirEntryId(dirEntryId); return false; } diff --git a/www/js/lib/uiUtil.js b/www/js/lib/uiUtil.js index 5341b0c3..fe14560b 100644 --- a/www/js/lib/uiUtil.js +++ b/www/js/lib/uiUtil.js @@ -91,23 +91,6 @@ define(['util'], function(util) { }; } - /** - * Checks whether an element is fully or partially in view - * This is useful for progressive download of images inside an article - * - * @param {Object} el - * @param {Boolean} fully - */ - function isElementInView(el, fully) { - var elemTop = el.getBoundingClientRect().top; - var elemBottom = el.getBoundingClientRect().bottom; - - var isVisible = fully ? elemTop < window.innerHeight && elemBottom >= 0 : - elemTop >= 0 && elemBottom <= window.innerHeight; - return isVisible; - } - - function makeReturnLink(title) { //Abbreviate title if necessary var shortTitle = title.substring(0, 25); @@ -435,6 +418,21 @@ define(['util'], function(util) { } } + /** + * Checks whether an element is partially or fully inside the current viewport + * + * @param {Element} el The DOM element for which to check visibility + * @param {Boolean} fully If true, checks that the entire element is inside the viewport + * @returns {Boolean} True if the element is fully or partially inside the current viewport + */ + function isElementInView(el, fully) { + var rect = el.getBoundingClientRect(); + if (fully) + return rect.top > 0 && rect.bottom < window.innerHeight && rect.left > 0 && rect.right < window.innerWidth; + else + return rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0; + } + /** * Functions and classes exposed by this module */