mirror of
https://github.com/zadam/trilium.git
synced 2025-12-12 10:24:23 +01: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") {
|
if (options.get("nativeTitleBarVisible") !== "true") {
|
||||||
initTitleBarButtons(style, currentWindow);
|
initTitleBarButtons(style, currentWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear navigation history on frontend refresh.
|
||||||
|
currentWindow.webContents.navigationHistory.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
|
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 SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
|
||||||
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
||||||
import Breadcrumb from "../widgets/Breadcrumb.jsx";
|
import Breadcrumb from "../widgets/Breadcrumb.jsx";
|
||||||
|
import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx";
|
||||||
|
|
||||||
export default class DesktopLayout {
|
export default class DesktopLayout {
|
||||||
|
|
||||||
@ -80,6 +81,7 @@ export default class DesktopLayout {
|
|||||||
.class("tab-row-container")
|
.class("tab-row-container")
|
||||||
.child(new FlexContainer("row").id("tab-row-left-spacer"))
|
.child(new FlexContainer("row").id("tab-row-left-spacer"))
|
||||||
.optChild(launcherPaneIsHorizontal, <LeftPaneToggle isHorizontalLayout={true} />)
|
.optChild(launcherPaneIsHorizontal, <LeftPaneToggle isHorizontalLayout={true} />)
|
||||||
|
.child(<TabHistoryNavigationButtons />)
|
||||||
.child(new TabRowWidget().class("full-width"))
|
.child(new TabRowWidget().class("full-width"))
|
||||||
.optChild(customTitleBarButtons, <TitleBarButtons />)
|
.optChild(customTitleBarButtons, <TitleBarButtons />)
|
||||||
.css("height", "40px")
|
.css("height", "40px")
|
||||||
@ -102,7 +104,12 @@ export default class DesktopLayout {
|
|||||||
new FlexContainer("column")
|
new FlexContainer("column")
|
||||||
.id("rest-pane")
|
.id("rest-pane")
|
||||||
.css("flex-grow", "1")
|
.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(
|
.child(
|
||||||
new FlexContainer("row")
|
new FlexContainer("row")
|
||||||
.filling()
|
.filling()
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import froca from "./froca.js";
|
|||||||
import hoistedNoteService from "./hoisted_note.js";
|
import hoistedNoteService from "./hoisted_note.js";
|
||||||
import appContext from "../components/app_context.js";
|
import appContext from "../components/app_context.js";
|
||||||
|
|
||||||
|
export const NOTE_PATH_TITLE_SEPARATOR = " › ";
|
||||||
|
|
||||||
async function resolveNotePath(notePath: string, hoistedNoteId = "root") {
|
async function resolveNotePath(notePath: string, hoistedNoteId = "root") {
|
||||||
const runPath = await resolveNotePathToSegments(notePath, hoistedNoteId);
|
const runPath = await resolveNotePathToSegments(notePath, hoistedNoteId);
|
||||||
|
|
||||||
@ -254,7 +256,7 @@ async function getNotePathTitle(notePath: string) {
|
|||||||
|
|
||||||
const titlePath = await getNotePathTitleComponents(notePath);
|
const titlePath = await getNotePathTitleComponents(notePath);
|
||||||
|
|
||||||
return titlePath.join(" / ");
|
return titlePath.join(NOTE_PATH_TITLE_SEPARATOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getNoteTitleWithPathAsSuffix(notePath: string) {
|
async function getNoteTitleWithPathAsSuffix(notePath: string) {
|
||||||
|
|||||||
@ -945,12 +945,26 @@ body.electron.background-effects.layout-horizontal .tab-row-container .toggle-bu
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
inset-inline-start: -10px;
|
inset-inline-start: -10px;
|
||||||
inset-inline-end: -10px;
|
inset-inline-end: -6px;
|
||||||
top: 32px;
|
top: 32px;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
|
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-left,
|
||||||
body.electron.background-effects.layout-horizontal .tab-row-container .tab-scroll-button-right {
|
body.electron.background-effects.layout-horizontal .tab-row-container .tab-scroll-button-right {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@ -2117,5 +2117,9 @@
|
|||||||
"unknown_http_error_title": "Communication error with the server",
|
"unknown_http_error_title": "Communication error with the server",
|
||||||
"unknown_http_error_content": "Status code: {{statusCode}}\nURL: {{method}} {{url}}\nMessage: {{message}}",
|
"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."
|
"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 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 { dynamicRequire, isElectron } from "../../services/utils";
|
||||||
import { LaunchBarActionButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
|
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 {
|
interface HistoryNavigationProps {
|
||||||
launcherNote: FNote;
|
launcherNote: FNote;
|
||||||
@ -16,26 +18,22 @@ const HISTORY_LIMIT = 20;
|
|||||||
|
|
||||||
export default function HistoryNavigationButton({ launcherNote, command }: HistoryNavigationProps) {
|
export default function HistoryNavigationButton({ launcherNote, command }: HistoryNavigationProps) {
|
||||||
const { icon, title } = useLauncherIconAndTitle(launcherNote);
|
const { icon, title } = useLauncherIconAndTitle(launcherNote);
|
||||||
const webContentsRef = useRef<WebContents>(null);
|
const webContents = useMemo(() => isElectron() ? dynamicRequire("@electron/remote").getCurrentWebContents() : undefined, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isElectron()) {
|
|
||||||
const webContents = dynamicRequire("@electron/remote").getCurrentWebContents();
|
|
||||||
// without this, the history is preserved across frontend reloads
|
|
||||||
webContents?.clearHistory();
|
|
||||||
webContentsRef.current = webContents;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LaunchBarActionButton
|
<LaunchBarActionButton
|
||||||
icon={icon}
|
icon={icon}
|
||||||
text={title}
|
text={title}
|
||||||
triggerCommand={command}
|
triggerCommand={command}
|
||||||
onContextMenu={async (e) => {
|
onContextMenu={webContents ? handleHistoryContextMenu(webContents) : undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleHistoryContextMenu(webContents: WebContents) {
|
||||||
|
return async (e: MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const webContents = webContentsRef.current;
|
|
||||||
if (!webContents || webContents.navigationHistory.length() < 2) {
|
if (!webContents || webContents.navigationHistory.length() < 2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -46,20 +44,19 @@ export default function HistoryNavigationButton({ launcherNote, command }: Histo
|
|||||||
const activeIndex = webContents.navigationHistory.getActiveIndex();
|
const activeIndex = webContents.navigationHistory.getActiveIndex();
|
||||||
|
|
||||||
for (const idx in history) {
|
for (const idx in history) {
|
||||||
const { notePath } = link.parseNavigationStateFromUrl(history[idx].url);
|
const { noteId, notePath } = link.parseNavigationStateFromUrl(history[idx].url);
|
||||||
if (!notePath) continue;
|
if (!noteId || !notePath) continue;
|
||||||
|
|
||||||
const title = await tree.getNotePathTitle(notePath);
|
const title = await tree.getNotePathTitle(notePath);
|
||||||
|
const index = parseInt(idx, 10);
|
||||||
|
const note = froca.getNoteFromCache(noteId);
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
title,
|
title,
|
||||||
command: idx,
|
command: idx,
|
||||||
uiIcon:
|
checked: index === activeIndex,
|
||||||
parseInt(idx) === activeIndex
|
enabled: index !== activeIndex,
|
||||||
? "bx bx-radio-circle-marked" // compare with type coercion!
|
uiIcon: note?.getIcon()
|
||||||
: parseInt(idx) < activeIndex
|
|
||||||
? "bx bx-left-arrow-alt"
|
|
||||||
: "bx bx-right-arrow-alt"
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +77,5 @@ export default function HistoryNavigationButton({ launcherNote, command }: Histo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
};
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -901,3 +901,27 @@ export function useChildNotes(parentNoteId: string | undefined) {
|
|||||||
|
|
||||||
return childNotes;
|
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 { NotePathRecord } from "../../entities/fnote";
|
||||||
import NoteLink from "../react/NoteLink";
|
import NoteLink from "../react/NoteLink";
|
||||||
import { joinElements } from "../react/react_utils";
|
import { joinElements } from "../react/react_utils";
|
||||||
|
import { NOTE_PATH_TITLE_SEPARATOR } from "../../services/tree";
|
||||||
|
|
||||||
export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabContext) {
|
export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabContext) {
|
||||||
const [ sortedNotePaths, setSortedNotePaths ] = useState<NotePathRecord[]>();
|
const [ sortedNotePaths, setSortedNotePaths ] = useState<NotePathRecord[]>();
|
||||||
@ -95,7 +96,7 @@ function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: strin
|
|||||||
<li class={classes}>
|
<li class={classes}>
|
||||||
{joinElements(fullNotePaths.map(notePath => (
|
{joinElements(fullNotePaths.map(notePath => (
|
||||||
<NoteLink notePath={notePath} noPreview />
|
<NoteLink notePath={notePath} noPreview />
|
||||||
)), " / ")}
|
)), NOTE_PATH_TITLE_SEPARATOR)}
|
||||||
|
|
||||||
{icons.map(({ icon, title }) => (
|
{icons.map(({ icon, title }) => (
|
||||||
<span class={icon} title={title} />
|
<span class={icon} title={title} />
|
||||||
|
|||||||
@ -36,10 +36,11 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
padding-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ribbon-tab-title .bx {
|
.ribbon-tab-title .bx {
|
||||||
font-size: 125%;
|
font-size: 150%;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user