diff --git a/Projet/metadata.md b/Projet/metadata.md new file mode 100644 index 0000000..b0f6bd1 --- /dev/null +++ b/Projet/metadata.md @@ -0,0 +1,49 @@ +#Structure Générale de metadata +{ + "type": "TITRE", + "metadata": { + ... // dépend du type + } +} + +#Détail par type + +##TEXTE +{ + "type": "TEXTE", + "metadata": {} +} + +##TITRE +{ + "type": "TITRE", + "metadata": { + "level": 1 // ou 2, 3... + } +} + +##LISTE +{ + "type": "LISTE", + "metadata": { + "style": "bullet" // ou "numbered" + } +} + +##CODE +{ + "type": "CODE", + "metadata": { + "language": "javascript", // ou "python", "html", etc. + } +} + +##PAGE +{ + "type": "PAGE", + "metadata": { + "pageId": "123", // ID de la page cible + "title": "Nom de la page liée" + } +} + diff --git a/Projet/src/main/java/projet/Bloc.java b/Projet/src/main/java/projet/Bloc.java index 74e1918..290e9a0 100644 --- a/Projet/src/main/java/projet/Bloc.java +++ b/Projet/src/main/java/projet/Bloc.java @@ -159,18 +159,22 @@ public class Bloc extends ParamBD { } - public static void updateBloc(int idBloc, String nouveauContenu) { + public static void updateBloc(int idBloc, String nouveauContenu, String type, String metadata) { try { Connection connexion = DriverManager.getConnection(bdURL, bdLogin, bdPassword); String sql = " UPDATE bloc" + " SET contenu = ?" + + ", type = ?" + + ", metadata = ?" + ", date_modification = ?" + " WHERE id = ?" + ";"; PreparedStatement pst = connexion.prepareStatement(sql); - pst.setString(1, nouveauContenu); - pst.setDate(2, Date.valueOf(LocalDate.now())); - pst.setInt(3, idBloc); + pst.setString(1, nouveauContenu); + pst.setString(2, type); + pst.setString(3, metadata); + pst.setDate(4, Date.valueOf(LocalDate.now())); + pst.setInt(5, idBloc); pst.executeUpdate(); pst.close(); diff --git a/Projet/src/main/java/projet/BlocCollaborative.java b/Projet/src/main/java/projet/BlocCollaborative.java new file mode 100644 index 0000000..be2ea27 --- /dev/null +++ b/Projet/src/main/java/projet/BlocCollaborative.java @@ -0,0 +1,76 @@ +package projet; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import jakarta.websocket.*; +import jakarta.websocket.server.ServerEndpoint; + + +@ServerEndpoint("/ws/bloc") +public class BlocCollaborative { + + private static final Map> sessionsParPage = new ConcurrentHashMap<>(); + + @OnOpen + public void onOpen(Session session) { + String query = session.getQueryString(); + String pageId = extractPageId(query); + session.getUserProperties().put("pageId", pageId); + + sessionsParPage.computeIfAbsent(pageId, k -> ConcurrentHashMap.newKeySet()).add(session); + } + + @OnMessage + public void onMessage(String message, Session session) { + String pageId = (String) session.getUserProperties().get("pageId"); + Set sessions = sessionsParPage.get(pageId); + + Lock sessionLock = new ReentrantLock(); + + synchronized (sessions) { + for (Session s : sessions) { + if (s.isOpen() && !s.equals(session)) { + sessionLock.lock(); + try { + s.getAsyncRemote().sendText(message); + } finally { + sessionLock.unlock(); + } + } + } + } + } + + @OnClose + public void onClose(Session session) { + String pageId = (String) session.getUserProperties().get("pageId"); + if (pageId != null) { + Set sessions = sessionsParPage.get(pageId); + if (sessions != null) { + sessions.remove(session); + if (sessions.isEmpty()) { + sessionsParPage.remove(pageId); + } + } + } + } + + @OnError + public void onError(Session session, Throwable throwable) { + throwable.printStackTrace(); + } + + private String extractPageId(String query) { + for (String param : query.split("&")) { + String[] kv = param.split("="); + if (kv.length == 2 && kv[0].equals("pageId")) { + return kv[1]; + } + } + return "default"; + } +} diff --git a/Projet/src/main/java/projet/ModifBloc.java b/Projet/src/main/java/projet/ModifBloc.java index 8b792ce..9ca11d2 100644 --- a/Projet/src/main/java/projet/ModifBloc.java +++ b/Projet/src/main/java/projet/ModifBloc.java @@ -35,10 +35,13 @@ public class ModifBloc extends HttpServlet { Utilisateur u = (Utilisateur) session.getAttribute("utilisateur"); if(u != null) { - String contenu = request.getParameter("contenu"); + String contenu = request.getParameter("contenu"); + String type = request.getParameter("type"); + String metadata = request.getParameter("metadata"); + int blocId = Integer.parseInt(request.getParameter("blocId")); - Bloc.updateBloc(blocId, contenu); + Bloc.updateBloc(blocId, contenu, type, metadata); response.sendRedirect("AfficherPage"); } else { response.sendRedirect("/Projet/"); diff --git a/Projet/src/main/java/projet/Partage.java b/Projet/src/main/java/projet/Partage.java new file mode 100644 index 0000000..32a3020 --- /dev/null +++ b/Projet/src/main/java/projet/Partage.java @@ -0,0 +1,41 @@ +package projet; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import java.io.IOException; + +@WebServlet("/Partage") +public class Partage extends HttpServlet { + private static final long serialVersionUID = 1L; + + public Partage() { + super(); + } + + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + HttpSession session = request.getSession(); + Utilisateur u = (Utilisateur) session.getAttribute("utilisateur"); + + String idPStr = request.getParameter("idP"); + String idPageStr = request.getParameter("idPage"); + + if(u != null && idPStr != null) { + int idP = Integer.parseInt(idPStr); + int idPage = Integer.parseInt(idPageStr); + + Page.partagerPage(idPage, u.getId(), idP); + response.sendRedirect("AfficherPage?id="+idPageStr); + }else { + response.sendRedirect("/Projet/"); + } + } + + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + } + +} diff --git a/Projet/src/main/java/projet/SupprimerMessage.java b/Projet/src/main/java/projet/SupprimerMessage.java new file mode 100644 index 0000000..0603a88 --- /dev/null +++ b/Projet/src/main/java/projet/SupprimerMessage.java @@ -0,0 +1,38 @@ +package projet; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import java.io.IOException; + +@WebServlet("/SupprimerMessage") +public class SupprimerMessage extends HttpServlet { + private static final long serialVersionUID = 1L; + + public SupprimerMessage() { + super(); + } + + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + HttpSession session = request.getSession(); + Utilisateur u = (Utilisateur) session.getAttribute("utilisateur"); + + if(u != null) { + if(u.getPrivilege().equals(Utilisateur.Privilege.ADMIN.name())) { + Message.effacerMessage(); + } + response.sendRedirect("AfficherPage"); + }else { + response.sendRedirect("/Projet/"); + } + } + + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + doGet(request, response); + } + +} diff --git a/Projet/src/main/webapp/script.js b/Projet/src/main/webapp/script.js new file mode 100644 index 0000000..e84205c --- /dev/null +++ b/Projet/src/main/webapp/script.js @@ -0,0 +1,493 @@ + + + // focus sur le dernier textarea quand la page s'ouvre + window.addEventListener('DOMContentLoaded', () => { + const blocs = document.querySelectorAll('#md [contenteditable="true"]'); + if (blocs.length > 0) { + blocs[blocs.length - 1].focus(); // focus sur le dernier bloc + } + + const container = document.querySelector(".messages-container"); + if(container) container.scrollTop = container.scrollHeight; // Pour voir le dernier message du Tchat + + + // Sélectionne tous les dropdowns sur la page + const dropdowns = document.querySelectorAll('.dropdown'); + + dropdowns.forEach(function (dropdown) { + const trigger = dropdown.querySelector('.dropdown-trigger button'); + + trigger.addEventListener('click', function (event) { + event.stopPropagation(); // Évite que le clic remonte jusqu'au body + dropdown.classList.toggle('is-active'); + }); + }); + + // Fermer le menu si on clique en dehors + document.addEventListener('click', function () { + dropdowns.forEach(function (dropdown) { + dropdown.classList.remove('is-active'); + }); + }); + + ajouterBlocVideSiBesoin(); + }); + + // Fonction pour ajouter un nouvel événement à chaque textarea + function addBlocEvent(bloc) { + + bloc.addEventListener('keydown', function(event) { + + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + + const currentBloc = event.target; + const texte = currentBloc.innerText.trim(); + + if (texte.startsWith("/")) { + // Gérer les commandes + handleSlashCommand(currentBloc, texte); + } else { + const blocId = currentBloc.getAttribute('data-id'); // Récupère l'ID du bloc + const type = currentBloc.getAttribute('data-type'); + const metadata = currentBloc.getAttribute('metadata'); + const params = new URLSearchParams(); + + let contenuTexte = currentBloc.innerHTML + .replace(//gi, '\n') // Remplace les
par \n + .replace(//gi, '**') // Remplace par ** + .replace(/<\/b>/gi, '**') // Remplace par ** + .replace(//gi, '*') // Remplace par * + .replace(/<\/i>/gi, '*'); // Remplace par * + params.append("contenu", contenuTexte); + params.append("blocId", blocId); + params.append("metadata", metadata); + params.append("type", type); + + fetch("/Projet/ModifBloc", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + body: params + }).then(response => { + if (!response.ok) { + console.error("Erreur lors de la mise à jour du bloc."); + } + }); + + ajouterBlocVideSiBesoin(); + } + } + }); + + bloc.addEventListener('input', function () { + autoResize(bloc); + }); + } + + function autoResize(textarea) { + textarea.style.height = 'auto'; + textarea.style.height = textarea.scrollHeight + 'px'; + } + + document.querySelectorAll('#md [contenteditable="true"]').forEach(bloc => { + addBlocEvent(bloc); + }); + + + function ajouterBlocVideSiBesoin() { + const allBlocs = document.querySelectorAll('#md [contenteditable="true"]'); + const pageId = new URLSearchParams(window.location.search).get("id"); + if(pageId === null) return; + + const message = document.querySelector('.column.is-half p'); + if (message && message.textContent.trim() === "Pas encore de page choisie.") { + console.log("Aucune page choisie, fonction arrêtée."); + return; // Arrête la fonction ici (se passe après avoir supprimé une page) + } + + if (allBlocs.length === 0 || allBlocs[allBlocs.length - 1].innerText.trim() !== "") { + const params = new URLSearchParams(); + params.append("contenu", ""); // Bloc vide + params.append("type", "TEXTE"); // Type par défaut : TEXTE + params.append("ordre", allBlocs.length); + params.append("pageId", new URLSearchParams(window.location.search).get("id")); + + fetch("/Projet/NouveauBloc", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + body: params.toString() + }).then(response => response.json()) + .then(data => { + if (data && data.idGenere) { + const idGenere = data.idGenere; + + const container = document.createElement('div'); + container.classList.add('field', 'is-grouped', 'is-align-items-flex-start', 'bloc-container'); + + const control = document.createElement('div'); + control.classList.add('control', 'is-expanded'); + + const newBloc = document.createElement('div'); + newBloc.classList.add('textarea', 'is-primary', 'editor'); + newBloc.setAttribute('contenteditable', 'true'); + newBloc.setAttribute('rows', '1'); + newBloc.setAttribute('data-type', 'TEXTE'); + newBloc.setAttribute('data-id', idGenere); + newBloc.setAttribute('metadata', 'TEXTE'); + + control.appendChild(newBloc); + container.appendChild(control); + + const delBtnWrapper = document.createElement('div'); + delBtnWrapper.classList.add('control'); + + const delBtn = document.createElement('button'); + delBtn.classList.add('delete', 'is-danger', 'delete-bloc-btn'); + delBtn.setAttribute('data-id', idGenere); + + delBtnWrapper.appendChild(delBtn); + container.appendChild(delBtnWrapper); + + document.getElementById('md').appendChild(container); + + addDeleteBloc(delBtn); + addBlocEvent(newBloc); + newBloc.focus(); + } + }); + } else { + // Vérifier si le dernier bloc est vide et lui appliquer le focus + const blocs = document.querySelectorAll('.editor'); // Sélectionne tous les blocs + if(blocs === null) return; + const lastBloc = blocs[blocs.length - 1]; // Dernier bloc + + if (lastBloc && lastBloc.innerHTML.trim() === '') { + lastBloc.focus(); // Mettre le focus sur le dernier bloc si vide + } + } + } + + + function addDeleteBloc(button) { + button.addEventListener('click', function () { + const blocId = button.dataset.id; + const blocContainer = button.closest('.bloc-container'); + const bloc = blocContainer.querySelector('[contenteditable="true"]'); + + if (bloc.innerText.trim() === "") { + return; // Empêche la suppression si le textarea est vide + } + + if (confirm("Voulez-vous vraiment supprimer ce bloc ?")) { + const params = new URLSearchParams(); + params.append("blocId", blocId); + + fetch("/Projet/SupprimerBloc", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + body: params + }).then(response => { + if (response.ok) { + blocContainer.remove(); // Supprime visuellement le bloc + ajouterBlocVideSiBesoin(); + } else { + console.error("Erreur lors de la suppression du bloc."); + } + }); + } + }); + } + + document.querySelectorAll('.delete-bloc-btn').forEach(t => { + addDeleteBloc(t); + }); + + function addDeletePage(button) { + button.addEventListener('click', function () { + const pageId = button.dataset.id; + + if (confirm("Voulez-vous vraiment supprimer cette page ?")) { + const params = new URLSearchParams(); + params.append("pageId", pageId); + + fetch("/Projet/SupprimerPage", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + body: params + }).then(response => { + if (response.ok) { + location.reload(); + } else { + console.error("Erreur lors de la suppression du bloc."); + } + }); + } + }); + } + + document.querySelectorAll('.delete-page-btn').forEach(t => { + addDeletePage(t); + }); + + // Pour le Tchat + const socketTchat = new WebSocket("ws://" + window.location.host + "/Projet/ws/tchat"); + + socketTchat.onmessage = function(event) { + const container = document.querySelector(".messages-container"); + const p = document.createElement("p"); + p.textContent = event.data; + container.appendChild(p); + container.scrollTop = container.scrollHeight; + }; + + const input = document.querySelector("input[name='contenu']"); + if (input) { + document.querySelector("input[name='contenu']").addEventListener("keydown", function(event) { + const login = document.getElementById("user-login").textContent; + if (event.key === "Enter") { + const message = this.value; + if (message.trim() !== "") { + socketTchat.send(login + " : " + message); + this.value = ""; + } + } + }); + } + + + + // Pour le travail collaboratif sur les blocs + const params = new URLSearchParams(window.location.search); + const id = params.get("id"); + const pageId = parseInt(id); + const socketBloc = new WebSocket("ws://" + window.location.host + "/Projet/ws/bloc?pageId=" + pageId); + + socketBloc.onmessage = (event) => { + const data = JSON.parse(event.data); + if (data.action === "update") { + const blocElement = document.querySelector(`.editor[data-id='${data.blocId}']`); + if (blocElement) { + blocElement.textContent = data.content; + blocElement.setAttribute('metadata', data.metadata); + } + } + }; + + document.querySelectorAll('.editor').forEach((bloc) => { + bloc.addEventListener('input', (event) => { + const blocId = event.target.getAttribute('data-id'); + const content = event.target.textContent; // Récupère le contenu modifié du bloc + const metadata = event.target.getAttribute('metadata'); // Récupère le metadata du bloc + + const modif = { + action: "update", + blocId: blocId, + content: content, + metadata: metadata + }; + + socketBloc.send(JSON.stringify(modif)); // Envoi de la modification via le WebSocket + }); + }); + + document.querySelectorAll('.editor').forEach(function(element) { + let content = element.innerHTML; + content = content.replace(/\n/g, '
'); + content = content.replace(/\*\*(.*?)\*\*/g, '$1'); + content = content.replace(/\*(.*?)\*/g, '$1'); + element.innerHTML = content; + }); + + + + + // pour modifier le rendu des blocs + function getBlocMetadata(bloc) { + try { + return JSON.parse(bloc.dataset.metadata || "{}"); + } catch (e) { + return {}; + } + } + + function setBlocMetadata(bloc, newMetadata) { + bloc.dataset.metadata = JSON.stringify(newMetadata); + } + + function applyBlocType(bloc, type, extra = {}) { + bloc.dataset.type = type; + + let metadata = {}; + + switch (type) { + case "TEXTE": + metadata = {}; + break; + case "TITRE": + metadata = { level: extra.level || 1 }; + break; + case "LISTE": + metadata = { style: extra.style || "bullet" }; + break; + case "CODE": + metadata = { language: extra.language || "plaintext", theme: "light" }; + break; + case "PAGE": + metadata = { + pageId: extra.pageId || "nouvelle-page-id", + title: extra.title || "Nouvelle page" + }; + break; + default: + console.warn("Type de bloc inconnu :", type); + } + + setBlocMetadata(bloc, metadata); + renderBlocStyle(bloc); + } + + function renderBlocStyle(bloc) { + if (!bloc || !bloc.dataset) return; + + // Nettoyage des anciennes classes + bloc.classList.remove('is-title', 'is-title-1', 'is-title-2', 'is-title-3'); + bloc.classList.remove('is-code-block', 'is-list', 'is-toggle'); + bloc.setAttribute('placeholder', 'Tapez ici...'); + bloc.style.whiteSpace = "normal"; // reset si code + + const type = bloc.dataset.type || 'TEXTE'; + + switch (type) { + case 'TEXTE': + bloc.classList.add('is-text'); + break; + + case 'TITRE': + const level = bloc.dataset.level || '2'; + bloc.classList.add('is-title', `is-title-${level}`); + bloc.setAttribute('placeholder', `Titre niveau ${level}`); + break; + + case 'LISTE': + bloc.classList.add('is-list'); + bloc.setAttribute('placeholder', '• Élément de liste'); + break; + + case 'CODE': + bloc.classList.add('is-code-block'); + bloc.style.whiteSpace = "pre"; + const lang = bloc.dataset.language || 'plaintext'; + bloc.setAttribute('placeholder', `Code (${lang})`); + break; + + case 'PAGE': + bloc.classList.add('is-page-block'); + bloc.setAttribute('placeholder', 'Nouvelle page...'); + break; + + case 'TOGGLE': + bloc.classList.add('is-toggle'); + bloc.setAttribute('placeholder', 'Cliquez pour développer...'); + break; + + default: + bloc.classList.add('is-text'); + break; + } + } + + + function handleSlashCommand(bloc, texte) { + const parts = texte.substring(1).split(" "); // Supprimer le "/" et diviser + const command = parts[0].toLowerCase(); + const param = parts.slice(1).join(" "); // Récupère les paramètres (par exemple "js" pour /code js) + + switch (command) { + case "page": + createNewPage(param || "Nouvelle page"); + bloc.textContent = param || "Nouvelle page"; // On vide le bloc après transformation + autoResize(bloc); + break; + + case "h1": + case "h2": + case "h3": + applyBlocType(bloc, "TITRE", { level: parseInt(command[1] || "1") }); + bloc.textContent = ''; // On vide le bloc après transformation + autoResize(bloc); + break; + + case "ul": + applyBlocType(bloc, "LISTE", { style: "bullet" }); + bloc.textContent = ''; // On vide le bloc après transformation + autoResize(bloc); + break; + + case "ol": + applyBlocType(bloc, "LISTE", { style: "numbered" }); + bloc.textContent = ''; // On vide le bloc après transformation + autoResize(bloc); + break; + + case "code": + applyBlocType(bloc, "CODE", { language: param || "plaintext" }); + bloc.textContent = ''; // On vide le bloc après transformation + autoResize(bloc); + break; + + default: + console.log(`Commande non reconnue: ${command}`); + break; + } + + bloc.focus(); + placeCursorAtEnd(bloc); + } + + function placeCursorAtEnd(element) { + const range = document.createRange(); + const selection = window.getSelection(); + + range.selectNodeContents(element); + range.collapse(false); + + selection.removeAllRanges(); + selection.addRange(range); + + element.scrollIntoView({ behavior: "smooth", block: "end" }); + } + + function createNewPage(titre) { + const params = new URLSearchParams(); + params.append("titre", titre); + + fetch("/Projet/NouvellePage", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + body: params.toString() + }) + .then(response => response.json()) + .then(data => { + if (data && data.pageId) { + // Rediriger vers la nouvelle page par exemple + window.location.href = "/Projet/Page?id=" + data.pageId; + } else { + console.error("Erreur lors de la création de la page."); + } + }) + .catch(err => { + console.error("Erreur réseau :", err); + }); + } + + \ No newline at end of file diff --git a/Projet/src/main/webapp/styles.css b/Projet/src/main/webapp/styles.css new file mode 100644 index 0000000..1d8dd45 --- /dev/null +++ b/Projet/src/main/webapp/styles.css @@ -0,0 +1,47 @@ +@charset "UTF-8"; + + +.editor { + resize: none; /* Empêche le redimensionnement par l'utilisateur */ + overflow-y: hidden; /* Masquer la barre de défilement verticale */ + border: none; + outline: none; +} +.tchat-container { + display: block; + padding: 1rem; +} +.messages-container { + overflow-y: auto; + margin-bottom: 0.5rem; + padding: 0.5rem; + border: 1px solid #ddd; + border-radius: 4px; + background: #f9f9f9; +} +.input-container { + margin-top: auto; +} + + +/* pour les blocs */ +.is-title-1 { font-size: 2em; font-weight: bold; } +.is-title-2 { font-size: 1.5em; font-weight: bold; } +.is-title-3 { font-size: 1.2em; font-weight: bold; } + +.is-code-block { + font-family: monospace; + background: #f6f6f6; + border: 1px solid #ddd; + padding: 0.5em; +} + +.is-list::before { + content: "• "; + color: #666; +} + +.is-toggle::before { + content: "▶ "; + cursor: pointer; +}