I'm implementing a custom color picker for Quill.js to allow users to select colors beyond the default palette. My solution involves using a native browser element, triggered by a custom 'color-picker' option in the toolbar. I have two main goals: A custom color picker: The user can open the browser's native color picker. Live preview: As the user changes the color in the picker (e.g., by dragging the cursor), the selected text in the Quill editor should update in real-time. My implementation works for applying the final color, but I'm facing a significant issue with the live preview. When listening to the input event on the color picker element, applying the format to the Quill editor causes the cursor to jump to the beginning of the editor with every color change. This makes the live preview unusable. : import { Injectable } from '@angular/core'; import { Quill } from 'quill'; export interface QuillColorPickerConfig { defaultColors?: string[]; colorPickerOption?: string; onColorChange?: (content: string, quillInstance: Quill) => void; } @Injectable({ providedIn: 'root' }) export class QuillCustomColorPickerService { private readonly QUILL_DEFAULT_COLORS = [ "#000000", "#e60000", "#ff9900", "#ffff00", "#008a00", "#0066cc", "#9933ff", "#ffffff", "#facccc", "#ffebcc", "#ffffcc", "#cce8cc", "#cce0f5", "#ebd6ff", "#bbbbbb", "#f06666", "#ffc266", "#ffff66", "#66b966", "#66a3e0", "#c285ff", "#888888", "#a10000", "#b26b00", "#b2b200", "#006100", "#0047b2", "#6b24b2", "#444444", "#5c0000", "#663d00", "#666600", "#003700", "#002966", "#3d1466" ]; private currentQuillInstance: Quill | null = null; private currentSelection: any = null; /** * Get default toolbar options with custom color picker */ getToolbarOptions(config?: QuillColorPickerConfig): any[] { const colors = config?.defaultColors || this.QUILL_DEFAULT_COLORS; const colorPickerOption = config?.colorPickerOption || 'color-picker'; const allCustomColors = [...colors, colorPickerOption]; return [ ['bold', 'italic', 'underline', 'strike'], ['blockquote', 'code-block'], [{ 'header': 1 }, { 'header': 2 }], [{ 'list': 'ordered' }, { 'list': 'bullet' }], [{ 'script': 'sub' }, { 'script': 'super' }], [{ 'indent': '-1' }, { 'indent': '+1' }], [{ 'direction': 'rtl' }], [{ 'size': ['small', false, 'large', 'huge'] }], [{ 'header': [1, 2, 3, 4, 5, 6, false] }], [{ 'color': allCustomColors }, { 'background': allCustomColors }], [{ 'font': [] }], [{ 'align': [] }], ['clean'], ['link'] ]; } /** * Setup custom color picker for a Quill editor instance */ setupCustomColorPicker(quillEditor: Quill, config?: QuillColorPickerConfig): void { if (!quillEditor) { console.warn('QuillCustomColorPickerService: Quill editor instance is required'); return; } this.currentQuillInstance = quillEditor; const toolbar = quillEditor.getModule('toolbar'); const colorPickerOption = config?.colorPickerOption || 'color-picker'; // Setup color handler toolbar.addHandler('color', (value: string) => { this.handleColorChange(quillEditor, value, 'color', colorPickerOption, config); }); // Setup background color handler toolbar.addHandler('background', (value: string) => { this.handleColorChange(quillEditor, value, 'background', colorPickerOption, config); }); } private handleColorChange( quillEditor: Quill, value: string, type: 'color' | 'background', colorPickerOption: string, config?: QuillColorPickerConfig ): void { // Store current selection this.currentSelection = quillEditor.getSelection(true); if (value === colorPickerOption) { // Show custom color picker this.showColorPicker(quillEditor, type, config); } else { // Apply predefined color this.applyColor(quillEditor, value, type, config); } } private showColorPicker( quillEditor: Quill, type: 'color' | 'background', config?: QuillColorPickerConfig ): void { // Remove existing color picker if any this.removeExistingColorPicker(); // Create color input element const colorInput = document.createElement('input'); colorInput.type = 'color'; colorInput.id = `quill-${type}-picker`; colorInput.style.position = 'absolute'; colorInput.style.visibility = 'hidden'; colorInput.style.width = '0'; colorInput.style.height = '0'; // Get current color value if text is selected const currentColor = this.getCurrentColor(quillEditor, type); if (currentColor) { colorInput.value = currentColor; } // Add event listeners colorInput.addEventListener('input', (event) => { const target = event.target as HTMLInputElement; this.applyColorWithPreview(quillEditor, target.value, type, config); }); colorInput.addEventListener('change', (event) => { const target = event.target as HTMLInputElement; this.applyColor(quillEditor, target.value, type, config); this.removeExistingColorPicker(); }); colorInput.addEventListener('blur', () => { setTimeout(() => this.removeExistingColorPicker(), 100); }); // Add to DOM and trigger click document.body.appendChild(colorInput); colorInput.click(); } private applyColorWithPreview( quillEditor: Quill, color: string, type: 'color' | 'background', config?: QuillColorPickerConfig ): void { if (!this.currentSelection) return; // Apply color to current selection if (this.currentSelection.length > 0) { quillEditor.setSelection(this.currentSelection.index, this.currentSelection.length); quillEditor.format(type, color); } else { // Set formatting for next input quillEditor.format(type, color); } // Notify color change this.notifyColorChange(quillEditor, config); } private applyColor( quillEditor: Quill, color: string, type: 'color' | 'background', config?: QuillColorPickerConfig ): void { if (!this.currentSelection) return; // Restore selection quillEditor.setSelection(this.currentSelection.index, this.currentSelection.length); if (this.currentSelection.length > 0) { // Apply to selected text quillEditor.formatText(this.currentSelection.index, this.currentSelection.length, type, color); } else { // Set formatting for next input quillEditor.format(type, color); } // Focus back to editor quillEditor.focus(); // Notify color change this.notifyColorChange(quillEditor, config); } private getCurrentColor(quillEditor: Quill, type: 'color' | 'background'): string | null { if (!this.currentSelection || this.currentSelection.length === 0) { return null; } const format = quillEditor.getFormat(this.currentSelection.index, this.currentSelection.length); return format[type] || null; } private removeExistingColorPicker(): void { const existingPickers = document.querySelectorAll('[id^="quill-"][id$="-picker"]'); existingPickers.forEach(picker => picker.remove()); } private notifyColorChange(quillEditor: Quill, config?: QuillColorPickerConfig): void { if (config?.onColorChange) { const htmlContent = quillEditor.root.innerHTML; config.onColorChange(htmlContent, quillEditor); } } /** * Clean up method to remove any remaining color pickers */ cleanup(): void { this.removeExistingColorPicker(); this.currentQuillInstance = null; this.currentSelection = null; } } Continue reading...