Danbooru Note Formatting Helper

A formatting helper toolbar for Danbooru note formatting dialogs, adding buttons to wrap highlighted text with HTML tags for easy formatting.

Från och med 2025-11-02. Se den senaste versionen.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name Danbooru Note Formatting Helper
// @namespace http://tampermonkey.net/
// @version 1.23.0
// @description A formatting helper toolbar for Danbooru note formatting dialogs, adding buttons to wrap highlighted text with HTML tags for easy formatting.
// @author       FunkyJustin
// @license      MIT
// @match https://danbooru.donmai.us/*
// @grant none
// ==/UserScript==
(function() {
    'use strict';
    // Global try-catch wrapper to prevent errors from halting execution
    try {
        // Configuration object for easy extension - declared first to avoid hoisting issues
        const CONFIG = {
            version: '1.23.0',
            toolbarId: 'note-formatting-toolbar',
            buttons: [
                // Font Group
                {
                    id: 'font-size-btn',
                    label: 'Size',
                    title: 'Font Size (prompt px/em)',
                    action: (ta) => promptFontSize(ta)
                },
                {
                    id: 'big-btn',
                    label: 'Big',
                    title: 'Big Text (<big>)',
                    action: (ta) => applyFormat(ta, '<big>', '</big>')
                },
                {
                    id: 'small-btn',
                    label: 'Small',
                    title: 'Small Text (<small>)',
                    action: (ta) => applyFormat(ta, '<small>', '</small>')
                },
                {
                    id: 'color-btn',
                    label: 'Color ▼',
                    title: 'Text Color (click for palette/picker)',
                    action: (ta) => openColorPicker(ta, 'color')
                },
                {
                    id: 'highlight-btn',
                    label: 'Highlight ▼',
                    title: 'Text Highlight (background-color, click for palette/picker)',
                    action: (ta) => openColorPicker(ta, 'background-color')
                },
                {
                    id: 'bold-btn',
                    label: '𝐁',
                    title: 'Bold (<b>) (Ctrl+B)',
                    action: (ta) => applyFormat(ta, '<b>', '</b>')
                },
                {
                    id: 'italic-btn',
                    label: '𝑰',
                    title: 'Italic (<i>) (Ctrl+I)',
                    action: (ta) => applyFormat(ta, '<i>', '</i>')
                },
                {
                    id: 'underline-btn',
                    label: 'U̲',
                    title: 'Underline (<u>) (Ctrl+U)',
                    action: (ta) => applyFormat(ta, '<u>', '</u>')
                },
                {
                    id: 'strikethrough-btn',
                    label: 'S̶',
                    title: 'Strikethrough (<s>)',
                    action: (ta) => applyFormat(ta, '<s>', '</s>')
                },
                // Paragraph Group
                {
                    id: 'align-left-btn',
                    label: '⟦',
                    title: 'Align Left (<div align="left">)',
                    action: (ta) => applyFormat(ta, '<div align="left">', '</div>')
                },
                {
                    id: 'align-center-btn',
                    label: '⦳',
                    title: 'Align Center (<div align="center">)',
                    action: (ta) => applyFormat(ta, '<div align="center">', '</div>')
                },
                {
                    id: 'align-right-btn',
                    label: '⟧',
                    title: 'Align Right (<div align="right">)',
                    action: (ta) => applyFormat(ta, '<div align="right">', '</div>')
                },
                // Insert Group
                {
                    id: 'link-btn',
                    label: '🔗',
                    title: 'Insert Link (<a href="...">)',
                    action: (ta) => insertLink(ta)
                },
                {
                    id: 'tn-btn',
                    label: 'TN',
                    title: 'Translator Note (<tn>)',
                    action: (ta) => applyFormat(ta, '<tn>', '</tn>')
                },
                // Clear/History/Settings Group
                {
                    id: 'clear-btn',
                    label: 'Clear',
                    title: 'Clear Formatting (strip HTML tags)',
                    action: (ta) => clearFormatting(ta)
                },
                {
                    id: 'undo-btn',
                    label: '↶',
                    title: 'Undo (Ctrl+Z)',
                    action: (ta) => undo(ta)
                },
                {
                    id: 'redo-btn',
                    label: '↷',
                    title: 'Redo (Ctrl+Y)',
                    action: (ta) => redo(ta)
                },
                {
                    id: 'toggle-resize-btn',
                    label: 'AR',
                    title: 'Toggle Auto-Resize On/Off',
                    action: (ta) => toggleAutoResize()
                },
                {
                    id: 'settings-btn',
                    label: '⚙',
                    title: 'Set Min Width/Height for Auto-Resize',
                    action: (ta) => openSettings()
                }
            ],
            shortcuts: {
                bold: 'Ctrl+B',
                italic: 'Ctrl+I',
                underline: 'Ctrl+U',
                undo: 'Ctrl+Z',
                redo: 'Ctrl+Y'
            },
            manualTrigger: 'Ctrl+Shift+I', // Force inject
            historyLimit: 50, // Max undo/redo states
            commonColors: ['#FF0000', '#FF8000', '#FFFF00', '#00FF00', '#0000FF', '#4B0082', '#8B00FF'], // ROYGBIV hex
            autoResize: {
                enabled: true,
                defaultWidth: 523,
                defaultHeight: 362
            },
            styles: `
                #${'note-formatting-toolbar'} {
                    display: flex;
                    flex-wrap: wrap;
                    gap: 10px;
                    margin: 5px 0;
                    padding: 5px;
                    background: #f0f0f0;
                    border: 1px solid #ccc;
                    border-radius: 4px;
                    font-family: sans-serif;
                    font-size: 12px;
                    justify-content: center;
                    overflow-x: auto;
                    max-width: 100%;
                }
                #${'note-formatting-toolbar'} .group {
                    display: flex;
                    gap: 2px;
                    padding: 2px 5px;
                    background: white;
                    border-radius: 3px;
                    border: 1px solid #ddd;
                }
                #${'note-formatting-toolbar'} button {
                    padding: 4px 6px;
                    border: 1px solid #ccc;
                    background: white;
                    border-radius: 3px;
                    cursor: pointer;
                    font-weight: bold;
                    font-size: 11px;
                    min-width: auto;
                }
                #${'note-formatting-toolbar'} button:hover {
                    background: #e0e0e0;
                }
                .note-edit-dialog .ui-dialog-content {
                    position: relative;
                }
                .note-edit-dialog #note-formatting-toolbar {
                    margin-bottom: 5px;
                }
                #toolbar-minimize {
                    margin-left: auto;
                    background: #ddd;
                    font-size: 10px;
                    padding: 2px 4px;
                }
                #debug-indicator {
                    position: fixed;
                    top: 10px;
                    right: 10px;
                    background: #ffeb3b;
                    color: #000;
                    padding: 5px;
                    border: 1px solid #ccc;
                    font-size: 12px;
                    z-index: 9999;
                    display: none;
                    max-width: 300px;
                }
                /* Color Picker Styles */
                #color-picker {
                    position: fixed;
                    top: 50%;
                    left: 50%;
                    transform: translate(-50%, -50%);
                    background: #1E1E2C;
                    color: #E0E0E0;
                    border: 1px solid #ccc;
                    border-radius: 8px;
                    padding: 20px;
                    box-shadow: 0 4px 20px rgba(0,0,0,0.5);
                    z-index: 10000;
                    min-width: 300px;
                    font-family: sans-serif;
                }
                #color-picker h3 {
                    margin: 0 0 10px 0;
                    text-align: center;
                    color: #F0F0F0;
                }
                .palette {
                    display: grid;
                    grid-template-columns: repeat(7, 1fr);
                    gap: 5px;
                    margin-bottom: 15px;
                }
                .palette button {
                    width: 30px;
                    height: 30px;
                    border: 1px solid #666;
                    border-radius: 4px;
                    cursor: pointer;
                }
                .custom-section {
                    margin-bottom: 10px;
                }
                .custom-section label {
                    display: block;
                    margin-bottom: 5px;
                    color: #D0D0D0;
                }
                .rgb-sliders {
                    display: flex;
                    gap: 10px;
                    margin-bottom: 10px;
                }
                .rgb-sliders input[type="range"] {
                    flex: 1;
                    background: #333;
                    color: #fff;
                }
                .rgb-inputs {
                    display: flex;
                    gap: 5px;
                }
                .rgb-inputs input[type="number"] {
                    width: 50px;
                    background: #333;
                    color: #fff;
                    border: 1px solid #666;
                }
                #brightness-slider {
                    margin-bottom: 10px;
                }
                #color-preview {
                    width: 100%;
                    height: 30px;
                    border: 1px solid #666;
                    border-radius: 4px;
                    margin-bottom: 10px;
                }
                .picker-buttons {
                    display: flex;
                    gap: 10px;
                    justify-content: center;
                }
                .picker-buttons button {
                    padding: 8px 16px;
                    border: 1px solid #666;
                    border-radius: 4px;
                    cursor: pointer;
                    background: #333;
                    color: #E0E0E0;
                }
                .picker-buttons button:hover {
                    background: #444;
                }
                #color-picker-overlay {
                    position: fixed;
                    top: 0;
                    left: 0;
                    width: 100%;
                    height: 100%;
                    background: rgba(0,0,0,0.5);
                    z-index: 9999;
                }
            `
        };
        // Global variables for state management (per-tab isolation for multi-tab support)
        let observer = null;
        let activeDialogs = new Set(); // Track multiple dialogs if possible (rare, but robust)
        let toolbar = null;
        let textarea = null;
        let isMinimized = false;
        let debugMode = true; // Enable for logging; set to false in prod if needed
        let manualTriggerHandler = null;
        let history = []; // Undo/redo history array of {value, start, end}
        let historyIndex = -1; // Current position in history
        let colorPicker = null; // Color picker dialog
        // Utility: Log with prefix for easy console filtering - uses CONFIG.version safely
        function log(message, data = null) {
            const version = CONFIG ? CONFIG.version : 'unknown';
            console.log(`[NoteFmtHelper v${version}] ${message}`, data || '');
        }
        // Load auto-resize config from localStorage (persistent across tabs/sessions)
        function loadAutoResizeConfig() {
            try {
                const savedEnabled = localStorage.getItem('nfh_autoResizeEnabled');
                if (savedEnabled !== null) {
                    CONFIG.autoResize.enabled = savedEnabled === 'true';
                }
                const savedWidth = localStorage.getItem('nfh_defaultWidth');
                if (savedWidth && !isNaN(parseInt(savedWidth))) {
                    CONFIG.autoResize.defaultWidth = parseInt(savedWidth);
                }
                const savedHeight = localStorage.getItem('nfh_defaultHeight');
                if (savedHeight && !isNaN(parseInt(savedHeight))) {
                    CONFIG.autoResize.defaultHeight = parseInt(savedHeight);
                }
                log('Auto-resize config loaded from localStorage.', CONFIG.autoResize);
            } catch (err) {
                log('Load auto-resize config error', err);
            }
        }
        // Utility: Inject styles if not already present - stringified to avoid const issues
        function injectStyles() {
            try {
                if (document.getElementById('note-formatting-styles')) return;
                const style = document.createElement('style');
                style.id = 'note-formatting-styles';
                style.textContent = CONFIG.styles; // Template interpolation already handles ID replacement
                document.head.appendChild(style);
                log('Styles injected.');
            } catch (err) {
                console.error('[NoteFmtHelper] Style injection error:', err);
            }
        }
        // Utility: Create or update toolbar with minimize button (Word-like ribbon with groups)
        function createToolbar() {
            try {
                if (toolbar) return toolbar;
                toolbar = document.createElement('div');
                toolbar.id = CONFIG.toolbarId;
                // Groups for Word-like layout
                const groups = {
                    font: ['font-size-btn', 'big-btn', 'small-btn', 'color-btn', 'highlight-btn', 'bold-btn', 'italic-btn', 'underline-btn', 'strikethrough-btn'],
                    paragraph: ['align-left-btn', 'align-center-btn', 'align-right-btn'],
                    insert: ['link-btn', 'tn-btn'],
                    history: ['clear-btn', 'undo-btn', 'redo-btn', 'toggle-resize-btn', 'settings-btn']
                };
                Object.entries(groups).forEach(([groupName, btnIds]) => {
                    const group = document.createElement('div');
                    group.className = 'group';
                    group.title = groupName.charAt(0).toUpperCase() + groupName.slice(1); // e.g., "Font"
                    btnIds.forEach(btnId => {
                        const btnConfig = CONFIG.buttons.find(b => b.id === btnId);
                        if (btnConfig) {
                            const button = document.createElement('button');
                            button.id = btnConfig.id;
                            button.type = 'button';
                            button.innerHTML = btnConfig.label;
                            button.title = btnConfig.title;
                            button.addEventListener('click', (e) => {
                                e.preventDefault();
                                e.stopPropagation();
                                if (textarea && !textarea.disabled) {
                                    if (document.activeElement !== textarea) textarea.focus();
                                    // Pre-snapshot for formatting actions (exclude undo/redo/toggle/settings)
                                    if (!['undo-btn', 'redo-btn', 'toggle-resize-btn', 'settings-btn'].includes(btnConfig.id)) {
                                        saveState(textarea);
                                    }
                                    btnConfig.action(textarea);
                                    log(`Button ${btnConfig.id} clicked.`);
                                } else if (['toggle-resize-btn', 'settings-btn'].includes(btnConfig.id)) {
                                    // Allow these even without textarea focus (global config)
                                    btnConfig.action();
                                    log(`${btnConfig.id} button clicked (global).`);
                                } else {
                                    log('Button clicked but textarea not ready.', { focused: document.activeElement });
                                }
                            });
                            group.appendChild(button);
                        }
                    });
                    toolbar.appendChild(group);
                });
                // Minimize button (smaller, optional for buttonset fit)
                const minimizeBtn = document.createElement('button');
                minimizeBtn.id = 'toolbar-minimize';
                minimizeBtn.type = 'button';
                minimizeBtn.innerHTML = '−'; // Unicode minus for minimize
                minimizeBtn.title = 'Minimize Formatting Toolbar';
                minimizeBtn.style.fontSize = '0.8em';
                minimizeBtn.style.padding = '0.3em 0.5em';
                minimizeBtn.addEventListener('click', (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    toggleMinimize();
                });
                toolbar.appendChild(minimizeBtn);
                // Version display (small span, not a button) - in corner as per style
                const versionSpan = document.createElement('span');
                versionSpan.id = 'toolbar-version';
                versionSpan.textContent = `v${CONFIG.version}`;
                versionSpan.className = 'ui-widget';
                versionSpan.style.cssText = 'margin-left: 5px; font-size: 0.8em; color: #666; padding: 0.4em 0; align-self: center;';
                toolbar.appendChild(versionSpan);
                // Responsive: Adjust on resize
                window.addEventListener('resize', adjustToolbarLayout);
                adjustToolbarLayout();
                log('Toolbar created as Word-like ribbon with groups and icons.');
                return toolbar;
            } catch (err) {
                log('Toolbar creation error', err);
                return null;
            }
        }
        // Minimize/Maximize functionality for better UX (hides main buttons)
        function toggleMinimize() {
            try {
                isMinimized = !isMinimized;
                const mainButtons = toolbar ? toolbar.querySelectorAll('button:not(#toolbar-minimize)') : [];
                const versionSpan = document.getElementById('toolbar-version');
                const minimizeBtn = document.getElementById('toolbar-minimize');
                if (isMinimized) {
                    mainButtons.forEach(btn => btn.style.display = 'none');
                    if (versionSpan) versionSpan.style.display = 'none';
                    if (minimizeBtn) {
                        minimizeBtn.innerHTML = '+'; // Plus for maximize
                        minimizeBtn.title = 'Expand Formatting Toolbar';
                    }
                    log('Formatting toolbar minimized.');
                } else {
                    mainButtons.forEach(btn => btn.style.display = '');
                    if (versionSpan) versionSpan.style.display = '';
                    if (minimizeBtn) {
                        minimizeBtn.innerHTML = '−';
                        minimizeBtn.title = 'Minimize Formatting Toolbar';
                    }
                    adjustToolbarLayout();
                    log('Formatting toolbar expanded.');
                }
            } catch (err) {
                log('Toggle minimize error', err);
            }
        }
        // Responsive layout adjustment (for buttonset fit)
        function adjustToolbarLayout() {
            try {
                if (toolbar && window.innerWidth < 800) { // Broader check for dialog context
                    toolbar.style.flexDirection = 'column';
                    toolbar.style.alignItems = 'stretch';
                    toolbar.querySelectorAll('button').forEach(btn => btn.style.minWidth = 'auto');
                } else {
                    toolbar.style.flexDirection = 'row';
                    toolbar.style.alignItems = 'center';
                    toolbar.querySelectorAll('button').forEach(btn => btn.style.minWidth = 'initial');
                }
            } catch (err) {
                log('Layout adjustment error', err);
            }
        }
        // Auto-expand dialog to fit ribbon + textarea
        function autoResizeDialog(dialog) {
            try {
                if (!CONFIG.autoResize.enabled) {
                    log('Auto-resize disabled; skipped.');
                    return;
                }
                if (dialog.dataset.helperResized === 'true') return; // One-time per dialog (reset via settings if needed)
                const tb = dialog.querySelector(`#${CONFIG.toolbarId}`);
                const ta = dialog.querySelector('textarea');
                if (!tb || !ta) {
                    log('Auto-resize skipped: Missing toolbar/textarea.');
                    return;
                }
                // Re-measure after height set (no inner timeout needed for immediacy)
                const tbRect = tb.getBoundingClientRect();
                const taRect = ta.getBoundingClientRect();
                // Calculate content dimensions
                const contentWidth = Math.max(tbRect.width, taRect.width) + 30; // Padding for borders/scroll
                const contentHeight = tbRect.height + taRect.height + 50; // Buffer for padding/margins
                const newWidth = Math.max(CONFIG.autoResize.defaultWidth, Math.max(500, contentWidth));
                const newHeight = Math.max(CONFIG.autoResize.defaultHeight, contentHeight + 150); // Header/footer/dialog chrome buffer
                // Always use direct style resize (reliable, no jQuery dependency/error)
                dialog.style.width = `${newWidth}px`;
                dialog.style.height = `${newHeight}px`;
                log(`Auto-resized dialog via direct style: ${newWidth}x${newHeight}px`);
                dialog.dataset.helperResized = 'true';
            } catch (err) {
                log('Auto-resize error', err);
            }
        }
        // Toggle auto-resize enable/disable
        function toggleAutoResize() {
            try {
                CONFIG.autoResize.enabled = !CONFIG.autoResize.enabled;
                localStorage.setItem('nfh_autoResizeEnabled', CONFIG.autoResize.enabled.toString());
                const status = CONFIG.autoResize.enabled ? 'enabled' : 'disabled';
                log(`Auto-resize ${status}.`);
                showDebugIndicator(`Auto-resize ${status}.`, 'info');
                // If enabling, re-apply to current dialog
                if (CONFIG.autoResize.enabled) {
                    const dialog = document.querySelector('.note-edit-dialog');
                    if (dialog) {
                        dialog.dataset.helperResized = 'false';
                        setTimeout(() => autoResizeDialog(dialog), 100);
                    }
                }
            } catch (err) {
                log('Toggle auto-resize error', err);
            }
        }
        // Open settings for width/height mins only
        function openSettings() {
            try {
                let newWidthStr = prompt('Enter default min width (px):', CONFIG.autoResize.defaultWidth.toString());
                let newWidth = parseInt(newWidthStr);
                if (isNaN(newWidth) || newWidth < 300) {
                    newWidth = CONFIG.autoResize.defaultWidth;
                    log('Invalid width; using default.');
                }
                CONFIG.autoResize.defaultWidth = newWidth;
                localStorage.setItem('nfh_defaultWidth', newWidth.toString());
                let newHeightStr = prompt('Enter default min height (px):', CONFIG.autoResize.defaultHeight.toString());
                let newHeight = parseInt(newHeightStr);
                if (isNaN(newHeight) || newHeight < 200) {
                    newHeight = CONFIG.autoResize.defaultHeight;
                    log('Invalid height; using default.');
                }
                CONFIG.autoResize.defaultHeight = newHeight;
                localStorage.setItem('nfh_defaultHeight', newHeight.toString());
                log('Auto-resize dimensions updated.', CONFIG.autoResize);
                showDebugIndicator(`Dimensions updated: ${newWidth}x${newHeight}`, 'info');
                // Re-apply to current dialog if enabled
                if (CONFIG.autoResize.enabled) {
                    const dialog = document.querySelector('.note-edit-dialog');
                    if (dialog) {
                        dialog.dataset.helperResized = 'false';
                        setTimeout(() => autoResizeDialog(dialog), 100);
                    }
                }
            } catch (err) {
                log('Settings error', err);
                alert('Settings update failed; check console.');
            }
        }
        // Unified apply format function for tags, styles, aligns (replaces applyStyle/applyAlign)
        function applyFormat(ta, openTag, closeTag) {
            try {
                const start = ta.selectionStart;
                const end = ta.selectionEnd;
                const text = ta.value;
                const selected = text.substring(start, end);
                const before = text.substring(0, start);
                const after = text.substring(end);
                let newText;
                let cursorStart, cursorEnd;
                if (selected) {
                    newText = before + openTag + selected + closeTag + after;
                    cursorStart = start + openTag.length;
                    cursorEnd = cursorStart + selected.length;
                } else {
                    const placeholder = openTag + 'text' + closeTag;
                    newText = before + placeholder + after;
                    cursorStart = start + openTag.length;
                    cursorEnd = cursorStart + 4; // Position inside 'text'
                }
                ta.value = newText;
                ta.selectionStart = cursorStart;
                ta.selectionEnd = cursorEnd;
                ta.focus();
                ta.dispatchEvent(new Event('input', { bubbles: true }));
                // Post-snapshot after successful apply
                saveState(ta);
                log(`Applied format: ${openTag.substring(0, 20)}...`);
            } catch (err) {
                log('Apply format error', err);
            }
        }
        // Prompt for font size and apply via applyFormat
        function promptFontSize(ta) {
            try {
                const size = prompt('Enter font size (e.g., 18px, 1.5em, large):', '16px');
                if (!size) return; // No post-save if cancelled
                const styleStr = `font-size: ${size};`;
                const openTag = `<span style="${styleStr}">`;
                applyFormat(ta, openTag, '</span>'); // Post-save inside applyFormat
            } catch (err) {
                log('Font size prompt error', err);
            }
        }
        // Open color picker for text color or highlight
        function openColorPicker(ta, type) {
            try {
                if (colorPicker) {
                    closeColorPicker();
                }
                // Overlay
                const overlay = document.createElement('div');
                overlay.id = 'color-picker-overlay';
                overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:9998;';
                document.body.appendChild(overlay);
                // Picker dialog
                colorPicker = document.createElement('div');
                colorPicker.id = 'color-picker';
                colorPicker.innerHTML = `
                    <h3>${type === 'background-color' ? 'Text Highlight' : 'Text Color'}</h3>
                    <div class="palette"></div>
                    <div class="custom-section">
                        <label>Custom RGB</label>
                        <div class="rgb-sliders">
                            <input type="range" id="r-slider" min="0" max="255" value="255" />
                            <input type="range" id="g-slider" min="0" max="255" value="0" />
                            <input type="range" id="b-slider" min="0" max="255" value="0" />
                        </div>
                        <div class="rgb-inputs">
                            <span>R:</span><input type="number" id="r-input" min="0" max="255" value="255" />
                            <span>G:</span><input type="number" id="g-input" min="0" max="255" value="0" />
                            <span>B:</span><input type="number" id="b-input" min="0" max="255" value="0" />
                        </div>
                        <label>Brightness (Alpha 0-1)</label>
                        <input type="range" id="alpha-slider" min="0" max="1" step="0.01" value="1" />
                        <span id="alpha-value">1.00</span>
                    </div>
                    <div id="color-preview" style="background-color: rgb(255,0,0);"></div>
                    <div class="picker-buttons">
                        <button id="picker-ok">OK</button>
                        <button id="picker-cancel">Cancel</button>
                    </div>
                `;
                document.body.appendChild(colorPicker);
                // Palette
                const palette = colorPicker.querySelector('.palette');
                CONFIG.commonColors.forEach(color => {
                    const btn = document.createElement('button');
                    btn.style.backgroundColor = color;
                    btn.addEventListener('click', () => {
                        const rgb = hexToRgb(color);
                        colorPicker.querySelector('#r-slider').value = rgb.r;
                        colorPicker.querySelector('#g-slider').value = rgb.g;
                        colorPicker.querySelector('#b-slider').value = rgb.b;
                        colorPicker.querySelector('#r-input').value = rgb.r;
                        colorPicker.querySelector('#g-input').value = rgb.g;
                        colorPicker.querySelector('#b-input').value = rgb.b;
                        updateColor(colorPicker);
                    });
                    palette.appendChild(btn);
                });
                // OK button - post-save after apply
                colorPicker.querySelector('#picker-ok').addEventListener('click', (e) => {
                    const r = colorPicker.querySelector('#r-slider').value;
                    const g = colorPicker.querySelector('#g-slider').value;
                    const b = colorPicker.querySelector('#b-slider').value;
                    const alpha = colorPicker.querySelector('#alpha-slider').value;
                    const colorValue = alpha < 1 ? `rgba(${r}, ${g}, ${b}, ${alpha})` : `rgb(${r}, ${g}, ${b})`;
                    const styleStr = `${type}: ${colorValue};`;
                    const openTag = `<span style="${styleStr}">`;
                    applyFormat(ta, openTag, '</span>'); // Post-save inside applyFormat
                    closeColorPicker();
                });
                // Cancel button
                colorPicker.querySelector('#picker-cancel').addEventListener('click', closeColorPicker);
                // Close on overlay click
                overlay.addEventListener('click', closeColorPicker);
                // ESC key to close
                const escHandler = (e) => {
                    if (e.key === 'Escape') closeColorPicker();
                };
                window.addEventListener('keydown', escHandler);
                // Update color preview
                function updateColor(picker) {
                    const r = picker.querySelector('#r-slider').value;
                    const g = picker.querySelector('#g-slider').value;
                    const b = picker.querySelector('#b-slider').value;
                    const alpha = picker.querySelector('#alpha-slider').value;
                    picker.querySelector('#alpha-value').textContent = alpha;
                    const color = `rgba(${r}, ${g}, ${b}, ${alpha})`;
                    picker.querySelector('#color-preview').style.backgroundColor = color;
                    picker.querySelector('#r-input').value = r;
                    picker.querySelector('#g-input').value = g;
                    picker.querySelector('#b-input').value = b;
                }
                // Update slider from input
                function updateSlider(channel, picker) {
                    const input = picker.querySelector(`#${channel}-input`).value;
                    picker.querySelector(`#${channel}-slider`).value = input;
                    updateColor(picker);
                }
                // Add event listeners for inputs/sliders
                colorPicker.addEventListener('input', (e) => {
                    if (e.target.matches('input[type="range"], input[type="number"]')) {
                        if (e.target.id.includes('input') && e.target.type === 'number') {
                            const channel = e.target.id.split('-')[0];
                            updateSlider(channel, colorPicker);
                        } else {
                            updateColor(colorPicker);
                        }
                    }
                });
                // Initial update
                updateColor(colorPicker);
                // Cleanup ESC on close
                function closeColorPicker() {
                    if (colorPicker) colorPicker.remove();
                    if (overlay) overlay.remove();
                    colorPicker = null;
                    window.removeEventListener('keydown', escHandler);
                }
                function hexToRgb(hex) {
                    const r = parseInt(hex.substr(1,2), 16);
                    const g = parseInt(hex.substr(3,2), 16);
                    const b = parseInt(hex.substr(5,2), 16);
                    return { r, g, b };
                }
            } catch (err) {
                log('Color picker open error', err);
            }
        }
        // Clear formatting: Strip HTML tags
        function clearFormatting(ta) {
            try {
                const text = ta.value.replace(/<[^>]*>/g, '');
                ta.value = text;
                ta.selectionStart = ta.selectionEnd = 0;
                ta.focus();
                ta.dispatchEvent(new Event('input', { bubbles: true }));
                // Post-snapshot after clear
                saveState(ta);
                log('Formatting cleared.');
            } catch (err) {
                log('Clear formatting error', err);
            }
        }
        // Insert link prompt with intelligent defaults
        function insertLink(ta) {
            try {
                const start = ta.selectionStart;
                const end = ta.selectionEnd;
                const selected = ta.value.substring(start, end);
                const url = prompt('Enter URL:');
                if (!url) return; // No post-save if cancelled
                let text;
                if (selected) {
                    text = prompt('Enter link text (optional):', selected) || selected;
                } else {
                    text = prompt('Enter link text (optional):', '') || url;
                }
                const linkHTML = `<a href="${url}">${text}</a>`;
                insertAtCursor(ta, linkHTML); // Post-save inside insertAtCursor
            } catch (err) {
                log('Link insert error', err);
            }
        }
        // Insert text at cursor
        function insertAtCursor(ta, text) {
            try {
                const start = ta.selectionStart;
                const end = ta.selectionEnd;
                ta.value = ta.value.substring(0, start) + text + ta.value.substring(end);
                ta.selectionStart = ta.selectionEnd = start + text.length;
                ta.focus();
                ta.dispatchEvent(new Event('input', { bubbles: true }));
                // Post-snapshot after insert
                saveState(ta);
            } catch (err) {
                log('Insert at cursor error', err);
            }
        }
        // History management for undo/redo - now saves {value, start, end}
        function saveState(ta) {
            try {
                const state = {
                    value: ta.value,
                    start: ta.selectionStart,
                    end: ta.selectionEnd
                };
                // Trim history to limit
                if (history.length > CONFIG.historyLimit) {
                    history.shift();
                    if (historyIndex > 0) historyIndex--;
                }
                // Push current state after current index (linear, discards branches)
                history = history.slice(0, historyIndex + 1);
                history.push(state);
                historyIndex = history.length - 1;
                log(`State saved. History length: ${history.length}`);
            } catch (err) {
                log('Save state error', err);
            }
        }
        function undo(ta) {
            try {
                if (historyIndex > 0) {
                    historyIndex--;
                    const state = history[historyIndex];
                    ta.value = state.value;
                    ta.selectionStart = state.start;
                    ta.selectionEnd = state.end;
                    ta.focus();
                    ta.dispatchEvent(new Event('input', { bubbles: true }));
                    log(`Undo to index ${historyIndex}`);
                } else {
                    log('Undo: At start of history.');
                }
            } catch (err) {
                log('Undo error', err);
            }
        }
        function redo(ta) {
            try {
                if (historyIndex < history.length - 1) {
                    historyIndex++;
                    const state = history[historyIndex];
                    ta.value = state.value;
                    ta.selectionStart = state.start;
                    ta.selectionEnd = state.end;
                    ta.focus();
                    ta.dispatchEvent(new Event('input', { bubbles: true }));
                    log(`Redo to index ${historyIndex}`);
                } else {
                    log('Redo: At end of history.');
                }
            } catch (err) {
                log('Redo error', err);
            }
        }
        // Setup initial history snapshot (no ongoing listener)
        function setupHistory(ta) {
            try {
                // Initial save (pre-any-changes backup)
                saveState(ta);
                log('Initial history snapshot set up.');
            } catch (err) {
                log('History setup error', err);
            }
        }
        // Keyboard shortcuts (per-tab, checks focus) - extended for undo/redo
        function setupShortcuts() {
            try {
                // Remove existing listener to avoid duplicates in multi-tab/reload
                document.removeEventListener('keydown', handleKeydown);
                document.addEventListener('keydown', handleKeydown, true);
                log('Shortcuts set up.');
            } catch (err) {
                log('Shortcuts setup error', err);
            }
        }
        function handleKeydown(e) {
            try {
                if (!textarea || document.activeElement !== textarea) return;
                if (e.ctrlKey) {
                    if (e.key === 'b') {
                        e.preventDefault();
                        CONFIG.buttons.find(b => b.id === 'bold-btn').action(textarea);
                    } else if (e.key === 'i') {
                        e.preventDefault();
                        CONFIG.buttons.find(b => b.id === 'italic-btn').action(textarea);
                    } else if (e.key === 'u') {
                        e.preventDefault();
                        CONFIG.buttons.find(b => b.id === 'underline-btn').action(textarea);
                    } else if (e.key === 'z' && !e.shiftKey) {
                        e.preventDefault();
                        CONFIG.buttons.find(b => b.id === 'undo-btn').action(textarea);
                    } else if (e.key === 'y' || (e.key === 'z' && e.shiftKey)) {
                        e.preventDefault();
                        CONFIG.buttons.find(b => b.id === 'redo-btn').action(textarea);
                    }
                }
            } catch (err) {
                log('Keydown handler error', err);
            }
        }
        // Manual force inject handler - defined early to avoid ReferenceError
        function setupManualTrigger() {
            try {
                if (manualTriggerHandler) return;
                manualTriggerHandler = (e) => {
                    if (e.ctrlKey && e.shiftKey && e.key === 'I') {
                        e.preventDefault();
                        const dialog = document.querySelector('.note-edit-dialog');
                        if (dialog) {
                            injectToolbar(dialog, true); // Force with banner
                            log('Manual trigger: Forced injection.');
                        } else {
                            showDebugIndicator('Manual trigger: No dialog open.', 'error');
                        }
                    }
                };
                document.addEventListener('keydown', manualTriggerHandler, true);
                log('Manual trigger (Ctrl+Shift+I) set up.');
            } catch (err) {
                log('Manual trigger setup error', err);
            }
        }
        // Dialog injection: Insert ribbon above textarea
        function injectToolbar(dialog, isManual = false) {
            try {
                log('Attempting injection for dialog:', dialog);
                const content = dialog.querySelector('.ui-dialog-content');
                if (!content) {
                    log('No .ui-dialog-content found.');
                    return;
                }
                const headerSpan = content.querySelector('span');
                if (!headerSpan) {
                    log('No header span found.');
                    return;
                }
                textarea = content.querySelector('textarea');
                if (!textarea) {
                    log('No textarea found in content.');
                    return;
                }
                // Stable injection check: Use dataset attribute to prevent re-inject
                if (dialog.dataset.helperInjected === 'true') {
                    log('Dialog already injected (dataset check). Skipping.');
                    return;
                }
                // Remove any existing toolbar in content
                const existing = content.querySelector(`#${CONFIG.toolbarId}`);
                if (existing) {
                    existing.remove();
                    log('Removed existing toolbar.');
                }
                const tb = createToolbar();
                if (tb) {
                    content.insertBefore(tb, textarea); // Insert after header, before textarea
                    log('Formatting ribbon inserted above textarea.');
                } else {
                    log('Failed to create toolbar.');
                    return;
                }
                // Early textarea height adjustment for immediate fit
                const taFullHeight = Math.max(200, textarea.scrollHeight);
                textarea.style.height = `${taFullHeight}px`;
                // Mark as injected
                dialog.dataset.helperInjected = 'true';
                setupShortcuts(); // Re-setup for this textarea
                setupHistory(textarea); // Initial snapshot only
                // Auto-resize dialog immediately after insertion (minimal delay for layout settle)
                setTimeout(() => autoResizeDialog(dialog), 100);
                // Cleanup on dialog close - remove dataset
                const observerCleanup = new MutationObserver((mutations) => {
                    if (!document.body.contains(dialog)) {
                        activeDialogs.delete(dialog); // Track by reference now
                        if (tb && tb.parentNode) tb.remove();
                        delete dialog.dataset.helperInjected;
                        delete dialog.dataset.helperResized;
                        history = [];
                        historyIndex = -1;
                        if (colorPicker) closeColorPicker();
                        observerCleanup.disconnect();
                        log(`Cleaned up for closed dialog.`);
                    }
                });
                observerCleanup.observe(document.body, { childList: true, subtree: true });
                // Only show banner on first auto or manual
                if (isManual || !activeDialogs.has(dialog)) {
                    log(`Ribbon injected above textarea for dialog.`);
                    showDebugIndicator('Word-like Ribbon Injected Above Textarea! Icons and Big/Small added.', 'success');
                }
                activeDialogs.add(dialog); // Track by reference for multi-dialog
            } catch (err) {
                log('Injection error', err);
                showDebugIndicator('Injection Failed - Check Console. Try Ctrl+Shift+I.', 'error');
                // Autonomous recovery: Poll will retry
            }
        }
        // Debug indicator: Temporary overlay for visibility issues
        function showDebugIndicator(message, type = 'info') {
            try {
                if (!debugMode) return;
                let indicator = document.getElementById('debug-indicator');
                if (!indicator) {
                    indicator = document.createElement('div');
                    indicator.id = 'debug-indicator';
                    document.body.appendChild(indicator);
                }
                indicator.textContent = `[NoteFmtHelper] ${message}`;
                indicator.style.background = type === 'error' ? '#ffcccc' : type === 'success' ? '#ccffcc' : '#ffeb3b';
                indicator.style.display = 'block';
                setTimeout(() => { indicator.style.display = 'none'; }, 5000); // Shorter now to reduce spam
                log(message);
            } catch (err) {
                console.error('[NoteFmtHelper] Debug indicator error', err);
            }
        }
        // Main observer for dialog detection (subtree for nested changes)
        function setupObserver() {
            try {
                if (observer) {
                    log('Observer already set up.');
                    return;
                }
                observer = new MutationObserver((mutations) => {
                    let detected = false;
                    mutations.forEach((mutation) => {
                        if (mutation.type === 'childList') {
                            mutation.addedNodes.forEach((node) => {
                                if (node.nodeType === 1 && node.matches && node.matches('.note-edit-dialog')) {
                                    log('Dialog added via observer:', node);
                                    detected = true;
                                    setTimeout(() => injectToolbar(node), 500); // Keep for render stability
                                }
                            });
                        }
                    });
                    if (detected) log('Observer triggered injection.');
                });
                observer.observe(document.body, { childList: true, subtree: true });
                log('Observer set up.');
            } catch (err) {
                log('Observer setup error', err);
            }
        }
        // Fallback poll for edge cases (e.g., observer misses, multi-tab glitches) - faster for persistence
        let pollInterval = null;
        function startPoll() {
            try {
                if (pollInterval) return;
                pollInterval = setInterval(() => {
                    const dialogs = document.querySelectorAll('.note-edit-dialog');
                    if (dialogs.length === 0) {
                        if (toolbar) {
                            toolbar.remove();
                            toolbar = null;
                            activeDialogs.clear();
                            log('No dialogs; cleaned up.');
                        }
                        return;
                    }
                    dialogs.forEach(dialog => {
                        // Use dataset check here too
                        if (dialog.dataset.helperInjected !== 'true') {
                            const content = dialog.querySelector('.ui-dialog-content');
                            const ta = content ? content.querySelector('textarea') : null;
                            if (content && ta) {
                                log('Poll detected eligible dialog:', dialog);
                                injectToolbar(dialog);
                            }
                        }
                    });
                }, 3000); // Slower poll (3s) now that dataset prevents retry
                log('Poll started (every 3s).');
            } catch (err) {
                log('Poll start error', err);
            }
        }
        // Init: Robust, multi-tab safe - setupManualTrigger called after definition
        function init() {
            try {
                loadAutoResizeConfig(); // Load user settings first
                injectStyles();
                setupObserver();
                startPoll(); // Always run poll for reliability
                setupManualTrigger(); // Defined early, no ReferenceError
                // Multi-tab: Listen for focus to re-inject if needed
                window.addEventListener('focus', (e) => {
                    setTimeout(() => {
                        setupObserver();
                        if (pollInterval) clearInterval(pollInterval);
                        startPoll();
                    }, 100);
                });
                showDebugIndicator(`Script Loaded v${CONFIG.version}! Open dialog for Word-like ribbon & advanced markup. Ctrl+Shift+I to force. AR/⚙ for auto-resize.`, 'info');
                log(`Note Formatting Helper v${CONFIG.version} initialized. Ready for multi-tab use. Open DevTools > Console to monitor logs.`);
            } catch (err) {
                console.error('[NoteFmtHelper] Init error:', err);
                showDebugIndicator('Init error - script partially loaded. Try Ctrl+Shift+I on dialog open.', 'error');
            }
        }
        // DOM ready check
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', init);
        } else {
            init();
        }
        // Handle page navigation (Danbooru hash changes)
        window.addEventListener('hashchange', (e) => {
            setTimeout(() => {
                setupObserver();
                if (pollInterval) clearInterval(pollInterval);
                startPoll();
            }, 500);
            log('Page navigated; re-setup observers.');
        });
    } catch (globalErr) {
        console.error('[NoteFmtHelper] Global error - script failed to load:', globalErr);
        // Fallback: Create minimal debug indicator
        const indicator = document.createElement('div');
        indicator.id = 'debug-indicator';
        indicator.style.cssText = 'position:fixed;top:10px;right:10px;background:#ffcccc;color:#000;padding:5px;border:1px solid #ccc;z-index:9999;';
        indicator.textContent = '[NoteFmtHelper] Fatal error - reinstall script. Check console.';
        document.body.appendChild(indicator);
    }
})();