grillades/index.html
2026-01-02 04:30:03 +01:00

944 lines
33 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Application de Mots Fléchés</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #f3f4f6;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
}
h2 {
font-size: 16px;
font-weight: 600;
margin-bottom: 10px;
}
.setup-screen {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.setup-box {
background: white;
padding: 32px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 16px;
}
label {
display: block;
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
}
input[type="number"], input[type="text"], textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #d1d5db;
border-radius: 4px;
font-size: 14px;
}
textarea {
font-family: monospace;
resize: vertical;
}
button {
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-primary {
background-color: #2563eb;
color: white;
width: 100%;
}
.btn-primary:hover {
background-color: #1d4ed8;
}
.btn-success {
background-color: #16a34a;
color: white;
}
.btn-success:hover {
background-color: #15803d;
}
.btn-danger {
background-color: #dc2626;
color: white;
margin-right: 10px;
}
.btn-danger:hover {
background-color: #b91c1c;
}
.btn-disabled {
background-color: #9ca3af;
cursor: not-allowed;
}
.card {
background: white;
padding: 16px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin-bottom: 16px;
}
.grid-screen {
display: none;
}
.grid-container {
background: white;
padding: 16px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
display: inline-block;
}
.grid {
display: inline-block;
position: relative;
}
.grid-row {
display: flex;
}
.cell {
width: 40px;
height: 40px;
border: 1px solid #6b7280;
position: relative;
cursor: pointer;
user-select: none;
}
.cell.letter {
background-color: white;
}
.cell.definition {
background-color: #d1d5db;
}
.cell.active {
outline: 2px solid #2563eb;
outline-offset: -2px;
z-index: 10;
}
.cell.highlighted {
background-color: #dbeafe;
}
.cell-content {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: bold;
}
.definition-text {
position: relative;
padding: 2px;
font-size: 5px;
line-height: 6px;
overflow: hidden;
height: 100%;
word-wrap: break-word;
}
.definition-split {
display: flex;
position: absolute;
inset: 0;
}
.definition-half {
width: 50%;
position: relative;
padding: 2px;
font-size: 5px;
line-height: 6px;
overflow: hidden;
word-wrap: break-word;
}
.definition-half:first-child {
border-right: 1px solid #6b7280;
}
/* Visual hint for hyphens: dotted border between cells */
.cell.hyphen {
border-right: 2px dotted #000;
box-shadow: none;
}
.arrow {
position: absolute;
bottom: 0;
right: 0;
}
.controls {
display: flex;
gap: 16px;
margin-top: 16px;
}
.add-controls {
display: flex;
align-items: center;
margin-left: 8px;
}
.add-btn, .remove-btn {
width: 20px;
height: 20px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: #e5e7eb;
border-radius: 3px;
}
.add-btn:hover {
background-color: #d1d5db;
}
.modal {
display: none;
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.show {
display: flex;
}
.modal-content {
background: white;
padding: 24px;
border-radius: 8px;
box-shadow: 0 20px 25px rgba(0, 0, 0, 0.15);
max-width: 600px;
max-height: 500px;
overflow-y: auto;
}
.modal-content h3 {
font-size: 18px;
font-weight: bold;
margin-bottom: 16px;
}
.definition-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
}
.definition-item {
width: 100%;
text-align: left;
padding: 12px;
border: 1px solid #d1d5db;
border-radius: 4px;
background: white;
cursor: pointer;
transition: background-color 0.2s;
}
.definition-item:hover {
background-color: #dbeafe;
}
.add-row-controls {
display: flex;
margin-top: 8px;
}
.add-col-control {
display: flex;
justify-content: center;
width: 40px;
}
</style>
</head>
<body>
<!-- Écran de configuration -->
<div id="setupScreen" class="setup-screen">
<div class="setup-box">
<h1>Créer une grille de mots fléchés</h1>
<div class="form-group">
<label>Largeur (colonnes)</label>
<input type="number" id="widthInput" value="10" min="3" max="30">
</div>
<div class="form-group">
<label>Hauteur (lignes)</label>
<input type="number" id="heightInput" value="10" min="3" max="30">
</div>
<button class="btn-primary" onclick="startGrid()">Commencer</button>
</div>
</div>
<!-- Écran de grille -->
<div id="gridScreen" class="grid-screen">
<div class="container">
<h1>Éditeur de mots fléchés</h1>
<div class="card">
<h2>Charger un dictionnaire</h2>
<div class="form-group">
<input type="text" id="dictionaryUrl" placeholder="Ou entrez une URL pour charger un dictionnaire (raw .txt)" style="width:100%; padding:8px 12px; border:1px solid #d1d5db; border-radius:4px; margin-bottom:8px;">
<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>