mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -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);
|
||||
}
|
||||
|
||||
.controls-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: auto; /* Push to the right */
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -338,11 +345,14 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.controls-right {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.toggle-folders-container {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.folder-tags-container {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
@import 'components/progress-panel.css';
|
||||
@import 'components/alphabet-bar.css'; /* Add alphabet bar component */
|
||||
@import 'components/duplicates.css'; /* Add duplicates component */
|
||||
@import 'components/keyboard-nav.css'; /* Add keyboard navigation component */
|
||||
|
||||
.initialization-notice {
|
||||
display: flex;
|
||||
|
||||
@@ -804,4 +804,131 @@ export class VirtualScroller {
|
||||
console.log(`Removed item with file path ${filePath} from virtual scroller data`);
|
||||
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
|
||||
grid.classList.add('virtual-scroll');
|
||||
|
||||
// Setup keyboard navigation
|
||||
setupKeyboardNavigation();
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error initializing virtual scroller for ${pageType}:`, 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 function refreshVirtualScroll() {
|
||||
if (state.virtualScroller) {
|
||||
|
||||
@@ -47,10 +47,38 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toggle-folders-container">
|
||||
<button class="toggle-folders-btn icon-only" title="Toggle folder tags">
|
||||
<i class="fas fa-tags"></i>
|
||||
</button>
|
||||
|
||||
<div class="controls-right">
|
||||
<div class="toggle-folders-container">
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user