@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
JavaScript
"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