Provides an API for userscripts to add tabs to a site's settings menu, with improved state handling.
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/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 ...
})();