This commit is contained in:
Axce 2026-01-02 04:30:03 +01:00
commit 4e3e22b175
2 changed files with 1111 additions and 0 deletions

167
cahier des charges.md Normal file
View File

@ -0,0 +1,167 @@
## Objectif
Développer une **application web daide à la création de mots fléchés** (et non de mots croisés). Le présent document décrit de manière **non ambiguë** le comportement attendu afin de permettre à un LLM (ex. ChatGPT) de **coder lapplication fidèlement**.
---
## 1. Démarrage et création de grille
- Lapplication propose un écran initial où lutilisateur choisit :
- la **largeur** (nombre de colonnes)
- la **hauteur** (nombre de lignes)
- Un bouton **« Commencer »** crée une **nouvelle page / vue** contenant une grille vide conforme aux dimensions choisies.
---
## 2. Grille : règles générales
- La grille est constituée de **cases parfaitement carrées**, jointives (sans espace).
- Chaque case est de lun des deux types suivants :
- **Case lettre**
- **Case définition**
- Les **cases définition** ont un **fond légèrement plus foncé** que les cases lettre.
---
## 3. Règles initiales des cases définition
À la création de la grille :
- Toutes les cases situées :
- sur la **première ligne**,
- ou sur la **première colonne**,
- **et dont lindice (ligne ou colonne) est impair**
sont automatiquement des **cases définition**.
- Les indices commencent à **1** (et non 0).
---
## 4. Saisie et édition des mots
### 4.1 Sélection et orientation
- Lutilisateur clique sur une **case lettre** pour commencer la saisie.
- Une **orientation active** est toujours définie :
- **horizontale** ou **verticale**.
- Lorientation active est visuellement indiquée par la **mise en surbrillance** de la ligne ou colonne concernée.
### 4.2 Changement dorientation
- Lorientation bascule (horizontal ⇄ vertical) si :
- lutilisateur **clique à nouveau sur la case active**, ou
- lutilisateur appuie sur la touche **TAB**.
### 4.3 Saisie du texte
- Chaque lettre tapée :
- est placée dans la case courante,
- puis le curseur avance dune case selon lorientation active.
- Touche **Backspace** :
- supprime la lettre courante,
- puis recule le curseur dune case.
### 4.4 Déplacements clavier
- Les **flèches directionnelles** permettent de déplacer le curseur librement dans la grille sans modifier le contenu.
---
## 5. Création automatique de cases définition
- Lorsquun utilisateur commence à saisir un mot :
- si la **case immédiatement précédente** (selon lorientation) est vide,
- alors cette case devient automatiquement une **case définition**.
---
## 6. Gestion des cases définition
### 6.1 Contenu
- Une case définition peut contenir :
- **une définition horizontale**,
- **une définition verticale**,
- ou les deux simultanément.
### 6.2 Découpage visuel
- Si une case définition est associée à **deux mots** (horizontal + vertical) :
- elle est **visuellement divisée en deux moitiés**.
- les interactions (clic, sélection de définition) sappliquent uniquement à la moitié cliquée.
### 6.3 Flèches directionnelles
- Chaque définition affichée doit être accompagnée dune **petite flèche noire** :
- pointant vers la **première lettre du mot correspondant**,
- pouvant être **droite ou courbée** selon la direction du mot.
---
## 7. Ajout et suppression de lignes / colonnes
### 7.1 Ajout
- Des icônes **« + »** permettent :
- dajouter une **colonne** (icônes à droite de la grille),
- dajouter une **ligne** (icônes en bas de la grille).
- Lors de lajout, les **règles des cases définition impaires** (voir section 3) doivent être **strictement respectées**.
### 7.2 Suppression
- Des icônes **« »** permettent :
- de supprimer une colonne,
- de supprimer une ligne.
- La suppression entraîne la mise à jour correcte des indices et des types de cases.
---
## 8. Dictionnaires de définitions
### 8.1 Format
- Les dictionnaires sont des fichiers texte contenant des lignes au format :
```
mot: définition
```
- Un même mot peut apparaître plusieurs fois avec des définitions différentes.
### 8.2 Chargement
- Lutilisateur fournit une **URL** pointant vers un fichier texte valide (ex. Pastebin raw).
- Le fichier peut être **très volumineux (jusquà plusieurs gigaoctets)** :
- limplémentation doit donc être **économe en mémoire** et adaptée au streaming ou au traitement progressif.
### 8.3 Règles lexicales
- Les **mots sont comparés sans accents**.
- Les mots sont traités en **MAJUSCULES**.
- Les **définitions conservent leurs accents et leur casse dorigine**.
- Les mots contenant des **tirets** :
- sont affichés avec des **bordures en pointillés** entre les cases correspondant aux tirets.
---
## 9. Sélection des définitions
- Lorsquun utilisateur clique sur une **case définition** (ou une moitié de case) :
- lapplication affiche la **liste des définitions compatibles** avec le mot associé.
- Lutilisateur peut sélectionner une définition :
- celle-ci est alors affichée dans la case définition correspondante.
---
## 10. Changement manuel du type de case
- Un **double-clic** sur une case bascule son type :
- **case lettre ⇄ case définition**.
- Ce changement met à jour le comportement et laffichage de la case immédiatement.
---
## 11. Contraintes générales
- Lapplication est **entièrement web** (HTML/CSS/JS ou équivalent).
- Le comportement doit être **déterministe**, reproductible et sans ambiguïté.
- Toute interaction décrite ci-dessus doit être implémentée exactement telle que spécifiée.

944
index.html Normal file
View File

@ -0,0 +1,944 @@
<!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;">
<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>
<button class="btn-primary" onclick="loadDictionaryFromUrl()">Charger depuis URL</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)
const wordInfoH = getWordAt(row, col + 1, 'horizontal');
const wordInfoV = getWordAt(row + 1, col, 'vertical');
let targetWord = null;
let targetDir = null;
if (half === 'h') {
targetWord = wordInfoH;
targetDir = 'horizontal';
} else if (half === 'v') {
targetWord = wordInfoV;
targetDir = 'vertical';
} else {
// if no half specified, pick the direction that actually has a word
if (wordInfoH && wordInfoH.word) {
targetWord = wordInfoH;
targetDir = 'horizontal';
} else if (wordInfoV && wordInfoV.word) {
targetWord = wordInfoV;
targetDir = 'vertical';
}
}
if (targetWord && targetWord.word) {
const normalized = normalizeWord(targetWord.word);
const defs = dictionaries[normalized] || [];
const uniqueDefs = [...new Set(defs)];
currentModal = { row, col, half, direction: targetDir };
showDefinitions(targetWord.word, uniqueDefs);
}
} 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 les définitions
function showDefinitions(word, definitions) {
document.getElementById('modalTitle').textContent = `Définitions pour "${word}"`;
const list = document.getElementById('definitionList');
list.innerHTML = '';
if (definitions.length === 0) {
list.innerHTML = '<p style="color: #6b7280;">Aucune définition disponible pour ce mot.</p>';
} else {
definitions.forEach(def => {
const btn = document.createElement('button');
btn.className = 'definition-item';
btn.textContent = def;
btn.onclick = () => selectDefinition(def);
list.appendChild(btn);
});
}
document.getElementById('definitionModal').classList.add('show');
}
// Sélectionne une définition
function selectDefinition(definition) {
if (!currentModal) return;
const { row, col, direction } = currentModal;
if (direction === 'horizontal') {
grid[row][col].definitionH = definition;
} else {
grid[row][col].definitionV = definition;
}
closeModal();
renderGrid();
}
// Ferme le modal
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;
halfH.innerHTML += '<svg class="arrow" width="8" height="8" viewBox="0 0 8 8"><path d="M 2 4 L 6 4 M 6 4 L 4 2 M 6 4 L 4 6" stroke="black" stroke-width="1" fill="none"/></svg>';
const halfV = document.createElement('div');
halfV.className = 'definition-half';
halfV.textContent = cell.definitionV;
halfV.innerHTML += '<svg class="arrow" width="8" height="8" viewBox="0 0 8 8"><path d="M 4 2 L 4 6 M 4 6 L 2 4 M 4 6 L 6 4" stroke="black" stroke-width="1" fill="none"/></svg>';
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;
if (cell.definitionH || cell.definitionV) {
if (cell.definitionV) {
text.innerHTML += '<svg class="arrow" width="8" height="8" viewBox="0 0 8 8"><path d="M 4 2 L 4 6 M 4 6 L 2 4 M 4 6 L 6 4" stroke="black" stroke-width="1" fill="none"/></svg>';
} else {
text.innerHTML += '<svg class="arrow" width="8" height="8" viewBox="0 0 8 8"><path d="M 2 4 L 6 4 M 6 4 L 4 2 M 6 4 L 4 6" stroke="black" stroke-width="1" fill="none"/></svg>';
}
}
cellEl.appendChild(text);
}
}
if (!hasBoth) {
cellEl.onclick = () => handleCellClick(rowIndex, colIndex);
}
cellEl.ondblclick = () => handleCellDoubleClick(rowIndex, colIndex);
rowEl.appendChild(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);
}
// 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>