diff options
Diffstat (limited to 'js-old/gui/KeyboardShortcuts.js')
-rw-r--r-- | js-old/gui/KeyboardShortcuts.js | 446 |
1 files changed, 446 insertions, 0 deletions
diff --git a/js-old/gui/KeyboardShortcuts.js b/js-old/gui/KeyboardShortcuts.js new file mode 100644 index 000000000..b6310fef5 --- /dev/null +++ b/js-old/gui/KeyboardShortcuts.js @@ -0,0 +1,446 @@ +/** + * Nextcloud - News + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + * + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @copyright Bernhard Posselt 2014 + */ + +/** + * Code in here acts only as a click shortcut mechanism. That's why its not + * being put into a directive since it has to be tested with protractor + * anyways and theres no benefit from wiring it into the angular app + */ +(function (window, document, $) { + 'use strict'; + + var noInputFocused = function (element) { + return !( + element.is('input') || + element.is('select') || + element.is('textarea') || + element.is('checkbox') + ); + }; + + var noModifierKey = function (event) { + return !( + event.shiftKey || + event.altKey || + event.ctrlKey || + event.metaKey + ); + }; + + var markAllRead = function (navigationArea) { + var selector = '.active > .app-navigation-entry-menu .mark-read button'; + var button = navigationArea.find(selector); + if (button.length > 0) { + button.trigger('click'); + } + }; + + var isInScrollView = function (elem, scrollArea) { + // offset().top adds the navigation bar too so we have to subract it + var elemTop = elem.offset().top - scrollArea.offset().top; + var elemBottom = elemTop + elem.height(); + + var areaBottom = scrollArea.height(); + + return elemTop >= 0 && elemBottom < areaBottom; + }; + + var scrollToNavigationElement = function (elem, scrollArea, toTop) { + if (elem.length === 0 || (!toTop && isInScrollView(elem, scrollArea))) { + return; + } + scrollArea.scrollTop( + elem.offset().top - scrollArea.offset().top + scrollArea.scrollTop() + ); + }; + + var scrollToActiveNavigationEntry = function (navigationArea) { + var element = navigationArea.find('.active'); + scrollToNavigationElement(element, navigationArea.children('ul'), true); + }; + + var reloadFeed = function (navigationArea) { + navigationArea.find('.active > a:visible').trigger('click'); + }; + + var activateNavigationEntry = function (element, navigationArea) { + element.children('a:visible').trigger('click'); + scrollToNavigationElement(element, navigationArea.children('ul')); + }; + + var nextFeed = function (navigationArea) { + var current = navigationArea.find('.active'); + var elements = navigationArea.find('.explore-feed,' + + '.subscriptions-feed:visible,' + + '.starred-feed:visible,' + + '.feed:visible'); + + if (current.hasClass('folder')) { + while (current.length > 0) { + var subfeeds = current.find('.feed:visible'); + if (subfeeds.length > 0) { + activateNavigationEntry($(subfeeds[0]), navigationArea); + return; + } + current = current.next('.folder'); + } + + // no subfeed found + return; + } + + // FIXME: O(n) runtime. If someone creates a nice and not fugly solution + // please create a PR + for (var i = 0; i < elements.length - 1; i += 1) { + var element = elements[i]; + + if (element === current[0]) { + var next = elements[i + 1]; + activateNavigationEntry($(next), navigationArea); + break; + } + } + }; + + var getParentFolder = function (current) { + return current.parent().parent('.folder'); + }; + + var selectFirstOrLastFolder = function (navigationArea, isLast) { + var folders = navigationArea.find('.folder:visible'); + + var index; + if (isLast) { + index = folders.length - 1; + } else { + index = 0; + } + + if (folders.length > 0) { + activateNavigationEntry($(folders[index]), navigationArea); + } + }; + + var previousFolder = function (navigationArea) { + var current = navigationArea.find('.active'); + + // cases: folder active, subfeed active, feed active, none active + if (current.hasClass('folder')) { + activateNavigationEntry(current.prevAll('.folder:visible').first(), + navigationArea); + } else if (current.hasClass('feed')) { + var parentFolder = getParentFolder(current); + if (parentFolder.length > 0) { + // first go to previous folder should select the parent folder + activateNavigationEntry(parentFolder, navigationArea); + } else { + selectFirstOrLastFolder(navigationArea, true); + } + } else { + selectFirstOrLastFolder(navigationArea, true); + } + }; + + var nextFolder = function (navigationArea) { + var current = navigationArea.find('.active'); + + // cases: folder active, subfeed active, feed active, none active + if (current.hasClass('folder')) { + activateNavigationEntry(current.nextAll('.folder:visible').first(), + navigationArea); + } else if (current.hasClass('feed')) { + var parentFolder = getParentFolder(current); + if (parentFolder.length > 0) { + activateNavigationEntry( + parentFolder.nextAll('.folder:visible').first(), + navigationArea + ); + } else { + selectFirstOrLastFolder(navigationArea); + } + } else { + selectFirstOrLastFolder(navigationArea); + } + }; + + var previousFeed = function (navigationArea) { + var current = navigationArea.find('.active'); + var elements = navigationArea.find('.explore-feed,' + + '.subscriptions-feed:visible,' + + '.starred-feed:visible,' + + '.feed:visible'); + + // special case: folder selected + if (current.hasClass('folder')) { + var previousFolder = current.prev('.folder'); + + while (previousFolder.length > 0) { + var subfeeds = previousFolder.find('.feed:visible'); + if (subfeeds.length > 0) { + activateNavigationEntry($(subfeeds[subfeeds.length - 1]), + navigationArea); + return; + } + previousFolder = previousFolder.prev('.folder'); + } + + // no subfeed found try visible feeds + var feeds = current.siblings('.feed'); + + if (feeds.length > 0) { + activateNavigationEntry($(feeds[feeds.length - 1]), + navigationArea); + return; + } + + + // no feed found, go to starred + var starred = $('.starred-feed:visible'); + if (starred.length > 0) { + activateNavigationEntry(starred, navigationArea); + } + + return; + } + + // FIXME: O(n) runtime. If someone creates a nice and not fugly solution + // please create a PR + for (var i = elements.length - 1; i > 0; i -= 1) { + var element = elements[i]; + + if (element === current[0]) { + var previous = elements[i - 1]; + activateNavigationEntry($(previous), navigationArea); + break; + } + } + }; + + var getActiveElement = function (scrollArea) { + return scrollArea.find('.item.active:first'); + }; + + var onActiveItem = function (scrollArea, callback) { + callback(getActiveElement(scrollArea)); + }; + + var toggleUnread = function (scrollArea) { + onActiveItem(scrollArea, function (item) { + item.find('.toggle-keep-unread').trigger('click'); + }); + }; + + var toggleStar = function (scrollArea) { + onActiveItem(scrollArea, function (item) { + item.find('.star').trigger('click'); + }); + }; + + var expandItem = function (scrollArea) { + onActiveItem(scrollArea, function (item) { + item.find('.utils').trigger('click'); + }); + }; + + var openLink = function (scrollArea) { + onActiveItem(scrollArea, function (item) { + item.trigger('click'); // mark read + var url = item.find('.external:visible').attr('href'); + var newWindow = window.open(url, '_blank'); + newWindow.opener = null; + }); + }; + + var setItemActive = function (element) { + element.dispatchEvent(new CustomEvent('set-active')); + }; + + var scrollToItem = function (scrollArea, item, expandItemInCompact) { + // if you go to the next article in compact view, it should + // expand the current one + scrollArea.scrollTop( + item.offset().top - 50 + ); + + setItemActive(item[0]); + + if (expandItemInCompact) { + if (!item.hasClass('open')) { + item.find('.utils').trigger('click'); + } + } + }; + + var scrollToNextItem = function (scrollArea, expandItemInCompact) { + var activeElement = getActiveElement(scrollArea); + // in expand in compact mode, jumping to the next item should open + // the current one if it's not open yet + if (expandItemInCompact && !activeElement.hasClass('open')) { + activeElement.find('.utils').trigger('click'); + } else { + var nextElement = activeElement.next(); + if (nextElement.length > 0) { + scrollToItem(scrollArea, nextElement, expandItemInCompact); + } else if (nextElement.length === 0) { + activeElement.find('.utils').trigger('click'); + } else { + // in case this is the last item it should still scroll below + // the + scrollArea.scrollTop(scrollArea.prop('scrollHeight')); + } + } + }; + + var scrollToPreviousItem = function (scrollArea, + expandItemInCompact) { + var activeElement = getActiveElement(scrollArea); + var previousElement = activeElement.prev(); + + // if the active element has been scrolled, the previous element + // should be the active one + if (activeElement.position().top + 20 <= 0) { + scrollToItem(scrollArea, activeElement, expandItemInCompact); + } else if (previousElement.length > 0) { + scrollToItem(scrollArea, previousElement, expandItemInCompact); + } else { + scrollArea.scrollTop(0); + } + }; + + // mark current item as active when scrolling + $(document).ready(function () { + var detectAndSetActiveItem = function () { + var items = $('#app-content').find('.item'); + items.each(function (index, item) { + var $item = $(item); + var bottom = $item.position().top + $item.outerHeight(true); + if ((bottom - 20) >= 0) { + setItemActive(item); + return false; + } + }); + }; + $('#app-content').scroll(_.debounce(detectAndSetActiveItem, 250)); + }); + + $(document).keyup(function (event) { + var keyCode = event.keyCode; + var scrollArea = $(document); + var navigationArea = $('#app-navigation'); + var isCompactView = $('#articles.compact').length > 0; + var isExpandItem = $('#articles') + .attr('news-compact-expand') === 'true'; + var expandItemInCompact = isCompactView && isExpandItem; + + if (noInputFocused($(':focus')) && noModifierKey(event)) { + // j, n, right arrow + if ([74, 78, 39].indexOf(keyCode) >= 0) { + + event.preventDefault(); + scrollToNextItem(scrollArea, expandItemInCompact); + + // k, p, left arrow + } else if ([75, 80, 37].indexOf(keyCode) >= 0) { + + event.preventDefault(); + scrollToPreviousItem(scrollArea, + expandItemInCompact); + + // u + } else if ([85].indexOf(keyCode) >= 0) { + + event.preventDefault(); + toggleUnread(scrollArea); + + // e + } else if ([69].indexOf(keyCode) >= 0) { + + event.preventDefault(); + expandItem(scrollArea); + + // s, i, l + } else if ([73, 83, 76].indexOf(keyCode) >= 0) { + + event.preventDefault(); + toggleStar(scrollArea); + + // h + } else if ([72].indexOf(keyCode) >= 0) { + + event.preventDefault(); + toggleStar(scrollArea); + scrollToNextItem(scrollArea); + + // o + } else if ([79].indexOf(keyCode) >= 0) { + + event.preventDefault(); + openLink(scrollArea); + + // r + } else if ([82].indexOf(keyCode) >= 0) { + + event.preventDefault(); + reloadFeed(navigationArea); + + // f + } else if ([70].indexOf(keyCode) >= 0) { + + event.preventDefault(); + nextFeed(navigationArea); + + // d + } else if ([68].indexOf(keyCode) >= 0) { + + event.preventDefault(); + previousFeed(navigationArea); + + // c + } else if ([67].indexOf(keyCode) >= 0) { + + event.preventDefault(); + previousFolder(navigationArea); + + // a + } else if ([65].indexOf(keyCode) >= 0) { + + event.preventDefault(); + scrollToActiveNavigationEntry(navigationArea); + + // v + } else if ([86].indexOf(keyCode) >= 0) { + + event.preventDefault(); + nextFolder(navigationArea); + + // q + } else if ([81].indexOf(keyCode) >= 0) { + + event.preventDefault(); + $('#searchbox').focus(); + + // page up + } + + // everything with shift, just the shift + } else if (noInputFocused($(':focus')) && event.shiftKey && + !event.ctrlKey && !event.altKey && !event.metaKey) { + + // shift + a + if ([65].indexOf(keyCode) >= 0) { + + event.preventDefault(); + markAllRead(navigationArea); + + } + } + }); + +}(window, document, $)); |