Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Clean Paste - Remove and Format Text</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script> | |
tailwind.config = { | |
darkMode: 'class', | |
theme: { | |
extend: { | |
animation: { | |
'fade-in': 'fadeIn 0.3s ease-in-out', | |
'fade-out': 'fadeOut 0.3s ease-in-out', | |
'pulse-slow': 'pulse 3s infinite', | |
}, | |
keyframes: { | |
fadeIn: { | |
'0%': { opacity: '0' }, | |
'100%': { opacity: '1' }, | |
}, | |
fadeOut: { | |
'0%': { opacity: '1' }, | |
'100%': { opacity: '0' }, | |
} | |
} | |
} | |
} | |
} | |
</script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
.text-area-container { | |
position: relative; | |
transition: all 0.3s ease; | |
} | |
.text-area-container:focus-within { | |
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5); | |
} | |
.paste-btn { | |
transition: all 0.2s ease; | |
} | |
.paste-btn:hover { | |
transform: translateY(-1px); | |
} | |
.copy-btn { | |
transition: all 0.2s ease; | |
} | |
.copy-btn:hover { | |
transform: translateY(-1px); | |
} | |
.format-btn { | |
transition: all 0.2s ease; | |
} | |
.format-btn:hover { | |
transform: translateY(-1px); | |
} | |
.character-count { | |
font-variant-numeric: tabular-nums; | |
} | |
.tooltip { | |
position: absolute; | |
top: -40px; | |
left: 50%; | |
transform: translateX(-50%); | |
background-color: #333; | |
color: white; | |
padding: 5px 10px; | |
border-radius: 4px; | |
font-size: 12px; | |
opacity: 0; | |
transition: opacity 0.3s; | |
pointer-events: none; | |
white-space: nowrap; | |
} | |
.tooltip:after { | |
content: ""; | |
position: absolute; | |
top: 100%; | |
left: 50%; | |
margin-left: -5px; | |
border-width: 5px; | |
border-style: solid; | |
border-color: #333 transparent transparent transparent; | |
} | |
.show-tooltip { | |
opacity: 1; | |
} | |
.active-format { | |
background-color: #3b82f6 ; | |
color: white ; | |
} | |
.dark .text-area-container { | |
background-color: #1e293b; | |
border-color: #334155; | |
} | |
.dark .text-area-container textarea { | |
background-color: #1e293b; | |
border-color: #334155; | |
color: #f8fafc; | |
} | |
.dark .text-area-container textarea::placeholder { | |
color: #64748b; | |
} | |
.dark .format-btn { | |
background-color: #334155; | |
color: #e2e8f0; | |
} | |
.dark .format-btn:hover { | |
background-color: #475569; | |
} | |
.dark .bg-gray-50 { | |
background-color: #1e293b; | |
} | |
.dark .bg-white { | |
background-color: #0f172a; | |
} | |
.dark .text-gray-600 { | |
color: #94a3b8; | |
} | |
.dark .text-gray-500 { | |
color: #94a3b8; | |
} | |
.dark .text-gray-700 { | |
color: #e2e8f0; | |
} | |
.dark .border-gray-200 { | |
border-color: #334155; | |
} | |
.code-btn { | |
background-color: #f59e0b; | |
color: white; | |
} | |
.code-btn:hover { | |
background-color: #d97706; | |
} | |
.dark .code-btn { | |
background-color: #92400e; | |
} | |
.dark .code-btn:hover { | |
background-color: #7c2d12; | |
} | |
.analysis-panel { | |
max-height: 0; | |
overflow: hidden; | |
transition: max-height 0.3s ease-out; | |
} | |
.analysis-panel.open { | |
max-height: 300px; | |
} | |
.word-frequency-item { | |
transition: all 0.2s ease; | |
} | |
.word-frequency-item:hover { | |
transform: translateX(3px); | |
} | |
.find-replace-panel { | |
max-height: 0; | |
overflow: hidden; | |
transition: max-height 0.3s ease-out; | |
} | |
.find-replace-panel.open { | |
max-height: 200px; | |
} | |
.highlight { | |
background-color: rgba(255, 255, 0, 0.4); | |
} | |
.current-highlight { | |
background-color: rgba(255, 165, 0, 0.6); | |
animation: pulse 1.5s infinite; | |
} | |
@keyframes pulse { | |
0% { background-color: rgba(255, 165, 0, 0.6); } | |
50% { background-color: rgba(255, 165, 0, 0.3); } | |
100% { background-color: rgba(255, 165, 0, 0.6); } | |
} | |
.speech-controls { | |
max-height: 0; | |
overflow: hidden; | |
transition: max-height 0.3s ease-out; | |
} | |
.speech-controls.open { | |
max-height: 120px; | |
} | |
.voice-active { | |
animation: pulse-slow 2s infinite; | |
} | |
.progress-bar { | |
height: 4px; | |
background-color: #e5e7eb; | |
border-radius: 2px; | |
overflow: hidden; | |
} | |
.progress-bar-fill { | |
height: 100%; | |
background-color: #3b82f6; | |
transition: width 0.1s linear; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-100 dark:bg-gray-900 min-h-screen flex flex-col items-center justify-center p-4 transition-colors duration-200"> | |
<div class="w-full max-w-3xl bg-white dark:bg-slate-800 rounded-xl shadow-lg overflow-hidden transition-colors duration-200"> | |
<div class="bg-blue-600 p-4"> | |
<div class="flex items-center justify-between"> | |
<div class="flex items-center space-x-2"> | |
<i class="fas fa-broom text-white text-2xl"></i> | |
<h1 class="text-white text-2xl font-bold">Clean Paste</h1> | |
</div> | |
<div class="flex items-center space-x-4"> | |
<span class="text-blue-100 text-sm hidden md:block">Paste → Clean → Format → Copy</span> | |
<button id="theme-toggle" class="text-blue-100 hover:text-white focus:outline-none"> | |
<i class="fas fa-moon" id="theme-icon"></i> | |
</button> | |
</div> | |
</div> | |
<p class="text-blue-100 mt-1 text-sm">Remove formatting and apply new styles to your text</p> | |
</div> | |
<div class="p-6"> | |
<div class="text-area-container bg-gray-50 dark:bg-slate-700 rounded-lg border border-gray-200 dark:border-slate-600 p-4"> | |
<div class="flex justify-between items-center mb-2"> | |
<label for="clean-text" class="text-gray-600 dark:text-gray-300 font-medium">Paste your formatted text here:</label> | |
<div class="flex items-center space-x-2"> | |
<span class="text-xs text-gray-500 dark:text-gray-400 character-count">0 characters</span> | |
<button id="paste-btn" class="paste-btn bg-blue-100 hover:bg-blue-200 dark:bg-blue-900 dark:hover:bg-blue-800 text-blue-700 dark:text-blue-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
<i class="fas fa-paste mr-1"></i> Paste | |
</button> | |
</div> | |
</div> | |
<textarea | |
id="clean-text" | |
class="w-full h-64 p-3 bg-white dark:bg-slate-700 border border-gray-300 dark:border-slate-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none dark:text-white" | |
placeholder="Paste your formatted text here (or click the Paste button above)..." | |
spellcheck="false" | |
></textarea> | |
</div> | |
<!-- Find & Replace Panel --> | |
<div id="find-replace-panel" class="find-replace-panel bg-gray-50 dark:bg-slate-700 rounded-lg mt-4 p-4 border border-gray-200 dark:border-slate-600"> | |
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
<div> | |
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Find:</label> | |
<div class="flex"> | |
<input type="text" id="find-input" class="flex-1 p-2 border border-gray-300 dark:border-slate-600 rounded-l-md dark:bg-slate-600 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
<button id="find-prev-btn" class="bg-gray-200 dark:bg-slate-600 px-3 border-t border-b border-gray-300 dark:border-slate-600"> | |
<i class="fas fa-chevron-up"></i> | |
</button> | |
<button id="find-next-btn" class="bg-gray-200 dark:bg-slate-600 px-3 border border-gray-300 dark:border-slate-600 rounded-r-md"> | |
<i class="fas fa-chevron-down"></i> | |
</button> | |
</div> | |
</div> | |
<div> | |
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Replace with:</label> | |
<div class="flex"> | |
<input type="text" id="replace-input" class="flex-1 p-2 border border-gray-300 dark:border-slate-600 rounded-l-md dark:bg-slate-600 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
<button id="replace-btn" class="bg-blue-500 text-white px-3 border border-blue-500 rounded-r-md hover:bg-blue-600"> | |
Replace | |
</button> | |
</div> | |
</div> | |
</div> | |
<div class="flex justify-between items-center mt-3"> | |
<div class="flex items-center space-x-2"> | |
<input type="checkbox" id="case-sensitive" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded dark:bg-slate-600 dark:border-slate-500"> | |
<label for="case-sensitive" class="text-sm text-gray-700 dark:text-gray-300">Case sensitive</label> | |
</div> | |
<div class="text-sm text-gray-500 dark:text-gray-400"> | |
<span id="find-results">0 matches</span> | |
</div> | |
</div> | |
</div> | |
<!-- Text to Speech Panel --> | |
<div id="speech-panel" class="speech-controls bg-gray-50 dark:bg-slate-700 rounded-lg mt-4 p-4 border border-gray-200 dark:border-slate-600"> | |
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4"> | |
<div class="flex-1"> | |
<div class="flex items-center space-x-3 mb-2"> | |
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">Voice:</label> | |
<select id="voice-select" class="flex-1 p-2 border border-gray-300 dark:border-slate-600 rounded-md dark:bg-slate-600 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"> | |
<option value="">Loading voices...</option> | |
</select> | |
</div> | |
<div class="grid grid-cols-2 gap-3"> | |
<div> | |
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 block mb-1">Rate:</label> | |
<input type="range" id="rate-control" min="0.5" max="2" step="0.1" value="1" class="w-full"> | |
<div class="text-xs text-gray-500 dark:text-gray-400 text-center" id="rate-value">1.0</div> | |
</div> | |
<div> | |
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 block mb-1">Pitch:</label> | |
<input type="range" id="pitch-control" min="0.5" max="2" step="0.1" value="1" class="w-full"> | |
<div class="text-xs text-gray-500 dark:text-gray-400 text-center" id="pitch-value">1.0</div> | |
</div> | |
</div> | |
</div> | |
<div class="flex flex-col items-center justify-center space-y-2"> | |
<div class="flex space-x-3"> | |
<button id="speak-btn" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-full flex items-center"> | |
<i class="fas fa-play mr-2"></i> Speak | |
</button> | |
<button id="pause-btn" class="bg-yellow-500 hover:bg-yellow-600 text-white px-4 py-2 rounded-full flex items-center"> | |
<i class="fas fa-pause mr-2"></i> Pause | |
</button> | |
<button id="stop-btn" class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-full flex items-center"> | |
<i class="fas fa-stop mr-2"></i> Stop | |
</button> | |
</div> | |
<div class="progress-bar w-full max-w-xs"> | |
<div id="progress-bar-fill" class="progress-bar-fill" style="width: 0%"></div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Text Analysis Panel --> | |
<div id="analysis-panel" class="analysis-panel bg-gray-50 dark:bg-slate-700 rounded-lg mt-4 p-4 border border-gray-200 dark:border-slate-600 overflow-y-auto"> | |
<h3 class="font-medium text-gray-700 dark:text-gray-300 mb-3">Text Analysis</h3> | |
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4"> | |
<div class="bg-white dark:bg-slate-600 p-3 rounded-lg shadow"> | |
<div class="text-sm text-gray-500 dark:text-gray-400">Characters</div> | |
<div class="text-xl font-bold text-gray-800 dark:text-white" id="analysis-chars">0</div> | |
</div> | |
<div class="bg-white dark:bg-slate-600 p-3 rounded-lg shadow"> | |
<div class="text-sm text-gray-500 dark:text-gray-400">Words</div> | |
<div class="text-xl font-bold text-gray-800 dark:text-white" id="analysis-words">0</div> | |
</div> | |
<div class="bg-white dark:bg-slate-600 p-3 rounded-lg shadow"> | |
<div class="text-sm text-gray-500 dark:text-gray-400">Sentences</div> | |
<div class="text-xl font-bold text-gray-800 dark:text-white" id="analysis-sentences">0</div> | |
</div> | |
<div class="bg-white dark:bg-slate-600 p-3 rounded-lg shadow"> | |
<div class="text-sm text-gray-500 dark:text-gray-400">Reading Time</div> | |
<div class="text-xl font-bold text-gray-800 dark:text-white" id="analysis-reading-time">0 min</div> | |
</div> | |
</div> | |
<div> | |
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-2">Word Frequency</h4> | |
<div class="max-h-32 overflow-y-auto" id="word-frequency-list"> | |
<div class="text-center text-gray-500 dark:text-gray-400 py-2">No words to analyze</div> | |
</div> | |
</div> | |
</div> | |
<div class="mt-4"> | |
<div class="flex flex-wrap gap-2 mb-4"> | |
<button id="lowercase-btn" class="format-btn bg-gray-200 hover:bg-gray-300 dark:bg-slate-600 dark:hover:bg-slate-500 text-gray-700 dark:text-gray-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
<i class="fas fa-text-height mr-1"></i> lowercase | |
</button> | |
<button id="uppercase-btn" class="format-btn bg-gray-200 hover:bg-gray-300 dark:bg-slate-600 dark:hover:bg-slate-500 text-gray-700 dark:text-gray-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
<i class="fas fa-text-height mr-1 transform rotate-180"></i> UPPERCASE | |
</button> | |
<button id="bold-btn" class="format-btn bg-gray-200 hover:bg-gray-300 dark:bg-slate-600 dark:hover:bg-slate-500 text-gray-700 dark:text-gray-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
<i class="fas fa-bold mr-1"></i> Bold | |
</button> | |
<button id="italic-btn" class="format-btn bg-gray-200 hover:bg-gray-300 dark:bg-slate-600 dark:hover:bg-slate-500 text-gray-700 dark:text-gray-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
<i class="fas fa-italic mr-1"></i> Italic | |
</button> | |
<button id="underline-btn" class="format-btn bg-gray-200 hover:bg-gray-300 dark:bg-slate-600 dark:hover:bg-slate-500 text-gray-700 dark:text-gray-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
<i class="fas fa-underline mr-1"></i> Underline | |
</button> | |
<button id="capitalize-btn" class="format-btn bg-gray-200 hover:bg-gray-300 dark:bg-slate-600 dark:hover:bg-slate-500 text-gray-700 dark:text-gray-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
<i class="fas fa-paragraph mr-1"></i> Capitalize | |
</button> | |
<button id="reverse-btn" class="format-btn bg-gray-200 hover:bg-gray-300 dark:bg-slate-600 dark:hover:bg-slate-500 text-gray-700 dark:text-gray-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
<i class="fas fa-exchange-alt mr-1"></i> Reverse | |
</button> | |
<button id="encode-btn" class="format-btn code-btn px-3 py-1 rounded-md text-sm flex items-center"> | |
<i class="fas fa-lock mr-1"></i> Encode | |
</button> | |
<button id="decode-btn" class="format-btn code-btn px-3 py-1 rounded-md text-sm flex items-center"> | |
<i class="fas fa-lock-open mr-1"></i> Decode | |
</button> | |
<button id="remove-format-btn" class="format-btn bg-red-100 hover:bg-red-200 dark:bg-red-900 dark:hover:bg-red-800 text-red-700 dark:text-red-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
<i class="fas fa-eraser mr-1"></i> Remove Format | |
</button> | |
<button id="find-replace-btn" class="format-btn bg-purple-100 hover:bg-purple-200 dark:bg-purple-900 dark:hover:bg-purple-800 text-purple-700 dark:text-purple-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
<i class="fas fa-search mr-1"></i> Find & Replace | |
</button> | |
<button id="analyze-btn" class="format-btn bg-green-100 hover:bg-green-200 dark:bg-green-900 dark:hover:bg-green-800 text-green-700 dark:text-green-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
<i class="fas fa-chart-bar mr-1"></i> Analyze Text | |
</button> | |
<button id="speak-toggle-btn" class="format-btn bg-indigo-100 hover:bg-indigo-200 dark:bg-indigo-900 dark:hover:bg-indigo-800 text-indigo-700 dark:text-indigo-200 px-3 py-1 rounded-md text-sm flex items-center"> | |
<i class="fas fa-volume-up mr-1"></i> Text to Speech | |
</button> | |
</div> | |
<div class="flex justify-between items-center"> | |
<div class="flex items-center space-x-2"> | |
<button id="clear-btn" class="bg-gray-200 hover:bg-gray-300 dark:bg-slate-600 dark:hover:bg-slate-500 text-gray-700 dark:text-gray-200 px-4 py-2 rounded-md flex items-center"> | |
<i class="fas fa-trash-alt mr-2"></i> Clear | |
</button> | |
<button id="copy-btn" class="copy-btn bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md flex items-center relative"> | |
<i class="fas fa-copy mr-2"></i> Copy Text | |
<span class="tooltip">Copied to clipboard!</span> | |
</button> | |
</div> | |
<div class="text-xs text-gray-500 dark:text-gray-400"> | |
<span id="word-count">0 words</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="bg-gray-50 dark:bg-slate-700 p-4 border-t border-gray-200 dark:border-slate-600"> | |
<div class="flex flex-wrap items-center justify-center gap-4 text-sm text-gray-600 dark:text-gray-300"> | |
<div class="flex items-center"> | |
<i class="fas fa-info-circle mr-1"></i> | |
<span>Formats removed: bold, italic, colors, fonts, etc.</span> | |
</div> | |
<div class="flex items-center"> | |
<i class="fas fa-keyboard mr-1"></i> | |
<span>Shortcut: Ctrl+V to paste, Ctrl+C to copy</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="mt-8 text-center text-gray-500 dark:text-gray-400 text-sm"> | |
<p>Clean Paste - Remove formatting and apply new styles to your text</p> | |
</div> | |
<audio id="click-sound" preload="auto"> | |
<source src="https://electronicenergysource.com/entirelibofsounds/Salvaged%20Files/1001%20Sound%20Effects/Video%20Game%20Sounds/Arcade%20Beep%2001.wav" type="audio/wav"> | |
</audio> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
const cleanTextArea = document.getElementById('clean-text'); | |
const pasteBtn = document.getElementById('paste-btn'); | |
const copyBtn = document.getElementById('copy-btn'); | |
const clearBtn = document.getElementById('clear-btn'); | |
const charCount = document.querySelector('.character-count'); | |
const wordCount = document.getElementById('word-count'); | |
const tooltip = document.querySelector('.tooltip'); | |
const clickSound = document.getElementById('click-sound'); | |
const themeToggle = document.getElementById('theme-toggle'); | |
const themeIcon = document.getElementById('theme-icon'); | |
// Format buttons | |
const lowercaseBtn = document.getElementById('lowercase-btn'); | |
const uppercaseBtn = document.getElementById('uppercase-btn'); | |
const boldBtn = document.getElementById('bold-btn'); | |
const italicBtn = document.getElementById('italic-btn'); | |
const underlineBtn = document.getElementById('underline-btn'); | |
const capitalizeBtn = document.getElementById('capitalize-btn'); | |
const reverseBtn = document.getElementById('reverse-btn'); | |
const encodeBtn = document.getElementById('encode-btn'); | |
const decodeBtn = document.getElementById('decode-btn'); | |
const removeFormatBtn = document.getElementById('remove-format-btn'); | |
// New feature buttons | |
const findReplaceBtn = document.getElementById('find-replace-btn'); | |
const analyzeBtn = document.getElementById('analyze-btn'); | |
const speakToggleBtn = document.getElementById('speak-toggle-btn'); | |
const findReplacePanel = document.getElementById('find-replace-panel'); | |
const analysisPanel = document.getElementById('analysis-panel'); | |
const speechPanel = document.getElementById('speech-panel'); | |
// Find & Replace elements | |
const findInput = document.getElementById('find-input'); | |
const replaceInput = document.getElementById('replace-input'); | |
const findNextBtn = document.getElementById('find-next-btn'); | |
const findPrevBtn = document.getElementById('find-prev-btn'); | |
const replaceBtn = document.getElementById('replace-btn'); | |
const caseSensitiveCheckbox = document.getElementById('case-sensitive'); | |
const findResults = document.getElementById('find-results'); | |
// Analysis elements | |
const analysisChars = document.getElementById('analysis-chars'); | |
const analysisWords = document.getElementById('analysis-words'); | |
const analysisSentences = document.getElementById('analysis-sentences'); | |
const analysisReadingTime = document.getElementById('analysis-reading-time'); | |
const wordFrequencyList = document.getElementById('word-frequency-list'); | |
// Speech synthesis elements | |
const voiceSelect = document.getElementById('voice-select'); | |
const rateControl = document.getElementById('rate-control'); | |
const pitchControl = document.getElementById('pitch-control'); | |
const rateValue = document.getElementById('rate-value'); | |
const pitchValue = document.getElementById('pitch-value'); | |
const speakBtn = document.getElementById('speak-btn'); | |
const pauseBtn = document.getElementById('pause-btn'); | |
const stopBtn = document.getElementById('stop-btn'); | |
const progressBarFill = document.getElementById('progress-bar-fill'); | |
// Variables for find & replace | |
let currentFindIndex = -1; | |
let findMatches = []; | |
// Variables for speech synthesis | |
let speechSynthesis = window.speechSynthesis; | |
let speechUtterance = null; | |
let voices = []; | |
let isSpeaking = false; | |
let isPaused = false; | |
let speechProgressInterval; | |
// Play click sound | |
function playClickSound() { | |
clickSound.currentTime = 0; | |
clickSound.play().catch(e => console.log("Audio play failed:", e)); | |
} | |
// Theme toggle | |
function toggleTheme() { | |
if (document.documentElement.classList.contains('dark')) { | |
document.documentElement.classList.remove('dark'); | |
localStorage.setItem('theme', 'light'); | |
themeIcon.classList.remove('fa-sun'); | |
themeIcon.classList.add('fa-moon'); | |
} else { | |
document.documentElement.classList.add('dark'); | |
localStorage.setItem('theme', 'dark'); | |
themeIcon.classList.remove('fa-moon'); | |
themeIcon.classList.add('fa-sun'); | |
} | |
playClickSound(); | |
} | |
// Check for saved theme preference | |
if (localStorage.getItem('theme') === 'dark' || | |
(!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) { | |
document.documentElement.classList.add('dark'); | |
themeIcon.classList.remove('fa-moon'); | |
themeIcon.classList.add('fa-sun'); | |
} | |
themeToggle.addEventListener('click', toggleTheme); | |
// Update character and word count | |
function updateCounts() { | |
const text = cleanTextArea.value; | |
charCount.textContent = `${text.length} characters`; | |
const words = text.trim() === '' ? 0 : text.trim().split(/\s+/).length; | |
wordCount.textContent = `${words} words`; | |
} | |
// Handle paste from button | |
pasteBtn.addEventListener('click', async function() { | |
playClickSound(); | |
try { | |
const text = await navigator.clipboard.readText(); | |
cleanTextArea.value = text; | |
updateCounts(); | |
// Show success feedback | |
pasteBtn.innerHTML = '<i class="fas fa-check mr-1"></i> Pasted!'; | |
pasteBtn.classList.remove('bg-blue-100', 'text-blue-700', 'dark:bg-blue-900', 'dark:text-blue-200'); | |
pasteBtn.classList.add('bg-green-100', 'text-green-700', 'dark:bg-green-900', 'dark:text-green-200'); | |
setTimeout(() => { | |
pasteBtn.innerHTML = '<i class="fas fa-paste mr-1"></i> Paste'; | |
pasteBtn.classList.remove('bg-green-100', 'text-green-700', 'dark:bg-green-900', 'dark:text-green-200'); | |
pasteBtn.classList.add('bg-blue-100', 'text-blue-700', 'dark:bg-blue-900', 'dark:text-blue-200'); | |
}, 1500); | |
} catch (err) { | |
alert('Failed to read clipboard. Please paste manually into the text area.'); | |
console.error('Failed to read clipboard contents:', err); | |
} | |
}); | |
// Handle copy button | |
copyBtn.addEventListener('click', function() { | |
playClickSound(); | |
if (cleanTextArea.value.trim() === '') { | |
alert('Nothing to copy! Please paste some text first.'); | |
return; | |
} | |
navigator.clipboard.writeText(cleanTextArea.value) | |
.then(() => { | |
// Show tooltip | |
tooltip.classList.add('show-tooltip'); | |
setTimeout(() => { | |
tooltip.classList.remove('show-tooltip'); | |
}, 2000); | |
}) | |
.catch(err => { | |
console.error('Failed to copy text: ', err); | |
alert('Failed to copy text. Please try again.'); | |
}); | |
}); | |
// Handle clear button | |
clearBtn.addEventListener('click', function() { | |
playClickSound(); | |
cleanTextArea.value = ''; | |
updateCounts(); | |
clearHighlights(); | |
findMatches = []; | |
currentFindIndex = -1; | |
findResults.textContent = '0 matches'; | |
// Stop any ongoing speech | |
stopSpeech(); | |
}); | |
// Formatting functions | |
function applyFormat(formatFn) { | |
if (cleanTextArea.value.trim() === '') { | |
alert('No text to format! Please paste some text first.'); | |
return; | |
} | |
const startPos = cleanTextArea.selectionStart; | |
const endPos = cleanTextArea.selectionEnd; | |
if (startPos === endPos) { | |
// No selection - format entire text | |
cleanTextArea.value = formatFn(cleanTextArea.value); | |
} else { | |
// Format only selected text | |
const selectedText = cleanTextArea.value.substring(startPos, endPos); | |
const formattedText = formatFn(selectedText); | |
cleanTextArea.value = cleanTextArea.value.substring(0, startPos) + | |
formattedText + | |
cleanTextArea.value.substring(endPos); | |
} | |
updateCounts(); | |
} | |
// Base64 encode function | |
function encodeBase64(text) { | |
return btoa(unescape(encodeURIComponent(text))); | |
} | |
// Base64 decode function | |
function decodeBase64(text) { | |
try { | |
return decodeURIComponent(escape(atob(text))); | |
} catch (e) { | |
alert("Invalid Base64 encoded string!"); | |
return text; | |
} | |
} | |
// Format button handlers | |
lowercaseBtn.addEventListener('click', function() { | |
playClickSound(); | |
applyFormat(text => text.toLowerCase()); | |
highlightActiveButton(lowercaseBtn); | |
}); | |
uppercaseBtn.addEventListener('click', function() { | |
playClickSound(); | |
applyFormat(text => text.toUpperCase()); | |
highlightActiveButton(uppercaseBtn); | |
}); | |
boldBtn.addEventListener('click', function() { | |
playClickSound(); | |
applyFormat(text => { | |
const startPos = cleanTextArea.selectionStart; | |
const endPos = cleanTextArea.selectionEnd; | |
if (startPos === endPos) { | |
return text.replace(/([^\n])([^\n]+)?/g, function(match, p1, p2) { | |
return p1 + (p2 ? '**' + p2 + '**' : ''); | |
}); | |
} else { | |
return '**' + text + '**'; | |
} | |
}); | |
highlightActiveButton(boldBtn); | |
}); | |
italicBtn.addEventListener('click', function() { | |
playClickSound(); | |
applyFormat(text => { | |
const startPos = cleanTextArea.selectionStart; | |
const endPos = cleanTextArea.selectionEnd; | |
if (startPos === endPos) { | |
return text.replace(/([^\n])([^\n]+)?/g, function(match, p1, p2) { | |
return p1 + (p2 ? '_' + p2 + '_' : ''); | |
}); | |
} else { | |
return '_' + text + '_'; | |
} | |
}); | |
highlightActiveButton(italicBtn); | |
}); | |
underlineBtn.addEventListener('click', function() { | |
playClickSound(); | |
applyFormat(text => { | |
const startPos = cleanTextArea.selectionStart; | |
const endPos = cleanTextArea.selectionEnd; | |
if (startPos === endPos) { | |
return text.replace(/([^\n])([^\n]+)?/g, function(match, p1, p2) { | |
return p1 + (p2 ? '<u>' + p2 + '</u>' : ''); | |
}); | |
} else { | |
return '<u>' + text + '</u>'; | |
} | |
}); | |
highlightActiveButton(underlineBtn); | |
}); | |
capitalizeBtn.addEventListener('click', function() { | |
playClickSound(); | |
applyFormat(text => { | |
return text.toLowerCase().replace(/(^|\s)\S/g, function(firstLetter) { | |
return firstLetter.toUpperCase(); | |
}); | |
}); | |
highlightActiveButton(capitalizeBtn); | |
}); | |
reverseBtn.addEventListener('click', function() { | |
playClickSound(); | |
applyFormat(text => { | |
return text.split('').reverse().join(''); | |
}); | |
highlightActiveButton(reverseBtn); | |
}); | |
encodeBtn.addEventListener('click', function() { | |
playClickSound(); | |
applyFormat(text => { | |
return encodeBase64(text); | |
}); | |
highlightActiveButton(encodeBtn); | |
}); | |
decodeBtn.addEventListener('click', function() { | |
playClickSound(); | |
applyFormat(text => { | |
return decodeBase64(text); | |
}); | |
highlightActiveButton(decodeBtn); | |
}); | |
removeFormatBtn.addEventListener('click', function() { | |
playClickSound(); | |
applyFormat(text => { | |
// Remove markdown formatting | |
let cleaned = text.replace(/(\*\*|__)(.*?)\1/g, '$2'); // bold | |
cleaned = cleaned.replace(/(\*|_)(.*?)\1/g, '$2'); // italic | |
cleaned = cleaned.replace(/<u>(.*?)<\/u>/g, '$1'); // underline | |
cleaned = cleaned.replace(/<[^>]+>/g, ''); // any HTML tags | |
return cleaned; | |
}); | |
highlightActiveButton(removeFormatBtn); | |
}); | |
// Highlight active format button | |
function highlightActiveButton(button) { | |
// Remove active class from all format buttons | |
document.querySelectorAll('.format-btn').forEach(btn => { | |
if (!btn.classList.contains('code-btn') && | |
!btn.classList.contains('bg-purple-100') && | |
!btn.classList.contains('bg-green-100') && | |
!btn.classList.contains('bg-indigo-100')) { | |
btn.classList.remove('active-format'); | |
btn.classList.add('bg-gray-200', 'text-gray-700', 'dark:bg-slate-600', 'dark:text-gray-200'); | |
btn.classList.remove('bg-blue-600', 'text-white'); | |
} | |
}); | |
// Add active class to clicked button | |
if (button.classList.contains('code-btn')) { | |
button.classList.add('bg-yellow-600', 'dark:bg-yellow-700'); | |
} else if (button.classList.contains('bg-purple-100')) { | |
button.classList.add('bg-purple-600', 'text-white'); | |
} else if (button.classList.contains('bg-green-100')) { | |
button.classList.add('bg-green-600', 'text-white'); | |
} else if (button.classList.contains('bg-indigo-100')) { | |
button.classList.add('bg-indigo-600', 'text-white'); | |
} else { | |
button.classList.add('active-format'); | |
button.classList.remove('bg-gray-200', 'text-gray-700', 'dark:bg-slate-600', 'dark:text-gray-200'); | |
button.classList.add('bg-blue-600', 'text-white'); | |
} | |
// Remove highlight after 1.5 seconds | |
setTimeout(() => { | |
if (button.classList.contains('code-btn')) { | |
button.classList.remove('bg-yellow-600', 'dark:bg-yellow-700'); | |
} else if (button.classList.contains('bg-purple-100')) { | |
button.classList.remove('bg-purple-600', 'text-white'); | |
} else if (button.classList.contains('bg-green-100')) { | |
button.classList.remove('bg-green-600', 'text-white'); | |
} else if (button.classList.contains('bg-indigo-100')) { | |
button.classList.remove('bg-indigo-600', 'text-white'); | |
} else { | |
button.classList.remove('active-format'); | |
button.classList.remove('bg-blue-600', 'text-white'); | |
button.classList.add('bg-gray-200', 'text-gray-700', 'dark:bg-slate-600', 'dark:text-gray-200'); | |
} | |
}, 1500); | |
} | |
// Toggle find & replace panel | |
findReplaceBtn.addEventListener('click', function() { | |
playClickSound(); | |
analysisPanel.classList.remove('open'); | |
speechPanel.classList.remove('open'); | |
findReplacePanel.classList.toggle('open'); | |
if (findReplacePanel.classList.contains('open')) { | |
highlightActiveButton(findReplaceBtn); | |
findInput.focus(); | |
} | |
}); | |
// Toggle analysis panel | |
analyzeBtn.addEventListener('click', function() { | |
playClickSound(); | |
findReplacePanel.classList.remove('open'); | |
speechPanel.classList.remove('open'); | |
analysisPanel.classList.toggle('open'); | |
if (analysisPanel.classList.contains('open')) { | |
highlightActiveButton(analyzeBtn); | |
analyzeText(); | |
} | |
}); | |
// Toggle speech panel | |
speakToggleBtn.addEventListener('click', function() { | |
playClickSound(); | |
findReplacePanel.classList.remove('open'); | |
analysisPanel.classList.remove('open'); | |
speechPanel.classList.toggle('open'); | |
if (speechPanel.classList.contains('open')) { | |
highlightActiveButton(speakToggleBtn); | |
loadVoices(); | |
} | |
}); | |
// Analyze text function | |
function analyzeText() { | |
const text = cleanTextArea.value; | |
// Basic stats | |
analysisChars.textContent = text.length; | |
const words = text.trim() === '' ? 0 : text.trim().split(/\s+/).length; | |
analysisWords.textContent = words; | |
// Count sentences (very basic implementation) | |
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0).length; | |
analysisSentences.textContent = sentences; | |
// Calculate reading time (average reading speed: 200 words per minute) | |
const readingTime = Math.ceil(words / 200); | |
analysisReadingTime.textContent = readingTime <= 1 ? '1 min' : `${readingTime} mins`; | |
// Word frequency analysis | |
if (text.trim() === '') { | |
wordFrequencyList.innerHTML = '<div class="text-center text-gray-500 dark:text-gray-400 py-2">No words to analyze</div>'; | |
return; | |
} | |
// Process words | |
const wordsArray = text.toLowerCase() | |
.replace(/[^\w\s]/g, '') // Remove punctuation | |
.split(/\s+/) | |
.filter(word => word.length > 0); | |
// Count word frequency | |
const wordFrequency = {}; | |
wordsArray.forEach(word => { | |
wordFrequency[word] = (wordFrequency[word] || 0) + 1; | |
}); | |
// Sort by frequency | |
const sortedWords = Object.keys(wordFrequency).sort((a, b) => wordFrequency[b] - wordFrequency[a]); | |
// Display top 20 words | |
wordFrequencyList.innerHTML = ''; | |
sortedWords.slice(0, 20).forEach(word => { | |
const frequency = wordFrequency[word]; | |
const percentage = Math.round((frequency / wordsArray.length) * 100); | |
const item = document.createElement('div'); | |
item.className = 'word-frequency-item flex justify-between items-center mb-1 p-2 bg-white dark:bg-slate-600 rounded'; | |
const wordSpan = document.createElement('span'); | |
wordSpan.className = 'font-medium dark:text-white'; | |
wordSpan.textContent = word; | |
const freqSpan = document.createElement('span'); | |
freqSpan.className = 'text-sm text-gray-500 dark:text-gray-300'; | |
freqSpan.textContent = `${frequency} (${percentage}%)`; | |
item.appendChild(wordSpan); | |
item.appendChild(freqSpan); | |
wordFrequencyList.appendChild(item); | |
}); | |
} | |
// Find text function | |
function findText(direction = 'next') { | |
const searchText = findInput.value; | |
if (!searchText) { | |
alert('Please enter text to find'); | |
return; | |
} | |
const text = cleanTextArea.value; | |
const caseSensitive = caseSensitiveCheckbox.checked; | |
const flags = caseSensitive ? 'g' : 'gi'; | |
// Clear previous highlights | |
clearHighlights(); | |
// Find all matches | |
const regex = new RegExp(escapeRegExp(searchText), flags); | |
findMatches = []; | |
let match; | |
while ((match = regex.exec(text)) !== null) { | |
findMatches.push({ | |
start: match.index, | |
end: match.index + match[0].length | |
}); | |
} | |
findResults.textContent = `${findMatches.length} matches`; | |
if (findMatches.length === 0) { | |
alert('No matches found'); | |
return; | |
} | |
// Navigate to next/previous match | |
if (direction === 'next') { | |
currentFindIndex = (currentFindIndex + 1) % findMatches.length; | |
} else { | |
currentFindIndex = (currentFindIndex - 1 + findMatches.length) % findMatches.length; | |
} | |
// Highlight all matches and scroll to current one | |
highlightMatches(); | |
// Scroll to current match | |
const currentMatch = findMatches[currentFindIndex]; | |
scrollToMatch(currentMatch.start, currentMatch.end); | |
} | |
// Helper function to escape regex special characters | |
function escapeRegExp(string) { | |
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
} | |
// Highlight all matches | |
function highlightMatches() { | |
const text = cleanTextArea.value; | |
let highlightedText = ''; | |
let lastIndex = 0; | |
findMatches.forEach((match, index) => { | |
// Add text before match | |
highlightedText += text.substring(lastIndex, match.start); | |
// Add highlighted match | |
const matchClass = index === currentFindIndex ? 'current-highlight': 'highlight'; | |
highlightedText += `<span class="${matchClass}">${text.substring(match.start, match.end)}</span>`; | |
lastIndex = match.end; | |
}); | |
// Add remaining text | |
highlightedText += text.substring(lastIndex); | |
// Update textarea with highlighted HTML (using a hidden div) | |
const hiddenDiv = document.createElement('div'); | |
hiddenDiv.style.display = 'none'; | |
hiddenDiv.innerHTML = highlightedText; | |
document.body.appendChild(hiddenDiv); | |
// Get plain text with markers for highlights | |
let plainText = ''; | |
const walker = document.createTreeWalker(hiddenDiv, NodeFilter.SHOW_TEXT, null, false); | |
let node; | |
while (node = walker.nextNode()) { | |
plainText += node.nodeValue; | |
} | |
// Remove the hidden div | |
document.body.removeChild(hiddenDiv); | |
// Update textarea with plain text | |
cleanTextArea.value = plainText; | |
} | |
// Clear all highlights | |
function clearHighlights() { | |
if (findMatches.length > 0) { | |
const text = cleanTextArea.value; | |
let plainText = ''; | |
let lastIndex = 0; | |
findMatches.forEach(match => { | |
plainText += text.substring(lastIndex, match.start); | |
plainText += text.substring(match.start, match.end); | |
lastIndex = match.end; | |
}); | |
plainText += text.substring(lastIndex); | |
cleanTextArea.value = plainText; | |
findMatches = []; | |
currentFindIndex = -1; | |
findResults.textContent = '0 matches'; | |
} | |
} | |
// Scroll to match position | |
function scrollToMatch(start, end) { | |
// Calculate line height (approximate) | |
const lineHeight = 20; // px | |
// Calculate number of lines to scroll | |
const textBefore = cleanTextArea.value.substring(0, start); | |
const linesBefore = textBefore.split('\n').length - 1; | |
// Scroll to position | |
cleanTextArea.scrollTop = linesBefore * lineHeight; | |
// Set selection | |
cleanTextArea.focus(); | |
cleanTextArea.setSelectionRange(start, end); | |
} | |
// Replace text function | |
function replaceText() { | |
if (findMatches.length === 0) { | |
findText(); | |
return; | |
} | |
const replaceText = replaceInput.value; | |
const currentMatch = findMatches[currentFindIndex]; | |
// Replace current match | |
const text = cleanTextArea.value; | |
const newText = text.substring(0, currentMatch.start) + replaceText + text.substring(currentMatch.end); | |
cleanTextArea.value = newText; | |
// Adjust positions of remaining matches | |
const lengthDiff = replaceText.length - (currentMatch.end - currentMatch.start); | |
findMatches.forEach((match, index) => { | |
if (match.start > currentMatch.start) { | |
match.start += lengthDiff; | |
match.end += lengthDiff; | |
} | |
}); | |
// Remove current match from array | |
findMatches.splice(currentFindIndex, 1); | |
if (findMatches.length === 0) { | |
findResults.textContent = '0 matches'; | |
currentFindIndex = -1; | |
return; | |
} | |
// Update current index and highlight | |
currentFindIndex = Math.min(currentFindIndex, findMatches.length - 1); | |
findResults.textContent = `${findMatches.length} matches`; | |
highlightMatches(); | |
} | |
// Speech Synthesis Functions | |
function loadVoices() { | |
// Chrome loads voices asynchronously | |
voices = speechSynthesis.getVoices(); | |
voiceSelect.innerHTML = ''; | |
if (voices.length === 0) { | |
voiceSelect.innerHTML = '<option value="">Loading voices...</option>'; | |
// Try again in 1 second if voices aren't loaded yet | |
setTimeout(loadVoices, 1000); | |
return; | |
} | |
// Filter voices to only include ones with localService (usually system voices) | |
const systemVoices = voices.filter(voice => voice.localService); | |
systemVoices.forEach((voice, index) => { | |
const option = document.createElement('option'); | |
option.value = index; | |
option.textContent = `${voice.name} (${voice.lang})`; | |
if (voice.default) { | |
option.textContent += ' [Default]'; | |
} | |
voiceSelect.appendChild(option); | |
}); | |
// Try to select a reasonable default voice | |
const defaultVoice = systemVoices.find(voice => | |
voice.lang.startsWith('en-') && voice.default | |
) || systemVoices.find(voice => voice.default) || systemVoices[0]; | |
if (defaultVoice) { | |
const defaultIndex = systemVoices.indexOf(defaultVoice); | |
voiceSelect.value = defaultIndex; | |
} | |
} | |
function speakText() { | |
if (isSpeaking) { | |
pauseSpeech(); | |
return; | |
} | |
if (isPaused) { | |
resumeSpeech(); | |
return; | |
} | |
const text = cleanTextArea.value; | |
if (!text.trim()) { | |
alert('No text to speak! Please paste some text first.'); | |
return; | |
} | |
// Stop any ongoing speech | |
stopSpeech(); | |
// Create a new utterance | |
speechUtterance = new SpeechSynthesisUtterance(text); | |
// Set voice | |
const selectedVoiceIndex = parseInt(voiceSelect.value); | |
if (!isNaN(selectedVoiceIndex) && voices[selectedVoiceIndex]) { | |
speechUtterance.voice = voices[selectedVoiceIndex]; | |
} | |
// Set rate and pitch | |
speechUtterance.rate = parseFloat(rateControl.value); | |
speechUtterance.pitch = parseFloat(pitchControl.value); | |
// Update UI | |
isSpeaking = true; | |
speakBtn.innerHTML = '<i class="fas fa-pause mr-2"></i> Pause'; | |
speakBtn.classList.remove('bg-green-500', 'hover:bg-green-600'); | |
speakBtn.classList.add('bg-yellow-500', 'hover:bg-yellow-600'); | |
speakToggleBtn.classList.add('voice-active'); | |
// Reset progress bar | |
progressBarFill.style.width = '0%'; | |
// Start progress tracking | |
const textLength = text.length; | |
let currentPosition = 0; | |
speechProgressInterval = setInterval(() => { | |
if (speechSynthesis.speaking && !isPaused) { | |
// Estimate position based on time elapsed | |
const elapsed = (speechUtterance.elapsedTime || 0) * 1000; // ms | |
const totalDuration = (textLength / (speechUtterance.rate * 8)) * 1000; // rough estimate | |
const progress = Math.min(100, (elapsed / totalDuration) * 100); | |
progressBarFill.style.width = `${progress}%`; | |
} | |
}, 100); | |
// Event handlers | |
speechUtterance.onend = function() { | |
stopSpeech(); | |
}; | |
speechUtterance.onerror = function(event) { | |
console.error('SpeechSynthesis error:', event); | |
stopSpeech(); | |
alert('Error occurred during speech synthesis: ' + event.error); | |
}; | |
speechUtterance.onboundary = function(event) { | |
if (event.name === 'word') { | |
currentPosition = event.charIndex; | |
} | |
}; | |
// Speak the text | |
speechSynthesis.speak(speechUtterance); | |
} | |
function pauseSpeech() { | |
if (isSpeaking && !isPaused) { | |
speechSynthesis.pause(); | |
isSpeaking = false; | |
isPaused = true; | |
speakBtn.innerHTML = '<i class="fas fa-play mr-2"></i> Resume'; | |
speakBtn.classList.remove('bg-yellow-500', 'hover:bg-yellow-600'); | |
speakBtn.classList.add('bg-green-500', 'hover:bg-green-600'); | |
} | |
} | |
function resumeSpeech() { | |
if (isPaused) { | |
speechSynthesis.resume(); | |
isSpeaking = true; | |
isPaused = false; | |
speakBtn.innerHTML = '<i class="fas fa-pause mr-2"></i> Pause'; | |
speakBtn.classList.remove('bg-green-500', 'hover:bg-green-600'); | |
speakBtn.classList.add('bg-yellow-500', 'hover:bg-yellow-600'); | |
} | |
} | |
function stopSpeech() { | |
speechSynthesis.cancel(); | |
isSpeaking = false; | |
isPaused = false; | |
// Clear interval | |
if (speechProgressInterval) { | |
clearInterval(speechProgressInterval); | |
speechProgressInterval = null; | |
} | |
// Reset UI | |
speakBtn.innerHTML = '<i class="fas fa-play mr-2"></i> Speak'; | |
speakBtn.classList.remove('bg-yellow-500', 'hover:bg-yellow-600'); | |
speakBtn.classList.add('bg-green-500', 'hover:bg-green-600'); | |
speakToggleBtn.classList.remove('voice-active'); | |
progressBarFill.style.width = '0%'; | |
} | |
// Initialize speech synthesis | |
if ('speechSynthesis' in window) { | |
// Chrome loads voices asynchronously | |
speechSynthesis.onvoiceschanged = loadVoices; | |
// Load voices immediately if they're already available | |
if (speechSynthesis.getVoices().length > 0) { | |
loadVoices(); | |
} | |
} else { | |
// Disable speech features if not supported | |
speakToggleBtn.disabled = true; | |
speakToggleBtn.title = 'Text-to-speech not supported in your browser'; | |
} | |
// Find & Replace event listeners | |
findNextBtn.addEventListener('click', function() { | |
playClickSound(); | |
findText('next'); | |
}); | |
findPrevBtn.addEventListener('click', function() { | |
playClickSound(); | |
findText('prev'); | |
}); | |
replaceBtn.addEventListener('click', function() { | |
playClickSound(); | |
replaceText(); | |
}); | |
findInput.addEventListener('keydown', function(e) { | |
if (e.key === 'Enter') { | |
playClickSound(); | |
findText('next'); | |
} | |
}); | |
// Speech synthesis event listeners | |
speakBtn.addEventListener('click', function() { | |
playClickSound(); | |
speakText(); | |
}); | |
pauseBtn.addEventListener('click', function() { | |
playClickSound(); | |
pauseSpeech(); | |
}); | |
stopBtn.addEventListener('click', function() { | |
playClickSound(); | |
stopSpeech(); | |
}); | |
rateControl.addEventListener('input', function() { | |
rateValue.textContent = this.value; | |
if (speechUtterance) { | |
speechUtterance.rate = parseFloat(this.value); | |
} | |
}); | |
pitchControl.addEventListener('input', function() { | |
pitchValue.textContent = this.value; | |
if (speechUtterance) { | |
speechUtterance.pitch = parseFloat(this.value); | |
} | |
}); | |
// Update counts when typing | |
cleanTextArea.addEventListener('input', function() { | |
updateCounts(); | |
// Clear find highlights if text changes | |
if (findMatches.length > 0) { | |
clearHighlights(); | |
} | |
// Stop speech if text changes while speaking | |
if (isSpeaking || isPaused) { | |
stopSpeech(); | |
} | |
}); | |
// Handle direct paste into textarea | |
cleanTextArea.addEventListener('paste', function(e) { | |
playClickSound(); | |
// Let the paste happen first | |
setTimeout(() => { | |
updateCounts(); | |
// Show feedback | |
const originalPlaceholder = cleanTextArea.placeholder; | |
cleanTextArea.placeholder = "✓ Text pasted!"; | |
setTimeout(() => { | |
cleanTextArea.placeholder = originalPlaceholder; | |
}, 1500); | |
}, 10); | |
}); | |
// Keyboard shortcuts | |
document.addEventListener('keydown', function(e) { | |
if ((e.ctrlKey || e.metaKey) && e.key === 'v' && document.activeElement !== cleanTextArea) { | |
e.preventDefault(); | |
pasteBtn.click(); | |
} | |
if ((e.ctrlKey || e.metaKey) && e.key === 'c' && document.activeElement === cleanTextArea) { | |
e.preventDefault(); | |
copyBtn.click(); | |
} | |
// Formatting shortcuts | |
if ((e.ctrlKey || e.metaKey) && document.activeElement === cleanTextArea) { | |
switch(e.key.toLowerCase()) { | |
case 'b': | |
e.preventDefault(); | |
boldBtn.click(); | |
break; | |
case 'i': | |
e.preventDefault(); | |
italicBtn.click(); | |
break; | |
case 'u': | |
e.preventDefault(); | |
underlineBtn.click(); | |
break; | |
case 'l': | |
e.preventDefault(); | |
lowercaseBtn.click(); | |
break; | |
case 'u': | |
if (e.shiftKey) { | |
e.preventDefault(); | |
uppercaseBtn.click(); | |
} | |
break; | |
case 'r': | |
e.preventDefault(); | |
reverseBtn.click(); | |
break; | |
case 'e': | |
e.preventDefault(); | |
encodeBtn.click(); | |
break; | |
case 'd': | |
e.preventDefault(); | |
decodeBtn.click(); | |
break; | |
case 'f': | |
e.preventDefault(); | |
findReplaceBtn.click(); | |
break; | |
case 'h': | |
e.preventDefault(); | |
analyzeBtn.click(); | |
break; | |
case 's': | |
e.preventDefault(); | |
speakToggleBtn.click(); | |
break; | |
} | |
} | |
// Find next/previous with F3/Shift+F3 | |
if (e.key === 'F3') { | |
e.preventDefault(); | |
if (findReplacePanel.classList.contains('open')) { | |
if (e.shiftKey) { | |
findPrevBtn.click(); | |
} else { | |
findNextBtn.click(); | |
} | |
} | |
} | |
}); | |
// Initialize counts | |
updateCounts(); | |
}); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=C50BARZ/clean-paste-10" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |