import "./InlineTitle.css"; import { NoteType } from "@triliumnext/commons"; import clsx from "clsx"; import { ComponentChild } from "preact"; import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks"; import { Trans } from "react-i18next"; import FNote from "../../entities/fnote"; import attributes from "../../services/attributes"; import froca from "../../services/froca"; import { t } from "../../services/i18n"; import { ViewScope } from "../../services/link"; import { NOTE_TYPES, NoteTypeMapping } from "../../services/note_types"; import server from "../../services/server"; import { formatDateTime } from "../../utils/formatters"; import NoteIcon from "../note_icon"; import NoteTitleWidget from "../note_title"; import { Badge, BadgeWithDropdown } from "../react/Badge"; import { FormDropdownDivider, FormListItem } from "../react/FormList"; import { useNoteBlob, useNoteContext, useNoteProperty, useStaticTooltip, useTriliumEvent } from "../react/hooks"; import { joinElements } from "../react/react_utils"; import { useNoteMetadata } from "../ribbon/NoteInfoTab"; import { onWheelHorizontalScroll } from "../widget_utils"; const supportedNoteTypes = new Set([ "text", "code" ]); export default function InlineTitle() { const { note, parentComponent, viewScope } = useNoteContext(); const type = useNoteProperty(note, "type"); const [ shown, setShown ] = useState(shouldShow(note?.noteId, type, viewScope)); const containerRef = useRef(null); const [ titleHidden, setTitleHidden ] = useState(false); useLayoutEffect(() => { setShown(shouldShow(note?.noteId, type, viewScope)); }, [ note, type, viewScope ]); useLayoutEffect(() => { if (!shown) return; const titleRow = parentComponent.$widget[0].closest(".note-split")?.querySelector(":scope > .title-row"); if (!titleRow) return; titleRow.classList.toggle("hide-title", true); const observer = new IntersectionObserver((entries) => { titleRow.classList.toggle("hide-title", entries[0].isIntersecting); setTitleHidden(!entries[0].isIntersecting); }, { threshold: 0.85 }); if (containerRef.current) { observer.observe(containerRef.current); } return () => { titleRow.classList.remove("hide-title"); observer.disconnect(); }; }, [ shown, parentComponent ]); return (
); } function shouldShow(noteId: string | undefined, type: NoteType | undefined, viewScope: ViewScope | undefined) { if (viewScope?.viewMode !== "default") return false; if (noteId?.startsWith("_options")) return true; return type && supportedNoteTypes.has(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 && ), (!isHiddenNote && metadata?.dateModified && ) ].filter(item => !!item); return items.length > 0 && (
{joinElements(items, " • ")}
); } function TextWithValue({ i18nKey, value, valueTooltip }: { i18nKey: string; value: string; valueTooltip: string; }) { const listItemRef = useRef(null); useStaticTooltip(listItemRef, { selector: "span.value", title: valueTooltip, popperConfig: { placement: "bottom" } }); return (
  • {value} as React.ReactElement }} />
  • ); } //#endregion //#region Note type switcher const SWITCHER_PINNED_NOTE_TYPES = new Set([ "text", "code", "book", "canvas" ]); function NoteTypeSwitcher() { const { note } = useNoteContext(); const blob = useNoteBlob(note); const currentNoteType = useNoteProperty(note, "type"); const { pinnedNoteTypes, restNoteTypes } = useMemo(() => { const pinnedNoteTypes: NoteTypeMapping[] = []; const restNoteTypes: NoteTypeMapping[] = []; for (const noteType of NOTE_TYPES) { if (noteType.reserved || noteType.static || noteType.type === "book") continue; if (SWITCHER_PINNED_NOTE_TYPES.has(noteType.type)) { pinnedNoteTypes.push(noteType); } else { restNoteTypes.push(noteType); } } return { pinnedNoteTypes, restNoteTypes }; }, []); const currentNoteTypeData = useMemo(() => NOTE_TYPES.find(t => t.type === currentNoteType), [ currentNoteType ]); const { builtinTemplates, collectionTemplates } = useBuiltinTemplates(); return (currentNoteType && supportedNoteTypes.has(currentNoteType) &&
    {note && blob?.contentLength === 0 && ( <>
    {t("note_title.note_type_switcher_label", { type: currentNoteTypeData?.title.toLocaleLowerCase() })}
    {pinnedNoteTypes.map(noteType => noteType.type !== currentNoteType && ( switchNoteType(note.noteId, noteType)} /> ))} {collectionTemplates.length > 0 && } {builtinTemplates.length > 0 && } {restNoteTypes.length > 0 && } )}
    ); } function MoreNoteTypes({ noteId, restNoteTypes }: { noteId: string, restNoteTypes: NoteTypeMapping[] }) { return ( {restNoteTypes.map(noteType => ( switchNoteType(noteId, noteType)} >{noteType.title} ))} ); } function CollectionNoteTypes({ noteId, collectionTemplates }: { noteId: string, collectionTemplates: FNote[] }) { return ( {collectionTemplates.map(collectionTemplate => ( setTemplate(noteId, collectionTemplate.noteId)} >{collectionTemplate.title} ))} ); } function TemplateNoteTypes({ noteId, builtinTemplates }: { noteId: string, builtinTemplates: FNote[] }) { const [ userTemplates, setUserTemplates ] = useState([]); async function refreshTemplates() { const templateNoteIds = await server.get("search-templates"); const templateNotes = await froca.getNotes(templateNoteIds); setUserTemplates(templateNotes); } // First load. useEffect(() => { refreshTemplates(); }, []); // React to external changes. useTriliumEvent("entitiesReloaded", ({ loadResults }) => { if (loadResults.getAttributeRows().some(attr => attr.type === "label" && attr.name === "template")) { refreshTemplates(); } }); return ( {userTemplates.map(template => )} {userTemplates.length > 0 && } {builtinTemplates.map(template => )} ); } function TemplateItem({ noteId, template }: { noteId: string, template: FNote }) { return ( setTemplate(noteId, template.noteId)} >{template.title} ); } function switchNoteType(noteId: string, { type, mime }: NoteTypeMapping) { return server.put(`notes/${noteId}/type`, { type, mime }); } function setTemplate(noteId: string, templateId: string) { return attributes.setRelation(noteId, "template", templateId); } function useBuiltinTemplates() { const [ templates, setTemplates ] = useState<{ builtinTemplates: FNote[]; collectionTemplates: FNote[]; }>({ builtinTemplates: [], collectionTemplates: [] }); async function loadBuiltinTemplates() { const templatesRoot = await froca.getNote("_templates"); if (!templatesRoot) return; const childNotes = await templatesRoot.getChildNotes(); const builtinTemplates: FNote[] = []; const collectionTemplates: FNote[] = []; for (const childNote of childNotes) { if (!childNote.hasLabel("template")) continue; if (childNote.hasLabel("collection")) { collectionTemplates.push(childNote); } else { builtinTemplates.push(childNote); } } setTemplates({ builtinTemplates, collectionTemplates }); } useEffect(() => { loadBuiltinTemplates(); }, []); return templates; } //#endregion