mirror of
https://github.com/zadam/trilium.git
synced 2025-10-20 15:19:01 +02:00
feat(react): port note icon
This commit is contained in:
parent
009fd63ce9
commit
aa608510d0
@ -18,7 +18,7 @@ import SqlTableSchemasWidget from "../widgets/sql_table_schemas.js";
|
|||||||
import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js";
|
import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js";
|
||||||
import ImagePropertiesWidget from "../widgets/ribbon_widgets/image_properties.js";
|
import ImagePropertiesWidget from "../widgets/ribbon_widgets/image_properties.js";
|
||||||
import NotePropertiesWidget from "../widgets/ribbon_widgets/note_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 SearchResultWidget from "../widgets/search_result.js";
|
||||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||||
import RootContainer from "../widgets/containers/root_container.js";
|
import RootContainer from "../widgets/containers/root_container.js";
|
||||||
@ -151,7 +151,7 @@ export default class DesktopLayout {
|
|||||||
.css("min-height", "50px")
|
.css("min-height", "50px")
|
||||||
.css("align-items", "center")
|
.css("align-items", "center")
|
||||||
.cssBlock(".title-row > * { margin: 5px; }")
|
.cssBlock(".title-row > * { margin: 5px; }")
|
||||||
.child(new NoteIconWidget())
|
.child(<NoteIconWidget />)
|
||||||
.child(<NoteTitleWidget />)
|
.child(<NoteTitleWidget />)
|
||||||
.child(new SpacerWidget(0, 1))
|
.child(new SpacerWidget(0, 1))
|
||||||
.child(new MovePaneButton(true))
|
.child(new MovePaneButton(true))
|
||||||
|
@ -24,7 +24,7 @@ import InfoDialog from "../widgets/dialogs/info.js";
|
|||||||
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
|
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
|
||||||
import PopupEditorDialog from "../widgets/dialogs/popup_editor.js";
|
import PopupEditorDialog from "../widgets/dialogs/popup_editor.js";
|
||||||
import FlexContainer from "../widgets/containers/flex_container.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 ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
|
||||||
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
|
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
|
||||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||||
@ -61,7 +61,7 @@ export function applyModals(rootContainer: RootContainer) {
|
|||||||
.class("title-row")
|
.class("title-row")
|
||||||
.css("align-items", "center")
|
.css("align-items", "center")
|
||||||
.cssBlock(".title-row > * { margin: 5px; }")
|
.cssBlock(".title-row > * { margin: 5px; }")
|
||||||
.child(new NoteIconWidget())
|
.child(<NoteIconWidget />)
|
||||||
.child(<NoteTitleWidget />))
|
.child(<NoteTitleWidget />))
|
||||||
.child(new ClassicEditorToolbar())
|
.child(new ClassicEditorToolbar())
|
||||||
.child(new PromotedAttributesWidget())
|
.child(new PromotedAttributesWidget())
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// taken from the HTML source of https://boxicons.com/
|
// taken from the HTML source of https://boxicons.com/
|
||||||
|
|
||||||
interface Category {
|
export interface Category {
|
||||||
name: string;
|
name: string;
|
||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
59
apps/client/src/widgets/note_icon.css
Normal file
59
apps/client/src/widgets/note_icon.css
Normal file
@ -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);
|
||||||
|
}
|
@ -7,86 +7,6 @@ import type { EventData } from "../components/app_context.js";
|
|||||||
import type { Icon } from "./icon_list.js";
|
import type { Icon } from "./icon_list.js";
|
||||||
import { Dropdown } from "bootstrap";
|
import { Dropdown } from "bootstrap";
|
||||||
|
|
||||||
const TPL = /*html*/`
|
|
||||||
<div class="note-icon-widget dropdown">
|
|
||||||
<style>
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<button class="btn dropdown-toggle note-icon" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="${t("note_icon.change_note_icon")}"></button>
|
|
||||||
<div class="dropdown-menu" aria-labelledby="note-path-list-button" style="width: 610px;">
|
|
||||||
<div class="filter-row">
|
|
||||||
<span>${t("note_icon.category")}</span> <select name="icon-category" class="form-select"></select>
|
|
||||||
|
|
||||||
<span>${t("note_icon.search")}</span> <input type="text" name="icon-search" class="form-control" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="icon-list"></div>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
interface IconToCountCache {
|
|
||||||
iconClassToCountMap: Record<string, number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class NoteIconWidget extends NoteContextAwareWidget {
|
export default class NoteIconWidget extends NoteContextAwareWidget {
|
||||||
|
|
||||||
private dropdown!: bootstrap.Dropdown;
|
private dropdown!: bootstrap.Dropdown;
|
||||||
@ -94,12 +14,8 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
|
|||||||
private $iconList!: JQuery<HTMLElement>;
|
private $iconList!: JQuery<HTMLElement>;
|
||||||
private $iconCategory!: JQuery<HTMLElement>;
|
private $iconCategory!: JQuery<HTMLElement>;
|
||||||
private $iconSearch!: JQuery<HTMLElement>;
|
private $iconSearch!: JQuery<HTMLElement>;
|
||||||
private iconToCountCache!: Promise<IconToCountCache | null> | null;
|
|
||||||
|
|
||||||
doRender() {
|
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.$icon = this.$widget.find("button.note-icon");
|
||||||
this.$iconList = this.$widget.find(".icon-list");
|
this.$iconList = this.$widget.find(".icon-list");
|
||||||
this.$iconList.on("click", "span", async (e) => {
|
this.$iconList.on("click", "span", async (e) => {
|
||||||
@ -153,9 +69,6 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async renderDropdown() {
|
async renderDropdown() {
|
||||||
const iconToCount = await this.getIconToCountMap();
|
|
||||||
const { icons } = (await import("./icon_list.js")).default;
|
|
||||||
|
|
||||||
this.$iconList.empty();
|
this.$iconList.empty();
|
||||||
|
|
||||||
if (this.getIconLabels().length > 0) {
|
if (this.getIconLabels().length > 0) {
|
||||||
@ -205,21 +118,6 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
|
|||||||
this.$iconSearch.focus();
|
this.$iconSearch.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getIconToCountMap() {
|
|
||||||
if (!this.iconToCountCache) {
|
|
||||||
this.iconToCountCache = server.get<IconToCountCache>("other/icon-usage");
|
|
||||||
setTimeout(() => (this.iconToCountCache = null), 20000); // invalidate cache after 20 seconds
|
|
||||||
}
|
|
||||||
|
|
||||||
return (await this.iconToCountCache)?.iconClassToCountMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderIcon(icon: Icon) {
|
|
||||||
return $("<span>")
|
|
||||||
.addClass("bx " + icon.className)
|
|
||||||
.attr("title", icon.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
getIconLabels() {
|
getIconLabels() {
|
||||||
if (!this.note) {
|
if (!this.note) {
|
||||||
return [];
|
return [];
|
99
apps/client/src/widgets/note_icon.tsx
Normal file
99
apps/client/src/widgets/note_icon.tsx
Normal file
@ -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<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IconData {
|
||||||
|
iconToCount: Record<string, number>;
|
||||||
|
icons: Icon[];
|
||||||
|
}
|
||||||
|
|
||||||
|
let fullIconData: {
|
||||||
|
categories: Category[];
|
||||||
|
icons: Icon[];
|
||||||
|
};
|
||||||
|
let iconToCountCache!: Promise<IconToCountCache> | 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 (
|
||||||
|
<Dropdown
|
||||||
|
className="note-icon-widget"
|
||||||
|
title={t("note_icon.change_note_icon")}
|
||||||
|
dropdownContainerStyle={{ width: "610px" }}
|
||||||
|
buttonClassName={`note-icon ${icon}`}
|
||||||
|
hideToggleArrow
|
||||||
|
>
|
||||||
|
<NoteIconList />
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NoteIconList() {
|
||||||
|
const [ filter, setFilter ] = useState<string>();
|
||||||
|
const [ iconData, setIconData ] = useState<IconData>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadIcons() {
|
||||||
|
const iconToCount = await getIconToCountMap();
|
||||||
|
if (!fullIconData) {
|
||||||
|
fullIconData = (await import("./icon_list.js")).default;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIconData({
|
||||||
|
iconToCount,
|
||||||
|
icons: fullIconData.icons
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
loadIcons();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="filter-row">
|
||||||
|
<span>{t("note_icon.category")}</span> <select name="icon-category" class="form-select"></select>
|
||||||
|
|
||||||
|
<span>{t("note_icon.search")}</span>
|
||||||
|
<FormTextBox
|
||||||
|
type="text"
|
||||||
|
name="icon-search"
|
||||||
|
currentValue={filter} onChange={setFilter}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="icon-list">
|
||||||
|
{(iconData?.icons ?? []).map(({className, name}) => (
|
||||||
|
<span class={`bx ${className}`} title={name} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getIconToCountMap() {
|
||||||
|
if (!iconToCountCache) {
|
||||||
|
iconToCountCache = server.get<IconToCountCache>("other/icon-usage");
|
||||||
|
setTimeout(() => (iconToCountCache = null), 20000); // invalidate cache after 20 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await iconToCountCache).iconClassToCountMap;
|
||||||
|
}
|
@ -1,14 +1,20 @@
|
|||||||
import { Dropdown as BootstrapDropdown } from "bootstrap";
|
import { Dropdown as BootstrapDropdown } from "bootstrap";
|
||||||
import { ComponentChildren } from "preact";
|
import { ComponentChildren } from "preact";
|
||||||
|
import { CSSProperties } from "preact/compat";
|
||||||
import { useEffect, useRef } from "preact/hooks";
|
import { useEffect, useRef } from "preact/hooks";
|
||||||
|
import { useUniqueName } from "./hooks";
|
||||||
|
|
||||||
interface DropdownProps {
|
interface DropdownProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
buttonClassName?: string;
|
||||||
isStatic?: boolean;
|
isStatic?: boolean;
|
||||||
children: ComponentChildren;
|
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<HTMLDivElement | null>(null);
|
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||||
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
@ -35,16 +41,27 @@ export default function Dropdown({ className, isStatic, children }: DropdownProp
|
|||||||
};
|
};
|
||||||
}, []); // Add dependency array
|
}, []); // Add dependency array
|
||||||
|
|
||||||
|
const ariaId = useUniqueName("button");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={dropdownRef} class="dropdown" style={{ display: "flex" }}>
|
<div ref={dropdownRef} class={`dropdown ${className ?? ""}`} style={{ display: "flex" }}>
|
||||||
<button
|
<button
|
||||||
|
className={`btn ${buttonClassName ?? ""} ${!hideToggleArrow ? "dropdown-toggle" : ""}`}
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
type="button"
|
type="button"
|
||||||
style={{ display: "none" }}
|
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
data-bs-display={ isStatic ? "static" : undefined } />
|
data-bs-display={ isStatic ? "static" : undefined }
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded="false"
|
||||||
|
title={title}
|
||||||
|
id={ariaId}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class={`dropdown-menu ${className ?? ""} ${isStatic ? "static" : undefined}`}>
|
<div
|
||||||
|
class={`dropdown-menu ${isStatic ? "static" : undefined}`}
|
||||||
|
style={dropdownContainerStyle}
|
||||||
|
aria-labelledby={ariaId}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user