Files
MechaBabyAi-ComfyUI-MechaBa…/web/js/nodeSearch.js
T
Mecha Baby 5ad28681d0 v1.3.2: Remove template literals from nodeSearch for ES5 compatibility
- Replace CSS template literals with plain concatenated strings
- Keep dialog and settings UI styles identical while avoiding backtick syntax
2026-01-07 17:45:07 +08:00

1254 lines
53 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* ComfyUI MechaBaby Node Search Extension
*
* 功能说明�?
* 1. 节点名称搜索定位 - 支持节点标题和类型的搜索
* 2. 节点属性搜�?- 搜索控件名称、控件值、属性名称、属性�?
* 3. 快捷键支�?- Ctrl+F 快速打开搜索对话�?
* 4. 键盘导航 - 支持上下箭头键选择,Enter 跳转,Esc 关闭
*
* 技术实现:
* - 使用 ComfyUI Extension API (app.registerExtension)
* - 访问 app.graph._nodes 获取所有节�?
* - 使用 app.canvas.centerOnNode() 实现节点定位
* - 创建自定义对话框 UI 显示搜索结果
*
* 依赖�?
* - ComfyUI 核心 API (app, app.graph, app.canvas)
* - LiteGraph API (LGraphCanvas)
*
* @file nodeSearch.js
* @author MechaBaby
* @version 1.3.2
*/
import { app } from "../../../scripts/app.js";
// 多语言资源
var i18n = {
'zh-CN': {
searchNodes: '搜索节点',
inputPlaceholder: '输入关键词搜索节点名称、ID、属性名称或�?..',
closeButton: '关闭 (Esc)',
noResults: '未找到匹配的节点',
foundNodes: '找到 {0} 个节点,{1} 个匹配项',
nodeTitle: '节点标题',
nodeType: '节点类型',
nodeId: '节点ID',
widget: '控件',
value: '�?,
property: '属�?,
propertyValue: '属性�?,
unknownNode: '未知节点',
errorNode: '错误节点',
nodeMayNotLoaded: '节点可能未正确加�?,
matches: '{0} 个匹�?,
moreMatches: '... 还有 {0} 个匹配项',
nodeLabel: '节点: ',
typeLabel: '类型: ',
nodeIdLabel: 'ID: ',
widgetLabel: '控件: ',
valueLabel: '�? ',
propertyLabel: '属�? ',
propertyValueLabel: '属性�? ',
nodeMayNotLoadedHint: ' | ⚠️ 节点可能未正确加�?,
settings: '设置',
nodeSearchSettings: '节点搜索设置',
language: '语言',
shortcut: '快捷�?,
save: '保存',
cancel: '取消',
selectLanguage: '选择语言',
selectShortcut: '设置快捷�?,
currentShortcut: '当前快捷�? ',
pressKey: '按下您想要的快捷键组�?..',
invalidShortcut: '无效的快捷键,请至少包含 Ctrl/Cmd 和一个按�?,
shortcutSaved: '快捷键已保存',
languageSaved: '语言已保存,请刷新页�?,
autoDetect: '自动检�?,
chinese: '中文',
english: 'English',
japanese: '日本�?,
korean: '한국�?,
russian: 'Русский'
},
'en-US': {
searchNodes: 'Search Nodes',
inputPlaceholder: 'Enter keywords to search node names, IDs, properties, or values...',
closeButton: 'Close (Esc)',
noResults: 'No matching nodes found',
foundNodes: 'Found {0} nodes, {1} matches',
nodeTitle: 'Node Title',
nodeType: 'Node Type',
nodeId: 'Node ID',
widget: 'Widget',
value: 'Value',
property: 'Property',
propertyValue: 'Property Value',
unknownNode: 'Unknown Node',
errorNode: 'Error Node',
nodeMayNotLoaded: 'Node may not be loaded correctly',
matches: '{0} matches',
moreMatches: '... {0} more matches',
nodeLabel: 'Node: ',
typeLabel: 'Type: ',
nodeIdLabel: 'ID: ',
widgetLabel: 'Widget: ',
valueLabel: 'Value: ',
propertyLabel: 'Property: ',
propertyValueLabel: 'Property Value: ',
nodeMayNotLoadedHint: ' | ⚠️ Node may not be loaded correctly',
settings: 'Settings',
nodeSearchSettings: 'NodeSearch Settings',
language: 'Language',
shortcut: 'Shortcut',
save: 'Save',
cancel: 'Cancel',
selectLanguage: 'Select Language',
selectShortcut: 'Set Shortcut',
currentShortcut: 'Current Shortcut: ',
pressKey: 'Press your desired shortcut combination...',
invalidShortcut: 'Invalid shortcut, please include at least Ctrl/Cmd and one key',
shortcutSaved: 'Shortcut saved',
languageSaved: 'Language saved, please refresh the page',
autoDetect: 'Auto Detect',
chinese: '中文',
english: 'English',
japanese: '日本�?,
korean: '한국�?,
russian: 'Русский'
},
'ja-JP': {
searchNodes: 'ノード検�?,
inputPlaceholder: 'キーワードを入力してノード名、ID、プロパティ、または値を検索...',
closeButton: '閉じ�?(Esc)',
noResults: '一致するノードが見つかりません',
foundNodes: '{0} 個のノード、{1} 個の一致が見つかりまし�?,
nodeTitle: 'ノードタイト�?,
nodeType: 'ノードタイプ',
nodeId: 'ノードID',
widget: 'ウィジェット',
value: '�?,
property: 'プロパテ�?,
propertyValue: 'プロパティ�?,
unknownNode: '不明なノード',
errorNode: 'エラーノード',
nodeMayNotLoaded: 'ノードが正しく読み込まれていない可能性があります',
matches: '{0} 個の一�?,
moreMatches: '... あと {0} 個の一�?,
nodeLabel: 'ノー�? ',
typeLabel: 'タイ�? ',
nodeIdLabel: 'ID: ',
widgetLabel: 'ウィジェット: ',
valueLabel: '�? ',
propertyLabel: 'プロパテ�? ',
propertyValueLabel: 'プロパティ�? ',
nodeMayNotLoadedHint: ' | ⚠️ ノードが正しく読み込まれていない可能性があります',
settings: '設定',
nodeSearchSettings: 'NodeSearch 設定',
language: '言�?,
shortcut: 'ショートカッ�?,
save: '保存',
cancel: 'キャンセ�?,
selectLanguage: '言語を選択',
selectShortcut: 'ショートカットを設定',
currentShortcut: '現在のショートカット: ',
pressKey: '希望のショートカットキーの組み合わせを押してください...',
invalidShortcut: '無効なショートカットです。Ctrl/Cmdと少なくとも1つのキーを含めてください',
shortcutSaved: 'ショートカットが保存されまし�?,
languageSaved: '言語が保存されました。ページを更新してくださ�?,
autoDetect: '自動検出',
chinese: '中文',
english: 'English',
japanese: '日本�?,
korean: '한국�?,
russian: 'Русский'
},
'ko-KR': {
searchNodes: '노드 검�?,
inputPlaceholder: '키워드를 입력하여 노드 이름, ID, 속성 또는 값을 검�?..',
closeButton: '닫기 (Esc)',
noResults: '일치하는 노드�?찾을 �?없습니다',
foundNodes: '{0}개의 노드, {1}개의 일치 항목�?찾았습니�?,
nodeTitle: '노드 제목',
nodeType: '노드 유형',
nodeId: '노드 ID',
widget: '위젯',
value: '�?,
property: '속성',
propertyValue: '속성 �?,
unknownNode: '�?�?없는 노드',
errorNode: '오류 노드',
nodeMayNotLoaded: '노드가 제대�?로드되지 않았�?�?있습니다',
matches: '{0}개의 일치',
moreMatches: '... {0}�?�?일치',
nodeLabel: '노드: ',
typeLabel: '유형: ',
nodeIdLabel: 'ID: ',
widgetLabel: '위젯: ',
valueLabel: '�? ',
propertyLabel: '속성: ',
propertyValueLabel: '속성 �? ',
nodeMayNotLoadedHint: ' | ⚠️ 노드가 제대�?로드되지 않았�?�?있습니다',
settings: '설정',
nodeSearchSettings: 'NodeSearch 설정',
language: '언어',
shortcut: '단축�?,
save: '저�?,
cancel: '취소',
selectLanguage: '언어 선택',
selectShortcut: '단축�?설정',
currentShortcut: '현재 단축�? ',
pressKey: '원하�?단축�?조합�?누르세요...',
invalidShortcut: '잘못�?단축키입니다. Ctrl/Cmd와 최소 하나�?키를 포함하세�?,
shortcutSaved: '단축키가 저장되었습니다',
languageSaved: '언어가 저장되었습니다. 페이지�?새로고침하세�?,
autoDetect: '자동 감지',
chinese: '中文',
english: 'English',
japanese: '日本�?,
korean: '한국�?,
russian: 'Русский'
},
'ru-RU': {
searchNodes: 'Поиск узлов',
inputPlaceholder: 'Введите ключевые слова для поиска имен узлов, ID, свойств или значений...',
closeButton: 'Закрыть (Esc)',
noResults: 'Совпадающие узлы не найдены',
foundNodes: 'Найдено {0} узлов, {1} совпадений',
nodeTitle: 'Заголовок узла',
nodeType: 'Тип узла',
nodeId: 'ID узла',
widget: 'Виджет',
value: 'Значение',
property: 'Свойство',
propertyValue: 'Значение свойства',
unknownNode: 'Неизвестный узел',
errorNode: 'Ошибочный узел',
nodeMayNotLoaded: 'Узел может быть загружен неправильно',
matches: '{0} совпадений',
moreMatches: '... еще {0} совпадений',
nodeLabel: 'Узел: ',
typeLabel: 'Тип: ',
nodeIdLabel: 'ID: ',
widgetLabel: 'Виджет: ',
valueLabel: 'Значение: ',
propertyLabel: 'Свойство: ',
propertyValueLabel: 'Значение свойства: ',
nodeMayNotLoadedHint: ' | ⚠️ Узел может быть загружен неправильно',
settings: 'Настройки',
nodeSearchSettings: 'NodeSearch Настройки',
language: 'Язык',
shortcut: 'Горячая клавиша',
save: 'Сохранить',
cancel: 'Отмена',
selectLanguage: 'Выберите язык',
selectShortcut: 'Установить горячую клавишу',
currentShortcut: 'Текущая горячая клавиша: ',
pressKey: 'Нажмите желаемую комбинацию клавиш...',
invalidShortcut: 'Неверная горячая клавиша, пожалуйста, включите как минимум Ctrl/Cmd и одну клавишу',
shortcutSaved: 'Горячая клавиша сохранена',
languageSaved: 'Язык сохранен, пожалуйста, обновите страницу',
autoDetect: 'Автоопределение',
chinese: '中文',
english: 'English',
japanese: '日本�?,
korean: '한국�?,
russian: 'Русский'
}
};
// 语言代码映射(将浏览器语言代码映射到支持的语言�?
var langMap = {
'zh': 'zh-CN',
'zh-CN': 'zh-CN',
'zh-TW': 'zh-CN',
'en': 'en-US',
'en-US': 'en-US',
'en-GB': 'en-US',
'ja': 'ja-JP',
'ja-JP': 'ja-JP',
'ko': 'ko-KR',
'ko-KR': 'ko-KR',
'ru': 'ru-RU',
'ru-RU': 'ru-RU'
};
// 配置管理
var config = {
// 获取当前语言
getLanguage: function() {
var saved = localStorage.getItem('mechababy.nodeSearch.language');
if (saved && i18n[saved]) {
return saved;
}
// 自动检测浏览器语言
var browserLang = navigator.language || navigator.userLanguage || 'en-US';
return langMap[browserLang] || langMap[browserLang.split('-')[0]] || 'en-US';
},
// 设置语言
setLanguage: function(lang) {
if (i18n[lang]) {
localStorage.setItem('mechababy.nodeSearch.language', lang);
return true;
}
return false;
},
// 获取当前快捷�?
getShortcut: function() {
var saved = localStorage.getItem('mechababy.nodeSearch.shortcut');
if (saved) {
try {
return JSON.parse(saved);
} catch (e) {
return { ctrl: true, key: 'f' };
}
}
return { ctrl: true, key: 'f' }; // 默认 Ctrl+F
},
// 设置快捷�?
setShortcut: function(shortcut) {
try {
localStorage.setItem('mechababy.nodeSearch.shortcut', JSON.stringify(shortcut));
return true;
} catch (e) {
return false;
}
},
// 格式化快捷键显示
formatShortcut: function(shortcut) {
if (!shortcut) shortcut = this.getShortcut();
var parts = [];
if (shortcut.ctrl) parts.push('Ctrl');
if (shortcut.alt) parts.push('Alt');
if (shortcut.shift) parts.push('Shift');
if (shortcut.meta) parts.push('Cmd');
if (shortcut.key) {
var key = shortcut.key;
if (key.length === 1) {
key = key.toUpperCase();
}
parts.push(key);
}
return parts.join('+');
}
};
// 获取当前语言的文�?
function t(key) {
var lang = config.getLanguage();
var texts = i18n[lang] || i18n['en-US'];
var text = texts[key] || i18n['en-US'][key] || key;
// 简单的格式化(支持 {0}, {1} 等占位符�?
if (arguments.length > 1) {
for (var i = 1; i < arguments.length; i++) {
text = text.replace('{' + (i - 1) + '}', arguments[i]);
}
}
return text;
}
// 模块级变量,用于在不�?hook 之间共享
var nodeSearchState = {
searchDialog: null,
searchResults: [],
currentResultIndex: -1,
keyboardHandlerBound: false,
openSearchDialog: null, // 将在 setup 中设�?
currentHandler: null, // 当前快捷键处理器
settingsDialog: null // 设置对话�?
};
app.registerExtension({
name: "MechaBaby.NodeSearch",
// 新的 Context Menu API hook
getCanvasMenuItems: function() {
var currentShortcut = config.formatShortcut();
return [
null, // separator
{
content: '🔍 ' + t('searchNodes') + ' (' + currentShortcut + ')',
callback: function() {
if (nodeSearchState.openSearchDialog) {
nodeSearchState.openSearchDialog();
}
}
},
null, // separator
{
content: '⚙️ ' + t('nodeSearchSettings'),
has_submenu: true,
submenu: {
options: [
{
content: t('language'),
has_submenu: true,
submenu: {
options: [
{ content: t('autoDetect'), callback: function() { config.setLanguage(config.getLanguage()); } },
null,
{ content: '🇨🇳 ' + t('chinese'), callback: function() { config.setLanguage('zh-CN'); alert(t('languageSaved')); } },
{ content: '🇺🇸 ' + t('english'), callback: function() { config.setLanguage('en-US'); alert(t('languageSaved')); } },
{ content: '🇯🇵 ' + t('japanese'), callback: function() { config.setLanguage('ja-JP'); alert(t('languageSaved')); } },
{ content: '🇰🇷 ' + t('korean'), callback: function() { config.setLanguage('ko-KR'); alert(t('languageSaved')); } },
{ content: '🇷🇺 ' + t('russian'), callback: function() { config.setLanguage('ru-RU'); alert(t('languageSaved')); } }
]
}
},
{
content: t('shortcut'),
callback: function() {
if (nodeSearchState.openSettingsDialog) {
nodeSearchState.openSettingsDialog();
}
}
}
]
}
}
];
},
setup: function() {
var searchDialog = nodeSearchState.searchDialog;
var searchResults = nodeSearchState.searchResults;
var currentResultIndex = nodeSearchState.currentResultIndex;
var keyboardHandlerBound = nodeSearchState.keyboardHandlerBound;
/**
* 搜索节点(包括名称、属性名称、属性值)
* @param {string} keyword - 搜索关键�?
*/
function searchNodes(keyword) {
if (!keyword || keyword.trim() === "") {
return [];
}
var keywordLower = keyword.toLowerCase().trim();
var results = [];
// 搜索工作流中的节�?
if (app.graph && app.graph._nodes) {
app.graph._nodes.forEach(function(node) {
// 使用 try-catch 保护,避免红色报错节点导致搜索中�?
try {
// 检查节点是否有效(红色报错节点可能缺少某些属性)
if (!node) return;
const matches = [];
let nodeTitle = "";
// 安全地获取节点标�?
try {
nodeTitle = node.getTitle ? node.getTitle() : (node.title || node.type || "");
} catch (e) {
nodeTitle = node.type || node.title || t('unknownNode');
}
// 1. 搜索节点标题
if (nodeTitle && nodeTitle.toLowerCase().includes(keywordLower)) {
matches.push({
type: 'node_title',
name: t('nodeTitle'),
value: nodeTitle,
display: t('nodeLabel') + nodeTitle
});
}
// 2. 搜索节点类型
if (node.type && node.type.toLowerCase().includes(keywordLower)) {
matches.push({
type: 'node_type',
name: t('nodeType'),
value: node.type,
display: t('typeLabel') + node.type
});
}
// 3. 搜索节点ID
if (node.id !== undefined && node.id !== null) {
var nodeIdStr = String(node.id);
if (nodeIdStr.toLowerCase().includes(keywordLower)) {
matches.push({
type: 'node_id',
name: t('nodeId'),
value: node.id,
display: t('nodeIdLabel') + nodeIdStr
});
}
}
// 4. 搜索控件名称和值(安全访问�?
try {
if (node.widgets && Array.isArray(node.widgets)) {
node.widgets.forEach(function(widget, index) {
try {
if (!widget) return;
const widgetName = widget.name || '';
const widgetValue = (node.widgets_values && node.widgets_values[index] !== undefined)
? node.widgets_values[index]
: (widget.value !== undefined ? widget.value : '');
// 搜索控件名称
if (widgetName && widgetName.toLowerCase().includes(keywordLower)) {
matches.push({
type: 'widget_name',
name: widgetName,
value: widgetValue,
display: t('widgetLabel') + widgetName + ' = ' + String(widgetValue).substring(0, 50)
});
}
// 搜索控件值(转换为字符串�?
const valueStr = String(widgetValue);
if (valueStr && valueStr.toLowerCase().includes(keywordLower) && widgetName) {
matches.push({
type: 'widget_value',
name: widgetName,
value: widgetValue,
display: t('valueLabel') + widgetName + ' = ' + valueStr.substring(0, 50)
});
}
} catch (widgetError) {
// 单个控件出错不影响其他控�?
console.debug("[MechaBaby NodeSearch] 搜索控件时出�?", widgetError);
}
});
}
} catch (widgetsError) {
// 控件访问出错,继续搜索其他属�?
console.debug("[MechaBaby NodeSearch] 访问节点控件时出�?", widgetsError);
}
// 5. 搜索节点属性(安全访问�?
try {
if (node.properties && typeof node.properties === 'object') {
Object.keys(node.properties).forEach(function(propName) {
try {
var propValue = node.properties[propName];
var propValueStr = String(propValue);
// 搜索属性名�?
if (propName && propName.toLowerCase().includes(keywordLower)) {
matches.push({
type: 'property_name',
name: propName,
value: propValue,
display: t('propertyLabel') + propName + ' = ' + propValueStr.substring(0, 50)
});
}
// 搜索属性�?
if (propValueStr && propValueStr.toLowerCase().includes(keywordLower)) {
matches.push({
type: 'property_value',
name: propName,
value: propValue,
display: t('propertyValueLabel') + propName + ' = ' + propValueStr.substring(0, 50)
});
}
} catch (propError) {
// 单个属性出错不影响其他属�?
console.debug("[MechaBaby NodeSearch] 搜索属性时出错:", propError);
}
});
}
} catch (propertiesError) {
// 属性访问出错,继续处理
console.debug("[MechaBaby NodeSearch] 访问节点属性时出错:", propertiesError);
}
// 如果有匹配,添加到结�?
if (matches.length > 0) {
results.push({
node: node,
nodeTitle: nodeTitle,
nodeType: node.type || "未知类型",
nodeId: node.id,
matches: matches,
matchCount: matches.length,
isAvailableNode: false // 工作流中的节�?
});
}
} catch (nodeError) {
// 节点处理出错,记录但继续搜索其他节点
const nodeTypeStr = (node && node.type) ? node.type : "未知";
console.debug("[MechaBaby NodeSearch] 搜索节点时出错(可能是未安装的节点):", nodeError, nodeTypeStr);
// 即使节点出错,也尝试搜索节点类型(如果可用)
if (node && node.type) {
try {
const nodeType = node.type;
if (nodeType.toLowerCase().includes(keywordLower)) {
results.push({
node: node,
nodeTitle: nodeType + ' (' + t('errorNode') + ')',
nodeType: nodeType,
nodeId: node.id,
matches: [{
type: 'node_type',
name: t('nodeType'),
value: nodeType,
display: t('typeLabel') + nodeType + ' (' + t('nodeMayNotLoaded') + ')'
}],
matchCount: 1,
isAvailableNode: false,
hasError: true // 标记为有错误的节�?
});
}
} catch (e) {
// 完全无法处理,跳�?
}
}
}
});
}
return results;
}
/**
* 跳转到节点并高亮
*/
function jumpToNode(node, matchIndex) {
if (matchIndex === undefined) {
matchIndex = 0;
}
if (!node) return;
// 跳转到节�?
app.canvas.centerOnNode(node);
// 选中节点
app.canvas.selectNode(node);
// 添加金黄色闪烁高亮效�?
highlightNode(node);
}
/**
* 高亮闪烁节点(金黄色效果�?
*/
function highlightNode(node) {
if (!node) return;
// 保存原始颜色
var originalColor = node.color;
var originalBgColor = node.bgcolor;
// 金黄色高亮颜�?
var highlightColor = "#FFD700";
var highlightBgColor = "#4a3d00";
var flashCount = 0;
var maxFlashes = 6; // 闪烁3次(6次切换)
var flashInterval = 150; // 每次闪烁间隔150ms
function flash() {
if (flashCount >= maxFlashes) {
// 恢复原始颜色
node.color = originalColor;
node.bgcolor = originalBgColor;
app.canvas.setDirty(true, true);
return;
}
if (flashCount % 2 === 0) {
// 高亮
node.color = highlightColor;
node.bgcolor = highlightBgColor;
} else {
// 恢复
node.color = originalColor;
node.bgcolor = originalBgColor;
}
app.canvas.setDirty(true, true);
flashCount++;
setTimeout(flash, flashInterval);
}
// 开始闪�?
flash();
}
/**
* 创建搜索对话�?
*/
function createSearchDialog() {
if (searchDialog) {
return searchDialog;
}
const dialog = document.createElement('div');
dialog.id = 'mechababy-node-search-dialog';
dialog.style.cssText =
'position: fixed;' +
'top: 50%;' +
'left: 50%;' +
'transform: translate(-50%, -50%);' +
'background: #2a2a2a;' +
'border: 2px solid #4a4a4a;' +
'border-radius: 8px;' +
'padding: 20px;' +
'z-index: 10000;' +
'min-width: 500px;' +
'max-width: 700px;' +
'max-height: 600px;' +
'display: flex;' +
'flex-direction: column;' +
'box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);' +
"font-family: 'Microsoft YaHei', 'SimHei', Arial, sans-serif;";
// 标题
const title = document.createElement('div');
title.textContent = '🔍 ' + t('searchNodes');
title.style.cssText =
'font-size: 18px;' +
'font-weight: bold;' +
'color: #e0e0e0;' +
'margin-bottom: 15px;' +
'border-bottom: 1px solid #4a4a4a;' +
'padding-bottom: 10px;';
dialog.appendChild(title);
// 搜索输入�?
const inputContainer = document.createElement('div');
inputContainer.style.cssText = 'margin-bottom: 15px;';
const input = document.createElement('input');
input.type = 'text';
input.placeholder = t('inputPlaceholder');
input.style.cssText =
'width: 100%;' +
'padding: 10px;' +
'background: #1a1a1a;' +
'border: 1px solid #4a4a4a;' +
'border-radius: 4px;' +
'color: #e0e0e0;' +
'font-size: 14px;' +
'box-sizing: border-box;';
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
if (searchResults.length > 0 && currentResultIndex >= 0) {
var result = searchResults[currentResultIndex];
jumpToNode(result.node);
closeDialog();
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
if (currentResultIndex < searchResults.length - 1) {
currentResultIndex++;
updateResultsList();
}
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (currentResultIndex > 0) {
currentResultIndex--;
updateResultsList();
}
} else if (e.key === 'Escape') {
closeDialog();
}
});
input.addEventListener('input', function(e) {
var keyword = e.target.value;
searchResults = searchNodes(keyword);
currentResultIndex = searchResults.length > 0 ? 0 : -1;
updateResultsList();
});
inputContainer.appendChild(input);
dialog.appendChild(inputContainer);
// 结果列表容器
const resultsContainer = document.createElement('div');
resultsContainer.id = 'search-results-container';
resultsContainer.style.cssText =
'flex: 1;' +
'overflow-y: auto;' +
'max-height: 400px;' +
'border: 1px solid #4a4a4a;' +
'border-radius: 4px;' +
'background: #1a1a1a;';
dialog.appendChild(resultsContainer);
// 提示信息
const info = document.createElement('div');
info.id = 'search-info';
info.style.cssText =
'margin-top: 10px;' +
'font-size: 12px;' +
'color: #888;' +
'text-align: center;';
dialog.appendChild(info);
// 关闭按钮
const closeBtn = document.createElement('button');
closeBtn.textContent = t('closeButton');
closeBtn.style.cssText =
'margin-top: 10px;' +
'padding: 8px 16px;' +
'background: #4a4a4a;' +
'border: none;' +
'border-radius: 4px;' +
'color: #e0e0e0;' +
'cursor: pointer;' +
'font-size: 14px;';
closeBtn.addEventListener('click', closeDialog);
closeBtn.addEventListener('mouseenter', function() {
closeBtn.style.background = '#5a5a5a';
});
closeBtn.addEventListener('mouseleave', function() {
closeBtn.style.background = '#4a4a4a';
});
dialog.appendChild(closeBtn);
// 更新结果列表
function updateResultsList() {
const container = resultsContainer;
container.innerHTML = '';
if (searchResults.length === 0) {
const emptyMsg = document.createElement('div');
emptyMsg.textContent = t('noResults');
emptyMsg.style.cssText =
'padding: 20px;' +
'text-align: center;' +
'color: #888;';
container.appendChild(emptyMsg);
info.textContent = '';
return;
}
var totalMatches = 0;
for (var i = 0; i < searchResults.length; i++) {
totalMatches += searchResults[i].matchCount;
}
info.textContent = t('foundNodes', searchResults.length, totalMatches);
for (var idx = 0; idx < searchResults.length; idx++) {
(function(index) {
var result = searchResults[index];
var item = document.createElement('div');
item.setAttribute('data-result-index', index);
item.style.cssText = 'padding: 12px; border-bottom: 1px solid #3a3a3a; cursor: pointer; background: ' + (index === currentResultIndex ? '#3a3a3a' : 'transparent') + '; transition: background 0.2s;';
item.addEventListener('mouseenter', function() {
if (index !== currentResultIndex) {
item.style.background = '#333';
}
});
item.addEventListener('mouseleave', function() {
if (index !== currentResultIndex) {
item.style.background = 'transparent';
}
});
item.addEventListener('click', function() {
currentResultIndex = index;
updateResultsList();
if (result.hasError) {
// 错误节点,尝试跳�?
if (result.node) {
try {
jumpToNode(result.node);
} catch (e) {
alert(t('nodeMayNotLoaded') + ': "' + result.nodeTitle + '"');
}
}
} else {
// 跳转到节�?
jumpToNode(result.node);
closeDialog();
}
});
// 节点标题
var title = document.createElement('div');
var statusBadge = result.hasError ? ' [' + t('errorNode') + ']' : '';
title.textContent = result.nodeTitle + statusBadge + ' (' + t('matches', result.matchCount) + ')';
title.style.cssText = 'font-weight: bold; color: ' + (result.hasError ? '#ff4a4a' : '#4a9eff') + '; margin-bottom: 5px; font-size: 14px;';
item.appendChild(title);
// 节点类型
var type = document.createElement('div');
var typeText = t('typeLabel') + result.nodeType;
if (result.hasError) {
typeText += t('nodeMayNotLoadedHint');
}
type.textContent = typeText;
type.style.cssText = 'color: ' + (result.hasError ? '#ff8888' : '#888') + '; font-size: 12px; margin-bottom: 8px;';
item.appendChild(type);
// 匹配项列表(最多显�?个)
var matchesList = document.createElement('div');
var displayMatches = result.matches.slice(0, 3);
for (var j = 0; j < displayMatches.length; j++) {
var match = displayMatches[j];
var matchItem = document.createElement('div');
matchItem.textContent = ' �?' + match.display;
matchItem.style.cssText = 'color: #aaa; font-size: 12px; margin-left: 10px; margin-bottom: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;';
matchesList.appendChild(matchItem);
}
if (result.matches.length > 3) {
var more = document.createElement('div');
more.textContent = ' ' + t('moreMatches', result.matches.length - 3);
more.style.cssText = 'color: #666; font-size: 11px; margin-left: 10px; font-style: italic;';
matchesList.appendChild(more);
}
item.appendChild(matchesList);
container.appendChild(item);
})(idx);
}
// 自动滚动到当前选中的项
if (currentResultIndex >= 0 && currentResultIndex < searchResults.length) {
setTimeout(function() {
var selectedItem = container.querySelector('[data-result-index="' + currentResultIndex + '"]');
if (selectedItem) {
selectedItem.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}
}, 0);
}
}
function closeDialog() {
if (dialog.parentNode) {
dialog.parentNode.removeChild(dialog);
}
searchDialog = null;
searchResults = [];
currentResultIndex = -1;
}
// 点击外部关闭
dialog.addEventListener('click', function(e) {
if (e.target === dialog) {
closeDialog();
}
});
// 初始�?
updateResultsList();
return dialog;
}
/**
* 打开搜索对话�?
*/
function openSearchDialog() {
if (!searchDialog) {
searchDialog = createSearchDialog();
document.body.appendChild(searchDialog);
}
// 每次打开都聚焦输入框
setTimeout(function() {
var input = searchDialog.querySelector('input');
if (input) {
input.focus();
input.select();
}
}, 50);
}
// 保存函数引用到模块级变量,供 getCanvasMenuItems hook 使用
nodeSearchState.openSearchDialog = openSearchDialog;
/**
* 检查快捷键是否匹配
*/
function checkShortcutMatch(e, shortcut) {
if (!shortcut) return false;
var ctrlMatch = shortcut.ctrl ? (e.ctrlKey || e.metaKey) : (!e.ctrlKey && !e.metaKey);
var altMatch = shortcut.alt ? e.altKey : !e.altKey;
var shiftMatch = shortcut.shift ? e.shiftKey : !e.shiftKey;
var metaMatch = shortcut.meta ? e.metaKey : !e.metaKey;
var keyMatch = shortcut.key && (e.key === shortcut.key || e.key === shortcut.key.toLowerCase() || e.key === shortcut.key.toUpperCase());
return ctrlMatch && altMatch && shiftMatch && metaMatch && keyMatch;
}
/**
* 绑定快捷键监听器(支持自定义快捷键)
*/
function bindKeyboardShortcut() {
// 移除旧的监听�?
if (nodeSearchState.currentHandler) {
document.removeEventListener('keydown', nodeSearchState.currentHandler, true);
window.removeEventListener('keydown', nodeSearchState.currentHandler, true);
if (app.canvas && app.canvas.canvas) {
app.canvas.canvas.removeEventListener('keydown', nodeSearchState.currentHandler, true);
}
}
var shortcut = config.getShortcut();
var handler = function(e) {
// 检查是否匹配自定义快捷�?
if (checkShortcutMatch(e, shortcut)) {
// 如果输入框有焦点,不拦截(让用户可以在搜索框中输入)
var activeElement = document.activeElement;
if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {
// 如果焦点在搜索对话框的输入框中,不拦�?
if (searchDialog && searchDialog.contains(activeElement)) {
return;
}
}
// 阻止默认行为(浏览器搜索�?
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
openSearchDialog();
return false;
}
};
// 保存处理器引�?
nodeSearchState.currentHandler = handler;
// 在多个地方绑定,确保能捕获到
// 1. document 级别(捕获阶段,优先级最高)
document.addEventListener('keydown', handler, true);
// 2. window 级别
window.addEventListener('keydown', handler, true);
// 3. 画布级别(如果可用)
if (app.canvas) {
if (app.canvas.canvas) {
app.canvas.canvas.addEventListener('keydown', handler, true);
}
// 等待画布完全加载
setTimeout(function() {
if (app.canvas && app.canvas.canvas) {
app.canvas.canvas.addEventListener('keydown', handler, true);
}
}, 500);
}
keyboardHandlerBound = true;
}
/**
* 创建设置对话�?
*/
function createSettingsDialog() {
if (nodeSearchState.settingsDialog) {
return nodeSearchState.settingsDialog;
}
var dialog = document.createElement('div');
dialog.id = 'mechababy-node-search-settings';
dialog.style.cssText =
'position: fixed;' +
'top: 50%;' +
'left: 50%;' +
'transform: translate(-50%, -50%);' +
'background: #2a2a2a;' +
'border: 2px solid #4a4a4a;' +
'border-radius: 8px;' +
'padding: 20px;' +
'z-index: 10001;' +
'min-width: 400px;' +
'max-width: 500px;' +
'box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);' +
"font-family: 'Microsoft YaHei', 'SimHei', Arial, sans-serif;";
// 标题
var title = document.createElement('div');
title.textContent = '⚙️ ' + t('settings');
title.style.cssText =
'font-size: 18px;' +
'font-weight: bold;' +
'color: #e0e0e0;' +
'margin-bottom: 20px;' +
'border-bottom: 1px solid #4a4a4a;' +
'padding-bottom: 10px;';
dialog.appendChild(title);
// 快捷键设�?
var shortcutSection = document.createElement('div');
shortcutSection.style.cssText = 'margin-bottom: 20px;';
var shortcutLabel = document.createElement('div');
shortcutLabel.textContent = t('shortcut') + ':';
shortcutLabel.style.cssText = 'color: #e0e0e0; margin-bottom: 8px; font-size: 14px;';
shortcutSection.appendChild(shortcutLabel);
var shortcutDisplay = document.createElement('div');
var currentShortcut = config.getShortcut();
shortcutDisplay.textContent = t('currentShortcut') + config.formatShortcut(currentShortcut);
shortcutDisplay.style.cssText = 'color: #4a9eff; margin-bottom: 10px; font-size: 13px; padding: 8px; background: #1a1a1a; border-radius: 4px;';
shortcutSection.appendChild(shortcutDisplay);
var shortcutInput = document.createElement('input');
shortcutInput.type = 'text';
shortcutInput.placeholder = t('pressKey');
shortcutInput.readOnly = true;
shortcutInput.style.cssText =
'width: 100%;' +
'padding: 10px;' +
'background: #1a1a1a;' +
'border: 1px solid #4a4a4a;' +
'border-radius: 4px;' +
'color: #e0e0e0;' +
'font-size: 14px;' +
'box-sizing: border-box;';
var capturing = false;
var capturedShortcut = null;
shortcutInput.addEventListener('focus', function() {
if (!capturing) {
capturing = true;
shortcutInput.placeholder = t('pressKey');
shortcutInput.value = '';
capturedShortcut = null;
}
});
shortcutInput.addEventListener('keydown', function(e) {
if (!capturing) return;
e.preventDefault();
e.stopPropagation();
// 忽略某些特殊�?
if (e.key === 'Tab' || e.key === 'Escape' || e.key === 'Enter') {
return;
}
// 至少需�?Ctrl/Cmd 和一个按�?
if (!e.ctrlKey && !e.metaKey && !e.altKey) {
shortcutInput.value = t('invalidShortcut');
return;
}
var shortcut = {
ctrl: e.ctrlKey || e.metaKey,
alt: e.altKey,
shift: e.shiftKey,
meta: e.metaKey,
key: e.key
};
capturedShortcut = shortcut;
shortcutInput.value = config.formatShortcut(shortcut);
});
shortcutSection.appendChild(shortcutInput);
dialog.appendChild(shortcutSection);
// 按钮
var buttonContainer = document.createElement('div');
buttonContainer.style.cssText = 'display: flex; gap: 10px; justify-content: flex-end;';
var saveBtn = document.createElement('button');
saveBtn.textContent = t('save');
saveBtn.style.cssText =
'padding: 8px 16px;' +
'background: #4a9eff;' +
'border: none;' +
'border-radius: 4px;' +
'color: #fff;' +
'cursor: pointer;' +
'font-size: 14px;';
saveBtn.addEventListener('click', function() {
if (capturedShortcut) {
// 验证快捷�?
if (!capturedShortcut.key || (!capturedShortcut.ctrl && !capturedShortcut.meta && !capturedShortcut.alt)) {
alert(t('invalidShortcut'));
return;
}
config.setShortcut(capturedShortcut);
bindKeyboardShortcut(); // 重新绑定快捷�?
alert(t('shortcutSaved'));
closeSettingsDialog();
} else {
closeSettingsDialog();
}
});
var cancelBtn = document.createElement('button');
cancelBtn.textContent = t('cancel');
cancelBtn.style.cssText =
'padding: 8px 16px;' +
'background: #4a4a4a;' +
'border: none;' +
'border-radius: 4px;' +
'color: #e0e0e0;' +
'cursor: pointer;' +
'font-size: 14px;';
cancelBtn.addEventListener('click', closeSettingsDialog);
buttonContainer.appendChild(cancelBtn);
buttonContainer.appendChild(saveBtn);
dialog.appendChild(buttonContainer);
function closeSettingsDialog() {
if (dialog.parentNode) {
dialog.parentNode.removeChild(dialog);
}
nodeSearchState.settingsDialog = null;
capturing = false;
capturedShortcut = null;
}
// 点击外部关闭
dialog.addEventListener('click', function(e) {
if (e.target === dialog) {
closeSettingsDialog();
}
});
nodeSearchState.settingsDialog = dialog;
return dialog;
}
/**
* 打开设置对话�?
*/
function openSettingsDialog() {
if (!nodeSearchState.settingsDialog) {
nodeSearchState.settingsDialog = createSettingsDialog();
document.body.appendChild(nodeSearchState.settingsDialog);
}
// 聚焦快捷键输入框
setTimeout(function() {
var input = nodeSearchState.settingsDialog.querySelector('input');
if (input) {
input.focus();
}
}, 50);
}
// 保存函数引用到模块级变量
nodeSearchState.openSettingsDialog = openSettingsDialog;
// 立即绑定快捷�?
bindKeyboardShortcut();
// 延迟再次绑定,确保在所有扩展加载后
setTimeout(function() {
bindKeyboardShortcut();
}, 1000);
// 右键菜单通过 getCanvasMenuItems hook 添加(新�?Context Menu API�?
var currentShortcut = config.formatShortcut();
console.log("[MechaBaby NodeSearch] 扩展已加�?- �?" + currentShortcut + " 打开搜索");
console.log("[MechaBaby NodeSearch] 当前语言: " + config.getLanguage());
}
});