Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 2 additions & 127 deletions src/modules/mobile-filter-logic.ts
Original file line number Diff line number Diff line change
@@ -1,127 +1,2 @@
// ========================================
// Mobile Filter Logic Module
// State synchronization, filter counting, and badge updates
// ========================================

import { getState } from './store.ts';
import { safeGetElementById, safeQuerySelector } from './utils.ts';
import { saveFilterState } from './filter-state.ts';
import type { FilterConfig } from './mobile-filter-sheet.ts';

type FilterInputElement = HTMLInputElement | HTMLSelectElement;

/**
* Sync filter values from main filters to bottom sheet
*/
export function syncFiltersToSheet(): void {
const sheet = safeGetElementById('filter-bottom-sheet');
if (!sheet) return;

const sheetInputs = sheet.querySelectorAll<FilterInputElement>('[data-filter-id]');

sheetInputs.forEach(sheetInput => {
const filterId = sheetInput.dataset.filterId;
if (!filterId) return;

const mainInput = safeGetElementById(filterId) as FilterInputElement | null;

if (mainInput) {
if (sheetInput instanceof HTMLInputElement && sheetInput.type === 'checkbox') {
sheetInput.checked = (mainInput as HTMLInputElement).checked;
} else if (sheetInput instanceof HTMLSelectElement && mainInput instanceof HTMLSelectElement) {
sheetInput.value = mainInput.value;
}
}
});
}

/**
* Apply filter values from bottom sheet to main filters
*/
export function applyFiltersFromSheet(): void {
const sheet = safeGetElementById('filter-bottom-sheet');
if (!sheet) return;

const sheetInputs = sheet.querySelectorAll<FilterInputElement>('[data-filter-id]');

sheetInputs.forEach(sheetInput => {
const filterId = sheetInput.dataset.filterId;
if (!filterId) return;

const mainInput = safeGetElementById(filterId) as FilterInputElement | null;

if (mainInput) {
if (sheetInput instanceof HTMLInputElement && sheetInput.type === 'checkbox') {
(mainInput as HTMLInputElement).checked = sheetInput.checked;
} else if (sheetInput instanceof HTMLSelectElement && mainInput instanceof HTMLSelectElement) {
mainInput.value = sheetInput.value;
}

mainInput.dispatchEvent(new Event('change', { bubbles: true }));
}
});

const currentTab = getState('currentTab');
if (currentTab) {
saveFilterState(currentTab);
}
}

/**
* Clear all filters in the bottom sheet
*/
export function clearSheetFilters(): void {
const sheet = safeGetElementById('filter-bottom-sheet');
if (!sheet) return;

const sheetInputs = sheet.querySelectorAll<FilterInputElement>('[data-filter-id]');

sheetInputs.forEach(sheetInput => {
if (sheetInput instanceof HTMLInputElement && sheetInput.type === 'checkbox') {
sheetInput.checked = false;
} else if (sheetInput instanceof HTMLSelectElement) {
sheetInput.value = 'all';
}
});
}

/**
* Count active filters
*/
export function countActiveFilters(tabFilters: Record<string, FilterConfig[]>): number {
let count = 0;
const currentTab = getState('currentTab');
const filters = tabFilters[currentTab || ''] || [];

filters.forEach(filter => {
const input = safeGetElementById(filter.id) as FilterInputElement | null;
if (!input) return;

if (input instanceof HTMLInputElement && input.type === 'checkbox') {
if (input.checked) count++;
} else if (input instanceof HTMLSelectElement) {
if (input.value !== 'all' && input.value !== 'name' && input.value !== 'date_desc') {
count++;
}
}
});

return count;
}

/**
* Update filter button badge
*/
export function updateFilterBadge(tabFilters: Record<string, FilterConfig[]>): void {
const btn = safeQuerySelector('.mobile-filter-btn') as HTMLElement | null;
if (!btn) return;

const count = countActiveFilters(tabFilters);
const badge = btn.querySelector<HTMLElement>('.filter-badge');

if (badge) {
badge.textContent = count.toString();
}

btn.classList.toggle('has-filters', count > 0);
}
// SHIM — module moved to mobile/mobile-filter-logic.ts (remove after all imports updated)
export * from './mobile/mobile-filter-logic.ts';
157 changes: 3 additions & 154 deletions src/modules/mobile-filter-sheet.ts
Original file line number Diff line number Diff line change
@@ -1,154 +1,3 @@
// ========================================
// Mobile Filter Sheet DOM/UI Module
// Bottom sheet creation, rendering, and event listeners
// ========================================

import { safeGetElementById } from './utils.ts';

// ========================================
// Types
// ========================================

interface FilterConfig {
id: string;
label: string;
type: 'select' | 'checkbox';
options?: { value: string; label: string }[];
}

export type { FilterConfig };

/**
* Create the filter bottom sheet HTML
*/
export function createFilterSheet(_tabName: string, filters: FilterConfig[]): HTMLElement {
const sheet = document.createElement('div');
sheet.id = 'filter-bottom-sheet';
sheet.className = 'filter-bottom-sheet';
sheet.setAttribute('role', 'dialog');
sheet.setAttribute('aria-modal', 'true');
sheet.setAttribute('aria-label', 'Filter options');

sheet.innerHTML = `
<div class="filter-sheet-backdrop" aria-hidden="true"></div>
<div class="filter-sheet-drawer" role="document">
<div class="filter-sheet-handle" aria-hidden="true"></div>
<div class="filter-sheet-header">
<span class="filter-sheet-title" id="filter-sheet-title">Filters</span>
<div class="filter-sheet-actions">
<button class="filter-sheet-clear" type="button">Clear All</button>
<button class="filter-sheet-close" aria-label="Close filters" type="button">
<span aria-hidden="true">×</span>
</button>
</div>
</div>
<div class="filter-sheet-content" id="filter-sheet-content">
${renderFilterGroups(filters)}
</div>
<div class="filter-sheet-apply">
<button type="button" id="filter-sheet-apply-btn">Apply Filters</button>
</div>
</div>
`;

return sheet;
}

/**
* Render filter groups HTML
*/
export function renderFilterGroups(filters: FilterConfig[]): string {
return filters
.map(filter => {
if (filter.type === 'checkbox') {
return `
<div class="filter-group">
<label class="filter-group-checkbox">
<input type="checkbox" id="sheet-${filter.id}" data-filter-id="${filter.id}" />
<span class="checkbox-label">${filter.label}</span>
</label>
</div>
`;
} else {
return `
<div class="filter-group">
<label class="filter-group-label" for="sheet-${filter.id}">${filter.label}</label>
<select id="sheet-${filter.id}" data-filter-id="${filter.id}">
${filter.options?.map(opt => `<option value="${opt.value}">${opt.label}</option>`).join('')}
</select>
</div>
`;
}
})
.join('');
}

/**
* Setup event listeners for the sheet
*/
export function setupSheetEventListeners(
sheet: HTMLElement,
hideFilterSheet: () => void,
clearSheetFilters: () => void,
applyFiltersFromSheet: () => void,
updateFilterBadge: () => void
): void {
const backdrop = sheet.querySelector('.filter-sheet-backdrop');
backdrop?.addEventListener('click', hideFilterSheet);

const closeBtn = sheet.querySelector('.filter-sheet-close');
closeBtn?.addEventListener('click', hideFilterSheet);

const clearBtn = sheet.querySelector('.filter-sheet-clear');
clearBtn?.addEventListener('click', () => {
clearSheetFilters();
});

const applyBtn = sheet.querySelector('#filter-sheet-apply-btn');
applyBtn?.addEventListener('click', () => {
applyFiltersFromSheet();
hideFilterSheet();
updateFilterBadge();
});
}

/**
* Handle keyboard navigation within sheet
*/
export function handleKeyboardNavigation(e: KeyboardEvent, isSheetOpen: boolean, hideFilterSheet: () => void): void {
if (!isSheetOpen) return;

if (e.key === 'Escape') {
e.preventDefault();
hideFilterSheet();
}
}

/**
* Handle focus trapping
*/
export function handleFocusTrap(e: KeyboardEvent, isSheetOpen: boolean): void {
if (e.key !== 'Tab' || !isSheetOpen) return;

const sheet = safeGetElementById('filter-bottom-sheet');
if (!sheet) return;

const focusableElements = Array.from(
sheet.querySelectorAll<HTMLElement>('button:not([disabled]), select, input, [tabindex]:not([tabindex="-1"])')
).filter(el => el.offsetParent !== null);

if (focusableElements.length === 0) return;

const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];

if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement?.focus();
}
} else if (document.activeElement === lastElement) {
e.preventDefault();
firstElement?.focus();
}
}
// SHIM — module moved to mobile/mobile-filter-sheet.ts (remove after all imports updated)
export * from './mobile/mobile-filter-sheet.ts';
export type { FilterConfig } from './mobile/mobile-filter-sheet.ts';
Loading
Loading