import "./note_icon.css"; import { IconRegistry } from "@triliumnext/commons"; import { Dropdown as BootstrapDropdown } from "bootstrap"; import { t } from "i18next"; import { RefObject } from "preact"; import { useEffect, useRef, useState } from "preact/hooks"; import FNote from "../entities/fnote"; import attributes from "../services/attributes"; import server from "../services/server"; import ActionButton from "./react/ActionButton"; import Dropdown from "./react/Dropdown"; import { FormDropdownDivider, FormListItem } from "./react/FormList"; import FormTextBox from "./react/FormTextBox"; import { useNoteContext, useNoteLabel } from "./react/hooks"; interface IconToCountCache { iconClassToCountMap: Record; } interface IconData { iconToCount: Record; icons: (IconRegistry["sources"][number]["icons"][number] & { iconPack: string })[]; } let iconToCountCache!: Promise | null; export default function NoteIcon() { const { note, viewScope } = useNoteContext(); const [ icon, setIcon ] = useState(); const [ iconClass ] = useNoteLabel(note, "iconClass"); const [ workspaceIconClass ] = useNoteLabel(note, "workspaceIconClass"); const dropdownRef = useRef(null); useEffect(() => { setIcon(note?.getIcon()); }, [ note, iconClass, workspaceIconClass ]); return ( { note && } ); } function NoteIconList({ note, dropdownRef }: { note: FNote, dropdownRef: RefObject; }) { const searchBoxRef = useRef(null); const [ search, setSearch ] = useState(); const [ iconData, setIconData ] = useState(); const [ filterByPrefix, setFilterByPrefix ] = useState(null); useEffect(() => { async function loadIcons() { // Filter by text and/or category. let icons: IconData["icons"] = [ ...glob.iconRegistry.sources.map(s => s.icons.map((i) => ({ ...i, iconPack: s.name, }))).flat() ]; const processedSearch = search?.trim()?.toLowerCase(); if (processedSearch || filterByPrefix !== null) { icons = icons.filter((icon) => { if (filterByPrefix) { if (!icon.id?.startsWith(`${filterByPrefix} `)) { return false; } } if (processedSearch) { if (!icon.terms?.some((t) => t.includes(processedSearch))) { return false; } } return true; }); } // Sort by count. const iconToCount = await getIconToCountMap(); if (iconToCount) { icons.sort((a, b) => { const countA = iconToCount[a.id ?? ""] || 0; const countB = iconToCount[b.id ?? ""] || 0; return countB - countA; }); } setIconData({ iconToCount, icons }); } loadIcons(); }, [ search, filterByPrefix ]); return ( <>
{t("note_icon.search")} {getIconLabels(note).length > 0 && (
{ if (!note) return; for (const label of getIconLabels(note)) { attributes.removeAttributeById(note.noteId, label.attributeId); } dropdownRef?.current?.hide(); }} />
)} {glob.iconRegistry.sources.length > 0 && }
{ // Make sure we are not clicking on something else than a button. const clickedTarget = e.target as HTMLElement; if (clickedTarget.tagName !== "SPAN" || clickedTarget.classList.length !== 2) return; const iconClass = Array.from(clickedTarget.classList.values()).join(" "); if (note) { const attributeToSet = note.hasOwnedLabel("workspace") ? "workspaceIconClass" : "iconClass"; attributes.setLabel(note.noteId, attributeToSet, iconClass); } dropdownRef?.current?.hide(); }} > {(iconData?.icons ?? []).map(({ id, terms, iconPack }) => ( ))}
); } function IconFilterContent({ filterByPrefix, setFilterByPrefix }: { filterByPrefix: string | null; setFilterByPrefix: (value: string | null) => void; }) { return ( <> setFilterByPrefix(null)} >{t("note_icon.filter-none")} setFilterByPrefix("bx")} >{t("note_icon.filter-default")} {glob.iconRegistry.sources.map(({ prefix, name, icon }) => ( prefix !== "bx" && setFilterByPrefix(prefix)} icon={icon} checked={filterByPrefix === prefix} >{name} ))} ); } async function getIconToCountMap() { if (!iconToCountCache) { iconToCountCache = server.get("other/icon-usage"); setTimeout(() => (iconToCountCache = null), 20000); // invalidate cache after 20 seconds } return (await iconToCountCache).iconClassToCountMap; } function getIconLabels(note: FNote) { if (!note) { return []; } return note.getOwnedLabels() .filter((label) => ["workspaceIconClass", "iconClass"] .includes(label.name)); }