This commit is contained in:
2026-10-23 22:31:08 -04:00
commit 270b402ccc
31 changed files with 15846 additions and 0 deletions

View 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 };
}