template html & css
This commit is contained in:
7
css/.vscode/settings.json
vendored
Normal file
7
css/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"workbench.colorCustomizations": {
|
||||
"editor.background": "#d0e1f7", // couleur de fond de l’éditeur
|
||||
"sideBar.background": "#d0e1f7", // couleur de fond de la barre latérale
|
||||
"activityBar.background": "#d0e1f7" // barre d’activité
|
||||
}
|
||||
}
|
||||
102
css/flexbox.html
Normal file
102
css/flexbox.html
Normal file
@@ -0,0 +1,102 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Flexbox</title>
|
||||
<style>
|
||||
.cluster {
|
||||
width: 1200px;
|
||||
}
|
||||
.flexbox {
|
||||
display: flex;
|
||||
width: 1200px;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.box {
|
||||
/* en dehors de la division */
|
||||
margin: 4px;
|
||||
/* dans la division */
|
||||
padding: 4px;
|
||||
background-color: #e1dfdf;
|
||||
border: 1px solid black;
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.box_3 {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
|
||||
.flexbox[data-flex-wrap] {
|
||||
/* flex-wrap: wrap; */
|
||||
flex-direction: column
|
||||
}
|
||||
|
||||
.flexbox[data-space] {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flexbox[data-align-center] {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Presentation styles
|
||||
.flexbox>* {
|
||||
max-width: 500px;
|
||||
}
|
||||
*/
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Ce code s'exécutera seulement après que tout le HTML sera chargé
|
||||
const switches = document.querySelectorAll('[role="switch"]');
|
||||
const flexbox = document.querySelector(".flexbox");
|
||||
|
||||
switches.forEach((item) => {
|
||||
item.addEventListener("change", () => {
|
||||
if (item.checked) {
|
||||
flexbox.setAttribute(`data-${item.getAttribute("id")}`, "");
|
||||
} else {
|
||||
flexbox.removeAttribute(`data-${item.getAttribute("id")}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
</body>
|
||||
<div class="cluster">
|
||||
<div >
|
||||
<label for="flex-wrap">
|
||||
<input type="checkbox" role="switch" id="flex-wrap" />
|
||||
<span>Turn on wrapping</span>
|
||||
|
||||
</label>
|
||||
<label for="align-center">
|
||||
<input type="checkbox" role="switch" id="align-center" />
|
||||
<span>Align items in the center</span>
|
||||
</label>
|
||||
<label for="space">
|
||||
<input type="checkbox" role="switch" id="space" />
|
||||
<span>Add space between</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flexbox">
|
||||
<div class="box">box 1</div>
|
||||
<div class="box">box 2</div>
|
||||
<div class="box box_3">box 3</div>
|
||||
</div>
|
||||
|
||||
</html>
|
||||
108
css/grid.html
Normal file
108
css/grid.html
Normal file
@@ -0,0 +1,108 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Grid</title>
|
||||
<style>
|
||||
.cluster {
|
||||
width: 1200px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
width: 1200px;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.box {
|
||||
/* Centrage Horizontal du texte/inline (EXISTANT) */
|
||||
text-align: center;
|
||||
|
||||
/* NOUVEAUX STYLES pour le Centrage Vertical */
|
||||
/* 1. Transforme la boîte en conteneur Flex */
|
||||
display: flex;
|
||||
|
||||
/* 2. Centre les éléments enfants sur l'axe vertical (axe croisé) */
|
||||
align-items: center;
|
||||
|
||||
/* 3. Centre les éléments enfants sur l'axe horizontal (axe principal) */
|
||||
/* C'est une alternative à text-align: center; pour les éléments de bloc */
|
||||
justify-content: center;
|
||||
/* dans la division */
|
||||
|
||||
padding: 4px;
|
||||
background-color: #e1dfdf;
|
||||
border: 1px solid black;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.box_3 {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
|
||||
.flexbox[data-flex-wrap] {
|
||||
/* flex-wrap: wrap; */
|
||||
flex-direction: column
|
||||
}
|
||||
|
||||
.flexbox[data-space] {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flexbox[data-align-center] {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Presentation styles
|
||||
.flexbox>* {
|
||||
max-width: 500px;
|
||||
}
|
||||
*/
|
||||
</style>
|
||||
|
||||
<script>
|
||||
/*
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Ce code s'exécutera seulement après que tout le HTML sera chargé
|
||||
const switches = document.querySelectorAll('[role="switch"]');
|
||||
const flexbox = document.querySelector(".flexbox");
|
||||
|
||||
switches.forEach((item) => {
|
||||
item.addEventListener("change", () => {
|
||||
if (item.checked) {
|
||||
flexbox.setAttribute(`data-${item.getAttribute("id")}`, "");
|
||||
} else {
|
||||
flexbox.removeAttribute(`data-${item.getAttribute("id")}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
*/
|
||||
|
||||
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
</body>
|
||||
|
||||
|
||||
<div class="cluster">
|
||||
<div class="grid">
|
||||
<div class="box">Item 1</div>
|
||||
<div class="box">Item 2</div>
|
||||
<div class="box">Item 3</div>
|
||||
<div class="box">Item 4</div>
|
||||
<div class="box">Item 5</div>
|
||||
<div class="box">Item 6</div>
|
||||
<div class="box">Item 7</div>
|
||||
<div class="box">Item 8</div>
|
||||
<div class="box">Item 9</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</html>
|
||||
93
css/interface_base.html
Normal file
93
css/interface_base.html
Normal file
@@ -0,0 +1,93 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Interface base </title>
|
||||
<style>
|
||||
/* Le conteneur parent pour toute l'application */
|
||||
.app-layout {
|
||||
display: grid;
|
||||
/* Définit une grille de 2 colonnes (250px et le reste) */
|
||||
grid-template-columns: 250px 1fr;
|
||||
/* Définit 3 lignes de haut en bas */
|
||||
grid-template-rows: auto 1fr auto;
|
||||
|
||||
/* Attribue un nom à chaque zone de la grille */
|
||||
grid-template-areas:
|
||||
"header header"
|
||||
"sidebar content"
|
||||
"footer footer";
|
||||
|
||||
/* Assure que la grille prend toute la hauteur visible */
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 1. Placement des zones */
|
||||
.header {
|
||||
grid-area: header;
|
||||
background-color: #333;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
grid-area: sidebar;
|
||||
background-color: #f4f4f4;
|
||||
padding: 15px;
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
grid-area: content;
|
||||
padding: 20px;
|
||||
/* Le défilement se produit ici si le contenu dépasse la hauteur */
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
grid-area: footer;
|
||||
background-color: #333;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Styles pour les éléments internes (textes, boutons, etc.) */
|
||||
.content-area button {
|
||||
padding: 10px 15px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<header class="header">
|
||||
Votre bandeau du haut
|
||||
</header>
|
||||
|
||||
<aside class="sidebar">
|
||||
Panneau latéral (ici, votre "panneau haut")
|
||||
</aside>
|
||||
|
||||
<main class="content-area" >
|
||||
<h1>Conteneur Principal</h1>
|
||||
<p>Ici vous ajoutez vos éléments : textes, boutons, etc.</p>
|
||||
<button>Un bouton</button>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
Bandeau du bas / Pied de page
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
326
css/interface_dynamique.html
Normal file
326
css/interface_dynamique.html
Normal file
@@ -0,0 +1,326 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Interface dynamique</title>
|
||||
<style>
|
||||
/* Le conteneur parent pour toute l'application */
|
||||
.app-layout {
|
||||
display: grid;
|
||||
/* Définit une grille de 2 colonnes (250px et le reste) */
|
||||
grid-template-columns: 250px 1fr;
|
||||
/* Définit 3 lignes de haut en bas */
|
||||
grid-template-rows: auto 1fr auto;
|
||||
|
||||
/* Attribue un nom à chaque zone de la grille */
|
||||
grid-template-areas:
|
||||
"header header"
|
||||
"sidebar content"
|
||||
"footer footer";
|
||||
|
||||
/* Assure que la grille prend toute la hauteur visible */
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 1. Placement des zones */
|
||||
.header {
|
||||
grid-area: header;
|
||||
background-color: #333;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
grid-area: sidebar;
|
||||
background-color: #f4f4f4;
|
||||
padding: 15px;
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
grid-area: content;
|
||||
padding: 20px;
|
||||
/* Le défilement se produit ici si le contenu dépasse la hauteur */
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
grid-area: footer;
|
||||
background-color: #333;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Styles pour les éléments internes (textes, boutons, etc.) */
|
||||
.content-area button {
|
||||
padding: 10px 15px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dynamic-text {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
background-color: #e6f7ff;
|
||||
border: 1px solid #b3e0ff;
|
||||
cursor: grab;
|
||||
/* Indique que l'élément peut être déplacé */
|
||||
}
|
||||
|
||||
.dynamic-button {
|
||||
padding: 10px 15px;
|
||||
margin: 5px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dynamic-combobox {
|
||||
padding: 8px 12px;
|
||||
margin: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
font-size: 16px;
|
||||
display: block;
|
||||
/* Pour qu'elle prenne sa propre ligne */
|
||||
}
|
||||
|
||||
/* liste */
|
||||
.dynamic-list {
|
||||
margin: 15px 0 15px 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #dcdcdc;
|
||||
background-color: #fafafa;
|
||||
/* Barre de couleur sur le côté */
|
||||
border-left: 15px solid #007bff;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.dynamic-list li {
|
||||
margin-bottom: 5px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
/* tableau */
|
||||
.dynamic-table {
|
||||
width: 80%;
|
||||
/* Prend 80% de la largeur du conteneur */
|
||||
border-collapse: collapse;
|
||||
/* Joint les bordures des cellules */
|
||||
margin: 20px 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.dynamic-table th,
|
||||
.dynamic-table td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.dynamic-table th {
|
||||
background-color: #f2f2f2;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// 1. Cible l'endroit où les nouveaux éléments seront insérés
|
||||
const contentArea = document.getElementById('content-area');
|
||||
|
||||
// 2. Cible les boutons de contrôle dans la sidebar
|
||||
const addTextBtn = document.getElementById('add-text-btn');
|
||||
const addButtonBtn = document.getElementById('add-button-btn');
|
||||
const addComboboxBtn = document.getElementById('add-cb-btn');
|
||||
const addListBtn = document.getElementById('add-list-btn');
|
||||
const addTableBtn = document.getElementById('add-table-btn');
|
||||
|
||||
// --- Fonction pour ajouter un Paragraphe ---
|
||||
addTextBtn.addEventListener('click', function () {
|
||||
// Créer un nouvel élément <p>
|
||||
const newParagraph = document.createElement('p');
|
||||
|
||||
// Lui donner du contenu et des styles
|
||||
newParagraph.textContent = 'Nouveau paragraphe ajouté dynamiquement.';
|
||||
newParagraph.className = 'dynamic-text';
|
||||
|
||||
// Insérer l'élément dans la zone de contenu
|
||||
contentArea.appendChild(newParagraph);
|
||||
|
||||
// Optionnel : rendre le texte éditable (clic pour modifier)
|
||||
newParagraph.setAttribute('contenteditable', 'true');
|
||||
});
|
||||
|
||||
// --- Fonction pour ajouter un Bouton ---
|
||||
addButtonBtn.addEventListener('click', function () {
|
||||
// Créer un nouvel élément <button>
|
||||
const newButton = document.createElement('button');
|
||||
|
||||
// Lui donner du contenu et des styles
|
||||
newButton.textContent = 'Bouton dynamique';
|
||||
newButton.className = 'dynamic-button';
|
||||
|
||||
// Insérer l'élément dans la zone de contenu
|
||||
contentArea.appendChild(newButton);
|
||||
});
|
||||
addComboboxBtn.addEventListener('click', function () {
|
||||
|
||||
// 1. Création de l'élément <select> (la combobox)
|
||||
const newSelect = document.createElement('select');
|
||||
newSelect.className = 'dynamic-combobox';
|
||||
newSelect.id = 'combobox-' + Date.now(); // Donne un ID unique
|
||||
|
||||
// 2. Création des options
|
||||
|
||||
// Option par défaut/neutre
|
||||
const defaultOption = document.createElement('option');
|
||||
defaultOption.textContent = 'Sélectionnez une option';
|
||||
defaultOption.value = '';
|
||||
defaultOption.disabled = true;
|
||||
defaultOption.selected = true;
|
||||
newSelect.appendChild(defaultOption);
|
||||
|
||||
// Option 1
|
||||
const option1 = document.createElement('option');
|
||||
option1.textContent = 'Choix A';
|
||||
option1.value = 'a';
|
||||
newSelect.appendChild(option1);
|
||||
|
||||
// Option 2
|
||||
const option2 = document.createElement('option');
|
||||
option2.textContent = 'Choix B';
|
||||
option2.value = 'b';
|
||||
newSelect.appendChild(option2);
|
||||
|
||||
// Option 3
|
||||
const option3 = document.createElement('option');
|
||||
option3.textContent = 'Choix C';
|
||||
option3.value = 'c';
|
||||
newSelect.appendChild(option3);
|
||||
|
||||
// 3. Insérer la combobox dans la zone de contenu
|
||||
contentArea.appendChild(newSelect);
|
||||
|
||||
});
|
||||
|
||||
addListBtn.addEventListener('click', function () {
|
||||
|
||||
// 1. Création de l'élément parent de la liste (non ordonnée)
|
||||
const newList = document.createElement('ul');
|
||||
newList.className = 'dynamic-list';
|
||||
newList.id = 'list-' + Date.now(); // ID unique
|
||||
|
||||
// 2. Création et ajout des éléments de la liste (<li>)
|
||||
|
||||
// Élément 1
|
||||
const item1 = document.createElement('li');
|
||||
item1.textContent = 'Premier élément de la liste';
|
||||
// Rendre le contenu éditable pour l'interface visuelle
|
||||
item1.setAttribute('contenteditable', 'true');
|
||||
newList.appendChild(item1);
|
||||
|
||||
// Élément 2
|
||||
const item2 = document.createElement('li');
|
||||
item2.textContent = 'Deuxième élément de la liste';
|
||||
item2.setAttribute('contenteditable', 'true');
|
||||
newList.appendChild(item2);
|
||||
|
||||
// Élément 3
|
||||
const item3 = document.createElement('li');
|
||||
item3.textContent = 'Troisième élément de la liste';
|
||||
item3.setAttribute('contenteditable', 'true');
|
||||
newList.appendChild(item3);
|
||||
|
||||
// 3. Insérer la liste complète dans la zone de contenu
|
||||
contentArea.appendChild(newList);
|
||||
|
||||
});
|
||||
|
||||
|
||||
addTableBtn.addEventListener('click', function () {
|
||||
|
||||
// 1. Création de l'élément parent <table>
|
||||
const newTable = document.createElement('table');
|
||||
newTable.className = 'dynamic-table';
|
||||
newTable.id = 'table-' + Date.now(); // ID unique
|
||||
|
||||
// 2. Création de l'en-tête du tableau (<thead>)
|
||||
const tableHead = newTable.createTHead();
|
||||
const headerRow = tableHead.insertRow();
|
||||
|
||||
// Ajout des cellules d'en-tête (<th>)
|
||||
['Nom', 'Âge', 'Ville'].forEach(headerText => {
|
||||
let th = document.createElement('th');
|
||||
th.textContent = headerText;
|
||||
headerRow.appendChild(th);
|
||||
});
|
||||
|
||||
// 3. Création du corps du tableau (<tbody>)
|
||||
const tableBody = newTable.createTBody();
|
||||
|
||||
// Données à insérer
|
||||
const data = [
|
||||
['Alice', 28, 'Paris'],
|
||||
['Bob', 35, 'Lyon']
|
||||
];
|
||||
|
||||
// Ajout des lignes de données (<tr>)
|
||||
data.forEach(rowData => {
|
||||
const row = tableBody.insertRow();
|
||||
rowData.forEach(cellData => {
|
||||
const cell = row.insertCell();
|
||||
cell.textContent = cellData;
|
||||
// Rendre les cellules éditables
|
||||
cell.setAttribute('contenteditable', 'true');
|
||||
});
|
||||
});
|
||||
|
||||
// 4. Insérer le tableau complet dans la zone de contenu
|
||||
contentArea.appendChild(newTable);
|
||||
|
||||
});
|
||||
|
||||
// --- (Optionnel) Ajout de la logique de Glisser-Déposer ---
|
||||
// Le vrai "glisser-déposer" (drag-and-drop) nécessite beaucoup plus de code
|
||||
// (utilisation des événements dragstart, dragover, drop...) pour être implémenté.
|
||||
// L'exemple ci-dessus est la méthode "Ajout par clic" qui est plus simple.
|
||||
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<header class="header">
|
||||
Votre bandeau du haut
|
||||
</header>
|
||||
|
||||
<aside class="sidebar">
|
||||
<h3>Outils</h3>
|
||||
<button id="add-text-btn" class="dynamic-button">Ajouter un Paragraphe</button>
|
||||
<button id="add-button-btn" class="dynamic-button">Ajouter un Bouton</button>
|
||||
<button id="add-cb-btn" class="dynamic-button">Ajouter un Combobox</button>
|
||||
<button id="add-list-btn" class="dynamic-button">Ajouter une liste</button>
|
||||
<button id="add-table-btn" class="dynamic-button">Ajouter un Tableau</button>
|
||||
</aside>
|
||||
|
||||
<main class="content-area" id="content-area">
|
||||
<h1>Conteneur Principal</h1>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
Bandeau du bas / Pied de page
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
445
css/interface_glisser.html
Normal file
445
css/interface_glisser.html
Normal file
@@ -0,0 +1,445 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Interface glisser</title>
|
||||
<style>
|
||||
/* Le conteneur parent pour toute l'application */
|
||||
.app-layout {
|
||||
display: grid;
|
||||
/* Définit une grille de 2 colonnes (250px et le reste) */
|
||||
grid-template-columns: 250px 1fr;
|
||||
/* Définit 3 lignes de haut en bas */
|
||||
grid-template-rows: auto 1fr auto;
|
||||
|
||||
/* Attribue un nom à chaque zone de la grille */
|
||||
grid-template-areas:
|
||||
"header header"
|
||||
"sidebar content"
|
||||
"footer footer";
|
||||
|
||||
/* Assure que la grille prend toute la hauteur visible */
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 1. Placement des zones */
|
||||
.header {
|
||||
grid-area: header;
|
||||
background-color: #333;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
grid-area: sidebar;
|
||||
background-color: #f4f4f4;
|
||||
padding: 15px;
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
grid-area: content;
|
||||
padding: 20px;
|
||||
/* Le défilement se produit ici si le contenu dépasse la hauteur */
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Style appliqué à la zone de contenu quand on glisse un élément dessus */
|
||||
.content-area.drag-over {
|
||||
border: 2px dashed #007bff;
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
|
||||
/* Style appliqué à l'élément fantôme pendant le glissement */
|
||||
.dynamic-item-dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.dynamic-item {
|
||||
/* Rendre l'élément visuellement déplaçable */
|
||||
cursor: grab;
|
||||
|
||||
/* Espacement pour séparer les éléments */
|
||||
margin-bottom: 10px;
|
||||
padding: 5px;
|
||||
|
||||
/* Indication visuelle subtile */
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.footer {
|
||||
grid-area: footer;
|
||||
background-color: #333;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Styles pour les éléments internes (textes, boutons, etc.) */
|
||||
.content-area button {
|
||||
padding: 10px 15px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dynamic-text {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
background-color: #e6f7ff;
|
||||
border: 1px solid #b3e0ff;
|
||||
cursor: grab;
|
||||
/* Indique que l'élément peut être déplacé */
|
||||
}
|
||||
|
||||
.dynamic-button {
|
||||
padding: 10px 15px;
|
||||
margin: 5px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dynamic-combobox {
|
||||
/* sur la même ligne */
|
||||
display: inline-block;
|
||||
|
||||
padding: 8px 12px;
|
||||
margin: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
font-size: 16px;
|
||||
/* Pour qu'elle prenne sa propre ligne
|
||||
display: block; */
|
||||
}
|
||||
|
||||
/* liste */
|
||||
.dynamic-list {
|
||||
/* sur la même ligne */
|
||||
display: inline-block;
|
||||
margin: 15px 0 15px 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #dcdcdc;
|
||||
background-color: #fafafa;
|
||||
/* Barre de couleur sur le côté */
|
||||
border-left: 15px solid #007bff;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.dynamic-list li {
|
||||
margin-bottom: 5px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
/* tableau */
|
||||
.dynamic-table {
|
||||
width: 80%;
|
||||
/* Prend 80% de la largeur du conteneur */
|
||||
border-collapse: collapse;
|
||||
/* Joint les bordures des cellules */
|
||||
margin: 20px 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.dynamic-table th,
|
||||
.dynamic-table td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
/* MODIFICATION CLÉ */
|
||||
}
|
||||
|
||||
.dynamic-table th {
|
||||
background-color: #f2f2f2;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let index = 0;
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// 1. Cible l'endroit où les nouveaux éléments seront insérés
|
||||
const contentArea = document.getElementById('content-area');
|
||||
|
||||
// 2. Cible les boutons de contrôle dans la sidebar
|
||||
const addTextBtn = document.getElementById('add-text-btn');
|
||||
const addButtonBtn = document.getElementById('add-button-btn');
|
||||
const addComboboxBtn = document.getElementById('add-cb-btn');
|
||||
const addListBtn = document.getElementById('add-list-btn');
|
||||
const addTableBtn = document.getElementById('add-table-btn');
|
||||
|
||||
// --- Fonction pour ajouter un Paragraphe ---
|
||||
addTextBtn.addEventListener('click', function () {
|
||||
// Créer un nouvel élément <p>
|
||||
const newParagraph = document.createElement('p');
|
||||
|
||||
// Lui donner du contenu et des styles
|
||||
newParagraph.textContent = 'Nouveau paragraphe ajouté dynamiquement.';
|
||||
newParagraph.className = 'dynamic-item dynamic-text';
|
||||
newParagraph.setAttribute('draggable', 'true');
|
||||
// Insérer l'élément dans la zone de contenu
|
||||
contentArea.appendChild(newParagraph);
|
||||
|
||||
// Optionnel : rendre le texte éditable (clic pour modifier)
|
||||
newParagraph.setAttribute('contenteditable', 'true');
|
||||
});
|
||||
|
||||
// --- Fonction pour ajouter un Bouton ---
|
||||
addButtonBtn.addEventListener('click', function () {
|
||||
// Créer un nouvel élément <button>
|
||||
const newButton = document.createElement('button');
|
||||
|
||||
// Lui donner du contenu et des styles
|
||||
newButton.textContent = 'Bouton ' + Number(++index);
|
||||
newButton.className = 'dynamic-item dynamic-button';
|
||||
newButton.setAttribute('draggable', 'true');
|
||||
// Insérer l'élément dans la zone de contenu
|
||||
contentArea.appendChild(newButton);
|
||||
});
|
||||
addComboboxBtn.addEventListener('click', function () {
|
||||
|
||||
// 1. Création de l'élément <select> (la combobox)
|
||||
const newSelect = document.createElement('select');
|
||||
newSelect.className = 'dynamic-item dynamic-combobox';
|
||||
newSelect.setAttribute('draggable', 'true');
|
||||
newSelect.id = 'combobox-' + Date.now(); // Donne un ID unique
|
||||
|
||||
// 2. Création des options
|
||||
|
||||
// Option par défaut/neutre
|
||||
const defaultOption = document.createElement('option');
|
||||
defaultOption.textContent = 'Sélectionnez une option';
|
||||
defaultOption.value = '';
|
||||
defaultOption.disabled = true;
|
||||
defaultOption.selected = true;
|
||||
newSelect.appendChild(defaultOption);
|
||||
|
||||
// Option 1
|
||||
const option1 = document.createElement('option');
|
||||
option1.textContent = 'Choix ' + Number(++index) ;
|
||||
option1.value = 'a';
|
||||
newSelect.appendChild(option1);
|
||||
|
||||
// Option 2
|
||||
const option2 = document.createElement('option');
|
||||
option2.textContent = 'Choix ' + Number(++index);
|
||||
option2.value = 'b';
|
||||
newSelect.appendChild(option2);
|
||||
|
||||
// Option 3
|
||||
const option3 = document.createElement('option');
|
||||
option3.textContent = 'Choix ' + Number(++index);
|
||||
option3.value = 'c';
|
||||
newSelect.appendChild(option3);
|
||||
|
||||
// 3. Insérer la combobox dans la zone de contenu
|
||||
contentArea.appendChild(newSelect);
|
||||
|
||||
});
|
||||
|
||||
addListBtn.addEventListener('click', function () {
|
||||
|
||||
// 1. Création de l'élément parent de la liste (non ordonnée)
|
||||
const newList = document.createElement('ul');
|
||||
newList.className = 'dynamic-item dynamic-list';
|
||||
newList.setAttribute('draggable', 'true');
|
||||
|
||||
newList.id = 'list-' + Date.now(); // ID unique
|
||||
|
||||
// 2. Création et ajout des éléments de la liste (<li>)
|
||||
|
||||
// Élément 1
|
||||
const item1 = document.createElement('li');
|
||||
item1.textContent = 'Item ' + Number(++index);
|
||||
// Rendre le contenu éditable pour l'interface visuelle
|
||||
item1.setAttribute('contenteditable', 'true');
|
||||
newList.appendChild(item1);
|
||||
|
||||
// Élément 2
|
||||
const item2 = document.createElement('li');
|
||||
item2.textContent = 'Item ' + Number(++index);
|
||||
item2.setAttribute('contenteditable', 'true');
|
||||
newList.appendChild(item2);
|
||||
|
||||
// Élément 3
|
||||
const item3 = document.createElement('li');
|
||||
item3.textContent = 'Item ' + Number(++index);
|
||||
item3.setAttribute('contenteditable', 'true');
|
||||
newList.appendChild(item3);
|
||||
|
||||
// 3. Insérer la liste complète dans la zone de contenu
|
||||
contentArea.appendChild(newList);
|
||||
|
||||
});
|
||||
|
||||
|
||||
addTableBtn.addEventListener('click', function () {
|
||||
|
||||
// 1. Création de l'élément parent <table>
|
||||
const newTable = document.createElement('table');
|
||||
newTable.className = 'dynamic-item dynamic-table';
|
||||
newTable.setAttribute('draggable', 'true');
|
||||
|
||||
newTable.id = 'table-' + Date.now(); // ID unique
|
||||
|
||||
// 2. Création de l'en-tête du tableau (<thead>)
|
||||
const tableHead = newTable.createTHead();
|
||||
const headerRow = tableHead.insertRow();
|
||||
|
||||
// Ajout des cellules d'en-tête (<th>)
|
||||
['Nom', 'Âge', 'Ville'].forEach(headerText => {
|
||||
let th = document.createElement('th');
|
||||
th.textContent = headerText;
|
||||
headerRow.appendChild(th);
|
||||
});
|
||||
|
||||
// 3. Création du corps du tableau (<tbody>)
|
||||
const tableBody = newTable.createTBody();
|
||||
|
||||
// Données à insérer
|
||||
const data = [
|
||||
['Alice ' + Number(++index), 28, 'Paris'],
|
||||
['Bob ' + Number(++index), 35, 'Lyon']
|
||||
];
|
||||
|
||||
// Ajout des lignes de données (<tr>)
|
||||
data.forEach(rowData => {
|
||||
const row = tableBody.insertRow();
|
||||
rowData.forEach(cellData => {
|
||||
const cell = row.insertCell();
|
||||
cell.textContent = cellData;
|
||||
// Rendre les cellules éditables
|
||||
cell.setAttribute('contenteditable', 'true');
|
||||
});
|
||||
});
|
||||
|
||||
// 4. Insérer le tableau complet dans la zone de contenu
|
||||
contentArea.appendChild(newTable);
|
||||
|
||||
});
|
||||
|
||||
// --- (Optionnel) Ajout de la logique de Glisser-Déposer ---
|
||||
// Le vrai "glisser-déposer" (drag-and-drop) nécessite beaucoup plus de code
|
||||
// (utilisation des événements dragstart, dragover, drop...) pour être implémenté.
|
||||
// L'exemple ci-dessus est la méthode "Ajout par clic" qui est plus simple.
|
||||
|
||||
let draggedItem = null; // Variable globale pour stocker l'élément en cours de glissement
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// A. RENDRE LES ÉLÉMENTS GLISSABLES (Drag Start)
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
// On utilise la délégation d'événement sur le conteneur parent
|
||||
contentArea.addEventListener('dragstart', function (e) {
|
||||
// Vérifie si l'élément glissé est un élément que nous voulons déplacer
|
||||
if (e.target.closest('.dynamic-item')) {
|
||||
draggedItem = e.target.closest('.dynamic-item');
|
||||
|
||||
// Ajoute une classe pour l'effet visuel de glissement (opacité)
|
||||
setTimeout(() => {
|
||||
draggedItem.classList.add('dynamic-item-dragging');
|
||||
}, 0);
|
||||
|
||||
// Stocke l'ID ou toute autre donnée dans l'objet de transfert de données
|
||||
e.dataTransfer.setData('text/plain', draggedItem.id || 'dynamic-item');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
}
|
||||
});
|
||||
|
||||
contentArea.addEventListener('dragend', function (e) {
|
||||
// Retire la classe quand le glissement est terminé (réussi ou annulé)
|
||||
e.target.classList.remove('dynamic-item-dragging');
|
||||
draggedItem = null;
|
||||
});
|
||||
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// B. GÉRER LA ZONE DE DÉPÔT (Drag Over & Drop)
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
// Empêche le comportement par défaut (qui est de ne pas autoriser le dépôt)
|
||||
contentArea.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
contentArea.classList.add('drag-over');
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
|
||||
// Logique de positionnement (où déposer l'élément)
|
||||
if (draggedItem && e.target.closest('.dynamic-item') && e.target.closest('.dynamic-item') !== draggedItem) {
|
||||
const currentHoveredItem = e.target.closest('.dynamic-item');
|
||||
|
||||
// Calcule si le curseur est dans la moitié supérieure ou inférieure de l'élément survolé
|
||||
const rect = currentHoveredItem.getBoundingClientRect();
|
||||
const midpoint = rect.y + rect.height / 2;
|
||||
|
||||
// Si le curseur est dans la moitié supérieure, on insère AVANT
|
||||
if (e.clientY < midpoint) {
|
||||
contentArea.insertBefore(draggedItem, currentHoveredItem);
|
||||
}
|
||||
// Sinon, on insère APRÈS l'élément suivant
|
||||
else {
|
||||
contentArea.insertBefore(draggedItem, currentHoveredItem.nextSibling);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Retire l'indicateur visuel quand l'élément quitte la zone
|
||||
contentArea.addEventListener('dragleave', function (e) {
|
||||
// Vérifie que l'on sort bien de la zone de contenu principale
|
||||
if (e.target === contentArea) {
|
||||
contentArea.classList.remove('drag-over');
|
||||
}
|
||||
});
|
||||
|
||||
// Gère le dépôt final
|
||||
contentArea.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
contentArea.classList.remove('drag-over');
|
||||
|
||||
// La logique d'insertion est déjà gérée dans dragover pour une réorganisation fluide.
|
||||
// Ici, on pourrait ajouter une logique de sauvegarde ou de confirmation.
|
||||
|
||||
if (draggedItem) {
|
||||
console.log("Élément déposé avec succès !");
|
||||
draggedItem.classList.remove('dynamic-item-dragging');
|
||||
draggedItem = null;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<header class="header">
|
||||
Votre bandeau du haut
|
||||
</header>
|
||||
|
||||
<aside class="sidebar">
|
||||
<h3>Outils</h3>
|
||||
<button id="add-text-btn" class="dynamic-button">Ajouter un Paragraphe</button>
|
||||
<button id="add-button-btn" class="dynamic-button">Ajouter un Bouton</button>
|
||||
<button id="add-cb-btn" class="dynamic-button">Ajouter un Combobox</button>
|
||||
<button id="add-list-btn" class="dynamic-button">Ajouter une liste</button>
|
||||
<button id="add-table-btn" class="dynamic-button">Ajouter un Tableau</button>
|
||||
</aside>
|
||||
|
||||
<main class="content-area" id="content-area">
|
||||
<h1>Conteneur Principal</h1>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
Bandeau du bas / Pied de page
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
551
css/interface_glisser_prop.html
Normal file
551
css/interface_glisser_prop.html
Normal file
@@ -0,0 +1,551 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Interface glisser</title>
|
||||
<style>
|
||||
/* Le conteneur parent pour toute l'application */
|
||||
.app-layout {
|
||||
display: grid;
|
||||
/* 3 Colonnes : Sidebar (250px) | Contenu (Reste) | Propriétés (250px) */
|
||||
grid-template-columns: 250px 1fr 250px;
|
||||
/* Définit 3 lignes de haut en bas */
|
||||
grid-template-rows: auto 1fr auto;
|
||||
|
||||
/* Attribue un nom à chaque zone de la grille */
|
||||
grid-template-areas:
|
||||
"header header header"
|
||||
"sidebar content properties-panel"
|
||||
"footer footer footer";
|
||||
|
||||
/* Assure que la grille prend toute la hauteur visible */
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 1. Placement des zones */
|
||||
.header {
|
||||
grid-area: header;
|
||||
background-color: #333;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
grid-area: sidebar;
|
||||
background-color: #f4f4f4;
|
||||
padding: 15px;
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
grid-area: content;
|
||||
padding: 20px;
|
||||
/* Le défilement se produit ici si le contenu dépasse la hauteur */
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Style appliqué à la zone de contenu quand on glisse un élément dessus */
|
||||
.content-area.drag-over {
|
||||
border: 2px dashed #007bff;
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
|
||||
/* Style appliqué à l'élément fantôme pendant le glissement */
|
||||
.dynamic-item-dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.dynamic-item {
|
||||
/* Rendre l'élément visuellement déplaçable */
|
||||
cursor: grab;
|
||||
|
||||
/* Espacement pour séparer les éléments */
|
||||
margin-bottom: 10px;
|
||||
padding: 5px;
|
||||
|
||||
/* Indication visuelle subtile */
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
grid-area: footer;
|
||||
background-color: #333;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Styles pour les éléments internes (textes, boutons, etc.) */
|
||||
.content-area button {
|
||||
padding: 10px 15px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Nouveau style pour le panneau de propriétés */
|
||||
.properties-panel {
|
||||
grid-area: properties-panel;
|
||||
background-color: #f9f9f9;
|
||||
padding: 15px;
|
||||
border-left: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.dynamic-text {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
background-color: #e6f7ff;
|
||||
border: 1px solid #b3e0ff;
|
||||
cursor: grab;
|
||||
/* Indique que l'élément peut être déplacé */
|
||||
}
|
||||
|
||||
.dynamic-button {
|
||||
padding: 10px 15px;
|
||||
margin: 5px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dynamic-combobox {
|
||||
/* sur la même ligne */
|
||||
display: inline-block;
|
||||
|
||||
padding: 8px 12px;
|
||||
margin: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
font-size: 16px;
|
||||
/* Pour qu'elle prenne sa propre ligne
|
||||
display: block; */
|
||||
}
|
||||
|
||||
/* liste */
|
||||
.dynamic-list {
|
||||
/* sur la même ligne */
|
||||
display: inline-block;
|
||||
margin: 15px 0 15px 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #dcdcdc;
|
||||
background-color: #fafafa;
|
||||
/* Barre de couleur sur le côté */
|
||||
border-left: 15px solid #007bff;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.dynamic-list li {
|
||||
margin-bottom: 5px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
/* tableau */
|
||||
.dynamic-table {
|
||||
width: 80%;
|
||||
/* Prend 80% de la largeur du conteneur */
|
||||
border-collapse: collapse;
|
||||
/* Joint les bordures des cellules */
|
||||
margin: 20px 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.dynamic-table th,
|
||||
.dynamic-table td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
/* MODIFICATION CLÉ */
|
||||
}
|
||||
|
||||
.dynamic-table th {
|
||||
background-color: #f2f2f2;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Style appliqué à l'élément actuellement en cours d'édition */
|
||||
.dynamic-item.is-selected {
|
||||
outline: 2px solid #ffaa00;
|
||||
/* Un contour orange pour le marquer */
|
||||
box-shadow: 0 0 5px rgba(255, 170, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Style des contrôles de propriétés */
|
||||
#property-controls label {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#property-controls input {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
let index = 0;
|
||||
let selectedElement = null; // Élément actuellement cliqué
|
||||
|
||||
// 1. Cible l'endroit où les nouveaux éléments seront insérés
|
||||
const contentArea = document.getElementById('content-area');
|
||||
const propertyControls = document.getElementById('property-controls');
|
||||
|
||||
// 2. Cible les boutons de contrôle dans la sidebar
|
||||
const addTextBtn = document.getElementById('add-text-btn');
|
||||
const addButtonBtn = document.getElementById('add-button-btn');
|
||||
const addComboboxBtn = document.getElementById('add-cb-btn');
|
||||
const addListBtn = document.getElementById('add-list-btn');
|
||||
const addTableBtn = document.getElementById('add-table-btn');
|
||||
|
||||
// --- Fonction pour ajouter un Paragraphe ---
|
||||
addTextBtn.addEventListener('click', function () {
|
||||
// Créer un nouvel élément <p>
|
||||
const newParagraph = document.createElement('p');
|
||||
|
||||
// Lui donner du contenu et des styles
|
||||
newParagraph.textContent = 'Nouveau paragraphe ajouté dynamiquement.';
|
||||
newParagraph.className = 'dynamic-item dynamic-text';
|
||||
newParagraph.setAttribute('draggable', 'true');
|
||||
// Insérer l'élément dans la zone de contenu
|
||||
contentArea.appendChild(newParagraph);
|
||||
|
||||
// Optionnel : rendre le texte éditable (clic pour modifier)
|
||||
newParagraph.setAttribute('contenteditable', 'true');
|
||||
});
|
||||
|
||||
// --- Fonction pour ajouter un Bouton ---
|
||||
addButtonBtn.addEventListener('click', function () {
|
||||
// Créer un nouvel élément <button>
|
||||
const newButton = document.createElement('button');
|
||||
|
||||
// Lui donner du contenu et des styles
|
||||
newButton.textContent = 'Bouton ' + Number(++index);
|
||||
newButton.className = 'dynamic-item dynamic-button';
|
||||
newButton.setAttribute('draggable', 'true');
|
||||
// Insérer l'élément dans la zone de contenu
|
||||
contentArea.appendChild(newButton);
|
||||
});
|
||||
addComboboxBtn.addEventListener('click', function () {
|
||||
|
||||
// 1. Création de l'élément <select> (la combobox)
|
||||
const newSelect = document.createElement('select');
|
||||
newSelect.className = 'dynamic-item dynamic-combobox';
|
||||
newSelect.setAttribute('draggable', 'true');
|
||||
newSelect.id = 'combobox-' + Date.now(); // Donne un ID unique
|
||||
|
||||
// 2. Création des options
|
||||
|
||||
// Option par défaut/neutre
|
||||
const defaultOption = document.createElement('option');
|
||||
defaultOption.textContent = 'Sélectionnez une option';
|
||||
defaultOption.value = '';
|
||||
defaultOption.disabled = true;
|
||||
defaultOption.selected = true;
|
||||
newSelect.appendChild(defaultOption);
|
||||
|
||||
// Option 1
|
||||
const option1 = document.createElement('option');
|
||||
option1.textContent = 'Choix ' + Number(++index);
|
||||
option1.value = 'a';
|
||||
newSelect.appendChild(option1);
|
||||
|
||||
// Option 2
|
||||
const option2 = document.createElement('option');
|
||||
option2.textContent = 'Choix ' + Number(++index);
|
||||
option2.value = 'b';
|
||||
newSelect.appendChild(option2);
|
||||
|
||||
// Option 3
|
||||
const option3 = document.createElement('option');
|
||||
option3.textContent = 'Choix ' + Number(++index);
|
||||
option3.value = 'c';
|
||||
newSelect.appendChild(option3);
|
||||
|
||||
// 3. Insérer la combobox dans la zone de contenu
|
||||
contentArea.appendChild(newSelect);
|
||||
|
||||
});
|
||||
|
||||
addListBtn.addEventListener('click', function () {
|
||||
|
||||
// 1. Création de l'élément parent de la liste (non ordonnée)
|
||||
const newList = document.createElement('ul');
|
||||
newList.className = 'dynamic-item dynamic-list';
|
||||
newList.setAttribute('draggable', 'true');
|
||||
|
||||
newList.id = 'list-' + Date.now(); // ID unique
|
||||
|
||||
// 2. Création et ajout des éléments de la liste (<li>)
|
||||
|
||||
// Élément 1
|
||||
const item1 = document.createElement('li');
|
||||
item1.textContent = 'Item ' + Number(++index);
|
||||
// Rendre le contenu éditable pour l'interface visuelle
|
||||
item1.setAttribute('contenteditable', 'true');
|
||||
newList.appendChild(item1);
|
||||
|
||||
// Élément 2
|
||||
const item2 = document.createElement('li');
|
||||
item2.textContent = 'Item ' + Number(++index);
|
||||
item2.setAttribute('contenteditable', 'true');
|
||||
newList.appendChild(item2);
|
||||
|
||||
// Élément 3
|
||||
const item3 = document.createElement('li');
|
||||
item3.textContent = 'Item ' + Number(++index);
|
||||
item3.setAttribute('contenteditable', 'true');
|
||||
newList.appendChild(item3);
|
||||
|
||||
// 3. Insérer la liste complète dans la zone de contenu
|
||||
contentArea.appendChild(newList);
|
||||
|
||||
});
|
||||
|
||||
|
||||
addTableBtn.addEventListener('click', function () {
|
||||
|
||||
// 1. Création de l'élément parent <table>
|
||||
const newTable = document.createElement('table');
|
||||
newTable.className = 'dynamic-item dynamic-table';
|
||||
newTable.setAttribute('draggable', 'true');
|
||||
|
||||
newTable.id = 'table-' + Date.now(); // ID unique
|
||||
|
||||
// 2. Création de l'en-tête du tableau (<thead>)
|
||||
const tableHead = newTable.createTHead();
|
||||
const headerRow = tableHead.insertRow();
|
||||
|
||||
// Ajout des cellules d'en-tête (<th>)
|
||||
['Nom', 'Âge', 'Ville'].forEach(headerText => {
|
||||
let th = document.createElement('th');
|
||||
th.textContent = headerText;
|
||||
headerRow.appendChild(th);
|
||||
});
|
||||
|
||||
// 3. Création du corps du tableau (<tbody>)
|
||||
const tableBody = newTable.createTBody();
|
||||
|
||||
// Données à insérer
|
||||
const data = [
|
||||
['Alice ' + Number(++index), 28, 'Paris'],
|
||||
['Bob ' + Number(++index), 35, 'Lyon']
|
||||
];
|
||||
|
||||
// Ajout des lignes de données (<tr>)
|
||||
data.forEach(rowData => {
|
||||
const row = tableBody.insertRow();
|
||||
rowData.forEach(cellData => {
|
||||
const cell = row.insertCell();
|
||||
cell.textContent = cellData;
|
||||
// Rendre les cellules éditables
|
||||
cell.setAttribute('contenteditable', 'true');
|
||||
});
|
||||
});
|
||||
|
||||
// 4. Insérer le tableau complet dans la zone de contenu
|
||||
contentArea.appendChild(newTable);
|
||||
|
||||
});
|
||||
|
||||
// --- (Optionnel) Ajout de la logique de Glisser-Déposer ---
|
||||
// Le vrai "glisser-déposer" (drag-and-drop) nécessite beaucoup plus de code
|
||||
// (utilisation des événements dragstart, dragover, drop...) pour être implémenté.
|
||||
// L'exemple ci-dessus est la méthode "Ajout par clic" qui est plus simple.
|
||||
|
||||
let draggedItem = null; // Variable globale pour stocker l'élément en cours de glissement
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// A. RENDRE LES ÉLÉMENTS GLISSABLES (Drag Start)
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
// On utilise la délégation d'événement sur le conteneur parent
|
||||
contentArea.addEventListener('dragstart', function (e) {
|
||||
// Vérifie si l'élément glissé est un élément que nous voulons déplacer
|
||||
if (e.target.closest('.dynamic-item')) {
|
||||
draggedItem = e.target.closest('.dynamic-item');
|
||||
|
||||
// Ajoute une classe pour l'effet visuel de glissement (opacité)
|
||||
setTimeout(() => {
|
||||
draggedItem.classList.add('dynamic-item-dragging');
|
||||
}, 0);
|
||||
|
||||
// Stocke l'ID ou toute autre donnée dans l'objet de transfert de données
|
||||
e.dataTransfer.setData('text/plain', draggedItem.id || 'dynamic-item');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
}
|
||||
});
|
||||
|
||||
contentArea.addEventListener('dragend', function (e) {
|
||||
// Retire la classe quand le glissement est terminé (réussi ou annulé)
|
||||
e.target.classList.remove('dynamic-item-dragging');
|
||||
draggedItem = null;
|
||||
});
|
||||
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// B. GÉRER LA ZONE DE DÉPÔT (Drag Over & Drop)
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
// Empêche le comportement par défaut (qui est de ne pas autoriser le dépôt)
|
||||
contentArea.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
contentArea.classList.add('drag-over');
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
|
||||
// Logique de positionnement (où déposer l'élément)
|
||||
if (draggedItem && e.target.closest('.dynamic-item') && e.target.closest('.dynamic-item') !== draggedItem) {
|
||||
const currentHoveredItem = e.target.closest('.dynamic-item');
|
||||
|
||||
// Calcule si le curseur est dans la moitié supérieure ou inférieure de l'élément survolé
|
||||
const rect = currentHoveredItem.getBoundingClientRect();
|
||||
const midpoint = rect.y + rect.height / 2;
|
||||
|
||||
// Si le curseur est dans la moitié supérieure, on insère AVANT
|
||||
if (e.clientY < midpoint) {
|
||||
contentArea.insertBefore(draggedItem, currentHoveredItem);
|
||||
}
|
||||
// Sinon, on insère APRÈS l'élément suivant
|
||||
else {
|
||||
contentArea.insertBefore(draggedItem, currentHoveredItem.nextSibling);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Retire l'indicateur visuel quand l'élément quitte la zone
|
||||
contentArea.addEventListener('dragleave', function (e) {
|
||||
// Vérifie que l'on sort bien de la zone de contenu principale
|
||||
if (e.target === contentArea) {
|
||||
contentArea.classList.remove('drag-over');
|
||||
}
|
||||
});
|
||||
|
||||
// Gère le dépôt final
|
||||
contentArea.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
contentArea.classList.remove('drag-over');
|
||||
|
||||
// La logique d'insertion est déjà gérée dans dragover pour une réorganisation fluide.
|
||||
// Ici, on pourrait ajouter une logique de sauvegarde ou de confirmation.
|
||||
|
||||
if (draggedItem) {
|
||||
console.log("Élément déposé avec succès !");
|
||||
draggedItem.classList.remove('dynamic-item-dragging');
|
||||
draggedItem = null;
|
||||
}
|
||||
});
|
||||
// ----------------------------------------------------------------
|
||||
// A. GESTION DU CLIC SUR UN ÉLÉMENT DYNAMIQUE
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
// Utilisation de la délégation pour capturer les clics sur les éléments
|
||||
contentArea.addEventListener('click', function (e) {
|
||||
// Vérifie si l'élément cliqué est un de nos éléments glissables
|
||||
const clickedItem = e.target.closest('.dynamic-item');
|
||||
|
||||
if (clickedItem) {
|
||||
// Empêche le clic de se propager (si l'élément est un bouton, etc.)
|
||||
e.stopPropagation();
|
||||
|
||||
// 1. Désélectionner l'ancien élément et sélectionner le nouveau
|
||||
if (selectedElement) {
|
||||
selectedElement.classList.remove('is-selected');
|
||||
}
|
||||
selectedElement = clickedItem;
|
||||
selectedElement.classList.add('is-selected');
|
||||
|
||||
// 2. Afficher les contrôles de propriétés
|
||||
displayProperties(selectedElement);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// B. GÉNÉRATION ET GESTION DES CHAMPS D'ENTRÉE (INPUTS)
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
function displayProperties(element) {
|
||||
// Vider les anciens contrôles
|
||||
propertyControls.innerHTML = '';
|
||||
|
||||
// 1. Contrôle du Texte (Pour tous les éléments avec du texte)
|
||||
if (element.textContent) {
|
||||
propertyControls.innerHTML += `
|
||||
<label for="prop-text">Texte :</label>
|
||||
<input type="text" id="prop-text" value="${element.textContent.trim()}">
|
||||
`;
|
||||
// Assurez-vous d'avoir une méthode pour mettre à jour le texte si contenteditable n'est pas utilisé
|
||||
document.getElementById('prop-text').addEventListener('input', (e) => {
|
||||
element.textContent = e.target.value;
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Contrôle de la Couleur (Exemple pour la couleur de fond)
|
||||
propertyControls.innerHTML += `
|
||||
<label for="prop-color">Couleur de fond :</label>
|
||||
<input type="color" id="prop-color" value="#ffffff">
|
||||
`;
|
||||
document.getElementById('prop-color').addEventListener('input', (e) => {
|
||||
element.style.backgroundColor = e.target.value;
|
||||
});
|
||||
|
||||
// 3. Contrôle de la Taille (Exemple pour la largeur)
|
||||
propertyControls.innerHTML += `
|
||||
<label for="prop-width">Largeur (px) :</label>
|
||||
<input type="number" id="prop-width" value="${element.offsetWidth}">
|
||||
`;
|
||||
document.getElementById('prop-width').addEventListener('input', (e) => {
|
||||
element.style.width = `${e.target.value}px`;
|
||||
});
|
||||
|
||||
// 4. Ajoutez ici des contrôles spécifiques (Ex: 'Taille Police' pour le texte, 'Border Radius' pour le bouton, etc.)
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<header class="header">
|
||||
Votre bandeau du haut
|
||||
</header>
|
||||
|
||||
<aside class="sidebar">
|
||||
<h3>Outils</h3>
|
||||
<button id="add-text-btn" class="dynamic-button">Ajouter un Paragraphe</button>
|
||||
<button id="add-button-btn" class="dynamic-button">Ajouter un Bouton</button>
|
||||
<button id="add-cb-btn" class="dynamic-button">Ajouter un Combobox</button>
|
||||
<button id="add-list-btn" class="dynamic-button">Ajouter une liste</button>
|
||||
<button id="add-table-btn" class="dynamic-button">Ajouter un Tableau</button>
|
||||
</aside>
|
||||
|
||||
<main class="content-area" id="content-area">
|
||||
<h1>Conteneur Principal</h1>
|
||||
</main>
|
||||
<div class="properties-panel" id="properties-panel">
|
||||
<h3>Propriétés de l'Élément</h3>
|
||||
<p>Cliquez sur un élément dans la zone principale pour modifier ses propriétés.</p>
|
||||
|
||||
<div id="property-controls">
|
||||
</div>
|
||||
</div>
|
||||
<footer class="footer">
|
||||
Bandeau du bas / Pied de page
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
20
css/template.html
Normal file
20
css/template.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Template</title>
|
||||
<style>
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
235
dialog.html
Normal file
235
dialog.html
Normal file
@@ -0,0 +1,235 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<title>Ma Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Titre de la page</h1>
|
||||
|
||||
<style>
|
||||
.dialog {
|
||||
position: absolute;
|
||||
background: #f9f9f9;
|
||||
border: 1px solid #999;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
|
||||
overflow: hidden;
|
||||
resize: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.dialog-bar {
|
||||
height: 30px;
|
||||
background: #2979ff;
|
||||
color: white;
|
||||
line-height: 30px;
|
||||
padding: 0 8px;
|
||||
font-weight: bold;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dialog-close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
}
|
||||
.dialog-close:hover {
|
||||
background: rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
width: 100%;
|
||||
height: calc(100% - 30px);
|
||||
overflow: auto;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Resizers */
|
||||
.resizer {
|
||||
position: absolute;
|
||||
background: transparent;
|
||||
}
|
||||
.resizer-n, .resizer-s {
|
||||
height: 6px; width: 100%; cursor: ns-resize;
|
||||
}
|
||||
.resizer-e, .resizer-w {
|
||||
width: 6px; height: 100%; cursor: ew-resize;
|
||||
}
|
||||
.resizer-n { top: -3px; left: 0; }
|
||||
.resizer-s { bottom: -3px; left: 0; }
|
||||
.resizer-e { right: -3px; top: 0; }
|
||||
.resizer-w { left: -3px; top: 0; }
|
||||
|
||||
/* corners */
|
||||
.resizer-ne, .resizer-nw, .resizer-se, .resizer-sw {
|
||||
width: 10px; height: 10px;
|
||||
position: absolute;
|
||||
background: transparent;
|
||||
}
|
||||
.resizer-ne { top: -3px; right: -3px; cursor: nesw-resize; }
|
||||
.resizer-nw { top: -3px; left: -3px; cursor: nwse-resize; }
|
||||
.resizer-se { bottom: -3px; right: -3px; cursor: nwse-resize; }
|
||||
.resizer-sw { bottom: -3px; left: -3px; cursor: nesw-resize; }
|
||||
|
||||
</style>
|
||||
<script>
|
||||
|
||||
|
||||
class Dialog {
|
||||
constructor(box, title, content) {
|
||||
this._id = 'dialog_' + Date.now();
|
||||
this._box = box;
|
||||
this._title = title;
|
||||
this._content = content;
|
||||
|
||||
this.createDialog();
|
||||
}
|
||||
|
||||
createDialog() {
|
||||
// --- conteneur principal ---
|
||||
this.el = document.createElement('div');
|
||||
this.el.className = 'dialog';
|
||||
Object.assign(this.el.style, {
|
||||
top: this._box.top + 'px',
|
||||
left: this._box.left + 'px',
|
||||
width: this._box.width + 'px',
|
||||
height: this._box.height + 'px'
|
||||
});
|
||||
|
||||
// --- barre de titre ---
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'dialog-bar';
|
||||
bar.textContent = this._title;
|
||||
|
||||
const close = document.createElement('span');
|
||||
close.className = 'dialog-close';
|
||||
close.innerHTML = '×';
|
||||
bar.appendChild(close);
|
||||
this.el.appendChild(bar);
|
||||
|
||||
// --- conteneur contenu ---
|
||||
const container = document.createElement('div');
|
||||
container.className = 'dialog-content';
|
||||
if (typeof this._content === 'string')
|
||||
container.innerHTML = this._content;
|
||||
else
|
||||
container.appendChild(this._content);
|
||||
this.el.appendChild(container);
|
||||
|
||||
document.body.appendChild(this.el);
|
||||
|
||||
// --- interactions ---
|
||||
this.makeDraggable(bar);
|
||||
this.makeResizable();
|
||||
close.addEventListener('click', () => this.close());
|
||||
}
|
||||
|
||||
makeDraggable(bar) {
|
||||
let isDragging = false;
|
||||
let offsetX, offsetY;
|
||||
|
||||
bar.addEventListener('mousedown', (e) => {
|
||||
isDragging = true;
|
||||
const rect = this.el.getBoundingClientRect();
|
||||
offsetX = e.clientX - rect.left;
|
||||
offsetY = e.clientY - rect.top;
|
||||
document.body.style.userSelect = 'none';
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
isDragging = false;
|
||||
document.body.style.userSelect = '';
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!isDragging) return;
|
||||
this.el.style.left = (e.clientX - offsetX) + 'px';
|
||||
this.el.style.top = (e.clientY - offsetY) + 'px';
|
||||
});
|
||||
}
|
||||
|
||||
makeResizable() {
|
||||
const sides = ['n', 'e', 's', 'w', 'ne', 'nw', 'se', 'sw'];
|
||||
sides.forEach(side => {
|
||||
const r = document.createElement('div');
|
||||
r.className = 'resizer resizer-' + side;
|
||||
this.el.appendChild(r);
|
||||
r.addEventListener('mousedown', (e) => this.startResize(e, side));
|
||||
});
|
||||
}
|
||||
|
||||
startResize(e, side) {
|
||||
e.preventDefault();
|
||||
const startX = e.clientX;
|
||||
const startY = e.clientY;
|
||||
const rect = this.el.getBoundingClientRect();
|
||||
|
||||
const startWidth = rect.width;
|
||||
const startHeight = rect.height;
|
||||
const startLeft = rect.left;
|
||||
const startTop = rect.top;
|
||||
|
||||
const onMove = (ev) => {
|
||||
const dx = ev.clientX - startX;
|
||||
const dy = ev.clientY - startY;
|
||||
|
||||
if (side.includes('e'))
|
||||
this.el.style.width = (startWidth + dx) + 'px';
|
||||
if (side.includes('s'))
|
||||
this.el.style.height = (startHeight + dy) + 'px';
|
||||
if (side.includes('w')) {
|
||||
this.el.style.width = (startWidth - dx) + 'px';
|
||||
this.el.style.left = (startLeft + dx) + 'px';
|
||||
}
|
||||
if (side.includes('n')) {
|
||||
this.el.style.height = (startHeight - dy) + 'px';
|
||||
this.el.style.top = (startTop + dy) + 'px';
|
||||
}
|
||||
};
|
||||
|
||||
const onUp = () => {
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.el && this.el.parentElement)
|
||||
this.el.parentElement.removeChild(this.el);
|
||||
}
|
||||
} ;
|
||||
|
||||
// --- helper de création ---
|
||||
function createDialog(box, title, content) {
|
||||
return new Dialog(box, title, content);
|
||||
}
|
||||
|
||||
|
||||
function lancerAuDemarrage() {
|
||||
createDialog({ top: 100, left: 100, width: 400, height: 300 }, "Exemple Texte",
|
||||
"<p>Bonjour 👋</p><p>Tu peux redimensionner et déplacer cette fenêtre.</p>"
|
||||
);
|
||||
}
|
||||
|
||||
// Appel direct de la fonction
|
||||
lancerAuDemarrage();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
97
explorer_md/server.py
Normal file
97
explorer_md/server.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# app.py
|
||||
# pip install flask
|
||||
from flask import Flask, render_template, jsonify, send_from_directory
|
||||
import os
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Le répertoire que nous voulons explorer (relatif à app.py)
|
||||
FILE_DIR = 'files'
|
||||
|
||||
# --- Configuration des Routes ---
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""Route principale qui sert la page HTML."""
|
||||
return render_template('index.html')
|
||||
|
||||
|
||||
@app.route('/images/<filename>')
|
||||
def get_image(filename):
|
||||
"""Sert une image depuis un dossier."""
|
||||
return send_from_directory('images', filename)
|
||||
|
||||
|
||||
|
||||
@app.route('/api/list_files')
|
||||
def list_files():
|
||||
"""Endpoint API pour lister les fichiers du répertoire."""
|
||||
try:
|
||||
# Assurez-vous que le répertoire existe
|
||||
if not os.path.isdir(FILE_DIR):
|
||||
return jsonify({'error': f"Le répertoire '{FILE_DIR}' n'existe pas."}), 500
|
||||
|
||||
# Liste tous les éléments dans le répertoire
|
||||
all_items = os.listdir(FILE_DIR)
|
||||
|
||||
# Filtre pour ne garder que les fichiers (et non les dossiers)
|
||||
file_list = [item for item in all_items if os.path.isfile(os.path.join(FILE_DIR, item))]
|
||||
|
||||
return jsonify(file_list)
|
||||
except Exception as e:
|
||||
app.logger.error(f"Erreur lors de la récupération de la liste des fichiers : {e}")
|
||||
return jsonify({'error': 'Erreur interne du serveur.'}), 500
|
||||
|
||||
@app.route('/api/get_content/<filename>')
|
||||
def get_content(filename):
|
||||
"""Endpoint API pour obtenir le contenu d'un fichier."""
|
||||
file_path = os.path.join(FILE_DIR, filename)
|
||||
|
||||
# Sécurité: Vérifiez que le chemin est bien dans le répertoire autorisé
|
||||
# Ceci empêche les attaques de 'Directory Traversal'
|
||||
if not os.path.abspath(file_path).startswith(os.path.abspath(FILE_DIR)):
|
||||
return jsonify({'error': 'Accès non autorisé.'}), 403
|
||||
|
||||
try:
|
||||
# Cas des fichiers textes lisibles
|
||||
if filename.endswith(('.txt', '.ini', '.json', '.html', '.css', '.js')):
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
return jsonify({'content': content, 'type': 'text'})
|
||||
elif filename.endswith(('.md')):
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
return jsonify({'content': content, 'type': 'text'})
|
||||
|
||||
# Cas des images ou autres binaires (on simule le contenu)
|
||||
elif filename.endswith(('.jpg', '.png', '.pdf')):
|
||||
return jsonify({
|
||||
'content': f"Fichier binaire ou non textuel. Le contenu de {filename} ne peut pas être affiché directement.",
|
||||
'type': 'binary'
|
||||
})
|
||||
|
||||
# Pour tout autre type de fichier non géré
|
||||
else:
|
||||
return jsonify({'content': f"Type de fichier non supporté pour l'affichage : {filename}", 'type': 'unsupported'})
|
||||
|
||||
except FileNotFoundError:
|
||||
return jsonify({'error': 'Fichier non trouvé.'}), 404
|
||||
except Exception as e:
|
||||
app.logger.error(f"Erreur lors de la lecture du fichier {filename} : {e}")
|
||||
return jsonify({'error': 'Erreur interne du serveur lors de la lecture.'}), 500
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Créez le répertoire des fichiers s'il n'existe pas
|
||||
if not os.path.exists(FILE_DIR):
|
||||
os.makedirs(FILE_DIR)
|
||||
# Créez quelques fichiers de test
|
||||
with open(os.path.join(FILE_DIR, 'Fichier 1.txt'), 'w') as f:
|
||||
f.write("Contenu du premier fichier texte.")
|
||||
with open(os.path.join(FILE_DIR, 'Config.ini'), 'w') as f:
|
||||
f.write("[Settings]\nVersion=1.0\nPath=/data")
|
||||
with open(os.path.join(FILE_DIR, 'Image.jpg'), 'w') as f: # Fichier binaire simulé
|
||||
f.write("Ceci n'est pas une image réelle.")
|
||||
|
||||
# Lancez l'application Flask
|
||||
app.run(debug=True)
|
||||
103
explorer_md/static/script.js
Normal file
103
explorer_md/static/script.js
Normal file
@@ -0,0 +1,103 @@
|
||||
// static/script.js
|
||||
|
||||
const fileListPanel = document.getElementById('file-list-panel');
|
||||
const resizer = document.getElementById('resizer');
|
||||
const container = document.querySelector('.container');
|
||||
const fileListUl = document.getElementById('file-list');
|
||||
const fileContentPre = document.getElementById('file-content');
|
||||
const contentTitle = document.getElementById('content-title');
|
||||
|
||||
let isResizing = false;
|
||||
|
||||
// 1. GESTION DU REDIMENSIONNEMENT (Même logique que précédemment)
|
||||
resizer.addEventListener('mousedown', function(e) {
|
||||
isResizing = true;
|
||||
container.classList.add('resizing');
|
||||
document.body.style.userSelect = 'none';
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', function(e) {
|
||||
if (!isResizing) return;
|
||||
|
||||
const newWidth = e.clientX - container.getBoundingClientRect().left;
|
||||
const minWidth = 150;
|
||||
const maxWidth = container.clientWidth * 0.5;
|
||||
|
||||
if (newWidth >= minWidth && newWidth <= maxWidth) {
|
||||
fileListPanel.style.width = newWidth + 'px';
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', function() {
|
||||
isResizing = false;
|
||||
container.classList.remove('resizing');
|
||||
document.body.style.userSelect = '';
|
||||
});
|
||||
|
||||
// 2. LOGIQUE DU CHARGEMENT DES FICHIERS (Communication avec Flask)
|
||||
|
||||
// A. Récupérer la liste des fichiers depuis le serveur
|
||||
async function fetchFileList() {
|
||||
try {
|
||||
const response = await fetch('/api/list_files');
|
||||
const files = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
fileListUl.innerHTML = ''; // Vider la liste existante
|
||||
if (files.length === 0) {
|
||||
fileListUl.innerHTML = '<li>Aucun fichier trouvé.</li>';
|
||||
fileContentPre.textContent = 'Le répertoire est vide.';
|
||||
} else {
|
||||
files.forEach(filename => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = filename;
|
||||
li.onclick = () => loadFileContent(filename, li);
|
||||
fileListUl.appendChild(li);
|
||||
});
|
||||
|
||||
// Charger le contenu du premier fichier par défaut
|
||||
if (files.length > 0) {
|
||||
loadFileContent(files[0], fileListUl.querySelector('li'));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Afficher l'erreur retournée par Flask
|
||||
fileContentPre.textContent = `Erreur: ${files.error}`;
|
||||
}
|
||||
} catch (error) {
|
||||
fileContentPre.textContent = `Erreur de connexion au serveur : ${error.message}`;
|
||||
console.error('Erreur lors de la récupération des fichiers:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// B. Charger le contenu du fichier sélectionné
|
||||
async function loadFileContent(filename, element) {
|
||||
// Retirer la classe 'selected' de tous les éléments
|
||||
document.querySelectorAll('#file-list li').forEach(li => {
|
||||
li.classList.remove('selected');
|
||||
});
|
||||
// Ajouter la classe 'selected' à l'élément cliqué
|
||||
if (element) {
|
||||
element.classList.add('selected');
|
||||
}
|
||||
|
||||
contentTitle.textContent = `Contenu de : ${filename}`;
|
||||
fileContentPre.textContent = 'Chargement en cours...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/get_content/${filename}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
// Enlève le slice(1, -1) sauf si ton serveur renvoie le contenu entre guillemets
|
||||
fileContentPre.innerHTML = marked.parse(data.content);
|
||||
} else {
|
||||
fileContentPre.innerHTML = `Erreur (${response.status}) : ${data.error}`;
|
||||
}
|
||||
} catch (error) {
|
||||
fileContentPre.textContent = `Erreur réseau lors du chargement : ${error.message}`;
|
||||
console.error('Erreur lors du chargement du contenu:', error);
|
||||
}
|
||||
}
|
||||
// Exécuter au chargement de la page
|
||||
document.addEventListener('DOMContentLoaded', fetchFileList);
|
||||
73
explorer_md/static/style.css
Normal file
73
explorer_md/static/style.css
Normal file
@@ -0,0 +1,73 @@
|
||||
/* static/style.css */
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
/* --- Panneau de la Liste des Fichiers (Gauche) --- */
|
||||
#file-list-panel {
|
||||
width: 300px;
|
||||
min-width: 150px;
|
||||
max-width: 50%;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
background-color: #f4f4f4;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#file-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#file-list li {
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
#file-list li:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
#file-list li.selected {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* --- Poignée de Redimensionnement --- */
|
||||
#resizer {
|
||||
width: 6px;
|
||||
cursor: col-resize;
|
||||
background-color: #ccc;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#resizer:hover {
|
||||
background-color: #aaa;
|
||||
}
|
||||
|
||||
/* --- Panneau de Contenu (Droite) --- */
|
||||
#content {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#file-content {
|
||||
padding: 15px;
|
||||
border: 1px dashed #ddd;
|
||||
background-color: #fff;
|
||||
min-height: 80%;
|
||||
}
|
||||
81
explorer_md/static/style_md.css
Normal file
81
explorer_md/static/style_md.css
Normal file
@@ -0,0 +1,81 @@
|
||||
/* static/style_md.css */
|
||||
|
||||
#file-content {
|
||||
background: white;
|
||||
padding: 30px 40px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
color: #333;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
border-bottom: 2px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #f4f4f4;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #2d2d2d;
|
||||
color: #f8f8f2;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid #ddd;
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0066cc;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f4f4f4;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.loader {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #c00;
|
||||
background: #fee;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
29
explorer_md/templates/index.html
Normal file
29
explorer_md/templates/index.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Explorateur de Fichiers Python/Flask</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style_md.css') }}">
|
||||
<!-- https://github.com/markedjs/marked -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div id="file-list-panel">
|
||||
<h3>Liste des Fichiers</h3>
|
||||
<ul id="file-list"></ul>
|
||||
</div>
|
||||
|
||||
<div id="resizer"></div>
|
||||
|
||||
<div id="content">
|
||||
<h3 id="content-title">Contenu du Fichier</h3>
|
||||
<div id="file-content">Chargement de la liste des fichiers...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
84
explorer_md/templates/index_md.html
Normal file
84
explorer_md/templates/index_md.html
Normal file
@@ -0,0 +1,84 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Visualiseur Markdown</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js"></script>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
line-height: 1.6;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
#content {
|
||||
background: white;
|
||||
padding: 30px 40px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1, h2, h3 { color: #333; margin-top: 1.5em; }
|
||||
h1 { border-bottom: 2px solid #eee; padding-bottom: 10px; }
|
||||
code {
|
||||
background: #f4f4f4;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
pre {
|
||||
background: #2d2d2d;
|
||||
color: #f8f8f2;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
pre code { background: none; color: inherit; }
|
||||
blockquote {
|
||||
border-left: 4px solid #ddd;
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
color: #666;
|
||||
}
|
||||
a { color: #0066cc; }
|
||||
table { border-collapse: collapse; width: 100%; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background: #f4f4f4; }
|
||||
img { max-width: 100%; }
|
||||
.loader { text-align: center; color: #666; padding: 40px; }
|
||||
.error { color: #c00; background: #fee; padding: 15px; border-radius: 6px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="content">
|
||||
<div class="loader">Chargement...</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ===== CONFIGURATION =====
|
||||
// Change le nom du fichier ici :
|
||||
const fichierMD = 'install_noMachine_linux.md';
|
||||
// =========================
|
||||
|
||||
async function chargerMarkdown() {
|
||||
const content = document.getElementById('content');
|
||||
try {
|
||||
const response = await fetch(fichierMD);
|
||||
if (!response.ok) throw new Error(`Fichier "${fichierMD}" non trouvé`);
|
||||
const md = await response.text();
|
||||
content.innerHTML = marked.parse(md);
|
||||
} catch (err) {
|
||||
content.innerHTML = `<div class="error">
|
||||
<strong>Erreur :</strong> ${err.message}<br><br>
|
||||
<strong>Solution :</strong> Lance un serveur local avec VS Code (extension Live Server)
|
||||
ou via terminal : <code>npx serve</code> ou <code>python -m http.server</code>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
chargerMarkdown();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
84
index.html
Normal file
84
index.html
Normal file
@@ -0,0 +1,84 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Visualiseur Markdown</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js"></script>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
line-height: 1.6;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
#content {
|
||||
background: white;
|
||||
padding: 30px 40px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1, h2, h3 { color: #333; margin-top: 1.5em; }
|
||||
h1 { border-bottom: 2px solid #eee; padding-bottom: 10px; }
|
||||
code {
|
||||
background: #f4f4f4;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
pre {
|
||||
background: #2d2d2d;
|
||||
color: #f8f8f2;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
pre code { background: none; color: inherit; }
|
||||
blockquote {
|
||||
border-left: 4px solid #ddd;
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
color: #666;
|
||||
}
|
||||
a { color: #0066cc; }
|
||||
table { border-collapse: collapse; width: 100%; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background: #f4f4f4; }
|
||||
img { max-width: 100%; }
|
||||
.loader { text-align: center; color: #666; padding: 40px; }
|
||||
.error { color: #c00; background: #fee; padding: 15px; border-radius: 6px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="content">
|
||||
<div class="loader">Chargement...</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ===== CONFIGURATION =====
|
||||
// Change le nom du fichier ici :
|
||||
const fichierMD = 'install_noMachine_linux.md';
|
||||
// =========================
|
||||
|
||||
async function chargerMarkdown() {
|
||||
const content = document.getElementById('content');
|
||||
try {
|
||||
const response = await fetch(fichierMD);
|
||||
if (!response.ok) throw new Error(`Fichier "${fichierMD}" non trouvé`);
|
||||
const md = await response.text();
|
||||
content.innerHTML = marked.parse(md);
|
||||
} catch (err) {
|
||||
content.innerHTML = `<div class="error">
|
||||
<strong>Erreur :</strong> ${err.message}<br><br>
|
||||
<strong>Solution :</strong> Lance un serveur local avec VS Code (extension Live Server)
|
||||
ou via terminal : <code>npx serve</code> ou <code>python -m http.server</code>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
chargerMarkdown();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
185
panel_resize.html
Normal file
185
panel_resize.html
Normal file
@@ -0,0 +1,185 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Page une-colonne redimensionnable</title>
|
||||
<style>
|
||||
:root{
|
||||
--min-panel: 40px;
|
||||
--handle-size: 8px;
|
||||
--bg-top: #2b6cb0;
|
||||
--bg-middle: #edf2f7;
|
||||
--bg-bottom: #2f855a;
|
||||
}
|
||||
|
||||
html,body{height:100%;margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,'Helvetica Neue',Arial}
|
||||
body{display:flex;align-items:center;justify-content:center;background:#f7fafc;padding:18px}
|
||||
|
||||
/* zone redimensionnable globale (vous pouvez la glisser depuis le coin) */
|
||||
#app{
|
||||
width:900px;
|
||||
height:600px;
|
||||
max-width:100%;
|
||||
max-height:100%;
|
||||
background:#fff;
|
||||
box-shadow:0 8px 30px rgba(2,6,23,.12);
|
||||
border-radius:10px;
|
||||
overflow:hidden; /* les panneaux gèrent leur propre overflow */
|
||||
resize:both; /* autorise redimension globale par coin */
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
}
|
||||
|
||||
/* panneaux */
|
||||
.panel{
|
||||
min-height:var(--min-panel);
|
||||
overflow:auto;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
padding:14px;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
|
||||
header{background:var(--bg-top);color:#fff}
|
||||
main{background:var(--bg-middle);color:#1a202c;flex:1}
|
||||
footer{background:var(--bg-bottom);color:#fff}
|
||||
|
||||
/* handle horizontal (séparateur entre panneaux) */
|
||||
.resizer{
|
||||
height:var(--handle-size);
|
||||
background:linear-gradient(90deg, rgba(0,0,0,0.08), rgba(0,0,0,0.02));
|
||||
cursor:row-resize;
|
||||
position:relative;
|
||||
z-index:10;
|
||||
}
|
||||
.resizer::after{
|
||||
content:'';position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
|
||||
width:40px;height:4px;border-radius:4px;background:rgba(0,0,0,0.12)
|
||||
}
|
||||
|
||||
/* petits controles d'aide */
|
||||
.hint{font-size:13px;opacity:.9}
|
||||
.controls{position:absolute;right:10px;top:8px;font-size:12px}
|
||||
.panel .title{font-weight:700}
|
||||
|
||||
/* styles adaptatifs pour petits écrans */
|
||||
@media (max-width:520px){
|
||||
#app{width:100%;height:80vh;border-radius:6px}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" role="application">
|
||||
<header class="panel" id="topPanel" style="height:120px;">
|
||||
<div>
|
||||
<div class="title">Bandeau du haut</div>
|
||||
<div class="hint">Glissez la barre grise sous ce bandeau pour redimensionner verticalement.</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="resizer" data-target="top"></div>
|
||||
|
||||
<main class="panel" id="centerPanel" style="height:calc(100% - 240px);">
|
||||
<div>
|
||||
<div class="title">Conteneur central</div>
|
||||
<div class="hint">Ce conteneur prend l'espace restant. Vous pouvez aussi redimensionner la fenêtre par le coin (resize).</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="resizer" data-target="bottom"></div>
|
||||
|
||||
<footer class="panel" id="bottomPanel" style="height:120px;">
|
||||
<div>
|
||||
<div class="title">Bandeau du bas</div>
|
||||
<div class="hint">Glissez la barre grise au-dessus pour modifier sa hauteur.</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Gestion du drag pour les séparateurs horizontaux
|
||||
(function(){
|
||||
const app = document.getElementById('app');
|
||||
const topPanel = document.getElementById('topPanel');
|
||||
const centerPanel = document.getElementById('centerPanel');
|
||||
const bottomPanel = document.getElementById('bottomPanel');
|
||||
const min = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--min-panel')) || 40;
|
||||
|
||||
let dragging = null;
|
||||
let startY = 0;
|
||||
let startTopH = 0;
|
||||
let startBottomH = 0;
|
||||
|
||||
function onDown(e){
|
||||
e.preventDefault();
|
||||
dragging = this.getAttribute('data-target');
|
||||
startY = e.touches ? e.touches[0].clientY : e.clientY;
|
||||
startTopH = topPanel.getBoundingClientRect().height;
|
||||
startBottomH = bottomPanel.getBoundingClientRect().height;
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
document.addEventListener('touchmove', onMove, {passive:false});
|
||||
document.addEventListener('touchend', onUp);
|
||||
app.style.userSelect = 'none';
|
||||
}
|
||||
|
||||
function onMove(e){
|
||||
if(!dragging) return;
|
||||
e.preventDefault();
|
||||
const y = e.touches ? e.touches[0].clientY : e.clientY;
|
||||
const dy = y - startY;
|
||||
|
||||
if(dragging === 'top'){
|
||||
// modifier hauteur top et center
|
||||
let newTop = startTopH + dy;
|
||||
let newCenter = centerPanel.getBoundingClientRect().height - dy;
|
||||
|
||||
if(newTop < min) newTop = min;
|
||||
if(newCenter < min) newTop = startTopH + (centerPanel.getBoundingClientRect().height - min);
|
||||
|
||||
topPanel.style.height = newTop + 'px';
|
||||
} else if(dragging === 'bottom'){
|
||||
// modifier hauteur bottom et center inversé
|
||||
let newBottom = startBottomH - dy; // parce que on déplace la ligne entre center et bottom
|
||||
let newCenter = centerPanel.getBoundingClientRect().height + dy;
|
||||
|
||||
if(newBottom < min) newBottom = min;
|
||||
if(newCenter < min) newBottom = startBottomH + (centerPanel.getBoundingClientRect().height - min);
|
||||
|
||||
bottomPanel.style.height = newBottom + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
function onUp(){
|
||||
dragging = null;
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
document.removeEventListener('touchmove', onMove);
|
||||
document.removeEventListener('touchend', onUp);
|
||||
app.style.userSelect = '';
|
||||
}
|
||||
|
||||
document.querySelectorAll('.resizer').forEach(r => {
|
||||
r.addEventListener('mousedown', onDown);
|
||||
r.addEventListener('touchstart', onDown, {passive:false});
|
||||
});
|
||||
|
||||
// Ajuster le panneau central automatiquement quand on redimensionne la boîte globale
|
||||
const ro = new ResizeObserver(() => {
|
||||
// calculer la hauteur restante pour center
|
||||
const total = app.clientHeight;
|
||||
const topH = topPanel.getBoundingClientRect().height;
|
||||
const bottomH = bottomPanel.getBoundingClientRect().height;
|
||||
const centerH = Math.max(min, total - topH - bottomH - 2 * parseInt(getComputedStyle(document.documentElement).getPropertyValue('--handle-size')));
|
||||
centerPanel.style.height = centerH + 'px';
|
||||
});
|
||||
ro.observe(app);
|
||||
|
||||
// initialisation (force le calcul)
|
||||
window.addEventListener('load', () => { ro.takeRecords(); });
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
182
panel_windows.html
Normal file
182
panel_windows.html
Normal file
@@ -0,0 +1,182 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Page une-colonne redimensionnable</title>
|
||||
<style>
|
||||
:root{
|
||||
--min-panel: 40px;
|
||||
--handle-size: 8px;
|
||||
--bg-top: #2b6cb0;
|
||||
--bg-middle: #edf2f7;
|
||||
--bg-bottom: #2f855a;
|
||||
}
|
||||
|
||||
html,body{height:100%;margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,'Helvetica Neue',Arial}
|
||||
body{display:flex;align-items:center;justify-content:center;background:#f7fafc;padding:18px}
|
||||
|
||||
/* zone redimensionnable globale (vous pouvez la glisser depuis le coin) */
|
||||
#app{
|
||||
width:100%;
|
||||
height:100%;
|
||||
background:#fff;
|
||||
box-shadow:0 8px 30px rgba(2,6,23,.12);
|
||||
border-radius:0;
|
||||
overflow:hidden;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
}
|
||||
|
||||
/* panneaux */
|
||||
.panel{
|
||||
min-height:var(--min-panel);
|
||||
overflow:auto;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
padding:14px;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
|
||||
header{background:var(--bg-top);color:#fff}
|
||||
main{background:var(--bg-middle);color:#1a202c;flex:1}
|
||||
footer{background:var(--bg-bottom);color:#fff}
|
||||
|
||||
/* handle horizontal (séparateur entre panneaux) */
|
||||
.resizer{
|
||||
height:var(--handle-size);
|
||||
background:linear-gradient(90deg, rgba(0,0,0,0.08), rgba(0,0,0,0.02));
|
||||
cursor:row-resize;
|
||||
position:relative;
|
||||
z-index:10;
|
||||
}
|
||||
.resizer::after{
|
||||
content:'';position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
|
||||
width:40px;height:4px;border-radius:4px;background:rgba(0,0,0,0.12)
|
||||
}
|
||||
|
||||
/* petits controles d'aide */
|
||||
.hint{font-size:13px;opacity:.9}
|
||||
.controls{position:absolute;right:10px;top:8px;font-size:12px}
|
||||
.panel .title{font-weight:700}
|
||||
|
||||
/* styles adaptatifs pour petits écrans */
|
||||
@media (max-width:520px){
|
||||
#app{width:100%;height:80vh;border-radius:6px}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" role="application">
|
||||
<header class="panel" id="topPanel" style="height:120px;">
|
||||
<div>
|
||||
<div class="title">Bandeau du haut</div>
|
||||
<div class="hint">Glissez la barre grise sous ce bandeau pour redimensionner verticalement.</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="resizer" data-target="top"></div>
|
||||
|
||||
<main class="panel" id="centerPanel" style="height:calc(100% - 240px);">
|
||||
<div>
|
||||
<div class="title">Conteneur central</div>
|
||||
<div class="hint">Ce conteneur prend l'espace restant. Vous pouvez aussi redimensionner la fenêtre par le coin (resize).</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="resizer" data-target="bottom"></div>
|
||||
|
||||
<footer class="panel" id="bottomPanel" style="height:120px;">
|
||||
<div>
|
||||
<div class="title">Bandeau du bas</div>
|
||||
<div class="hint">Glissez la barre grise au-dessus pour modifier sa hauteur.</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Gestion du drag pour les séparateurs horizontaux
|
||||
(function(){
|
||||
const app = document.getElementById('app');
|
||||
const topPanel = document.getElementById('topPanel');
|
||||
const centerPanel = document.getElementById('centerPanel');
|
||||
const bottomPanel = document.getElementById('bottomPanel');
|
||||
const min = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--min-panel')) || 40;
|
||||
|
||||
let dragging = null;
|
||||
let startY = 0;
|
||||
let startTopH = 0;
|
||||
let startBottomH = 0;
|
||||
|
||||
function onDown(e){
|
||||
e.preventDefault();
|
||||
dragging = this.getAttribute('data-target');
|
||||
startY = e.touches ? e.touches[0].clientY : e.clientY;
|
||||
startTopH = topPanel.getBoundingClientRect().height;
|
||||
startBottomH = bottomPanel.getBoundingClientRect().height;
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
document.addEventListener('touchmove', onMove, {passive:false});
|
||||
document.addEventListener('touchend', onUp);
|
||||
app.style.userSelect = 'none';
|
||||
}
|
||||
|
||||
function onMove(e){
|
||||
if(!dragging) return;
|
||||
e.preventDefault();
|
||||
const y = e.touches ? e.touches[0].clientY : e.clientY;
|
||||
const dy = y - startY;
|
||||
|
||||
if(dragging === 'top'){
|
||||
// modifier hauteur top et center
|
||||
let newTop = startTopH + dy;
|
||||
let newCenter = centerPanel.getBoundingClientRect().height - dy;
|
||||
|
||||
if(newTop < min) newTop = min;
|
||||
if(newCenter < min) newTop = startTopH + (centerPanel.getBoundingClientRect().height - min);
|
||||
|
||||
topPanel.style.height = newTop + 'px';
|
||||
} else if(dragging === 'bottom'){
|
||||
// modifier hauteur bottom et center inversé
|
||||
let newBottom = startBottomH - dy; // parce que on déplace la ligne entre center et bottom
|
||||
let newCenter = centerPanel.getBoundingClientRect().height + dy;
|
||||
|
||||
if(newBottom < min) newBottom = min;
|
||||
if(newCenter < min) newBottom = startBottomH + (centerPanel.getBoundingClientRect().height - min);
|
||||
|
||||
bottomPanel.style.height = newBottom + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
function onUp(){
|
||||
dragging = null;
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
document.removeEventListener('touchmove', onMove);
|
||||
document.removeEventListener('touchend', onUp);
|
||||
app.style.userSelect = '';
|
||||
}
|
||||
|
||||
document.querySelectorAll('.resizer').forEach(r => {
|
||||
r.addEventListener('mousedown', onDown);
|
||||
r.addEventListener('touchstart', onDown, {passive:false});
|
||||
});
|
||||
|
||||
// Ajuster le panneau central automatiquement quand on redimensionne la boîte globale
|
||||
const ro = new ResizeObserver(() => {
|
||||
// calculer la hauteur restante pour center
|
||||
const total = app.clientHeight;
|
||||
const topH = topPanel.getBoundingClientRect().height;
|
||||
const bottomH = bottomPanel.getBoundingClientRect().height;
|
||||
const centerH = Math.max(min, total - topH - bottomH - 2 * parseInt(getComputedStyle(document.documentElement).getPropertyValue('--handle-size')));
|
||||
centerPanel.style.height = centerH + 'px';
|
||||
});
|
||||
ro.observe(app);
|
||||
|
||||
// initialisation (force le calcul)
|
||||
window.addEventListener('load', () => { ro.takeRecords(); });
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
182
panel_windows_full.html
Normal file
182
panel_windows_full.html
Normal file
@@ -0,0 +1,182 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Page une-colonne redimensionnable</title>
|
||||
<style>
|
||||
:root{
|
||||
--min-panel: 40px;
|
||||
--handle-size: 8px;
|
||||
--bg-top: #2b6cb0;
|
||||
--bg-middle: #edf2f7;
|
||||
--bg-bottom: #2f855a;
|
||||
}
|
||||
|
||||
html,body{height:100%;margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,'Helvetica Neue',Arial;overflow:hidden}
|
||||
body{display:flex;align-items:center;justify-content:center;background:#f7fafc;padding:18px}
|
||||
|
||||
/* zone redimensionnable globale (vous pouvez la glisser depuis le coin) */
|
||||
#app{
|
||||
width:100%;
|
||||
height:100%;
|
||||
background:#fff;
|
||||
box-shadow:0 8px 30px rgba(2,6,23,.12);
|
||||
border-radius:0;
|
||||
overflow:hidden;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
}
|
||||
|
||||
/* panneaux */
|
||||
.panel{
|
||||
min-height:var(--min-panel);
|
||||
overflow:auto;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
padding:14px;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
|
||||
header{background:var(--bg-top);color:#fff}
|
||||
main{background:var(--bg-middle);color:#1a202c;flex:1}
|
||||
footer{background:var(--bg-bottom);color:#fff}
|
||||
|
||||
/* handle horizontal (séparateur entre panneaux) */
|
||||
.resizer{
|
||||
height:var(--handle-size);
|
||||
background:linear-gradient(90deg, rgba(0,0,0,0.08), rgba(0,0,0,0.02));
|
||||
cursor:row-resize;
|
||||
position:relative;
|
||||
z-index:10;
|
||||
}
|
||||
.resizer::after{
|
||||
content:'';position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
|
||||
width:40px;height:4px;border-radius:4px;background:rgba(0,0,0,0.12)
|
||||
}
|
||||
|
||||
/* petits controles d'aide */
|
||||
.hint{font-size:13px;opacity:.9}
|
||||
.controls{position:absolute;right:10px;top:8px;font-size:12px}
|
||||
.panel .title{font-weight:700}
|
||||
|
||||
/* styles adaptatifs pour petits écrans */
|
||||
@media (max-width:520px){
|
||||
#app{width:100%;height:80vh;border-radius:6px}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" role="application">
|
||||
<header class="panel" id="topPanel" style="height:120px;">
|
||||
<div>
|
||||
<div class="title">Bandeau du haut</div>
|
||||
<div class="hint">Glissez la barre grise sous ce bandeau pour redimensionner verticalement.</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="resizer" data-target="top"></div>
|
||||
|
||||
<main class="panel" id="centerPanel" style="height:calc(100% - 240px);">
|
||||
<div>
|
||||
<div class="title">Conteneur central</div>
|
||||
<div class="hint">Ce conteneur prend l'espace restant. Vous pouvez aussi redimensionner la fenêtre par le coin (resize).</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="resizer" data-target="bottom"></div>
|
||||
|
||||
<footer class="panel" id="bottomPanel" style="height:120px;">
|
||||
<div>
|
||||
<div class="title">Bandeau du bas</div>
|
||||
<div class="hint">Glissez la barre grise au-dessus pour modifier sa hauteur.</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Gestion du drag pour les séparateurs horizontaux
|
||||
(function(){
|
||||
const app = document.getElementById('app');
|
||||
const topPanel = document.getElementById('topPanel');
|
||||
const centerPanel = document.getElementById('centerPanel');
|
||||
const bottomPanel = document.getElementById('bottomPanel');
|
||||
const min = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--min-panel')) || 40;
|
||||
|
||||
let dragging = null;
|
||||
let startY = 0;
|
||||
let startTopH = 0;
|
||||
let startBottomH = 0;
|
||||
|
||||
function onDown(e){
|
||||
e.preventDefault();
|
||||
dragging = this.getAttribute('data-target');
|
||||
startY = e.touches ? e.touches[0].clientY : e.clientY;
|
||||
startTopH = topPanel.getBoundingClientRect().height;
|
||||
startBottomH = bottomPanel.getBoundingClientRect().height;
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
document.addEventListener('touchmove', onMove, {passive:false});
|
||||
document.addEventListener('touchend', onUp);
|
||||
app.style.userSelect = 'none';
|
||||
}
|
||||
|
||||
function onMove(e){
|
||||
if(!dragging) return;
|
||||
e.preventDefault();
|
||||
const y = e.touches ? e.touches[0].clientY : e.clientY;
|
||||
const dy = y - startY;
|
||||
|
||||
if(dragging === 'top'){
|
||||
// modifier hauteur top et center
|
||||
let newTop = startTopH + dy;
|
||||
let newCenter = centerPanel.getBoundingClientRect().height - dy;
|
||||
|
||||
if(newTop < min) newTop = min;
|
||||
if(newCenter < min) newTop = startTopH + (centerPanel.getBoundingClientRect().height - min);
|
||||
|
||||
topPanel.style.height = newTop + 'px';
|
||||
} else if(dragging === 'bottom'){
|
||||
// modifier hauteur bottom et center inversé
|
||||
let newBottom = startBottomH - dy; // parce que on déplace la ligne entre center et bottom
|
||||
let newCenter = centerPanel.getBoundingClientRect().height + dy;
|
||||
|
||||
if(newBottom < min) newBottom = min;
|
||||
if(newCenter < min) newBottom = startBottomH + (centerPanel.getBoundingClientRect().height - min);
|
||||
|
||||
bottomPanel.style.height = newBottom + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
function onUp(){
|
||||
dragging = null;
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
document.removeEventListener('touchmove', onMove);
|
||||
document.removeEventListener('touchend', onUp);
|
||||
app.style.userSelect = '';
|
||||
}
|
||||
|
||||
document.querySelectorAll('.resizer').forEach(r => {
|
||||
r.addEventListener('mousedown', onDown);
|
||||
r.addEventListener('touchstart', onDown, {passive:false});
|
||||
});
|
||||
|
||||
// Ajuster le panneau central automatiquement quand on redimensionne la boîte globale
|
||||
const ro = new ResizeObserver(() => {
|
||||
// calculer la hauteur restante pour center
|
||||
const total = app.clientHeight;
|
||||
const topH = topPanel.getBoundingClientRect().height;
|
||||
const bottomH = bottomPanel.getBoundingClientRect().height;
|
||||
const centerH = Math.max(min, total - topH - bottomH - 2 * parseInt(getComputedStyle(document.documentElement).getPropertyValue('--handle-size')));
|
||||
centerPanel.style.height = centerH + 'px';
|
||||
});
|
||||
ro.observe(app);
|
||||
|
||||
// initialisation (force le calcul)
|
||||
window.addEventListener('load', () => { ro.takeRecords(); });
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
221
svg.html
Normal file
221
svg.html
Normal file
@@ -0,0 +1,221 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<title>SVG from LINESTRINGs</title>
|
||||
<style>
|
||||
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; padding: 12px; }
|
||||
#mapWrap { width: 100%; height: 80vh; border: 1px solid #ddd; background: #f8f9fb; }
|
||||
svg { width: 100%; height: 100%; display: block; }
|
||||
.line-sens-1 { stroke: #1f77b4; stroke-width: 3; fill: none; stroke-linecap: round; stroke-linejoin: round; }
|
||||
.line-sens-2 { stroke: #ff7f0e; stroke-width: 3; fill: none; stroke-linecap: round; stroke-linejoin: round; }
|
||||
.hovered { stroke-width: 5; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.2)); }
|
||||
.vertex { r: 2.2; fill: rgba(0,0,0,0.5); pointer-events: none; }
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
background: rgba(0,0,0,0.75);
|
||||
color: white;
|
||||
padding: 4px 6px;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
transform: translate(8px, 8px);
|
||||
display: none;
|
||||
z-index: 10;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Visualisation SVG des LINESTRING</h1>
|
||||
<div id="mapWrap"></div>
|
||||
<div id="tooltip" class="tooltip"></div>
|
||||
|
||||
<script>
|
||||
/* --- données fournies par l'utilisateur --- */
|
||||
const data = [
|
||||
{
|
||||
"fk_route_uuid": "8d004027-3279-4eb8-8de4-7f40ef542111",
|
||||
"sens": 1,
|
||||
"st_astext": "LINESTRING(240786.25000013656 79036.06584277935,240789.6701001395 79034.92654277757,240795.80500013664 79032.88284277637,240816.54300013464 79021.48284277692,240848.48400013373 79003.72684277594,240862.6130001347 78996.93084277771,240869.41630013572 78995.22494277824,240875.52820013772 78995.85994277615,240878.9446763978 78999.20556702279,240883.3350001378 79003.50484277774,240885.7940001368 79005.9478427777,240890.99600013572 79019.1748427758,240893.19700013375 79022.76474277768,240895.1890001397 79026.01384277735)"
|
||||
},
|
||||
{
|
||||
"fk_route_uuid": "8d004027-3279-4eb8-8de4-7f40ef542111",
|
||||
"sens": 2,
|
||||
"st_astext": "LINESTRING(240876.93050013762 79027.10734277777,240880.75500013976 79023.49284277484,240883.52900013572 79017.29684277717,240884.25950013375 79012.2112427773,240884.10070013476 79007.92494277749,240883.3350001378 79003.50484277774,240878.94468326174 78999.20557374507,240875.52820013772 78995.85994277615,240869.41630013572 78995.22494277824,240862.6130001347 78996.93084277771,240848.48400013373 79003.72684277594,240816.54300013464 79021.48284277692,240795.80500013664 79032.88284277637,240789.6701001395 79034.92654277757)"
|
||||
},
|
||||
{
|
||||
"fk_route_uuid": "e8af74b2-62bb-4ed9-841a-d72f01a471bf",
|
||||
"sens": 1,
|
||||
"st_astext": "LINESTRING(240788.70500013442 79077.95804277528,240726.47800013627 79107.39084277768,240719.76930013526 79110.84164277464,240718.30300013922 79111.59584277496,240713.81800014016 79116.28184277564,240711.32100013818 79123.2998427758,240711.94600013812 79131.3228427777,240710.94900013716 79134.09984277561,240718.46600013407 79143.87984277774,240728.20900013513 79148.93084277678,240737.10500013322 79148.99684277643,240749.14700013614 79143.51984277926,240772.7741277222 79129.7665901715,240784.77000013928 79122.7838427769,240811.7210001374 79105.72584277857)"
|
||||
},
|
||||
{
|
||||
"fk_route_uuid": "e8af74b2-62bb-4ed9-841a-d72f01a471bf",
|
||||
"sens": 2,
|
||||
"st_astext": "LINESTRING(240811.7210001374 79105.72584277857,240784.77000013928 79122.7838427769,240772.77412333922 79129.76659272239,240749.14700013614 79143.51984277926,240737.10500013322 79148.99684277643,240728.20900013513 79148.93084277678,240718.46600013407 79143.87984277774,240710.94900013716 79134.09984277561,240708.14400013717 79132.68284277525,240702.59600013716 79122.08384277672,240697.10920013717 79117.31394277606)"
|
||||
}
|
||||
];
|
||||
|
||||
/* --- utilitaires --- */
|
||||
|
||||
/** Parse un WKT LINESTRING et renvoie un array de [x,y] */
|
||||
function parseLinestring(wkt) {
|
||||
// wkt attendu: "LINESTRING(x1 y1,x2 y2,...)"
|
||||
const inner = wkt.replace(/^\s*LINESTRING\s*\(|\)\s*$/gi, '');
|
||||
const parts = inner.split(',');
|
||||
return parts.map(p => {
|
||||
const [xs, ys] = p.trim().split(/\s+/);
|
||||
return [parseFloat(xs), parseFloat(ys)];
|
||||
});
|
||||
}
|
||||
|
||||
/** calcule bbox pour toutes les géométries */
|
||||
function computeBBox(allCoords) {
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
allCoords.forEach(coords => {
|
||||
coords.forEach(([x,y]) => {
|
||||
if (x < minX) minX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (x > maxX) maxX = x;
|
||||
if (y > maxY) maxY = y;
|
||||
});
|
||||
});
|
||||
return { minX, minY, maxX, maxY };
|
||||
}
|
||||
|
||||
/* --- construction du SVG --- */
|
||||
(function render() {
|
||||
const parsed = data.map(item => {
|
||||
return {
|
||||
fk_route_uuid: item.fk_route_uuid,
|
||||
sens: item.sens,
|
||||
coords: parseLinestring(item.st_astext)
|
||||
};
|
||||
});
|
||||
|
||||
const allCoords = parsed.map(p => p.coords);
|
||||
const bbox = computeBBox(allCoords);
|
||||
|
||||
// padding en unités réelles (10% de la plus grande dimension)
|
||||
const padFactor = 0.08;
|
||||
const widthReal = bbox.maxX - bbox.minX || 1;
|
||||
const heightReal = bbox.maxY - bbox.minY || 1;
|
||||
const padX = widthReal * padFactor;
|
||||
const padY = heightReal * padFactor;
|
||||
const viewMinX = bbox.minX - padX;
|
||||
const viewMinY = bbox.minY - padY;
|
||||
const viewMaxX = bbox.maxX + padX;
|
||||
const viewMaxY = bbox.maxY + padY;
|
||||
const viewWidth = viewMaxX - viewMinX;
|
||||
const viewHeight = viewMaxY - viewMinY;
|
||||
|
||||
// on veut que Y augmente vers le bas en SVG => on va inverser Y en transformant
|
||||
// la vue en utilisant viewBox et en appliquant une transformation sur un <g>
|
||||
const mapWrap = document.getElementById('mapWrap');
|
||||
const svgNS = 'http://www.w3.org/2000/svg';
|
||||
|
||||
const svg = document.createElementNS(svgNS, 'svg');
|
||||
// viewBox: minX minY width height — on met minX, minY = viewMinX, viewMinY, mais
|
||||
// pour inverser Y on va créer un group <g transform="scale(1,-1) translate(0, -H)">
|
||||
svg.setAttribute('viewBox', `${viewMinX} ${viewMinY} ${viewWidth} ${viewHeight}`);
|
||||
svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
|
||||
|
||||
// container group : on doit inverser Y axis because coordinates are in projected system
|
||||
// Approach: apply transform to flip Y around center. Simpler: we apply transform to map coordinates to SVG coords when building path.
|
||||
// Here we'll use transform on group: scale(1,-1) and translate(0, - (viewMinY + viewMaxY) )
|
||||
const g = document.createElementNS(svgNS, 'g');
|
||||
// translate to flip around the middle of viewbox
|
||||
const translateY = -(viewMinY + viewMaxY);
|
||||
g.setAttribute('transform', `scale(1,-1) translate(0, ${translateY})`);
|
||||
|
||||
// for each parsed geometry create a path
|
||||
parsed.forEach((item, idx) => {
|
||||
const d = item.coords.map((pt, i) => {
|
||||
const [x, y] = pt;
|
||||
return (i === 0 ? 'M ' : 'L ') + x + ' ' + y;
|
||||
}).join(' ');
|
||||
|
||||
const path = document.createElementNS(svgNS, 'path');
|
||||
const cls = item.sens === 1 ? 'line-sens-1' : 'line-sens-2';
|
||||
path.setAttribute('d', d);
|
||||
path.setAttribute('class', cls);
|
||||
path.setAttribute('data-index', idx);
|
||||
path.setAttribute('data-uuid', item.fk_route_uuid);
|
||||
path.setAttribute('data-sens', item.sens);
|
||||
// cursor crosshair on hover:
|
||||
path.style.cursor = 'crosshair';
|
||||
|
||||
// events
|
||||
path.addEventListener('mouseenter', (e) => {
|
||||
path.classList.add('hovered');
|
||||
showTooltip(e, `route: ${item.fk_route_uuid}<br>sens: ${item.sens}`);
|
||||
});
|
||||
path.addEventListener('mousemove', (e) => {
|
||||
moveTooltip(e);
|
||||
});
|
||||
path.addEventListener('mouseleave', () => {
|
||||
path.classList.remove('hovered');
|
||||
hideTooltip();
|
||||
});
|
||||
path.addEventListener('dblclick', (e) => {
|
||||
e.stopPropagation();
|
||||
// action au double-clic : highlight et log — personnalise ici
|
||||
console.log('Double-clic sur', {
|
||||
uuid: item.fk_route_uuid,
|
||||
sens: item.sens,
|
||||
index: idx
|
||||
});
|
||||
// effet visuel bref
|
||||
path.classList.add('hovered');
|
||||
setTimeout(() => path.classList.remove('hovered'), 700);
|
||||
// si tu veux centrer / zoomer sur la géométrie, tu peux ajouter ici
|
||||
});
|
||||
|
||||
g.appendChild(path);
|
||||
|
||||
// optionally draw small vertices (disable pointer events to avoid fuss)
|
||||
item.coords.forEach(([x,y]) => {
|
||||
const c = document.createElementNS(svgNS, 'circle');
|
||||
c.setAttribute('cx', x);
|
||||
c.setAttribute('cy', y);
|
||||
c.setAttribute('r', 0.0); // mettre petit rayon si tu veux les voir, sinon 0
|
||||
c.setAttribute('class', 'vertex');
|
||||
g.appendChild(c);
|
||||
});
|
||||
});
|
||||
|
||||
svg.appendChild(g);
|
||||
mapWrap.appendChild(svg);
|
||||
|
||||
|
||||
/* Tooltip utilities */
|
||||
const tooltip = document.getElementById('tooltip');
|
||||
function showTooltip(evt, html) {
|
||||
tooltip.innerHTML = html;
|
||||
tooltip.style.display = 'block';
|
||||
moveTooltip(evt);
|
||||
}
|
||||
function moveTooltip(evt) {
|
||||
// placer le tooltip à côté du pointeur (page coords)
|
||||
const x = evt.clientX;
|
||||
const y = evt.clientY;
|
||||
tooltip.style.left = (x + 10) + 'px';
|
||||
tooltip.style.top = (y + 10) + 'px';
|
||||
}
|
||||
function hideTooltip() {
|
||||
tooltip.style.display = 'none';
|
||||
}
|
||||
|
||||
// clic global (ex: décocher highlight / action)
|
||||
svg.addEventListener('click', () => {
|
||||
// exemple : retirer highlight de toutes les lignes
|
||||
svg.querySelectorAll('path.hovered').forEach(p => p.classList.remove('hovered'));
|
||||
});
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user