UNPKG

@jager-ai/holy-editor

Version:

Rich text editor with Bible verse slash commands and PWA keyboard tracking, extracted from Holy Habit project

379 lines 13.2 kB
"use strict"; /** * Text Formatter * * Text formatting engine for rich text editing * Extracted from Holy Habit holy-editor-pro.js */ Object.defineProperty(exports, "__esModule", { value: true }); exports.TextFormatter = void 0; class TextFormatter { constructor(editorId = 'holyEditor') { this.editorId = editorId; } /** * Get current selection information */ getCurrentSelection() { const selection = window.getSelection(); if (!selection || !selection.rangeCount) return null; const range = selection.getRangeAt(0); return { range, selectedText: range.toString(), hasSelection: !range.collapsed }; } /** * Toggle bold formatting */ toggleBold() { const selectionInfo = this.getCurrentSelection(); if (!selectionInfo) return; const { range, selectedText, hasSelection } = selectionInfo; if (hasSelection) { const isBold = this.isCurrentlyBold(range); if (isBold) { this.removeBoldFromSelection(range); } else { this.applyBoldToSelection(range, selectedText); } } else { this.setNextInputStyle('bold'); } } /** * Toggle underline formatting */ toggleUnderline() { const selectionInfo = this.getCurrentSelection(); if (!selectionInfo) return; const { range, selectedText, hasSelection } = selectionInfo; if (hasSelection) { const isUnderlined = this.isCurrentlyUnderlined(range); if (isUnderlined) { this.removeUnderlineFromSelection(range); } else { this.applyUnderlineToSelection(range, selectedText); } } else { this.setNextInputStyle('underline'); } } /** * Toggle heading1 formatting */ toggleHeading1() { const selectionInfo = this.getCurrentSelection(); if (!selectionInfo) return; const { range, selectedText, hasSelection } = selectionInfo; if (hasSelection) { const isHeading = this.isCurrentlyHeading(range); if (isHeading) { this.removeHeadingFromSelection(range); } else { this.applyHeading1ToSelection(range, selectedText); } } else { this.setNextInputStyle('heading1'); } } /** * Toggle quote formatting */ toggleQuote() { const selectionInfo = this.getCurrentSelection(); if (!selectionInfo) return; const { range, selectedText, hasSelection } = selectionInfo; if (hasSelection) { const isQuote = this.isCurrentlyQuote(range); if (isQuote) { this.removeQuoteFromSelection(range); } else { this.applyQuoteToSelection(range, selectedText); } } else { this.createEmptyInlineQuote(); } } /** * Apply text color */ applyTextColor(color) { const selectionInfo = this.getCurrentSelection(); if (!selectionInfo) return; const { range, selectedText, hasSelection } = selectionInfo; if (hasSelection) { this.applyColorToSelection(range, selectedText, color); } else { this.setNextInputStyle('color', color); } } /** * Toggle any formatting style */ toggleStyle(action) { switch (action) { case 'bold': this.toggleBold(); break; case 'underline': this.toggleUnderline(); break; case 'heading1': this.toggleHeading1(); break; case 'quote': this.toggleQuote(); break; default: console.warn('❌ Unsupported format action:', action); } } /** * Check current format state */ getFormatState() { const selectionInfo = this.getCurrentSelection(); if (!selectionInfo) { return { bold: false, underline: false, heading1: false, quote: false, textcolor: false }; } const { range } = selectionInfo; return { bold: this.isCurrentlyBold(range), underline: this.isCurrentlyUnderlined(range), heading1: this.isCurrentlyHeading(range), quote: this.isCurrentlyQuote(range), textcolor: this.isCurrentlyTextColor(range) }; } // Private methods for format detection isCurrentlyBold(range) { return this.hasFormatTag(range, (node) => node.tagName === 'STRONG' || node.tagName === 'B'); } isCurrentlyUnderlined(range) { return this.hasFormatTag(range, (node) => node.tagName === 'U'); } isCurrentlyHeading(range) { return this.hasFormatTag(range, (node) => node.tagName === 'SPAN' && node.style.fontSize === '20px' && node.style.fontWeight === 'bold'); } isCurrentlyQuote(range) { return this.hasFormatTag(range, (node) => node.tagName === 'BLOCKQUOTE' && node.classList && node.classList.contains('inline-quote')); } isCurrentlyTextColor(range) { return this.hasFormatTag(range, (node) => node.style && node.style.color && node.style.color !== '' && node.style.color !== 'inherit'); } hasFormatTag(range, checkFunction) { const container = range.commonAncestorContainer; let parent = container.nodeType === Node.TEXT_NODE ? container.parentNode : container; while (parent && parent !== document.getElementById(this.editorId)) { if (parent instanceof HTMLElement && checkFunction(parent)) { return true; } parent = parent.parentNode; } return false; } // Private methods for applying formats applyBoldToSelection(range, selectedText) { const strong = document.createElement('strong'); strong.textContent = selectedText; range.deleteContents(); range.insertNode(strong); this.selectNode(range, strong); console.log('💪 Bold formatting applied'); } applyUnderlineToSelection(range, selectedText) { const u = document.createElement('u'); u.textContent = selectedText; range.deleteContents(); range.insertNode(u); this.selectNode(range, u); console.log('✏️ Underline formatting applied'); } applyHeading1ToSelection(range, selectedText) { const span = document.createElement('span'); span.style.fontSize = '20px'; span.style.fontWeight = 'bold'; span.textContent = selectedText; range.deleteContents(); range.insertNode(span); this.selectNode(range, span); console.log('📝 Heading1 formatting applied'); } applyQuoteToSelection(range, selectedText) { const quote = document.createElement('blockquote'); quote.className = 'inline-quote'; quote.textContent = selectedText; range.deleteContents(); range.insertNode(quote); // Move cursor after quote range.setStartAfter(quote); range.collapse(true); this.updateSelection(range); console.log('💬 Quote formatting applied'); } applyColorToSelection(range, selectedText, color) { const colorSpan = document.createElement('span'); colorSpan.style.color = color; try { range.surroundContents(colorSpan); } catch (e) { // Fallback for complex selections const contents = range.extractContents(); colorSpan.appendChild(contents); range.insertNode(colorSpan); } this.selectNode(range, colorSpan); console.log('🎨 Color formatting applied:', color); } // Private methods for removing formats removeBoldFromSelection(range) { this.removeFormatFromSelection(range, (node) => node.tagName === 'STRONG' || node.tagName === 'B'); console.log('💪 Bold formatting removed'); } removeUnderlineFromSelection(range) { this.removeFormatFromSelection(range, (node) => node.tagName === 'U'); console.log('✏️ Underline formatting removed'); } removeHeadingFromSelection(range) { this.removeFormatFromSelection(range, (node) => node.tagName === 'SPAN' && node.style.fontSize === '20px' && node.style.fontWeight === 'bold'); console.log('📝 Heading formatting removed'); } removeQuoteFromSelection(range) { this.removeFormatFromSelection(range, (node) => node.tagName === 'BLOCKQUOTE' && node.classList && node.classList.contains('inline-quote')); console.log('💬 Quote formatting removed'); } removeFormatFromSelection(range, checkFunction) { const container = range.commonAncestorContainer; let parent = container.nodeType === Node.TEXT_NODE ? container.parentNode : container; while (parent && parent !== document.getElementById(this.editorId)) { if (parent instanceof HTMLElement && checkFunction(parent)) { const fragment = this.extractNodeContents(parent); parent.parentNode?.insertBefore(fragment, parent); parent.parentNode?.removeChild(parent); this.restoreSelectionFromFragment(range, fragment, parent.parentNode); break; } parent = parent.parentNode; } } // Utility methods selectNode(range, node) { range.selectNodeContents(node); const selection = window.getSelection(); selection?.removeAllRanges(); selection?.addRange(range); } updateSelection(range) { const selection = window.getSelection(); selection?.removeAllRanges(); selection?.addRange(range); } extractNodeContents(node) { const fragment = document.createDocumentFragment(); while (node.firstChild) { fragment.appendChild(node.firstChild); } return fragment; } restoreSelectionFromFragment(range, fragment, parentNode) { const newRange = document.createRange(); const startNode = fragment.firstChild || parentNode; const endNode = fragment.lastChild || parentNode; if (startNode && endNode && parentNode) { const endOffset = endNode.textContent ? endNode.textContent.length : 0; newRange.setStart(startNode, 0); newRange.setEnd(endNode, endOffset); this.updateSelection(newRange); } } createEmptyInlineQuote() { const selectionInfo = this.getCurrentSelection(); if (!selectionInfo) return; const { range } = selectionInfo; const quote = document.createElement('blockquote'); quote.className = 'inline-quote'; quote.innerHTML = '​'; // Zero-width space range.insertNode(quote); // Move cursor inside quote range.selectNodeContents(quote); range.collapse(false); this.updateSelection(range); console.log('💬 Empty quote block created'); } setNextInputStyle(style, value) { const selectionInfo = this.getCurrentSelection(); if (!selectionInfo) return; const { range } = selectionInfo; let marker; switch (style) { case 'bold': marker = document.createElement('strong'); break; case 'underline': marker = document.createElement('u'); break; case 'heading1': marker = document.createElement('span'); marker.style.fontSize = '20px'; marker.style.fontWeight = 'bold'; break; case 'color': marker = document.createElement('span'); if (value) { marker.style.color = value; } break; default: return; } marker.innerHTML = '​'; // Zero-width space range.insertNode(marker); range.setStartAfter(marker); range.collapse(true); this.updateSelection(range); // Move cursor inside marker range.selectNodeContents(marker); range.collapse(false); this.updateSelection(range); console.log('🎯 Next input style set:', style); } } exports.TextFormatter = TextFormatter; //# sourceMappingURL=TextFormatter.js.map