remove duplicated files
All checks were successful
Build and Deploy / build (push) Successful in 38s
All checks were successful
Build and Deploy / build (push) Successful in 38s
This commit is contained in:
parent
356ba16c88
commit
49e024343e
@ -1,257 +0,0 @@
|
||||
/**
|
||||
* Base search module that provides core search functionality
|
||||
* This module can be extended for specific search implementations
|
||||
*/
|
||||
|
||||
/**
|
||||
* Initialize search functionality for any content type
|
||||
* @param {string} contentSelector - CSS selector for searchable items
|
||||
* @param {Object} options - Configuration options
|
||||
* @returns {Object} Alpine.js data object with search functionality
|
||||
*/
|
||||
export function initializeBaseSearch(contentSelector = '.searchable-item', options = {}) {
|
||||
const defaults = {
|
||||
nameAttribute: 'data-name',
|
||||
tagsAttribute: 'data-tags',
|
||||
categoryAttribute: 'data-category',
|
||||
additionalAttributes: [],
|
||||
noResultsMessage: 'No results found',
|
||||
allItemsMessage: 'Showing all items',
|
||||
resultCountMessage: (count) => `Found ${count} items`,
|
||||
itemLabel: 'items',
|
||||
debounceTime: 150 // ms to debounce search input
|
||||
};
|
||||
|
||||
const config = { ...defaults, ...options };
|
||||
|
||||
return {
|
||||
searchQuery: '',
|
||||
hasResults: true,
|
||||
visibleCount: 0,
|
||||
loading: false,
|
||||
focusedItemIndex: -1,
|
||||
debounceTimeout: null,
|
||||
|
||||
init() {
|
||||
// Initialize the visible count
|
||||
this.visibleCount = document.querySelectorAll(contentSelector).length;
|
||||
this.setupWatchers();
|
||||
this.setupKeyboardShortcuts();
|
||||
|
||||
// Handle theme changes
|
||||
window.addEventListener('theme-changed', () => {
|
||||
this.filterContent(this.searchQuery);
|
||||
});
|
||||
},
|
||||
|
||||
setupWatchers() {
|
||||
this.$watch('searchQuery', (query) => {
|
||||
// Debounce search for better performance
|
||||
if (this.debounceTimeout) {
|
||||
clearTimeout(this.debounceTimeout);
|
||||
}
|
||||
|
||||
this.debounceTimeout = setTimeout(() => {
|
||||
this.filterContent(query);
|
||||
}, config.debounceTime);
|
||||
});
|
||||
},
|
||||
|
||||
setupKeyboardShortcuts() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// '/' key focuses the search input
|
||||
if (e.key === '/' && document.activeElement.id !== 'app-search') {
|
||||
e.preventDefault();
|
||||
document.getElementById('app-search').focus();
|
||||
}
|
||||
|
||||
// Escape key clears the search
|
||||
if (e.key === 'Escape' && this.searchQuery !== '') {
|
||||
this.searchQuery = '';
|
||||
document.getElementById('app-search').focus();
|
||||
this.focusedItemIndex = -1;
|
||||
this.clearItemFocus();
|
||||
}
|
||||
|
||||
// Arrow key navigation through results
|
||||
if (this.searchQuery && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
|
||||
e.preventDefault();
|
||||
this.handleArrowNavigation(e.key);
|
||||
}
|
||||
|
||||
// Enter key selects the focused item
|
||||
if (e.key === 'Enter' && this.focusedItemIndex >= 0) {
|
||||
this.handleEnterSelection();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleArrowNavigation(key) {
|
||||
const visibleItems = this.getVisibleItems();
|
||||
if (visibleItems.length === 0) return;
|
||||
|
||||
// Update focused item index
|
||||
if (key === 'ArrowDown') {
|
||||
this.focusedItemIndex = Math.min(this.focusedItemIndex + 1, visibleItems.length - 1);
|
||||
} else {
|
||||
this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, -1);
|
||||
}
|
||||
|
||||
// Clear previous focus
|
||||
this.clearItemFocus();
|
||||
|
||||
// If we're back at -1, focus the search input
|
||||
if (this.focusedItemIndex === -1) {
|
||||
document.getElementById('app-search').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Focus the new item
|
||||
const itemToFocus = visibleItems[this.focusedItemIndex];
|
||||
this.focusItem(itemToFocus);
|
||||
},
|
||||
|
||||
handleEnterSelection() {
|
||||
const visibleItems = this.getVisibleItems();
|
||||
if (visibleItems.length === 0) return;
|
||||
|
||||
const selectedItem = visibleItems[this.focusedItemIndex];
|
||||
const link = selectedItem.querySelector('a');
|
||||
if (link) {
|
||||
link.click();
|
||||
}
|
||||
},
|
||||
|
||||
getVisibleItems() {
|
||||
return Array.from(document.querySelectorAll(contentSelector))
|
||||
.filter(item => item.style.display !== 'none');
|
||||
},
|
||||
|
||||
clearItemFocus() {
|
||||
// Remove focus styling from all items
|
||||
document.querySelectorAll(`${contentSelector}.keyboard-focus`).forEach(item => {
|
||||
item.classList.remove('keyboard-focus');
|
||||
});
|
||||
},
|
||||
|
||||
focusItem(item) {
|
||||
// Add focus styling
|
||||
item.classList.add('keyboard-focus');
|
||||
|
||||
// Scroll into view with options for smooth scrolling
|
||||
const scrollBehavior = window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth';
|
||||
item.scrollIntoView({ behavior: scrollBehavior, block: 'nearest' });
|
||||
},
|
||||
|
||||
filterContent(query) {
|
||||
query = query.toLowerCase().trim();
|
||||
let anyResults = false;
|
||||
let visibleCount = 0;
|
||||
|
||||
// Process all content items
|
||||
document.querySelectorAll(contentSelector).forEach((item) => {
|
||||
const isMatch = this.isItemMatch(item, query);
|
||||
|
||||
if (isMatch) {
|
||||
item.style.display = '';
|
||||
anyResults = true;
|
||||
visibleCount++;
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Update category visibility if applicable
|
||||
this.updateCategoryVisibility(query);
|
||||
|
||||
// Update parent containers if needed
|
||||
this.updateContainerVisibility(query);
|
||||
this.updateResultsStatus(query, anyResults, visibleCount);
|
||||
},
|
||||
|
||||
isItemMatch(item, query) {
|
||||
// If query is empty, show all items
|
||||
if (query === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get searchable attributes
|
||||
const name = (item.getAttribute(config.nameAttribute) || '').toLowerCase();
|
||||
const tags = (item.getAttribute(config.tagsAttribute) || '').toLowerCase();
|
||||
const category = (item.getAttribute(config.categoryAttribute) || '').toLowerCase();
|
||||
|
||||
// Check additional attributes if specified
|
||||
const additionalMatches = config.additionalAttributes.some(attr => {
|
||||
const value = (item.getAttribute(attr) || '').toLowerCase();
|
||||
return value.includes(query);
|
||||
});
|
||||
|
||||
// Check if any attribute matches the query
|
||||
return name.includes(query) ||
|
||||
tags.includes(query) ||
|
||||
category.includes(query) ||
|
||||
additionalMatches;
|
||||
},
|
||||
|
||||
updateCategoryVisibility(query) {
|
||||
// Only proceed if we have category sections
|
||||
const categorySections = document.querySelectorAll('.category-section');
|
||||
if (categorySections.length === 0) return;
|
||||
|
||||
// For each category section, check if it has any visible items
|
||||
categorySections.forEach((categorySection) => {
|
||||
const categoryId = categorySection.getAttribute('data-category');
|
||||
const items = categorySection.querySelectorAll(contentSelector);
|
||||
|
||||
// Count visible items in this category
|
||||
const visibleItems = Array.from(items).filter(item =>
|
||||
item.style.display !== 'none'
|
||||
).length;
|
||||
|
||||
// If no visible items and we're searching, hide the category
|
||||
if (query !== '' && visibleItems === 0) {
|
||||
categorySection.style.display = 'none';
|
||||
} else {
|
||||
categorySection.style.display = '';
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
updateContainerVisibility(query) {
|
||||
// If there are container elements that should be hidden when empty
|
||||
const containers = document.querySelectorAll('.content-container');
|
||||
if (containers.length > 0) {
|
||||
containers.forEach((container) => {
|
||||
const hasVisibleItems = Array.from(
|
||||
container.querySelectorAll(contentSelector)
|
||||
).some((item) => item.style.display !== 'none');
|
||||
|
||||
if (query === '' || hasVisibleItems) {
|
||||
container.style.display = '';
|
||||
} else {
|
||||
container.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updateResultsStatus(query, anyResults, count) {
|
||||
// Update results status
|
||||
this.hasResults = query === '' || anyResults;
|
||||
this.visibleCount = count;
|
||||
|
||||
// Update screen reader status
|
||||
const statusEl = document.getElementById('search-status');
|
||||
if (statusEl) {
|
||||
if (query === '') {
|
||||
statusEl.textContent = config.allItemsMessage;
|
||||
this.visibleCount = document.querySelectorAll(contentSelector).length;
|
||||
} else if (this.hasResults) {
|
||||
statusEl.textContent = config.resultCountMessage(count);
|
||||
} else {
|
||||
statusEl.textContent = config.noResultsMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
/**
|
||||
* Content search module for articles and projects
|
||||
* Provides specialized search functionality for content items
|
||||
*/
|
||||
|
||||
import { initializeBaseSearch } from './baseSearch.js';
|
||||
|
||||
/**
|
||||
* Initialize search functionality for articles
|
||||
* @returns {Object} Alpine.js data object with search functionality
|
||||
*/
|
||||
export function initializeArticlesSearch() {
|
||||
return initializeBaseSearch('.article-item', {
|
||||
nameAttribute: 'data-title',
|
||||
tagsAttribute: 'data-tags',
|
||||
additionalAttributes: ['data-description'],
|
||||
noResultsMessage: 'No articles found',
|
||||
allItemsMessage: 'Showing all articles',
|
||||
resultCountMessage: (count) => `Found ${count} articles`,
|
||||
itemLabel: 'articles'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize search functionality for projects
|
||||
* @returns {Object} Alpine.js data object with search functionality
|
||||
*/
|
||||
export function initializeProjectsSearch() {
|
||||
return initializeBaseSearch('.project-item', {
|
||||
nameAttribute: 'data-title',
|
||||
tagsAttribute: 'data-tags',
|
||||
additionalAttributes: ['data-description', 'data-github', 'data-live'],
|
||||
noResultsMessage: 'No projects found',
|
||||
allItemsMessage: 'Showing all projects',
|
||||
resultCountMessage: (count) => `Found ${count} projects`,
|
||||
itemLabel: 'projects'
|
||||
});
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Main search module that registers all search components with Alpine.js
|
||||
*/
|
||||
|
||||
import { initializeServicesSearch } from './servicesSearch.js';
|
||||
import { initializeArticlesSearch, initializeProjectsSearch } from './contentSearch.js';
|
||||
|
||||
/**
|
||||
* Register all search components with Alpine.js
|
||||
* This function is called when Alpine.js is initialized
|
||||
*/
|
||||
export function registerSearchComponents() {
|
||||
// Register services search
|
||||
window.Alpine.data('searchServices', initializeServicesSearch);
|
||||
|
||||
// Register articles search
|
||||
window.Alpine.data('searchArticles', initializeArticlesSearch);
|
||||
|
||||
// Register projects search
|
||||
window.Alpine.data('searchProjects', initializeProjectsSearch);
|
||||
}
|
||||
|
||||
// Register components when Alpine.js is initialized
|
||||
document.addEventListener('alpine:init', registerSearchComponents);
|
@ -1,243 +0,0 @@
|
||||
/**
|
||||
* Services search module for homelab services
|
||||
* Extends the base search functionality with service-specific features
|
||||
*/
|
||||
|
||||
import { initializeBaseSearch } from './baseSearch.js';
|
||||
|
||||
/**
|
||||
* Initialize search functionality for homelab services
|
||||
* @returns {Object} Alpine.js data object with search functionality
|
||||
*/
|
||||
export function initializeServicesSearch() {
|
||||
// Create base search with service-specific configuration
|
||||
const baseSearch = initializeBaseSearch('.app-card', {
|
||||
nameAttribute: 'data-app-name',
|
||||
tagsAttribute: 'data-app-tags',
|
||||
categoryAttribute: 'data-app-category',
|
||||
noResultsMessage: 'No services found',
|
||||
allItemsMessage: 'Showing all services',
|
||||
resultCountMessage: (count) => `Found ${count} services`,
|
||||
itemLabel: 'services'
|
||||
});
|
||||
|
||||
// Extend with service-specific functionality
|
||||
return {
|
||||
...baseSearch,
|
||||
|
||||
// View mode properties
|
||||
iconSizeValue: 2, // Slider value: 1=small, 2=medium, 3=large
|
||||
iconSize: 'medium', // small, medium, large
|
||||
viewMode: 'grid', // grid or list
|
||||
displayMode: 'both', // both, image, or name
|
||||
debounceTimeout: null, // For debouncing slider changes
|
||||
|
||||
init() {
|
||||
// Call the base init method
|
||||
baseSearch.init.call(this);
|
||||
|
||||
// Apply initial icon size, view mode, and display mode
|
||||
this.applyIconSize();
|
||||
this.applyViewMode();
|
||||
this.applyDisplayMode();
|
||||
|
||||
// Save preferences to localStorage if available
|
||||
this.loadPreferences();
|
||||
|
||||
// Listen for window resize events to optimize layout
|
||||
this.setupResizeListener();
|
||||
},
|
||||
|
||||
// Load user preferences from localStorage
|
||||
loadPreferences() {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
try {
|
||||
// Load icon size
|
||||
const savedIconSize = localStorage.getItem('services-icon-size');
|
||||
if (savedIconSize) {
|
||||
this.setIconSize(parseFloat(savedIconSize));
|
||||
}
|
||||
|
||||
// Load view mode
|
||||
const savedViewMode = localStorage.getItem('services-view-mode');
|
||||
if (savedViewMode) {
|
||||
this.setViewMode(savedViewMode);
|
||||
}
|
||||
|
||||
// Load display mode
|
||||
const savedDisplayMode = localStorage.getItem('services-display-mode');
|
||||
if (savedDisplayMode) {
|
||||
this.setDisplayMode(savedDisplayMode);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading preferences:', e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Save user preferences to localStorage
|
||||
savePreferences() {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
try {
|
||||
localStorage.setItem('services-icon-size', this.iconSizeValue.toString());
|
||||
localStorage.setItem('services-view-mode', this.viewMode);
|
||||
localStorage.setItem('services-display-mode', this.displayMode);
|
||||
} catch (e) {
|
||||
console.error('Error saving preferences:', e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Setup listener for window resize events
|
||||
setupResizeListener() {
|
||||
const handleResize = () => {
|
||||
// Switch to list view on small screens if not explicitly set by user
|
||||
const userHasSetViewMode = localStorage.getItem('services-view-mode') !== null;
|
||||
|
||||
if (!userHasSetViewMode) {
|
||||
const smallScreen = window.innerWidth < 640; // sm breakpoint
|
||||
|
||||
if (smallScreen && this.viewMode !== 'list') {
|
||||
this.setViewMode('list', false); // Don't save to preferences
|
||||
} else if (!smallScreen && this.viewMode !== 'grid') {
|
||||
this.setViewMode('grid', false); // Don't save to preferences
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Initial check
|
||||
handleResize();
|
||||
|
||||
// Add resize listener with debounce
|
||||
let resizeTimeout;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(handleResize, 250);
|
||||
});
|
||||
},
|
||||
|
||||
// Icon size methods
|
||||
setIconSize(size, savePreference = true) {
|
||||
if (typeof size === 'string') {
|
||||
// Handle legacy string values (small, medium, large)
|
||||
this.iconSize = size;
|
||||
this.iconSizeValue = size === 'small' ? 1 : size === 'medium' ? 2 : 3;
|
||||
} else {
|
||||
// Handle slider numeric values
|
||||
this.iconSizeValue = parseFloat(size);
|
||||
|
||||
// Map slider value to size name
|
||||
if (this.iconSizeValue <= 1.33) {
|
||||
this.iconSize = 'small';
|
||||
} else if (this.iconSizeValue <= 2.33) {
|
||||
this.iconSize = 'medium';
|
||||
} else {
|
||||
this.iconSize = 'large';
|
||||
}
|
||||
}
|
||||
|
||||
this.applyIconSize();
|
||||
|
||||
// Save preference if requested
|
||||
if (savePreference) {
|
||||
this.savePreferences();
|
||||
}
|
||||
},
|
||||
|
||||
// Handle slider input with debounce
|
||||
handleSliderChange(event) {
|
||||
const value = event.target.value;
|
||||
|
||||
// Clear any existing timeout
|
||||
if (this.debounceTimeout) {
|
||||
clearTimeout(this.debounceTimeout);
|
||||
}
|
||||
|
||||
// Set a new timeout
|
||||
this.debounceTimeout = setTimeout(() => {
|
||||
this.setIconSize(value);
|
||||
}, 50); // 50ms debounce
|
||||
},
|
||||
|
||||
applyIconSize() {
|
||||
const appList = document.getElementById('app-list');
|
||||
if (!appList) return;
|
||||
|
||||
// Remove existing size classes
|
||||
appList.classList.remove('icon-size-small', 'icon-size-medium', 'icon-size-large');
|
||||
|
||||
// Add the new size class
|
||||
appList.classList.add(`icon-size-${this.iconSize}`);
|
||||
|
||||
// Apply custom CSS variable for fine-grained control
|
||||
appList.style.setProperty('--icon-scale', this.iconSizeValue);
|
||||
},
|
||||
|
||||
// View mode methods
|
||||
toggleViewMode() {
|
||||
this.viewMode = this.viewMode === 'grid' ? 'list' : 'grid';
|
||||
this.applyViewMode();
|
||||
this.savePreferences();
|
||||
},
|
||||
|
||||
setViewMode(mode, savePreference = true) {
|
||||
this.viewMode = mode;
|
||||
this.applyViewMode();
|
||||
|
||||
// Save preference if requested
|
||||
if (savePreference) {
|
||||
this.savePreferences();
|
||||
}
|
||||
},
|
||||
|
||||
applyViewMode() {
|
||||
const appList = document.getElementById('app-list');
|
||||
if (!appList) return;
|
||||
|
||||
// Remove existing view mode classes
|
||||
appList.classList.remove('view-mode-grid', 'view-mode-list');
|
||||
|
||||
// Add the new view mode class
|
||||
appList.classList.add(`view-mode-${this.viewMode}`);
|
||||
|
||||
// Update all category sections
|
||||
document.querySelectorAll('.category-section').forEach(section => {
|
||||
const gridContainer = section.querySelector('.grid');
|
||||
if (gridContainer) {
|
||||
// Update grid classes based on view mode
|
||||
if (this.viewMode === 'grid') {
|
||||
gridContainer.classList.remove('grid-cols-1');
|
||||
gridContainer.classList.add('grid-cols-2', 'sm:grid-cols-3', 'lg:grid-cols-4');
|
||||
} else {
|
||||
gridContainer.classList.remove('grid-cols-2', 'sm:grid-cols-3', 'lg:grid-cols-4');
|
||||
gridContainer.classList.add('grid-cols-1');
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Display mode methods
|
||||
setDisplayMode(mode) {
|
||||
this.displayMode = mode;
|
||||
this.applyDisplayMode();
|
||||
this.savePreferences();
|
||||
},
|
||||
|
||||
applyDisplayMode() {
|
||||
const appList = document.getElementById('app-list');
|
||||
if (!appList) return;
|
||||
|
||||
// Remove existing display mode classes
|
||||
appList.classList.remove('display-both', 'display-image-only', 'display-name-only');
|
||||
|
||||
// Add the new display mode class
|
||||
if (this.displayMode === 'image') {
|
||||
appList.classList.add('display-image-only');
|
||||
} else if (this.displayMode === 'name') {
|
||||
appList.classList.add('display-name-only');
|
||||
} else {
|
||||
appList.classList.add('display-both');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@ -1,176 +0,0 @@
|
||||
/**
|
||||
* Client-side search functionality for filtering content across the site
|
||||
*/
|
||||
|
||||
/**
|
||||
* Initialize search functionality for any content type
|
||||
* @param {string} contentSelector - CSS selector for the items to filter
|
||||
* @param {Object} options - Configuration options
|
||||
* @returns {Object} Alpine.js data object with search functionality
|
||||
*/
|
||||
function initializeSearch(contentSelector = '.searchable-item', options = {}) {
|
||||
const defaults = {
|
||||
nameAttribute: 'data-name',
|
||||
tagsAttribute: 'data-tags',
|
||||
categoryAttribute: 'data-category',
|
||||
additionalAttributes: [],
|
||||
noResultsMessage: 'No results found',
|
||||
allItemsMessage: 'Showing all items',
|
||||
resultCountMessage: (count) => `Found ${count} items`,
|
||||
itemLabel: 'items'
|
||||
};
|
||||
|
||||
const config = { ...defaults, ...options };
|
||||
|
||||
return {
|
||||
searchQuery: '',
|
||||
hasResults: true,
|
||||
visibleCount: 0,
|
||||
|
||||
init() {
|
||||
// Initialize the visible count
|
||||
this.visibleCount = document.querySelectorAll(contentSelector).length;
|
||||
this.setupWatchers();
|
||||
this.setupKeyboardShortcuts();
|
||||
},
|
||||
|
||||
setupWatchers() {
|
||||
this.$watch('searchQuery', (query) => {
|
||||
this.filterContent(query);
|
||||
});
|
||||
},
|
||||
|
||||
setupKeyboardShortcuts() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// '/' key focuses the search input
|
||||
if (e.key === '/' && document.activeElement.id !== 'app-search') {
|
||||
e.preventDefault();
|
||||
document.getElementById('app-search').focus();
|
||||
}
|
||||
|
||||
// Escape key clears the search
|
||||
if (e.key === 'Escape' && this.searchQuery !== '') {
|
||||
this.searchQuery = '';
|
||||
document.getElementById('app-search').focus();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
filterContent(query) {
|
||||
query = query.toLowerCase();
|
||||
let anyResults = false;
|
||||
let visibleCount = 0;
|
||||
|
||||
// Process all content items
|
||||
document.querySelectorAll(contentSelector).forEach((item) => {
|
||||
// Get searchable attributes
|
||||
const name = (item.getAttribute(config.nameAttribute) || '').toLowerCase();
|
||||
const tags = (item.getAttribute(config.tagsAttribute) || '').toLowerCase();
|
||||
const category = (item.getAttribute(config.categoryAttribute) || '').toLowerCase();
|
||||
|
||||
// Check additional attributes if specified
|
||||
const additionalMatches = config.additionalAttributes.some(attr => {
|
||||
const value = (item.getAttribute(attr) || '').toLowerCase();
|
||||
return value.includes(query);
|
||||
});
|
||||
|
||||
const isMatch = query === '' ||
|
||||
name.includes(query) ||
|
||||
tags.includes(query) ||
|
||||
category.includes(query) ||
|
||||
additionalMatches;
|
||||
|
||||
if (isMatch) {
|
||||
item.style.display = '';
|
||||
anyResults = true;
|
||||
visibleCount++;
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Update parent containers if needed
|
||||
this.updateContainerVisibility(query);
|
||||
this.updateResultsStatus(query, anyResults, visibleCount);
|
||||
},
|
||||
|
||||
updateContainerVisibility(query) {
|
||||
// If there are container elements that should be hidden when empty
|
||||
const containers = document.querySelectorAll('.content-container');
|
||||
if (containers.length > 0) {
|
||||
containers.forEach((container) => {
|
||||
const hasVisibleItems = Array.from(
|
||||
container.querySelectorAll(contentSelector)
|
||||
).some((item) => item.style.display !== 'none');
|
||||
|
||||
if (query === '' || hasVisibleItems) {
|
||||
container.style.display = '';
|
||||
} else {
|
||||
container.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updateResultsStatus(query, anyResults, count) {
|
||||
// Update results status
|
||||
this.hasResults = query === '' || anyResults;
|
||||
this.visibleCount = count;
|
||||
|
||||
// Update screen reader status
|
||||
const statusEl = document.getElementById('search-status');
|
||||
if (statusEl) {
|
||||
if (query === '') {
|
||||
statusEl.textContent = config.allItemsMessage;
|
||||
this.visibleCount = document.querySelectorAll(contentSelector).length;
|
||||
} else if (this.hasResults) {
|
||||
statusEl.textContent = config.resultCountMessage(count);
|
||||
} else {
|
||||
statusEl.textContent = config.noResultsMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Register Alpine.js data components when Alpine is loaded
|
||||
document.addEventListener('alpine:init', () => {
|
||||
// Homelab search
|
||||
window.Alpine.data('searchServices', () => {
|
||||
return initializeSearch('.app-card', {
|
||||
nameAttribute: 'data-app-name',
|
||||
tagsAttribute: 'data-app-tags',
|
||||
categoryAttribute: 'data-app-category',
|
||||
noResultsMessage: 'No services found',
|
||||
allItemsMessage: 'Showing all services',
|
||||
resultCountMessage: (count) => `Found ${count} services`,
|
||||
itemLabel: 'services'
|
||||
});
|
||||
});
|
||||
|
||||
// Blog search
|
||||
window.Alpine.data('searchArticles', () => {
|
||||
return initializeSearch('.article-item', {
|
||||
nameAttribute: 'data-title',
|
||||
tagsAttribute: 'data-tags',
|
||||
additionalAttributes: ['data-description'],
|
||||
noResultsMessage: 'No articles found',
|
||||
allItemsMessage: 'Showing all articles',
|
||||
resultCountMessage: (count) => `Found ${count} articles`,
|
||||
itemLabel: 'articles'
|
||||
});
|
||||
});
|
||||
|
||||
// Projects search
|
||||
window.Alpine.data('searchProjects', () => {
|
||||
return initializeSearch('.project-item', {
|
||||
nameAttribute: 'data-title',
|
||||
tagsAttribute: 'data-tags',
|
||||
additionalAttributes: ['data-description', 'data-github', 'data-live'],
|
||||
noResultsMessage: 'No projects found',
|
||||
allItemsMessage: 'Showing all projects',
|
||||
resultCountMessage: (count) => `Found ${count} projects`,
|
||||
itemLabel: 'projects'
|
||||
});
|
||||
});
|
||||
});
|
@ -1,257 +0,0 @@
|
||||
/**
|
||||
* Base search module that provides core search functionality
|
||||
* This module can be extended for specific search implementations
|
||||
*/
|
||||
|
||||
/**
|
||||
* Initialize search functionality for any content type
|
||||
* @param {string} contentSelector - CSS selector for searchable items
|
||||
* @param {Object} options - Configuration options
|
||||
* @returns {Object} Alpine.js data object with search functionality
|
||||
*/
|
||||
export function initializeBaseSearch(contentSelector = '.searchable-item', options = {}) {
|
||||
const defaults = {
|
||||
nameAttribute: 'data-name',
|
||||
tagsAttribute: 'data-tags',
|
||||
categoryAttribute: 'data-category',
|
||||
additionalAttributes: [],
|
||||
noResultsMessage: 'No results found',
|
||||
allItemsMessage: 'Showing all items',
|
||||
resultCountMessage: (count) => `Found ${count} items`,
|
||||
itemLabel: 'items',
|
||||
debounceTime: 150 // ms to debounce search input
|
||||
};
|
||||
|
||||
const config = { ...defaults, ...options };
|
||||
|
||||
return {
|
||||
searchQuery: '',
|
||||
hasResults: true,
|
||||
visibleCount: 0,
|
||||
loading: false,
|
||||
focusedItemIndex: -1,
|
||||
debounceTimeout: null,
|
||||
|
||||
init() {
|
||||
// Initialize the visible count
|
||||
this.visibleCount = document.querySelectorAll(contentSelector).length;
|
||||
this.setupWatchers();
|
||||
this.setupKeyboardShortcuts();
|
||||
|
||||
// Handle theme changes
|
||||
window.addEventListener('theme-changed', () => {
|
||||
this.filterContent(this.searchQuery);
|
||||
});
|
||||
},
|
||||
|
||||
setupWatchers() {
|
||||
this.$watch('searchQuery', (query) => {
|
||||
// Debounce search for better performance
|
||||
if (this.debounceTimeout) {
|
||||
clearTimeout(this.debounceTimeout);
|
||||
}
|
||||
|
||||
this.debounceTimeout = setTimeout(() => {
|
||||
this.filterContent(query);
|
||||
}, config.debounceTime);
|
||||
});
|
||||
},
|
||||
|
||||
setupKeyboardShortcuts() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// '/' key focuses the search input
|
||||
if (e.key === '/' && document.activeElement.id !== 'app-search') {
|
||||
e.preventDefault();
|
||||
document.getElementById('app-search').focus();
|
||||
}
|
||||
|
||||
// Escape key clears the search
|
||||
if (e.key === 'Escape' && this.searchQuery !== '') {
|
||||
this.searchQuery = '';
|
||||
document.getElementById('app-search').focus();
|
||||
this.focusedItemIndex = -1;
|
||||
this.clearItemFocus();
|
||||
}
|
||||
|
||||
// Arrow key navigation through results
|
||||
if (this.searchQuery && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
|
||||
e.preventDefault();
|
||||
this.handleArrowNavigation(e.key);
|
||||
}
|
||||
|
||||
// Enter key selects the focused item
|
||||
if (e.key === 'Enter' && this.focusedItemIndex >= 0) {
|
||||
this.handleEnterSelection();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleArrowNavigation(key) {
|
||||
const visibleItems = this.getVisibleItems();
|
||||
if (visibleItems.length === 0) return;
|
||||
|
||||
// Update focused item index
|
||||
if (key === 'ArrowDown') {
|
||||
this.focusedItemIndex = Math.min(this.focusedItemIndex + 1, visibleItems.length - 1);
|
||||
} else {
|
||||
this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, -1);
|
||||
}
|
||||
|
||||
// Clear previous focus
|
||||
this.clearItemFocus();
|
||||
|
||||
// If we're back at -1, focus the search input
|
||||
if (this.focusedItemIndex === -1) {
|
||||
document.getElementById('app-search').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Focus the new item
|
||||
const itemToFocus = visibleItems[this.focusedItemIndex];
|
||||
this.focusItem(itemToFocus);
|
||||
},
|
||||
|
||||
handleEnterSelection() {
|
||||
const visibleItems = this.getVisibleItems();
|
||||
if (visibleItems.length === 0) return;
|
||||
|
||||
const selectedItem = visibleItems[this.focusedItemIndex];
|
||||
const link = selectedItem.querySelector('a');
|
||||
if (link) {
|
||||
link.click();
|
||||
}
|
||||
},
|
||||
|
||||
getVisibleItems() {
|
||||
return Array.from(document.querySelectorAll(contentSelector))
|
||||
.filter(item => item.style.display !== 'none');
|
||||
},
|
||||
|
||||
clearItemFocus() {
|
||||
// Remove focus styling from all items
|
||||
document.querySelectorAll(`${contentSelector}.keyboard-focus`).forEach(item => {
|
||||
item.classList.remove('keyboard-focus');
|
||||
});
|
||||
},
|
||||
|
||||
focusItem(item) {
|
||||
// Add focus styling
|
||||
item.classList.add('keyboard-focus');
|
||||
|
||||
// Scroll into view with options for smooth scrolling
|
||||
const scrollBehavior = window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth';
|
||||
item.scrollIntoView({ behavior: scrollBehavior, block: 'nearest' });
|
||||
},
|
||||
|
||||
filterContent(query) {
|
||||
query = query.toLowerCase().trim();
|
||||
let anyResults = false;
|
||||
let visibleCount = 0;
|
||||
|
||||
// Process all content items
|
||||
document.querySelectorAll(contentSelector).forEach((item) => {
|
||||
const isMatch = this.isItemMatch(item, query);
|
||||
|
||||
if (isMatch) {
|
||||
item.style.display = '';
|
||||
anyResults = true;
|
||||
visibleCount++;
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Update category visibility if applicable
|
||||
this.updateCategoryVisibility(query);
|
||||
|
||||
// Update parent containers if needed
|
||||
this.updateContainerVisibility(query);
|
||||
this.updateResultsStatus(query, anyResults, visibleCount);
|
||||
},
|
||||
|
||||
isItemMatch(item, query) {
|
||||
// If query is empty, show all items
|
||||
if (query === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get searchable attributes
|
||||
const name = (item.getAttribute(config.nameAttribute) || '').toLowerCase();
|
||||
const tags = (item.getAttribute(config.tagsAttribute) || '').toLowerCase();
|
||||
const category = (item.getAttribute(config.categoryAttribute) || '').toLowerCase();
|
||||
|
||||
// Check additional attributes if specified
|
||||
const additionalMatches = config.additionalAttributes.some(attr => {
|
||||
const value = (item.getAttribute(attr) || '').toLowerCase();
|
||||
return value.includes(query);
|
||||
});
|
||||
|
||||
// Check if any attribute matches the query
|
||||
return name.includes(query) ||
|
||||
tags.includes(query) ||
|
||||
category.includes(query) ||
|
||||
additionalMatches;
|
||||
},
|
||||
|
||||
updateCategoryVisibility(query) {
|
||||
// Only proceed if we have category sections
|
||||
const categorySections = document.querySelectorAll('.category-section');
|
||||
if (categorySections.length === 0) return;
|
||||
|
||||
// For each category section, check if it has any visible items
|
||||
categorySections.forEach((categorySection) => {
|
||||
const categoryId = categorySection.getAttribute('data-category');
|
||||
const items = categorySection.querySelectorAll(contentSelector);
|
||||
|
||||
// Count visible items in this category
|
||||
const visibleItems = Array.from(items).filter(item =>
|
||||
item.style.display !== 'none'
|
||||
).length;
|
||||
|
||||
// If no visible items and we're searching, hide the category
|
||||
if (query !== '' && visibleItems === 0) {
|
||||
categorySection.style.display = 'none';
|
||||
} else {
|
||||
categorySection.style.display = '';
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
updateContainerVisibility(query) {
|
||||
// If there are container elements that should be hidden when empty
|
||||
const containers = document.querySelectorAll('.content-container');
|
||||
if (containers.length > 0) {
|
||||
containers.forEach((container) => {
|
||||
const hasVisibleItems = Array.from(
|
||||
container.querySelectorAll(contentSelector)
|
||||
).some((item) => item.style.display !== 'none');
|
||||
|
||||
if (query === '' || hasVisibleItems) {
|
||||
container.style.display = '';
|
||||
} else {
|
||||
container.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updateResultsStatus(query, anyResults, count) {
|
||||
// Update results status
|
||||
this.hasResults = query === '' || anyResults;
|
||||
this.visibleCount = count;
|
||||
|
||||
// Update screen reader status
|
||||
const statusEl = document.getElementById('search-status');
|
||||
if (statusEl) {
|
||||
if (query === '') {
|
||||
statusEl.textContent = config.allItemsMessage;
|
||||
this.visibleCount = document.querySelectorAll(contentSelector).length;
|
||||
} else if (this.hasResults) {
|
||||
statusEl.textContent = config.resultCountMessage(count);
|
||||
} else {
|
||||
statusEl.textContent = config.noResultsMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
/**
|
||||
* Content search module for articles and projects
|
||||
* Provides specialized search functionality for content items
|
||||
*/
|
||||
|
||||
import { initializeBaseSearch } from './baseSearch.js';
|
||||
|
||||
/**
|
||||
* Initialize search functionality for articles
|
||||
* @returns {Object} Alpine.js data object with search functionality
|
||||
*/
|
||||
export function initializeArticlesSearch() {
|
||||
return initializeBaseSearch('.article-item', {
|
||||
nameAttribute: 'data-title',
|
||||
tagsAttribute: 'data-tags',
|
||||
additionalAttributes: ['data-description'],
|
||||
noResultsMessage: 'No articles found',
|
||||
allItemsMessage: 'Showing all articles',
|
||||
resultCountMessage: (count) => `Found ${count} articles`,
|
||||
itemLabel: 'articles'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize search functionality for projects
|
||||
* @returns {Object} Alpine.js data object with search functionality
|
||||
*/
|
||||
export function initializeProjectsSearch() {
|
||||
return initializeBaseSearch('.project-item', {
|
||||
nameAttribute: 'data-title',
|
||||
tagsAttribute: 'data-tags',
|
||||
additionalAttributes: ['data-description', 'data-github', 'data-live'],
|
||||
noResultsMessage: 'No projects found',
|
||||
allItemsMessage: 'Showing all projects',
|
||||
resultCountMessage: (count) => `Found ${count} projects`,
|
||||
itemLabel: 'projects'
|
||||
});
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Main search module that registers all search components with Alpine.js
|
||||
*/
|
||||
|
||||
import { initializeServicesSearch } from './servicesSearch.js';
|
||||
import { initializeArticlesSearch, initializeProjectsSearch } from './contentSearch.js';
|
||||
|
||||
/**
|
||||
* Register all search components with Alpine.js
|
||||
* This function is called when Alpine.js is initialized
|
||||
*/
|
||||
export function registerSearchComponents() {
|
||||
// Register services search
|
||||
window.Alpine.data('searchServices', initializeServicesSearch);
|
||||
|
||||
// Register articles search
|
||||
window.Alpine.data('searchArticles', initializeArticlesSearch);
|
||||
|
||||
// Register projects search
|
||||
window.Alpine.data('searchProjects', initializeProjectsSearch);
|
||||
}
|
||||
|
||||
// Register components when Alpine.js is initialized
|
||||
document.addEventListener('alpine:init', registerSearchComponents);
|
@ -1,243 +0,0 @@
|
||||
/**
|
||||
* Services search module for homelab services
|
||||
* Extends the base search functionality with service-specific features
|
||||
*/
|
||||
|
||||
import { initializeBaseSearch } from './baseSearch.js';
|
||||
|
||||
/**
|
||||
* Initialize search functionality for homelab services
|
||||
* @returns {Object} Alpine.js data object with search functionality
|
||||
*/
|
||||
export function initializeServicesSearch() {
|
||||
// Create base search with service-specific configuration
|
||||
const baseSearch = initializeBaseSearch('.app-card', {
|
||||
nameAttribute: 'data-app-name',
|
||||
tagsAttribute: 'data-app-tags',
|
||||
categoryAttribute: 'data-app-category',
|
||||
noResultsMessage: 'No services found',
|
||||
allItemsMessage: 'Showing all services',
|
||||
resultCountMessage: (count) => `Found ${count} services`,
|
||||
itemLabel: 'services'
|
||||
});
|
||||
|
||||
// Extend with service-specific functionality
|
||||
return {
|
||||
...baseSearch,
|
||||
|
||||
// View mode properties
|
||||
iconSizeValue: 2, // Slider value: 1=small, 2=medium, 3=large
|
||||
iconSize: 'medium', // small, medium, large
|
||||
viewMode: 'grid', // grid or list
|
||||
displayMode: 'both', // both, image, or name
|
||||
debounceTimeout: null, // For debouncing slider changes
|
||||
|
||||
init() {
|
||||
// Call the base init method
|
||||
baseSearch.init.call(this);
|
||||
|
||||
// Apply initial icon size, view mode, and display mode
|
||||
this.applyIconSize();
|
||||
this.applyViewMode();
|
||||
this.applyDisplayMode();
|
||||
|
||||
// Save preferences to localStorage if available
|
||||
this.loadPreferences();
|
||||
|
||||
// Listen for window resize events to optimize layout
|
||||
this.setupResizeListener();
|
||||
},
|
||||
|
||||
// Load user preferences from localStorage
|
||||
loadPreferences() {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
try {
|
||||
// Load icon size
|
||||
const savedIconSize = localStorage.getItem('services-icon-size');
|
||||
if (savedIconSize) {
|
||||
this.setIconSize(parseFloat(savedIconSize));
|
||||
}
|
||||
|
||||
// Load view mode
|
||||
const savedViewMode = localStorage.getItem('services-view-mode');
|
||||
if (savedViewMode) {
|
||||
this.setViewMode(savedViewMode);
|
||||
}
|
||||
|
||||
// Load display mode
|
||||
const savedDisplayMode = localStorage.getItem('services-display-mode');
|
||||
if (savedDisplayMode) {
|
||||
this.setDisplayMode(savedDisplayMode);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading preferences:', e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Save user preferences to localStorage
|
||||
savePreferences() {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
try {
|
||||
localStorage.setItem('services-icon-size', this.iconSizeValue.toString());
|
||||
localStorage.setItem('services-view-mode', this.viewMode);
|
||||
localStorage.setItem('services-display-mode', this.displayMode);
|
||||
} catch (e) {
|
||||
console.error('Error saving preferences:', e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Setup listener for window resize events
|
||||
setupResizeListener() {
|
||||
const handleResize = () => {
|
||||
// Switch to list view on small screens if not explicitly set by user
|
||||
const userHasSetViewMode = localStorage.getItem('services-view-mode') !== null;
|
||||
|
||||
if (!userHasSetViewMode) {
|
||||
const smallScreen = window.innerWidth < 640; // sm breakpoint
|
||||
|
||||
if (smallScreen && this.viewMode !== 'list') {
|
||||
this.setViewMode('list', false); // Don't save to preferences
|
||||
} else if (!smallScreen && this.viewMode !== 'grid') {
|
||||
this.setViewMode('grid', false); // Don't save to preferences
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Initial check
|
||||
handleResize();
|
||||
|
||||
// Add resize listener with debounce
|
||||
let resizeTimeout;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(handleResize, 250);
|
||||
});
|
||||
},
|
||||
|
||||
// Icon size methods
|
||||
setIconSize(size, savePreference = true) {
|
||||
if (typeof size === 'string') {
|
||||
// Handle legacy string values (small, medium, large)
|
||||
this.iconSize = size;
|
||||
this.iconSizeValue = size === 'small' ? 1 : size === 'medium' ? 2 : 3;
|
||||
} else {
|
||||
// Handle slider numeric values
|
||||
this.iconSizeValue = parseFloat(size);
|
||||
|
||||
// Map slider value to size name
|
||||
if (this.iconSizeValue <= 1.33) {
|
||||
this.iconSize = 'small';
|
||||
} else if (this.iconSizeValue <= 2.33) {
|
||||
this.iconSize = 'medium';
|
||||
} else {
|
||||
this.iconSize = 'large';
|
||||
}
|
||||
}
|
||||
|
||||
this.applyIconSize();
|
||||
|
||||
// Save preference if requested
|
||||
if (savePreference) {
|
||||
this.savePreferences();
|
||||
}
|
||||
},
|
||||
|
||||
// Handle slider input with debounce
|
||||
handleSliderChange(event) {
|
||||
const value = event.target.value;
|
||||
|
||||
// Clear any existing timeout
|
||||
if (this.debounceTimeout) {
|
||||
clearTimeout(this.debounceTimeout);
|
||||
}
|
||||
|
||||
// Set a new timeout
|
||||
this.debounceTimeout = setTimeout(() => {
|
||||
this.setIconSize(value);
|
||||
}, 50); // 50ms debounce
|
||||
},
|
||||
|
||||
applyIconSize() {
|
||||
const appList = document.getElementById('app-list');
|
||||
if (!appList) return;
|
||||
|
||||
// Remove existing size classes
|
||||
appList.classList.remove('icon-size-small', 'icon-size-medium', 'icon-size-large');
|
||||
|
||||
// Add the new size class
|
||||
appList.classList.add(`icon-size-${this.iconSize}`);
|
||||
|
||||
// Apply custom CSS variable for fine-grained control
|
||||
appList.style.setProperty('--icon-scale', this.iconSizeValue);
|
||||
},
|
||||
|
||||
// View mode methods
|
||||
toggleViewMode() {
|
||||
this.viewMode = this.viewMode === 'grid' ? 'list' : 'grid';
|
||||
this.applyViewMode();
|
||||
this.savePreferences();
|
||||
},
|
||||
|
||||
setViewMode(mode, savePreference = true) {
|
||||
this.viewMode = mode;
|
||||
this.applyViewMode();
|
||||
|
||||
// Save preference if requested
|
||||
if (savePreference) {
|
||||
this.savePreferences();
|
||||
}
|
||||
},
|
||||
|
||||
applyViewMode() {
|
||||
const appList = document.getElementById('app-list');
|
||||
if (!appList) return;
|
||||
|
||||
// Remove existing view mode classes
|
||||
appList.classList.remove('view-mode-grid', 'view-mode-list');
|
||||
|
||||
// Add the new view mode class
|
||||
appList.classList.add(`view-mode-${this.viewMode}`);
|
||||
|
||||
// Update all category sections
|
||||
document.querySelectorAll('.category-section').forEach(section => {
|
||||
const gridContainer = section.querySelector('.grid');
|
||||
if (gridContainer) {
|
||||
// Update grid classes based on view mode
|
||||
if (this.viewMode === 'grid') {
|
||||
gridContainer.classList.remove('grid-cols-1');
|
||||
gridContainer.classList.add('grid-cols-2', 'sm:grid-cols-3', 'lg:grid-cols-4');
|
||||
} else {
|
||||
gridContainer.classList.remove('grid-cols-2', 'sm:grid-cols-3', 'lg:grid-cols-4');
|
||||
gridContainer.classList.add('grid-cols-1');
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Display mode methods
|
||||
setDisplayMode(mode) {
|
||||
this.displayMode = mode;
|
||||
this.applyDisplayMode();
|
||||
this.savePreferences();
|
||||
},
|
||||
|
||||
applyDisplayMode() {
|
||||
const appList = document.getElementById('app-list');
|
||||
if (!appList) return;
|
||||
|
||||
// Remove existing display mode classes
|
||||
appList.classList.remove('display-both', 'display-image-only', 'display-name-only');
|
||||
|
||||
// Add the new display mode class
|
||||
if (this.displayMode === 'image') {
|
||||
appList.classList.add('display-image-only');
|
||||
} else if (this.displayMode === 'name') {
|
||||
appList.classList.add('display-name-only');
|
||||
} else {
|
||||
appList.classList.add('display-both');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user