@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
JavaScript
"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 = '​'; // 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