grillades/index.html
2026-01-02 04:52:37 +01:00

1086 lines
41 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Application de Mots Fléchés</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #f3f4f6;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
}
h2 {
font-size: 16px;
font-weight: 600;
margin-bottom: 10px;
}
.setup-screen {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.setup-box {
background: white;
padding: 32px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 16px;
}
label {
display: block;
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
}
input[type="number"], input[type="text"], textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #d1d5db;
border-radius: 4px;
font-size: 14px;
}
textarea {
font-family: monospace;
resize: vertical;
}
button {
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-primary {
background-color: #2563eb;
color: white;
width: 100%;
}
.btn-primary:hover {
background-color: #1d4ed8;
}
.btn-success {
background-color: #16a34a;
color: white;
}
.btn-success:hover {
background-color: #15803d;
}
.btn-danger {
background-color: #dc2626;
color: white;
margin-right: 10px;
}
.btn-danger:hover {
background-color: #b91c1c;
}
.btn-disabled {
background-color: #9ca3af;
cursor: not-allowed;
}
.card {
background: white;
padding: 16px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin-bottom: 16px;
}
.grid-screen {
display: none;
}
.grid-container {
background: white;
padding: 16px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
display: inline-block;
}
.grid {
display: inline-block;
position: relative;
}
.grid-row {
display: flex;
}
.cell {
width: 40px;
height: 40px;
border: 1px solid #6b7280;
position: relative;
cursor: pointer;
user-select: none;
}
.cell.letter {
background-color: white;
}
.cell.definition {
background-color: #d1d5db;
}
.cell.active {
outline: 2px solid #2563eb;
outline-offset: -2px;
z-index: 10;
}
.cell.highlighted {
background-color: #dbeafe;
}
.cell-content {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: bold;
}
.definition-text {
position: relative;
padding: 2px;
font-size: 5px;
line-height: 6px;
overflow: hidden;
height: 100%;
word-wrap: break-word;
}
.definition-split {
display: flex;
position: absolute;
inset: 0;
}
.definition-half {
width: 50%;
position: relative;
padding: 2px;
font-size: 5px;
line-height: 6px;
overflow: hidden;
word-wrap: break-word;
}
.definition-half:first-child {
border-right: 1px solid #6b7280;
}
/* Visual hint for hyphens: dotted border between cells */
.cell.hyphen {
border-right: 2px dotted #000;
box-shadow: none;
}
.arrow {
position: absolute;
bottom: 0;
right: 0;
}
.controls {
display: flex;
gap: 16px;
margin-top: 16px;
}
.add-controls {
display: flex;
align-items: center;
margin-left: 8px;
}
.add-btn, .remove-btn {
width: 20px;
height: 20px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: #e5e7eb;
border-radius: 3px;
}
.add-btn:hover {
background-color: #d1d5db;
}
.modal {
display: none;
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.show {
display: flex;
}
.modal-content {
background: white;
padding: 24px;
border-radius: 8px;
box-shadow: 0 20px 25px rgba(0, 0, 0, 0.15);
max-width: 600px;
max-height: 500px;
overflow-y: auto;
}
.modal-content h3 {
font-size: 18px;
font-weight: bold;
margin-bottom: 16px;
}
.definition-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
}
.definition-item {
width: 100%;
text-align: left;
padding: 12px;
border: 1px solid #d1d5db;
border-radius: 4px;
background: white;
cursor: pointer;
transition: background-color 0.2s;
}
.definition-item:hover {
background-color: #dbeafe;
}
.add-row-controls {
display: flex;
margin-top: 8px;
}
.add-col-control {
display: flex;
justify-content: center;
width: 40px;
}
</style>
</head>
<body>
<!-- Écran de configuration -->
<div id="setupScreen" class="setup-screen">
<div class="setup-box">
<h1>Créer une grille de mots fléchés</h1>
<div class="form-group">
<label>Largeur (colonnes)</label>
<input type="number" id="widthInput" value="10" min="3" max="30">
</div>
<div class="form-group">
<label>Hauteur (lignes)</label>
<input type="number" id="heightInput" value="10" min="3" max="30">
</div>
<button class="btn-primary" onclick="startGrid()">Commencer</button>
</div>
</div>
<!-- Écran de grille -->
<div id="gridScreen" class="grid-screen">
<div class="container">
<h1>Éditeur de mots fléchés</h1>
<div class="card">
<h2>Charger un dictionnaire</h2>
<div class="form-group">
<input type="text" id="dictionaryUrl" placeholder="Ou entrez une URL pour charger un dictionnaire (raw .txt)" style="width:100%; padding:8px 12px; border:1px solid #d1d5db; border-radius:4px; margin-bottom:8px;">
<div style="margin-top:8px;margin-bottom:8px;"><button class="btn-primary" onclick="loadDictionaryFromUrl()">Charger depuis URL</button></div>
<textarea id="dictionaryText" rows="5" placeholder="Collez le contenu du dictionnaire ici (format: mot: définition)"></textarea>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:8px;">
<button class="btn-success" onclick="loadDictionaryFromTextarea()">Charger depuis zone</button>
</div>
</div>
<div class="grid-container">
<div id="grid" class="grid"></div>
<div class="controls">
<button class="btn-danger" onclick="removeRow()"> Supprimer ligne</button>
<button class="btn-danger" onclick="removeColumn()"> Supprimer colonne</button>
</div>
</div>
</div>
</div>
<!-- Modal de sélection de définition -->
<div id="definitionModal" class="modal">
<div class="modal-content">
<h3 id="modalTitle">Définitions</h3>
<div id="definitionList" class="definition-list"></div>
<button class="btn-primary" onclick="closeModal()">Fermer</button>
</div>
</div>
<script>
// État de l'application
let grid = [];
let activeCell = null;
let orientation = 'horizontal';
let dictionaries = {};
let currentModal = null;
const CELL_SIZE = 40;
// Normalise un mot (majuscules, sans accents)
function normalizeWord(word) {
return word.toUpperCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
// Crée une grille vide
function createGrid(width, height) {
const newGrid = [];
for (let row = 0; row < height; row++) {
const rowData = [];
for (let col = 0; col < width; col++) {
// indices are 1-based in spec: definition if on first row or first column and index is odd (1-based)
const isDefinition = ((row === 0 && ((col + 1) % 2 === 1)) || (col === 0 && ((row + 1) % 2 === 1)));
rowData.push({
type: isDefinition ? 'definition' : 'letter',
letter: '',
definitionH: '',
definitionV: ''
});
}
newGrid.push(rowData);
}
return newGrid;
}
// Démarre la grille
function startGrid() {
const width = parseInt(document.getElementById('widthInput').value);
const height = parseInt(document.getElementById('heightInput').value);
grid = createGrid(width, height);
// Ensure the first-row/first-column odd-index rule is strictly applied
updateDefinitionTypes();
document.getElementById('setupScreen').style.display = 'none';
document.getElementById('gridScreen').style.display = 'block';
renderGrid();
}
// Re-evaluate and enforce first-row/first-column definition rule after structural changes
function updateDefinitionTypes() {
for (let r = 0; r < grid.length; r++) {
for (let c = 0; c < grid[0].length; c++) {
const onFirstRowRule = (r === 0 && ((c + 1) % 2 === 1));
const onFirstColRule = (c === 0 && ((r + 1) % 2 === 1));
if (onFirstRowRule || onFirstColRule) {
grid[r][c].type = 'definition';
} else if (r === 0 || c === 0) {
// if on first row/col but not meeting the odd-index rule, ensure it's a letter
grid[r][c].type = 'letter';
}
}
}
}
// Obtient le mot à une position donnée
function getWordAt(row, col, dir) {
if (!grid[row] || !grid[row][col]) return { word: '', startRow: row, startCol: col };
let word = '';
let startRow = row;
let startCol = col;
// Trouve le début du mot
if (dir === 'horizontal') {
while (startCol > 0 && grid[startRow][startCol - 1]?.type === 'letter' && grid[startRow][startCol - 1]?.letter) {
startCol--;
}
// Construit le mot
let c = startCol;
while (c < grid[0].length && grid[startRow][c]?.type === 'letter' && grid[startRow][c]?.letter) {
word += grid[startRow][c].letter;
c++;
}
} else {
while (startRow > 0 && grid[startRow - 1][startCol]?.type === 'letter' && grid[startRow - 1][startCol]?.letter) {
startRow--;
}
// Construit le mot
let r = startRow;
while (r < grid.length && grid[r][startCol]?.type === 'letter' && grid[r][startCol]?.letter) {
word += grid[r][startCol].letter;
r++;
}
}
return { word, startRow, startCol };
}
// Gère le clic sur une case
function handleCellClick(row, col, half = null) {
const cell = grid[row][col];
if (cell.type === 'definition') {
// Determine associated words in both directions (starting at the neighboring letter cell)
// Special rules for top row and left column mappings
let wordInfoH;
let wordInfoV;
// Horizontal definition normally maps to word starting at (row, col+1) horizontally,
// but if this definition is on the top row, its horizontal definition maps to the vertical word starting at (row, col+1).
if (row === 0) {
wordInfoH = getWordAt(row, col + 1, 'vertical');
} else {
wordInfoH = getWordAt(row, col + 1, 'horizontal');
}
// Vertical definition normally maps to word starting at (row+1, col) vertically,
// but if this definition is on the first column, its vertical definition maps to the horizontal word starting at (row+1, col).
if (col === 0) {
wordInfoV = getWordAt(row + 1, col, 'horizontal');
} else {
wordInfoV = getWordAt(row + 1, col, 'vertical');
}
// Build words and definitions for both sides (may be empty)
const wordH = (wordInfoH && wordInfoH.word) ? wordInfoH.word : null;
const wordV = (wordInfoV && wordInfoV.word) ? wordInfoV.word : null;
const defsH = wordH ? ([...new Set(dictionaries[normalizeWord(wordH)] || [])]) : [];
const defsV = wordV ? ([...new Set(dictionaries[normalizeWord(wordV)] || [])]) : [];
// If half specified, still show modal but prioritize that half for selection via UI
currentModal = { row, col, half };
showDefinitionModal(row, col, wordH, defsH, wordV, defsV);
} else {
if (activeCell && activeCell.row === row && activeCell.col === col) {
orientation = orientation === 'horizontal' ? 'vertical' : 'horizontal';
} else {
activeCell = { row, col };
}
renderGrid();
}
}
// Gère le double-clic
function handleCellDoubleClick(row, col) {
const cell = grid[row][col];
cell.type = cell.type === 'letter' ? 'definition' : 'letter';
if (cell.type === 'letter') {
cell.definitionH = '';
cell.definitionV = '';
} else {
cell.letter = '';
}
renderGrid();
}
// Affiche le modal de sélection des définitions, en deux parties (horizontal / vertical)
function showDefinitionModal(row, col, wordH, defsH, wordV, defsV) {
currentModal = { row, col };
document.getElementById('modalTitle').textContent = `Définitions`;
const list = document.getElementById('definitionList');
list.innerHTML = '';
const sectionH = document.createElement('div');
const titleH = document.createElement('h4');
titleH.textContent = 'Mot horizontal';
sectionH.appendChild(titleH);
if (!wordH) {
const p = document.createElement('p');
p.style.color = '#6b7280';
p.textContent = 'Aucun mot associé';
sectionH.appendChild(p);
} else {
const wordP = document.createElement('p');
wordP.style.fontWeight = '600';
wordP.textContent = wordH;
sectionH.appendChild(wordP);
if (defsH.length === 0) {
const p = document.createElement('p');
p.style.color = '#6b7280';
p.textContent = 'Aucune définition disponible pour ce mot.';
sectionH.appendChild(p);
} else {
defsH.forEach(def => {
const btn = document.createElement('button');
btn.className = 'definition-item';
btn.textContent = def;
btn.onclick = () => {
grid[row][col].definitionH = def;
closeModal();
renderGrid();
};
sectionH.appendChild(btn);
});
}
}
const sectionV = document.createElement('div');
const titleV = document.createElement('h4');
titleV.textContent = 'Mot vertical';
sectionV.appendChild(titleV);
if (!wordV) {
const p = document.createElement('p');
p.style.color = '#6b7280';
p.textContent = 'Aucun mot associé';
sectionV.appendChild(p);
} else {
const wordP = document.createElement('p');
wordP.style.fontWeight = '600';
wordP.textContent = wordV;
sectionV.appendChild(wordP);
if (defsV.length === 0) {
const p = document.createElement('p');
p.style.color = '#6b7280';
p.textContent = 'Aucune définition disponible pour ce mot.';
sectionV.appendChild(p);
} else {
defsV.forEach(def => {
const btn = document.createElement('button');
btn.className = 'definition-item';
btn.textContent = def;
btn.onclick = () => {
grid[row][col].definitionV = def;
closeModal();
renderGrid();
};
sectionV.appendChild(btn);
});
}
}
// Delete button: transforms case into letter empty
const deleteBtn = document.createElement('button');
deleteBtn.className = 'btn-danger';
deleteBtn.style.marginTop = '12px';
deleteBtn.textContent = 'Supprimer (transformer en case lettre)';
deleteBtn.onclick = () => {
const cell = grid[row][col];
const hasDefs = !!(cell.definitionH || cell.definitionV);
if (hasDefs) {
if (!confirm('La case contient des définitions. Confirmer la transformation en case lettre vide ?')) return;
}
cell.type = 'letter';
cell.letter = '';
cell.definitionH = '';
cell.definitionV = '';
closeModal();
renderGrid();
};
// Append sections to list
list.appendChild(sectionH);
list.appendChild(sectionV);
list.appendChild(deleteBtn);
document.getElementById('definitionModal').classList.add('show');
}
function closeModal() {
document.getElementById('definitionModal').classList.remove('show');
currentModal = null;
}
// Charge un dictionnaire
// Helper to process a single line from a dictionary source
function processDictionaryLine(line) {
const match = line.match(/^(.+?):\s*(.+)$/);
if (match) {
const word = normalizeWord(match[1].trim());
const definition = match[2].trim();
if (!dictionaries[word]) {
dictionaries[word] = [];
}
dictionaries[word].push(definition);
}
}
// Load from the textarea (legacy/paste)
function loadDictionaryFromTextarea() {
const text = document.getElementById('dictionaryText').value;
if (!text.trim()) {
alert('Aucun contenu dans la zone de texte.');
return;
}
dictionaries = {};
const lines = text.split('\n');
for (const line of lines) {
processDictionaryLine(line);
}
alert('Dictionnaire chargé avec succès !');
}
// Load a potentially very large dictionary from a URL using streaming
async function loadDictionaryFromUrl() {
const url = document.getElementById('dictionaryUrl').value.trim();
if (!url) {
alert('Entrez une URL valide.');
return;
}
dictionaries = {};
try {
const resp = await fetch(url);
if (!resp.ok || !resp.body) {
alert('Impossible de récupérer le fichier depuis l\'URL fournie.');
return;
}
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let { value, done } = await reader.read();
let chunk = '';
while (!done) {
chunk += decoder.decode(value, { stream: true });
const lines = chunk.split(/\r?\n/);
chunk = lines.pop();
for (const line of lines) {
processDictionaryLine(line);
}
({ value, done } = await reader.read());
}
if (chunk) processDictionaryLine(chunk);
alert('Dictionnaire chargé depuis URL avec succès !');
} catch (err) {
console.error(err);
alert('Erreur lors du chargement du dictionnaire.');
}
}
// Ajoute une ligne
function addRow() {
const rowIndex = grid.length;
const newRow = [];
for (let col = 0; col < grid[0].length; col++) {
const isDefinition = (col === 0 && ((rowIndex + 1) % 2 === 1));
newRow.push({
type: isDefinition ? 'definition' : 'letter',
letter: '',
definitionH: '',
definitionV: ''
});
}
grid.push(newRow);
// Re-evaluate first row/column rule strictly after structural change
updateDefinitionTypes();
renderGrid();
}
// Ajoute une colonne
function addColumn() {
grid.forEach((row, rowIndex) => {
const colIndex = row.length;
const isDefinition = (rowIndex === 0 && ((colIndex + 1) % 2 === 1));
row.push({
type: isDefinition ? 'definition' : 'letter',
letter: '',
definitionH: '',
definitionV: ''
});
});
updateDefinitionTypes();
renderGrid();
}
// Supprime une ligne
function removeRow() {
if (grid.length > 1) {
const lastRowIndex = grid.length - 1;
const rowToCheck = grid[lastRowIndex];
let hasContent = false;
for (const cell of rowToCheck) {
if ((cell.type === 'letter' && cell.letter) || (cell.type === 'definition' && (cell.definitionH || cell.definitionV))) {
hasContent = true;
break;
}
}
if (hasContent) {
const ok = confirm('La ligne contient des lettres ou des définitions. Confirmer la suppression ?');
if (!ok) return;
}
grid.pop();
if (activeCell && activeCell.row >= grid.length) {
activeCell = null;
}
updateDefinitionTypes();
renderGrid();
}
}
// Supprime une colonne
function removeColumn() {
if (grid[0].length > 1) {
const lastColIndex = grid[0].length - 1;
let hasContent = false;
for (const row of grid) {
const cell = row[lastColIndex];
if ((cell.type === 'letter' && cell.letter) || (cell.type === 'definition' && (cell.definitionH || cell.definitionV))) {
hasContent = true;
break;
}
}
if (hasContent) {
const ok = confirm('La colonne contient des lettres ou des définitions. Confirmer la suppression ?');
if (!ok) return;
}
grid.forEach(row => row.pop());
if (activeCell && activeCell.col >= grid[0].length) {
activeCell = null;
}
updateDefinitionTypes();
renderGrid();
}
}
// Rend la grille
function renderGrid() {
const gridEl = document.getElementById('grid');
gridEl.innerHTML = '';
grid.forEach((row, rowIndex) => {
const rowEl = document.createElement('div');
rowEl.className = 'grid-row';
row.forEach((cell, colIndex) => {
const cellEl = document.createElement('div');
cellEl.className = 'cell ' + cell.type;
const isActive = activeCell?.row === rowIndex && activeCell?.col === colIndex;
const isHighlighted = activeCell && cell.type === 'letter' && (
(orientation === 'horizontal' && activeCell.row === rowIndex) ||
(orientation === 'vertical' && activeCell.col === colIndex)
);
if (isActive) cellEl.classList.add('active');
if (isHighlighted) cellEl.classList.add('highlighted');
let hasBoth = false;
if (cell.type === 'letter') {
const content = document.createElement('div');
content.className = 'cell-content';
content.textContent = cell.letter;
cellEl.appendChild(content);
// If the letter is a hyphen, add visual dotted border indication
if (cell.letter === '-') {
cellEl.classList.add('hyphen');
}
} else {
hasBoth = cell.definitionH && cell.definitionV;
if (hasBoth) {
const split = document.createElement('div');
split.className = 'definition-split';
const halfH = document.createElement('div');
halfH.className = 'definition-half';
halfH.textContent = cell.definitionH;
const halfV = document.createElement('div');
halfV.className = 'definition-half';
halfV.textContent = cell.definitionV;
split.appendChild(halfH);
split.appendChild(halfV);
cellEl.appendChild(split);
halfH.onclick = (e) => {
e.stopPropagation();
handleCellClick(rowIndex, colIndex, 'h');
};
halfV.onclick = (e) => {
e.stopPropagation();
handleCellClick(rowIndex, colIndex, 'v');
};
} else {
const text = document.createElement('div');
text.className = 'definition-text';
text.textContent = cell.definitionH || cell.definitionV;
cellEl.appendChild(text);
}
}
if (!hasBoth) {
cellEl.onclick = () => handleCellClick(rowIndex, colIndex);
}
cellEl.ondblclick = () => handleCellDoubleClick(rowIndex, colIndex);
rowEl.appendChild(cellEl);
// store reference for later arrow placement
if (!window._cellEls) window._cellEls = [];
if (!window._cellEls[rowIndex]) window._cellEls[rowIndex] = [];
window._cellEls[rowIndex][colIndex] = cellEl;
});
// Bouton d'ajout de colonne
const addColBtn = document.createElement('div');
addColBtn.className = 'add-controls';
addColBtn.innerHTML = '<button class="add-btn" onclick="addColumn()">+</button>';
rowEl.appendChild(addColBtn);
gridEl.appendChild(rowEl);
});
// Ligne d'ajout
const addRowLine = document.createElement('div');
addRowLine.className = 'add-row-controls';
for (let i = 0; i < grid[0].length; i++) {
const addBtn = document.createElement('div');
addBtn.className = 'add-col-control';
addBtn.innerHTML = '<button class="add-btn" onclick="addRow()">+</button>';
addRowLine.appendChild(addBtn);
}
gridEl.appendChild(addRowLine);
// After grid built, place arrows in the first-letter cells based on adjacent definition cells
// Clear any previous arrows
if (window._cellEls) {
for (let r = 0; r < grid.length; r++) {
for (let c = 0; c < grid[0].length; c++) {
const el = window._cellEls[r] && window._cellEls[r][c];
if (el) {
const prev = el.querySelector('.def-arrow');
if (prev) prev.remove();
}
}
}
// For each definition cell, compute target first-letter cell and append arrow there
for (let dr = 0; dr < grid.length; dr++) {
for (let dc = 0; dc < grid[0].length; dc++) {
const defCell = grid[dr][dc];
if (defCell.type !== 'definition') continue;
// Horizontal-definition mapping
if (defCell.definitionH) {
const tr = dr;
const tc = dc + 1;
if (tr >= 0 && tr < grid.length && tc >= 0 && tc < grid[0].length) {
const targetEl = window._cellEls[tr] && window._cellEls[tr][tc];
if (targetEl) {
const arrow = document.createElement('div');
arrow.className = 'def-arrow';
// special top-row: curve down
if (dr === 0) {
arrow.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1" class="arrow-down-curved"><path d="M12 0 C12 0 12 6 12 6 L12 16"/><path d="M12 16 L9 13 M12 16 L15 13"/></svg>';
arrow.style.position = 'absolute';
arrow.style.top = '2px';
arrow.style.left = '50%';
arrow.style.transform = 'translateX(-50%)';
} else {
// left-pointing arrow near left border
arrow.innerHTML = '<svg width="12" height="12" viewBox="0 0 8 8" fill="none" stroke="black" stroke-width="1"><path d="M6 1 L2 4 L6 7"/></svg>';
arrow.style.position = 'absolute';
arrow.style.left = '4px';
arrow.style.top = '50%';
arrow.style.transform = 'translateY(-50%)';
}
arrow.style.pointerEvents = 'none';
arrow.classList.add('def-arrow');
targetEl.appendChild(arrow);
}
}
}
// Vertical-definition mapping
if (defCell.definitionV) {
const tr = dr + 1;
const tc = dc;
if (tr >= 0 && tr < grid.length && tc >= 0 && tc < grid[0].length) {
const targetEl = window._cellEls[tr] && window._cellEls[tr][tc];
if (targetEl) {
const arrow = document.createElement('div');
arrow.className = 'def-arrow';
if (dc === 0) {
// left-edge special: curve right
arrow.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1"><path d="M0 12 C0 12 6 12 6 12 L16 12"/><path d="M16 12 L13 9 M16 12 L13 15"/></svg>';
arrow.style.position = 'absolute';
arrow.style.left = '2px';
arrow.style.top = '50%';
arrow.style.transform = 'translateY(-50%)';
} else {
// up-pointing arrow near top border
arrow.innerHTML = '<svg width="12" height="12" viewBox="0 0 8 8" fill="none" stroke="black" stroke-width="1"><path d="M1 6 L4 2 L7 6"/></svg>';
arrow.style.position = 'absolute';
arrow.style.top = '4px';
arrow.style.left = '50%';
arrow.style.transform = 'translateX(-50%)';
}
arrow.style.pointerEvents = 'none';
arrow.classList.add('def-arrow');
targetEl.appendChild(arrow);
}
}
}
}
}
}
}
// Gestion du clavier
document.addEventListener('keydown', (e) => {
if (!activeCell || document.getElementById('gridScreen').style.display === 'none') return;
const { row, col } = activeCell;
if (e.key === 'Tab') {
e.preventDefault();
orientation = orientation === 'horizontal' ? 'vertical' : 'horizontal';
renderGrid();
return;
}
if (e.key === 'Backspace') {
e.preventDefault();
if (grid[row][col].letter) {
grid[row][col].letter = '';
renderGrid();
} else {
if (orientation === 'horizontal' && col > 0) {
const prevCol = col - 1;
if (grid[row][prevCol].type === 'letter') {
grid[row][prevCol].letter = '';
activeCell = { row, col: prevCol };
renderGrid();
}
} else if (orientation === 'vertical' && row > 0) {
const prevRow = row - 1;
if (grid[prevRow][col].type === 'letter') {
grid[prevRow][col].letter = '';
activeCell = { row: prevRow, col };
renderGrid();
}
}
}
return;
}
if (e.key.startsWith('Arrow')) {
e.preventDefault();
let newRow = row;
let newCol = col;
if (e.key === 'ArrowUp') newRow = Math.max(0, row - 1);
if (e.key === 'ArrowDown') newRow = Math.min(grid.length - 1, row + 1);
if (e.key === 'ArrowLeft') newCol = Math.max(0, col - 1);
if (e.key === 'ArrowRight') newCol = Math.min(grid[0].length - 1, col + 1);
activeCell = { row: newRow, col: newCol };
renderGrid();
return;
}
if (e.key.length === 1 && /[a-zA-Z]/.test(e.key)) {
e.preventDefault();
if (grid[row][col].type !== 'letter') return;
// Crée automatiquement une case définition si nécessaire
if (orientation === 'horizontal' && col > 0) {
const prevCell = grid[row][col - 1];
if (prevCell.type === 'letter' && !prevCell.letter) {
prevCell.type = 'definition';
}
} else if (orientation === 'vertical' && row > 0) {
const prevCell = grid[row - 1][col];
if (prevCell.type === 'letter' && !prevCell.letter) {
prevCell.type = 'definition';
}
}
grid[row][col].letter = e.key.toUpperCase();
// Avance à la case suivante
if (orientation === 'horizontal' && col < grid[0].length - 1) {
let nextCol = col + 1;
while (nextCol < grid[0].length && grid[row][nextCol].type !== 'letter') {
nextCol++;
}
if (nextCol < grid[0].length) {
activeCell = { row, col: nextCol };
}
} else if (orientation === 'vertical' && row < grid.length - 1) {
let nextRow = row + 1;
while (nextRow < grid.length && grid[nextRow][col].type !== 'letter') {
nextRow++;
}
if (nextRow < grid.length) {
activeCell = { row: nextRow, col };
}
}
renderGrid();
}
});
// Ensure tab toggles orientation even if focus is elsewhere
document.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
e.preventDefault();
orientation = orientation === 'horizontal' ? 'vertical' : 'horizontal';
renderGrid();
}
});
</script>
</body>
</html>