2025-12-04 15:24:44 +08:00
|
|
|
|
import './index.css';
|
|
|
|
|
|
import type {
|
|
|
|
|
|
OptButton,
|
|
|
|
|
|
ButtonGroup,
|
|
|
|
|
|
ButtonGroupOptions,
|
|
|
|
|
|
ButtonConfig,
|
|
|
|
|
|
ButtonGroupColors
|
|
|
|
|
|
} from './index.type';
|
|
|
|
|
|
import { t, localeManager } from '../../services/locale';
|
|
|
|
|
|
import { themeManager } from '../../services/theme';
|
|
|
|
|
|
import type { ThemeConfig } from '../../themes/types';
|
|
|
|
|
|
import { IBimComponent } from '../../types/component';
|
2026-01-22 15:23:57 +08:00
|
|
|
|
import { ManagerRegistry } from '../../core/manager-registry';
|
2025-12-08 10:02:24 +08:00
|
|
|
|
import { EngineEvents } from '../../types/events';
|
2025-12-04 15:24:44 +08:00
|
|
|
|
|
|
|
|
|
|
export class BimButtonGroup implements IBimComponent {
|
|
|
|
|
|
private container: HTMLElement;
|
|
|
|
|
|
private options: ButtonGroupOptions;
|
|
|
|
|
|
private groups: ButtonGroup[] = [];
|
|
|
|
|
|
private activeBtnIds: Set<string> = new Set();
|
|
|
|
|
|
private btnRefs: Map<string, HTMLElement> = new Map();
|
|
|
|
|
|
private dropdownElement: HTMLElement | null = null;
|
|
|
|
|
|
private hoverTimeout: number | null = null;
|
2026-01-22 15:23:57 +08:00
|
|
|
|
private customColors: Set<keyof ButtonGroupColors> = new Set();
|
2025-12-04 15:24:44 +08:00
|
|
|
|
private unsubscribeLocale: (() => void) | null = null;
|
|
|
|
|
|
private unsubscribeTheme: (() => void) | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
private readonly DEFAULT_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg>';
|
|
|
|
|
|
|
|
|
|
|
|
constructor(options: ButtonGroupOptions) {
|
|
|
|
|
|
const el = typeof options.container === 'string'
|
|
|
|
|
|
? document.getElementById(options.container)
|
|
|
|
|
|
: options.container;
|
|
|
|
|
|
|
|
|
|
|
|
if (!el) throw new Error('Container not found');
|
|
|
|
|
|
|
|
|
|
|
|
this.container = el;
|
|
|
|
|
|
// 合并默认配置
|
|
|
|
|
|
this.options = {
|
|
|
|
|
|
showLabel: true,
|
|
|
|
|
|
visibility: {},
|
|
|
|
|
|
direction: 'row', // 默认横向
|
|
|
|
|
|
position: 'static', // 默认静态定位
|
|
|
|
|
|
align: 'vertical', // 默认图标在上
|
|
|
|
|
|
expand: 'down', // 默认向下展开
|
|
|
|
|
|
...options
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 记录初始传入的自定义颜色
|
|
|
|
|
|
const colorKeys: (keyof ButtonGroupColors)[] = [
|
|
|
|
|
|
'backgroundColor', 'btnBackgroundColor', 'btnHoverColor',
|
|
|
|
|
|
'btnActiveColor', 'iconColor', 'iconActiveColor',
|
|
|
|
|
|
'textColor', 'textActiveColor'
|
|
|
|
|
|
];
|
|
|
|
|
|
colorKeys.forEach(key => {
|
|
|
|
|
|
if (options[key]) {
|
|
|
|
|
|
this.customColors.add(key);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.initContainer();
|
|
|
|
|
|
this.applyStyles();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-08 10:02:24 +08:00
|
|
|
|
protected emit<K extends keyof EngineEvents>(event: K, payload: EngineEvents[K]) {
|
2026-01-22 15:23:57 +08:00
|
|
|
|
const registry = ManagerRegistry.getInstance();
|
|
|
|
|
|
registry.emit(event, payload);
|
2025-12-08 10:02:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 15:24:44 +08:00
|
|
|
|
private initContainer(): void {
|
|
|
|
|
|
this.container.innerHTML = '';
|
|
|
|
|
|
this.container.classList.add('bim-btn-group-root');
|
|
|
|
|
|
|
|
|
|
|
|
if (this.options.direction === 'column') {
|
|
|
|
|
|
this.container.classList.add('dir-column');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.container.classList.add('dir-row');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (this.options.className) {
|
|
|
|
|
|
this.container.classList.add(this.options.className);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 15:50:07 +08:00
|
|
|
|
if (this.options.type && this.options.type !== 'default') {
|
|
|
|
|
|
this.container.classList.add(`type-${this.options.type}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 15:24:44 +08:00
|
|
|
|
this.updatePosition();
|
2025-12-22 18:48:38 +08:00
|
|
|
|
|
2025-12-16 11:57:44 +08:00
|
|
|
|
// 添加事件拦截,防止点击穿透到 3D 引擎
|
|
|
|
|
|
this.setupEventInterception(this.container);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-12-22 18:48:38 +08:00
|
|
|
|
* 设置事件拦截,防止事件冒泡到下层元素(如 3D 引擎)
|
2025-12-16 11:57:44 +08:00
|
|
|
|
*/
|
|
|
|
|
|
private setupEventInterception(el: HTMLElement): void {
|
|
|
|
|
|
const stopPropagation = (e: Event) => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const events = [
|
|
|
|
|
|
'click', 'dblclick', 'contextmenu', 'wheel',
|
|
|
|
|
|
'mousedown', 'mouseup', 'mousemove',
|
|
|
|
|
|
'touchstart', 'touchend', 'touchmove',
|
|
|
|
|
|
'pointerdown', 'pointerup', 'pointermove', 'pointerenter', 'pointerleave', 'pointerover', 'pointerout'
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
events.forEach(eventType => {
|
|
|
|
|
|
el.addEventListener(eventType, stopPropagation, { passive: false });
|
|
|
|
|
|
});
|
2025-12-04 15:24:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private updatePosition() {
|
|
|
|
|
|
const pos = this.options.position;
|
|
|
|
|
|
const style = this.container.style;
|
|
|
|
|
|
|
|
|
|
|
|
style.top = ''; style.bottom = ''; style.left = ''; style.right = ''; style.transform = '';
|
|
|
|
|
|
|
|
|
|
|
|
if (pos === 'static') {
|
|
|
|
|
|
this.container.classList.add('static');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.container.classList.remove('static');
|
|
|
|
|
|
this.container.style.position = 'absolute';
|
|
|
|
|
|
|
|
|
|
|
|
if (typeof pos === 'object' && 'x' in pos) {
|
|
|
|
|
|
style.left = `${pos.x}px`;
|
|
|
|
|
|
style.top = `${pos.y}px`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const margin = '20px';
|
|
|
|
|
|
switch (pos) {
|
|
|
|
|
|
case 'top-left':
|
|
|
|
|
|
style.top = margin; style.left = margin;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'top-center':
|
|
|
|
|
|
style.top = margin; style.left = '50%'; style.transform = 'translateX(-50%)';
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'top-right':
|
|
|
|
|
|
style.top = margin; style.right = margin;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'bottom-left':
|
|
|
|
|
|
style.bottom = margin; style.left = margin;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'bottom-center':
|
|
|
|
|
|
style.bottom = margin; style.left = '50%'; style.transform = 'translateX(-50%)';
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'bottom-right':
|
|
|
|
|
|
style.bottom = margin; style.right = margin;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'left-center':
|
|
|
|
|
|
style.left = margin; style.top = '50%'; style.transform = 'translateY(-50%)';
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'right-center':
|
|
|
|
|
|
style.right = margin; style.top = '50%'; style.transform = 'translateY(-50%)';
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'center':
|
|
|
|
|
|
style.top = '50%'; style.left = '50%'; style.transform = 'translate(-50%, -50%)';
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用样式到容器
|
|
|
|
|
|
*/
|
|
|
|
|
|
private applyStyles(): void {
|
|
|
|
|
|
const style = this.container.style;
|
|
|
|
|
|
if (this.options.backgroundColor) style.setProperty('--bim-btn-group-section-bg', this.options.backgroundColor);
|
|
|
|
|
|
if (this.options.btnBackgroundColor) style.setProperty('--bim-btn-bg', this.options.btnBackgroundColor);
|
|
|
|
|
|
if (this.options.btnHoverColor) style.setProperty('--bim-btn-hover-bg', this.options.btnHoverColor);
|
|
|
|
|
|
if (this.options.btnActiveColor) style.setProperty('--bim-btn-active-bg', this.options.btnActiveColor);
|
|
|
|
|
|
if (this.options.iconColor) style.setProperty('--bim-icon-color', this.options.iconColor);
|
|
|
|
|
|
if (this.options.iconActiveColor) style.setProperty('--bim-icon-active-color', this.options.iconActiveColor);
|
|
|
|
|
|
if (this.options.textColor) style.setProperty('--bim-btn-text-color', this.options.textColor);
|
|
|
|
|
|
if (this.options.textActiveColor) style.setProperty('--bim-btn-text-active-color', this.options.textActiveColor);
|
2025-12-24 19:02:34 +08:00
|
|
|
|
|
|
|
|
|
|
// 同步更新所有已存在的dropdown元素的CSS变量(dropdown被添加到body,无法继承容器的CSS变量)
|
|
|
|
|
|
const dropdowns = document.querySelectorAll('.opt-btn-dropdown');
|
|
|
|
|
|
dropdowns.forEach(dropdown => {
|
|
|
|
|
|
const dropdownStyle = (dropdown as HTMLElement).style;
|
|
|
|
|
|
if (this.options.iconColor) dropdownStyle.setProperty('--bim-icon-color', this.options.iconColor);
|
|
|
|
|
|
if (this.options.iconActiveColor) dropdownStyle.setProperty('--bim-icon-active-color', this.options.iconActiveColor);
|
|
|
|
|
|
if (this.options.textColor) dropdownStyle.setProperty('--bim-btn-text-color', this.options.textColor);
|
|
|
|
|
|
if (this.options.textActiveColor) dropdownStyle.setProperty('--bim-btn-text-active-color', this.options.textActiveColor);
|
|
|
|
|
|
if (this.options.btnBackgroundColor) dropdownStyle.setProperty('--bim-btn-bg', this.options.btnBackgroundColor);
|
|
|
|
|
|
if (this.options.btnHoverColor) dropdownStyle.setProperty('--bim-btn-hover-bg', this.options.btnHoverColor);
|
|
|
|
|
|
if (this.options.btnActiveColor) dropdownStyle.setProperty('--bim-btn-active-bg', this.options.btnActiveColor);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置主题的primary颜色(用于边框等)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private setPrimaryColor(color: string): void {
|
|
|
|
|
|
this.container.style.setProperty('--bim-primary-color', color);
|
|
|
|
|
|
|
|
|
|
|
|
// 同步更新所有dropdown(dropdown被添加到body,无法继承容器的CSS变量)
|
|
|
|
|
|
const dropdowns = document.querySelectorAll('.opt-btn-dropdown');
|
|
|
|
|
|
dropdowns.forEach(dropdown => {
|
|
|
|
|
|
(dropdown as HTMLElement).style.setProperty('--bim-primary-color', color);
|
|
|
|
|
|
});
|
2025-12-04 15:24:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置主题颜色
|
|
|
|
|
|
* 只会应用到没有被用户自定义的颜色属性上
|
|
|
|
|
|
*/
|
|
|
|
|
|
public setTheme(theme: ThemeConfig): void {
|
|
|
|
|
|
const themeColors: ButtonGroupColors = {
|
2026-01-21 15:50:07 +08:00
|
|
|
|
backgroundColor: theme.bgElevated,
|
|
|
|
|
|
btnBackgroundColor: theme.componentBg,
|
|
|
|
|
|
btnHoverColor: theme.componentBgHover,
|
|
|
|
|
|
btnActiveColor: theme.componentBgActive,
|
|
|
|
|
|
iconColor: theme.iconDefault,
|
2025-12-04 15:24:44 +08:00
|
|
|
|
iconActiveColor: theme.iconActive,
|
|
|
|
|
|
textColor: theme.textSecondary,
|
|
|
|
|
|
textActiveColor: theme.textPrimary
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Object.entries(themeColors).forEach(([key, value]) => {
|
|
|
|
|
|
const colorKey = key as keyof ButtonGroupColors;
|
|
|
|
|
|
if (!this.customColors.has(colorKey)) {
|
|
|
|
|
|
this.options[colorKey] = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-21 15:50:07 +08:00
|
|
|
|
this.container.classList.remove('theme-dark', 'theme-light');
|
|
|
|
|
|
this.container.classList.add(`theme-${theme.name}`);
|
|
|
|
|
|
|
2025-12-04 15:24:44 +08:00
|
|
|
|
this.applyStyles();
|
2025-12-24 19:02:34 +08:00
|
|
|
|
this.setPrimaryColor(theme.primary);
|
2026-01-21 15:50:07 +08:00
|
|
|
|
this.applyThemeCssVars(theme);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用主题系统的 CSS 变量到容器
|
|
|
|
|
|
* 供 glass-pill 等样式变体使用
|
|
|
|
|
|
*/
|
|
|
|
|
|
private applyThemeCssVars(theme: ThemeConfig): void {
|
|
|
|
|
|
const style = this.container.style;
|
|
|
|
|
|
|
|
|
|
|
|
style.setProperty('--bim-primary', theme.primary);
|
|
|
|
|
|
style.setProperty('--bim-primary-hover', theme.primaryHover);
|
|
|
|
|
|
style.setProperty('--bim-primary-active', theme.primaryActive);
|
|
|
|
|
|
|
|
|
|
|
|
style.setProperty('--bim-bg-glass', theme.bgGlass);
|
|
|
|
|
|
style.setProperty('--bim-bg-glass-blur', theme.bgGlassBlur);
|
|
|
|
|
|
style.setProperty('--bim-bg-elevated', theme.bgElevated);
|
|
|
|
|
|
style.setProperty('--bim-bg-overlay', theme.bgOverlay);
|
|
|
|
|
|
style.setProperty('--bim-bg-inset', theme.bgInset);
|
|
|
|
|
|
|
|
|
|
|
|
style.setProperty('--bim-text-primary', theme.textPrimary);
|
|
|
|
|
|
style.setProperty('--bim-text-secondary', theme.textSecondary);
|
|
|
|
|
|
style.setProperty('--bim-text-tertiary', theme.textTertiary);
|
|
|
|
|
|
style.setProperty('--bim-text-inverse', theme.textInverse);
|
|
|
|
|
|
|
|
|
|
|
|
style.setProperty('--bim-icon-default', theme.iconDefault);
|
|
|
|
|
|
style.setProperty('--bim-icon-hover', theme.iconHover);
|
|
|
|
|
|
style.setProperty('--bim-icon-active', theme.iconActive);
|
|
|
|
|
|
style.setProperty('--bim-icon-inverse', theme.iconInverse);
|
|
|
|
|
|
|
|
|
|
|
|
style.setProperty('--bim-border-default', theme.borderDefault);
|
|
|
|
|
|
style.setProperty('--bim-border-subtle', theme.borderSubtle);
|
|
|
|
|
|
|
|
|
|
|
|
style.setProperty('--bim-component-bg-hover', theme.componentBgHover);
|
|
|
|
|
|
style.setProperty('--bim-component-bg-active', theme.componentBgActive);
|
|
|
|
|
|
|
|
|
|
|
|
style.setProperty('--bim-shadow-sm', theme.shadowSm);
|
|
|
|
|
|
style.setProperty('--bim-shadow-md', theme.shadowMd);
|
|
|
|
|
|
style.setProperty('--bim-shadow-lg', theme.shadowLg);
|
|
|
|
|
|
style.setProperty('--bim-shadow-glow', theme.shadowGlow);
|
|
|
|
|
|
|
2026-01-21 16:46:14 +08:00
|
|
|
|
style.setProperty('--bim-floating-bg', theme.floatingBg);
|
|
|
|
|
|
style.setProperty('--bim-floating-border', theme.floatingBorder);
|
|
|
|
|
|
style.setProperty('--bim-floating-shadow', theme.floatingShadow);
|
|
|
|
|
|
style.setProperty('--bim-floating-btn-bg', theme.floatingBtnBg);
|
|
|
|
|
|
style.setProperty('--bim-floating-btn-border', theme.floatingBtnBorder);
|
|
|
|
|
|
style.setProperty('--bim-floating-btn-shadow', theme.floatingBtnShadow);
|
|
|
|
|
|
style.setProperty('--bim-floating-btn-bg-hover', theme.floatingBtnBgHover);
|
|
|
|
|
|
style.setProperty('--bim-floating-btn-shadow-hover', theme.floatingBtnShadowHover);
|
|
|
|
|
|
style.setProperty('--bim-floating-icon-color', theme.floatingIconColor);
|
|
|
|
|
|
style.setProperty('--bim-floating-icon-color-hover', theme.floatingIconColorHover);
|
2026-01-21 15:50:07 +08:00
|
|
|
|
|
|
|
|
|
|
this.syncDropdownCssVars(theme);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 同步 CSS 变量到所有 dropdown 元素
|
|
|
|
|
|
* dropdown 被添加到 body,无法继承容器的 CSS 变量
|
|
|
|
|
|
*/
|
|
|
|
|
|
private syncDropdownCssVars(theme: ThemeConfig): void {
|
|
|
|
|
|
const dropdowns = document.querySelectorAll('.opt-btn-dropdown');
|
|
|
|
|
|
dropdowns.forEach(dropdown => {
|
|
|
|
|
|
const style = (dropdown as HTMLElement).style;
|
|
|
|
|
|
|
|
|
|
|
|
style.setProperty('--bim-primary', theme.primary);
|
|
|
|
|
|
style.setProperty('--bim-bg-overlay', theme.bgOverlay);
|
2026-01-21 16:50:00 +08:00
|
|
|
|
style.setProperty('--bim-border-default', theme.borderDefault);
|
2026-01-21 15:50:07 +08:00
|
|
|
|
style.setProperty('--bim-text-primary', theme.textPrimary);
|
|
|
|
|
|
style.setProperty('--bim-text-inverse', theme.textInverse);
|
|
|
|
|
|
style.setProperty('--bim-shadow-lg', theme.shadowLg);
|
2026-01-21 16:50:00 +08:00
|
|
|
|
style.setProperty('--bim-floating-btn-bg', theme.floatingBtnBg);
|
|
|
|
|
|
style.setProperty('--bim-floating-btn-bg-hover', theme.floatingBtnBgHover);
|
|
|
|
|
|
style.setProperty('--bim-floating-icon-color', theme.floatingIconColor);
|
2026-01-21 15:50:07 +08:00
|
|
|
|
});
|
2025-12-04 15:24:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 直接设置颜色(强制覆盖)
|
|
|
|
|
|
* 设置的颜色会被标记为自定义,后续的 setTheme 不会覆盖它们
|
|
|
|
|
|
*/
|
|
|
|
|
|
public setColors(colors: ButtonGroupColors): void {
|
|
|
|
|
|
// 更新 options
|
|
|
|
|
|
this.options = { ...this.options, ...colors };
|
|
|
|
|
|
|
|
|
|
|
|
// 标记这些颜色为自定义
|
|
|
|
|
|
Object.keys(colors).forEach(key => {
|
|
|
|
|
|
this.customColors.add(key as keyof ButtonGroupColors);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.applyStyles();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async init(): Promise<void> {
|
|
|
|
|
|
this.render();
|
|
|
|
|
|
|
|
|
|
|
|
// 自动订阅语言变更
|
|
|
|
|
|
this.unsubscribeLocale = localeManager.subscribe(() => {
|
|
|
|
|
|
this.setLocales();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 自动订阅主题变更
|
|
|
|
|
|
this.unsubscribeTheme = themeManager.subscribe((theme) => {
|
|
|
|
|
|
this.setTheme(theme);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public setLocales(): void {
|
|
|
|
|
|
this.render();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public addGroup(groupId: string, beforeGroupId?: string): void {
|
|
|
|
|
|
if (this.groups.some(g => g.id === groupId)) return;
|
|
|
|
|
|
const newGroup: ButtonGroup = { id: groupId, buttons: [] };
|
|
|
|
|
|
if (beforeGroupId) {
|
|
|
|
|
|
const index = this.groups.findIndex(g => g.id === beforeGroupId);
|
|
|
|
|
|
index !== -1 ? this.groups.splice(index, 0, newGroup) : this.groups.push(newGroup);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.groups.push(newGroup);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public addButton(config: ButtonConfig): void {
|
|
|
|
|
|
const { groupId, parentId } = config;
|
|
|
|
|
|
const group = this.groups.find(g => g.id === groupId);
|
|
|
|
|
|
if (!group) return;
|
|
|
|
|
|
|
|
|
|
|
|
const button: OptButton = { ...config, children: config.children || [] };
|
|
|
|
|
|
if (parentId) {
|
|
|
|
|
|
const parentBtn = this.findButton(group.buttons, parentId);
|
|
|
|
|
|
if (parentBtn) {
|
|
|
|
|
|
if (!parentBtn.children) parentBtn.children = [];
|
|
|
|
|
|
parentBtn.children.push(button);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
group.buttons.push(button);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private findButton(buttons: OptButton[], id: string): OptButton | undefined {
|
|
|
|
|
|
for (const btn of buttons) {
|
|
|
|
|
|
if (btn.id === id) return btn;
|
|
|
|
|
|
if (btn.children) {
|
|
|
|
|
|
const found = this.findButton(btn.children, id);
|
|
|
|
|
|
if (found) return found;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public render(): void {
|
|
|
|
|
|
this.container.innerHTML = '';
|
|
|
|
|
|
this.btnRefs.clear();
|
|
|
|
|
|
|
|
|
|
|
|
this.groups.forEach((group, index) => {
|
|
|
|
|
|
const groupElement = this.renderGroup(group, index, this.groups.length);
|
|
|
|
|
|
this.container.appendChild(groupElement);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private renderGroup(group: ButtonGroup, index: number, total: number): HTMLElement {
|
|
|
|
|
|
const groupEl = document.createElement('div');
|
|
|
|
|
|
groupEl.className = 'bim-btn-group-section';
|
|
|
|
|
|
|
|
|
|
|
|
if (index < total - 1) {
|
|
|
|
|
|
groupEl.classList.add('has-divider');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
group.buttons.forEach(button => {
|
|
|
|
|
|
if (this.isVisible(button.id)) {
|
|
|
|
|
|
const btnWrapper = this.renderButton(button);
|
|
|
|
|
|
groupEl.appendChild(btnWrapper);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
return groupEl;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private renderButton(button: OptButton): HTMLElement {
|
|
|
|
|
|
const wrapper = document.createElement('div');
|
|
|
|
|
|
wrapper.className = 'opt-btn-wrapper';
|
|
|
|
|
|
|
|
|
|
|
|
const btnEl = document.createElement('div');
|
|
|
|
|
|
btnEl.className = 'opt-btn';
|
|
|
|
|
|
|
2025-12-22 18:48:38 +08:00
|
|
|
|
// 初始化时根据 button 自身的属性同步 active 状态
|
|
|
|
|
|
if (button.isActive) {
|
|
|
|
|
|
this.activeBtnIds.add(button.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 15:24:44 +08:00
|
|
|
|
// 按钮优先使用自己的 align,否则使用全局配置,默认为 vertical
|
|
|
|
|
|
const align = button.align || this.options.align || 'vertical';
|
|
|
|
|
|
if (align === 'horizontal') {
|
|
|
|
|
|
btnEl.classList.add('align-horizontal');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
btnEl.classList.add('align-vertical');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (this.activeBtnIds.has(button.id)) btnEl.classList.add('active');
|
|
|
|
|
|
if (button.disabled) btnEl.classList.add('disabled');
|
|
|
|
|
|
|
|
|
|
|
|
// 判断是否显示 label
|
|
|
|
|
|
const hasLabel = this.options.showLabel && button.label;
|
|
|
|
|
|
if (!hasLabel) {
|
|
|
|
|
|
btnEl.classList.add('no-label');
|
2025-12-24 19:02:34 +08:00
|
|
|
|
// 当不显示 label 时,添加 title 属性作为 tooltip
|
|
|
|
|
|
if (button.label) {
|
|
|
|
|
|
btnEl.title = t(button.label);
|
|
|
|
|
|
}
|
2025-12-04 15:24:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 应用按钮的自定义样式
|
|
|
|
|
|
const iconSize = button.iconSize || 32;
|
|
|
|
|
|
const minWidth = button.minWidth || 50;
|
|
|
|
|
|
btnEl.style.minWidth = `${minWidth}px`;
|
|
|
|
|
|
|
|
|
|
|
|
const icon = document.createElement('div');
|
|
|
|
|
|
icon.className = 'opt-btn-icon';
|
|
|
|
|
|
icon.style.width = `${iconSize}px`;
|
|
|
|
|
|
icon.style.height = `${iconSize}px`;
|
|
|
|
|
|
icon.innerHTML = this.getIcon(button.icon);
|
|
|
|
|
|
btnEl.appendChild(icon);
|
|
|
|
|
|
|
|
|
|
|
|
// 创建文字和箭头的容器,确保它们始终在一起(无论主轴是横是竖)
|
|
|
|
|
|
const textWrapper = document.createElement('div');
|
|
|
|
|
|
textWrapper.className = 'opt-btn-text-wrapper';
|
|
|
|
|
|
|
|
|
|
|
|
if (this.options.showLabel && button.label) {
|
|
|
|
|
|
const label = document.createElement('span');
|
|
|
|
|
|
label.className = 'opt-btn-label';
|
|
|
|
|
|
label.textContent = t(button.label);
|
|
|
|
|
|
textWrapper.appendChild(label);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 15:50:07 +08:00
|
|
|
|
// TODO: 暂时隐藏下拉箭头
|
|
|
|
|
|
// if (button.children && button.children.length > 0) {
|
|
|
|
|
|
// const arrow = document.createElement('span');
|
|
|
|
|
|
// arrow.className = 'opt-btn-arrow';
|
|
|
|
|
|
// arrow.textContent = '▼';
|
|
|
|
|
|
// textWrapper.appendChild(arrow);
|
|
|
|
|
|
// }
|
2025-12-04 15:24:44 +08:00
|
|
|
|
|
|
|
|
|
|
// 只有当有内容时才添加 wrapper
|
|
|
|
|
|
if (textWrapper.hasChildNodes()) {
|
|
|
|
|
|
btnEl.appendChild(textWrapper);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
btnEl.addEventListener('click', () => this.handleClick(button));
|
|
|
|
|
|
btnEl.addEventListener('mouseenter', () => this.handleMouseEnter(button, btnEl));
|
|
|
|
|
|
btnEl.addEventListener('mouseleave', () => this.handleMouseLeave());
|
|
|
|
|
|
|
|
|
|
|
|
this.btnRefs.set(button.id, btnEl);
|
|
|
|
|
|
wrapper.appendChild(btnEl);
|
|
|
|
|
|
return wrapper;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 18:48:38 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 设置按钮的激活状态
|
|
|
|
|
|
* @param id 按钮 ID
|
|
|
|
|
|
* @param active 可选,如果不传则切换(toggle)当前状态
|
|
|
|
|
|
*/
|
|
|
|
|
|
public setBtnActive(id: string, active?: boolean): void {
|
|
|
|
|
|
const button = this.findButtonById(id);
|
|
|
|
|
|
if (!button) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 确定最终状态
|
|
|
|
|
|
const newState = active !== undefined ? active : !this.activeBtnIds.has(id);
|
|
|
|
|
|
|
|
|
|
|
|
if (newState) {
|
|
|
|
|
|
this.activeBtnIds.add(id);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.activeBtnIds.delete(id);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 同步对象状态并更新 DOM
|
|
|
|
|
|
button.isActive = newState;
|
|
|
|
|
|
this.updateButtonState(id);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 15:24:44 +08:00
|
|
|
|
private handleClick(button: OptButton): void {
|
|
|
|
|
|
if (button.disabled) return;
|
|
|
|
|
|
if (!button.children || button.children.length === 0) {
|
|
|
|
|
|
if (button.keepActive) {
|
2025-12-24 19:02:34 +08:00
|
|
|
|
// 1) 先切换自身激活状态(onClick 里通常会根据 isActive 决定“打开/关闭”逻辑)
|
|
|
|
|
|
const wasActive = this.activeBtnIds.has(button.id);
|
|
|
|
|
|
const newState = !wasActive;
|
|
|
|
|
|
this.setBtnActive(button.id, newState);
|
|
|
|
|
|
|
|
|
|
|
|
// 2) 互斥逻辑:仅在“本次切换为激活”时触发
|
|
|
|
|
|
// - 一级按钮:同 groupId 下其它一级按钮互斥
|
|
|
|
|
|
// - 二级按钮:同 groupId + 同 parentId 下其它二级按钮互斥
|
|
|
|
|
|
// - 被关闭的按钮需要触发 onClick(用于执行关闭逻辑)
|
|
|
|
|
|
if (newState && button.exclusive && button.groupId) {
|
|
|
|
|
|
this.deactivateExclusiveSiblings(button);
|
|
|
|
|
|
}
|
2025-12-04 15:24:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
this.closeDropdown();
|
|
|
|
|
|
if (button.onClick) button.onClick(button);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-24 19:02:34 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 互斥关闭同范围内的其它已激活按钮,并触发它们的 onClick
|
|
|
|
|
|
* @param button 当前被激活的按钮
|
|
|
|
|
|
*/
|
|
|
|
|
|
private deactivateExclusiveSiblings(button: OptButton): void {
|
|
|
|
|
|
const group = this.groups.find(g => g.id === button.groupId);
|
|
|
|
|
|
if (!group) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 二级按钮:同 groupId + 同 parentId
|
|
|
|
|
|
if (button.parentId) {
|
|
|
|
|
|
const parent = this.findButton(group.buttons, button.parentId);
|
|
|
|
|
|
const siblings = parent?.children || [];
|
|
|
|
|
|
for (const sib of siblings) {
|
|
|
|
|
|
if (!sib) continue;
|
|
|
|
|
|
if (sib.id === button.id) continue;
|
|
|
|
|
|
if (sib.parentId !== button.parentId) continue;
|
|
|
|
|
|
if (sib.groupId !== button.groupId) continue;
|
|
|
|
|
|
|
|
|
|
|
|
if (this.activeBtnIds.has(sib.id)) {
|
|
|
|
|
|
this.setBtnActive(sib.id, false);
|
|
|
|
|
|
// 触发被关闭按钮的 onClick(此时 sib.isActive 已经同步为 false)
|
|
|
|
|
|
if (sib.onClick) sib.onClick(sib);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 一级按钮:同 groupId 下其它一级按钮(不包含二级)
|
|
|
|
|
|
for (const sib of group.buttons) {
|
|
|
|
|
|
if (sib.id === button.id) continue;
|
|
|
|
|
|
if (sib.groupId !== button.groupId) continue;
|
|
|
|
|
|
if (sib.parentId) continue; // 只处理一级按钮
|
|
|
|
|
|
|
|
|
|
|
|
if (this.activeBtnIds.has(sib.id)) {
|
|
|
|
|
|
this.setBtnActive(sib.id, false);
|
|
|
|
|
|
if (sib.onClick) sib.onClick(sib);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 15:24:44 +08:00
|
|
|
|
private handleMouseEnter(button: OptButton, btnEl: HTMLElement): void {
|
|
|
|
|
|
if (this.hoverTimeout) clearTimeout(this.hoverTimeout);
|
|
|
|
|
|
if (button.children && button.children.length > 0) {
|
|
|
|
|
|
this.showDropdown(button, btnEl);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.closeDropdown();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private handleMouseLeave(): void {
|
|
|
|
|
|
this.hoverTimeout = window.setTimeout(() => this.closeDropdown(), 200);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private showDropdown(button: OptButton, btnEl: HTMLElement): void {
|
|
|
|
|
|
this.closeDropdown();
|
|
|
|
|
|
if (!button.children) return;
|
|
|
|
|
|
|
|
|
|
|
|
const dropdown = document.createElement('div');
|
|
|
|
|
|
dropdown.className = 'opt-btn-dropdown';
|
|
|
|
|
|
if (this.options.backgroundColor) dropdown.style.setProperty('--bim-toolbar-bg', this.options.backgroundColor);
|
|
|
|
|
|
|
2025-12-24 19:02:34 +08:00
|
|
|
|
// 将主题CSS变量复制到dropdown元素上(因为dropdown被添加到body,无法继承容器的CSS变量)
|
|
|
|
|
|
const dropdownStyle = dropdown.style;
|
|
|
|
|
|
if (this.options.iconColor) dropdownStyle.setProperty('--bim-icon-color', this.options.iconColor);
|
|
|
|
|
|
if (this.options.iconActiveColor) dropdownStyle.setProperty('--bim-icon-active-color', this.options.iconActiveColor);
|
|
|
|
|
|
if (this.options.textColor) dropdownStyle.setProperty('--bim-btn-text-color', this.options.textColor);
|
|
|
|
|
|
if (this.options.textActiveColor) dropdownStyle.setProperty('--bim-btn-text-active-color', this.options.textActiveColor);
|
|
|
|
|
|
if (this.options.btnBackgroundColor) dropdownStyle.setProperty('--bim-btn-bg', this.options.btnBackgroundColor);
|
|
|
|
|
|
if (this.options.btnHoverColor) dropdownStyle.setProperty('--bim-btn-hover-bg', this.options.btnHoverColor);
|
|
|
|
|
|
if (this.options.btnActiveColor) dropdownStyle.setProperty('--bim-btn-active-bg', this.options.btnActiveColor);
|
|
|
|
|
|
|
2025-12-04 15:24:44 +08:00
|
|
|
|
// 获取按钮的位置信息
|
|
|
|
|
|
const btnRect = btnEl.getBoundingClientRect();
|
|
|
|
|
|
const expand = this.options.expand || 'down';
|
|
|
|
|
|
|
|
|
|
|
|
// 根据主按钮组的方向设置下拉菜单的布局方向
|
|
|
|
|
|
if (this.options.direction === 'row') {
|
|
|
|
|
|
dropdown.style.flexDirection = 'column'; // 横向按钮组,菜单纵向排列
|
|
|
|
|
|
} else {
|
|
|
|
|
|
dropdown.style.flexDirection = 'row'; // 纵向按钮组,菜单横向排列
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 15:50:07 +08:00
|
|
|
|
if (this.options.type && this.options.type !== 'default') {
|
|
|
|
|
|
dropdown.classList.add(`type-${this.options.type}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const currentTheme = themeManager.getTheme();
|
|
|
|
|
|
dropdown.classList.add(`theme-${currentTheme.name}`);
|
|
|
|
|
|
|
2026-01-21 16:50:00 +08:00
|
|
|
|
dropdownStyle.setProperty('--bim-primary', currentTheme.primary);
|
|
|
|
|
|
dropdownStyle.setProperty('--bim-bg-overlay', currentTheme.bgOverlay);
|
|
|
|
|
|
dropdownStyle.setProperty('--bim-border-default', currentTheme.borderDefault);
|
|
|
|
|
|
dropdownStyle.setProperty('--bim-text-primary', currentTheme.textPrimary);
|
|
|
|
|
|
dropdownStyle.setProperty('--bim-text-inverse', currentTheme.textInverse);
|
|
|
|
|
|
dropdownStyle.setProperty('--bim-shadow-lg', currentTheme.shadowLg);
|
|
|
|
|
|
dropdownStyle.setProperty('--bim-floating-btn-bg', currentTheme.floatingBtnBg);
|
|
|
|
|
|
dropdownStyle.setProperty('--bim-floating-btn-bg-hover', currentTheme.floatingBtnBgHover);
|
|
|
|
|
|
dropdownStyle.setProperty('--bim-floating-icon-color', currentTheme.floatingIconColor);
|
|
|
|
|
|
|
2025-12-04 15:24:44 +08:00
|
|
|
|
document.body.appendChild(dropdown);
|
2025-12-22 18:48:38 +08:00
|
|
|
|
|
2025-12-16 11:57:44 +08:00
|
|
|
|
// 添加事件拦截
|
|
|
|
|
|
this.setupEventInterception(dropdown);
|
2025-12-04 15:24:44 +08:00
|
|
|
|
|
|
|
|
|
|
// 添加菜单项
|
|
|
|
|
|
button.children.forEach(subBtn => {
|
|
|
|
|
|
if (this.isVisible(subBtn.id)) {
|
|
|
|
|
|
const item = this.renderDropdownItem(subBtn);
|
|
|
|
|
|
dropdown.appendChild(item);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 获取下拉菜单的实际尺寸
|
|
|
|
|
|
const dropdownRect = dropdown.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
|
|
if (expand === 'up') {
|
|
|
|
|
|
// 向上展开,与按钮水平居中对齐
|
|
|
|
|
|
dropdown.style.bottom = (window.innerHeight - btnRect.top + 8) + 'px';
|
|
|
|
|
|
dropdown.style.left = (btnRect.left + (btnRect.width - dropdownRect.width) / 2) + 'px';
|
|
|
|
|
|
} else if (expand === 'down') {
|
|
|
|
|
|
// 向下展开,与按钮水平居中对齐
|
|
|
|
|
|
dropdown.style.top = (btnRect.bottom + 8) + 'px';
|
|
|
|
|
|
dropdown.style.left = (btnRect.left + (btnRect.width - dropdownRect.width) / 2) + 'px';
|
|
|
|
|
|
} else if (expand === 'right') {
|
|
|
|
|
|
// 向右展开,与按钮垂直居中对齐
|
|
|
|
|
|
dropdown.style.top = (btnRect.top + (btnRect.height - dropdownRect.height) / 2) + 'px';
|
|
|
|
|
|
dropdown.style.left = (btnRect.right + 8) + 'px';
|
|
|
|
|
|
} else if (expand === 'left') {
|
|
|
|
|
|
// 向左展开,与按钮垂直居中对齐
|
|
|
|
|
|
dropdown.style.top = (btnRect.top + (btnRect.height - dropdownRect.height) / 2) + 'px';
|
|
|
|
|
|
dropdown.style.right = (window.innerWidth - btnRect.left + 8) + 'px';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
dropdown.addEventListener('mouseenter', () => { if (this.hoverTimeout) clearTimeout(this.hoverTimeout); });
|
|
|
|
|
|
dropdown.addEventListener('mouseleave', () => this.handleMouseLeave());
|
|
|
|
|
|
this.dropdownElement = dropdown;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private renderDropdownItem(button: OptButton): HTMLElement {
|
|
|
|
|
|
const item = document.createElement('div');
|
|
|
|
|
|
item.className = 'opt-btn-dropdown-item';
|
|
|
|
|
|
|
|
|
|
|
|
// 应用按钮的 align 设置,默认为 horizontal(图标在左)
|
|
|
|
|
|
const align = button.align || 'horizontal';
|
|
|
|
|
|
if (align === 'horizontal') {
|
|
|
|
|
|
item.classList.add('align-horizontal');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
item.classList.add('align-vertical');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-24 19:02:34 +08:00
|
|
|
|
// 二级菜单项的 active 状态渲染(修复 keepActive 在二级按钮“看起来不生效”的问题)
|
|
|
|
|
|
// 说明:
|
|
|
|
|
|
// - keepActive 的状态会记录在 activeBtnIds / button.isActive 上
|
|
|
|
|
|
// - 下拉菜单每次打开都会重新渲染,因此必须在这里同步一次 active 样式
|
|
|
|
|
|
if (this.activeBtnIds.has(button.id) || button.isActive) {
|
|
|
|
|
|
item.classList.add('active');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 15:24:44 +08:00
|
|
|
|
// 应用按钮的自定义样式
|
|
|
|
|
|
const iconSize = button.iconSize || 32; // 二级菜单默认图标更小
|
|
|
|
|
|
const minWidth = button.minWidth; // 不设置默认值,让下拉菜单项保持紧凑
|
|
|
|
|
|
if (minWidth) {
|
|
|
|
|
|
item.style.minWidth = `${minWidth}px`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const icon = document.createElement('div');
|
|
|
|
|
|
icon.className = 'opt-btn-icon';
|
|
|
|
|
|
icon.style.width = `${iconSize}px`;
|
|
|
|
|
|
icon.style.height = `${iconSize}px`;
|
|
|
|
|
|
icon.innerHTML = this.getIcon(button.icon);
|
|
|
|
|
|
item.appendChild(icon);
|
|
|
|
|
|
|
|
|
|
|
|
// 只有在 showLabel 为 true 时才显示 label
|
|
|
|
|
|
if (this.options.showLabel && button.label) {
|
|
|
|
|
|
const label = document.createElement('span');
|
|
|
|
|
|
label.className = 'opt-btn-dropdown-label';
|
|
|
|
|
|
label.textContent = t(button.label);
|
|
|
|
|
|
item.appendChild(label);
|
2025-12-24 19:02:34 +08:00
|
|
|
|
} else if (button.label) {
|
|
|
|
|
|
// 当不显示 label 时,添加 title 属性作为 tooltip
|
|
|
|
|
|
item.title = t(button.label);
|
2025-12-04 15:24:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
item.addEventListener('click', (e) => { e.stopPropagation(); this.handleClick(button); });
|
|
|
|
|
|
return item;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private closeDropdown(): void {
|
|
|
|
|
|
if (this.dropdownElement) {
|
|
|
|
|
|
this.dropdownElement.remove();
|
|
|
|
|
|
this.dropdownElement = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.btnRefs.forEach(btnEl => {
|
|
|
|
|
|
const arrow = btnEl.querySelector('.opt-btn-arrow');
|
|
|
|
|
|
if (arrow) arrow.classList.remove('rotated');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private updateButtonState(buttonId: string): void {
|
|
|
|
|
|
const btnEl = this.btnRefs.get(buttonId);
|
|
|
|
|
|
if (btnEl) {
|
2025-12-22 18:48:38 +08:00
|
|
|
|
if (this.activeBtnIds.has(buttonId)) {
|
|
|
|
|
|
btnEl.classList.add('active');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
btnEl.classList.remove('active');
|
|
|
|
|
|
}
|
2025-12-04 15:24:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private getIcon(icon?: string): string { return icon || this.DEFAULT_ICON; }
|
2025-12-22 18:48:38 +08:00
|
|
|
|
|
2025-12-04 15:24:44 +08:00
|
|
|
|
public updateButtonVisibility(id: string, visible: boolean): void {
|
|
|
|
|
|
if (!this.options.visibility) this.options.visibility = {};
|
|
|
|
|
|
this.options.visibility[id] = visible;
|
|
|
|
|
|
this.render();
|
|
|
|
|
|
}
|
2025-12-22 18:48:38 +08:00
|
|
|
|
|
2025-12-04 15:24:44 +08:00
|
|
|
|
public setShowLabel(show: boolean): void {
|
|
|
|
|
|
this.options.showLabel = show;
|
|
|
|
|
|
this.updateLabelsVisibility();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private updateLabelsVisibility(): void {
|
|
|
|
|
|
this.btnRefs.forEach((btnEl, buttonId) => {
|
|
|
|
|
|
// 查找按钮配置
|
|
|
|
|
|
const button = this.findButtonById(buttonId);
|
|
|
|
|
|
if (!button) return;
|
|
|
|
|
|
|
|
|
|
|
|
const hasLabel = this.options.showLabel && button.label;
|
|
|
|
|
|
|
2025-12-24 19:02:34 +08:00
|
|
|
|
// 更新 no-label 类和 title 属性
|
2025-12-04 15:24:44 +08:00
|
|
|
|
if (hasLabel) {
|
|
|
|
|
|
btnEl.classList.remove('no-label');
|
2025-12-24 19:02:34 +08:00
|
|
|
|
// 显示标签时,移除 title(避免重复显示)
|
|
|
|
|
|
btnEl.removeAttribute('title');
|
2025-12-04 15:24:44 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
btnEl.classList.add('no-label');
|
2025-12-24 19:02:34 +08:00
|
|
|
|
// 隐藏标签时,添加 title 作为 tooltip
|
|
|
|
|
|
if (button.label) {
|
|
|
|
|
|
btnEl.title = t(button.label);
|
|
|
|
|
|
}
|
2025-12-04 15:24:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private findButtonById(id: string): OptButton | undefined {
|
|
|
|
|
|
for (const group of this.groups) {
|
|
|
|
|
|
const found = this.findButton(group.buttons, id);
|
|
|
|
|
|
if (found) return found;
|
|
|
|
|
|
}
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
2025-12-22 18:48:38 +08:00
|
|
|
|
|
2025-12-04 15:24:44 +08:00
|
|
|
|
public setBackgroundColor(color: string): void { this.setColors({ backgroundColor: color }); }
|
|
|
|
|
|
private isVisible(id: string): boolean { return this.options.visibility?.[id] !== false; }
|
2025-12-22 18:48:38 +08:00
|
|
|
|
|
2026-01-28 12:00:55 +08:00
|
|
|
|
public setType(type: 'default' | 'glass-pill'): void {
|
|
|
|
|
|
this.container.classList.remove('type-default', 'type-glass-pill');
|
|
|
|
|
|
this.options.type = type;
|
|
|
|
|
|
|
|
|
|
|
|
if (type && type !== 'default') {
|
|
|
|
|
|
this.container.classList.add(`type-${type}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.render();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public getType(): 'default' | 'glass-pill' {
|
|
|
|
|
|
return this.options.type || 'default';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 15:24:44 +08:00
|
|
|
|
public destroy(): void {
|
|
|
|
|
|
if (this.unsubscribeLocale) {
|
|
|
|
|
|
this.unsubscribeLocale();
|
|
|
|
|
|
this.unsubscribeLocale = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.unsubscribeTheme) {
|
|
|
|
|
|
this.unsubscribeTheme();
|
|
|
|
|
|
this.unsubscribeTheme = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.closeDropdown();
|
|
|
|
|
|
this.container.innerHTML = '';
|
|
|
|
|
|
this.btnRefs.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|