# Rich Text Editor Skill

> **Technology:** TinyMCE and custom editor components for comment input

---

## Purpose

This skill covers rich text editor integration, TinyMCE configuration, content filtering, toolbar customization, and editor component patterns for the Comments Press Zone plugin.

---

## Editor Architecture

### Frontend Editor Component

**Location:** `assets/js/components/Editor.js`

The plugin uses a custom vanilla JS editor component that can optionally integrate with TinyMCE.

---

## TinyMCE Configuration

### Initialize TinyMCE for Comments

```php
public function enqueue_tinymce(): void {
    // Only load TinyMCE if enabled in settings
    $settings = get_option('presszone_comments_settings', []);
    if (empty($settings['enable_rich_editor'])) {
        return;
    }
    
    wp_enqueue_editor();
    
    wp_localize_script(
        'presszone-comments-frontend',
        'presszoneCommentsEditorConfig',
        [
            'enabled' => true,
            'settings' => [
                'tinymce' => [
                    'toolbar1' => 'bold,italic,underline,strikethrough,|,bullist,numlist,|,link,unlink,|,undo,redo',
                    'toolbar2' => '',
                    'plugins' => 'lists,link,paste',
                    'menubar' => false,
                    'statusbar' => false,
                    'content_css' => PRESSZONE_COMMENTS_URL . 'assets/css/editor-content.css',
                    'body_class' => 'presszone-comments-editor-content',
                    'paste_as_text' => true,
                    'max_height' => 400,
                    'min_height' => 150,
                ],
            ],
        ]
    );
}
```

---

## Custom Editor Component

### JavaScript Editor Class

```javascript
// assets/js/components/Editor.js
export default class Editor {
    constructor(options = {}) {
        this.options = {
            container: null,
            mode: 'plain', // 'plain' or 'rich'
            placeholder: 'Write your comment...',
            maxLength: 2000,
            onSubmit: null,
            onCancel: null,
            ...options
        };
        
        this.element = null;
        this.textarea = null;
        this.tinymceInstance = null;
        
        this.init();
    }
    
    init() {
        this.render();
        this.bindEvents();
        
        if (this.options.mode === 'rich' && window.tinymce) {
            this.initTinyMCE();
        }
    }
    
    render() {
        this.element = document.createElement('div');
        this.element.className = 'presszone-comments-editor';
        
        // Toolbar
        if (this.options.mode === 'plain') {
            const toolbar = this.createPlainToolbar();
            this.element.appendChild(toolbar);
        }
        
        // Textarea
        this.textarea = document.createElement('textarea');
        this.textarea.className = 'presszone-comments-editor__textarea';
        this.textarea.placeholder = this.options.placeholder;
        this.textarea.maxLength = this.options.maxLength;
        this.element.appendChild(this.textarea);
        
        // Footer (character count, buttons)
        const footer = this.createFooter();
        this.element.appendChild(footer);
        
        // Mount to container
        if (this.options.container) {
            this.options.container.appendChild(this.element);
        }
    }
    
    createPlainToolbar() {
        const toolbar = document.createElement('div');
        toolbar.className = 'presszone-comments-editor__toolbar';
        
        const buttons = [
            { icon: '𝐁', title: 'Bold', tag: 'strong' },
            { icon: '𝐼', title: 'Italic', tag: 'em' },
            { icon: '🔗', title: 'Link', action: 'link' },
        ];
        
        buttons.forEach(btn => {
            const button = document.createElement('button');
            button.type = 'button';
            button.className = 'presszone-comments-editor__btn';
            button.textContent = btn.icon;
            button.title = btn.title;
            button.onclick = () => this.insertTag(btn.tag || btn.action);
            toolbar.appendChild(button);
        });
        
        return toolbar;
    }
    
    createFooter() {
        const footer = document.createElement('div');
        footer.className = 'presszone-comments-editor__footer';
        
        // Character count
        const charCount = document.createElement('span');
        charCount.className = 'presszone-comments-editor__char-count';
        charCount.textContent = `0 / ${this.options.maxLength}`;
        footer.appendChild(charCount);
        
        // Submit button
        const submitBtn = document.createElement('button');
        submitBtn.type = 'button';
        submitBtn.className = 'presszone-comments-btn presszone-comments-btn--primary';
        submitBtn.textContent = 'Post Comment';
        submitBtn.onclick = () => this.submit();
        footer.appendChild(submitBtn);
        
        return footer;
    }
    
    initTinyMCE() {
        const config = window.presszoneCommentsEditorConfig?.settings?.tinymce || {};
        
        wp.editor.initialize(this.textarea, {
            tinymce: {
                ...config,
                setup: (editor) => {
                    this.tinymceInstance = editor;
                    editor.on('change', () => this.updateCharCount());
                }
            },
            quicktags: false,
        });
    }
    
    insertTag(tag) {
        const start = this.textarea.selectionStart;
        const end = this.textarea.selectionEnd;
        const selected = this.textarea.value.substring(start, end);
        const before = this.textarea.value.substring(0, start);
        const after = this.textarea.value.substring(end);
        
        if (tag === 'link') {
            const url = prompt('Enter URL:');
            if (url) {
                const linkText = selected || 'link';
                this.textarea.value = `${before}<a href="${url}">${linkText}</a>${after}`;
            }
        } else {
            this.textarea.value = `${before}<${tag}>${selected}</${tag}>${after}`;
        }
        
        this.updateCharCount();
        this.textarea.focus();
    }
    
    updateCharCount() {
        const content = this.getContent();
        const length = content.length;
        const counter = this.element.querySelector('.presszone-comments-editor__char-count');
        
        if (counter) {
            counter.textContent = `${length} / ${this.options.maxLength}`;
            counter.classList.toggle('warning', length > this.options.maxLength * 0.9);
        }
    }
    
    getContent() {
        if (this.tinymceInstance) {
            return this.tinymceInstance.getContent();
        }
        return this.textarea.value;
    }
    
    setContent(content) {
        if (this.tinymceInstance) {
            this.tinymceInstance.setContent(content);
        } else {
            this.textarea.value = content;
        }
        this.updateCharCount();
    }
    
    clear() {
        this.setContent('');
    }
    
    submit() {
        const content = this.getContent();
        
        if (content.trim().length === 0) {
            alert('Please enter a comment.');
            return;
        }
        
        if (content.length > this.options.maxLength) {
            alert(`Comment is too long. Maximum ${this.options.maxLength} characters.`);
            return;
        }
        
        if (this.options.onSubmit) {
            this.options.onSubmit({
                content,
                mode: this.options.mode
            });
        }
    }
    
    bindEvents() {
        this.textarea.addEventListener('input', () => this.updateCharCount());
        this.textarea.addEventListener('keydown', (e) => {
            // Ctrl+Enter to submit
            if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
                e.preventDefault();
                this.submit();
            }
        });
    }
    
    destroy() {
        if (this.tinymceInstance) {
            wp.editor.remove(this.textarea);
        }
        if (this.element && this.element.parentNode) {
            this.element.parentNode.removeChild(this.element);
        }
    }
}
```

---

## Content Filtering & Sanitization

### Backend Sanitization

```php
public function sanitize_comment_content(string $content): string {
    // Define allowed HTML tags for comments
    $allowed_tags = [
        'p' => [],
        'br' => [],
        'strong' => [],
        'em' => [],
        'u' => [],
        'a' => [
            'href' => true,
            'title' => true,
            'rel' => true,
        ],
        'ul' => [],
        'ol' => [],
        'li' => [],
        'blockquote' => [],
        'code' => [],
    ];
    
    // Sanitize with allowed tags
    $content = wp_kses($content, $allowed_tags);
    
    // Auto-add nofollow to links
    $content = wp_rel_nofollow($content);
    
    return $content;
}
```

### Apply Filter Hook

```php
add_filter('preprocess_comment', function($commentdata) {
    $commentdata['comment_content'] = $this->sanitize_comment_content(
        $commentdata['comment_content']
    );
    return $commentdata;
});
```

---

## Paste Handling

### Strip Formatting on Paste

```javascript
textarea.addEventListener('paste', (e) => {
    e.preventDefault();
    
    // Get plain text from clipboard
    const text = (e.clipboardData || window.clipboardData).getData('text/plain');
    
    // Insert at cursor position
    const start = textarea.selectionStart;
    const end = textarea.selectionEnd;
    const before = textarea.value.substring(0, start);
    const after = textarea.value.substring(end);
    
    textarea.value = before + text + after;
    
    // Update cursor position
    textarea.selectionStart = textarea.selectionEnd = start + text.length;
    
    // Trigger input event for character count update
    textarea.dispatchEvent(new Event('input'));
});
```

---

## Media Embedding (Optional)

### Image Upload Button

```javascript
createImageButton() {
    const button = document.createElement('button');
    button.type = 'button';
    button.className = 'presszone-comments-editor__btn';
    button.textContent = '🖼️';
    button.title = 'Insert Image';
    button.onclick = () => this.openMediaLibrary();
    return button;
}

openMediaLibrary() {
    if (!wp.media) return;
    
    const frame = wp.media({
        title: 'Select Image',
        button: { text: 'Insert' },
        multiple: false,
        library: { type: 'image' }
    });
    
    frame.on('select', () => {
        const attachment = frame.state().get('selection').first().toJSON();
        this.insertImage(attachment.url, attachment.alt);
    });
    
    frame.open();
}

insertImage(url, alt = '') {
    const img = `<img src="${url}" alt="${alt}" />`;
    
    if (this.tinymceInstance) {
        this.tinymceInstance.insertContent(img);
    } else {
        const start = this.textarea.selectionStart;
        const before = this.textarea.value.substring(0, start);
        const after = this.textarea.value.substring(start);
        this.textarea.value = before + img + after;
    }
}
```

---

## Editor Configuration Options

```php
public function get_editor_config(): array {
    $settings = get_option('presszone_comments_settings', []);
    
    return [
        'enabled' => $settings['enable_rich_editor'] ?? false,
        'mode' => $settings['editor_mode'] ?? 'plain', // 'plain' or 'rich'
        'max_length' => $settings['max_comment_length'] ?? 2000,
        'allowed_tags' => ['strong', 'em', 'u', 'a', 'ul', 'ol', 'li', 'blockquote', 'code'],
        'allow_images' => $settings['allow_comment_images'] ?? false,
        'toolbar' => $settings['editor_toolbar'] ?? 'basic', // 'basic' or 'full'
    ];
}
```

---

## Common Mistakes to Avoid

| Mistake | Fix |
|---------|-----|
| Not sanitizing editor content | Always use `wp_kses()` with allowed tags |
| Allowing all HTML tags | Restrict to safe subset |
| Not handling paste events | Strip formatting or use `paste_as_text` |
| Missing character limit | Enforce max length client and server side |
| Not cleaning up TinyMCE | Call `wp.editor.remove()` on destroy |
| Using `innerHTML` with content | Use `textContent` or DOMParser |

---

## Testing Checklist

- [ ] Plain text editor works
- [ ] Rich text editor (TinyMCE) initializes
- [ ] Toolbar buttons insert correct HTML
- [ ] Character count updates in real-time
- [ ] Max length enforced
- [ ] Paste strips formatting
- [ ] Content sanitized on backend
- [ ] Only allowed HTML tags pass through
- [ ] Links auto-get nofollow attribute
- [ ] Editor destroys cleanly (no memory leaks)
