Quill color and Cursor issue

Discussão em 'Angular' iniciado por Gulab_786, Agosto 2, 2025.

  1. Gulab_786

    Gulab_786 Guest

    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...

Compartilhe esta Página