chore(shortcuts): remove the never-wired shortcut organizer
ui_shortcut_organizer.js and shortcut_layout.js only referenced each other; the bottom bar is the fixed one built in ui_toolbox.js. Also drops their eleven locale keys and marks the customizable-shortcuts plan as abandoned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
55208061a8
commit
bd7fb8beaa
@@ -83,7 +83,7 @@
|
||||
在阶段 1 稳定后,从 `createDOM()` 提取:
|
||||
|
||||
- `ui_settings_hub.js`:设置中心与模型显示设置。接口覆盖 open/close/refreshLanguage,保留原来的设置来源、键名和生效时机。
|
||||
- `ui_toolbox.js`:工具箱内容、开关和底部按钮装配;复用 `tool_registry.js`、`shortcut_layout.js`、`sidebar_actions.js`、`ui_shortcut_organizer.js`。按**当前代码**保持固定栏与工具过滤规则,不顺便实施旧计划中的另一种交互。
|
||||
- `ui_toolbox.js`:工具箱内容、开关和底部按钮装配;复用 `tool_registry.js`、`sidebar_actions.js`。按**当前代码**保持固定栏与工具过滤规则,不顺便实施旧计划中的另一种交互。
|
||||
- `ui_browser_navigation.js`:主面板切换与返回协调。先收拢现有 `hideAllPanels/closeWorkspace` 相关调用,不引入新的路由系统;笔记模块最终不再独占其他工作台的公共关闭逻辑。
|
||||
- 窗口拖动、缩放、停靠先保留在外壳;仍形成独立大段时再提取 `browser_window.js`,复用生命周期工具,不改变尺寸偏好。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 工具箱与快捷栏自定义:交给 Gemini 的实施计划
|
||||
|
||||
状态:待实施的产品与工程计划,不代表当前版本已经具备以下功能。
|
||||
状态:已放弃。当前是固定快捷栏(见 `ui_toolbox.js`);按本计划写的 `shortcut_layout.js`、`ui_shortcut_organizer.js` 从未接入,已删除。只作历史参考,不要照此实施。
|
||||
基线:`77ae7ce88`;开工时以实际 Git 差异和现有代码为准,不回退后续工作。
|
||||
|
||||
## 目标与范围
|
||||
|
||||
@@ -77,17 +77,6 @@ export const i18n = {
|
||||
versionError_unknown_tag: "这不是已发布的版本。",
|
||||
versionError_no_previous: "没有可以撤销的切换。",
|
||||
versionError_no_default_branch: "无法确定默认分支。",
|
||||
shortcutBarPinned: "已固定",
|
||||
shortcutBarPin: "固定到快捷栏",
|
||||
shortcutBarUnpin: "从快捷栏移除",
|
||||
shortcutBarMoveLeft: "向左移动",
|
||||
shortcutBarMoveRight: "向右移动",
|
||||
shortcutBarCapacityFull: "快捷栏已满,请先将一个工具移回工具箱",
|
||||
shortcutBarReset: "恢复默认快捷栏",
|
||||
shortcutBarResetConfirm: "确定要将快捷栏恢复为默认布局吗?",
|
||||
shortcutDropHint: "拖到下方,固定常用工具;拖回这里,收起快捷入口",
|
||||
shortcutDropRelease: "松开,从快捷栏移除",
|
||||
shortcutBarAnchorTip: "固定入口,不可移动",
|
||||
updateGuideStartTour: "🎯 界面按键遮罩导览",
|
||||
updateGuideTourBanner: "💡 想在主界面实地体验?点击开启按键遮罩导览 ›",
|
||||
materialDragGlobalHint: "💡 提示:按住卡片直接拖到画布节点上注入参数,拖到空白处载入工作流",
|
||||
@@ -1810,17 +1799,6 @@ export const i18n = {
|
||||
versionError_unknown_tag: "That is not a published version.",
|
||||
versionError_no_previous: "There is no switch to undo.",
|
||||
versionError_no_default_branch: "Couldn't determine the default branch.",
|
||||
shortcutBarPinned: "Pinned",
|
||||
shortcutBarPin: "Pin to shortcuts",
|
||||
shortcutBarUnpin: "Remove from shortcuts",
|
||||
shortcutBarMoveLeft: "Move left",
|
||||
shortcutBarMoveRight: "Move right",
|
||||
shortcutBarCapacityFull: "Shortcut bar is full. Move a tool back to Toolbox first",
|
||||
shortcutBarReset: "Reset shortcut bar",
|
||||
shortcutBarResetConfirm: "Reset the shortcut bar to default layout?",
|
||||
shortcutDropHint: "Drag down to pin shortcuts; drag back here to unpin",
|
||||
shortcutDropRelease: "Release to remove from shortcuts",
|
||||
shortcutBarAnchorTip: "Fixed anchor, cannot be moved",
|
||||
updateGuideStartTour: "🎯 Spotlight Tour",
|
||||
updateGuideTourBanner: "💡 Prefer to see them in action? Start Spotlight Tour ›",
|
||||
materialDragGlobalHint: "💡 Tip: Drag cards onto canvas nodes to inject values, or onto empty canvas to load the workflow",
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
/**
|
||||
* shortcut_layout.js - State and Persistence for Customizable Shortcut Bar
|
||||
*
|
||||
* Implements schema validation, capacity limits, pinning, unpinning,
|
||||
* reordering and reset logic with localStorage persistence.
|
||||
*/
|
||||
|
||||
import { CATALOG_TOOLS } from './tool_registry.js';
|
||||
|
||||
export const SHORTCUT_STORAGE_KEY = 'anomalous_shortcut_layout_v1';
|
||||
export const SHORTCUT_SCHEMA_VERSION = 1;
|
||||
export const MAX_PINNED_SHORTCUTS = 4;
|
||||
|
||||
export const DEFAULT_PINNED_SHORTCUTS = Object.freeze([
|
||||
'scan',
|
||||
'doctor',
|
||||
'assistant',
|
||||
'materials',
|
||||
]);
|
||||
|
||||
// In-memory fallback if localStorage fails or is blocked
|
||||
let sessionMemoryLayout = null;
|
||||
|
||||
const VALID_CATALOG_IDS = new Set(CATALOG_TOOLS.map(t => t.id));
|
||||
|
||||
export function isValidCatalogToolId(id) {
|
||||
return VALID_CATALOG_IDS.has(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes an array of tool IDs:
|
||||
* - Drops invalid IDs, anchor IDs and non-strings
|
||||
* - Deduplicates preserving first occurrence
|
||||
* - Truncates to MAX_PINNED_SHORTCUTS
|
||||
*/
|
||||
export function normalizePinnedIds(ids) {
|
||||
if (!Array.isArray(ids)) return [...DEFAULT_PINNED_SHORTCUTS];
|
||||
const seen = new Set();
|
||||
const result = [];
|
||||
for (const id of ids) {
|
||||
if (typeof id === 'string' && VALID_CATALOG_IDS.has(id) && !seen.has(id)) {
|
||||
seen.add(id);
|
||||
result.push(id);
|
||||
if (result.length >= MAX_PINNED_SHORTCUTS) break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load shortcut layout from storage.
|
||||
* Explicit empty array is valid and preserved.
|
||||
*/
|
||||
export function loadShortcutLayout(storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
|
||||
if (sessionMemoryLayout !== null && !storage) {
|
||||
return [...sessionMemoryLayout];
|
||||
}
|
||||
|
||||
if (!storage) {
|
||||
return sessionMemoryLayout !== null ? [...sessionMemoryLayout] : [...DEFAULT_PINNED_SHORTCUTS];
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = storage.getItem(SHORTCUT_STORAGE_KEY);
|
||||
if (raw === null || raw === undefined) {
|
||||
return [...DEFAULT_PINNED_SHORTCUTS];
|
||||
}
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed || parsed.schemaVersion !== SHORTCUT_SCHEMA_VERSION || !Array.isArray(parsed.pinned)) {
|
||||
return [...DEFAULT_PINNED_SHORTCUTS];
|
||||
}
|
||||
return normalizePinnedIds(parsed.pinned);
|
||||
} catch (err) {
|
||||
console.warn('[AMB] Failed to read shortcut layout from storage, resetting to defaults:', err);
|
||||
sessionMemoryLayout = [...DEFAULT_PINNED_SHORTCUTS];
|
||||
return [...DEFAULT_PINNED_SHORTCUTS];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist shortcut layout to storage and memory.
|
||||
*/
|
||||
export function saveShortcutLayout(pinnedIds, storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
|
||||
const normalized = normalizePinnedIds(pinnedIds);
|
||||
sessionMemoryLayout = [...normalized];
|
||||
|
||||
if (!storage) {
|
||||
return { success: true, layout: normalized, memoryOnly: true };
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = JSON.stringify({
|
||||
schemaVersion: SHORTCUT_SCHEMA_VERSION,
|
||||
pinned: normalized,
|
||||
});
|
||||
storage.setItem(SHORTCUT_STORAGE_KEY, payload);
|
||||
return { success: true, layout: normalized, memoryOnly: false };
|
||||
} catch (err) {
|
||||
console.warn('[AMB] Failed to write shortcut layout to storage, saved to session memory:', err);
|
||||
return { success: true, layout: normalized, memoryOnly: true, error: err };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a tool to the shortcut bar.
|
||||
* If already pinned, reorder to targetIndex.
|
||||
* If not pinned and at capacity, rejects with 'capacity_full'.
|
||||
*/
|
||||
export function pinTool(toolId, targetIndex = -1, storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
|
||||
if (!isValidCatalogToolId(toolId)) {
|
||||
return { success: false, reason: 'unknown_tool', layout: loadShortcutLayout(storage) };
|
||||
}
|
||||
|
||||
const current = loadShortcutLayout(storage);
|
||||
const existingIndex = current.indexOf(toolId);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
// Already pinned: reorder to target position
|
||||
current.splice(existingIndex, 1);
|
||||
const insertAt = (targetIndex >= 0 && targetIndex <= current.length) ? targetIndex : current.length;
|
||||
current.splice(insertAt, 0, toolId);
|
||||
saveShortcutLayout(current, storage);
|
||||
return { success: true, reordered: true, layout: current };
|
||||
}
|
||||
|
||||
if (current.length >= MAX_PINNED_SHORTCUTS) {
|
||||
return { success: false, reason: 'capacity_full', layout: current };
|
||||
}
|
||||
|
||||
const insertAt = (targetIndex >= 0 && targetIndex <= current.length) ? targetIndex : current.length;
|
||||
current.splice(insertAt, 0, toolId);
|
||||
saveShortcutLayout(current, storage);
|
||||
return { success: true, reordered: false, layout: current };
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin a tool from the shortcut bar (returns it to toolbox only).
|
||||
*/
|
||||
export function unpinTool(toolId, storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
|
||||
const current = loadShortcutLayout(storage);
|
||||
const index = current.indexOf(toolId);
|
||||
if (index === -1) {
|
||||
return { success: true, changed: false, layout: current };
|
||||
}
|
||||
current.splice(index, 1);
|
||||
saveShortcutLayout(current, storage);
|
||||
return { success: true, changed: true, layout: current };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder an item in the shortcut bar.
|
||||
*/
|
||||
export function reorderShortcut(fromIndex, toIndex, storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
|
||||
const current = loadShortcutLayout(storage);
|
||||
if (fromIndex < 0 || fromIndex >= current.length || toIndex < 0 || toIndex >= current.length || fromIndex === toIndex) {
|
||||
return { success: false, layout: current };
|
||||
}
|
||||
const [item] = current.splice(fromIndex, 1);
|
||||
current.splice(toIndex, 0, item);
|
||||
saveShortcutLayout(current, storage);
|
||||
return { success: true, layout: current };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset shortcut layout to defaults.
|
||||
*/
|
||||
export function resetShortcutLayout(storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
|
||||
saveShortcutLayout(DEFAULT_PINNED_SHORTCUTS, storage);
|
||||
return { success: true, layout: [...DEFAULT_PINNED_SHORTCUTS] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool is currently pinned.
|
||||
*/
|
||||
export function isToolPinned(toolId, storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
|
||||
const current = loadShortcutLayout(storage);
|
||||
return current.includes(toolId);
|
||||
}
|
||||
@@ -1,503 +0,0 @@
|
||||
/**
|
||||
* ui_shortcut_organizer.js - Drag-and-drop & Menu Management for Shortcut Bar & Toolbox
|
||||
*
|
||||
* Implements:
|
||||
* 1. Pointer drag-and-drop between Toolbox and Shortcut Bar (with 6px deadzone).
|
||||
* 2. 350ms auto-expand when dragging over closed Toolbox button.
|
||||
* 3. Drop indicators and visual drop-zones.
|
||||
* 4. Capacity limit feedback (max 4 pinned items).
|
||||
* 5. Accessible context / "More" menus for organizing without mouse drag.
|
||||
* 6. Full cancel/cleanup on Escape, pointercancel, or window blur.
|
||||
*/
|
||||
|
||||
import { translate as t } from './locales.js';
|
||||
import { getToolDefinition, CATALOG_TOOLS, FIXED_ANCHORS } from './tool_registry.js';
|
||||
import {
|
||||
loadShortcutLayout,
|
||||
pinTool,
|
||||
unpinTool,
|
||||
reorderShortcut,
|
||||
resetShortcutLayout,
|
||||
isToolPinned,
|
||||
MAX_PINNED_SHORTCUTS,
|
||||
} from './shortcut_layout.js';
|
||||
import { setDraggingState, hideTooltipImmediately } from './sidebar_actions.js';
|
||||
import { showWorkbenchToast } from './ui_prompt_toast.js';
|
||||
|
||||
let activeDrag = null;
|
||||
let autoOpenTimer = null;
|
||||
let activeMenuEl = null;
|
||||
|
||||
function closeActiveMenu() {
|
||||
if (activeMenuEl) {
|
||||
activeMenuEl.remove();
|
||||
activeMenuEl = null;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', (e) => {
|
||||
if (activeMenuEl && !activeMenuEl.contains(e.target)) {
|
||||
closeActiveMenu();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
if (activeDrag) {
|
||||
cancelCurrentDrag();
|
||||
}
|
||||
closeActiveMenu();
|
||||
}
|
||||
});
|
||||
|
||||
export function isOrganizingDragActive() {
|
||||
return Boolean(activeDrag && activeDrag.isDragging);
|
||||
}
|
||||
|
||||
function showCapacityFullToast() {
|
||||
showWorkbenchToast(t('shortcutBarCapacityFull'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and shows a context dropdown menu.
|
||||
*/
|
||||
function showDropdownMenu(items, x, y) {
|
||||
closeActiveMenu();
|
||||
if (!items || items.length === 0) return;
|
||||
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'anomalous-shortcut-context-menu';
|
||||
menu.style.position = 'fixed';
|
||||
menu.style.zIndex = '1000002';
|
||||
menu.style.display = 'flex';
|
||||
menu.style.flexDirection = 'column';
|
||||
menu.style.minWidth = '140px';
|
||||
menu.style.padding = '4px';
|
||||
menu.style.borderRadius = '8px';
|
||||
menu.style.background = 'rgba(20, 22, 28, 0.96)';
|
||||
menu.style.backdropFilter = 'blur(16px)';
|
||||
menu.style.border = '1px solid rgba(255, 255, 255, 0.14)';
|
||||
menu.style.boxShadow = '0 12px 36px rgba(0, 0, 0, 0.7), 0 0 1px 1px rgba(255, 255, 255, 0.1)';
|
||||
|
||||
for (const item of items) {
|
||||
if (item.separator) {
|
||||
const sep = document.createElement('div');
|
||||
sep.style.height = '1px';
|
||||
sep.style.margin = '4px 2px';
|
||||
sep.style.background = 'rgba(255, 255, 255, 0.08)';
|
||||
menu.appendChild(sep);
|
||||
continue;
|
||||
}
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.textContent = item.label;
|
||||
btn.disabled = Boolean(item.disabled);
|
||||
btn.style.display = 'flex';
|
||||
btn.style.alignItems = 'center';
|
||||
btn.style.gap = '8px';
|
||||
btn.style.width = '100%';
|
||||
btn.style.padding = '6px 10px';
|
||||
btn.style.border = 'none';
|
||||
btn.style.borderRadius = '5px';
|
||||
btn.style.background = 'transparent';
|
||||
btn.style.color = item.disabled ? '#64748b' : (item.danger ? '#f87171' : '#e2e8f0');
|
||||
btn.style.fontSize = '12px';
|
||||
btn.style.cursor = item.disabled ? 'not-allowed' : 'pointer';
|
||||
btn.style.textAlign = 'left';
|
||||
btn.style.transition = 'all 0.12s ease';
|
||||
|
||||
if (!item.disabled) {
|
||||
btn.onmouseover = () => {
|
||||
btn.style.background = 'rgba(255, 255, 255, 0.08)';
|
||||
btn.style.color = '#ffffff';
|
||||
};
|
||||
btn.onmouseout = () => {
|
||||
btn.style.background = 'transparent';
|
||||
btn.style.color = item.danger ? '#f87171' : '#e2e8f0';
|
||||
};
|
||||
btn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
closeActiveMenu();
|
||||
item.action();
|
||||
};
|
||||
}
|
||||
|
||||
menu.appendChild(btn);
|
||||
}
|
||||
|
||||
document.body.appendChild(menu);
|
||||
|
||||
// Bounding clamp
|
||||
const menuRect = menu.getBoundingClientRect();
|
||||
let left = x;
|
||||
let top = y;
|
||||
|
||||
if (left + menuRect.width > window.innerWidth - 8) {
|
||||
left = window.innerWidth - menuRect.width - 8;
|
||||
}
|
||||
if (left < 8) left = 8;
|
||||
|
||||
if (top + menuRect.height > window.innerHeight - 8) {
|
||||
top = y - menuRect.height;
|
||||
}
|
||||
if (top < 8) top = 8;
|
||||
|
||||
menu.style.left = `${Math.round(left)}px`;
|
||||
menu.style.top = `${Math.round(top)}px`;
|
||||
|
||||
activeMenuEl = menu;
|
||||
}
|
||||
|
||||
export function openToolboxCardMenu(toolId, targetEl, onLayoutChange) {
|
||||
if (!targetEl || !toolId) return;
|
||||
const isPinned = isToolPinned(toolId);
|
||||
const layout = loadShortcutLayout();
|
||||
const isFull = layout.length >= MAX_PINNED_SHORTCUTS;
|
||||
|
||||
const items = [];
|
||||
if (isPinned) {
|
||||
items.push({
|
||||
label: t('shortcutBarUnpin'),
|
||||
action: () => {
|
||||
unpinTool(toolId);
|
||||
onLayoutChange?.();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
items.push({
|
||||
label: isFull ? `${t('shortcutBarPin')} (${t('shortcutBarCapacityFull') ? '已满' : 'Full'})` : t('shortcutBarPin'),
|
||||
disabled: isFull,
|
||||
action: () => {
|
||||
const res = pinTool(toolId);
|
||||
if (!res.success && res.reason === 'capacity_full') {
|
||||
showCapacityFullToast();
|
||||
} else {
|
||||
onLayoutChange?.();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const rect = targetEl.getBoundingClientRect();
|
||||
showDropdownMenu(items, rect.left, rect.bottom + 4);
|
||||
}
|
||||
|
||||
export function openShortcutButtonMenu(toolId, buttonEl, onLayoutChange) {
|
||||
if (!buttonEl || !toolId) return;
|
||||
const layout = loadShortcutLayout();
|
||||
const idx = layout.indexOf(toolId);
|
||||
if (idx === -1) return;
|
||||
|
||||
const items = [
|
||||
{
|
||||
label: t('shortcutBarMoveLeft'),
|
||||
disabled: idx <= 0,
|
||||
action: () => {
|
||||
reorderShortcut(idx, idx - 1);
|
||||
onLayoutChange?.();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('shortcutBarMoveRight'),
|
||||
disabled: idx >= layout.length - 1,
|
||||
action: () => {
|
||||
reorderShortcut(idx, idx + 1);
|
||||
onLayoutChange?.();
|
||||
}
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
label: t('shortcutBarUnpin'),
|
||||
danger: true,
|
||||
action: () => {
|
||||
unpinTool(toolId);
|
||||
onLayoutChange?.();
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const rect = buttonEl.getBoundingClientRect();
|
||||
showDropdownMenu(items, rect.left, rect.top - 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel and clean up active drag operation
|
||||
*/
|
||||
function cancelCurrentDrag() {
|
||||
if (autoOpenTimer) {
|
||||
clearTimeout(autoOpenTimer);
|
||||
autoOpenTimer = null;
|
||||
}
|
||||
if (activeDrag) {
|
||||
if (activeDrag.ghostEl) {
|
||||
activeDrag.ghostEl.remove();
|
||||
}
|
||||
if (activeDrag.indicatorEl) {
|
||||
activeDrag.indicatorEl.remove();
|
||||
}
|
||||
if (activeDrag.dropzoneEl) {
|
||||
activeDrag.dropzoneEl.classList.remove('is-drag-over');
|
||||
const txt = activeDrag.dropzoneEl.querySelector('.anomalous-toolbox-dropzone-text');
|
||||
if (txt) txt.textContent = t('shortcutDropHint');
|
||||
}
|
||||
if (activeDrag.originEl) {
|
||||
activeDrag.originEl.style.opacity = '1';
|
||||
}
|
||||
}
|
||||
setDraggingState(false);
|
||||
activeDrag = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds pointer drag & drop to a shortcut bar button or toolbox card.
|
||||
*/
|
||||
export function bindDraggableTool(element, {
|
||||
toolId,
|
||||
source, // 'shortcut' | 'toolbox'
|
||||
toolboxModal,
|
||||
toolboxBtn,
|
||||
sidebarActionsEl,
|
||||
onLayoutChange,
|
||||
onToolClick,
|
||||
}) {
|
||||
if (!element || !toolId) return;
|
||||
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let hasMoved = false;
|
||||
|
||||
const onPointerDown = (e) => {
|
||||
// Only primary mouse button (or touch/pen)
|
||||
if (e.button !== undefined && e.button !== 0 && e.pointerType === 'mouse') return;
|
||||
// Ignore if clicking a "More" menu button inside card
|
||||
if (e.target?.closest?.('.anomalous-tool-more-btn')) return;
|
||||
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
hasMoved = false;
|
||||
|
||||
const spec = getToolDefinition(toolId);
|
||||
if (!spec) return;
|
||||
|
||||
const onPointerMove = (moveEvent) => {
|
||||
const dx = moveEvent.clientX - startX;
|
||||
const dy = moveEvent.clientY - startY;
|
||||
const dist = Math.hypot(dx, dy);
|
||||
|
||||
if (!hasMoved) {
|
||||
if (dist > 6) {
|
||||
hasMoved = true;
|
||||
setDraggingState(true);
|
||||
hideTooltipImmediately();
|
||||
closeActiveMenu();
|
||||
|
||||
// Create ghost element
|
||||
const ghost = document.createElement('div');
|
||||
ghost.className = 'anomalous-shortcut-drag-ghost';
|
||||
ghost.innerHTML = `
|
||||
<div class="anomalous-ghost-icon">${spec.icon}</div>
|
||||
<div class="anomalous-ghost-title">${t(spec.labelKey)}</div>
|
||||
`;
|
||||
ghost.style.position = 'fixed';
|
||||
ghost.style.zIndex = '1000003';
|
||||
ghost.style.pointerEvents = 'none';
|
||||
ghost.style.transform = 'translate(-50%, -50%)';
|
||||
ghost.style.display = 'flex';
|
||||
ghost.style.alignItems = 'center';
|
||||
ghost.style.gap = '6px';
|
||||
ghost.style.padding = '6px 12px';
|
||||
ghost.style.borderRadius = '8px';
|
||||
ghost.style.background = 'rgba(28, 30, 38, 0.95)';
|
||||
ghost.style.border = '1px solid rgba(255, 255, 255, 0.25)';
|
||||
ghost.style.boxShadow = '0 12px 30px rgba(0,0,0,0.6)';
|
||||
ghost.style.color = '#ffffff';
|
||||
ghost.style.fontSize = '12px';
|
||||
ghost.style.fontWeight = '600';
|
||||
document.body.appendChild(ghost);
|
||||
|
||||
// Create drop indicator line for shortcut bar
|
||||
const indicator = document.createElement('div');
|
||||
indicator.className = 'anomalous-shortcut-drop-indicator';
|
||||
indicator.style.display = 'none';
|
||||
indicator.style.position = 'absolute';
|
||||
indicator.style.width = '2px';
|
||||
indicator.style.height = '24px';
|
||||
indicator.style.background = '#ffffff';
|
||||
indicator.style.boxShadow = '0 0 8px rgba(255, 255, 255, 0.8)';
|
||||
indicator.style.borderRadius = '1px';
|
||||
indicator.style.zIndex = '10';
|
||||
indicator.style.pointerEvents = 'none';
|
||||
if (sidebarActionsEl) {
|
||||
sidebarActionsEl.appendChild(indicator);
|
||||
}
|
||||
|
||||
const dropzone = toolboxModal ? toolboxModal.querySelector('.anomalous-toolbox-dropzone') : null;
|
||||
|
||||
activeDrag = {
|
||||
isDragging: true,
|
||||
toolId,
|
||||
source,
|
||||
originEl: element,
|
||||
ghostEl: ghost,
|
||||
indicatorEl: indicator,
|
||||
dropzoneEl: dropzone,
|
||||
targetIndex: -1,
|
||||
dropAction: null, // 'pin' | 'unpin' | 'reorder' | null
|
||||
};
|
||||
|
||||
element.style.opacity = '0.4';
|
||||
}
|
||||
}
|
||||
|
||||
if (activeDrag && activeDrag.isDragging) {
|
||||
// Move ghost
|
||||
activeDrag.ghostEl.style.left = `${moveEvent.clientX}px`;
|
||||
activeDrag.ghostEl.style.top = `${moveEvent.clientY}px`;
|
||||
|
||||
const overEl = typeof document.elementFromPoint === 'function'
|
||||
? document.elementFromPoint(moveEvent.clientX, moveEvent.clientY)
|
||||
: null;
|
||||
|
||||
// Check if over closed toolbox button (auto-expand after 350ms)
|
||||
if (toolboxBtn && toolboxBtn.contains(overEl)) {
|
||||
if (toolboxModal && toolboxModal.style.display === 'none') {
|
||||
if (!autoOpenTimer) {
|
||||
autoOpenTimer = setTimeout(() => {
|
||||
toolboxModal.style.display = 'flex';
|
||||
}, 350);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (autoOpenTimer) {
|
||||
clearTimeout(autoOpenTimer);
|
||||
autoOpenTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if over Toolbox or dropzone (for unpinning)
|
||||
const isOverToolbox = toolboxModal && toolboxModal.style.display !== 'none' && (toolboxModal.contains(overEl) || (toolboxBtn && toolboxBtn.contains(overEl)));
|
||||
if (isOverToolbox) {
|
||||
if (activeDrag.dropzoneEl) {
|
||||
activeDrag.dropzoneEl.classList.add('is-drag-over');
|
||||
const txt = activeDrag.dropzoneEl.querySelector('.anomalous-toolbox-dropzone-text');
|
||||
if (txt) txt.textContent = t('shortcutDropRelease');
|
||||
}
|
||||
if (activeDrag.indicatorEl) activeDrag.indicatorEl.style.display = 'none';
|
||||
activeDrag.dropAction = 'unpin';
|
||||
return;
|
||||
} else {
|
||||
if (activeDrag.dropzoneEl) {
|
||||
activeDrag.dropzoneEl.classList.remove('is-drag-over');
|
||||
const txt = activeDrag.dropzoneEl.querySelector('.anomalous-toolbox-dropzone-text');
|
||||
if (txt) txt.textContent = t('shortcutDropHint');
|
||||
}
|
||||
}
|
||||
|
||||
// Check if over Shortcut Bar (for pinning / reordering)
|
||||
const isOverShortcutBar = sidebarActionsEl && sidebarActionsEl.contains(overEl);
|
||||
if (isOverShortcutBar) {
|
||||
const buttons = Array.from(sidebarActionsEl.querySelectorAll('button[data-tool-id]'));
|
||||
let foundIndex = buttons.length;
|
||||
let indicatorX = 0;
|
||||
|
||||
const barRect = sidebarActionsEl.getBoundingClientRect();
|
||||
|
||||
for (let i = 0; i < buttons.length; i++) {
|
||||
const bRect = buttons[i].getBoundingClientRect();
|
||||
const bMidX = bRect.left + bRect.width / 2;
|
||||
if (moveEvent.clientX < bMidX) {
|
||||
foundIndex = i;
|
||||
indicatorX = bRect.left - barRect.left - 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundIndex === buttons.length && buttons.length > 0) {
|
||||
const lastRect = buttons[buttons.length - 1].getBoundingClientRect();
|
||||
indicatorX = lastRect.right - barRect.left + 2;
|
||||
}
|
||||
|
||||
activeDrag.targetIndex = foundIndex;
|
||||
activeDrag.dropAction = 'pin';
|
||||
|
||||
if (activeDrag.indicatorEl) {
|
||||
activeDrag.indicatorEl.style.display = 'block';
|
||||
activeDrag.indicatorEl.style.left = `${Math.round(indicatorX)}px`;
|
||||
activeDrag.indicatorEl.style.top = '6px';
|
||||
}
|
||||
} else {
|
||||
if (activeDrag.indicatorEl) {
|
||||
activeDrag.indicatorEl.style.display = 'none';
|
||||
}
|
||||
activeDrag.dropAction = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onPointerUp = (upEvent) => {
|
||||
document.removeEventListener('pointermove', onPointerMove);
|
||||
document.removeEventListener('pointerup', onPointerUp);
|
||||
document.removeEventListener('pointercancel', onPointerCancel);
|
||||
|
||||
if (autoOpenTimer) {
|
||||
clearTimeout(autoOpenTimer);
|
||||
autoOpenTimer = null;
|
||||
}
|
||||
|
||||
if (!hasMoved) {
|
||||
// Ordinary click!
|
||||
if (onToolClick) {
|
||||
onToolClick(upEvent);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Drag finished: handle drop action
|
||||
if (activeDrag && activeDrag.isDragging) {
|
||||
upEvent.preventDefault();
|
||||
upEvent.stopPropagation();
|
||||
|
||||
const { dropAction, targetIndex } = activeDrag;
|
||||
|
||||
if (dropAction === 'unpin') {
|
||||
unpinTool(toolId);
|
||||
onLayoutChange?.();
|
||||
} else if (dropAction === 'pin') {
|
||||
const res = pinTool(toolId, targetIndex);
|
||||
if (!res.success && res.reason === 'capacity_full') {
|
||||
showCapacityFullToast();
|
||||
} else {
|
||||
onLayoutChange?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cancelCurrentDrag();
|
||||
};
|
||||
|
||||
const onPointerCancel = () => {
|
||||
document.removeEventListener('pointermove', onPointerMove);
|
||||
document.removeEventListener('pointerup', onPointerUp);
|
||||
document.removeEventListener('pointercancel', onPointerCancel);
|
||||
cancelCurrentDrag();
|
||||
};
|
||||
|
||||
document.addEventListener('pointermove', onPointerMove);
|
||||
document.addEventListener('pointerup', onPointerUp);
|
||||
document.addEventListener('pointercancel', onPointerCancel);
|
||||
};
|
||||
|
||||
element.addEventListener('pointerdown', onPointerDown);
|
||||
|
||||
// Also support right-click context menu
|
||||
element.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (source === 'shortcut') {
|
||||
openShortcutButtonMenu(toolId, element, onLayoutChange);
|
||||
} else {
|
||||
openToolboxCardMenu(toolId, element, onLayoutChange);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user