init
This commit is contained in:
commit
4e3e22b175
167
cahier des charges.md
Normal file
167
cahier des charges.md
Normal file
@ -0,0 +1,167 @@
|
||||
## Objectif
|
||||
Développer une **application web d’aide à 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 l’application fidèlement**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Démarrage et création de grille
|
||||
|
||||
- L’application propose un écran initial où l’utilisateur 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 l’un 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 l’indice (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
|
||||
|
||||
- L’utilisateur clique sur une **case lettre** pour commencer la saisie.
|
||||
- Une **orientation active** est toujours définie :
|
||||
- **horizontale** ou **verticale**.
|
||||
- L’orientation active est visuellement indiquée par la **mise en surbrillance** de la ligne ou colonne concernée.
|
||||
|
||||
### 4.2 Changement d’orientation
|
||||
|
||||
- L’orientation bascule (horizontal ⇄ vertical) si :
|
||||
- l’utilisateur **clique à nouveau sur la case active**, ou
|
||||
- l’utilisateur 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 d’une case selon l’orientation active.
|
||||
- Touche **Backspace** :
|
||||
- supprime la lettre courante,
|
||||
- puis recule le curseur d’une 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
|
||||
|
||||
- Lorsqu’un utilisateur commence à saisir un mot :
|
||||
- si la **case immédiatement précédente** (selon l’orientation) 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) s’appliquent uniquement à la moitié cliquée.
|
||||
|
||||
### 6.3 Flèches directionnelles
|
||||
|
||||
- Chaque définition affichée doit être accompagnée d’une **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 :
|
||||
- d’ajouter une **colonne** (icônes à droite de la grille),
|
||||
- d’ajouter une **ligne** (icônes en bas de la grille).
|
||||
- Lors de l’ajout, 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
|
||||
|
||||
- L’utilisateur fournit une **URL** pointant vers un fichier texte valide (ex. Pastebin raw).
|
||||
- Le fichier peut être **très volumineux (jusqu’à plusieurs gigaoctets)** :
|
||||
- l’implé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 d’origine**.
|
||||
- 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
|
||||
|
||||
- Lorsqu’un utilisateur clique sur une **case définition** (ou une moitié de case) :
|
||||
- l’application affiche la **liste des définitions compatibles** avec le mot associé.
|
||||
- L’utilisateur 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 l’affichage de la case immédiatement.
|
||||
|
||||
---
|
||||
|
||||
## 11. Contraintes générales
|
||||
|
||||
- L’application 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
944
index.html
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user