// code à réorganiser
// seules certaines fonctions ont leur place dans Editor, d'autres servent à manipuler les articles d'une autre manière (déplacer, supprimer...)
// => encapsuler Editor dans une classe Article (comme la balise) qui existe même quand l'éditeur est fermé
/* -- utilisé par les évènements -- */
let editors = {};
function openEditor(id){
if(!editors[id]){
editors[id] = new Editor(id); // appel de init à l'intérieur
}
//else{editors[id].reopen();}
}
// placement d'un nouvel article dans un bloc "Articles libres"
function setArticlePlacement(id){
if(editors[id]){
editors[id].setArticlePlacement(id);
}
}
function closeEditor(id, restore_old){
if(editors[id]){
editors[id].close(restore_old);
}
}
function submitArticle(id, clone = null)
{
// bouton Valider de l'éditeur
if(editors[id]){
editors[id].submit(clone);
}
// bouton Tout enregistrer
else if(window.Config.page === "article" && id[0] === 'n'){
if(Object.keys(editors).length === 0){ // vérifier qu'il n'y a pas d'éditeur ouvert
editors[id] = new Editor(id);
editors[id].submit();
}
else{
toastNotify("Un editeur est ouvert. Validez ou annulez d'abord votre saisie dans chaque éditeur.");
}
}
}
// standalone contraîrement aux autres fonctions ici
function deleteArticle(id){
if(confirm('Voulez-vous vraiment supprimer cet article ?'))
{
// Envoyer une requête au serveur pour supprimer l'article
fetch('index.php?action=delete_article', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ id: id })
})
.then(response => response.json())
.then(data => {
if(data.success)
{
// Supprimer l'article du DOM
const articleElement = document.getElementById(id);
findParentByTagName(articleElement, 'article').remove();
toastNotify("L'article a été supprimé.");
}
else{
toastNotify('Erreur lors de la suppression de l\'article.');
}
})
.catch(error => {
console.error('Erreur:', error);
});
}
}
class Editor
{
extensions_white_list = ['pdf', 'rtf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp'];
// = $extensions_white_list côté PHP
constructor(id){
this.id = id;
this.article = document.getElementById(this.id);
this.creation_mode = this.id[0] === 'n' ? true : false;
//this.isOpen = false;
this.tiny_instance = null;
// moche, on ne devrait sortir l'envoi des données avec fetch de Editor.submit
if(!this.creation_mode || window.Config.page !== 'article'){
if(this.creation_mode && window.Config.page !== 'article'){
this.setArticlePlacement(this.id);
}
else{
// insérer le contenu de l'article dans l'éditeur
this.article.setAttribute('data-original-content', this.article.innerHTML);
}
this.init();
}
//else // bouton Tout enregistrer, pas d'éditeur
}
setArticlePlacement(id_block){
const checked_button = document.querySelector('input[name="article_placement-' + id_block + '"]:checked');
if(checked_button){ // vrai clic
this.placement = checked_button.value;
}
else{
document.getElementById('radio_last-' + id_block).checked = true; // faux clic
this.placement = 'last';
}
}
init(){
tinymce.init({
selector: `[id="${this.id}"]`, // écrire [id="246"] au lieu de #246 parce que l'id commence par un chiffre
language: 'fr_FR',
language_url: 'js/tinymce-langs/fr_FR.js',
license_key: 'gpl',
branding: false,
plugins: 'lists link autolink table image media autoresize help',
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',
menubar: false,
toolbar_mode: 'wrap',
statusbar: false,
link_title: false, // supprime le champ compliqué "titre" (apparaît au survol du lien) dans la fenêtre "link"
/*link_attributes_postprocess: (attrs) => { // modifier les attributs des liens créés
console.log(attrs);
if (attrs.rel) {
attrs.rel += 'noreferrer'; // cacher la page d'où on vient
}
},*/
// les fonctions fléchées permettent de garder le contexte (= this)
setup: (editor) => {
editor.on('init', () => {
this.tiny_instance = editor;
// boutons "Modifier", "Supprimer", "déplacer vers le haut", "déplacer vers le bas", "Annuler" et "Soumettre"
document.getElementById(`cancel-${this.id}`).classList.remove('hidden');
document.getElementById(`submit-${this.id}`).classList.remove('hidden');
const radio = document.getElementById(`radio-${this.id}`);
if(radio){
radio.classList.remove('hidden');
}
if(this.creation_mode){
document.getElementById(`new-${this.id}`).classList.add('hidden'); // id = new-new-id_node
}
else{
document.getElementById(`edit-${this.id}`).classList.add('hidden');
if(window.Config.page !== 'article'){
document.getElementById(`position_up-${this.id}`).classList.add('hidden');
document.getElementById(`position_down-${this.id}`).classList.add('hidden');
document.getElementById(`delete-${this.id}`).classList.add('hidden');
}
}
});
let skipPastePreProcess = false;
editor.on('Paste', (e) => { // déclenchement AVANT PastePreProcess et quelque que soit le contenu collé
if(!e.clipboardData){ // e.clipboardData: DataTransfer
return;
}
const items = e.clipboardData.items; // base64
const files = e.clipboardData.files; // explorateur de fichiers
let found_file = false;
// données dans files
if(files && files.length > 0){ // noter que files peut être vide, alors que items non
for(let i = 0; i < files.length; i++){
let file = files[i];
if(this.extensions_white_list.includes(file.name.split('.').pop()?.toLowerCase() || '')){
found_file = true;
this.uploadDocument(file, editor);
}
else if(file.type.indexOf('image') !== -1){
found_file = true;
this.uploadImageBase64(file, editor);
}
}
}
// données dans items
else{ // les images collées depuis l'explorateur sont aussi dans items, or elles sont déjà gérées plus haut
for(let i = 0; i < items.length; i++){
let item = items[i];
if(item.type.indexOf('image') !== -1){ // test type MIME contenant image
found_file = true;
const file = item.getAsFile(); // presse-papier => fichier lisible
if(file){
this.uploadImageBase64(file, editor);
}
else{
console.error('fichier invalide');
}
}
}
}
if(found_file){
e.preventDefault(); // supprime le collage automatiue
skipPastePreProcess = true; // désactiver le PastePreProcess pour ce collage
}
});
editor.on('PastePreProcess', function (e){ // déclenchement au collage AVANT insertion dans l'éditeur
if(skipPastePreProcess){
skipPastePreProcess = false; // réinitialiser pour la prochaine fois
return; // ignorer ce traitement
}
const parser = new DOMParser();
const doc = parser.parseFromString(e.content, 'text/html');
const images = doc.querySelectorAll('img');
let downloads_in_progress = [];
images.forEach(img => {
if(img.src.startsWith('file://')){ // détection d'images non insérables
console.warn('Image locale non insérable dans tinymce :', img.src);
img.outerHTML = `
"Image locale non insérée (vient-elle d'un document LibreOffice ?). Effacez ce message rouge et copiez-collez l'image seule.
`;
}
else if(img.src.startsWith('http')){ // détection d'images web
const promise = fetch('index.php?action=upload_image_url', { // promesse d'un fichier téléchargeable sur le serveur
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image_url: img.src })
})
.then(response => response.json())
.then(data => {
if(data.location){
img.src = data.location; // remplacer l'image par celle du serveur
}
})
.catch(error => {
console.error('Erreur lors de l’upload de l’image distante:', error);
});
downloads_in_progress.push(promise);
}
});
// une image web ou plus: différer l'insertion dans l'éditeur le temps que le serveur télécharge les images
if(downloads_in_progress.length > 0){
e.preventDefault();
Promise.all(downloads_in_progress).then(() => {
e.content = doc.body.innerHTML; // remplacement du HTML dans l'éditeur par la copie modifiée (doc)
editor.insertContent(e.content);
});
}
else{
e.content = doc.body.innerHTML; // remplacement du HTML dans l'éditeur par la copie modifiée (doc)
}
});
// glisser-déposer de fichiers (sauf images qui sont déjà gérées nativement)
editor.on('drop', (e) => {
const data = e.dataTransfer;
if(!data || !data.files || data.files.length === 0){
return; // Laisser TinyMCE gérer (texte, images déjà supportées, etc.)
}
const files = data.files;
let has_documents = false;
for(let i = 0; i < files.length; i++){
if(this.extensions_white_list.includes(files[i].name.split('.').pop()?.toLowerCase() || '')){
has_documents = true;
break;
}
}
if(has_documents){
e.preventDefault();
e.stopPropagation();
for(let i = 0; i < files.length; i++){
let file = files[i];
if(this.extensions_white_list.includes(file.name.split('.').pop()?.toLowerCase() || '')){
this.uploadDocument(file, editor);
}
else if(file.type.indexOf('image') !== -1){
this.uploadImageBase64(file, editor);
}
}
}
// autres cas: tinymce gère tout seul
});
},
// upload d'image avec le bouton "Insérer une image"
images_upload_handler: this.images_upload_handler, // = fonction fléchée
// upload de documents avec le bouton "insérer un lien"
files_upload_handler: this.files_upload_handler, // = fonction fléchée
documents_file_types: [ // files_upload_handler a besoin qu'on lui donne tous les types mime
{ mimeType: 'application/pdf', extensions: [ 'pdf' ] },
{ mimeType: 'application/rtf', extensions: [ 'rtf' ] },
{ mimeType: 'application/msword', extensions: [ 'doc' ] },
{ mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', extensions: [ 'docx' ] },
{ mimeType: 'application/vnd.ms-excel', extensions: [ 'xls' ] },
{ mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', extensions: [ 'xlsx' ] },
{ mimeType: 'application/vnd.ms-powerpoint', extensions: [ 'ppt' ] },
{ mimeType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', extensions: [ 'pptx' ] },
{ mimeType: 'application/vnd.oasis.opendocument.text', extensions: [ 'odt' ] },
{ mimeType: 'application/vnd.oasis.opendocument.spreadsheet', extensions: [ 'ods' ] },
{ mimeType: 'application/vnd.oasis.opendocument.presentation', extensions: [ 'odp' ] }
],
image_caption: true
});
}
close(restore_old = true){
tinymce.remove(`[id="${this.id}"]`); // comme dans tinymce.init
delete editors[this.id];
// Restaurer le contenu d'origine de l'article
if(restore_old){
const original_content = document.getElementById(this.id).getAttribute('data-original-content');
document.getElementById(this.id).innerHTML = original_content;
}
// boutons: "Nouvel article", Modifier", "Supprimer", "déplacer vers le haut", "déplacer vers le bas", "Annuler" et "Valider"
document.getElementById(`cancel-${this.id}`).classList.add('hidden');
document.getElementById(`submit-${this.id}`).classList.add('hidden');
const radio = document.getElementById(`radio-${this.id}`);
if(radio){
document.querySelector('input[name="article_placement-' + this.id + '"]:checked').checked = false; // décoche l'option "en mémoire"
radio.classList.add('hidden');
}
if(this.creation_mode){
document.getElementById(`new-${this.id}`).classList.remove('hidden'); // id = new-new-id_node
}
else{
document.getElementById(`edit-${this.id}`).classList.remove('hidden');
if(window.Config.page !== 'article'){
document.getElementById(`position_up-${this.id}`).classList.remove('hidden');
document.getElementById(`position_down-${this.id}`).classList.remove('hidden');
document.getElementById(`delete-${this.id}`).classList.remove('hidden');
}
}
}
submit(clone = null){
let content;
const params = new URL(document.location).searchParams; // "search" = ? et paramètres, searchParams = objet avec des getters
// à comparer avec: new URLSearchParams(window.location.search);
// c'est pareil ou pas?
// clic sur "Tout enregistrer" (ne devrait pas se situer dans Editor)
if(this.creation_mode && window.Config.page === 'article'){
const prefixes = ['t', 'p', 'i', 'd'];
const allElemsWithId = document.querySelectorAll('.data');
content = {};
let id_from_builder;
allElemsWithId.forEach(element => {
const first_letter = element.id.charAt(0).toLowerCase();
if(prefixes.includes(first_letter)){
content[first_letter] = element.innerHTML;
if(first_letter === 'i'){
id_from_builder = element.id;
}
else if(first_letter === 'd'){
content[first_letter] = element.getAttribute('date-utc');
}
}
})
content['d'] = new Date(content['d']).toISOString().slice(0, 16); // date UTC, format: 2025-09-18T15:21
}
// champs à remplir des nouvelles "news"
else if(window.Config.page === 'article' && params != null && params.get("id")[0] === 'n'){
this.close(false);
return;
}
// dans les autres cas, on doit pouvoir récupérer l'éditeur
else{
// l'éditeur correspond à l'article OU si page = "article" à un élément: titre, aperçu, article
if(!this.tiny_instance){
console.error("Éditeur non trouvé pour l'article:", this.id);
return;
}
content = this.tiny_instance.getContent();
}
let fetch_params = {
id: this.id,
content: content,
from: new URLSearchParams(window.location.search).get('from') // le "$_GET" de javascript
};
if(this.placement){
fetch_params['placement'] = this.placement;
}
// Envoi AJAX au serveur
fetch('index.php?action=editor_submit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(fetch_params)
})
.then(response => response.json())
.then(data => {
if(data.success)
{
if(this.creation_mode && window.Config.page === 'article'){
console.log('données envoyées au serveur avec succès.');
// redirection page de l'article
window.setTimeout(function(){
const url_params = new URLSearchParams(window.location.search); // le "$_GET" de javascript
location.href = "index.php?page=article&id=" + data.article_id + "&from=" + url_params.get('from');
}, 0);
}
else{
// Fermer l'éditeur et mettre à jour le contenu de l'article
this.close(false);
if(this.creation_mode){
makeNewArticleButtons(this.id, data.article_id, clone, this.placement);
}
}
}
else{
alert('Erreur lors de la sauvegarde de l\'article.');
}
})
.catch(error => {
console.error('Erreur:', error);
});
}
//reopen(){}
/*destroy(){
this.close();
delete editors[this.id];
console.log(`Editor ${this.id} détruit.`);
}*/
images_upload_handler = (blobInfo, progress) => new Promise((resolve, reject) => {
const formData = new FormData();
formData.append("file", blobInfo.blob());
fetch('index.php?action=upload_image_tinymce', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if(data.location){
resolve(data.location);
}
else{
reject("Erreur: Chemin d'image invalide");
}
})
.catch(error => {
reject("Erreur lors de l'upload");
});
});
files_upload_handler = (blobInfo, progress) => new Promise((resolve, reject) => { // utilisation = bouton "link" (OU drag & drop, et oui)
const formData = new FormData();
formData.append("file", blobInfo.blob());
fetch('index.php?action=upload_file_tinymce', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if(data.location){
// resolve et reject fonctionne avec Promise => type de retour standardisé et évite l'utilistion de callbacks
resolve({
url: data.location,
fileName: blobInfo.filename(),
});
}
else{
reject("Erreur: Chemin du fichier invalide");
}
})
.catch(error => {
reject("Erreur lors de l'upload");
});
});
uploadImageBase64(file, editor){
const reader = new FileReader();
reader.onload = function (event){ // fonction exécutée lorsque reader.readAsDataURL(file) est terminée
const base64_target = event.target;
if(!base64_target || !base64_target.result){
console.error("erreur de lecture du fichier");
return;
}
fetch('index.php?action=upload_image_base64', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image_base64: base64_target.result })
})
.then(response => response.json())
.then(data => {
if(data.location){
editor.insertContent('
');
}
})
.catch(error => {
console.error('Erreur lors de l’upload de l’image base64 :', error);
});
};
reader.readAsDataURL(file); // lecture asynchrone du fichier
}
uploadDocument(file, editor){ // utilisation = copier-coller de l'explorateur de fichiers
const formData = new FormData();
formData.append("file", file);
fetch('index.php?action=upload_file_tinymce', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if(data.location){
// créer le lien
const file_name = file.name;
const extension = file_name.split('.').pop()?.toLowerCase() || '';
const target = extension === 'pdf' ? 'target="_blank"' : ''; // PDF = page
editor.insertContent(`[${extension}] ${file_name}`);
}
else {
console.error("Erreur: Chemin du fichier invalide");
}
})
.catch(error => {
console.error("Erreur lors de l'upload du document :", error);
});
}
}
// restera ici jusqu'à ce que la gestion des balises soient faite ailleurs
function makeNewArticleButtons(id, article_id, clone, placement = 'last')
{
let share_btn = document.querySelector(`.share.hidden`); // combinaison de deux classes
let new_btn = document.getElementById(`new-${id}`);
let edit_btn = document.getElementById(`edit-${id}`);
let pos_up_btn = document.getElementById(`position_up-${id}`);
let pos_down_btn = document.getElementById(`position_down-${id}`);
let delete_btn = document.getElementById(`delete-${id}`);
let cancel_btn = document.getElementById(`cancel-${id}`);
let submit_btn = document.getElementById(`submit-${id}`);
share_btn.classList.remove('hidden');
new_btn.classList.add('hidden');
edit_btn.classList.remove('hidden');
pos_up_btn.classList.remove('hidden');
pos_down_btn.classList.remove('hidden');
delete_btn.classList.remove('hidden');
//cancel_btn.classList.add('hidden');
//submit_btn.classList.add('hidden');
let article = document.getElementById(id);
let article_elem_parent = findParentByTagName(article, 'article');
share_btn.setAttribute('onclick', "copyInClipBoard('" + window.location.href + article_id + "')"); // # de l'ancre ajouté au clic sur le lien ouvrant l'éditeur
article.id = article_id;
edit_btn.id = 'edit-' + article_id;
edit_btn.querySelector('.action_icon').setAttribute('onclick', "openEditor('" + article_id + "')");
pos_up_btn.id = 'position_up-' + article_id;
pos_up_btn.querySelector('.action_icon').setAttribute('onclick', "switchPositions('" + article_id + "', 'up')");
pos_down_btn.id = 'position_down-' + article_id;
pos_down_btn.querySelector('.action_icon').setAttribute('onclick', "switchPositions('" + article_id + "', 'down')");
delete_btn.id = 'delete-' + article_id;
delete_btn.querySelector('.action_icon').setAttribute('onclick', "deleteArticle('" + article_id + "')");
cancel_btn.id = 'cancel-' + article_id;
cancel_btn.querySelector('button').setAttribute('onclick', "closeEditor('" + article_id + "')");
submit_btn.id = 'submit-' + article_id;
submit_btn.querySelector('button').setAttribute('onclick', "submitArticle('" + article_id + "')");
let section_child = article_elem_parent.parentNode.querySelector('.section_child'); // renommer section_child
// parentNode vise la balise section
article_elem_parent.parentNode.replaceChild(clone.cloneNode(true), article_elem_parent); // clone du squelette pour le garder intact
if(placement === 'first'){
section_child.insertBefore(article_elem_parent, section_child.firstChild);
}
else{ // = 'last'
section_child.appendChild(article_elem_parent);
}
}