475 lines
18 KiB
TypeScript
475 lines
18 KiB
TypeScript
|
|
import './index.css';
|
|||
|
|
import type { RadialToolbarOptions, RadialMenuItem } from './types';
|
|||
|
|
import type { ThemeConfig } from '../../themes/types';
|
|||
|
|
import { IBimComponent } from '../../types/component';
|
|||
|
|
import { getIcon } from '../../utils/icon-manager';
|
|||
|
|
import { themeManager } from '../../services/theme';
|
|||
|
|
import { localeManager, t } from '../../services/locale';
|
|||
|
|
|
|||
|
|
export class RadialToolbar implements IBimComponent {
|
|||
|
|
private container: HTMLElement;
|
|||
|
|
private wrapper: HTMLElement;
|
|||
|
|
private mainButton: HTMLButtonElement;
|
|||
|
|
private items: RadialMenuItem[] = [];
|
|||
|
|
private itemElements: HTMLButtonElement[] = [];
|
|||
|
|
private itemButtonMap: Map<string, HTMLButtonElement> = new Map();
|
|||
|
|
private toggleStateMap: Map<string, boolean> = new Map();
|
|||
|
|
private isActive = false;
|
|||
|
|
private timer: number | null = null;
|
|||
|
|
private unsubscribeTheme: (() => void) | null = null;
|
|||
|
|
private unsubscribeLocale: (() => void) | null = null;
|
|||
|
|
private readonly mainButtonLabel: string;
|
|||
|
|
private readonly onMainButtonClick?: () => void;
|
|||
|
|
private pointerX = Number.NaN;
|
|||
|
|
private pointerY = Number.NaN;
|
|||
|
|
private maxInteractiveRadius = 220;
|
|||
|
|
|
|||
|
|
private readonly closeDelay: number;
|
|||
|
|
private readonly itemsPerRing: number;
|
|||
|
|
|
|||
|
|
// 主按钮直径(px)
|
|||
|
|
private readonly MAIN_BUTTON_SIZE = 60;
|
|||
|
|
// 子按钮直径(px)
|
|||
|
|
private readonly SUB_BUTTON_SIZE = 46;
|
|||
|
|
// 第一环“子按钮中心”到“主按钮中心”的距离(px)
|
|||
|
|
// 不做防重叠兜底,允许在小半径时出现重叠。
|
|||
|
|
private readonly BASE_RADIUS = 80;
|
|||
|
|
// 多环时,相邻两环的中心半径差(px)
|
|||
|
|
private readonly RING_GAP = 40;
|
|||
|
|
// 扇形展开角度范围:当前 180~270(实际就是 90 度)
|
|||
|
|
private readonly FAN_START_DEG = 170;
|
|||
|
|
private readonly FAN_END_DEG = 280;
|
|||
|
|
// 扇形边缘留白(px),防止按钮贴边或裁切
|
|||
|
|
private readonly CANVAS_PADDING = 28;
|
|||
|
|
|
|||
|
|
constructor(options: RadialToolbarOptions) {
|
|||
|
|
this.container = options.container;
|
|||
|
|
this.items = options.items === undefined ? this.createDefaultItems() : [...options.items];
|
|||
|
|
this.mainButtonLabel = options.mainButtonLabel ?? 'toolbar.home';
|
|||
|
|
this.onMainButtonClick = options.onMainButtonClick;
|
|||
|
|
this.itemsPerRing = Math.max(3, options.itemsPerRing ?? 5);
|
|||
|
|
this.closeDelay = Math.max(100, options.closeDelay ?? 260);
|
|||
|
|
|
|||
|
|
this.wrapper = this.createWrapper();
|
|||
|
|
this.mainButton = this.createMainButton(options.mainButtonIcon);
|
|||
|
|
this.wrapper.appendChild(this.mainButton);
|
|||
|
|
|
|||
|
|
this.container.appendChild(this.wrapper);
|
|||
|
|
this.renderItems();
|
|||
|
|
this.updateLayoutMetrics();
|
|||
|
|
this.bindEvents();
|
|||
|
|
|
|||
|
|
this.setTheme(themeManager.getTheme());
|
|||
|
|
this.updateLocales();
|
|||
|
|
this.unsubscribeTheme = themeManager.subscribe((theme) => {
|
|||
|
|
this.setTheme(theme);
|
|||
|
|
});
|
|||
|
|
this.unsubscribeLocale = localeManager.subscribe(() => {
|
|||
|
|
this.updateLocales();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private createDefaultItems(): RadialMenuItem[] {
|
|||
|
|
return [
|
|||
|
|
{ id: 'zoom', label: 'toolbar.zoomBox', icon: getIcon('框选放大') },
|
|||
|
|
{ id: 'measure', label: 'toolbar.measure', icon: getIcon('测量') },
|
|||
|
|
{ id: 'section', label: 'toolbar.section', icon: getIcon('剖切') },
|
|||
|
|
{ id: 'walk', label: 'toolbar.walk', icon: getIcon('漫游') },
|
|||
|
|
{ id: 'setting', label: 'toolbar.setting', icon: getIcon('设置') }
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private createWrapper(): HTMLElement {
|
|||
|
|
const el = document.createElement('div');
|
|||
|
|
el.className = 'radial-toolbar-wrapper';
|
|||
|
|
return el;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private createMainButton(icon?: string): HTMLButtonElement {
|
|||
|
|
const btn = document.createElement('button');
|
|||
|
|
btn.className = 'radial-main-btn';
|
|||
|
|
btn.type = 'button';
|
|||
|
|
btn.innerHTML = icon ?? getIcon('主视角');
|
|||
|
|
return btn;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private bindEvents(): void {
|
|||
|
|
this.mainButton.addEventListener('mouseenter', this.handlePointerEnter);
|
|||
|
|
this.mainButton.addEventListener('mouseleave', this.handlePointerLeave);
|
|||
|
|
this.mainButton.addEventListener('click', this.handleMainButtonClick);
|
|||
|
|
document.addEventListener('click', this.handleDocumentClick);
|
|||
|
|
document.addEventListener('mousemove', this.handleDocumentMouseMove);
|
|||
|
|
document.addEventListener('mouseleave', this.handleDocumentMouseLeave);
|
|||
|
|
document.addEventListener('visibilitychange', this.handleVisibilityChange);
|
|||
|
|
window.addEventListener('blur', this.handleWindowBlur);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private readonly handleDocumentMouseMove = (event: MouseEvent): void => {
|
|||
|
|
this.pointerX = event.clientX;
|
|||
|
|
this.pointerY = event.clientY;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
private readonly handleDocumentMouseLeave = (): void => {
|
|||
|
|
this.pointerX = Number.NaN;
|
|||
|
|
this.pointerY = Number.NaN;
|
|||
|
|
this.collapse();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
private readonly handleVisibilityChange = (): void => {
|
|||
|
|
if (!document.hidden) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
this.pointerX = Number.NaN;
|
|||
|
|
this.pointerY = Number.NaN;
|
|||
|
|
this.collapse();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
private readonly handleWindowBlur = (): void => {
|
|||
|
|
this.pointerX = Number.NaN;
|
|||
|
|
this.pointerY = Number.NaN;
|
|||
|
|
this.collapse();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
private readonly handlePointerEnter = (): void => {
|
|||
|
|
if (this.timer) {
|
|||
|
|
clearTimeout(this.timer);
|
|||
|
|
this.timer = null;
|
|||
|
|
}
|
|||
|
|
this.expand();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
private readonly handlePointerLeave = (event: MouseEvent): void => {
|
|||
|
|
const relatedTarget = event.relatedTarget;
|
|||
|
|
if (relatedTarget instanceof Node && this.wrapper.contains(relatedTarget)) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
this.scheduleCollapse();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
private readonly handleMainButtonClick = (event: MouseEvent): void => {
|
|||
|
|
event.stopPropagation();
|
|||
|
|
if (this.onMainButtonClick) {
|
|||
|
|
this.onMainButtonClick();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (this.isActive) {
|
|||
|
|
this.collapse();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
this.expand();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
private readonly handleDocumentClick = (event: MouseEvent): void => {
|
|||
|
|
const target = event.target;
|
|||
|
|
if (!(target instanceof Node)) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (!this.wrapper.contains(target)) {
|
|||
|
|
this.collapse();
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
private renderItems(): void {
|
|||
|
|
this.itemElements.forEach((el) => el.remove());
|
|||
|
|
this.itemElements = [];
|
|||
|
|
this.itemButtonMap.clear();
|
|||
|
|
this.toggleStateMap.clear();
|
|||
|
|
|
|||
|
|
this.items.forEach((item, index) => {
|
|||
|
|
this.toggleStateMap.set(item.id, Boolean(item.isActive));
|
|||
|
|
const btn = this.createSubButton(item, index);
|
|||
|
|
this.wrapper.insertBefore(btn, this.mainButton);
|
|||
|
|
this.itemElements.push(btn);
|
|||
|
|
this.itemButtonMap.set(item.id, btn);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.updateItemPositions();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private createSubButton(item: RadialMenuItem, index: number): HTMLButtonElement {
|
|||
|
|
const btn = document.createElement('button');
|
|||
|
|
btn.className = 'radial-sub-btn';
|
|||
|
|
btn.type = 'button';
|
|||
|
|
btn.dataset.index = String(index);
|
|||
|
|
this.applyItemActiveClass(btn, item);
|
|||
|
|
|
|||
|
|
const iconContainer = document.createElement('span');
|
|||
|
|
iconContainer.className = 'radial-sub-btn-icon';
|
|||
|
|
if (item.icon) {
|
|||
|
|
iconContainer.innerHTML = item.icon;
|
|||
|
|
} else {
|
|||
|
|
iconContainer.textContent = this.getFallbackLabel(item.label);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
btn.appendChild(iconContainer);
|
|||
|
|
btn.addEventListener('mouseenter', this.handlePointerEnter);
|
|||
|
|
btn.addEventListener('mouseleave', this.handlePointerLeave);
|
|||
|
|
btn.addEventListener('click', (event) => {
|
|||
|
|
event.stopPropagation();
|
|||
|
|
if (item.isToggle) {
|
|||
|
|
const current = this.toggleStateMap.get(item.id) ?? false;
|
|||
|
|
const next = !current;
|
|||
|
|
this.toggleStateMap.set(item.id, next);
|
|||
|
|
item.isActive = next;
|
|||
|
|
this.applyItemActiveClass(btn, item);
|
|||
|
|
item.onToggle?.(next, item);
|
|||
|
|
this.collapse();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
item.onClick?.(item);
|
|||
|
|
this.collapse();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return btn;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private updateItemPositions(): void {
|
|||
|
|
const total = this.itemElements.length;
|
|||
|
|
const fanSpan = this.FAN_END_DEG - this.FAN_START_DEG;
|
|||
|
|
|
|||
|
|
this.itemElements.forEach((btn, globalIndex) => {
|
|||
|
|
const ringIndex = Math.floor(globalIndex / this.itemsPerRing);
|
|||
|
|
const ringStart = ringIndex * this.itemsPerRing;
|
|||
|
|
const ringCount = Math.min(this.itemsPerRing, total - ringStart);
|
|||
|
|
const ringLocalIndex = globalIndex - ringStart;
|
|||
|
|
const ratio = ringCount === 1 ? 0.5 : ringLocalIndex / (ringCount - 1);
|
|||
|
|
const angleDeg = this.FAN_START_DEG + fanSpan * ratio;
|
|||
|
|
const angleRad = (angleDeg * Math.PI) / 180;
|
|||
|
|
const radius = this.getBaseRadius(ringCount) + ringIndex * this.RING_GAP;
|
|||
|
|
|
|||
|
|
const x = Math.cos(angleRad) * radius;
|
|||
|
|
const y = Math.sin(angleRad) * radius;
|
|||
|
|
|
|||
|
|
const openDelay = (ringLocalIndex + ringIndex * 0.5) * 0.045;
|
|||
|
|
const closeDelay = (ringCount - 1 - ringLocalIndex + ringIndex * 0.4) * 0.032;
|
|||
|
|
|
|||
|
|
btn.style.setProperty('--rt-x', `${x.toFixed(2)}px`);
|
|||
|
|
btn.style.setProperty('--rt-y', `${y.toFixed(2)}px`);
|
|||
|
|
btn.style.setProperty('--rt-open-delay', `${openDelay.toFixed(3)}s`);
|
|||
|
|
btn.style.setProperty('--rt-close-delay', `${closeDelay.toFixed(3)}s`);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private updateLayoutMetrics(): void {
|
|||
|
|
const total = this.items.length;
|
|||
|
|
const ringCount = Math.max(1, Math.ceil(total / this.itemsPerRing));
|
|||
|
|
const maxItemsPerRing = Math.max(1, Math.min(total, this.itemsPerRing));
|
|||
|
|
const maxRadius = this.getBaseRadius(maxItemsPerRing) + (ringCount - 1) * this.RING_GAP;
|
|||
|
|
const size = Math.ceil(maxRadius + this.MAIN_BUTTON_SIZE + this.SUB_BUTTON_SIZE + this.CANVAS_PADDING * 2);
|
|||
|
|
this.maxInteractiveRadius = maxRadius + this.SUB_BUTTON_SIZE * 0.7;
|
|||
|
|
|
|||
|
|
this.wrapper.style.width = `${size}px`;
|
|||
|
|
this.wrapper.style.height = `${size}px`;
|
|||
|
|
this.wrapper.style.setProperty('--rt-main-size', `${this.MAIN_BUTTON_SIZE}px`);
|
|||
|
|
this.wrapper.style.setProperty('--rt-sub-size', `${this.SUB_BUTTON_SIZE}px`);
|
|||
|
|
this.wrapper.style.setProperty('--rt-main-offset', `${(this.MAIN_BUTTON_SIZE - this.SUB_BUTTON_SIZE) / 2}px`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private getBaseRadius(_itemsInRing: number): number {
|
|||
|
|
return this.BASE_RADIUS;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private scheduleCollapse(): void {
|
|||
|
|
if (this.timer) {
|
|||
|
|
clearTimeout(this.timer);
|
|||
|
|
}
|
|||
|
|
this.timer = window.setTimeout(() => {
|
|||
|
|
this.timer = null;
|
|||
|
|
if (this.isPointerInsideToolbar()) {
|
|||
|
|
if (this.isActive) {
|
|||
|
|
this.scheduleCollapse();
|
|||
|
|
}
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
this.collapse();
|
|||
|
|
}, this.closeDelay);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private isPointerInsideToolbar(): boolean {
|
|||
|
|
if (this.mainButton.matches(':hover')) {
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (this.itemElements.some((button) => button.matches(':hover'))) {
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return this.isPointerInsideFanRegion();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private isPointerInsideFanRegion(): boolean {
|
|||
|
|
if (!Number.isFinite(this.pointerX) || !Number.isFinite(this.pointerY)) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const mainRect = this.mainButton.getBoundingClientRect();
|
|||
|
|
const centerX = mainRect.left + mainRect.width / 2;
|
|||
|
|
const centerY = mainRect.top + mainRect.height / 2;
|
|||
|
|
const dx = this.pointerX - centerX;
|
|||
|
|
const dy = this.pointerY - centerY;
|
|||
|
|
const distance = Math.hypot(dx, dy);
|
|||
|
|
|
|||
|
|
if (distance <= this.MAIN_BUTTON_SIZE / 2) {
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (distance > this.maxInteractiveRadius) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let angle = (Math.atan2(dy, dx) * 180) / Math.PI;
|
|||
|
|
if (angle < 0) {
|
|||
|
|
angle += 360;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (this.FAN_START_DEG <= this.FAN_END_DEG) {
|
|||
|
|
return angle >= this.FAN_START_DEG && angle <= this.FAN_END_DEG;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return angle >= this.FAN_START_DEG || angle <= this.FAN_END_DEG;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private expand(): void {
|
|||
|
|
if (this.isActive || this.items.length === 0) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
this.isActive = true;
|
|||
|
|
this.wrapper.classList.add('is-active');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private collapse(): void {
|
|||
|
|
if (!this.isActive) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
this.isActive = false;
|
|||
|
|
this.wrapper.classList.remove('is-active');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private updateLocales(): void {
|
|||
|
|
const mainLabel = t(this.mainButtonLabel);
|
|||
|
|
this.mainButton.title = mainLabel;
|
|||
|
|
this.mainButton.setAttribute('aria-label', mainLabel);
|
|||
|
|
this.itemElements.forEach((el, index) => {
|
|||
|
|
const item = this.items[index];
|
|||
|
|
if (!item) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const text = t(item.label);
|
|||
|
|
el.title = text;
|
|||
|
|
el.setAttribute('aria-label', text);
|
|||
|
|
this.applyItemActiveClass(el, item);
|
|||
|
|
|
|||
|
|
if (!item.icon) {
|
|||
|
|
const iconEl = el.querySelector('.radial-sub-btn-icon');
|
|||
|
|
if (iconEl) {
|
|||
|
|
iconEl.textContent = this.getFallbackLabel(item.label);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private applyItemActiveClass(button: HTMLButtonElement, item: RadialMenuItem): void {
|
|||
|
|
if (!item.isToggle) {
|
|||
|
|
button.classList.remove('is-active');
|
|||
|
|
button.dataset.active = 'false';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const active = this.toggleStateMap.get(item.id) ?? Boolean(item.isActive);
|
|||
|
|
button.classList.toggle('is-active', active);
|
|||
|
|
button.dataset.active = active ? 'true' : 'false';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private getFallbackLabel(label: string): string {
|
|||
|
|
const translated = t(label).trim();
|
|||
|
|
if (!translated) {
|
|||
|
|
return '?';
|
|||
|
|
}
|
|||
|
|
return translated.charAt(0).toUpperCase();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public setTheme(theme: ThemeConfig): void {
|
|||
|
|
this.wrapper.classList.remove('theme-light', 'theme-dark');
|
|||
|
|
this.wrapper.classList.add(`theme-${theme.name}`);
|
|||
|
|
|
|||
|
|
const style = this.wrapper.style;
|
|||
|
|
style.setProperty('--bim-primary', theme.primary);
|
|||
|
|
style.setProperty('--bim-primary-hover', theme.primaryHover);
|
|||
|
|
style.setProperty('--bim-primary-active', theme.primaryActive);
|
|||
|
|
style.setProperty('--bim-text-inverse', theme.textInverse);
|
|||
|
|
style.setProperty('--bim-icon-inverse', theme.iconInverse);
|
|||
|
|
style.setProperty('--bim-shadow-glow', theme.shadowGlow);
|
|||
|
|
|
|||
|
|
const isDark = theme.name === 'dark';
|
|||
|
|
|
|||
|
|
style.setProperty('--rt-main-bg', isDark ? 'rgba(55, 68, 86, 0.92)' : theme.floatingBtnBg);
|
|||
|
|
style.setProperty('--rt-main-bg-hover', isDark ? 'rgba(66, 82, 104, 0.96)' : theme.floatingBtnBgHover);
|
|||
|
|
style.setProperty('--rt-main-border', isDark ? 'rgba(117, 133, 154, 0.56)' : theme.floatingBtnBorder);
|
|||
|
|
style.setProperty('--rt-main-shadow', isDark ? '0 2px 8px rgba(15, 23, 42, 0.32), 0 4px 12px rgba(15, 23, 42, 0.24)' : theme.floatingBtnShadow);
|
|||
|
|
style.setProperty('--rt-main-shadow-hover', isDark ? '0 4px 12px rgba(15, 23, 42, 0.38), 0 6px 20px rgba(15, 23, 42, 0.3)' : theme.floatingBtnShadowHover);
|
|||
|
|
style.setProperty('--rt-main-icon', isDark ? '#e2e8f0' : theme.floatingIconColor);
|
|||
|
|
style.setProperty('--rt-main-icon-hover', isDark ? '#f8fafc' : theme.floatingIconColorHover);
|
|||
|
|
|
|||
|
|
style.setProperty('--rt-sub-bg', isDark ? 'rgba(55, 68, 86, 0.92)' : theme.floatingBtnBg);
|
|||
|
|
style.setProperty('--rt-sub-bg-hover', isDark ? 'rgba(66, 82, 104, 0.96)' : theme.floatingBtnBgHover);
|
|||
|
|
style.setProperty('--rt-sub-border', isDark ? 'rgba(117, 133, 154, 0.56)' : theme.floatingBtnBorder);
|
|||
|
|
style.setProperty('--rt-sub-shadow', isDark ? '0 2px 8px rgba(15, 23, 42, 0.32), 0 4px 12px rgba(15, 23, 42, 0.24)' : theme.floatingBtnShadow);
|
|||
|
|
style.setProperty('--rt-sub-shadow-hover', isDark ? '0 4px 12px rgba(15, 23, 42, 0.38), 0 6px 20px rgba(15, 23, 42, 0.3)' : theme.floatingBtnShadowHover);
|
|||
|
|
style.setProperty('--rt-sub-icon', isDark ? '#e2e8f0' : theme.floatingIconColor);
|
|||
|
|
style.setProperty('--rt-sub-icon-hover', isDark ? '#f8fafc' : theme.floatingIconColorHover);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public setItemActive(id: string, active: boolean): void {
|
|||
|
|
const item = this.items.find((entry) => entry.id === id);
|
|||
|
|
if (!item || !item.isToggle) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
item.isActive = active;
|
|||
|
|
this.toggleStateMap.set(id, active);
|
|||
|
|
const button = this.itemButtonMap.get(id);
|
|||
|
|
if (button) {
|
|||
|
|
this.applyItemActiveClass(button, item);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public init(): void { }
|
|||
|
|
|
|||
|
|
public setLocales(): void {
|
|||
|
|
this.updateLocales();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public destroy(): void {
|
|||
|
|
if (this.timer) {
|
|||
|
|
clearTimeout(this.timer);
|
|||
|
|
this.timer = null;
|
|||
|
|
}
|
|||
|
|
if (this.unsubscribeTheme) {
|
|||
|
|
this.unsubscribeTheme();
|
|||
|
|
this.unsubscribeTheme = null;
|
|||
|
|
}
|
|||
|
|
if (this.unsubscribeLocale) {
|
|||
|
|
this.unsubscribeLocale();
|
|||
|
|
this.unsubscribeLocale = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
document.removeEventListener('click', this.handleDocumentClick);
|
|||
|
|
document.removeEventListener('mousemove', this.handleDocumentMouseMove);
|
|||
|
|
document.removeEventListener('mouseleave', this.handleDocumentMouseLeave);
|
|||
|
|
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
|
|||
|
|
window.removeEventListener('blur', this.handleWindowBlur);
|
|||
|
|
this.mainButton.removeEventListener('mouseenter', this.handlePointerEnter);
|
|||
|
|
this.mainButton.removeEventListener('mouseleave', this.handlePointerLeave);
|
|||
|
|
this.mainButton.removeEventListener('click', this.handleMainButtonClick);
|
|||
|
|
|
|||
|
|
this.itemElements.forEach((button) => {
|
|||
|
|
button.removeEventListener('mouseenter', this.handlePointerEnter);
|
|||
|
|
button.removeEventListener('mouseleave', this.handlePointerLeave);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (this.wrapper.parentNode) {
|
|||
|
|
this.wrapper.parentNode.removeChild(this.wrapper);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|