From aa608510d0ee2572473556fdb4e871b0d3078069 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 21 Aug 2025 14:41:59 +0300 Subject: [PATCH] feat(react): port note icon --- apps/client/src/layouts/desktop_layout.tsx | 4 +- apps/client/src/layouts/layout_commons.tsx | 4 +- apps/client/src/widgets/icon_list.ts | 2 +- apps/client/src/widgets/note_icon.css | 59 ++++++++++ .../{note_icon.ts => note_icon.ts.bak} | 102 ------------------ apps/client/src/widgets/note_icon.tsx | 99 +++++++++++++++++ apps/client/src/widgets/react/Dropdown.tsx | 27 ++++- 7 files changed, 185 insertions(+), 112 deletions(-) create mode 100644 apps/client/src/widgets/note_icon.css rename apps/client/src/widgets/{note_icon.ts => note_icon.ts.bak} (60%) create mode 100644 apps/client/src/widgets/note_icon.tsx diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index 85dcb531f..4b094ba3b 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -18,7 +18,7 @@ import SqlTableSchemasWidget from "../widgets/sql_table_schemas.js"; import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js"; import ImagePropertiesWidget from "../widgets/ribbon_widgets/image_properties.js"; import NotePropertiesWidget from "../widgets/ribbon_widgets/note_properties.js"; -import NoteIconWidget from "../widgets/note_icon.js"; +import NoteIconWidget from "../widgets/note_icon.jsx"; import SearchResultWidget from "../widgets/search_result.js"; import ScrollingContainer from "../widgets/containers/scrolling_container.js"; import RootContainer from "../widgets/containers/root_container.js"; @@ -151,7 +151,7 @@ export default class DesktopLayout { .css("min-height", "50px") .css("align-items", "center") .cssBlock(".title-row > * { margin: 5px; }") - .child(new NoteIconWidget()) + .child() .child() .child(new SpacerWidget(0, 1)) .child(new MovePaneButton(true)) diff --git a/apps/client/src/layouts/layout_commons.tsx b/apps/client/src/layouts/layout_commons.tsx index e3be51a3a..87d446e18 100644 --- a/apps/client/src/layouts/layout_commons.tsx +++ b/apps/client/src/layouts/layout_commons.tsx @@ -24,7 +24,7 @@ import InfoDialog from "../widgets/dialogs/info.js"; import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js"; import PopupEditorDialog from "../widgets/dialogs/popup_editor.js"; import FlexContainer from "../widgets/containers/flex_container.js"; -import NoteIconWidget from "../widgets/note_icon.js"; +import NoteIconWidget from "../widgets/note_icon"; import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js"; import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js"; import NoteDetailWidget from "../widgets/note_detail.js"; @@ -61,7 +61,7 @@ export function applyModals(rootContainer: RootContainer) { .class("title-row") .css("align-items", "center") .cssBlock(".title-row > * { margin: 5px; }") - .child(new NoteIconWidget()) + .child() .child()) .child(new ClassicEditorToolbar()) .child(new PromotedAttributesWidget()) diff --git a/apps/client/src/widgets/icon_list.ts b/apps/client/src/widgets/icon_list.ts index 7de49017b..6282d8b60 100644 --- a/apps/client/src/widgets/icon_list.ts +++ b/apps/client/src/widgets/icon_list.ts @@ -1,6 +1,6 @@ // taken from the HTML source of https://boxicons.com/ -interface Category { +export interface Category { name: string; id: number; } diff --git a/apps/client/src/widgets/note_icon.css b/apps/client/src/widgets/note_icon.css new file mode 100644 index 000000000..67eeadecf --- /dev/null +++ b/apps/client/src/widgets/note_icon.css @@ -0,0 +1,59 @@ +.note-icon-widget { + padding-top: 3px; + padding-left: 7px; + margin-right: 0; + width: 50px; + height: 50px; +} + +.note-icon-widget button.note-icon { + font-size: 180%; + background-color: transparent; + border: 1px solid transparent; + cursor: pointer; + padding: 6px; + color: var(--main-text-color); +} + +.note-icon-widget button.note-icon:hover { + border: 1px solid var(--main-border-color); +} + +.note-icon-widget .dropdown-menu { + border-radius: 10px; + border-width: 2px; + box-shadow: 10px 10px 93px -25px black; + padding: 10px 15px 10px 15px !important; +} + +.note-icon-widget .filter-row { + padding-top: 10px; + padding-bottom: 10px; + padding-right: 20px; + display: flex; + align-items: baseline; +} + +.note-icon-widget .filter-row span { + display: block; + padding-left: 15px; + padding-right: 15px; + font-weight: bold; +} + +.note-icon-widget .icon-list { + height: 500px; + overflow: auto; +} + +.note-icon-widget .icon-list span { + display: inline-block; + padding: 10px; + cursor: pointer; + border: 1px solid transparent; + font-size: 180%; +} + +.note-icon-widget .icon-list span:hover { + border: 1px solid var(--main-border-color); +} \ No newline at end of file diff --git a/apps/client/src/widgets/note_icon.ts b/apps/client/src/widgets/note_icon.ts.bak similarity index 60% rename from apps/client/src/widgets/note_icon.ts rename to apps/client/src/widgets/note_icon.ts.bak index b5623db87..50a846bc3 100644 --- a/apps/client/src/widgets/note_icon.ts +++ b/apps/client/src/widgets/note_icon.ts.bak @@ -7,86 +7,6 @@ import type { EventData } from "../components/app_context.js"; import type { Icon } from "./icon_list.js"; import { Dropdown } from "bootstrap"; -const TPL = /*html*/` -`; - -interface IconToCountCache { - iconClassToCountMap: Record; -} - export default class NoteIconWidget extends NoteContextAwareWidget { private dropdown!: bootstrap.Dropdown; @@ -94,12 +14,8 @@ export default class NoteIconWidget extends NoteContextAwareWidget { private $iconList!: JQuery; private $iconCategory!: JQuery; private $iconSearch!: JQuery; - private iconToCountCache!: Promise | null; doRender() { - this.$widget = $(TPL); - this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0]); - this.$icon = this.$widget.find("button.note-icon"); this.$iconList = this.$widget.find(".icon-list"); this.$iconList.on("click", "span", async (e) => { @@ -153,9 +69,6 @@ export default class NoteIconWidget extends NoteContextAwareWidget { } async renderDropdown() { - const iconToCount = await this.getIconToCountMap(); - const { icons } = (await import("./icon_list.js")).default; - this.$iconList.empty(); if (this.getIconLabels().length > 0) { @@ -205,21 +118,6 @@ export default class NoteIconWidget extends NoteContextAwareWidget { this.$iconSearch.focus(); } - async getIconToCountMap() { - if (!this.iconToCountCache) { - this.iconToCountCache = server.get("other/icon-usage"); - setTimeout(() => (this.iconToCountCache = null), 20000); // invalidate cache after 20 seconds - } - - return (await this.iconToCountCache)?.iconClassToCountMap; - } - - renderIcon(icon: Icon) { - return $("") - .addClass("bx " + icon.className) - .attr("title", icon.name); - } - getIconLabels() { if (!this.note) { return []; diff --git a/apps/client/src/widgets/note_icon.tsx b/apps/client/src/widgets/note_icon.tsx new file mode 100644 index 000000000..7d11bf341 --- /dev/null +++ b/apps/client/src/widgets/note_icon.tsx @@ -0,0 +1,99 @@ +import Dropdown from "./react/Dropdown"; +import "./note_icon.css"; +import { t } from "i18next"; +import { useNoteContext } from "./react/hooks"; +import { useCallback, useEffect, useState } from "preact/hooks"; +import server from "../services/server"; +import type { Category, Icon } from "./icon_list"; +import FormTextBox from "./react/FormTextBox"; + +interface IconToCountCache { + iconClassToCountMap: Record; +} + +interface IconData { + iconToCount: Record; + icons: Icon[]; +} + +let fullIconData: { + categories: Category[]; + icons: Icon[]; +}; +let iconToCountCache!: Promise | null; + +export default function NoteIcon() { + const { note } = useNoteContext(); + const [ icon, setIcon ] = useState("bx bx-empty"); + + const refreshIcon = useCallback(() => { + if (note) { + setIcon(note.getIcon()); + } + }, [ note ]); + + useEffect(refreshIcon, [ note ]); + + return ( + + + + ) +} + +function NoteIconList() { + const [ filter, setFilter ] = useState(); + const [ iconData, setIconData ] = useState(); + + useEffect(() => { + async function loadIcons() { + const iconToCount = await getIconToCountMap(); + if (!fullIconData) { + fullIconData = (await import("./icon_list.js")).default; + } + + setIconData({ + iconToCount, + icons: fullIconData.icons + }) + } + + loadIcons(); + }, []); + + return ( + <> +
+ {t("note_icon.category")} + + {t("note_icon.search")} + +
+ +
+ {(iconData?.icons ?? []).map(({className, 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; +} \ No newline at end of file diff --git a/apps/client/src/widgets/react/Dropdown.tsx b/apps/client/src/widgets/react/Dropdown.tsx index 58a3bd654..1d64d40fd 100644 --- a/apps/client/src/widgets/react/Dropdown.tsx +++ b/apps/client/src/widgets/react/Dropdown.tsx @@ -1,14 +1,20 @@ import { Dropdown as BootstrapDropdown } from "bootstrap"; import { ComponentChildren } from "preact"; +import { CSSProperties } from "preact/compat"; import { useEffect, useRef } from "preact/hooks"; +import { useUniqueName } from "./hooks"; interface DropdownProps { className?: string; + buttonClassName?: string; isStatic?: boolean; children: ComponentChildren; + title?: string; + dropdownContainerStyle?: CSSProperties; + hideToggleArrow?: boolean; } -export default function Dropdown({ className, isStatic, children }: DropdownProps) { +export default function Dropdown({ className, buttonClassName, isStatic, children, title, dropdownContainerStyle, hideToggleArrow }: DropdownProps) { const dropdownRef = useRef(null); const triggerRef = useRef(null); @@ -35,16 +41,27 @@ export default function Dropdown({ className, isStatic, children }: DropdownProp }; }, []); // Add dependency array + const ariaId = useUniqueName("button"); + return ( -