mirror of
https://github.com/TriliumNext/Trilium.git
synced 2025-12-10 21:07:05 -06:00
Back/forward navigation in tab bar (#8003)
This commit is contained in:
commit
3b8dabc9d2
@ -64,6 +64,9 @@ function initOnElectron() {
|
||||
if (options.get("nativeTitleBarVisible") !== "true") {
|
||||
initTitleBarButtons(style, currentWindow);
|
||||
}
|
||||
|
||||
// Clear navigation history on frontend refresh.
|
||||
currentWindow.webContents.navigationHistory.clear();
|
||||
}
|
||||
|
||||
function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
|
||||
|
||||
@ -45,6 +45,7 @@ import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
|
||||
import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
|
||||
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
||||
import Breadcrumb from "../widgets/Breadcrumb.jsx";
|
||||
import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx";
|
||||
|
||||
export default class DesktopLayout {
|
||||
|
||||
@ -80,6 +81,7 @@ export default class DesktopLayout {
|
||||
.class("tab-row-container")
|
||||
.child(new FlexContainer("row").id("tab-row-left-spacer"))
|
||||
.optChild(launcherPaneIsHorizontal, <LeftPaneToggle isHorizontalLayout={true} />)
|
||||
.child(<TabHistoryNavigationButtons />)
|
||||
.child(new TabRowWidget().class("full-width"))
|
||||
.optChild(customTitleBarButtons, <TitleBarButtons />)
|
||||
.css("height", "40px")
|
||||
@ -102,7 +104,12 @@ export default class DesktopLayout {
|
||||
new FlexContainer("column")
|
||||
.id("rest-pane")
|
||||
.css("flex-grow", "1")
|
||||
.optChild(!fullWidthTabBar, new FlexContainer("row").child(new TabRowWidget()).optChild(customTitleBarButtons, <TitleBarButtons />).css("height", "40px"))
|
||||
.optChild(!fullWidthTabBar,
|
||||
new FlexContainer("row")
|
||||
.child(<TabHistoryNavigationButtons />)
|
||||
.child(new TabRowWidget())
|
||||
.optChild(customTitleBarButtons, <TitleBarButtons />)
|
||||
.css("height", "40px"))
|
||||
.child(
|
||||
new FlexContainer("row")
|
||||
.filling()
|
||||
|
||||
@ -4,6 +4,8 @@ import froca from "./froca.js";
|
||||
import hoistedNoteService from "./hoisted_note.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
|
||||
export const NOTE_PATH_TITLE_SEPARATOR = " › ";
|
||||
|
||||
async function resolveNotePath(notePath: string, hoistedNoteId = "root") {
|
||||
const runPath = await resolveNotePathToSegments(notePath, hoistedNoteId);
|
||||
|
||||
@ -254,7 +256,7 @@ async function getNotePathTitle(notePath: string) {
|
||||
|
||||
const titlePath = await getNotePathTitleComponents(notePath);
|
||||
|
||||
return titlePath.join(" / ");
|
||||
return titlePath.join(NOTE_PATH_TITLE_SEPARATOR);
|
||||
}
|
||||
|
||||
async function getNoteTitleWithPathAsSuffix(notePath: string) {
|
||||
|
||||
@ -945,12 +945,26 @@ body.electron.background-effects.layout-horizontal .tab-row-container .toggle-bu
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
inset-inline-start: -10px;
|
||||
inset-inline-end: -10px;
|
||||
inset-inline-end: -6px;
|
||||
top: 32px;
|
||||
height: 1px;
|
||||
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
|
||||
}
|
||||
|
||||
body.electron.background-effects.layout-horizontal .tab-row-container .tab-history-navigation-buttons {
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: -7px;
|
||||
height: 1px;
|
||||
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
body.electron.background-effects.layout-horizontal .tab-row-container .tab-scroll-button-left,
|
||||
body.electron.background-effects.layout-horizontal .tab-row-container .tab-scroll-button-right {
|
||||
position: relative;
|
||||
|
||||
@ -2117,5 +2117,9 @@
|
||||
"unknown_http_error_title": "Communication error with the server",
|
||||
"unknown_http_error_content": "Status code: {{statusCode}}\nURL: {{method}} {{url}}\nMessage: {{message}}",
|
||||
"traefik_blocks_requests": "If you are using the Traefik reverse proxy, it introduced a breaking change which affects the communication with the server."
|
||||
},
|
||||
"tab_history_navigation_buttons": {
|
||||
"go-back": "Go back to previous note",
|
||||
"go-forward": "Go forward to next note"
|
||||
}
|
||||
}
|
||||
|
||||
7
apps/client/src/widgets/TabHistoryNavigationButtons.css
Normal file
7
apps/client/src/widgets/TabHistoryNavigationButtons.css
Normal file
@ -0,0 +1,7 @@
|
||||
.component.tab-history-navigation-buttons {
|
||||
contain: none;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-inline-end: 0.5em;
|
||||
}
|
||||
64
apps/client/src/widgets/TabHistoryNavigationButtons.tsx
Normal file
64
apps/client/src/widgets/TabHistoryNavigationButtons.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import "./TabHistoryNavigationButtons.css";
|
||||
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
|
||||
import { t } from "../services/i18n";
|
||||
import { dynamicRequire, isElectron } from "../services/utils";
|
||||
import { handleHistoryContextMenu } from "./launch_bar/HistoryNavigation";
|
||||
import ActionButton from "./react/ActionButton";
|
||||
import { useLauncherVisibility } from "./react/hooks";
|
||||
|
||||
export default function TabHistoryNavigationButtons() {
|
||||
const webContents = useMemo(() => isElectron() ? dynamicRequire("@electron/remote").getCurrentWebContents() : undefined, []);
|
||||
const onContextMenu = webContents ? handleHistoryContextMenu(webContents) : undefined;
|
||||
const { canGoBack, canGoForward } = useBackForwardState(webContents);
|
||||
const legacyBackVisible = useLauncherVisibility("_lbBackInHistory");
|
||||
const legacyForwardVisible = useLauncherVisibility("_lbForwardInHistory");
|
||||
|
||||
return (isElectron() &&
|
||||
<div className="tab-history-navigation-buttons">
|
||||
{!legacyBackVisible && <ActionButton
|
||||
icon="bx bx-left-arrow-alt"
|
||||
text={t("tab_history_navigation_buttons.go-back")}
|
||||
triggerCommand="backInNoteHistory"
|
||||
onContextMenu={onContextMenu}
|
||||
disabled={!canGoBack}
|
||||
/>}
|
||||
{!legacyForwardVisible && <ActionButton
|
||||
icon="bx bx-right-arrow-alt"
|
||||
text={t("tab_history_navigation_buttons.go-forward")}
|
||||
triggerCommand="forwardInNoteHistory"
|
||||
onContextMenu={onContextMenu}
|
||||
disabled={!canGoForward}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useBackForwardState(webContents: Electron.WebContents | undefined) {
|
||||
const [ canGoBack, setCanGoBack ] = useState(webContents?.navigationHistory.canGoBack());
|
||||
const [ canGoForward, setCanGoForward ] = useState(webContents?.navigationHistory.canGoForward());
|
||||
|
||||
useEffect(() => {
|
||||
if (!webContents) return;
|
||||
|
||||
const updateNavigationState = () => {
|
||||
setCanGoBack(webContents.navigationHistory.canGoBack());
|
||||
setCanGoForward(webContents.navigationHistory.canGoForward());
|
||||
};
|
||||
|
||||
webContents.on("did-navigate", updateNavigationState);
|
||||
webContents.on("did-navigate-in-page", updateNavigationState);
|
||||
|
||||
return () => {
|
||||
webContents.removeListener("did-navigate", updateNavigationState);
|
||||
webContents.removeListener("did-navigate-in-page", updateNavigationState);
|
||||
};
|
||||
}, [ webContents ]);
|
||||
|
||||
if (!webContents) {
|
||||
return { canGoBack: true, canGoForward: true };
|
||||
}
|
||||
|
||||
return { canGoBack, canGoForward };
|
||||
}
|
||||
@ -1,11 +1,13 @@
|
||||
import { useEffect, useRef } from "preact/hooks";
|
||||
import type { WebContents } from "electron";
|
||||
import { useMemo } from "preact/hooks";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import contextMenu, { MenuCommandItem } from "../../menus/context_menu";
|
||||
import froca from "../../services/froca";
|
||||
import link from "../../services/link";
|
||||
import tree from "../../services/tree";
|
||||
import { dynamicRequire, isElectron } from "../../services/utils";
|
||||
import { LaunchBarActionButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
|
||||
import type { WebContents } from "electron";
|
||||
import contextMenu, { MenuCommandItem } from "../../menus/context_menu";
|
||||
import tree from "../../services/tree";
|
||||
import link from "../../services/link";
|
||||
|
||||
interface HistoryNavigationProps {
|
||||
launcherNote: FNote;
|
||||
@ -16,26 +18,22 @@ const HISTORY_LIMIT = 20;
|
||||
|
||||
export default function HistoryNavigationButton({ launcherNote, command }: HistoryNavigationProps) {
|
||||
const { icon, title } = useLauncherIconAndTitle(launcherNote);
|
||||
const webContentsRef = useRef<WebContents>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isElectron()) {
|
||||
const webContents = dynamicRequire("@electron/remote").getCurrentWebContents();
|
||||
// without this, the history is preserved across frontend reloads
|
||||
webContents?.clearHistory();
|
||||
webContentsRef.current = webContents;
|
||||
}
|
||||
}, []);
|
||||
const webContents = useMemo(() => isElectron() ? dynamicRequire("@electron/remote").getCurrentWebContents() : undefined, []);
|
||||
|
||||
return (
|
||||
<LaunchBarActionButton
|
||||
icon={icon}
|
||||
text={title}
|
||||
triggerCommand={command}
|
||||
onContextMenu={async (e) => {
|
||||
onContextMenu={webContents ? handleHistoryContextMenu(webContents) : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function handleHistoryContextMenu(webContents: WebContents) {
|
||||
return async (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const webContents = webContentsRef.current;
|
||||
if (!webContents || webContents.navigationHistory.length() < 2) {
|
||||
return;
|
||||
}
|
||||
@ -46,20 +44,19 @@ export default function HistoryNavigationButton({ launcherNote, command }: Histo
|
||||
const activeIndex = webContents.navigationHistory.getActiveIndex();
|
||||
|
||||
for (const idx in history) {
|
||||
const { notePath } = link.parseNavigationStateFromUrl(history[idx].url);
|
||||
if (!notePath) continue;
|
||||
const { noteId, notePath } = link.parseNavigationStateFromUrl(history[idx].url);
|
||||
if (!noteId || !notePath) continue;
|
||||
|
||||
const title = await tree.getNotePathTitle(notePath);
|
||||
const index = parseInt(idx, 10);
|
||||
const note = froca.getNoteFromCache(noteId);
|
||||
|
||||
items.push({
|
||||
title,
|
||||
command: idx,
|
||||
uiIcon:
|
||||
parseInt(idx) === activeIndex
|
||||
? "bx bx-radio-circle-marked" // compare with type coercion!
|
||||
: parseInt(idx) < activeIndex
|
||||
? "bx bx-left-arrow-alt"
|
||||
: "bx bx-right-arrow-alt"
|
||||
checked: index === activeIndex,
|
||||
enabled: index !== activeIndex,
|
||||
uiIcon: note?.getIcon()
|
||||
});
|
||||
}
|
||||
|
||||
@ -80,7 +77,5 @@ export default function HistoryNavigationButton({ launcherNote, command }: Histo
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
@ -901,3 +901,27 @@ export function useChildNotes(parentNoteId: string | undefined) {
|
||||
|
||||
return childNotes;
|
||||
}
|
||||
|
||||
export function useLauncherVisibility(launchNoteId: string) {
|
||||
const checkIfVisible = useCallback(() => {
|
||||
const note = froca.getNoteFromCache(launchNoteId);
|
||||
return note?.getParentBranches().some(branch =>
|
||||
[ "_lbVisibleLaunchers", "_lbMobileVisibleLaunchers" ].includes(branch.parentNoteId)) ?? false;
|
||||
}, [ launchNoteId ]);
|
||||
|
||||
const [ isVisible, setIsVisible ] = useState<boolean>(checkIfVisible());
|
||||
|
||||
// React to note not being available in the cache.
|
||||
useEffect(() => {
|
||||
froca.getNote(launchNoteId).then(() => setIsVisible(checkIfVisible()));
|
||||
}, [ launchNoteId, checkIfVisible ]);
|
||||
|
||||
// React to changes.
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (loadResults.getBranchRows().some(branch => branch.noteId === launchNoteId)) {
|
||||
setIsVisible(checkIfVisible());
|
||||
}
|
||||
});
|
||||
|
||||
return isVisible;
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
import { NotePathRecord } from "../../entities/fnote";
|
||||
import NoteLink from "../react/NoteLink";
|
||||
import { joinElements } from "../react/react_utils";
|
||||
import { NOTE_PATH_TITLE_SEPARATOR } from "../../services/tree";
|
||||
|
||||
export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabContext) {
|
||||
const [ sortedNotePaths, setSortedNotePaths ] = useState<NotePathRecord[]>();
|
||||
@ -95,7 +96,7 @@ function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: strin
|
||||
<li class={classes}>
|
||||
{joinElements(fullNotePaths.map(notePath => (
|
||||
<NoteLink notePath={notePath} noPreview />
|
||||
)), " / ")}
|
||||
)), NOTE_PATH_TITLE_SEPARATOR)}
|
||||
|
||||
{icons.map(({ icon, title }) => (
|
||||
<span class={icon} title={title} />
|
||||
|
||||
@ -36,10 +36,11 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.ribbon-tab-title .bx {
|
||||
font-size: 125%;
|
||||
font-size: 150%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user