# SortableList Component

A fully-featured drag-and-drop sortable list component with keyboard navigation, touch support, and accessibility features.

## Features

- **Mouse Drag-and-Drop**: Full mouse drag-and-drop support with visual feedback
- **Touch Support**: Native touch events for mobile devices
- **Keyboard Navigation**: Complete keyboard control with arrow keys and space bar
- **Accessibility**: Full ARIA attributes and screen reader support
- **Visual Feedback**: Ghost elements, placeholders, and smooth animations
- **Customizable**: Custom item renderer and styling options
- **Loading & Empty States**: Built-in states for loading and empty lists
- **Disabled State**: Can be disabled to prevent interaction

## Installation

```javascript
import SortableList from './components/SortableList.js';
```

Make sure to include the styles:

```scss
@import './styles/components/sortable-list';
```

## Basic Usage

```javascript
// Create a sortable list
const sortable = new SortableList({
    container: '#language-list',
    items: [
        { id: 1, name: 'English' },
        { id: 2, name: 'Spanish' },
        { id: 3, name: 'French' }
    ],
    onChange: (newOrder) => {
        console.log('New order:', newOrder);
        // Save order to server
    }
});
```

## API Reference

### Constructor Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `container` | `string\|Element` | **required** | Container element or selector |
| `items` | `Array<Object>` | `[]` | Array of items (must have `id` property) |
| `onChange` | `Function` | `() => {}` | Callback when order changes |
| `renderItem` | `Function` | default | Custom item renderer |
| `disabled` | `boolean` | `false` | Disabled state |
| `loading` | `boolean` | `false` | Loading state |
| `emptyMessage` | `string` | 'No items' | Message when list is empty |

### Methods

#### `render()`
Re-renders the list. Called automatically when state changes.

```javascript
sortable.render();
```

#### `setItems(items)`
Updates the items and re-renders.

```javascript
sortable.setItems([
    { id: 1, name: 'New Item 1' },
    { id: 2, name: 'New Item 2' }
]);
```

#### `getItems()`
Returns the current items in their current order.

```javascript
const currentOrder = sortable.getItems();
```

#### `setDisabled(disabled)`
Sets the disabled state.

```javascript
sortable.setDisabled(true);  // Disable interactions
sortable.setDisabled(false); // Enable interactions
```

#### `setLoading(loading)`
Sets the loading state.

```javascript
sortable.setLoading(true);  // Show loading spinner
sortable.setLoading(false); // Hide loading spinner
```

#### `destroy()`
Cleans up and removes the component.

```javascript
sortable.destroy();
```

## Custom Item Renderer

You can customize how each item is rendered:

```javascript
const sortable = new SortableList({
    container: '#language-list',
    items: languages,
    renderItem: (item) => {
        // Return a string
        return `
            <div class="language-item">
                <img src="${item.flag}" alt="${item.name}" />
                <span class="language-name">${item.name}</span>
                <span class="language-code">${item.code}</span>
            </div>
        `;
    },
    onChange: handleOrderChange
});
```

Or return a DOM element:

```javascript
import { el } from '../utils/dom.js';

const sortable = new SortableList({
    container: '#language-list',
    items: languages,
    renderItem: (item) => {
        return el('div', { class: 'language-item' },
            el('img', { src: item.flag, alt: item.name }),
            el('span', { class: 'language-name' }, item.name),
            el('span', { class: 'language-code' }, item.code)
        );
    },
    onChange: handleOrderChange
});
```

## Complete Examples

### Example 1: Language Ordering

```javascript
import SortableList from './components/SortableList.js';
import { el } from './utils/dom.js';

// Language data
const languages = [
    { id: 1, name: 'English', code: 'en', flag: '🇬🇧' },
    { id: 2, name: 'Spanish', code: 'es', flag: '🇪🇸' },
    { id: 3, name: 'French', code: 'fr', flag: '🇫🇷' },
    { id: 4, name: 'German', code: 'de', flag: '🇩🇪' }
];

// Create sortable list
const languageList = new SortableList({
    container: '#language-priority-list',
    items: languages,
    renderItem: (lang) => {
        return el('div', { class: 'language-item' },
            el('span', { class: 'language-flag' }, lang.flag),
            el('div', { class: 'language-info' },
                el('div', { class: 'language-name' }, lang.name),
                el('div', { class: 'language-code' }, lang.code)
            )
        );
    },
    onChange: async (newOrder) => {
        // Save to server
        try {
            const response = await fetch('/wp-admin/admin-ajax.php', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                },
                body: new URLSearchParams({
                    action: 'update_language_order',
                    nonce: mpzData.nonce,
                    order: JSON.stringify(newOrder.map(item => item.id))
                })
            });

            const data = await response.json();

            if (data.success) {
                console.log('Order saved successfully');
            }
        } catch (error) {
            console.error('Failed to save order:', error);
        }
    }
});
```

### Example 2: Loading Data Asynchronously

```javascript
const sortable = new SortableList({
    container: '#language-list',
    items: [],
    loading: true,
    onChange: handleOrderChange
});

// Load data
async function loadLanguages() {
    try {
        const response = await fetch('/wp-admin/admin-ajax.php?action=get_languages');
        const data = await response.json();

        sortable.setLoading(false);
        sortable.setItems(data.languages);
    } catch (error) {
        console.error('Failed to load languages:', error);
        sortable.setLoading(false);
    }
}

loadLanguages();
```

### Example 3: Disabled State

```javascript
const sortable = new SortableList({
    container: '#language-list',
    items: languages,
    disabled: false,
    onChange: handleOrderChange
});

// Disable while saving
async function saveOrder(order) {
    sortable.setDisabled(true);

    try {
        await fetch('/api/save-order', {
            method: 'POST',
            body: JSON.stringify(order)
        });
    } finally {
        sortable.setDisabled(false);
    }
}
```

## Keyboard Navigation

The component supports full keyboard navigation:

| Key | Action |
|-----|--------|
| `Tab` | Move focus between items |
| `Space` | Grab/drop the focused item |
| `ArrowUp` | Move grabbed item up (when grabbed) |
| `ArrowDown` | Move grabbed item down (when grabbed) |
| `Escape` | Cancel grab operation |

### Keyboard Usage Flow

1. Press `Tab` to focus an item
2. Press `Space` to grab it
3. Press `ArrowUp`/`ArrowDown` to move it
4. Press `Space` again to drop it in the new position
5. Or press `Escape` to cancel and return to original position

## Touch Support

The component has full touch support for mobile devices:

- **Touch and Hold**: Touch an item to start dragging
- **Drag**: Move your finger to drag the item
- **Release**: Release to drop the item in the new position

Visual feedback is provided throughout the drag operation.

## Accessibility

The component is fully accessible:

- **ARIA Attributes**: Proper `role`, `aria-label`, `aria-grabbed` attributes
- **Screen Reader Announcements**: Live announcements for drag operations
- **Keyboard Navigation**: Full keyboard control
- **Focus Management**: Proper focus states and indicators
- **High Contrast Mode**: Enhanced visibility in high contrast mode
- **Reduced Motion**: Respects `prefers-reduced-motion` for users who need it

## Styling

The component includes comprehensive styles with:

- **Light & Dark Mode**: Automatic theme support
- **Smooth Animations**: Visual feedback with smooth transitions
- **Touch Optimizations**: Larger touch targets on mobile
- **Ghost Elements**: Visual feedback during drag
- **Placeholders**: Drop zone indicators
- **Loading State**: Spinner animation
- **Empty State**: Styled empty message

### Custom Styling

You can override styles using CSS:

```css
/* Custom item styles */
.mpz-sortable-item {
    padding: 20px;
    background: #f5f5f5;
}

/* Custom handle color */
.mpz-sortable-item__handle {
    color: #0073aa;
}

/* Custom ghost opacity */
.mpz-sortable-item__ghost {
    opacity: 0.5;
}
```

## Browser Support

- Chrome/Edge: ✅ Full support
- Firefox: ✅ Full support
- Safari: ✅ Full support
- Mobile Safari: ✅ Full support (touch)
- Chrome Android: ✅ Full support (touch)

## Performance

The component is optimized for performance:

- Minimal DOM manipulation
- Efficient event handling
- No external dependencies
- Smooth 60fps animations
- Works with large lists (100+ items)

## Troubleshooting

### Items not dragging

Make sure the container element exists before creating the SortableList:

```javascript
// Wait for DOM to be ready
document.addEventListener('DOMContentLoaded', () => {
    const sortable = new SortableList({
        container: '#language-list',
        // ...
    });
});
```

### Styles not applied

Make sure you've imported the SCSS file:

```scss
@import './components/sortable-list';
```

And compiled the styles:

```bash
npm run build:css
```

### onChange not firing

Make sure you're passing a function:

```javascript
// ✅ Correct
onChange: (newOrder) => {
    console.log('Order changed:', newOrder);
}

// ❌ Wrong - function call instead of reference
onChange: handleOrderChange()  // Remove the ()
```

## License

Part of the MultilingualPressZone plugin.
