File size: 10,330 Bytes
494c9e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
/**
 * 菜单按钮组件
 * 为每个demo项或文件夹项提供操作菜单(移动、重命名、删除)
 */
import * as d3 from 'd3';
import { isMobileDevice } from '../utils/responsive';
import { tr } from '../lang/i18n-lite';

// 全局菜单管理器:确保同时只有一个菜单打开
let currentOpenMenu: ItemMenu | null = null;

export type ItemMenu = {
    show: () => void;
    hide: () => void;
    remove: () => void;
    showButton: () => void;
    hideButton: () => void;
};

export function createItemMenu(
    item: { type: 'folder' | 'file', name: string, path: string },
    onMove: () => void,
    onRename: () => void,
    onDelete: () => void,
    buttonNode?: HTMLElement | null  // 可选的按钮节点,用于定位菜单
): ItemMenu {
    let menuVisible = false;
    let menuElement: d3.Selection<HTMLDivElement, any, any, any> | null = null;
    let actualButtonNode: HTMLElement | null = buttonNode || null;

    // 检测是否为移动端
    const isMobile = isMobileDevice();

    // 创建菜单按钮(汉堡图标)
    const menuButton = d3.create('button')
        .attr('class', 'demo-item-menu-btn')
        .html('☰')
        .attr('title', tr('More actions'))
        .style('background', 'transparent')
        .style('border', 'none')
        .style('color', 'var(--text-color)')
        .style('cursor', 'pointer')
        .style('font-size', '16px')
        .style('line-height', '1')
        .style('padding', '0 4px')
        .style('opacity', isMobile ? '0.4' : '0')
        .style('transition', 'opacity 0.2s')
        .style('flex-shrink', '0')
        .style('margin-left', '1px')
        .on('mouseenter', function() {
            if (!menuVisible) {
                d3.select(this).style('opacity', '1');
            }
        })
        .on('mouseleave', function() {
            if (!menuVisible) {
                // 不在这里隐藏按钮,由demo-item的mouseleave事件控制
                d3.select(this).style('opacity', '0.6');
            }
        })
        .on('click', function(event) {
            event.stopPropagation();
            // 更新实际按钮节点
            actualButtonNode = this as HTMLElement;
            if (menuVisible) {
                hide();
            } else {
                show();
            }
        });

    // 创建菜单实例对象(用于全局管理)
    const menuInstance: ItemMenu = {
        show: () => {},
        hide: () => {},
        remove: () => {},
        showButton: () => {},
        hideButton: () => {}
    };

    const show = () => {
        if (menuVisible) return;
        
        // 关闭之前打开的菜单
        if (currentOpenMenu && currentOpenMenu !== menuInstance) {
            currentOpenMenu.hide();
        }
        currentOpenMenu = menuInstance;
        
        menuVisible = true;
        if (actualButtonNode) {
            d3.select(actualButtonNode).style('opacity', '1');
        } else {
            menuButton.style('opacity', '1');
        }

        // 使用实际按钮节点进行定位
        const button = actualButtonNode || menuButton.node();
        if (!button) return;

        const rect = button.getBoundingClientRect();
        const menuWidth = 120; // 菜单宽度
        const spacing = 4; // 间距
        
        // 计算菜单位置:显示在按钮下方,右对齐到按钮
        let left = rect.right - menuWidth;
        let top = rect.bottom + spacing;
        
        // 确保菜单不超出视口边界
        if (left < 0) left = rect.left;
        if (left + menuWidth > window.innerWidth) left = window.innerWidth - menuWidth - spacing;
        if (top < 0) top = spacing;
        
        menuElement = d3.select('body').append('div')
            .attr('class', 'demo-item-menu')
            .style('position', 'fixed')
            .style('background', 'var(--bg-color, #fff)')
            .style('border', '1px solid var(--border-color, #ddd)')
            .style('border-radius', '4px')
            .style('box-shadow', '0 2px 8px rgba(0,0,0,0.15)')
            .style('z-index', '1000')
            .style('min-width', `${menuWidth}px`)
            .style('padding', '4px 0')
            .style('left', `${left}px`)
            .style('top', `${top}px`);

        // 菜单项(简约风格 SVG 图标)
        const menuItems = [
            { 
                icon: '<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 6h8M7 3l3 3-3 3"/></svg>',
                label: tr('Move to...'), 
                action: onMove 
            },
            { 
                icon: '<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2l2 2-6 6H2V8l6-6z"/></svg>',
                label: tr('Rename'), 
                action: onRename 
            },
            { 
                icon: '<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3l6 6M9 3l-6 6"/></svg>',
                label: tr('Delete'), 
                action: onDelete 
            }
        ];

        const menuItemSelection = menuElement.selectAll('.menu-item')
            .data(menuItems)
            .join('div')
            .attr('class', 'menu-item')
            .style('padding', '6px 16px')
            .style('cursor', 'pointer')
            .style('color', 'var(--text-color)')
            .style('font-size', '13px')
            .style('transition', 'background 0.2s')
            .style('display', 'flex')
            .style('align-items', 'center')
            .style('gap', '8px')
            .on('mouseenter', function() {
                d3.select(this).style('background', 'var(--hover-bg-color, #f0f0f0)');
            })
            .on('mouseleave', function() {
                d3.select(this).style('background', 'transparent');
            })
            .on('click', function(event, d) {
                event.stopPropagation();
                hide();
                d.action();
            });
        
        // 添加图标
        menuItemSelection.each(function(d) {
            const container = d3.select(this);
            container.append('span')
                .style('display', 'inline-flex')
                .style('align-items', 'center')
                .style('opacity', '0.7')
                .html(d.icon);
            container.append('span')
                .text(d.label);
        });

        // 点击外部关闭菜单
        const clickHandler = (event: MouseEvent) => {
            const target = event.target as Node;
            if (menuElement && menuElement.node() && !menuElement.node()?.contains(target) && 
                button && !button.contains(target)) {
                hide();
                document.removeEventListener('click', clickHandler);
            }
        };
        
        // 延迟添加事件监听,避免立即触发
        setTimeout(() => {
            document.addEventListener('click', clickHandler);
        }, 0);
    };

    const hide = () => {
        if (!menuVisible) return;
        menuVisible = false;
        if (actualButtonNode) {
            d3.select(actualButtonNode).style('opacity', '0.6');
        } else {
            menuButton.style('opacity', '0.6');
        }
        if (menuElement) {
            menuElement.remove();
            menuElement = null;
        }
        // 清除全局引用
        if (currentOpenMenu === menuInstance) {
            currentOpenMenu = null;
        }
    };

    const remove = () => {
        hide();
        menuButton.remove();
    };

    const showButton = () => {
        if (menuVisible) return; // 如果菜单已打开,不改变按钮透明度
        if (actualButtonNode) {
            d3.select(actualButtonNode).style('opacity', '0.6');
        } else {
            menuButton.style('opacity', '0.6');
        }
    };

    const hideButton = () => {
        if (menuVisible) return; // 如果菜单已打开,保持按钮可见
        // 移动端不隐藏按钮
        if (isMobile) return;
        if (actualButtonNode) {
            d3.select(actualButtonNode).style('opacity', '0');
        } else {
            menuButton.style('opacity', '0');
        }
    };

    // 更新菜单实例对象
    menuInstance.show = show;
    menuInstance.hide = hide;
    menuInstance.remove = remove;
    menuInstance.showButton = showButton;
    menuInstance.hideButton = hideButton;

    return menuInstance;
}

// 导出菜单按钮的创建函数,供外部使用
export function createMenuButton(
    item: { type: 'folder' | 'file', name: string, path: string },
    onMove: () => void,
    onRename: () => void,
    onDelete: () => void
): { button: d3.Selection<HTMLButtonElement, any, any, any>, menu: ItemMenu } {
    // 检测是否为移动端
    const isMobile = isMobileDevice();
    
    // 创建菜单按钮(汉堡图标)
    const menuButton = d3.create('button')
        .attr('class', 'demo-item-menu-btn')
        .html('☰')
        .attr('title', tr('More actions'))
        .style('background', 'transparent')
        .style('border', 'none')
        .style('color', 'var(--text-color)')
        .style('cursor', 'pointer')
        .style('font-size', '16px')
        .style('line-height', '1')
        .style('padding', '0 4px')
        .style('opacity', isMobile ? '0.4' : '0')
        .style('transition', 'opacity 0.2s')
        .style('flex-shrink', '0')
        .style('margin-left', '1px')
        .on('mouseenter', function() {
            d3.select(this).style('opacity', '1');
        })
        .on('mouseleave', function() {
            // 不在这里隐藏按钮,由demo-item的mouseleave事件控制
            d3.select(this).style('opacity', '0.6');
        });
    
    // 创建菜单,传递按钮节点
    const menu = createItemMenu(item, onMove, onRename, onDelete, menuButton.node());
    
    // 设置按钮点击事件
    menuButton.on('click', function(event) {
        event.stopPropagation();
        menu.show();
    });
    
    return { button: menuButton, menu };
}