1
0
mirror of https://github.com/chylex/Nextcloud-News.git synced 2024-11-24 22:42:46 +01:00
Nextcloud-News/js/gui/KeyboardShortcuts.js
Kuba Orlik a0ab07fdb9 Make the "open" keyboard shortcut work faster
Previously when pressing the `O` key on article list, the handler for
that keypress first simulated a click on that event in order to mark
it as read, and only then opened the website that item links to in
another tab. When having a lot of items on screen this caused a huge
delay between pressing `O` and opening the linked article in a new
tab. The delay was sometimes 5, even 10 whole seconds. This simple fix
makes it so the article opens first, and then the click simulation
happens afterwards.

Signed-off-by: Kuba Orlik <kontakt@kuba-orlik.name>
Signed-off-by: Benjamin Brahmer <info@b-brahmer.de>
2023-01-29 19:14:30 +01:00

461 lines
15 KiB
JavaScript

/**
* 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 scrollElement = function() {
// This should be in sync with the same function in js/directive/NewsScroll.js
if (window.NEWS_NC_MAJOR_VERSION >= 25) {
return $('#app-content');
}
return $(window);
};
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 () {
return $('#app-content').find('.item.active:first');
};
var onActiveItem = function (callback) {
callback(getActiveElement());
};
var toggleUnread = function () {
onActiveItem(function (item) {
item.find('.toggle-keep-unread').trigger('click');
});
};
var toggleStar = function () {
onActiveItem(function (item) {
item.find('.star').trigger('click');
});
};
var expandItem = function () {
onActiveItem(function (item) {
item.find('.utils').trigger('click');
});
};
var openLink = function () {
onActiveItem(function (item) {
var url = item.find('.external:visible').attr('href');
var newWindow = window.open(url, '_blank');
newWindow.opener = null;
setTimeout(()=>item.trigger('click'), 0); // mark read
});
};
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
if (window.NEWS_NC_MAJOR_VERSION >= 25) {
scrollArea.scrollTop(scrollArea.scrollTop() + item.offset().top - 50);
} else {
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();
// 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();
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);
var scrollBottom = scrollElement().scrollTop();
if (bottom - 20 >= scrollBottom) {
setItemActive(item);
return false;
}
});
};
scrollElement().scroll(_.debounce(detectAndSetActiveItem, 250));
});
$(document).keyup(function (event) {
var keyCode = event.keyCode;
var scrollArea = scrollElement();
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, $));