Settings Tab Manager (STM)

Provides an API for other userscripts to add tabs to a site's settings menu.

このスクリプトは単体で利用できません。右のようなメタデータを含むスクリプトから、ライブラリとして読み込まれます: // @require https://update.greatest.deepsurf.us/scripts/533630/1575960/Settings%20Tab%20Manager%20%28STM%29.js

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
  1. // ==UserScript==
  2. // @name Settings Tab Manager (STM)
  3. // @namespace shared-settings-manager
  4. // @version 1.1.4d
  5. // @description Provides an API for userscripts to add tabs to a site's settings menu, with improved state handling.
  6. // @author Gemini & User Input
  7. // @license MIT
  8. // @match https://8chan.moe/*
  9. // @match https://8chan.se/*
  10. // @grant GM_addStyle
  11. // @run-at document-idle
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. // --- Constants ---
  18. const MANAGER_ID = 'SettingsTabManager';
  19. const log = (...args) => console.log(`[${MANAGER_ID}]`, ...args);
  20. const warn = (...args) => console.warn(`[${MANAGER_ID}]`, ...args);
  21. const error = (...args) => console.error(`[${MANAGER_ID}]`, ...args);
  22.  
  23. const SELECTORS = Object.freeze({
  24. SETTINGS_MENU: '#settingsMenu',
  25. TAB_CONTAINER: '#settingsMenu .floatingContainer > div:first-child',
  26. PANEL_CONTAINER: '#settingsMenu .menuContentPanel',
  27. SITE_TAB: '.settingsTab',
  28. SITE_PANEL: '.panelContents',
  29. SITE_SEPARATOR: '.settingsTabSeparator',
  30. });
  31. const ACTIVE_CLASSES = Object.freeze({
  32. TAB: 'selectedTab',
  33. PANEL: 'selectedPanel',
  34. });
  35. const ATTRS = Object.freeze({
  36. SCRIPT_ID: 'data-stm-script-id',
  37. MANAGED: 'data-stm-managed',
  38. SEPARATOR: 'data-stm-main-separator',
  39. ORDER: 'data-stm-order',
  40. });
  41.  
  42. // --- State ---
  43. let isInitialized = false;
  44. let settingsMenuEl = null;
  45. let tabContainerEl = null;
  46. let panelContainerEl = null;
  47. let activeStmTabId = null; // Renamed: Tracks the scriptId of the *STM* tab that is active
  48. const registeredTabs = new Map(); // Stores { scriptId: config }
  49. const pendingRegistrations = []; // Stores { config } for tabs registered before init
  50. let isSeparatorAdded = false; // Flag for single separator
  51.  
  52. // --- Readiness Promise ---
  53. let resolveReadyPromise;
  54. const readyPromise = new Promise(resolve => { resolveReadyPromise = resolve; });
  55.  
  56. // --- Public API Definition ---
  57. const publicApi = Object.freeze({
  58. ready: readyPromise,
  59. registerTab: (config) => registerTabImpl(config),
  60. activateTab: (scriptId) => activateTabImpl(scriptId),
  61. getPanelElement: (scriptId) => getPanelElementImpl(scriptId),
  62. getTabElement: (scriptId) => getTabElementImpl(scriptId)
  63. });
  64.  
  65. // --- Styling ---
  66. GM_addStyle(`
  67. /* Ensure panels added by STM behave like native ones */
  68. ${SELECTORS.PANEL_CONTAINER} > div[${ATTRS.MANAGED}] {
  69. display: none; /* Hide inactive panels */
  70. }
  71. ${SELECTORS.PANEL_CONTAINER} > div[${ATTRS.MANAGED}].${ACTIVE_CLASSES.PANEL} {
  72. display: block; /* Show active panel */
  73. }
  74. /* Optional: Basic styling for the added tabs */
  75. ${SELECTORS.TAB_CONTAINER} > span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}] {
  76. cursor: pointer;
  77. }
  78. /* Styling for the single separator */
  79. ${SELECTORS.TAB_CONTAINER} > span[${ATTRS.SEPARATOR}] {
  80. cursor: default;
  81. margin: 0 5px; /* Add some spacing around the separator */
  82. }
  83. `);
  84.  
  85. // --- Core Logic Implementation Functions ---
  86.  
  87. /** Finds the essential DOM elements for the settings UI. Returns true if all found. */
  88. function findSettingsElements() {
  89. settingsMenuEl = document.querySelector(SELECTORS.SETTINGS_MENU);
  90. if (!settingsMenuEl) return false;
  91.  
  92. tabContainerEl = settingsMenuEl.querySelector(SELECTORS.TAB_CONTAINER);
  93. panelContainerEl = settingsMenuEl.querySelector(SELECTORS.PANEL_CONTAINER);
  94.  
  95. if (!tabContainerEl) {
  96. warn('Tab container not found within settings menu using selector:', SELECTORS.TAB_CONTAINER);
  97. return false;
  98. }
  99. if (!panelContainerEl) {
  100. warn('Panel container not found within settings menu using selector:', SELECTORS.PANEL_CONTAINER);
  101. return false;
  102. }
  103. // Ensure the elements are still in the document (relevant for re-init checks)
  104. if (!document.body.contains(settingsMenuEl) || !document.body.contains(tabContainerEl) || !document.body.contains(panelContainerEl)) {
  105. warn('Found settings elements are detached from the DOM.');
  106. settingsMenuEl = null;
  107. tabContainerEl = null;
  108. panelContainerEl = null;
  109. isSeparatorAdded = false; // Reset separator if containers are gone
  110. return false;
  111. }
  112. return true;
  113. }
  114.  
  115. /**
  116. * Deactivates the STM tab specified by the scriptId.
  117. * Removes active classes and calls the onDeactivate callback.
  118. * Does NOT change activeStmTabId itself.
  119. * @param {string} scriptId The ID of the STM tab to deactivate visuals/callbacks for.
  120. */
  121. function _deactivateStmTabVisualsAndCallback(scriptId) {
  122. if (!scriptId) return; // Nothing to deactivate
  123.  
  124. const config = registeredTabs.get(scriptId);
  125. // If config not found, still try to remove visuals defensively but don't warn/error.
  126.  
  127. const tab = getTabElementImpl(scriptId);
  128. const panel = getPanelElementImpl(scriptId);
  129.  
  130. if (tab) tab.classList.remove(ACTIVE_CLASSES.TAB);
  131. // else { log(`Tab element not found for ${scriptId} during deactivation.`); }
  132.  
  133. if (panel) {
  134. panel.classList.remove(ACTIVE_CLASSES.PANEL);
  135. panel.style.display = 'none';
  136. }
  137. // else { log(`Panel element not found for ${scriptId} during deactivation.`); }
  138.  
  139. // Call the script's deactivate hook only if config exists
  140. if (config) {
  141. try {
  142. // Pass potentially null elements if lookup failed earlier
  143. config.onDeactivate?.(panel, tab);
  144. } catch (e) {
  145. error(`Error during onDeactivate for ${scriptId}:`, e);
  146. }
  147. }
  148. }
  149.  
  150. /**
  151. * Activates the STM tab specified by the scriptId.
  152. * Adds active classes, ensures panel display, and calls the onActivate callback.
  153. * Does NOT change activeStmTabId itself.
  154. * Does NOT deactivate other tabs (STM or native).
  155. * @param {string} scriptId The ID of the STM tab to activate visuals/callbacks for.
  156. * @returns {boolean} True if activation visuals/callback were attempted, false if elements/config not found.
  157. */
  158. function _activateStmTabVisualsAndCallback(scriptId) {
  159. const config = registeredTabs.get(scriptId);
  160. if (!config) {
  161. error(`Cannot activate visuals: ${scriptId}. Config not found.`);
  162. return false;
  163. }
  164.  
  165. const tab = getTabElementImpl(scriptId);
  166. const panel = getPanelElementImpl(scriptId);
  167.  
  168. if (!tab || !panel) {
  169. error(`Cannot activate visuals: ${scriptId}. Tab or Panel element not found.`);
  170. return false;
  171. }
  172.  
  173. // Activate the new STM tab/panel visuals
  174. tab.classList.add(ACTIVE_CLASSES.TAB);
  175. panel.classList.add(ACTIVE_CLASSES.PANEL);
  176. panel.style.display = 'block';
  177.  
  178. // Call the script's activation hook
  179. try {
  180. config.onActivate?.(panel, tab);
  181. } catch (e) {
  182. error(`Error during onActivate for ${scriptId}:`, e);
  183. // Activation visuals already applied, hard to revert cleanly. Error logged.
  184. }
  185. return true; // Activation attempted
  186. }
  187.  
  188. /** Handles clicks within the tab container to switch tabs. */
  189. function handleTabClick(event) {
  190. const clickedTabElement = event.target.closest(SELECTORS.SITE_TAB); // Get the specific tab element clicked
  191. if (!clickedTabElement || !tabContainerEl || !panelContainerEl) {
  192. return; // Clicked outside a known tab or containers not ready
  193. }
  194.  
  195. const isStmTab = clickedTabElement.matches(`span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}]`);
  196. const clickedStmScriptId = isStmTab ? clickedTabElement.getAttribute(ATTRS.SCRIPT_ID) : null;
  197.  
  198. // --- Case 1: Clicked an STM Tab ---
  199. if (isStmTab && clickedStmScriptId) {
  200. event.stopPropagation(); // Prevent site handler interference
  201.  
  202. if (clickedStmScriptId === activeStmTabId) {
  203. // log(`Clicked already active STM tab: ${clickedStmScriptId}`);
  204. return; // Already active, do nothing
  205. }
  206.  
  207. // --- Deactivate previous tab (if any) ---
  208. const previousActiveStmId = activeStmTabId; // Store the ID before changing state
  209. if (previousActiveStmId) {
  210. // Deactivate the previously active *STM* tab
  211. _deactivateStmTabVisualsAndCallback(previousActiveStmId);
  212. } else {
  213. // If no STM tab was active, ensure any *native* site tab is visually deactivated
  214. // This prepares for the STM tab to become the only active one.
  215. panelContainerEl.querySelectorAll(`:scope > ${SELECTORS.SITE_PANEL}.${ACTIVE_CLASSES.PANEL}:not([${ATTRS.MANAGED}])`)
  216. .forEach(p => p.classList.remove(ACTIVE_CLASSES.PANEL));
  217. tabContainerEl.querySelectorAll(`:scope > ${SELECTORS.SITE_TAB}.${ACTIVE_CLASSES.TAB}:not([${ATTRS.MANAGED}])`)
  218. .forEach(t => t.classList.remove(ACTIVE_CLASSES.TAB));
  219. }
  220.  
  221. // --- Activate the clicked STM tab ---
  222. if (_activateStmTabVisualsAndCallback(clickedStmScriptId)) {
  223. activeStmTabId = clickedStmScriptId; // Update the state *after* successful activation attempt
  224. } else {
  225. // Activation failed (elements missing?), revert state if needed?
  226. // For now, log the error and potentially leave state inconsistent.
  227. activeStmTabId = null; // Or revert to previousActiveStmId? Null is safer.
  228. }
  229.  
  230. return; // Handled by STM
  231. }
  232.  
  233. // --- Case 2: Clicked a Native Site Tab ---
  234. if (!isStmTab && clickedTabElement.matches(`${SELECTORS.SITE_TAB}:not([${ATTRS.MANAGED}])`)) {
  235. // log(`Native site tab clicked.`);
  236.  
  237. // If an STM tab *was* active, deactivate its visuals and clear STM's active state.
  238. if (activeStmTabId) {
  239. // log(`Deactivating current STM tab (${activeStmTabId}) due to native tab click.`);
  240. _deactivateStmTabVisualsAndCallback(activeStmTabId);
  241. activeStmTabId = null; // Clear STM state
  242. }
  243.  
  244. // **Allow propagation** - Let the site's own click handler run to manage the native tab.
  245. // log("Allowing event propagation for native tab handler.");
  246. return;
  247. }
  248.  
  249. // --- Case 3: Clicked the STM Separator ---
  250. if (clickedTabElement.matches(`span[${ATTRS.SEPARATOR}]`)) {
  251. event.stopPropagation(); // Do nothing, prevent site handler
  252. // log("Separator clicked, propagation stopped.");
  253. return;
  254. }
  255.  
  256. // If click was somewhere else (e.g., empty space in tab container), do nothing specific.
  257. // log("Clicked non-tab area.");
  258. }
  259.  
  260. /** Attaches the main click listener to the tab container. */
  261. function attachTabClickListener() {
  262. if (!tabContainerEl) return;
  263. tabContainerEl.removeEventListener('click', handleTabClick, true);
  264. tabContainerEl.addEventListener('click', handleTabClick, true); // Use capture phase
  265. log('Tab click listener attached.');
  266. }
  267.  
  268. /** Helper to create the SINGLE separator span */
  269. function createSeparator() {
  270. const separator = document.createElement('span');
  271. separator.className = SELECTORS.SITE_SEPARATOR ? SELECTORS.SITE_SEPARATOR.substring(1) : 'settings-tab-separator-fallback';
  272. separator.setAttribute(ATTRS.MANAGED, 'true');
  273. separator.setAttribute(ATTRS.SEPARATOR, 'true');
  274. separator.textContent = '|';
  275. return separator;
  276. }
  277.  
  278. /** Creates and inserts the tab and panel elements for a given script config. */
  279. function createTabAndPanel(config) {
  280. if (!tabContainerEl || !panelContainerEl) { error(`Cannot create tab/panel for ${config.scriptId}: Containers not found.`); return; }
  281. if (tabContainerEl.querySelector(`span[${ATTRS.SCRIPT_ID}="${config.scriptId}"]`)) { log(`Tab element already exists for ${config.scriptId}, skipping creation.`); return; }
  282.  
  283. log(`Creating tab/panel for: ${config.scriptId}`);
  284.  
  285. const newTab = document.createElement('span');
  286. newTab.className = SELECTORS.SITE_TAB.substring(1);
  287. newTab.textContent = config.tabTitle;
  288. newTab.setAttribute(ATTRS.SCRIPT_ID, config.scriptId);
  289. newTab.setAttribute(ATTRS.MANAGED, 'true');
  290. newTab.setAttribute('title', `${config.tabTitle} (Settings by ${config.scriptId})`);
  291. const desiredOrder = typeof config.order === 'number' ? config.order : Infinity;
  292. newTab.setAttribute(ATTRS.ORDER, desiredOrder);
  293.  
  294. const newPanel = document.createElement('div');
  295. newPanel.className = SELECTORS.SITE_PANEL.substring(1);
  296. newPanel.setAttribute(ATTRS.SCRIPT_ID, config.scriptId);
  297. newPanel.setAttribute(ATTRS.MANAGED, 'true');
  298. newPanel.id = `${MANAGER_ID}-${config.scriptId}-panel`;
  299.  
  300. // --- Insertion Logic (Single Separator & Ordered Tabs) ---
  301. let insertBeforeTab = null;
  302. const existingStmTabs = Array.from(tabContainerEl.querySelectorAll(`span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}]`));
  303. existingStmTabs.sort((a, b) => (parseInt(a.getAttribute(ATTRS.ORDER) || Infinity, 10)) - (parseInt(b.getAttribute(ATTRS.ORDER) || Infinity, 10)));
  304. for (const existingTab of existingStmTabs) {
  305. if (desiredOrder < (parseInt(existingTab.getAttribute(ATTRS.ORDER) || Infinity, 10))) {
  306. insertBeforeTab = existingTab; break;
  307. }
  308. }
  309.  
  310. const isFirstStmTabBeingAdded = existingStmTabs.length === 0;
  311. let separatorInstance = null;
  312. if (!isSeparatorAdded && isFirstStmTabBeingAdded) {
  313. separatorInstance = createSeparator();
  314. isSeparatorAdded = true;
  315. log('Adding the main STM separator.');
  316. }
  317.  
  318. if (insertBeforeTab) {
  319. if (separatorInstance) tabContainerEl.insertBefore(separatorInstance, insertBeforeTab);
  320. tabContainerEl.insertBefore(newTab, insertBeforeTab);
  321. } else {
  322. if (separatorInstance) tabContainerEl.appendChild(separatorInstance);
  323. tabContainerEl.appendChild(newTab);
  324. }
  325. // --- End Insertion Logic ---
  326.  
  327. panelContainerEl.appendChild(newPanel);
  328.  
  329. // --- Initialize Panel Content ---
  330. try {
  331. Promise.resolve(config.onInit(newPanel, newTab)).catch(e => {
  332. error(`Error during async onInit for ${config.scriptId}:`, e);
  333. newPanel.innerHTML = `<p style="color: red;">Error initializing settings panel for ${config.scriptId}. See console.</p>`;
  334. });
  335. } catch (e) {
  336. error(`Error during sync onInit for ${config.scriptId}:`, e);
  337. newPanel.innerHTML = `<p style="color: red;">Error initializing settings panel for ${config.scriptId}. See console.</p>`;
  338. }
  339. }
  340.  
  341. /** Sorts and processes all pending registrations once the manager is initialized. */
  342. function processPendingRegistrations() {
  343. if (!isInitialized) return;
  344. log(`Processing ${pendingRegistrations.length} pending registrations...`);
  345. pendingRegistrations.sort((a, b) => {
  346. const orderA = typeof a.order === 'number' ? a.order : Infinity;
  347. const orderB = typeof b.order === 'number' ? b.order : Infinity;
  348. return orderA - orderB;
  349. });
  350. while (pendingRegistrations.length > 0) {
  351. const config = pendingRegistrations.shift();
  352. if (!registeredTabs.has(config.scriptId)) {
  353. registeredTabs.set(config.scriptId, config);
  354. createTabAndPanel(config);
  355. } else {
  356. warn(`Script ID ${config.scriptId} was already registered. Skipping pending registration.`);
  357. }
  358. }
  359. log('Finished processing pending registrations.');
  360. }
  361.  
  362. // --- Initialization and Observation ---
  363.  
  364. /** Main initialization routine. Finds elements, attaches listener, processes queue. */
  365. function initializeManager() {
  366. if (!findSettingsElements()) { return false; }
  367. if (isInitialized && settingsMenuEl && tabContainerEl && panelContainerEl) {
  368. attachTabClickListener(); return true;
  369. }
  370. log('Initializing Settings Tab Manager...');
  371. attachTabClickListener();
  372. isInitialized = true;
  373. log('Manager is ready.');
  374. resolveReadyPromise(publicApi);
  375. processPendingRegistrations();
  376. return true;
  377. }
  378.  
  379. // --- Mutation Observer ---
  380. const observer = new MutationObserver((mutationsList, obs) => {
  381. let needsReInitCheck = false;
  382. if (!isInitialized && document.querySelector(SELECTORS.SETTINGS_MENU)) { needsReInitCheck = true; }
  383. else if (isInitialized && settingsMenuEl && !document.body.contains(settingsMenuEl)) {
  384. warn('Settings menu seems to have been removed from DOM.');
  385. isInitialized = false; settingsMenuEl = null; tabContainerEl = null; panelContainerEl = null; isSeparatorAdded = false; activeStmTabId = null;
  386. needsReInitCheck = true;
  387. }
  388. if (!settingsMenuEl || needsReInitCheck) {
  389. for (const mutation of mutationsList) {
  390. if (mutation.addedNodes) {
  391. for (const node of mutation.addedNodes) {
  392. if (node.nodeType === Node.ELEMENT_NODE) {
  393. const menu = (node.matches && node.matches(SELECTORS.SETTINGS_MENU)) ? node : (node.querySelector ? node.querySelector(SELECTORS.SETTINGS_MENU) : null);
  394. if (menu) { log('Settings menu detected in DOM via MutationObserver.'); needsReInitCheck = true; break; }
  395. }
  396. }
  397. }
  398. if (needsReInitCheck) break;
  399. }
  400. }
  401. if (needsReInitCheck) { setTimeout(() => { if (initializeManager()) { log('Manager initialized/re-initialized successfully via MutationObserver.'); } }, 0); }
  402. });
  403.  
  404. // Start observing
  405. observer.observe(document.body, { childList: true, subtree: true });
  406. log('Mutation observer started for settings menu detection.');
  407.  
  408. // Attempt initial initialization
  409. setTimeout(initializeManager, 0);
  410.  
  411.  
  412. // --- API Implementation Functions ---
  413.  
  414. /** Public API function to register a new settings tab. */
  415. function registerTabImpl(config) {
  416. // --- Input Validation ---
  417. if (!config || typeof config !== 'object') { error('Registration failed: Invalid config object provided.'); return false; }
  418. const { scriptId, tabTitle, onInit } = config;
  419. if (typeof scriptId !== 'string' || !scriptId.trim()) { error('Registration failed: Invalid or missing scriptId (string).', config); return false; }
  420. if (typeof tabTitle !== 'string' || !tabTitle.trim()) { error('Registration failed: Invalid or missing tabTitle (string).', config); return false; }
  421. if (typeof onInit !== 'function') { error('Registration failed: onInit callback must be a function.', config); return false; }
  422. if (config.onActivate && typeof config.onActivate !== 'function') { error(`Registration for ${scriptId} failed: onActivate (if provided) must be a function.`); return false; }
  423. if (config.onDeactivate && typeof config.onDeactivate !== 'function') { error(`Registration for ${scriptId} failed: onDeactivate (if provided) must be a function.`); return false; }
  424. if (config.order !== undefined && typeof config.order !== 'number') { warn(`Registration for ${scriptId}: Invalid order value provided. Defaulting to end.`, config); delete config.order; }
  425. if (registeredTabs.has(scriptId) || pendingRegistrations.some(p => p.scriptId === scriptId)) { warn(`Registration failed: Script ID "${scriptId}" is already registered or pending.`); return false; }
  426. // --- End Validation ---
  427.  
  428. log(`Registration accepted for: ${scriptId}`);
  429. const registrationData = { ...config }; // Shallow clone
  430.  
  431. if (isInitialized) {
  432. registeredTabs.set(scriptId, registrationData);
  433. createTabAndPanel(registrationData);
  434. } else {
  435. log(`Manager not ready, queueing registration for ${scriptId}`);
  436. pendingRegistrations.push(registrationData);
  437. pendingRegistrations.sort((a, b) => { // Keep queue sorted
  438. const orderA = typeof a.order === 'number' ? a.order : Infinity;
  439. const orderB = typeof b.order === 'number' ? b.order : Infinity;
  440. return orderA - orderB;
  441. });
  442. }
  443. return true; // Registration accepted
  444. }
  445.  
  446. /** Public API function to programmatically activate a registered tab. */
  447. function activateTabImpl(scriptId) {
  448. // Add trace for debugging client script calls
  449. console.trace(`[${MANAGER_ID}] activateTabImpl called with ID: ${scriptId}`);
  450.  
  451. if (typeof scriptId !== 'string' || !scriptId.trim()) {
  452. error('activateTab failed: Invalid scriptId provided.'); return;
  453. }
  454. if (!isInitialized) {
  455. warn(`Cannot activate tab ${scriptId} yet, manager not initialized.`); return;
  456. }
  457. if (!registeredTabs.has(scriptId)) {
  458. error(`activateTab failed: Script ID "${scriptId}" is not registered.`); return;
  459. }
  460. if (scriptId === activeStmTabId) {
  461. // log(`activateTab: Tab ${scriptId} is already active.`);
  462. return; // Already active, do nothing
  463. }
  464.  
  465. // --- Deactivate previous tab (if any) ---
  466. const previousActiveStmId = activeStmTabId; // Store the ID before changing state
  467. if (previousActiveStmId) {
  468. _deactivateStmTabVisualsAndCallback(previousActiveStmId);
  469. } else {
  470. // If no STM tab was active, ensure any *native* site tab is visually deactivated
  471. panelContainerEl?.querySelectorAll(`:scope > ${SELECTORS.SITE_PANEL}.${ACTIVE_CLASSES.PANEL}:not([${ATTRS.MANAGED}])`).forEach(p => p.classList.remove(ACTIVE_CLASSES.PANEL));
  472. tabContainerEl?.querySelectorAll(`:scope > ${SELECTORS.SITE_TAB}.${ACTIVE_CLASSES.TAB}:not([${ATTRS.MANAGED}])`).forEach(t => t.classList.remove(ACTIVE_CLASSES.TAB));
  473. }
  474.  
  475. // --- Activate the requested STM tab ---
  476. if (_activateStmTabVisualsAndCallback(scriptId)) {
  477. activeStmTabId = scriptId; // Update the state *after* successful activation attempt
  478. log(`Programmatically activated tab: ${scriptId}`);
  479. } else {
  480. // Activation failed (elements missing?)
  481. warn(`Programmatic activation failed for tab: ${scriptId}`);
  482. // Attempt to restore previous state? Or leave as is? Leave as is for now.
  483. // If previousActiveStmId existed, maybe try to reactivate it? Complex.
  484. activeStmTabId = null; // Set to null if activation fails
  485. }
  486. }
  487.  
  488. /** Public API function to get the DOM element for a tab's panel. */
  489. function getPanelElementImpl(scriptId) {
  490. if (!isInitialized || !panelContainerEl) return null;
  491. if (typeof scriptId !== 'string' || !scriptId.trim()) return null;
  492. return panelContainerEl.querySelector(`div[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}="${scriptId}"]`);
  493. }
  494.  
  495. /** Public API function to get the DOM element for a tab's clickable part. */
  496. function getTabElementImpl(scriptId) {
  497. if (!isInitialized || !tabContainerEl) return null;
  498. if (typeof scriptId !== 'string' || !scriptId.trim()) return null;
  499. return tabContainerEl.querySelector(`span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}="${scriptId}"]`);
  500. }
  501.  
  502. // --- Global Exposure ---
  503. if (window.SettingsTabManager && window.SettingsTabManager !== publicApi) {
  504. warn('window.SettingsTabManager is already defined by another script or instance! Potential conflict.');
  505. } else if (!window.SettingsTabManager) {
  506. Object.defineProperty(window, 'SettingsTabManager', {
  507. value: publicApi,
  508. writable: false,
  509. configurable: true
  510. });
  511. log('SettingsTabManager API exposed on window.');
  512. }
  513.  
  514. })(); // End of IIFE