# Skill: Frontend JavaScript

## Identity
- **Skill ID**: `frontend-javascript`
- **Domain**: Frontend JavaScript Development
- **Technologies**: Vanilla ES6+, ES Modules, Fetch API
- **Source Agent**: `frontend-js-expert.md`

## When to Load This Skill
- Task involves frontend JavaScript
- Implementing AJAX functionality
- DOM manipulation
- Building interactive UI components
- Files matching: `assets/js/**/*.js`, `admin/src-vanilla/**/*.js`

## Core Patterns

### Global Naming (WordPress.org Compliance)
```javascript
// CORRECT - 4+ character descriptive names
const PresszoneInternationalApp = {};
window.presszoneInternationalData = { ajaxUrl, nonce };

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

### XSS Prevention (CRITICAL)
```javascript
// CORRECT - Always use textContent for user data
element.textContent = userData.name;
container.textContent = apiResponse.message;

// WRONG - XSS vulnerability - NEVER set .innerHTML with user data
```

### Secure AJAX Requests
```javascript
async function submitForm(formData) {
    formData.append('action', 'presszone_international_save');
    formData.append('nonce', window.presszoneInternationalData.nonce);

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

        const result = await response.json();

        if (result.success) {
            showToast(result.data.message, 'success');
        } else {
            showToast(result.data.message || 'Error occurred', 'error');

            // Handle nonce expiration
            if (result.data?.code === 'invalid_nonce') {
                location.reload();
            }
        }
    } catch (error) {
        console.error('Request failed:', error);
        showToast('Network error', 'error');
    }
}
```

### Keyboard Accessibility (MANDATORY)
```javascript
// CORRECT - Always add keyboard support to interactive elements
function makeClickable(element, handler) {
    element.addEventListener('click', handler);
    element.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' || e.key === ' ') {
            e.preventDefault();
            handler(e);
        }
    });

    if (!element.hasAttribute('tabindex')) {
        element.setAttribute('tabindex', '0');
    }
}
```

### Focus Management in Modals
```javascript
class Modal {
    open() {
        this.previousFocus = document.activeElement;
        this.modal.style.display = 'block';
        this.trapFocus();

        const firstFocusable = this.modal.querySelector(
            '[autofocus], button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
        );
        firstFocusable?.focus();
    }

    close() {
        this.modal.style.display = 'none';
        this.previousFocus?.focus();
    }

    trapFocus() {
        this.modal.addEventListener('keydown', (e) => {
            if (e.key === 'Escape') {
                this.close();
                return;
            }

            if (e.key !== 'Tab') return;

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

            if (e.shiftKey && document.activeElement === first) {
                e.preventDefault();
                last.focus();
            } else if (!e.shiftKey && document.activeElement === last) {
                e.preventDefault();
                first.focus();
            }
        });
    }
}
```

### Screen Reader Announcements
```javascript
function announceToScreenReader(message, priority = 'polite') {
    const announcement = document.createElement('div');
    announcement.setAttribute('aria-live', priority);
    announcement.setAttribute('aria-atomic', 'true');
    announcement.className = 'sr-only';
    announcement.textContent = message;

    document.body.appendChild(announcement);
    setTimeout(() => document.body.removeChild(announcement), 1000);
}
```

## Anti-Patterns (Forbidden)

| Mistake | Fix |
|---------|-----|
| Setting .innerHTML with user data | Use `textContent` or `DOMParser` |
| Missing nonce in AJAX | Always include `nonce` in POST data |
| No `credentials: 'same-origin'` | Required for authenticated Fetch |
| Short global names (<4 chars) | Use descriptive 4+ char names |
| Hardcoded strings | Use `wp.i18n.__()` for user-facing text |
| Missing keyboard handlers | Add Enter/Space handlers to custom controls |
| Direct DOM without check | Check if element exists first |
| Dynamic code execution | Never execute arbitrary code strings |

## WordPress.org Compliance

### Script Registration
```php
wp_enqueue_script(
    'presszone-international-frontend',
    plugins_url('assets/js/frontend.js', __FILE__),
    [],
    PRESSZONE_INTERNATIONAL_VERSION,
    true
);

wp_localize_script('presszone-international-frontend', 'presszoneInternationalData', [
    'ajaxUrl' => admin_url('admin-ajax.php'),
    'nonce' => wp_create_nonce('presszone_international_nonce'),
    'i18n' => [
        'error' => __('An error occurred', 'international-press-zone'),
        'success' => __('Operation successful', 'international-press-zone'),
    ]
]);
```

### Translation with wp.i18n
```javascript
const { __, sprintf } = wp.i18n;

const message = __('Settings saved', 'international-press-zone');
const greeting = sprintf(__('Hello, %s', 'international-press-zone'), userName);
```

## Integration with Other Skills
- **Often combined with**: `frontend-styling-scss`, `accessibility-wcag`
- **For AJAX handlers**: Load `wordpress-php-integration`
- **For form validation**: Load `wordpress-security`

## Quick Reference

### DOM Utilities Pattern
```javascript
const qs = (sel, ctx = document) => ctx.querySelector(sel);
const qsa = (sel, ctx = document) => [...ctx.querySelectorAll(sel)];

function el(tag, attrs = {}, ...children) {
    const element = document.createElement(tag);

    Object.entries(attrs).forEach(([key, value]) => {
        if (key === 'class') element.className = value;
        else if (key.startsWith('on')) element[key] = value;
        else if (key.startsWith('data-')) element.setAttribute(key, value);
        else if (key.startsWith('aria-')) element.setAttribute(key, value);
        else element[key] = value;
    });

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

    return element;
}
```

## Validation Checklist
- [ ] All user data uses `textContent`, not .innerHTML
- [ ] AJAX requests include nonce
- [ ] Interactive elements have keyboard support (Enter/Space)
- [ ] Focus managed properly in modals/dynamic content
- [ ] ARIA attributes for screen readers
- [ ] Dynamic changes announced to assistive tech
- [ ] Global identifiers use 4+ character names
- [ ] No dynamic code execution
