GitHub Location Filter

Makes GitHub job listing tables user-friendly by adding location-based filtering with customizable city dropdown - Perfect for browsing careers pages

// ==UserScript==
// @name         GitHub Location Filter
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  Makes GitHub job listing tables user-friendly by adding location-based filtering with customizable city dropdown - Perfect for browsing careers pages
// @author       sacrosaunt
// @match        https://github.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    'use strict';

    // Default target cities
    let targetCities = ['remote'];
    
    // Track filtered state
    let isFiltered = false;
    let originalRows = new Map();

    // Drag state variables
    let isDragging = false;
    let dragStarted = false;
    let dragOffset = { x: 0, y: 0 };
    let currentPosition = { x: 0, y: 0 };
    let ensureBoundsTimeoutId = null;
    let dragRectWidth = null;
    let dragRectHeight = null;

    // Storage functions
    function loadCitiesFromStorage() {
        try {
            const storedCities = GM_getValue('locationFilterCities', null);
            if (storedCities) {
                const cities = JSON.parse(storedCities);
                if (Array.isArray(cities) && cities.length > 0) {
                    targetCities = cities;
                }
            }
        } catch (error) {
            console.warn('Failed to load cities from storage:', error);
        }
    }

    function saveCitiesToStorage() {
        try {
            GM_setValue('locationFilterCities', JSON.stringify(targetCities));
        } catch (error) {
            console.warn('Failed to save cities to storage:', error);
        }
    }

    function loadFilterStateFromStorage() {
        try {
            const storedState = GM_getValue('locationFilterEnabled', false);
            return storedState;
        } catch (error) {
            console.warn('Failed to load filter state from storage:', error);
            return false;
        }
    }

    function saveFilterStateToStorage(enabled) {
        try {
            GM_setValue('locationFilterEnabled', enabled);
        } catch (error) {
            console.warn('Failed to save filter state to storage:', error);
        }
    }

    // Create and inject the dropdown interface
    function createDropdownInterface() {
        // Remove existing interface if present
        const existingInterface = document.getElementById('location-filter-interface');
        if (existingInterface) {
            existingInterface.remove();
        }

        // Create main container
        const container = document.createElement('div');
        container.id = 'location-filter-interface';
        container.innerHTML = `
            <div class="filter-header" id="filter-header">
                <span class="filter-title">Location Filter</span>
                <div class="header-controls">
                    <button class="toggle-filter-btn" id="toggle-filter-btn">OFF</button>
                </div>
            </div>
            <div class="filter-content collapsed" id="filter-content">
                <div class="city-input-section">
                    <label for="city-input">Add City:</label>
                    <div class="input-group">
                        <input type="text" id="city-input" placeholder="Enter city name" />
                        <button id="add-city-btn">Add</button>
                    </div>
                </div>
                <div class="cities-section">
                    <label>Selected Cities:</label>
                    <div class="cities-list" id="cities-list"></div>
                </div>
            </div>
        `;

        // Add styles with dark mode support
        const styles = `
            #location-filter-interface {
                position: fixed;
                top: 100px;
                right: 20px;
                width: 240px;
                background: #1976d2;
                border: 2px solid #1976d2;
                border-radius: 8px;
                box-shadow: 0 4px 12px rgba(0,0,0,0.15);
                z-index: 10000;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                font-size: 14px;
                transition: none;
                overflow: hidden;
                scroll-behavior: auto;
                transform-origin: right top;
                user-select: none;
                -webkit-user-select: none;
                -moz-user-select: none;
                -ms-user-select: none;
            }

            #location-filter-interface.dragging {
                transition: none;
                box-shadow: 0 8px 20px rgba(0,0,0,0.3);
            }

            #location-filter-interface.repositioning {
                transition: left 0.3s ease, top 0.3s ease;
            }

            /* Ensure dragging always disables transitions, even if repositioning is active */
            #location-filter-interface.repositioning.dragging {
                transition: none;
            }

            #location-filter-interface:not(.collapsed) {
                width: 240px;
            }

            #location-filter-interface.active {
                background: #4caf50;
                border-color: #4caf50;
            }

            .filter-header {
                background: #1976d2;
                color: white;
                padding: 8px 12px;
                display: flex;
                justify-content: space-between;
                align-items: center;
                cursor: move;
                transition: background-color 0.3s ease;
                gap: 4px;
                margin: -2px -2px 0 -2px;
                border-radius: 8px 8px 0 0;
                position: relative;
            }

            .filter-header:active {
                cursor: grabbing;
            }

            .filter-header.dragging {
                cursor: grabbing;
                background: #1565c0;
            }

            .filter-header.active {
                background: #4caf50;
            }

            .filter-title {
                font-weight: bold;
                font-size: 13px;
            }

            .header-controls {
                display: flex;
                align-items: center;
                gap: 8px;
            }

            .toggle-filter-btn {
                background: rgba(255,255,255,0.2);
                border: 1px solid rgba(255,255,255,0.3);
                color: white;
                cursor: pointer;
                font-size: 11px;
                padding: 3px 8px;
                border-radius: 12px;
                font-weight: 500;
                transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
                min-width: 35px;
                user-select: none;
                -webkit-user-select: none;
                -moz-user-select: none;
                -ms-user-select: none;
            }

            .toggle-filter-btn:hover {
                background: rgba(255,255,255,0.3);
            }

            .toggle-filter-btn.active {
                background: rgba(255,255,255,0.9);
                color: #4caf50;
            }

            .filter-content {
                padding: 0 15px;
                background: white;
                border-radius: 0 0 6px 6px;
                max-height: 0;
                opacity: 0;
                overflow: hidden;
                transition: max-height 0.3s ease, opacity 0.2s ease, padding 0.3s ease;
            }

            .filter-content:not(.collapsed) {
                padding: 15px;
                max-height: 1000px; /* sufficient for content */
                opacity: 1;
            }

            .city-input-section {
                margin-bottom: 15px;
            }

            .city-input-section label {
                display: block;
                margin-bottom: 5px;
                font-weight: 500;
                color: #333;
            }

            .input-group {
                display: flex;
                gap: 5px;
                width: 100%;
            }

            #city-input {
                flex: 1 1 auto;
                width: auto;
                min-width: 0;
                padding: 6px 8px;
                border: 1px solid #ddd;
                border-radius: 4px;
                font-size: 13px;
                background: white;
                color: #333;
            }

            #add-city-btn {
                background: #4caf50;
                color: white;
                border: none;
                padding: 6px 12px;
                border-radius: 4px;
                cursor: pointer;
                font-size: 13px;
                font-weight: 500;
                white-space: nowrap;
            }

            #add-city-btn:hover {
                background: #45a049;
            }

            .cities-section {
                margin-bottom: 15px;
            }

            .cities-section label {
                display: block;
                margin-bottom: 8px;
                font-weight: 500;
                color: #333;
            }

            .cities-list {
                display: flex;
                flex-wrap: wrap;
                gap: 5px;
                min-height: 30px;
                padding: 8px;
                border: 1px solid #eee;
                border-radius: 4px;
                background: #f9f9f9;
            }

            .city-tag {
                background: #e3f2fd;
                color: #1976d2;
                padding: 4px 8px;
                border-radius: 12px;
                font-size: 12px;
                font-weight: 500;
                display: flex;
                align-items: center;
                gap: 5px;
            }

            .city-remove {
                background: none;
                border: none;
                color: #1976d2;
                cursor: pointer;
                font-size: 14px;
                padding: 0;
                width: 16px;
                height: 16px;
                border-radius: 50%;
                display: flex;
                align-items: center;
                justify-content: center;
            }

            .city-remove:hover {
                background: #1976d2;
                color: white;
            }

            .cities-list:empty::after {
                content: "No cities added";
                color: #999;
                font-style: italic;
                font-size: 12px;
            }

            /* Responsive width adjustments */
            @media (max-width: 768px) {
                #location-filter-interface {
                    width: 220px;
                }
                #location-filter-interface:not(.collapsed) {
                    width: 220px;
                }
                #city-input {
                    flex: 1 1 auto;
                    width: auto;
                    min-width: 0;
                }
            }

            @media (max-width: 480px) {
                #location-filter-interface {
                    width: 200px;
                }
                #location-filter-interface:not(.collapsed) {
                    width: 200px;
                }
                #city-input {
                    flex: 1 1 auto;
                    width: auto;
                    min-width: 0;
                }
            }

            /* Dark mode styles */
            @media (prefers-color-scheme: dark) {
                #location-filter-interface {
                    box-shadow: 0 4px 12px rgba(0,0,0,0.4);
                }

                .filter-content {
                    background: #2d2d2d;
                    color: #e0e0e0;
                }

                .city-input-section label,
                .cities-section label {
                    color: #e0e0e0;
                }

                #city-input {
                    background: #404040;
                    border: 1px solid #555;
                    color: #e0e0e0;
                }

                #city-input::placeholder {
                    color: #999;
                }

                #city-input:focus {
                    outline: none;
                    border-color: #1976d2;
                    box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.2);
                }

                .cities-list {
                    background: #404040;
                    border: 1px solid #555;
                }

                .city-tag {
                    background: #1e3a5f;
                    color: #64b5f6;
                }

                .city-remove {
                    color: #64b5f6;
                }

                .city-remove:hover {
                    background: #64b5f6;
                    color: #1e3a5f;
                }

                .cities-list:empty::after {
                    color: #888;
                }
            }
        `;

        // Inject styles
        const styleSheet = document.createElement('style');
        styleSheet.textContent = styles;
        document.head.appendChild(styleSheet);

        // Add to page
        document.body.appendChild(container);

        // Set up event listeners
        setupEventListeners();
        
        // Initialize cities display
        updateCitiesDisplay();
        
        // Start in collapsed state
        container.classList.add('collapsed');
    }

    // Drag functionality
    function constrainPosition(x, y, forceCurrentWidth = false) {
        const container = document.getElementById('location-filter-interface');
        if (!container) return { x, y };
        
        const viewportWidth = window.innerWidth;
        const viewportHeight = window.innerHeight;
        
        // Get current dimensions, accounting for expanded/collapsed state
        let width, height;
        if (forceCurrentWidth) {
            if (dragRectWidth !== null && dragRectHeight !== null) {
                width = dragRectWidth;
                height = dragRectHeight;
            } else {
                const rect = container.getBoundingClientRect();
                width = rect.width;
                height = rect.height;
            }
        } else {
            // Use fixed width by viewport to keep width constant across states
            if (viewportWidth <= 480) {
                width = 200;
            } else if (viewportWidth <= 768) {
                width = 220;
            } else {
                width = 240;
            }
            
            const rect = container.getBoundingClientRect();
            height = rect.height;
        }
        
        // Constrain to 15px from edges
        const minX = 15;
        const maxX = viewportWidth - width - 15;
        const minY = 15;
        const maxY = viewportHeight - height - 15;
        
        return {
            x: Math.max(minX, Math.min(maxX, x)),
            y: Math.max(minY, Math.min(maxY, y))
        };
    }

    function updatePosition(x, y, animate = false) {
        const container = document.getElementById('location-filter-interface');
        if (!container) return;
        
        const constrained = constrainPosition(x, y);
        currentPosition.x = constrained.x;
        currentPosition.y = constrained.y;
        
        // Add animation class if needed
        if (animate) {
            container.classList.add('repositioning');
            // Remove animation class after transition
            setTimeout(() => {
                container.classList.remove('repositioning');
            }, 400);
        }
        
        // Use left positioning for manual dragging
        container.style.left = constrained.x + 'px';
        container.style.top = constrained.y + 'px';
        container.style.right = 'auto'; // Remove right positioning
    }

    function animatedRepositionToConstraints(x, y) {
        if (isDragging) {
            updatePosition(x, y, false);
            return;
        }
        updatePosition(x, y, true);
    }

    function setInitialPosition() {
        const container = document.getElementById('location-filter-interface');
        if (!container) return;
        
        // Calculate initial position for right-aligned interface
        const viewportWidth = window.innerWidth;
        const rect = container.getBoundingClientRect();
        
        // Find the GitHub header
        const header = document.querySelector('.AppHeader');
        let topOffset = 120; // Default fallback
        
        if (header) {
            topOffset = header.offsetHeight + 20; // 20px padding below header
        }
        
        // Position in top-right corner with responsive margins
        let rightMargin = 20;
        if (viewportWidth <= 768) {
            rightMargin = 15;
        }
        if (viewportWidth <= 480) {
            rightMargin = 10;
        }
        
        // Keep using CSS right positioning for initial state
        container.style.right = rightMargin + 'px';
        container.style.top = Math.max(topOffset, 15) + 'px';
        container.style.left = 'auto';
        
        // Don't set currentPosition yet - let it remain 0,0 for initial state
    }

    function setupDragFunctionality() {
        const header = document.getElementById('filter-header');
        const container = document.getElementById('location-filter-interface');
        
        if (!header || !container) return;
        
        header.addEventListener('mousedown', (e) => {
            // Only start drag on left mouse button
            if (e.button !== 0) return;
            
            // Don't drag if clicking on the toggle button
            if (e.target.id === 'toggle-filter-btn' || e.target.closest('.toggle-filter-btn')) {
                return;
            }
            
            isDragging = true;
            dragStarted = false;
            
            const rect = container.getBoundingClientRect();
            dragOffset.x = e.clientX - rect.left;
            dragOffset.y = e.clientY - rect.top;
            dragRectWidth = rect.width;
            dragRectHeight = rect.height;
            
            // Set initial position if not already set
            if (currentPosition.x === 0 && currentPosition.y === 0) {
                currentPosition.x = rect.left;
                currentPosition.y = rect.top;
            }
            
            header.classList.add('dragging');
            container.classList.add('dragging');
            container.classList.remove('repositioning');

            if (ensureBoundsTimeoutId) {
                clearTimeout(ensureBoundsTimeoutId);
                ensureBoundsTimeoutId = null;
            }
            
            e.preventDefault();
        });
        
        document.addEventListener('mousemove', (e) => {
            if (!isDragging) return;
            
            dragStarted = true;
            
            const newX = e.clientX - dragOffset.x;
            const newY = e.clientY - dragOffset.y;
            
            // Use current width during drag to ensure proper constraints
            const constrained = constrainPosition(newX, newY, true);
            currentPosition.x = constrained.x;
            currentPosition.y = constrained.y;
            
            const container = document.getElementById('location-filter-interface');
            if (container) {
                container.style.left = constrained.x + 'px';
                container.style.top = constrained.y + 'px';
                container.style.right = 'auto';
            }
            
            e.preventDefault();
        });
        
        document.addEventListener('mouseup', (e) => {
            if (!isDragging) return;
            
            isDragging = false;
            header.classList.remove('dragging');
            container.classList.remove('dragging');
            dragRectWidth = null;
            dragRectHeight = null;
            
            // Prevent click event if we actually dragged
            if (dragStarted) {
                setTimeout(() => {
                    dragStarted = false;
                }, 10);
            }
        });
        
        // Handle window resize to reposition if needed (throttled)
        window.addEventListener('resize', throttle(() => {
            if (isDragging) return;
            if (currentPosition.x !== 0 || currentPosition.y !== 0) {
                animatedRepositionToConstraints(currentPosition.x, currentPosition.y);
            } else {
                // If no manual position set, reposition automatically
                adjustFilterPosition();
            }
        }, 100));
    }

    function setupEventListeners() {
        // Toggle dropdown by clicking header
        const header = document.getElementById('filter-header');
        const content = document.getElementById('filter-content');
        const container = document.getElementById('location-filter-interface');
        
        // Setup drag functionality
        setupDragFunctionality();
        
        header.addEventListener('click', (e) => {
            // Don't toggle if clicking the toggle filter button or if we just finished dragging
            if (e.target.id === 'toggle-filter-btn' || dragStarted) {
                return;
            }
            
            content.classList.toggle('collapsed');
            container.classList.toggle('collapsed', content.classList.contains('collapsed'));
            
            // Ensure interface stays within bounds after expansion/collapse
            if (ensureBoundsTimeoutId) {
                clearTimeout(ensureBoundsTimeoutId);
            }
            ensureBoundsTimeoutId = setTimeout(() => {
                ensureBoundsTimeoutId = null;
                ensureWithinBounds();
            }, 0); // Run on next tick so layout updates are applied, no extra delay
        });

        // After expand/collapse animation completes, re-ensure bounds in case height changed further
        content.addEventListener('transitionend', (e) => {
            if (e.target !== content) return;
            if (e.propertyName === 'max-height' || e.propertyName === 'opacity' || e.propertyName === 'padding') {
                ensureWithinBounds();
            }
        });

        // Toggle filter on/off
        const toggleFilterBtn = document.getElementById('toggle-filter-btn');
        toggleFilterBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            if (isFiltered) {
                clearFilter();
            } else {
                applyFilter(true); // Show alert if no cities when manually toggling on
            }
        });

        // Add city
        const addBtn = document.getElementById('add-city-btn');
        const cityInput = document.getElementById('city-input');
        
        addBtn.addEventListener('click', addCity);
        cityInput.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') {
                addCity();
            }
        });
    }

    function addCity() {
        const cityInput = document.getElementById('city-input');
        const cityName = cityInput.value.trim().toLowerCase();
        
        if (cityName && !targetCities.includes(cityName)) {
            targetCities.push(cityName);
            cityInput.value = '';
            saveCitiesToStorage();
            updateCitiesDisplay();
            
            // Automatically apply filter if there are tables on the page
            const tables = document.querySelectorAll('table');
            if (tables.length > 0) {
                applyFilter();
            }
        }
    }

    function removeCity(cityName) {
        targetCities = targetCities.filter(city => city !== cityName);
        saveCitiesToStorage();
        updateCitiesDisplay();
        
        // If filter is active, reapply it
        if (isFiltered) {
            applyFilter();
        }
    }

    function updateCitiesDisplay() {
        const citiesList = document.getElementById('cities-list');
        citiesList.innerHTML = '';
        
        targetCities.forEach(city => {
            const cityTag = document.createElement('div');
            cityTag.className = 'city-tag';
            cityTag.innerHTML = `
                ${city}
                <button class="city-remove" title="Remove city">×</button>
            `;
            
            // Add event listener to the remove button
            const removeBtn = cityTag.querySelector('.city-remove');
            removeBtn.addEventListener('click', () => removeCity(city));
            
            citiesList.appendChild(cityTag);
        });
    }

    function findLocationColumnIndex(table) {
        const headerRow = table.querySelector('thead tr, tr:first-child');
        if (!headerRow) return -1;

        const headers = headerRow.querySelectorAll('th, td');
        for (let i = 0; i < headers.length; i++) {
            const headerText = headers[i].textContent.toLowerCase().trim();
            if (headerText.includes('location') || headerText.includes('city') || headerText.includes('office')) {
                return i;
            }
        }
        return -1;
    }

    function containsTargetCity(locationCell) {
        // Get all text content including from details/summary elements
        let text = '';
        
        // If it's a DOM element, extract all text including hidden content
        if (locationCell && locationCell.textContent !== undefined) {
            text = locationCell.textContent;
        } else {
            // Fallback for string input
            text = locationCell || '';
        }
        
        // Clean up the text - remove extra whitespace and normalize
        const textLower = text.toLowerCase().replace(/\s+/g, ' ').trim();
        
        return targetCities.some(city => {
            const cityLower = city.toLowerCase().trim();
            
            // Try multiple matching strategies
            // 1. Exact word boundary match
            const wordBoundaryRegex = new RegExp(`\\b${cityLower.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
            if (wordBoundaryRegex.test(textLower)) {
                return true;
            }
            
            // 2. Simple substring match (for cases where word boundaries might not work)
            if (textLower.includes(cityLower)) {
                return true;
            }
            
            // 3. Match with common separators (comma, space, etc.)
            const separatorRegex = new RegExp(`(^|[,\\s])${cityLower.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([,\\s]|$)`, 'i');
            if (separatorRegex.test(textLower)) {
                return true;
            }
            
            return false;
        });
    }

    function filterTable(table) {
        const locationColumnIndex = findLocationColumnIndex(table);
        if (locationColumnIndex === -1) return;

        const tbody = table.querySelector('tbody');
        const rows = tbody ? tbody.querySelectorAll('tr') : table.querySelectorAll('tr:not(:first-child)');
        
        // Store original rows if not already stored
        if (!originalRows.has(table)) {
            originalRows.set(table, Array.from(rows).map(row => ({
                element: row,
                display: row.style.display
            })));
        }

        rows.forEach(row => {
            const cells = row.querySelectorAll('td, th');
            if (cells.length > locationColumnIndex) {
                const locationCell = cells[locationColumnIndex];
                
                if (!containsTargetCity(locationCell)) {
                    row.style.display = 'none';
                    row.classList.add('location-filtered');
                } else {
                    row.style.display = '';
                    row.classList.remove('location-filtered');
                }
            }
        });
    }

    function restoreTable(table) {
        const storedRows = originalRows.get(table);
        if (!storedRows) return;

        storedRows.forEach(rowData => {
            rowData.element.style.display = rowData.display;
            rowData.element.classList.remove('location-filtered');
        });
    }

    function applyFilter(showAlertIfNoCities = false) {
        if (targetCities.length === 0) {
            if (showAlertIfNoCities) {
                alert('Please add at least one city to filter by.');
            }
            clearFilter();
            return;
        }

        const tables = document.querySelectorAll('table');
        tables.forEach(table => filterTable(table));
        
        isFiltered = true;
        saveFilterStateToStorage(true);
        updateFilterStatus();
    }

    function clearFilter() {
        const tables = document.querySelectorAll('table');
        tables.forEach(table => restoreTable(table));
        
        isFiltered = false;
        saveFilterStateToStorage(false);
        updateFilterStatus();
    }

    function updateFilterStatus() {
        const header = document.getElementById('filter-header');
        const toggleBtn = document.getElementById('toggle-filter-btn');
        const container = document.getElementById('location-filter-interface');
        
        if (isFiltered) {
            header.classList.add('active');
            toggleBtn.classList.add('active');
            container.classList.add('active');
            toggleBtn.textContent = 'ON';
        } else {
            header.classList.remove('active');
            toggleBtn.classList.remove('active');
            container.classList.remove('active');
            toggleBtn.textContent = 'OFF';
        }
    }

    function applyFilterToNewTables() {
        if (isFiltered) {
            const tables = document.querySelectorAll('table');
            tables.forEach(table => {
                if (!originalRows.has(table)) {
                    filterTable(table);
                }
            });
        }
    }

    // Observer for dynamically added tables
    const observer = new MutationObserver((mutations) => {
        let hasNewTables = false;
        mutations.forEach((mutation) => {
            mutation.addedNodes.forEach((node) => {
                if (node.nodeType === Node.ELEMENT_NODE) {
                    if (node.tagName === 'TABLE' || node.querySelector('table')) {
                        hasNewTables = true;
                    }
                }
            });
        });
        
        if (hasNewTables) {
            // Check if interface should be shown when new tables are added
            const existingInterface = document.getElementById('location-filter-interface');
            if (!existingInterface && hasLocationColumns()) {
                // Location columns found and no interface exists, create it
                loadCitiesFromStorage();
                const savedFilterState = loadFilterStateFromStorage();
                createDropdownInterface();
                
                if (savedFilterState && targetCities.length > 0) {
                    setTimeout(() => {
                        isFiltered = savedFilterState;
                        if (isFiltered) {
                            const tables = document.querySelectorAll('table');
                            tables.forEach(table => filterTable(table));
                        }
                        updateFilterStatus();
                    }, 100);
                }
            }
            
            applyFilterToNewTables();
        }
    });


    // Check if any tables have location columns
    function hasLocationColumns() {
        const tables = document.querySelectorAll('table');
        for (let table of tables) {
            if (findLocationColumnIndex(table) !== -1) {
                return true;
            }
        }
        return false;
    }

    // Throttle function for performance
    function throttle(func, limit) {
        let inThrottle;
        return function() {
            const args = arguments;
            const context = this;
            if (!inThrottle) {
                func.apply(context, args);
                inThrottle = true;
                setTimeout(() => inThrottle = false, limit);
            }
        }
    }

    // Function to adjust filter position based on header height and responsiveness
    function adjustFilterPosition() {
        const filterInterface = document.getElementById('location-filter-interface');
        if (!filterInterface) return;
        
        // If the element has been manually positioned by dragging, reapply constraints
        if (currentPosition.x !== 0 || currentPosition.y !== 0) {
            updatePosition(currentPosition.x, currentPosition.y);
            return;
        }
        
        // For initial positioning, use CSS right positioning
        setInitialPosition();
    }

    // Function to ensure interface stays within bounds when expanding/collapsing
    function ensureWithinBounds() {
        const filterInterface = document.getElementById('location-filter-interface');
        if (!filterInterface) return;
        if (isDragging) return;
        
        // If already manually positioned (dragged), reapply constraints with current state and animation
        if (currentPosition.x !== 0 || currentPosition.y !== 0) {
            animatedRepositionToConstraints(currentPosition.x, currentPosition.y);
            return;
        }
        
        // For right-positioned interface, check if it goes off screen when expanded
        const rect = filterInterface.getBoundingClientRect();
        const viewportWidth = window.innerWidth;
        const viewportHeight = window.innerHeight;
        
        let targetX = rect.left;
        let targetY = rect.top;
        let needsReposition = false;
        
        // Horizontal bounds
        if (rect.left < 15) {
            targetX = 15;
            needsReposition = true;
        } else if (rect.right > viewportWidth - 15) {
            targetX = viewportWidth - rect.width - 15;
            needsReposition = true;
        }
        
        // Vertical bounds
        if (rect.top < 15) {
            targetY = 15;
            needsReposition = true;
        } else if (rect.bottom > viewportHeight - 15) {
            targetY = Math.max(15, viewportHeight - rect.height - 15);
            needsReposition = true;
        }
        
        if (needsReposition) {
            // Convert to left/top baseline at current visual position to allow smooth transition
            filterInterface.style.left = rect.left + 'px';
            filterInterface.style.top = rect.top + 'px';
            filterInterface.style.right = 'auto';
            
            animatedRepositionToConstraints(targetX, targetY);
        }
    }

    // Initialize when DOM is ready
    function initialize() {
        // Check if there are any tables with location columns
        if (!hasLocationColumns()) {
            // No location columns found, don't display the interface
            return;
        }
        
        // Load cities from storage first
        loadCitiesFromStorage();
        
        // Load filter state from storage
        const savedFilterState = loadFilterStateFromStorage();
        
        createDropdownInterface();
        
        // Set initial position after creating interface
        setTimeout(setInitialPosition, 100);
        
        // Restore filter state if it was previously enabled
        if (savedFilterState && targetCities.length > 0) {
            // Wait a bit for tables to load, then apply filter
            setTimeout(() => {
                isFiltered = savedFilterState;
                if (isFiltered) {
                    const tables = document.querySelectorAll('table');
                    tables.forEach(table => filterTable(table));
                }
                updateFilterStatus();
            }, 100);
        }
        
        // Start observing for new tables
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
        
        // Listen for window resize to adjust position (throttled)
        window.addEventListener('resize', throttle(() => {
            if (isDragging) return;
            if (currentPosition.x !== 0 || currentPosition.y !== 0) {
                animatedRepositionToConstraints(currentPosition.x, currentPosition.y);
            } else {
                adjustFilterPosition();
            }
        }, 100));
        
        // Listen for scroll to ensure filter stays visible (throttled)
        window.addEventListener('scroll', throttle(adjustFilterPosition, 100));
        
        // Listen for navigation changes (GitHub uses pjax)
        document.addEventListener('pjax:end', adjustFilterPosition);
        
        // Also adjust position periodically to catch any dynamic changes
        setInterval(adjustFilterPosition, 2000);
    }

    // Initialize when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize();
    }

})();