mirror of
https://github.com/zadam/trilium.git
synced 2025-10-20 15:19:01 +02:00
182 lines
5.7 KiB
TypeScript
182 lines
5.7 KiB
TypeScript
import { Dropdown as BootstrapDropdown, Tooltip } from "bootstrap";
|
|
import { ComponentChildren } from "preact";
|
|
import Icon from "./Icon";
|
|
import { useEffect, useMemo, useRef, useState, type CSSProperties } from "preact/compat";
|
|
import "./FormList.css";
|
|
import { CommandNames } from "../../components/app_context";
|
|
import { useStaticTooltip } from "./hooks";
|
|
import { handleRightToLeftPlacement, isMobile } from "../../services/utils";
|
|
|
|
interface FormListOpts {
|
|
children: ComponentChildren;
|
|
onSelect?: (value: string) => void;
|
|
style?: CSSProperties;
|
|
fullHeight?: boolean;
|
|
}
|
|
|
|
export default function FormList({ children, onSelect, style, fullHeight }: FormListOpts) {
|
|
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
|
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!triggerRef.current || !wrapperRef.current) {
|
|
return;
|
|
}
|
|
|
|
const $wrapperRef = $(wrapperRef.current);
|
|
const dropdown = BootstrapDropdown.getOrCreateInstance(triggerRef.current);
|
|
$wrapperRef.on("hide.bs.dropdown", (e) => e.preventDefault());
|
|
|
|
return () => {
|
|
$wrapperRef.off("hide.bs.dropdown");
|
|
dropdown.dispose();
|
|
}
|
|
}, [ triggerRef, wrapperRef ]);
|
|
|
|
const builtinStyles = useMemo(() => {
|
|
const style: CSSProperties = {};
|
|
if (fullHeight) {
|
|
style.height = "100%";
|
|
style.overflow = "auto";
|
|
}
|
|
return style;
|
|
}, [ fullHeight ]);
|
|
|
|
return (
|
|
<div className="dropdownWrapper" ref={wrapperRef} style={builtinStyles}>
|
|
<div className="dropdown" style={builtinStyles}>
|
|
<button
|
|
ref={triggerRef}
|
|
type="button" style="display: none;"
|
|
data-bs-toggle="dropdown" data-bs-display="static">
|
|
</button>
|
|
|
|
<div class="dropdown-menu static show" style={{
|
|
...style ?? {},
|
|
...builtinStyles,
|
|
position: "relative",
|
|
}} onClick={(e) => {
|
|
const dropdownItem = (e.target as HTMLElement).closest(".dropdown-item") as HTMLElement | null;
|
|
const value = dropdownItem?.dataset?.value;
|
|
if (value && onSelect) {
|
|
onSelect(value);
|
|
}
|
|
}}>
|
|
{children}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export interface FormListBadge {
|
|
className?: string;
|
|
text: string;
|
|
}
|
|
|
|
interface FormListItemOpts {
|
|
children: ComponentChildren;
|
|
icon?: string;
|
|
value?: string;
|
|
title?: string;
|
|
active?: boolean;
|
|
badges?: FormListBadge[];
|
|
disabled?: boolean;
|
|
checked?: boolean | null;
|
|
selected?: boolean;
|
|
container?: boolean;
|
|
onClick?: (e: MouseEvent) => void;
|
|
triggerCommand?: CommandNames;
|
|
description?: string;
|
|
className?: string;
|
|
rtl?: boolean;
|
|
}
|
|
|
|
const TOOLTIP_CONFIG: Partial<Tooltip.Options> = {
|
|
placement: handleRightToLeftPlacement("right"),
|
|
fallbackPlacements: [ handleRightToLeftPlacement("right") ]
|
|
}
|
|
|
|
export function FormListItem({ className, icon, value, title, active, disabled, checked, container, onClick, selected, rtl, triggerCommand, description, ...contentProps }: FormListItemOpts) {
|
|
const itemRef = useRef<HTMLLIElement>(null);
|
|
|
|
if (checked) {
|
|
icon = "bx bx-check";
|
|
}
|
|
|
|
useStaticTooltip(itemRef, TOOLTIP_CONFIG);
|
|
|
|
return (
|
|
<li
|
|
ref={itemRef}
|
|
class={`dropdown-item ${active ? "active" : ""} ${disabled ? "disabled" : ""} ${selected ? "selected" : ""} ${container ? "dropdown-container-item": ""} ${className ?? ""}`}
|
|
data-value={value} title={title}
|
|
tabIndex={container ? -1 : 0}
|
|
onClick={onClick}
|
|
data-trigger-command={triggerCommand}
|
|
dir={rtl ? "rtl" : undefined}
|
|
>
|
|
<Icon icon={icon} />
|
|
{description ? (
|
|
<div>
|
|
<FormListContent description={description} {...contentProps} />
|
|
</div>
|
|
) : (
|
|
<FormListContent description={description} {...contentProps} />
|
|
)}
|
|
</li>
|
|
);
|
|
}
|
|
|
|
function FormListContent({ children, badges, description }: Pick<FormListItemOpts, "children" | "badges" | "description">) {
|
|
return <>
|
|
{children}
|
|
{badges && badges.map(({ className, text }) => (
|
|
<span className={`badge ${className ?? ""}`}>{text}</span>
|
|
))}
|
|
{description && <div className="description">{description}</div>}
|
|
</>;
|
|
}
|
|
|
|
interface FormListHeaderOpts {
|
|
text: string;
|
|
}
|
|
|
|
export function FormListHeader({ text }: FormListHeaderOpts) {
|
|
return (
|
|
<li>
|
|
<h6 className="dropdown-header">{text}</h6>
|
|
</li>
|
|
)
|
|
}
|
|
|
|
export function FormDropdownDivider() {
|
|
return <div className="dropdown-divider" />;
|
|
}
|
|
|
|
export function FormDropdownSubmenu({ icon, title, children }: { icon: string, title: ComponentChildren, children: ComponentChildren }) {
|
|
const [ openOnMobile, setOpenOnMobile ] = useState(false);
|
|
|
|
return (
|
|
<li className={`dropdown-item dropdown-submenu ${openOnMobile ? "submenu-open" : ""}`}>
|
|
<span
|
|
className="dropdown-toggle"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
|
|
if (isMobile()) {
|
|
setOpenOnMobile(!openOnMobile);
|
|
}
|
|
}}
|
|
>
|
|
<Icon icon={icon} />{" "}
|
|
{title}
|
|
</span>
|
|
|
|
<ul className={`dropdown-menu ${openOnMobile ? "show" : ""}`}>
|
|
{children}
|
|
</ul>
|
|
</li>
|
|
)
|
|
}
|