commit 3080726c9b389cabaff5b2c56a785a5065550eec Author: Skycel Date: Mon Nov 10 16:31:08 2025 +0100 Initial commit diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/custom-libs.iml b/.idea/custom-libs.iml new file mode 100644 index 0000000..c956989 --- /dev/null +++ b/.idea/custom-libs.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/discord.xml b/.idea/discord.xml new file mode 100644 index 0000000..104c42f --- /dev/null +++ b/.idea/discord.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..016fef1 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 0000000..f324872 --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/JS/JustifedGallery/README.md b/JS/JustifedGallery/README.md new file mode 100644 index 0000000..c1558b0 --- /dev/null +++ b/JS/JustifedGallery/README.md @@ -0,0 +1,28 @@ +# đŸ–Œïž Justified Gallery + +**Justified Gallery** est une librairie JavaScript lĂ©gĂšre qui permet de gĂ©nĂ©rer une **galerie d’images justifiĂ©e**, responsive, fluide et entiĂšrement autonome. +Elle gĂšre aussi une **modal intĂ©grĂ©e** pour afficher les images en grand, avec navigation clavier, zoom et dĂ©placement Ă  la souris. + +--- + +## ✹ FonctionnalitĂ©s + +- đŸ§© Mise en page automatique des images (justifiĂ©e selon le conteneur). +- đŸ“± Responsive grĂące aux **breakpoints configurables**. +- đŸ–±ïž Navigation **clavier (flĂšches, entrĂ©e)** et **clic**. +- đŸȘŸ **Modal intĂ©grĂ©e** avec : + - navigation entre images, + - zoom, drag, et fermeture via *Escape*. +- 🔗 Option pour encapsuler automatiquement les images dans des liens. +- 🧠 Aucun framework requis (pur JavaScript ES6+). + +--- + +## 🚀 Installation + +Inclure simplement le fichier dans ton projet : + +```html + diff --git a/JS/JustifedGallery/assets/css/lib/gallery.css b/JS/JustifedGallery/assets/css/lib/gallery.css new file mode 100644 index 0000000..c8655fd --- /dev/null +++ b/JS/JustifedGallery/assets/css/lib/gallery.css @@ -0,0 +1,81 @@ +/* Conteneur de la galerie */ +.justified-gallery { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; +} + +/* Images */ +.justified-gallery img { + display: block; + object-fit: cover; + vertical-align: top; + transition: all 0.2s; + filter: blur(2px) brightness(0.8); +} +.justified-gallery img:hover { + transform: scale(1.03); + filter: blur(0px) brightness(1); +} + +/* Modal */ +.jg-modal { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + display: none; + justify-content: center; + align-items: center; + background: rgba(0,0,0,0.8); + z-index: 1000; +} + +.jg-modal img { + max-width: 90%; + max-height: 90%; + transition: transform 0.2s ease; + cursor: grab; +} + +.jg-close { + position: absolute; + top: 20px; + right: 35px; + color: #fff; + font-size: 40px; + font-weight: bold; + cursor: pointer; + z-index: 1001; +} + +.jg-controls span { + position: absolute; + top: 50%; + color: #fff; + font-size: 50px; + font-weight: bold; + cursor: pointer; + user-select: none; + padding: 10px; + background: rgba(0,0,0,0.3); + border-radius: 50%; + transform: translateY(-50%); +} + +.jg-prev { left: 20px; } +.jg-next { right: 20px; } + +.jg-prev, .jg-next { + position: absolute; + top: 50%; + transform: translateY(-50%); + background: rgba(255,255,255,0.7); + border: none; + font-size: 2rem; + padding: 0.5rem 1rem; + cursor: pointer; +} + + diff --git a/JS/JustifedGallery/assets/css/style.css b/JS/JustifedGallery/assets/css/style.css new file mode 100644 index 0000000..e3410d1 --- /dev/null +++ b/JS/JustifedGallery/assets/css/style.css @@ -0,0 +1,29 @@ +* { + box-sizing: border-box; +} +body { + width: 100%; + height: 100%; + margin: 0; + padding: 30px; + background-color: #fcfcfc; +} + +h1 { + text-align: center; +} +.gallery { + margin-top: 100px; + width: 100%; + display: flex; + flex-wrap: wrap; +} +.gallery a { + overflow: hidden; + display: block; + margin-bottom: 10px; + width: fit-content; +} +.gallery img { + width: 100%; +} \ No newline at end of file diff --git a/JS/JustifedGallery/assets/img/PANA4649_1.jpg b/JS/JustifedGallery/assets/img/PANA4649_1.jpg new file mode 100644 index 0000000..63ba92d Binary files /dev/null and b/JS/JustifedGallery/assets/img/PANA4649_1.jpg differ diff --git a/JS/JustifedGallery/assets/img/PANA6308.jpg b/JS/JustifedGallery/assets/img/PANA6308.jpg new file mode 100644 index 0000000..ad16b93 Binary files /dev/null and b/JS/JustifedGallery/assets/img/PANA6308.jpg differ diff --git a/JS/JustifedGallery/assets/img/PANA6677.jpg b/JS/JustifedGallery/assets/img/PANA6677.jpg new file mode 100644 index 0000000..feaad4c Binary files /dev/null and b/JS/JustifedGallery/assets/img/PANA6677.jpg differ diff --git a/JS/JustifedGallery/assets/img/PANA7701.jpg b/JS/JustifedGallery/assets/img/PANA7701.jpg new file mode 100644 index 0000000..37f1dac Binary files /dev/null and b/JS/JustifedGallery/assets/img/PANA7701.jpg differ diff --git a/JS/JustifedGallery/assets/img/PANA7846.jpg b/JS/JustifedGallery/assets/img/PANA7846.jpg new file mode 100644 index 0000000..69ffab1 Binary files /dev/null and b/JS/JustifedGallery/assets/img/PANA7846.jpg differ diff --git a/JS/JustifedGallery/assets/js/index.js b/JS/JustifedGallery/assets/js/index.js new file mode 100644 index 0000000..08ba64d --- /dev/null +++ b/JS/JustifedGallery/assets/js/index.js @@ -0,0 +1,10 @@ +import JustifiedGallery from "./lib/gallery.js"; + +document.addEventListener("DOMContentLoaded", () => { + let element = document.getElementById("gallery") + + new JustifiedGallery(element, { + wrapLinks: true, + }); +}) + diff --git a/JS/JustifedGallery/assets/js/lib/gallery.js b/JS/JustifedGallery/assets/js/lib/gallery.js new file mode 100644 index 0000000..56fc447 --- /dev/null +++ b/JS/JustifedGallery/assets/js/lib/gallery.js @@ -0,0 +1,387 @@ +export default class JustifiedGallery { + + constructor(container, options = {}) { + this.container = container; + this.options = { + margin: options.margin || 5, + columns: options.columns || 3, + breakpoints: options.breakpoints || { 768: 2, 480: 1 }, + wrapLinks: options.wrapLinks || false, // active l'encapsulation + linkAttribute: options.linkAttribute || 'src', // attribut Ă  utiliser pour href + justifyLastRow: options.justifyLastRow || false, + }; + this.currentIndex = 0; + + this.isOpenModal = false; + + this.init(); + window.addEventListener('resize', () => this.layoutGallery()); + } + + init() { + this.images = this.getImages(); + + // Encapsule les images si nĂ©cessaire + if (this.options.wrapLinks) { + this.images.forEach(img => this.wrapImage(img)); + this.images = this.getImages(); // rafraĂźchit la liste + } + + // S'assure que toutes les images sont chargĂ©es + const promises = this.images.map(img => this.waitForImageLoad(img)); + Promise.all(promises).then(() => { + this.layoutGallery(); + this.setupModal(); + }); + + this.container.addEventListener('keydown', e => { + e.preventDefault(); + if (e.key === 'ArrowRight') { + e.target.nextSibling.nextSibling.focus() + } + if (e.key === "ArrowLeft") { + e.target.previousSibling.previousSibling.focus() + } + if (e.key === "ArrowDown") { + + const images = this.getImages(); + let target = e.target; + + for (let i = 0; i < this.options.columns; i++) { + if (Math.ceil(images.length / this.options.columns) <= i) { + // On rĂ©cupĂšre le dernier Ă©lĂ©ment si on dĂ©passe la derniĂšre ligne + target = images[images.length - 1].parentNode; + break; + } + // Utiliser nextElementSibling pour Ă©viter les text nodes + if (target.nextElementSibling) { + target = target.nextElementSibling; + } + } + + if (target) { + target.focus(); + // Scroll pour voir la ligne oĂč se trouve l'image focus + target.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + } + if (e.key === "ArrowUp") { + let target = e.target + for (let i = 0; this.options.columns > i; i++) { + if (this.getImages().length < i) { + target = this.getImages()[0]; + break; + } + target = target.previousSibling.previousSibling + } + if (target) { + target.focus(); + // Scroll pour voir la ligne oĂč se trouve l'image focus + target.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + } + if (e.key === "Enter") { + e.target.click() + } + }) + } + + wrapImage(img) { + const href = img.getAttribute(this.options.linkAttribute) || img.src; + const a = document.createElement('a'); + a.href = href; + a.style.display = 'inline-block'; + img.parentNode.insertBefore(a, img); + a.appendChild(img); + } + + getImages() { + return Array.from(this.container.querySelectorAll('img')); + } + + waitForImageLoad(img) { + return new Promise(resolve => { + if (img.complete) resolve(); + else { + img.onload = () => resolve(); + img.onerror = () => resolve(); + } + }); + } + + getColumns() { + const width = this.container.clientWidth; + let cols = this.options.columns; + for (let bp in this.options.breakpoints) { + if (width <= bp) cols = this.options.breakpoints[bp]; + } + return cols; + } + + layoutGallery() { + const containerWidth = this.container.clientWidth; + const margin = this.options.margin; + const cols = this.getColumns(); + + let rowImages = []; + let totalRatio = 0; + + this.images.forEach((img, index) => { + const ratio = img.naturalWidth / img.naturalHeight; + + if (this.options.wrapLinks) { + rowImages.push({ img: img.parentNode, ratio }); + } else { + rowImages.push({ img, ratio }); + } + + totalRatio += ratio; + + const isLastRow = index === this.images.length - 1; + if (rowImages.length === cols || isLastRow) { + let rowHeight; + if (isLastRow && rowImages.length < cols && !this.options.justifyLastRow) { + // Ne pas Ă©tirer la derniĂšre ligne + const avgRatio = totalRatio / rowImages.length; + rowHeight = Math.floor((containerWidth / cols) / avgRatio); + } else { + // Étire normalement pour remplir la ligne + rowHeight = Math.floor((containerWidth - margin * (rowImages.length - 1)) / totalRatio); + } + this.scaleRow(rowImages, rowHeight, margin); + rowImages = []; + totalRatio = 0; + } + }); + } + + scaleRow(row, rowHeight, margin) { + row.forEach((item, index) => { + const width = rowHeight * item.ratio; + + + if (this.options.wrapLinks) { + item.img.style.overflow = 'hidden'; + item.img.firstChild.style.width = '100%'; + } + + item.img.style.width = `${width}px`; + item.img.style.height = `${rowHeight}px`; + item.img.style.marginRight = index < row.length - 1 ? `${margin}px` : '0'; + item.img.style.marginBottom = `${margin}px`; + item.img.style.cursor = 'pointer'; + }); + } + + setupModal() { + // CrĂ©ation du modal + this.modal = document.createElement('div'); + this.modal.classList.add('jg-modal'); + this.modal.innerHTML = ` + × + +
+ + +
+ `; + document.body.appendChild(this.modal); + + // RĂ©fĂ©rences + this.modalImg = this.modal.querySelector('.jg-modal-img'); + this.closeBtn = this.modal.querySelector('.jg-close'); + this.prevBtn = this.modal.querySelector('.jg-prev'); + this.nextBtn = this.modal.querySelector('.jg-next'); + + // ÉvĂ©nements + this.images.forEach((img, idx) => { + img.addEventListener('click', e => { + e.preventDefault(); // empĂȘche le clic sur le lien + this.openModal(idx); + }); + img.parentNode.addEventListener('keydown', e => { + if (e.key === 'Enter') { + e.preventDefault(); + this.openModal(idx); + } + }) + }); + this.closeBtn.addEventListener('click', () => this.closeModal()); + this.prevBtn.addEventListener('click', () => this.prevImage()); + this.nextBtn.addEventListener('click', () => this.nextImage()); + this.modal.addEventListener('click', e => { + if (e.target === this.modal) this.closeModal(); + }); + document.addEventListener('keydown', e => { + if (this.modal.style.display === 'flex') { + if (e.key === 'ArrowLeft') this.prevImage(); + if (e.key === 'ArrowRight') this.nextImage(); + if (e.key === 'Escape') this.closeModal(); + } + }); + + // Variables pour drag + zoom + this.modalImg.currentScale = 1; + let isDragging = false; + let startX, startY; + let currentTranslateX = 0, currentTranslateY = 0; + +// Drag start + this.modalImg.addEventListener('mousedown', e => { + if (e.button !== 0) return; + isDragging = true; + startX = e.clientX; + startY = e.clientY; + this.modalImg.style.cursor = 'grabbing'; + e.preventDefault(); + }); + +// Drag move + this.modal.addEventListener('mousemove', e => { + if (!isDragging) return; + const dx = e.clientX - startX; + const dy = e.clientY - startY; + startX = e.clientX; + startY = e.clientY; + + currentTranslateX += dx; + currentTranslateY += dy; + + this.modalImg.style.transform = `translate(${currentTranslateX}px, ${currentTranslateY}px) scale(${this.modalImg.currentScale})`; + }); + +// Drag end + this.modal.addEventListener('mouseup', () => { + isDragging = false; + this.modalImg.style.cursor = 'grab'; + }); + this.modal.addEventListener('mouseleave', () => { + isDragging = false; + this.modalImg.style.cursor = 'grab'; + }); + +// Scroll pour zoom + this.modalImg.addEventListener('wheel', e => { + e.preventDefault(); + const scaleAmount = e.deltaY > 0 ? -0.1 : 0.1; + this.modalImg.currentScale = Math.min(Math.max(this.modalImg.currentScale + scaleAmount, 0.5), 3); // limites min/max + this.modalImg.style.transform = `translate(${currentTranslateX}px, ${currentTranslateY}px) scale(${this.modalImg.currentScale})`; + }); + } + + openModal(index) { + this.isOpenModal = true; + this.currentIndex = index; + this.modal.style.display = 'flex'; + this.modalImg.src = this.images[this.currentIndex].src; + + document.body.style.overflow = 'hidden'; + } + + closeModal() { + this.isOpenModal = false; + this.modal.style.display = 'none'; + document.body.style.overflow = 'scroll'; + + // Reset zoom et position + this.modalImg.currentScale = 1; + this.modalImg.style.transform = `translate(0px, 0px) scale(1)`; + currentTranslateX = 0; + currentTranslateY = 0; + } + + prevImage() { + this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length; + this.modalImg.src = this.images[this.currentIndex].src; + } + + nextImage() { + this.currentIndex = (this.currentIndex + 1) % this.images.length; + this.modalImg.src = this.images[this.currentIndex].src; + } +} + + + +class Gallery { + constructor(container, options = {}) { + this.container = container; + this.options = { + margin: options.margin || 5, + columns: options.columns || 4, // nombre de colonnes par dĂ©faut + breakpoints: options.breakpoints || { 768: 2, 480: 1 }, // largeur => colonnes + }; + + this.init(); + window.addEventListener('resize', () => this.layoutGallery()); + } + + init() { + this.images = this.getImages(); + + // On attend que toutes les images soient chargĂ©es pour avoir leurs dimensions naturelles + const promises = this.images.map(img => this.waitForImageLoad(img)); + Promise.all(promises).then(() => this.layoutGallery()); + } + + getImages() { + return Array.from(this.container.querySelectorAll('img')); + } + + waitForImageLoad(img) { + return new Promise(resolve => { + if (img.complete) resolve(); + else { + img.onload = () => resolve(); + img.onerror = () => resolve(); + } + }); + } + + calculateRatio(img) { + return img.naturalWidth / img.naturalHeight; + } + + getColumns() { + const width = this.container.clientWidth; + let cols = this.options.columns; + + // VĂ©rifie les breakpoints pour adapter le nombre de colonnes + for (let bp in this.options.breakpoints) { + if (width <= bp) { + cols = this.options.breakpoints[bp]; + } + } + + return cols; + } + + layoutGallery() { + const containerWidth = this.container.clientWidth; + const margin = this.options.margin; + const cols = this.getColumns(); + const rowHeight = (containerWidth - margin * (cols - 1)) / cols; // largeur d'une "colonne" comme base + let rowImages = []; + + this.images.forEach((img, index) => { + const ratio = this.calculateRatio(img); + rowImages.push({ img, ratio }); + + if (rowImages.length === cols || index === this.images.length - 1) { + this.scaleRow(rowImages, rowHeight, margin); + rowImages = []; + } + }); + } + + scaleRow(row, rowHeight, margin) { + row.forEach((item, index) => { + const width = rowHeight * item.ratio; + item.img.style.width = `${width}px`; + item.img.style.height = `${rowHeight}px`; + item.img.style.marginRight = index < row.length - 1 ? `${margin}px` : '0'; + item.img.style.marginBottom = `${margin}px`; + }); + } +} + diff --git a/JS/JustifedGallery/index.html b/JS/JustifedGallery/index.html new file mode 100644 index 0000000..a33301d --- /dev/null +++ b/JS/JustifedGallery/index.html @@ -0,0 +1,27 @@ + + + + + + + + Test de crĂ©ation d'une gallerie justifiĂ© + + + + + + + +

Laboratoire - Gallery library

+