mirror of
https://github.com/TriliumNext/Trilium.git
synced 2025-12-11 05:45:26 -06:00
Add breadcrumbs to navigation (#7995)
This commit is contained in:
commit
e688f2cdb6
@ -498,10 +498,6 @@ type EventMappings = {
|
|||||||
noteIds: string[];
|
noteIds: string[];
|
||||||
};
|
};
|
||||||
refreshData: { ntxId: string | null | undefined };
|
refreshData: { ntxId: string | null | undefined };
|
||||||
contentSafeMarginChanged: {
|
|
||||||
top: number;
|
|
||||||
noteContext: NoteContext;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EventListener<T extends EventNames> = {
|
export type EventListener<T extends EventNames> = {
|
||||||
|
|||||||
@ -44,6 +44,7 @@ import NoteDetail from "../widgets/NoteDetail.jsx";
|
|||||||
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
|
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";
|
||||||
|
|
||||||
export default class DesktopLayout {
|
export default class DesktopLayout {
|
||||||
|
|
||||||
@ -117,29 +118,37 @@ export default class DesktopLayout {
|
|||||||
new NoteWrapperWidget()
|
new NoteWrapperWidget()
|
||||||
.child(
|
.child(
|
||||||
new FlexContainer("row")
|
new FlexContainer("row")
|
||||||
.class("title-row")
|
.class("breadcrumb-row")
|
||||||
.css("height", "50px")
|
.css("height", "30px")
|
||||||
.css("min-height", "50px")
|
.css("min-height", "30px")
|
||||||
.css("align-items", "center")
|
.css("align-items", "center")
|
||||||
.cssBlock(".title-row > * { margin: 5px; }")
|
.css("padding", "10px")
|
||||||
.child(<NoteIconWidget />)
|
.cssBlock(".breadcrumb-row > * { margin: 5px; }")
|
||||||
.child(<NoteTitleWidget />)
|
.child(<Breadcrumb />)
|
||||||
.child(<SpacerWidget baseSize={0} growthFactor={1} />)
|
.child(<SpacerWidget baseSize={0} growthFactor={1} />)
|
||||||
.child(<MovePaneButton direction="left" />)
|
.child(<MovePaneButton direction="left" />)
|
||||||
.child(<MovePaneButton direction="right" />)
|
.child(<MovePaneButton direction="right" />)
|
||||||
.child(<ClosePaneButton />)
|
.child(<ClosePaneButton />)
|
||||||
.child(<CreatePaneButton />)
|
.child(<CreatePaneButton />)
|
||||||
)
|
)
|
||||||
.child(<Ribbon />)
|
|
||||||
.child(new WatchedFileUpdateStatusWidget())
|
.child(new WatchedFileUpdateStatusWidget())
|
||||||
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
|
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
|
||||||
.child(
|
.child(
|
||||||
new ScrollingContainer()
|
new ScrollingContainer()
|
||||||
.filling()
|
.filling()
|
||||||
.child(new ContentHeader()
|
.child(new ContentHeader()
|
||||||
|
.child(new FlexContainer("row")
|
||||||
|
.class("title-row")
|
||||||
|
.css("height", "50px")
|
||||||
|
.css("min-height", "50px")
|
||||||
|
.css("align-items", "center")
|
||||||
|
.child(<NoteIconWidget />)
|
||||||
|
.child(<NoteTitleWidget />)
|
||||||
|
)
|
||||||
.child(<ReadOnlyNoteInfoBar />)
|
.child(<ReadOnlyNoteInfoBar />)
|
||||||
.child(<SharedInfo />)
|
.child(<SharedInfo />)
|
||||||
)
|
)
|
||||||
|
.child(<Ribbon />)
|
||||||
.child(<PromotedAttributes />)
|
.child(<PromotedAttributes />)
|
||||||
.child(<SqlTableSchemas />)
|
.child(<SqlTableSchemas />)
|
||||||
.child(<NoteDetail />)
|
.child(<NoteDetail />)
|
||||||
|
|||||||
@ -99,7 +99,7 @@ async function createLink(notePath: string | undefined, options: CreateLinkOptio
|
|||||||
const viewMode = viewScope.viewMode || "default";
|
const viewMode = viewScope.viewMode || "default";
|
||||||
let linkTitle = options.title;
|
let linkTitle = options.title;
|
||||||
|
|
||||||
if (!linkTitle) {
|
if (linkTitle === undefined) {
|
||||||
if (viewMode === "attachments" && viewScope.attachmentId) {
|
if (viewMode === "attachments" && viewScope.attachmentId) {
|
||||||
const attachment = await froca.getAttachment(viewScope.attachmentId);
|
const attachment = await froca.getAttachment(viewScope.attachmentId);
|
||||||
|
|
||||||
|
|||||||
@ -24,8 +24,8 @@
|
|||||||
--bs-body-font-family: var(--main-font-family) !important;
|
--bs-body-font-family: var(--main-font-family) !important;
|
||||||
--bs-body-font-weight: var(--main-font-weight) !important;
|
--bs-body-font-weight: var(--main-font-weight) !important;
|
||||||
--bs-body-color: var(--main-text-color) !important;
|
--bs-body-color: var(--main-text-color) !important;
|
||||||
--bs-body-bg: var(--main-background-color) !important;
|
--bs-body-bg: var(--main-background-color) !important;
|
||||||
--ck-mention-list-max-height: 500px;
|
--ck-mention-list-max-height: 500px;
|
||||||
--tn-modal-max-height: 90vh;
|
--tn-modal-max-height: 90vh;
|
||||||
|
|
||||||
--tree-item-light-theme-max-color-lightness: 50;
|
--tree-item-light-theme-max-color-lightness: 50;
|
||||||
@ -471,7 +471,7 @@ body.mobile .dropdown .dropdown-submenu > span {
|
|||||||
padding-inline-start: 12px;
|
padding-inline-start: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu kbd {
|
.dropdown-menu kbd {
|
||||||
color: var(--muted-text-color);
|
color: var(--muted-text-color);
|
||||||
border: none;
|
border: none;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@ -487,7 +487,7 @@ body.mobile .dropdown .dropdown-submenu > span {
|
|||||||
border: 1px solid transparent !important;
|
border: 1px solid transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* This is a workaround for Firefox not supporting break-before / break-after: avoid on columns.
|
/* This is a workaround for Firefox not supporting break-before / break-after: avoid on columns.
|
||||||
* It usually wraps a menu item followed by a separator / header and another menu item. */
|
* It usually wraps a menu item followed by a separator / header and another menu item. */
|
||||||
.dropdown-no-break {
|
.dropdown-no-break {
|
||||||
break-inside: avoid;
|
break-inside: avoid;
|
||||||
@ -1591,7 +1591,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
|||||||
inset-inline-start: 0;
|
inset-inline-start: 0;
|
||||||
inset-inline-end: 0;
|
inset-inline-end: 0;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
max-height: 85vh;
|
max-height: 85vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2093,7 +2093,7 @@ body.zen .note-split.type-text .scrolling-container {
|
|||||||
|
|
||||||
body.zen:not(.backdrop-effects-disabled) .note-split.type-text .scrolling-container {
|
body.zen:not(.backdrop-effects-disabled) .note-split.type-text .scrolling-container {
|
||||||
--padding-top: 50px; /* Should be enough to cover the title row */
|
--padding-top: 50px; /* Should be enough to cover the title row */
|
||||||
|
|
||||||
padding-top: var(--padding-top);
|
padding-top: var(--padding-top);
|
||||||
scroll-padding-top: var(--padding-top);
|
scroll-padding-top: var(--padding-top);
|
||||||
}
|
}
|
||||||
@ -2365,7 +2365,7 @@ footer.webview-footer button {
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admonition::before {
|
.admonition::before {
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
font-family: boxicons !important;
|
font-family: boxicons !important;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -2391,7 +2391,7 @@ footer.webview-footer button {
|
|||||||
|
|
||||||
.ck-content ul.todo-list li:has(> span.todo-list__label input[type="checkbox"]:checked) > span.todo-list__label span.todo-list__label__description {
|
.ck-content ul.todo-list li:has(> span.todo-list__label input[type="checkbox"]:checked) > span.todo-list__label span.todo-list__label__description {
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-options-container {
|
.chat-options-container {
|
||||||
@ -2524,6 +2524,7 @@ iframe.print-iframe {
|
|||||||
position: relative;
|
position: relative;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Calendar collection */
|
/* Calendar collection */
|
||||||
@ -2538,7 +2539,7 @@ iframe.print-iframe {
|
|||||||
body.mobile {
|
body.mobile {
|
||||||
.split-note-container-widget {
|
.split-note-container-widget {
|
||||||
flex-direction: column !important;
|
flex-direction: column !important;
|
||||||
|
|
||||||
.note-split {
|
.note-split {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@ -2553,4 +2554,4 @@ iframe.print-iframe {
|
|||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -164,7 +164,7 @@ ul.editability-dropdown li.dropdown-item > div {
|
|||||||
background: var(--cmd-button-hover-background-color);
|
background: var(--cmd-button-hover-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Note info
|
* Note info
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -177,4 +177,16 @@ ul.editability-dropdown li.dropdown-item > div {
|
|||||||
/* Narrow width layout */
|
/* Narrow width layout */
|
||||||
.note-info-widget {
|
.note-info-widget {
|
||||||
container: info-section / inline-size;
|
container: info-section / inline-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Styling as a floating toolbar
|
||||||
|
*/
|
||||||
|
.ribbon-container {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--main-background-color);
|
||||||
|
z-index: 997;
|
||||||
|
}
|
||||||
|
|||||||
55
apps/client/src/widgets/Breadcrumb.css
Normal file
55
apps/client/src/widgets/Breadcrumb.css
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
.breadcrumb-row {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.component.breadcrumb {
|
||||||
|
contain: none;
|
||||||
|
display: flex;
|
||||||
|
margin: 0;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.9em;
|
||||||
|
gap: 0.25em;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 85%;
|
||||||
|
|
||||||
|
> span,
|
||||||
|
> span > span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 150px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
display: block;
|
||||||
|
flex-shrink: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> span:last-of-type a {
|
||||||
|
max-width: 300px;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
flex-direction: column;
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item span,
|
||||||
|
.dropdown-item strong {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
display: block;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
166
apps/client/src/widgets/Breadcrumb.tsx
Normal file
166
apps/client/src/widgets/Breadcrumb.tsx
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import "./Breadcrumb.css";
|
||||||
|
|
||||||
|
import { useMemo } from "preact/hooks";
|
||||||
|
import { Fragment } from "preact/jsx-runtime";
|
||||||
|
|
||||||
|
import NoteContext from "../components/note_context";
|
||||||
|
import froca from "../services/froca";
|
||||||
|
import ActionButton from "./react/ActionButton";
|
||||||
|
import Dropdown from "./react/Dropdown";
|
||||||
|
import { FormListItem } from "./react/FormList";
|
||||||
|
import { useChildNotes, useNoteContext, useNoteLabel, useNoteProperty } from "./react/hooks";
|
||||||
|
import Icon from "./react/Icon";
|
||||||
|
import NoteLink from "./react/NoteLink";
|
||||||
|
import link_context_menu from "../menus/link_context_menu";
|
||||||
|
|
||||||
|
const COLLAPSE_THRESHOLD = 5;
|
||||||
|
const INITIAL_ITEMS = 2;
|
||||||
|
const FINAL_ITEMS = 2;
|
||||||
|
|
||||||
|
export default function Breadcrumb() {
|
||||||
|
const { note, noteContext } = useNoteContext();
|
||||||
|
const notePath = buildNotePaths(noteContext?.notePathArray);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="breadcrumb">
|
||||||
|
{notePath.length > COLLAPSE_THRESHOLD ? (
|
||||||
|
<>
|
||||||
|
{notePath.slice(0, INITIAL_ITEMS).map((item, index) => (
|
||||||
|
<Fragment key={item}>
|
||||||
|
{index === 0
|
||||||
|
? <BreadcrumbRoot noteContext={noteContext} />
|
||||||
|
: <BreadcrumbItem notePath={item} />
|
||||||
|
}
|
||||||
|
<BreadcrumbSeparator notePath={item} activeNotePath={notePath[index + 1]} noteContext={noteContext} />
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
<BreadcrumbCollapsed items={notePath.slice(INITIAL_ITEMS, -FINAL_ITEMS)} noteContext={noteContext} />
|
||||||
|
{notePath.slice(-FINAL_ITEMS).map((item, index) => (
|
||||||
|
<Fragment key={item}>
|
||||||
|
<BreadcrumbSeparator notePath={notePath[notePath.length - FINAL_ITEMS - (1 - index)]} activeNotePath={item} noteContext={noteContext} />
|
||||||
|
<BreadcrumbItem notePath={item} />
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
notePath.map((item, index) => (
|
||||||
|
<Fragment key={item}>
|
||||||
|
{index === 0
|
||||||
|
? <BreadcrumbRoot noteContext={noteContext} />
|
||||||
|
: <BreadcrumbItem notePath={item} />
|
||||||
|
}
|
||||||
|
{(index < notePath.length - 1 || note?.hasChildren()) &&
|
||||||
|
<BreadcrumbSeparator notePath={item} activeNotePath={notePath[index + 1]} noteContext={noteContext} />}
|
||||||
|
</Fragment>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbRoot({ noteContext }: { noteContext: NoteContext | undefined }) {
|
||||||
|
const note = useMemo(() => froca.getNoteFromCache("root"), []);
|
||||||
|
useNoteLabel(note, "iconClass");
|
||||||
|
const title = useNoteProperty(note, "title");
|
||||||
|
|
||||||
|
return (note &&
|
||||||
|
<ActionButton
|
||||||
|
icon={note.getIcon()}
|
||||||
|
text={title ?? ""}
|
||||||
|
onClick={() => noteContext?.setNote("root")}
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
link_context_menu.openContextMenu(note.noteId, e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbItem({ notePath }: { notePath: string }) {
|
||||||
|
return (
|
||||||
|
<NoteLink
|
||||||
|
notePath={notePath}
|
||||||
|
noPreview
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbSeparator({ notePath, noteContext, activeNotePath }: { notePath: string, activeNotePath: string, noteContext: NoteContext | undefined }) {
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
text={<Icon icon="bx bx-chevron-right" />}
|
||||||
|
noSelectButtonStyle
|
||||||
|
buttonClassName="icon-action"
|
||||||
|
hideToggleArrow
|
||||||
|
dropdownOptions={{ popperConfig: { strategy: "fixed" } }}
|
||||||
|
>
|
||||||
|
<BreadcrumbSeparatorDropdownContent notePath={notePath} noteContext={noteContext} activeNotePath={activeNotePath} />
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbSeparatorDropdownContent({ notePath, noteContext, activeNotePath }: { notePath: string, activeNotePath: string, noteContext: NoteContext | undefined }) {
|
||||||
|
const notePathComponents = notePath.split("/");
|
||||||
|
const parentNoteId = notePathComponents.at(-1);
|
||||||
|
const childNotes = useChildNotes(parentNoteId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="breadcrumb-child-list">
|
||||||
|
{childNotes.map((note) => {
|
||||||
|
const childNotePath = `${notePath}/${note.noteId}`;
|
||||||
|
return <li key={note.noteId}>
|
||||||
|
<FormListItem
|
||||||
|
icon={note.getIcon()}
|
||||||
|
onClick={() => noteContext?.setNote(childNotePath)}
|
||||||
|
>
|
||||||
|
{childNotePath !== activeNotePath
|
||||||
|
? <span>{note.title}</span>
|
||||||
|
: <strong>{note.title}</strong>}
|
||||||
|
</FormListItem>
|
||||||
|
</li>;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbCollapsed({ items, noteContext }: { items: string[], noteContext: NoteContext | undefined }) {
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
text={<Icon icon="bx bx-dots-horizontal-rounded" />}
|
||||||
|
noSelectButtonStyle
|
||||||
|
buttonClassName="icon-action"
|
||||||
|
hideToggleArrow
|
||||||
|
dropdownOptions={{ popperConfig: { strategy: "fixed" } }}
|
||||||
|
>
|
||||||
|
<ul className="breadcrumb-child-list">
|
||||||
|
{items.map((notePath) => {
|
||||||
|
const notePathComponents = notePath.split("/");
|
||||||
|
const noteId = notePathComponents[notePathComponents.length - 1];
|
||||||
|
const note = froca.getNoteFromCache(noteId);
|
||||||
|
if (!note) return null;
|
||||||
|
|
||||||
|
return <li key={note.noteId}>
|
||||||
|
<FormListItem
|
||||||
|
icon={note.getIcon()}
|
||||||
|
onClick={() => noteContext?.setNote(notePath)}
|
||||||
|
>
|
||||||
|
<span>{note.title}</span>
|
||||||
|
</FormListItem>
|
||||||
|
</li>;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildNotePaths(notePathArray: string[] | undefined) {
|
||||||
|
if (!notePathArray) return [];
|
||||||
|
|
||||||
|
let prefix = "";
|
||||||
|
const output: string[] = [];
|
||||||
|
for (const notePath of notePathArray) {
|
||||||
|
output.push(`${prefix}${notePath}`);
|
||||||
|
prefix += `${notePath}/`;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
@ -6,11 +6,12 @@
|
|||||||
.floating-buttons-children,
|
.floating-buttons-children,
|
||||||
.show-floating-buttons {
|
.show-floating-buttons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: var(--floating-buttons-vert-offset, 14px);
|
top: calc(var(--floating-buttons-vert-offset, 14px) + var(--ribbon-height, 0px) + var(--content-header-height, 0px));
|
||||||
inset-inline-end: var(--floating-buttons-horiz-offset, 10px);
|
inset-inline-end: var(--floating-buttons-horiz-offset, 10px);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
transition: top 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-split.rtl .floating-buttons-children,
|
.note-split.rtl .floating-buttons-children,
|
||||||
|
|||||||
@ -48,12 +48,6 @@ export default function FloatingButtons({ items }: FloatingButtonsProps) {
|
|||||||
const [ visible, setVisible ] = useState(true);
|
const [ visible, setVisible ] = useState(true);
|
||||||
useEffect(() => setVisible(true), [ note ]);
|
useEffect(() => setVisible(true), [ note ]);
|
||||||
|
|
||||||
useTriliumEvent("contentSafeMarginChanged", (e) => {
|
|
||||||
if (e.noteContext === noteContext) {
|
|
||||||
setTop(e.top);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="floating-buttons no-print" style={{top}}>
|
<div className="floating-buttons no-print" style={{top}}>
|
||||||
<div className={`floating-buttons-children ${!visible ? "temporarily-hidden" : ""}`}>
|
<div className={`floating-buttons-children ${!visible ? "temporarily-hidden" : ""}`}>
|
||||||
@ -93,9 +87,9 @@ function CloseFloatingButton({ setVisible }: { setVisible(visible: boolean): voi
|
|||||||
className="close-floating-buttons-button"
|
className="close-floating-buttons-button"
|
||||||
icon="bx bx-chevrons-right"
|
icon="bx bx-chevrons-right"
|
||||||
text={t("hide_floating_buttons_button.button_title")}
|
text={t("hide_floating_buttons_button.button_title")}
|
||||||
onClick={() => setVisible(false)}
|
onClick={() => setVisible(false)}
|
||||||
noIconActionClass
|
noIconActionClass
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,9 +2,13 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1em;
|
top: 1em;
|
||||||
right: 1em;
|
right: 1em;
|
||||||
|
|
||||||
|
.floating-buttons-children {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.presentation-container {
|
.presentation-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
11
apps/client/src/widgets/containers/content_header.css
Normal file
11
apps/client/src/widgets/containers/content_header.css
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.content-header-widget {
|
||||||
|
position: relative;
|
||||||
|
z-index: 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header-widget.floating {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 11;
|
||||||
|
background-color: var(--main-background-color, #fff);
|
||||||
|
}
|
||||||
@ -2,15 +2,19 @@ import { EventData } from "../../components/app_context";
|
|||||||
import BasicWidget from "../basic_widget";
|
import BasicWidget from "../basic_widget";
|
||||||
import Container from "./container";
|
import Container from "./container";
|
||||||
import NoteContext from "../../components/note_context";
|
import NoteContext from "../../components/note_context";
|
||||||
|
import "./content_header.css";
|
||||||
|
|
||||||
export default class ContentHeader extends Container<BasicWidget> {
|
export default class ContentHeader extends Container<BasicWidget> {
|
||||||
|
|
||||||
noteContext?: NoteContext;
|
noteContext?: NoteContext;
|
||||||
thisElement?: HTMLElement;
|
thisElement?: HTMLElement;
|
||||||
parentElement?: HTMLElement;
|
parentElement?: HTMLElement;
|
||||||
resizeObserver: ResizeObserver;
|
resizeObserver: ResizeObserver;
|
||||||
currentHeight: number = 0;
|
currentHeight: number = 0;
|
||||||
currentSafeMargin: number = NaN;
|
currentSafeMargin: number = NaN;
|
||||||
|
previousScrollTop: number = 0;
|
||||||
|
isFloating: boolean = false;
|
||||||
|
scrollThreshold: number = 10; // pixels before triggering float
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@ -35,19 +39,39 @@ export default class ContentHeader extends Container<BasicWidget> {
|
|||||||
this.thisElement = this.$widget.get(0)!;
|
this.thisElement = this.$widget.get(0)!;
|
||||||
|
|
||||||
this.resizeObserver.observe(this.thisElement);
|
this.resizeObserver.observe(this.thisElement);
|
||||||
this.parentElement.addEventListener("scroll", this.updateSafeMargin.bind(this));
|
this.parentElement.addEventListener("scroll", this.updateScrollState.bind(this), { passive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateScrollState() {
|
||||||
|
const currentScrollTop = this.parentElement!.scrollTop;
|
||||||
|
const isScrollingUp = currentScrollTop < this.previousScrollTop;
|
||||||
|
const hasMovedEnough = Math.abs(currentScrollTop - this.previousScrollTop) > this.scrollThreshold;
|
||||||
|
|
||||||
|
if (hasMovedEnough) {
|
||||||
|
this.setFloating(isScrollingUp);
|
||||||
|
}
|
||||||
|
this.previousScrollTop = currentScrollTop;
|
||||||
|
this.updateSafeMargin();
|
||||||
|
}
|
||||||
|
|
||||||
|
setFloating(shouldFloat: boolean) {
|
||||||
|
if (shouldFloat !== this.isFloating) {
|
||||||
|
this.isFloating = shouldFloat;
|
||||||
|
|
||||||
|
if (shouldFloat) {
|
||||||
|
this.$widget.addClass("floating");
|
||||||
|
} else {
|
||||||
|
this.$widget.removeClass("floating");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSafeMargin() {
|
updateSafeMargin() {
|
||||||
const newSafeMargin = Math.max(this.currentHeight - this.parentElement!.scrollTop, 0);
|
const parentEl = this.parentElement?.closest<HTMLDivElement>(".note-split");
|
||||||
|
if (this.isFloating || this.parentElement!.scrollTop === 0) {
|
||||||
if (newSafeMargin !== this.currentSafeMargin) {
|
parentEl!.style.setProperty("--content-header-height", `${this.currentHeight}px`);
|
||||||
this.currentSafeMargin = newSafeMargin;
|
} else {
|
||||||
|
parentEl!.style.removeProperty("--content-header-height");
|
||||||
this.triggerEvent("contentSafeMarginChanged", {
|
|
||||||
top: newSafeMargin,
|
|
||||||
noteContext: this.noteContext!
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,4 +84,4 @@ export default class ContentHeader extends Container<BasicWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
.note-icon-widget {
|
.note-icon-widget {
|
||||||
padding-inline-start: 7px;
|
padding-inline-start: 10px;
|
||||||
margin-inline-end: 0;
|
margin-inline-end: 0;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
@ -13,7 +13,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--muted-text-color);
|
color: var(--muted-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-icon-widget button.note-icon:disabled {
|
.note-icon-widget button.note-icon:disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
opacity: .75;
|
opacity: .75;
|
||||||
@ -68,4 +68,4 @@
|
|||||||
border: 1px dashed var(--muted-text-color);
|
border: 1px dashed var(--muted-text-color);
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,4 +27,4 @@ body.mobile .note-title-widget input.note-title {
|
|||||||
|
|
||||||
body.desktop .note-title-widget input.note-title {
|
body.desktop .note-title-widget input.note-title {
|
||||||
font-size: 180%;
|
font-size: 180%;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,6 @@
|
|||||||
.info-bar-subtle {
|
.info-bar-subtle {
|
||||||
color: var(--muted-text-color);
|
color: var(--muted-text-color);
|
||||||
background: var(--main-background-color);
|
background: var(--main-background-color);
|
||||||
border-bottom: 1px solid var(--main-border-color);
|
|
||||||
margin-block: 0;
|
margin-block: 0;
|
||||||
padding-inline: 22px;
|
padding-inline: 22px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -886,12 +886,15 @@ async function isNoteReadOnly(note: FNote, noteContext: NoteContext) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useChildNotes(parentNoteId: string) {
|
export function useChildNotes(parentNoteId: string | undefined) {
|
||||||
const [ childNotes, setChildNotes ] = useState<FNote[]>([]);
|
const [ childNotes, setChildNotes ] = useState<FNote[]>([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async function() {
|
(async function() {
|
||||||
const parentNote = await froca.getNote(parentNoteId);
|
let childNotes: FNote[] | undefined;
|
||||||
const childNotes = await parentNote?.getChildNotes();
|
if (parentNoteId) {
|
||||||
|
const parentNote = await froca.getNote(parentNoteId);
|
||||||
|
childNotes = await parentNote?.getChildNotes();
|
||||||
|
}
|
||||||
setChildNotes(childNotes ?? []);
|
setChildNotes(childNotes ?? []);
|
||||||
})();
|
})();
|
||||||
}, [ parentNoteId ]);
|
}, [ parentNoteId ]);
|
||||||
|
|||||||
@ -44,7 +44,7 @@ export function disposeReactWidget(container: Element) {
|
|||||||
render(null, container);
|
render(null, container);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function joinElements(components: ComponentChild[] | undefined, separator = ", ") {
|
export function joinElements(components: ComponentChild[] | undefined, separator: ComponentChild = ", ") {
|
||||||
if (!components) return <></>;
|
if (!components) return <></>;
|
||||||
|
|
||||||
const joinedComponents: ComponentChild[] = [];
|
const joinedComponents: ComponentChild[] = [];
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||||
import { useNoteContext, useNoteProperty, useStaticTooltipWithKeyboardShortcut, useTriliumEvents } from "../react/hooks";
|
import { useElementSize, useNoteContext, useNoteProperty, useStaticTooltipWithKeyboardShortcut, useTriliumEvents } from "../react/hooks";
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
|
|
||||||
import { Indexed, numberObjectsInPlace } from "../../services/utils";
|
import { Indexed, numberObjectsInPlace } from "../../services/utils";
|
||||||
@ -42,6 +42,16 @@ export default function Ribbon() {
|
|||||||
refresh();
|
refresh();
|
||||||
}, [ note, noteType, isReadOnlyTemporarilyDisabled ]);
|
}, [ note, noteType, isReadOnlyTemporarilyDisabled ]);
|
||||||
|
|
||||||
|
// Manage height.
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const size = useElementSize(containerRef);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current || !size) return;
|
||||||
|
const parentEl = containerRef.current.closest<HTMLDivElement>(".note-split");
|
||||||
|
if (!parentEl) return;
|
||||||
|
parentEl.style.setProperty("--ribbon-height", `${size.height}px`);
|
||||||
|
}, [ size ]);
|
||||||
|
|
||||||
// Automatically activate the first ribbon tab that needs to be activated whenever a note changes.
|
// Automatically activate the first ribbon tab that needs to be activated whenever a note changes.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!computedTabs) return;
|
if (!computedTabs) return;
|
||||||
@ -65,6 +75,7 @@ export default function Ribbon() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={containerRef}
|
||||||
className={clsx("ribbon-container", noteContext?.viewScope?.viewMode !== "default" && "hidden-ext")}
|
className={clsx("ribbon-container", noteContext?.viewScope?.viewMode !== "default" && "hidden-ext")}
|
||||||
style={{ contain: "none" }}
|
style={{ contain: "none" }}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,5 +1,14 @@
|
|||||||
.ribbon-container {
|
.ribbon-container {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When content header is floating, ribbon sticks below it */
|
||||||
|
.scrolling-container:has(.content-header-widget.floating) .ribbon-container {
|
||||||
|
position: sticky;
|
||||||
|
top: var(--content-header-height, 100px);
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ribbon-top-row {
|
.ribbon-top-row {
|
||||||
@ -24,12 +33,14 @@
|
|||||||
max-width: max-content;
|
max-width: max-content;
|
||||||
flex-grow: 10;
|
flex-grow: 10;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ribbon-tab-title .bx {
|
.ribbon-tab-title .bx {
|
||||||
font-size: 150%;
|
font-size: 125%;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 3px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ribbon-tab-title.active {
|
.ribbon-tab-title.active {
|
||||||
@ -71,12 +82,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
border-bottom: 1px solid var(--main-border-color);
|
border-bottom: 1px solid var(--main-border-color);
|
||||||
margin-inline-end: 5px;
|
margin-inline-end: 5px;
|
||||||
}
|
align-items: center;
|
||||||
|
height: 36px;
|
||||||
.ribbon-button-container > * {
|
gap: 10px;
|
||||||
position: relative;
|
|
||||||
top: -3px;
|
|
||||||
margin-inline-start: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ribbon-body {
|
.ribbon-body {
|
||||||
@ -386,6 +394,8 @@ body[dir=rtl] .attribute-list-editor {
|
|||||||
.note-actions {
|
.note-actions {
|
||||||
width: 35px;
|
width: 35px;
|
||||||
height: 35px;
|
height: 35px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-actions .dropdown-menu {
|
.note-actions .dropdown-menu {
|
||||||
@ -404,4 +414,4 @@ body[dir=rtl] .attribute-list-editor {
|
|||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
pointer-events: none; /* makes it unclickable */
|
pointer-events: none; /* makes it unclickable */
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|||||||
@ -63,7 +63,7 @@ const mainConfig = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ["**/*.{js,ts,mjs,cjs}"],
|
files: ["**/*.{js,ts,mjs,cjs,tsx}"],
|
||||||
|
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
parser: tsParser
|
parser: tsParser
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user