mirror of
https://github.com/TriliumNext/Trilium.git
synced 2025-12-10 03:53:37 -06:00
Add breadcrumbs to navigation (#7995)
This commit is contained in:
commit
e688f2cdb6
@ -498,10 +498,6 @@ type EventMappings = {
|
||||
noteIds: string[];
|
||||
};
|
||||
refreshData: { ntxId: string | null | undefined };
|
||||
contentSafeMarginChanged: {
|
||||
top: number;
|
||||
noteContext: NoteContext;
|
||||
}
|
||||
};
|
||||
|
||||
export type EventListener<T extends EventNames> = {
|
||||
|
||||
@ -44,6 +44,7 @@ import NoteDetail from "../widgets/NoteDetail.jsx";
|
||||
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";
|
||||
|
||||
export default class DesktopLayout {
|
||||
|
||||
@ -117,29 +118,37 @@ export default class DesktopLayout {
|
||||
new NoteWrapperWidget()
|
||||
.child(
|
||||
new FlexContainer("row")
|
||||
.class("title-row")
|
||||
.css("height", "50px")
|
||||
.css("min-height", "50px")
|
||||
.class("breadcrumb-row")
|
||||
.css("height", "30px")
|
||||
.css("min-height", "30px")
|
||||
.css("align-items", "center")
|
||||
.cssBlock(".title-row > * { margin: 5px; }")
|
||||
.child(<NoteIconWidget />)
|
||||
.child(<NoteTitleWidget />)
|
||||
.css("padding", "10px")
|
||||
.cssBlock(".breadcrumb-row > * { margin: 5px; }")
|
||||
.child(<Breadcrumb />)
|
||||
.child(<SpacerWidget baseSize={0} growthFactor={1} />)
|
||||
.child(<MovePaneButton direction="left" />)
|
||||
.child(<MovePaneButton direction="right" />)
|
||||
.child(<ClosePaneButton />)
|
||||
.child(<CreatePaneButton />)
|
||||
)
|
||||
.child(<Ribbon />)
|
||||
.child(new WatchedFileUpdateStatusWidget())
|
||||
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
|
||||
.child(
|
||||
new ScrollingContainer()
|
||||
.filling()
|
||||
.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(<SharedInfo />)
|
||||
)
|
||||
.child(<Ribbon />)
|
||||
.child(<PromotedAttributes />)
|
||||
.child(<SqlTableSchemas />)
|
||||
.child(<NoteDetail />)
|
||||
|
||||
@ -99,7 +99,7 @@ async function createLink(notePath: string | undefined, options: CreateLinkOptio
|
||||
const viewMode = viewScope.viewMode || "default";
|
||||
let linkTitle = options.title;
|
||||
|
||||
if (!linkTitle) {
|
||||
if (linkTitle === undefined) {
|
||||
if (viewMode === "attachments" && 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-weight: var(--main-font-weight) !important;
|
||||
--bs-body-color: var(--main-text-color) !important;
|
||||
--bs-body-bg: var(--main-background-color) !important;
|
||||
--ck-mention-list-max-height: 500px;
|
||||
--bs-body-bg: var(--main-background-color) !important;
|
||||
--ck-mention-list-max-height: 500px;
|
||||
--tn-modal-max-height: 90vh;
|
||||
|
||||
--tree-item-light-theme-max-color-lightness: 50;
|
||||
@ -471,7 +471,7 @@ body.mobile .dropdown .dropdown-submenu > span {
|
||||
padding-inline-start: 12px;
|
||||
}
|
||||
|
||||
.dropdown-menu kbd {
|
||||
.dropdown-menu kbd {
|
||||
color: var(--muted-text-color);
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
@ -487,7 +487,7 @@ body.mobile .dropdown .dropdown-submenu > span {
|
||||
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. */
|
||||
.dropdown-no-break {
|
||||
break-inside: avoid;
|
||||
@ -1591,7 +1591,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
margin: 0 !important;
|
||||
max-height: 85vh;
|
||||
max-height: 85vh;
|
||||
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 {
|
||||
--padding-top: 50px; /* Should be enough to cover the title row */
|
||||
|
||||
|
||||
padding-top: var(--padding-top);
|
||||
scroll-padding-top: var(--padding-top);
|
||||
}
|
||||
@ -2365,7 +2365,7 @@ footer.webview-footer button {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.admonition::before {
|
||||
.admonition::before {
|
||||
color: var(--accent-color);
|
||||
font-family: boxicons !important;
|
||||
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 {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.6;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.chat-options-container {
|
||||
@ -2524,6 +2524,7 @@ iframe.print-iframe {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Calendar collection */
|
||||
@ -2538,7 +2539,7 @@ iframe.print-iframe {
|
||||
body.mobile {
|
||||
.split-note-container-widget {
|
||||
flex-direction: column !important;
|
||||
|
||||
|
||||
.note-split {
|
||||
width: 100%;
|
||||
}
|
||||
@ -2553,4 +2554,4 @@ iframe.print-iframe {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -164,7 +164,7 @@ ul.editability-dropdown li.dropdown-item > div {
|
||||
background: var(--cmd-button-hover-background-color);
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* Note info
|
||||
*/
|
||||
|
||||
@ -177,4 +177,16 @@ ul.editability-dropdown li.dropdown-item > div {
|
||||
/* Narrow width layout */
|
||||
.note-info-widget {
|
||||
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,
|
||||
.show-floating-buttons {
|
||||
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);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
z-index: 100;
|
||||
transition: top 0.3s ease;
|
||||
}
|
||||
|
||||
.note-split.rtl .floating-buttons-children,
|
||||
|
||||
@ -48,12 +48,6 @@ export default function FloatingButtons({ items }: FloatingButtonsProps) {
|
||||
const [ visible, setVisible ] = useState(true);
|
||||
useEffect(() => setVisible(true), [ note ]);
|
||||
|
||||
useTriliumEvent("contentSafeMarginChanged", (e) => {
|
||||
if (e.noteContext === noteContext) {
|
||||
setTop(e.top);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="floating-buttons no-print" style={{top}}>
|
||||
<div className={`floating-buttons-children ${!visible ? "temporarily-hidden" : ""}`}>
|
||||
@ -93,9 +87,9 @@ function CloseFloatingButton({ setVisible }: { setVisible(visible: boolean): voi
|
||||
className="close-floating-buttons-button"
|
||||
icon="bx bx-chevrons-right"
|
||||
text={t("hide_floating_buttons_button.button_title")}
|
||||
onClick={() => setVisible(false)}
|
||||
onClick={() => setVisible(false)}
|
||||
noIconActionClass
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,9 +2,13 @@
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: 1em;
|
||||
|
||||
.floating-buttons-children {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.presentation-container {
|
||||
width: 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 Container from "./container";
|
||||
import NoteContext from "../../components/note_context";
|
||||
import "./content_header.css";
|
||||
|
||||
export default class ContentHeader extends Container<BasicWidget> {
|
||||
|
||||
|
||||
noteContext?: NoteContext;
|
||||
thisElement?: HTMLElement;
|
||||
parentElement?: HTMLElement;
|
||||
resizeObserver: ResizeObserver;
|
||||
currentHeight: number = 0;
|
||||
currentSafeMargin: number = NaN;
|
||||
previousScrollTop: number = 0;
|
||||
isFloating: boolean = false;
|
||||
scrollThreshold: number = 10; // pixels before triggering float
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@ -35,19 +39,39 @@ export default class ContentHeader extends Container<BasicWidget> {
|
||||
this.thisElement = this.$widget.get(0)!;
|
||||
|
||||
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() {
|
||||
const newSafeMargin = Math.max(this.currentHeight - this.parentElement!.scrollTop, 0);
|
||||
|
||||
if (newSafeMargin !== this.currentSafeMargin) {
|
||||
this.currentSafeMargin = newSafeMargin;
|
||||
|
||||
this.triggerEvent("contentSafeMarginChanged", {
|
||||
top: newSafeMargin,
|
||||
noteContext: this.noteContext!
|
||||
});
|
||||
const parentEl = this.parentElement?.closest<HTMLDivElement>(".note-split");
|
||||
if (this.isFloating || this.parentElement!.scrollTop === 0) {
|
||||
parentEl!.style.setProperty("--content-header-height", `${this.currentHeight}px`);
|
||||
} else {
|
||||
parentEl!.style.removeProperty("--content-header-height");
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,4 +84,4 @@ export default class ContentHeader extends Container<BasicWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
.note-icon-widget {
|
||||
padding-inline-start: 7px;
|
||||
padding-inline-start: 10px;
|
||||
margin-inline-end: 0;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
@ -13,7 +13,7 @@
|
||||
cursor: pointer;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
|
||||
.note-icon-widget button.note-icon:disabled {
|
||||
cursor: default;
|
||||
opacity: .75;
|
||||
@ -68,4 +68,4 @@
|
||||
border: 1px dashed var(--muted-text-color);
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,4 +27,4 @@ body.mobile .note-title-widget input.note-title {
|
||||
|
||||
body.desktop .note-title-widget input.note-title {
|
||||
font-size: 180%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,7 +17,6 @@
|
||||
.info-bar-subtle {
|
||||
color: var(--muted-text-color);
|
||||
background: var(--main-background-color);
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
margin-block: 0;
|
||||
padding-inline: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -886,12 +886,15 @@ async function isNoteReadOnly(note: FNote, noteContext: NoteContext) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function useChildNotes(parentNoteId: string) {
|
||||
export function useChildNotes(parentNoteId: string | undefined) {
|
||||
const [ childNotes, setChildNotes ] = useState<FNote[]>([]);
|
||||
useEffect(() => {
|
||||
(async function() {
|
||||
const parentNote = await froca.getNote(parentNoteId);
|
||||
const childNotes = await parentNote?.getChildNotes();
|
||||
let childNotes: FNote[] | undefined;
|
||||
if (parentNoteId) {
|
||||
const parentNote = await froca.getNote(parentNoteId);
|
||||
childNotes = await parentNote?.getChildNotes();
|
||||
}
|
||||
setChildNotes(childNotes ?? []);
|
||||
})();
|
||||
}, [ parentNoteId ]);
|
||||
|
||||
@ -44,7 +44,7 @@ export function disposeReactWidget(container: Element) {
|
||||
render(null, container);
|
||||
}
|
||||
|
||||
export function joinElements(components: ComponentChild[] | undefined, separator = ", ") {
|
||||
export function joinElements(components: ComponentChild[] | undefined, separator: ComponentChild = ", ") {
|
||||
if (!components) return <></>;
|
||||
|
||||
const joinedComponents: ComponentChild[] = [];
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 { Indexed, numberObjectsInPlace } from "../../services/utils";
|
||||
@ -42,6 +42,16 @@ export default function Ribbon() {
|
||||
refresh();
|
||||
}, [ 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.
|
||||
useEffect(() => {
|
||||
if (!computedTabs) return;
|
||||
@ -65,6 +75,7 @@ export default function Ribbon() {
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx("ribbon-container", noteContext?.viewScope?.viewMode !== "default" && "hidden-ext")}
|
||||
style={{ contain: "none" }}
|
||||
>
|
||||
|
||||
@ -1,5 +1,14 @@
|
||||
.ribbon-container {
|
||||
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 {
|
||||
@ -24,12 +33,14 @@
|
||||
max-width: max-content;
|
||||
flex-grow: 10;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.ribbon-tab-title .bx {
|
||||
font-size: 150%;
|
||||
font-size: 125%;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
.ribbon-tab-title.active {
|
||||
@ -71,12 +82,9 @@
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
margin-inline-end: 5px;
|
||||
}
|
||||
|
||||
.ribbon-button-container > * {
|
||||
position: relative;
|
||||
top: -3px;
|
||||
margin-inline-start: 10px;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ribbon-body {
|
||||
@ -386,6 +394,8 @@ body[dir=rtl] .attribute-list-editor {
|
||||
.note-actions {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.note-actions .dropdown-menu {
|
||||
@ -404,4 +414,4 @@ body[dir=rtl] .attribute-list-editor {
|
||||
background-color: transparent !important;
|
||||
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: {
|
||||
parser: tsParser
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user