ModularGrid Manufacturer Exception Filter

Add brand exclusion filtering to ModularGrid module browser

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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);
    }
})();