您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Provides an API for other userscripts to add tabs to a site's settings menu, with a single separator.
当前为
此脚本不应直接安装,它是供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/533630/1575927/Settings%20Tab%20Manager%20%28STM%29.js
- // ==UserScript==
- // @name Settings Tab Manager (STM)
- // @namespace shared-settings-manager
- // @version 1.1.2
- // @description Provides an API for other userscripts to add tabs to a site's settings menu, with a single separator.
- // @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';
- 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);
- // --- Configuration ---
- const SELECTORS = Object.freeze({
- SETTINGS_MENU: '#settingsMenu',
- TAB_CONTAINER: '#settingsMenu .floatingContainer > div:first-child',
- PANEL_CONTAINER: '#settingsMenu .menuContentPanel',
- SITE_TAB: '.settingsTab',
- SITE_PANEL: '.panelContents',
- SITE_SEPARATOR: '.settingsTabSeparator', // Keep this if the site uses it visually
- });
- const ACTIVE_CLASSES = Object.freeze({
- TAB: 'selectedTab',
- PANEL: 'selectedPanel',
- });
- const ATTRS = Object.freeze({
- SCRIPT_ID: 'data-stm-script-id',
- MANAGED: 'data-stm-managed',
- SEPARATOR: 'data-stm-main-separator', // Attribute for the single separator
- ORDER: 'data-stm-order', // Attribute to store the order on the tab element
- });
- // --- State ---
- let isInitialized = false;
- let settingsMenuEl = null;
- let tabContainerEl = null;
- let panelContainerEl = null;
- let activeTabId = null;
- const registeredTabs = new Map(); // Stores { scriptId: config } for registered tabs
- const pendingRegistrations = []; // Stores { config } for tabs registered before init
- let isSeparatorAdded = false; // Flag to ensure only one separator is added
- // --- Readiness Promise ---
- let resolveReadyPromise;
- const readyPromise = new Promise(resolve => {
- resolveReadyPromise = resolve;
- });
- // --- Public API Definition ---
- const publicApi = Object.freeze({
- ready: readyPromise,
- registerTab: (config) => {
- return registerTabImpl(config);
- },
- activateTab: (scriptId) => {
- activateTabImpl(scriptId);
- },
- getPanelElement: (scriptId) => {
- return getPanelElementImpl(scriptId);
- },
- getTabElement: (scriptId) => {
- return getTabElementImpl(scriptId);
- }
- });
- // --- Styling ---
- GM_addStyle(`
- /* Ensure panels added by STM behave like native ones */
- ${SELECTORS.PANEL_CONTAINER} > div[${ATTRS.MANAGED}] {
- display: none; /* Hide inactive panels */
- }
- ${SELECTORS.PANEL_CONTAINER} > div[${ATTRS.MANAGED}].${ACTIVE_CLASSES.PANEL} {
- display: block; /* Show active panel */
- }
- /* Optional: Basic styling for the added tabs */
- ${SELECTORS.TAB_CONTAINER} > span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}] {
- cursor: pointer;
- }
- /* Styling for the single separator */
- ${SELECTORS.TAB_CONTAINER} > span[${ATTRS.SEPARATOR}] {
- cursor: default;
- margin: 0 5px; /* Add some spacing around the separator */
- }
- `);
- // --- Core Logic Implementation Functions ---
- /** Finds the essential DOM elements for the settings UI. Returns true if all found. */
- function findSettingsElements() {
- settingsMenuEl = document.querySelector(SELECTORS.SETTINGS_MENU);
- if (!settingsMenuEl) return false;
- tabContainerEl = settingsMenuEl.querySelector(SELECTORS.TAB_CONTAINER);
- panelContainerEl = settingsMenuEl.querySelector(SELECTORS.PANEL_CONTAINER);
- if (!tabContainerEl) {
- warn('Tab container not found within settings menu using selector:', SELECTORS.TAB_CONTAINER);
- return false;
- }
- if (!panelContainerEl) {
- warn('Panel container not found within settings menu using selector:', SELECTORS.PANEL_CONTAINER);
- return false;
- }
- // Ensure the elements are still in the document (relevant for re-init checks)
- if (!document.body.contains(settingsMenuEl) || !document.body.contains(tabContainerEl) || !document.body.contains(panelContainerEl)) {
- warn('Found settings elements are detached from the DOM.');
- settingsMenuEl = null;
- tabContainerEl = null;
- panelContainerEl = null;
- isSeparatorAdded = false; // Reset separator if containers are gone
- return false;
- }
- return true;
- }
- /** Deactivates the currently active STM tab (if any) and ensures native tabs are also visually deactivated. */
- function deactivateCurrentTab() {
- // Deactivate STM-managed tab
- if (activeTabId && registeredTabs.has(activeTabId)) {
- const config = registeredTabs.get(activeTabId);
- const tab = getTabElementImpl(activeTabId); // Use API getter
- const panel = getPanelElementImpl(activeTabId); // Use API getter
- tab?.classList.remove(ACTIVE_CLASSES.TAB);
- panel?.classList.remove(ACTIVE_CLASSES.PANEL);
- if (panel) panel.style.display = 'none'; // Explicitly hide panel via style
- try {
- config.onDeactivate?.(panel, tab);
- } catch (e) {
- error(`Error during onDeactivate for ${activeTabId}:`, e);
- }
- }
- activeTabId = null; // Clear active STM tab ID
- // Remove active class from any site tabs/panels managed outside STM
- 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));
- }
- /** Internal implementation for activating a specific STM tab */
- function activateTab(scriptId) {
- if (!registeredTabs.has(scriptId) || !tabContainerEl || !panelContainerEl) {
- warn(`Cannot activate tab: ${scriptId}. Not registered or containers not found.`);
- return;
- }
- if (activeTabId === scriptId) return; // Already active, do nothing
- deactivateCurrentTab(); // Deactivate previous one first (handles both STM and native)
- const config = registeredTabs.get(scriptId);
- const tab = getTabElementImpl(scriptId);
- const panel = getPanelElementImpl(scriptId);
- if (!tab || !panel) {
- error(`Tab or Panel element not found for ${scriptId} during activation.`);
- return;
- }
- tab.classList.add(ACTIVE_CLASSES.TAB);
- panel.classList.add(ACTIVE_CLASSES.PANEL);
- panel.style.display = 'block'; // Ensure panel is visible via style
- activeTabId = scriptId; // Set the new active STM tab ID
- try {
- config.onActivate?.(panel, tab);
- } catch (e) {
- error(`Error during onActivate for ${scriptId}:`, e);
- }
- }
- /** Handles clicks within the tab container to switch tabs. */
- function handleTabClick(event) {
- // Check if an STM-managed tab was clicked
- const clickedStmTab = event.target.closest(`span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}]`);
- if (clickedStmTab) {
- event.stopPropagation(); // Prevent potential conflicts with site listeners
- const scriptId = clickedStmTab.getAttribute(ATTRS.SCRIPT_ID);
- if (scriptId && scriptId !== activeTabId) {
- activateTab(scriptId); // Activate the clicked STM tab
- }
- return; // Handled
- }
- // Check if a native site tab was clicked (that isn't also an STM tab)
- const clickedSiteTab = event.target.closest(`${SELECTORS.SITE_TAB}:not([${ATTRS.MANAGED}])`);
- if (clickedSiteTab) {
- // If a native tab is clicked, ensure any active STM tab is deactivated
- if (activeTabId) {
- deactivateCurrentTab();
- // Note: We rely on the site's own JS to handle activating the native tab/panel visually
- }
- return; // Let the site handle its own tab activation
- }
- // Check if the separator was clicked (do nothing)
- if (event.target.closest(`span[${ATTRS.SEPARATOR}]`)) {
- event.stopPropagation();
- return;
- }
- }
- /** Attaches the main click listener to the tab container. */
- function attachTabClickListener() {
- if (!tabContainerEl) return;
- // Use capture phase to potentially handle clicks before site's listeners if needed
- tabContainerEl.removeEventListener('click', handleTabClick, true);
- tabContainerEl.addEventListener('click', handleTabClick, true);
- log('Tab click listener attached.');
- }
- /** Helper to create the SINGLE separator span */
- function createSeparator() {
- const separator = document.createElement('span');
- // Use the site's separator class if defined, otherwise a fallback
- separator.className = SELECTORS.SITE_SEPARATOR ? SELECTORS.SITE_SEPARATOR.substring(1) : 'settings-tab-separator-fallback';
- separator.setAttribute(ATTRS.MANAGED, 'true'); // Mark as managed by STM (helps selection)
- separator.setAttribute(ATTRS.SEPARATOR, 'true'); // Mark as the main separator
- separator.textContent = '|'; // Or any desired visual separator
- // Style is handled by GM_addStyle now
- return separator;
- }
- /** Creates and inserts the tab and panel elements for a given script config. */
- function createTabAndPanel(config) {
- if (!tabContainerEl || !panelContainerEl) {
- error(`Cannot create tab/panel for ${config.scriptId}: Containers not found.`);
- return;
- }
- // Check if tab *element* exists already
- if (tabContainerEl.querySelector(`span[${ATTRS.SCRIPT_ID}="${config.scriptId}"]`)) {
- log(`Tab element already exists for ${config.scriptId}, skipping creation.`);
- return; // Avoid duplicates if registration happens multiple times somehow
- }
- log(`Creating tab/panel for: ${config.scriptId}`);
- // --- Create Tab ---
- const newTab = document.createElement('span');
- newTab.className = SELECTORS.SITE_TAB.substring(1); // Use site's base tab class
- newTab.textContent = config.tabTitle;
- newTab.setAttribute(ATTRS.SCRIPT_ID, config.scriptId);
- newTab.setAttribute(ATTRS.MANAGED, 'true');
- newTab.setAttribute('title', `${config.tabTitle} (Settings by ${config.scriptId})`);
- const desiredOrder = typeof config.order === 'number' ? config.order : Infinity;
- newTab.setAttribute(ATTRS.ORDER, desiredOrder); // Store order attribute
- // --- Create Panel ---
- const newPanel = document.createElement('div');
- newPanel.className = SELECTORS.SITE_PANEL.substring(1); // Use site's base panel class
- newPanel.setAttribute(ATTRS.SCRIPT_ID, config.scriptId);
- newPanel.setAttribute(ATTRS.MANAGED, 'true');
- newPanel.id = `${MANAGER_ID}-${config.scriptId}-panel`; // Unique ID for the panel
- // --- Insertion Logic (Single Separator & Ordered Tabs) ---
- let insertBeforeTab = null; // The specific STM tab element to insert *before*
- const existingStmTabs = Array.from(tabContainerEl.querySelectorAll(`span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}]`));
- // Sort existing STM tabs DOM elements by their order attribute to find the correct insertion point
- existingStmTabs.sort((a, b) => {
- const orderA = parseInt(a.getAttribute(ATTRS.ORDER) || Infinity, 10);
- const orderB = parseInt(b.getAttribute(ATTRS.ORDER) || Infinity, 10);
- return orderA - orderB;
- });
- // Find the first existing STM tab with a higher order number than the new tab
- for (const existingTab of existingStmTabs) {
- const existingOrder = parseInt(existingTab.getAttribute(ATTRS.ORDER) || Infinity, 10);
- if (desiredOrder < existingOrder) {
- insertBeforeTab = existingTab;
- break;
- }
- }
- // Check if this is the very first STM tab being added *to the DOM*
- const isFirstStmTabBeingAdded = existingStmTabs.length === 0;
- let separatorInstance = null;
- // Add the single separator *only* if it hasn't been added yet AND this is the first STM tab going into the DOM
- if (!isSeparatorAdded && isFirstStmTabBeingAdded) {
- separatorInstance = createSeparator(); // Create the single separator instance
- isSeparatorAdded = true; // Mark it as added so it doesn't happen again
- log('Adding the main STM separator.');
- }
- // Insert the separator (if created) and then the tab
- if (insertBeforeTab) {
- // Insert before a specific existing STM tab
- if (separatorInstance) {
- // This case should technically not happen if separator is only added for the *first* tab,
- // but we keep it for robustness. It means we are inserting the *first* tab before another.
- tabContainerEl.insertBefore(separatorInstance, insertBeforeTab);
- }
- tabContainerEl.insertBefore(newTab, insertBeforeTab);
- } else {
- // Append after all existing STM tabs (or as the very first STM elements)
- if (separatorInstance) {
- // Append the separator first because this is the first STM tab
- tabContainerEl.appendChild(separatorInstance);
- }
- // Append the new tab (either after the new separator or after the last existing STM tab)
- tabContainerEl.appendChild(newTab);
- }
- // Append Panel to the panel container
- panelContainerEl.appendChild(newPanel);
- // --- Initialize Panel Content ---
- // Use Promise.resolve to handle both sync and async onInit functions gracefully
- try {
- Promise.resolve(config.onInit(newPanel, newTab)).catch(e => {
- error(`Error during async onInit for ${config.scriptId}:`, e);
- newPanel.innerHTML = `<p style="color: red;">Error initializing settings panel for ${config.scriptId}. See console.</p>`;
- });
- } catch (e) { // Catch synchronous errors from onInit
- error(`Error during sync onInit for ${config.scriptId}:`, e);
- newPanel.innerHTML = `<p style="color: red;">Error initializing settings panel for ${config.scriptId}. See console.</p>`;
- }
- }
- /** Sorts and processes all pending registrations once the manager is initialized. */
- function processPendingRegistrations() {
- if (!isInitialized) return; // Should not happen if called correctly, but safety check
- log(`Processing ${pendingRegistrations.length} pending registrations...`);
- // Sort pending registrations by 'order' BEFORE creating elements
- pendingRegistrations.sort((a, b) => {
- const orderA = typeof a.order === 'number' ? a.order : Infinity;
- const orderB = typeof b.order === 'number' ? b.order : Infinity;
- // If orders are equal, maintain original registration order (stable sort behavior desired)
- return orderA - orderB;
- });
- // Process the sorted queue
- while (pendingRegistrations.length > 0) {
- const config = pendingRegistrations.shift(); // Get the next config from the front
- if (!registeredTabs.has(config.scriptId)) {
- registeredTabs.set(config.scriptId, config); // Add to map first
- // createTabAndPanel will handle DOM insertion and separator logic
- createTabAndPanel(config);
- } else {
- // This case should be rare now due to checks in registerTabImpl, but good to keep
- warn(`Script ID ${config.scriptId} was already registered. Skipping pending registration.`);
- }
- }
- log('Finished processing pending registrations.');
- }
- // --- Initialization and Observation ---
- /** Main initialization routine. Finds elements, attaches listener, processes queue. */
- function initializeManager() {
- if (!findSettingsElements()) {
- // log('Settings elements not found or invalid on init check.');
- return false; // Keep observer active
- }
- // Check if already initialized *and* elements are still valid
- if (isInitialized && settingsMenuEl && tabContainerEl && panelContainerEl) {
- // log('Manager already initialized and elements seem valid.');
- attachTabClickListener(); // Re-attach listener just in case it got removed
- return true;
- }
- log('Initializing Settings Tab Manager...');
- attachTabClickListener(); // Attach the main click listener
- isInitialized = true; // Set flag *before* resolving promise and processing queue
- log('Manager is ready.');
- // Resolve the public promise *after* setting isInitialized flag
- // This signals to waiting scripts that the manager is ready
- resolveReadyPromise(publicApi);
- // Process any registrations that occurred before initialization was complete
- processPendingRegistrations();
- return true; // Initialization successful
- }
- // --- Mutation Observer ---
- // Observes the body for the appearance of the settings menu
- const observer = new MutationObserver((mutationsList, obs) => {
- let needsReInitCheck = false;
- // 1. Check if the menu exists but we aren't initialized yet
- if (!isInitialized && document.querySelector(SELECTORS.SETTINGS_MENU)) {
- needsReInitCheck = true;
- }
- // 2. Check if the menu *was* found previously but is no longer in the DOM
- else if (isInitialized && settingsMenuEl && !document.body.contains(settingsMenuEl)) {
- warn('Settings menu seems to have been removed from DOM.');
- isInitialized = false; // Force re-initialization if it reappears
- settingsMenuEl = null;
- tabContainerEl = null;
- panelContainerEl = null;
- isSeparatorAdded = false; // Reset separator flag
- activeTabId = null;
- // Don't resolve the promise again, but new scripts might await it
- // Reset the promise state if needed (more complex, usually not necessary)
- // readyPromise = new Promise(resolve => { resolveReadyPromise = resolve; });
- needsReInitCheck = true; // Check if it got immediately re-added
- }
- // 3. Scan mutations if we haven't found the menu yet or if it was removed
- if (!settingsMenuEl || needsReInitCheck) {
- for (const mutation of mutationsList) {
- if (mutation.addedNodes) {
- for (const node of mutation.addedNodes) {
- if (node.nodeType === Node.ELEMENT_NODE) {
- // Check if the added node *is* the menu or *contains* the menu
- const menu = (node.matches && node.matches(SELECTORS.SETTINGS_MENU))
- ? node
- : (node.querySelector ? node.querySelector(SELECTORS.SETTINGS_MENU) : null);
- if (menu) {
- log('Settings menu detected in DOM via MutationObserver.');
- needsReInitCheck = true;
- break; // Found it in this mutation
- }
- }
- }
- }
- if (needsReInitCheck) break; // Found it, no need to check other mutations
- }
- }
- // 4. Attempt initialization if needed
- if (needsReInitCheck) {
- // Use setTimeout to allow the DOM to potentially stabilize after the mutation
- setTimeout(() => {
- if (initializeManager()) {
- log('Manager initialized/re-initialized successfully via MutationObserver.');
- // Optional: If the settings menu is known to be stable once added,
- // you *could* disconnect the observer here to save resources.
- // However, leaving it active is safer if the menu might be rebuilt.
- // obs.disconnect();
- // log('Mutation observer disconnected.');
- } else {
- // log('Initialization check failed after mutation, observer remains active.');
- }
- }, 0); // Delay slightly
- }
- });
- // Start observing the body for additions/removals in the subtree
- observer.observe(document.body, {
- childList: true, // Watch for direct children changes (adding/removing nodes)
- subtree: true // Watch the entire subtree under document.body
- });
- log('Mutation observer started for settings menu detection.');
- // --- Attempt initial initialization on script load ---
- // Use setTimeout to ensure this runs after the current execution context,
- // allowing the global API exposure to happen first.
- setTimeout(initializeManager, 0);
- // --- API Implementation Functions ---
- /** Public API function to register a new settings tab. */
- function registerTabImpl(config) {
- // --- Input Validation ---
- if (!config || typeof config !== 'object') {
- error('Registration failed: Invalid config object provided.'); return false;
- }
- const { scriptId, tabTitle, onInit } = config;
- if (typeof scriptId !== 'string' || !scriptId.trim()) {
- error('Registration failed: Invalid or missing scriptId (string).', config); return false;
- }
- if (typeof tabTitle !== 'string' || !tabTitle.trim()) {
- error('Registration failed: Invalid or missing tabTitle (string).', config); return false;
- }
- if (typeof onInit !== 'function') {
- error('Registration failed: onInit callback must be a function.', config); return false;
- }
- // Optional callbacks validation
- if (config.onActivate && typeof config.onActivate !== 'function') {
- error(`Registration for ${scriptId} failed: onActivate (if provided) must be a function.`); return false;
- }
- if (config.onDeactivate && typeof config.onDeactivate !== 'function') {
- error(`Registration for ${scriptId} failed: onDeactivate (if provided) must be a function.`); return false;
- }
- // Optional order validation
- if (config.order !== undefined && typeof config.order !== 'number') {
- warn(`Registration for ${scriptId}: Invalid order value provided (must be a number). Defaulting to end.`, config);
- // Allow registration but ignore invalid order
- delete config.order;
- }
- // Prevent duplicate registrations by scriptId
- if (registeredTabs.has(scriptId) || pendingRegistrations.some(p => p.scriptId === scriptId)) {
- warn(`Registration failed: Script ID "${scriptId}" is already registered or pending.`); return false;
- }
- // --- Registration Logic ---
- log(`Registration accepted for: ${scriptId}`);
- // Clone config to avoid external modification? (Shallow clone is usually sufficient)
- const registrationData = { ...config };
- if (isInitialized) {
- // Manager ready: Register immediately and create elements
- registeredTabs.set(scriptId, registrationData);
- // createTabAndPanel handles sorting/insertion/separator logic
- createTabAndPanel(registrationData);
- } else {
- // Manager not ready: Add to pending queue
- log(`Manager not ready, queueing registration for ${scriptId}`);
- pendingRegistrations.push(registrationData);
- // Sort pending queue immediately upon adding to maintain correct insertion order later
- pendingRegistrations.sort((a, b) => {
- const orderA = typeof a.order === 'number' ? a.order : Infinity;
- const orderB = typeof b.order === 'number' ? b.order : Infinity;
- return orderA - orderB;
- });
- }
- return true; // Indicate successful acceptance of registration (not necessarily immediate creation)
- }
- /** 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) {
- // Call the internal function which handles the logic
- activateTab(scriptId);
- } else {
- // Queue activation? Or just warn? Warning is simpler.
- warn(`Cannot activate tab ${scriptId} yet, manager not initialized.`);
- // Could potentially store a desired initial tab and activate it in processPendingRegistrations
- }
- }
- /** Public API function to get the DOM element for a tab's panel. */
- function getPanelElementImpl(scriptId) {
- if (!isInitialized || !panelContainerEl) return null;
- if (typeof scriptId !== 'string' || !scriptId.trim()) return null;
- // Use attribute selector for robustness
- return panelContainerEl.querySelector(`div[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}="${scriptId}"]`);
- }
- /** Public API function to get the DOM element for a tab's clickable part. */
- function getTabElementImpl(scriptId) {
- if (!isInitialized || !tabContainerEl) return null;
- if (typeof scriptId !== 'string' || !scriptId.trim()) return null;
- // Use attribute selector for robustness
- return tabContainerEl.querySelector(`span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}="${scriptId}"]`);
- }
- // --- Global Exposure ---
- // Expose the public API on the window object, checking for conflicts.
- if (window.SettingsTabManager && window.SettingsTabManager !== publicApi) {
- // A different instance or script already exists. Log a warning.
- // Depending on requirements, could throw an error, merge APIs, or namespace.
- warn('window.SettingsTabManager is already defined by another script or instance! Potential conflict.');
- } else if (!window.SettingsTabManager) {
- // Define the API object on the window, making it non-writable but configurable.
- Object.defineProperty(window, 'SettingsTabManager', {
- value: publicApi,
- writable: false, // Prevents accidental overwriting of the API object itself
- configurable: true // Allows users to delete or redefine it if necessary (e.g., for debugging)
- });
- log('SettingsTabManager API exposed on window.');
- }
- })(); // End of IIFE