您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Provides an API for userscripts to add tabs to a site's settings menu, with improved state handling.
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.icu/scripts/533630/1575940/Settings%20Tab%20Manager%20%28STM%29.js
// ==UserScript== // @name Settings Tab Manager (STM) // @namespace shared-settings-manager // @version 1.1.4 // @description Provides an API for userscripts to add tabs to a site's settings menu, with improved state handling. // @author Gemini & User Input // @license MIT // @match https://8chan.moe/* // @match https://8chan.se/* // @grant GM_addStyle // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // --- Keep Constants, State (isSeparatorAdded etc.), Promise, publicApi, Styling the same --- const MANAGER_ID = 'SettingsTabManager'; const log = (...args) => console.log(`[${MANAGER_ID}]`, ...args); const warn = (...args) => console.warn(`[${MANAGER_ID}]`, ...args); const error = (...args) => console.error(`[${MANAGER_ID}]`, ...args); const SELECTORS = Object.freeze({ /* ... same ... */ }); const ACTIVE_CLASSES = Object.freeze({ /* ... same ... */ }); const ATTRS = Object.freeze({ /* ... same ... */ }); let isInitialized = false; let settingsMenuEl = null; let tabContainerEl = null; let panelContainerEl = null; let activeStmTabId = null; // Renamed for clarity: Tracks the ID of the *STM* tab that is active const registeredTabs = new Map(); const pendingRegistrations = []; let isSeparatorAdded = false; let resolveReadyPromise; const readyPromise = new Promise(resolve => { resolveReadyPromise = resolve; }); const publicApi = Object.freeze({ /* ... same ... */ }); GM_addStyle(`/* ... same styles ... */`); // --- Core Logic Implementation Functions --- function findSettingsElements() { /* ... same ... */ } /** * Deactivates the STM tab specified by the scriptId. * Removes active classes and calls the onDeactivate callback. * Does NOT change activeStmTabId itself. * @param {string} scriptId The ID of the STM tab to deactivate visuals/callbacks for. */ function _deactivateStmTabVisualsAndCallback(scriptId) { if (!scriptId) return; // Nothing to deactivate const config = registeredTabs.get(scriptId); // Don't warn if config not found, might be called defensively // if (!config) { warn(`Config not found for tab ID during deactivation: ${scriptId}`); } const tab = getTabElementImpl(scriptId); const panel = getPanelElementImpl(scriptId); if (tab) tab.classList.remove(ACTIVE_CLASSES.TAB); // else { warn(`Could not find tab element for ${scriptId} during deactivation.`); } if (panel) { panel.classList.remove(ACTIVE_CLASSES.PANEL); panel.style.display = 'none'; } // else { warn(`Could not find panel element for ${scriptId} during deactivation.`); } // Call the script's deactivate hook if config exists if (config) { try { config.onDeactivate?.(panel, tab); } catch (e) { error(`Error during onDeactivate for ${scriptId}:`, e); } } } /** * Activates the STM tab specified by the scriptId. * Adds active classes, ensures panel display, and calls the onActivate callback. * Does NOT change activeStmTabId itself. * Does NOT deactivate other tabs (STM or native). * @param {string} scriptId The ID of the STM tab to activate visuals/callbacks for. */ function _activateStmTabVisualsAndCallback(scriptId) { const config = registeredTabs.get(scriptId); if (!config) { error(`Cannot activate tab: ${scriptId}. Config not found.`); return; } const tab = getTabElementImpl(scriptId); const panel = getPanelElementImpl(scriptId); if (!tab || !panel) { error(`Cannot activate tab: ${scriptId}. Tab or Panel element not found.`); return; } // Activate the new STM tab/panel tab.classList.add(ACTIVE_CLASSES.TAB); panel.classList.add(ACTIVE_CLASSES.PANEL); panel.style.display = 'block'; // Call the script's activation hook try { config.onActivate?.(panel, tab); } catch (e) { error(`Error during onActivate for ${scriptId}:`, e); // Consider reverting visual activation on error? Maybe too complex. } } /** Handles clicks within the tab container to switch tabs. */ function handleTabClick(event) { const clickedTabElement = event.target.closest(SELECTORS.SITE_TAB); // Get the clicked tab element if (!clickedTabElement) return; // Clicked outside any tab const isStmTab = clickedTabElement.matches(`span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}]`); const clickedStmScriptId = isStmTab ? clickedTabElement.getAttribute(ATTRS.SCRIPT_ID) : null; // --- Case 1: Clicked an STM Tab --- if (isStmTab && clickedStmScriptId) { event.stopPropagation(); // Prevent site handler from running if (clickedStmScriptId === activeStmTabId) { // log(`Clicked already active STM tab: ${clickedStmScriptId}`); return; // Do nothing } // --- Deactivate previous tab (if any) --- const previousActiveStmId = activeStmTabId; if (previousActiveStmId) { _deactivateStmTabVisualsAndCallback(previousActiveStmId); } else { // If no STM tab was active, ensure any *native* tab is visually deactivated panelContainerEl?.querySelectorAll(`:scope > ${SELECTORS.SITE_PANEL}.${ACTIVE_CLASSES.PANEL}:not([${ATTRS.MANAGED}])`) .forEach(p => p.classList.remove(ACTIVE_CLASSES.PANEL)); tabContainerEl?.querySelectorAll(`:scope > ${SELECTORS.SITE_TAB}.${ACTIVE_CLASSES.TAB}:not([${ATTRS.MANAGED}])`) .forEach(t => t.classList.remove(ACTIVE_CLASSES.TAB)); } // --- Activate the clicked STM tab --- _activateStmTabVisualsAndCallback(clickedStmScriptId); activeStmTabId = clickedStmScriptId; // Update the state *after* successful activation steps return; // Handled } // --- Case 2: Clicked a Native Site Tab --- if (!isStmTab && clickedTabElement.matches(`${SELECTORS.SITE_TAB}:not([${ATTRS.MANAGED}])`)) { // log(`Native site tab clicked.`); // If an STM tab was active, deactivate it visually and clear STM state. if (activeStmTabId) { _deactivateStmTabVisualsAndCallback(activeStmTabId); activeStmTabId = null; // Clear STM state } // **Allow propagation** - Let the site's own click handler manage activating the native tab. return; } // --- Case 3: Clicked the STM Separator --- if (clickedTabElement.matches(`span[${ATTRS.SEPARATOR}]`)) { event.stopPropagation(); // Do nothing, prevent site handler return; } } function attachTabClickListener() { /* ... same ... */ } function createSeparator() { /* ... same ... */ } function createTabAndPanel(config) { /* ... same ... */ } function processPendingRegistrations() { /* ... same, ensure sorting ... */ } function initializeManager() { /* ... same ... */ } const observer = new MutationObserver(/* ... same observer logic ... */); // observer.observe(...) // setTimeout(initializeManager, 0); // --- API Implementation Functions --- function registerTabImpl(config) { /* ... same validation, sorting pending queue ... */ } /** Public API function to programmatically activate a registered tab. */ function activateTabImpl(scriptId) { if (typeof scriptId !== 'string' || !scriptId.trim()) { error('activateTab failed: Invalid scriptId provided.'); return; } if (!isInitialized) { warn(`Cannot activate tab ${scriptId} yet, manager not initialized.`); return; } if (!registeredTabs.has(scriptId)) { error(`activateTab failed: Script ID "${scriptId}" is not registered.`); return; } if (scriptId === activeStmTabId) { log(`activateTab: Tab ${scriptId} is already active.`); return; // Already active } // --- Deactivate previous tab (if any) --- const previousActiveStmId = activeStmTabId; if (previousActiveStmId) { _deactivateStmTabVisualsAndCallback(previousActiveStmId); } else { // Clear any active native tab visuals panelContainerEl?.querySelectorAll(`:scope > ${SELECTORS.SITE_PANEL}.${ACTIVE_CLASSES.PANEL}:not([${ATTRS.MANAGED}])`).forEach(p => p.classList.remove(ACTIVE_CLASSES.PANEL)); tabContainerEl?.querySelectorAll(`:scope > ${SELECTORS.SITE_TAB}.${ACTIVE_CLASSES.TAB}:not([${ATTRS.MANAGED}])`).forEach(t => t.classList.remove(ACTIVE_CLASSES.TAB)); } // --- Activate the requested STM tab --- _activateStmTabVisualsAndCallback(scriptId); activeStmTabId = scriptId; // Update the state log(`Programmatically activated tab: ${scriptId}`); } function getPanelElementImpl(scriptId) { /* ... same ... */ } function getTabElementImpl(scriptId) { /* ... same ... */ } // --- Global Exposure --- // ... same ... })();