// Admin JavaScript for the backend management interface // Global object to hold Quill instances let quillInstances = {}; document.addEventListener('DOMContentLoaded', function() { // Initialize theme initTheme(); // Setup dashboard functionality setupDashboardCards(); // Setup admin forms setupMatiereForm(); setupSousCategorieForm(); setupTexteForm(); // Basic setup (e.g., category dropdowns if needed) // Setup content block editor specifically for edit_texte.html if (document.getElementById('blocks-container')) { setupContentBlockEditor(); } // Setup image management (gallery interaction, maybe general upload elsewhere) setupImageGallery(); // Handles selecting from the modal // Note: The specific AJAX upload logic is now mainly in edit_texte.html's script block // but we keep setupImageUploader for potential preview logic or other upload forms. setupImageUploader(); // Sets up preview for the upload form // Setup theme toggle setupThemeToggle(); }); // Initialize theme based on user preference function initTheme() { const userPreference = localStorage.getItem('theme') || 'light'; document.documentElement.setAttribute('data-theme', userPreference); updateThemeIcon(userPreference); } // Setup theme toggle functionality function setupThemeToggle() { const themeToggle = document.getElementById('theme-toggle'); if (!themeToggle) return; themeToggle.addEventListener('click', function() { const currentTheme = document.documentElement.getAttribute('data-theme'); const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', newTheme); localStorage.setItem('theme', newTheme); updateThemeIcon(newTheme); saveThemePreference(newTheme); // Optional: Inform server }); } // Update the theme toggle icon based on current theme function updateThemeIcon(theme) { const themeToggle = document.getElementById('theme-toggle'); if (!themeToggle) return; const icon = themeToggle.querySelector('i'); if (!icon) return; if (theme === 'dark') { icon.classList.remove('fa-moon'); icon.classList.add('fa-sun'); themeToggle.setAttribute('title', 'Activer le mode clair'); } else { icon.classList.remove('fa-sun'); icon.classList.add('fa-moon'); themeToggle.setAttribute('title', 'Activer le mode sombre'); } } // Save theme preference to server (Optional) function saveThemePreference(theme) { // Example using fetch API - adjust URL and method as needed /* fetch('/set_theme_preference', { // Replace with your actual endpoint method: 'POST', headers: { 'Content-Type': 'application/json', // Add CSRF token header if needed // 'X-CSRFToken': getCsrfToken() }, body: JSON.stringify({ theme: theme }) }) .then(response => response.json()) .then(data => { if(data.success) console.log('Theme preference saved on server.'); else console.error('Failed to save theme preference on server.'); }) .catch(error => { console.error('Error saving theme preference:', error); }); */ } // Setup dashboard cards with hover effects function setupDashboardCards() { const dashboardCards = document.querySelectorAll('.admin-card'); dashboardCards.forEach(card => { card.addEventListener('mouseenter', function() { this.style.transform = 'translateY(-5px)'; this.style.boxShadow = 'var(--hover-shadow)'; // Keep transition consistent (defined in CSS is better) }); card.addEventListener('mouseleave', function() { this.style.transform = 'translateY(0)'; this.style.boxShadow = 'var(--shadow)'; }); }); } // Setup matiere form functionality (if on matiere page) function setupMatiereForm() { const editButtons = document.querySelectorAll('.edit-matiere-btn'); editButtons.forEach(button => { button.addEventListener('click', function() { const matiereId = this.getAttribute('data-id'); const matiereName = this.getAttribute('data-name'); const matiereColor = this.getAttribute('data-color'); const editForm = document.getElementById('edit-matiere-form'); const addSection = document.getElementById('add-matiere-section'); const editSection = document.getElementById('edit-matiere-section'); if (editForm && addSection && editSection) { editForm.querySelector('input[name="matiere_id"]').value = matiereId; editForm.querySelector('input[name="nom"]').value = matiereName; const colorInput = editForm.querySelector('input[name="color_code"]'); colorInput.value = matiereColor; // Trigger input event to update preview if color picker exists if(colorInput) colorInput.dispatchEvent(new Event('input')); addSection.classList.add('d-none'); editSection.classList.remove('d-none'); editSection.scrollIntoView({ behavior: 'smooth' }); } }); }); const cancelEditButton = document.getElementById('cancel-edit-matiere'); if (cancelEditButton) { cancelEditButton.addEventListener('click', function(e) { e.preventDefault(); document.getElementById('add-matiere-section')?.classList.remove('d-none'); document.getElementById('edit-matiere-section')?.classList.add('d-none'); }); } // Color picker preview logic const colorPickers = document.querySelectorAll('input[type="color"]'); colorPickers.forEach(picker => { const previewId = picker.id + '-preview'; // Assuming a convention like id="color" and preview span id="color-preview" let preview = document.getElementById(previewId); if (!preview) { // Create preview dynamically if not present preview = document.createElement('span'); preview.className = 'color-preview'; preview.id = previewId; preview.style.display = 'inline-block'; preview.style.width = '24px'; preview.style.height = '24px'; preview.style.borderRadius = '4px'; preview.style.marginLeft = '10px'; preview.style.verticalAlign = 'middle'; preview.style.border = '1px solid var(--border-color)'; picker.parentNode.insertBefore(preview, picker.nextSibling); } picker.addEventListener('input', function() { preview.style.backgroundColor = this.value; }); // Initialize preview color preview.style.backgroundColor = picker.value; }); } // Setup sous categorie form functionality (if on sous_categorie page) function setupSousCategorieForm() { const editButtons = document.querySelectorAll('.edit-sous-categorie-btn'); editButtons.forEach(button => { button.addEventListener('click', function() { const sousCategorieId = this.getAttribute('data-id'); const sousCategorieName = this.getAttribute('data-name'); const matiereId = this.getAttribute('data-matiere-id'); const editForm = document.getElementById('edit-sous-categorie-form'); const addSection = document.getElementById('add-sous-categorie-section'); const editSection = document.getElementById('edit-sous-categorie-section'); if (editForm && addSection && editSection) { editForm.querySelector('input[name="sous_categorie_id"]').value = sousCategorieId; editForm.querySelector('input[name="nom"]').value = sousCategorieName; editForm.querySelector('select[name="matiere_id"]').value = matiereId; addSection.classList.add('d-none'); editSection.classList.remove('d-none'); editSection.scrollIntoView({ behavior: 'smooth' }); } }); }); const cancelEditButton = document.getElementById('cancel-edit-sous-categorie'); if (cancelEditButton) { cancelEditButton.addEventListener('click', function(e) { e.preventDefault(); document.getElementById('add-sous-categorie-section')?.classList.remove('d-none'); document.getElementById('edit-sous-categorie-section')?.classList.add('d-none'); }); } // Filter logic if present const matiereFilterSelect = document.getElementById('matiere-filter'); if (matiereFilterSelect) { matiereFilterSelect.addEventListener('change', function() { const selectedMatiereId = this.value; const tableBody = document.querySelector('.table tbody'); // Adjust selector if needed if (!tableBody) return; const sousCategorieRows = tableBody.querySelectorAll('tr'); // Assuming each row represents a sous-categorie sousCategorieRows.forEach(row => { // Check if the row has a data attribute identifying the matiere const rowMatiereId = row.getAttribute('data-matiere-id'); if (rowMatiereId) { if (selectedMatiereId === '' || rowMatiereId === selectedMatiereId) { row.style.display = ''; // Show row } else { row.style.display = 'none'; // Hide row } } }); }); // Trigger change on load to apply initial filter if a value is pre-selected matiereFilterSelect.dispatchEvent(new Event('change')); } } // Setup general texte form functionality (e.g., dynamic dropdowns if creating new text) function setupTexteForm() { // Example: If there's a matiere dropdown that populates sous-categories on a *creation* page const matiereSelect = document.getElementById('matiere-select'); // Adjust ID if different const sousCategorieSelect = document.getElementById('sous-categorie-select'); // Adjust ID if (matiereSelect && sousCategorieSelect) { matiereSelect.addEventListener('change', function() { const matiereId = this.value; // Clear current sous-categorie options (except the default placeholder) sousCategorieSelect.innerHTML = ''; sousCategorieSelect.disabled = true; if (matiereId) { // Fetch sous-categories for the selected matiere fetch(`/api/get_sous_categories/${matiereId}`) // Adjust API endpoint .then(response => { if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(data => { if (data && data.length > 0) { data.forEach(sc => { const option = document.createElement('option'); option.value = sc.id; option.textContent = sc.nom; sousCategorieSelect.appendChild(option); }); sousCategorieSelect.disabled = false; } else { // Handle case with no sous-categories const option = document.createElement('option'); option.textContent = 'Aucune sous-catégorie trouvée'; option.disabled = true; sousCategorieSelect.appendChild(option); } }) .catch(error => { console.error('Error loading sous-categories:', error); // Optionally display an error message to the user const option = document.createElement('option'); option.textContent = 'Erreur de chargement'; option.disabled = true; sousCategorieSelect.appendChild(option); }); } }); // Trigger change on load if a matiere is pre-selected if(matiereSelect.value) { matiereSelect.dispatchEvent(new Event('change')); } } } // --- Content Block Editor Specific Functions --- function setupContentBlockEditor() { const blocksContainer = document.getElementById('blocks-container'); const addBlockButton = document.getElementById('add-block-button'); const saveBlocksButton = document.getElementById('save-blocks-button'); if (!blocksContainer) return; // Only run if the container exists // Initialize SortableJS for drag-and-drop if (window.Sortable) { new Sortable(blocksContainer, { animation: 150, handle: '.block-handle', // Class for the drag handle element ghostClass: 'block-ghost', // Class applied to the ghost element onEnd: function(evt) { updateBlockOrder(); // Renumber blocks after drag } }); } else { console.warn('SortableJS not loaded. Drag and drop for blocks disabled.'); } // Add new block button listener if (addBlockButton) { addBlockButton.addEventListener('click', function() { addContentBlock(); // Add an empty block }); } // Save blocks button listener if (saveBlocksButton) { saveBlocksButton.addEventListener('click', function() { saveContentBlocks(); // Gather data and submit the form }); } // Setup controls and Quill for existing blocks on page load setupExistingBlockControls(); } // Initialize Quill Editor on a given container function initializeQuillEditor(container, hiddenInput, initialContent = '') { if (!container || !hiddenInput) { console.error("Quill initialization failed: Container or hidden input missing."); return; } const blockId = container.id.replace('quill-editor-', ''); // Prevent re-initialization if (quillInstances[blockId]) { console.warn(`Quill instance for block ${blockId} already exists.`); return quillInstances[blockId]; // Return existing instance } try { const quill = new Quill(container, { theme: 'snow', // Use the 'snow' theme (matches the CSS) modules: { toolbar: [ [{ 'header': [1, 2, 3, 4, false] }], ['bold', 'italic', 'underline', 'strike'], // toggled buttons [{ 'color': [] }, { 'background': [] }], // dropdown with defaults from theme [{ 'list': 'ordered'}, { 'list': 'bullet' }], [{ 'script': 'sub'}, { 'script': 'super' }], // superscript/subscript [{ 'indent': '-1'}, { 'indent': '+1' }], // outdent/indent [{ 'direction': 'rtl' }], // text direction [{ 'align': [] }], ['link'], // Add link button // Removed 'image' and 'video' as they are handled per block ['blockquote', 'code-block'], ['clean'] // remove formatting button ] }, placeholder: 'Saisissez le contenu du bloc ici...', }); // Set initial content if provided. Use dangerouslyPasteHTML for raw HTML. if (initialContent && initialContent.trim() !== '


') { // Avoid pasting empty paragraph quill.clipboard.dangerouslyPasteHTML(0, initialContent); } // Update hidden input whenever text changes quill.on('text-change', (delta, oldDelta, source) => { // Get HTML content from Quill. Handles empty case too. let htmlContent = quill.root.innerHTML; // Quill often leaves an empty paragraph tag (


) when cleared. Treat it as empty. if (htmlContent === '


') { htmlContent = ''; } hiddenInput.value = htmlContent; }); quillInstances[blockId] = quill; // Store the instance console.log(`Quill initialized for block ${blockId}`); return quill; } catch (error) { console.error(`Error initializing Quill for block ${blockId}:`, error); container.innerHTML = `
Erreur d'initialisation de l'éditeur pour ce bloc. Contenu brut :
`; // Still update the hidden input with the original content for safety hiddenInput.value = initialContent; return null; } } // Setup controls for existing blocks on page load function setupExistingBlockControls() { const blockEditors = document.querySelectorAll('#blocks-container .block-editor'); blockEditors.forEach((blockEditor, index) => { const blockId = blockEditor.getAttribute('data-block-id'); if (!blockId) { console.error("Block editor found without data-block-id attribute.", blockEditor); return; // Skip this block } // Initialize Quill for this block const editorContainer = blockEditor.querySelector(`#quill-editor-${blockId}`); const hiddenInput = blockEditor.querySelector(`#block-${blockId}-content`); if (editorContainer && hiddenInput) { initializeQuillEditor(editorContainer, hiddenInput, hiddenInput.value); // Pass initial content from hidden input } else { console.error(`Could not find editor container or hidden input for block ${blockId}`); } // Setup Delete Button const deleteButton = blockEditor.querySelector('.delete-block-btn'); if (deleteButton) { deleteButton.addEventListener('click', function() { if (confirm('Êtes-vous sûr de vouloir supprimer ce bloc ? Cette action est irréversible.')) { // Clean up Quill instance before removing the element if (quillInstances[blockId]) { delete quillInstances[blockId]; // Remove reference console.log(`Quill instance for block ${blockId} cleaned up.`); } blockEditor.remove(); updateBlockOrder(); // Renumber remaining blocks } }); } else { console.warn(`Delete button not found for block ${blockId}`); } // Setup Image Selection Button const selectButton = blockEditor.querySelector('.select-image-btn'); if (selectButton) { selectButton.addEventListener('click', function() { const galleryModalElement = document.getElementById('image-gallery-modal'); if (galleryModalElement) { // Store the target block ID on the modal before showing galleryModalElement.setAttribute('data-target-block', blockId); // Use Bootstrap's JS to show the modal const galleryModal = bootstrap.Modal.getOrCreateInstance(galleryModalElement); galleryModal.show(); } else { alert("Erreur : La galerie d'images n'a pas pu être trouvée."); } }); } else { console.warn(`Select image button not found for block ${blockId}`); } // Setup Image Removal Button const removeButton = blockEditor.querySelector('.remove-image-btn'); if (removeButton) { removeButton.addEventListener('click', function() { const imageIdInput = blockEditor.querySelector('.block-image-id'); const imagePreview = blockEditor.querySelector('.image-preview'); if (imageIdInput) imageIdInput.value = ''; // Clear the hidden ID if (imagePreview) { imagePreview.src = ''; imagePreview.style.display = 'none'; // Hide preview imagePreview.alt = 'Preview'; } // Toggle button visibility this.style.display = 'none'; // Hide remove button const selectBtn = blockEditor.querySelector('.select-image-btn'); if(selectBtn) selectBtn.style.display = 'inline-block'; // Show select button // Hide position controls (handled by specific function in edit_texte.html) const event = new CustomEvent('imageRemoved', { bubbles: true, detail: { blockEditor: blockEditor } }); blockEditor.dispatchEvent(event); }); } else { console.warn(`Remove image button not found for block ${blockId}`); } // Setup Image Position Select (if needed, though primary logic might be in HTML script) const positionSelect = blockEditor.querySelector('.image-position-select'); if (positionSelect) { positionSelect.addEventListener('change', function() { // Optional: Add JS logic here if needed, e.g., apply preview classes console.log(`Block ${blockId} image position changed to: ${this.value}`); }); } }); updateBlockOrder(); // Ensure initial numbering is correct } // Add a new content block to the editor function addContentBlock(data = null) { const blocksContainer = document.getElementById('blocks-container'); if (!blocksContainer) return; const blockCount = blocksContainer.children.length; const newBlockIndex = blockCount + 1; const blockId = 'new-block-' + Date.now(); // Unique ID for new blocks // Default values if data is not provided const initialData = { id: blockId, // Use the generated temporary ID title: data?.title || '', content: data?.content || '', // Default empty content for Quill image: data?.image || null, // { id: ..., src: ..., alt: ... } image_position: data?.image_position || 'left' }; // Create block HTML using template literals const blockHtml = `

Bloc #${newBlockIndex}

${initialData.image?.alt || 'Prévisualisation'}
`; // Add the block HTML to the container blocksContainer.insertAdjacentHTML('beforeend', blockHtml); // Get the newly added block element const newBlockElement = blocksContainer.lastElementChild; // Initialize Quill for the new block const editorContainer = newBlockElement.querySelector(`#quill-editor-${initialData.id}`); const hiddenInput = newBlockElement.querySelector(`#block-${initialData.id}-content`); if(editorContainer && hiddenInput) { initializeQuillEditor(editorContainer, hiddenInput, initialData.content); } else { console.error(`Failed to find editor elements for new block ${initialData.id}`); } // Add event listeners for the new block's controls (delete, select image, remove image) // Delegate event listeners might be more efficient, but direct binding is simpler here // Delete button const deleteButton = newBlockElement.querySelector('.delete-block-btn'); if (deleteButton) { deleteButton.addEventListener('click', function() { if (confirm('Êtes-vous sûr de vouloir supprimer ce bloc ?')) { // Clean up Quill instance if (quillInstances[initialData.id]) { delete quillInstances[initialData.id]; console.log(`Quill instance for block ${initialData.id} cleaned up.`); } newBlockElement.remove(); updateBlockOrder(); } }); } // Select image button const selectButton = newBlockElement.querySelector('.select-image-btn'); if (selectButton) { selectButton.addEventListener('click', function() { const galleryModalElement = document.getElementById('image-gallery-modal'); if (galleryModalElement) { galleryModalElement.setAttribute('data-target-block', initialData.id); const galleryModal = bootstrap.Modal.getOrCreateInstance(galleryModalElement); galleryModal.show(); } else { alert("Erreur : La galerie d'images n'a pas pu être trouvée."); } }); } // Remove image button const removeButton = newBlockElement.querySelector('.remove-image-btn'); if (removeButton) { removeButton.addEventListener('click', function() { const imageIdInput = newBlockElement.querySelector('.block-image-id'); const imagePreview = newBlockElement.querySelector('.image-preview'); if (imageIdInput) imageIdInput.value = ''; if (imagePreview) { imagePreview.src = ''; imagePreview.style.display = 'none'; imagePreview.alt = 'Preview'; } this.style.display = 'none'; // Hide remove button const selectBtn = newBlockElement.querySelector('.select-image-btn'); if (selectBtn) selectBtn.style.display = 'inline-block'; // Show select button // Hide position controls const event = new CustomEvent('imageRemoved', { bubbles: true, detail: { blockEditor: newBlockElement } }); newBlockElement.dispatchEvent(event); }); } // Scroll to the new block newBlockElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); } // Update block order numbers in the UI titles function updateBlockOrder() { const blocks = document.querySelectorAll('#blocks-container .block-editor'); blocks.forEach((block, index) => { const titleEl = block.querySelector('.block-editor-title'); if (titleEl) { titleEl.textContent = `Bloc #${index + 1}`; } }); console.log("Block order updated."); } // Save content blocks by gathering data and submitting the form function saveContentBlocks() { const blocksContainer = document.getElementById('blocks-container'); const blocksDataInput = document.getElementById('blocks-data'); const blocksForm = document.getElementById('blocks-form'); if (!blocksContainer || !blocksDataInput || !blocksForm) { console.error("Cannot save blocks: Missing container, data input, or form element."); alert("Erreur : Impossible de sauvegarder les blocs. Éléments de formulaire manquants."); return; } const blockElements = blocksContainer.querySelectorAll('.block-editor'); const blocksData = []; console.log(`Gathering data for ${blockElements.length} blocks...`); blockElements.forEach((blockElement, index) => { const blockId = blockElement.getAttribute('data-block-id'); const titleInput = blockElement.querySelector('.block-title'); const hiddenContentInput = blockElement.querySelector('.block-content-hidden'); const imageIdInput = blockElement.querySelector('.block-image-id'); const imagePositionSelect = blockElement.querySelector('.image-position-select'); let content = ''; // Crucially, get the latest content from Quill instance if available if (quillInstances[blockId]) { content = quillInstances[blockId].root.innerHTML; // Treat empty Quill editor as empty string if (content === '


') { content = ''; } // Ensure hidden input is also synced before submission (belt and suspenders) if (hiddenContentInput) hiddenContentInput.value = content; } else if (hiddenContentInput) { // Fallback to hidden input if Quill instance is missing (error condition) content = hiddenContentInput.value; console.warn(`Quill instance missing for block ${blockId}. Using hidden input value.`); } else { console.error(`Cannot get content for block ${blockId}: No Quill instance and no hidden input.`); } const blockDataItem = { // Include the original block ID if it's not a new one, // otherwise, the backend should know how to handle blocks without a numeric ID id: blockId.startsWith('new-block-') ? null : blockId, temp_id: blockId.startsWith('new-block-') ? blockId : null, // Send temp ID for new blocks if needed title: titleInput ? titleInput.value.trim() : '', content: content, image_id: imageIdInput ? (imageIdInput.value || null) : null, // Send null if empty image_position: imagePositionSelect ? imagePositionSelect.value : 'left', order: index // Set the order based on current position }; blocksData.push(blockDataItem); }); // Set the JSON data in the hidden input field blocksDataInput.value = JSON.stringify(blocksData); console.log("Blocks data prepared:", blocksDataInput.value); // Submit the form // Add loading indicator? const saveButton = document.getElementById('save-blocks-button'); if(saveButton) { saveButton.disabled = true; saveButton.innerHTML = ' Sauvegarde...'; } blocksForm.submit(); } // --- Image Management Functions --- // Setup image uploader preview (for the form in edit_texte.html) function setupImageUploader() { const imageFileInput = document.getElementById('image-file'); // ID from edit_texte.html form const imagePreview = document.getElementById('upload-image-preview'); // ID from edit_texte.html form if (imageFileInput && imagePreview) { imageFileInput.addEventListener('change', function() { if (this.files && this.files[0]) { const reader = new FileReader(); reader.onload = function(e) { imagePreview.src = e.target.result; imagePreview.style.display = 'block'; // Show preview }; reader.readAsDataURL(this.files[0]); } else { // Clear preview if no file selected imagePreview.src = '#'; imagePreview.style.display = 'none'; } }); } } // Setup image gallery modal interaction function setupImageGallery() { const galleryModalElement = document.getElementById('image-gallery-modal'); if (!galleryModalElement) return; const galleryContent = galleryModalElement.querySelector('#image-gallery-content'); // Container for images // Use event delegation on the modal body for image clicks galleryModalElement.addEventListener('click', function(event) { const galleryItem = event.target.closest('.gallery-item'); if (!galleryItem) return; // Clicked outside an item const imageId = galleryItem.getAttribute('data-image-id'); const imageElement = galleryItem.querySelector('img'); const imageSrc = imageElement ? imageElement.src : ''; const imageAlt = imageElement ? imageElement.alt : ''; // Get the ID of the block that opened the modal const targetBlockId = galleryModalElement.getAttribute('data-target-block'); if (!targetBlockId) { console.error("Target block ID not found on modal."); return; } // Find the target block editor element const blockEditor = document.querySelector(`.block-editor[data-block-id="${targetBlockId}"]`); if (blockEditor && imageId && imageSrc) { // Update the block's image ID input const imageIdInput = blockEditor.querySelector('.block-image-id'); if (imageIdInput) imageIdInput.value = imageId; // Update the block's image preview const imagePreview = blockEditor.querySelector('.image-preview'); if (imagePreview) { imagePreview.src = imageSrc; imagePreview.alt = imageAlt || 'Prévisualisation'; imagePreview.style.display = 'block'; // Ensure preview is visible } // Toggle visibility of select/remove buttons const selectButton = blockEditor.querySelector('.select-image-btn'); const removeButton = blockEditor.querySelector('.remove-image-btn'); if (selectButton) selectButton.style.display = 'none'; if (removeButton) removeButton.style.display = 'inline-block'; // Show position controls (handled by specific function in edit_texte.html) const customEvent = new CustomEvent('imageSelected', { bubbles: true, detail: { blockEditor: blockEditor } }); blockEditor.dispatchEvent(customEvent); // Close the modal const galleryModalInstance = bootstrap.Modal.getInstance(galleryModalElement); if (galleryModalInstance) { galleryModalInstance.hide(); } } else { console.error(`Failed to update block ${targetBlockId}. Block editor or image data missing.`); } }); // Optional: Add logic here to dynamically load gallery images via AJAX if needed // E.g., on modal 'show.bs.modal' event }