Refactor Similar Tags UI to use table structure and grid display for improved layout and accessibility

This commit is contained in:
newtextdoc1111
2025-05-04 01:20:35 +09:00
parent 750240d805
commit 8f683e40c0
2 changed files with 242 additions and 183 deletions
+97 -75
View File
@@ -1,126 +1,148 @@
/* Basic styles for autocomplete - will be refined later */
#autocomplete-plus-list {
background-color: var(--comfy-input-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
min-width: 200px; /* Increased min width */
overflow-y: auto;
position: absolute; /* Ensure position is absolute */
z-index: 10000; /* High z-index */
color: var(--input-text);
font-size: 0.9em; /* Slightly smaller font */
background-color: var(--comfy-input-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
min-width: 200px; /* Increased min width */
overflow-y: auto;
position: absolute; /* Ensure position is absolute */
z-index: 10000; /* High z-index */
color: var(--input-text);
font-size: 0.9em; /* Slightly smaller font */
}
.autocomplete-plus-item {
padding: 5px 10px; /* Adjusted padding */
cursor: pointer;
white-space: nowrap; /* Prevent wrapping */
overflow: hidden;
text-overflow: ellipsis; /* Show ellipsis if too long */
border-bottom: 1px solid var(--border-color); /* Separator line */
padding: 5px 10px; /* Adjusted padding */
cursor: pointer;
white-space: nowrap; /* Prevent wrapping */
overflow: hidden;
text-overflow: ellipsis; /* Show ellipsis if too long */
border-bottom: 1px solid var(--border-color); /* Separator line */
}
.autocomplete-plus-item:last-child {
border-bottom: none; /* No border for the last item */
border-bottom: none; /* No border for the last item */
}
/* Alternating row colors */
.autocomplete-plus-item:nth-child(even) {
background-color: var(--comfy-menu-bg); /* Slightly different background for even rows */
background-color: var(
--comfy-menu-bg
); /* Slightly different background for even rows */
}
.autocomplete-plus-item:nth-child(odd) {
background-color: var(--comfy-input-bg);
background-color: var(--comfy-input-bg);
}
/* Hover effect */
.autocomplete-plus-item:hover {
background-color: var(--comfy-hover-bg); /* Use a hover variable if available, or adjust */
color: var(--desc-text); /* Adjust text color on hover if needed */
background-color: var(
--comfy-hover-bg
); /* Use a hover variable if available, or adjust */
color: var(--desc-text); /* Adjust text color on hover if needed */
}
/* Selected item style */
.autocomplete-plus-item.selected {
background-color: var(--comfy-menu-bg-selected, var(--comfy-button-bg)); /* Use selection color, fallback to button */
color: var(--comfy-menu-text-selected, var(--button-text)); /* Use selected text color, fallback */
font-weight: bold;
background-color: var(
--comfy-menu-bg-selected,
var(--comfy-button-bg)
); /* Use selection color, fallback to button */
color: var(
--comfy-menu-text-selected,
var(--button-text)
); /* Use selected text color, fallback */
font-weight: bold;
}
/* Similar Tags UI Styles */
#similar-tags-container {
background-color: var(--comfy-input-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
min-width: 200px;
max-width: 300px;
overflow-y: auto;
position: absolute;
z-index: 10000;
color: var(--input-text);
font-size: 0.9em;
top: 0;
left: 0;
background-color: var(--comfy-input-bg);
position: absolute;
display: none;
z-index: 10000;
color: var(--input-text);
width: 800px;
}
#similar-tags-header {
background-color: var(--comfy-menu-bg);
color: var(--desc-text);
padding: 8px 12px;
font-weight: bold;
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
z-index: 1;
#similar-tags-container table {
table-layout: fixed;
width: 100%;
}
#similar-tags-list {
max-height: 300px;
overflow-y: auto;
padding: 5px;
#similar-tags-container thead {
background-color: var(--comfy-menu-bg);
color: var(--desc-text);
padding: 8px 12px;
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
z-index: 1;
}
.similar-tags-header-tag-name {
font-weight: bold;
}
#similar-tags-container tbody {
display: grid;
grid-auto-rows: auto;
grid-template-columns: max-content 1fr auto;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
padding: 5px;
}
.similar-tag-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 8px;
margin: 2px 0;
cursor: pointer;
border-radius: 3px;
transition: background-color 0.2s;
border-bottom: 1px solid var(--border-color-translucent);
display: contents;
padding: 5px 10px; /* Adjusted padding */
cursor: pointer;
white-space: nowrap; /* Prevent wrapping */
border-bottom: 1px solid var(--border-color); /* Separator line */
}
.similar-tag-item:last-child {
border-bottom: none;
border-bottom: none;
}
.similar-tag-item:hover {
background-color: var(--comfy-hover-bg);
background-color: var(--comfy-hover-bg);
}
.similar-tag-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 8px;
white-space: nowrap;
}
.similar-tag-alias {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.similar-tag-similarity {
color: var(--desc-text);
font-size: 0.85em;
padding: 2px 6px;
background-color: var(--comfy-menu-bg);
border-radius: 10px;
min-width: 36px;
text-align: center;
color: var(--desc-text);
background-color: var(--comfy-menu-bg);
border-radius: 10px;
white-space: nowrap;
text-align: right;
}
.similar-tags-loading-message {
padding: 12px;
text-align: center;
font-style: italic;
}
.similar-tags-empty {
padding: 12px;
text-align: center;
color: var(--error-text);
font-style: italic;
padding: 12px;
text-align: center;
color: var(--error-text);
font-style: italic;
}
+145 -108
View File
@@ -15,35 +15,40 @@ import {
class SimilarTagsUI {
constructor() {
// Create the main container
this.element = document.createElement('div');
this.element.id = 'similar-tags-container';
this.element.style.display = 'none'; // Initially hidden
this.element.style.position = 'absolute';
this.element.style.zIndex = '10000';
this.root = document.createElement('div');
this.root.id = 'similar-tags-container';
// Header with title
this.header = document.createElement('div');
this.header.id = 'similar-tags-header';
this.header.textContent = 'Similar Tags';
this.element.appendChild(this.header);
// Create the table element
this.table = document.createElement('table');
this.table.id = 'similar-tags-table';
this.root.appendChild(this.table);
// Container for tags list
this.tagsContainer = document.createElement('div');
this.tagsContainer.id = 'similar-tags-list';
this.element.appendChild(this.tagsContainer);
// Create header row
this.thead = document.createElement('thead');
const headerRow = document.createElement('tr');
const headerCell = document.createElement('th');
// headerCell.colSpan = 3; // Span across all columns
headerCell.textContent = 'Similar Tags';
headerRow.appendChild(headerCell);
this.thead.appendChild(headerRow);
this.table.appendChild(this.thead);
// Create a tbody for the tags
this.tbody = document.createElement('tbody');
this.table.appendChild(this.tbody);
// Add to DOM
document.body.appendChild(this.element);
document.body.appendChild(this.root);
// Track the active input and current tag
this.activeInput = null;
this.currentTag = null;
// Add click handler for tag selection
this.tagsContainer.addEventListener('click', (e) => {
const tagElement = e.target.closest('.similar-tag-item');
if (tagElement && tagElement.dataset.tag) {
this.selectTag(tagElement.dataset.tag);
this.tbody.addEventListener('mousedown', (e) => {
const row = e.target.closest('tr');
if (row && row.dataset.tag) {
this.selectTag(row.dataset.tag);
e.preventDefault();
e.stopPropagation();
}
@@ -72,17 +77,17 @@ class SimilarTagsUI {
this.updatePosition(inputElement);
// Make visible
this.element.style.display = 'block';
this.root.style.display = 'block';
}
/**
* Hides the similar tags UI.
*/
hide() {
this.element.style.display = 'none';
this.root.style.display = 'none';
this.activeInput = null;
this.currentTag = null;
this.tagsContainer.innerHTML = '';
this.tbody.innerHTML = '';
}
/**
@@ -90,83 +95,107 @@ class SimilarTagsUI {
* @param {Array<{tag: string, similarity: number, count: number, alias?: string[]}>} similarTags
*/
updateContent(similarTags) {
this.tagsContainer.innerHTML = '';
this.tbody.innerHTML = '';
// Update header with current tag
this.header.innerHTML = ''; // Clear previous content
const headerText = document.createTextNode('Similar to: ');
this.header.appendChild(headerText);
this.thead.innerHTML = ''; // Clear previous content
const headerRow = document.createElement('tr');
const headerCell = document.createElement('th');
// headerCell.colSpan = 3;
headerCell.textContent = 'Similar Tags: ';
const tagNameSpan = document.createElement('span');
tagNameSpan.className = 'similar-tags-header-tag-name';
tagNameSpan.textContent = this.currentTag;
this.header.appendChild(tagNameSpan);
headerCell.appendChild(tagNameSpan);
headerRow.appendChild(headerCell);
this.thead.appendChild(headerRow);
this.table.appendChild(this.thead);
if(!autoCompleteData.cooccurrenceLoaded){
const noTags = document.createElement('div');
// noTags.className = 'similar-tags-empty';
noTags.textContent = 'Initializing cooccurrence data...';
this.tagsContainer.appendChild(noTags);
if (!autoCompleteData.cooccurrenceLoaded) {
const emptyRow = document.createElement('tr');
const messageCell = document.createElement('td');
messageCell.className = 'similar-tags-loading-message';
// messageCell.colSpan = 3; // Span across all columns
messageCell.textContent = 'Initializing cooccurrence data...';
emptyRow.appendChild(messageCell);
this.tbody.appendChild(emptyRow);
return;
}
if (!similarTags || similarTags.length === 0) {
const noTags = document.createElement('div');
// noTags.className = 'similar-tags-empty';
noTags.textContent = 'No similar tags found';
this.tagsContainer.appendChild(noTags);
const emptyRow = document.createElement('tr');
const messageCell = document.createElement('td');
// messageCell.colSpan = 3; // Span across all columns
messageCell.textContent = 'No similar tags found';
emptyRow.appendChild(messageCell);
this.tbody.appendChild(emptyRow);
return;
}
// Create tag items
// Create tag rows
similarTags.forEach(tagData => {
const tagElement = this.createTagElement(tagData);
this.tagsContainer.appendChild(tagElement);
const tagRow = this.createTagElement(tagData);
this.tbody.appendChild(tagRow);
});
}
/**
* Creates an HTML element for a similar tag.
* Creates an HTML table row for a similar tag.
* @param {{tag: string, similarity: number, count: number, alias?: string[]}} tagData
* @returns {HTMLElement} The tag element
* @returns {HTMLTableRowElement} The tag row element
*/
createTagElement(tagData) {
const tagElement = document.createElement('div');
tagElement.className = 'similar-tag-item';
tagElement.dataset.tag = tagData.tag;
const tagRow = document.createElement('tr');
tagRow.className = 'similar-tag-item';
tagRow.dataset.tag = tagData.tag;
tagRow.style.cursor = 'pointer';
// Tag name
const tagName = document.createElement('span');
tagName.className = 'similar-tag-name';
tagName.textContent = tagData.tag;
tagElement.appendChild(tagName);
// Tag name cell
const tagNameCell = document.createElement('td');
tagNameCell.className = 'similar-tag-name';
tagNameCell.textContent = tagData.tag;
// Tag similarity (as percentage)
const similarity = document.createElement('span');
similarity.className = 'similar-tag-similarity';
similarity.textContent = `${(tagData.similarity * 100).toFixed(2)}%`;
tagElement.appendChild(similarity);
// Alias cell (middle column)
const aliasCell = document.createElement('td');
aliasCell.className = 'similar-tag-alias';
// Display alias if available
if (tagData.alias && tagData.alias.length > 0) {
let aliasText = tagData.alias.join(', ');
aliasCell.textContent = aliasText;
aliasCell.title = tagData.alias.join(', '); // Full alias on hover
}
// Similarity cell
const similarityCell = document.createElement('td');
similarityCell.className = 'similar-tag-similarity';
similarityCell.textContent = `${(tagData.similarity * 100).toFixed(2)}%`;
// Create tooltip with more info
let tooltipText = `Tag: ${tagData.tag}\nSimilarity: ${tagData.similarity}\nCount: ${tagData.count}`;
let tooltipText = `Tag: ${tagData.tag}\nSimilarity: ${(tagData.similarity * 100).toFixed(2)}%\nCount: ${tagData.count}`;
if (tagData.alias && tagData.alias.length > 0) {
tooltipText += `\nAlias: ${tagData.alias.join(', ')}`;
}
tagElement.title = tooltipText;
tagRow.title = tooltipText;
return tagElement;
// Add cells to row
tagRow.appendChild(tagNameCell);
tagRow.appendChild(aliasCell);
tagRow.appendChild(similarityCell);
return tagRow;
}
/**
* Updates the position of the similar tags panel.
* Position is calculated based on the input element, available space,
* and the setting `similarTagsDisplayPosition`.
* @param {HTMLElement} inputElement
* @param {HTMLElement} inputElement The input element to position
*/
updatePosition(inputElement) {
// Reset position for accurate measurement
this.element.style.maxHeight = '';
this.element.style.maxWidth = '';
this.root.style.maxHeight = '';
this.root.style.maxWidth = '';
// Get the bounds of the input element
const inputRect = inputElement.getBoundingClientRect();
@@ -174,49 +203,46 @@ class SimilarTagsUI {
// Get viewport dimensions
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const mergin = 10; // Margin around the element
const mergin = 10;
// Get ComfyUI canvas scale if available
const scale = window.app?.canvas?.ds?.scale ?? 1.0; // Note: scale is not currently used in positioning logic
// Make element briefly visible for measurement but not actually showing
this.element.style.visibility = 'hidden';
this.element.style.display = 'block';
const elemRect = this.element.getBoundingClientRect();
this.element.style.display = 'none';
this.element.style.visibility = 'visible';
this.root.style.visibility = 'hidden';
this.root.style.display = 'block';
const elemRect = this.root.getBoundingClientRect();
this.root.style.display = 'none';
this.root.style.visibility = 'visible';
// Calculate dimensions
const elemWidth = elemRect.width;
const elemHeight = elemRect.height;
let left, top;
let [left, top] = [0, 0];
// Determine initial position based on setting
if (settingValues.similarTagsDisplayPosition === 'vertical') {
// --- Vertical Positioning ---
// Initial position: below the textarea
// Calculate available space above and below the input
const spaceAbove = inputRect.top;
const spaceBelow = viewportHeight - inputRect.bottom;
// Determine if we should place above or below based on which has more space
const placeAbove = spaceAbove > spaceBelow;
if (placeAbove) {
// Place above the input with appropriate margin
top = inputRect.top - elemRect.height - mergin;
// Adjust max height to not exceed available space
this.root.style.maxHeight = `${spaceAbove - (mergin * 2)}px`;
} else {
// Place below the input with appropriate margin
top = inputRect.bottom + mergin;
// Adjust max height to not exceed available space
this.root.style.maxHeight = `${spaceBelow - (mergin * 2)}px`;
}
// Ensure horizontal alignment within viewport if placed vertically
left = Math.max(mergin, Math.min(viewportWidth - elemRect.width - mergin, left));
// Align left edge with input where possible
left = inputRect.left;
top = inputRect.bottom + mergin;
// Check if enough space below
const bottomSpace = viewportHeight - top;
if (bottomSpace < elemHeight && top > viewportHeight / 2) { // Only move above if it fits better
// Not enough space below, try placing it above
top = inputRect.top - elemHeight - mergin;
}
// Fallback to horizontal if vertical doesn't fit well (e.g., goes off top)
if (top < 0) {
left = inputRect.right + mergin;
top = inputRect.top;
const rightSpace = viewportWidth - left;
if (rightSpace < elemWidth) {
left = inputRect.left - elemWidth - mergin;
}
}
// Ensure horizontal alignment within viewport if placed vertically
left = Math.max(mergin, Math.min(viewportWidth - elemWidth - mergin, left));
} else {
// --- Horizontal Positioning (Default) ---
@@ -226,9 +252,9 @@ class SimilarTagsUI {
// Check if we have enough space to the right
const rightSpace = viewportWidth - left;
if (rightSpace < elemWidth && left > viewportWidth / 2) { // Only move left if it fits better
if (rightSpace < elemRect.width && left > viewportWidth / 2) { // Only move left if it fits better
// Not enough space to the right, try placing it to the left
left = inputRect.left - elemWidth - mergin;
left = inputRect.left - elemRect.width - mergin;
}
// Fallback to vertical if horizontal doesn't fit well (e.g., goes off left)
@@ -236,30 +262,29 @@ class SimilarTagsUI {
left = inputRect.left;
top = inputRect.bottom + mergin;
const bottomSpace = viewportHeight - top;
if (bottomSpace < elemHeight) {
top = inputRect.top - elemHeight - mergin;
if (bottomSpace < elemRect.height) {
top = inputRect.top - elemRect.height - mergin;
}
}
// Ensure vertical alignment within viewport if placed horizontally
top = Math.max(mergin, Math.min(viewportHeight - elemHeight - mergin, top));
top = Math.max(mergin, Math.min(viewportHeight - elemRect.height - mergin, top));
}
// Ensure the element stays within viewport (final check for both cases)
left = Math.max(mergin, Math.min(viewportWidth - elemWidth - mergin, left));
top = Math.max(mergin, Math.min(viewportHeight - elemHeight - mergin, top));
// left = Math.max(mergin, Math.min(viewportWidth - elw - mergin, left));
// top = Math.max(mergin, Math.min(viewportHeight - elemHeight - mergin, top));
// Update element position
this.element.style.left = `${left}px`;
this.element.style.top = `${top}px`;
this.root.style.left = `${left}px`;
this.root.style.top = `${top}px`;
// Set max dimensions to ensure scrolling if content is too large
// Adjust max height based on final top position
const maxHeight = viewportHeight - top - 20;
this.element.style.maxHeight = `${maxHeight}px`;
this.root.style.maxHeight = `${maxHeight}px`;
// Adjust max width based on final left position (less critical usually)
const maxWidth = Math.min(300, viewportWidth - left - 20); // Example: limit width and ensure it fits
this.element.style.maxWidth = `${maxWidth}px`;
const maxWidth = Math.min(elemRect.width, viewportWidth - left - 20);
this.root.style.maxWidth = `${maxWidth}px`;
}
/**
@@ -282,7 +307,7 @@ class SimilarTagsUI {
* @returns {boolean}
*/
isVisible() {
return this.element.style.display !== 'none';
return this.root.style.display !== 'none';
}
}
@@ -513,7 +538,19 @@ export class SimilarTagsEventHandler {
}
handleBlur(event) {
// Handle blur event
// Use setTimeout to delay hiding. This allows clicks on the similar tags UI
// to be processed before the UI is hidden.
setTimeout(() => {
// Check if the new focused element is part of the similar tags UI.
// document.activeElement refers to the currently focused element.
const activeElement = document.activeElement;
const similarTagsElement = similarTagsUI.root;
// If the focus is not within the similar tags UI, hide it.
if (similarTagsUI && !similarTagsElement.contains(activeElement)) {
// similarTagsUI.hide();
}
}, 150); // Delay in milliseconds (adjust if necessary)
}
handleKeyDown(event) {