Spaces:
Running
Running
File size: 5,965 Bytes
f421004 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | // ===== Données exemple (contexte terrain / chantier) =====
let products = [
{ id: 1, name: "Gants de manutention", unit: "paire", qty: 3 },
{ id: 2, name: "Vis autoperceuse 4.8×38", unit: "boîte", qty: 2 },
{ id: 3, name: "Marteau de coffreur 700g", unit: "pièce", qty: 1 },
{ id: 4, name: "Chaussures de sécurité S3", unit: "paire", qty: 1 },
{ id: 5, name: "Niveau à bulle 40 cm", unit: "pièce", qty: 1 },
{ id: 6, name: "Ruban de mesure 5 m", unit: "pièce", qty: 2 },
{ id: 7, name: "Bobine de fil de tension", unit: "pièce", qty: 1 },
{ id: 8, name: "Serre-joints 80 cm", unit: "pièce", qty: 2 },
{ id: 9, name: "Cutter lame 18 mm", unit: "pièce", qty: 3 },
{ id: 10, name: "Lunettes de protection", unit: "pièce", qty: 5 },
{ id: 11, name: "Coffret forets béton SDS", unit: "boîte", qty: 1 },
{ id: 12, name: "Gilet haute visibilité Classe 2", unit: "pièce", qty: 4 },
];
let activeId = null;
const listEl = document.getElementById('product-list');
const headerCountEl = document.getElementById('header-count');
const totalQtyEl = document.getElementById('total-qty');
// ===== Rendu initial =====
function init() {
renderList();
updateSummary();
lucide.createIcons();
}
function renderList() {
listEl.innerHTML = products.map(p => `
<div class="product-row" data-id="${p.id}">
<div class="row-main">
<div class="product-meta">
<div class="product-name">${escapeHtml(p.name)}</div>
<div class="product-unit">${escapeHtml(p.unit)}</div>
</div>
<span class="qty-badge" data-qty-id="${p.id}">${p.qty}</span>
</div>
<div class="row-actions">
<button class="btn-action btn-minus" data-action="minus" aria-label="Diminuer la quantité">
<i data-lucide="minus" class="w-5 h-5"></i>
</button>
<button class="btn-action btn-plus" data-action="plus" aria-label="Augmenter la quantité">
<i data-lucide="plus" class="w-5 h-5"></i>
</button>
<button class="btn-action btn-delete" data-action="delete" aria-label="Supprimer le produit">
<i data-lucide="trash-2" class="w-5 h-5"></i>
</button>
</div>
</div>
`).join('');
lucide.createIcons();
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// ===== Mise à jour du résumé =====
function updateSummary() {
const total = products.reduce((sum, p) => sum + p.qty, 0);
const count = products.length;
headerCountEl.textContent = `${count} article${count > 1 ? 's' : ''}`;
totalQtyEl.textContent = total;
}
// ===== Gestion du focus / activation =====
function setActiveId(id) {
// Désactiver l’ancien
if (activeId !== null) {
const oldRow = document.querySelector(`.product-row[data-id="${activeId}"]`);
if (oldRow) oldRow.classList.remove('active');
}
activeId = id;
// Activer le nouveau
if (activeId !== null) {
const newRow = document.querySelector(`.product-row[data-id="${activeId}"]`);
if (newRow) newRow.classList.add('active');
}
}
// ===== Actions quantité / suppression =====
function updateQty(id, delta) {
const product = products.find(p => p.id === id);
if (!product) return;
const newQty = product.qty + delta;
if (newQty < 1) return; // empêche de descendre sous 1 (utiliser la poubelle)
product.qty = newQty;
// Mise à jour DOM ciblée (pas de re-render complet pour garder l’état visuel)
const badge = document.querySelector(`.qty-badge[data-qty-id="${id}"]`);
if (badge) badge.textContent = product.qty;
updateSummary();
}
function deleteProduct(id) {
const row = document.querySelector(`.product-row[data-id="${id}"]`);
if (row) {
// Animation rapide de sortie
row.style.transition = "opacity 0.2s ease, transform 0.2s ease, max-height 0.25s ease";
row.style.opacity = "0";
row.style.transform = "translateX(-1rem)";
row.style.maxHeight = row.offsetHeight + "px";
requestAnimationFrame(() => {
row.style.maxHeight = "0px";
row.style.padding = "0px";
row.style.margin = "0px";
row.style.overflow = "hidden";
});
}
setTimeout(() => {
products = products.filter(p => p.id !== id);
if (activeId === id) activeId = null;
renderList();
updateSummary();
// Ré-applique l’état actif si un autre était déjà sélectionné (théoriquement impossible ici car 1 seul actif)
if (activeId !== null) {
const remain = document.querySelector(`.product-row[data-id="${activeId}"]`);
if (remain) remain.classList.add('active');
}
}, 220);
}
// ===== Écouteurs d’événements =====
// Clics sur les lignes et boutons (délégation)
listEl.addEventListener('click', (e) => {
const btn = e.target.closest('[data-action]');
// 1. Bouton d’action (+, –, poubelle)
if (btn) {
e.stopPropagation();
const row = btn.closest('.product-row');
const id = parseInt(row.dataset.id, 10);
const action = btn.dataset.action;
if (action === 'plus') updateQty(id, 1);
else if (action === 'minus') updateQty(id, -1);
else if (action === 'delete') deleteProduct(id);
return;
}
// 2. Ligne produit (hors bouton)
const row = e.target.closest('.product-row');
if (row) {
const id = parseInt(row.dataset.id, 10);
// Si on tape la ligne déjà active → on la referme
// Sinon on active la nouvelle
setActiveId(activeId === id ? null : id);
}
});
// Clic en dehors des lignes (zone vide, header, footer…) → désactive tout
document.getElementById('app').addEventListener('click', (e) => {
const row = e.target.closest('.product-row');
const btn = e.target.closest('[data-action]');
if (!row && !btn) {
setActiveId(null);
}
});
// Démarrage
init(); |