UNPKG

@jager-ai/holy-editor

Version:

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

712 lines β€’ 26 kB
"use strict"; /** * Holy Editor * * Main editor class that integrates all components * Extracted from Holy Habit holy-editor-pro.js */ Object.defineProperty(exports, "__esModule", { value: true }); exports.HolyEditor = void 0; const Editor_1 = require("./types/Editor"); const BibleVerseEngine_1 = require("./core/BibleVerseEngine"); const TextFormatter_1 = require("./core/TextFormatter"); const PWAKeyboardTracker_1 = require("./pwa/PWAKeyboardTracker"); const ToastManager_1 = require("./ui/ToastManager"); const ColorPicker_1 = require("./ui/ColorPicker"); const AutoSaveManager_1 = require("./utils/AutoSaveManager"); class HolyEditor { constructor(editorId, config) { this.isInitialized = false; this.autoSaveManager = null; // Event listeners for cleanup this.eventListeners = []; // State management this.isProcessingSlashCommand = false; this.lastInputTime = 0; this.inputDebounceMs = 300; const editorElement = document.getElementById(editorId); if (!editorElement) { throw new Editor_1.EditorError(`Editor element with id "${editorId}" not found`); } this.editorElement = editorElement; this.config = { enableBibleVerses: true, enableTextFormatting: true, enablePWAKeyboard: true, enableColorPicker: true, enableAutoSave: true, apiEndpoint: '/api/bible_verse_full.php', debounceMs: 300, autoSaveInterval: 30000, autoSaveKey: undefined, keyboardSettings: { threshold: 10, keyboardMin: 150, debounceTime: 0 }, ...config }; // Initialize components this.initializeComponents(); console.log('πŸ“ HolyEditor created for element:', editorId); } /** * Initialize all editor components */ initializeComponents() { try { // Initialize Bible verse engine if (this.config.enableBibleVerses) { this.bibleEngine = BibleVerseEngine_1.BibleVerseEngine.getInstance(this.config.apiEndpoint, this.config.debounceMs); } // Initialize text formatter if (this.config.enableTextFormatting) { this.textFormatter = new TextFormatter_1.TextFormatter(this.editorElement.id); } // Initialize PWA keyboard tracker if (this.config.enablePWAKeyboard) { this.keyboardTracker = new PWAKeyboardTracker_1.PWAKeyboardTracker(this.config.keyboardSettings); } // Initialize UI components this.toastManager = ToastManager_1.ToastManager.getInstance(); if (this.config.enableColorPicker) { this.colorPicker = ColorPicker_1.ColorPicker.getInstance(); } // Initialize auto-save manager if (this.config.enableAutoSave) { this.autoSaveManager = new AutoSaveManager_1.AutoSaveManager(this.editorElement.id, { interval: this.config.autoSaveInterval, key: this.config.autoSaveKey, onSave: (data) => { this.toastManager.success('λ‚΄μš©μ΄ μžλ™ μ €μž₯λ˜μ—ˆμŠ΅λ‹ˆλ‹€', 2000); }, onError: (error) => { console.error('❌ Auto-save error:', error); this.toastManager.error('μžλ™ μ €μž₯ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€'); } }); } console.log('πŸ”§ HolyEditor components initialized'); } catch (error) { console.error('❌ Failed to initialize components:', error); throw new Editor_1.EditorError('Component initialization failed', 'INIT_ERROR', error); } } /** * Initialize the editor */ initialize() { if (this.isInitialized) { console.warn('⚠️ HolyEditor already initialized'); return; } try { this.setupEditor(); this.setupEventListeners(); this.initializePWAFeatures(); this.initializeAutoSave(); this.isInitialized = true; console.log('βœ… HolyEditor initialized successfully'); this.toastManager.success('에디터가 μ€€λΉ„λ˜μ—ˆμŠ΅λ‹ˆλ‹€'); } catch (error) { console.error('❌ HolyEditor initialization failed:', error); throw new Editor_1.EditorError('Editor initialization failed', 'INIT_ERROR', error); } } /** * Setup editor element */ setupEditor() { // Make contenteditable if not already if (!this.editorElement.hasAttribute('contenteditable')) { this.editorElement.setAttribute('contenteditable', 'true'); } // Add editor class for styling this.editorElement.classList.add('holy-editor'); // Set placeholder if empty if (!this.editorElement.textContent?.trim()) { this.editorElement.innerHTML = '<p>μ„±κ²½ κ΅¬μ ˆμ„ μž…λ ₯ν•˜λ €λ©΄ /갈2:20 같은 ν˜•μ‹μœΌλ‘œ μž…λ ₯ν•˜μ„Έμš”...</p>'; } // Ensure editor has focus capabilities if (!this.editorElement.hasAttribute('tabindex')) { this.editorElement.setAttribute('tabindex', '0'); } } /** * Setup all event listeners */ setupEventListeners() { // Input events for slash command detection this.addEventListener(this.editorElement, 'input', this.handleInput.bind(this)); this.addEventListener(this.editorElement, 'keydown', this.handleKeyDown.bind(this)); this.addEventListener(this.editorElement, 'paste', this.handlePaste.bind(this)); // Focus events for PWA keyboard tracking this.addEventListener(this.editorElement, 'focus', this.handleFocus.bind(this)); this.addEventListener(this.editorElement, 'blur', this.handleBlur.bind(this)); // Selection change for formatting state this.addEventListener(document, 'selectionchange', this.handleSelectionChange.bind(this)); console.log('🎯 Event listeners setup complete'); } /** * Initialize PWA features */ initializePWAFeatures() { if (this.config.enablePWAKeyboard && this.keyboardTracker) { this.keyboardTracker.initialize(this.editorElement); } } /** * Initialize auto-save features */ initializeAutoSave() { if (!this.autoSaveManager || !this.config.enableAutoSave) return; // Check for saved content const savedContent = this.autoSaveManager.restore(); if (savedContent && savedContent.trim()) { // Ask user if they want to restore const saveInfo = this.autoSaveManager.getSaveInfo(); if (saveInfo) { const ageMinutes = Math.floor((Date.now() - saveInfo.timestamp) / 60000); const shouldRestore = confirm(`이전에 μž‘μ„±ν•˜λ˜ λ‚΄μš©μ΄ μžˆμŠ΅λ‹ˆλ‹€ (${ageMinutes}λΆ„ μ „).\nλ³΅μ›ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?`); if (shouldRestore) { this.setContent(savedContent); this.toastManager.success('μ €μž₯된 λ‚΄μš©μ„ λ³΅μ›ν–ˆμŠ΅λ‹ˆλ‹€'); } else { // Clear the saved content if user declines this.autoSaveManager.clear(); } } } // Start auto-saving this.autoSaveManager.start(() => this.getContent()); } /** * Handle input events */ handleInput(event) { if (this.isProcessingSlashCommand) return; const now = Date.now(); this.lastInputTime = now; // Debounced slash command processing setTimeout(() => { if (this.lastInputTime === now && this.config.enableBibleVerses) { this.processSlashCommands(); } }, this.inputDebounceMs); // Trigger auto-save on input (auto-save manager handles its own debouncing) if (this.autoSaveManager && this.autoSaveManager.isRunning()) { // Content will be saved at the next interval } } /** * Handle keydown events */ handleKeyDown(event) { // Handle formatting shortcuts if (event.ctrlKey || event.metaKey) { switch (event.key.toLowerCase()) { case 'b': event.preventDefault(); this.toggleFormat('bold'); break; case 'u': event.preventDefault(); this.toggleFormat('underline'); break; case 'h': event.preventDefault(); this.toggleFormat('heading1'); break; case 'q': event.preventDefault(); this.toggleFormat('quote'); break; } } // Handle Enter key in quotes if (event.key === 'Enter') { const selection = window.getSelection(); if (selection && selection.anchorNode) { const quoteParent = this.findParentQuote(selection.anchorNode); if (quoteParent) { event.preventDefault(); this.handleEnterInQuote(); } } } } /** * Handle paste events */ handlePaste(event) { event.preventDefault(); const text = event.clipboardData?.getData('text/plain') || ''; if (text) { // Insert as plain text document.execCommand('insertText', false, text); // Process any slash commands in pasted text setTimeout(() => { if (this.config.enableBibleVerses) { this.processSlashCommands(); } }, 100); } } /** * Handle focus events */ handleFocus() { console.log('πŸ“ Editor focused'); this.editorElement.classList.add('holy-editor-focused'); } /** * Handle blur events */ handleBlur() { console.log('πŸ“ Editor blurred'); this.editorElement.classList.remove('holy-editor-focused'); } /** * Handle selection change */ handleSelectionChange() { // Update formatting state indicators if needed if (this.config.enableTextFormatting && this.textFormatter) { const formatState = this.textFormatter.getFormatState(); this.updateFormatButtons(formatState); } } /** * Process slash commands in editor content */ async processSlashCommands() { if (!this.bibleEngine || this.isProcessingSlashCommand) return; this.isProcessingSlashCommand = true; try { const content = this.editorElement.textContent || ''; const matches = this.bibleEngine.parseSlashCommands(content); for (const match of matches) { await this.processSlashCommand(match); } } catch (error) { console.error('❌ Error processing slash commands:', error); if (error instanceof Editor_1.BibleApiError) { this.toastManager.handleApiError(error); } else { this.toastManager.error('μŠ¬λž˜μ‹œ λͺ…λ Ήμ–΄ 처리 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€'); } } finally { this.isProcessingSlashCommand = false; } } /** * Process individual slash command */ async processSlashCommand(match) { const { ref, position, fullMatch } = match; console.log('πŸ“– Processing slash command:', ref); // Show loading toast const hideLoading = this.toastManager.showLoading(`${ref} ꡬ절 검색 쀑...`); try { const verseData = await this.bibleEngine.loadVerse(ref); hideLoading(); if (verseData) { this.insertVerseAtPosition(verseData, position, fullMatch.length); this.bibleEngine.updateInsertionState(ref); this.toastManager.success(`${ref} ꡬ절이 μ‚½μž…λ˜μ—ˆμŠ΅λ‹ˆλ‹€`); } else { this.toastManager.error(`${ref} κ΅¬μ ˆμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€`); } } catch (error) { hideLoading(); console.error('❌ Failed to load verse:', ref, error); if (error instanceof Editor_1.BibleApiError) { this.toastManager.handleApiError(error, ref); } else { this.toastManager.error(`${ref} ꡬ절 λ‘œλ“œ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€`); } } } /** * Insert verse at specific position */ insertVerseAtPosition(verseData, position, commandLength) { const content = this.editorElement.textContent || ''; const beforeText = content.substring(0, position); const afterText = content.substring(position + commandLength); // Create verse HTML const verseHtml = this.createVerseHtml(verseData); // Replace content const newContent = beforeText + verseHtml + afterText; this.editorElement.innerHTML = newContent; // Position cursor after inserted verse this.positionCursorAfterVerse(position + verseHtml.length); } /** * Create HTML for verse display */ createVerseHtml(verseData) { if (verseData.isRange && verseData.verses.length > 1) { // Multiple verses (range) const versesHtml = verseData.verses.map(verse => `<span class="verse-text">${verse.text}</span>`).join(' '); const firstVerse = verseData.verses[0]; const lastVerse = verseData.verses[verseData.verses.length - 1]; const reference = `${firstVerse.book} ${firstVerse.chapter}:${firstVerse.verse}-${lastVerse.verse}`; return `<blockquote class="bible-verse-range" data-reference="${reference}"> ${versesHtml} <cite class="verse-reference">${reference}</cite> </blockquote>`; } else { // Single verse const verse = verseData.verses[0]; const reference = `${verse.book} ${verse.chapter}:${verse.verse}`; return `<blockquote class="bible-verse" data-reference="${reference}"> <span class="verse-text">${verse.text}</span> <cite class="verse-reference">${reference}</cite> </blockquote>`; } } /** * Position cursor after inserted content */ positionCursorAfterVerse(position) { const range = document.createRange(); const selection = window.getSelection(); if (selection) { try { const textNode = this.findTextNodeAtPosition(position); if (textNode) { range.setStart(textNode, Math.min(position, textNode.textContent?.length || 0)); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); } } catch (error) { console.warn('⚠️ Could not position cursor:', error); } } } /** * Find text node at specific position */ findTextNodeAtPosition(position) { const walker = document.createTreeWalker(this.editorElement, NodeFilter.SHOW_TEXT, null); let currentPosition = 0; let node = walker.nextNode(); while (node) { const nodeLength = node.textContent?.length || 0; if (currentPosition + nodeLength >= position) { return node; } currentPosition += nodeLength; node = walker.nextNode(); } return null; } /** * Toggle text formatting */ toggleFormat(action) { if (!this.config.enableTextFormatting || !this.textFormatter) { console.warn('⚠️ Text formatting is disabled'); return; } try { this.textFormatter.toggleStyle(action); // Update button states const formatState = this.textFormatter.getFormatState(); this.updateFormatButtons(formatState); } catch (error) { console.error('❌ Format toggle failed:', error); this.toastManager.error('ν…μŠ€νŠΈ ν¬λ§·νŒ… 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€'); } } /** * Apply text color */ applyTextColor(color) { if (!this.config.enableTextFormatting || !this.textFormatter) { console.warn('⚠️ Text formatting is disabled'); return; } try { this.textFormatter.applyTextColor(color); } catch (error) { console.error('❌ Color application failed:', error); this.toastManager.error('색상 적용 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€'); } } /** * Show color picker */ showColorPicker() { if (!this.config.enableColorPicker || !this.colorPicker) { console.warn('⚠️ Color picker is disabled'); return; } this.colorPicker.show((color) => { this.applyTextColor(color); }); } /** * Get current editor content */ getContent() { return this.editorElement.innerHTML; } /** * Set editor content */ setContent(html) { this.editorElement.innerHTML = html; } /** * Get plain text content */ getTextContent() { return this.editorElement.textContent || ''; } /** * Clear editor content */ clear() { this.editorElement.innerHTML = ''; } /** * Focus the editor */ focus() { this.editorElement.focus(); } /** * Check if editor has focus */ isFocused() { return document.activeElement === this.editorElement; } /** * Get current format state */ getFormatState() { if (!this.config.enableTextFormatting || !this.textFormatter) { return null; } return this.textFormatter.getFormatState(); } /** * Insert verse programmatically */ async insertVerse(ref) { if (!this.config.enableBibleVerses || !this.bibleEngine) { console.warn('⚠️ Bible verses are disabled'); return false; } try { if (!this.bibleEngine.isValidBibleRef(ref)) { this.toastManager.error(`μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ μ„±κ²½ μ°Έμ‘°: ${ref}`); return false; } const hideLoading = this.toastManager.showLoading(`${ref} ꡬ절 검색 쀑...`); const verseData = await this.bibleEngine.loadVerse(ref); hideLoading(); if (verseData) { const verseHtml = this.createVerseHtml(verseData); // Insert at current cursor position const selection = window.getSelection(); if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0); range.deleteContents(); const tempDiv = document.createElement('div'); tempDiv.innerHTML = verseHtml; const fragment = document.createDocumentFragment(); while (tempDiv.firstChild) { fragment.appendChild(tempDiv.firstChild); } range.insertNode(fragment); range.collapse(false); selection.removeAllRanges(); selection.addRange(range); } else { // No selection, append to end this.editorElement.insertAdjacentHTML('beforeend', verseHtml); } this.toastManager.success(`${ref} ꡬ절이 μ‚½μž…λ˜μ—ˆμŠ΅λ‹ˆλ‹€`); return true; } else { this.toastManager.error(`${ref} κ΅¬μ ˆμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€`); return false; } } catch (error) { console.error('❌ Failed to insert verse:', error); if (error instanceof Editor_1.BibleApiError) { this.toastManager.handleApiError(error, ref); } else { this.toastManager.error('ꡬ절 μ‚½μž… 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€'); } return false; } } /** * Destroy the editor and cleanup */ destroy() { if (!this.isInitialized) return; try { // Remove all event listeners this.eventListeners.forEach(({ element, event, handler }) => { element.removeEventListener(event, handler); }); this.eventListeners = []; // Destroy PWA keyboard tracker if (this.keyboardTracker) { this.keyboardTracker.destroy(); } // Hide color picker if open if (this.colorPicker && this.colorPicker.isOpen()) { this.colorPicker.hide(); } // Stop auto-save if (this.autoSaveManager) { this.autoSaveManager.stop(); } // Clear editor classes this.editorElement.classList.remove('holy-editor', 'holy-editor-focused'); this.isInitialized = false; console.log('πŸ—‘οΈ HolyEditor destroyed'); } catch (error) { console.error('❌ Error during editor destruction:', error); } } // Private utility methods addEventListener(element, event, handler) { element.addEventListener(event, handler); this.eventListeners.push({ element, event, handler }); } findParentQuote(node) { let current = node.nodeType === Node.TEXT_NODE ? node.parentNode : node; while (current && current !== this.editorElement) { if (current instanceof Element && (current.tagName === 'BLOCKQUOTE' || current.classList.contains('inline-quote'))) { return current; } current = current.parentNode; } return null; } handleEnterInQuote() { // Create new paragraph after quote const p = document.createElement('p'); p.innerHTML = '&#8203;'; // Zero-width space const selection = window.getSelection(); if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0); const quote = this.findParentQuote(range.startContainer); if (quote && quote.parentNode) { quote.parentNode.insertBefore(p, quote.nextSibling); // Move cursor to new paragraph range.selectNodeContents(p); range.collapse(false); selection.removeAllRanges(); selection.addRange(range); } } } updateFormatButtons(formatState) { // This would update external format buttons if they exist // Can be overridden by implementing applications const event = new CustomEvent('holyeditor:formatstatechange', { detail: formatState }); this.editorElement.dispatchEvent(event); } /** * Manually save content (auto-save) */ saveContent() { if (!this.autoSaveManager || !this.config.enableAutoSave) { console.warn('⚠️ Auto-save is disabled'); return false; } return this.autoSaveManager.save(this.getContent()); } /** * Clear saved content */ clearSavedContent() { if (!this.autoSaveManager) return false; return this.autoSaveManager.clear(); } /** * Check if there's saved content */ hasSavedContent() { if (!this.autoSaveManager) return false; return this.autoSaveManager.hasSavedContent(); } /** * Get auto-save info */ getAutoSaveInfo() { if (!this.autoSaveManager) { return { isRunning: false, hasContent: false }; } const saveInfo = this.autoSaveManager.getSaveInfo(); return { isRunning: this.autoSaveManager.isRunning(), lastSave: saveInfo?.timestamp, hasContent: this.autoSaveManager.hasSavedContent() }; } /** * Update auto-save interval */ updateAutoSaveInterval(intervalMs) { if (!this.autoSaveManager) return; this.autoSaveManager.updateInterval(intervalMs, () => this.getContent()); this.config.autoSaveInterval = intervalMs; } /** * Toggle auto-save */ toggleAutoSave(enable) { const shouldEnable = enable !== undefined ? enable : !this.config.enableAutoSave; this.config.enableAutoSave = shouldEnable; if (!this.autoSaveManager) return; if (shouldEnable) { this.autoSaveManager.start(() => this.getContent()); this.toastManager.success('μžλ™ μ €μž₯이 ν™œμ„±ν™”λ˜μ—ˆμŠ΅λ‹ˆλ‹€'); } else { this.autoSaveManager.stop(); this.toastManager.info('μžλ™ μ €μž₯이 λΉ„ν™œμ„±ν™”λ˜μ—ˆμŠ΅λ‹ˆλ‹€'); } } } exports.HolyEditor = HolyEditor; //# sourceMappingURL=HolyEditor.js.map