trilium/apps/client/src/widgets/layout/InlineTitle.tsx

150 lines
4.7 KiB
TypeScript

import "./InlineTitle.css";
import { NoteType } from "@triliumnext/commons";
import clsx from "clsx";
import { ComponentChild } from "preact";
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { Trans } from "react-i18next";
import FNote from "../../entities/fnote";
import { ViewScope } from "../../services/link";
import { formatDateTime } from "../../utils/formatters";
import NoteIcon from "../note_icon";
import NoteTitleWidget from "../note_title";
import { useNoteContext, useNoteProperty, useStaticTooltip } from "../react/hooks";
import { joinElements } from "../react/react_utils";
import { useNoteMetadata } from "../ribbon/NoteInfoTab";
import { NOTE_TYPES } from "../../services/note_types";
import { Badge } from "../react/Badge";
import server from "../../services/server";
const supportedNoteTypes = new Set<NoteType>([
"text", "code"
]);
export default function InlineTitle() {
const { note, parentComponent, viewScope } = useNoteContext();
const [ shown, setShown ] = useState(shouldShow(note, viewScope));
const containerRef= useRef<HTMLDivElement>(null);
useEffect(() => {
setShown(shouldShow(note, viewScope));
}, [ note, viewScope ]);
useEffect(() => {
if (!shown) return;
const titleRow = parentComponent.$widget[0]
.closest(".note-split")
?.querySelector("&> .title-row");
if (!titleRow) return;
const observer = new IntersectionObserver((entries) => {
titleRow.classList.toggle("collapse", entries[0].isIntersecting);
});
if (containerRef.current) {
observer.observe(containerRef.current);
}
return () => {
titleRow.classList.remove("collapse");
observer.disconnect();
};
}, [ shown, parentComponent ]);
return (
<div
ref={containerRef}
className={clsx("inline-title", !shown && "hidden")}
>
<div class="inline-title-row">
<NoteIcon />
<NoteTitleWidget />
</div>
<NoteTitleDetails />
<NoteTypeSwitcher />
</div>
);
}
function shouldShow(note: FNote | null | undefined, viewScope: ViewScope | undefined) {
if (!note) return false;
if (viewScope?.viewMode !== "default") return false;
if (note.noteId.startsWith("_options")) return true;
return supportedNoteTypes.has(note.type);
}
//#region Title details
export function NoteTitleDetails() {
const { note } = useNoteContext();
const { metadata } = useNoteMetadata(note);
const isHiddenNote = note?.noteId.startsWith("_");
const items: ComponentChild[] = [
(!isHiddenNote && metadata?.dateCreated &&
<TextWithValue
i18nKey="note_title.created_on"
value={formatDateTime(metadata.dateCreated, "medium", "none")}
valueTooltip={formatDateTime(metadata.dateCreated, "full", "long")}
/>),
(!isHiddenNote && metadata?.dateModified &&
<TextWithValue
i18nKey="note_title.last_modified"
value={formatDateTime(metadata.dateModified, "medium", "none")}
valueTooltip={formatDateTime(metadata.dateModified, "full", "long")}
/>)
].filter(item => !!item);
return items.length > 0 && (
<div className="title-details">
{joinElements(items, " • ")}
</div>
);
}
function TextWithValue({ i18nKey, value, valueTooltip }: {
i18nKey: string;
value: string;
valueTooltip: string;
}) {
const listItemRef = useRef<HTMLLIElement>(null);
useStaticTooltip(listItemRef, {
selector: "span.value",
title: valueTooltip,
popperConfig: { placement: "bottom" }
});
return (
<li ref={listItemRef}>
<Trans
i18nKey={i18nKey}
components={{
Value: <span className="value">{value}</span> as React.ReactElement
}}
/>
</li>
);
}
//#endregion
//#region Note type switcher
function NoteTypeSwitcher() {
const { note } = useNoteContext();
const currentNoteType = useNoteProperty(note, "type");
const noteTypes = useMemo(() => NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static), []);
return (note &&
<div className="note-type-switcher">
{noteTypes.map(noteType => noteType.type !== currentNoteType && (
<Badge
key={noteType.type}
text={noteType.title}
onClick={() => server.put(`notes/${note.noteId}/type`, { type: noteType.type, mime: noteType.mime })}
/>
))}
</div>
);
}
//#endregion