Wazeopedia Core UI Library

Biblioteca de funciones de UI y utilidades para scripts de Wazeopedia.

Verzia zo dňa 06.06.2025. Pozri najnovšiu verziu.

Tento skript by nemal byť nainštalovaný priamo. Je to knižnica pre ďalšie skripty, ktorú by mali používať cez meta príkaz // @require https://update.greasyforks.org/scripts/538610/1602967/Wazeopedia%20Core%20UI%20Library.js

// ==UserScript==
// @name         Wazeopedia Core UI Library
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  Biblioteca de funciones de UI y utilidades para scripts de Wazeopedia.
// @author       Annthizze
// @match        https://www.waze.com/discuss/*
// @license      MIT
// ==/UserScript==

var WazeopediaUI = (function() {
    'use strict';

    // --- Funciones de Modal General ---
    function showModal(message, type = 'alert', callback, isSubModal = false) {
        if (!isSubModal) closeAllModals();
        const overlay = document.createElement('div');
        overlay.className = 'wz-modal-overlay';
        if (isSubModal) overlay.style.zIndex = 2000 + document.querySelectorAll('.wz-modal-overlay').length;
        const content = document.createElement('div'); content.className = 'wz-modal-content';
        const messageP = document.createElement('p'); messageP.style.textAlign = 'center'; messageP.textContent = message; content.appendChild(messageP);
        const buttonsDiv = document.createElement('div'); buttonsDiv.className = 'wz-modal-buttons'; buttonsDiv.style.textAlign = 'center';
        if (type === 'confirm') {
            buttonsDiv.appendChild(createButton('Sí', 'wz-confirm', () => { closeAllModals(); if (callback) callback(true); }));
            buttonsDiv.appendChild(createButton('No', 'wz-cancel', () => { closeAllModals(); if (callback) callback(false); }));
        } else {
            buttonsDiv.appendChild(createButton('Aceptar', 'wz-confirm', () => { overlay.remove(); if (callback) callback(true); }));
        }
        content.appendChild(buttonsDiv); overlay.appendChild(content); document.body.appendChild(overlay);
        setupModalEscape(overlay, type, callback);
    }

    function createButton(text, className, onClick) {
        const button = document.createElement('button');
        button.textContent = text; button.className = className; button.onclick = onClick;
        return button;
    }

    function setupModalEscape(overlay, type, callback) {
        const escapeHandler = e => {
            if (e.key === 'Escape') {
                const allOverlays = document.querySelectorAll('.wz-modal-overlay, .wz-toc-guide-modal-overlay');
                let maxZ = 0; allOverlays.forEach(ov => maxZ = Math.max(maxZ, parseInt(getComputedStyle(ov).zIndex) || 0));
                const overlayZ = parseInt(getComputedStyle(overlay).zIndex) || 0;
                if (overlayZ < maxZ) return;
                closeAllModals();
                if (type === 'confirm' && callback) callback(false); else if (type === 'form' && callback) callback(false); else if (callback) callback(true);
                document.removeEventListener('keydown', escapeHandler);
            }
        };
        overlay.tabIndex = -1; overlay.focus(); document.addEventListener('keydown', escapeHandler, { once: true });
    }

    function closeAllModals() {
        document.querySelectorAll('.wz-modal-overlay, .wz-toc-guide-modal').forEach(modal => modal.remove());
        const tocOverlay = document.getElementById('wz-toc-guide-overlay');
        if (tocOverlay) tocOverlay.remove();
    }

    // --- Funciones de Utilidad ---
    function insertTextAtCursor(textarea, text, cursorOffsetInfo = false) {
        const start = textarea.selectionStart;
        const end = textarea.selectionEnd;
        const textBefore = textarea.value.substring(0, start);
        const textAfter = textarea.value.substring(end);
        textarea.value = textBefore + text + textAfter;
        if (cursorOffsetInfo && typeof cursorOffsetInfo.position === 'number') {
            const finalPos = start + cursorOffsetInfo.position;
            textarea.selectionStart = textarea.selectionEnd = finalPos;
        } else if (cursorOffsetInfo === false) {
            textarea.selectionStart = start;
            textarea.selectionEnd = start + text.length;
        } else {
            textarea.selectionStart = textarea.selectionEnd = start + text.length;
        }
        textarea.focus();
        textarea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
    }

    function applyHeadingFormatting(textarea, level, text = '') {
        const selectedText = text || textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);
        const markdownPrefix = '#'.repeat(level) + ' ';
        const wzhTagOpen = `[wzh=${level}]`; const wzhTagClose = `[/wzh]`;
        let coreText = selectedText ? `${markdownPrefix}${wzhTagOpen}${selectedText}${wzhTagClose}` : `${markdownPrefix}${wzhTagOpen}${wzhTagClose}`;
        const textBeforeSelection = textarea.value.substring(0, textarea.selectionStart);
        let prefix = "";
        if (textarea.selectionStart > 0 && !textBeforeSelection.endsWith("\n\n") && !textBeforeSelection.endsWith("\n")) { prefix = "\n\n"; }
        else if (textarea.selectionStart > 0 && textBeforeSelection.endsWith("\n") && !textBeforeSelection.endsWith("\n\n")) { prefix = "\n"; }
        let textToInsert = prefix + coreText;
        const cursorPosition = selectedText ? textToInsert.length : (prefix + markdownPrefix + wzhTagOpen).length;
        insertTextAtCursor(textarea, textToInsert, { position: cursorPosition });
    }

    function applyHrFormatting(textarea) {
        let textToInsert = "\n---\n";
        const textBefore = textarea.value.substring(0, textarea.selectionStart);
        if (textBefore.trim() === '') { textToInsert = "---\n\n"; }
        else if (!textBefore.endsWith('\n\n')) { textToInsert = (textBefore.endsWith('\n') ? '\n' : '\n\n') + '---'; }
        else { textToInsert = '---'; }
        const textAfter = textarea.value.substring(textarea.selectionEnd);
        if (textAfter.trim() === '') { textToInsert += '\n'; }
        else if (!textAfter.startsWith('\n\n')) { textToInsert += (textAfter.startsWith('\n') ? '\n' : '\n\n'); }
        insertTextAtCursor(textarea, textToInsert, { position: textToInsert.length });
    }

    // --- Lógica de Desplegables y Botones ---
    function closeAllDropdowns() {
        document.querySelectorAll('.wz-dropdown-content.wz-show').forEach(dd => dd.classList.remove('wz-show'));
        document.removeEventListener('click', handleClickOutsideDropdowns);
    }
    function handleClickOutsideDropdowns(event) {
        if (!event.target.closest('.wz-dropdown')) closeAllDropdowns();
    }
    function toggleDropdown(buttonElement, dropdownContentElement) {
        const isCurrentlyShown = dropdownContentElement.classList.contains('wz-show');
        closeAllDropdowns();
        if (!isCurrentlyShown) {
            dropdownContentElement.classList.add('wz-show');
            setTimeout(() => document.addEventListener('click', handleClickOutsideDropdowns), 0);
        }
    }
    function addCustomButtons(buttonConfigs) {
        const editorToolbar = document.querySelector('div.d-editor-button-bar, div.discourse-markdown-toolbar, .editor-toolbar');
        if (!editorToolbar) return;
        let buttonBarContainer = editorToolbar.querySelector('.wz-button-container');
        if (buttonBarContainer && buttonBarContainer.dataset.wzToolsProcessed === 'true') return;
        if (!buttonBarContainer) {
            buttonBarContainer = document.createElement('div');
            buttonBarContainer.className = 'wz-button-container';
            const lastGroup = Array.from(editorToolbar.children).filter(el => el.matches('.btn-group, button')).pop();
            if (lastGroup) lastGroup.insertAdjacentElement('afterend', buttonBarContainer); else editorToolbar.appendChild(buttonBarContainer);
        }
        buttonBarContainer.innerHTML = '';
        buttonBarContainer.dataset.wzToolsProcessed = 'true';
        const textarea = document.querySelector('textarea.d-editor-input, #reply-control textarea, .composer-container textarea');
        if (!textarea) return;
        buttonConfigs.forEach(config => {
            if (config.isDropdown) {
                const wrapper = document.createElement('div');
                wrapper.className = 'wz-dropdown';
                const btn = createButton(config.text, 'wz-custom-button btn wz-dropdown-toggle', e => { e.stopPropagation(); toggleDropdown(btn, content); });
                btn.id = config.id; btn.title = config.title;
                const content = document.createElement('div'); content.className = 'wz-dropdown-content';
                config.dropdownItems.forEach(item => {
                    if (item.isSeparator) {
                        const separator = document.createElement('hr');
                        separator.style.margin = '4px 8px';
                        separator.style.borderColor = '#ddd';
                        content.appendChild(separator);
                        return;
                    }
                    const ddBtn = createButton(item.text, '', e => {
                        e.stopPropagation();
                        if (typeof item.action === 'function') { item.action(textarea); }
                        closeAllDropdowns();
                    });
                    ddBtn.title = item.title || `Insertar ${item.text}`;
                    content.appendChild(ddBtn);
                });
                wrapper.append(btn, content);
                buttonBarContainer.appendChild(wrapper);
            } else {
                const btn = createButton(config.text, 'wz-custom-button btn', e => {
                    e.preventDefault(); e.stopPropagation();
                    if (typeof config.action === 'function') { config.action(textarea); }
                });
                btn.id = config.id; btn.title = config.title;
                buttonBarContainer.appendChild(btn);
            }
        });
    }

    // --- INICIALIZACIÓN Y GESTIÓN DEL TEMA ---
    function applyTheme() {
        const isDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
        document.body.classList.toggle('wz-dark-mode', isDark);
    }

    function init(buttonConfigs) {
        const editorObserver = new MutationObserver(() => {
            if (document.querySelector('div.d-editor-button-bar, div.discourse-markdown-toolbar, .editor-toolbar') && !document.getElementById('wz-btn-toc')) {
                addCustomButtons(buttonConfigs);
                applyTheme();
            }
        });
        editorObserver.observe(document.body, { childList: true, subtree: true });
        window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', applyTheme);
        addCustomButtons(buttonConfigs);
        applyTheme();
    }

    // API Pública de la biblioteca
    return {
        showModal,
        createButton,
        setupModalEscape,
        closeAllModals,
        insertTextAtCursor,
        applyHeadingFormatting,
        applyHrFormatting,
        init
    };
})();

长期地址
遇到问题?请前往 GitHub 提 Issues。