Refactor: modularize toolbar buttons, integrate config, and fix layout positioning
This commit is contained in:
@@ -1,19 +0,0 @@
|
||||
export class BimEngine {
|
||||
private container: HTMLElement;
|
||||
|
||||
constructor(container: HTMLElement | string) {
|
||||
const el = typeof container === 'string' ? document.getElementById(container) : container;
|
||||
if (!el) throw new Error('Container not found');
|
||||
this.container = el;
|
||||
this.init();
|
||||
}
|
||||
|
||||
private init() {
|
||||
this.container.innerHTML = `
|
||||
<div style="font-family: sans-serif; color: #333;">
|
||||
<h1>BimEngine</h1>
|
||||
<p>这是一个纯 TypeScript 组件入口。</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
26
src/bim-engine.css
Normal file
26
src/bim-engine.css
Normal file
@@ -0,0 +1,26 @@
|
||||
.bim-engine-wrapper {
|
||||
position: relative;
|
||||
/* 添加相对定位作为参照物 */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: sans-serif;
|
||||
color: #333;
|
||||
padding: 20px;
|
||||
background-color: #e16969;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
box-sizing: border-box;
|
||||
/* 确保 padding 不会撑大容器 */
|
||||
}
|
||||
|
||||
/* ... (中间代码不变) ... */
|
||||
|
||||
/* 操作按钮组容器 */
|
||||
.bim-engine-opt-btn-container {
|
||||
position: absolute;
|
||||
/* 改为绝对定位 */
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 100;
|
||||
}
|
||||
69
src/bim-engine.ts
Normal file
69
src/bim-engine.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import './bim-engine.css';
|
||||
import { OptBtnGroups } from './toolbar';
|
||||
|
||||
export class BimEngine {
|
||||
private container: HTMLElement;
|
||||
private optBtnGroups: OptBtnGroups | null = null;
|
||||
|
||||
constructor(container: HTMLElement | string) {
|
||||
const el = typeof container === 'string' ? document.getElementById(container) : container;
|
||||
if (!el) throw new Error('Container not found');
|
||||
this.container = el;
|
||||
this.init();
|
||||
}
|
||||
|
||||
private init() {
|
||||
// 1. 清空容器可能存在的旧内容
|
||||
this.container.innerHTML = '';
|
||||
|
||||
// 2. 创建外层容器 div
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'bim-engine-wrapper';
|
||||
|
||||
// 3. 创建标题 h1
|
||||
const title = document.createElement('h1');
|
||||
title.textContent = 'BimEngine';
|
||||
title.className = 'bim-engine-title';
|
||||
|
||||
// 4. 创建段落 p
|
||||
const desc = document.createElement('p');
|
||||
desc.textContent = '这是一个使用BIM-ENGINE。';
|
||||
desc.className = 'bim-engine-desc';
|
||||
|
||||
// 6. 创建操作按钮组容器
|
||||
const btnGroupContainer = document.createElement('div');
|
||||
btnGroupContainer.id = 'opt-btn-groups';
|
||||
btnGroupContainer.className = 'bim-engine-opt-btn-container';
|
||||
|
||||
// 7. 组装元素
|
||||
wrapper.appendChild(title);
|
||||
wrapper.appendChild(desc);
|
||||
wrapper.appendChild(btnGroupContainer); // 将按钮组放入 wrapper 中
|
||||
|
||||
// 8. 挂载到主容器
|
||||
this.container.appendChild(wrapper);
|
||||
|
||||
// 9. 初始化操作按钮组
|
||||
this.initOptBtnGroups(btnGroupContainer);
|
||||
}
|
||||
|
||||
private initOptBtnGroups(container: HTMLElement) {
|
||||
this.optBtnGroups = new OptBtnGroups({
|
||||
container,
|
||||
showLabel: true
|
||||
});
|
||||
|
||||
// 初始化并加载默认按钮
|
||||
this.optBtnGroups.init().catch(err => {
|
||||
console.error('Failed to initialize OptBtnGroups:', err);
|
||||
});
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
if (this.optBtnGroups) {
|
||||
this.optBtnGroups.destroy();
|
||||
this.optBtnGroups = null;
|
||||
}
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { BimEngine } from './BimEngine';
|
||||
|
||||
import { BimEngine } from './bim-engine';
|
||||
export { OptBtnGroups } from './toolbar';
|
||||
export type { OptButton, ButtonGroup, OptBtnGroupsOptions, ClickPayload } from './toolbar/index.type';
|
||||
export { BimEngine };
|
||||
|
||||
16
src/toolbar/buttons/home/index.ts
Normal file
16
src/toolbar/buttons/home/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { ButtonConfig } from '../../index.type';
|
||||
|
||||
/**
|
||||
* 首页按钮配置
|
||||
*/
|
||||
export const homeButton: ButtonConfig = {
|
||||
id: 'home',
|
||||
groupId: 'group-1',
|
||||
type: 'button',
|
||||
label: '首页',
|
||||
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M4 21V9l8-6l8 6v12h-6v-7h-4v7z"/></svg>',
|
||||
keepActive: true,
|
||||
onClick: (button) => {
|
||||
console.log('首页按钮被点击:', button.id);
|
||||
}
|
||||
};
|
||||
16
src/toolbar/buttons/info/index.ts
Normal file
16
src/toolbar/buttons/info/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { ButtonConfig } from '../../index.type';
|
||||
|
||||
/**
|
||||
* 定位按钮配置
|
||||
*/
|
||||
export const infoButton: ButtonConfig = {
|
||||
id: 'info',
|
||||
groupId: 'group-2',
|
||||
type: 'button',
|
||||
label: '信息',
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 7q.425 0 .713-.288T13 6t-.288-.712T12 5t-.712.288T11 6t.288.713T12 7m0 8q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9t-.712.288T11 10v4q0 .425.288.713T12 15m-6 3l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18z"/></svg>',
|
||||
keepActive: false,
|
||||
onClick: (button) => {
|
||||
console.log('信息按钮被点击:', button.id);
|
||||
}
|
||||
};
|
||||
16
src/toolbar/buttons/location/index.ts
Normal file
16
src/toolbar/buttons/location/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { ButtonConfig } from '../../index.type';
|
||||
|
||||
/**
|
||||
* 定位按钮配置
|
||||
*/
|
||||
export const locationButton: ButtonConfig = {
|
||||
id: 'location',
|
||||
groupId: 'group-1',
|
||||
type: 'button',
|
||||
label: '定位',
|
||||
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M9 13h2v-2.75h2V13h2V8.25l-3-2l-3 2zm3 9q-4.025-3.425-6.012-6.362T4 10.2q0-3.75 2.413-5.975T12 2t5.588 2.225T20 10.2q0 2.5-1.987 5.438T12 22"/></svg>',
|
||||
keepActive: false,
|
||||
onClick: (button) => {
|
||||
console.log('定位按钮被点击:', button.id);
|
||||
}
|
||||
};
|
||||
16
src/toolbar/buttons/setting/index.ts
Normal file
16
src/toolbar/buttons/setting/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { ButtonConfig } from '../../index.type';
|
||||
|
||||
/**
|
||||
* 定位按钮配置
|
||||
*/
|
||||
export const settingButton: ButtonConfig = {
|
||||
id: 'setting',
|
||||
groupId: 'group-2',
|
||||
type: 'button',
|
||||
label: '设置',
|
||||
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="m9.25 22l-.4-3.2q-.325-.125-.612-.3t-.563-.375L4.7 19.375l-2.75-4.75l2.575-1.95Q4.5 12.5 4.5 12.338v-.675q0-.163.025-.338L1.95 9.375l2.75-4.75l2.975 1.25q.275-.2.575-.375t.6-.3l.4-3.2h5.5l.4 3.2q.325.125.613.3t.562.375l2.975-1.25l2.75 4.75l-2.575 1.95q.025.175.025.338v.674q0 .163-.05.338l2.575 1.95l-2.75 4.75l-2.95-1.25q-.275.2-.575.375t-.6.3l-.4 3.2zM11 20h1.975l.35-2.65q.775-.2 1.438-.587t1.212-.938l2.475 1.025l.975-1.7l-2.15-1.625q.125-.35.175-.737T17.5 12t-.05-.787t-.175-.738l2.15-1.625l-.975-1.7l-2.475 1.05q-.55-.575-1.212-.962t-1.438-.588L13 4h-1.975l-.35 2.65q-.775.2-1.437.588t-1.213.937L5.55 7.15l-.975 1.7l2.15 1.6q-.125.375-.175.75t-.05.8q0 .4.05.775t.175.75l-2.15 1.625l.975 1.7l2.475-1.05q.55.575 1.213.963t1.437.587zm1.05-4.5q1.45 0 2.475-1.025T15.55 12t-1.025-2.475T12.05 8.5q-1.475 0-2.488 1.025T8.55 12t1.013 2.475T12.05 15.5M12 12"/></svg>',
|
||||
keepActive: false,
|
||||
onClick: (button) => {
|
||||
console.log('设置按钮被点击:', button.id);
|
||||
}
|
||||
};
|
||||
13
src/toolbar/buttons/walk/walk-bird/index.ts
Normal file
13
src/toolbar/buttons/walk/walk-bird/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { ButtonConfig } from '../../../index.type';
|
||||
|
||||
export const walkBirdButton: ButtonConfig = {
|
||||
id: 'walk-bird',
|
||||
groupId: 'group-1',
|
||||
parentId: 'walk',
|
||||
type: 'button',
|
||||
label: '鸟瞰漫游',
|
||||
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M9 22V8.775q-2.275-.6-3.637-2.512T4 2h2q0 2.075 1.338 3.538T10.75 7h2.5q.75 0 1.4.275t1.175.8L20.35 12.6l-1.4 1.4L15 10.05V22h-2v-6h-2v6zm3-16q-.825 0-1.412-.587T10 4t.588-1.412T12 2t1.413.588T14 4t-.587 1.413T12 6"/></svg>',
|
||||
onClick: (button) => {
|
||||
console.log('鸟瞰漫游被点击:', button.id);
|
||||
}
|
||||
};
|
||||
16
src/toolbar/buttons/walk/walk-menu/index.ts
Normal file
16
src/toolbar/buttons/walk/walk-menu/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { ButtonConfig } from '../../../index.type';
|
||||
|
||||
/**
|
||||
* 漫游菜单按钮配置
|
||||
*/
|
||||
export const walkMenuButton: ButtonConfig = {
|
||||
id: 'walk',
|
||||
groupId: 'group-1',
|
||||
type: 'menu',
|
||||
label: '漫游',
|
||||
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M9 22V8.775q-2.275-.6-3.637-2.512T4 2h2q0 2.075 1.338 3.538T10.75 7h2.5q.75 0 1.4.275t1.175.8L20.35 12.6l-1.4 1.4L15 10.05V22h-2v-6h-2v6zm3-16q-.825 0-1.412-.587T10 4t.588-1.412T12 2t1.413.588T14 4t-.587 1.413T12 6"/></svg>',
|
||||
keepActive: true,
|
||||
onClick: (button) => {
|
||||
console.log('漫游按钮被点击:', button.id);
|
||||
}
|
||||
};
|
||||
13
src/toolbar/buttons/walk/walk-person/index.ts
Normal file
13
src/toolbar/buttons/walk/walk-person/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { ButtonConfig } from '../../../index.type';
|
||||
|
||||
export const walkPersonButton: ButtonConfig = {
|
||||
id: 'walk-person',
|
||||
groupId: 'group-1',
|
||||
parentId: 'walk',
|
||||
type: 'button',
|
||||
label: '人视漫游',
|
||||
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M9 22V8.775q-2.275-.6-3.637-2.512T4 2h2q0 2.075 1.338 3.538T10.75 7h2.5q.75 0 1.4.275t1.175.8L20.35 12.6l-1.4 1.4L15 10.05V22h-2v-6h-2v6zm3-16q-.825 0-1.412-.587T10 4t.588-1.412T12 2t1.413.588T14 4t-.587 1.413T12 6"/></svg>',
|
||||
onClick: (button) => {
|
||||
console.log('人视漫游被点击:', button.id);
|
||||
}
|
||||
};
|
||||
181
src/toolbar/index.css
Normal file
181
src/toolbar/index.css
Normal file
@@ -0,0 +1,181 @@
|
||||
/* 容器样式 */
|
||||
.toolbar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* 容器本身不需要背景 */
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
/* Firefox 隐藏滚动条 */
|
||||
-ms-overflow-style: none;
|
||||
/* IE 10+ 隐藏滚动条 */
|
||||
}
|
||||
|
||||
.toolbar-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
/* Chrome/Safari 隐藏滚动条 */
|
||||
}
|
||||
|
||||
/* 按钮组样式 */
|
||||
.opt-btn-group {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
background-color: rgba(17, 17, 17, 0.88);
|
||||
/* 每个组独立的背景 */
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.has-divider {
|
||||
margin-right: 16px;
|
||||
/* 增加右边距来分隔组 */
|
||||
}
|
||||
|
||||
/* 按钮包装器 */
|
||||
.opt-btn-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.opt-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 50px;
|
||||
min-height: 50px;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
color: #ccc;
|
||||
transition: all 0.2s;
|
||||
border-bottom: 2px solid transparent;
|
||||
/* 默认透明底边 */
|
||||
}
|
||||
|
||||
.opt-btn:hover {
|
||||
background-color: #444;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.opt-btn.active {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
/* 白色半透明背景 */
|
||||
color: #fff;
|
||||
border-bottom: 2px solid #fff;
|
||||
/* 纯白色底部横条 */
|
||||
}
|
||||
|
||||
.opt-btn.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 图标样式 */
|
||||
.opt-btn-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
/* 防止图标被压缩 */
|
||||
}
|
||||
|
||||
.opt-btn-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 文字标签样式 */
|
||||
.opt-btn-label {
|
||||
font-size: 10px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* 下拉箭头样式 */
|
||||
.opt-btn-arrow {
|
||||
font-size: 8px;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
opacity: 0.6;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* 箭头旋转 (菜单展开时) */
|
||||
.opt-btn-arrow.rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* 无标签模式调整 */
|
||||
.opt-btn.no-label .opt-btn-arrow {
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
/* 下拉菜单样式 */
|
||||
.opt-btn-dropdown {
|
||||
position: fixed;
|
||||
/* 固定定位 */
|
||||
transform: translate(-50%, -100%);
|
||||
/* 水平居中并向上移动 */
|
||||
background-color: rgba(17, 17, 17, 0.88);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
min-width: 50px;
|
||||
z-index: 9999;
|
||||
/* 高层级 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 下拉菜单项样式 */
|
||||
.opt-btn-dropdown-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* 垂直布局 */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #b3b4b4;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
white-space: nowrap;
|
||||
min-width: 50px;
|
||||
min-height: 50px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.opt-btn-dropdown-item:last-child {
|
||||
border-bottom: none;
|
||||
/* 移除最后一项的分隔线 */
|
||||
}
|
||||
|
||||
.opt-btn-dropdown-item:hover {
|
||||
background-color: #444;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.opt-btn-dropdown-item .opt-btn-icon.small {
|
||||
width: 30px;
|
||||
/* 与主按钮图标大小一致 */
|
||||
height: 30px;
|
||||
margin-right: 0;
|
||||
/* 移除右边距 */
|
||||
margin-bottom: 4px;
|
||||
/* 添加下边距 */
|
||||
}
|
||||
|
||||
.opt-btn-dropdown-item span {
|
||||
font-size: 10px;
|
||||
/* 较小的文字 */
|
||||
}
|
||||
|
||||
/* 无标签模式调整 */
|
||||
.opt-btn.no-label .opt-btn-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
387
src/toolbar/index.ts
Normal file
387
src/toolbar/index.ts
Normal file
@@ -0,0 +1,387 @@
|
||||
import './index.css';
|
||||
import type {
|
||||
OptButton,
|
||||
ButtonGroup,
|
||||
OptBtnGroupsOptions,
|
||||
ButtonConfig
|
||||
} from './index.type';
|
||||
|
||||
export class OptBtnGroups {
|
||||
private container: HTMLElement;
|
||||
private options: OptBtnGroupsOptions;
|
||||
// 改用 Array 存储 Group,方便控制顺序
|
||||
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;
|
||||
|
||||
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: OptBtnGroupsOptions) {
|
||||
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: {},
|
||||
...options
|
||||
};
|
||||
|
||||
this.initContainer();
|
||||
}
|
||||
|
||||
private initContainer(): void {
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加按钮组
|
||||
* @param groupId 组ID
|
||||
* @param beforeGroupId 在哪个组之前插入(可选),不传则插入到最后
|
||||
*/
|
||||
public addGroup(groupId: string, beforeGroupId?: string): void {
|
||||
if (this.groups.some(g => g.id === groupId)) {
|
||||
console.warn('Group ' + groupId + ' already exists');
|
||||
return;
|
||||
}
|
||||
|
||||
const newGroup: ButtonGroup = { id: groupId, buttons: [] };
|
||||
|
||||
if (beforeGroupId) {
|
||||
const index = this.groups.findIndex(g => g.id === beforeGroupId);
|
||||
if (index !== -1) {
|
||||
this.groups.splice(index, 0, newGroup);
|
||||
} else {
|
||||
console.warn(`Target group ${beforeGroupId} not found, appending ${groupId} to end.`);
|
||||
this.groups.push(newGroup);
|
||||
}
|
||||
} else {
|
||||
this.groups.push(newGroup);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加按钮
|
||||
* @param config 按钮配置(必须包含 groupId,可选包含 parentId)
|
||||
*/
|
||||
public addButton(config: ButtonConfig): void {
|
||||
const { groupId, parentId } = config;
|
||||
|
||||
if (!groupId) {
|
||||
throw new Error(`Button ${config.id} config must contain 'groupId'`);
|
||||
}
|
||||
|
||||
const group = this.groups.find(g => g.id === groupId);
|
||||
if (!group) {
|
||||
throw new Error(`Group ${groupId} not found. Please call addGroup first.`);
|
||||
}
|
||||
|
||||
const button: OptButton = {
|
||||
...config,
|
||||
children: config.children || []
|
||||
};
|
||||
|
||||
if (parentId) {
|
||||
// Add as sub-button
|
||||
const parentBtn = this.findButton(group.buttons, parentId);
|
||||
if (!parentBtn) {
|
||||
throw new Error(`Parent button ${parentId} not found in group ${groupId}`);
|
||||
}
|
||||
if (!parentBtn.children) {
|
||||
parentBtn.children = [];
|
||||
}
|
||||
parentBtn.children.push(button);
|
||||
} else {
|
||||
// Add as main button
|
||||
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 async init(): Promise<void> {
|
||||
const { homeButton } = await import('./buttons/home');
|
||||
const { locationButton } = await import('./buttons/location');
|
||||
const { walkMenuButton } = await import('./buttons/walk/walk-menu');
|
||||
const { walkPersonButton } = await import('./buttons/walk/walk-person');
|
||||
const { walkBirdButton } = await import('./buttons/walk/walk-bird');
|
||||
const { settingButton } = await import('./buttons/setting');
|
||||
const { infoButton } = await import('./buttons/info');
|
||||
|
||||
// 添加组1
|
||||
this.addGroup('group-1');
|
||||
this.addButton(homeButton);
|
||||
this.addButton(walkMenuButton);
|
||||
this.addButton(walkPersonButton);
|
||||
this.addButton(walkBirdButton);
|
||||
this.addButton(locationButton);
|
||||
this.addGroup('group-2');
|
||||
this.addButton(settingButton);
|
||||
this.addButton(infoButton);
|
||||
this.render();
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
this.container.innerHTML = '';
|
||||
this.btnRefs.clear();
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'toolbar-container';
|
||||
|
||||
// 直接遍历数组,顺序由 addGroup 控制
|
||||
this.groups.forEach((group, index) => {
|
||||
const groupElement = this.renderGroup(group, index, this.groups.length);
|
||||
wrapper.appendChild(groupElement);
|
||||
});
|
||||
|
||||
this.container.appendChild(wrapper);
|
||||
}
|
||||
|
||||
private renderGroup(group: ButtonGroup, index: number, total: number): HTMLElement {
|
||||
const groupEl = document.createElement('div');
|
||||
groupEl.className = 'opt-btn-group';
|
||||
|
||||
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';
|
||||
|
||||
if (this.activeBtnIds.has(button.id)) {
|
||||
btnEl.classList.add('active');
|
||||
}
|
||||
|
||||
if (button.disabled) {
|
||||
btnEl.classList.add('disabled');
|
||||
}
|
||||
|
||||
if (!this.options.showLabel) {
|
||||
btnEl.classList.add('no-label');
|
||||
if (button.label) {
|
||||
btnEl.title = button.label;
|
||||
}
|
||||
}
|
||||
|
||||
const icon = document.createElement('div');
|
||||
icon.className = 'opt-btn-icon';
|
||||
icon.innerHTML = this.getIcon(button.icon);
|
||||
btnEl.appendChild(icon);
|
||||
|
||||
if (this.options.showLabel && button.label) {
|
||||
const label = document.createElement('span');
|
||||
label.className = 'opt-btn-label';
|
||||
label.textContent = button.label;
|
||||
btnEl.appendChild(label);
|
||||
}
|
||||
|
||||
if (button.children && button.children.length > 0) {
|
||||
const arrow = document.createElement('span');
|
||||
arrow.className = 'opt-btn-arrow';
|
||||
arrow.textContent = '▼';
|
||||
btnEl.appendChild(arrow);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private handleClick(button: OptButton): void {
|
||||
if (button.disabled) return;
|
||||
|
||||
if (!button.children || button.children.length === 0) {
|
||||
if (button.keepActive) {
|
||||
const wasActive = this.activeBtnIds.has(button.id);
|
||||
if (wasActive) {
|
||||
this.activeBtnIds.delete(button.id);
|
||||
} else {
|
||||
this.activeBtnIds.add(button.id);
|
||||
}
|
||||
this.updateButtonState(button.id);
|
||||
}
|
||||
|
||||
this.closeDropdown();
|
||||
|
||||
if (button.onClick) {
|
||||
button.onClick(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleSubClick(button: OptButton): void {
|
||||
if (button.keepActive) {
|
||||
const wasActive = this.activeBtnIds.has(button.id);
|
||||
if (wasActive) {
|
||||
this.activeBtnIds.delete(button.id);
|
||||
} else {
|
||||
this.activeBtnIds.add(button.id);
|
||||
}
|
||||
this.updateButtonState(button.id);
|
||||
}
|
||||
|
||||
this.closeDropdown();
|
||||
|
||||
if (button.onClick) {
|
||||
button.onClick(button);
|
||||
}
|
||||
}
|
||||
|
||||
private handleMouseEnter(button: OptButton, btnEl: HTMLElement): void {
|
||||
if (this.hoverTimeout) {
|
||||
clearTimeout(this.hoverTimeout);
|
||||
this.hoverTimeout = null;
|
||||
}
|
||||
|
||||
if (button.children && button.children.length > 0) {
|
||||
this.showDropdown(button, btnEl);
|
||||
|
||||
const arrow = btnEl.querySelector('.opt-btn-arrow');
|
||||
if (arrow) {
|
||||
arrow.classList.add('rotated');
|
||||
}
|
||||
} 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';
|
||||
|
||||
const rect = btnEl.getBoundingClientRect();
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
|
||||
dropdown.style.top = rect.top - 8 + 'px';
|
||||
dropdown.style.left = centerX + 'px';
|
||||
|
||||
button.children.forEach(subBtn => {
|
||||
if (this.isVisible(subBtn.id)) {
|
||||
const item = this.renderDropdownItem(subBtn);
|
||||
dropdown.appendChild(item);
|
||||
}
|
||||
});
|
||||
|
||||
dropdown.addEventListener('mouseenter', () => {
|
||||
if (this.hoverTimeout) {
|
||||
clearTimeout(this.hoverTimeout);
|
||||
this.hoverTimeout = null;
|
||||
}
|
||||
});
|
||||
|
||||
dropdown.addEventListener('mouseleave', () => this.handleMouseLeave());
|
||||
|
||||
document.body.appendChild(dropdown);
|
||||
this.dropdownElement = dropdown;
|
||||
}
|
||||
|
||||
private renderDropdownItem(button: OptButton): HTMLElement {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'opt-btn-dropdown-item';
|
||||
|
||||
const icon = document.createElement('div');
|
||||
icon.className = 'opt-btn-icon small';
|
||||
icon.innerHTML = this.getIcon(button.icon);
|
||||
item.appendChild(icon);
|
||||
|
||||
if (this.options.showLabel) {
|
||||
const label = document.createElement('span');
|
||||
label.textContent = button.label;
|
||||
item.appendChild(label);
|
||||
}
|
||||
|
||||
item.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.handleSubClick(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) {
|
||||
if (this.activeBtnIds.has(buttonId)) {
|
||||
btnEl.classList.add('active');
|
||||
} else {
|
||||
btnEl.classList.remove('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getIcon(icon?: string): string {
|
||||
return icon || this.DEFAULT_ICON;
|
||||
}
|
||||
|
||||
private isVisible(id: string): boolean {
|
||||
return this.options.visibility?.[id] !== false;
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.closeDropdown();
|
||||
if (this.hoverTimeout) {
|
||||
clearTimeout(this.hoverTimeout);
|
||||
}
|
||||
this.container.innerHTML = '';
|
||||
this.btnRefs.clear();
|
||||
this.activeBtnIds.clear();
|
||||
this.groups = []; // 清空数组
|
||||
}
|
||||
}
|
||||
51
src/toolbar/index.type.ts
Normal file
51
src/toolbar/index.type.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
export type ButtonType = 'button' | 'menu';
|
||||
|
||||
/**
|
||||
* 按钮配置接口(用于外部定义按钮)
|
||||
*/
|
||||
export interface ButtonConfig {
|
||||
id: string; // 唯一标识
|
||||
type: ButtonType; // 按钮类型
|
||||
label: string; // 按钮文字
|
||||
icon?: string; // SVG 图标(内联 SVG 字符串)
|
||||
keepActive?: boolean; // 是否保持激活状态(默认 false)
|
||||
disabled?: boolean; // 是否禁用
|
||||
onClick?: (button: OptButton) => void; // 点击回调
|
||||
children?: ButtonConfig[]; // 子按钮配置(可选,用于菜单按钮)
|
||||
|
||||
groupId?: string; // 所属组ID
|
||||
parentId?: string; // 父按钮ID(如果是子按钮)
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作按钮接口(内部使用,继承配置)
|
||||
*/
|
||||
export interface OptButton extends ButtonConfig {
|
||||
children?: OptButton[]; // 内部使用的子按钮列表
|
||||
}
|
||||
|
||||
/**
|
||||
* 按钮组接口
|
||||
*/
|
||||
export interface ButtonGroup {
|
||||
id: string;
|
||||
buttons: OptButton[];
|
||||
}
|
||||
|
||||
/**
|
||||
* OptBtnGroups 配置选项
|
||||
*/
|
||||
export interface OptBtnGroupsOptions {
|
||||
container: HTMLElement | string;
|
||||
showLabel?: boolean;
|
||||
visibility?: Record<string, boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击事件载荷
|
||||
*/
|
||||
export interface ClickPayload {
|
||||
button: OptButton;
|
||||
action: 'activate' | 'deactivate' | 'trigger';
|
||||
isActive?: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user