init
This commit is contained in:
159
src/components/WordBuilder.js
Normal file
159
src/components/WordBuilder.js
Normal file
@@ -0,0 +1,159 @@
|
||||
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 hasTiles = state.activeTileIds.length > 0;
|
||||
const tileObjects = state.activeTileIds.map(id => state.boardTiles.find(t => t.id === id)).filter(Boolean);
|
||||
|
||||
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>${tile.text}</span>
|
||||
<span class="remove-icon">✕</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-clear" title="Clear all tiles" ${!hasTiles ? 'style="opacity:0.3;pointer-events:none;"' : ''}>
|
||||
✕
|
||||
</button>
|
||||
<button class="btn-icon-tray" id="btn-builder-backspace" title="Remove last tile" ${!hasTiles ? 'style="opacity:0.3;pointer-events:none;"' : ''}>
|
||||
⌫
|
||||
</button>
|
||||
<button class="btn-submit-tray" id="btn-builder-submit" ${!hasTiles ? 'disabled' : ''}>
|
||||
<span>✓</span>
|
||||
<span>ENTER</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 };
|
||||
}
|
||||
Reference in New Issue
Block a user