// TabsExtension — организация вкладок расширений + поиск по вкладкам // ECMAScript 2020+, A1111/Forge совместим class TabsExtension { /** @type {TabsExtensionConfigs} */ static #config; /** Активные вкладки по режимам */ /** @type {Object.} */ static #activeExtension = { "txt": null, "img": null }; /** Пары "чекбокс включения" <-> "кнопка вкладки" для подсветки active */ /** @type {Array.<[HTMLInputElement, HTMLButtonElement]>} */ static #enablePairs = []; /** Таймер отложенного обновления подсветки active */ /** @type {number|null} */ static #refreshQueue = null; /** @param {HTMLDivElement} extension @returns {HTMLInputElement|null} */ static #tryFindEnableToggle(extension) { let temp = null; for (const checkbox of extension.querySelectorAll('input[type=checkbox]')) { const labelText = checkbox.parentNode?.querySelector('span')?.textContent?.toLowerCase(); if (labelText?.includes('enable')) return checkbox; if (!temp && labelText?.includes('active')) temp = checkbox; } return temp; } /** Синхронизация active-подсветки кнопок с реальным состоянием чекбоксов */ static #refreshEnableCheckbox() { if (this.#refreshQueue) clearTimeout(this.#refreshQueue); this.#refreshQueue = window.setTimeout(() => { for (const [toggle, button] of this.#enablePairs) { if (toggle.checked) button.classList.add('active'); else button.classList.remove('active'); } }, 250); } /** * Сортировка расширений по конфигу, если включено * @param {Object.} extensions * @param {Object.} configs * @returns {Object.} */ static #sort_extensions(extensions, configs) { if (!this.#config.sort) return extensions; const sorted = {}; for (const key of Object.keys(configs)) { if (Object.prototype.hasOwnProperty.call(extensions, key)) { sorted[key] = extensions[key]; delete extensions[key]; } } for (const key of Object.keys(extensions)) sorted[key] = extensions[key]; return sorted; } /** * Главная раскладка * @param {'txt'|'img'} mode * @param {Object.} extensions * @param {Object.} configs * @returns {Object.} */ static #setup_tabs(mode, extensions, configs) { const container = { 'left': document.getElementById(`${mode}2img_script_container`), 'right': document.getElementById(`${mode}2img_results`) }; /** @type {string} */ const mainSide = configs['tabs']; /** @type {string} */ const oppSide = (mainSide === 'left') ? 'right' : 'left'; // Если вдруг контейнеров нет — выходим аккуратно if (!container[mainSide] || !container[oppSide]) return configs; // Заготовим области const extensionContainers = {}; for (const side of ['above', 'left', 'below', 'right']) { const div = document.createElement("div"); div.id = `tabs_ex_content-${mode}2img-${side}`; div.style.overflow = "visible"; extensionContainers[side] = div; } const buttonContainer = document.createElement("div"); extensionContainers[mainSide].appendChild(buttonContainer); buttonContainer.id = `tabs_ex_${mode}`; container[mainSide].appendChild(extensionContainers['above']); container[mainSide].appendChild(extensionContainers[mainSide]); container[mainSide].appendChild(extensionContainers['below']); container[oppSide].appendChild(extensionContainers[oppSide]); /** Карта: ключ вкладки -> кнопка */ /** @type {Object.} */ const allButtons = {}; // === Поиск по вкладкам (компактный) === const searchWrapper = document.createElement("div"); searchWrapper.style.display = "flex"; searchWrapper.style.justifyContent = "center"; searchWrapper.style.width = "100%"; searchWrapper.style.marginBottom = "6px"; const searchInput = document.createElement("input"); searchInput.type = "text"; searchInput.placeholder = "Filter extensions..."; searchInput.autocomplete = "off"; searchInput.style.width = "100%"; searchInput.style.maxWidth = "480px"; // чтобы не расползался как на скрине searchInput.style.minWidth = "240px"; searchInput.style.padding = "6px 10px"; searchInput.style.borderRadius = "6px"; searchInput.style.border = "1px solid var(--block-border-color, #444)"; searchWrapper.appendChild(searchInput); buttonContainer.appendChild(searchWrapper); const self = this; // нужен доступ к #activeExtension внутри коллбэка const applyFilter = (queryText) => { const q = (queryText || "").trim().toLowerCase(); for (const [tabKey, btn] of Object.entries(allButtons)) { const label = (btn.textContent || "").toLowerCase(); btn.style.display = (q.length === 0 || label.includes(q)) ? "" : "none"; } // если активная вкладка скрыта фильтром — мягко гасим её контент const active = self.#activeExtension[mode]; if (active && allButtons[active] && allButtons[active].style.display === "none") { allButtons[active].classList.remove('selected'); if (extensions[active]) extensions[active].style.display = "none"; self.#activeExtension[mode] = null; } }; searchInput.addEventListener("input", (e) => applyFilter(e.target.value)); // Кнопки и раскладка for (const tabKey of Object.keys(extensions)) { if (!Object.prototype.hasOwnProperty.call(configs, tabKey)) { configs[tabKey] = configs['default']; // новое расширение — дефолтная зона } const pos = configs[tabKey]; if (pos === "hide") continue; if (pos === "above" || pos === "below") { extensionContainers[pos].appendChild(extensions[tabKey]); extensions[tabKey].style.display = "block"; continue; } // Подпись кнопки const btnSpan = document.createElement('span'); btnSpan.className = 'tab_label'; const extensionName = (this.#config.version) ? tabKey : extensions[tabKey].getAttribute("ext-label"); btnSpan.textContent = (!this.#config.forge) ? extensionName : (extensionName || "").split('Integrated')[0].trim(); // Кнопка вкладки const tabButton = document.createElement("button"); tabButton.classList.add('tab_button'); tabButton.appendChild(btnSpan); buttonContainer.appendChild(tabButton); allButtons[tabKey] = tabButton; tabButton.addEventListener("click", (e) => { // режим RMB: Ctrl/Cmd не перехватываем if (!this.#config.rmb && (e.ctrlKey || e.metaKey)) return; if (this.#activeExtension[mode] != null) { allButtons[this.#activeExtension[mode]]?.classList.remove('selected'); const prev = this.#activeExtension[mode]; if (prev && extensions[prev]) extensions[prev].style.display = "none"; } this.#activeExtension[mode] = ( (this.#config.toggle) && (this.#activeExtension[mode] === tabKey) ) ? null : tabKey; if (this.#activeExtension[mode] != null) { allButtons[this.#activeExtension[mode]]?.classList.add('selected'); extensions[this.#activeExtension[mode]].style.display = "block"; } }); // Переносим содержимое вкладки в нужную сторону extensionContainers[configs[tabKey]].appendChild(extensions[tabKey]); // Особый случай: Scripts if (tabKey === 'Scripts') { const scriptsDropdown = extensions[tabKey].querySelector('input'); const observer = new MutationObserver(() => { if (!scriptsDropdown) return; if (scriptsDropdown.value !== 'None') allButtons['Scripts']?.classList.add('active'); else allButtons['Scripts']?.classList.remove('active'); }); observer.observe(extensions[tabKey], { childList: true, subtree: true }); if (this.#config.scripts_toggle) { const btn = document.getElementById(`TABSEX_${mode}2img_s_toggle`); if (btn) { if (this.#config.rmb) { allButtons[tabKey].addEventListener("contextmenu", (e) => { e.preventDefault(); btn.click(); return false; }); } else { allButtons[tabKey].addEventListener("click", (e) => { if (e.ctrlKey || e.metaKey) btn.click(); }); } } } continue; } // Подсветка активных по чекбоксу Enable/Active const enableToggle = this.#tryFindEnableToggle(extensions[tabKey]); if (!enableToggle) continue; if (this.#config.rmb) { allButtons[tabKey].addEventListener("contextmenu", (e) => { e.preventDefault(); enableToggle.click(); return false; }); } else { allButtons[tabKey].addEventListener("click", (e) => { if (e.ctrlKey || e.metaKey) enableToggle.click(); }); } if (enableToggle.checked) allButtons[tabKey].classList.add('active'); this.#enablePairs.push([enableToggle, allButtons[tabKey]]); } // Чистим пустые области for (const side of ['above', 'left', 'below', 'right']) { if (extensionContainers[side].children.length === 0) { extensionContainers[side].remove(); } } if (this.#config.open) { const firstBtn = Object.values(allButtons)[0]; if (firstBtn) firstBtn.click(); } return configs; } /** Точка входа */ static init() { this.#config = new TabsExtensionConfigs(); const configs = this.#config.parseConfigs(); const processedConfigs = {}; for (const mode of ['txt', 'img']) { let extensions = null; const keepExtensions = Object.keys(configs[mode]) .filter((ext) => configs[mode][ext] === "keep"); try { const parsed = TabsExtensionParser.parse(mode, keepExtensions); extensions = this.#sort_extensions(parsed, configs[mode]); } catch (e) { alert(`[TabsExtension] Something went wrong while parsing ${mode}2img extensions:\n${e}`); continue; } try { processedConfigs[mode] = this.#setup_tabs(mode, extensions, configs[mode]); if (this.#config.container) { const styler = document.getElementById(`${mode}2img_script_container`) ?.querySelector(".styler"); if (styler) styler.style.display = "none"; } } catch (e) { alert(`[TabsExtension] Something went wrong during ${mode}2img setup:\n${e}`); continue; } } try { this.#config.saveConfigs(processedConfigs); } catch (e) { alert(`[TabsExtension] Something went wrong while saving configs:\n${e}`); } document.addEventListener("click", () => this.#refreshEnableCheckbox()); } } // Инициализация после загрузки UI (function () { onUiLoaded(() => { const slider = document.getElementById("setting_tabs_ex_delay") ?.querySelector("input[type=range]"); const delay = parseInt(slider?.value ?? "50", 10); setTimeout(() => { if (typeof TabsExtension?.init === "function") TabsExtension.init(); }, Number.isNaN(delay) ? 50 : delay); }); })();