Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f84125262 | ||
|
|
906ba108cc | ||
|
|
6d54bbc7ca | ||
|
|
bda6f4fa9d | ||
|
|
2237b0cb52 | ||
|
|
6e23ad89b6 | ||
|
|
389a79cac9 | ||
|
|
1c4e081f9a | ||
|
|
9c57062011 | ||
|
|
38975c11ab | ||
|
|
8459e3dac9 | ||
|
|
e7eab66d8f | ||
|
|
26772ac8dd | ||
|
|
f68c0c7dc2 | ||
|
|
79fb35ff8c | ||
|
|
4f776d676a | ||
|
|
9f8078c408 | ||
|
|
ab2e83ec38 | ||
|
|
2715f1f165 | ||
|
|
56f506110d | ||
|
|
6785a2b6a5 | ||
|
|
e8b0e44cf4 | ||
|
|
4b64fae70d | ||
|
|
e622148b27 | ||
|
|
7a279d71c3 | ||
|
|
ee8b265173 | ||
|
|
80d254c6b4 | ||
|
|
8f6d451fef | ||
|
|
f2920add1d | ||
|
|
dbb5f66a1f | ||
|
|
cacee5ddad | ||
|
|
7f23b02812 | ||
|
|
edbea2bb43 | ||
|
|
70f916b825 |
@@ -13,6 +13,7 @@
|
||||
- **:zap:No setup required**: Automatically downloads CSV data optimized for Danbooru tags.
|
||||
- **:mag:Autocomplete**: Displays tag suggestions in real-time based on your input as you type.
|
||||
- **:file_cabinet:Related Tags Display**: Shows a list of tags highly related to the selected tag.
|
||||
- **:triangular_ruler:Auto Formatter**: Automatically formats prompt text when the textarea loses focus, cleaning up extra spaces and commas.
|
||||
- **:earth_asia:Multilingual Support**: Supports input completion in Japanese, Chinese, and Korean.
|
||||
- **:computer_mouse:Intuitive Operation**:
|
||||
- Supports both mouse and keyboard operations.
|
||||
@@ -55,6 +56,20 @@ When you select any tag in a text input area, a list of highly related tags is d
|
||||
- Tags that have already been entered are displayed grayed out. If you try to insert a grayed-out tag, the already entered tag will instead be selected.
|
||||
- You can display related tags for the cursor position by pressing `Ctrl+Shift+Space`.
|
||||
|
||||
## Auto Formatter
|
||||
|
||||
When a text input area loses focus (e.g., by clicking outside or pressing Tab), the prompt text is automatically formatted. This feature improves readability when editing large amounts of text.
|
||||
|
||||
Detailed behavior is as follows:
|
||||
- Automatically adds a comma and space after each tag for proper separation
|
||||
- Removes extra commas and spaces between tags
|
||||
- You can manually trigger formatting using the keyboard shortcut `Alt+Shift+F` (keybinding can be customized in ComfyUI settings)
|
||||
- You can enable/disable this feature in the settings
|
||||
|
||||
> [!NOTE]
|
||||
> Auto-formatting is disabled for certain nodes to prevent errors.
|
||||
> Example: [Power Puter (rgthree)](https://github.com/rgthree/rgthree-comfy/wiki/Node:-Power-Puter) `code` field, [LoraLoaderBlockWeight (Inspire)](https://github.com/ltdrdata/ComfyUI-Inspire-Pack) `block_vector` field
|
||||
|
||||
## CSV Data
|
||||
|
||||
Two basic CSV data files are required for operation. These are managed on [HuggingFace](https://huggingface.co/datasets/newtextdoc1111/danbooru-tag-csv) and are automatically downloaded when ComfyUI is first launched after installation, so no setup is required.
|
||||
@@ -166,6 +181,13 @@ For example, by preparing the following CSV, you can quickly insert correspondin
|
||||
|
||||
- **Hide Alias**: Hide/show the Alias column in autocomplete and related tags (default is show)
|
||||
|
||||
### Auto Formatter
|
||||
|
||||
- **Enable Auto Format**: Enable/disable the automatically format prompt text when the textarea loses focus.
|
||||
- **Auto Format Trigger**: Choose when formatting is applied.
|
||||
- **Auto**: Format automatically when leaving text field
|
||||
- **Manual**: Format only via keyboard shortcut (default: `Alt+Shift+F`)
|
||||
|
||||
## Advanced Settings
|
||||
|
||||
### Disabling CSV Update Check on Startup
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
- **:zap:セットアップ不要**: Danbooruタグに最適化された CSV データを自動でダウンロード
|
||||
- **:mag:オートコンプリート**: テキスト入力中に、入力内容に基づいてタグ候補をリアルタイムで表示
|
||||
- **:file_cabinet:関連タグ表示機能**: 選択したタグと関連性の高いタグを一覧表示
|
||||
- **:triangular_ruler:自動フォーマット**: テキストエリアがフォーカスを失った際に、プロンプトテキストを自動的にフォーマットし、余分なスペースやカンマを整理
|
||||
- **:earth_asia:多言語対応**: 日本語、中国語、韓国語での入力補完をサポート
|
||||
- **:computer_mouse:直感的な操作**:
|
||||
- マウスとキーボードどちらの操作にも対応
|
||||
@@ -53,6 +54,20 @@
|
||||
- 入力済みのタグはグレーアウトで表示されます。グレーアウトしたタグを挿入しようとした場合、代わりに入力済みのタグを選択状態にします
|
||||
- `Ctrl+Shift+Space` キーでカーソル位置の関連タグを表示できます
|
||||
|
||||
## 自動フォーマット
|
||||
|
||||
テキスト入力エリアがフォーカスを失った際(例:外側をクリック、Tabキーを押す)、プロンプトテキストを自動的にフォーマットする機能です。大量のテキストを編集する際に可読性を向上させる事が出来ます。
|
||||
|
||||
詳細な動作は以下になります:
|
||||
- 各タグの後に自動的にカンマとスペースを追加し、適切に区切ります
|
||||
- タグ間の余分なカンマとスペースを削除します
|
||||
- キーボードショートカット `Alt+Shift+F` を使って手動でフォーマットすることも出来ます(キーバインドはComfyUIの設定から変更可能です)
|
||||
- 設定から機能の有効化/無効化を切り替えられます
|
||||
|
||||
> [!NOTE]
|
||||
> 一部のノードはエラーに繋がるため、自動フォーマットは実行されません。
|
||||
> 例: [Power Puter (rgthree)](https://github.com/rgthree/rgthree-comfy/wiki/Node:-Power-Puter) の `code` フィールド、 [LoraLoaderBlockWeight (Inspire)](https://github.com/ltdrdata/ComfyUI-Inspire-Pack) の `block_vector` フィールド
|
||||
|
||||
## CSV データ
|
||||
|
||||
動作には基本となる CSV データが2つ必要です。これらは [HuggingFace](https://huggingface.co/datasets/newtextdoc1111/danbooru-tag-csv) で管理されており、 ComfyUI にインストール後の初回起動時に自動でダウンロードされるのでセットアップは不要です。
|
||||
@@ -164,6 +179,13 @@ worst_quality,5,9999999,
|
||||
|
||||
- **Hide Alias**: オートコンプリートと関連タグで表示されるエイリアス列の非表示/表示を切り替え(デフォルトは表示です)
|
||||
|
||||
### 自動フォーマット
|
||||
|
||||
- **Enable Auto Format**: テキストエリアがフォーカスを失った際にプロンプトテキストを自動的にフォーマットする機能の有効化/無効化
|
||||
- **Auto Format Trigger**: フォーマットを適用するタイミングを選択します
|
||||
- **自動**: テキスト欄からフォーカスが外れた際に自動でフォーマットします
|
||||
- **手動**: キーボードショートカットでのみフォーマットします(デフォルト: `Alt+Shift+F`)
|
||||
|
||||
## 上級者向け設定
|
||||
|
||||
### 起動時のCSV更新チェックを無効化する
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"Tag Source": "Tag Source",
|
||||
"Autocompletion": "Autocompletion",
|
||||
"Related Tags": "Related Tags",
|
||||
"Display": "Display"
|
||||
"Display": "Display",
|
||||
"AutoFormatter": "AutoFormatter"
|
||||
}
|
||||
}
|
||||
@@ -70,5 +70,16 @@
|
||||
"AutocompletePlus_Display_HideAlias": {
|
||||
"name": "Hide Alias",
|
||||
"tooltip": "Hide alias column in the autocomplete and related tags display."
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_Trigger": {
|
||||
"name": "Auto Format Trigger",
|
||||
"tooltip": "Auto: Format automatically when leaving text field.\nManual: Format only via keyboard shortcut. default keybind: (Alt+Shift+F)",
|
||||
"options": {
|
||||
"auto": "Auto",
|
||||
"manual": "Manual"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_EnableAutoFormat": {
|
||||
"name": "Enable Auto Format"
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
"Tag Source": "タグソース",
|
||||
"Autocompletion": "オートコンプリート",
|
||||
"Related Tags": "関連タグ",
|
||||
"Display": "表示"
|
||||
"Display": "表示",
|
||||
"AutoFormatter": "自動フォーマット"
|
||||
}
|
||||
}
|
||||
@@ -70,5 +70,16 @@
|
||||
"AutocompletePlus_Display_HideAlias": {
|
||||
"name": "エイリアスを非表示",
|
||||
"tooltip": "オートコンプリートと関連タグで表示されるエイリアス列を非表示にします"
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_Trigger": {
|
||||
"name": "自動フォーマットのトリガー",
|
||||
"tooltip": "自動: テキスト欄からフォーカスが外れた際に自動でフォーマットします\n手動: キーボードショートカットでのみフォーマットします。デフォルトのキーバインド: (Alt+Shift+F)",
|
||||
"options": {
|
||||
"auto": "自動",
|
||||
"manual": "手動"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_EnableAutoFormat": {
|
||||
"name": "自動フォーマット機能の有効化"
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
"Tag Source": "標籤來源",
|
||||
"Autocompletion": "自動完成",
|
||||
"Related Tags": "相關標籤",
|
||||
"Display": "顯示"
|
||||
"Display": "顯示",
|
||||
"AutoFormatter": "自動格式化"
|
||||
}
|
||||
}
|
||||
@@ -70,5 +70,16 @@
|
||||
"AutocompletePlus_Display_HideAlias": {
|
||||
"name": "隱藏別名",
|
||||
"tooltip": "隱藏自動完成和相關標籤顯示中的別名欄"
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_Trigger": {
|
||||
"name": "自動格式化觸發方式",
|
||||
"tooltip": "自動: 離開文字方塊時自動格式化\n手動: 僅透過鍵盤快速鍵格式化。預設快速鍵: (Alt+Shift+F)",
|
||||
"options": {
|
||||
"auto": "自動",
|
||||
"manual": "手動"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_EnableAutoFormat": {
|
||||
"name": "啟用自動格式化"
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
"Tag Source": "标签来源",
|
||||
"Autocompletion": "自动完成",
|
||||
"Related Tags": "相关标签",
|
||||
"Display": "显示"
|
||||
"Display": "显示",
|
||||
"AutoFormatter": "自动格式化"
|
||||
}
|
||||
}
|
||||
@@ -70,5 +70,16 @@
|
||||
"AutocompletePlus_Display_HideAlias": {
|
||||
"name": "隐藏别名",
|
||||
"tooltip": "隐藏自动完成和相关标签显示中的别名列"
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_Trigger": {
|
||||
"name": "自动格式化触发方式",
|
||||
"tooltip": "自动: 离开文本框时自动格式化\n手动: 仅通过键盘快捷键格式化。默认快捷键: (Alt+Shift+F)",
|
||||
"options": {
|
||||
"auto": "自动",
|
||||
"manual": "手动"
|
||||
}
|
||||
},
|
||||
"AutocompletePlus_AutoFormatter_EnableAutoFormat": {
|
||||
"name": "启用自动格式化"
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-autocomplete-plus"
|
||||
description = "Autocomplete and Related Tag display for ComfyUI"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
license = {file = "LICENSE"}
|
||||
dependencies = ["",]
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
formatPromptText,
|
||||
__test__
|
||||
} from "../../web/js/auto-formatter.js";
|
||||
|
||||
const {
|
||||
shouldAutoFormat
|
||||
} = __test__;
|
||||
|
||||
describe('AutoFormatter Functions', () => {
|
||||
|
||||
describe('shouldAutoFormat', () => {
|
||||
const mockNodeInfo = (nodeType, inputName) => ({
|
||||
nodeType,
|
||||
inputName
|
||||
});
|
||||
|
||||
test('should return false for empty text', () => {
|
||||
expect(shouldAutoFormat('', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
expect(shouldAutoFormat(' ', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false for blocklisted nodes', () => {
|
||||
expect(shouldAutoFormat('some text,', mockNodeInfo('Power Puter (rgthree)', 'code'))).toBe(false);
|
||||
expect(shouldAutoFormat('some text,', mockNodeInfo('LoraLoaderBlockWeight //Inspire', 'block_vector'))).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false for numeric data or single-letter placeholders', () => {
|
||||
expect(shouldAutoFormat('0,0,0,1,1,1', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
expect(shouldAutoFormat('0.5, -1.2, 0.8', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
expect(shouldAutoFormat('A,B,R', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
expect(shouldAutoFormat('X, 1.5, Y', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
});
|
||||
|
||||
test('should return true for text with "word + comma" pattern', () => {
|
||||
expect(shouldAutoFormat('1girl, blue hair,', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(true);
|
||||
expect(shouldAutoFormat('tag1, tag2', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false if "word + comma" pattern is not found', () => {
|
||||
expect(shouldAutoFormat('hello world', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
expect(shouldAutoFormat('tag1 tag2', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatPromptText', () => {
|
||||
test('should format text by adding comma and space after tags', () => {
|
||||
const input = 'tag1,tag2,tag3';
|
||||
const expected = 'tag1, tag2, tag3, ';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should remove extra spaces around tags', () => {
|
||||
const input = ' tag1 , tag2 ';
|
||||
const expected = 'tag1, tag2, ';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should preserve special syntax like weights', () => {
|
||||
const input = '(tag1:1.2), [tag2]';
|
||||
// Note: The current implementation splits by comma.
|
||||
// If the input is "(tag1:1.2), [tag2]", it splits into "(tag1:1.2)" and "[tag2]".
|
||||
// Then joins with ", ".
|
||||
const expected = '(tag1:1.2), [tag2], ';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should handle multiple lines', () => {
|
||||
const input = 'tag1, tag2\ntag3, tag4';
|
||||
const expected = 'tag1, tag2, \ntag3, tag4, ';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should keep empty lines unchanged', () => {
|
||||
const input = 'tag1, tag2\n\ntag3, tag4';
|
||||
const expected = 'tag1, tag2, \n\ntag3, tag4, ';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should handle empty input', () => {
|
||||
expect(formatPromptText('')).toBe('');
|
||||
expect(formatPromptText(null)).toBe(null);
|
||||
expect(formatPromptText(undefined)).toBe(undefined);
|
||||
});
|
||||
|
||||
test('should handle input with only spaces', () => {
|
||||
expect(formatPromptText(' ')).toBe(' ');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,199 @@
|
||||
import { settingValues } from './settings.js';
|
||||
import { NodeInfo } from './node-info.js';
|
||||
|
||||
|
||||
/**
|
||||
* Determines if the text content should be auto-formatted.
|
||||
*
|
||||
* Format conditions:
|
||||
* 1. Skip formatting if node is in blocklist
|
||||
* 2. Skip formatting if text contains only numbers or single letters (separated by commas)
|
||||
* 3. Format if text contains "word + comma" pattern at least twice
|
||||
* 4. Otherwise, don't format
|
||||
*
|
||||
* @param {NodeInfo} nodeInfo - The node information.
|
||||
* @returns {boolean} - True if the text should be formatted, false otherwise.
|
||||
*/
|
||||
function shouldAutoFormat(text, nodeInfo) {
|
||||
if (!text || text.trim().length === 0) return false;
|
||||
|
||||
// 1. Check if the node name is in the blocklist
|
||||
const blocklist = [
|
||||
["Power Puter (rgthree)", "code"],
|
||||
["LoraLoaderBlockWeight //Inspire", "block_vector"]
|
||||
];
|
||||
|
||||
const isBlocklisted = blocklist.some(([type, input]) =>
|
||||
type === nodeInfo.nodeType && input === nodeInfo.inputName
|
||||
);
|
||||
|
||||
if (isBlocklisted) {
|
||||
// console.debug(`[Autocomplete-Plus] auto-formatter on blur => nodeType: ${nodeInfo.nodeType}, inputName: ${nodeInfo.inputName} => blocklisted`);
|
||||
return false;
|
||||
} else {
|
||||
// console.debug(`[Autocomplete-Plus] auto-formatter on blur => nodeType: ${nodeInfo.nodeType}, inputName: ${nodeInfo.inputName}`);
|
||||
}
|
||||
|
||||
|
||||
const trimmedText = text.trim();
|
||||
|
||||
// 2. Check if the text is purely numeric data or single-letter placeholders with commas
|
||||
// (e.g., "0,0,0,1,1,1" or "0.5, -1.2, 0.8" or "A,B,R" for LoRA Block Weight)
|
||||
const elements = trimmedText.split(',').map(el => el.trim());
|
||||
const isSingleLetterOrNumeric = elements.every(el => {
|
||||
if (/^[A-Za-z]$/.test(el)) return true;
|
||||
if (/^-?\d+(\.\d+)?$/.test(el)) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (isSingleLetterOrNumeric && elements.length > 0) {
|
||||
return false; // Don't format numeric data or single-letter template patterns
|
||||
}
|
||||
|
||||
// 3. Check if the text contains the pattern "word + comma"
|
||||
const wordCommaPattern = /\w+\s*,/g;
|
||||
const matches = trimmedText.match(wordCommaPattern);
|
||||
|
||||
if (matches == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true; // Text should be formatted
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the prompt text: add a comma and space after each tag, and remove extra spaces.
|
||||
* Preserve special syntax such as weights (tag:1.2), parentheses, and wildcards.
|
||||
* @param {string} text - The original text.
|
||||
* @returns {string} - The formatted text.
|
||||
*/
|
||||
export function formatPromptText(text) {
|
||||
if (!text || text.trim().length === 0) return text;
|
||||
|
||||
// Split text into individual lines for processing
|
||||
const lines = text.split('\n');
|
||||
const formattedLines = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
// Keep empty lines unchanged
|
||||
if (trimmedLine.length === 0) {
|
||||
formattedLines.push('');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Split the line by commas into raw tag segments
|
||||
const rawTags = trimmedLine.split(',');
|
||||
|
||||
const cleanedTags = [];
|
||||
for (const tag of rawTags) {
|
||||
// Trim spaces around each tag
|
||||
const trimmedTag = tag.trim();
|
||||
|
||||
// Keep only non-empty tags
|
||||
if (trimmedTag.length > 0) {
|
||||
cleanedTags.push(trimmedTag);
|
||||
}
|
||||
}
|
||||
|
||||
// Rejoin cleaned tags with ", "
|
||||
let formattedLine = cleanedTags.join(', ');
|
||||
|
||||
// Add a trailing comma and space if the line contains valid tags
|
||||
if (formattedLine.length > 0) {
|
||||
formattedLine += ', ';
|
||||
}
|
||||
|
||||
formattedLines.push(formattedLine);
|
||||
}
|
||||
|
||||
// Rejoin all lines with newline characters
|
||||
return formattedLines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the content of a textarea when it loses focus.
|
||||
* @param {HTMLTextAreaElement} textarea - The target textarea element.
|
||||
*/
|
||||
export function formatTextareaOnBlur(textarea) {
|
||||
const originalText = textarea.value;
|
||||
const formattedText = formatPromptText(originalText);
|
||||
|
||||
if (originalText !== formattedText) {
|
||||
const cursorPos = textarea.selectionStart;
|
||||
textarea.value = formattedText;
|
||||
|
||||
// Try to preserve cursor position
|
||||
const newCursorPos = Math.min(cursorPos, formattedText.length);
|
||||
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for auto-formatting functionality
|
||||
*/
|
||||
export class AutoFormatterEventHandler {
|
||||
constructor() { }
|
||||
|
||||
/**
|
||||
* Handle blur event to trigger auto-formatting
|
||||
* @param {Event} event - The blur event
|
||||
*/
|
||||
handleBlur(event, nodeInfo) {
|
||||
if (
|
||||
settingValues.enableAutoFormat &&
|
||||
settingValues.autoFormatTrigger === 'auto' &&
|
||||
event.target.tagName === 'TEXTAREA'
|
||||
) {
|
||||
const textarea = event.target;
|
||||
const text = textarea.value;
|
||||
|
||||
// Check if the content should be auto-formatted
|
||||
if (shouldAutoFormat(text, nodeInfo)) {
|
||||
formatTextareaOnBlur(textarea);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder methods to maintain consistency with other event handlers
|
||||
handleInput(event) { }
|
||||
handleFocus(event) { }
|
||||
handleKeyDown(event) { }
|
||||
handleKeyUp(event) { }
|
||||
handleMouseMove(event) { }
|
||||
handleClick(event) { }
|
||||
|
||||
/**
|
||||
* Format textarea content via manual trigger (e.g., keyboard shortcut)
|
||||
* @param {HTMLTextAreaElement} textarea - The textarea element to format
|
||||
* @param {NodeInfo} nodeInfo - The node information
|
||||
* @returns {boolean} - True if formatting was performed, false otherwise
|
||||
*/
|
||||
applyFormatTextarea(textarea, nodeInfo) {
|
||||
if (!textarea || textarea.tagName !== 'TEXTAREA') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!settingValues.enableAutoFormat) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const text = textarea.value;
|
||||
|
||||
if (shouldAutoFormat(text, nodeInfo)) {
|
||||
formatTextareaOnBlur(textarea);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Export functions for testing
|
||||
const isTestEnvironment = typeof process !== 'undefined' && process.env.NODE_ENV === 'test';
|
||||
export const __test__ = isTestEnvironment
|
||||
? {
|
||||
shouldAutoFormat
|
||||
}
|
||||
: undefined;
|
||||
+12
-8
@@ -306,7 +306,7 @@ function getCurrentPartialTag(inputElement) {
|
||||
* Inserts the selected tag into the textarea, replacing the partial tag,
|
||||
* making the change undoable.
|
||||
* @param {HTMLTextAreaElement} inputElement
|
||||
* @param {TagData} tagDataToInsert The raw tag string to insert.
|
||||
* @param {TagData} tagDataToInsert
|
||||
*/
|
||||
function insertTagToTextArea(inputElement, tagDataToInsert) {
|
||||
if (!inputElement || !tagDataToInsert) {
|
||||
@@ -408,8 +408,11 @@ class AutocompleteUI {
|
||||
// Add event listener for clicks on items
|
||||
this.tagsList.addEventListener('mousedown', (e) => {
|
||||
const row = e.target.closest('.autocomplete-plus-item');
|
||||
if (row && row.dataset.tag) {
|
||||
this.#insertTag(row.dataset);
|
||||
if (row && row.dataset.index !== undefined) {
|
||||
const tagData = this.candidates[parseInt(row.dataset.index, 10)];
|
||||
if (tagData) {
|
||||
this.#insertTag(tagData);
|
||||
}
|
||||
e.preventDefault(); // Prevent focus loss from input
|
||||
e.stopPropagation();
|
||||
}
|
||||
@@ -500,26 +503,27 @@ class AutocompleteUI {
|
||||
const existingTags = extractTagsFromTextArea(this.target);
|
||||
const currentTag = getCurrentPartialTag(this.target);
|
||||
|
||||
this.candidates.forEach((tagData) => {
|
||||
this.candidates.forEach((tagData, index) => {
|
||||
const isExactMatch = tagData.tag === currentTag && (existingTags.filter(item => item === currentTag).length == 1);
|
||||
const isExistingTag = !isExactMatch && existingTags.includes(tagData.tag);
|
||||
this.#createTagElement(tagData, isExistingTag);
|
||||
this.#createTagElement(tagData, index, isExistingTag);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a tag element for the autocomplete list.
|
||||
* @param {TagData} tagData
|
||||
* @param {number} tagDataIndex
|
||||
* @param {boolean} isExisting
|
||||
*/
|
||||
#createTagElement(tagData, isExisting) {
|
||||
#createTagElement(tagData, tagDataIndex, isExisting) {
|
||||
const categoryText = TagCategory[tagData.source][tagData.category] || "unknown";
|
||||
const aliasText = tagData.alias.join(', ');
|
||||
|
||||
const tagRow = document.createElement('div');
|
||||
tagRow.classList.add('autocomplete-plus-item', tagData.source);
|
||||
tagRow.dataset.tag = tagData.tag;
|
||||
tagRow.dataset.tagCategory = categoryText;
|
||||
tagRow.dataset.index = tagDataIndex;
|
||||
tagRow.dataset.tagCategory = categoryText; // Used to color by CSS
|
||||
|
||||
// Tag icon and name
|
||||
const tagSourceIconHtml = `<svg class="autocomplete-plus-tag-icon-svg"><use xlink:href="#autocomplete-plus-icon-${tagData.source}"></use></svg>`;
|
||||
|
||||
+110
-14
@@ -6,23 +6,27 @@ import { loadCSS } from "./utils.js";
|
||||
import { TagSource, loadDataAsync } from "./data.js";
|
||||
import { AutocompleteEventHandler } from "./autocomplete.js";
|
||||
import { RelatedTagsEventHandler } from "./related-tags.js";
|
||||
import { AutoFormatterEventHandler } from "./auto-formatter.js";
|
||||
import { NodeInfo } from "./node-info.js";
|
||||
|
||||
// --- Constants ---
|
||||
const id = "AutocompletePlus";
|
||||
const name = "Autocomplete Plus";
|
||||
|
||||
// --- Module-level variables ---
|
||||
const autocompleteEventHandler = new AutocompleteEventHandler();
|
||||
const relatedTagsEventHandler = new RelatedTagsEventHandler();
|
||||
const autoFormatterEventHandler = new AutoFormatterEventHandler();
|
||||
const attachedElementNodeInfoMap = new WeakMap(); // Map to track attached elements and their node info
|
||||
|
||||
// --- Functions ---
|
||||
/**
|
||||
* Initialize event handlers for the autocomplete and related tags features.
|
||||
*/
|
||||
function initializeEventHandlers() {
|
||||
const autocompleteEventHandler = new AutocompleteEventHandler();
|
||||
const relatedTagsEventHandler = new RelatedTagsEventHandler();
|
||||
const attachedElements = new WeakSet(); // Keep track of elements that have listeners attached
|
||||
|
||||
// Function to attach listeners
|
||||
function attachListeners(element) {
|
||||
if (attachedElements.has(element)) return; // Prevent double attachment
|
||||
function attachListeners(element, nodeInfo) {
|
||||
if (attachedElementNodeInfoMap.has(element)) return; // Prevent double attachment
|
||||
|
||||
element.addEventListener('input', handleInput);
|
||||
element.addEventListener('focus', handleFocus);
|
||||
@@ -35,7 +39,7 @@ function initializeEventHandlers() {
|
||||
element.addEventListener('mousemove', handleMouseMove);
|
||||
element.addEventListener('click', handleClick);
|
||||
|
||||
attachedElements.add(element); // Mark as attached
|
||||
attachedElementNodeInfoMap.set(element, nodeInfo); // Mark as attached and store node info
|
||||
}
|
||||
|
||||
// Attempt Widget Override as the primary method
|
||||
@@ -48,12 +52,14 @@ function initializeEventHandlers() {
|
||||
|
||||
// Check if the widget has an inputEl and if it's a TEXTAREA
|
||||
// This is to ensure we are targeting multiline text inputs, related to '.comfy-multiline-input'
|
||||
if (result && result.widget && result.widget.inputEl && result.widget.inputEl.tagName === 'TEXTAREA') {
|
||||
if (result && result.widget
|
||||
&& result.widget.inputEl && result.widget.inputEl.tagName === 'TEXTAREA' && !result.widget.inputEl.readOnly) {
|
||||
const widgetConfig = inputData && inputData[1] ? inputData[1] : {};
|
||||
// Future: Add checks for Autocomplete Plus specific configurations if needed
|
||||
// e.g., if (widgetConfig["AutocompletePlus.enabled"] === false) return result;
|
||||
|
||||
attachListeners(result.widget.inputEl);
|
||||
const nodeInfo = new NodeInfo(node.comfyClass || node.constructor.name, inputName);
|
||||
attachListeners(result.widget.inputEl, nodeInfo);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -73,9 +79,11 @@ function initializeEventHandlers() {
|
||||
targetSelectors.forEach(selector => {
|
||||
// Check if the added node itself matches or contains matching elements
|
||||
if (node.matches(selector)) {
|
||||
attachListeners(node);
|
||||
attachListeners(node, new NodeInfo('Fallback', 'unknown'));
|
||||
} else {
|
||||
node.querySelectorAll(selector).forEach(attachListeners);
|
||||
node.querySelectorAll(selector).forEach(el => {
|
||||
attachListeners(el, new NodeInfo('Fallback', 'unknown'));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -85,48 +93,72 @@ function initializeEventHandlers() {
|
||||
|
||||
// Initial scan for existing elements
|
||||
targetSelectors.forEach(selector => {
|
||||
document.querySelectorAll(selector).forEach(attachListeners);
|
||||
document.querySelectorAll(selector).forEach(el => {
|
||||
attachListeners(el, new NodeInfo('Fallback', 'unknown'));
|
||||
});
|
||||
});
|
||||
|
||||
// Start observing the document body for changes
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get NodeInfo for the event target element
|
||||
* @param {Event} event - The DOM event
|
||||
* @returns {Object|null} NodeInfo object or undefined if not found
|
||||
*/
|
||||
function getNodeInfo(event) {
|
||||
const nodeInfo = attachedElementNodeInfoMap.get(event.target);
|
||||
if (!nodeInfo) {
|
||||
console.warn('[Autocomplete-Plus] Node info not found for element in ', event.target);
|
||||
return null;
|
||||
}
|
||||
|
||||
return nodeInfo;
|
||||
}
|
||||
|
||||
function handleInput(event) {
|
||||
autocompleteEventHandler.handleInput(event);
|
||||
relatedTagsEventHandler.handleInput(event);
|
||||
autoFormatterEventHandler.handleInput(event);
|
||||
}
|
||||
|
||||
function handleFocus(event) {
|
||||
autocompleteEventHandler.handleFocus(event);
|
||||
relatedTagsEventHandler.handleFocus(event);
|
||||
autoFormatterEventHandler.handleFocus(event);
|
||||
}
|
||||
|
||||
function handleBlur(event) {
|
||||
const nodeInfo = getNodeInfo(event); // Get node info to pass to auto formatter
|
||||
|
||||
autocompleteEventHandler.handleBlur(event);
|
||||
relatedTagsEventHandler.handleBlur(event);
|
||||
autoFormatterEventHandler.handleBlur(event, nodeInfo);
|
||||
}
|
||||
|
||||
function handleKeyDown(event) {
|
||||
autocompleteEventHandler.handleKeyDown(event);
|
||||
relatedTagsEventHandler.handleKeyDown(event);
|
||||
autoFormatterEventHandler.handleKeyDown(event);
|
||||
}
|
||||
|
||||
function handleKeyUp(event) {
|
||||
autocompleteEventHandler.handleKeyUp(event);
|
||||
relatedTagsEventHandler.handleKeyUp(event);
|
||||
autoFormatterEventHandler.handleKeyUp(event);
|
||||
}
|
||||
|
||||
// New event handler for mousemove to show related tags on hover
|
||||
function handleMouseMove(event) {
|
||||
autocompleteEventHandler.handleMouseMove(event);
|
||||
relatedTagsEventHandler.handleMouseMove(event);
|
||||
autoFormatterEventHandler.handleMouseMove(event);
|
||||
}
|
||||
|
||||
// New event handler for click to show related tags
|
||||
function handleClick(event) {
|
||||
autocompleteEventHandler.handleClick(event);
|
||||
relatedTagsEventHandler.handleClick(event);
|
||||
autoFormatterEventHandler.handleClick(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,6 +276,46 @@ app.registerExtension({
|
||||
await loadDataAsync();
|
||||
},
|
||||
|
||||
// --- Commands ---
|
||||
commands: [
|
||||
{
|
||||
id: id + ".formatPrompt",
|
||||
label: name + ": Format Prompt",
|
||||
function: () => {
|
||||
const activeEl = document.activeElement;
|
||||
|
||||
if (!activeEl || activeEl.tagName !== 'TEXTAREA') {
|
||||
// console.debug('[Autocomplete-Plus] Format command: No textarea is currently focused');
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeInfo = attachedElementNodeInfoMap.get(activeEl);
|
||||
if (!nodeInfo) {
|
||||
console.warn('[Autocomplete-Plus] Format command: Node info not found for focused textarea');
|
||||
// Use fallback NodeInfo
|
||||
const fallbackNodeInfo = new NodeInfo('Unknown', 'unknown');
|
||||
autoFormatterEventHandler.applyFormatTextarea(activeEl, fallbackNodeInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
const formatted = autoFormatterEventHandler.applyFormatTextarea(activeEl, nodeInfo);
|
||||
if (formatted) {
|
||||
// console.debug('[Autocomplete-Plus] Format command: Formatting applied');
|
||||
} else {
|
||||
// console.debug('[Autocomplete-Plus] Format command: Formatting skipped (blocklisted or not applicable)');
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
// --- Keybindings ---
|
||||
keybindings: [
|
||||
{
|
||||
combo: { key: "f", alt: true, shift: true },
|
||||
commandId: id + ".formatPrompt"
|
||||
}
|
||||
],
|
||||
|
||||
// One the Settings Screen, displays reverse order in same category
|
||||
settings: [
|
||||
// --- Tag source Settings ---
|
||||
@@ -416,5 +488,29 @@ app.registerExtension({
|
||||
settingValues.hideAlias = newVal;
|
||||
}
|
||||
},
|
||||
|
||||
// --- Auto format settings ---
|
||||
{
|
||||
id: id + '.AutoFormatter.Trigger',
|
||||
name: 'Auto Format Trigger',
|
||||
tooltip: 'Auto: Format automatically when leaving text field.\nManual: Format only via keyboard shortcut. default keybind: (Alt+Shift+F)',
|
||||
type: 'combo',
|
||||
options: ['auto', 'manual'],
|
||||
defaultValue: 'auto',
|
||||
category: [name, 'AutoFormatter', 'Auto Format Trigger'],
|
||||
onChange: (newVal, oldVal) => {
|
||||
settingValues.autoFormatTrigger = newVal;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: id + '.AutoFormatter.EnableAutoFormat',
|
||||
name: 'Enable Auto Format',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
category: [name, 'AutoFormatter', 'Enable Auto Format'],
|
||||
onChange: (newVal, oldVal) => {
|
||||
settingValues.enableAutoFormat = newVal;
|
||||
},
|
||||
},
|
||||
]
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Class to hold information about the node attached to an input element.
|
||||
* Used to control behavior based on node information.
|
||||
*/
|
||||
export class NodeInfo {
|
||||
/**
|
||||
* @param {string} nodeType - The type/class name of the node
|
||||
* @param {string} inputName - The name of the input widget
|
||||
*/
|
||||
constructor(nodeType, inputName) {
|
||||
this.nodeType = nodeType;
|
||||
this.inputName = inputName;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,10 @@ export const settingValues = {
|
||||
// Display settings
|
||||
hideAlias: false, // Hide alias in the autocomplete and related tags display
|
||||
|
||||
// Auto format settings
|
||||
enableAutoFormat: true,
|
||||
autoFormatTrigger: 'auto', // Options: 'auto' (format on blur + shortcut), 'manual' (shortcut only)
|
||||
|
||||
|
||||
// Internal logic settings
|
||||
_useFallbackAttachmentForEventListener: false, // Fallback to attach event listener when somthing goes wrong
|
||||
|
||||
Reference in New Issue
Block a user