import { IS_MOBILE } from '@blocksuite/global/env';
import {
CheckBoxCheckSolidIcon,
CheckBoxUnIcon,
DoneIcon,
} from '@blocksuite/icons/lit';
import type { ReadonlySignal } from '@preact/signals-core';
import { cssVarV2 } from '@toeverything/theme/v2';
import { css, html, type TemplateResult, unsafeCSS } from 'lit';
import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { keyed } from 'lit/directives/keyed.js';
import type { ClassInfo } from 'lit-html/directives/class-map.js';
import { ifDefined } from 'lit-html/directives/if-defined.js';
import { MenuFocusable } from './focusable.js';
import type { Menu } from './menu.js';
import type { MenuClass, MenuItemRender } from './types.js';
export type MenuButtonData = {
content: () => TemplateResult;
class: ClassInfo;
select: (ele: HTMLElement) => void | false;
onHover?: (hover: boolean) => void;
testId?: string;
};
export class MenuButton extends MenuFocusable {
static override styles = css`
.affine-menu-button {
display: flex;
width: 100%;
font-size: 20px;
cursor: pointer;
align-items: center;
padding: 4px;
gap: 8px;
border-radius: 4px;
color: var(--affine-icon-color);
}
.affine-menu-button:hover,
affine-menu-button.active .affine-menu-button {
background-color: var(--affine-hover-color);
}
.affine-menu-button .affine-menu-action-text {
flex: 1;
font-size: 14px;
line-height: 22px;
color: var(--affine-text-primary-color);
}
.affine-menu-button.focused {
outline: 1px solid ${unsafeCSS(cssVarV2.layer.insideBorder.primaryBorder)};
}
.affine-menu-button.delete-item:hover {
background-color: var(--affine-background-error-color);
color: var(--affine-error-color);
}
.affine-menu-button.delete-item:hover .affine-menu-action-text {
color: var(--affine-error-color);
}
`;
override connectedCallback() {
super.connectedCallback();
this.disposables.addFromEvent(this, 'mouseenter', () => {
this.data.onHover?.(true);
this.menu.closeSubMenu();
});
this.disposables.addFromEvent(this, 'mouseleave', () => {
this.data.onHover?.(false);
});
this.disposables.addFromEvent(this, 'click', this.onClick);
}
override disconnectedCallback() {
super.disconnectedCallback();
this.data.onHover?.(false);
}
onClick() {
if (this.data.select(this) !== false) {
this.menu.options.onComplete?.();
this.menu.close();
}
}
override onPressEnter() {
this.onClick();
}
protected override render(): unknown {
const classString = classMap({
'affine-menu-button': true,
focused: this.isFocused$.value,
...this.data.class,
});
return html`
${this.data.content()}
`;
}
@property({ attribute: false })
accessor data!: MenuButtonData;
}
export class MobileMenuButton extends MenuFocusable {
static override styles = css`
.mobile-menu-button {
display: flex;
width: 100%;
cursor: pointer;
align-items: center;
font-size: 20px;
padding: 11px 8px;
gap: 8px;
border-radius: 4px;
color: var(--affine-icon-color);
}
.mobile-menu-button .affine-menu-action-text {
flex: 1;
color: var(--affine-text-primary-color);
font-size: 17px;
line-height: 22px;
}
.mobile-menu-button.delete-item {
color: var(--affine-error-color);
}
.mobile-menu-button.delete-item .mobile-menu-action-text {
color: var(--affine-error-color);
}
`;
override connectedCallback() {
super.connectedCallback();
this.disposables.addFromEvent(this, 'click', this.onClick);
}
// eslint-disable-next-line sonarjs/no-identical-functions
onClick() {
if (this.data.select(this) !== false) {
this.menu.options.onComplete?.();
this.menu.close();
}
}
override onPressEnter() {
this.onClick();
}
protected override render(): unknown {
const classString = classMap({
'mobile-menu-button': true,
focused: this.isFocused$.value,
...this.data.class,
});
return html`
${this.data.content()}
`;
}
@property({ attribute: false })
accessor data!: MenuButtonData;
}
const renderButton = (data: MenuButtonData, menu: Menu) => {
if (IS_MOBILE) {
return html``;
}
return html``;
};
export const menuButtonItems = {
action:
(config: {
name: string;
label?: () => TemplateResult;
prefix?: TemplateResult;
postfix?: TemplateResult;
isSelected?: boolean;
select: (ele: HTMLElement) => void | false;
onHover?: (hover: boolean) => void;
class?: MenuClass;
hide?: () => boolean;
testId?: string;
}) =>
menu => {
if (config.hide?.() || !menu.search(config.name)) {
return;
}
const data: MenuButtonData = {
content: () => {
return html`
${config.prefix}
${config.postfix ?? (config.isSelected ? DoneIcon() : undefined)}
`;
},
onHover: config.onHover,
select: config.select,
class: {
'selected-item': config.isSelected ?? false,
...config.class,
},
testId: config.testId,
};
return renderButton(data, menu);
},
checkbox:
(config: {
name: string;
checked: ReadonlySignal;
postfix?: TemplateResult;
label?: () => TemplateResult;
select: (checked: boolean) => boolean;
class?: ClassInfo;
testId?: string;
}) =>
menu => {
if (!menu.search(config.name)) {
return;
}
const data: MenuButtonData = {
content: () => html`
${config.checked.value
? CheckBoxCheckSolidIcon({ style: `color:#1E96EB` })
: CheckBoxUnIcon()}
${config.postfix}
`,
select: () => {
config.select(config.checked.value);
return false;
},
class: config.class ?? {},
testId: config.testId,
};
return html`${keyed(config.name, renderButton(data, menu))}`;
},
toggleSwitch:
(config: {
name: string;
on: boolean;
prefix?: TemplateResult;
postfix?: TemplateResult;
label?: () => TemplateResult;
onChange: (on: boolean) => void;
class?: ClassInfo;
testId?: string;
}) =>
menu => {
if (!menu.search(config.name)) {
return;
}
const onChange = (on: boolean) => {
config.onChange(on);
};
const data: MenuButtonData = {
content: () => html`
${config.prefix}
${config.postfix}
`,
select: () => {
config.onChange(config.on);
return false;
},
class: config.class ?? {},
testId: config.testId,
};
return html`${keyed(config.name, renderButton(data, menu))}`;
},
} satisfies Record>;