1
0
Fork 0
mirror of https://github.com/miniflux/v2.git synced 2025-08-11 17:51:01 +00:00

refactor(js): reorder functions and add comments

This commit is contained in:
Frédéric Guillot 2025-08-01 21:32:54 -07:00
parent 7a25cf5037
commit 50197c2be3
2 changed files with 292 additions and 218 deletions

View file

@ -1,3 +1,15 @@
// Sentinel values for specific list navigation
const TOP = 9999;
const BOTTOM = -9999;
/**
* Get the CSRF token from the HTML document.
*
* @returns {string} The CSRF token.
*/
function getCsrfToken() {
return document.body.dataset.csrfToken || "";
}
/**
* Open a new tab with the given URL.
@ -11,6 +23,59 @@ function openNewTab(url) {
win.focus();
}
/**
* Scroll the page to the given element.
*
* @param {Element} element
* @param {boolean} evenIfOnScreen
*/
function scrollPageTo(element, evenIfOnScreen) {
const windowScrollPosition = window.scrollY;
const windowHeight = document.documentElement.clientHeight;
const viewportPosition = windowScrollPosition + windowHeight;
const itemBottomPosition = element.offsetTop + element.offsetHeight;
if (evenIfOnScreen || viewportPosition - itemBottomPosition < 0 || viewportPosition - element.offsetTop > windowHeight) {
window.scrollTo(0, element.offsetTop - 10);
}
}
/**
* Attach a click event listener to elements matching the selector.
*
* @param {string} selector
* @param {function} callback
* @param {boolean} noPreventDefault
*/
function onClick(selector, callback, noPreventDefault) {
document.querySelectorAll(selector).forEach((element) => {
element.onclick = (event) => {
if (!noPreventDefault) {
event.preventDefault();
}
callback(event);
};
});
}
/**
* Attach an auxiliary click event listener to elements matching the selector.
*
* @param {string} selector
* @param {function} callback
* @param {boolean} noPreventDefault
*/
function onAuxClick(selector, callback, noPreventDefault) {
document.querySelectorAll(selector).forEach((element) => {
element.onauxclick = (event) => {
if (!noPreventDefault) {
event.preventDefault();
}
callback(event);
};
});
}
/**
* Filter visible elements based on the selector.
*
@ -32,43 +97,231 @@ function getVisibleEntries() {
}
/**
* Scroll the page to the given element.
* Check if the current view is a list view.
*
* @param {Element} element
* @param {boolean} evenIfOnScreen
* @returns {boolean}
*/
function scrollPageTo(element, evenIfOnScreen) {
const windowScrollPosition = window.scrollY;
const windowHeight = document.documentElement.clientHeight;
const viewportPosition = windowScrollPosition + windowHeight;
const itemBottomPosition = element.offsetTop + element.offsetHeight;
function isListView() {
return document.querySelector(".items") !== null;
}
if (evenIfOnScreen || viewportPosition - itemBottomPosition < 0 || viewportPosition - element.offsetTop > windowHeight) {
window.scrollTo(0, element.offsetTop - 10);
/**
* Check if the current view is an entry view.
*
* @return {boolean}
*/
function isEntryView() {
return document.querySelector("section.entry") !== null;
}
/**
* Find the entry element for the given element.
*
* @returns {Element|null}
*/
function findEntry(element) {
if (isListView()) {
if (element) {
return element.closest(".item");
}
return document.querySelector(".current-item");
}
return document.querySelector(".entry");
}
/**
* Navigate to a specific page.
*
* @param {string} page - The page to navigate to.
* @param {boolean} reloadOnFail - If true, reload the current page if the target page is not found.
*/
function goToPage(page, reloadOnFail = false) {
const element = document.querySelector(":is(a, button)[data-page=" + page + "]");
if (element) {
document.location.href = element.href;
} else if (reloadOnFail) {
window.location.reload();
}
}
// OnClick attaches a listener to the elements that match the selector.
function onClick(selector, callback, noPreventDefault) {
document.querySelectorAll(selector).forEach((element) => {
element.onclick = (event) => {
if (!noPreventDefault) {
event.preventDefault();
}
callback(event);
};
});
/**
* Navigate to the previous page.
*
* If the offset is a KeyboardEvent, it will navigate to the previous item in the list.
* If the offset is a number, it will jump that many items in the list.
* If the offset is TOP, it will jump to the first item in the list.
* If the offset is BOTTOM, it will jump to the last item in the list.
* If the current view is an entry view, it will redirect to the previous page.
*
* @param {number|KeyboardEvent} offset - How many items to jump for focus.
*/
function goToPreviousPage(offset) {
if (offset instanceof KeyboardEvent) offset = -1;
if (isListView()) {
goToListItem(offset);
} else {
goToPage("previous");
}
}
function onAuxClick(selector, callback, noPreventDefault) {
document.querySelectorAll(selector).forEach((element) => {
element.onauxclick = (event) => {
if (!noPreventDefault) {
event.preventDefault();
/**
* Navigate to the next page.
*
* If the offset is a KeyboardEvent, it will navigate to the next item in the list.
* If the offset is a number, it will jump that many items in the list.
* If the offset is TOP, it will jump to the first item in the list.
* If the offset is BOTTOM, it will jump to the last item in the list.
* If the current view is an entry view, it will redirect to the next page.
*
* @param {number|KeyboardEvent} offset - How many items to jump for focus.
*/
function goToNextPage(offset) {
if (offset instanceof KeyboardEvent) offset = 1;
if (isListView()) {
goToListItem(offset);
} else {
goToPage("next");
}
}
/**
* Navigate to the individual feed or feeds page.
*
* If the current view is an entry view, it will redirect to the feed link of the entry.
* If the current view is a list view, it will redirect to the feeds page.
*/
function goToFeedOrFeedsPage() {
if (isEntryView()) {
goToFeedPage();
} else {
goToPage("feeds");
}
}
/**
* Navigate to the feed page of the current entry.
*
* If the current view is an entry view, it will redirect to the feed link of the entry.
* If the current view is a list view, it will redirect to the feed link of the currently selected item.
* If no feed link is available, it will do nothing.
*/
function goToFeedPage() {
if (isEntryView()) {
const feedAnchor = document.querySelector("span.entry-website a");
if (feedAnchor !== null) {
window.location.href = feedAnchor.href;
}
} else {
const currentItemFeed = document.querySelector(".current-item :is(a, button)[data-feed-link]");
if (currentItemFeed !== null) {
window.location.href = currentItemFeed.getAttribute("href");
}
}
}
/**
* Navigate to the add subscription page.
*
* @returns {void}
*/
function goToAddSubscriptionPage() {
window.location.href = document.body.dataset.addSubscriptionUrl;
}
/**
* Navigate to the next or previous item in the list.
*
* If the offset is TOP, it will jump to the first item in the list.
* If the offset is BOTTOM, it will jump to the last item in the list.
* If the offset is a number, it will jump that many items in the list.
* If the current view is an entry view, it will redirect to the next or previous page.
*
* @param {number} offset - How many items to jump for focus.
* @return {void}
*/
function goToListItem(offset) {
const items = getVisibleEntries();
if (items.length === 0) {
return;
}
if (document.querySelector(".current-item") === null) {
items[0].classList.add("current-item");
items[0].focus();
return;
}
for (let i = 0; i < items.length; i++) {
if (items[i].classList.contains("current-item")) {
items[i].classList.remove("current-item");
// By default adjust selection by offset
let itemOffset = (i + offset + items.length) % items.length;
// Allow jumping to top or bottom
if (offset === TOP) {
itemOffset = 0;
} else if (offset === BOTTOM) {
itemOffset = items.length - 1;
}
callback(event);
};
});
const item = items[itemOffset];
item.classList.add("current-item");
scrollPageTo(item);
item.focus();
break;
}
}
}
/**
* Handle the share action for the entry.
*
* If the share status is "shared", it will trigger the Web Share API.
* If the share status is "share", it will send an Ajax request to fetch the share URL and then trigger the Web Share API.
* If the Web Share API is not supported, it will redirect to the entry URL.
*/
async function handleShare() {
const link = document.querySelector(':is(a, button)[data-share-status]');
const title = document.querySelector(".entry-header > h1 > a");
if (link.dataset.shareStatus === "shared") {
await triggerWebShare(title, link.href);
}
else if (link.dataset.shareStatus === "share") {
const request = new RequestBuilder(link.href);
request.withCallback((r) => {
// Ensure title is not null before passing to triggerWebShare
triggerWebShare(title, r.url);
});
request.withHttpMethod("GET");
request.execute();
}
}
/**
* Trigger the Web Share API to share the entry.
*
* If the Web Share API is not supported, it will redirect to the entry URL.
*
* @param {Element} title - The title element of the entry.
* @param {string} url - The URL of the entry to share.
*/
async function triggerWebShare(title, url) {
if (!navigator.canShare) {
console.error("Your browser doesn't support the Web Share API.");
window.location = url;
return;
}
try {
await navigator.share({
title: title ? title.textContent : url,
url: url
});
} catch (err) {
console.error(err);
}
window.location.reload();
}
// make logo element as button on mobile layout
@ -490,114 +743,6 @@ function unsubscribeFromFeed() {
}
}
/**
* @param {string} page Page to redirect to.
* @param {boolean} fallbackSelf Refresh actual page if the page is not found.
*/
function goToPage(page, fallbackSelf = false) {
const element = document.querySelector(":is(a, button)[data-page=" + page + "]");
if (element) {
document.location.href = element.href;
} else if (fallbackSelf) {
window.location.reload();
}
}
/**
*
* @param {(number|event)} offset - many items to jump for focus.
*/
function goToPrevious(offset) {
if (offset instanceof KeyboardEvent) {
offset = -1;
}
if (isListView()) {
goToListItem(offset);
} else {
goToPage("previous");
}
}
/**
*
* @param {(number|event)} offset - How many items to jump for focus.
*/
function goToNext(offset) {
if (offset instanceof KeyboardEvent) {
offset = 1;
}
if (isListView()) {
goToListItem(offset);
} else {
goToPage("next");
}
}
function goToFeedOrFeeds() {
if (isEntry()) {
goToFeed();
} else {
goToPage('feeds');
}
}
function goToFeed() {
if (isEntry()) {
const feedAnchor = document.querySelector("span.entry-website a");
if (feedAnchor !== null) {
window.location.href = feedAnchor.href;
}
} else {
const currentItemFeed = document.querySelector(".current-item :is(a, button)[data-feed-link]");
if (currentItemFeed !== null) {
window.location.href = currentItemFeed.getAttribute("href");
}
}
}
// Sentinel values for specific list navigation
const TOP = 9999;
const BOTTOM = -9999;
/**
* @param {number} offset How many items to jump for focus.
*/
function goToListItem(offset) {
const items = getVisibleEntries();
if (items.length === 0) {
return;
}
if (document.querySelector(".current-item") === null) {
items[0].classList.add("current-item");
items[0].focus();
return;
}
for (let i = 0; i < items.length; i++) {
if (items[i].classList.contains("current-item")) {
items[i].classList.remove("current-item");
// By default adjust selection by offset
let itemOffset = (i + offset + items.length) % items.length;
// Allow jumping to top or bottom
if (offset === TOP) {
itemOffset = 0;
} else if (offset === BOTTOM) {
itemOffset = items.length - 1;
}
const item = items[itemOffset];
item.classList.add("current-item");
scrollPageTo(item);
item.focus();
break;
}
}
}
function scrollToCurrentItem() {
const currentItem = document.querySelector(".current-item");
if (currentItem !== null) {
@ -636,24 +781,6 @@ function updateUnreadCounterValue(callback) {
}
}
function isEntry() {
return document.querySelector("section.entry") !== null;
}
function isListView() {
return document.querySelector(".items") !== null;
}
function findEntry(element) {
if (isListView()) {
if (element) {
return element.closest(".item");
}
return document.querySelector(".current-item");
}
return document.querySelector(".entry");
}
function handleConfirmationMessage(linkElement, callback) {
if (linkElement.tagName !== 'A' && linkElement.tagName !== "BUTTON") {
linkElement = linkElement.parentNode;
@ -724,11 +851,6 @@ function showToast(label, iconElement) {
}, 100);
}
/** Navigate to the new subscription page. */
function goToAddSubscription() {
window.location.href = document.body.dataset.addSubscriptionUrl;
}
/**
* save player position to allow to resume playback later
* @param {Element} playerElement
@ -773,54 +895,6 @@ function isPlayerPlaying(element) {
element.readyState > 2; //https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState
}
/**
* handle new share entires and already shared entries
*/
async function handleShare() {
const link = document.querySelector(':is(a, button)[data-share-status]');
const title = document.querySelector(".entry-header > h1 > a");
if (link.dataset.shareStatus === "shared") {
await checkShareAPI(title, link.href);
}
if (link.dataset.shareStatus === "share") {
const request = new RequestBuilder(link.href);
request.withCallback((r) => {
checkShareAPI(title, r.url);
});
request.withHttpMethod("GET");
request.execute();
}
}
/**
* wrapper for Web Share API
*/
async function checkShareAPI(title, url) {
if (!navigator.canShare) {
console.error("Your browser doesn't support the Web Share API.");
window.location = url;
return;
}
try {
await navigator.share({
title: title ? title.textContent : url,
url: url
});
} catch (err) {
console.error(err);
}
window.location.reload();
}
/**
* Get the CSRF token from the HTML document.
*
* @returns {string} The CSRF token.
*/
function getCsrfToken() {
return document.body.dataset.csrfToken || "";
}
/**
* Handle all clicks on media player controls button on enclosures.
* Will change the current speed and position of the player accordingly.

View file

@ -6,17 +6,17 @@ document.addEventListener("DOMContentLoaded", () => {
keyboardHandler.on("g u", () => goToPage("unread"));
keyboardHandler.on("g b", () => goToPage("starred"));
keyboardHandler.on("g h", () => goToPage("history"));
keyboardHandler.on("g f", goToFeedOrFeeds);
keyboardHandler.on("g f", goToFeedOrFeedsPage);
keyboardHandler.on("g c", () => goToPage("categories"));
keyboardHandler.on("g s", () => goToPage("settings"));
keyboardHandler.on("g g", () => goToPrevious(TOP));
keyboardHandler.on("G", () => goToNext(BOTTOM));
keyboardHandler.on("ArrowLeft", goToPrevious);
keyboardHandler.on("ArrowRight", goToNext);
keyboardHandler.on("k", goToPrevious);
keyboardHandler.on("p", goToPrevious);
keyboardHandler.on("j", goToNext);
keyboardHandler.on("n", goToNext);
keyboardHandler.on("g g", () => goToPreviousPage(TOP));
keyboardHandler.on("G", () => goToNextPage(BOTTOM));
keyboardHandler.on("ArrowLeft", goToPreviousPage);
keyboardHandler.on("ArrowRight", goToNextPage);
keyboardHandler.on("k", goToPreviousPage);
keyboardHandler.on("p", goToPreviousPage);
keyboardHandler.on("j", goToNextPage);
keyboardHandler.on("n", goToNextPage);
keyboardHandler.on("h", () => goToPage("previous"));
keyboardHandler.on("l", () => goToPage("next"));
keyboardHandler.on("z t", scrollToCurrentItem);
@ -32,10 +32,10 @@ document.addEventListener("DOMContentLoaded", () => {
keyboardHandler.on("s", () => handleSaveEntry());
keyboardHandler.on("d", handleFetchOriginalContent);
keyboardHandler.on("f", () => handleBookmark());
keyboardHandler.on("F", goToFeed);
keyboardHandler.on("F", goToFeedPage);
keyboardHandler.on("R", handleRefreshAllFeeds);
keyboardHandler.on("?", showKeyboardShortcuts);
keyboardHandler.on("+", goToAddSubscription);
keyboardHandler.on("+", goToAddSubscriptionPage);
keyboardHandler.on("#", unsubscribeFromFeed);
keyboardHandler.on("/", () => goToPage("search"));
keyboardHandler.on("a", () => {