Enhanced Tag Column with Multi-Tag Selection and Dynamic Filtering
Summary
Implemented a comprehensive enhancement to the TagColumn component, enabling multi-tag selection with "OR" logic filtering, dynamic card sorting based on tag match count, and an improved user interface with sorting controls.
Why Care
This enhancement significantly improves the toolkit browsing experience by allowing users to filter content with multiple tags simultaneously. The intuitive tag selection mechanism, combined with dynamic card sorting based on relevance, makes finding specific tools much faster and more efficient, especially for users with large collections of tools.
Implementation
Changes Made
/Users/mpstaton/code/lossless-monorepo/site/src/components/tool-components/TagColumn.astro
- Completely redesigned the component with a more sophisticated architecture
- Added support for multiple tag selection with URL parameter tracking
- Implemented dynamic card sorting based on tag match count
- Added sorting controls (alphabetical and frequency)
- Enhanced search functionality with improved UI
- Added responsive design for mobile devices
- Implemented accessibility improvements
Technical Details
Multi-Tag Selection with URL Parameter Tracking
typescript
// Function to toggle tag selection
function toggleTagSelection(tag: string) {
if (selectedTags.includes(tag)) {
// Remove tag if already selected
selectedTags = selectedTags.filter(t => t !== tag);
} else {
// Add tag if not selected
selectedTags.push(tag);
}
// Update URL to reflect selected tags
updateURL();
// Update the UI to reflect selected tags
updateTagSelectionUI();
// Filter content based on selected tags
filterContent(selectedTags);
// Reorder tags to show selected tags at the top
reorderTags();
}
// Function to update the URL with selected tags
function updateURL() {
const newUrl = new URL(window.location.href);
if (selectedTags.length > 0) {
newUrl.searchParams.set('tags', selectedTags.join(','));
} else {
newUrl.searchParams.delete('tags');
}
history.pushState({}, '', newUrl);
}
Dynamic Card Sorting Based on Tag Match Count
typescript
// Function to filter content based on selected tags
function filterContent(selectedTags: string[]) {
// Get all tool cards
const toolCards = document.querySelectorAll('.tool-card');
// If no tags selected, show all cards
if (selectedTags.length === 0) {
toolCards.forEach(card => {
(card as HTMLElement).style.display = '';
});
return;
}
// Create an array to track cards and their match counts
const cardMatches: {card: HTMLElement, matchCount: number}[] = [];
// Filter cards based on selected tags
toolCards.forEach(card => {
// Get the card's tags
const cardTagsStr = (card as HTMLElement).dataset.tags;
if (!cardTagsStr) return;
const cardTags = JSON.parse(cardTagsStr);
// Count how many selected tags match this card's tags
const matchCount = selectedTags.filter(tag => cardTags.includes(tag)).length;
// If the card has at least one matching tag, add it to our array with its match count
if (matchCount > 0) {
cardMatches.push({
card: card as HTMLElement,
matchCount
});
} else {
// Hide cards with no matches
(card as HTMLElement).style.display = 'none';
}
});
// Sort cards by match count (descending)
cardMatches.sort((a, b) => b.matchCount - a.matchCount);
// Get the parent container of the cards
const cardContainer = toolCards[0]?.parentElement;
if (!cardContainer) return;
// Remove all cards from the DOM
toolCards.forEach(card => card.remove());
// Add cards back in the new sorted order
cardMatches.forEach(({card, matchCount}) => {
// Show the card
card.style.display = '';
// Add a data attribute showing the match count
card.setAttribute('data-match-count', matchCount.toString());
// Add the card back to the container
cardContainer.appendChild(card);
});
}
Tag Sorting Controls
typescript
// Sort function
function getSortedTags(tags: string[], sortType: string, selectedTags: string[] = []): string[] {
// First sort by selection status (selected tags first)
return [...tags].sort((a, b) => {
const aSelected = selectedTags.includes(a);
const bSelected = selectedTags.includes(b);
// Prioritize selected tags
if (aSelected && !bSelected) return -1;
if (!aSelected && bSelected) return 1;
// If both tags have the same selection status, sort by the chosen criteria
if (sortType === 'frequency-desc') return tagFrequencies[b] - tagFrequencies[a];
if (sortType === 'frequency-asc') return tagFrequencies[a] - tagFrequencies[b];
if (sortType === 'alpha-asc') return a.localeCompare(b);
if (sortType === 'alpha-desc') return b.localeCompare(a);
return 0; // Default fallback
});
}
Improved UI with Accessibility Features
html
<!-- Tag search input with improved accessibility -->
<form class="tag-search-form" role="search">
<label for="tag-search" class="visually-hidden">Search tags</label>
<div class="search-input-wrapper">
<input
type="search"
id="tag-search"
name="tag-search"
placeholder="Search tags..."
list="tag-options"
autocomplete="off"
/>
<!-- Left side search icon -->
<IconListSearch class="search-icon" />
<!-- Right side filter icon - made interactive -->
<button type="button" class="filter-button" aria-label="Show tag options" title="Show tag options">
<IconFilterDown class="filter-icon" />
</button>
</div>
<datalist id="tag-options">
{initialSortedTags.map(tag => (
<option value={tag} />
))}
</datalist>
</form>
Integration Points
- The TagColumn component integrates with the ToolCard components through data attributes
- The component uses URL parameters to maintain state across page loads
- The tag filtering functionality works in conjunction with the CardGrid component
- The component respects the site's design system with consistent styling
Documentation
- The implementation follows the project's component architecture guidelines
- The code includes comprehensive comments explaining the functionality
- The component is fully responsive and works on mobile devices
- Accessibility features include:
- Proper ARIA attributes for interactive elements
- Keyboard navigation support
- Visually hidden labels for screen readers
- Proper color contrast for text elements
- Focus states for interactive elements