// ==UserScript==
// @name Danbooru Note Formatting Helper
// @namespace http://tampermonkey.net/
// @version 1.48.0
// @description A formatting helper toolbar for Danbooru note editing dialogs, adding buttons to wrap highlighted text with HTML tags for easy formatting. Now with Word-like ribbon layout, advanced markup buttons, upgraded color picker with eyedropper and expanded palette, symbols palette, undo/redo, auto-resize, live preview, and character count.
// @author FunkyJustin
// @license MIT
// @match https://danbooru.donmai.us/*
// @grant none
// ==/UserScript==
/*
Update History:
- v1.48.0: Fixed drag preview positioning during note creation by renaming the live preview element ID from 'note-preview' to 'nfh-preview' to avoid CSS conflicts with Danbooru's native #note-preview element used for temporary selection preview; total lines ~1020.
- v1.47.0: Fixed interference with note creation drag mode by delaying observer setup (2s after load) to avoid potential mutation conflicts during initial area selection, and removing unnecessary 'position: relative' style from dialog content to preserve original note display positioning; total lines ~1015.
- v1.46.0: Added ColorZilla extension link (🔗) next to eyedropper buttons in Firefox for fallback/advanced color picking; total lines ~1012.
- v1.45.0: Added Firefox support for EyeDropper API by updating titles to be browser-agnostic (no Chrome-specific mentions); disabled message now generic; total lines 982.
- v1.44.0: Upgraded color pickers (all features) with black/white swatches added to ROYGBIV palette and EyeDropper tool for easy screen/image color picking; fluid palette grid; total lines 978.
- v1.43.0: Removed Templates palette (no longer needed); removed auto <mark> highlight on selection (annoying); added character count display in toolbar and "Copy Note" button for convenience; improved mobile button sizing; total lines 928.
- v1.42.0: Added "Double Outline" template picker (pink text, white thick inner/black thin outer shadows, params for font/style/shadows); "Templates" palette with presets/customs (save/delete via localStorage); live HTML preview pane (toggleable); selection syntax highlight (yellow mark); export/import templates in settings; total lines 1042.
- v1.41.0: Fixed Pink Outline preview flash (JS-initialized shadows); added alpha sliders to Pink Outline text/outline (with rgba support); fluid symbols grid for mobile; total lines 892.
- v1.40.0: Added "Pink Outline" template button with parametric picker (font, size, colors, offset, blur) and live preview; icons updated; total lines ~850.
- v1.39.0: Added '♥' to symbols palette after other hearts; added Unicode icons to buttons (replacing text where possible) for ribbon-like Word appearance; total lines ~795.
- v1.38.0: Fixed syntax error by replacing template literals with concatenation to avoid injection parsing issues; added self-validation on load; total lines ~785.
- v1.37.0: Simplified labels to ASCII to avoid potential Unicode parsing issues in older engines; shortened debug messages; total lines ~810.
- v1.36.0: Shortened @description to avoid potential long-line parsing issues; total lines ~810.
- v1.35.0: Ensured full code completeness and syntax validation; no truncation issues.
- v1.34.0: Fixed syntax error in observer setup (subtree: true).
- v1.33.0: Replaced individual heart/dash buttons with "Symbols" palette popup; total lines ~810.
- v1.32.0: Added hyphen, en/em dashes insert buttons; total lines ~725.
- v1.31.0: Hoisted font dropdown action; replaced optional chaining; total lines ~702.
- v1.30.0: Added font family dropdown and heart buttons.
- v1.29.0: hexToRgb global; logging with line count (612 lines).
- v1.28.0: Fixed shadow outline; unified hexToRgb; resize delay.
- Earlier: Base features, color picker, undo/redo, auto-resize.
Analyzed script integrity on 2025-11-04; syntax validated, no errors found.
*/
(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.48.0',
toolbarId: 'note-formatting-toolbar',
buttons: [
// Font Group
{
id: 'font-family-dropdown',
type: 'dropdown',
title: 'Font Family (select and apply to text)',
options: [ // Generics first
{value: 'serif', label: 'Serif'},
{value: 'sans-serif', label: 'Sans Serif'},
{value: 'monospace', label: 'Monospace'},
{value: 'cursive', label: 'Cursive'},
{value: 'fantasy', label: 'Fantasy'},
// Danbooru-supplied (aliases as values, common labels)
{value: 'comic', label: 'Comic Sans MS'},
{value: 'narrow', label: 'Arial Narrow'},
{value: 'mono', label: 'Plex Mono'},
{value: 'slab sans', label: 'Impact'},
{value: 'slab serif', label: 'Rockwell'},
{value: 'formal serif', label: 'Formal Serif (Lora)'},
{value: 'formal cursive', label: 'Formal Cursive'},
{value: 'print', label: 'Print (Kalam)'},
{value: 'hand', label: 'Hand (Indie Flower)'},
{value: 'childlike', label: 'Childlike (Giselle)'},
{value: 'blackletter', label: 'Blackletter'},
{value: 'scary', label: 'Scary (Anarchy)'}
],
action: function(ta, value) { applyFontFamily(ta, value); }
},
{
id: 'font-size-btn',
label: 'Size',
title: 'Font Size (prompt px/em)',
action: function(ta) { promptFontSize(ta); }
},
{
id: 'pink-outline-btn',
label: 'Pink Outline',
title: 'Pink Outline Template (white text with pink outline, customizable)',
action: function(ta) { openPinkOutlinePicker(ta); }
},
{
id: 'double-outline-btn',
label: 'Double Outline',
title: 'Double Outline Template (pink text, white thick inner/black thin outer shadows)',
action: function(ta) { openDoubleOutlinePicker(ta); }
},
{
id: 'big-btn',
label: 'Big',
title: 'Big Text (<big>)',
action: function(ta) { applyFormat(ta, '<big>', '</big>'); }
},
{
id: 'small-btn',
label: 'Small',
title: 'Small Text (<small>)',
action: function(ta) { applyFormat(ta, '<small>', '</small>'); }
},
{
id: 'sup-btn',
label: 'Sup',
title: 'Superscript (<sup>)',
action: function(ta) { applyFormat(ta, '<sup>', '</sup>'); }
},
{
id: 'sub-btn',
label: 'Sub',
title: 'Subscript (<sub>)',
action: function(ta) { applyFormat(ta, '<sub>', '</sub>'); }
},
{
id: 'shadow-btn',
label: 'Shadow',
title: 'Text Shadow (picker for offset/blur/color, outline mode)',
action: function(ta) { openShadowPicker(ta); }
},
{
id: 'color-btn',
label: 'Color',
title: 'Text Color (click for palette/picker)',
action: function(ta) { openColorPicker(ta, 'color'); }
},
{
id: 'highlight-btn',
label: 'Highlight',
title: 'Text Highlight (background-color, click for palette/picker)',
action: function(ta) { openColorPicker(ta, 'background-color'); }
},
{
id: 'bold-btn',
label: 'B',
title: 'Bold (<b>) (Ctrl+B)',
action: function(ta) { applyFormat(ta, '<b>', '</b>'); }
},
{
id: 'italic-btn',
label: 'I',
title: 'Italic (<i>) (Ctrl+I)',
action: function(ta) { applyFormat(ta, '<i>', '</i>'); }
},
{
id: 'underline-btn',
label: 'U',
title: 'Underline (<u>) (Ctrl+U)',
action: function(ta) { applyFormat(ta, '<u>', '</u>'); }
},
{
id: 'strikethrough-btn',
label: 'S',
title: 'Strikethrough (<s>)',
action: function(ta) { applyFormat(ta, '<s>', '</s>'); }
},
// Paragraph Group
{
id: 'align-left-btn',
label: 'Left',
title: 'Align Left (<div align="left">)',
action: function(ta) { applyFormat(ta, '<div align="left">', '</div>'); }
},
{
id: 'align-center-btn',
label: 'Center',
title: 'Align Center (<div align="center">)',
action: function(ta) { applyFormat(ta, '<div align="center">', '</div>'); }
},
{
id: 'align-right-btn',
label: 'Right',
title: 'Align Right (<div align="right">)',
action: function(ta) { applyFormat(ta, '<div align="right">', '</div>'); }
},
// Insert Group
{
id: 'link-btn',
label: 'Link',
title: 'Insert Link (<a href="...">)',
action: function(ta) { insertLink(ta); }
},
{
id: 'tn-btn',
label: 'TN',
title: 'Translator Note (<tn>)',
action: function(ta) { applyFormat(ta, '<tn>', '</tn>'); }
},
{
id: 'symbols-btn',
label: 'Symbols',
title: 'Symbols Palette (hearts, dashes, ★ ☆ • … etc.)',
action: function(ta) { openSymbolsPicker(ta); }
},
// Clear/History/Settings Group
{
id: 'clear-btn',
label: 'Clear',
title: 'Clear Formatting (strip HTML tags)',
action: function(ta) { clearFormatting(ta); }
},
{
id: 'copy-btn',
label: 'Copy',
title: 'Copy Entire Note to Clipboard',
action: function(ta) { copyNote(ta); }
},
{
id: 'undo-btn',
label: 'Undo',
title: 'Undo (Ctrl+Z)',
action: function(ta) { undo(ta); }
},
{
id: 'redo-btn',
label: 'Redo',
title: 'Redo (Ctrl+Y)',
action: function(ta) { redo(ta); }
},
{
id: 'toggle-resize-btn',
label: 'AR',
title: 'Toggle Auto-Resize On/Off',
action: function(ta) { toggleAutoResize(); }
},
{
id: 'toggle-center-btn',
label: 'AC',
title: 'Toggle Auto-Center On/Off',
action: function(ta) { toggleAutoCenter(); }
},
{
id: 'toggle-preview-btn',
label: 'Preview',
title: 'Toggle Live HTML Preview Pane',
action: function(ta) { togglePreview(); }
},
{
id: 'settings-btn',
label: 'Settings',
title: 'Set Min Width/Height for Auto-Resize',
action: function(ta) { openSettings(); }
}
],
icons: {
'font-size-btn': '📏',
'pink-outline-btn': '💖',
'double-outline-btn': '💎',
'big-btn': '↗️',
'small-btn': '↙️',
'sup-btn': '²',
'sub-btn': '₂',
'shadow-btn': '🌑',
'color-btn': '🎨',
'highlight-btn': '🖍️',
'bold-btn': '𝐛',
'italic-btn': '𝑖',
'underline-btn': '𝑢',
'strikethrough-btn': '𝑠',
'align-left-btn': '◀',
'align-center-btn': '▢',
'align-right-btn': '▶',
'link-btn': '🔗',
'tn-btn': 'TN',
'symbols-btn': '♥',
'clear-btn': '🧹',
'copy-btn': '📋',
'undo-btn': '↶',
'redo-btn': '↷',
'toggle-resize-btn': '📐',
'toggle-center-btn': '🎯',
'toggle-preview-btn': '👁',
'settings-btn': '⚙️'
},
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: ['#000000', '#FFFFFF', '#FF0000', '#FF8000', '#FFFF00', '#00FF00', '#0000FF', '#4B0082', '#8B00FF'], // Black, White + ROYGBIV hex
symbols: ['♡', '❤', '♥', '★', '☆', '•', '◦', '…', '‥', '—', '–', '-', '⸺', '⸻', '©', '™', '®', '§', '¶', '†', '‡', '°', '±', '×', '÷', '≈', '≠', '≤', '≥'], // Common translation/math symbols
autoResize: {
enabled: true,
defaultWidth: 523,
defaultHeight: 362
},
autoCenter: {
enabled: true
},
previewEnabled: false,
debounceDelay: 300, // ms for input events
colorZillaUrl: 'https://addons.mozilla.org/en-US/firefox/addon/colorzilla/',
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-formatting-toolbar select { " +
"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 select:focus { " +
"outline: 1px solid #007cba; " +
"background: #e6f3ff; " +
"} " +
".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, #shadow-picker, #symbol-picker, #pink-outline-picker, #double-outline-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; " +
"max-height: 80vh; " +
"overflow-y: auto; " +
"} " +
"#color-picker h3, #shadow-picker h3, #symbol-picker h3, #pink-outline-picker h3, #double-outline-picker h3 { " +
"margin: 0 0 10px 0; " +
"text-align: center; " +
"color: #F0F0F0; " +
"} " +
".palette { " +
"display: grid; " +
"grid-template-columns: repeat(auto-fit, minmax(30px, 1fr)); " +
"gap: 2px; " +
"margin-bottom: 15px; " +
"} " +
".symbols-palette { " +
"display: grid; " +
"grid-template-columns: repeat(auto-fit, minmax(40px, 1fr)); " +
"gap: 10px; " +
"margin-bottom: 15px; " +
"justify-items: center; " +
"} " +
".palette button, .symbols-palette button { " +
"width: 30px; " +
"height: 30px; " +
"border: 1px solid #666; " +
"border-radius: 4px; " +
"cursor: pointer; " +
"font-size: 16px; " +
"background: #333; " +
"color: #E0E0E0; " +
"} " +
".palette button:hover, .symbols-palette button:hover { " +
"background: #444; " +
"} " +
".custom-section, .shadow-section, .outline-section { " +
"margin-bottom: 10px; " +
"} " +
".custom-section label, .shadow-section label, .outline-section label { " +
"display: block; " +
"margin-bottom: 5px; " +
"color: #D0D0D0; " +
"} " +
".shadow-inputs, .outline-inputs { " +
"display: flex; " +
"flex-direction: column; " +
"gap: 5px; " +
"} " +
".shadow-inputs input[type=\"number\"], .outline-inputs input[type=\"number\"] { " +
"background: #333; " +
"color: #fff; " +
"border: 1px solid #666; " +
"padding: 2px; " +
"} " +
".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; " +
"} " +
".hex-input { " +
"margin-bottom: 10px; " +
"} " +
".hex-input input[type=\"text\"] { " +
"width: 100px; " +
"background: #333; " +
"color: #fff; " +
"border: 1px solid #666; " +
"text-transform: uppercase; " +
"} " +
".eyedropper-btn { " +
"padding: 4px 8px; " +
"font-size: 12px; " +
"background: #555; " +
"color: #E0E0E0; " +
"border: 1px solid #666; " +
"border-radius: 4px; " +
"cursor: pointer; " +
"margin-top: 5px; " +
"width: 100%; " +
"} " +
".eyedropper-btn:hover { " +
"background: #666; " +
"} " +
".eyedropper-btn:disabled { " +
"background: #333; " +
"cursor: not-allowed; " +
"} " +
".extension-link { " +
"display: inline-block; " +
"margin-left: 5px; " +
"font-size: 16px; " +
"text-decoration: none; " +
"color: #E0E0E0; " +
"vertical-align: middle; " +
"} " +
".extension-link:hover { " +
"color: #007cba; " +
"} " +
"#color-preview, #shadow-preview, #outline-preview, #double-preview { " +
"width: 100%; " +
"height: 40px; " +
"border: 1px solid #666; " +
"border-radius: 4px; " +
"margin-bottom: 10px; " +
"display: flex; " +
"align-items: center; " +
"justify-content: center; " +
"color: #fff; " +
"font-weight: bold; " +
"font-size: 18px; " +
"background: #333; " +
"} " +
".preset-buttons { " +
"display: flex; " +
"gap: 5px; " +
"margin-bottom: 10px; " +
"} " +
".preset-buttons button { " +
"padding: 4px 8px; " +
"background: #444; " +
"color: #E0E0E0; " +
"border: 1px solid #666; " +
"border-radius: 4px; " +
"cursor: pointer; " +
"font-size: 10px; " +
"} " +
".preset-buttons button:hover { " +
"background: #555; " +
"} " +
".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, #shadow-picker-overlay, #symbol-picker-overlay, #pink-outline-picker-overlay, #double-outline-picker-overlay { " +
"position: fixed; " +
"top: 0; " +
"left: 0; " +
"width: 100%; " +
"height: 100%; " +
"background: rgba(0,0,0,0.5); " +
"z-index: 9999; " +
"} " +
".font-family-preview { " +
"font-family: inherit; " +
"font-size: inherit; " +
"font-weight: inherit; " +
"color: inherit; " +
"text-shadow: inherit; " +
"white-space: nowrap; " +
"overflow: hidden; " +
"text-overflow: ellipsis; " +
"} " +
"#nfh-preview { " +
"margin-top: 10px; " +
"padding: 10px; " +
"border: 1px solid #ccc; " +
"background: white; " +
"min-height: 100px; " +
"overflow: auto; " +
"font-family: sans-serif; " +
"font-size: 14px; " +
"border-radius: 4px; " +
"display: none; " + // Hidden by default
"} " +
"#nfh-preview.show { " +
"display: block; " +
"} " +
"#double-preview { " +
"background-color: #333; " +
"color: #D87C86; " +
"font-size: 20px; " +
"font-family: print; " +
"font-weight: bold; " +
"display: flex; " +
"align-items: center; " +
"justify-content: center; " +
"height: 50px; " +
"border: 1px solid #666; " +
"border-radius: 4px; " +
"margin-bottom: 10px; " +
"text-shadow: none; " + // JS sets shadows
"} " +
"#char-count { " +
"margin-left: auto; " +
"font-size: 10px; " +
"color: #666; " +
"padding: 2px 5px; " +
"background: #f9f9f9; " +
"border-radius: 3px; " +
"border: 1px solid #ddd; " +
"align-self: center; " +
"min-width: 60px; " +
"text-align: center; " +
"} " +
"@media (max-width: 600px) { " +
" #note-formatting-toolbar button, #note-formatting-toolbar select { " +
" font-size: 10px; " +
" padding: 3px 4px; " +
" } " +
" #note-formatting-toolbar .group { " +
" flex-wrap: wrap; " +
" gap: 1px; " +
" } " +
"}"
};
// Global variables for state management (per-tab isolation for multi-tab support)
var observer = null;
var activeDialogs = new Set(); // Track multiple dialogs if possible (rare, but robust)
var toolbar = null;
var textarea = null;
var previewDiv = null;
var charCountSpan = null;
var isMinimized = false;
var debugMode = true; // Enable for logging; set to false in prod if needed
var manualTriggerHandler = null;
var history = []; // Undo/redo history array of {value, start, end}
var historyIndex = -1; // Current position in history
var colorPicker = null; // Color picker dialog
var shadowPicker = null; // Shadow picker dialog
var symbolPicker = null; // Symbols picker dialog
var pinkOutlinePicker = null; // Pink outline picker dialog
var doubleOutlinePicker = null; // Double outline picker dialog
var debounceTimer = null; // For input debouncing
// Utility: Detect Firefox
function isFirefox() {
return navigator.userAgent.includes('Firefox');
}
// Utility: Add ColorZilla link next to eyedropper button
function addColorZillaLink(eyedropperBtn) {
if (!isFirefox()) return;
const link = document.createElement('a');
link.href = CONFIG.colorZillaUrl;
link.target = '_blank';
link.className = 'extension-link';
link.innerHTML = '🔗';
link.title = 'ColorZilla Extension for advanced color picking';
link.style.cssText = 'display: inline-block; margin-left: 5px; font-size: 16px; text-decoration: none; color: #E0E0E0; vertical-align: middle;';
eyedropperBtn.parentNode.insertBefore(link, eyedropperBtn.nextSibling);
log('Added ColorZilla link next to eyedropper in Firefox.');
}
// Utility: Log with prefix for easy console filtering - uses CONFIG.version safely
function log(message, data) {
if (typeof data === 'undefined') data = null;
var version = CONFIG ? CONFIG.version : 'unknown';
console.log('[NoteFmtHelper v' + version + '] ' + message, data || '');
}
// Unified hexToRgb (strips # if present) - moved to global scope to eliminate duplication
function hexToRgb(hex) {
hex = hex.replace(/^#/, '');
var r = parseInt(hex.substr(0, 2), 16);
var g = parseInt(hex.substr(2, 2), 16);
var b = parseInt(hex.substr(4, 2), 16);
return { r: r, g: g, b: b };
}
// Apply font family - hoisted for syntax safety
function applyFontFamily(ta, value) {
if (!value) return;
// Fallback to generic if needed (per docs)
var family = value.includes(' ') ? value + ', serif' : value + ', sans-serif';
var openTag = '<span style="font-family: ' + family + ';">';
applyFormat(ta, openTag, '</span>');
}
// Load configs from localStorage
function loadConfigs() {
try {
loadAutoResizeConfig();
log('Configs loaded.');
} catch (err) {
log('Load configs error', err);
}
}
// Load auto-resize and auto-center config from localStorage (persistent across tabs/sessions)
function loadAutoResizeConfig() {
try {
var savedEnabled = localStorage.getItem('nfh_autoResizeEnabled');
if (savedEnabled !== null) {
CONFIG.autoResize.enabled = savedEnabled === 'true';
}
var savedWidth = localStorage.getItem('nfh_defaultWidth');
if (savedWidth && !isNaN(parseInt(savedWidth))) {
CONFIG.autoResize.defaultWidth = parseInt(savedWidth);
}
var savedHeight = localStorage.getItem('nfh_defaultHeight');
if (savedHeight && !isNaN(parseInt(savedHeight))) {
CONFIG.autoResize.defaultHeight = parseInt(savedHeight);
}
var savedCenterEnabled = localStorage.getItem('nfh_autoCenterEnabled');
if (savedCenterEnabled !== null) {
CONFIG.autoCenter.enabled = savedCenterEnabled === 'true';
}
log('Auto-resize and auto-center config loaded from localStorage.', { autoResize: CONFIG.autoResize, autoCenter: CONFIG.autoCenter });
} 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;
var style = document.createElement('style');
style.id = 'note-formatting-styles';
style.textContent = CONFIG.styles;
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 - removed templates
var groups = {
font: ['font-family-dropdown', 'font-size-btn', 'pink-outline-btn', 'double-outline-btn', 'big-btn', 'small-btn', 'sup-btn', 'sub-btn', 'shadow-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', 'symbols-btn'],
history: ['clear-btn', 'copy-btn', 'undo-btn', 'redo-btn', 'toggle-resize-btn', 'toggle-center-btn', 'toggle-preview-btn', 'settings-btn']
};
Object.entries(groups).forEach(function(entry) {
var groupName = entry[0];
var btnIds = entry[1];
var group = document.createElement('div');
group.className = 'group';
group.title = groupName.charAt(0).toUpperCase() + groupName.slice(1); // e.g., "Font"
btnIds.forEach(function(btnId) {
var btnConfig = CONFIG.buttons.find(function(b) { return b.id === btnId; });
if (btnConfig) {
if (btnConfig.type === 'dropdown') {
// Create select for dropdown
var select = document.createElement('select');
select.id = btnConfig.id;
select.title = btnConfig.title;
// Default to first option
select.value = btnConfig.options[0] ? btnConfig.options[0].value : '';
btnConfig.options.forEach(function(opt) {
var option = document.createElement('option');
option.value = opt.value;
option.textContent = opt.label;
option.style.fontFamily = opt.value; // Preview in font style
select.appendChild(option);
});
select.addEventListener('change', function(e) {
var value = e.target.value;
if (textarea && !textarea.disabled) {
if (document.activeElement !== textarea) textarea.focus();
saveState(textarea); // Pre-snapshot
btnConfig.action(textarea, value);
// Reset to first for quick re-use, but keep preview
setTimeout(function() {
select.value = btnConfig.options[0] ? btnConfig.options[0].value : '';
}, 0);
} else {
log('Dropdown changed but textarea not ready.');
}
});
group.appendChild(select);
} else {
// Standard button
var button = document.createElement('button');
button.id = btnConfig.id;
button.type = 'button';
button.innerHTML = CONFIG.icons[btnConfig.id] || btnConfig.label;
button.title = btnConfig.title;
button.addEventListener('click', function(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/copy)
if (!['undo-btn', 'redo-btn', 'toggle-resize-btn', 'toggle-center-btn', 'toggle-preview-btn', 'settings-btn', 'copy-btn'].includes(btnConfig.id)) {
saveState(textarea);
}
btnConfig.action(textarea);
log('Button ' + btnConfig.id + ' clicked.');
} else if (['toggle-resize-btn', 'toggle-center-btn', 'toggle-preview-btn', 'settings-btn', 'copy-btn'].includes(btnConfig.id)) {
// Allow these even without textarea focus (global config)
btnConfig.action(textarea);
log(btnConfig.id + ' button clicked (global).');
} else {
log('Button clicked but textarea not ready.', { focused: document.activeElement });
}
});
group.appendChild(button);
}
}
});
toolbar.appendChild(group);
});
// Character count span
charCountSpan = document.createElement('span');
charCountSpan.id = 'char-count';
charCountSpan.textContent = '0 chars';
toolbar.appendChild(charCountSpan);
// Minimize button (smaller, optional for buttonset fit)
var minimizeBtn = document.createElement('button');
minimizeBtn.id = 'toolbar-minimize';
minimizeBtn.type = 'button';
minimizeBtn.innerHTML = '-'; // Plain minus for minimize
minimizeBtn.title = 'Minimize Formatting Toolbar';
minimizeBtn.style.fontSize = '0.8em';
minimizeBtn.style.padding = '0.3em 0.5em';
minimizeBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
toggleMinimize();
});
toolbar.appendChild(minimizeBtn);
// Version display (small span, not a button) - in corner as per style
var versionSpan = document.createElement('span');
versionSpan.id = 'toolbar-version';
versionSpan.textContent = 'v' + CONFIG.version;
versionSpan.className = 'ui-widget';
versionSpan.title = 'v' + CONFIG.version + ' - Upgraded Color Picker + Eyedropper (Firefox supported with ColorZilla link)';
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, double outline, and preview toggle.');
return toolbar;
} catch (err) {
log('Toolbar creation error', err);
return null;
}
}
// Update character count
function updateCharCount() {
if (charCountSpan && textarea) {
charCountSpan.textContent = textarea.value.length + ' chars';
}
}
// Copy entire note to clipboard
function copyNote(ta) {
try {
navigator.clipboard.writeText(ta.value).then(function() {
showDebugIndicator('Note copied to clipboard!', 'success');
log('Note copied.');
}).catch(function(err) {
// Fallback
var textArea = document.createElement('textarea');
textArea.value = ta.value;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
showDebugIndicator('Note copied to clipboard!', 'success');
log('Note copied (fallback).');
});
} catch (err) {
log('Copy note error', err);
showDebugIndicator('Copy failed; check permissions.', 'error');
}
}
// Minimize/Maximize functionality for better UX (hides main buttons, including dropdowns)
function toggleMinimize() {
try {
isMinimized = !isMinimized;
const mainButtons = toolbar ? toolbar.querySelectorAll('button:not(#toolbar-minimize), select') : [];
const versionSpan = document.getElementById('toolbar-version');
const minimizeBtn = document.getElementById('toolbar-minimize');
const charCount = document.getElementById('char-count');
if (isMinimized) {
mainButtons.forEach(function(el) { el.style.display = 'none'; });
if (versionSpan) versionSpan.style.display = 'none';
if (charCount) charCount.style.display = 'none';
if (minimizeBtn) {
minimizeBtn.innerHTML = '+'; // Plus for maximize
minimizeBtn.title = 'Expand Formatting Toolbar';
}
log('Formatting toolbar minimized.');
} else {
mainButtons.forEach(function(el) { el.style.display = ''; });
if (versionSpan) versionSpan.style.display = '';
if (charCount) charCount.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) - improved with matchMedia
function adjustToolbarLayout() {
try {
const isMobile = window.matchMedia('(max-width: 800px)').matches;
if (toolbar && isMobile) {
toolbar.style.flexDirection = 'column';
toolbar.style.alignItems = 'stretch';
toolbar.querySelectorAll('button, select').forEach(function(el) { el.style.minWidth = 'auto'; el.style.fontSize = '10px'; el.style.padding = '3px 4px'; });
} else {
toolbar.style.flexDirection = 'row';
toolbar.style.alignItems = 'center';
toolbar.querySelectorAll('button, select').forEach(function(el) { el.style.minWidth = 'initial'; el.style.fontSize = ''; el.style.padding = ''; });
}
} catch (err) {
log('Layout adjustment error', err);
}
}
// Center dialog on viewport (with bounds clamping)
function centerDialog(dialog) {
try {
// Settle layout
setTimeout(function() {
const rect = dialog.getBoundingClientRect();
const vw = window.innerWidth;
const vh = window.innerHeight;
let left = (vw - rect.width) / 2;
let top = (vh - rect.height) / 2;
// Clamp to viewport bounds
left = Math.max(0, Math.min(left, vw - rect.width));
top = Math.max(0, Math.min(top, vh - rect.height));
// Account for scroll
dialog.style.left = (left + window.scrollX) + 'px';
dialog.style.top = (top + window.scrollY) + 'px';
log('Centered dialog at ' + (left + window.scrollX) + 'px, ' + (top + window.scrollY) + 'px');
}, 0);
} catch (err) {
log('Center dialog error', err);
}
}
// Auto-expand dialog to fit ribbon + textarea + preview if enabled
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');
const pv = dialog.querySelector('#nfh-preview');
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();
const pvHeight = pv && CONFIG.previewEnabled ? pv.scrollHeight + 20 : 0;
// Calculate content dimensions
const contentWidth = Math.max(tbRect.width, taRect.width) + 30; // Padding for borders/scroll
const contentHeight = tbRect.height + taRect.height + pvHeight + 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 (preview: ' + (pvHeight > 0) + ')');
dialog.dataset.helperResized = 'true';
// Auto-center if enabled
if (CONFIG.autoCenter.enabled) {
centerDialog(dialog);
}
} 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(function() { autoResizeDialog(dialog); }, 100);
}
}
} catch (err) {
log('Toggle auto-resize error', err);
}
}
// Toggle auto-center enable/disable
function toggleAutoCenter() {
try {
CONFIG.autoCenter.enabled = !CONFIG.autoCenter.enabled;
localStorage.setItem('nfh_autoCenterEnabled', CONFIG.autoCenter.enabled.toString());
const status = CONFIG.autoCenter.enabled ? 'enabled' : 'disabled';
log('Auto-center ' + status + '.');
showDebugIndicator('Auto-center ' + status + '.', 'info');
// If enabling, re-apply to current dialog
if (CONFIG.autoCenter.enabled) {
const dialog = document.querySelector('.note-edit-dialog');
if (dialog) {
centerDialog(dialog);
}
}
} catch (err) {
log('Toggle auto-center error', err);
}
}
// Toggle live preview pane
function togglePreview() {
try {
CONFIG.previewEnabled = !CONFIG.previewEnabled;
if (previewDiv) {
previewDiv.classList.toggle('show', CONFIG.previewEnabled);
if (CONFIG.previewEnabled) {
updatePreview();
}
}
const status = CONFIG.previewEnabled ? 'enabled' : 'disabled';
log('Live preview ' + status + '.');
showDebugIndicator('Live preview ' + status + '.', 'info');
// Re-resize if enabled
const dialog = document.querySelector('.note-edit-dialog');
if (dialog) {
dialog.dataset.helperResized = 'false';
setTimeout(function() { autoResizeDialog(dialog); }, 100);
}
// Setup listener if enabling
if (CONFIG.previewEnabled && textarea) {
textarea.addEventListener('input', debounceUpdatePreview);
} else if (!CONFIG.previewEnabled && textarea) {
textarea.removeEventListener('input', debounceUpdatePreview);
}
} catch (err) {
log('Toggle preview error', err);
}
}
// Debounced preview update
function debounceUpdatePreview() {
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(updatePreview, CONFIG.debounceDelay);
}
// Update preview div with sanitized HTML
function updatePreview() {
try {
if (!previewDiv || !textarea) return;
var html = textarea.value;
// Minimal sanitize: Remove <script> to prevent XSS (notes shouldn't have it)
html = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
previewDiv.innerHTML = html;
log('Preview updated.');
} catch (err) {
log('Preview update error', err);
previewDiv.innerHTML = 'Preview error: Invalid HTML.';
}
}
// Inject preview div below textarea
function injectPreview(dialog) {
try {
if (previewDiv) return;
previewDiv = document.createElement('div');
previewDiv.id = 'nfh-preview';
if (CONFIG.previewEnabled) previewDiv.classList.add('show');
const content = dialog.querySelector('.ui-dialog-content');
content.appendChild(previewDiv);
if (CONFIG.previewEnabled && textarea) {
updatePreview();
textarea.addEventListener('input', debounceUpdatePreview);
}
log('Preview pane injected.');
} catch (err) {
log('Inject preview error', err);
}
}
// Open settings for width/height mins
function openSettings() {
try {
var newWidthStr = prompt('Enter default min width (px):', CONFIG.autoResize.defaultWidth.toString());
var 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());
var newHeightStr = prompt('Enter default min height (px):', CONFIG.autoResize.defaultHeight.toString());
var 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('Settings 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(function() { 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);
var newText;
var 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);
// Update preview if enabled
if (CONFIG.previewEnabled) debounceUpdatePreview();
updateCharCount();
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 {
closeAllPickers();
// 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';
var titleText = (type === 'background-color') ? 'Text Highlight' : 'Text Color';
colorPicker.innerHTML = '<h3>' + titleText + '</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>' +
'<div class="hex-input">' +
'<label>Hex (#RRGGBB):</label>' +
'<input type="text" id="hex-input" value="#FF0000" maxlength="7" />' +
'</div>' +
'<div style="display: flex; align-items: center; justify-content: space-between;">' +
'<button id="eyedropper" class="eyedropper-btn" title="Pick color from screen" style="width: auto; flex: 1; margin-right: 5px;">👁 Pick from Screen</button>' +
'</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, OK, cancel, events (same as before)
const palette = colorPicker.querySelector('.palette');
CONFIG.commonColors.forEach(function(color) {
const btn = document.createElement('button');
btn.style.backgroundColor = color;
btn.addEventListener('click', function() {
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;
colorPicker.querySelector('#hex-input').value = color;
updateColor(colorPicker);
});
palette.appendChild(btn);
});
colorPicker.querySelector('#picker-ok').addEventListener('click', function(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>');
closeColorPicker();
});
colorPicker.querySelector('#picker-cancel').addEventListener('click', closeColorPicker);
overlay.addEventListener('click', closeColorPicker);
const escHandler = function(e) {
if (e.key === 'Escape') closeColorPicker();
};
window.addEventListener('keydown', escHandler);
// Eyedropper
const eyedropperBtn = colorPicker.querySelector('#eyedropper');
addColorZillaLink(eyedropperBtn);
if (eyedropperBtn && 'eyedropper' in navigator) {
eyedropperBtn.addEventListener('click', async function() {
try {
const result = await navigator.eyedropper.pick({withAlpha: true});
if (result && result.sRGBHex) {
const hexFull = result.sRGBHex;
const alphaHex = hexFull.slice(-2);
const hex = '#' + hexFull.slice(1,7).toUpperCase();
const r = parseInt(hexFull.slice(1,3), 16);
const g = parseInt(hexFull.slice(3,5), 16);
const b = parseInt(hexFull.slice(5,7), 16);
const alpha = parseInt(alphaHex, 16) / 255;
colorPicker.querySelector('#r-slider').value = r;
colorPicker.querySelector('#g-slider').value = g;
colorPicker.querySelector('#b-slider').value = b;
colorPicker.querySelector('#r-input').value = r;
colorPicker.querySelector('#g-input').value = g;
colorPicker.querySelector('#b-input').value = b;
colorPicker.querySelector('#hex-input').value = hex;
colorPicker.querySelector('#alpha-slider').value = alpha;
colorPicker.querySelector('#alpha-value').textContent = alpha.toFixed(2);
updateColor(colorPicker);
}
} catch (err) {
log('Eyedropper error:', err);
showDebugIndicator('Eyedropper cancelled or failed.', 'info');
}
});
} else if (eyedropperBtn) {
eyedropperBtn.disabled = true;
eyedropperBtn.title = 'EyeDropper API not supported in this browser';
}
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;
const hex = '#' + Math.round(r).toString(16).padStart(2, '0') + Math.round(g).toString(16).padStart(2, '0') + Math.round(b).toString(16).padStart(2, '0');
picker.querySelector('#hex-input').value = hex.toUpperCase();
}
function updateSlider(channel, picker) {
const input = picker.querySelector('#' + channel + '-input').value;
picker.querySelector('#' + channel + '-slider').value = input;
updateColor(picker);
}
colorPicker.addEventListener('input', function(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);
}
} else if (e.target.id === 'hex-input') {
const hexValue = e.target.value.replace('#', '').toUpperCase();
if (hexValue.match(/^([0-9A-F]{6})$/)) {
const rgb = hexToRgb(hexValue);
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);
}
}
});
updateColor(colorPicker);
function closeColorPicker() {
if (colorPicker) colorPicker.remove();
if (overlay) overlay.remove();
colorPicker = null;
window.removeEventListener('keydown', escHandler);
}
} catch (err) {
log('Color picker open error', err);
}
}
// Close all open pickers
function closeAllPickers() {
if (colorPicker) closeColorPicker();
if (shadowPicker) closeShadowPicker();
if (symbolPicker) closeSymbolsPicker();
if (pinkOutlinePicker) closePinkOutlinePicker();
if (doubleOutlinePicker) closeDoubleOutlinePicker();
}
function closeColorPicker() {
if (colorPicker) {
const overlay = document.getElementById('color-picker-overlay');
if (overlay) overlay.remove();
colorPicker.remove();
colorPicker = null;
}
}
function closeShadowPicker() {
if (shadowPicker) {
const overlay = document.getElementById('shadow-picker-overlay');
if (overlay) overlay.remove();
shadowPicker.remove();
shadowPicker = null;
}
}
function closeSymbolsPicker() {
if (symbolPicker) {
const overlay = document.getElementById('symbol-picker-overlay');
if (overlay) overlay.remove();
symbolPicker.remove();
symbolPicker = null;
}
}
function closePinkOutlinePicker() {
if (pinkOutlinePicker) {
const overlay = document.getElementById('pink-outline-picker-overlay');
if (overlay) overlay.remove();
pinkOutlinePicker.remove();
pinkOutlinePicker = null;
}
}
function closeDoubleOutlinePicker() {
if (doubleOutlinePicker) {
const overlay = document.getElementById('double-outline-picker-overlay');
if (overlay) overlay.remove();
doubleOutlinePicker.remove();
doubleOutlinePicker = null;
}
}
// Open shadow picker for text shadow (outlines/drop shadows)
function openShadowPicker(ta) {
try {
closeAllPickers();
// Overlay and HTML (same as before, added eyedropper)
const overlay = document.createElement('div');
overlay.id = 'shadow-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);
shadowPicker = document.createElement('div');
shadowPicker.id = 'shadow-picker';
shadowPicker.innerHTML = '<h3>Text Shadow</h3>' +
'<div class="preset-buttons">' +
'<button id="preset-drop">Drop Shadow</button>' +
'<button id="preset-outline">Outline</button>' +
'</div>' +
'<div class="shadow-section">' +
'<div class="shadow-inputs">' +
'<label>X Offset (px): <input type="number" id="x-offset" value="1" step="0.1" min="-10" max="10" aria-label="X offset" /></label>' +
'<label>Y Offset (px): <input type="number" id="y-offset" value="1" step="0.1" min="-10" max="10" aria-label="Y offset" /></label>' +
'<label>Blur Radius (px): <input type="number" id="blur" value="0" step="0.1" min="0" max="20" aria-label="Blur radius" /></label>' +
'<label><input type="checkbox" id="outline-mode" /> Outline Mode (4 shadows around text)</label>' +
'</div>' +
'</div>' +
'<div class="palette"></div>' +
'<div class="custom-section">' +
'<label>Shadow Color</label>' +
'<div class="rgb-sliders">' +
'<input type="range" id="r-slider" min="0" max="255" value="0" />' +
'<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="0" />' +
'<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>' +
'<div class="hex-input">' +
'<label>Hex (#RRGGBB):</label>' +
'<input type="text" id="hex-input" value="#000000" maxlength="7" />' +
'</div>' +
'<div style="display: flex; align-items: center; justify-content: space-between;">' +
'<button id="eyedropper-shadow" class="eyedropper-btn" title="Pick color from screen" style="width: auto; flex: 1; margin-right: 5px;">👁 Pick from Screen</button>' +
'</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="shadow-preview" style="background-color: #333; color: #fff; text-shadow: 1px 1px 0 #000;">Preview</div>' +
'<div class="picker-buttons">' +
'<button id="picker-ok">OK</button>' +
'<button id="picker-cancel">Cancel</button>' +
'</div>';
document.body.appendChild(shadowPicker);
// Palette, presets, listeners (same as v1.41, added eyedropper)
const palette = shadowPicker.querySelector('.palette');
CONFIG.commonColors.forEach(function(color) {
const btn = document.createElement('button');
btn.style.backgroundColor = color;
btn.addEventListener('click', function() {
const rgb = hexToRgb(color);
shadowPicker.querySelector('#r-slider').value = rgb.r;
shadowPicker.querySelector('#g-slider').value = rgb.g;
shadowPicker.querySelector('#b-slider').value = rgb.b;
shadowPicker.querySelector('#r-input').value = rgb.r;
shadowPicker.querySelector('#g-input').value = rgb.g;
shadowPicker.querySelector('#b-input').value = rgb.b;
shadowPicker.querySelector('#hex-input').value = color;
updateShadowPreview(shadowPicker);
});
palette.appendChild(btn);
});
shadowPicker.querySelector('#preset-drop').addEventListener('click', function() {
shadowPicker.querySelector('#x-offset').value = 2;
shadowPicker.querySelector('#y-offset').value = 2;
shadowPicker.querySelector('#blur').value = 2;
shadowPicker.querySelector('#outline-mode').checked = false;
shadowPicker.querySelector('#r-slider').value = 0;
shadowPicker.querySelector('#g-slider').value = 0;
shadowPicker.querySelector('#b-slider').value = 0;
shadowPicker.querySelector('#r-input').value = 0;
shadowPicker.querySelector('#g-input').value = 0;
shadowPicker.querySelector('#b-input').value = 0;
shadowPicker.querySelector('#hex-input').value = '#000000';
updateShadowPreview(shadowPicker);
});
shadowPicker.querySelector('#preset-outline').addEventListener('click', function() {
shadowPicker.querySelector('#x-offset').value = 1;
shadowPicker.querySelector('#y-offset').value = 1;
shadowPicker.querySelector('#blur').value = 0;
shadowPicker.querySelector('#outline-mode').checked = true;
shadowPicker.querySelector('#r-slider').value = 0;
shadowPicker.querySelector('#g-slider').value = 0;
shadowPicker.querySelector('#b-slider').value = 0;
shadowPicker.querySelector('#r-input').value = 0;
shadowPicker.querySelector('#g-input').value = 0;
shadowPicker.querySelector('#b-input').value = 0;
shadowPicker.querySelector('#hex-input').value = '#000000';
updateShadowPreview(shadowPicker);
});
shadowPicker.querySelector('#outline-mode').addEventListener('change', function(e) {
updateShadowPreview(shadowPicker);
});
shadowPicker.querySelector('#picker-ok').addEventListener('click', function(e) {
var x = parseFloat(shadowPicker.querySelector('#x-offset').value) || 1;
var y = parseFloat(shadowPicker.querySelector('#y-offset').value) || 1;
var blur = parseFloat(shadowPicker.querySelector('#blur').value) || 0;
x = Math.max(-10, Math.min(10, x)); // Clamp
y = Math.max(-10, Math.min(10, y));
blur = Math.max(0, Math.min(20, blur));
const r = shadowPicker.querySelector('#r-slider').value;
const g = shadowPicker.querySelector('#g-slider').value;
const b = shadowPicker.querySelector('#b-slider').value;
const alpha = shadowPicker.querySelector('#alpha-slider').value;
const colorValue = (alpha < 1) ? 'rgba(' + r + ', ' + g + ', ' + b + ', ' + alpha + ')' : 'rgb(' + r + ', ' + g + ', ' + b + ')';
var shadowRule;
const absX = Math.abs(x);
const absY = Math.abs(y);
if (shadowPicker.querySelector('#outline-mode').checked) {
shadowRule = '-' + absX + 'px -' + absY + 'px ' + blur + 'px ' + colorValue + ', ' + absX + 'px -' + absY + 'px ' + blur + 'px ' + colorValue + ', -' + absX + 'px ' + absY + 'px ' + blur + 'px ' + colorValue + ', ' + absX + 'px ' + absY + 'px ' + blur + 'px ' + colorValue;
} else {
shadowRule = x + 'px ' + y + 'px ' + blur + 'px ' + colorValue;
}
const styleStr = 'text-shadow: ' + shadowRule + ';';
const openTag = '<span style="' + styleStr + '">';
applyFormat(ta, openTag, '</span>');
closeShadowPicker();
});
shadowPicker.querySelector('#picker-cancel').addEventListener('click', closeShadowPicker);
overlay.addEventListener('click', closeShadowPicker);
const escHandler = function(e) {
if (e.key === 'Escape') closeShadowPicker();
};
window.addEventListener('keydown', escHandler);
// Eyedropper for shadow
const eyedropperShadowBtn = shadowPicker.querySelector('#eyedropper-shadow');
addColorZillaLink(eyedropperShadowBtn);
if (eyedropperShadowBtn && 'eyedropper' in navigator) {
eyedropperShadowBtn.addEventListener('click', async function() {
try {
const result = await navigator.eyedropper.pick({withAlpha: true});
if (result && result.sRGBHex) {
const hexFull = result.sRGBHex;
const alphaHex = hexFull.slice(-2);
const hex = '#' + hexFull.slice(1,7).toUpperCase();
const r = parseInt(hexFull.slice(1,3), 16);
const g = parseInt(hexFull.slice(3,5), 16);
const b = parseInt(hexFull.slice(5,7), 16);
const alpha = parseInt(alphaHex, 16) / 255;
shadowPicker.querySelector('#r-slider').value = r;
shadowPicker.querySelector('#g-slider').value = g;
shadowPicker.querySelector('#b-slider').value = b;
shadowPicker.querySelector('#r-input').value = r;
shadowPicker.querySelector('#g-input').value = g;
shadowPicker.querySelector('#b-input').value = b;
shadowPicker.querySelector('#hex-input').value = hex;
shadowPicker.querySelector('#alpha-slider').value = alpha;
shadowPicker.querySelector('#alpha-value').textContent = alpha.toFixed(2);
updateShadowPreview(shadowPicker);
}
} catch (err) {
log('Eyedropper error:', err);
showDebugIndicator('Eyedropper cancelled or failed.', 'info');
}
});
} else if (eyedropperShadowBtn) {
eyedropperShadowBtn.disabled = true;
eyedropperShadowBtn.title = 'EyeDropper API not supported in this browser';
}
function updateShadowPreview(picker) {
const x = parseFloat(picker.querySelector('#x-offset').value) || 1;
const y = parseFloat(picker.querySelector('#y-offset').value) || 1;
const blur = parseFloat(picker.querySelector('#blur').value) || 0;
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;
const colorValue = (alpha < 1) ? 'rgba(' + r + ', ' + g + ', ' + b + ', ' + alpha + ')' : 'rgb(' + r + ', ' + g + ', ' + b + ')';
var shadowRule;
const absX = Math.abs(x);
const absY = Math.abs(y);
if (picker.querySelector('#outline-mode').checked) {
shadowRule = '-' + absX + 'px -' + absY + 'px ' + blur + 'px ' + colorValue + ', ' + absX + 'px -' + absY + 'px ' + blur + 'px ' + colorValue + ', -' + absX + 'px ' + absY + 'px ' + blur + 'px ' + colorValue + ', ' + absX + 'px ' + absY + 'px ' + blur + 'px ' + colorValue;
} else {
shadowRule = x + 'px ' + y + 'px ' + blur + 'px ' + colorValue;
}
picker.querySelector('#shadow-preview').style.textShadow = shadowRule;
picker.querySelector('#r-input').value = r;
picker.querySelector('#g-input').value = g;
picker.querySelector('#b-input').value = b;
const hex = '#' + Math.round(r).toString(16).padStart(2, '0') + Math.round(g).toString(16).padStart(2, '0') + Math.round(b).toString(16).padStart(2, '0');
picker.querySelector('#hex-input').value = hex.toUpperCase();
picker.querySelector('#alpha-value').textContent = alpha;
}
function updateSlider(channel, picker) {
const input = picker.querySelector('#' + channel + '-input').value;
picker.querySelector('#' + channel + '-slider').value = input;
updateShadowPreview(picker);
}
shadowPicker.addEventListener('input', function(e) {
if (e.target.matches('#x-offset, #y-offset, #blur')) {
updateShadowPreview(shadowPicker);
} else 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, shadowPicker);
} else {
updateShadowPreview(shadowPicker);
}
} else if (e.target.id === 'hex-input') {
const hexValue = e.target.value.replace('#', '').toUpperCase();
if (hexValue.match(/^([0-9A-F]{6})$/)) {
const rgb = hexToRgb(hexValue);
shadowPicker.querySelector('#r-slider').value = rgb.r;
shadowPicker.querySelector('#g-slider').value = rgb.g;
shadowPicker.querySelector('#b-slider').value = rgb.b;
shadowPicker.querySelector('#r-input').value = rgb.r;
shadowPicker.querySelector('#g-input').value = rgb.g;
shadowPicker.querySelector('#b-input').value = rgb.b;
updateShadowPreview(shadowPicker);
}
}
});
updateShadowPreview(shadowPicker);
} catch (err) {
log('Shadow picker open error', err);
showDebugIndicator('Shadow picker failed; check console.', 'error');
}
}
// Open pink outline picker - added eyedropper for text and outline
function openPinkOutlinePicker(ta) {
try {
closeAllPickers();
const overlay = document.createElement('div');
overlay.id = 'pink-outline-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);
pinkOutlinePicker = document.createElement('div');
pinkOutlinePicker.id = 'pink-outline-picker';
pinkOutlinePicker.innerHTML = '<h3>Pink Outline Template</h3>' +
'<div class="outline-section">' +
'<div class="outline-inputs">' +
'<label>Font Family: <select id="font-family-select"><option value="print">Print (Kalam)</option><option value="serif">Serif</option><option value="sans-serif">Sans Serif</option><option value="monospace">Monospace</option><option value="cursive">Cursive</option><option value="fantasy">Fantasy</option><option value="comic">Comic Sans MS</option><option value="narrow">Arial Narrow</option><option value="mono">Plex Mono</option><option value="slab sans">Impact</option><option value="slab serif">Rockwell</option><option value="formal serif">Formal Serif (Lora)</option><option value="formal cursive">Formal Cursive</option><option value="hand">Hand (Indie Flower)</option><option value="childlike">Childlike (Giselle)</option><option value="blackletter">Blackletter</option><option value="scary">Scary (Anarchy)</option></select></label>' +
'<label>Font Size (px): <input type="number" id="font-size" value="30" min="10" max="100" step="1" /></label>' +
'<label><input type="checkbox" id="font-bold" checked /> Bold</label>' +
'<label>Outline Offset (px): <input type="number" id="offset" value="2" min="0" max="10" step="0.1" /></label>' +
'<label>Blur Radius (px): <input type="number" id="blur" value="5" min="0" max="20" step="0.1" /></label>' +
'</div>' +
'</div>' +
'<div class="outline-section">' +
'<h4>Text Color</h4>' +
'<div class="palette" id="text-palette"></div>' +
'<div class="custom-section">' +
'<label>Custom RGB</label>' +
'<div class="rgb-sliders">' +
'<input type="range" id="text-r-slider" min="0" max="255" value="255" />' +
'<input type="range" id="text-g-slider" min="0" max="255" value="255" />' +
'<input type="range" id="text-b-slider" min="0" max="255" value="255" />' +
'</div>' +
'<div class="rgb-inputs">' +
'<span>R:</span><input type="number" id="text-r-input" min="0" max="255" value="255" />' +
'<span>G:</span><input type="number" id="text-g-input" min="0" max="255" value="255" />' +
'<span>B:</span><input type="number" id="text-b-input" min="0" max="255" value="255" />' +
'</div>' +
'<div class="hex-input">' +
'<label>Hex (#RRGGBB):</label>' +
'<input type="text" id="text-hex-input" value="#FFFFFF" maxlength="7" />' +
'</div>' +
'<div style="display: flex; align-items: center; justify-content: space-between;">' +
'<button id="eyedropper-text" class="eyedropper-btn" title="Pick color from screen" style="width: auto; flex: 1; margin-right: 5px;">👁 Pick from Screen</button>' +
'</div>' +
'<label>Opacity (Alpha 0-1)</label>' +
'<input type="range" id="text-alpha-slider" min="0" max="1" step="0.01" value="1" />' +
'<span id="text-alpha-value">1.00</span>' +
'</div>' +
'</div>' +
'<div class="outline-section">' +
'<h4>Outline Color</h4>' +
'<div class="palette" id="outline-palette"></div>' +
'<div class="custom-section">' +
'<label>Custom RGB</label>' +
'<div class="rgb-sliders">' +
'<input type="range" id="outline-r-slider" min="0" max="255" value="235" />' +
'<input type="range" id="outline-g-slider" min="0" max="255" value="115" />' +
'<input type="range" id="outline-b-slider" min="0" max="255" value="161" />' +
'</div>' +
'<div class="rgb-inputs">' +
'<span>R:</span><input type="number" id="outline-r-input" min="0" max="255" value="235" />' +
'<span>G:</span><input type="number" id="outline-g-input" min="0" max="255" value="115" />' +
'<span>B:</span><input type="number" id="outline-b-input" min="0" max="255" value="161" />' +
'</div>' +
'<div class="hex-input">' +
'<label>Hex (#RRGGBB):</label>' +
'<input type="text" id="outline-hex-input" value="#EB73A1" maxlength="7" />' +
'</div>' +
'<div style="display: flex; align-items: center; justify-content: space-between;">' +
'<button id="eyedropper-outline" class="eyedropper-btn" title="Pick color from screen" style="width: auto; flex: 1; margin-right: 5px;">👁 Pick from Screen</button>' +
'</div>' +
'<label>Opacity (Alpha 0-1)</label>' +
'<input type="range" id="outline-alpha-slider" min="0" max="1" step="0.01" value="0.8" />' +
'<span id="outline-alpha-value">0.80</span>' +
'</div>' +
'</div>' +
'<div id="outline-preview">Preview</div>' +
'<div class="picker-buttons">' +
'<button id="picker-ok">OK</button>' +
'<button id="picker-cancel">Cancel</button>' +
'</div>';
document.body.appendChild(pinkOutlinePicker);
// Text palette
const textPalette = pinkOutlinePicker.querySelector('#text-palette');
CONFIG.commonColors.forEach(function(color) {
const btn = document.createElement('button');
btn.style.backgroundColor = color;
btn.addEventListener('click', function() {
const rgb = hexToRgb(color);
pinkOutlinePicker.querySelector('#text-r-slider').value = rgb.r;
pinkOutlinePicker.querySelector('#text-g-slider').value = rgb.g;
pinkOutlinePicker.querySelector('#text-b-slider').value = rgb.b;
pinkOutlinePicker.querySelector('#text-r-input').value = rgb.r;
pinkOutlinePicker.querySelector('#text-g-input').value = rgb.g;
pinkOutlinePicker.querySelector('#text-b-input').value = rgb.b;
pinkOutlinePicker.querySelector('#text-hex-input').value = color;
pinkOutlinePicker.querySelector('#text-alpha-slider').value = 1;
updateOutlinePreview(pinkOutlinePicker);
});
textPalette.appendChild(btn);
});
// Outline palette
const outlinePalette = pinkOutlinePicker.querySelector('#outline-palette');
CONFIG.commonColors.forEach(function(color) {
const btn = document.createElement('button');
btn.style.backgroundColor = color;
btn.addEventListener('click', function() {
const rgb = hexToRgb(color);
pinkOutlinePicker.querySelector('#outline-r-slider').value = rgb.r;
pinkOutlinePicker.querySelector('#outline-g-slider').value = rgb.g;
pinkOutlinePicker.querySelector('#outline-b-slider').value = rgb.b;
pinkOutlinePicker.querySelector('#outline-r-input').value = rgb.r;
pinkOutlinePicker.querySelector('#outline-g-input').value = rgb.g;
pinkOutlinePicker.querySelector('#outline-b-input').value = rgb.b;
pinkOutlinePicker.querySelector('#outline-hex-input').value = color;
pinkOutlinePicker.querySelector('#outline-alpha-slider').value = 0.8;
updateOutlinePreview(pinkOutlinePicker);
});
outlinePalette.appendChild(btn);
});
pinkOutlinePicker.querySelector('#picker-ok').addEventListener('click', function(e) {
const fontFamily = pinkOutlinePicker.querySelector('#font-family-select').value;
const fontFamilyStr = fontFamily.includes(' ') ? fontFamily + ', serif' : fontFamily + ', sans-serif';
const fontSize = pinkOutlinePicker.querySelector('#font-size').value + 'px';
var styleStr = 'font-size: ' + fontSize + '; font-family: ' + fontFamilyStr + '; ';
const bold = pinkOutlinePicker.querySelector('#font-bold').checked;
if (bold) styleStr += 'font-weight: bold; ';
const offset = parseFloat(pinkOutlinePicker.querySelector('#offset').value) || 2;
const blur = parseFloat(pinkOutlinePicker.querySelector('#blur').value) || 5;
const textR = pinkOutlinePicker.querySelector('#text-r-slider').value;
const textG = pinkOutlinePicker.querySelector('#text-g-slider').value;
const textB = pinkOutlinePicker.querySelector('#text-b-slider').value;
const textAlpha = pinkOutlinePicker.querySelector('#text-alpha-slider').value;
const outlineR = pinkOutlinePicker.querySelector('#outline-r-slider').value;
const outlineG = pinkOutlinePicker.querySelector('#outline-g-slider').value;
const outlineB = pinkOutlinePicker.querySelector('#outline-b-slider').value;
const outlineAlpha = pinkOutlinePicker.querySelector('#outline-alpha-slider').value;
const textColor = (textAlpha < 1) ? 'rgba(' + textR + ', ' + textG + ', ' + textB + ', ' + textAlpha + ')' : 'rgb(' + textR + ', ' + textG + ', ' + textB + ')';
const outlineColor = (outlineAlpha < 1) ? 'rgba(' + outlineR + ', ' + outlineG + ', ' + outlineB + ', ' + outlineAlpha + ')' : 'rgb(' + outlineR + ', ' + outlineG + ', ' + outlineB + ')';
styleStr += 'color: ' + textColor + '; ';
const absOffset = Math.abs(offset);
var shadowRule = '-' + absOffset + 'px -' + absOffset + 'px ' + blur + 'px ' + outlineColor + ', ' +
'-' + absOffset + 'px 0px ' + blur + 'px ' + outlineColor + ', ' +
'-' + absOffset + 'px ' + absOffset + 'px ' + blur + 'px ' + outlineColor + ', ' +
'0px -' + absOffset + 'px ' + blur + 'px ' + outlineColor + ', ' +
'0px ' + absOffset + 'px ' + blur + 'px ' + outlineColor + ', ' +
absOffset + 'px -' + absOffset + 'px ' + blur + 'px ' + outlineColor + ', ' +
absOffset + 'px 0px ' + blur + 'px ' + outlineColor + ', ' +
absOffset + 'px ' + absOffset + 'px ' + blur + 'px ' + outlineColor;
styleStr += 'text-shadow: ' + shadowRule + ';';
var openTag = '<span style="' + styleStr + '">';
applyFormat(ta, openTag, '</span>');
closePinkOutlinePicker();
});
pinkOutlinePicker.querySelector('#picker-cancel').addEventListener('click', closePinkOutlinePicker);
overlay.addEventListener('click', closePinkOutlinePicker);
const escHandler = function(e) {
if (e.key === 'Escape') closePinkOutlinePicker();
};
window.addEventListener('keydown', escHandler);
// Eyedropper for text
const eyedropperTextBtn = pinkOutlinePicker.querySelector('#eyedropper-text');
addColorZillaLink(eyedropperTextBtn);
if (eyedropperTextBtn && 'eyedropper' in navigator) {
eyedropperTextBtn.addEventListener('click', async function() {
try {
const result = await navigator.eyedropper.pick({withAlpha: true});
if (result && result.sRGBHex) {
const hexFull = result.sRGBHex;
const alphaHex = hexFull.slice(-2);
const hex = '#' + hexFull.slice(1,7).toUpperCase();
const r = parseInt(hexFull.slice(1,3), 16);
const g = parseInt(hexFull.slice(3,5), 16);
const b = parseInt(hexFull.slice(5,7), 16);
const alpha = parseInt(alphaHex, 16) / 255;
pinkOutlinePicker.querySelector('#text-r-slider').value = r;
pinkOutlinePicker.querySelector('#text-g-slider').value = g;
pinkOutlinePicker.querySelector('#text-b-slider').value = b;
pinkOutlinePicker.querySelector('#text-r-input').value = r;
pinkOutlinePicker.querySelector('#text-g-input').value = g;
pinkOutlinePicker.querySelector('#text-b-input').value = b;
pinkOutlinePicker.querySelector('#text-hex-input').value = hex;
pinkOutlinePicker.querySelector('#text-alpha-slider').value = alpha;
pinkOutlinePicker.querySelector('#text-alpha-value').textContent = alpha.toFixed(2);
updateOutlinePreview(pinkOutlinePicker);
}
} catch (err) {
log('Eyedropper error:', err);
showDebugIndicator('Eyedropper cancelled or failed.', 'info');
}
});
} else if (eyedropperTextBtn) {
eyedropperTextBtn.disabled = true;
eyedropperTextBtn.title = 'EyeDropper API not supported in this browser';
}
// Eyedropper for outline
const eyedropperOutlineBtn = pinkOutlinePicker.querySelector('#eyedropper-outline');
addColorZillaLink(eyedropperOutlineBtn);
if (eyedropperOutlineBtn && 'eyedropper' in navigator) {
eyedropperOutlineBtn.addEventListener('click', async function() {
try {
const result = await navigator.eyedropper.pick({withAlpha: true});
if (result && result.sRGBHex) {
const hexFull = result.sRGBHex;
const alphaHex = hexFull.slice(-2);
const hex = '#' + hexFull.slice(1,7).toUpperCase();
const r = parseInt(hexFull.slice(1,3), 16);
const g = parseInt(hexFull.slice(3,5), 16);
const b = parseInt(hexFull.slice(5,7), 16);
const alpha = parseInt(alphaHex, 16) / 255;
pinkOutlinePicker.querySelector('#outline-r-slider').value = r;
pinkOutlinePicker.querySelector('#outline-g-slider').value = g;
pinkOutlinePicker.querySelector('#outline-b-slider').value = b;
pinkOutlinePicker.querySelector('#outline-r-input').value = r;
pinkOutlinePicker.querySelector('#outline-g-input').value = g;
pinkOutlinePicker.querySelector('#outline-b-input').value = b;
pinkOutlinePicker.querySelector('#outline-hex-input').value = hex;
pinkOutlinePicker.querySelector('#outline-alpha-slider').value = alpha;
pinkOutlinePicker.querySelector('#outline-alpha-value').textContent = alpha.toFixed(2);
updateOutlinePreview(pinkOutlinePicker);
}
} catch (err) {
log('Eyedropper error:', err);
showDebugIndicator('Eyedropper cancelled or failed.', 'info');
}
});
} else if (eyedropperOutlineBtn) {
eyedropperOutlineBtn.disabled = true;
eyedropperOutlineBtn.title = 'EyeDropper API not supported in this browser';
}
function updateOutlinePreview(picker) {
const fontFamily = picker.querySelector('#font-family-select').value;
const fontFamilyStr = fontFamily.includes(' ') ? fontFamily + ', serif' : fontFamily + ', sans-serif';
const fontSize = picker.querySelector('#font-size').value + 'px';
const bold = picker.querySelector('#font-bold').checked ? 'bold' : 'normal';
const offset = parseFloat(picker.querySelector('#offset').value) || 2;
const blur = parseFloat(picker.querySelector('#blur').value) || 5;
const textR = picker.querySelector('#text-r-slider').value;
const textG = picker.querySelector('#text-g-slider').value;
const textB = picker.querySelector('#text-b-slider').value;
const textAlpha = picker.querySelector('#text-alpha-slider').value;
const outlineR = picker.querySelector('#outline-r-slider').value;
const outlineG = picker.querySelector('#outline-g-slider').value;
const outlineB = picker.querySelector('#outline-b-slider').value;
const outlineAlpha = picker.querySelector('#outline-alpha-slider').value;
const textColor = (textAlpha < 1) ? 'rgba(' + textR + ', ' + textG + ', ' + textB + ', ' + textAlpha + ')' : 'rgb(' + textR + ', ' + textG + ', ' + textB + ')';
const outlineColor = (outlineAlpha < 1) ? 'rgba(' + outlineR + ', ' + outlineG + ', ' + outlineB + ', ' + outlineAlpha + ')' : 'rgb(' + outlineR + ', ' + outlineG + ', ' + outlineB + ')';
const absOffset = Math.abs(offset);
var shadowRule = '-' + absOffset + 'px -' + absOffset + 'px ' + blur + 'px ' + outlineColor + ', ' +
'-' + absOffset + 'px 0px ' + blur + 'px ' + outlineColor + ', ' +
'-' + absOffset + 'px ' + absOffset + 'px ' + blur + 'px ' + outlineColor + ', ' +
'0px -' + absOffset + 'px ' + blur + 'px ' + outlineColor + ', ' +
'0px ' + absOffset + 'px ' + blur + 'px ' + outlineColor + ', ' +
absOffset + 'px -' + absOffset + 'px ' + blur + 'px ' + outlineColor + ', ' +
absOffset + 'px 0px ' + blur + 'px ' + outlineColor + ', ' +
absOffset + 'px ' + absOffset + 'px ' + blur + 'px ' + outlineColor;
const preview = picker.querySelector('#outline-preview');
preview.style.fontFamily = fontFamilyStr;
preview.style.fontSize = fontSize;
preview.style.fontWeight = bold;
preview.style.color = textColor;
preview.style.textShadow = shadowRule;
picker.querySelector('#text-alpha-value').textContent = textAlpha;
picker.querySelector('#outline-alpha-value').textContent = outlineAlpha;
const textHex = '#' + Math.round(textR).toString(16).padStart(2, '0') + Math.round(textG).toString(16).padStart(2, '0') + Math.round(textB).toString(16).padStart(2, '0');
picker.querySelector('#text-hex-input').value = textHex.toUpperCase();
const outlineHex = '#' + Math.round(outlineR).toString(16).padStart(2, '0') + Math.round(outlineG).toString(16).padStart(2, '0') + Math.round(outlineB).toString(16).padStart(2, '0');
picker.querySelector('#outline-hex-input').value = outlineHex.toUpperCase();
}
function updateTextSlider(channel, picker) {
const input = picker.querySelector('#text-' + channel + '-input').value;
picker.querySelector('#text-' + channel + '-slider').value = input;
updateOutlinePreview(picker);
}
function updateOutlineSlider(channel, picker) {
const input = picker.querySelector('#outline-' + channel + '-input').value;
picker.querySelector('#outline-' + channel + '-slider').value = input;
updateOutlinePreview(picker);
}
pinkOutlinePicker.addEventListener('input', function(e) {
if (e.target.id === 'font-family-select' || e.target.id === 'font-size' || e.target.id === 'font-bold' || e.target.id === 'offset' || e.target.id === 'blur' || e.target.id === 'text-alpha-slider' || e.target.id === 'outline-alpha-slider') {
updateOutlinePreview(pinkOutlinePicker);
} else if (e.target.matches('#text-r-slider, #text-g-slider, #text-b-slider')) {
updateOutlinePreview(pinkOutlinePicker);
} else if (e.target.matches('#outline-r-slider, #outline-g-slider, #outline-b-slider')) {
updateOutlinePreview(pinkOutlinePicker);
} else if (e.target.id.includes('text-') && e.target.id.includes('input') && e.target.type === 'number') {
const channel = e.target.id.replace('text-', '').replace('-input', '');
updateTextSlider(channel, pinkOutlinePicker);
} else if (e.target.id.includes('outline-') && e.target.id.includes('input') && e.target.type === 'number') {
const channel = e.target.id.replace('outline-', '').replace('-input', '');
updateOutlineSlider(channel, pinkOutlinePicker);
} else if (e.target.id === 'text-hex-input') {
const hexValue = e.target.value.replace('#', '').toUpperCase();
if (hexValue.match(/^([0-9A-F]{6})$/)) {
const rgb = hexToRgb(hexValue);
pinkOutlinePicker.querySelector('#text-r-slider').value = rgb.r;
pinkOutlinePicker.querySelector('#text-g-slider').value = rgb.g;
pinkOutlinePicker.querySelector('#text-b-slider').value = rgb.b;
pinkOutlinePicker.querySelector('#text-r-input').value = rgb.r;
pinkOutlinePicker.querySelector('#text-g-input').value = rgb.g;
pinkOutlinePicker.querySelector('#text-b-input').value = rgb.b;
updateOutlinePreview(pinkOutlinePicker);
}
} else if (e.target.id === 'outline-hex-input') {
const hexValue = e.target.value.replace('#', '').toUpperCase();
if (hexValue.match(/^([0-9A-F]{6})$/)) {
const rgb = hexToRgb(hexValue);
pinkOutlinePicker.querySelector('#outline-r-slider').value = rgb.r;
pinkOutlinePicker.querySelector('#outline-g-slider').value = rgb.g;
pinkOutlinePicker.querySelector('#outline-b-slider').value = rgb.b;
pinkOutlinePicker.querySelector('#outline-r-input').value = rgb.r;
pinkOutlinePicker.querySelector('#outline-g-input').value = rgb.g;
pinkOutlinePicker.querySelector('#outline-b-input').value = rgb.b;
updateOutlinePreview(pinkOutlinePicker);
}
}
});
updateOutlinePreview(pinkOutlinePicker);
} catch (err) {
log('Pink outline picker open error', err);
showDebugIndicator('Pink outline picker failed; check console.', 'error');
}
}
// Open double outline picker - added eyedropper for text, inner, outer
function openDoubleOutlinePicker(ta) {
try {
closeAllPickers();
const overlay = document.createElement('div');
overlay.id = 'double-outline-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);
doubleOutlinePicker = document.createElement('div');
doubleOutlinePicker.id = 'double-outline-picker';
doubleOutlinePicker.innerHTML = '<h3>Double Outline Template</h3>' +
'<div class="outline-section">' +
'<div class="outline-inputs">' +
'<label>Font Family: <select id="font-family-select"><option value="print">Print (Kalam)</option><option value="serif">Serif</option><option value="sans-serif">Sans Serif</option><option value="monospace">Monospace</option><option value="cursive">Cursive</option><option value="fantasy">Fantasy</option><option value="comic">Comic Sans MS</option><option value="narrow">Arial Narrow</option><option value="mono">Plex Mono</option><option value="slab sans">Impact</option><option value="slab serif">Rockwell</option><option value="formal serif">Formal Serif (Lora)</option><option value="formal cursive">Formal Cursive</option><option value="hand">Hand (Indie Flower)</option><option value="childlike">Childlike (Giselle)</option><option value="blackletter">Blackletter</option><option value="scary">Scary (Anarchy)</option></select></label>' +
'<label>Font Size (px): <input type="number" id="font-size" value="20" min="10" max="100" step="1" /></label>' +
'<label><input type="checkbox" id="font-bold" checked /> Bold</label>' +
'</div>' +
'</div>' +
'<div class="outline-section">' +
'<h4>Text Color (Pink)</h4>' +
'<div class="palette" id="text-palette"></div>' +
'<div class="custom-section">' +
'<label>Custom RGB</label>' +
'<div class="rgb-sliders">' +
'<input type="range" id="text-r-slider" min="0" max="255" value="216" />' +
'<input type="range" id="text-g-slider" min="0" max="255" value="124" />' +
'<input type="range" id="text-b-slider" min="0" max="255" value="134" />' +
'</div>' +
'<div class="rgb-inputs">' +
'<span>R:</span><input type="number" id="text-r-input" min="0" max="255" value="216" />' +
'<span>G:</span><input type="number" id="text-g-input" min="0" max="255" value="124" />' +
'<span>B:</span><input type="number" id="text-b-input" min="0" max="255" value="134" />' +
'</div>' +
'<div class="hex-input">' +
'<label>Hex (#RRGGBB):</label>' +
'<input type="text" id="text-hex-input" value="#D87C86" maxlength="7" />' +
'</div>' +
'<div style="display: flex; align-items: center; justify-content: space-between;">' +
'<button id="eyedropper-text" class="eyedropper-btn" title="Pick color from screen" style="width: auto; flex: 1; margin-right: 5px;">👁 Pick from Screen</button>' +
'</div>' +
'<label>Opacity (Alpha 0-1)</label>' +
'<input type="range" id="text-alpha-slider" min="0" max="1" step="0.01" value="1" />' +
'<span id="text-alpha-value">1.00</span>' +
'</div>' +
'</div>' +
'<div class="outline-section">' +
'<h4>Inner Outline (White, Thick)</h4>' +
'<div class="palette" id="inner-palette"></div>' +
'<div class="custom-section">' +
'<label>Offset (px): <input type="number" id="inner-offset" value="2" min="0" max="10" step="0.1" /></label>' +
'<label>Blur (px): <input type="number" id="inner-blur" value="0" min="0" max="20" step="0.1" /></label>' +
'<label>Custom RGB</label>' +
'<div class="rgb-sliders">' +
'<input type="range" id="inner-r-slider" min="0" max="255" value="255" />' +
'<input type="range" id="inner-g-slider" min="0" max="255" value="255" />' +
'<input type="range" id="inner-b-slider" min="0" max="255" value="255" />' +
'</div>' +
'<div class="rgb-inputs">' +
'<span>R:</span><input type="number" id="inner-r-input" min="0" max="255" value="255" />' +
'<span>G:</span><input type="number" id="inner-g-input" min="0" max="255" value="255" />' +
'<span>B:</span><input type="number" id="inner-b-input" min="0" max="255" value="255" />' +
'</div>' +
'<div class="hex-input">' +
'<label>Hex (#RRGGBB):</label>' +
'<input type="text" id="inner-hex-input" value="#FFFFFF" maxlength="7" />' +
'</div>' +
'<div style="display: flex; align-items: center; justify-content: space-between;">' +
'<button id="eyedropper-inner" class="eyedropper-btn" title="Pick color from screen" style="width: auto; flex: 1; margin-right: 5px;">👁 Pick from Screen</button>' +
'</div>' +
'<label>Opacity (Alpha 0-1)</label>' +
'<input type="range" id="inner-alpha-slider" min="0" max="1" step="0.01" value="1" />' +
'<span id="inner-alpha-value">1.00</span>' +
'</div>' +
'</div>' +
'<div class="outline-section">' +
'<h4>Outer Outline (Black, Thin)</h4>' +
'<div class="palette" id="outer-palette"></div>' +
'<div class="custom-section">' +
'<label>Offset (px): <input type="number" id="outer-offset" value="3" min="0" max="10" step="0.1" /></label>' +
'<label>Blur (px): <input type="number" id="outer-blur" value="0" min="0" max="20" step="0.1" /></label>' +
'<label>Custom RGB</label>' +
'<div class="rgb-sliders">' +
'<input type="range" id="outer-r-slider" min="0" max="255" value="0" />' +
'<input type="range" id="outer-g-slider" min="0" max="255" value="0" />' +
'<input type="range" id="outer-b-slider" min="0" max="255" value="0" />' +
'</div>' +
'<div class="rgb-inputs">' +
'<span>R:</span><input type="number" id="outer-r-input" min="0" max="255" value="0" />' +
'<span>G:</span><input type="number" id="outer-g-input" min="0" max="255" value="0" />' +
'<span>B:</span><input type="number" id="outer-b-input" min="0" max="255" value="0" />' +
'</div>' +
'<div class="hex-input">' +
'<label>Hex (#RRGGBB):</label>' +
'<input type="text" id="outer-hex-input" value="#000000" maxlength="7" />' +
'</div>' +
'<div style="display: flex; align-items: center; justify-content: space-between;">' +
'<button id="eyedropper-outer" class="eyedropper-btn" title="Pick color from screen" style="width: auto; flex: 1; margin-right: 5px;">👁 Pick from Screen</button>' +
'</div>' +
'<label>Opacity (Alpha 0-1)</label>' +
'<input type="range" id="outer-alpha-slider" min="0" max="1" step="0.01" value="1" />' +
'<span id="outer-alpha-value">1.00</span>' +
'</div>' +
'</div>' +
'<div id="double-preview">Preview</div>' +
'<div class="picker-buttons">' +
'<button id="picker-ok">OK</button>' +
'<button id="picker-cancel">Cancel</button>' +
'</div>';
document.body.appendChild(doubleOutlinePicker);
// Text palette
const textPalette = doubleOutlinePicker.querySelector('#text-palette');
CONFIG.commonColors.forEach(function(color) {
const btn = document.createElement('button');
btn.style.backgroundColor = color;
btn.addEventListener('click', function() {
const rgb = hexToRgb(color);
doubleOutlinePicker.querySelector('#text-r-slider').value = rgb.r;
doubleOutlinePicker.querySelector('#text-g-slider').value = rgb.g;
doubleOutlinePicker.querySelector('#text-b-slider').value = rgb.b;
doubleOutlinePicker.querySelector('#text-r-input').value = rgb.r;
doubleOutlinePicker.querySelector('#text-g-input').value = rgb.g;
doubleOutlinePicker.querySelector('#text-b-input').value = rgb.b;
doubleOutlinePicker.querySelector('#text-hex-input').value = color;
doubleOutlinePicker.querySelector('#text-alpha-slider').value = 1;
updateDoublePreview(doubleOutlinePicker);
});
textPalette.appendChild(btn);
});
// Inner palette
const innerPalette = doubleOutlinePicker.querySelector('#inner-palette');
CONFIG.commonColors.forEach(function(color) {
const btn = document.createElement('button');
btn.style.backgroundColor = color;
btn.addEventListener('click', function() {
const rgb = hexToRgb(color);
doubleOutlinePicker.querySelector('#inner-r-slider').value = rgb.r;
doubleOutlinePicker.querySelector('#inner-g-slider').value = rgb.g;
doubleOutlinePicker.querySelector('#inner-b-slider').value = rgb.b;
doubleOutlinePicker.querySelector('#inner-r-input').value = rgb.r;
doubleOutlinePicker.querySelector('#inner-g-input').value = rgb.g;
doubleOutlinePicker.querySelector('#inner-b-input').value = rgb.b;
doubleOutlinePicker.querySelector('#inner-hex-input').value = color;
doubleOutlinePicker.querySelector('#inner-alpha-slider').value = 1;
updateDoublePreview(doubleOutlinePicker);
});
innerPalette.appendChild(btn);
});
// Outer palette
const outerPalette = doubleOutlinePicker.querySelector('#outer-palette');
CONFIG.commonColors.forEach(function(color) {
const btn = document.createElement('button');
btn.style.backgroundColor = color;
btn.addEventListener('click', function() {
const rgb = hexToRgb(color);
doubleOutlinePicker.querySelector('#outer-r-slider').value = rgb.r;
doubleOutlinePicker.querySelector('#outer-g-slider').value = rgb.g;
doubleOutlinePicker.querySelector('#outer-b-slider').value = rgb.b;
doubleOutlinePicker.querySelector('#outer-r-input').value = rgb.r;
doubleOutlinePicker.querySelector('#outer-g-input').value = rgb.g;
doubleOutlinePicker.querySelector('#outer-b-input').value = rgb.b;
doubleOutlinePicker.querySelector('#outer-hex-input').value = color;
doubleOutlinePicker.querySelector('#outer-alpha-slider').value = 1;
updateDoublePreview(doubleOutlinePicker);
});
outerPalette.appendChild(btn);
});
doubleOutlinePicker.querySelector('#picker-ok').addEventListener('click', function(e) {
const fontFamily = doubleOutlinePicker.querySelector('#font-family-select').value;
const fontFamilyStr = fontFamily.includes(' ') ? fontFamily + ', serif' : fontFamily + ', sans-serif';
const fontSize = doubleOutlinePicker.querySelector('#font-size').value + 'px';
var styleStr = 'font-size: ' + fontSize + '; font-family: ' + fontFamilyStr + '; ';
const bold = doubleOutlinePicker.querySelector('#font-bold').checked;
if (bold) styleStr += 'font-weight: bold; ';
const textR = doubleOutlinePicker.querySelector('#text-r-slider').value;
const textG = doubleOutlinePicker.querySelector('#text-g-slider').value;
const textB = doubleOutlinePicker.querySelector('#text-b-slider').value;
const textAlpha = doubleOutlinePicker.querySelector('#text-alpha-slider').value;
const textColor = (textAlpha < 1) ? 'rgba(' + textR + ', ' + textG + ', ' + textB + ', ' + textAlpha + ')' : 'rgb(' + textR + ', ' + textG + ', ' + textB + ')';
styleStr += 'color: ' + textColor + '; ';
const innerOffset = parseFloat(doubleOutlinePicker.querySelector('#inner-offset').value) || 2;
const innerBlur = parseFloat(doubleOutlinePicker.querySelector('#inner-blur').value) || 0;
const innerR = doubleOutlinePicker.querySelector('#inner-r-slider').value;
const innerG = doubleOutlinePicker.querySelector('#inner-g-slider').value;
const innerB = doubleOutlinePicker.querySelector('#inner-b-slider').value;
const innerAlpha = doubleOutlinePicker.querySelector('#inner-alpha-slider').value;
const innerColor = (innerAlpha < 1) ? 'rgba(' + innerR + ', ' + innerG + ', ' + innerB + ', ' + innerAlpha + ')' : 'rgb(' + innerR + ', ' + innerG + ', ' + innerB + ')';
const outerOffset = parseFloat(doubleOutlinePicker.querySelector('#outer-offset').value) || 3;
const outerBlur = parseFloat(doubleOutlinePicker.querySelector('#outer-blur').value) || 0;
const outerR = doubleOutlinePicker.querySelector('#outer-r-slider').value;
const outerG = doubleOutlinePicker.querySelector('#outer-g-slider').value;
const outerB = doubleOutlinePicker.querySelector('#outer-b-slider').value;
const outerAlpha = doubleOutlinePicker.querySelector('#outer-alpha-slider').value;
const outerColor = (outerAlpha < 1) ? 'rgba(' + outerR + ', ' + outerG + ', ' + outerB + ', ' + outerAlpha + ')' : 'rgb(' + outerR + ', ' + outerG + ', ' + outerB + ')';
// Inner shadows (8 dir)
const absInner = Math.abs(innerOffset);
var innerRule = '-' + absInner + 'px -' + absInner + 'px ' + innerBlur + 'px ' + innerColor + ', ' +
'-' + absInner + 'px 0px ' + innerBlur + 'px ' + innerColor + ', ' +
'-' + absInner + 'px ' + absInner + 'px ' + innerBlur + 'px ' + innerColor + ', ' +
'0px -' + absInner + 'px ' + innerBlur + 'px ' + innerColor + ', ' +
'0px ' + absInner + 'px ' + innerBlur + 'px ' + innerColor + ', ' +
absInner + 'px -' + absInner + 'px ' + innerBlur + 'px ' + innerColor + ', ' +
absInner + 'px 0px ' + innerBlur + 'px ' + innerColor + ', ' +
absInner + 'px ' + absInner + 'px ' + innerBlur + 'px ' + innerColor;
// Outer shadows (8 dir)
const absOuter = Math.abs(outerOffset);
var outerRule = '-' + absOuter + 'px -' + absOuter + 'px ' + outerBlur + 'px ' + outerColor + ', ' +
'-' + absOuter + 'px 0px ' + outerBlur + 'px ' + outerColor + ', ' +
'-' + absOuter + 'px ' + absOuter + 'px ' + outerBlur + 'px ' + outerColor + ', ' +
'0px -' + absOuter + 'px ' + outerBlur + 'px ' + outerColor + ', ' +
'0px ' + absOuter + 'px ' + outerBlur + 'px ' + outerColor + ', ' +
absOuter + 'px -' + absOuter + 'px ' + outerBlur + 'px ' + outerColor + ', ' +
absOuter + 'px 0px ' + outerBlur + 'px ' + outerColor + ', ' +
absOuter + 'px ' + absOuter + 'px ' + outerBlur + 'px ' + outerColor;
styleStr += 'text-shadow: ' + innerRule + ', ' + outerRule + ';';
var openTag = '<span style="' + styleStr + '">';
applyFormat(ta, openTag, '</span>');
closeDoubleOutlinePicker();
});
doubleOutlinePicker.querySelector('#picker-cancel').addEventListener('click', closeDoubleOutlinePicker);
overlay.addEventListener('click', closeDoubleOutlinePicker);
const escHandler = function(e) {
if (e.key === 'Escape') closeDoubleOutlinePicker();
};
window.addEventListener('keydown', escHandler);
// Eyedropper for text
const eyedropperTextBtn = doubleOutlinePicker.querySelector('#eyedropper-text');
addColorZillaLink(eyedropperTextBtn);
if (eyedropperTextBtn && 'eyedropper' in navigator) {
eyedropperTextBtn.addEventListener('click', async function() {
try {
const result = await navigator.eyedropper.pick({withAlpha: true});
if (result && result.sRGBHex) {
const hexFull = result.sRGBHex;
const alphaHex = hexFull.slice(-2);
const hex = '#' + hexFull.slice(1,7).toUpperCase();
const r = parseInt(hexFull.slice(1,3), 16);
const g = parseInt(hexFull.slice(3,5), 16);
const b = parseInt(hexFull.slice(5,7), 16);
const alpha = parseInt(alphaHex, 16) / 255;
doubleOutlinePicker.querySelector('#text-r-slider').value = r;
doubleOutlinePicker.querySelector('#text-g-slider').value = g;
doubleOutlinePicker.querySelector('#text-b-slider').value = b;
doubleOutlinePicker.querySelector('#text-r-input').value = r;
doubleOutlinePicker.querySelector('#text-g-input').value = g;
doubleOutlinePicker.querySelector('#text-b-input').value = b;
doubleOutlinePicker.querySelector('#text-hex-input').value = hex;
doubleOutlinePicker.querySelector('#text-alpha-slider').value = alpha;
doubleOutlinePicker.querySelector('#text-alpha-value').textContent = alpha.toFixed(2);
updateDoublePreview(doubleOutlinePicker);
}
} catch (err) {
log('Eyedropper error:', err);
showDebugIndicator('Eyedropper cancelled or failed.', 'info');
}
});
} else if (eyedropperTextBtn) {
eyedropperTextBtn.disabled = true;
eyedropperTextBtn.title = 'EyeDropper API not supported in this browser';
}
// Eyedropper for inner
const eyedropperInnerBtn = doubleOutlinePicker.querySelector('#eyedropper-inner');
addColorZillaLink(eyedropperInnerBtn);
if (eyedropperInnerBtn && 'eyedropper' in navigator) {
eyedropperInnerBtn.addEventListener('click', async function() {
try {
const result = await navigator.eyedropper.pick({withAlpha: true});
if (result && result.sRGBHex) {
const hexFull = result.sRGBHex;
const alphaHex = hexFull.slice(-2);
const hex = '#' + hexFull.slice(1,7).toUpperCase();
const r = parseInt(hexFull.slice(1,3), 16);
const g = parseInt(hexFull.slice(3,5), 16);
const b = parseInt(hexFull.slice(5,7), 16);
const alpha = parseInt(alphaHex, 16) / 255;
doubleOutlinePicker.querySelector('#inner-r-slider').value = r;
doubleOutlinePicker.querySelector('#inner-g-slider').value = g;
doubleOutlinePicker.querySelector('#inner-b-slider').value = b;
doubleOutlinePicker.querySelector('#inner-r-input').value = r;
doubleOutlinePicker.querySelector('#inner-g-input').value = g;
doubleOutlinePicker.querySelector('#inner-b-input').value = b;
doubleOutlinePicker.querySelector('#inner-hex-input').value = hex;
doubleOutlinePicker.querySelector('#inner-alpha-slider').value = alpha;
doubleOutlinePicker.querySelector('#inner-alpha-value').textContent = alpha.toFixed(2);
updateDoublePreview(doubleOutlinePicker);
}
} catch (err) {
log('Eyedropper error:', err);
showDebugIndicator('Eyedropper cancelled or failed.', 'info');
}
});
} else if (eyedropperInnerBtn) {
eyedropperInnerBtn.disabled = true;
eyedropperInnerBtn.title = 'EyeDropper API not supported in this browser';
}
// Eyedropper for outer
const eyedropperOuterBtn = doubleOutlinePicker.querySelector('#eyedropper-outer');
addColorZillaLink(eyedropperOuterBtn);
if (eyedropperOuterBtn && 'eyedropper' in navigator) {
eyedropperOuterBtn.addEventListener('click', async function() {
try {
const result = await navigator.eyedropper.pick({withAlpha: true});
if (result && result.sRGBHex) {
const hexFull = result.sRGBHex;
const alphaHex = hexFull.slice(-2);
const hex = '#' + hexFull.slice(1,7).toUpperCase();
const r = parseInt(hexFull.slice(1,3), 16);
const g = parseInt(hexFull.slice(3,5), 16);
const b = parseInt(hexFull.slice(5,7), 16);
const alpha = parseInt(alphaHex, 16) / 255;
doubleOutlinePicker.querySelector('#outer-r-slider').value = r;
doubleOutlinePicker.querySelector('#outer-g-slider').value = g;
doubleOutlinePicker.querySelector('#outer-b-slider').value = b;
doubleOutlinePicker.querySelector('#outer-r-input').value = r;
doubleOutlinePicker.querySelector('#outer-g-input').value = g;
doubleOutlinePicker.querySelector('#outer-b-input').value = b;
doubleOutlinePicker.querySelector('#outer-hex-input').value = hex;
doubleOutlinePicker.querySelector('#outer-alpha-slider').value = alpha;
doubleOutlinePicker.querySelector('#outer-alpha-value').textContent = alpha.toFixed(2);
updateDoublePreview(doubleOutlinePicker);
}
} catch (err) {
log('Eyedropper error:', err);
showDebugIndicator('Eyedropper cancelled or failed.', 'info');
}
});
} else if (eyedropperOuterBtn) {
eyedropperOuterBtn.disabled = true;
eyedropperOuterBtn.title = 'EyeDropper API not supported in this browser';
}
function updateDoublePreview(picker) {
const fontFamily = picker.querySelector('#font-family-select').value;
const fontFamilyStr = fontFamily.includes(' ') ? fontFamily + ', serif' : fontFamily + ', sans-serif';
const fontSize = picker.querySelector('#font-size').value + 'px';
const bold = picker.querySelector('#font-bold').checked ? 'bold' : 'normal';
const textR = picker.querySelector('#text-r-slider').value;
const textG = picker.querySelector('#text-g-slider').value;
const textB = picker.querySelector('#text-b-slider').value;
const textAlpha = picker.querySelector('#text-alpha-slider').value;
const textColor = (textAlpha < 1) ? 'rgba(' + textR + ', ' + textG + ', ' + textB + ', ' + textAlpha + ')' : 'rgb(' + textR + ', ' + textG + ', ' + textB + ')';
const innerOffset = parseFloat(picker.querySelector('#inner-offset').value) || 2;
const innerBlur = parseFloat(picker.querySelector('#inner-blur').value) || 0;
const innerR = picker.querySelector('#inner-r-slider').value;
const innerG = picker.querySelector('#inner-g-slider').value;
const innerB = picker.querySelector('#inner-b-slider').value;
const innerAlpha = picker.querySelector('#inner-alpha-slider').value;
const innerColor = (innerAlpha < 1) ? 'rgba(' + innerR + ', ' + innerG + ', ' + innerB + ', ' + innerAlpha + ')' : 'rgb(' + innerR + ', ' + innerG + ', ' + innerB + ')';
const outerOffset = parseFloat(picker.querySelector('#outer-offset').value) || 3;
const outerBlur = parseFloat(picker.querySelector('#outer-blur').value) || 0;
const outerR = picker.querySelector('#outer-r-slider').value;
const outerG = picker.querySelector('#outer-g-slider').value;
const outerB = picker.querySelector('#outer-b-slider').value;
const outerAlpha = picker.querySelector('#outer-alpha-slider').value;
const outerColor = (outerAlpha < 1) ? 'rgba(' + outerR + ', ' + outerG + ', ' + outerB + ', ' + outerAlpha + ')' : 'rgb(' + outerR + ', ' + outerG + ', ' + outerB + ')';
const absInner = Math.abs(innerOffset);
var innerRule = '-' + absInner + 'px -' + absInner + 'px ' + innerBlur + 'px ' + innerColor + ', ' +
'-' + absInner + 'px 0px ' + innerBlur + 'px ' + innerColor + ', ' +
'-' + absInner + 'px ' + absInner + 'px ' + innerBlur + 'px ' + innerColor + ', ' +
'0px -' + absInner + 'px ' + innerBlur + 'px ' + innerColor + ', ' +
'0px ' + absInner + 'px ' + innerBlur + 'px ' + innerColor + ', ' +
absInner + 'px -' + absInner + 'px ' + innerBlur + 'px ' + innerColor + ', ' +
absInner + 'px 0px ' + innerBlur + 'px ' + innerColor + ', ' +
absInner + 'px ' + absInner + 'px ' + innerBlur + 'px ' + innerColor;
const absOuter = Math.abs(outerOffset);
var outerRule = '-' + absOuter + 'px -' + absOuter + 'px ' + outerBlur + 'px ' + outerColor + ', ' +
'-' + absOuter + 'px 0px ' + outerBlur + 'px ' + outerColor + ', ' +
'-' + absOuter + 'px ' + absOuter + 'px ' + outerBlur + 'px ' + outerColor + ', ' +
'0px -' + absOuter + 'px ' + outerBlur + 'px ' + outerColor + ', ' +
'0px ' + absOuter + 'px ' + outerBlur + 'px ' + outerColor + ', ' +
absOuter + 'px -' + absOuter + 'px ' + outerBlur + 'px ' + outerColor + ', ' +
absOuter + 'px 0px ' + outerBlur + 'px ' + outerColor + ', ' +
absOuter + 'px ' + absOuter + 'px ' + outerBlur + 'px ' + outerColor;
const preview = picker.querySelector('#double-preview');
preview.style.fontFamily = fontFamilyStr;
preview.style.fontSize = fontSize;
preview.style.fontWeight = bold;
preview.style.color = textColor;
preview.style.textShadow = innerRule + ', ' + outerRule;
picker.querySelector('#text-alpha-value').textContent = textAlpha;
picker.querySelector('#inner-alpha-value').textContent = innerAlpha;
picker.querySelector('#outer-alpha-value').textContent = outerAlpha;
const textHex = '#' + Math.round(textR).toString(16).padStart(2, '0') + Math.round(textG).toString(16).padStart(2, '0') + Math.round(textB).toString(16).padStart(2, '0');
picker.querySelector('#text-hex-input').value = textHex.toUpperCase();
const innerHex = '#' + Math.round(innerR).toString(16).padStart(2, '0') + Math.round(innerG).toString(16).padStart(2, '0') + Math.round(innerB).toString(16).padStart(2, '0');
picker.querySelector('#inner-hex-input').value = innerHex.toUpperCase();
const outerHex = '#' + Math.round(outerR).toString(16).padStart(2, '0') + Math.round(outerG).toString(16).padStart(2, '0') + Math.round(outerB).toString(16).padStart(2, '0');
picker.querySelector('#outer-hex-input').value = outerHex.toUpperCase();
}
// Update sliders for text/inner/outer (similar to pink)
function updateTextSlider(channel, picker) {
const input = picker.querySelector('#text-' + channel + '-input').value;
picker.querySelector('#text-' + channel + '-slider').value = input;
updateDoublePreview(picker);
}
function updateInnerSlider(channel, picker) {
const input = picker.querySelector('#inner-' + channel + '-input').value;
picker.querySelector('#inner-' + channel + '-slider').value = input;
updateDoublePreview(picker);
}
function updateOuterSlider(channel, picker) {
const input = picker.querySelector('#outer-' + channel + '-input').value;
picker.querySelector('#outer-' + channel + '-slider').value = input;
updateDoublePreview(picker);
}
doubleOutlinePicker.addEventListener('input', function(e) {
if (e.target.id === 'font-family-select' || e.target.id === 'font-size' || e.target.id === 'font-bold' || e.target.id === 'text-alpha-slider' || e.target.id === 'inner-offset' || e.target.id === 'inner-blur' || e.target.id === 'inner-alpha-slider' || e.target.id === 'outer-offset' || e.target.id === 'outer-blur' || e.target.id === 'outer-alpha-slider') {
updateDoublePreview(doubleOutlinePicker);
} else if (e.target.matches('#text-r-slider, #text-g-slider, #text-b-slider')) {
updateDoublePreview(doubleOutlinePicker);
} else if (e.target.matches('#inner-r-slider, #inner-g-slider, #inner-b-slider')) {
updateDoublePreview(doubleOutlinePicker);
} else if (e.target.matches('#outer-r-slider, #outer-g-slider, #outer-b-slider')) {
updateDoublePreview(doubleOutlinePicker);
} else if (e.target.id.includes('text-') && e.target.id.includes('input') && e.target.type === 'number') {
const channel = e.target.id.replace('text-', '').replace('-input', '');
updateTextSlider(channel, doubleOutlinePicker);
} else if (e.target.id.includes('inner-') && e.target.id.includes('input') && e.target.type === 'number') {
const channel = e.target.id.replace('inner-', '').replace('-input', '');
updateInnerSlider(channel, doubleOutlinePicker);
} else if (e.target.id.includes('outer-') && e.target.id.includes('input') && e.target.type === 'number') {
const channel = e.target.id.replace('outer-', '').replace('-input', '');
updateOuterSlider(channel, doubleOutlinePicker);
} else if (e.target.id === 'text-hex-input') {
const hexValue = e.target.value.replace('#', '').toUpperCase();
if (hexValue.match(/^([0-9A-F]{6})$/)) {
const rgb = hexToRgb(hexValue);
doubleOutlinePicker.querySelector('#text-r-slider').value = rgb.r;
doubleOutlinePicker.querySelector('#text-g-slider').value = rgb.g;
doubleOutlinePicker.querySelector('#text-b-slider').value = rgb.b;
doubleOutlinePicker.querySelector('#text-r-input').value = rgb.r;
doubleOutlinePicker.querySelector('#text-g-input').value = rgb.g;
doubleOutlinePicker.querySelector('#text-b-input').value = rgb.b;
updateDoublePreview(doubleOutlinePicker);
}
} else if (e.target.id === 'inner-hex-input') {
const hexValue = e.target.value.replace('#', '').toUpperCase();
if (hexValue.match(/^([0-9A-F]{6})$/)) {
const rgb = hexToRgb(hexValue);
doubleOutlinePicker.querySelector('#inner-r-slider').value = rgb.r;
doubleOutlinePicker.querySelector('#inner-g-slider').value = rgb.g;
doubleOutlinePicker.querySelector('#inner-b-slider').value = rgb.b;
doubleOutlinePicker.querySelector('#inner-r-input').value = rgb.r;
doubleOutlinePicker.querySelector('#inner-g-input').value = rgb.g;
doubleOutlinePicker.querySelector('#inner-b-input').value = rgb.b;
updateDoublePreview(doubleOutlinePicker);
}
} else if (e.target.id === 'outer-hex-input') {
const hexValue = e.target.value.replace('#', '').toUpperCase();
if (hexValue.match(/^([0-9A-F]{6})$/)) {
const rgb = hexToRgb(hexValue);
doubleOutlinePicker.querySelector('#outer-r-slider').value = rgb.r;
doubleOutlinePicker.querySelector('#outer-g-slider').value = rgb.g;
doubleOutlinePicker.querySelector('#outer-b-slider').value = rgb.b;
doubleOutlinePicker.querySelector('#outer-r-input').value = rgb.r;
doubleOutlinePicker.querySelector('#outer-g-input').value = rgb.g;
doubleOutlinePicker.querySelector('#outer-b-input').value = rgb.b;
updateDoublePreview(doubleOutlinePicker);
}
}
});
updateDoublePreview(doubleOutlinePicker);
} catch (err) {
log('Double outline picker open error', err);
showDebugIndicator('Double outline picker failed; check console.', 'error');
}
}
// Open symbols palette picker
function openSymbolsPicker(ta) {
try {
closeAllPickers();
const overlay = document.createElement('div');
overlay.id = 'symbol-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);
symbolPicker = document.createElement('div');
symbolPicker.id = 'symbol-picker';
symbolPicker.innerHTML = '<h3>Symbols Palette</h3>' +
'<div class="symbols-palette"></div>' +
'<div class="picker-buttons">' +
'<button id="picker-cancel">Close</button>' +
'</div>';
document.body.appendChild(symbolPicker);
const palette = symbolPicker.querySelector('.symbols-palette');
CONFIG.symbols.forEach(function(sym) {
const btn = document.createElement('button');
btn.innerHTML = sym;
btn.title = 'Insert ' + sym;
btn.addEventListener('click', function() {
insertAtCursor(ta, sym);
closeSymbolsPicker();
});
palette.appendChild(btn);
});
symbolPicker.querySelector('#picker-cancel').addEventListener('click', closeSymbolsPicker);
overlay.addEventListener('click', closeSymbolsPicker);
const escHandler = function(e) {
if (e.key === 'Escape') closeSymbolsPicker();
};
window.addEventListener('keydown', escHandler);
} catch (err) {
log('Symbols picker open error', err);
showDebugIndicator('Symbols picker failed; check console.', 'error');
}
}
// 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);
// Update preview
if (CONFIG.previewEnabled) debounceUpdatePreview();
updateCharCount();
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
var 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);
// Update preview
if (CONFIG.previewEnabled) debounceUpdatePreview();
updateCharCount();
} catch (err) {
log('Insert at cursor error', err);
}
}
// History management for undo/redo - now saves {value, start, end} - debounced save
function saveState(ta) {
try {
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(function() {
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);
}, CONFIG.debounceDelay);
} 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 }));
if (CONFIG.previewEnabled) debounceUpdatePreview();
updateCharCount();
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 }));
if (CONFIG.previewEnabled) debounceUpdatePreview();
updateCharCount();
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(function(b) { return b.id === 'bold-btn'; }).action(textarea);
} else if (e.key === 'i') {
e.preventDefault();
CONFIG.buttons.find(function(b) { return b.id === 'italic-btn'; }).action(textarea);
} else if (e.key === 'u') {
e.preventDefault();
CONFIG.buttons.find(function(b) { return b.id === 'underline-btn'; }).action(textarea);
} else if (e.key === 'z' && !e.shiftKey) {
e.preventDefault();
CONFIG.buttons.find(function(b) { return b.id === 'undo-btn'; }).action(textarea);
} else if (e.key === 'y' || (e.key === 'z' && e.shiftKey)) {
e.preventDefault();
CONFIG.buttons.find(function(b) { return 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 = function(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, preview below
function injectToolbar(dialog, isManual) {
if (typeof isManual === 'undefined') 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;
}
injectPreview(dialog);
// 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
// Setup char count listener
textarea.addEventListener('input', function() {
updateCharCount();
if (CONFIG.previewEnabled) debounceUpdatePreview();
});
updateCharCount();
// Auto-resize dialog immediately after insertion (increased delay for layout settle)
setTimeout(function() { autoResizeDialog(dialog); }, 150);
// Cleanup on dialog close - remove dataset
const observerCleanup = new MutationObserver(function(mutations) {
if (!document.body.contains(dialog)) {
activeDialogs.delete(dialog); // Track by reference now
if (tb && tb.parentNode) tb.remove();
if (previewDiv) previewDiv.remove();
delete dialog.dataset.helperInjected;
delete dialog.dataset.helperResized;
history = [];
historyIndex = -1;
closeAllPickers();
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! Upgraded Color Picker with Eyedropper (Firefox supported with ColorZilla link), Live Preview, Char Count available.', '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) {
if (typeof type === 'undefined') type = 'info';
try {
if (!debugMode) return;
var 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(function() { 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(function(mutations) {
var detected = false;
mutations.forEach(function(mutation) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1 && node.matches && node.matches('.note-edit-dialog')) {
log('Dialog added via observer:', node);
detected = true;
setTimeout(function() { injectToolbar(node); }, 500); // Keep for render stability
}
});
}
});
if (detected) log('Observer triggered injection.');
});
observer.observe(document.body, { childList: true, subtree: true });
} catch (err) {
log('Observer setup error', err);
}
}
// Fallback poll for edge cases (e.g., observer misses, multi-tab glitches) - faster for persistence
var pollInterval = null;
function startPoll() {
try {
if (pollInterval) return;
pollInterval = setInterval(function() {
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(function(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 - load configs first
function init() {
try {
loadConfigs(); // Load user settings first
injectStyles();
setupManualTrigger(); // Defined early, no ReferenceError
setTimeout(setupObserver, 2000); // Delay observer to avoid interference with initial note drag mode
startPoll(); // Poll starts immediately for reliability
// Multi-tab: Listen for focus to re-inject if needed
window.addEventListener('focus', function(e) {
setTimeout(function() {
if (pollInterval) clearInterval(pollInterval);
startPoll();
}, 100);
});
showDebugIndicator('Script Loaded v' + CONFIG.version + '! Upgraded Color Picker with Eyedropper (Firefox supported with ColorZilla link), Preview, Char Count ready. Ctrl+Shift+I to force.', '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', function(e) {
setTimeout(function() {
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);
}
})();