ModularGrid Manufacturer Exception Filter

Add brand exclusion filtering to ModularGrid module browser

// ==UserScript==
// @name         ModularGrid Manufacturer Exception Filter
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Add brand exclusion filtering to ModularGrid module browser
// @author       Your name
// @match        https://modulargrid.net/e/modules/browser*
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    const SELECTORS = {
        MARKETPLACE: '#SearchMarketplace',
        VENDOR: '#SearchVendor',
        SUBMIT_BOX: '.submit-box',
        CHECKBOXES: '#form-search .grid.last-row .input.cb',
        MODULES: '.box-module',
        VENDOR_LINK: '.lnk-vendor',
        MODULE_COUNT: '.browser .count',
        RESET_BUTTON: '#btn-reset-search',
        BRAND_EXCLUSION: '.brand-exclusion'
    };

    const STYLES = {
        CHECKBOX_CONTAINER: {
            display: 'flex',
            gap: '20px',
            marginTop: '10px'
        },
        HIDDEN_SPAN: {
            visibility: 'hidden',
            position: 'absolute',
            whiteSpace: 'nowrap'
        },
        ADDITIONAL_FILTER: {
            marginBottom: '5px'
        },
        ADDITIONAL_WRAPPER: {
            marginTop: '5px'
        }
    };

    class ModuleFilter {
        constructor() {
            this.init = this.init.bind(this);
            this.applyAllFilters = this.applyAllFilters.bind(this);
            this.resetFilters = this.resetFilters.bind(this);
            
            this.init();
        }

        init() {
            const elements = this.getRequiredElements();
            if (!elements) return;

            const { marketplaceContainer, submitBox, checkboxes } = elements;
            
            const exclusionContainer = this.createMainContainer();
            const additionalFiltersWrapper = this.createAdditionalFiltersWrapper();
            const mainFilter = this.createMainFilter(additionalFiltersWrapper);
            
            exclusionContainer.appendChild(this.createLabel());
            exclusionContainer.appendChild(mainFilter);
            exclusionContainer.appendChild(additionalFiltersWrapper);

            this.insertFilter(exclusionContainer, marketplaceContainer);
            this.setupCheckboxes(checkboxes, submitBox);
            this.setupResetButton();
        }

        appendElements(parent, elements) {
            elements.forEach(element => {
                if (element) parent.appendChild(element);
            });
        }

        getRequiredElements() {
            const marketplaceSelect = document.querySelector(SELECTORS.MARKETPLACE);
            const marketplaceContainer = marketplaceSelect?.closest('.input.select');
            const submitBox = document.querySelector(SELECTORS.SUBMIT_BOX);
            const checkboxes = Array.from(document.querySelectorAll(SELECTORS.CHECKBOXES));

            if (!marketplaceContainer || !submitBox || !checkboxes.length) {
                console.warn('Required elements not found');
                return null;
            }

            return { marketplaceContainer, submitBox, checkboxes };
        }

        createMainContainer() {
            const container = document.createElement('div');
            container.className = 'input select';
            return container;
        }

        createLabel() {
            const label = document.createElement('label');
            label.textContent = 'Exclude Manufacturer';
            return label;
        }

        createAdditionalFiltersWrapper() {
            const wrapper = document.createElement('div');
            Object.assign(wrapper.style, STYLES.ADDITIONAL_WRAPPER);
            return wrapper;
        }

        createMainFilter(additionalFiltersWrapper) {
            const inputAppend = document.createElement('div');
            inputAppend.className = 'input input-append';

            const select = this.createDropdown();
            const addButton = this.createButton('+', () => this.addNewFilter(additionalFiltersWrapper));

            inputAppend.appendChild(select);
            inputAppend.appendChild(addButton);
            this.adjustSelectWidth(select, { min: 100, max: 300, exact: null });
            select.addEventListener('change', () => this.applyAllFilters());

            return inputAppend;
        }

        createDropdown() {
            const select = document.createElement('select');
            select.className = 'brand-exclusion';

            const defaultOption = document.createElement('option');
            defaultOption.value = '';
            defaultOption.text = '-';
            select.appendChild(defaultOption);

            const manufacturerSelect = document.querySelector(SELECTORS.VENDOR);
            if (manufacturerSelect) {
                Array.from(manufacturerSelect.options)
                    .filter(opt => opt.value !== '')
                    .forEach(opt => {
                        const option = document.createElement('option');
                        option.value = opt.value;
                        option.text = opt.text;
                        select.appendChild(option);
                    });
            }

            return select;
        }

        createButton(text, onClick) {
            const button = document.createElement('button');
            button.textContent = text;
            button.className = 'btn';
            button.type = 'button';
            
            button.addEventListener('click', (e) => {
                try {
                    onClick(e);
                } catch (error) {
                    console.warn('Button click handler failed:', error);
                }
            });
            
            return button;
        }

        addNewFilter(container) {
            const inputAppend = document.createElement('div');
            inputAppend.className = 'input input-append';
            Object.assign(inputAppend.style, STYLES.ADDITIONAL_FILTER);

            const select = this.createDropdown();
            const removeButton = this.createButton('×', () => {
                inputAppend.remove();
                this.applyAllFilters();
            });

            inputAppend.appendChild(select);
            inputAppend.appendChild(removeButton);
            container.appendChild(inputAppend);

            this.adjustSelectWidth(select, { min: 100, max: 300, exact: null });
            select.addEventListener('change', () => this.applyAllFilters());
        }

        adjustSelectWidth(select, { min = 100, max = 300, exact = null } = {}) {
            const span = document.createElement('span');
            Object.assign(span.style, { ...STYLES.HIDDEN_SPAN, font: window.getComputedStyle(select).font });
            document.body.appendChild(span);

            const maxWidth = Array.from(select.options).reduce((max, option) => {
                span.textContent = option.text;
                return Math.max(max, span.offsetWidth);
            }, 0);

            span.remove();
            const calculatedWidth = Math.min(Math.max(maxWidth + 30, min), max);
            select.style.width = exact ? `${exact}px` : `${calculatedWidth}px`;
        }

        setupCheckboxes(checkboxes, submitBox) {
            const container = document.createElement('div');
            Object.assign(container.style, STYLES.CHECKBOX_CONTAINER);

            checkboxes.forEach(checkbox => {
                checkbox.remove();
                container.appendChild(checkbox);
            });

            submitBox.parentNode.insertBefore(container, submitBox.nextSibling);
        }

        setupResetButton() {
            const resetButton = document.querySelector(SELECTORS.RESET_BUTTON);
            if (!resetButton) return;

            const originalClick = resetButton.onclick;
            resetButton.onclick = (e) => {
                if (originalClick) originalClick.call(resetButton, e);
                this.resetFilters();
            };
        }

        insertFilter(filter, referenceNode) {
            referenceNode.parentNode.insertBefore(filter, referenceNode.nextSibling);
        }

        applyAllFilters() {
            const excludedBrandIds = Array.from(document.querySelectorAll(SELECTORS.BRAND_EXCLUSION))
                .map(select => select.value)
                .filter(Boolean);

            excludedBrandIds.length ? this.hideExcludedModules(excludedBrandIds) : this.showAllModules();
        }

        showAllModules() {
            document.querySelectorAll(SELECTORS.MODULES)
                .forEach(module => module.style.display = '');
            this.updateModuleCount();
        }

        hideExcludedModules(excludedBrandIds) {
            let visibleCount = 0;
            document.querySelectorAll(SELECTORS.MODULES).forEach(module => {
                const manufacturerLink = module.querySelector(SELECTORS.VENDOR_LINK);
                if (manufacturerLink) {
                    const vendorId = manufacturerLink.href.split('/').pop();
                    module.style.display = excludedBrandIds.includes(vendorId) ? 'none' : '';
                    if (module.style.display === '') visibleCount++;
                }
            });
            this.updateModuleCount();
        }

        updateModuleCount() {
            const countElement = document.querySelector(SELECTORS.MODULE_COUNT);
            if (countElement) {
                const visibleCount = document.querySelectorAll(`${SELECTORS.MODULES}[style="display: ;"]`).length;
                countElement.textContent = `${visibleCount} module${visibleCount !== 1 ? 's' : ''}`;
            }
        }

        resetFilters() {
            document.querySelectorAll(SELECTORS.BRAND_EXCLUSION)
                .forEach(filter => filter.value = '');
            this.showAllModules();
        }
    }

    function initializeFilter() {
        try {
            new ModuleFilter();
        } catch (error) {
            console.warn('ModuleFilter initialization failed:', error);
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initializeFilter);
    } else {
        setTimeout(initializeFilter, 100);
    }
})();