mirror of
https://github.com/DominikDoom/a1111-sd-webui-tagcomplete.git
synced 2026-01-26 19:19:57 +00:00
363 lines
12 KiB
JavaScript
363 lines
12 KiB
JavaScript
// Style for new elements. Gets appended to the Gradio root.
|
|
const autocompleteCSS_dark = `
|
|
#autocompleteResults {
|
|
position: absolute;
|
|
z-index: 999;
|
|
margin: 5px 0 0 0;
|
|
background-color: #0b0f19 !important;
|
|
border: 1px solid #4b5563 !important;
|
|
border-radius: 12px !important;
|
|
overflow: hidden;
|
|
}
|
|
#autocompleteResultsList > li:nth-child(odd) {
|
|
background-color: #111827;
|
|
}
|
|
#autocompleteResultsList > li {
|
|
list-style-type: none;
|
|
padding: 10px;
|
|
cursor: pointer;
|
|
}
|
|
#autocompleteResultsList > li:hover {
|
|
background-color: #1f2937;
|
|
}
|
|
#autocompleteResultsList > li.selected {
|
|
background-color: #374151;
|
|
}
|
|
`;
|
|
const autocompleteCSS_light = `
|
|
#autocompleteResults {
|
|
position: absolute;
|
|
z-index: 999;
|
|
margin: 5px 0 0 0;
|
|
background-color: #ffffff !important;
|
|
border: 1.5px solid #e5e7eb !important;
|
|
border-radius: 12px !important;
|
|
overflow: hidden;
|
|
}
|
|
#autocompleteResultsList > li:nth-child(odd) {
|
|
background-color: #f9fafb;
|
|
}
|
|
#autocompleteResultsList > li {
|
|
list-style-type: none;
|
|
padding: 10px;
|
|
cursor: pointer;
|
|
}
|
|
#autocompleteResultsList > li:hover {
|
|
background-color: #f5f6f8;
|
|
}
|
|
#autocompleteResultsList > li.selected {
|
|
background-color: #e5e7eb;
|
|
}
|
|
`;
|
|
|
|
var acConfig = null;
|
|
|
|
// Parse the CSV file into a 2D array. Doesn't use regex, so it is very lightweight.
|
|
function parseCSV(str) {
|
|
var arr = [];
|
|
var quote = false; // 'true' means we're inside a quoted field
|
|
|
|
// Iterate over each character, keep track of current row and column (of the returned array)
|
|
for (var row = 0, col = 0, c = 0; c < str.length; c++) {
|
|
var cc = str[c], nc = str[c+1]; // Current character, next character
|
|
arr[row] = arr[row] || []; // Create a new row if necessary
|
|
arr[row][col] = arr[row][col] || ''; // Create a new column (start with empty string) if necessary
|
|
|
|
// If the current character is a quotation mark, and we're inside a
|
|
// quoted field, and the next character is also a quotation mark,
|
|
// add a quotation mark to the current column and skip the next character
|
|
if (cc == '"' && quote && nc == '"') { arr[row][col] += cc; ++c; continue; }
|
|
|
|
// If it's just one quotation mark, begin/end quoted field
|
|
if (cc == '"') { quote = !quote; continue; }
|
|
|
|
// If it's a comma and we're not in a quoted field, move on to the next column
|
|
if (cc == ',' && !quote) { ++col; continue; }
|
|
|
|
// If it's a newline (CRLF) and we're not in a quoted field, skip the next character
|
|
// and move on to the next row and move to column 0 of that new row
|
|
if (cc == '\r' && nc == '\n' && !quote) { ++row; col = 0; ++c; continue; }
|
|
|
|
// If it's a newline (LF or CR) and we're not in a quoted field,
|
|
// move on to the next row and move to column 0 of that new row
|
|
if (cc == '\n' && !quote) { ++row; col = 0; continue; }
|
|
if (cc == '\r' && !quote) { ++row; col = 0; continue; }
|
|
|
|
// Otherwise, append the current character to the current column
|
|
arr[row][col] += cc;
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
// Load file
|
|
function readFile(filePath) {
|
|
let request = new XMLHttpRequest();
|
|
request.open("GET", filePath, false);
|
|
request.send(null);
|
|
return request.responseText;
|
|
}
|
|
|
|
function loadCSV() {
|
|
let text = readFile(`file/tags/${acConfig.tagFile}`);
|
|
return parseCSV(text);
|
|
}
|
|
|
|
// Debounce function to prevent spamming the autocomplete function
|
|
var dbTimeOut;
|
|
const debounce = (func, wait = 300) => {
|
|
return function(...args) {
|
|
if (dbTimeOut) {
|
|
clearTimeout(dbTimeOut);
|
|
}
|
|
|
|
dbTimeOut = setTimeout(() => {
|
|
func.apply(this, args);
|
|
}, wait);
|
|
}
|
|
}
|
|
|
|
// Difference function to fix duplicates not being seen as changes in normal filter
|
|
function difference(a, b) {
|
|
if (a.length == 0) {
|
|
return b;
|
|
}
|
|
if (b.length == 0) {
|
|
return a;
|
|
}
|
|
|
|
return [...b.reduce( (acc, v) => acc.set(v, (acc.get(v) || 0) - 1),
|
|
a.reduce( (acc, v) => acc.set(v, (acc.get(v) || 0) + 1), new Map() )
|
|
)].reduce( (acc, [v, count]) => acc.concat(Array(Math.abs(count)).fill(v)), [] );
|
|
}
|
|
|
|
// Create the result list div and necessary styling
|
|
function createResultsDiv() {
|
|
let resultsDiv = document.createElement("div");
|
|
let resultsList = document.createElement('ul');
|
|
|
|
resultsDiv.setAttribute('id', 'autocompleteResults');
|
|
resultsList.setAttribute('id', 'autocompleteResultsList');
|
|
resultsDiv.appendChild(resultsList);
|
|
|
|
return resultsDiv;
|
|
}
|
|
|
|
// The selected tag index. Needs to be up here so hide can access it.
|
|
var selectedTag = null;
|
|
|
|
// Show or hide the results div
|
|
var isVisible = false;
|
|
function showResults() {
|
|
let resultsDiv = gradioApp().querySelector('#autocompleteResults');
|
|
resultsDiv.style.display = "block";
|
|
isVisible = true;
|
|
}
|
|
function hideResults() {
|
|
let resultsDiv = gradioApp().querySelector('#autocompleteResults');
|
|
resultsDiv.style.display = "none";
|
|
isVisible = false;
|
|
selectedTag = null;
|
|
}
|
|
|
|
// On click, insert the tag into the prompt textbox with respect to the cursor position
|
|
function insertTextAtCursor(text, tagword) {
|
|
let promptTextbox = gradioApp().querySelector('#txt2img_prompt > label > textarea');
|
|
let cursorPos = promptTextbox.selectionStart;
|
|
let sanitizedText = acConfig.replaceUnderscores ? text.replaceAll("_", " ") : text;
|
|
sanitizedText = acConfig.escapeParentheses ? sanitizedText.replaceAll("(", "\\(").replaceAll(")", "\\)") : sanitizedText;
|
|
|
|
var prompt = promptTextbox.value;
|
|
let optionalComma = (prompt[cursorPos] === "," || prompt[cursorPos + tagword.length] === ",") ? "" : ", ";
|
|
|
|
// Edit prompt text
|
|
let direction = prompt.substring(cursorPos, cursorPos + tagword.length) === tagword ? 1 : -1;
|
|
if (direction === 1) {
|
|
promptTextbox.value = prompt.substring(0, cursorPos) + sanitizedText + optionalComma + prompt.substring(cursorPos + tagword.length)
|
|
// Update cursor position to after the inserted text
|
|
promptTextbox.selectionStart = cursorPos + sanitizedText.length + optionalComma.length;
|
|
} else {
|
|
promptTextbox.value = prompt.substring(0, cursorPos - tagword.length) + sanitizedText + optionalComma + prompt.substring(cursorPos)
|
|
promptTextbox.selectionStart = cursorPos - tagword.length + sanitizedText.length + optionalComma.length;
|
|
}
|
|
prompt = promptTextbox.value;
|
|
promptTextbox.selectionEnd = promptTextbox.selectionStart;
|
|
|
|
// Since we've modified a Gradio Textbox component manually, we need to simulate an `input` DOM event to ensure its
|
|
// internal Svelte data binding remains in sync.
|
|
promptTextbox.dispatchEvent(new Event("input", { bubbles: true }));
|
|
|
|
// Hide results after inserting
|
|
hideResults();
|
|
|
|
// Update previous tags with the edited prompt to prevent re-searching the same term
|
|
let tags = prompt.match(/[^, ]+/g);
|
|
previousTags = tags;
|
|
}
|
|
|
|
function addResultsToList(results, tagword) {
|
|
let resultsList = gradioApp().querySelector('#autocompleteResultsList');
|
|
resultsList.innerHTML = "";
|
|
|
|
// Find right colors from config
|
|
let tagFileName = acConfig.tagFile.split(".")[0];
|
|
let tagColors = acConfig.colors;
|
|
//let colorIndex = Object.keys(tagColors).findIndex(key => key === tagFileName);
|
|
//let colorValues = Object.values(tagColors)[colorIndex];
|
|
|
|
let mode = gradioApp().querySelector('.dark') ? 0 : 1;
|
|
|
|
for (let i = 0; i < results.length; i++) {
|
|
let result = results[i];
|
|
let li = document.createElement("li");
|
|
li.innerHTML = result[0];
|
|
|
|
// Set the color of the tag
|
|
let tagType = result[1];
|
|
li.style = `color: ${tagColors[tagFileName][tagType][mode]};`;
|
|
// Add listener
|
|
li.addEventListener("click", function() { insertTextAtCursor(result[0], tagword); });
|
|
// Add element to list
|
|
resultsList.appendChild(li);
|
|
}
|
|
}
|
|
|
|
function updateSelectionStyle(num) {
|
|
let resultsList = gradioApp().querySelector('#autocompleteResultsList');
|
|
let items = resultsList.getElementsByTagName('li');
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
items[i].classList.remove('selected');
|
|
}
|
|
|
|
items[num].classList.add('selected');
|
|
}
|
|
|
|
allTags = [];
|
|
previousTags = [];
|
|
results = [];
|
|
tagword = "";
|
|
resultCount = 0;
|
|
function autocomplete(prompt) {
|
|
// Guard for empty prompt
|
|
if (prompt.length === 0) {
|
|
hideResults();
|
|
return;
|
|
}
|
|
|
|
// Match tags with RegEx to get the last edited one
|
|
let tags = prompt.match(/[^, ]+/g);
|
|
let diff = difference(tags, previousTags)
|
|
previousTags = tags;
|
|
|
|
// Guard for no difference / only whitespace remaining
|
|
if (diff === undefined || diff.length === 0) {
|
|
hideResults();
|
|
return;
|
|
}
|
|
|
|
tagword = diff[0]
|
|
|
|
// Guard for empty tagword
|
|
if (tagword === undefined || tagword.length === 0) {
|
|
hideResults();
|
|
return;
|
|
}
|
|
|
|
results = allTags.filter(x => x[0].includes(tagword)).slice(0, acConfig.maxResults);
|
|
resultCount = results.length;
|
|
|
|
// Guard for empty results
|
|
if (resultCount === 0) {
|
|
hideResults();
|
|
return;
|
|
}
|
|
|
|
showResults();
|
|
addResultsToList(results, tagword);
|
|
}
|
|
|
|
function navigateInList(event) {
|
|
validKeys = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Enter", "Escape"];
|
|
|
|
if (!validKeys.includes(event.key)) return;
|
|
if (!isVisible) return
|
|
|
|
switch (event.key) {
|
|
case "ArrowUp":
|
|
if (selectedTag === null) {
|
|
selectedTag = resultCount - 1;
|
|
} else {
|
|
selectedTag = (selectedTag - 1 + resultCount) % resultCount;
|
|
}
|
|
break;
|
|
case "ArrowDown":
|
|
if (selectedTag === null) {
|
|
selectedTag = 0;
|
|
} else {
|
|
selectedTag = (selectedTag + 1) % resultCount;
|
|
}
|
|
break;
|
|
case "ArrowLeft":
|
|
selectedTag = 0;
|
|
break;
|
|
case "ArrowRight":
|
|
selectedTag = resultCount - 1;
|
|
break;
|
|
case "Enter":
|
|
if (selectedTag !== null) {
|
|
insertTextAtCursor(results[selectedTag][0], tagword);
|
|
}
|
|
break;
|
|
case "Escape":
|
|
hideResults();
|
|
break;
|
|
}
|
|
// Update highlighting
|
|
if (selectedTag !== null)
|
|
updateSelectionStyle(selectedTag);
|
|
|
|
// Prevent default behavior
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
|
|
onUiUpdate(function(){
|
|
// One-time CSV setup
|
|
if (acConfig === null) acConfig = JSON.parse(readFile("file/tags/config.json"));
|
|
if (allTags.length === 0) allTags = loadCSV();
|
|
|
|
let promptTextbox = gradioApp().querySelector('#txt2img_prompt > label > textarea');
|
|
|
|
if (promptTextbox === null) return;
|
|
if (gradioApp().querySelector('#autocompleteResults') != null) return;
|
|
|
|
// Only add listeners once
|
|
if (!promptTextbox.classList.contains('autocomplete')) {
|
|
// Add our new element
|
|
var resultsDiv = gradioApp().querySelector('#autocompleteResults') ?? createResultsDiv();
|
|
promptTextbox.parentNode.insertBefore(resultsDiv, promptTextbox.nextSibling);
|
|
// Hide by default so it doesn't show up on page load
|
|
hideResults();
|
|
|
|
// Add autocomplete event listener
|
|
promptTextbox.addEventListener('input', debounce(() => autocomplete(promptTextbox.value), 100));
|
|
// Add focusout event listener
|
|
promptTextbox.addEventListener('focusout', debounce(() => hideResults(), 400));
|
|
// Add up and down arrow event listener
|
|
promptTextbox.addEventListener('keydown', (e) => navigateInList(e));
|
|
|
|
// Add class so we know we've already added the listeners
|
|
promptTextbox.classList.add('autocomplete');
|
|
|
|
// Add style to dom
|
|
let acStyle = document.createElement('style');
|
|
|
|
let css = gradioApp().querySelector('.dark') ? autocompleteCSS_dark : autocompleteCSS_light;
|
|
if (acStyle.styleSheet) {
|
|
acStyle.styleSheet.cssText = css;
|
|
} else {
|
|
acStyle.appendChild(document.createTextNode(css));
|
|
}
|
|
gradioApp().appendChild(acStyle);
|
|
}
|
|
}); |