diff options
| author | polo <ordipolo@gmx.fr> | 2025-09-05 11:27:37 +0200 |
|---|---|---|
| committer | polo <ordipolo@gmx.fr> | 2025-09-05 11:27:37 +0200 |
| commit | 46b455a0d3e96099e78975f53a25365a2ca9dcb4 (patch) | |
| tree | 54f4616a103410d2bc77827b738f592806dbff67 | |
| parent | f477314613beb26b0ce4c61ec0b1900df1de1cb1 (diff) | |
| download | cms-46b455a0d3e96099e78975f53a25365a2ca9dcb4.tar.gz cms-46b455a0d3e96099e78975f53a25365a2ca9dcb4.tar.bz2 cms-46b455a0d3e96099e78975f53a25365a2ca9dcb4.zip | |
classe Editor et encapsulation, placement article premier ou dernier, variable window.Config.page, séparation openEditor et initEditor
| -rw-r--r-- | public/css/calendar.css | 6 | ||||
| -rw-r--r-- | public/css/modif_page.css | 13 | ||||
| -rw-r--r-- | public/js/main.js | 17 | ||||
| -rw-r--r-- | public/js/menu.js | 87 | ||||
| -rw-r--r-- | public/js/modif_page.js | 9 | ||||
| -rw-r--r-- | public/js/new_page.js | 5 | ||||
| -rw-r--r-- | public/js/tinymce.js | 647 | ||||
| -rw-r--r-- | src/controller/ArticleController.php | 26 | ||||
| -rw-r--r-- | src/controller/PageManagementController.php | 7 | ||||
| -rw-r--r-- | src/view/MainBuilder.php | 6 | ||||
| -rw-r--r-- | src/view/NewBuilder.php | 20 | ||||
| -rw-r--r-- | src/view/NewsBlockBuilder.php | 2 | ||||
| -rw-r--r-- | src/view/PostBlockBuilder.php | 13 | ||||
| -rw-r--r-- | src/view/PostBuilder.php | 1 | ||||
| -rw-r--r-- | src/view/templates/head.php | 1 |
15 files changed, 428 insertions, 432 deletions
diff --git a/public/css/calendar.css b/public/css/calendar.css index 314a5ad..1b05fbd 100644 --- a/public/css/calendar.css +++ b/public/css/calendar.css | |||
| @@ -63,15 +63,15 @@ td .fc-timegrid-axis{ | |||
| 63 | #calendar_zone{ | 63 | #calendar_zone{ |
| 64 | padding: 0; | 64 | padding: 0; |
| 65 | } | 65 | } |
| 66 | #calendar_zone p, .event > button{ | ||
| 67 | font-size: smaller; | ||
| 68 | } | ||
| 66 | .fc-toolbar-title{ | 69 | .fc-toolbar-title{ |
| 67 | /*font-size: large !important;*/ | 70 | /*font-size: large !important;*/ |
| 68 | } | 71 | } |
| 69 | .fc-header-toolbar{ | 72 | .fc-header-toolbar{ |
| 70 | font-size: smaller; | 73 | font-size: smaller; |
| 71 | } | 74 | } |
| 72 | h3, p, .event > button{ | ||
| 73 | font-size: smaller; | ||
| 74 | } | ||
| 75 | } | 75 | } |
| 76 | @media screen and (max-width: 650px){ | 76 | @media screen and (max-width: 650px){ |
| 77 | .fc-header-toolbar{ | 77 | .fc-header-toolbar{ |
diff --git a/public/css/modif_page.css b/public/css/modif_page.css index 9048f05..8095766 100644 --- a/public/css/modif_page.css +++ b/public/css/modif_page.css | |||
| @@ -85,13 +85,20 @@ | |||
| 85 | .modify_one_block > div | 85 | .modify_one_block > div |
| 86 | { | 86 | { |
| 87 | display: flex; | 87 | display: flex; |
| 88 | align-items: baseline; | 88 | align-items: center; |
| 89 | } | ||
| 90 | .block_options | ||
| 91 | { | ||
| 92 | flex-wrap: wrap; | ||
| 93 | } | ||
| 94 | .block_options > div /* bloc boutons flèches et supprimer */ | ||
| 95 | { | ||
| 96 | display: flex; | ||
| 89 | } | 97 | } |
| 90 | .block_options > * | 98 | .block_options > * |
| 91 | { | 99 | { |
| 92 | /*display: flex; | ||
| 93 | flex-wrap: wrap;*/ | ||
| 94 | text-wrap: nowrap; | 100 | text-wrap: nowrap; |
| 101 | align-items: center; | ||
| 95 | } | 102 | } |
| 96 | .grid_options | 103 | .grid_options |
| 97 | { | 104 | { |
diff --git a/public/js/main.js b/public/js/main.js index 4be7843..8f2b214 100644 --- a/public/js/main.js +++ b/public/js/main.js | |||
| @@ -1,10 +1,10 @@ | |||
| 1 | function newPassword(page, id = ''){ | 1 | function newPassword(id = ''){ |
| 2 | if(id != ''){ | 2 | if(id != ''){ |
| 3 | id = '&id=' + id; | 3 | id = '&id=' + id; |
| 4 | } | 4 | } |
| 5 | alert('Le mot de passe a été modifié.'); | 5 | alert('Le mot de passe a été modifié.'); |
| 6 | window.setTimeout(function(){ | 6 | window.setTimeout(function(){ |
| 7 | location.href = "index.php?page=" + page + "&message=nouveau_mdp" + id; | 7 | location.href = "index.php?page=" + window.Config.page + "&message=nouveau_mdp" + id; |
| 8 | }, 0); | 8 | }, 0); |
| 9 | } | 9 | } |
| 10 | 10 | ||
| @@ -71,14 +71,16 @@ document.addEventListener('DOMContentLoaded', () => { // pour pouvoir attraper l | |||
| 71 | nav_zone.style.height = entry.contentRect.height + 'px'; | 71 | nav_zone.style.height = entry.contentRect.height + 'px'; |
| 72 | } | 72 | } |
| 73 | }); | 73 | }); |
| 74 | resize_observer.observe(nav); | 74 | if(nav){ |
| 75 | resize_observer.observe(nav); | ||
| 76 | } | ||
| 75 | }); | 77 | }); |
| 76 | 78 | ||
| 77 | 79 | ||
| 78 | // complète les fonctions dans tinymce.js | 80 | // complète les fonctions dans tinymce.js |
| 79 | function switchPositions(article_id, direction) | 81 | function switchPositions(article_id, direction) |
| 80 | { | 82 | { |
| 81 | const current_article = findParent(document.getElementById(article_id), 'article'); // l'id n'est pas sur la bonne balise | 83 | const current_article = findParentByTagName(document.getElementById(article_id), 'article'); // l'id n'est pas sur la bonne balise |
| 82 | var other_article; | 84 | var other_article; |
| 83 | 85 | ||
| 84 | if(direction == 'down'){ | 86 | if(direction == 'down'){ |
| @@ -232,10 +234,9 @@ function updateDate(id_date, date_input){ | |||
| 232 | return date_input; | 234 | return date_input; |
| 233 | } | 235 | } |
| 234 | 236 | ||
| 235 | function findParent(element, tag_name){ | 237 | function findParentByTagName(element, tag_name){ |
| 236 | while (element !== null) { | 238 | while(element !== null){ |
| 237 | if (element.tagName === tag_name.toUpperCase()) // tagName est en majuscules | 239 | if(element.tagName === tag_name.toUpperCase()){ // tagName est en majuscules |
| 238 | { | ||
| 239 | return element; | 240 | return element; |
| 240 | } | 241 | } |
| 241 | element = element.parentElement; | 242 | element = element.parentElement; |
diff --git a/public/js/menu.js b/public/js/menu.js index ac6d35e..7f48ac9 100644 --- a/public/js/menu.js +++ b/public/js/menu.js | |||
| @@ -63,93 +63,6 @@ function moveOneLevelDown(page_id) | |||
| 63 | .catch(error => { | 63 | .catch(error => { |
| 64 | console.error('Erreur:', error); | 64 | console.error('Erreur:', error); |
| 65 | }); | 65 | }); |
| 66 | |||
| 67 | /*const element = document.getElementById(page_id); // div parente du bouton cliqué | ||
| 68 | let previous_element = element.previousElementSibling; | ||
| 69 | |||
| 70 | if(previous_element != null) | ||
| 71 | { | ||
| 72 | // si l'element précédent n'a pas de chemin relatif, donc est une URL, on vérifie le précédent également | ||
| 73 | if(previous_element.querySelector(".path") == null){ | ||
| 74 | let test_previous = previous_element; | ||
| 75 | while(test_previous.querySelector(".url") != null){ | ||
| 76 | console.log(test_previous); | ||
| 77 | //if() | ||
| 78 | test_previous = test_previous.previousElementSibling; | ||
| 79 | if(test_previous == null){ | ||
| 80 | console.log("pas d'élément précédent"); | ||
| 81 | return; | ||
| 82 | } | ||
| 83 | console.log(test_previous); | ||
| 84 | } | ||
| 85 | previous_element = test_previous; | ||
| 86 | } | ||
| 87 | |||
| 88 | fetch('index.php?menu_edit=move_one_level_down', { | ||
| 89 | method: 'POST', | ||
| 90 | headers: { | ||
| 91 | 'Content-Type': 'application/json' | ||
| 92 | }, | ||
| 93 | body: JSON.stringify({ id: element.id }) | ||
| 94 | }) | ||
| 95 | .then(response => response.json()) | ||
| 96 | .then(data => { | ||
| 97 | if(data.success) | ||
| 98 | { | ||
| 99 | // | ||
| 100 | |||
| 101 | // menu régénéré | ||
| 102 | nav_zone.innerHTML = ''; | ||
| 103 | nav_zone.insertAdjacentHTML('afterbegin', data.nav); | ||
| 104 | } | ||
| 105 | else { | ||
| 106 | |||
| 107 | console.error('Échec de l\'inversion'); | ||
| 108 | } | ||
| 109 | }) | ||
| 110 | .catch(error => { | ||
| 111 | console.error('Erreur:', error); | ||
| 112 | }); | ||
| 113 | |||
| 114 | // nouveau parent | ||
| 115 | let level_div = previous_element.querySelector(".level"); | ||
| 116 | if(level_div == null){ | ||
| 117 | // créer une <div class="level"> | ||
| 118 | level_div = document.createElement("div"); | ||
| 119 | level_div.classList.add("level"); | ||
| 120 | previous_element.appendChild(level_div); | ||
| 121 | } | ||
| 122 | |||
| 123 | // déplacement | ||
| 124 | level_div.appendChild(element); | ||
| 125 | |||
| 126 | // marges | ||
| 127 | let margin_left = parseInt(element.style.marginLeft); | ||
| 128 | margin_left += 29; | ||
| 129 | element.style.marginLeft = String(margin_left) + "px"; | ||
| 130 | |||
| 131 | // MAJ des chemins affichés si c'est un chemin relatif (les liens URL ne peuvent avoir d'enfants) | ||
| 132 | const element_path = element.querySelector(".path"); | ||
| 133 | if(element_path != null){ | ||
| 134 | const previous_element_path = previous_element.querySelector(".path"); | ||
| 135 | element_path.innerHTML = previous_element_path.innerHTML + "/" + element_path.innerHTML.split("/").slice(-1); | ||
| 136 | |||
| 137 | // même chose pour tous les enfants sauf les URL vers l'extérieur | ||
| 138 | if(element.querySelector(".level") != null){ | ||
| 139 | element.querySelector(".level").querySelectorAll(".path").forEach( (one_elem) => { | ||
| 140 | const parent_elem_path = one_elem.parentNode.parentNode.parentNode.querySelector(".path"); // => div de l'élém => div class level => div du parent | ||
| 141 | const end_of_path = one_elem.innerHTML.split("/").slice(-1); | ||
| 142 | one_elem.innerHTML = parent_elem_path.innerHTML + "/" + end_of_path[0]; | ||
| 143 | }); | ||
| 144 | } | ||
| 145 | } | ||
| 146 | |||
| 147 | // dernier problème à corriger: le parent est une URL vers l'extérieur | ||
| 148 | } | ||
| 149 | else{ | ||
| 150 | // ne rien faire | ||
| 151 | console.log("pas d'élément précédent"); | ||
| 152 | }*/ | ||
| 153 | } | 66 | } |
| 154 | 67 | ||
| 155 | function switchMenuPositions(page_id, direction) | 68 | function switchMenuPositions(page_id, direction) |
diff --git a/public/js/modif_page.js b/public/js/modif_page.js index 615f34a..dd7271e 100644 --- a/public/js/modif_page.js +++ b/public/js/modif_page.js | |||
| @@ -2,10 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | // même fonction que dans new_page.js | 3 | // même fonction que dans new_page.js |
| 4 | function makePageNamePath(){ | 4 | function makePageNamePath(){ |
| 5 | const page_name = document.getElementById("page_name"); | 5 | document.getElementById("page_name_path").value = document.getElementById("page_name").value |
| 6 | const page_name_path = document.getElementById("page_name_path"); | ||
| 7 | |||
| 8 | page_name_path.value = page_name.value | ||
| 9 | .normalize("NFD") // décompose lettres + accents: é devient "e + accent aigu" | 6 | .normalize("NFD") // décompose lettres + accents: é devient "e + accent aigu" |
| 10 | .replace(/[\u0300-\u036f]/g, "") // supprime les accents | 7 | .replace(/[\u0300-\u036f]/g, "") // supprime les accents |
| 11 | .replace(/[^a-zA-Z0-9]+/g, " ") // supprime tout ce qu'il n'est pas alphanuméric | 8 | .replace(/[^a-zA-Z0-9]+/g, " ") // supprime tout ce qu'il n'est pas alphanuméric |
| @@ -130,7 +127,7 @@ function renamePageBloc(bloc_id){ | |||
| 130 | }); | 127 | }); |
| 131 | } | 128 | } |
| 132 | 129 | ||
| 133 | function switchBlocsPositions(bloc_id, direction, current_page) { | 130 | function switchBlocsPositions(bloc_id, direction) { |
| 134 | const current_bloc = document.getElementById(bloc_id); | 131 | const current_bloc = document.getElementById(bloc_id); |
| 135 | const current_bloc_edit_zone = document.getElementById("bloc_edit_" + bloc_id); | 132 | const current_bloc_edit_zone = document.getElementById("bloc_edit_" + bloc_id); |
| 136 | var other_bloc; | 133 | var other_bloc; |
| @@ -149,7 +146,7 @@ function switchBlocsPositions(bloc_id, direction, current_page) { | |||
| 149 | } | 146 | } |
| 150 | const other_bloc_edit_zone = document.getElementById("bloc_edit_" + other_bloc.id); | 147 | const other_bloc_edit_zone = document.getElementById("bloc_edit_" + other_bloc.id); |
| 151 | 148 | ||
| 152 | fetch('index.php?page=' + current_page + '&bloc_edit=switch_blocs_positions', { | 149 | fetch('index.php?page=' + window.Config.page + '&bloc_edit=switch_blocs_positions', { |
| 153 | method: 'POST', | 150 | method: 'POST', |
| 154 | headers: { 'Content-Type': 'application/json' }, | 151 | headers: { 'Content-Type': 'application/json' }, |
| 155 | body: JSON.stringify({ id1: bloc_id, id2: parseInt(other_bloc.id) }) | 152 | body: JSON.stringify({ id1: bloc_id, id2: parseInt(other_bloc.id) }) |
diff --git a/public/js/new_page.js b/public/js/new_page.js index 5b1c5c5..4b49060 100644 --- a/public/js/new_page.js +++ b/public/js/new_page.js | |||
| @@ -2,10 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | // même fonction que dans modif_page.js | 3 | // même fonction que dans modif_page.js |
| 4 | function makePageNamePath(){ | 4 | function makePageNamePath(){ |
| 5 | const page_name = document.getElementById("page_name"); | 5 | document.getElementById("page_name_path").value = document.getElementById("page_name").value |
| 6 | const page_name_path = document.getElementById("page_name_path"); | ||
| 7 | |||
| 8 | page_name_path.value = page_name.value | ||
| 9 | .normalize("NFD") // décompose lettres + accents: é devient "e + accent aigu" | 6 | .normalize("NFD") // décompose lettres + accents: é devient "e + accent aigu" |
| 10 | .replace(/[\u0300-\u036f]/g, "") // supprime les accents | 7 | .replace(/[\u0300-\u036f]/g, "") // supprime les accents |
| 11 | .replace(/[^a-zA-Z0-9]+/g, " ") // supprime tout ce qu'il n'est pas alphanuméric | 8 | .replace(/[^a-zA-Z0-9]+/g, " ") // supprime tout ce qu'il n'est pas alphanuméric |
diff --git a/public/js/tinymce.js b/public/js/tinymce.js index f153246..071c61b 100644 --- a/public/js/tinymce.js +++ b/public/js/tinymce.js | |||
| @@ -1,178 +1,40 @@ | |||
| 1 | let editors = {}; | 1 | // code à réorganiser |
| 2 | // seule certaines fonctions ont leur place dans Editor, d'autres servent à manipuler les articles d'une autre manière (déplacer, supprimer...) | ||
| 3 | // => encapsuler Editor dans une classe Article (comme la balise) qui existe même quand l'éditeur est fermé | ||
| 2 | 4 | ||
| 3 | function openEditor(id, page = '') { | ||
| 4 | var creation_mode; | ||
| 5 | var real_id; | ||
| 6 | var article; | ||
| 7 | 5 | ||
| 8 | // création ou modification d'un article? | 6 | /* -- utilisé par les évènements -- */ |
| 9 | if(id[0] === 'n'){ | 7 | let editors = {}; |
| 10 | creation_mode = true; | 8 | function openEditor(id){ |
| 11 | article = document.getElementById(id); | 9 | if(!editors[id]){ |
| 10 | editors[id] = new Editor(id); // appel de init à l'intérieur | ||
| 12 | } | 11 | } |
| 13 | else{ | 12 | //else{editors[id].reopen();} |
| 14 | creation_mode = false; | 13 | } |
| 15 | // Récupérer et sauvegarder le contenu d'origine de l'article | 14 | // placement d'un nouvel article dans un bloc "Articles libres" |
| 16 | real_id = 'i' + id.slice(1); | 15 | function setArticlePlacement(id){ |
| 17 | article = document.getElementById(id); | 16 | if(editors[id]){ |
| 18 | document.getElementById(id).setAttribute('data-original-content', article.innerHTML); | 17 | editors[id].setArticlePlacement(id); |
| 19 | } | 18 | } |
| 20 | 19 | } | |
| 21 | tinymce.init({ | 20 | function closeEditor(id, restore_old){ |
| 22 | selector: `#${id}`, | 21 | if(editors[id]){ |
| 23 | language: 'fr_FR', // téléchargement ici: https://www.tiny.cloud/get-tiny/language-packages/ | 22 | editors[id].close(restore_old); |
| 24 | language_url: 'js/tinymce-langs/fr_FR.js', // ou installer tweeb/tinymce-i18n avec composer | ||
| 25 | license_key: 'gpl', | ||
| 26 | branding: false, | ||
| 27 | plugins: 'lists link autolink table image media autoresize help', | ||
| 28 | toolbar: 'undo redo newdocument print selectall styles bold italic underline strikethrough fontsizeinput forecolor backcolor fontfamily align numlist bullist outdent indent table link image media help', | ||
| 29 | menubar: false, | ||
| 30 | toolbar_mode: 'wrap', | ||
| 31 | statusbar: false, | ||
| 32 | setup: function (editor) { | ||
| 33 | editor.on('init', function (){ | ||
| 34 | editors[id] = editor; | ||
| 35 | |||
| 36 | // boutons "Modifier", "Supprimer", "déplacer vers le haut", "déplacer vers le bas", "Annuler" et "Soumettre" | ||
| 37 | document.querySelector(`#cancel-${id}`).classList.remove('hidden'); | ||
| 38 | document.querySelector(`#submit-${id}`).classList.remove('hidden'); | ||
| 39 | if(creation_mode === false){ | ||
| 40 | document.querySelector(`#edit-${id}`).classList.add('hidden'); | ||
| 41 | if(page != 'article'){ | ||
| 42 | document.querySelector(`#position_up-${id}`).classList.add('hidden'); | ||
| 43 | document.querySelector(`#position_down-${id}`).classList.add('hidden'); | ||
| 44 | document.querySelector(`#delete-${real_id}`).classList.add('hidden'); | ||
| 45 | } | ||
| 46 | } | ||
| 47 | else{ | ||
| 48 | document.querySelector(`#new-${id}`).classList.add('hidden'); // id = new-new-id_node | ||
| 49 | } | ||
| 50 | }); | ||
| 51 | let skipPastePreProcess = false; | ||
| 52 | editor.on('Paste', function (e){ // déclenchement AVANT PastePreProcess et quelque que soit le contenu collé | ||
| 53 | const clipboardData = (e.clipboardData || e.originalEvent.clipboardData); | ||
| 54 | if(!clipboardData){ | ||
| 55 | return; | ||
| 56 | } | ||
| 57 | const items = clipboardData.items; | ||
| 58 | let foundImage = false; | ||
| 59 | |||
| 60 | for(let i = 0; i < items.length; i++){ | ||
| 61 | let item = items[i]; | ||
| 62 | |||
| 63 | if(item.type.indexOf('image') !== -1){ // test type MIME contenant image | ||
| 64 | foundImage = true; | ||
| 65 | |||
| 66 | const file = item.getAsFile(); // presse-papier => fichier lisible | ||
| 67 | const reader = new FileReader(); | ||
| 68 | |||
| 69 | reader.onload = function (event){ // fonction exécutée lorsque reader.readAsDataURL(file) est terminée | ||
| 70 | const base64Data = event.target.result; // données de l'image | ||
| 71 | |||
| 72 | fetch('index.php?action=upload_image_base64', { | ||
| 73 | method: 'POST', | ||
| 74 | headers: { 'Content-Type': 'application/json' }, | ||
| 75 | body: JSON.stringify({ image_base64: base64Data }) | ||
| 76 | }) | ||
| 77 | .then(response => response.json()) | ||
| 78 | .then(data => { | ||
| 79 | if(data.location){ | ||
| 80 | editor.insertContent('<img src="' + data.location + '">'); | ||
| 81 | } | ||
| 82 | }) | ||
| 83 | .catch(error => { | ||
| 84 | console.error('Erreur lors de l’upload de l’image base64 :', error); | ||
| 85 | }); | ||
| 86 | }; | ||
| 87 | reader.readAsDataURL(file); // lecture asynchrone du fichier | ||
| 88 | } | ||
| 89 | } | ||
| 90 | |||
| 91 | if(foundImage){ | ||
| 92 | e.preventDefault(); // supprime le collage automatiue | ||
| 93 | skipPastePreProcess = true; // désactiver le PastePreProcess pour ce collage | ||
| 94 | } | ||
| 95 | }); | ||
| 96 | editor.on('PastePreProcess', function (e){ // déclenchement au collage AVANT insertion dans l'éditeur | ||
| 97 | const parser = new DOMParser(); | ||
| 98 | const doc = parser.parseFromString(e.content, 'text/html'); | ||
| 99 | const images = doc.querySelectorAll('img'); | ||
| 100 | |||
| 101 | let downloads_in_progress = []; | ||
| 102 | |||
| 103 | images.forEach(img => { | ||
| 104 | if(img.src.startsWith('file://')){ // détection d'images non insérables | ||
| 105 | console.warn('Image locale non insérable dans tinymce :', img.src); | ||
| 106 | img.outerHTML = `<div style="border:1px solid red; padding:10px; margin:5px 0; background-color:#ffe6e6; color:#a94442; font-size:14px;"> | ||
| 107 | "Image locale non insérée (vient-elle d'un document LibreOffice ?). Effacez ce message rouge et copiez-collez l'image seule.</div>`; | ||
| 108 | } | ||
| 109 | else if(img.src.startsWith('http')){ // détection d'images web | ||
| 110 | const promise = fetch('index.php?action=upload_image_url', { // promesse d'un fichier téléchargeable sur le serveur | ||
| 111 | method: 'POST', | ||
| 112 | headers: { 'Content-Type': 'application/json' }, | ||
| 113 | body: JSON.stringify({ image_url: img.src }) | ||
| 114 | }) | ||
| 115 | .then(response => response.json()) | ||
| 116 | .then(data => { | ||
| 117 | if(data.location){ | ||
| 118 | img.src = data.location; // remplacer l'image par celle du serveur | ||
| 119 | } | ||
| 120 | }) | ||
| 121 | .catch(error => { | ||
| 122 | console.error('Erreur lors de l’upload de l’image distante:', error); | ||
| 123 | }); | ||
| 124 | |||
| 125 | downloads_in_progress.push(promise); | ||
| 126 | } | ||
| 127 | }); | ||
| 128 | |||
| 129 | // une image web ou plus: différer l'insertion dans l'éditeur le temps que le serveur télécharge les images | ||
| 130 | if(downloads_in_progress.length > 0){ | ||
| 131 | e.preventDefault(); | ||
| 132 | |||
| 133 | Promise.all(downloads_in_progress).then(() => { | ||
| 134 | e.content = doc.body.innerHTML; // remplacement du HTML dans l'éditeur par la copie modifiée (doc) | ||
| 135 | editor.insertContent(e.content); | ||
| 136 | }); | ||
| 137 | } | ||
| 138 | else{ | ||
| 139 | e.content = doc.body.innerHTML; // remplacement du HTML dans l'éditeur par la copie modifiée (doc) | ||
| 140 | } | ||
| 141 | }); // fin editor.on('PastePreProcess'... | ||
| 142 | }, | ||
| 143 | // upload d'image natif de tinymce avec le bouton "Insérer une image" | ||
| 144 | images_upload_handler: (blobInfo, progress) => new Promise((resolve, reject) => { | ||
| 145 | const formData = new FormData(); | ||
| 146 | formData.append("file", blobInfo.blob()); | ||
| 147 | |||
| 148 | fetch("index.php?action=upload_image_tinymce", { | ||
| 149 | method: "POST", | ||
| 150 | body: formData | ||
| 151 | }) | ||
| 152 | .then(response => response.json()) | ||
| 153 | .then(data => { | ||
| 154 | if(data.location) { | ||
| 155 | resolve(data.location); | ||
| 156 | } | ||
| 157 | else { | ||
| 158 | reject("Erreur: Chemin d'image invalide"); | ||
| 159 | } | ||
| 160 | }) | ||
| 161 | .catch(error => { | ||
| 162 | reject("Erreur lors de l'upload"); | ||
| 163 | }); | ||
| 164 | }), | ||
| 165 | image_caption: true | ||
| 166 | }); | ||
| 167 | |||
| 168 | // Remplacer le contenu de l'article par l'éditeur | ||
| 169 | if(creation_mode === false){ | ||
| 170 | document.getElementById(id).innerHTML = article.innerHTML; | ||
| 171 | } | 23 | } |
| 172 | } | 24 | } |
| 173 | 25 | function submitArticle(id, clone = null) | |
| 174 | function deleteArticle(id, page = '') { | 26 | { |
| 175 | if (confirm('Voulez-vous vraiment supprimer cet article ?')) | 27 | if(editors[id]){ |
| 28 | editors[id].submit(clone); | ||
| 29 | } | ||
| 30 | else if(window.Config.page === "article" && id[0] === 'n'){ // bouton Tout enregistrer (pas d'éditeur) | ||
| 31 | editors[id] = new Editor(id); | ||
| 32 | editors[id].submit(); | ||
| 33 | } | ||
| 34 | } | ||
| 35 | // standalone contraîrement aux autres fonctions ici | ||
| 36 | function deleteArticle(id){ | ||
| 37 | if(confirm('Voulez-vous vraiment supprimer cet article ?')) | ||
| 176 | { | 38 | { |
| 177 | // Envoyer une requête au serveur pour supprimer l'article | 39 | // Envoyer une requête au serveur pour supprimer l'article |
| 178 | fetch('index.php?action=delete_article', { | 40 | fetch('index.php?action=delete_article', { |
| @@ -199,146 +61,337 @@ function deleteArticle(id, page = '') { | |||
| 199 | } | 61 | } |
| 200 | } | 62 | } |
| 201 | 63 | ||
| 202 | function closeEditor(id, page = '', restore_old = true) | 64 | |
| 65 | |||
| 66 | class Editor | ||
| 203 | { | 67 | { |
| 204 | var creation_mode; | 68 | constructor(id){ |
| 205 | var real_id; | 69 | this.id = id; |
| 206 | var article; | 70 | this.article = document.getElementById(this.id); |
| 207 | var parent; | 71 | this.creation_mode = this.id[0] === 'n' ? true : false; |
| 208 | 72 | //this.isOpen = false; | |
| 209 | // création ou modification d'un article? | 73 | this.tiny_instance = null; |
| 210 | if(id[0] === 'n'){ | ||
| 211 | creation_mode = true; | ||
| 212 | } | ||
| 213 | else{ | ||
| 214 | creation_mode = false; | ||
| 215 | } | ||
| 216 | 74 | ||
| 217 | // Fermer l'éditeur | 75 | // moche, on ne devrait sortir l'envoi des données avec fetch de Editor.submit |
| 218 | tinymce.remove(`#${id}`); | 76 | if(!this.creation_mode || window.Config.page !== 'article'){ |
| 219 | delete editors[id]; | 77 | if(this.creation_mode && window.Config.page !== 'article'){ |
| 220 | 78 | this.setArticlePlacement(this.id); | |
| 221 | if(creation_mode){ | 79 | } |
| 222 | article = document.getElementById(id); | 80 | else{ |
| 223 | parent = findParent(article, 'section'); | 81 | // insérer le contenu de l'article dans l'éditeur |
| 224 | } | 82 | this.article.setAttribute('data-original-content', this.article.innerHTML); |
| 225 | else{ | 83 | } |
| 226 | real_id = 'i' + id.slice(1); | 84 | this.init(); |
| 85 | } | ||
| 86 | //else // bouton Tout enregistrer, pas d'éditeur | ||
| 227 | } | 87 | } |
| 228 | 88 | ||
| 229 | // Restaurer le contenu d'origine de l'article | 89 | setArticlePlacement(id_block){ |
| 230 | if(restore_old){ | 90 | const checked_button = document.querySelector('input[name="article_placement-' + id_block + '"]:checked'); |
| 231 | const originalContent = document.getElementById(id).getAttribute('data-original-content'); | 91 | if(checked_button){ // vrai clic |
| 232 | document.getElementById(id).innerHTML = originalContent; | 92 | this.placement = checked_button.value; |
| 93 | } | ||
| 94 | else{ | ||
| 95 | document.getElementById('radio_last-' + id_block).checked = true; // faux clic | ||
| 96 | this.placement = 'last'; | ||
| 97 | } | ||
| 233 | } | 98 | } |
| 99 | |||
| 100 | init(){ | ||
| 101 | tinymce.init({ | ||
| 102 | selector: `#${this.id}`, // avec un # comme dans querySelector | ||
| 103 | language: 'fr_FR', // téléchargement ici: https://www.tiny.cloud/get-tiny/language-packages/ | ||
| 104 | language_url: 'js/tinymce-langs/fr_FR.js', // ou installer tweeb/tinymce-i18n avec composer | ||
| 105 | license_key: 'gpl', | ||
| 106 | branding: false, | ||
| 107 | plugins: 'lists link autolink table image media autoresize help', | ||
| 108 | toolbar: 'undo redo newdocument print selectall styles bold italic underline strikethrough fontsizeinput forecolor backcolor fontfamily align numlist bullist outdent indent table link image media help', | ||
| 109 | menubar: false, | ||
| 110 | toolbar_mode: 'wrap', | ||
| 111 | statusbar: false, | ||
| 112 | // les fonctions fléchées permettent de garder le contexte (= this) | ||
| 113 | setup: (editor) => { | ||
| 114 | editor.on('init', () => { | ||
| 115 | this.tiny_instance = editor; | ||
| 116 | |||
| 117 | // boutons "Modifier", "Supprimer", "déplacer vers le haut", "déplacer vers le bas", "Annuler" et "Soumettre" | ||
| 118 | document.getElementById(`cancel-${this.id}`).classList.remove('hidden'); | ||
| 119 | document.getElementById(`submit-${this.id}`).classList.remove('hidden'); | ||
| 120 | const radio = document.getElementById(`radio-${this.id}`); | ||
| 121 | if(radio){ | ||
| 122 | radio.classList.remove('hidden'); | ||
| 123 | } | ||
| 124 | if(this.creation_mode){ | ||
| 125 | document.getElementById(`new-${this.id}`).classList.add('hidden'); // id = new-new-id_node | ||
| 126 | } | ||
| 127 | else{ | ||
| 128 | document.getElementById(`edit-${this.id}`).classList.add('hidden'); | ||
| 129 | if(window.Config.page !== 'article'){ | ||
| 130 | document.getElementById(`position_up-${this.id}`).classList.add('hidden'); | ||
| 131 | document.getElementById(`position_down-${this.id}`).classList.add('hidden'); | ||
| 132 | document.getElementById(`delete-${('i' + this.id.slice(1))}`).classList.add('hidden'); | ||
| 133 | } | ||
| 134 | } | ||
| 135 | }); | ||
| 136 | let skipPastePreProcess = false; | ||
| 137 | editor.on('Paste', function (e){ // déclenchement AVANT PastePreProcess et quelque que soit le contenu collé | ||
| 138 | const clipboardData = (e.clipboardData || e.originalEvent.clipboardData); | ||
| 139 | if(!clipboardData){ | ||
| 140 | return; | ||
| 141 | } | ||
| 142 | const items = clipboardData.items; | ||
| 143 | let foundImage = false; | ||
| 144 | |||
| 145 | for(let i = 0; i < items.length; i++){ | ||
| 146 | let item = items[i]; | ||
| 147 | |||
| 148 | if(item.type.indexOf('image') !== -1){ // test type MIME contenant image | ||
| 149 | foundImage = true; | ||
| 234 | 150 | ||
| 235 | // boutons: "Nouvel article", Modifier", "Supprimer", "déplacer vers le haut", "déplacer vers le bas", "Annuler" et "Valider" | 151 | const file = item.getAsFile(); // presse-papier => fichier lisible |
| 236 | document.querySelector(`#cancel-${id}`).classList.add('hidden'); | 152 | const reader = new FileReader(); |
| 237 | document.querySelector(`#submit-${id}`).classList.add('hidden'); | 153 | |
| 238 | if(creation_mode){ | 154 | reader.onload = function (event){ // fonction exécutée lorsque reader.readAsDataURL(file) est terminée |
| 239 | document.querySelector(`#new-${id}`).classList.remove('hidden'); // id = new-new-id_node | 155 | const base64Data = event.target.result; // données de l'image |
| 156 | |||
| 157 | fetch('index.php?action=upload_image_base64', { | ||
| 158 | method: 'POST', | ||
| 159 | headers: { 'Content-Type': 'application/json' }, | ||
| 160 | body: JSON.stringify({ image_base64: base64Data }) | ||
| 161 | }) | ||
| 162 | .then(response => response.json()) | ||
| 163 | .then(data => { | ||
| 164 | if(data.location){ | ||
| 165 | editor.insertContent('<img src="' + data.location + '">'); | ||
| 166 | } | ||
| 167 | }) | ||
| 168 | .catch(error => { | ||
| 169 | console.error('Erreur lors de l’upload de l’image base64 :', error); | ||
| 170 | }); | ||
| 171 | }; | ||
| 172 | reader.readAsDataURL(file); // lecture asynchrone du fichier | ||
| 173 | } | ||
| 174 | } | ||
| 175 | |||
| 176 | if(foundImage){ | ||
| 177 | e.preventDefault(); // supprime le collage automatiue | ||
| 178 | skipPastePreProcess = true; // désactiver le PastePreProcess pour ce collage | ||
| 179 | } | ||
| 180 | }); | ||
| 181 | editor.on('PastePreProcess', function (e){ // déclenchement au collage AVANT insertion dans l'éditeur | ||
| 182 | const parser = new DOMParser(); | ||
| 183 | const doc = parser.parseFromString(e.content, 'text/html'); | ||
| 184 | const images = doc.querySelectorAll('img'); | ||
| 185 | |||
| 186 | let downloads_in_progress = []; | ||
| 187 | |||
| 188 | images.forEach(img => { | ||
| 189 | if(img.src.startsWith('file://')){ // détection d'images non insérables | ||
| 190 | console.warn('Image locale non insérable dans tinymce :', img.src); | ||
| 191 | img.outerHTML = `<div style="border:1px solid red; padding:10px; margin:5px 0; background-color:#ffe6e6; color:#a94442; font-size:14px;"> | ||
| 192 | "Image locale non insérée (vient-elle d'un document LibreOffice ?). Effacez ce message rouge et copiez-collez l'image seule.</div>`; | ||
| 193 | } | ||
| 194 | else if(img.src.startsWith('http')){ // détection d'images web | ||
| 195 | const promise = fetch('index.php?action=upload_image_url', { // promesse d'un fichier téléchargeable sur le serveur | ||
| 196 | method: 'POST', | ||
| 197 | headers: { 'Content-Type': 'application/json' }, | ||
| 198 | body: JSON.stringify({ image_url: img.src }) | ||
| 199 | }) | ||
| 200 | .then(response => response.json()) | ||
| 201 | .then(data => { | ||
| 202 | if(data.location){ | ||
| 203 | img.src = data.location; // remplacer l'image par celle du serveur | ||
| 204 | } | ||
| 205 | }) | ||
| 206 | .catch(error => { | ||
| 207 | console.error('Erreur lors de l’upload de l’image distante:', error); | ||
| 208 | }); | ||
| 209 | |||
| 210 | downloads_in_progress.push(promise); | ||
| 211 | } | ||
| 212 | }); | ||
| 213 | |||
| 214 | // une image web ou plus: différer l'insertion dans l'éditeur le temps que le serveur télécharge les images | ||
| 215 | if(downloads_in_progress.length > 0){ | ||
| 216 | e.preventDefault(); | ||
| 217 | |||
| 218 | Promise.all(downloads_in_progress).then(() => { | ||
| 219 | e.content = doc.body.innerHTML; // remplacement du HTML dans l'éditeur par la copie modifiée (doc) | ||
| 220 | editor.insertContent(e.content); | ||
| 221 | }); | ||
| 222 | } | ||
| 223 | else{ | ||
| 224 | e.content = doc.body.innerHTML; // remplacement du HTML dans l'éditeur par la copie modifiée (doc) | ||
| 225 | } | ||
| 226 | }); // fin editor.on('PastePreProcess'... | ||
| 227 | }, | ||
| 228 | // upload d'image natif de tinymce avec le bouton "Insérer une image" | ||
| 229 | images_upload_handler: (blobInfo, progress) => new Promise((resolve, reject) => { | ||
| 230 | const formData = new FormData(); | ||
| 231 | formData.append("file", blobInfo.blob()); | ||
| 232 | |||
| 233 | fetch("index.php?action=upload_image_tinymce", { | ||
| 234 | method: "POST", | ||
| 235 | body: formData | ||
| 236 | }) | ||
| 237 | .then(response => response.json()) | ||
| 238 | .then(data => { | ||
| 239 | if(data.location) { | ||
| 240 | resolve(data.location); | ||
| 241 | } | ||
| 242 | else { | ||
| 243 | reject("Erreur: Chemin d'image invalide"); | ||
| 244 | } | ||
| 245 | }) | ||
| 246 | .catch(error => { | ||
| 247 | reject("Erreur lors de l'upload"); | ||
| 248 | }); | ||
| 249 | }), | ||
| 250 | image_caption: true | ||
| 251 | }); | ||
| 240 | } | 252 | } |
| 241 | else{ | 253 | |
| 242 | document.querySelector(`#edit-${id}`).classList.remove('hidden'); | 254 | close(restore_old = true){ |
| 243 | if(page != 'article'){ | 255 | tinymce.remove(`#${this.id}`); // avec un # comme dans querySelector |
| 244 | document.querySelector(`#position_up-${id}`).classList.remove('hidden'); | 256 | delete editors[this.id]; |
| 245 | document.querySelector(`#position_down-${id}`).classList.remove('hidden'); | 257 | |
| 246 | document.querySelector(`#delete-${id}`).classList.remove('hidden'); | 258 | // Restaurer le contenu d'origine de l'article |
| 259 | if(restore_old){ | ||
| 260 | const original_content = document.getElementById(this.id).getAttribute('data-original-content'); | ||
| 261 | document.getElementById(this.id).innerHTML = original_content; | ||
| 247 | } | 262 | } |
| 248 | } | ||
| 249 | } | ||
| 250 | 263 | ||
| 251 | function submitArticle(id, page = '', clone = null) | 264 | // boutons: "Nouvel article", Modifier", "Supprimer", "déplacer vers le haut", "déplacer vers le bas", "Annuler" et "Valider" |
| 252 | { | 265 | document.getElementById(`cancel-${this.id}`).classList.add('hidden'); |
| 253 | var editor; | 266 | document.getElementById(`submit-${this.id}`).classList.add('hidden'); |
| 254 | var content; | 267 | |
| 255 | const params = new URL(document.location).searchParams; // "search" = ? et paramètres, searchParams = objet avec des getters | 268 | const radio = document.getElementById(`radio-${this.id}`); |
| 256 | 269 | if(radio){ | |
| 257 | // clic sur "Tout enregistrer" | 270 | document.querySelector('input[name="article_placement-' + this.id + '"]:checked').checked = false; // décoche l'option "en mémoire" |
| 258 | if(id[0] === 'n' && page === 'article'){ | 271 | radio.classList.add('hidden'); |
| 259 | const prefixes = ['t', 'p', 'i', 'd']; | 272 | } |
| 260 | const allElemsWithId = document.querySelectorAll('.data'); | 273 | |
| 261 | content = {}; | 274 | if(this.creation_mode){ |
| 262 | var id_from_builder; | 275 | document.getElementById(`new-${this.id}`).classList.remove('hidden'); // id = new-new-id_node |
| 263 | 276 | } | |
| 264 | allElemsWithId.forEach(element => { | 277 | else{ |
| 265 | const first_letter = element.id.charAt(0).toLowerCase(); | 278 | document.getElementById(`edit-${this.id}`).classList.remove('hidden'); |
| 266 | if(prefixes.includes(first_letter)){ | 279 | if(window.Config.page !== 'article'){ |
| 267 | content[first_letter] = element.innerHTML; | 280 | document.getElementById(`position_up-${this.id}`).classList.remove('hidden'); |
| 268 | if(first_letter === 'i'){ | 281 | document.getElementById(`position_down-${this.id}`).classList.remove('hidden'); |
| 269 | id_from_builder = element.id; | 282 | document.getElementById(`delete-${this.id}`).classList.remove('hidden'); |
| 270 | } | ||
| 271 | } | 283 | } |
| 272 | }) | ||
| 273 | content['d'] = dateToISO(content['d']); | ||
| 274 | } | ||
| 275 | // champs à remplir des nouvelles "news" | ||
| 276 | else if(page === 'article' && params != null && params.get("id")[0] === 'n'){ | ||
| 277 | closeEditor(id, page, false); | ||
| 278 | //makeNewArticleButtons(id, id, clone); | ||
| 279 | return; | ||
| 280 | } | ||
| 281 | // dans les autres cas, on doit pouvoir récupérer l'éditeur | ||
| 282 | else{ | ||
| 283 | // l'éditeur correspond à l'article OU page "article" à un élément: titre, aperçu, article | ||
| 284 | editor = editors[id]; | ||
| 285 | if(!editor) { | ||
| 286 | console.error('Éditeur non trouvé pour l\'article:', id); | ||
| 287 | return; | ||
| 288 | } | 284 | } |
| 289 | content = editor.getContent(); | ||
| 290 | } | 285 | } |
| 291 | 286 | ||
| 292 | // Envoi AJAX au serveur | 287 | submit(clone = null){ |
| 293 | fetch('index.php?action=editor_submit', { | 288 | //var editor; |
| 294 | method: 'POST', | 289 | var content; |
| 295 | headers: { | 290 | const params = new URL(document.location).searchParams; // "search" = ? et paramètres, searchParams = objet avec des getters |
| 296 | 'Content-Type': 'application/json' | 291 | // à comparer avec: new URLSearchParams(window.location.search); |
| 297 | }, | 292 | // c'est pareil ou pas? |
| 298 | body: JSON.stringify({id: id, content: content}) | 293 | |
| 299 | }) | 294 | // clic sur "Tout enregistrer" (ne devrait pas se situer dans Editor) |
| 300 | .then(response => response.json()) | 295 | if(this.creation_mode && window.Config.page === 'article'){ |
| 301 | .then(data => { | 296 | const prefixes = ['t', 'p', 'i', 'd']; |
| 302 | if(data.success) { | 297 | const allElemsWithId = document.querySelectorAll('.data'); |
| 303 | //console.log(data.article_id); | 298 | content = {}; |
| 304 | if(id[0] === 'n' && page === 'article'){ | 299 | var id_from_builder; |
| 305 | console.log('données envoyées au serveur avec succès.'); | 300 | |
| 306 | 301 | allElemsWithId.forEach(element => { | |
| 307 | // redirection page de l'article | 302 | const first_letter = element.id.charAt(0).toLowerCase(); |
| 308 | window.setTimeout(function(){ | 303 | if(prefixes.includes(first_letter)){ |
| 309 | const url_params = new URLSearchParams(window.location.search); // le "$_GET" de javascript | 304 | content[first_letter] = element.innerHTML; |
| 310 | location.href = "index.php?page=article&id=" + data.article_id + "&from=" + url_params.get('from'); | 305 | if(first_letter === 'i'){ |
| 311 | }, 0); | 306 | id_from_builder = element.id; |
| 312 | } | 307 | } |
| 313 | else{ | ||
| 314 | // Fermer l'éditeur et mettre à jour le contenu de l'article | ||
| 315 | closeEditor(id, page, false); | ||
| 316 | if(id[0] === 'n'){ | ||
| 317 | makeNewArticleButtons(id, data.article_id, clone); | ||
| 318 | } | 308 | } |
| 319 | } | 309 | }) |
| 310 | content['d'] = dateToISO(content['d']); | ||
| 320 | } | 311 | } |
| 312 | // champs à remplir des nouvelles "news" | ||
| 313 | else if(window.Config.page === 'article' && params != null && params.get("id")[0] === 'n'){ | ||
| 314 | this.close(false); | ||
| 315 | return; | ||
| 316 | } | ||
| 317 | // dans les autres cas, on doit pouvoir récupérer l'éditeur | ||
| 321 | else{ | 318 | else{ |
| 322 | alert('Erreur lors de la sauvegarde de l\'article.'); | 319 | // l'éditeur correspond à l'article OU si page = "article" à un élément: titre, aperçu, article |
| 320 | //editor = editors[id]; | ||
| 321 | if(!this.tiny_instance){ | ||
| 322 | console.error("Éditeur non trouvé pour l'article:", this.id); | ||
| 323 | return; | ||
| 324 | } | ||
| 325 | content = this.tiny_instance.getContent(); | ||
| 323 | } | 326 | } |
| 324 | }) | 327 | |
| 325 | .catch(error => { | 328 | let fetch_params = {id: this.id, content: content}; |
| 326 | console.error('Erreur:', error); | 329 | if(this.placement){ |
| 327 | }); | 330 | fetch_params['placement'] = this.placement; |
| 331 | } | ||
| 332 | |||
| 333 | // Envoi AJAX au serveur | ||
| 334 | fetch('index.php?action=editor_submit', { | ||
| 335 | method: 'POST', | ||
| 336 | headers: {'Content-Type': 'application/json'}, | ||
| 337 | body: JSON.stringify(fetch_params) | ||
| 338 | }) | ||
| 339 | .then(response => response.json()) | ||
| 340 | .then(data => { | ||
| 341 | if(data.success) | ||
| 342 | { | ||
| 343 | if(this.creation_mode && window.Config.page === 'article'){ | ||
| 344 | console.log('données envoyées au serveur avec succès.'); | ||
| 345 | |||
| 346 | // redirection page de l'article | ||
| 347 | window.setTimeout(function(){ | ||
| 348 | const url_params = new URLSearchParams(window.location.search); // le "$_GET" de javascript | ||
| 349 | location.href = "index.php?page=article&id=" + data.article_id + "&from=" + url_params.get('from'); | ||
| 350 | }, 0); | ||
| 351 | } | ||
| 352 | else{ | ||
| 353 | // Fermer l'éditeur et mettre à jour le contenu de l'article | ||
| 354 | this.close(false); | ||
| 355 | if(this.creation_mode){ | ||
| 356 | makeNewArticleButtons(this.id, data.article_id, clone, this.placement); | ||
| 357 | } | ||
| 358 | } | ||
| 359 | } | ||
| 360 | else{ | ||
| 361 | alert('Erreur lors de la sauvegarde de l\'article.'); | ||
| 362 | } | ||
| 363 | }) | ||
| 364 | .catch(error => { | ||
| 365 | console.error('Erreur:', error); | ||
| 366 | }); | ||
| 367 | } | ||
| 368 | |||
| 369 | //reopen(){} | ||
| 370 | |||
| 371 | /*destroy(){ | ||
| 372 | this.close(); | ||
| 373 | delete editors[this.id]; | ||
| 374 | console.log(`Editor ${this.id} détruit.`); | ||
| 375 | }*/ | ||
| 328 | } | 376 | } |
| 329 | 377 | ||
| 330 | function makeNewArticleButtons(id, article_id, clone) | 378 | |
| 379 | |||
| 380 | |||
| 381 | |||
| 382 | // restera ici jusqu'à ce que la gestion des balises soient faite ailleurs | ||
| 383 | function makeNewArticleButtons(id, article_id, clone, placement = 'last') | ||
| 331 | { | 384 | { |
| 332 | var share_btn = document.querySelector(`.share.hidden`); // combinaison de deux classes | 385 | var share_btn = document.querySelector(`.share.hidden`); // combinaison de deux classes |
| 333 | var new_btn = document.querySelector(`#new-${id}`); | 386 | var new_btn = document.getElementById(`new-${id}`); |
| 334 | var edit_btn = document.querySelector(`#edit-${id}`); | 387 | var edit_btn = document.getElementById(`edit-${id}`); |
| 335 | var pos_up_btn = document.querySelector(`#position_up-${id}`); | 388 | var pos_up_btn = document.getElementById(`position_up-${id}`); |
| 336 | var pos_down_btn = document.querySelector(`#position_down-${id}`); | 389 | var pos_down_btn = document.getElementById(`position_down-${id}`); |
| 337 | var delete_btn = document.querySelector(`#delete-${id}`); | 390 | var delete_btn = document.getElementById(`delete-${id}`); |
| 338 | var cancel_btn = document.querySelector(`#cancel-${id}`); | 391 | var cancel_btn = document.getElementById(`cancel-${id}`); |
| 339 | var submit_btn = document.querySelector(`#submit-${id}`); | 392 | var submit_btn = document.getElementById(`submit-${id}`); |
| 340 | 393 | ||
| 341 | share_btn.classList.remove('hidden') | 394 | share_btn.classList.remove('hidden'); |
| 342 | new_btn.classList.add('hidden'); | 395 | new_btn.classList.add('hidden'); |
| 343 | edit_btn.classList.remove('hidden'); | 396 | edit_btn.classList.remove('hidden'); |
| 344 | pos_up_btn.classList.remove('hidden'); | 397 | pos_up_btn.classList.remove('hidden'); |
| @@ -348,7 +401,7 @@ function makeNewArticleButtons(id, article_id, clone) | |||
| 348 | //submit_btn.classList.add('hidden'); | 401 | //submit_btn.classList.add('hidden'); |
| 349 | 402 | ||
| 350 | var article = document.getElementById(id); | 403 | var article = document.getElementById(id); |
| 351 | var parent = findParent(article, 'article'); | 404 | var article_elem_parent = findParentByTagName(article, 'article'); |
| 352 | 405 | ||
| 353 | share_btn.setAttribute('onclick', "copyInClipBoard('" + window.location.href + article_id + "')"); // # de l'ancre ajouté au clic sur le lien ouvrant l'éditeur | 406 | share_btn.setAttribute('onclick', "copyInClipBoard('" + window.location.href + article_id + "')"); // # de l'ancre ajouté au clic sur le lien ouvrant l'éditeur |
| 354 | article.id = article_id; | 407 | article.id = article_id; |
| @@ -364,8 +417,16 @@ function makeNewArticleButtons(id, article_id, clone) | |||
| 364 | cancel_btn.querySelector('button').setAttribute('onclick', "closeEditor('" + article_id + "')"); | 417 | cancel_btn.querySelector('button').setAttribute('onclick', "closeEditor('" + article_id + "')"); |
| 365 | submit_btn.id = 'submit-' + article_id; | 418 | submit_btn.id = 'submit-' + article_id; |
| 366 | submit_btn.querySelector('button').setAttribute('onclick', "submitArticle('" + article_id + "')"); | 419 | submit_btn.querySelector('button').setAttribute('onclick', "submitArticle('" + article_id + "')"); |
| 420 | |||
| 421 | var section_child = article_elem_parent.parentNode.querySelector('.section_child'); // renommer section_child | ||
| 422 | |||
| 423 | // parentNode vise la balise section | ||
| 424 | article_elem_parent.parentNode.replaceChild(clone.cloneNode(true), article_elem_parent); // clone du squelette pour le garder intact | ||
| 367 | 425 | ||
| 368 | var next_div = parent.nextElementSibling.nextElementSibling; | 426 | if(placement === 'first'){ |
| 369 | parent.parentNode.replaceChild(clone.cloneNode(true), parent); // clone du squelette pour le garder intact | 427 | section_child.insertBefore(article_elem_parent, section_child.firstChild); |
| 370 | next_div.appendChild(parent); | 428 | } |
| 429 | else{ // = 'last' | ||
| 430 | section_child.appendChild(article_elem_parent); | ||
| 431 | } | ||
| 371 | } \ No newline at end of file | 432 | } \ No newline at end of file |
diff --git a/src/controller/ArticleController.php b/src/controller/ArticleController.php index b8af290..5cebad6 100644 --- a/src/controller/ArticleController.php +++ b/src/controller/ArticleController.php | |||
| @@ -16,15 +16,15 @@ class ArticleController | |||
| 16 | { | 16 | { |
| 17 | $id = $json['id']; | 17 | $id = $json['id']; |
| 18 | $director = new Director($entityManager); | 18 | $director = new Director($entityManager); |
| 19 | $content = $json['content']; | ||
| 19 | 20 | ||
| 20 | // cas d'une nouvelle "news" | 21 | // nettoyage |
| 21 | if(is_array($json['content'])){ | 22 | if(is_array($content)){ // cas d'une nouvelle "news" |
| 22 | foreach($json['content'] as $one_input){ | 23 | foreach($content as $one_input){ |
| 23 | $one_input = Security::secureHTML($one_input); | 24 | $one_input = Security::secureHTML($one_input); |
| 24 | } | 25 | } |
| 25 | $content = $json['content']; | ||
| 26 | } | 26 | } |
| 27 | else{ | 27 | else{ // autres cas |
| 28 | $content = Security::secureHTML($json['content']); | 28 | $content = Security::secureHTML($json['content']); |
| 29 | } | 29 | } |
| 30 | 30 | ||
| @@ -39,21 +39,25 @@ class ArticleController | |||
| 39 | $director->makeSectionNode(); | 39 | $director->makeSectionNode(); |
| 40 | $node = $director->getNode(); // = <section> | 40 | $node = $director->getNode(); // = <section> |
| 41 | 41 | ||
| 42 | if(is_array($content)){ | 42 | if(is_array($content)){ // cas d'une nouvelle "news" |
| 43 | $date = new \DateTime($content['d']); | 43 | $date = new \DateTime($content['d']); |
| 44 | $article = new Article($content['i'], $date, $content['t'], $content['p']); | 44 | $article = new Article($content['i'], $date, $content['t'], $content['p']); |
| 45 | $article_node = new Node('new', 'i' . (string)$date->getTimestamp(), [], count($node->getChildren()) + 1, $node, $node->getPage(), $article); | 45 | $article_node = new Node('new', 'i' . (string)$date->getTimestamp(), [], count($node->getChildren()) + 1, $node, $node->getPage(), $article); |
| 46 | |||
| 47 | // id_node tout juste généré | ||
| 48 | //$article_node->getId(); | ||
| 49 | } | 46 | } |
| 50 | else{ | 47 | else{ // autres cas |
| 51 | $timestamp = time(); | 48 | $timestamp = time(); |
| 52 | $date = new \DateTime; | 49 | $date = new \DateTime; |
| 53 | $date->setTimestamp($timestamp); | 50 | $date->setTimestamp($timestamp); |
| 54 | 51 | ||
| 55 | $article = new Article($content, $date); // le "current" timestamp est obtenu par la BDD | 52 | $article = new Article($content, $date); // le "current" timestamp est obtenu par la BDD |
| 56 | $article_node = new Node('post', 'i' . (string)$timestamp, [], count($node->getChildren()) + 1, $node, $node->getPage(), $article); | 53 | |
| 54 | $placement = $json['placement'] === 'first' ? 0 : count($node->getChildren()) + 1; // | ||
| 55 | $article_node = new Node('post', 'i' . (string)$timestamp, [], $placement, $node, $node->getPage(), $article); | ||
| 56 | |||
| 57 | if($json['placement'] === 'first'){ | ||
| 58 | $node->addChild($article_node); | ||
| 59 | $node->reindexPositions(); // régénère les positions (0 devient 1, 1 devient 2...) | ||
| 60 | } | ||
| 57 | } | 61 | } |
| 58 | 62 | ||
| 59 | $entityManager->persist($article_node); | 63 | $entityManager->persist($article_node); |
diff --git a/src/controller/PageManagementController.php b/src/controller/PageManagementController.php index 4528810..75967eb 100644 --- a/src/controller/PageManagementController.php +++ b/src/controller/PageManagementController.php | |||
| @@ -286,7 +286,12 @@ class PageManagementController | |||
| 286 | $director->getNode()->getNodeData()->setPresentation($presentation); | 286 | $director->getNode()->getNodeData()->setPresentation($presentation); |
| 287 | 287 | ||
| 288 | $entityManager->flush(); | 288 | $entityManager->flush(); |
| 289 | echo json_encode(['success' => true, 'presentation' => $json['presentation'], 'cols_min_width' => $director->getNode()->getNodeData()->getColsMinWidth()]); | 289 | |
| 290 | $response_data = ['success' => true, 'presentation' => $json['presentation']]; | ||
| 291 | if($json['presentation'] === 'grid'){ | ||
| 292 | $response_data['cols_min_width'] = $director->getNode()->getNodeData()->getColsMinWidth(); | ||
| 293 | } | ||
| 294 | echo json_encode($response_data); | ||
| 290 | } | 295 | } |
| 291 | else{ | 296 | else{ |
| 292 | echo json_encode(['success' => false]); | 297 | echo json_encode(['success' => false]); |
diff --git a/src/view/MainBuilder.php b/src/view/MainBuilder.php index fc80cd1..b07fa81 100644 --- a/src/view/MainBuilder.php +++ b/src/view/MainBuilder.php | |||
| @@ -85,10 +85,10 @@ class MainBuilder extends AbstractBuilder | |||
| 85 | <button onclick="renamePageBloc(' . $child_node->getId() . ')">Renommer</button> | 85 | <button onclick="renamePageBloc(' . $child_node->getId() . ')">Renommer</button> |
| 86 | </p>'. "\n"; | 86 | </p>'. "\n"; |
| 87 | // déplacement d'un bloc | 87 | // déplacement d'un bloc |
| 88 | $bloc_edit .= '<div style="display: flex; flex-wrap: wrap;"> | 88 | $bloc_edit .= '<div> |
| 89 | <p> | 89 | <p> |
| 90 | <img class="action_icon" onclick="switchBlocsPositions(' . $child_node->getId() . ', \'up\', \'' . CURRENT_PAGE . '\')" src="assets/arrow-up.svg"> | 90 | <img class="action_icon" onclick="switchBlocsPositions(' . $child_node->getId() . ', \'up\')" src="assets/arrow-up.svg"> |
| 91 | <img class="action_icon" onclick="switchBlocsPositions(' . $child_node->getId() . ', \'down\', \'' . CURRENT_PAGE . '\')" src="assets/arrow-down.svg"> | 91 | <img class="action_icon" onclick="switchBlocsPositions(' . $child_node->getId() . ', \'down\')" src="assets/arrow-down.svg"> |
| 92 | </p>' . "\n"; | 92 | </p>' . "\n"; |
| 93 | // suppression d'un bloc | 93 | // suppression d'un bloc |
| 94 | $bloc_edit .= '<form method="post" action="' . new URL(['page' => CURRENT_PAGE]) . '"> | 94 | $bloc_edit .= '<form method="post" action="' . new URL(['page' => CURRENT_PAGE]) . '"> |
diff --git a/src/view/NewBuilder.php b/src/view/NewBuilder.php index 2d66238..332d92b 100644 --- a/src/view/NewBuilder.php +++ b/src/view/NewBuilder.php | |||
| @@ -69,27 +69,27 @@ class NewBuilder extends AbstractBuilder | |||
| 69 | if($_SESSION['admin']) | 69 | if($_SESSION['admin']) |
| 70 | { | 70 | { |
| 71 | if(Director::$page_path->getLast()->getEndOfPath() === 'article'){ | 71 | if(Director::$page_path->getLast()->getEndOfPath() === 'article'){ |
| 72 | $title_js = 'onclick="openEditor(\'' . $id_title . '\', \'article\')"'; | 72 | $title_js = 'onclick="openEditor(\'' . $id_title . '\')"'; |
| 73 | $modify_title = '<p id="edit-' . $id_title . '"><button ' . $title_js . '><img class="action_icon" src="assets/edit.svg">Titre</button></p>' . "\n"; | 73 | $modify_title = '<p id="edit-' . $id_title . '"><button ' . $title_js . '><img class="action_icon" src="assets/edit.svg">Titre</button></p>' . "\n"; |
| 74 | $close_js_title = 'onclick="closeEditor(\'' . $id_title . '\', \'article\', \'preview\')"'; | 74 | $close_js_title = 'onclick="closeEditor(\'' . $id_title . '\')"'; |
| 75 | $close_editor_title = '<p id="cancel-' . $id_title . '" class="hidden"><button ' . $close_js_title . '>Annuler</button></p>'; | 75 | $close_editor_title = '<p id="cancel-' . $id_title . '" class="hidden"><button ' . $close_js_title . '>Annuler</button></p>'; |
| 76 | $submit_js_title = 'onclick="submitArticle(\'' . $id_title . '\', \'article\')"'; | 76 | $submit_js_title = 'onclick="submitArticle(\'' . $id_title . '\')"'; |
| 77 | $submit_title = '<p id="submit-' . $id_title . '" class="hidden"><button ' . $submit_js_title . '>Valider</button></p>'; | 77 | $submit_title = '<p id="submit-' . $id_title . '" class="hidden"><button ' . $submit_js_title . '>Valider</button></p>'; |
| 78 | $title_buttons = '<div class="button_zone">' . $modify_title . $close_editor_title . $submit_title . '</div>'; | 78 | $title_buttons = '<div class="button_zone">' . $modify_title . $close_editor_title . $submit_title . '</div>'; |
| 79 | 79 | ||
| 80 | $preview_js = 'onclick="openEditor(\'' . $id_preview . '\', \'article\')"'; | 80 | $preview_js = 'onclick="openEditor(\'' . $id_preview . '\')"'; |
| 81 | $modify_preview = '<p id="edit-' . $id_preview . '"><button ' . $preview_js . '><img class="action_icon" src="assets/edit.svg">Aperçu</button></a></p>' . "\n"; | 81 | $modify_preview = '<p id="edit-' . $id_preview . '"><button ' . $preview_js . '><img class="action_icon" src="assets/edit.svg">Aperçu</button></a></p>' . "\n"; |
| 82 | $close_js_preview = 'onclick="closeEditor(\'' . $id_preview . '\', \'article\', \'preview\')"'; | 82 | $close_js_preview = 'onclick="closeEditor(\'' . $id_preview . '\')"'; |
| 83 | $close_editor_preview = '<p id="cancel-' . $id_preview . '" class="hidden"><button ' . $close_js_preview . '>Annuler</button></p>'; | 83 | $close_editor_preview = '<p id="cancel-' . $id_preview . '" class="hidden"><button ' . $close_js_preview . '>Annuler</button></p>'; |
| 84 | $submit_js_preview = 'onclick="submitArticle(\'' . $id_preview . '\', \'article\')"'; | 84 | $submit_js_preview = 'onclick="submitArticle(\'' . $id_preview . '\')"'; |
| 85 | $submit_preview = '<p id="submit-' . $id_preview . '" class="hidden"><button ' . $submit_js_preview . '>Valider</button></p>'; | 85 | $submit_preview = '<p id="submit-' . $id_preview . '" class="hidden"><button ' . $submit_js_preview . '>Valider</button></p>'; |
| 86 | $preview_buttons = '<div class="button_zone">' . $modify_preview . $close_editor_preview . $submit_preview . '</div>'; | 86 | $preview_buttons = '<div class="button_zone">' . $modify_preview . $close_editor_preview . $submit_preview . '</div>'; |
| 87 | 87 | ||
| 88 | $article_js = 'onclick="openEditor(\'' . $id . '\', \'article\')"'; | 88 | $article_js = 'onclick="openEditor(\'' . $id . '\')"'; |
| 89 | $modify_article = '<p id="edit-' . $id . '"><button ' . $article_js . '><img class="action_icon" src="assets/edit.svg">Article</button></p>' . "\n"; | 89 | $modify_article = '<p id="edit-' . $id . '"><button ' . $article_js . '><img class="action_icon" src="assets/edit.svg">Article</button></p>' . "\n"; |
| 90 | $close_js_article = 'onclick="closeEditor(\'' . $id . '\', \'article\')"'; | 90 | $close_js_article = 'onclick="closeEditor(\'' . $id . '\')"'; |
| 91 | $close_editor_article = '<p id="cancel-' . $id . '" class="hidden"><button ' . $close_js_article . '>Annuler</button></p>'; | 91 | $close_editor_article = '<p id="cancel-' . $id . '" class="hidden"><button ' . $close_js_article . '>Annuler</button></p>'; |
| 92 | $submit_js_article = 'onclick="submitArticle(\'' . $id . '\', \'article\')"'; | 92 | $submit_js_article = 'onclick="submitArticle(\'' . $id . '\')"'; |
| 93 | $submit_article = '<p id="submit-' . $id . '" class="hidden"><button ' . $submit_js_article . '>Valider</button></p>'; | 93 | $submit_article = '<p id="submit-' . $id . '" class="hidden"><button ' . $submit_js_article . '>Valider</button></p>'; |
| 94 | $article_buttons = '<div class="button_zone">' . $modify_article . $close_editor_article . $submit_article . '</div>'; | 94 | $article_buttons = '<div class="button_zone">' . $modify_article . $close_editor_article . $submit_article . '</div>'; |
| 95 | 95 | ||
| @@ -105,7 +105,7 @@ class NewBuilder extends AbstractBuilder | |||
| 105 | if(self::$new_article_mode){ | 105 | if(self::$new_article_mode){ |
| 106 | $delete_article = ''; | 106 | $delete_article = ''; |
| 107 | // valider la création d'un nouvel article | 107 | // valider la création d'un nouvel article |
| 108 | $submit_js = 'onclick="submitArticle(\'' . $_GET['id'] . '\', \'' . Director::$page_path->getLast()->getEndOfPath() . '\')"'; | 108 | $submit_js = 'onclick="submitArticle(\'' . $_GET['id'] . '\')"'; |
| 109 | $submit_article = '<p id="save-' . $id . '"><button ' . $submit_js . '><img class="action_icon" src="assets/edit.svg"><span class="delete_button">Tout<br>enregistrer</span></button></p>' . "\n"; | 109 | $submit_article = '<p id="save-' . $id . '"><button ' . $submit_js . '><img class="action_icon" src="assets/edit.svg"><span class="delete_button">Tout<br>enregistrer</span></button></p>' . "\n"; |
| 110 | } | 110 | } |
| 111 | // mode article existant | 111 | // mode article existant |
diff --git a/src/view/NewsBlockBuilder.php b/src/view/NewsBlockBuilder.php index 861949a..4716c63 100644 --- a/src/view/NewsBlockBuilder.php +++ b/src/view/NewsBlockBuilder.php | |||
| @@ -53,7 +53,7 @@ class NewsBlockBuilder extends AbstractBuilder | |||
| 53 | $close_js = 'onclick="closeEditor(\'' . $id . '\')"'; | 53 | $close_js = 'onclick="closeEditor(\'' . $id . '\')"'; |
| 54 | $close_editor = '<p id="cancel-' . $id . '" class="hidden"><button ' . $close_js . '>Annuler</button></p>'; | 54 | $close_editor = '<p id="cancel-' . $id . '" class="hidden"><button ' . $close_js . '>Annuler</button></p>'; |
| 55 | 55 | ||
| 56 | $submit_js = 'onclick="submitArticle(\'' . $id . '\', \'\', clone' . $this->id_node . ')"'; | 56 | $submit_js = 'onclick="submitArticle(\'' . $id . '\', clone' . $this->id_node . ')"'; |
| 57 | $submit_article = '<p id="submit-' . $id . '" class="hidden"><button ' . $submit_js . '>Valider</button></p>'; | 57 | $submit_article = '<p id="submit-' . $id . '" class="hidden"><button ' . $submit_js . '>Valider</button></p>'; |
| 58 | 58 | ||
| 59 | $html = ''; | 59 | $html = ''; |
diff --git a/src/view/PostBlockBuilder.php b/src/view/PostBlockBuilder.php index 8ad0498..017e78e 100644 --- a/src/view/PostBlockBuilder.php +++ b/src/view/PostBlockBuilder.php | |||
| @@ -29,6 +29,7 @@ class PostBlockBuilder extends AbstractBuilder | |||
| 29 | } | 29 | } |
| 30 | 30 | ||
| 31 | // ajouter un article | 31 | // ajouter un article |
| 32 | // => fait un peu double emploi avec PostBuilder | ||
| 32 | $new_article = ''; | 33 | $new_article = ''; |
| 33 | if($_SESSION['admin']) | 34 | if($_SESSION['admin']) |
| 34 | { | 35 | { |
| @@ -52,12 +53,20 @@ class PostBlockBuilder extends AbstractBuilder | |||
| 52 | 53 | ||
| 53 | $close_js = 'onclick="closeEditor(\'' . $id . '\')"'; | 54 | $close_js = 'onclick="closeEditor(\'' . $id . '\')"'; |
| 54 | $close_editor = '<p id="cancel-' . $id . '" class="hidden"><button ' . $close_js . '>Annuler</button></p>'; | 55 | $close_editor = '<p id="cancel-' . $id . '" class="hidden"><button ' . $close_js . '>Annuler</button></p>'; |
| 56 | |||
| 57 | $position = '<div id="radio-' . $id . '" class="hidden" style="margin: 5px; font-size: 90%;"> | ||
| 58 | Placement:<br> | ||
| 59 | <input type="radio" id="radio_first-' . $id . '" name="article_placement-' . $id . '" value="first" onchange="setArticlePlacement(\'' . $id . '\')"> | ||
| 60 | <label for="radio_start">en premier</label><br> | ||
| 61 | <input type="radio" id="radio_last-' . $id . '" name="article_placement-' . $id . '" value="last" onchange="setArticlePlacement(\'' . $id . '\')"> | ||
| 62 | <label for="radio_end">en dernier</label> | ||
| 63 | </div>'; | ||
| 55 | 64 | ||
| 56 | $submit_js = 'onclick="submitArticle(\'' . $id . '\', \'\', clone' . $this->id_node . ')"'; | 65 | $submit_js = 'onclick="submitArticle(\'' . $id . '\', clone' . $this->id_node . ')"'; |
| 57 | $submit_article = '<p id="submit-' . $id . '" class="hidden"><button ' . $submit_js . '>Valider</button></p>'; | 66 | $submit_article = '<p id="submit-' . $id . '" class="hidden"><button ' . $submit_js . '>Valider</button></p>'; |
| 58 | 67 | ||
| 59 | $html = ''; | 68 | $html = ''; |
| 60 | $admin_buttons = $new_button . $modify_article . $up_button . $down_button . $delete_article . $close_editor . $submit_article; | 69 | $admin_buttons = $new_button . $modify_article . $up_button . $down_button . $delete_article . $close_editor . $submit_article . $position; |
| 61 | 70 | ||
| 62 | // squelette d'un nouvel article | 71 | // squelette d'un nouvel article |
| 63 | ob_start(); | 72 | ob_start(); |
diff --git a/src/view/PostBuilder.php b/src/view/PostBuilder.php index 03a6990..ae5cc9b 100644 --- a/src/view/PostBuilder.php +++ b/src/view/PostBuilder.php | |||
| @@ -30,6 +30,7 @@ class PostBuilder extends AbstractBuilder | |||
| 30 | $share_button = '<p class="share" ' . $share_js . '><img class="action_icon" src="assets/share.svg"></p>' . "\n"; | 30 | $share_button = '<p class="share" ' . $share_js . '><img class="action_icon" src="assets/share.svg"></p>' . "\n"; |
| 31 | 31 | ||
| 32 | // modifier un article | 32 | // modifier un article |
| 33 | // => fait un peu double emploi avec PostBlockBuilder | ||
| 33 | $admin_buttons = ''; | 34 | $admin_buttons = ''; |
| 34 | if($_SESSION['admin']) | 35 | if($_SESSION['admin']) |
| 35 | { | 36 | { |
diff --git a/src/view/templates/head.php b/src/view/templates/head.php index b74335e..83a0e7c 100644 --- a/src/view/templates/head.php +++ b/src/view/templates/head.php | |||
| @@ -7,6 +7,7 @@ | |||
| 7 | <link rel="icon" type="image/png" href="<?= $favicon ?>" alt="<?= $alt ?>"> | 7 | <link rel="icon" type="image/png" href="<?= $favicon ?>" alt="<?= $alt ?>"> |
| 8 | <meta name="description" content="<?= $description ?>"> | 8 | <meta name="description" content="<?= $description ?>"> |
| 9 | <meta name="viewport" content="width=device-width"> | 9 | <meta name="viewport" content="width=device-width"> |
| 10 | <script>window.Config = {page: "<?= CURRENT_PAGE ?>"};</script> | ||
| 10 | <?= $css ?> | 11 | <?= $css ?> |
| 11 | <?= $js ?> | 12 | <?= $js ?> |
| 12 | </head> \ No newline at end of file | 13 | </head> \ No newline at end of file |
