2025-09-06 12:26:42 +03:00
|
|
|
import type { ComponentChildren, RefObject } from "preact";
|
2025-08-10 15:21:49 +03:00
|
|
|
import type { CSSProperties } from "preact/compat";
|
2025-08-10 17:19:39 +03:00
|
|
|
import { memo } from "preact/compat";
|
2026-01-31 12:59:09 +02:00
|
|
|
import { useMemo } from "preact/hooks";
|
|
|
|
|
|
2025-08-22 16:58:28 +03:00
|
|
|
import { CommandNames } from "../../components/app_context";
|
2026-01-31 12:59:09 +02:00
|
|
|
import { isDesktop } from "../../services/utils";
|
|
|
|
|
import ActionButton from "./ActionButton";
|
2025-11-26 10:36:00 +02:00
|
|
|
import Icon from "./Icon";
|
2025-08-03 19:06:21 +03:00
|
|
|
|
2025-08-22 20:25:15 +03:00
|
|
|
export interface ButtonProps {
|
2025-08-20 19:10:41 +03:00
|
|
|
name?: string;
|
2025-08-03 23:20:32 +03:00
|
|
|
/** Reference to the button element. Mostly useful for requesting focus. */
|
2025-08-04 12:58:42 +03:00
|
|
|
buttonRef?: RefObject<HTMLButtonElement>;
|
2025-08-03 19:06:21 +03:00
|
|
|
text: string;
|
|
|
|
|
className?: string;
|
2025-08-05 15:39:49 +03:00
|
|
|
icon?: string;
|
2025-08-03 19:06:21 +03:00
|
|
|
keyboardShortcut?: string;
|
2025-08-03 20:01:54 +03:00
|
|
|
/** Called when the button is clicked. If not set, the button will submit the form (if any). */
|
2025-08-03 19:50:39 +03:00
|
|
|
onClick?: () => void;
|
2025-08-05 18:05:41 +03:00
|
|
|
primary?: boolean;
|
2025-08-05 23:03:38 +03:00
|
|
|
disabled?: boolean;
|
2025-08-14 21:42:48 +03:00
|
|
|
size?: "normal" | "small" | "micro";
|
2025-08-06 16:16:30 +03:00
|
|
|
style?: CSSProperties;
|
2025-08-22 16:58:28 +03:00
|
|
|
triggerCommand?: CommandNames;
|
2025-08-23 23:22:07 +03:00
|
|
|
title?: string;
|
2025-08-03 19:06:21 +03:00
|
|
|
}
|
|
|
|
|
|
2025-08-25 18:41:48 +03:00
|
|
|
const Button = memo(({ name, buttonRef, className, text, onClick, keyboardShortcut, icon, primary, disabled, size, style, triggerCommand, ...restProps }: ButtonProps) => {
|
2025-08-10 17:19:39 +03:00
|
|
|
// Memoize classes array to prevent recreation
|
|
|
|
|
const classes = useMemo(() => {
|
|
|
|
|
const classList: string[] = ["btn"];
|
|
|
|
|
if (primary) {
|
|
|
|
|
classList.push("btn-primary");
|
|
|
|
|
} else {
|
|
|
|
|
classList.push("btn-secondary");
|
|
|
|
|
}
|
|
|
|
|
if (className) {
|
|
|
|
|
classList.push(className);
|
|
|
|
|
}
|
2025-08-14 21:31:09 +03:00
|
|
|
if (size === "small") {
|
2025-08-10 17:19:39 +03:00
|
|
|
classList.push("btn-sm");
|
2025-08-14 21:31:09 +03:00
|
|
|
} else if (size === "micro") {
|
|
|
|
|
classList.push("btn-micro");
|
2025-08-10 17:19:39 +03:00
|
|
|
}
|
|
|
|
|
return classList.join(" ");
|
2025-08-14 21:31:09 +03:00
|
|
|
}, [primary, className, size]);
|
2025-08-03 19:06:21 +03:00
|
|
|
|
2025-08-10 17:19:39 +03:00
|
|
|
// Memoize keyboard shortcut rendering
|
|
|
|
|
const shortcutElements = useMemo(() => {
|
|
|
|
|
if (!keyboardShortcut) return null;
|
|
|
|
|
const splitShortcut = keyboardShortcut.split("+");
|
|
|
|
|
return splitShortcut.map((key, index) => (
|
|
|
|
|
<>
|
|
|
|
|
<kbd key={index}>{key.toUpperCase()}</kbd>
|
|
|
|
|
{index < splitShortcut.length - 1 ? "+" : ""}
|
|
|
|
|
</>
|
|
|
|
|
));
|
|
|
|
|
}, [keyboardShortcut]);
|
2025-08-03 19:06:21 +03:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<button
|
2025-08-20 19:10:41 +03:00
|
|
|
name={name}
|
2025-08-10 17:19:39 +03:00
|
|
|
className={classes}
|
2025-08-22 16:58:28 +03:00
|
|
|
type={onClick || triggerCommand ? "button" : "submit"}
|
2025-08-03 19:06:21 +03:00
|
|
|
onClick={onClick}
|
|
|
|
|
ref={buttonRef}
|
2025-08-05 23:03:38 +03:00
|
|
|
disabled={disabled}
|
2025-08-06 16:16:30 +03:00
|
|
|
style={style}
|
2025-08-22 16:58:28 +03:00
|
|
|
data-trigger-command={triggerCommand}
|
2025-08-23 23:22:07 +03:00
|
|
|
{...restProps}
|
2025-08-03 19:06:21 +03:00
|
|
|
>
|
2025-11-26 10:36:00 +02:00
|
|
|
{icon && <Icon icon={`bx ${icon}`} />}
|
2025-08-10 17:19:39 +03:00
|
|
|
{text} {shortcutElements}
|
2025-08-03 19:06:21 +03:00
|
|
|
</button>
|
|
|
|
|
);
|
2025-08-10 17:19:39 +03:00
|
|
|
});
|
|
|
|
|
|
2025-09-06 12:26:42 +03:00
|
|
|
export function ButtonGroup({ children }: { children: ComponentChildren }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="btn-group" role="group">
|
|
|
|
|
{children}
|
|
|
|
|
</div>
|
2026-01-31 12:59:09 +02:00
|
|
|
);
|
2025-09-06 12:26:42 +03:00
|
|
|
}
|
|
|
|
|
|
2025-11-26 11:59:46 +02:00
|
|
|
export function SplitButton({ text, icon, children, ...restProps }: {
|
2025-11-26 10:36:00 +02:00
|
|
|
text: string;
|
|
|
|
|
icon?: string;
|
2025-11-26 11:59:46 +02:00
|
|
|
title?: string;
|
2025-11-26 10:36:00 +02:00
|
|
|
/** Click handler for the main button component (not the split). */
|
|
|
|
|
onClick?: () => void;
|
|
|
|
|
/** The children inside the dropdown of the split. */
|
|
|
|
|
children: ComponentChildren;
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<ButtonGroup>
|
2025-11-26 11:59:46 +02:00
|
|
|
<button type="button" class="btn btn-secondary" {...restProps}>
|
2025-11-26 10:36:00 +02:00
|
|
|
{icon && <Icon icon={`bx ${icon}`} />}
|
|
|
|
|
{text}
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
|
|
|
|
|
<span class="visually-hidden">Toggle Dropdown</span>
|
|
|
|
|
</button>
|
|
|
|
|
<ul class="dropdown-menu">
|
|
|
|
|
{children}
|
|
|
|
|
</ul>
|
|
|
|
|
</ButtonGroup>
|
2026-01-31 12:59:09 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function ButtonOrActionButton(props: {
|
|
|
|
|
text: string;
|
|
|
|
|
icon: string;
|
2026-01-31 13:26:54 +02:00
|
|
|
} & Pick<ButtonProps, "onClick" | "triggerCommand" | "disabled" | "title">) {
|
2026-01-31 12:59:09 +02:00
|
|
|
if (isDesktop()) {
|
|
|
|
|
return <Button {...props} />;
|
|
|
|
|
}
|
|
|
|
|
return <ActionButton {...props} />;
|
2025-11-26 10:36:00 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-06 12:26:42 +03:00
|
|
|
export default Button;
|