1185 lines
46 KiB
HTML
1185 lines
46 KiB
HTML
<!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: 28px;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.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%;
|
||
}
|
||
|
||
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: 56px;
|
||
height: 56px;
|
||
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: 22px;
|
||
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;
|
||
flex-direction: column;
|
||
position: absolute;
|
||
inset: 0;
|
||
}
|
||
|
||
.definition-half {
|
||
height: 50%;
|
||
position: relative;
|
||
padding: 2px;
|
||
font-size: 7px;
|
||
line-height: 8px;
|
||
overflow: hidden;
|
||
word-wrap: break-word;
|
||
display:flex;
|
||
align-items:center;
|
||
justify-content:center;
|
||
font-variant: small-caps;
|
||
text-transform:uppercase;
|
||
}
|
||
|
||
.definition-half:first-child {
|
||
border-bottom: 1px solid #6b7280;
|
||
}
|
||
|
||
.definition-text {
|
||
font-variant: small-caps;
|
||
text-transform:uppercase;
|
||
font-size:9px;
|
||
display:flex;
|
||
align-items:center;
|
||
justify-content:center;
|
||
}
|
||
|
||
.definition-item.selected { background-color: #fde68a; }
|
||
|
||
.def-arrow { position:absolute; pointer-events:none; }
|
||
|
||
/* Visual hint for hyphens: dotted border between cells */
|
||
.cell.hyphen {
|
||
border-right: 2px dotted #000;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.arrow {
|
||
position: absolute;
|
||
bottom: 0;
|
||
right: 0;
|
||
}
|
||
|
||
.controls {
|
||
display: flex;
|
||
gap: 16px;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.add-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.add-btn, .remove-btn {
|
||
width: 20px;
|
||
height: 20px;
|
||
padding: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: #e5e7eb;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.add-btn:hover {
|
||
background-color: #d1d5db;
|
||
}
|
||
|
||
.modal {
|
||
display: none;
|
||
position: fixed;
|
||
inset: 0;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
z-index: 1000;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.modal.show {
|
||
display: flex;
|
||
}
|
||
|
||
.modal-content {
|
||
background: white;
|
||
padding: 24px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 20px 25px rgba(0, 0, 0, 0.15);
|
||
max-width: 600px;
|
||
max-height: 500px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.modal-content h3 {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.definition-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.definition-item {
|
||
width: 100%;
|
||
text-align: left;
|
||
padding: 12px;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 4px;
|
||
background: white;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s;
|
||
}
|
||
|
||
.definition-item:hover {
|
||
background-color: #dbeafe;
|
||
}
|
||
|
||
.add-row-controls {
|
||
display: flex;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.add-col-control {
|
||
display: flex;
|
||
justify-content: center;
|
||
width: 40px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- Écran de configuration -->
|
||
<div id="setupScreen" class="setup-screen">
|
||
<div class="setup-box">
|
||
<h1>Créer une grille de mots fléchés</h1>
|
||
<div class="form-group">
|
||
<label>Largeur (colonnes)</label>
|
||
<input type="number" id="widthInput" value="10" min="3" max="30">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Hauteur (lignes)</label>
|
||
<input type="number" id="heightInput" value="10" min="3" max="30">
|
||
</div>
|
||
<button class="btn-primary" onclick="startGrid()">Commencer</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Écran de grille -->
|
||
<div id="gridScreen" class="grid-screen">
|
||
<div class="container">
|
||
<h1>Éditeur de mots fléchés</h1>
|
||
|
||
<div class="card">
|
||
<h2>Charger un dictionnaire</h2>
|
||
<div class="form-group">
|
||
<input type="text" id="dictionaryUrl" placeholder="Ou entrez une URL pour charger un dictionnaire (raw .txt)" style="width:100%; padding:8px 12px; border:1px solid #d1d5db; border-radius:4px; margin-bottom:8px;">
|
||
<div style="margin-top:8px;margin-bottom:8px;"><button class="btn-primary" onclick="loadDictionaryFromUrl()">Charger depuis URL</button></div>
|
||
<textarea id="dictionaryText" rows="5" placeholder="Collez le contenu du dictionnaire ici (format: mot: définition)"></textarea>
|
||
</div>
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:8px;">
|
||
<button class="btn-success" onclick="loadDictionaryFromTextarea()">Charger depuis zone</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid-container">
|
||
<div id="grid" class="grid"></div>
|
||
<div style="display:flex;gap:8px;margin-top:12px;align-items:center;">
|
||
<button class="btn-success" onclick="addRow()">Ajouter ligne</button>
|
||
<button class="btn-success" onclick="addColumn()">Ajouter colonne</button>
|
||
<button class="btn-primary" onclick="transformActiveToDefinition()">Transformer cellule active en définition</button>
|
||
<button class="btn-danger" onclick="removeRow()">− Supprimer ligne</button>
|
||
<button class="btn-danger" onclick="removeColumn()">− Supprimer colonne</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal de sélection de définition -->
|
||
<div id="definitionModal" class="modal">
|
||
<div class="modal-content">
|
||
<h3 id="modalTitle">Définitions</h3>
|
||
<div id="definitionList" class="definition-list"></div>
|
||
<button class="btn-primary" onclick="closeModal()">Fermer</button>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// État de l'application
|
||
let grid = [];
|
||
let activeCell = null;
|
||
let orientation = 'horizontal';
|
||
let dictionaries = {};
|
||
let currentModal = null;
|
||
|
||
const CELL_SIZE = 40;
|
||
|
||
// Normalise un mot (majuscules, sans accents)
|
||
function normalizeWord(word) {
|
||
return word.toUpperCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
||
}
|
||
|
||
// Crée une grille vide
|
||
function createGrid(width, height) {
|
||
const newGrid = [];
|
||
for (let row = 0; row < height; row++) {
|
||
const rowData = [];
|
||
for (let col = 0; col < width; col++) {
|
||
// indices are 1-based in spec: definition if on first row or first column and index is odd (1-based)
|
||
const isDefinition = ((row === 0 && ((col + 1) % 2 === 1)) || (col === 0 && ((row + 1) % 2 === 1)));
|
||
rowData.push({
|
||
type: isDefinition ? 'definition' : 'letter',
|
||
letter: '',
|
||
definitionH: '',
|
||
definitionV: ''
|
||
});
|
||
}
|
||
newGrid.push(rowData);
|
||
}
|
||
return newGrid;
|
||
}
|
||
|
||
// Démarre la grille
|
||
function startGrid() {
|
||
const width = parseInt(document.getElementById('widthInput').value);
|
||
const height = parseInt(document.getElementById('heightInput').value);
|
||
|
||
grid = createGrid(width, height);
|
||
// Ensure the first-row/first-column odd-index rule is strictly applied
|
||
updateDefinitionTypes();
|
||
|
||
document.getElementById('setupScreen').style.display = 'none';
|
||
document.getElementById('gridScreen').style.display = 'block';
|
||
|
||
renderGrid();
|
||
}
|
||
|
||
// Re-evaluate and enforce first-row/first-column definition rule after structural changes
|
||
function updateDefinitionTypes() {
|
||
for (let r = 0; r < grid.length; r++) {
|
||
for (let c = 0; c < grid[0].length; c++) {
|
||
const onFirstRowRule = (r === 0 && ((c + 1) % 2 === 1));
|
||
const onFirstColRule = (c === 0 && ((r + 1) % 2 === 1));
|
||
if (onFirstRowRule || onFirstColRule) {
|
||
grid[r][c].type = 'definition';
|
||
} else if (r === 0 || c === 0) {
|
||
// if on first row/col but not meeting the odd-index rule, ensure it's a letter
|
||
grid[r][c].type = 'letter';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Obtient le mot à une position donnée
|
||
function getWordAt(row, col, dir) {
|
||
if (!grid[row] || !grid[row][col]) return { word: '', startRow: row, startCol: col };
|
||
|
||
let word = '';
|
||
let startRow = row;
|
||
let startCol = col;
|
||
|
||
// Trouve le début du mot
|
||
if (dir === 'horizontal') {
|
||
while (startCol > 0 && grid[startRow][startCol - 1]?.type === 'letter' && grid[startRow][startCol - 1]?.letter) {
|
||
startCol--;
|
||
}
|
||
// Construit le mot
|
||
let c = startCol;
|
||
while (c < grid[0].length && grid[startRow][c]?.type === 'letter' && grid[startRow][c]?.letter) {
|
||
word += grid[startRow][c].letter;
|
||
c++;
|
||
}
|
||
} else {
|
||
while (startRow > 0 && grid[startRow - 1][startCol]?.type === 'letter' && grid[startRow - 1][startCol]?.letter) {
|
||
startRow--;
|
||
}
|
||
// Construit le mot
|
||
let r = startRow;
|
||
while (r < grid.length && grid[r][startCol]?.type === 'letter' && grid[r][startCol]?.letter) {
|
||
word += grid[r][startCol].letter;
|
||
r++;
|
||
}
|
||
}
|
||
|
||
return { word, startRow, startCol };
|
||
}
|
||
|
||
// Gère le clic sur une case
|
||
function handleCellClick(row, col, half = null) {
|
||
const cell = grid[row][col];
|
||
|
||
if (cell.type === 'definition') {
|
||
// Determine associated words in both directions (starting at the neighboring letter cell)
|
||
// Special rules for top row and left column mappings
|
||
let wordInfoH;
|
||
let wordInfoV;
|
||
|
||
// Horizontal definition normally maps to word starting at (row, col+1) horizontally,
|
||
// but if this definition is on the top row, its horizontal definition maps to the vertical word starting at (row, col+1).
|
||
if (row === 0) {
|
||
wordInfoH = getWordAt(row, col + 1, 'vertical');
|
||
} else {
|
||
wordInfoH = getWordAt(row, col + 1, 'horizontal');
|
||
}
|
||
|
||
// Vertical definition normally maps to word starting at (row+1, col) vertically,
|
||
// but if this definition is on the first column, its vertical definition maps to the horizontal word starting at (row+1, col).
|
||
if (col === 0) {
|
||
wordInfoV = getWordAt(row + 1, col, 'horizontal');
|
||
} else {
|
||
wordInfoV = getWordAt(row + 1, col, 'vertical');
|
||
}
|
||
|
||
// Build words and definitions for both sides (may be empty)
|
||
const wordH = (wordInfoH && wordInfoH.word) ? wordInfoH.word : null;
|
||
const wordV = (wordInfoV && wordInfoV.word) ? wordInfoV.word : null;
|
||
|
||
const defsH = wordH ? ([...new Set(dictionaries[normalizeWord(wordH)] || [])]) : [];
|
||
const defsV = wordV ? ([...new Set(dictionaries[normalizeWord(wordV)] || [])]) : [];
|
||
|
||
// If half specified, still show modal but prioritize that half for selection via UI
|
||
currentModal = { row, col, half };
|
||
showDefinitionModal(row, col, wordH, defsH, wordV, defsV);
|
||
} else {
|
||
if (activeCell && activeCell.row === row && activeCell.col === col) {
|
||
orientation = orientation === 'horizontal' ? 'vertical' : 'horizontal';
|
||
} else {
|
||
activeCell = { row, col };
|
||
}
|
||
renderGrid();
|
||
}
|
||
}
|
||
|
||
// Gère le double-clic
|
||
function handleCellDoubleClick(row, col) {
|
||
const cell = grid[row][col];
|
||
if (cell.type === 'letter' && !cell.letter) {
|
||
// empty letter -> become empty definition
|
||
cell.type = 'definition';
|
||
cell.definitionH = '';
|
||
cell.definitionV = '';
|
||
} else {
|
||
// otherwise toggle
|
||
cell.type = cell.type === 'letter' ? 'definition' : 'letter';
|
||
if (cell.type === 'letter') {
|
||
cell.definitionH = '';
|
||
cell.definitionV = '';
|
||
} else {
|
||
cell.letter = '';
|
||
}
|
||
}
|
||
renderGrid();
|
||
}
|
||
|
||
// Affiche le modal de sélection des définitions, en deux parties (horizontal / vertical)
|
||
function showDefinitionModal(row, col, wordH, defsH, wordV, defsV) {
|
||
currentModal = { row, col };
|
||
document.getElementById('modalTitle').textContent = `Définitions`;
|
||
const list = document.getElementById('definitionList');
|
||
list.innerHTML = '';
|
||
|
||
const sectionH = document.createElement('div');
|
||
const titleH = document.createElement('h4');
|
||
titleH.textContent = 'Mot horizontal';
|
||
sectionH.appendChild(titleH);
|
||
if (!wordH) {
|
||
const p = document.createElement('p');
|
||
p.style.color = '#6b7280';
|
||
p.textContent = 'Aucun mot associé';
|
||
sectionH.appendChild(p);
|
||
} else {
|
||
const wordP = document.createElement('p');
|
||
wordP.style.fontWeight = '600';
|
||
wordP.textContent = wordH;
|
||
sectionH.appendChild(wordP);
|
||
|
||
if (defsH.length === 0) {
|
||
const p = document.createElement('p');
|
||
p.style.color = '#6b7280';
|
||
p.textContent = 'Aucune définition disponible pour ce mot.';
|
||
sectionH.appendChild(p);
|
||
} else {
|
||
defsH.forEach(def => {
|
||
const btn = document.createElement('button');
|
||
btn.className = 'definition-item';
|
||
btn.textContent = def;
|
||
if (grid[row][col].definitionH === def) btn.classList.add('selected');
|
||
btn.onclick = () => {
|
||
grid[row][col].definitionH = def;
|
||
closeModal();
|
||
renderGrid();
|
||
};
|
||
sectionH.appendChild(btn);
|
||
});
|
||
}
|
||
|
||
// Custom input for horizontal definition
|
||
const customH = document.createElement('div');
|
||
customH.style.display = 'flex';
|
||
customH.style.gap = '8px';
|
||
customH.style.marginTop = '8px';
|
||
const inputH = document.createElement('input');
|
||
inputH.type = 'text';
|
||
inputH.value = grid[row][col].definitionH || '';
|
||
inputH.style.flex = '1';
|
||
const saveH = document.createElement('button');
|
||
saveH.className = 'btn-primary';
|
||
saveH.textContent = 'Enregistrer (H)';
|
||
saveH.onclick = () => {
|
||
grid[row][col].definitionH = inputH.value.trim();
|
||
closeModal();
|
||
renderGrid();
|
||
};
|
||
inputH.addEventListener('keydown', (ev) => { if (ev.key === 'Enter') { ev.preventDefault(); saveH.click(); } });
|
||
customH.appendChild(inputH);
|
||
customH.appendChild(saveH);
|
||
sectionH.appendChild(customH);
|
||
}
|
||
|
||
const sectionV = document.createElement('div');
|
||
const titleV = document.createElement('h4');
|
||
titleV.textContent = 'Mot vertical';
|
||
sectionV.appendChild(titleV);
|
||
if (!wordV) {
|
||
const p = document.createElement('p');
|
||
p.style.color = '#6b7280';
|
||
p.textContent = 'Aucun mot associé';
|
||
sectionV.appendChild(p);
|
||
} else {
|
||
const wordP = document.createElement('p');
|
||
wordP.style.fontWeight = '600';
|
||
wordP.textContent = wordV;
|
||
sectionV.appendChild(wordP);
|
||
|
||
if (defsV.length === 0) {
|
||
const p = document.createElement('p');
|
||
p.style.color = '#6b7280';
|
||
p.textContent = 'Aucune définition disponible pour ce mot.';
|
||
sectionV.appendChild(p);
|
||
} else {
|
||
defsV.forEach(def => {
|
||
const btn = document.createElement('button');
|
||
btn.className = 'definition-item';
|
||
btn.textContent = def;
|
||
if (grid[row][col].definitionV === def) btn.classList.add('selected');
|
||
btn.onclick = () => {
|
||
grid[row][col].definitionV = def;
|
||
closeModal();
|
||
renderGrid();
|
||
};
|
||
sectionV.appendChild(btn);
|
||
});
|
||
}
|
||
|
||
// Custom input for vertical definition
|
||
const customV = document.createElement('div');
|
||
customV.style.display = 'flex';
|
||
customV.style.gap = '8px';
|
||
customV.style.marginTop = '8px';
|
||
const inputV = document.createElement('input');
|
||
inputV.type = 'text';
|
||
inputV.value = grid[row][col].definitionV || '';
|
||
inputV.style.flex = '1';
|
||
const saveV = document.createElement('button');
|
||
saveV.className = 'btn-primary';
|
||
saveV.textContent = 'Enregistrer (V)';
|
||
saveV.onclick = () => {
|
||
grid[row][col].definitionV = inputV.value.trim();
|
||
closeModal();
|
||
renderGrid();
|
||
};
|
||
inputV.addEventListener('keydown', (ev) => { if (ev.key === 'Enter') { ev.preventDefault(); saveV.click(); } });
|
||
customV.appendChild(inputV);
|
||
customV.appendChild(saveV);
|
||
sectionV.appendChild(customV);
|
||
}
|
||
|
||
// Delete button: transforms case into letter empty
|
||
const deleteBtn = document.createElement('button');
|
||
deleteBtn.className = 'btn-danger';
|
||
deleteBtn.style.marginTop = '12px';
|
||
deleteBtn.textContent = 'Supprimer (transformer en case lettre)';
|
||
deleteBtn.onclick = () => {
|
||
const cell = grid[row][col];
|
||
const hasDefs = !!(cell.definitionH || cell.definitionV);
|
||
if (hasDefs) {
|
||
if (!confirm('La case contient des définitions. Confirmer la transformation en case lettre vide ?')) return;
|
||
}
|
||
cell.type = 'letter';
|
||
cell.letter = '';
|
||
cell.definitionH = '';
|
||
cell.definitionV = '';
|
||
closeModal();
|
||
renderGrid();
|
||
};
|
||
|
||
// Append sections to list
|
||
list.appendChild(sectionH);
|
||
list.appendChild(sectionV);
|
||
list.appendChild(deleteBtn);
|
||
|
||
document.getElementById('definitionModal').classList.add('show');
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('definitionModal').classList.remove('show');
|
||
currentModal = null;
|
||
}
|
||
|
||
// Charge un dictionnaire
|
||
// Helper to process a single line from a dictionary source
|
||
function processDictionaryLine(line) {
|
||
const match = line.match(/^(.+?):\s*(.+)$/);
|
||
if (match) {
|
||
const word = normalizeWord(match[1].trim());
|
||
const definition = match[2].trim();
|
||
|
||
if (!dictionaries[word]) {
|
||
dictionaries[word] = [];
|
||
}
|
||
dictionaries[word].push(definition);
|
||
}
|
||
}
|
||
|
||
// Load from the textarea (legacy/paste)
|
||
function loadDictionaryFromTextarea() {
|
||
const text = document.getElementById('dictionaryText').value;
|
||
if (!text.trim()) {
|
||
alert('Aucun contenu dans la zone de texte.');
|
||
return;
|
||
}
|
||
dictionaries = {};
|
||
const lines = text.split('\n');
|
||
for (const line of lines) {
|
||
processDictionaryLine(line);
|
||
}
|
||
alert('Dictionnaire chargé avec succès !');
|
||
}
|
||
|
||
// Load a potentially very large dictionary from a URL using streaming
|
||
async function loadDictionaryFromUrl() {
|
||
const url = document.getElementById('dictionaryUrl').value.trim();
|
||
if (!url) {
|
||
alert('Entrez une URL valide.');
|
||
return;
|
||
}
|
||
|
||
dictionaries = {};
|
||
try {
|
||
const resp = await fetch(url);
|
||
if (!resp.ok || !resp.body) {
|
||
alert('Impossible de récupérer le fichier depuis l\'URL fournie.');
|
||
return;
|
||
}
|
||
|
||
const reader = resp.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
let { value, done } = await reader.read();
|
||
let chunk = '';
|
||
while (!done) {
|
||
chunk += decoder.decode(value, { stream: true });
|
||
const lines = chunk.split(/\r?\n/);
|
||
chunk = lines.pop();
|
||
for (const line of lines) {
|
||
processDictionaryLine(line);
|
||
}
|
||
({ value, done } = await reader.read());
|
||
}
|
||
if (chunk) processDictionaryLine(chunk);
|
||
|
||
alert('Dictionnaire chargé depuis URL avec succès !');
|
||
} catch (err) {
|
||
console.error(err);
|
||
alert('Erreur lors du chargement du dictionnaire.');
|
||
}
|
||
}
|
||
|
||
// Ajoute une ligne
|
||
function addRow() {
|
||
const rowIndex = grid.length;
|
||
const newRow = [];
|
||
|
||
for (let col = 0; col < grid[0].length; col++) {
|
||
const isDefinition = (col === 0 && ((rowIndex + 1) % 2 === 1));
|
||
newRow.push({
|
||
type: isDefinition ? 'definition' : 'letter',
|
||
letter: '',
|
||
definitionH: '',
|
||
definitionV: ''
|
||
});
|
||
}
|
||
|
||
grid.push(newRow);
|
||
// Re-evaluate first row/column rule strictly after structural change
|
||
updateDefinitionTypes();
|
||
renderGrid();
|
||
}
|
||
|
||
function transformActiveToDefinition() {
|
||
if (!activeCell) {
|
||
alert('Aucune cellule active sélectionnée.');
|
||
return;
|
||
}
|
||
const { row, col } = activeCell;
|
||
const cell = grid[row][col];
|
||
if (cell.type === 'definition') return;
|
||
if (cell.letter || cell.definitionH || cell.definitionV) {
|
||
if (!confirm('La transformation effacera la lettre actuelle et/ou définitions existantes. Confirmer ?')) return;
|
||
}
|
||
cell.type = 'definition';
|
||
cell.letter = '';
|
||
cell.definitionH = '';
|
||
cell.definitionV = '';
|
||
renderGrid();
|
||
}
|
||
|
||
// Ajoute une colonne
|
||
function addColumn() {
|
||
grid.forEach((row, rowIndex) => {
|
||
const colIndex = row.length;
|
||
const isDefinition = (rowIndex === 0 && ((colIndex + 1) % 2 === 1));
|
||
row.push({
|
||
type: isDefinition ? 'definition' : 'letter',
|
||
letter: '',
|
||
definitionH: '',
|
||
definitionV: ''
|
||
});
|
||
});
|
||
updateDefinitionTypes();
|
||
renderGrid();
|
||
}
|
||
|
||
// Supprime une ligne
|
||
function removeRow() {
|
||
if (grid.length > 1) {
|
||
const lastRowIndex = grid.length - 1;
|
||
const rowToCheck = grid[lastRowIndex];
|
||
let hasContent = false;
|
||
for (const cell of rowToCheck) {
|
||
if ((cell.type === 'letter' && cell.letter) || (cell.type === 'definition' && (cell.definitionH || cell.definitionV))) {
|
||
hasContent = true;
|
||
break;
|
||
}
|
||
}
|
||
if (hasContent) {
|
||
const ok = confirm('La ligne contient des lettres ou des définitions. Confirmer la suppression ?');
|
||
if (!ok) return;
|
||
}
|
||
|
||
grid.pop();
|
||
if (activeCell && activeCell.row >= grid.length) {
|
||
activeCell = null;
|
||
}
|
||
updateDefinitionTypes();
|
||
renderGrid();
|
||
}
|
||
}
|
||
|
||
// Supprime une colonne
|
||
function removeColumn() {
|
||
if (grid[0].length > 1) {
|
||
const lastColIndex = grid[0].length - 1;
|
||
let hasContent = false;
|
||
for (const row of grid) {
|
||
const cell = row[lastColIndex];
|
||
if ((cell.type === 'letter' && cell.letter) || (cell.type === 'definition' && (cell.definitionH || cell.definitionV))) {
|
||
hasContent = true;
|
||
break;
|
||
}
|
||
}
|
||
if (hasContent) {
|
||
const ok = confirm('La colonne contient des lettres ou des définitions. Confirmer la suppression ?');
|
||
if (!ok) return;
|
||
}
|
||
|
||
grid.forEach(row => row.pop());
|
||
if (activeCell && activeCell.col >= grid[0].length) {
|
||
activeCell = null;
|
||
}
|
||
updateDefinitionTypes();
|
||
renderGrid();
|
||
}
|
||
}
|
||
|
||
// Rend la grille
|
||
function renderGrid() {
|
||
const gridEl = document.getElementById('grid');
|
||
gridEl.innerHTML = '';
|
||
|
||
grid.forEach((row, rowIndex) => {
|
||
const rowEl = document.createElement('div');
|
||
rowEl.className = 'grid-row';
|
||
|
||
row.forEach((cell, colIndex) => {
|
||
const cellEl = document.createElement('div');
|
||
cellEl.className = 'cell ' + cell.type;
|
||
|
||
const isActive = activeCell?.row === rowIndex && activeCell?.col === colIndex;
|
||
const isHighlighted = activeCell && cell.type === 'letter' && (
|
||
(orientation === 'horizontal' && activeCell.row === rowIndex) ||
|
||
(orientation === 'vertical' && activeCell.col === colIndex)
|
||
);
|
||
|
||
if (isActive) cellEl.classList.add('active');
|
||
if (isHighlighted) cellEl.classList.add('highlighted');
|
||
|
||
let hasBoth = false;
|
||
|
||
if (cell.type === 'letter') {
|
||
const content = document.createElement('div');
|
||
content.className = 'cell-content';
|
||
content.textContent = cell.letter;
|
||
cellEl.appendChild(content);
|
||
// If the letter is a hyphen, add visual dotted border indication
|
||
if (cell.letter === '-') {
|
||
cellEl.classList.add('hyphen');
|
||
}
|
||
} else {
|
||
hasBoth = cell.definitionH && cell.definitionV;
|
||
|
||
if (hasBoth) {
|
||
const split = document.createElement('div');
|
||
split.className = 'definition-split';
|
||
|
||
const halfH = document.createElement('div');
|
||
halfH.className = 'definition-half';
|
||
halfH.textContent = cell.definitionH;
|
||
|
||
const halfV = document.createElement('div');
|
||
halfV.className = 'definition-half';
|
||
halfV.textContent = cell.definitionV;
|
||
|
||
split.appendChild(halfH);
|
||
split.appendChild(halfV);
|
||
cellEl.appendChild(split);
|
||
|
||
halfH.onclick = (e) => {
|
||
e.stopPropagation();
|
||
handleCellClick(rowIndex, colIndex, 'h');
|
||
};
|
||
halfV.onclick = (e) => {
|
||
e.stopPropagation();
|
||
handleCellClick(rowIndex, colIndex, 'v');
|
||
};
|
||
} else {
|
||
const text = document.createElement('div');
|
||
text.className = 'definition-text';
|
||
text.textContent = cell.definitionH || cell.definitionV;
|
||
cellEl.appendChild(text);
|
||
}
|
||
}
|
||
|
||
if (!hasBoth) {
|
||
cellEl.onclick = () => handleCellClick(rowIndex, colIndex);
|
||
}
|
||
cellEl.ondblclick = () => handleCellDoubleClick(rowIndex, colIndex);
|
||
|
||
rowEl.appendChild(cellEl);
|
||
// store reference for later arrow placement
|
||
if (!window._cellEls) window._cellEls = [];
|
||
if (!window._cellEls[rowIndex]) window._cellEls[rowIndex] = [];
|
||
window._cellEls[rowIndex][colIndex] = cellEl;
|
||
});
|
||
|
||
// Bouton d'ajout de colonne
|
||
const addColBtn = document.createElement('div');
|
||
addColBtn.className = 'add-controls';
|
||
addColBtn.innerHTML = '<button class="add-btn" onclick="addColumn()">+</button>';
|
||
rowEl.appendChild(addColBtn);
|
||
|
||
gridEl.appendChild(rowEl);
|
||
});
|
||
|
||
// No per-cell + buttons (global controls only)
|
||
|
||
// After grid built, place arrows in the first-letter cells based on adjacent definition cells
|
||
if (window._cellEls) {
|
||
for (let r = 0; r < grid.length; r++) {
|
||
for (let c = 0; c < grid[0].length; c++) {
|
||
const el = window._cellEls[r] && window._cellEls[r][c];
|
||
if (el) {
|
||
const prev = el.querySelectorAll('.def-arrow');
|
||
prev.forEach(p => p.remove());
|
||
}
|
||
}
|
||
}
|
||
|
||
for (let dr = 0; dr < grid.length; dr++) {
|
||
for (let dc = 0; dc < grid[0].length; dc++) {
|
||
const defCell = grid[dr][dc];
|
||
if (defCell.type !== 'definition') continue;
|
||
|
||
// Horizontal mapping: def at (dr,dc) -> word starts at (dr,dc+1) normally
|
||
let trH = dr;
|
||
let tcH = dc + 1;
|
||
// top-row special: horizontal definition maps to vertical word starting at (dr,dc+1)
|
||
let horizontalIsVertical = false;
|
||
if (dr === 0) {
|
||
horizontalIsVertical = true;
|
||
trH = dr;
|
||
tcH = dc + 1;
|
||
}
|
||
|
||
if (defCell.definitionH && tcH < grid[0].length) {
|
||
const targetEl = window._cellEls[trH] && window._cellEls[trH][tcH];
|
||
if (targetEl) {
|
||
const arrow = document.createElement('div');
|
||
arrow.className = 'def-arrow';
|
||
if (horizontalIsVertical) {
|
||
// arrow downwards from top border
|
||
arrow.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1"><path d="M12 3 L12 18"/><path d="M12 18 L8 14 M12 18 L16 14"/></svg>';
|
||
arrow.style.top = '-10px';
|
||
arrow.style.left = '50%';
|
||
arrow.style.transform = 'translateX(-50%)';
|
||
} else {
|
||
// arrow pointing leftwards from left border into cell (originates from definition at left)
|
||
arrow.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1"><path d="M3 12 L18 12"/><path d="M18 12 L14 8 M18 12 L14 16"/></svg>';
|
||
arrow.style.left = '-10px';
|
||
arrow.style.top = '50%';
|
||
arrow.style.transform = 'translateY(-50%)';
|
||
}
|
||
targetEl.appendChild(arrow);
|
||
}
|
||
}
|
||
|
||
// Vertical mapping: def at (dr,dc) -> word starts at (dr+1,dc) normally
|
||
let trV = dr + 1;
|
||
let tcV = dc;
|
||
// left-column special: vertical definition maps to horizontal word starting at (dr+1,dc)
|
||
let verticalIsHorizontal = false;
|
||
if (dc === 0) {
|
||
verticalIsHorizontal = true;
|
||
trV = dr + 1;
|
||
tcV = dc;
|
||
}
|
||
|
||
if (defCell.definitionV && trV < grid.length) {
|
||
const targetEl = window._cellEls[trV] && window._cellEls[trV][tcV];
|
||
if (targetEl) {
|
||
const arrow = document.createElement('div');
|
||
arrow.className = 'def-arrow';
|
||
if (verticalIsHorizontal) {
|
||
// arrow rightwards from left border
|
||
arrow.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1"><path d="M3 12 L18 12"/><path d="M18 12 L14 8 M18 12 L14 16"/></svg>';
|
||
arrow.style.left = '-10px';
|
||
arrow.style.top = '50%';
|
||
arrow.style.transform = 'translateY(-50%)';
|
||
} else {
|
||
// arrow pointing upwards from top border? we want from definition to word, so arrow downwards from top border
|
||
arrow.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1"><path d="M12 3 L12 18"/><path d="M12 18 L8 14 M12 18 L16 14"/></svg>';
|
||
arrow.style.top = '-10px';
|
||
arrow.style.left = '50%';
|
||
arrow.style.transform = 'translateX(-50%)';
|
||
}
|
||
targetEl.appendChild(arrow);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Gestion du clavier (saisie, navigation, suppression)
|
||
document.addEventListener('keydown', (e) => {
|
||
// don't intercept when modifier keys are pressed
|
||
if (e.ctrlKey || e.altKey || e.metaKey) return;
|
||
if (document.getElementById('gridScreen').style.display === 'none') return;
|
||
|
||
// Toggle orientation with Tab when an active cell exists
|
||
if (e.key === 'Tab') {
|
||
if (!activeCell) return;
|
||
e.preventDefault();
|
||
orientation = orientation === 'horizontal' ? 'vertical' : 'horizontal';
|
||
renderGrid();
|
||
return;
|
||
}
|
||
|
||
if (!activeCell) return;
|
||
const { row, col } = activeCell;
|
||
|
||
// Enter: transform empty letter into definition (or open modal if definition exists)
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
if (grid[row][col].type === 'letter' && !grid[row][col].letter) {
|
||
transformActiveToDefinition();
|
||
return;
|
||
}
|
||
if (grid[row][col].type === 'definition') {
|
||
// open modal for this definition
|
||
const wordInfoH = (row === 0) ? getWordAt(row, col + 1, 'vertical') : getWordAt(row, col + 1, 'horizontal');
|
||
const wordInfoV = (col === 0) ? getWordAt(row + 1, col, 'horizontal') : getWordAt(row + 1, col, 'vertical');
|
||
const wordH = (wordInfoH && wordInfoH.word) ? wordInfoH.word : null;
|
||
const wordV = (wordInfoV && wordInfoV.word) ? wordInfoV.word : null;
|
||
const defsH = wordH ? ([...new Set(dictionaries[normalizeWord(wordH)] || [])]) : [];
|
||
const defsV = wordV ? ([...new Set(dictionaries[normalizeWord(wordV)] || [])]) : [];
|
||
showDefinitionModal(row, col, wordH, defsH, wordV, defsV);
|
||
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();
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |