mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 07:05:43 -03:00
Add keyboard navigation support and related styles for enhanced user experience
This commit is contained in:
96
static/css/components/keyboard-nav.css
Normal file
96
static/css/components/keyboard-nav.css
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/* Keyboard navigation indicator and help */
|
||||||
|
.keyboard-nav-hint {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
cursor: help;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard-nav-hint:hover {
|
||||||
|
background: var(--lora-accent);
|
||||||
|
color: white;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard-nav-hint i {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip styling */
|
||||||
|
.tooltip {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip .tooltiptext {
|
||||||
|
visibility: hidden;
|
||||||
|
width: 240px;
|
||||||
|
background-color: var(--lora-surface);
|
||||||
|
color: var(--text-color);
|
||||||
|
text-align: center;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
padding: 8px;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 9999; /* 确保在卡片上方显示 */
|
||||||
|
left: 120%; /* 将tooltip显示在图标右侧 */
|
||||||
|
top: 50%; /* 垂直居中 */
|
||||||
|
transform: translateY(-50%); /* 垂直居中 */
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
border: 1px solid var(--lora-border);
|
||||||
|
font-size: 0.85em;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip .tooltiptext::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%; /* 箭头垂直居中 */
|
||||||
|
right: 100%; /* 箭头在左侧 */
|
||||||
|
margin-top: -5px;
|
||||||
|
border-width: 5px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent var(--lora-border) transparent transparent; /* 箭头指向左侧 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip:hover .tooltiptext {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keyboard shortcuts table */
|
||||||
|
.keyboard-shortcuts {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard-shortcuts td {
|
||||||
|
padding: 4px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard-shortcuts td:first-child {
|
||||||
|
font-weight: bold;
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key {
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--bg-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 1px 5px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
@@ -35,6 +35,13 @@
|
|||||||
margin-bottom: var(--space-2);
|
margin-bottom: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.controls-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: auto; /* Push to the right */
|
||||||
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -338,11 +345,14 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.controls-right {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.toggle-folders-container {
|
.toggle-folders-container {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.folder-tags-container {
|
.folder-tags-container {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
@import 'components/progress-panel.css';
|
@import 'components/progress-panel.css';
|
||||||
@import 'components/alphabet-bar.css'; /* Add alphabet bar component */
|
@import 'components/alphabet-bar.css'; /* Add alphabet bar component */
|
||||||
@import 'components/duplicates.css'; /* Add duplicates component */
|
@import 'components/duplicates.css'; /* Add duplicates component */
|
||||||
|
@import 'components/keyboard-nav.css'; /* Add keyboard navigation component */
|
||||||
|
|
||||||
.initialization-notice {
|
.initialization-notice {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -804,4 +804,131 @@ export class VirtualScroller {
|
|||||||
console.log(`Removed item with file path ${filePath} from virtual scroller data`);
|
console.log(`Removed item with file path ${filePath} from virtual scroller data`);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add keyboard navigation methods
|
||||||
|
handlePageUpDown(direction) {
|
||||||
|
// Prevent duplicate animations by checking last trigger time
|
||||||
|
const now = Date.now();
|
||||||
|
if (this.lastPageNavTime && now - this.lastPageNavTime < 300) {
|
||||||
|
return; // Ignore rapid repeated triggers
|
||||||
|
}
|
||||||
|
this.lastPageNavTime = now;
|
||||||
|
|
||||||
|
const scrollContainer = this.scrollContainer;
|
||||||
|
const viewportHeight = scrollContainer.clientHeight;
|
||||||
|
|
||||||
|
// Calculate scroll distance (one viewport minus 10% overlap for context)
|
||||||
|
const scrollDistance = viewportHeight * 0.9;
|
||||||
|
|
||||||
|
// Determine the new scroll position
|
||||||
|
const newScrollTop = scrollContainer.scrollTop + (direction === 'down' ? scrollDistance : -scrollDistance);
|
||||||
|
|
||||||
|
// Remove any existing transition indicators
|
||||||
|
this.removeExistingTransitionIndicator();
|
||||||
|
|
||||||
|
// Scroll to the new position with smooth animation
|
||||||
|
scrollContainer.scrollTo({
|
||||||
|
top: newScrollTop,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Page transition indicator removed
|
||||||
|
// this.showTransitionIndicator();
|
||||||
|
|
||||||
|
// Force render after scrolling
|
||||||
|
setTimeout(() => this.renderItems(), 100);
|
||||||
|
setTimeout(() => this.renderItems(), 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to remove existing indicators
|
||||||
|
removeExistingTransitionIndicator() {
|
||||||
|
const existingIndicator = document.querySelector('.page-transition-indicator');
|
||||||
|
if (existingIndicator) {
|
||||||
|
existingIndicator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a more contained transition indicator - commented out as it's no longer needed
|
||||||
|
/*
|
||||||
|
showTransitionIndicator() {
|
||||||
|
const container = this.containerElement;
|
||||||
|
const indicator = document.createElement('div');
|
||||||
|
indicator.className = 'page-transition-indicator';
|
||||||
|
|
||||||
|
// Get container position to properly position the indicator
|
||||||
|
const containerRect = container.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Style the indicator to match just the container area
|
||||||
|
indicator.style.position = 'fixed';
|
||||||
|
indicator.style.top = `${containerRect.top}px`;
|
||||||
|
indicator.style.left = `${containerRect.left}px`;
|
||||||
|
indicator.style.width = `${containerRect.width}px`;
|
||||||
|
indicator.style.height = `${containerRect.height}px`;
|
||||||
|
|
||||||
|
document.body.appendChild(indicator);
|
||||||
|
|
||||||
|
// Remove after animation completes
|
||||||
|
setTimeout(() => {
|
||||||
|
if (indicator.parentNode) {
|
||||||
|
indicator.remove();
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
scrollToTop() {
|
||||||
|
this.removeExistingTransitionIndicator();
|
||||||
|
|
||||||
|
// Page transition indicator removed
|
||||||
|
// this.showTransitionIndicator();
|
||||||
|
|
||||||
|
this.scrollContainer.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Force render after scrolling
|
||||||
|
setTimeout(() => this.renderItems(), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToBottom() {
|
||||||
|
this.removeExistingTransitionIndicator();
|
||||||
|
|
||||||
|
// Page transition indicator removed
|
||||||
|
// this.showTransitionIndicator();
|
||||||
|
|
||||||
|
// Start loading all remaining pages to ensure content is available
|
||||||
|
this.loadRemainingPages().then(() => {
|
||||||
|
// After loading all content, scroll to the very bottom
|
||||||
|
const maxScroll = this.scrollContainer.scrollHeight - this.scrollContainer.clientHeight;
|
||||||
|
this.scrollContainer.scrollTo({
|
||||||
|
top: maxScroll,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// New method to load all remaining pages
|
||||||
|
async loadRemainingPages() {
|
||||||
|
// If we're already at the end or loading, don't proceed
|
||||||
|
if (!this.hasMore || this.isLoading) return;
|
||||||
|
|
||||||
|
console.log('Loading all remaining pages for End key navigation...');
|
||||||
|
|
||||||
|
// Keep loading pages until we reach the end
|
||||||
|
while (this.hasMore && !this.isLoading) {
|
||||||
|
await this.loadMoreItems();
|
||||||
|
|
||||||
|
// Force render after each page load
|
||||||
|
this.renderItems();
|
||||||
|
|
||||||
|
// Small delay to prevent overwhelming the browser
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Finished loading all pages');
|
||||||
|
|
||||||
|
// Final render to ensure all content is displayed
|
||||||
|
this.renderItems();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,6 +131,9 @@ async function initializeVirtualScroll(pageType) {
|
|||||||
// Add grid class for CSS styling
|
// Add grid class for CSS styling
|
||||||
grid.classList.add('virtual-scroll');
|
grid.classList.add('virtual-scroll');
|
||||||
|
|
||||||
|
// Setup keyboard navigation
|
||||||
|
setupKeyboardNavigation();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error initializing virtual scroller for ${pageType}:`, error);
|
console.error(`Error initializing virtual scroller for ${pageType}:`, error);
|
||||||
showToast(`Failed to initialize ${pageType} page. Please reload.`, 'error');
|
showToast(`Failed to initialize ${pageType} page. Please reload.`, 'error');
|
||||||
@@ -145,6 +148,61 @@ async function initializeVirtualScroll(pageType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add keyboard navigation setup function
|
||||||
|
function setupKeyboardNavigation() {
|
||||||
|
// Keep track of the last keypress time to prevent multiple rapid triggers
|
||||||
|
let lastKeyTime = 0;
|
||||||
|
const keyDelay = 300; // ms between allowed keypresses
|
||||||
|
|
||||||
|
// Store the event listener reference so we can remove it later if needed
|
||||||
|
const keyboardNavHandler = (event) => {
|
||||||
|
// Only handle keyboard events when not in form elements
|
||||||
|
if (event.target.matches('input, textarea, select')) return;
|
||||||
|
|
||||||
|
// Prevent rapid keypresses
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastKeyTime < keyDelay) return;
|
||||||
|
lastKeyTime = now;
|
||||||
|
|
||||||
|
// Handle navigation keys
|
||||||
|
if (event.key === 'PageUp') {
|
||||||
|
event.preventDefault();
|
||||||
|
if (state.virtualScroller) {
|
||||||
|
state.virtualScroller.handlePageUpDown('up');
|
||||||
|
}
|
||||||
|
} else if (event.key === 'PageDown') {
|
||||||
|
event.preventDefault();
|
||||||
|
if (state.virtualScroller) {
|
||||||
|
state.virtualScroller.handlePageUpDown('down');
|
||||||
|
}
|
||||||
|
} else if (event.key === 'Home') {
|
||||||
|
event.preventDefault();
|
||||||
|
if (state.virtualScroller) {
|
||||||
|
state.virtualScroller.scrollToTop();
|
||||||
|
}
|
||||||
|
} else if (event.key === 'End') {
|
||||||
|
event.preventDefault();
|
||||||
|
if (state.virtualScroller) {
|
||||||
|
state.virtualScroller.scrollToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the event listener
|
||||||
|
document.addEventListener('keydown', keyboardNavHandler);
|
||||||
|
|
||||||
|
// Store the handler in state for potential cleanup
|
||||||
|
state.keyboardNavHandler = keyboardNavHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add cleanup function to remove keyboard navigation when needed
|
||||||
|
export function cleanupKeyboardNavigation() {
|
||||||
|
if (state.keyboardNavHandler) {
|
||||||
|
document.removeEventListener('keydown', state.keyboardNavHandler);
|
||||||
|
state.keyboardNavHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Export a method to refresh the virtual scroller when filters change
|
// Export a method to refresh the virtual scroller when filters change
|
||||||
export function refreshVirtualScroll() {
|
export function refreshVirtualScroll() {
|
||||||
if (state.virtualScroller) {
|
if (state.virtualScroller) {
|
||||||
|
|||||||
@@ -47,10 +47,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="toggle-folders-container">
|
|
||||||
<button class="toggle-folders-btn icon-only" title="Toggle folder tags">
|
<div class="controls-right">
|
||||||
<i class="fas fa-tags"></i>
|
<div class="toggle-folders-container">
|
||||||
</button>
|
<button class="toggle-folders-btn icon-only" title="Toggle folder tags">
|
||||||
|
<i class="fas fa-tags"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="keyboard-nav-hint tooltip">
|
||||||
|
<i class="fas fa-keyboard"></i>
|
||||||
|
<span class="tooltiptext">
|
||||||
|
Keyboard Navigation:
|
||||||
|
<table class="keyboard-shortcuts">
|
||||||
|
<tr>
|
||||||
|
<td><span class="key">Page Up</span></td>
|
||||||
|
<td>Scroll up one page</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><span class="key">Page Down</span></td>
|
||||||
|
<td>Scroll down one page</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><span class="key">Home</span></td>
|
||||||
|
<td>Jump to top</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><span class="key">End</span></td>
|
||||||
|
<td>Jump to bottom</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user