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 (
-
+
+ data-bs-display={ isStatic ? "static" : undefined }
+ aria-haspopup="true"
+ aria-expanded="false"
+ title={title}
+ id={ariaId}
+ />
-