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