159 lines
5.2 KiB
JavaScript
159 lines
5.2 KiB
JavaScript
import { sound } from '../engine/audio.js';
|
|
|
|
export function createWordBuilder({ onTileRemoved, onTilesReordered, onSubmitWord, onClearWord, onBackspaceWord }) {
|
|
const wrapper = document.createElement('div');
|
|
wrapper.className = 'word-builder-wrapper';
|
|
|
|
let draggedIndex = null;
|
|
let touchStartX = 0;
|
|
let touchMoved = false;
|
|
|
|
function render(state) {
|
|
const tileCount = state.activeTileIds.length;
|
|
const hasTiles = tileCount > 0;
|
|
const tileObjects = state.activeTileIds.map(id => state.boardTiles.find(t => t.id === id)).filter(Boolean);
|
|
const pts = tileCount === 4 ? 8 : tileCount === 3 ? 4 : tileCount === 2 ? 2 : 1;
|
|
|
|
wrapper.innerHTML = `
|
|
<div class="word-builder ${hasTiles ? 'has-tiles' : ''}" id="active-word-builder">
|
|
<div class="active-tiles-container" id="active-tiles-tray">
|
|
${
|
|
hasTiles
|
|
? tileObjects
|
|
.map(
|
|
(tile, idx) => `
|
|
<div class="active-tile-chip" data-idx="${idx}" draggable="true" title="Tap to remove or drag to reorder">
|
|
<span class="chip-text">${tile.text}</span>
|
|
</div>
|
|
`
|
|
)
|
|
.join('')
|
|
: `<span class="builder-placeholder">Tap tiles below to build a word</span>`
|
|
}
|
|
</div>
|
|
<div class="builder-actions">
|
|
<button class="btn-icon-tray" id="btn-builder-backspace" title="Remove last tile" ${!hasTiles ? 'style="opacity:0.25;pointer-events:none;"' : ''}>
|
|
⌫
|
|
</button>
|
|
<button class="btn-submit-tray" id="btn-builder-submit" ${!hasTiles ? 'disabled' : ''} title="Submit word (Enter)">
|
|
<span class="submit-icon">✓</span>
|
|
<span class="submit-text">ENTER</span>
|
|
${hasTiles ? `<span class="submit-pts">+${pts}</span>` : ''}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Event listeners for active chips
|
|
const chips = wrapper.querySelectorAll('.active-tile-chip');
|
|
chips.forEach(chip => {
|
|
const idx = parseInt(chip.getAttribute('data-idx'), 10);
|
|
|
|
// Tap chip to remove (only if not dragged)
|
|
chip.addEventListener('click', e => {
|
|
if (!touchMoved) {
|
|
sound.playTileRemove();
|
|
onTileRemoved(idx);
|
|
}
|
|
});
|
|
|
|
// Desktop HTML5 drag and drop
|
|
chip.addEventListener('dragstart', e => {
|
|
draggedIndex = idx;
|
|
chip.classList.add('dragging');
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
});
|
|
|
|
chip.addEventListener('dragend', () => {
|
|
chip.classList.remove('dragging');
|
|
chips.forEach(c => c.classList.remove('drag-over'));
|
|
});
|
|
|
|
chip.addEventListener('dragover', e => {
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = 'move';
|
|
chip.classList.add('drag-over');
|
|
});
|
|
|
|
chip.addEventListener('dragleave', () => {
|
|
chip.classList.remove('drag-over');
|
|
});
|
|
|
|
chip.addEventListener('drop', e => {
|
|
e.preventDefault();
|
|
chip.classList.remove('drag-over');
|
|
if (draggedIndex !== null && draggedIndex !== idx) {
|
|
onTilesReordered(draggedIndex, idx);
|
|
}
|
|
draggedIndex = null;
|
|
});
|
|
|
|
// Touch events for mobile reordering
|
|
chip.addEventListener(
|
|
'touchstart',
|
|
e => {
|
|
touchStartX = e.touches[0].clientX;
|
|
touchMoved = false;
|
|
draggedIndex = idx;
|
|
},
|
|
{ passive: true }
|
|
);
|
|
|
|
chip.addEventListener(
|
|
'touchmove',
|
|
e => {
|
|
const currentX = e.touches[0].clientX;
|
|
if (Math.abs(currentX - touchStartX) > 10) {
|
|
touchMoved = true;
|
|
chip.classList.add('dragging');
|
|
}
|
|
},
|
|
{ passive: true }
|
|
);
|
|
|
|
chip.addEventListener('touchend', e => {
|
|
chip.classList.remove('dragging');
|
|
if (touchMoved) {
|
|
const endX = e.changedTouches[0].clientX;
|
|
const endY = e.changedTouches[0].clientY;
|
|
// Find which chip the touch ended on
|
|
const targetEl = document.elementFromPoint(endX, endY);
|
|
const targetChip = targetEl ? targetEl.closest('.active-tile-chip') : null;
|
|
if (targetChip) {
|
|
const targetIdx = parseInt(targetChip.getAttribute('data-idx'), 10);
|
|
if (!isNaN(targetIdx) && targetIdx !== idx) {
|
|
onTilesReordered(idx, targetIdx);
|
|
}
|
|
}
|
|
setTimeout(() => {
|
|
touchMoved = false;
|
|
}, 50);
|
|
}
|
|
});
|
|
});
|
|
|
|
wrapper.querySelector('#btn-builder-clear')?.addEventListener('click', () => {
|
|
sound.playTileRemove();
|
|
onClearWord();
|
|
});
|
|
|
|
wrapper.querySelector('#btn-builder-backspace')?.addEventListener('click', () => {
|
|
sound.playTileRemove();
|
|
onBackspaceWord();
|
|
});
|
|
|
|
wrapper.querySelector('#btn-builder-submit')?.addEventListener('click', onSubmitWord);
|
|
}
|
|
|
|
function triggerShake() {
|
|
const builder = wrapper.querySelector('#active-word-builder');
|
|
if (builder) {
|
|
builder.classList.remove('invalid-shake');
|
|
void builder.offsetWidth; // Trigger reflow
|
|
builder.classList.add('invalid-shake');
|
|
}
|
|
}
|
|
|
|
return { element: wrapper, render, triggerShake };
|
|
}
|