Settings Tab Manager (STM)

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

当前为 2025-04-23 提交的版本,查看 最新版本

此脚本不应直接安装,它是供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/533630/1575927/Settings%20Tab%20Manager%20%28STM%29.js

  1. // ==UserScript==
  2. // @name Settings Tab Manager (STM)
  3. // @namespace shared-settings-manager
  4. // @version 1.1.2
  5. // @description Provides an API for other userscripts to add tabs to a site's settings menu, with a single separator.
  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. const MANAGER_ID = 'SettingsTabManager';
  18. const log = (...args) => console.log(`[${MANAGER_ID}]`, ...args);
  19. const warn = (...args) => console.warn(`[${MANAGER_ID}]`, ...args);
  20. const error = (...args) => console.error(`[${MANAGER_ID}]`, ...args);
  21.  
  22. // --- Configuration ---
  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', // Keep this if the site uses it visually
  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', // Attribute for the single separator
  39. ORDER: 'data-stm-order', // Attribute to store the order on the tab element
  40. });
  41.  
  42. // --- State ---
  43. let isInitialized = false;
  44. let settingsMenuEl = null;
  45. let tabContainerEl = null;
  46. let panelContainerEl = null;
  47. let activeTabId = null;
  48. const registeredTabs = new Map(); // Stores { scriptId: config } for registered tabs
  49. const pendingRegistrations = []; // Stores { config } for tabs registered before init
  50. let isSeparatorAdded = false; // Flag to ensure only one separator is added
  51.  
  52. // --- Readiness Promise ---
  53. let resolveReadyPromise;
  54. const readyPromise = new Promise(resolve => {
  55. resolveReadyPromise = resolve;
  56. });
  57.  
  58. // --- Public API Definition ---
  59. const publicApi = Object.freeze({
  60. ready: readyPromise,
  61. registerTab: (config) => {
  62. return registerTabImpl(config);
  63. },
  64. activateTab: (scriptId) => {
  65. activateTabImpl(scriptId);
  66. },
  67. getPanelElement: (scriptId) => {
  68. return getPanelElementImpl(scriptId);
  69. },
  70. getTabElement: (scriptId) => {
  71. return getTabElementImpl(scriptId);
  72. }
  73. });
  74.  
  75. // --- Styling ---
  76. GM_addStyle(`
  77. /* Ensure panels added by STM behave like native ones */
  78. ${SELECTORS.PANEL_CONTAINER} > div[${ATTRS.MANAGED}] {
  79. display: none; /* Hide inactive panels */
  80. }
  81. ${SELECTORS.PANEL_CONTAINER} > div[${ATTRS.MANAGED}].${ACTIVE_CLASSES.PANEL} {
  82. display: block; /* Show active panel */
  83. }
  84. /* Optional: Basic styling for the added tabs */
  85. ${SELECTORS.TAB_CONTAINER} > span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}] {
  86. cursor: pointer;
  87. }
  88. /* Styling for the single separator */
  89. ${SELECTORS.TAB_CONTAINER} > span[${ATTRS.SEPARATOR}] {
  90. cursor: default;
  91. margin: 0 5px; /* Add some spacing around the separator */
  92. }
  93. `);
  94.  
  95. // --- Core Logic Implementation Functions ---
  96.  
  97. /** Finds the essential DOM elements for the settings UI. Returns true if all found. */
  98. function findSettingsElements() {
  99. settingsMenuEl = document.querySelector(SELECTORS.SETTINGS_MENU);
  100. if (!settingsMenuEl) return false;
  101.  
  102. tabContainerEl = settingsMenuEl.querySelector(SELECTORS.TAB_CONTAINER);
  103. panelContainerEl = settingsMenuEl.querySelector(SELECTORS.PANEL_CONTAINER);
  104.  
  105. if (!tabContainerEl) {
  106. warn('Tab container not found within settings menu using selector:', SELECTORS.TAB_CONTAINER);
  107. return false;
  108. }
  109. if (!panelContainerEl) {
  110. warn('Panel container not found within settings menu using selector:', SELECTORS.PANEL_CONTAINER);
  111. return false;
  112. }
  113. // Ensure the elements are still in the document (relevant for re-init checks)
  114. if (!document.body.contains(settingsMenuEl) || !document.body.contains(tabContainerEl) || !document.body.contains(panelContainerEl)) {
  115. warn('Found settings elements are detached from the DOM.');
  116. settingsMenuEl = null;
  117. tabContainerEl = null;
  118. panelContainerEl = null;
  119. isSeparatorAdded = false; // Reset separator if containers are gone
  120. return false;
  121. }
  122. return true;
  123. }
  124.  
  125. /** Deactivates the currently active STM tab (if any) and ensures native tabs are also visually deactivated. */
  126. function deactivateCurrentTab() {
  127. // Deactivate STM-managed tab
  128. if (activeTabId && registeredTabs.has(activeTabId)) {
  129. const config = registeredTabs.get(activeTabId);
  130. const tab = getTabElementImpl(activeTabId); // Use API getter
  131. const panel = getPanelElementImpl(activeTabId); // Use API getter
  132.  
  133. tab?.classList.remove(ACTIVE_CLASSES.TAB);
  134. panel?.classList.remove(ACTIVE_CLASSES.PANEL);
  135. if (panel) panel.style.display = 'none'; // Explicitly hide panel via style
  136.  
  137. try {
  138. config.onDeactivate?.(panel, tab);
  139. } catch (e) {
  140. error(`Error during onDeactivate for ${activeTabId}:`, e);
  141. }
  142. }
  143. activeTabId = null; // Clear active STM tab ID
  144.  
  145. // Remove active class from any site tabs/panels managed outside STM
  146. panelContainerEl?.querySelectorAll(`:scope > ${SELECTORS.SITE_PANEL}.${ACTIVE_CLASSES.PANEL}:not([${ATTRS.MANAGED}])`)
  147. .forEach(p => p.classList.remove(ACTIVE_CLASSES.PANEL));
  148. tabContainerEl?.querySelectorAll(`:scope > ${SELECTORS.SITE_TAB}.${ACTIVE_CLASSES.TAB}:not([${ATTRS.MANAGED}])`)
  149. .forEach(t => t.classList.remove(ACTIVE_CLASSES.TAB));
  150. }
  151.  
  152. /** Internal implementation for activating a specific STM tab */
  153. function activateTab(scriptId) {
  154. if (!registeredTabs.has(scriptId) || !tabContainerEl || !panelContainerEl) {
  155. warn(`Cannot activate tab: ${scriptId}. Not registered or containers not found.`);
  156. return;
  157. }
  158.  
  159. if (activeTabId === scriptId) return; // Already active, do nothing
  160.  
  161. deactivateCurrentTab(); // Deactivate previous one first (handles both STM and native)
  162.  
  163. const config = registeredTabs.get(scriptId);
  164. const tab = getTabElementImpl(scriptId);
  165. const panel = getPanelElementImpl(scriptId);
  166.  
  167. if (!tab || !panel) {
  168. error(`Tab or Panel element not found for ${scriptId} during activation.`);
  169. return;
  170. }
  171.  
  172. tab.classList.add(ACTIVE_CLASSES.TAB);
  173. panel.classList.add(ACTIVE_CLASSES.PANEL);
  174. panel.style.display = 'block'; // Ensure panel is visible via style
  175.  
  176. activeTabId = scriptId; // Set the new active STM tab ID
  177.  
  178. try {
  179. config.onActivate?.(panel, tab);
  180. } catch (e) {
  181. error(`Error during onActivate for ${scriptId}:`, e);
  182. }
  183. }
  184.  
  185. /** Handles clicks within the tab container to switch tabs. */
  186. function handleTabClick(event) {
  187. // Check if an STM-managed tab was clicked
  188. const clickedStmTab = event.target.closest(`span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}]`);
  189. if (clickedStmTab) {
  190. event.stopPropagation(); // Prevent potential conflicts with site listeners
  191. const scriptId = clickedStmTab.getAttribute(ATTRS.SCRIPT_ID);
  192. if (scriptId && scriptId !== activeTabId) {
  193. activateTab(scriptId); // Activate the clicked STM tab
  194. }
  195. return; // Handled
  196. }
  197.  
  198. // Check if a native site tab was clicked (that isn't also an STM tab)
  199. const clickedSiteTab = event.target.closest(`${SELECTORS.SITE_TAB}:not([${ATTRS.MANAGED}])`);
  200. if (clickedSiteTab) {
  201. // If a native tab is clicked, ensure any active STM tab is deactivated
  202. if (activeTabId) {
  203. deactivateCurrentTab();
  204. // Note: We rely on the site's own JS to handle activating the native tab/panel visually
  205. }
  206. return; // Let the site handle its own tab activation
  207. }
  208.  
  209. // Check if the separator was clicked (do nothing)
  210. if (event.target.closest(`span[${ATTRS.SEPARATOR}]`)) {
  211. event.stopPropagation();
  212. return;
  213. }
  214. }
  215.  
  216.  
  217. /** Attaches the main click listener to the tab container. */
  218. function attachTabClickListener() {
  219. if (!tabContainerEl) return;
  220. // Use capture phase to potentially handle clicks before site's listeners if needed
  221. tabContainerEl.removeEventListener('click', handleTabClick, true);
  222. tabContainerEl.addEventListener('click', handleTabClick, true);
  223. log('Tab click listener attached.');
  224. }
  225.  
  226. /** Helper to create the SINGLE separator span */
  227. function createSeparator() {
  228. const separator = document.createElement('span');
  229. // Use the site's separator class if defined, otherwise a fallback
  230. separator.className = SELECTORS.SITE_SEPARATOR ? SELECTORS.SITE_SEPARATOR.substring(1) : 'settings-tab-separator-fallback';
  231. separator.setAttribute(ATTRS.MANAGED, 'true'); // Mark as managed by STM (helps selection)
  232. separator.setAttribute(ATTRS.SEPARATOR, 'true'); // Mark as the main separator
  233. separator.textContent = '|'; // Or any desired visual separator
  234. // Style is handled by GM_addStyle now
  235. return separator;
  236. }
  237.  
  238. /** Creates and inserts the tab and panel elements for a given script config. */
  239. function createTabAndPanel(config) {
  240. if (!tabContainerEl || !panelContainerEl) {
  241. error(`Cannot create tab/panel for ${config.scriptId}: Containers not found.`);
  242. return;
  243. }
  244. // Check if tab *element* exists already
  245. if (tabContainerEl.querySelector(`span[${ATTRS.SCRIPT_ID}="${config.scriptId}"]`)) {
  246. log(`Tab element already exists for ${config.scriptId}, skipping creation.`);
  247. return; // Avoid duplicates if registration happens multiple times somehow
  248. }
  249.  
  250. log(`Creating tab/panel for: ${config.scriptId}`);
  251.  
  252. // --- Create Tab ---
  253. const newTab = document.createElement('span');
  254. newTab.className = SELECTORS.SITE_TAB.substring(1); // Use site's base tab class
  255. newTab.textContent = config.tabTitle;
  256. newTab.setAttribute(ATTRS.SCRIPT_ID, config.scriptId);
  257. newTab.setAttribute(ATTRS.MANAGED, 'true');
  258. newTab.setAttribute('title', `${config.tabTitle} (Settings by ${config.scriptId})`);
  259. const desiredOrder = typeof config.order === 'number' ? config.order : Infinity;
  260. newTab.setAttribute(ATTRS.ORDER, desiredOrder); // Store order attribute
  261.  
  262. // --- Create Panel ---
  263. const newPanel = document.createElement('div');
  264. newPanel.className = SELECTORS.SITE_PANEL.substring(1); // Use site's base panel class
  265. newPanel.setAttribute(ATTRS.SCRIPT_ID, config.scriptId);
  266. newPanel.setAttribute(ATTRS.MANAGED, 'true');
  267. newPanel.id = `${MANAGER_ID}-${config.scriptId}-panel`; // Unique ID for the panel
  268.  
  269. // --- Insertion Logic (Single Separator & Ordered Tabs) ---
  270. let insertBeforeTab = null; // The specific STM tab element to insert *before*
  271. const existingStmTabs = Array.from(tabContainerEl.querySelectorAll(`span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}]`));
  272.  
  273. // Sort existing STM tabs DOM elements by their order attribute to find the correct insertion point
  274. existingStmTabs.sort((a, b) => {
  275. const orderA = parseInt(a.getAttribute(ATTRS.ORDER) || Infinity, 10);
  276. const orderB = parseInt(b.getAttribute(ATTRS.ORDER) || Infinity, 10);
  277. return orderA - orderB;
  278. });
  279.  
  280. // Find the first existing STM tab with a higher order number than the new tab
  281. for (const existingTab of existingStmTabs) {
  282. const existingOrder = parseInt(existingTab.getAttribute(ATTRS.ORDER) || Infinity, 10);
  283. if (desiredOrder < existingOrder) {
  284. insertBeforeTab = existingTab;
  285. break;
  286. }
  287. }
  288.  
  289. // Check if this is the very first STM tab being added *to the DOM*
  290. const isFirstStmTabBeingAdded = existingStmTabs.length === 0;
  291. let separatorInstance = null;
  292.  
  293. // Add the single separator *only* if it hasn't been added yet AND this is the first STM tab going into the DOM
  294. if (!isSeparatorAdded && isFirstStmTabBeingAdded) {
  295. separatorInstance = createSeparator(); // Create the single separator instance
  296. isSeparatorAdded = true; // Mark it as added so it doesn't happen again
  297. log('Adding the main STM separator.');
  298. }
  299.  
  300. // Insert the separator (if created) and then the tab
  301. if (insertBeforeTab) {
  302. // Insert before a specific existing STM tab
  303. if (separatorInstance) {
  304. // This case should technically not happen if separator is only added for the *first* tab,
  305. // but we keep it for robustness. It means we are inserting the *first* tab before another.
  306. tabContainerEl.insertBefore(separatorInstance, insertBeforeTab);
  307. }
  308. tabContainerEl.insertBefore(newTab, insertBeforeTab);
  309. } else {
  310. // Append after all existing STM tabs (or as the very first STM elements)
  311. if (separatorInstance) {
  312. // Append the separator first because this is the first STM tab
  313. tabContainerEl.appendChild(separatorInstance);
  314. }
  315. // Append the new tab (either after the new separator or after the last existing STM tab)
  316. tabContainerEl.appendChild(newTab);
  317. }
  318.  
  319. // Append Panel to the panel container
  320. panelContainerEl.appendChild(newPanel);
  321.  
  322. // --- Initialize Panel Content ---
  323. // Use Promise.resolve to handle both sync and async onInit functions gracefully
  324. try {
  325. Promise.resolve(config.onInit(newPanel, newTab)).catch(e => {
  326. error(`Error during async onInit for ${config.scriptId}:`, e);
  327. newPanel.innerHTML = `<p style="color: red;">Error initializing settings panel for ${config.scriptId}. See console.</p>`;
  328. });
  329. } catch (e) { // Catch synchronous errors from onInit
  330. error(`Error during sync onInit for ${config.scriptId}:`, e);
  331. newPanel.innerHTML = `<p style="color: red;">Error initializing settings panel for ${config.scriptId}. See console.</p>`;
  332. }
  333. }
  334.  
  335. /** Sorts and processes all pending registrations once the manager is initialized. */
  336. function processPendingRegistrations() {
  337. if (!isInitialized) return; // Should not happen if called correctly, but safety check
  338. log(`Processing ${pendingRegistrations.length} pending registrations...`);
  339.  
  340. // Sort pending registrations by 'order' BEFORE creating elements
  341. pendingRegistrations.sort((a, b) => {
  342. const orderA = typeof a.order === 'number' ? a.order : Infinity;
  343. const orderB = typeof b.order === 'number' ? b.order : Infinity;
  344. // If orders are equal, maintain original registration order (stable sort behavior desired)
  345. return orderA - orderB;
  346. });
  347.  
  348. // Process the sorted queue
  349. while (pendingRegistrations.length > 0) {
  350. const config = pendingRegistrations.shift(); // Get the next config from the front
  351. if (!registeredTabs.has(config.scriptId)) {
  352. registeredTabs.set(config.scriptId, config); // Add to map first
  353. // createTabAndPanel will handle DOM insertion and separator logic
  354. createTabAndPanel(config);
  355. } else {
  356. // This case should be rare now due to checks in registerTabImpl, but good to keep
  357. warn(`Script ID ${config.scriptId} was already registered. Skipping pending registration.`);
  358. }
  359. }
  360. log('Finished processing pending registrations.');
  361. }
  362.  
  363. // --- Initialization and Observation ---
  364.  
  365. /** Main initialization routine. Finds elements, attaches listener, processes queue. */
  366. function initializeManager() {
  367. if (!findSettingsElements()) {
  368. // log('Settings elements not found or invalid on init check.');
  369. return false; // Keep observer active
  370. }
  371.  
  372. // Check if already initialized *and* elements are still valid
  373. if (isInitialized && settingsMenuEl && tabContainerEl && panelContainerEl) {
  374. // log('Manager already initialized and elements seem valid.');
  375. attachTabClickListener(); // Re-attach listener just in case it got removed
  376. return true;
  377. }
  378.  
  379. log('Initializing Settings Tab Manager...');
  380. attachTabClickListener(); // Attach the main click listener
  381.  
  382. isInitialized = true; // Set flag *before* resolving promise and processing queue
  383. log('Manager is ready.');
  384.  
  385. // Resolve the public promise *after* setting isInitialized flag
  386. // This signals to waiting scripts that the manager is ready
  387. resolveReadyPromise(publicApi);
  388.  
  389. // Process any registrations that occurred before initialization was complete
  390. processPendingRegistrations();
  391.  
  392. return true; // Initialization successful
  393. }
  394.  
  395. // --- Mutation Observer ---
  396. // Observes the body for the appearance of the settings menu
  397. const observer = new MutationObserver((mutationsList, obs) => {
  398. let needsReInitCheck = false;
  399.  
  400. // 1. Check if the menu exists but we aren't initialized yet
  401. if (!isInitialized && document.querySelector(SELECTORS.SETTINGS_MENU)) {
  402. needsReInitCheck = true;
  403. }
  404. // 2. Check if the menu *was* found previously but is no longer in the DOM
  405. else if (isInitialized && settingsMenuEl && !document.body.contains(settingsMenuEl)) {
  406. warn('Settings menu seems to have been removed from DOM.');
  407. isInitialized = false; // Force re-initialization if it reappears
  408. settingsMenuEl = null;
  409. tabContainerEl = null;
  410. panelContainerEl = null;
  411. isSeparatorAdded = false; // Reset separator flag
  412. activeTabId = null;
  413. // Don't resolve the promise again, but new scripts might await it
  414. // Reset the promise state if needed (more complex, usually not necessary)
  415. // readyPromise = new Promise(resolve => { resolveReadyPromise = resolve; });
  416. needsReInitCheck = true; // Check if it got immediately re-added
  417. }
  418.  
  419. // 3. Scan mutations if we haven't found the menu yet or if it was removed
  420. if (!settingsMenuEl || needsReInitCheck) {
  421. for (const mutation of mutationsList) {
  422. if (mutation.addedNodes) {
  423. for (const node of mutation.addedNodes) {
  424. if (node.nodeType === Node.ELEMENT_NODE) {
  425. // Check if the added node *is* the menu or *contains* the menu
  426. const menu = (node.matches && node.matches(SELECTORS.SETTINGS_MENU))
  427. ? node
  428. : (node.querySelector ? node.querySelector(SELECTORS.SETTINGS_MENU) : null);
  429. if (menu) {
  430. log('Settings menu detected in DOM via MutationObserver.');
  431. needsReInitCheck = true;
  432. break; // Found it in this mutation
  433. }
  434. }
  435. }
  436. }
  437. if (needsReInitCheck) break; // Found it, no need to check other mutations
  438. }
  439. }
  440.  
  441. // 4. Attempt initialization if needed
  442. if (needsReInitCheck) {
  443. // Use setTimeout to allow the DOM to potentially stabilize after the mutation
  444. setTimeout(() => {
  445. if (initializeManager()) {
  446. log('Manager initialized/re-initialized successfully via MutationObserver.');
  447. // Optional: If the settings menu is known to be stable once added,
  448. // you *could* disconnect the observer here to save resources.
  449. // However, leaving it active is safer if the menu might be rebuilt.
  450. // obs.disconnect();
  451. // log('Mutation observer disconnected.');
  452. } else {
  453. // log('Initialization check failed after mutation, observer remains active.');
  454. }
  455. }, 0); // Delay slightly
  456. }
  457. });
  458.  
  459. // Start observing the body for additions/removals in the subtree
  460. observer.observe(document.body, {
  461. childList: true, // Watch for direct children changes (adding/removing nodes)
  462. subtree: true // Watch the entire subtree under document.body
  463. });
  464. log('Mutation observer started for settings menu detection.');
  465.  
  466. // --- Attempt initial initialization on script load ---
  467. // Use setTimeout to ensure this runs after the current execution context,
  468. // allowing the global API exposure to happen first.
  469. setTimeout(initializeManager, 0);
  470.  
  471.  
  472. // --- API Implementation Functions ---
  473.  
  474. /** Public API function to register a new settings tab. */
  475. function registerTabImpl(config) {
  476. // --- Input Validation ---
  477. if (!config || typeof config !== 'object') {
  478. error('Registration failed: Invalid config object provided.'); return false;
  479. }
  480. const { scriptId, tabTitle, onInit } = config;
  481. if (typeof scriptId !== 'string' || !scriptId.trim()) {
  482. error('Registration failed: Invalid or missing scriptId (string).', config); return false;
  483. }
  484. if (typeof tabTitle !== 'string' || !tabTitle.trim()) {
  485. error('Registration failed: Invalid or missing tabTitle (string).', config); return false;
  486. }
  487. if (typeof onInit !== 'function') {
  488. error('Registration failed: onInit callback must be a function.', config); return false;
  489. }
  490. // Optional callbacks validation
  491. if (config.onActivate && typeof config.onActivate !== 'function') {
  492. error(`Registration for ${scriptId} failed: onActivate (if provided) must be a function.`); return false;
  493. }
  494. if (config.onDeactivate && typeof config.onDeactivate !== 'function') {
  495. error(`Registration for ${scriptId} failed: onDeactivate (if provided) must be a function.`); return false;
  496. }
  497. // Optional order validation
  498. if (config.order !== undefined && typeof config.order !== 'number') {
  499. warn(`Registration for ${scriptId}: Invalid order value provided (must be a number). Defaulting to end.`, config);
  500. // Allow registration but ignore invalid order
  501. delete config.order;
  502. }
  503.  
  504. // Prevent duplicate registrations by scriptId
  505. if (registeredTabs.has(scriptId) || pendingRegistrations.some(p => p.scriptId === scriptId)) {
  506. warn(`Registration failed: Script ID "${scriptId}" is already registered or pending.`); return false;
  507. }
  508.  
  509. // --- Registration Logic ---
  510. log(`Registration accepted for: ${scriptId}`);
  511. // Clone config to avoid external modification? (Shallow clone is usually sufficient)
  512. const registrationData = { ...config };
  513.  
  514. if (isInitialized) {
  515. // Manager ready: Register immediately and create elements
  516. registeredTabs.set(scriptId, registrationData);
  517. // createTabAndPanel handles sorting/insertion/separator logic
  518. createTabAndPanel(registrationData);
  519. } else {
  520. // Manager not ready: Add to pending queue
  521. log(`Manager not ready, queueing registration for ${scriptId}`);
  522. pendingRegistrations.push(registrationData);
  523. // Sort pending queue immediately upon adding to maintain correct insertion order later
  524. pendingRegistrations.sort((a, b) => {
  525. const orderA = typeof a.order === 'number' ? a.order : Infinity;
  526. const orderB = typeof b.order === 'number' ? b.order : Infinity;
  527. return orderA - orderB;
  528. });
  529. }
  530. return true; // Indicate successful acceptance of registration (not necessarily immediate creation)
  531. }
  532.  
  533. /** Public API function to programmatically activate a registered tab. */
  534. function activateTabImpl(scriptId) {
  535. if (typeof scriptId !== 'string' || !scriptId.trim()) {
  536. error('activateTab failed: Invalid scriptId provided.');
  537. return;
  538. }
  539. if (isInitialized) {
  540. // Call the internal function which handles the logic
  541. activateTab(scriptId);
  542. } else {
  543. // Queue activation? Or just warn? Warning is simpler.
  544. warn(`Cannot activate tab ${scriptId} yet, manager not initialized.`);
  545. // Could potentially store a desired initial tab and activate it in processPendingRegistrations
  546. }
  547. }
  548.  
  549. /** Public API function to get the DOM element for a tab's panel. */
  550. function getPanelElementImpl(scriptId) {
  551. if (!isInitialized || !panelContainerEl) return null;
  552. if (typeof scriptId !== 'string' || !scriptId.trim()) return null;
  553. // Use attribute selector for robustness
  554. return panelContainerEl.querySelector(`div[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}="${scriptId}"]`);
  555. }
  556.  
  557. /** Public API function to get the DOM element for a tab's clickable part. */
  558. function getTabElementImpl(scriptId) {
  559. if (!isInitialized || !tabContainerEl) return null;
  560. if (typeof scriptId !== 'string' || !scriptId.trim()) return null;
  561. // Use attribute selector for robustness
  562. return tabContainerEl.querySelector(`span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}="${scriptId}"]`);
  563. }
  564.  
  565.  
  566. // --- Global Exposure ---
  567. // Expose the public API on the window object, checking for conflicts.
  568. if (window.SettingsTabManager && window.SettingsTabManager !== publicApi) {
  569. // A different instance or script already exists. Log a warning.
  570. // Depending on requirements, could throw an error, merge APIs, or namespace.
  571. warn('window.SettingsTabManager is already defined by another script or instance! Potential conflict.');
  572. } else if (!window.SettingsTabManager) {
  573. // Define the API object on the window, making it non-writable but configurable.
  574. Object.defineProperty(window, 'SettingsTabManager', {
  575. value: publicApi,
  576. writable: false, // Prevents accidental overwriting of the API object itself
  577. configurable: true // Allows users to delete or redefine it if necessary (e.g., for debugging)
  578. });
  579. log('SettingsTabManager API exposed on window.');
  580. }
  581.  
  582. })(); // End of IIFE