# JavaScript Development Skill

> **Technology:** Vanilla ES6+ JavaScript (NO frameworks) for frontend interactions

---

## Purpose

This skill covers frontend JavaScript patterns, DOM manipulation, AJAX communication, and XSS prevention for the Comments Press Zone plugin.

---

## Tech Stack

| Technology | Details |
|------------|---------|
| **JavaScript** | Vanilla ES6+ (NO jQuery, NO frameworks) |
| **Pattern** | IIFE for entry points + ES Modules for components |
| **Imports** | Dynamic `import()` for lazy loading |
| **DOM** | Direct manipulation with event delegation |
| **i18n** | WordPress `wp.i18n` integration |

---

## WordPress.org Compliance

### Global Naming (4+ Characters REQUIRED)

```javascript
// ✅ CORRECT
const CommentsPresszoneApp = {};
window.presszoneCommentsData = { ajaxUrl, nonce };
window.presszoneCommentsAdmin = {};

// ❌ FORBIDDEN - TOO SHORT
const PZ = {};
window.pzData = {};
```

---

## Security - XSS Prevention (CRITICAL)

### NEVER use innerHTML with user data

```javascript
// ✅ CORRECT - Use textContent for user data
element.textContent = userInput;
nameElement.textContent = comment.author_name;

// ❌ FORBIDDEN - XSS vulnerability
element.innerHTML = userInput;
```

### Use DOMParser for trusted HTML

```javascript
// ✅ CORRECT - Parse server HTML response
const parser = new DOMParser();
const doc = parser.parseFromString(htmlString, 'text/html');
const content = doc.body.firstChild;
container.appendChild(content);
```

### Create elements safely

```javascript
// ✅ CORRECT - Build DOM programmatically
const button = document.createElement('button');
button.className = 'presszone-comments-btn';
button.textContent = 'Vote'; // Safe
button.setAttribute('data-id', commentId);
button.onclick = handleVote;
```

---

## Module Architecture

### Entry Point (IIFE)

```javascript
// assets/js/frontend.js
(function() {
    'use strict';

    class CommentsPresszoneApp {
        constructor() {
            this.config = window.presszoneCommentsData || {};
            this.init();
        }

        async init() {
            await this.loadComponents();
            this.bindEvents();
        }

        async loadComponents() {
            // Dynamic imports for code splitting
            const { default: Editor } = await import('./components/Editor.js');
            this.editor = new Editor({
                onSubmit: (data) => this.handleSubmit(data)
            });
        }

        bindEvents() {
            document.addEventListener('click', (e) => {
                if (e.target.closest('.presszone-comments-vote-btn')) {
                    this.handleVote(e);
                }
            });
        }
    }

    // Initialize when DOM ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
            window.CommentsPresszoneApp = new CommentsPresszoneApp();
        });
    } else {
        window.CommentsPresszoneApp = new CommentsPresszoneApp();
    }
})();
```

### ES Module Component

```javascript
// assets/js/components/Editor.js
export default class Editor {
    constructor(options = {}) {
        this.options = options;
        this.element = null;
        this.init();
    }

    init() {
        this.render();
        this.bindEvents();
    }

    render() {
        // Build UI
    }

    bindEvents() {
        // Attach event listeners
    }
}
```

---

## AJAX Communication Patterns

### Fetch API with AJAX

```javascript
async function submitComment(formData) {
    // Add action and nonce
    formData.append('action', 'presszone_comments_submit');
    formData.append('nonce', window.presszoneCommentsData.nonce);

    try {
        const response = await fetch(window.presszoneCommentsData.ajaxUrl, {
            method: 'POST',
            body: formData,
            credentials: 'same-origin' // Required for cookies
        });

        const result = await response.json();

        if (result.success) {
            this.showToast(result.data.message, 'success');
            this.updateUI(result.data);
        } else {
            this.showToast(result.data.message, 'error');
        }
    } catch (error) {
        console.error('AJAX Error:', error);
        this.showToast('Network error occurred', 'error');
    }
}
```

### Vote Action

```javascript
async handleVote(commentId, type) {
    const formData = new FormData();
    formData.append('action', 'presszone_comments_vote');
    formData.append('nonce', this.config.nonce);
    formData.append('comment_id', commentId);
    formData.append('type', type);

    try {
        const response = await fetch(this.config.ajaxUrl, {
            method: 'POST',
            body: formData,
            credentials: 'same-origin'
        });

        const result = await response.json();

        if (result.success) {
            this.updateVoteUI(commentId, result.data);
        }
    } catch (error) {
        console.error('Vote error:', error);
    }
}
```

---

## DOM Manipulation

### Query Selectors

```javascript
// Single element
const container = document.querySelector('#presszone-comments-container');
const button = document.querySelector('.presszone-comments-submit-btn');

// Multiple elements
const comments = document.querySelectorAll('.presszone-comments-item');

// Check existence before accessing
if (container) {
    container.classList.add('active');
}
```

### Creating Elements

```javascript
// Helper function
function createElement(tag, attrs = {}, children = []) {
    const element = document.createElement(tag);
    
    Object.entries(attrs).forEach(([key, value]) => {
        if (key === 'className') {
            element.className = value;
        } else if (key === 'textContent') {
            element.textContent = value;
        } else if (key.startsWith('on')) {
            element[key] = value;
        } else {
            element.setAttribute(key, value);
        }
    });

    children.forEach(child => {
        if (typeof child === 'string') {
            element.appendChild(document.createTextNode(child));
        } else {
            element.appendChild(child);
        }
    });

    return element;
}

// Usage
const button = createElement('button', {
    className: 'presszone-comments-btn',
    textContent: 'Submit',
    onclick: handleSubmit
});
```

---

## Event Handling

### Event Delegation

```javascript
// Attach to parent, handle child clicks
document.addEventListener('click', (e) => {
    const voteBtn = e.target.closest('.presszone-comments-vote-btn');
    if (voteBtn) {
        e.preventDefault();
        const commentId = voteBtn.dataset.commentId;
        const type = voteBtn.dataset.type;
        this.handleVote(commentId, type);
    }
});
```

### Custom Events

```javascript
// Dispatch custom event
const event = new CustomEvent('presszone:comments:submitted', {
    detail: { commentId: 123 }
});
document.dispatchEvent(event);

// Listen for custom event
document.addEventListener('presszone:comments:submitted', (e) => {
    console.log('Comment submitted:', e.detail.commentId);
});
```

---

## Accessibility (Keyboard Support)

### Make clickable elements keyboard accessible

```javascript
button.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        handleAction();
    }
});

// Or use native button elements (automatically accessible)
const button = document.createElement('button');
button.onclick = handleAction; // Works for both click and keyboard
```

### Focus Management in Modals

```javascript
class Modal {
    open() {
        this.previousFocus = document.activeElement;
        this.element.style.display = 'block';
        this.element.querySelector('.presszone-comments-modal__close').focus();
        this.trapFocus();
    }

    close() {
        this.element.style.display = 'none';
        if (this.previousFocus) {
            this.previousFocus.focus();
        }
    }

    trapFocus() {
        const focusable = this.element.querySelectorAll(
            'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
        );
        const first = focusable[0];
        const last = focusable[focusable.length - 1];

        this.element.addEventListener('keydown', (e) => {
            if (e.key === 'Tab') {
                if (e.shiftKey && document.activeElement === first) {
                    e.preventDefault();
                    last.focus();
                } else if (!e.shiftKey && document.activeElement === last) {
                    e.preventDefault();
                    first.focus();
                }
            } else if (e.key === 'Escape') {
                this.close();
            }
        });
    }
}
```

---

## Translation (i18n)

```javascript
// Using WordPress i18n
const { __ } = wp.i18n;

const message = __('Comment submitted successfully', 'comments-press-zone');
const plural = _n('1 comment', '%d comments', count, 'comments-press-zone');
```

---

## Admin SPA Patterns

### Router

```javascript
class AdminRouter {
    constructor() {
        this.routes = {
            '': this.renderDashboard,
            '#/': this.renderDashboard,
            '#/moderation': this.renderModeration,
            '#/settings': this.renderSettings,
        };
        this.init();
    }

    init() {
        window.addEventListener('hashchange', () => this.route());
        this.route();
    }

    route() {
        const hash = window.location.hash;
        const handler = this.routes[hash] || this.routes[''];
        handler.call(this);
    }
}
```

---

## Common Patterns

### Debounce

```javascript
function debounce(func, wait) {
    let timeout;
    return function(...args) {
        clearTimeout(timeout);
        timeout = setTimeout(() => func.apply(this, args), wait);
    };
}

// Usage
const searchInput = document.querySelector('#search');
searchInput.addEventListener('input', debounce((e) => {
    performSearch(e.target.value);
}, 300));
```

### Throttle

```javascript
function throttle(func, limit) {
    let inThrottle;
    return function(...args) {
        if (!inThrottle) {
            func.apply(this, args);
            inThrottle = true;
            setTimeout(() => inThrottle = false, limit);
        }
    };
}
```

---

## Common Mistakes to Avoid

| Mistake | Fix |
|---------|-----|
| Using `innerHTML` with user data | Use `textContent` instead |
| Missing `credentials: 'same-origin'` in fetch | Required for authenticated requests |
| Short global names (<4 chars) | Use 4+ character names |
| No keyboard handlers on custom focusable elements | Add Enter/Space key handlers |
| Not restoring focus after modal close | Save and restore `document.activeElement` |
| Forgetting text domain in i18n | Always include `'comments-press-zone'` |
| Not checking element existence | Always check `if (element)` before accessing |

---

## Testing Checklist

- [ ] No `innerHTML` with user data
- [ ] All AJAX includes nonce
- [ ] All global names are 4+ characters
- [ ] Keyboard navigation works
- [ ] Focus management in modals
- [ ] All strings translated
- [ ] Console has no errors
- [ ] Works without JS enabled (progressive enhancement where possible)
