Section Marker UI has been updated

This commit is contained in:
Ashim Kumar
2026-01-16 18:56:45 +06:00
parent 93119fcee6
commit f1748d2c8e
2 changed files with 651 additions and 71 deletions

View File

@@ -212,51 +212,308 @@ body {
.floating-btn:hover .tooltip-text { opacity: 1; } .floating-btn:hover .tooltip-text { opacity: 1; }
/* ============================================ /* ============================================
Chapter/Section Markers Chapter/Section Markers - Base Styles
============================================= */ ============================================= */
.editor-marker { .editor-marker {
padding: 15px; padding: 12px 16px;
margin: 20px 0; margin: 20px 0;
border-radius: 10px; border-radius: 12px;
user-select: none; user-select: none;
cursor: default; cursor: default;
position: relative; position: relative;
border: 1px solid rgba(0,0,0,0.1); border: 1px solid rgba(0,0,0,0.08);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
} }
.chapter-marker { .chapter-marker {
background: #fff0f0; background: linear-gradient(135deg, #fff5f5 0%, #ffe3e3 100%);
border-left: 5px solid #FF6B6B; border-left: 4px solid #FF6B6B;
} }
.section-marker { .section-marker {
background: #f0f0ff; background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 100%);
border-left: 5px solid #4834d4; border-left: 4px solid #4834d4;
margin-left: 20px; margin-left: 20px;
} }
.marker-header { /* ============================================
display: flex; Section Marker Toolbar Styles
align-items: center; ============================================= */
gap: 15px; .editor-marker .marker-header {
display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center;
gap: 12px;
} }
.marker-title { .editor-marker .marker-controls {
font-family: 'Poppins', sans-serif; display: flex;
font-weight: 700; flex-wrap: wrap;
text-transform: uppercase; align-items: center;
letter-spacing: 1px; gap: 10px;
font-size: 14px; flex: 1;
} }
.chapter-marker .marker-title { color: #d63031; } .editor-marker .marker-title {
.section-marker .marker-title { color: #4834d4; } font-family: 'Poppins', sans-serif;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
font-size: 12px;
min-width: 65px;
padding: 4px 0;
}
.marker-controls { .chapter-marker .marker-title {
display: flex; color: #dc2626;
align-items: center; }
gap: 10px;
.section-marker .marker-title {
color: #5b21b6;
}
/* Toolbar Groups */
.toolbar-group {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
background: rgba(255, 255, 255, 0.8);
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.06);
}
.toolbar-group-label {
font-size: 9px;
font-weight: 700;
color: #9ca3af;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-right: 6px;
white-space: nowrap;
}
/* Toolbar Buttons */
.toolbar-btn {
width: 32px;
height: 32px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #e5e7eb;
background: white;
border-radius: 6px;
color: #4b5563;
font-size: 14px;
transition: all 0.2s ease;
cursor: pointer;
}
.toolbar-btn:hover {
background: #f9fafb;
border-color: #d1d5db;
color: #1f2937;
}
.toolbar-btn:active {
background: #f3f4f6;
transform: scale(0.95);
}
.toolbar-btn.dropdown-toggle {
width: auto;
padding: 0 10px;
gap: 4px;
font-size: 11px;
font-weight: 600;
}
.toolbar-btn.dropdown-toggle::after {
font-size: 8px;
margin-left: 2px;
vertical-align: middle;
}
/* Dropdown Menus in Toolbar */
.editor-marker .dropdown-menu {
border-radius: 10px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
border: 1px solid rgba(0, 0, 0, 0.08);
padding: 6px;
min-width: 170px;
font-size: 13px;
}
.editor-marker .dropdown-item {
border-radius: 6px;
padding: 9px 12px;
font-size: 13px;
font-weight: 500;
transition: all 0.15s ease;
color: #374151;
}
.editor-marker .dropdown-item:hover {
background: linear-gradient(135deg, var(--accent-primary) 0%, var(--pill-bg-gradient-end) 100%);
color: white;
}
.editor-marker .dropdown-item i {
width: 20px;
text-align: center;
font-size: 14px;
}
/* Voice Select */
.voice-select {
min-width: 140px;
max-width: 160px;
height: 32px;
font-size: 11px;
font-weight: 500;
border-radius: 6px;
border: 1px solid #e5e7eb;
padding: 0 8px;
background: white;
color: #374151;
cursor: pointer;
}
.voice-select:focus {
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15);
outline: none;
}
/* Number Input */
.marker-number-input {
width: 50px !important;
height: 32px;
font-size: 13px;
font-weight: 600;
text-align: center;
border-radius: 0 6px 6px 0 !important;
border: 1px solid #e5e7eb;
border-left: none;
}
.marker-number-input:focus {
border-color: var(--accent-primary);
box-shadow: none;
outline: none;
}
.marker-number-group {
display: flex;
align-items: center;
}
.marker-number-group .input-group-text {
height: 32px;
padding: 0 10px;
font-size: 12px;
font-weight: 700;
background: #f9fafb;
border: 1px solid #e5e7eb;
border-right: none;
border-radius: 6px 0 0 6px;
color: #6b7280;
}
/* Action Buttons */
.toolbar-action-btn {
height: 32px;
padding: 0 14px;
font-size: 11px;
font-weight: 600;
border-radius: 6px;
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
transition: all 0.2s ease;
cursor: pointer;
border: none;
}
.toolbar-action-btn i {
font-size: 13px;
}
.toolbar-action-btn.btn-primary {
background: linear-gradient(135deg, var(--accent-primary) 0%, var(--pill-bg-gradient-end) 100%);
border: none;
color: white;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
}
.toolbar-action-btn.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.toolbar-action-btn.btn-danger {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
border: none;
color: white;
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
}
.toolbar-action-btn.btn-danger:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
}
.toolbar-action-btn.btn-outline-danger {
border: 1px solid #fca5a5;
color: #dc2626;
background: white;
}
.toolbar-action-btn.btn-outline-danger:hover {
background: #fef2f2;
border-color: #f87171;
}
/* Image Button */
.toolbar-btn.image-btn {
color: #059669;
border-color: #a7f3d0;
background: #ecfdf5;
}
.toolbar-btn.image-btn:hover {
background: #d1fae5;
border-color: #6ee7b7;
color: #047857;
}
/* TTS Button */
.toolbar-btn.tts-btn {
width: auto;
padding: 0 10px;
gap: 4px;
color: #7c3aed;
border-color: #ddd6fe;
background: #f5f3ff;
font-weight: 700;
font-size: 10px;
}
.toolbar-btn.tts-btn:hover {
background: #ede9fe;
border-color: #c4b5fd;
color: #6d28d9;
}
/* Divider */
.toolbar-divider {
width: 1px;
height: 24px;
background: #e5e7eb;
margin: 0 2px;
flex-shrink: 0;
} }
/* ============================================ /* ============================================
@@ -589,23 +846,23 @@ body {
Image Upload Styles for Section Markers Image Upload Styles for Section Markers
============================================= */ ============================================= */
.section-image-container { .section-image-container {
margin: 10px 0; margin: 12px 0 0 0;
padding: 10px; padding: 12px;
background: #f8f9fa; background: rgba(255, 255, 255, 0.6);
border-radius: 8px; border-radius: 10px;
border: 2px dashed #dee2e6; border: 2px dashed #d1d5db;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.section-image-container.drag-over { .section-image-container.drag-over {
border-color: #667eea; border-color: var(--accent-primary);
background: #f0f4ff; background: rgba(102, 126, 234, 0.05);
} }
.section-image-container.has-image { .section-image-container.has-image {
border-style: solid; border-style: solid;
border-color: #28a745; border-color: #10b981;
background: #f8fff8; background: rgba(16, 185, 129, 0.05);
} }
.image-drop-zone { .image-drop-zone {
@@ -613,24 +870,25 @@ body {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 80px; min-height: 70px;
cursor: pointer; cursor: pointer;
color: #6c757d; color: #9ca3af;
font-size: 14px; font-size: 13px;
transition: all 0.2s;
} }
.image-drop-zone i { .image-drop-zone i {
font-size: 24px; font-size: 22px;
margin-bottom: 8px; margin-bottom: 6px;
color: #adb5bd; color: #d1d5db;
} }
.image-drop-zone:hover { .image-drop-zone:hover {
color: #495057; color: #6b7280;
} }
.image-drop-zone:hover i { .image-drop-zone:hover i {
color: #667eea; color: var(--accent-primary);
} }
.image-preview-wrapper { .image-preview-wrapper {
@@ -681,8 +939,8 @@ body {
.image-info { .image-info {
font-size: 11px; font-size: 11px;
color: #6c757d; color: #6b7280;
margin-top: 5px; margin-top: 6px;
text-align: center; text-align: center;
} }
@@ -693,7 +951,7 @@ body {
/* Save Button Styles */ /* Save Button Styles */
.save-project-btn { .save-project-btn {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%); background: linear-gradient(135deg, #10b981 0%, #059669 100%);
border: none; border: none;
color: white; color: white;
font-weight: bold; font-weight: bold;
@@ -704,7 +962,7 @@ body {
.save-project-btn:hover { .save-project-btn:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.4); box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
color: white; color: white;
} }
@@ -758,3 +1016,111 @@ body {
.dropdown-divider { .dropdown-divider {
margin: 8px 0; margin: 8px 0;
} }
/* ============================================
Responsive Adjustments for Toolbar
============================================= */
@media (max-width: 1400px) {
.editor-marker .marker-controls {
gap: 8px;
}
.toolbar-group {
padding: 3px 8px;
}
.toolbar-group-label {
display: none;
}
.voice-select {
min-width: 120px;
max-width: 140px;
}
}
@media (max-width: 1200px) {
.toolbar-btn {
width: 30px;
height: 30px;
font-size: 13px;
}
.toolbar-action-btn {
height: 30px;
padding: 0 12px;
font-size: 10px;
}
.voice-select {
height: 30px;
font-size: 10px;
}
.marker-number-input {
height: 30px;
}
.marker-number-group .input-group-text {
height: 30px;
}
}
@media (max-width: 992px) {
.editor-marker .marker-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.editor-marker .marker-controls {
width: 100%;
justify-content: flex-start;
}
.section-marker {
margin-left: 10px;
}
}
@media (max-width: 768px) {
.toolbar-group {
padding: 3px 6px;
}
.toolbar-divider {
display: none;
}
.editor-marker .marker-controls {
gap: 6px;
}
.toolbar-action-btn span {
display: none;
}
.toolbar-action-btn {
padding: 0 10px;
}
.voice-select {
min-width: 100px;
max-width: 120px;
}
}
@media (max-width: 576px) {
.toolbar-group {
flex-wrap: wrap;
}
.editor-marker {
padding: 10px 12px;
margin: 15px 0;
}
.section-marker {
margin-left: 5px;
}
}

View File

@@ -134,7 +134,6 @@ window.insertSectionMarker = insertSectionMarker;
function createMarkerHTML(type, num, voice = 'af_alloy', markerId = null) { function createMarkerHTML(type, num, voice = 'af_alloy', markerId = null) {
const id = markerId || (Date.now() + '_' + Math.random().toString(36).substr(2, 9)); const id = markerId || (Date.now() + '_' + Math.random().toString(36).substr(2, 9));
const title = type.toUpperCase(); const title = type.toUpperCase();
const btnClass = type === 'chapter' ? 'btn-danger' : 'btn-primary';
const voiceOpts = VOICES.map(v => const voiceOpts = VOICES.map(v =>
`<option value="${v.val}" ${v.val === voice ? 'selected' : ''}>${v.label}</option>` `<option value="${v.val}" ${v.val === voice ? 'selected' : ''}>${v.label}</option>`
@@ -146,26 +145,71 @@ function createMarkerHTML(type, num, voice = 'af_alloy', markerId = null) {
if (type === 'section') { if (type === 'section') {
extraControls = ` extraControls = `
<div class="vr bg-secondary mx-2"></div> <div class="toolbar-divider"></div>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-dark dropdown-toggle" type="button" data-bs-toggle="dropdown" onmousedown="event.preventDefault()">Style</button> <!-- Formatting Group -->
<ul class="dropdown-menu"> <div class="toolbar-group">
<li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); formatBlock('p')">Normal</button></li> <span class="toolbar-group-label">Format</span>
<li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); formatBlock('h1')">Heading 1</button></li> <div class="dropdown">
<li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); formatBlock('h2')">Heading 2</button></li> <button class="toolbar-btn dropdown-toggle" type="button" data-bs-toggle="dropdown" onmousedown="event.preventDefault()" title="Text Style">
<li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); formatBlock('h3')">Heading 3</button></li> Style
</ul> </button>
<button class="btn btn-outline-dark" title="Bold" onmousedown="event.preventDefault(); applyFormat('bold')"><i class="bi bi-type-bold"></i></button> <ul class="dropdown-menu">
<button class="btn btn-outline-dark" title="Italic" onmousedown="event.preventDefault(); applyFormat('italic')"><i class="bi bi-type-italic"></i></button> <li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); formatBlock('p')"><i class="bi bi-text-paragraph me-2"></i>Normal</button></li>
<button class="btn btn-outline-dark" title="Normalize (select text first, or normalizes entire section)" onmousedown="event.preventDefault(); normalizeSection('${id}')"><i class="bi bi-eraser"></i></button> <li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); formatBlock('h1')"><i class="bi bi-type-h1 me-2"></i>Heading 1</button></li>
<div class="vr bg-secondary mx-2"></div> <li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); formatBlock('h2')"><i class="bi bi-type-h2 me-2"></i>Heading 2</button></li>
<button class="btn btn-outline-success" onclick="triggerImageUpload('${id}')" title="Add Image"> <li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); formatBlock('h3')"><i class="bi bi-type-h3 me-2"></i>Heading 3</button></li>
</ul>
</div>
<button class="toolbar-btn" title="Bold (Ctrl+B)" onmousedown="event.preventDefault(); applyFormat('bold')">
<i class="bi bi-type-bold"></i>
</button>
<button class="toolbar-btn" title="Italic (Ctrl+I)" onmousedown="event.preventDefault(); applyFormat('italic')">
<i class="bi bi-type-italic"></i>
</button>
</div>
<!-- Text Tools Group -->
<div class="toolbar-group">
<span class="toolbar-group-label">Tools</span>
<button class="toolbar-btn" title="Normalize - Clear formatting" onmousedown="event.preventDefault(); normalizeSection('${id}')">
<i class="bi bi-eraser"></i>
</button>
<div class="dropdown">
<button class="toolbar-btn dropdown-toggle" type="button" data-bs-toggle="dropdown" onmousedown="event.preventDefault()" title="Change Case">
Aa
</button>
<ul class="dropdown-menu">
<li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); changeCase('sentence')"><i class="bi bi-type me-2"></i>Sentence case</button></li>
<li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); changeCase('lower')"><i class="bi bi-alphabet me-2"></i>lower case</button></li>
<li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); changeCase('upper')"><i class="bi bi-alphabet-uppercase me-2"></i>UPPER CASE</button></li>
<li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); changeCase('capitalized')"><i class="bi bi-card-text me-2"></i>Capitalized Case</button></li>
<li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); changeCase('title')"><i class="bi bi-blockquote-left me-2"></i>Title Case</button></li>
</ul>
</div>
</div>
<!-- Media Group -->
<div class="toolbar-group">
<span class="toolbar-group-label">Media</span>
<button class="toolbar-btn image-btn" onclick="triggerImageUpload('${id}')" title="Add Image">
<i class="bi bi-image"></i> <i class="bi bi-image"></i>
</button> </button>
<button class="btn btn-outline-primary fw-bold" onclick="openTTSEditor('${id}')" title="Edit TTS Text"> <button class="toolbar-btn tts-btn" onclick="openTTSEditor('${id}')" title="Edit TTS Text">
<i class="bi bi-pencil-square"></i> TTS <i class="bi bi-pencil-square"></i> TTS
</button> </button>
</div> </div>
<div class="toolbar-divider"></div>
<!-- Action Buttons -->
<button class="toolbar-action-btn btn-primary" onclick="generateMarkerAudio('${id}')" title="Generate Audio">
<i class="bi bi-play-circle-fill"></i>
<span>Generate</span>
</button>
<button class="toolbar-action-btn btn-outline-danger" title="Remove Section" onclick="removeMarker('${id}')">
<i class="bi bi-trash3"></i>
</button>
`; `;
imageSection = ` imageSection = `
@@ -186,6 +230,18 @@ function createMarkerHTML(type, num, voice = 'af_alloy', markerId = null) {
</div> </div>
</div> </div>
`; `;
} else {
// Chapter marker - simpler controls
extraControls = `
<div class="toolbar-divider"></div>
<button class="toolbar-action-btn btn-danger" onclick="generateMarkerAudio('${id}')" title="Generate All Sections">
<i class="bi bi-play-circle-fill"></i>
<span>Generate All</span>
</button>
<button class="toolbar-action-btn btn-outline-danger" title="Remove Chapter" onclick="removeMarker('${id}')">
<i class="bi bi-trash3"></i>
</button>
`;
} }
return ` return `
@@ -193,21 +249,15 @@ function createMarkerHTML(type, num, voice = 'af_alloy', markerId = null) {
<div class="marker-header"> <div class="marker-header">
<span class="marker-title">${title}</span> <span class="marker-title">${title}</span>
<div class="marker-controls"> <div class="marker-controls">
<div class="input-group input-group-sm"> <!-- Number & Voice -->
<div class="marker-number-group input-group input-group-sm">
<span class="input-group-text">#</span> <span class="input-group-text">#</span>
<input type="number" class="form-control" value="${num}" style="width: 60px;" onchange="updateMarkerData('${id}', 'num', this.value)"> <input type="number" class="form-control marker-number-input" value="${num}" onchange="updateMarkerData('${id}', 'num', this.value)">
</div> </div>
<select class="form-select form-select-sm" style="width: 140px;" onchange="updateMarkerData('${id}', 'voice', this.value)"> <select class="form-select voice-select" onchange="updateMarkerData('${id}', 'voice', this.value)">
${voiceOpts} ${voiceOpts}
</select> </select>
${extraControls} ${extraControls}
<div class="vr bg-secondary mx-2"></div>
<button class="btn btn-sm ${btnClass} fw-bold" onclick="generateMarkerAudio('${id}')">
<i class="bi bi-play-circle me-1"></i> Generate ${type === 'chapter' ? 'All' : 'Audio'}
</button>
<button class="btn btn-sm btn-outline-danger ms-2" title="Remove Marker" onclick="removeMarker('${id}')">
<i class="bi bi-trash"></i>
</button>
</div> </div>
</div> </div>
${imageSection} ${imageSection}
@@ -573,6 +623,170 @@ function formatBlock(tag) {
// Make it globally available // Make it globally available
window.formatBlock = formatBlock; window.formatBlock = formatBlock;
// ==========================================
// CASE CHANGE FUNCTIONS
// ==========================================
/**
* Change the case of selected text
* @param {string} caseType - Type of case: 'sentence', 'lower', 'upper', 'capitalized', 'title'
*/
function changeCase(caseType) {
const selection = window.getSelection();
// Check if there's a valid selection with actual content
if (!selection || selection.isCollapsed || selection.toString().trim().length === 0) {
alert('Please select some text first to change its case.');
return;
}
const selectedText = selection.toString();
let transformedText = '';
switch (caseType) {
case 'sentence':
transformedText = toSentenceCase(selectedText);
break;
case 'lower':
transformedText = selectedText.toLowerCase();
break;
case 'upper':
transformedText = selectedText.toUpperCase();
break;
case 'capitalized':
transformedText = toCapitalizedCase(selectedText);
break;
case 'title':
transformedText = toTitleCase(selectedText);
break;
default:
transformedText = selectedText;
}
// Replace the selected text with transformed text
replaceSelectedText(selection, transformedText);
}
// Make it globally available
window.changeCase = changeCase;
/**
* Convert text to Sentence case
* First letter of each sentence is capitalized, rest is lowercase
* @param {string} text - Input text
* @returns {string} Transformed text
*/
function toSentenceCase(text) {
// First convert everything to lowercase
let result = text.toLowerCase();
// Capitalize the first letter
result = result.charAt(0).toUpperCase() + result.slice(1);
// Capitalize letter after sentence-ending punctuation (. ! ?)
result = result.replace(/([.!?]\s*)([a-z])/g, (match, punctuation, letter) => {
return punctuation + letter.toUpperCase();
});
// Also handle newlines as sentence breaks
result = result.replace(/(\n\s*)([a-z])/g, (match, newline, letter) => {
return newline + letter.toUpperCase();
});
return result;
}
/**
* Convert text to Capitalized Case
* First letter of every word is capitalized
* @param {string} text - Input text
* @returns {string} Transformed text
*/
function toCapitalizedCase(text) {
return text.toLowerCase().replace(/\b\w/g, (letter) => letter.toUpperCase());
}
/**
* Convert text to Title Case
* Like Capitalized Case but keeps small words (articles, prepositions, conjunctions) lowercase
* unless they're the first or last word
* @param {string} text - Input text
* @returns {string} Transformed text
*/
function toTitleCase(text) {
// Words that should remain lowercase (unless first or last)
const smallWords = [
'a', 'an', 'and', 'as', 'at', 'but', 'by', 'for', 'in', 'nor',
'of', 'on', 'or', 'so', 'the', 'to', 'up', 'yet', 'is', 'be',
'with', 'from', 'into', 'over', 'after', 'under', 'above'
];
const words = text.toLowerCase().split(/(\s+)/); // Split but keep whitespace
let isFirstWord = true;
let result = [];
for (let i = 0; i < words.length; i++) {
const word = words[i];
// If it's whitespace, just add it
if (/^\s+$/.test(word)) {
result.push(word);
continue;
}
// Check if this is the last actual word
let isLastWord = true;
for (let j = i + 1; j < words.length; j++) {
if (!/^\s+$/.test(words[j])) {
isLastWord = false;
break;
}
}
// Capitalize if it's first word, last word, or not a small word
if (isFirstWord || isLastWord || !smallWords.includes(word.toLowerCase())) {
result.push(word.charAt(0).toUpperCase() + word.slice(1));
} else {
result.push(word);
}
isFirstWord = false;
}
return result.join('');
}
/**
* Replace selected text with new text while preserving cursor position
* @param {Selection} selection - Current selection
* @param {string} newText - Text to insert
*/
function replaceSelectedText(selection, newText) {
if (!selection.rangeCount) return;
const range = selection.getRangeAt(0);
// Delete the selected content
range.deleteContents();
// Create a text node with the new text
const textNode = document.createTextNode(newText);
// Insert the new text
range.insertNode(textNode);
// Move cursor to end of inserted text
range.setStartAfter(textNode);
range.setEndAfter(textNode);
selection.removeAllRanges();
selection.addRange(range);
}
// ==========================================
// NORMALIZE FUNCTIONS
// ==========================================
/** /**
* Normalize section text (clean up formatting) * Normalize section text (clean up formatting)
* If text is selected, only normalize the selection * If text is selected, only normalize the selection