Docfile commited on
Commit
7b84ca6
·
verified ·
1 Parent(s): 50b44f1

Update api/static/js/admin.js

Browse files
Files changed (1) hide show
  1. api/static/js/admin.js +585 -438
api/static/js/admin.js CHANGED
@@ -1,24 +1,31 @@
1
  // Admin JavaScript for the backend management interface
2
 
 
 
 
3
  document.addEventListener('DOMContentLoaded', function() {
4
  // Initialize theme
5
  initTheme();
6
-
7
  // Setup dashboard functionality
8
  setupDashboardCards();
9
-
10
  // Setup admin forms
11
  setupMatiereForm();
12
  setupSousCategorieForm();
13
- setupTexteForm();
14
-
15
- // Setup content block editor
16
- setupContentBlockEditor();
17
-
18
- // Setup image management
19
- setupImageUploader();
20
- setupImageGallery();
21
-
 
 
 
 
22
  // Setup theme toggle
23
  setupThemeToggle();
24
  });
@@ -27,8 +34,6 @@ document.addEventListener('DOMContentLoaded', function() {
27
  function initTheme() {
28
  const userPreference = localStorage.getItem('theme') || 'light';
29
  document.documentElement.setAttribute('data-theme', userPreference);
30
-
31
- // Update theme icon
32
  updateThemeIcon(userPreference);
33
  }
34
 
@@ -36,22 +41,14 @@ function initTheme() {
36
  function setupThemeToggle() {
37
  const themeToggle = document.getElementById('theme-toggle');
38
  if (!themeToggle) return;
39
-
40
  themeToggle.addEventListener('click', function() {
41
  const currentTheme = document.documentElement.getAttribute('data-theme');
42
  const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
43
-
44
- // Update theme attribute
45
  document.documentElement.setAttribute('data-theme', newTheme);
46
-
47
- // Save preference to localStorage
48
  localStorage.setItem('theme', newTheme);
49
-
50
- // Update icon
51
  updateThemeIcon(newTheme);
52
-
53
- // Send theme preference to server
54
- saveThemePreference(newTheme);
55
  });
56
  }
57
 
@@ -59,46 +56,53 @@ function setupThemeToggle() {
59
  function updateThemeIcon(theme) {
60
  const themeToggle = document.getElementById('theme-toggle');
61
  if (!themeToggle) return;
62
-
63
- // Update icon based on theme
 
64
  if (theme === 'dark') {
65
- themeToggle.innerHTML = '<i class="fas fa-sun"></i>';
 
66
  themeToggle.setAttribute('title', 'Activer le mode clair');
67
  } else {
68
- themeToggle.innerHTML = '<i class="fas fa-moon"></i>';
 
69
  themeToggle.setAttribute('title', 'Activer le mode sombre');
70
  }
71
  }
72
 
73
- // Save theme preference to server
74
  function saveThemePreference(theme) {
75
- const formData = new FormData();
76
- formData.append('theme', theme);
77
-
78
- fetch('/set_theme', {
79
  method: 'POST',
80
- body: formData
 
 
 
 
 
81
  })
82
  .then(response => response.json())
83
  .then(data => {
84
- console.log('Theme preference saved:', data);
 
85
  })
86
  .catch(error => {
87
  console.error('Error saving theme preference:', error);
88
  });
 
89
  }
90
 
91
  // Setup dashboard cards with hover effects
92
  function setupDashboardCards() {
93
  const dashboardCards = document.querySelectorAll('.admin-card');
94
-
95
  dashboardCards.forEach(card => {
96
  card.addEventListener('mouseenter', function() {
97
  this.style.transform = 'translateY(-5px)';
98
  this.style.boxShadow = 'var(--hover-shadow)';
99
- this.style.transition = 'transform 0.3s ease, box-shadow 0.3s ease';
100
  });
101
-
102
  card.addEventListener('mouseleave', function() {
103
  this.style.transform = 'translateY(0)';
104
  this.style.boxShadow = 'var(--shadow)';
@@ -106,571 +110,714 @@ function setupDashboardCards() {
106
  });
107
  }
108
 
109
- // Setup matiere form functionality
110
  function setupMatiereForm() {
111
- // Show edit form when edit button is clicked
112
  const editButtons = document.querySelectorAll('.edit-matiere-btn');
113
  editButtons.forEach(button => {
114
  button.addEventListener('click', function() {
115
  const matiereId = this.getAttribute('data-id');
116
  const matiereName = this.getAttribute('data-name');
117
  const matiereColor = this.getAttribute('data-color');
118
-
119
  const editForm = document.getElementById('edit-matiere-form');
120
- if (editForm) {
121
- const idInput = editForm.querySelector('input[name="matiere_id"]');
122
- const nameInput = editForm.querySelector('input[name="nom"]');
 
 
 
123
  const colorInput = editForm.querySelector('input[name="color_code"]');
124
-
125
- idInput.value = matiereId;
126
- nameInput.value = matiereName;
127
  colorInput.value = matiereColor;
128
-
129
- // Show the edit form
130
- document.getElementById('add-matiere-section').classList.add('d-none');
131
- document.getElementById('edit-matiere-section').classList.remove('d-none');
132
-
133
- // Scroll to edit form
134
- editForm.scrollIntoView({ behavior: 'smooth' });
135
  }
136
  });
137
  });
138
-
139
- // Cancel edit button
140
  const cancelEditButton = document.getElementById('cancel-edit-matiere');
141
  if (cancelEditButton) {
142
  cancelEditButton.addEventListener('click', function(e) {
143
  e.preventDefault();
144
- document.getElementById('add-matiere-section').classList.remove('d-none');
145
- document.getElementById('edit-matiere-section').classList.add('d-none');
146
  });
147
  }
148
-
149
- // Color picker preview
150
  const colorPickers = document.querySelectorAll('input[type="color"]');
151
  colorPickers.forEach(picker => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  picker.addEventListener('input', function() {
153
- // Find adjacent preview element or create one
154
- let preview = this.nextElementSibling;
155
- if (!preview || !preview.classList.contains('color-preview')) {
156
- preview = document.createElement('span');
157
- preview.className = 'color-preview';
158
- preview.style.display = 'inline-block';
159
- preview.style.width = '24px';
160
- preview.style.height = '24px';
161
- preview.style.borderRadius = '50%';
162
- preview.style.marginLeft = '10px';
163
- this.parentNode.insertBefore(preview, this.nextSibling);
164
- }
165
-
166
  preview.style.backgroundColor = this.value;
167
  });
168
-
169
- // Trigger once to initialize
170
- const event = new Event('input');
171
- picker.dispatchEvent(event);
172
  });
173
  }
174
 
175
- // Setup sous categorie form functionality
176
  function setupSousCategorieForm() {
177
- // Show edit form when edit button is clicked
178
  const editButtons = document.querySelectorAll('.edit-sous-categorie-btn');
179
  editButtons.forEach(button => {
180
  button.addEventListener('click', function() {
181
  const sousCategorieId = this.getAttribute('data-id');
182
  const sousCategorieName = this.getAttribute('data-name');
183
  const matiereId = this.getAttribute('data-matiere-id');
184
-
185
  const editForm = document.getElementById('edit-sous-categorie-form');
186
- if (editForm) {
187
- const idInput = editForm.querySelector('input[name="sous_categorie_id"]');
188
- const nameInput = editForm.querySelector('input[name="nom"]');
189
- const matiereSelect = editForm.querySelector('select[name="matiere_id"]');
190
-
191
- idInput.value = sousCategorieId;
192
- nameInput.value = sousCategorieName;
193
- matiereSelect.value = matiereId;
194
-
195
- // Show the edit form
196
- document.getElementById('add-sous-categorie-section').classList.add('d-none');
197
- document.getElementById('edit-sous-categorie-section').classList.remove('d-none');
198
-
199
- // Scroll to edit form
200
- editForm.scrollIntoView({ behavior: 'smooth' });
201
  }
202
  });
203
  });
204
-
205
- // Cancel edit button
206
  const cancelEditButton = document.getElementById('cancel-edit-sous-categorie');
207
  if (cancelEditButton) {
208
  cancelEditButton.addEventListener('click', function(e) {
209
  e.preventDefault();
210
- document.getElementById('add-sous-categorie-section').classList.remove('d-none');
211
- document.getElementById('edit-sous-categorie-section').classList.add('d-none');
212
  });
213
  }
214
-
215
- // Matiere select filter
216
  const matiereFilterSelect = document.getElementById('matiere-filter');
217
  if (matiereFilterSelect) {
218
  matiereFilterSelect.addEventListener('change', function() {
219
  const selectedMatiereId = this.value;
220
- const sousCategorieRows = document.querySelectorAll('.sous-categorie-row');
221
-
 
 
222
  sousCategorieRows.forEach(row => {
223
- if (selectedMatiereId === '' || row.getAttribute('data-matiere-id') === selectedMatiereId) {
224
- row.style.display = '';
225
- } else {
226
- row.style.display = 'none';
227
- }
 
 
 
 
228
  });
229
  });
 
 
230
  }
231
  }
232
 
233
- // Setup texte form functionality
234
  function setupTexteForm() {
235
- // Matiere select change - populate sous-categories
236
- const matiereSelect = document.getElementById('matiere-select');
237
- if (matiereSelect) {
 
 
238
  matiereSelect.addEventListener('change', function() {
239
  const matiereId = this.value;
240
- const sousCategorieSelect = document.getElementById('sous-categorie-select');
241
-
242
- if (matiereId && sousCategorieSelect) {
243
- // Clear current options
244
- sousCategorieSelect.innerHTML = '<option value="">Sélectionnez une sous-catégorie</option>';
245
-
246
  // Fetch sous-categories for the selected matiere
247
- fetch(`/get_sous_categories/${matiereId}`)
248
- .then(response => response.json())
 
 
 
 
 
249
  .then(data => {
250
- data.forEach(sousCategorie => {
251
- const option = document.createElement('option');
252
- option.value = sousCategorie.id;
253
- option.textContent = sousCategorie.nom;
254
- sousCategorieSelect.appendChild(option);
255
- });
 
 
 
 
 
 
 
 
 
256
  })
257
  .catch(error => {
258
  console.error('Error loading sous-categories:', error);
 
 
 
 
 
259
  });
260
  }
261
  });
 
 
 
 
262
  }
263
  }
264
 
265
- // Setup content block editor
 
266
  function setupContentBlockEditor() {
267
  const blocksContainer = document.getElementById('blocks-container');
268
  const addBlockButton = document.getElementById('add-block-button');
269
  const saveBlocksButton = document.getElementById('save-blocks-button');
270
-
271
- if (!blocksContainer) return;
272
-
273
- // Add new block
274
- if (addBlockButton) {
275
- addBlockButton.addEventListener('click', function() {
276
- addContentBlock();
277
- });
278
- }
279
-
280
- // Make blocks sortable
281
  if (window.Sortable) {
282
  new Sortable(blocksContainer, {
283
  animation: 150,
284
- handle: '.block-handle',
285
- ghostClass: 'block-ghost',
286
- onEnd: function() {
287
- // Update order numbers
288
- updateBlockOrder();
289
  }
290
  });
 
 
 
 
 
 
 
 
 
291
  }
292
-
293
- // Save blocks
294
  if (saveBlocksButton) {
295
  saveBlocksButton.addEventListener('click', function() {
296
- saveContentBlocks();
297
  });
298
  }
299
-
300
- // Add event listeners for existing blocks
301
  setupExistingBlockControls();
302
  }
303
 
304
- // Setup controls for existing blocks
305
- function setupExistingBlockControls() {
306
- // Setup delete buttons
307
- const deleteButtons = document.querySelectorAll('.delete-block-btn');
308
- deleteButtons.forEach(button => {
309
- button.addEventListener('click', function() {
310
- if (confirm('Êtes-vous sûr de vouloir supprimer ce bloc ?')) {
311
- const blockEditor = this.closest('.block-editor');
312
- blockEditor.remove();
313
- updateBlockOrder();
314
- }
315
- });
316
- });
317
-
318
- // Setup image position selects
319
- const positionSelects = document.querySelectorAll('.image-position-select');
320
- positionSelects.forEach(select => {
321
- select.addEventListener('change', function() {
322
- updateBlockImagePreview(this.closest('.block-editor'));
323
- });
324
- });
325
-
326
- // Setup image selection buttons
327
- const imageSelectButtons = document.querySelectorAll('.select-image-btn');
328
- imageSelectButtons.forEach(button => {
329
- button.addEventListener('click', function() {
330
- const blockEditor = this.closest('.block-editor');
331
- const galleryModal = document.getElementById('image-gallery-modal');
332
-
333
- if (galleryModal) {
334
- // Set current block ID as data attribute for the modal
335
- galleryModal.setAttribute('data-target-block', blockEditor.getAttribute('data-block-id'));
336
-
337
- // Show the modal
338
- const modal = new bootstrap.Modal(galleryModal);
339
- modal.show();
340
- }
341
  });
342
- });
343
-
344
- // Setup image remove buttons
345
- const removeImageButtons = document.querySelectorAll('.remove-image-btn');
346
- removeImageButtons.forEach(button => {
347
- button.addEventListener('click', function() {
348
- const blockEditor = this.closest('.block-editor');
349
- const imageIdInput = blockEditor.querySelector('.block-image-id');
350
- const imagePreview = blockEditor.querySelector('.image-preview');
351
-
352
- if (imageIdInput) {
353
- imageIdInput.value = '';
354
- }
355
-
356
- if (imagePreview) {
357
- imagePreview.src = '';
358
- imagePreview.style.display = 'none';
359
- }
360
-
361
- // Hide remove button
362
- this.style.display = 'none';
363
-
364
- // Show select button
365
- const selectButton = blockEditor.querySelector('.select-image-btn');
366
- if (selectButton) {
367
- selectButton.style.display = 'inline-block';
368
  }
 
369
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  });
 
 
371
  }
372
 
 
373
  // Add a new content block to the editor
374
  function addContentBlock(data = null) {
375
  const blocksContainer = document.getElementById('blocks-container');
376
  if (!blocksContainer) return;
377
-
378
- // Generate a unique ID for the block
379
- const blockId = 'block-' + Date.now();
380
-
381
- // Create block HTML
 
 
 
 
 
 
 
 
 
 
382
  const blockHtml = `
383
- <div class="block-editor" data-block-id="${blockId}">
384
  <div class="block-editor-header">
385
  <div class="d-flex align-items-center">
386
  <span class="block-handle"><i class="fas fa-grip-vertical"></i></span>
387
- <h3 class="block-editor-title">Bloc #${blocksContainer.children.length + 1}</h3>
388
  </div>
389
  <div class="block-editor-actions">
390
- <button type="button" class="btn btn-danger btn-sm delete-block-btn">
391
  <i class="fas fa-trash"></i>
392
  </button>
393
  </div>
394
  </div>
395
- <div class="form-group">
396
- <label for="${blockId}-title">Titre du bloc (optionnel)</label>
397
- <input type="text" class="form-control block-title" id="${blockId}-title" value="${data?.title || ''}">
398
  </div>
399
- <div class="form-group">
400
- <label for="${blockId}-content">Contenu du bloc</label>
401
- <textarea class="form-control block-content" id="${blockId}-content" rows="5">${data?.content || ''}</textarea>
 
 
 
402
  </div>
403
  <div class="form-group">
404
- <label>Image</label>
405
  <div class="d-flex align-items-center mb-2">
406
- <button type="button" class="btn btn-primary btn-sm select-image-btn" style="${data?.image ? 'display:none;' : ''}">
407
  <i class="fas fa-image"></i> Sélectionner une image
408
  </button>
409
- <button type="button" class="btn btn-warning btn-sm remove-image-btn ml-2" style="${data?.image ? '' : 'display:none;'}">
410
  <i class="fas fa-times"></i> Retirer l'image
411
  </button>
412
  </div>
413
- <input type="hidden" class="block-image-id" value="${data?.image?.id || ''}">
414
- <img src="${data?.image?.src || ''}" alt="Preview" class="image-preview mb-2" style="${data?.image ? '' : 'display:none;'}">
415
- <div class="form-group">
416
- <label for="${blockId}-image-position">Position de l'image</label>
417
- <select class="form-control image-position-select" id="${blockId}-image-position">
418
- <option value="left" ${data?.image_position === 'left' ? 'selected' : ''}>Gauche</option>
419
- <option value="right" ${data?.image_position === 'right' ? 'selected' : ''}>Droite</option>
420
- <option value="top" ${data?.image_position === 'top' ? 'selected' : ''}>Haut</option>
 
 
 
 
421
  </select>
422
  </div>
423
  </div>
424
  </div>
425
  `;
426
-
427
- // Add the block to the container
428
  blocksContainer.insertAdjacentHTML('beforeend', blockHtml);
429
-
430
- // Setup event listeners for the new block
431
- const newBlock = blocksContainer.lastElementChild;
432
-
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  // Delete button
434
- const deleteButton = newBlock.querySelector('.delete-block-btn');
435
  if (deleteButton) {
436
  deleteButton.addEventListener('click', function() {
437
- if (confirm('Êtes-vous sûr de vouloir supprimer ce bloc ?')) {
438
- newBlock.remove();
439
- updateBlockOrder();
440
- }
441
- });
442
- }
443
-
444
- // Image position select
445
- const positionSelect = newBlock.querySelector('.image-position-select');
446
- if (positionSelect) {
447
- positionSelect.addEventListener('change', function() {
448
- updateBlockImagePreview(newBlock);
449
- });
450
- }
451
-
452
- // Image selection button
453
- const imageSelectButton = newBlock.querySelector('.select-image-btn');
454
- if (imageSelectButton) {
455
- imageSelectButton.addEventListener('click', function() {
456
- const galleryModal = document.getElementById('image-gallery-modal');
457
-
458
- if (galleryModal) {
459
- // Set current block ID as data attribute for the modal
460
- galleryModal.setAttribute('data-target-block', blockId);
461
-
462
- // Show the modal
463
- const modal = new bootstrap.Modal(galleryModal);
464
- modal.show();
465
- }
466
  });
467
  }
468
-
469
- // Image remove button
470
- const removeImageButton = newBlock.querySelector('.remove-image-btn');
471
- if (removeImageButton) {
472
- removeImageButton.addEventListener('click', function() {
473
- const imageIdInput = newBlock.querySelector('.block-image-id');
474
- const imagePreview = newBlock.querySelector('.image-preview');
475
-
476
- if (imageIdInput) {
477
- imageIdInput.value = '';
478
- }
479
-
480
- if (imagePreview) {
481
- imagePreview.src = '';
482
- imagePreview.style.display = 'none';
483
- }
484
-
485
- // Hide remove button
486
- removeImageButton.style.display = 'none';
487
-
488
- // Show select button
489
- if (imageSelectButton) {
490
- imageSelectButton.style.display = 'inline-block';
491
- }
492
  });
493
  }
494
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
  // Scroll to the new block
496
- newBlock.scrollIntoView({ behavior: 'smooth' });
497
  }
498
 
499
- // Update block order numbers in the UI
 
500
  function updateBlockOrder() {
501
- const blocks = document.querySelectorAll('.block-editor');
502
  blocks.forEach((block, index) => {
503
  const titleEl = block.querySelector('.block-editor-title');
504
  if (titleEl) {
505
  titleEl.textContent = `Bloc #${index + 1}`;
506
  }
507
  });
 
508
  }
509
 
510
- // Update image preview based on position
511
- function updateBlockImagePreview(blockEditor) {
512
- // This function would apply CSS classes to show how the image position
513
- // will look in the frontend
514
- const positionSelect = blockEditor.querySelector('.image-position-select');
515
- const imagePreview = blockEditor.querySelector('.image-preview');
516
-
517
- if (!positionSelect || !imagePreview || imagePreview.style.display === 'none') {
518
- return;
519
- }
520
-
521
- const position = positionSelect.value;
522
-
523
- // Remove existing position classes
524
- imagePreview.classList.remove('position-left', 'position-right', 'position-top');
525
-
526
- // Add the selected position class
527
- imagePreview.classList.add(`position-${position}`);
528
-
529
- // Apply some simple styling to demonstrate the position
530
- switch (position) {
531
- case 'left':
532
- imagePreview.style.float = 'left';
533
- imagePreview.style.marginRight = '15px';
534
- imagePreview.style.marginBottom = '10px';
535
- imagePreview.style.width = '30%';
536
- break;
537
- case 'right':
538
- imagePreview.style.float = 'right';
539
- imagePreview.style.marginLeft = '15px';
540
- imagePreview.style.marginBottom = '10px';
541
- imagePreview.style.width = '30%';
542
- break;
543
- case 'top':
544
- imagePreview.style.float = 'none';
545
- imagePreview.style.marginRight = '0';
546
- imagePreview.style.marginLeft = '0';
547
- imagePreview.style.marginBottom = '15px';
548
- imagePreview.style.width = '100%';
549
- break;
550
- }
551
- }
552
-
553
- // Save content blocks
554
  function saveContentBlocks() {
555
  const blocksContainer = document.getElementById('blocks-container');
556
  const blocksDataInput = document.getElementById('blocks-data');
557
-
558
- if (!blocksContainer || !blocksDataInput) return;
559
-
560
- const blocks = blocksContainer.querySelectorAll('.block-editor');
 
 
 
 
 
561
  const blocksData = [];
562
-
563
- blocks.forEach((block, index) => {
564
- const blockId = block.getAttribute('data-block-id');
565
- const title = block.querySelector('.block-title').value;
566
- const content = block.querySelector('.block-content').value;
567
- const imageId = block.querySelector('.block-image-id').value;
568
- const imagePosition = block.querySelector('.image-position-select').value;
569
-
570
- blocksData.push({
571
- id: blockId,
572
- title: title,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
  content: content,
574
- image_id: imageId,
575
- image_position: imagePosition,
576
- order: index
577
- });
 
578
  });
579
-
580
- // Set the blocks data as JSON in the hidden input
581
  blocksDataInput.value = JSON.stringify(blocksData);
582
-
 
 
583
  // Submit the form
584
- const form = document.getElementById('blocks-form');
585
- if (form) {
586
- form.submit();
 
 
587
  }
 
 
588
  }
589
 
590
- // Setup image uploader
 
 
591
  function setupImageUploader() {
592
- const imageUploadForm = document.getElementById('image-upload-form');
593
- const imageFileInput = document.getElementById('image-file');
594
- const imagePreview = document.getElementById('upload-image-preview');
595
-
596
  if (imageFileInput && imagePreview) {
597
  imageFileInput.addEventListener('change', function() {
598
  if (this.files && this.files[0]) {
599
  const reader = new FileReader();
600
-
601
  reader.onload = function(e) {
602
  imagePreview.src = e.target.result;
603
- imagePreview.style.display = 'block';
604
  };
605
-
606
  reader.readAsDataURL(this.files[0]);
607
- }
608
- });
609
- }
610
-
611
- if (imageUploadForm) {
612
- imageUploadForm.addEventListener('submit', function(e) {
613
- const fileInput = this.querySelector('#image-file');
614
-
615
- if (!fileInput.files || fileInput.files.length === 0) {
616
- e.preventDefault();
617
- alert('Veuillez sélectionner une image.');
618
  }
619
  });
620
  }
621
  }
622
 
623
- // Setup image gallery
624
  function setupImageGallery() {
625
- // Handle image selection from gallery
626
- const galleryItems = document.querySelectorAll('.gallery-item');
627
-
628
- galleryItems.forEach(item => {
629
- item.addEventListener('click', function() {
630
- const imageId = this.getAttribute('data-image-id');
631
- const imageSrc = this.querySelector('img').src;
632
- const galleryModal = document.getElementById('image-gallery-modal');
633
-
634
- if (galleryModal) {
635
- const targetBlockId = galleryModal.getAttribute('data-target-block');
636
- const blockEditor = document.querySelector(`.block-editor[data-block-id="${targetBlockId}"]`);
637
-
638
- if (blockEditor) {
639
- // Update the image ID input
640
- const imageIdInput = blockEditor.querySelector('.block-image-id');
641
- if (imageIdInput) {
642
- imageIdInput.value = imageId;
643
- }
644
-
645
- // Update the image preview
646
- const imagePreview = blockEditor.querySelector('.image-preview');
647
- if (imagePreview) {
648
- imagePreview.src = imageSrc;
649
- imagePreview.style.display = 'block';
650
- }
651
-
652
- // Hide select button and show remove button
653
- const selectButton = blockEditor.querySelector('.select-image-btn');
654
- const removeButton = blockEditor.querySelector('.remove-image-btn');
655
-
656
- if (selectButton) {
657
- selectButton.style.display = 'none';
658
- }
659
-
660
- if (removeButton) {
661
- removeButton.style.display = 'inline-block';
662
- }
663
-
664
- // Update image preview position
665
- updateBlockImagePreview(blockEditor);
666
- }
667
-
668
- // Close the modal
669
- const modal = bootstrap.Modal.getInstance(galleryModal);
670
- if (modal) {
671
- modal.hide();
672
- }
673
  }
674
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
675
  });
676
- }
 
 
 
 
1
  // Admin JavaScript for the backend management interface
2
 
3
+ // Global object to hold Quill instances
4
+ let quillInstances = {};
5
+
6
  document.addEventListener('DOMContentLoaded', function() {
7
  // Initialize theme
8
  initTheme();
9
+
10
  // Setup dashboard functionality
11
  setupDashboardCards();
12
+
13
  // Setup admin forms
14
  setupMatiereForm();
15
  setupSousCategorieForm();
16
+ setupTexteForm(); // Basic setup (e.g., category dropdowns if needed)
17
+
18
+ // Setup content block editor specifically for edit_texte.html
19
+ if (document.getElementById('blocks-container')) {
20
+ setupContentBlockEditor();
21
+ }
22
+
23
+ // Setup image management (gallery interaction, maybe general upload elsewhere)
24
+ setupImageGallery(); // Handles selecting from the modal
25
+ // Note: The specific AJAX upload logic is now mainly in edit_texte.html's script block
26
+ // but we keep setupImageUploader for potential preview logic or other upload forms.
27
+ setupImageUploader(); // Sets up preview for the upload form
28
+
29
  // Setup theme toggle
30
  setupThemeToggle();
31
  });
 
34
  function initTheme() {
35
  const userPreference = localStorage.getItem('theme') || 'light';
36
  document.documentElement.setAttribute('data-theme', userPreference);
 
 
37
  updateThemeIcon(userPreference);
38
  }
39
 
 
41
  function setupThemeToggle() {
42
  const themeToggle = document.getElementById('theme-toggle');
43
  if (!themeToggle) return;
44
+
45
  themeToggle.addEventListener('click', function() {
46
  const currentTheme = document.documentElement.getAttribute('data-theme');
47
  const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
 
 
48
  document.documentElement.setAttribute('data-theme', newTheme);
 
 
49
  localStorage.setItem('theme', newTheme);
 
 
50
  updateThemeIcon(newTheme);
51
+ saveThemePreference(newTheme); // Optional: Inform server
 
 
52
  });
53
  }
54
 
 
56
  function updateThemeIcon(theme) {
57
  const themeToggle = document.getElementById('theme-toggle');
58
  if (!themeToggle) return;
59
+ const icon = themeToggle.querySelector('i');
60
+ if (!icon) return;
61
+
62
  if (theme === 'dark') {
63
+ icon.classList.remove('fa-moon');
64
+ icon.classList.add('fa-sun');
65
  themeToggle.setAttribute('title', 'Activer le mode clair');
66
  } else {
67
+ icon.classList.remove('fa-sun');
68
+ icon.classList.add('fa-moon');
69
  themeToggle.setAttribute('title', 'Activer le mode sombre');
70
  }
71
  }
72
 
73
+ // Save theme preference to server (Optional)
74
  function saveThemePreference(theme) {
75
+ // Example using fetch API - adjust URL and method as needed
76
+ /*
77
+ fetch('/set_theme_preference', { // Replace with your actual endpoint
 
78
  method: 'POST',
79
+ headers: {
80
+ 'Content-Type': 'application/json',
81
+ // Add CSRF token header if needed
82
+ // 'X-CSRFToken': getCsrfToken()
83
+ },
84
+ body: JSON.stringify({ theme: theme })
85
  })
86
  .then(response => response.json())
87
  .then(data => {
88
+ if(data.success) console.log('Theme preference saved on server.');
89
+ else console.error('Failed to save theme preference on server.');
90
  })
91
  .catch(error => {
92
  console.error('Error saving theme preference:', error);
93
  });
94
+ */
95
  }
96
 
97
  // Setup dashboard cards with hover effects
98
  function setupDashboardCards() {
99
  const dashboardCards = document.querySelectorAll('.admin-card');
 
100
  dashboardCards.forEach(card => {
101
  card.addEventListener('mouseenter', function() {
102
  this.style.transform = 'translateY(-5px)';
103
  this.style.boxShadow = 'var(--hover-shadow)';
104
+ // Keep transition consistent (defined in CSS is better)
105
  });
 
106
  card.addEventListener('mouseleave', function() {
107
  this.style.transform = 'translateY(0)';
108
  this.style.boxShadow = 'var(--shadow)';
 
110
  });
111
  }
112
 
113
+ // Setup matiere form functionality (if on matiere page)
114
  function setupMatiereForm() {
 
115
  const editButtons = document.querySelectorAll('.edit-matiere-btn');
116
  editButtons.forEach(button => {
117
  button.addEventListener('click', function() {
118
  const matiereId = this.getAttribute('data-id');
119
  const matiereName = this.getAttribute('data-name');
120
  const matiereColor = this.getAttribute('data-color');
121
+
122
  const editForm = document.getElementById('edit-matiere-form');
123
+ const addSection = document.getElementById('add-matiere-section');
124
+ const editSection = document.getElementById('edit-matiere-section');
125
+
126
+ if (editForm && addSection && editSection) {
127
+ editForm.querySelector('input[name="matiere_id"]').value = matiereId;
128
+ editForm.querySelector('input[name="nom"]').value = matiereName;
129
  const colorInput = editForm.querySelector('input[name="color_code"]');
 
 
 
130
  colorInput.value = matiereColor;
131
+ // Trigger input event to update preview if color picker exists
132
+ if(colorInput) colorInput.dispatchEvent(new Event('input'));
133
+
134
+
135
+ addSection.classList.add('d-none');
136
+ editSection.classList.remove('d-none');
137
+ editSection.scrollIntoView({ behavior: 'smooth' });
138
  }
139
  });
140
  });
141
+
 
142
  const cancelEditButton = document.getElementById('cancel-edit-matiere');
143
  if (cancelEditButton) {
144
  cancelEditButton.addEventListener('click', function(e) {
145
  e.preventDefault();
146
+ document.getElementById('add-matiere-section')?.classList.remove('d-none');
147
+ document.getElementById('edit-matiere-section')?.classList.add('d-none');
148
  });
149
  }
150
+
151
+ // Color picker preview logic
152
  const colorPickers = document.querySelectorAll('input[type="color"]');
153
  colorPickers.forEach(picker => {
154
+ const previewId = picker.id + '-preview'; // Assuming a convention like id="color" and preview span id="color-preview"
155
+ let preview = document.getElementById(previewId);
156
+ if (!preview) { // Create preview dynamically if not present
157
+ preview = document.createElement('span');
158
+ preview.className = 'color-preview';
159
+ preview.id = previewId;
160
+ preview.style.display = 'inline-block';
161
+ preview.style.width = '24px';
162
+ preview.style.height = '24px';
163
+ preview.style.borderRadius = '4px';
164
+ preview.style.marginLeft = '10px';
165
+ preview.style.verticalAlign = 'middle';
166
+ preview.style.border = '1px solid var(--border-color)';
167
+ picker.parentNode.insertBefore(preview, picker.nextSibling);
168
+ }
169
+
170
  picker.addEventListener('input', function() {
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  preview.style.backgroundColor = this.value;
172
  });
173
+ // Initialize preview color
174
+ preview.style.backgroundColor = picker.value;
 
 
175
  });
176
  }
177
 
178
+ // Setup sous categorie form functionality (if on sous_categorie page)
179
  function setupSousCategorieForm() {
 
180
  const editButtons = document.querySelectorAll('.edit-sous-categorie-btn');
181
  editButtons.forEach(button => {
182
  button.addEventListener('click', function() {
183
  const sousCategorieId = this.getAttribute('data-id');
184
  const sousCategorieName = this.getAttribute('data-name');
185
  const matiereId = this.getAttribute('data-matiere-id');
186
+
187
  const editForm = document.getElementById('edit-sous-categorie-form');
188
+ const addSection = document.getElementById('add-sous-categorie-section');
189
+ const editSection = document.getElementById('edit-sous-categorie-section');
190
+
191
+ if (editForm && addSection && editSection) {
192
+ editForm.querySelector('input[name="sous_categorie_id"]').value = sousCategorieId;
193
+ editForm.querySelector('input[name="nom"]').value = sousCategorieName;
194
+ editForm.querySelector('select[name="matiere_id"]').value = matiereId;
195
+
196
+ addSection.classList.add('d-none');
197
+ editSection.classList.remove('d-none');
198
+ editSection.scrollIntoView({ behavior: 'smooth' });
 
 
 
 
199
  }
200
  });
201
  });
202
+
 
203
  const cancelEditButton = document.getElementById('cancel-edit-sous-categorie');
204
  if (cancelEditButton) {
205
  cancelEditButton.addEventListener('click', function(e) {
206
  e.preventDefault();
207
+ document.getElementById('add-sous-categorie-section')?.classList.remove('d-none');
208
+ document.getElementById('edit-sous-categorie-section')?.classList.add('d-none');
209
  });
210
  }
211
+
212
+ // Filter logic if present
213
  const matiereFilterSelect = document.getElementById('matiere-filter');
214
  if (matiereFilterSelect) {
215
  matiereFilterSelect.addEventListener('change', function() {
216
  const selectedMatiereId = this.value;
217
+ const tableBody = document.querySelector('.table tbody'); // Adjust selector if needed
218
+ if (!tableBody) return;
219
+ const sousCategorieRows = tableBody.querySelectorAll('tr'); // Assuming each row represents a sous-categorie
220
+
221
  sousCategorieRows.forEach(row => {
222
+ // Check if the row has a data attribute identifying the matiere
223
+ const rowMatiereId = row.getAttribute('data-matiere-id');
224
+ if (rowMatiereId) {
225
+ if (selectedMatiereId === '' || rowMatiereId === selectedMatiereId) {
226
+ row.style.display = ''; // Show row
227
+ } else {
228
+ row.style.display = 'none'; // Hide row
229
+ }
230
+ }
231
  });
232
  });
233
+ // Trigger change on load to apply initial filter if a value is pre-selected
234
+ matiereFilterSelect.dispatchEvent(new Event('change'));
235
  }
236
  }
237
 
238
+ // Setup general texte form functionality (e.g., dynamic dropdowns if creating new text)
239
  function setupTexteForm() {
240
+ // Example: If there's a matiere dropdown that populates sous-categories on a *creation* page
241
+ const matiereSelect = document.getElementById('matiere-select'); // Adjust ID if different
242
+ const sousCategorieSelect = document.getElementById('sous-categorie-select'); // Adjust ID
243
+
244
+ if (matiereSelect && sousCategorieSelect) {
245
  matiereSelect.addEventListener('change', function() {
246
  const matiereId = this.value;
247
+ // Clear current sous-categorie options (except the default placeholder)
248
+ sousCategorieSelect.innerHTML = '<option value="">Sélectionnez une sous-catégorie</option>';
249
+ sousCategorieSelect.disabled = true;
250
+
251
+ if (matiereId) {
 
252
  // Fetch sous-categories for the selected matiere
253
+ fetch(`/api/get_sous_categories/${matiereId}`) // Adjust API endpoint
254
+ .then(response => {
255
+ if (!response.ok) {
256
+ throw new Error('Network response was not ok');
257
+ }
258
+ return response.json();
259
+ })
260
  .then(data => {
261
+ if (data && data.length > 0) {
262
+ data.forEach(sc => {
263
+ const option = document.createElement('option');
264
+ option.value = sc.id;
265
+ option.textContent = sc.nom;
266
+ sousCategorieSelect.appendChild(option);
267
+ });
268
+ sousCategorieSelect.disabled = false;
269
+ } else {
270
+ // Handle case with no sous-categories
271
+ const option = document.createElement('option');
272
+ option.textContent = 'Aucune sous-catégorie trouvée';
273
+ option.disabled = true;
274
+ sousCategorieSelect.appendChild(option);
275
+ }
276
  })
277
  .catch(error => {
278
  console.error('Error loading sous-categories:', error);
279
+ // Optionally display an error message to the user
280
+ const option = document.createElement('option');
281
+ option.textContent = 'Erreur de chargement';
282
+ option.disabled = true;
283
+ sousCategorieSelect.appendChild(option);
284
  });
285
  }
286
  });
287
+ // Trigger change on load if a matiere is pre-selected
288
+ if(matiereSelect.value) {
289
+ matiereSelect.dispatchEvent(new Event('change'));
290
+ }
291
  }
292
  }
293
 
294
+ // --- Content Block Editor Specific Functions ---
295
+
296
  function setupContentBlockEditor() {
297
  const blocksContainer = document.getElementById('blocks-container');
298
  const addBlockButton = document.getElementById('add-block-button');
299
  const saveBlocksButton = document.getElementById('save-blocks-button');
300
+
301
+ if (!blocksContainer) return; // Only run if the container exists
302
+
303
+ // Initialize SortableJS for drag-and-drop
 
 
 
 
 
 
 
304
  if (window.Sortable) {
305
  new Sortable(blocksContainer, {
306
  animation: 150,
307
+ handle: '.block-handle', // Class for the drag handle element
308
+ ghostClass: 'block-ghost', // Class applied to the ghost element
309
+ onEnd: function(evt) {
310
+ updateBlockOrder(); // Renumber blocks after drag
 
311
  }
312
  });
313
+ } else {
314
+ console.warn('SortableJS not loaded. Drag and drop for blocks disabled.');
315
+ }
316
+
317
+ // Add new block button listener
318
+ if (addBlockButton) {
319
+ addBlockButton.addEventListener('click', function() {
320
+ addContentBlock(); // Add an empty block
321
+ });
322
  }
323
+
324
+ // Save blocks button listener
325
  if (saveBlocksButton) {
326
  saveBlocksButton.addEventListener('click', function() {
327
+ saveContentBlocks(); // Gather data and submit the form
328
  });
329
  }
330
+
331
+ // Setup controls and Quill for existing blocks on page load
332
  setupExistingBlockControls();
333
  }
334
 
335
+ // Initialize Quill Editor on a given container
336
+ function initializeQuillEditor(container, hiddenInput, initialContent = '') {
337
+ if (!container || !hiddenInput) {
338
+ console.error("Quill initialization failed: Container or hidden input missing.");
339
+ return;
340
+ }
341
+
342
+ const blockId = container.id.replace('quill-editor-', '');
343
+
344
+ // Prevent re-initialization
345
+ if (quillInstances[blockId]) {
346
+ console.warn(`Quill instance for block ${blockId} already exists.`);
347
+ return quillInstances[blockId]; // Return existing instance
348
+ }
349
+
350
+ try {
351
+ const quill = new Quill(container, {
352
+ theme: 'snow', // Use the 'snow' theme (matches the CSS)
353
+ modules: {
354
+ toolbar: [
355
+ [{ 'header': [1, 2, 3, 4, false] }],
356
+ ['bold', 'italic', 'underline', 'strike'], // toggled buttons
357
+ [{ 'color': [] }, { 'background': [] }], // dropdown with defaults from theme
358
+ [{ 'list': 'ordered'}, { 'list': 'bullet' }],
359
+ [{ 'script': 'sub'}, { 'script': 'super' }], // superscript/subscript
360
+ [{ 'indent': '-1'}, { 'indent': '+1' }], // outdent/indent
361
+ [{ 'direction': 'rtl' }], // text direction
362
+ [{ 'align': [] }],
363
+ ['link'], // Add link button // Removed 'image' and 'video' as they are handled per block
364
+ ['blockquote', 'code-block'],
365
+ ['clean'] // remove formatting button
366
+ ]
367
+ },
368
+ placeholder: 'Saisissez le contenu du bloc ici...',
 
 
 
369
  });
370
+
371
+ // Set initial content if provided. Use dangerouslyPasteHTML for raw HTML.
372
+ if (initialContent && initialContent.trim() !== '<p><br></p>') { // Avoid pasting empty paragraph
373
+ quill.clipboard.dangerouslyPasteHTML(0, initialContent);
374
+ }
375
+
376
+ // Update hidden input whenever text changes
377
+ quill.on('text-change', (delta, oldDelta, source) => {
378
+ // Get HTML content from Quill. Handles empty case too.
379
+ let htmlContent = quill.root.innerHTML;
380
+ // Quill often leaves an empty paragraph tag (<p><br></p>) when cleared. Treat it as empty.
381
+ if (htmlContent === '<p><br></p>') {
382
+ htmlContent = '';
 
 
 
 
 
 
 
 
 
 
 
 
 
383
  }
384
+ hiddenInput.value = htmlContent;
385
  });
386
+
387
+ quillInstances[blockId] = quill; // Store the instance
388
+ console.log(`Quill initialized for block ${blockId}`);
389
+ return quill;
390
+
391
+ } catch (error) {
392
+ console.error(`Error initializing Quill for block ${blockId}:`, error);
393
+ container.innerHTML = `<div class="alert alert-danger">Erreur d'initialisation de l'éditeur pour ce bloc. Contenu brut :</div><textarea class="form-control" rows="3" disabled>${initialContent}</textarea>`;
394
+ // Still update the hidden input with the original content for safety
395
+ hiddenInput.value = initialContent;
396
+ return null;
397
+ }
398
+ }
399
+
400
+
401
+ // Setup controls for existing blocks on page load
402
+ function setupExistingBlockControls() {
403
+ const blockEditors = document.querySelectorAll('#blocks-container .block-editor');
404
+
405
+ blockEditors.forEach((blockEditor, index) => {
406
+ const blockId = blockEditor.getAttribute('data-block-id');
407
+ if (!blockId) {
408
+ console.error("Block editor found without data-block-id attribute.", blockEditor);
409
+ return; // Skip this block
410
+ }
411
+
412
+ // Initialize Quill for this block
413
+ const editorContainer = blockEditor.querySelector(`#quill-editor-${blockId}`);
414
+ const hiddenInput = blockEditor.querySelector(`#block-${blockId}-content`);
415
+ if (editorContainer && hiddenInput) {
416
+ initializeQuillEditor(editorContainer, hiddenInput, hiddenInput.value); // Pass initial content from hidden input
417
+ } else {
418
+ console.error(`Could not find editor container or hidden input for block ${blockId}`);
419
+ }
420
+
421
+ // Setup Delete Button
422
+ const deleteButton = blockEditor.querySelector('.delete-block-btn');
423
+ if (deleteButton) {
424
+ deleteButton.addEventListener('click', function() {
425
+ if (confirm('Êtes-vous sûr de vouloir supprimer ce bloc ? Cette action est irréversible.')) {
426
+ // Clean up Quill instance before removing the element
427
+ if (quillInstances[blockId]) {
428
+ delete quillInstances[blockId]; // Remove reference
429
+ console.log(`Quill instance for block ${blockId} cleaned up.`);
430
+ }
431
+ blockEditor.remove();
432
+ updateBlockOrder(); // Renumber remaining blocks
433
+ }
434
+ });
435
+ } else {
436
+ console.warn(`Delete button not found for block ${blockId}`);
437
+ }
438
+
439
+ // Setup Image Selection Button
440
+ const selectButton = blockEditor.querySelector('.select-image-btn');
441
+ if (selectButton) {
442
+ selectButton.addEventListener('click', function() {
443
+ const galleryModalElement = document.getElementById('image-gallery-modal');
444
+ if (galleryModalElement) {
445
+ // Store the target block ID on the modal before showing
446
+ galleryModalElement.setAttribute('data-target-block', blockId);
447
+ // Use Bootstrap's JS to show the modal
448
+ const galleryModal = bootstrap.Modal.getOrCreateInstance(galleryModalElement);
449
+ galleryModal.show();
450
+ } else {
451
+ alert("Erreur : La galerie d'images n'a pas pu être trouvée.");
452
+ }
453
+ });
454
+ } else {
455
+ console.warn(`Select image button not found for block ${blockId}`);
456
+ }
457
+
458
+ // Setup Image Removal Button
459
+ const removeButton = blockEditor.querySelector('.remove-image-btn');
460
+ if (removeButton) {
461
+ removeButton.addEventListener('click', function() {
462
+ const imageIdInput = blockEditor.querySelector('.block-image-id');
463
+ const imagePreview = blockEditor.querySelector('.image-preview');
464
+
465
+ if (imageIdInput) imageIdInput.value = ''; // Clear the hidden ID
466
+ if (imagePreview) {
467
+ imagePreview.src = '';
468
+ imagePreview.style.display = 'none'; // Hide preview
469
+ imagePreview.alt = 'Preview';
470
+ }
471
+
472
+ // Toggle button visibility
473
+ this.style.display = 'none'; // Hide remove button
474
+ const selectBtn = blockEditor.querySelector('.select-image-btn');
475
+ if(selectBtn) selectBtn.style.display = 'inline-block'; // Show select button
476
+
477
+ // Hide position controls (handled by specific function in edit_texte.html)
478
+ const event = new CustomEvent('imageRemoved', { bubbles: true, detail: { blockEditor: blockEditor } });
479
+ blockEditor.dispatchEvent(event);
480
+ });
481
+ } else {
482
+ console.warn(`Remove image button not found for block ${blockId}`);
483
+ }
484
+
485
+ // Setup Image Position Select (if needed, though primary logic might be in HTML script)
486
+ const positionSelect = blockEditor.querySelector('.image-position-select');
487
+ if (positionSelect) {
488
+ positionSelect.addEventListener('change', function() {
489
+ // Optional: Add JS logic here if needed, e.g., apply preview classes
490
+ console.log(`Block ${blockId} image position changed to: ${this.value}`);
491
+ });
492
+ }
493
  });
494
+
495
+ updateBlockOrder(); // Ensure initial numbering is correct
496
  }
497
 
498
+
499
  // Add a new content block to the editor
500
  function addContentBlock(data = null) {
501
  const blocksContainer = document.getElementById('blocks-container');
502
  if (!blocksContainer) return;
503
+
504
+ const blockCount = blocksContainer.children.length;
505
+ const newBlockIndex = blockCount + 1;
506
+ const blockId = 'new-block-' + Date.now(); // Unique ID for new blocks
507
+
508
+ // Default values if data is not provided
509
+ const initialData = {
510
+ id: blockId, // Use the generated temporary ID
511
+ title: data?.title || '',
512
+ content: data?.content || '', // Default empty content for Quill
513
+ image: data?.image || null, // { id: ..., src: ..., alt: ... }
514
+ image_position: data?.image_position || 'left'
515
+ };
516
+
517
+ // Create block HTML using template literals
518
  const blockHtml = `
519
+ <div class="block-editor mb-4" data-block-id="${initialData.id}">
520
  <div class="block-editor-header">
521
  <div class="d-flex align-items-center">
522
  <span class="block-handle"><i class="fas fa-grip-vertical"></i></span>
523
+ <h3 class="block-editor-title">Bloc #${newBlockIndex}</h3>
524
  </div>
525
  <div class="block-editor-actions">
526
+ <button type="button" class="btn btn-danger btn-sm delete-block-btn" title="Supprimer ce bloc">
527
  <i class="fas fa-trash"></i>
528
  </button>
529
  </div>
530
  </div>
531
+ <div class="form-group mb-3">
532
+ <label for="block-${initialData.id}-title">Titre du bloc (optionnel)</label>
533
+ <input type="text" class="form-control block-title" id="block-${initialData.id}-title" value="${initialData.title}">
534
  </div>
535
+ <div class="form-group mb-3">
536
+ <label for="quill-editor-${initialData.id}">Contenu du bloc</label>
537
+ <!-- Quill editor container -->
538
+ <div id="quill-editor-${initialData.id}" class="block-content-editor"></div>
539
+ <!-- Hidden input to store Quill's HTML content -->
540
+ <input type="hidden" class="block-content-hidden" id="block-${initialData.id}-content" value="${initialData.content}">
541
  </div>
542
  <div class="form-group">
543
+ <label>Image (optionnel)</label>
544
  <div class="d-flex align-items-center mb-2">
545
+ <button type="button" class="btn btn-primary btn-sm select-image-btn" style="${initialData.image ? 'display:none;' : ''}">
546
  <i class="fas fa-image"></i> Sélectionner une image
547
  </button>
548
+ <button type="button" class="btn btn-warning btn-sm remove-image-btn ml-2" style="${initialData.image ? '' : 'display:none;'}">
549
  <i class="fas fa-times"></i> Retirer l'image
550
  </button>
551
  </div>
552
+ <input type="hidden" class="block-image-id" value="${initialData.image?.id || ''}">
553
+
554
+ <div class="block-image-container">
555
+ <img src="${initialData.image?.src || ''}" alt="${initialData.image?.alt || 'Prévisualisation'}" class="image-preview" style="${initialData.image ? '' : 'display:none;'}">
556
+ </div>
557
+
558
+ <div class="form-group mt-3 image-position-controls ${initialData.image ? '' : 'd-none'}">
559
+ <label for="block-${initialData.id}-image-position">Position de l'image</label>
560
+ <select class="form-control image-position-select" id="block-${initialData.id}-image-position">
561
+ <option value="left" ${initialData.image_position === 'left' ? 'selected' : ''}>Gauche</option>
562
+ <option value="right" ${initialData.image_position === 'right' ? 'selected' : ''}>Droite</option>
563
+ <option value="top" ${initialData.image_position === 'top' ? 'selected' : ''}>Haut</option>
564
  </select>
565
  </div>
566
  </div>
567
  </div>
568
  `;
569
+
570
+ // Add the block HTML to the container
571
  blocksContainer.insertAdjacentHTML('beforeend', blockHtml);
572
+
573
+ // Get the newly added block element
574
+ const newBlockElement = blocksContainer.lastElementChild;
575
+
576
+ // Initialize Quill for the new block
577
+ const editorContainer = newBlockElement.querySelector(`#quill-editor-${initialData.id}`);
578
+ const hiddenInput = newBlockElement.querySelector(`#block-${initialData.id}-content`);
579
+ if(editorContainer && hiddenInput) {
580
+ initializeQuillEditor(editorContainer, hiddenInput, initialData.content);
581
+ } else {
582
+ console.error(`Failed to find editor elements for new block ${initialData.id}`);
583
+ }
584
+
585
+
586
+ // Add event listeners for the new block's controls (delete, select image, remove image)
587
+ // Delegate event listeners might be more efficient, but direct binding is simpler here
588
+
589
  // Delete button
590
+ const deleteButton = newBlockElement.querySelector('.delete-block-btn');
591
  if (deleteButton) {
592
  deleteButton.addEventListener('click', function() {
593
+ if (confirm('Êtes-vous sûr de vouloir supprimer ce bloc ?')) {
594
+ // Clean up Quill instance
595
+ if (quillInstances[initialData.id]) {
596
+ delete quillInstances[initialData.id];
597
+ console.log(`Quill instance for block ${initialData.id} cleaned up.`);
598
+ }
599
+ newBlockElement.remove();
600
+ updateBlockOrder();
601
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
602
  });
603
  }
604
+
605
+ // Select image button
606
+ const selectButton = newBlockElement.querySelector('.select-image-btn');
607
+ if (selectButton) {
608
+ selectButton.addEventListener('click', function() {
609
+ const galleryModalElement = document.getElementById('image-gallery-modal');
610
+ if (galleryModalElement) {
611
+ galleryModalElement.setAttribute('data-target-block', initialData.id);
612
+ const galleryModal = bootstrap.Modal.getOrCreateInstance(galleryModalElement);
613
+ galleryModal.show();
614
+ } else {
615
+ alert("Erreur : La galerie d'images n'a pas pu être trouvée.");
616
+ }
 
 
 
 
 
 
 
 
 
 
 
617
  });
618
  }
619
+
620
+ // Remove image button
621
+ const removeButton = newBlockElement.querySelector('.remove-image-btn');
622
+ if (removeButton) {
623
+ removeButton.addEventListener('click', function() {
624
+ const imageIdInput = newBlockElement.querySelector('.block-image-id');
625
+ const imagePreview = newBlockElement.querySelector('.image-preview');
626
+
627
+ if (imageIdInput) imageIdInput.value = '';
628
+ if (imagePreview) {
629
+ imagePreview.src = '';
630
+ imagePreview.style.display = 'none';
631
+ imagePreview.alt = 'Preview';
632
+ }
633
+
634
+ this.style.display = 'none'; // Hide remove button
635
+ const selectBtn = newBlockElement.querySelector('.select-image-btn');
636
+ if (selectBtn) selectBtn.style.display = 'inline-block'; // Show select button
637
+
638
+ // Hide position controls
639
+ const event = new CustomEvent('imageRemoved', { bubbles: true, detail: { blockEditor: newBlockElement } });
640
+ newBlockElement.dispatchEvent(event);
641
+ });
642
+ }
643
+
644
+
645
  // Scroll to the new block
646
+ newBlockElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
647
  }
648
 
649
+
650
+ // Update block order numbers in the UI titles
651
  function updateBlockOrder() {
652
+ const blocks = document.querySelectorAll('#blocks-container .block-editor');
653
  blocks.forEach((block, index) => {
654
  const titleEl = block.querySelector('.block-editor-title');
655
  if (titleEl) {
656
  titleEl.textContent = `Bloc #${index + 1}`;
657
  }
658
  });
659
+ console.log("Block order updated.");
660
  }
661
 
662
+ // Save content blocks by gathering data and submitting the form
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
663
  function saveContentBlocks() {
664
  const blocksContainer = document.getElementById('blocks-container');
665
  const blocksDataInput = document.getElementById('blocks-data');
666
+ const blocksForm = document.getElementById('blocks-form');
667
+
668
+ if (!blocksContainer || !blocksDataInput || !blocksForm) {
669
+ console.error("Cannot save blocks: Missing container, data input, or form element.");
670
+ alert("Erreur : Impossible de sauvegarder les blocs. Éléments de formulaire manquants.");
671
+ return;
672
+ }
673
+
674
+ const blockElements = blocksContainer.querySelectorAll('.block-editor');
675
  const blocksData = [];
676
+
677
+ console.log(`Gathering data for ${blockElements.length} blocks...`);
678
+
679
+ blockElements.forEach((blockElement, index) => {
680
+ const blockId = blockElement.getAttribute('data-block-id');
681
+ const titleInput = blockElement.querySelector('.block-title');
682
+ const hiddenContentInput = blockElement.querySelector('.block-content-hidden');
683
+ const imageIdInput = blockElement.querySelector('.block-image-id');
684
+ const imagePositionSelect = blockElement.querySelector('.image-position-select');
685
+
686
+ let content = '';
687
+ // Crucially, get the latest content from Quill instance if available
688
+ if (quillInstances[blockId]) {
689
+ content = quillInstances[blockId].root.innerHTML;
690
+ // Treat empty Quill editor as empty string
691
+ if (content === '<p><br></p>') {
692
+ content = '';
693
+ }
694
+ // Ensure hidden input is also synced before submission (belt and suspenders)
695
+ if (hiddenContentInput) hiddenContentInput.value = content;
696
+ } else if (hiddenContentInput) {
697
+ // Fallback to hidden input if Quill instance is missing (error condition)
698
+ content = hiddenContentInput.value;
699
+ console.warn(`Quill instance missing for block ${blockId}. Using hidden input value.`);
700
+ } else {
701
+ console.error(`Cannot get content for block ${blockId}: No Quill instance and no hidden input.`);
702
+ }
703
+
704
+ const blockDataItem = {
705
+ // Include the original block ID if it's not a new one,
706
+ // otherwise, the backend should know how to handle blocks without a numeric ID
707
+ id: blockId.startsWith('new-block-') ? null : blockId,
708
+ temp_id: blockId.startsWith('new-block-') ? blockId : null, // Send temp ID for new blocks if needed
709
+ title: titleInput ? titleInput.value.trim() : '',
710
  content: content,
711
+ image_id: imageIdInput ? (imageIdInput.value || null) : null, // Send null if empty
712
+ image_position: imagePositionSelect ? imagePositionSelect.value : 'left',
713
+ order: index // Set the order based on current position
714
+ };
715
+ blocksData.push(blockDataItem);
716
  });
717
+
718
+ // Set the JSON data in the hidden input field
719
  blocksDataInput.value = JSON.stringify(blocksData);
720
+
721
+ console.log("Blocks data prepared:", blocksDataInput.value);
722
+
723
  // Submit the form
724
+ // Add loading indicator?
725
+ const saveButton = document.getElementById('save-blocks-button');
726
+ if(saveButton) {
727
+ saveButton.disabled = true;
728
+ saveButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sauvegarde...';
729
  }
730
+
731
+ blocksForm.submit();
732
  }
733
 
734
+ // --- Image Management Functions ---
735
+
736
+ // Setup image uploader preview (for the form in edit_texte.html)
737
  function setupImageUploader() {
738
+ const imageFileInput = document.getElementById('image-file'); // ID from edit_texte.html form
739
+ const imagePreview = document.getElementById('upload-image-preview'); // ID from edit_texte.html form
740
+
 
741
  if (imageFileInput && imagePreview) {
742
  imageFileInput.addEventListener('change', function() {
743
  if (this.files && this.files[0]) {
744
  const reader = new FileReader();
 
745
  reader.onload = function(e) {
746
  imagePreview.src = e.target.result;
747
+ imagePreview.style.display = 'block'; // Show preview
748
  };
 
749
  reader.readAsDataURL(this.files[0]);
750
+ } else {
751
+ // Clear preview if no file selected
752
+ imagePreview.src = '#';
753
+ imagePreview.style.display = 'none';
 
 
 
 
 
 
 
754
  }
755
  });
756
  }
757
  }
758
 
759
+ // Setup image gallery modal interaction
760
  function setupImageGallery() {
761
+ const galleryModalElement = document.getElementById('image-gallery-modal');
762
+ if (!galleryModalElement) return;
763
+
764
+ const galleryContent = galleryModalElement.querySelector('#image-gallery-content'); // Container for images
765
+
766
+ // Use event delegation on the modal body for image clicks
767
+ galleryModalElement.addEventListener('click', function(event) {
768
+ const galleryItem = event.target.closest('.gallery-item');
769
+ if (!galleryItem) return; // Clicked outside an item
770
+
771
+ const imageId = galleryItem.getAttribute('data-image-id');
772
+ const imageElement = galleryItem.querySelector('img');
773
+ const imageSrc = imageElement ? imageElement.src : '';
774
+ const imageAlt = imageElement ? imageElement.alt : '';
775
+
776
+ // Get the ID of the block that opened the modal
777
+ const targetBlockId = galleryModalElement.getAttribute('data-target-block');
778
+
779
+ if (!targetBlockId) {
780
+ console.error("Target block ID not found on modal.");
781
+ return;
782
+ }
783
+
784
+ // Find the target block editor element
785
+ const blockEditor = document.querySelector(`.block-editor[data-block-id="${targetBlockId}"]`);
786
+
787
+ if (blockEditor && imageId && imageSrc) {
788
+ // Update the block's image ID input
789
+ const imageIdInput = blockEditor.querySelector('.block-image-id');
790
+ if (imageIdInput) imageIdInput.value = imageId;
791
+
792
+ // Update the block's image preview
793
+ const imagePreview = blockEditor.querySelector('.image-preview');
794
+ if (imagePreview) {
795
+ imagePreview.src = imageSrc;
796
+ imagePreview.alt = imageAlt || 'Prévisualisation';
797
+ imagePreview.style.display = 'block'; // Ensure preview is visible
 
 
 
 
 
 
 
 
 
 
 
798
  }
799
+
800
+ // Toggle visibility of select/remove buttons
801
+ const selectButton = blockEditor.querySelector('.select-image-btn');
802
+ const removeButton = blockEditor.querySelector('.remove-image-btn');
803
+ if (selectButton) selectButton.style.display = 'none';
804
+ if (removeButton) removeButton.style.display = 'inline-block';
805
+
806
+ // Show position controls (handled by specific function in edit_texte.html)
807
+ const customEvent = new CustomEvent('imageSelected', { bubbles: true, detail: { blockEditor: blockEditor } });
808
+ blockEditor.dispatchEvent(customEvent);
809
+
810
+
811
+ // Close the modal
812
+ const galleryModalInstance = bootstrap.Modal.getInstance(galleryModalElement);
813
+ if (galleryModalInstance) {
814
+ galleryModalInstance.hide();
815
+ }
816
+ } else {
817
+ console.error(`Failed to update block ${targetBlockId}. Block editor or image data missing.`);
818
+ }
819
  });
820
+
821
+ // Optional: Add logic here to dynamically load gallery images via AJAX if needed
822
+ // E.g., on modal 'show.bs.modal' event
823
+ }