/** * Rich Tagger JavaScript - Complete Rich Text Editor Implementation * * This file contains a comprehensive rich text editor class with advanced features * including formatting, color selection, lists, alignment, undo/redo, and more. */ /** * RichTaggerEditor Class * * A feature-rich WYSIWYG text editor with toolbar controls for formatting, * styling, and content manipulation. Supports undo/redo, fullscreen mode, * and real-time HTML output display. */ class RichTaggerEditor { // ======================================== // CONSTRUCTOR AND INITIALIZATION // ======================================== /** * Constructor - Initialize the editor with container and options * @param {string} containerId - ID of the container element * @param {Object} options - Configuration options for the editor */ constructor(containerId, options = {}) { this.container = document.getElementById(containerId); // Default configuration with toolbar buttons and height this.options = { height: '350px', toolbar: [ 'fontsize', 'fontfamily', 'heading', '|', 'bold', 'italic', 'underline', 'strikethrough', '|', 'textcolor', 'highlight', '|', 'alignleft', 'aligncenter', 'alignright', '|', 'unorderedlist', 'orderedlist', 'codeblock', 'indent', '|', 'link', 'superscript', 'subscript', 'hr', '|', 'find', 'insertdate', 'fullscreen', 'clearformat', '|', 'undo', 'redo' ], ...options }; // Initialize undo/redo system properties this.undoStack = []; // Stack for undo operations this.redoStack = []; // Stack for redo operations this.maxUndoSteps = 50; // Maximum number of undo steps to keep this.savedRange = null; // Saved selection range for dropdown operations // Start the initialization process this.init(); } /** * Initialize the editor components in sequence * Creates toolbar, editor area, and binds event handlers */ init() { this.createToolbar(); // Create the formatting toolbar this.createEditor(); // Create the content editor area this.bindEvents(); // Attach event listeners } // ======================================== // TOOLBAR CREATION AND MANAGEMENT // ======================================== /** * Create the toolbar with all formatting buttons and dropdowns * Builds toolbar based on the options.toolbar configuration */ createToolbar() { // Create main toolbar container this.toolbar = document.createElement('div'); this.toolbar.className = 'rte-toolbar d-flex flex-wrap gap-1'; // Define all available toolbar buttons with their properties const buttons = { // Font and text styling fontsize: { type: 'dropdown', icon: 'Size', action: 'fontsize', title: 'Font Size', options: ['8px', '10px', '12px', '14px', '16px', '18px', '24px', '36px'] }, fontfamily: { type: 'dropdown', icon: 'Font', action: 'fontfamily', title: 'Font Family', options: ['Arial', 'Times New Roman', 'Helvetica', 'Georgia', 'Courier New'] }, heading: { type: 'dropdown', icon: 'Heading', action: 'heading', title: 'H1, H2, H3', options: ['Normal', 'H1', 'H2', 'H3'] }, // Basic formatting buttons bold: { icon: 'B', action: 'bold', title: 'Bold (Ctrl+B)' }, italic: { icon: 'I', action: 'italic', title: 'Italic (Ctrl+I)' }, underline: { icon: 'U', action: 'underline', title: 'Underline (Ctrl+U)' }, strikethrough: { icon: 'S', action: 'strikethrough', title: 'Strikethrough' }, // Color formatting textcolor: { type: 'dropdown', icon: 'A', action: 'textcolor', title: 'Text Colour', options: ['black', 'red', 'blue', 'green', 'purple', 'orange', 'brown', 'gray', 'darkred', 'darkblue', 'darkgreen', 'darkviolet', 'darkorange', 'darkgray', 'pink', 'cyan', 'lime', 'gold'] }, highlight: { type: 'dropdown', icon: 'A', action: 'highlight', title: 'Text Highlight', options: ['yellow', 'lightgreen', 'lightblue', 'pink', 'orange', 'lavender', 'lightyellow', 'lightcyan', 'lightpink', 'peachpuff', 'lightgray', 'lightcoral', 'mistyrose', 'lightsteelblue', 'lightseagreen', 'moccasin', 'thistle', 'lemonchiffon'] }, // Text alignment alignleft: { icon: '', action: 'alignleft', title: 'Align Left' }, aligncenter: { icon: '', action: 'aligncenter', title: 'Align Center' }, alignright: { icon: '', action: 'alignright', title: 'Align Right' }, // Content insertion and structure link: { icon: '🔗', action: 'link', title: 'Insert Link' }, unorderedlist: { icon: '', action: 'unorderedlist', title: 'Bullet List' }, orderedlist: { icon: '', action: 'orderedlist', title: 'Numbered List' }, codeblock: { icon: '{ }', action: 'codeblock', title: 'Code Block' }, indent: { icon: '', action: 'indent', title: 'Toggle Indent' }, // Advanced formatting superscript: { icon: 'X²', action: 'superscript', title: 'Superscript' }, subscript: { icon: 'X₂', action: 'subscript', title: 'Subscript' }, hr: { icon: '—', action: 'hr', title: 'Insert Horizontal Rule' }, // Editor tools find: { icon: '🔍', action: 'find', title: 'Find & Replace (Ctrl+F)' }, insertdate: { icon: '📅', action: 'insertdate', title: 'Insert Current Date' }, fullscreen: { icon: '⛶', action: 'fullscreen', title: 'Toggle Fullscreen' }, clearformat: { icon: '🗑️', action: 'clearformat', title: 'Clear Formatting' }, // Undo/Redo undo: { icon: '↶', action: 'undo', title: 'Undo (Ctrl+Z)' }, redo: { icon: '↷', action: 'redo', title: 'Redo (Ctrl+Y)' } }; // Build toolbar by iterating through configured buttons this.options.toolbar.forEach(buttonName => { if (buttonName === '|') { // Create visual divider between button groups const divider = document.createElement('div'); divider.className = 'toolbar-divider'; divider.style.cssText = 'width: 1px; height: 24px; background-color: #dee2e6; margin: 0 4px; align-self: center;'; this.toolbar.appendChild(divider); } else if (buttons[buttonName]) { // Create button or dropdown for this toolbar item const btn = this.createToolbarButton(buttons[buttonName]); this.toolbar.appendChild(btn); } }); // Add completed toolbar to the container this.container.appendChild(this.toolbar); } /** * Create individual toolbar button (regular button or dropdown) * @param {Object} config - Button configuration object * @returns {HTMLElement} The created button element */ createToolbarButton(config) { if (config.type === 'dropdown') { return this.createDropdown(config); } // Create regular toolbar button const button = document.createElement('button'); button.type = 'button'; button.className = 'rte-toolbar-btn btn btn-sm btn-outline-secondary'; button.innerHTML = config.icon; button.title = config.title; button.dataset.action = config.action; // Add click handler to execute the button's action button.onclick = (e) => { e.preventDefault(); this.executeAction(config.action); }; return button; } /** * Create dropdown button with options (for font size, color, etc.) * @param {Object} config - Dropdown configuration object * @returns {HTMLElement} The created dropdown element */ createDropdown(config) { const dropdown = document.createElement('div'); dropdown.className = 'dropdown'; // Special styling for color picker dropdowns const isColorDropdown = config.action === 'textcolor' || config.action === 'highlight'; // Build dropdown HTML structure dropdown.innerHTML = ` ${isColorDropdown ? // Color grid layout for color dropdowns `' : // Regular list layout for other dropdowns `' }`; // Save current selection when dropdown is opened (to restore later) dropdown.addEventListener('mousedown', (e) => { if (e.target.closest('.dropdown-menu')) { const selection = window.getSelection(); if (selection.rangeCount > 0) { this.savedRange = selection.getRangeAt(0).cloneRange(); } } }); // Handle dropdown option selection dropdown.addEventListener('click', (e) => { if (e.target.dataset.action || e.target.closest('[data-action]')) { e.preventDefault(); e.stopPropagation(); const actionElement = e.target.dataset.action ? e.target : e.target.closest('[data-action]'); this.executeAction(actionElement.dataset.action, actionElement.dataset.value); // Close dropdown after selection const dropdownMenu = dropdown.querySelector('.dropdown-menu'); if (dropdownMenu) { dropdownMenu.classList.remove('show'); } const dropdownButton = dropdown.querySelector('[data-bs-toggle="dropdown"]'); if (dropdownButton) { dropdownButton.classList.remove('show'); dropdownButton.setAttribute('aria-expanded', 'false'); } } }); return dropdown; } // ======================================== // EDITOR CREATION AND SETUP // ======================================== /** * Create the main content editor area * Sets up the contentEditable div with styling and initial content */ createEditor() { this.editor = document.createElement('div'); this.editor.contentEditable = true; this.editor.style.height = this.options.height; this.editor.className = 'rte-editor border p-3'; this.editor.style.outline = 'none'; this.editor.style.backgroundColor = 'white'; this.editor.style.borderRadius = '0 0 0.375rem 0.375rem'; this.editor.style.minHeight = '250px'; this.editor.style.maxHeight = this.options.height; this.editor.style.overflowY = 'auto'; this.editor.style.boxSizing = 'border-box'; this.editor.innerHTML = '

Start typing here to explore all the advanced features...

'; // Add editor to container this.container.appendChild(this.editor); } // ======================================== // EVENT BINDING AND HANDLERS // ======================================== /** * Bind all necessary event listeners to the editor * Handles input, selection changes, keyboard shortcuts, and state saving */ bindEvents() { // Save initial state for undo functionality this.saveState(); // Update toolbar button states when selection changes this.editor.addEventListener('mouseup', () => this.updateToolbarState()); this.editor.addEventListener('keyup', () => this.updateToolbarState()); // Handle content changes this.editor.addEventListener('input', () => { this.updateOutput(); this.updateToolbarState(); // Debounced state saving to avoid excessive undo entries clearTimeout(this.saveTimeout); this.saveTimeout = setTimeout(() => this.saveState(), 500); }); // Update output when editor loses focus this.editor.addEventListener('blur', () => this.updateOutput()); // Save state on important key presses this.editor.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === 'Backspace' || e.key === 'Delete') { clearTimeout(this.saveTimeout); this.saveTimeout = setTimeout(() => this.saveState(), 100); } }); // Initial output update after brief delay setTimeout(() => { this.updateOutput(); this.updateToolbarState(); }, 100); } // ======================================== // ACTION EXECUTION AND COORDINATION // ======================================== /** * Execute a toolbar action (formatting, insertion, etc.) * @param {string} action - The action to perform * @param {string} value - Optional value for the action (e.g., color, font size) */ executeAction(action, value = null) { this.editor.focus(); // Restore saved selection range if available (from dropdown usage) let range = null; if (this.savedRange) { range = this.savedRange; const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(this.savedRange); this.savedRange = null; } else { const selection = window.getSelection(); range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null; } // Route to appropriate method based on action type switch(action) { // Font and text styling case 'fontsize': this.setFontSize(range, value); break; case 'fontfamily': this.setFontFamily(range, value); break; case 'bold': this.toggleFormat(range, 'strong'); break; case 'italic': this.toggleFormat(range, 'em'); break; case 'underline': this.toggleFormat(range, 'u'); break; case 'strikethrough': this.toggleFormat(range, 's'); break; case 'textcolor': if (value) this.setTextColor(range, value); break; case 'highlight': if (value) this.setBackgroundColor(range, value); break; // Headings and structure case 'heading': this.formatHeading(range, value); break; case 'h1': case 'h2': case 'h3': this.setHeading(range, action); break; // Text alignment case 'alignleft': case 'aligncenter': case 'alignright': this.alignText(range, action.replace('align', '')); break; // Content insertion case 'link': this.insertLink(range); break; case 'unorderedlist': this.toggleList(range, 'ul'); break; case 'orderedlist': this.toggleList(range, 'ol'); break; case 'codeblock': this.insertCodeBlock(range); break; case 'indent': this.toggleIndent(range); break; case 'superscript': this.toggleFormat(range, 'sup'); break; case 'subscript': this.toggleFormat(range, 'sub'); break; case 'hr': this.insertHorizontalRule(range); break; // Editor tools case 'find': this.openFindReplace(); break; case 'insertdate': this.insertDateTime('date'); break; case 'fullscreen': this.toggleFullscreen(); break; case 'clearformat': this.clearFormatting(range); break; // Undo/Redo (don't save state for these) case 'undo': this.undo(); return; case 'redo': this.redo(); return; } // Update UI and save state after action (except undo/redo) this.updateOutput(); this.updateToolbarState(); this.saveState(); } // ======================================== // TOOLBAR STATE MANAGEMENT // ======================================== /** * Update toolbar button states based on current selection * Highlights active formatting buttons and updates dropdown displays */ updateToolbarState() { const selection = window.getSelection(); if (selection.rangeCount === 0) return; const range = selection.getRangeAt(0); const buttons = this.toolbar.querySelectorAll('.rte-toolbar-btn'); // Check each button to see if its formatting is active buttons.forEach(btn => { btn.classList.remove('active'); if (btn.dataset.action) { const action = btn.dataset.action; let isActive = false; // Determine if this formatting is currently applied switch(action) { case 'bold': isActive = this.isSelectionFormatted(range, 'strong'); break; case 'italic': isActive = this.isSelectionFormatted(range, 'em'); break; case 'underline': isActive = this.isSelectionFormatted(range, 'u'); break; case 'strikethrough': isActive = this.isSelectionFormatted(range, 's'); break; case 'superscript': isActive = this.isSelectionFormatted(range, 'sup'); break; case 'subscript': isActive = this.isSelectionFormatted(range, 'sub'); break; case 'heading': // Update heading dropdown to show current level const headingLevel = this.getCurrentHeading(range); btn.innerHTML = headingLevel || 'Heading'; break; case 'codeblock': isActive = this.isCurrentCodeBlock(range); break; case 'indent': isActive = this.isCurrentlyIndented(range); break; } // Apply active styling if this formatting is present if (isActive) { btn.classList.add('active'); } } }); } /** * Check if current selection is within a specific heading level * @param {Range} range - The current selection range * @param {string} level - The heading level to check (h1, h2, h3) * @returns {boolean} True if selection is in the specified heading */ isCurrentHeading(range, level) { let element = range.commonAncestorContainer; if (element.nodeType === Node.TEXT_NODE) { element = element.parentElement; } // Walk up DOM tree to find block-level element while (element && element !== this.editor && !['P', 'DIV', 'H1', 'H2', 'H3', 'H4'].includes(element.tagName)) { element = element.parentElement; } return element && element.tagName && element.tagName.toLowerCase() === level.toLowerCase(); } /** * Get the current heading level of the selection * @param {Range} range - The current selection range * @returns {string} The heading level (H1, H2, H3) or 'Normal' */ getCurrentHeading(range) { if (!range) return 'Heading'; let element = range.commonAncestorContainer; if (element.nodeType === Node.TEXT_NODE) { element = element.parentElement; } // Find heading element in parent chain while (element && element !== this.editor) { if (element.tagName && ['H1', 'H2', 'H3'].includes(element.tagName)) { return element.tagName; } element = element.parentElement; } return 'Normal'; } /** * Check if current selection is within a code block * @param {Range} range - The current selection range * @returns {boolean} True if selection is in a code block */ isCurrentCodeBlock(range) { if (!range) return false; let element = range.commonAncestorContainer; if (element.nodeType === Node.TEXT_NODE) { element = element.parentElement; } // Look for pre element in parent chain while (element && element !== this.editor) { if (element.tagName && element.tagName.toLowerCase() === 'pre') { return true; } element = element.parentElement; } return false; } /** * Check if current selection is indented * @param {Range} range - The current selection range * @returns {boolean} True if selection is indented */ isCurrentlyIndented(range) { if (!range) return false; let blockElement = range.commonAncestorContainer; if (blockElement.nodeType === Node.TEXT_NODE) { blockElement = blockElement.parentElement; } // Find block-level element while (blockElement && blockElement !== this.editor && !['P', 'DIV', 'H1', 'H2', 'H3', 'LI', 'BLOCKQUOTE'].includes(blockElement.tagName)) { blockElement = blockElement.parentElement; } if (blockElement && blockElement !== this.editor) { const currentIndent = blockElement.style.marginLeft || '0px'; return currentIndent !== '0px' && currentIndent !== ''; } return false; } // ======================================== // TEXT FORMATTING METHODS // ======================================== /** * Set font size for selected text * @param {Range} range - The selection range * @param {string} size - Font size (e.g., '16px') */ setFontSize(range, size) { if (range && !range.collapsed) { // Remove existing font-size styling first this.removeStyleFromRange(range, 'fontSize'); // Apply new font size const span = document.createElement('span'); span.style.fontSize = size; try { range.surroundContents(span); } catch(e) { const contents = range.extractContents(); span.appendChild(contents); range.insertNode(span); } } } /** * Set font family for selected text * @param {Range} range - The selection range * @param {string} family - Font family name */ setFontFamily(range, family) { if (range && !range.collapsed) { // Remove existing font-family styling first this.removeStyleFromRange(range, 'fontFamily'); // Apply new font family const span = document.createElement('span'); span.style.fontFamily = family; try { range.surroundContents(span); } catch(e) { const contents = range.extractContents(); span.appendChild(contents); range.insertNode(span); } } } /** * Set text color for selected text * @param {Range} range - The selection range * @param {string} color - Color value */ setTextColor(range, color) { if (range && !range.collapsed) { // Remove existing text color styling first this.removeStyleFromRange(range, 'color'); // Apply new text color const span = document.createElement('span'); span.style.color = color; try { range.surroundContents(span); } catch(e) { const contents = range.extractContents(); span.appendChild(contents); range.insertNode(span); } } } /** * Set background color (highlight) for selected text * @param {Range} range - The selection range * @param {string} color - Background color value */ setBackgroundColor(range, color) { if (range && !range.collapsed) { // Remove existing background color styling first this.removeStyleFromRange(range, 'backgroundColor'); // Apply new background color const span = document.createElement('span'); span.style.backgroundColor = color; try { range.surroundContents(span); } catch(e) { const contents = range.extractContents(); span.appendChild(contents); range.insertNode(span); } } } /** * Toggle formatting (bold, italic, underline, etc.) on selected text * @param {Range} range - The selection range * @param {string} tagName - HTML tag name for the formatting */ toggleFormat(range, tagName) { if (range && !range.collapsed) { // Check if formatting is already applied if (this.isSelectionFormatted(range, tagName)) { // Remove the formatting this.removeFormatting(range, tagName); } else { // Apply the formatting const element = document.createElement(tagName); try { range.surroundContents(element); } catch(e) { const contents = range.extractContents(); element.appendChild(contents); range.insertNode(element); } } } } /** * Check if selection has specific formatting applied * @param {Range} range - The selection range * @param {string} tagName - HTML tag name to check for * @returns {boolean} True if formatting is present */ isSelectionFormatted(range, tagName) { let container = range.commonAncestorContainer; // If container is text node, check its parent if (container.nodeType === Node.TEXT_NODE) { container = container.parentElement; } // Check parent elements for the target tag let current = container; while (current && current !== this.editor) { if (current.tagName && current.tagName.toLowerCase() === tagName.toLowerCase()) { return true; } current = current.parentElement; } // Also check if selection contains the target tag if (container.querySelector) { const targetElements = container.querySelectorAll(tagName.toLowerCase()); for (let element of targetElements) { if (range.intersectsNode(element)) { return true; } } } return false; } /** * Remove specific formatting from selected range * @param {Range} range - The selection range * @param {string} tagName - HTML tag name to remove */ removeFormatting(range, tagName) { const selection = window.getSelection(); const originalRange = range.cloneRange(); let container = range.commonAncestorContainer; if (container.nodeType === Node.TEXT_NODE) { container = container.parentElement; } // Remove from parent elements first let current = container; while (current && current !== this.editor) { if (current.tagName && current.tagName.toLowerCase() === tagName.toLowerCase()) { this.unwrapElement(current); break; } current = current.parentElement; } // Remove from nested elements within selection if (container.querySelectorAll) { const elements = Array.from(container.querySelectorAll(tagName.toLowerCase())); elements.forEach(element => { if (originalRange.intersectsNode(element)) { this.unwrapElement(element); } }); } // Restore selection try { selection.removeAllRanges(); selection.addRange(originalRange); } catch(e) { // Selection restoration failed but formatting was still applied } } /** * Remove specific CSS style from elements in range * @param {Range} range - The selection range * @param {string} styleProperty - CSS property to remove */ removeStyleFromRange(range, styleProperty) { const container = range.commonAncestorContainer; let elementsToCheck = []; if (container.nodeType === Node.TEXT_NODE) { // Check parent elements let parent = container.parentElement; while (parent && parent !== this.editor) { if (parent.style && parent.style[styleProperty]) { elementsToCheck.push(parent); } parent = parent.parentElement; } } else { // Check all child elements const spans = container.querySelectorAll ? container.querySelectorAll('span') : []; spans.forEach(span => { if (span.style && span.style[styleProperty]) { elementsToCheck.push(span); } }); } // Remove the style property from found elements elementsToCheck.forEach(element => { element.style[styleProperty] = ''; // Remove element entirely if no other styles remain if (!element.getAttribute('style') || element.getAttribute('style').trim() === '') { this.unwrapElement(element); } }); } /** * Remove an element while keeping its content * @param {HTMLElement} element - Element to unwrap */ unwrapElement(element) { const parent = element.parentNode; while (element.firstChild) { parent.insertBefore(element.firstChild, element); } parent.removeChild(element); } // ======================================== // CONTENT STRUCTURE METHODS // ======================================== /** * Format current block as heading or normal text * @param {Range} range - The selection range * @param {string} value - Heading level (H1, H2, H3) or 'Normal' */ formatHeading(range, value) { if (!range) return; // Find the current block element let blockElement = range.commonAncestorContainer; if (blockElement.nodeType === Node.TEXT_NODE) { blockElement = blockElement.parentElement; } // Walk up to find paragraph or heading element while (blockElement && blockElement !== this.editor && !['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(blockElement.tagName)) { blockElement = blockElement.parentElement; } if (blockElement && blockElement !== this.editor) { let newElement; // Create appropriate element type if (value === 'Normal') { newElement = document.createElement('p'); } else { newElement = document.createElement(value.toLowerCase()); } // Copy content and replace element newElement.innerHTML = blockElement.innerHTML; blockElement.parentNode.replaceChild(newElement, blockElement); // Restore selection to new element const selection = window.getSelection(); selection.removeAllRanges(); const newRange = document.createRange(); newRange.selectNodeContents(newElement); newRange.collapse(false); selection.addRange(newRange); } } /** * Set heading level for current block * @param {Range} range - The selection range * @param {string} level - Heading level (h1, h2, h3) */ setHeading(range, level) { let element = range.commonAncestorContainer; if (element.nodeType === Node.TEXT_NODE) { element = element.parentElement; } // Find block-level element while (element && element !== this.editor && !['P', 'DIV', 'H1', 'H2', 'H3', 'H4'].includes(element.tagName)) { element = element.parentElement; } if (element && element !== this.editor) { const currentTag = element.tagName.toLowerCase(); const targetTag = level.toLowerCase(); // Toggle between heading and paragraph if same level if (currentTag === targetTag) { const paragraph = document.createElement('p'); paragraph.innerHTML = element.innerHTML; if (element.style.cssText) { paragraph.style.cssText = element.style.cssText; } element.parentNode.replaceChild(paragraph, element); } else { // Convert to requested heading level const heading = document.createElement(level.toUpperCase()); heading.innerHTML = element.innerHTML; if (element.style.cssText) { heading.style.cssText = element.style.cssText; } element.parentNode.replaceChild(heading, element); } } } /** * Set text alignment for current block * @param {Range} range - The selection range * @param {string} alignment - Alignment value (left, center, right) */ alignText(range, alignment) { let element = range.commonAncestorContainer; if (element.nodeType === Node.TEXT_NODE) { element = element.parentElement; } // Find block-level element while (element && element !== this.editor && !['P', 'DIV', 'H1', 'H2', 'H3', 'H4'].includes(element.tagName)) { element = element.parentElement; } if (element && element !== this.editor) { element.style.textAlign = alignment; } } /** * Toggle indentation for current block * @param {Range} range - The selection range */ toggleIndent(range) { if (!range) return; // Find block element let blockElement = range.commonAncestorContainer; if (blockElement.nodeType === Node.TEXT_NODE) { blockElement = blockElement.parentElement; } while (blockElement && blockElement !== this.editor && !['P', 'DIV', 'H1', 'H2', 'H3', 'LI', 'BLOCKQUOTE'].includes(blockElement.tagName)) { blockElement = blockElement.parentElement; } if (blockElement && blockElement !== this.editor) { const currentIndent = blockElement.style.marginLeft || '0px'; // Toggle between indented and normal if (currentIndent === '0px' || currentIndent === '') { blockElement.style.marginLeft = '40px'; } else { blockElement.style.marginLeft = '0px'; } } } /** * Insert or toggle code block formatting * @param {Range} range - The selection range */ insertCodeBlock(range) { if (!range) return; // Find current block element let blockElement = range.commonAncestorContainer; if (blockElement.nodeType === Node.TEXT_NODE) { blockElement = blockElement.parentElement; } while (blockElement && blockElement !== this.editor && !['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'PRE', 'LI'].includes(blockElement.tagName)) { blockElement = blockElement.parentElement; } if (blockElement && blockElement !== this.editor) { if (blockElement.tagName.toLowerCase() === 'pre') { // Convert code block back to paragraph const paragraph = document.createElement('p'); const codeElement = blockElement.querySelector('code'); if (codeElement) { paragraph.textContent = codeElement.textContent; } else { paragraph.textContent = blockElement.textContent; } blockElement.parentNode.replaceChild(paragraph, blockElement); } else { // Convert to code block const pre = document.createElement('pre'); const code = document.createElement('code'); pre.appendChild(code); pre.className = 'bg-dark text-light p-3 rounded'; code.textContent = blockElement.textContent || 'Enter code here...'; blockElement.parentNode.replaceChild(pre, blockElement); } } } // ======================================== // CONTENT INSERTION METHODS // ======================================== /** * Insert a hyperlink at current selection * @param {Range} range - The selection range */ insertLink(range) { const url = prompt('Enter URL:', 'https://'); if (url && url !== 'https://') { const link = document.createElement('a'); link.href = url; link.target = '_blank'; if (range.collapsed) { // No selection, use URL as link text link.textContent = url; range.insertNode(link); } else { // Wrap selected text in link try { range.surroundContents(link); } catch(e) { const contents = range.extractContents(); link.appendChild(contents); range.insertNode(link); } } } } /** * Create a list from selected text * @param {Range} range - The selection range * @param {string} listType - List type ('ul' or 'ol') */ toggleList(range, listType) { if (!range.collapsed) { const list = document.createElement(listType); const listItem = document.createElement('li'); try { const contents = range.extractContents(); listItem.appendChild(contents); list.appendChild(listItem); range.insertNode(list); } catch(e) { console.warn('List creation failed:', e); } } } /** * Insert horizontal rule at cursor * @param {Range} range - The selection range */ insertHorizontalRule(range) { const hr = document.createElement('hr'); range.insertNode(hr); // Move cursor after the hr range.setStartAfter(hr); range.collapse(true); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); } /** * Insert current date at cursor position * @param {string} type - Type of date insertion ('date' or 'time') */ insertDateTime(type) { const now = new Date(); const text = type === 'date' ? now.toLocaleDateString() : now.toLocaleTimeString(); const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); range.deleteContents(); range.insertNode(document.createTextNode(text)); } } // ======================================== // EDITOR TOOLS AND UTILITIES // ======================================== /** * Open find and replace dialog * Simple implementation using browser prompts */ openFindReplace() { const findText = prompt('Find text:'); if (!findText) return; const replaceText = prompt('Replace with:', ''); if (replaceText === null) return; // Perform global case-insensitive replacement const content = this.editor.innerHTML; const regex = new RegExp(findText, 'gi'); const newContent = content.replace(regex, replaceText); this.editor.innerHTML = newContent; } /** * Toggle fullscreen mode for the editor */ toggleFullscreen() { this.container.classList.toggle('rte-fullscreen'); if (this.container.classList.contains('rte-fullscreen')) { // Enter fullscreen mode this.editor.style.height = 'calc(100vh - 200px)'; this.container.style.position = 'fixed'; this.container.style.top = '0'; this.container.style.left = '0'; this.container.style.width = '100%'; this.container.style.height = '100%'; this.container.style.backgroundColor = 'white'; this.container.style.zIndex = '9999'; this.container.style.padding = '20px'; } else { // Exit fullscreen mode this.editor.style.height = this.options.height; this.container.style.position = ''; this.container.style.top = ''; this.container.style.left = ''; this.container.style.width = ''; this.container.style.height = ''; this.container.style.backgroundColor = ''; this.container.style.zIndex = ''; this.container.style.padding = ''; } } /** * Clear all formatting from selected text * @param {Range} range - The selection range */ clearFormatting(range) { if (!range.collapsed) { const text = range.toString(); range.deleteContents(); range.insertNode(document.createTextNode(text)); } } // ======================================== // UNDO/REDO SYSTEM // ======================================== /** * Save current editor state for undo functionality */ saveState() { const currentState = this.editor.innerHTML; // Don't save duplicate states if (this.undoStack.length > 0 && this.undoStack[this.undoStack.length - 1] === currentState) { return; } this.undoStack.push(currentState); // Limit stack size to prevent memory issues if (this.undoStack.length > this.maxUndoSteps) { this.undoStack.shift(); } // Clear redo stack when new action is performed this.redoStack = []; this.updateUndoRedoButtons(); } /** * Undo last action */ undo() { if (this.undoStack.length > 1) { // Move current state to redo stack const currentState = this.undoStack.pop(); this.redoStack.push(currentState); // Restore previous state const previousState = this.undoStack[this.undoStack.length - 1]; this.editor.innerHTML = previousState; this.updateOutput(); this.updateUndoRedoButtons(); } } /** * Redo last undone action */ redo() { if (this.redoStack.length > 0) { // Move state from redo stack back to undo stack const nextState = this.redoStack.pop(); this.undoStack.push(nextState); // Restore next state this.editor.innerHTML = nextState; this.updateOutput(); this.updateUndoRedoButtons(); } } /** * Update undo/redo button states * Disables buttons when stacks are empty */ updateUndoRedoButtons() { const undoBtn = this.toolbar.querySelector('[data-action="undo"]'); const redoBtn = this.toolbar.querySelector('[data-action="redo"]'); if (undoBtn) { undoBtn.disabled = this.undoStack.length <= 1; undoBtn.style.opacity = undoBtn.disabled ? '0.5' : '1'; } if (redoBtn) { redoBtn.disabled = this.redoStack.length === 0; redoBtn.style.opacity = redoBtn.disabled ? '0.5' : '1'; } } // ======================================== // CONTENT MANAGEMENT AND OUTPUT // ======================================== /** * Update HTML output displays on the page * Shows raw HTML and sanitized content in designated areas */ updateOutput() { const htmlOutput = document.getElementById('advanced-html-output'); const sanitizedOutput = document.getElementById('advanced-sanitized-output'); if (htmlOutput) { const content = this.getContent(); htmlOutput.textContent = content; } if (sanitizedOutput) { const sanitized = this.sanitizeContent(this.getContent()); sanitizedOutput.innerHTML = sanitized; } } /** * Get current editor content as HTML * @returns {string} The HTML content of the editor */ getContent() { return this.editor.innerHTML; } /** * Set editor content * @param {string} html - HTML content to set */ setContent(html) { this.editor.innerHTML = html; this.updateOutput(); } /** * Clear editor content */ clear() { this.editor.innerHTML = '


'; this.updateOutput(); } /** * Basic HTML sanitization for security * Removes script tags and event handlers * @param {string} html - HTML content to sanitize * @returns {string} Sanitized HTML */ sanitizeContent(html) { let sanitized = html; // Remove script tags sanitized = sanitized.replace(/)<[^<]*)*<\/script>/gi, ''); // Remove event handlers sanitized = sanitized.replace(/\s*on\w+\s*=\s*["'][^"']*["']/gi, ''); return sanitized; } } // ======================================== // RICH TAGGER INITIALIZATION AND DEMO // ======================================== // Global Rich Tagger editor instance let richTaggerEditor; /** * Initialize Rich Tagger editor when page loads * Creates the editor instance and sets up demo functions * Only runs on the Rich Tagger page to avoid conflicts */ document.addEventListener('DOMContentLoaded', function() { // Only initialize if we're on the Rich Tagger page and container exists const container = document.getElementById('advanced-editor-container'); const isRichTaggerPage = window.location.pathname.includes('/RichTagger') || document.title.includes('Rich Tagger') || document.body.textContent.includes('Rich Tagger Integration Tutorial'); if (container && isRichTaggerPage) { // Initialize Rich Tagger editor with slight delay setTimeout(() => { if (typeof RichTaggerEditor !== 'undefined') { richTaggerEditor = new RichTaggerEditor('advanced-editor-container', { height: '300px' }); } }, 100); } }); /** * Load sample content into the Rich Tagger editor * Demonstrates various formatting features and capabilities */ function loadAdvancedSample() { if (richTaggerEditor) { const advancedContent = `

Rich Tagger Demo

This advanced editor includes font sizing, different fonts, and colored text.

Mathematical formulas: E = mc2 and H2O

function example() {
    console.log("Rich Tagger code blocks!");
    return "Advanced features";
}

Centered text with alignment controls


Links: Visit Example.com

Try all the toolbar features above to edit this content!

`; richTaggerEditor.setContent(advancedContent); } } /** * Clear all content from the Rich Tagger editor * Resets the editor to its initial empty state */ function clearAdvancedEditor() { if (richTaggerEditor) { richTaggerEditor.clear(); } }