Fix normalize button to only normalize selected text
This commit is contained in:
@@ -157,7 +157,7 @@ function createMarkerHTML(type, num, voice = 'af_alloy', markerId = null) {
|
||||
</ul>
|
||||
<button class="btn btn-outline-dark" title="Bold" onmousedown="event.preventDefault(); applyFormat('bold')"><i class="bi bi-type-bold"></i></button>
|
||||
<button class="btn btn-outline-dark" title="Italic" onmousedown="event.preventDefault(); applyFormat('italic')"><i class="bi bi-type-italic"></i></button>
|
||||
<button class="btn btn-outline-dark" title="Normalize" onmousedown="event.preventDefault(); normalizeSection('${id}')"><i class="bi bi-eraser"></i></button>
|
||||
<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>
|
||||
<div class="vr bg-secondary mx-2"></div>
|
||||
<button class="btn btn-outline-success" onclick="triggerImageUpload('${id}')" title="Add Image">
|
||||
<i class="bi bi-image"></i>
|
||||
@@ -575,9 +575,307 @@ window.formatBlock = formatBlock;
|
||||
|
||||
/**
|
||||
* Normalize section text (clean up formatting)
|
||||
* If text is selected, only normalize the selection
|
||||
* If no text is selected, normalize the entire section
|
||||
* @param {string} markerId - Marker ID
|
||||
*/
|
||||
function normalizeSection(markerId) {
|
||||
const selection = window.getSelection();
|
||||
|
||||
// Check if there's a valid selection with actual content
|
||||
if (selection && !selection.isCollapsed && selection.toString().trim().length > 0) {
|
||||
// Normalize only the selected text
|
||||
normalizeSelection(selection, markerId);
|
||||
} else {
|
||||
// No selection - normalize entire section
|
||||
normalizeEntireSection(markerId);
|
||||
}
|
||||
}
|
||||
|
||||
// Make it globally available
|
||||
window.normalizeSection = normalizeSection;
|
||||
|
||||
/**
|
||||
* Normalize only the selected text - removes all formatting
|
||||
* @param {Selection} selection - Current selection
|
||||
* @param {string} markerId - Marker ID for context
|
||||
*/
|
||||
function normalizeSelection(selection, markerId) {
|
||||
if (!selection.rangeCount) return;
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
|
||||
// Get the selected text content (plain text, no formatting)
|
||||
const selectedText = selection.toString();
|
||||
|
||||
// Clean the text: remove extra whitespace, normalize line breaks
|
||||
const cleanText = selectedText
|
||||
.replace(/[\r\n]+/g, ' ') // Replace line breaks with spaces
|
||||
.replace(/\s+/g, ' ') // Collapse multiple spaces
|
||||
.trim();
|
||||
|
||||
if (!cleanText) return;
|
||||
|
||||
// Get the start and end block elements
|
||||
const startContainer = range.startContainer;
|
||||
const endContainer = range.endContainer;
|
||||
const startBlock = getParentBlock(startContainer);
|
||||
const endBlock = getParentBlock(endContainer);
|
||||
|
||||
// Check if selection is entirely within a single formatting block (h1, h2, h3, etc.)
|
||||
if (startBlock && startBlock === endBlock && isFormattingBlock(startBlock)) {
|
||||
const blockText = startBlock.textContent.trim();
|
||||
const selectedTrimmed = cleanText;
|
||||
|
||||
// If we're selecting most or all of the block content, replace the entire block
|
||||
if (blockText === selectedTrimmed || selectedTrimmed.length >= blockText.length * 0.8) {
|
||||
// Replace the entire block with a paragraph
|
||||
const p = document.createElement('p');
|
||||
p.textContent = cleanText;
|
||||
startBlock.parentNode.replaceChild(p, startBlock);
|
||||
|
||||
// Set cursor at end of new paragraph
|
||||
const newRange = document.createRange();
|
||||
newRange.selectNodeContents(p);
|
||||
newRange.collapse(false);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(newRange);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if selection spans multiple blocks
|
||||
if (startBlock !== endBlock) {
|
||||
// Handle multi-block selection
|
||||
normalizeMultiBlockSelection(range, selection, cleanText);
|
||||
return;
|
||||
}
|
||||
|
||||
// For inline formatting (bold, italic, etc.) within a single block
|
||||
// First, try using execCommand to remove formatting
|
||||
try {
|
||||
// Store the clean text before manipulation
|
||||
const textToInsert = cleanText;
|
||||
|
||||
// Remove inline formatting using execCommand
|
||||
document.execCommand('removeFormat', false, null);
|
||||
|
||||
// After removeFormat, check if we need to handle any remaining issues
|
||||
const newSelection = window.getSelection();
|
||||
if (newSelection.rangeCount > 0) {
|
||||
const newRange = newSelection.getRangeAt(0);
|
||||
const currentText = newSelection.toString();
|
||||
|
||||
// If the text is still the same (formatting was removed), we're done
|
||||
// But if we're inside a heading, we need to convert it
|
||||
const parentBlock = getParentBlock(newRange.commonAncestorContainer);
|
||||
if (parentBlock && isFormattingBlock(parentBlock)) {
|
||||
// Convert the heading to a paragraph
|
||||
const p = document.createElement('p');
|
||||
p.innerHTML = parentBlock.innerHTML;
|
||||
parentBlock.parentNode.replaceChild(p, parentBlock);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('execCommand failed, using manual approach:', e);
|
||||
manualNormalizeSelection(range, selection, cleanText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle normalization when selection spans multiple blocks
|
||||
* @param {Range} range - Selection range
|
||||
* @param {Selection} selection - Current selection
|
||||
* @param {string} cleanText - Cleaned text to insert
|
||||
*/
|
||||
function normalizeMultiBlockSelection(range, selection, cleanText) {
|
||||
const startBlock = getParentBlock(range.startContainer);
|
||||
const endBlock = getParentBlock(range.endContainer);
|
||||
const editor = document.getElementById('bulk-editor');
|
||||
|
||||
if (!startBlock || !endBlock || !editor) {
|
||||
manualNormalizeSelection(range, selection, cleanText);
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect all blocks between start and end
|
||||
const blocksToRemove = [];
|
||||
let currentBlock = startBlock.nextElementSibling;
|
||||
|
||||
while (currentBlock && currentBlock !== endBlock) {
|
||||
// Don't remove editor markers
|
||||
if (!currentBlock.classList || !currentBlock.classList.contains('editor-marker')) {
|
||||
blocksToRemove.push(currentBlock);
|
||||
}
|
||||
currentBlock = currentBlock.nextElementSibling;
|
||||
}
|
||||
|
||||
// Create a new paragraph with the clean text
|
||||
const p = document.createElement('p');
|
||||
p.textContent = cleanText;
|
||||
|
||||
// Replace the start block with the new paragraph
|
||||
startBlock.parentNode.replaceChild(p, startBlock);
|
||||
|
||||
// Remove intermediate blocks
|
||||
blocksToRemove.forEach(block => block.remove());
|
||||
|
||||
// Remove the end block if it's different from start
|
||||
if (endBlock && endBlock !== startBlock && endBlock.parentNode) {
|
||||
endBlock.remove();
|
||||
}
|
||||
|
||||
// Set cursor at end of new paragraph
|
||||
const newRange = document.createRange();
|
||||
newRange.selectNodeContents(p);
|
||||
newRange.collapse(false);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(newRange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual fallback for normalizing selection
|
||||
* @param {Range} range - Selection range
|
||||
* @param {Selection} selection - Current selection
|
||||
* @param {string} cleanText - Cleaned text to insert
|
||||
*/
|
||||
function manualNormalizeSelection(range, selection, cleanText) {
|
||||
const startBlock = getParentBlock(range.startContainer);
|
||||
|
||||
// Delete the selected content
|
||||
range.deleteContents();
|
||||
|
||||
// If we're in a formatting block that's now empty or nearly empty, replace it
|
||||
if (startBlock && isFormattingBlock(startBlock)) {
|
||||
const remainingText = startBlock.textContent.trim();
|
||||
if (remainingText.length === 0) {
|
||||
// Block is empty, replace with paragraph containing clean text
|
||||
const p = document.createElement('p');
|
||||
p.textContent = cleanText;
|
||||
startBlock.parentNode.replaceChild(p, startBlock);
|
||||
|
||||
// Set cursor at end
|
||||
const newRange = document.createRange();
|
||||
newRange.selectNodeContents(p);
|
||||
newRange.collapse(false);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(newRange);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert clean text node
|
||||
const textNode = document.createTextNode(cleanText);
|
||||
range.insertNode(textNode);
|
||||
|
||||
// Clean up any empty formatting elements
|
||||
const commonAncestor = range.commonAncestorContainer;
|
||||
cleanEmptyFormattingElements(commonAncestor);
|
||||
|
||||
// Also clean parent elements
|
||||
if (commonAncestor.parentNode) {
|
||||
cleanEmptyFormattingElements(commonAncestor.parentNode);
|
||||
}
|
||||
|
||||
// Move cursor to end of inserted text
|
||||
const newRange = document.createRange();
|
||||
newRange.setStartAfter(textNode);
|
||||
newRange.setEndAfter(textNode);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(newRange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent block element of a node
|
||||
* @param {Node} node - The node to find parent block for
|
||||
* @returns {Element|null} The parent block element or null
|
||||
*/
|
||||
function getParentBlock(node) {
|
||||
const blockTags = ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'DIV', 'LI', 'BLOCKQUOTE'];
|
||||
let current = node;
|
||||
|
||||
// If it's a text node, start with parent
|
||||
while (current && current.nodeType !== 1) {
|
||||
current = current.parentNode;
|
||||
}
|
||||
|
||||
while (current) {
|
||||
if (current.nodeType === 1 && blockTags.includes(current.tagName)) {
|
||||
return current;
|
||||
}
|
||||
// Stop at the editor boundary
|
||||
if (current.id === 'bulk-editor') {
|
||||
return null;
|
||||
}
|
||||
current = current.parentNode;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an element is a formatting block (heading, etc.)
|
||||
* @param {Element} element - Element to check
|
||||
* @returns {boolean} True if it's a formatting block
|
||||
*/
|
||||
function isFormattingBlock(element) {
|
||||
if (!element || element.nodeType !== 1) return false;
|
||||
const formattingTags = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE'];
|
||||
return formattingTags.includes(element.tagName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up empty formatting elements after deletion
|
||||
* @param {Node} container - Container to clean
|
||||
*/
|
||||
function cleanEmptyFormattingElements(container) {
|
||||
if (!container || container.nodeType !== 1) return;
|
||||
|
||||
const formattingTags = ['B', 'STRONG', 'I', 'EM', 'U', 'STRIKE', 'S', 'SPAN', 'FONT'];
|
||||
|
||||
// First pass: clean direct children
|
||||
formattingTags.forEach(tag => {
|
||||
let elements;
|
||||
try {
|
||||
elements = container.querySelectorAll ? container.querySelectorAll(tag) : [];
|
||||
} catch (e) {
|
||||
elements = [];
|
||||
}
|
||||
|
||||
elements.forEach(el => {
|
||||
// Remove if empty or contains only whitespace
|
||||
if (!el.textContent.trim()) {
|
||||
el.remove();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Second pass: unwrap formatting elements that contain only text (to flatten the structure)
|
||||
formattingTags.forEach(tag => {
|
||||
let elements;
|
||||
try {
|
||||
elements = container.getElementsByTagName ? container.getElementsByTagName(tag) : [];
|
||||
} catch (e) {
|
||||
elements = [];
|
||||
}
|
||||
|
||||
// Convert to array since we'll be modifying the DOM
|
||||
Array.from(elements).forEach(el => {
|
||||
// If this element only contains a text node, unwrap it
|
||||
if (el.childNodes.length === 1 && el.childNodes[0].nodeType === 3) {
|
||||
const textContent = el.textContent;
|
||||
const textNode = document.createTextNode(textContent);
|
||||
el.parentNode.replaceChild(textNode, el);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the entire section after a marker
|
||||
* @param {string} markerId - Marker ID
|
||||
*/
|
||||
function normalizeEntireSection(markerId) {
|
||||
const marker = document.getElementById(`marker-${markerId}`);
|
||||
if (!marker) return;
|
||||
|
||||
@@ -600,9 +898,6 @@ function normalizeSection(markerId) {
|
||||
nodesToRemove.forEach(n => n.remove());
|
||||
}
|
||||
|
||||
// Make it globally available
|
||||
window.normalizeSection = normalizeSection;
|
||||
|
||||
/**
|
||||
* Insert HTML at cursor position
|
||||
* @param {string} html - HTML string to insert
|
||||
|
||||
Reference in New Issue
Block a user