diff options
| author | polo <ordipolo@gmx.fr> | 2026-01-12 17:16:41 +0100 |
|---|---|---|
| committer | polo <ordipolo@gmx.fr> | 2026-01-12 17:16:41 +0100 |
| commit | 505cab0210e83bf62085b665abad0698a8d31352 (patch) | |
| tree | 6e66d20d2041bae3f800ee99fdde1957c7bb9f51 /public/js | |
| parent | dfed030ca6edf832397d273fbb0ad4b0b5d35877 (diff) | |
| download | cms-505cab0210e83bf62085b665abad0698a8d31352.tar.gz cms-505cab0210e83bf62085b665abad0698a8d31352.tar.bz2 cms-505cab0210e83bf62085b665abad0698a8d31352.zip | |
upload de fichiers (JS + contrôleur + htaccess), collage de fichiers dans l'éditeur (JS), amélioration utilisation de Imagick (ImageUploadController)
Diffstat (limited to 'public/js')
| -rw-r--r-- | public/js/tinymce.js | 261 |
1 files changed, 201 insertions, 60 deletions
diff --git a/public/js/tinymce.js b/public/js/tinymce.js index a187189..7b59506 100644 --- a/public/js/tinymce.js +++ b/public/js/tinymce.js | |||
| @@ -72,6 +72,9 @@ function deleteArticle(id){ | |||
| 72 | 72 | ||
| 73 | class Editor | 73 | class Editor |
| 74 | { | 74 | { |
| 75 | extensions_white_list = ['pdf', 'rtf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp']; | ||
| 76 | // = $extensions_white_list côté PHP | ||
| 77 | |||
| 75 | constructor(id){ | 78 | constructor(id){ |
| 76 | this.id = id; | 79 | this.id = id; |
| 77 | this.article = document.getElementById(this.id); | 80 | this.article = document.getElementById(this.id); |
| @@ -108,7 +111,7 @@ class Editor | |||
| 108 | tinymce.init({ | 111 | tinymce.init({ |
| 109 | selector: `[id="${this.id}"]`, // écrire [id="246"] au lieu de #246 parce que l'id commence par un chiffre | 112 | selector: `[id="${this.id}"]`, // écrire [id="246"] au lieu de #246 parce que l'id commence par un chiffre |
| 110 | language: 'fr_FR', | 113 | language: 'fr_FR', |
| 111 | language_url: 'js/tinymce-langs/fr_FR.js', // téléchargement ici: https://cdn.jsdelivr.net/npm/tinymce-lang/langs/fr_FR.min.js | 114 | language_url: 'js/tinymce-langs/fr_FR.js', |
| 112 | license_key: 'gpl', | 115 | license_key: 'gpl', |
| 113 | branding: false, | 116 | branding: false, |
| 114 | plugins: 'lists link autolink table image media autoresize help', | 117 | plugins: 'lists link autolink table image media autoresize help', |
| @@ -116,6 +119,13 @@ class Editor | |||
| 116 | menubar: false, | 119 | menubar: false, |
| 117 | toolbar_mode: 'wrap', | 120 | toolbar_mode: 'wrap', |
| 118 | statusbar: false, | 121 | statusbar: false, |
| 122 | link_title: false, // supprime le champ compliqué "titre" (apparaît au survol du lien) dans la fenêtre "link" | ||
| 123 | /*link_attributes_postprocess: (attrs) => { // modifier les attributs des liens créés | ||
| 124 | console.log(attrs); | ||
| 125 | if (attrs.rel) { | ||
| 126 | attrs.rel += 'noreferrer'; // cacher la page d'où on vient | ||
| 127 | } | ||
| 128 | },*/ | ||
| 119 | // les fonctions fléchées permettent de garder le contexte (= this) | 129 | // les fonctions fléchées permettent de garder le contexte (= this) |
| 120 | setup: (editor) => { | 130 | setup: (editor) => { |
| 121 | editor.on('init', () => { | 131 | editor.on('init', () => { |
| @@ -141,55 +151,61 @@ class Editor | |||
| 141 | } | 151 | } |
| 142 | }); | 152 | }); |
| 143 | let skipPastePreProcess = false; | 153 | let skipPastePreProcess = false; |
| 144 | editor.on('Paste', function (e){ // déclenchement AVANT PastePreProcess et quelque que soit le contenu collé | 154 | editor.on('Paste', (e) => { // déclenchement AVANT PastePreProcess et quelque que soit le contenu collé |
| 145 | const clipboardData = (e.clipboardData || e.originalEvent.clipboardData); | 155 | if(!e.clipboardData){ // e.clipboardData: DataTransfer |
| 146 | if(!clipboardData){ | ||
| 147 | return; | 156 | return; |
| 148 | } | 157 | } |
| 149 | const items = clipboardData.items; | 158 | const items = e.clipboardData.items; // base64 |
| 150 | let foundImage = false; | 159 | const files = e.clipboardData.files; // explorateur de fichiers |
| 151 | 160 | let found_file = false; | |
| 152 | for(let i = 0; i < items.length; i++){ | 161 | |
| 153 | let item = items[i]; | 162 | // données dans files |
| 154 | 163 | if(files && files.length > 0){ // noter que files peut être vide, alors que items non | |
| 155 | if(item.type.indexOf('image') !== -1){ // test type MIME contenant image | 164 | for(let i = 0; i < files.length; i++){ |
| 156 | foundImage = true; | 165 | let file = files[i]; |
| 157 | 166 | ||
| 158 | const file = item.getAsFile(); // presse-papier => fichier lisible | 167 | if(this.extensions_white_list.includes(file.name.split('.').pop()?.toLowerCase() || '')){ |
| 159 | const reader = new FileReader(); | 168 | found_file = true; |
| 160 | 169 | this.uploadDocument(file, editor); | |
| 161 | reader.onload = function (event){ // fonction exécutée lorsque reader.readAsDataURL(file) est terminée | 170 | } |
| 162 | const base64Data = event.target.result; // données de l'image | 171 | else if(file.type.indexOf('image') !== -1){ |
| 163 | 172 | found_file = true; | |
| 164 | fetch('index.php?action=upload_image_base64', { | 173 | this.uploadImageBase64(file, editor); |
| 165 | method: 'POST', | 174 | } |
| 166 | headers: { 'Content-Type': 'application/json' }, | 175 | } |
| 167 | body: JSON.stringify({ image_base64: base64Data }) | 176 | } |
| 168 | }) | 177 | // données dans items |
| 169 | .then(response => response.json()) | 178 | else{ // les images collées depuis l'explorateur sont aussi dans items, or elles sont déjà gérées plus haut |
| 170 | .then(data => { | 179 | for(let i = 0; i < items.length; i++){ |
| 171 | if(data.location){ | 180 | let item = items[i]; |
| 172 | editor.insertContent('<img src="' + data.location + '">'); | 181 | |
| 173 | } | 182 | if(item.type.indexOf('image') !== -1){ // test type MIME contenant image |
| 174 | }) | 183 | found_file = true; |
| 175 | .catch(error => { | 184 | const file = item.getAsFile(); // presse-papier => fichier lisible |
| 176 | console.error('Erreur lors de l’upload de l’image base64 :', error); | 185 | if(file){ |
| 177 | }); | 186 | this.uploadImageBase64(file, editor); |
| 178 | }; | 187 | } |
| 179 | reader.readAsDataURL(file); // lecture asynchrone du fichier | 188 | else{ |
| 189 | console.error('fichier invalide'); | ||
| 190 | } | ||
| 191 | } | ||
| 180 | } | 192 | } |
| 181 | } | 193 | } |
| 182 | 194 | ||
| 183 | if(foundImage){ | 195 | if(found_file){ |
| 184 | e.preventDefault(); // supprime le collage automatiue | 196 | e.preventDefault(); // supprime le collage automatiue |
| 185 | skipPastePreProcess = true; // désactiver le PastePreProcess pour ce collage | 197 | skipPastePreProcess = true; // désactiver le PastePreProcess pour ce collage |
| 186 | } | 198 | } |
| 187 | }); | 199 | }); |
| 188 | editor.on('PastePreProcess', function (e){ // déclenchement au collage AVANT insertion dans l'éditeur | 200 | editor.on('PastePreProcess', function (e){ // déclenchement au collage AVANT insertion dans l'éditeur |
| 201 | if(skipPastePreProcess){ | ||
| 202 | skipPastePreProcess = false; // réinitialiser pour la prochaine fois | ||
| 203 | return; // ignorer ce traitement | ||
| 204 | } | ||
| 205 | |||
| 189 | const parser = new DOMParser(); | 206 | const parser = new DOMParser(); |
| 190 | const doc = parser.parseFromString(e.content, 'text/html'); | 207 | const doc = parser.parseFromString(e.content, 'text/html'); |
| 191 | const images = doc.querySelectorAll('img'); | 208 | const images = doc.querySelectorAll('img'); |
| 192 | |||
| 193 | let downloads_in_progress = []; | 209 | let downloads_in_progress = []; |
| 194 | 210 | ||
| 195 | images.forEach(img => { | 211 | images.forEach(img => { |
| @@ -221,7 +237,6 @@ class Editor | |||
| 221 | // une image web ou plus: différer l'insertion dans l'éditeur le temps que le serveur télécharge les images | 237 | // une image web ou plus: différer l'insertion dans l'éditeur le temps que le serveur télécharge les images |
| 222 | if(downloads_in_progress.length > 0){ | 238 | if(downloads_in_progress.length > 0){ |
| 223 | e.preventDefault(); | 239 | e.preventDefault(); |
| 224 | |||
| 225 | Promise.all(downloads_in_progress).then(() => { | 240 | Promise.all(downloads_in_progress).then(() => { |
| 226 | e.content = doc.body.innerHTML; // remplacement du HTML dans l'éditeur par la copie modifiée (doc) | 241 | e.content = doc.body.innerHTML; // remplacement du HTML dans l'éditeur par la copie modifiée (doc) |
| 227 | editor.insertContent(e.content); | 242 | editor.insertContent(e.content); |
| @@ -230,30 +245,58 @@ class Editor | |||
| 230 | else{ | 245 | else{ |
| 231 | e.content = doc.body.innerHTML; // remplacement du HTML dans l'éditeur par la copie modifiée (doc) | 246 | e.content = doc.body.innerHTML; // remplacement du HTML dans l'éditeur par la copie modifiée (doc) |
| 232 | } | 247 | } |
| 233 | }); // fin editor.on('PastePreProcess'... | 248 | }); |
| 234 | }, | 249 | // glisser-déposer de fichiers (sauf images qui sont déjà gérées nativement) |
| 235 | // upload d'image natif de tinymce avec le bouton "Insérer une image" | 250 | editor.on('drop', (e) => { |
| 236 | images_upload_handler: (blobInfo, progress) => new Promise((resolve, reject) => { | 251 | const data = e.dataTransfer; |
| 237 | const formData = new FormData(); | 252 | if(!data || !data.files || data.files.length === 0){ |
| 238 | formData.append("file", blobInfo.blob()); | 253 | return; // Laisser TinyMCE gérer (texte, images déjà supportées, etc.) |
| 239 | |||
| 240 | fetch("index.php?action=upload_image_tinymce", { | ||
| 241 | method: "POST", | ||
| 242 | body: formData | ||
| 243 | }) | ||
| 244 | .then(response => response.json()) | ||
| 245 | .then(data => { | ||
| 246 | if(data.location) { | ||
| 247 | resolve(data.location); | ||
| 248 | } | 254 | } |
| 249 | else { | 255 | const files = data.files; |
| 250 | reject("Erreur: Chemin d'image invalide"); | 256 | |
| 257 | let has_documents = false; | ||
| 258 | for(let i = 0; i < files.length; i++){ | ||
| 259 | if(this.extensions_white_list.includes(files[i].name.split('.').pop()?.toLowerCase() || '')){ | ||
| 260 | has_documents = true; | ||
| 261 | break; | ||
| 262 | } | ||
| 251 | } | 263 | } |
| 252 | }) | 264 | |
| 253 | .catch(error => { | 265 | if(has_documents){ |
| 254 | reject("Erreur lors de l'upload"); | 266 | e.preventDefault(); |
| 267 | e.stopPropagation(); | ||
| 268 | |||
| 269 | for(let i = 0; i < files.length; i++){ | ||
| 270 | let file = files[i]; | ||
| 271 | |||
| 272 | if(this.extensions_white_list.includes(file.name.split('.').pop()?.toLowerCase() || '')){ | ||
| 273 | this.uploadDocument(file, editor); | ||
| 274 | } | ||
| 275 | else if(file.type.indexOf('image') !== -1){ | ||
| 276 | this.uploadImageBase64(file, editor); | ||
| 277 | } | ||
| 278 | } | ||
| 279 | } | ||
| 280 | // autres cas: tinymce gère tout seul | ||
| 255 | }); | 281 | }); |
| 256 | }), | 282 | }, |
| 283 | // upload d'image avec le bouton "Insérer une image" | ||
| 284 | images_upload_handler: this.images_upload_handler, // = fonction fléchée | ||
| 285 | // upload de documents avec le bouton "insérer un lien" | ||
| 286 | files_upload_handler: this.files_upload_handler, // = fonction fléchée | ||
| 287 | documents_file_types: [ // files_upload_handler a besoin qu'on lui donne tous les types mime | ||
| 288 | { mimeType: 'application/pdf', extensions: [ 'pdf' ] }, | ||
| 289 | { mimeType: 'application/rtf', extensions: [ 'rtf' ] }, | ||
| 290 | { mimeType: 'application/msword', extensions: [ 'doc' ] }, | ||
| 291 | { mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', extensions: [ 'docx' ] }, | ||
| 292 | { mimeType: 'application/vnd.ms-excel', extensions: [ 'xls' ] }, | ||
| 293 | { mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', extensions: [ 'xlsx' ] }, | ||
| 294 | { mimeType: 'application/vnd.ms-powerpoint', extensions: [ 'ppt' ] }, | ||
| 295 | { mimeType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', extensions: [ 'pptx' ] }, | ||
| 296 | { mimeType: 'application/vnd.oasis.opendocument.text', extensions: [ 'odt' ] }, | ||
| 297 | { mimeType: 'application/vnd.oasis.opendocument.spreadsheet', extensions: [ 'ods' ] }, | ||
| 298 | { mimeType: 'application/vnd.oasis.opendocument.presentation', extensions: [ 'odp' ] } | ||
| 299 | ], | ||
| 257 | image_caption: true | 300 | image_caption: true |
| 258 | }); | 301 | }); |
| 259 | } | 302 | } |
| @@ -385,9 +428,107 @@ class Editor | |||
| 385 | delete editors[this.id]; | 428 | delete editors[this.id]; |
| 386 | console.log(`Editor ${this.id} détruit.`); | 429 | console.log(`Editor ${this.id} détruit.`); |
| 387 | }*/ | 430 | }*/ |
| 388 | } | ||
| 389 | 431 | ||
| 432 | images_upload_handler = (blobInfo, progress) => new Promise((resolve, reject) => { | ||
| 433 | const formData = new FormData(); | ||
| 434 | formData.append("file", blobInfo.blob()); | ||
| 435 | |||
| 436 | fetch('index.php?action=upload_image_tinymce', { | ||
| 437 | method: 'POST', | ||
| 438 | body: formData | ||
| 439 | }) | ||
| 440 | .then(response => response.json()) | ||
| 441 | .then(data => { | ||
| 442 | if(data.location){ | ||
| 443 | resolve(data.location); | ||
| 444 | } | ||
| 445 | else{ | ||
| 446 | reject("Erreur: Chemin d'image invalide"); | ||
| 447 | } | ||
| 448 | }) | ||
| 449 | .catch(error => { | ||
| 450 | reject("Erreur lors de l'upload"); | ||
| 451 | }); | ||
| 452 | }); | ||
| 453 | files_upload_handler = (blobInfo, progress) => new Promise((resolve, reject) => { // utilisation = bouton "link" (OU drag & drop, et oui) | ||
| 454 | const formData = new FormData(); | ||
| 455 | formData.append("file", blobInfo.blob()); | ||
| 456 | |||
| 457 | fetch('index.php?action=upload_file_tinymce', { | ||
| 458 | method: 'POST', | ||
| 459 | body: formData | ||
| 460 | }) | ||
| 461 | .then(response => response.json()) | ||
| 462 | .then(data => { | ||
| 463 | if(data.location){ | ||
| 464 | // resolve et reject fonctionne avec Promise => type de retour standardisé et évite l'utilistion de callbacks | ||
| 465 | resolve({ | ||
| 466 | url: data.location, | ||
| 467 | fileName: blobInfo.filename(), | ||
| 468 | }); | ||
| 469 | } | ||
| 470 | else{ | ||
| 471 | reject("Erreur: Chemin du fichier invalide"); | ||
| 472 | } | ||
| 473 | }) | ||
| 474 | .catch(error => { | ||
| 475 | reject("Erreur lors de l'upload"); | ||
| 476 | }); | ||
| 477 | }); | ||
| 478 | |||
| 479 | uploadImageBase64(file, editor){ | ||
| 480 | const reader = new FileReader(); | ||
| 390 | 481 | ||
| 482 | reader.onload = function (event){ // fonction exécutée lorsque reader.readAsDataURL(file) est terminée | ||
| 483 | const base64_target = event.target; | ||
| 484 | if(!base64_target || !base64_target.result){ | ||
| 485 | console.error("erreur de lecture du fichier"); | ||
| 486 | return; | ||
| 487 | } | ||
| 488 | |||
| 489 | fetch('index.php?action=upload_image_base64', { | ||
| 490 | method: 'POST', | ||
| 491 | headers: { 'Content-Type': 'application/json' }, | ||
| 492 | body: JSON.stringify({ image_base64: base64_target.result }) | ||
| 493 | }) | ||
| 494 | .then(response => response.json()) | ||
| 495 | .then(data => { | ||
| 496 | if(data.location){ | ||
| 497 | editor.insertContent('<img src="' + data.location + '">'); | ||
| 498 | } | ||
| 499 | }) | ||
| 500 | .catch(error => { | ||
| 501 | console.error('Erreur lors de l’upload de l’image base64 :', error); | ||
| 502 | }); | ||
| 503 | }; | ||
| 504 | reader.readAsDataURL(file); // lecture asynchrone du fichier | ||
| 505 | } | ||
| 506 | uploadDocument(file, editor){ // utilisation = copier-coller de l'explorateur de fichiers | ||
| 507 | const formData = new FormData(); | ||
| 508 | formData.append("file", file); | ||
| 509 | |||
| 510 | fetch('index.php?action=upload_file_tinymce', { | ||
| 511 | method: 'POST', | ||
| 512 | body: formData | ||
| 513 | }) | ||
| 514 | .then(response => response.json()) | ||
| 515 | .then(data => { | ||
| 516 | if(data.location){ | ||
| 517 | // créer le lien <a> | ||
| 518 | const file_name = file.name; | ||
| 519 | const extension = file_name.split('.').pop()?.toLowerCase() || ''; | ||
| 520 | const target = extension === 'pdf' ? 'target="_blank"' : ''; // PDF = page | ||
| 521 | editor.insertContent(`<a href="${data.location}" ${target} title="${file_name}">[${extension}] ${file_name}</a>`); | ||
| 522 | } | ||
| 523 | else { | ||
| 524 | console.error("Erreur: Chemin du fichier invalide"); | ||
| 525 | } | ||
| 526 | }) | ||
| 527 | .catch(error => { | ||
| 528 | console.error("Erreur lors de l'upload du document :", error); | ||
| 529 | }); | ||
| 530 | } | ||
| 531 | } | ||
| 391 | 532 | ||
| 392 | 533 | ||
| 393 | 534 | ||
