UNPKG

angular-rich-text-editor

Version:

A lightweight, configurable rich-text editor component for Angular applications.

1,191 lines (1,171 loc) 44.6 kB
import * as i0 from '@angular/core'; import { InjectionToken, Injectable, Inject, forwardRef, Component, Optional, ViewChild, Input, NgModule } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; import { NgControl, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms'; import * as i2 from '@angular/common'; import { CommonModule } from '@angular/common'; // src/lib/paths.ts // Default paths const DEFAULT_RICHTEXTEDITOR_ASSETS_PATH = 'assets/richtexteditor'; // Injection token for configuration const RICHTEXTEDITOR_ASSETS_PATH = new InjectionToken('RICHTEXTEDITOR_ASSETS_PATH', { providedIn: 'root', factory: () => DEFAULT_RICHTEXTEDITOR_ASSETS_PATH }); class RichTextEditorService { assetsPath; currentEditor = null; // Reference to the current RTE component contentSubject = new BehaviorSubject(''); // Observable for content changes content$ = this.contentSubject.asObservable(); constructor(assetsPath) { this.assetsPath = assetsPath; } getContentCssUrl() { return `${this.assetsPath}/runtime/richtexteditor_content.css`; } getPreviewCssUrl() { return `${this.assetsPath}/runtime/richtexteditor_preview.css`; } getPreviewScriptUrl() { return `${this.assetsPath}/runtime/richtexteditor_preview.js`; } // Simple editor management setCurrentEditor(component) { this.currentEditor = component; } clearCurrentEditor() { this.currentEditor = null; this.contentSubject.next(''); } // Content manipulation methods insertContentAtCursor(content) { if (!this.currentEditor) { console.warn('[RTE Service] No editor is currently active'); return false; } try { this.currentEditor.insertContentAtCursor(content); // Update the observable after insertion const newContent = this.getContent(); this.contentSubject.next(newContent); return true; } catch (error) { console.error('[RTE Service] Failed to insert content:', error); return false; } } /** * Get HTML content from current editor * @returns HTML string (empty string if no content/editor) */ getContent() { if (!this.currentEditor?.editorInstance) { console.warn('[RTE Service] No active editor found'); return ''; } try { const htmlContent = this.currentEditor.editorInstance.getHTMLCode(); // Handle null/undefined cases if (htmlContent === null || htmlContent === undefined) { return this.getContentFallback(); } return htmlContent; } catch (error) { console.error('[RTE Service] Failed to get content:', error); return this.getContentFallback(); } } /** * Fallback method to retrieve content */ getContentFallback() { try { // Try to get from iframe directly const iframe = this.currentEditor?.editorContainer?.nativeElement?.querySelector('iframe'); if (iframe?.contentDocument?.body) { return iframe.contentDocument.body.innerHTML || ''; } // Try to get from component's value if (this.currentEditor?.value) { return this.currentEditor.value; } return ''; } catch (error) { console.error('[RTE Service] Fallback retrieval failed:', error); return ''; } } /** * Set HTML content for current editor */ setContent(content) { if (!this.currentEditor?.editorInstance) { console.warn('[RTE Service] No active editor found'); return false; } try { this.currentEditor.editorInstance.setHTMLCode(content); // Ensure component state is synced if (this.currentEditor.value !== content) { this.currentEditor.value = content; } // Trigger change event if needed if (this.currentEditor.onChange) { this.currentEditor.onChange(content); } // Update observable this.contentSubject.next(content); return true; } catch (error) { console.error('[RTE Service] Failed to set content:', error); return false; } } /** * Clear editor content */ clearContent() { return this.setContent('<p><br></p>'); } /** * Focus current editor */ focus() { if (!this.currentEditor) { console.warn('[RTE Service] No active editor found'); return false; } try { // Try editor's focus method first if (this.currentEditor.editorInstance?.focus) { this.currentEditor.editorInstance.focus(); return true; } // Fallback to iframe focus const iframe = this.currentEditor.editorContainer?.nativeElement?.querySelector('iframe'); if (iframe?.contentDocument?.body) { iframe.contentDocument.body.focus(); return true; } return false; } catch (error) { console.error('[RTE Service] Failed to focus editor:', error); return false; } } /** * Execute command on the editor */ executeCommand(command, value) { if (!this.currentEditor?.editorInstance) { console.warn('[RTE Service] No active editor found'); return false; } try { // Try editor's execCommand if available if (typeof this.currentEditor.editorInstance.execCommand === 'function') { this.currentEditor.editorInstance.execCommand(command, false, value); return true; } // Fallback to iframe execCommand const iframe = this.currentEditor.editorContainer?.nativeElement?.querySelector('iframe'); if (iframe?.contentDocument) { iframe.contentDocument.execCommand(command, false, value); return true; } return false; } catch (error) { console.error('[RTE Service] Failed to execute command:', error); return false; } } /** * Get selected text from editor */ getSelectedText() { if (!this.currentEditor?.editorContainer) { return ''; } try { const iframe = this.currentEditor.editorContainer.nativeElement.querySelector('iframe'); if (iframe?.contentWindow) { const selection = iframe.contentWindow.getSelection(); return selection ? selection.toString() : ''; } return ''; } catch (error) { console.error('[RTE Service] Failed to get selected text:', error); return ''; } } /** * Check if content is empty */ isContentEmpty() { const content = this.getContent(); if (!content) return true; // Create a temporary div to parse HTML const div = document.createElement('div'); div.innerHTML = content; // Get text content and clean it const text = div.textContent?.replace(/\u00A0/g, '').trim() || ''; // Check if only contains empty tags const cleaned = div.innerHTML .replace(/<br\s*\/?>/gi, '') .replace(/<div>(\s|&nbsp;)*<\/div>/gi, '') .replace(/<p>(\s|&nbsp;)*<\/p>/gi, '') .replace(/&nbsp;/gi, '') .trim(); return !text && cleaned.length === 0; } /** * Get character count */ getCharacterCount() { const content = this.getContent(); if (!content) return 0; const div = document.createElement('div'); div.innerHTML = content; const text = div.textContent?.replace(/\u00A0/g, '').trim() || ''; return text.length; } /** * Get word count */ getWordCount() { const content = this.getContent(); if (!content) return 0; const div = document.createElement('div'); div.innerHTML = content; const text = div.textContent?.replace(/\u00A0/g, '').trim() || ''; if (!text) return 0; const words = text.match(/\b\w+\b/g); return words ? words.length : 0; } // Check if editor is readonly isReadonly() { return this.currentEditor?.readonly || false; } // Check if editor is available isAvailable() { return !!this.currentEditor?.editorInstance; } /** * Hide all floating panels (useful for cleanup) */ hideFloatingPanels() { if (this.currentEditor?.hideAllFloatPanels) { this.currentEditor.hideAllFloatPanels(); } } /** * Removes the last inserted image with a temporary blob or data URL. */ removeLastPlaceholderImage() { if (!this.currentEditor) return false; const iframe = this.currentEditor?.editorContainer?.nativeElement?.querySelector('iframe'); const body = iframe?.contentDocument?.body; if (!body) return false; const images = Array.from(body.querySelectorAll('img')); for (let i = images.length - 1; i >= 0; i--) { const img = images[i]; if (img.src.startsWith('blob:') || img.src.startsWith('data:')) { img.parentElement?.removeChild(img); console.debug('[RTE Service] Removed temporary placeholder image.'); return true; } } return false; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: RichTextEditorService, deps: [{ token: RICHTEXTEDITOR_ASSETS_PATH }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: RichTextEditorService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: RichTextEditorService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: () => [{ type: undefined, decorators: [{ type: Inject, args: [RICHTEXTEDITOR_ASSETS_PATH] }] }] }); const RTE_TOOLBAR_PRESETS = { BASIC: 'bold,italic,underline|fontname,fontsize|forecolor,backcolor|removeformat', STANDARD: 'bold,italic,underline,strikethrough|fontname,fontsize|forecolor,backcolor|removeformat|undo,redo', FULL: "{bold,italic,underline,forecolor,backcolor}|{justifyleft,justifycenter,justifyright,justifyfull}|{insertorderedlist,insertunorderedlist,indent,outdent}{superscript,subscript}" + " #{paragraphs:toggle,fontname:toggle,fontsize:toggle,inlinestyle,lineheight}" + " / {removeformat,cut,copy,paste,delete,find}|{insertlink,unlink,insertblockquote,insertemoji,insertchars,inserttable,insertimage,insertgallery,insertvideo,insertdocument,insertcode}" + "#{preview,code,selectall}" + " /{paragraphs:dropdown | fontname:dropdown | fontsize:dropdown} {paragraphstyle,toggle_paragraphop,menu_paragraphop}" + "#{toggleborder,fullscreenenter,fullscreenexit,undo,redo,togglemore}", MINIMAL: 'bold,italic|fontsize|forecolor|removeformat' }; // rich-text-editor-license.token.ts const RTE_LICENSE_KEY = new InjectionToken('RTE_LICENSE_KEY'); function cleanToolbarString(toolbar) { let cleaned = toolbar; // Remove :toggle and :dropdown cleaned = cleaned.replace(/:toggle/g, '').replace(/:dropdown/g, ''); // Fix spacing and redundancy cleaned = cleaned .replace(/,+/g, ',') .replace(/\{,+/g, '{') .replace(/,+\}/g, '}') .replace(/\|+/g, '|') .replace(/\{\s*\|/g, '{') .replace(/\|\s*\}/g, '}') .replace(/\{\s*\}/g, '') .replace(/\s*,\s*/g, ',') .replace(/\s*\|\s*/g, '|') .replace(/\{\s+/g, '{') .replace(/\s+\}/g, '}'); // Fix tool concatenation issues cleaned = cleaned.replace(/\b([a-z]),(?=[a-z],|[a-z]\b)/g, '$1'); let previousCleaned = ''; while (previousCleaned !== cleaned) { previousCleaned = cleaned; cleaned = cleaned.replace(/\b([a-z]),(?=[a-z],|[a-z]\b)/g, '$1'); } // Process sections const sections = cleaned.split(/([/#])/); const processedSections = []; for (let i = 0; i < sections.length; i++) { const section = sections[i]; if (section === '/' || section === '#') { processedSections.push(section); continue; } if (!section.trim()) continue; const groups = section.split('|'); const processedGroups = []; for (let group of groups) { const hasBraces = group.includes('{') || group.includes('}'); let content = group.replace(/[{}]/g, '').trim(); if (!content) continue; // Fix common concatenation issues content = content .replace(/(?<=fontname)(?=fontsize)/g, ',') .replace(/(?<=fontsize)(?=inlinestyle)/g, ',') .replace(/(?<=inlinestyle)(?=lineheight)/g, ',') .replace(/(?<=paragraphs)(?=fontname)/g, ',') .replace(/(?<=paragraphstyle)(?=menu_)/g, ',') .replace(/underlinefore/g, 'underline,fore') .replace(/forecolorback/g, 'forecolor,back') .replace(/backcolor/g, 'backcolor') .replace(/outdentsuperscript/g, 'outdent,superscript') .replace(/insertlinkun/g, 'insertlink,un') .replace(/unlinkinsert/g, 'unlink,insert') .replace(/insertblockquote/g, 'insertblockquote') .replace(/inserttable/g, 'inserttable') .replace(/insertimage/g, 'insertimage') .replace(/removeformat/g, 'removeformat'); content = content.replace(/,+/g, ',').trim(); if (content) { processedGroups.push(hasBraces ? `{${content}}` : content); } } if (processedGroups.length > 0) { processedSections.push(processedGroups.join('|')); } } cleaned = processedSections.join(''); // Final cleanup cleaned = cleaned .replace(/\{\s*\}/g, '') .replace(/\|+/g, '|') .replace(/\/+/g, '/') .replace(/#+/g, '#') .replace(/^[|/#]+|[|/#]+$/g, '') .replace(/\s+/g, ' ') .trim(); return cleaned; } class EditorEventManager { editorInstance; listeners = []; constructor(editorInstance) { this.editorInstance = editorInstance; } attach(event, handler) { if (this.editorInstance?.attachEvent) { this.editorInstance.attachEvent(event, handler); this.listeners.push({ event, handler }); } } attachMany(events, handler) { events.forEach(event => this.attach(event, handler)); } detachAll() { if (this.editorInstance?.detachEvent) { this.listeners.forEach(({ event, handler }) => { try { this.editorInstance.detachEvent(event, handler); } catch { } }); } this.listeners = []; } } function safeCleanupFloatingPanels() { try { const selectors = [ 'rte-floatpanel', '.rte-floatpanel', '.rte-floatpanel-paragraphop', '[class*="rte-float"]', '[class*="rte-popup"]', '.rte-toolbar-float', '.rte-dropdown-panel', ]; selectors.forEach((selector) => { const elements = document.querySelectorAll(selector); elements.forEach((element) => { try { if (element && element.parentNode && document.body.contains(element)) { element.parentNode.removeChild(element); } } catch (e) { if (element instanceof HTMLElement) { element.style.display = 'none'; element.style.visibility = 'hidden'; } } }); }); cleanupOrphanedElements(); } catch (error) { // Silent fail } } function cleanupOrphanedElements() { try { const rteElements = document.querySelectorAll('[id*="rte_"], [class*="rte_"]'); rteElements.forEach((element) => { try { if (!document.body.contains(element)) { element.remove(); } } catch (e) { // Ignore } }); } catch (e) { // Silent fail } } /** * Monkey patch Node.prototype.removeChild to avoid NotFoundError * when removing already-detached DOM elements. */ function patchRemoveChildIfDetached() { const originalRemoveChild = Node.prototype.removeChild; Node.prototype.removeChild = function (child) { if (child && child.parentNode === this) { return originalRemoveChild.call(this, child); } return child; }; } function hasRequiredValidator(control) { if (!control || !control.validator) return false; const result = control.validator({ value: null }); return !!(result && result['required']); } /** * Enhanced empty check that considers images and media as content */ function isTrulyEmpty(html) { if (!html || html.trim() === '') return true; const div = document.createElement('div'); div.innerHTML = html; const hasImages = div.querySelectorAll('img').length > 0; if (hasImages) return false; const hasVideos = div.querySelectorAll('video, iframe').length > 0; if (hasVideos) return false; const hasEmbeds = div.querySelectorAll('embed, object, audio').length > 0; if (hasEmbeds) return false; const text = div.textContent?.replace(/\u00A0/g, '').trim() || ''; const cleaned = div.innerHTML .replace(/<br\s*\/?>/gi, '') .replace(/<div>(\s|&nbsp;)*<\/div>/gi, '') .replace(/<p>(\s|&nbsp;)*<\/p>/gi, '') .replace(/&nbsp;/gi, '') .trim(); return !text && cleaned.length === 0; } class RichTextEditorComponent { injector; rteService; cdr; ngZone; globalLicenseKey; editorContainer; licenseKey = ''; config = {}; rtePreset = null; imageToolbarItems = null; excludedToolbarItems = []; initialContent = ''; errorMessages = { required: 'This field is required.', }; fileUploadHandler = () => { }; enableImageUpload = false; enableVideoEmbed = false; readonly = false; eventManager = null; editorInstance; value = ''; ngControl = null; changeTimer; isDestroyed = false; cleanupAttempts = 0; eventListeners = []; domCleanupTimer; onChange = (value) => { }; onTouched = () => { }; constructor(injector, rteService, cdr, ngZone, globalLicenseKey) { this.injector = injector; this.rteService = rteService; this.cdr = cdr; this.ngZone = ngZone; this.globalLicenseKey = globalLicenseKey; } ngOnInit() { patchRemoveChildIfDetached(); this.rteService.setCurrentEditor(this); try { this.ngControl = this.injector.get(NgControl); if (this.ngControl) { this.ngControl.valueAccessor = this; } } catch { // Safe fallback } } ngAfterViewInit() { // Run outside Angular zone to prevent change detection issues this.ngZone.runOutsideAngular(() => { setTimeout(() => { if (!this.isDestroyed) { this.initEditor(); } }, 100); }); } initEditor() { try { // Clean up any existing instance first this.cleanupExistingEditor(); const fullConfig = this.prepareConfiguration(); this._applyCustomStyles(); // Create editor instance this.editorInstance = new RichTextEditor(this.editorContainer.nativeElement, fullConfig); // Set initial content if (this.value) { this.editorInstance.setHTMLCode(this.value); } else if (this.initialContent) { this.value = this.initialContent; this.editorInstance.setHTMLCode(this.initialContent); this.ngZone.run(() => { this.onChange(this.initialContent); this.onTouched(); }); } if (this.readonly && this.editorInstance?.setReadOnly) { this.editorInstance.setReadOnly(true); } this.setupEventListeners(); // Update image toolbar if needed if (this.imageToolbarItems && this.editorInstance) { this.updateImageToolbar(); } } catch (error) { console.error('[RTE] Failed to initialize editor:', error); } } cleanupExistingEditor() { if (this.editorInstance) { try { // Remove all event listeners first this.removeAllEventListeners(); // Try to destroy the editor instance if (typeof this.editorInstance.destroy === 'function') { this.editorInstance.destroy(); } } catch (e) { console.warn('[RTE] Error during editor cleanup:', e); } this.editorInstance = null; } } setupEventListeners() { if (!this.editorInstance) return; this.eventManager = new EditorEventManager(this.editorInstance); const triggerUpdate = () => { if (this.changeTimer) clearTimeout(this.changeTimer); this.changeTimer = setTimeout(() => { if (this.isDestroyed || !this.editorInstance) return; this.ngZone.run(() => { try { const html = this.editorInstance.getHTMLCode() || ''; this.value = html; this.onChange(html); this.onTouched(); if (this.ngControl?.control) { const finalValue = isTrulyEmpty(html) ? '' : html; this.ngControl.control.setValue(finalValue, { emitEvent: false }); this.ngControl.control.updateValueAndValidity(); } } catch (error) { console.error('[RTE] Error in update handler:', error); } }); }, 150); }; // Change-related events this.eventManager.attachMany(['change', 'keyup', 'paste', 'input'], triggerUpdate); // Blur this.eventManager.attach('blur', () => { this.ngZone.run(() => { this.onTouched(); const control = this.ngControl?.control; if (control) { control.markAsTouched(); control.updateValueAndValidity(); } }); }); // Selection change (image toolbar) this.eventManager.attach('selectionchange', () => { setTimeout(() => this.checkImageSelection(), 100); }); } removeAllEventListeners() { this.eventManager?.detachAll(); this.eventManager = null; } updateImageToolbar() { if (this.editorInstance && this.imageToolbarItems) { const hasSlash = this.imageToolbarItems.includes('/'); let imageToolbarString = ''; if (hasSlash) { imageToolbarString = this.imageToolbarItems.join(''); } else { imageToolbarString = `{${this.imageToolbarItems.join(',')}}`; } if (this.editorInstance.config) { this.editorInstance.config.controltoolbar_IMG = imageToolbarString; } try { if (typeof this.editorInstance.setConfig === 'function') { this.editorInstance.setConfig('controltoolbar_IMG', imageToolbarString); } } catch (e) { // Some versions might not have setConfig } } } checkImageSelection() { if (!this.editorInstance || this.isDestroyed) return; try { const iframe = this.editorContainer.nativeElement.querySelector('iframe'); if (iframe?.contentWindow && iframe.contentDocument) { const selection = iframe.contentWindow.getSelection(); if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0); const container = range.commonAncestorContainer; let imgElement = null; if (container.nodeType === Node.ELEMENT_NODE) { imgElement = container.querySelector('img'); } else if (container.parentElement) { imgElement = container.parentElement.closest('img'); } if (imgElement && this.imageToolbarItems) { this.editorInstance.updateToolbar && this.editorInstance.updateToolbar(); } } } } catch (e) { // Ignore errors during selection check } } writeValue(value) { const incomingValue = value ?? this.initialContent ?? ''; this.value = incomingValue; if (this.editorInstance && !this.isDestroyed) { const current = this.editorInstance.getHTMLCode() || ''; if (this.normalizeHtml(current) !== this.normalizeHtml(incomingValue)) { try { this.editorInstance.setHTMLCode(incomingValue); } catch (e) { console.warn('[RTE] Error setting HTML code:', e); } } } } normalizeHtml(html) { return (html || '') .replace(/\u00A0/g, '') .replace(/\s+/g, ' ') .replace(/<br\s*\/?>/gi, '') .replace(/<p>\s*<\/p>/gi, '') .trim(); } registerOnChange(fn) { this.onChange = fn; } registerOnTouched(fn) { this.onTouched = fn; } setDisabledState(isDisabled) { const shouldDisable = isDisabled || this.readonly; if (this.editorInstance?.setReadOnly && !this.isDestroyed) { try { this.editorInstance.setReadOnly(shouldDisable); } catch (e) { console.warn('[RTE] Error setting disabled state:', e); } } } ngOnDestroy() { this.isDestroyed = true; // Clear timers if (this.changeTimer) { clearTimeout(this.changeTimer); } if (this.domCleanupTimer) { clearTimeout(this.domCleanupTimer); } // Clear service reference this.rteService.clearCurrentEditor(); // Schedule cleanup outside Angular zone this.ngZone.runOutsideAngular(() => { // Immediate cleanup attempt this.performCleanup(); // Schedule additional cleanup attempts this.domCleanupTimer = setTimeout(() => { this.performCleanup(); }, 100); }); } performCleanup() { // Remove event listeners this.removeAllEventListeners(); // Clean up editor instance if (this.editorInstance) { try { if (typeof this.editorInstance.destroy === 'function') { this.editorInstance.destroy(); } } catch (error) { // Silently ignore destroy errors } this.editorInstance = null; } // Clean up floating panels with safe DOM manipulation safeCleanupFloatingPanels(); } validate(control) { const value = control?.value || ''; const isEmpty = isTrulyEmpty(value); if (hasRequiredValidator(control) && isEmpty) { return { required: true }; } return null; } fixCharacterCount() { if (!this.editorInstance || this.isDestroyed) return; try { const html = this.editorInstance.getHTMLCode() || ''; const div = document.createElement('div'); div.innerHTML = html; const text = div.textContent || ''; const count = text.replace(/\u00A0/g, '').trim().length; const counter = this.editorContainer.nativeElement.querySelector('.character-count'); if (counter) { counter.textContent = `characters: ${count}`; } } catch (e) { // Ignore character count errors } } getCharacterCount() { try { const html = this.editorInstance?.getHTMLCode?.() || ''; const div = document.createElement('div'); div.innerHTML = html; const text = div.textContent || ''; return text.replace(/\u00A0/g, '').trim().length; } catch (e) { return 0; } } get showError() { const control = this.ngControl?.control; if (!control) return false; const isRequired = hasRequiredValidator(control); return !!(control.invalid && control.touched && (isRequired || control.errors?.['required'])); } get currentErrorMessage() { const errors = this.ngControl?.control?.errors; if (!errors) return null; const firstKey = Object.keys(errors)[0]; return this.errorMessages[firstKey] || 'Invalid field'; } getMobileExpandedToolbar() { const basicMobileTools = [ 'paragraphs:dropdown', 'paragraphs:toggle', 'fontname:toggle', 'fontsize:toggle', 'bold', 'italic', 'underline', 'fontname', 'fontsize', 'insertlink', 'insertemoji', 'insertimage', 'insertvideo', 'removeformat', 'code', 'toggleborder', 'fullscreenenter', 'fullscreenexit', 'undo', 'redo', 'togglemore', 'fontname:dropdown', 'fontsize:dropdown', ]; if (this.rtePreset && RTE_TOOLBAR_PRESETS[this.rtePreset]) { let fullToolbar = RTE_TOOLBAR_PRESETS[this.rtePreset]; for (const tool of basicMobileTools) { const escapedTool = tool.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const toolPattern = new RegExp(`\\b${escapedTool}\\b`, 'g'); fullToolbar = fullToolbar.replace(toolPattern, ''); } // 🧼 Clean both desktop/mobile exclusions fullToolbar = this.excludeToolbarItems(fullToolbar); return fullToolbar || this.getDefaultMobileExpandedToolbar(); } return this.getDefaultMobileExpandedToolbar(); } getDefaultMobileExpandedToolbar() { return `{strike,subscript,superscript}|{forecolor,backcolor}| {justifyleft,justifycenter,justifyright,justifyfull}| {insertorderedlist,insertunorderedlist}|{outdent,indent}| {inserthorizontalrule,insertblockquote,inserttable}| {cut,copy,paste,pastetext,pasteword}| {find,replace}|{selectall,print,spellcheck}|{help}`; } prepareConfiguration() { const baseConfig = { ...this.config }; if (!baseConfig.height) { baseConfig.height = 300; } const enhancedConfig = { ...baseConfig, license: this.globalLicenseKey || this.licenseKey, enableObjectResizing: true, enableImageUpload: this.enableImageUpload, enableVideoEmbed: this.enableVideoEmbed, file_upload_handler: (file, callback, optionalIndex, optionalFiles) => { const wrappedCallback = (url, errorCode) => { if (!url) { // 🚨 Upload failed — clean up placeholder this.rteService.removeLastPlaceholderImage(); console.warn('[RTE] Upload failed. Placeholder removed.'); } callback(url, errorCode); }; this.fileUploadHandler(file, wrappedCallback, optionalIndex, optionalFiles); }, content_changed_callback: () => this.fixCharacterCount(), showFloatingToolbar: false, forceDesktopMode: true, disableMobileMode: true, toolbarModeViewport: 'always-desktop', showBottomToolbar: false, contentCssUrl: '', toolbarMobile: 'basic', subtoolbar_more_mobile: this.getMobileExpandedToolbar(), showControlBoxOnImageSelection: true, enableImageFloatStyle: true, contentCSSText: ` /* Custom styles */ body { overflow-y: hidden; padding: 0px; margin: 0px; } body, table, p, div { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; color: #414141; font-size: 14px; line-height: 1.6; } img { cursor: default; } `, }; if (this.imageToolbarItems && Array.isArray(this.imageToolbarItems)) { const hasSlash = this.imageToolbarItems.includes('/'); let imageToolbarString = ''; if (hasSlash) { imageToolbarString = this.imageToolbarItems.join(''); } else { imageToolbarString = `{${this.imageToolbarItems.join(',')}}`; } enhancedConfig.controltoolbar_IMG = imageToolbarString; enhancedConfig.imagecontrolbar = imageToolbarString; enhancedConfig.image_toolbar = imageToolbarString; } if (this.rtePreset && RTE_TOOLBAR_PRESETS[this.rtePreset]) { let fullToolbar = RTE_TOOLBAR_PRESETS[this.rtePreset]; fullToolbar = this.excludeToolbarItems(fullToolbar); enhancedConfig.toolbar = 'custom'; enhancedConfig.toolbar_custom = fullToolbar; } return enhancedConfig; } _applyCustomStyles() { if (!document.getElementById('rte-consistent-toolbar-styles')) { const styleEl = document.createElement('style'); styleEl.id = 'rte-consistent-toolbar-styles'; styleEl.innerHTML = ` /* Custom mobile styles to fix toolbar */ @media (max-width: 992px) { .rte-toolbar-desktop, .rte-toolbar { display: flex !important; flex-wrap: wrap !important; overflow-x: auto !important; white-space: nowrap !important; -webkit-overflow-scrolling: touch !important; max-width: 100% !important; padding: 4px 0 !important; } .rte-toolbar button, .rte-toolbar .rte-dropdown { flex-shrink: 0 !important; min-width: 28px !important; height: 28px !important; margin: 2px !important; } .rte-toolbar-desktop { display: flex !important; } } /* Force image toolbar visibility */ .rte-image-controlbox { display: block !important; opacity: 1 !important; visibility: visible !important; } /* Prevent orphaned floating panels */ rte-floatpanel { z-index: 10000; } `; document.head.appendChild(styleEl); } } insertContentAtCursor(content) { if (this.readonly || this.isDestroyed) return; try { const iframe = this.editorContainer.nativeElement.querySelector('iframe'); if (!iframe?.contentWindow || !iframe.contentDocument) { console.warn('[RTE] iframe not found or inaccessible'); return; } const iframeDoc = iframe.contentDocument; const editableBody = iframeDoc.body; if (!editableBody?.isContentEditable) { console.warn('[RTE] iframe body is not editable'); return; } editableBody.focus(); iframeDoc.execCommand('insertHTML', false, content); const html = this.editorInstance.getHTMLCode(); this.value = html; this.ngZone.run(() => { this.onChange(html); this.onTouched(); if (this.ngControl?.control) { this.ngControl.control.setValue(html, { emitEvent: false }); this.ngControl.control.updateValueAndValidity(); } }); } catch (error) { console.error('[RTE] Failed to inject content into iframe:', error); } } hideAllFloatPanels() { safeCleanupFloatingPanels(); } excludeToolbarItems(toolbar) { if (!toolbar || !this.excludedToolbarItems?.length) return toolbar; for (const tool of this.excludedToolbarItems) { const escapedTool = tool.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const toolPattern = new RegExp(`\\b${escapedTool}\\b`, 'g'); toolbar = toolbar.replace(toolPattern, ''); } return cleanToolbarString(toolbar); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: RichTextEditorComponent, deps: [{ token: i0.Injector }, { token: RichTextEditorService }, { token: i0.ChangeDetectorRef }, { token: i0.NgZone }, { token: RTE_LICENSE_KEY, optional: true }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: RichTextEditorComponent, selector: "lib-rich-text-editor", inputs: { licenseKey: "licenseKey", config: "config", rtePreset: "rtePreset", imageToolbarItems: "imageToolbarItems", excludedToolbarItems: "excludedToolbarItems", initialContent: "initialContent", errorMessages: "errorMessages", fileUploadHandler: "fileUploadHandler", enableImageUpload: "enableImageUpload", enableVideoEmbed: "enableVideoEmbed", readonly: "readonly" }, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RichTextEditorComponent), multi: true, }, { provide: NG_VALIDATORS, useExisting: forwardRef(() => RichTextEditorComponent), multi: true, }, ], viewQueries: [{ propertyName: "editorContainer", first: true, predicate: ["editorContainer"], descendants: true, static: true }], ngImport: i0, template: ` <div #editorContainer [class.invalid]="showError"></div> <div class="error-message" *ngIf="showError"> {{ currentErrorMessage }} </div> `, isInline: true, styles: [":host{display:block;position:relative}.invalid{border:1px solid red}.error-message{color:red;font-size:12px;margin-top:4px}\n"], dependencies: [{ kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: RichTextEditorComponent, decorators: [{ type: Component, args: [{ selector: 'lib-rich-text-editor', template: ` <div #editorContainer [class.invalid]="showError"></div> <div class="error-message" *ngIf="showError"> {{ currentErrorMessage }} </div> `, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RichTextEditorComponent), multi: true, }, { provide: NG_VALIDATORS, useExisting: forwardRef(() => RichTextEditorComponent), multi: true, }, ], styles: [":host{display:block;position:relative}.invalid{border:1px solid red}.error-message{color:red;font-size:12px;margin-top:4px}\n"] }] }], ctorParameters: () => [{ type: i0.Injector }, { type: RichTextEditorService }, { type: i0.ChangeDetectorRef }, { type: i0.NgZone }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [RTE_LICENSE_KEY] }] }], propDecorators: { editorContainer: [{ type: ViewChild, args: ['editorContainer', { static: true }] }], licenseKey: [{ type: Input }], config: [{ type: Input }], rtePreset: [{ type: Input }], imageToolbarItems: [{ type: Input }], excludedToolbarItems: [{ type: Input }], initialContent: [{ type: Input }], errorMessages: [{ type: Input }], fileUploadHandler: [{ type: Input }], enableImageUpload: [{ type: Input }], enableVideoEmbed: [{ type: Input }], readonly: [{ type: Input }] } }); class RichTextEditorModule { static forRoot(licenseKey) { return { ngModule: RichTextEditorModule, providers: [ { provide: RTE_LICENSE_KEY, useValue: licenseKey } ] }; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: RichTextEditorModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.2.13", ngImport: i0, type: RichTextEditorModule, declarations: [RichTextEditorComponent], imports: [CommonModule], exports: [RichTextEditorComponent] }); static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: RichTextEditorModule, imports: [CommonModule] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: RichTextEditorModule, decorators: [{ type: NgModule, args: [{ declarations: [RichTextEditorComponent], imports: [CommonModule], exports: [RichTextEditorComponent] }] }] }); /* * Public API Surface of rich-text-editor */ /** * Generated bundle index. Do not edit. */ export { DEFAULT_RICHTEXTEDITOR_ASSETS_PATH, RICHTEXTEDITOR_ASSETS_PATH, RTE_LICENSE_KEY, RTE_TOOLBAR_PRESETS, RichTextEditorComponent, RichTextEditorModule, RichTextEditorService }; //# sourceMappingURL=angular-rich-text-editor.mjs.map