SKYNET
par henoc Gnaoule · mis à jour il y a 2 h
Skynet est un projet qui pour but de developper une Intelligence artificielle générale
JavaScript Licence MIT 13 fichiers
SKYNET / script_cv_panneau.js
360 lignes · 14,9 Ko
/* ==========================================================================
Panneau CV — Eliora
Quand l'utilisateur choisit son CV dans un champ <input type="file"> de
la page (ici #ecv-fichier, le champ de eliora_cv_widget.php), le fichier
s'ouvre dans un panneau à droite de la conversation.
Formats affichés : PDF, Word (.docx), texte (.txt), PNG et JPG.
L'envoi du CV vers eliora_cv.php n'est pas modifié : ce script se
contente d'afficher le fichier, il n'envoie rien.
Appel direct possible depuis un autre script :
ElioraCV.ouvrir(fichier); // fichier = objet File
ElioraCV.fermer();
Styles : css/cv_panneau.css
========================================================================== */
(function () {
'use strict';
var CONFIG = {
// Lecteurs chargés seulement au premier CV ouvert. Pour ne pas dépendre
// des CDN, copier ces fichiers dans js/ et remplacer les adresses.
pdfJs: 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js',
pdfWorker: 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js',
mammoth: 'https://cdn.jsdelivr.net/npm/mammoth@1.6.0/mammoth.browser.min.js', // lecteur Word
tailleMax: 3 * 1024 * 1024, // même limite que le widget (3 Mo) : au-delà, le panneau ne s'ouvre pas
pagesMax: 20, // nombre maximal de pages PDF affichées
largeurMin: 360, // largeur minimale du panneau (px)
conversationMin: 480, // largeur minimale laissée à la conversation (px)
seuilPleinEcran: 900 // sous cette largeur de fenêtre, le panneau prend tout l'écran
};
var ZOOMS = [0.5, 0.75, 1, 1.25, 1.5, 2];
var CLE_LARGEUR = 'eliora-cv-largeur';
var panneau, scene, pages, note, nomEl, metaEl, zoomEl, btnMoins, btnPlus, btnFermer, poignee, onglet;
var courant = null; // document affiché
var indexZoom = 2;
var largeurVoulue = null;
var scriptsCharges = {};
/* ---------- Construction du panneau ---------- */
function construire() {
panneau = document.createElement('div');
panneau.id = 'cv-panneau';
panneau.setAttribute('role', 'complementary');
panneau.setAttribute('aria-label', 'Aperçu du CV');
panneau.innerHTML =
'<div id="cv-poignee" role="separator" aria-orientation="vertical" aria-label="Largeur du panneau CV" tabindex="0"></div>' +
'<div id="cv-entete">' +
'<div id="cv-infos"><span id="cv-nom"></span><span id="cv-meta"></span></div>' +
'<div id="cv-outils">' +
'<button type="button" class="cv-bouton" id="cv-moins" aria-label="Réduire">−</button>' +
'<span id="cv-zoom" aria-live="polite">100 %</span>' +
'<button type="button" class="cv-bouton" id="cv-plus" aria-label="Agrandir">+</button>' +
'<button type="button" class="cv-bouton" id="cv-fermer" aria-label="Fermer l\'aperçu du CV">×</button>' +
'</div>' +
'</div>' +
'<div id="cv-scene"><p id="cv-note"></p><div id="cv-pages"></div></div>';
document.body.appendChild(panneau);
onglet = document.createElement('button');
onglet.type = 'button';
onglet.id = 'cv-onglet';
onglet.innerHTML =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="18" height="18" aria-hidden="true">' +
'<path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"></path><path d="M14 3v5h5M9 13h6M9 17h4"></path></svg>' +
'Voir mon CV';
document.body.appendChild(onglet);
scene = document.getElementById('cv-scene');
pages = document.getElementById('cv-pages');
note = document.getElementById('cv-note');
nomEl = document.getElementById('cv-nom');
metaEl = document.getElementById('cv-meta');
zoomEl = document.getElementById('cv-zoom');
btnMoins = document.getElementById('cv-moins');
btnPlus = document.getElementById('cv-plus');
btnFermer = document.getElementById('cv-fermer');
poignee = document.getElementById('cv-poignee');
btnFermer.addEventListener('click', fermer);
onglet.addEventListener('click', function () { montrer(); btnFermer.focus(); });
btnMoins.addEventListener('click', function () { if (indexZoom > 0) { indexZoom--; appliquerZoom(); } });
btnPlus.addEventListener('click', function () { if (indexZoom < ZOOMS.length - 1) { indexZoom++; appliquerZoom(); } });
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && document.body.classList.contains('cv-ouvert')) { fermer(); }
});
brancherPoignee();
window.addEventListener('resize', ajusterLargeur);
try {
var memo = parseInt(localStorage.getItem(CLE_LARGEUR), 10);
if (memo > 0) { largeurVoulue = memo; }
} catch (e) {}
appliquerZoom();
ajusterLargeur();
}
/* ---------- Ouverture / fermeture ---------- */
function montrer() { document.body.classList.add('cv-ouvert'); ajusterLargeur(); }
function fermer() {
if (!document.body.classList.contains('cv-ouvert')) { return; }
document.body.classList.remove('cv-ouvert');
if (courant) { onglet.focus(); }
}
function ecrireNote(texte) { note.textContent = texte || ''; note.style.display = texte ? 'block' : 'none'; }
function appliquerZoom() {
var z = ZOOMS[indexZoom];
pages.style.setProperty('--cv-zoom', z);
zoomEl.textContent = Math.round(z * 100) + ' %';
btnMoins.disabled = indexZoom === 0;
btnPlus.disabled = indexZoom === ZOOMS.length - 1;
}
/* ---------- Largeur du panneau ---------- */
function ajusterLargeur() {
var fenetre = window.innerWidth;
var plein = fenetre < CONFIG.seuilPleinEcran;
document.body.classList.toggle('cv-plein', plein);
if (plein) { return; }
var voulue = largeurVoulue || Math.round(fenetre * 0.46);
var max = Math.max(CONFIG.largeurMin, fenetre - CONFIG.conversationMin);
var w = Math.max(CONFIG.largeurMin, Math.min(voulue, max));
document.documentElement.style.setProperty('--cv-w', w + 'px');
poignee.setAttribute('aria-valuenow', Math.round(w / fenetre * 100));
}
function memoriserLargeur() { try { localStorage.setItem(CLE_LARGEUR, String(largeurVoulue)); } catch (e) {} }
function brancherPoignee() {
var actif = false;
poignee.addEventListener('pointerdown', function (e) {
actif = true; poignee.setPointerCapture(e.pointerId);
document.body.classList.add('cv-redim'); e.preventDefault();
});
poignee.addEventListener('pointermove', function (e) {
if (!actif) { return; }
largeurVoulue = Math.round(window.innerWidth - e.clientX);
ajusterLargeur();
});
function fin() {
if (!actif) { return; }
actif = false; document.body.classList.remove('cv-redim'); memoriserLargeur();
}
poignee.addEventListener('pointerup', fin);
poignee.addEventListener('pointercancel', fin);
poignee.addEventListener('keydown', function (e) {
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') { return; }
e.preventDefault();
largeurVoulue = Math.round(panneau.getBoundingClientRect().width + (e.key === 'ArrowLeft' ? 32 : -32));
ajusterLargeur(); memoriserLargeur();
});
}
/* ---------- Lecture du fichier ---------- */
function typeDe(fichier) {
var n = (fichier.name || '').toLowerCase();
if (fichier.type === 'application/pdf' || /\.pdf$/.test(n)) { return 'pdf'; }
if (/\.docx$/.test(n)) { return 'docx'; }
if (/\.txt$/.test(n)) { return 'texte'; }
if (fichier.type === 'image/png' || fichier.type === 'image/jpeg' || /\.(png|jpe?g)$/.test(n)) { return 'image'; }
return null;
}
function taille(octets) {
if (octets < 1024 * 1024) { return Math.max(1, Math.round(octets / 1024)) + ' ko'; }
return (octets / (1024 * 1024)).toFixed(1).replace('.', ',') + ' Mo';
}
// Charge un script externe une seule fois et renvoie l'objet global qu'il définit.
function chargerScript(adresse, nomGlobal) {
if (window[nomGlobal]) { return Promise.resolve(window[nomGlobal]); }
if (!scriptsCharges[adresse]) {
scriptsCharges[adresse] = new Promise(function (ok, echec) {
var s = document.createElement('script');
s.src = adresse;
s.onload = function () {
if (window[nomGlobal]) { ok(window[nomGlobal]); }
else { delete scriptsCharges[adresse]; echec(new Error('LECTEUR')); }
};
s.onerror = function () { delete scriptsCharges[adresse]; echec(new Error('LECTEUR')); };
document.head.appendChild(s);
});
}
return scriptsCharges[adresse];
}
function chargerPdfJs() {
return chargerScript(CONFIG.pdfJs, 'pdfjsLib').then(function (pdfjs) {
pdfjs.GlobalWorkerOptions.workerSrc = pdfjs.GlobalWorkerOptions.workerSrc || CONFIG.pdfWorker;
return pdfjs;
});
}
function ajouterPage(doc, noeud) {
if (courant !== doc) { return; } // un autre CV a été choisi entre-temps
pages.appendChild(noeud);
}
function afficherPdf(fichier, doc) {
return chargerPdfJs().then(function (pdfjs) {
return fichier.arrayBuffer().then(function (tampon) {
return pdfjs.getDocument({ data: new Uint8Array(tampon), isEvalSupported: false }).promise;
});
}).then(function (pdf) {
var total = pdf.numPages, affichees = Math.min(total, CONFIG.pagesMax);
var dpr = Math.min(window.devicePixelRatio || 1, 2);
var cible = Math.min(1800, Math.max(1000, (scene.clientWidth || 700) * dpr * 1.25));
doc.pages = total; doc.tronque = total > affichees;
var suite = Promise.resolve();
for (var i = 1; i <= affichees; i++) {
(function (numero) {
suite = suite.then(function () {
if (courant !== doc) { return null; }
return pdf.getPage(numero).then(function (page) {
var base = page.getViewport({ scale: 1 });
var vue = page.getViewport({ scale: cible / base.width });
var c = document.createElement('canvas');
c.className = 'cv-page';
c.width = Math.round(vue.width); c.height = Math.round(vue.height);
c.setAttribute('role', 'img');
c.setAttribute('aria-label', 'Page ' + numero + ' sur ' + total);
return page.render({ canvasContext: c.getContext('2d'), viewport: vue }).promise.then(function () {
ajouterPage(doc, c);
});
});
});
})(i);
}
return suite;
});
}
function afficherImage(fichier, doc) {
return new Promise(function (ok, echec) {
var img = new Image();
img.className = 'cv-page';
img.alt = 'CV : ' + fichier.name;
img.onload = function () { doc.pages = 1; ajouterPage(doc, img); ok(); };
img.onerror = function () { echec(new Error('IMAGE')); };
img.src = URL.createObjectURL(fichier);
});
}
function feuille() {
var f = document.createElement('div');
f.className = 'cv-page cv-feuille';
return f;
}
function afficherTexte(fichier, doc) {
return fichier.text().then(function (texte) {
var f = feuille();
f.classList.add('cv-texte');
f.textContent = texte;
ajouterPage(doc, f);
});
}
// Le HTML produit à partir du fichier Word n'est pas inséré tel quel : on ne garde
// que des balises de mise en forme, sans attribut, sans lien et sans script.
var BALISES_OK = { P: 1, H1: 1, H2: 1, H3: 1, H4: 1, H5: 1, H6: 1, UL: 1, OL: 1, LI: 1, STRONG: 1, EM: 1, B: 1, I: 1, U: 1, S: 1,
SUP: 1, SUB: 1, BR: 1, TABLE: 1, THEAD: 1, TBODY: 1, TR: 1, TD: 1, TH: 1, IMG: 1, SPAN: 1, DIV: 1 };
function nettoyer(source, cible) {
for (var i = 0; i < source.childNodes.length; i++) {
var n = source.childNodes[i];
if (n.nodeType === 3) { cible.appendChild(document.createTextNode(n.nodeValue)); continue; }
if (n.nodeType !== 1) { continue; }
var nom = n.tagName.toUpperCase();
if (nom === 'SCRIPT' || nom === 'STYLE' || nom === 'IFRAME' || nom === 'OBJECT' || nom === 'EMBED') { continue; }
if (!BALISES_OK[nom]) { nettoyer(n, cible); continue; } // balise inconnue (lien, etc.) : on garde seulement son texte
var copie = document.createElement(nom);
if (nom === 'IMG') {
var src = n.getAttribute('src') || '';
if (!/^data:image\/(png|jpe?g|gif);base64,/i.test(src)) { continue; }
copie.setAttribute('src', src);
copie.setAttribute('alt', '');
}
if (nom === 'TD' || nom === 'TH') {
var c = parseInt(n.getAttribute('colspan'), 10), r = parseInt(n.getAttribute('rowspan'), 10);
if (c > 1) { copie.setAttribute('colspan', c); }
if (r > 1) { copie.setAttribute('rowspan', r); }
}
nettoyer(n, copie);
cible.appendChild(copie);
}
}
function afficherDocx(fichier, doc) {
return chargerScript(CONFIG.mammoth, 'mammoth').then(function (mammoth) {
return fichier.arrayBuffer().then(function (tampon) {
return mammoth.convertToHtml({ arrayBuffer: tampon });
});
}).then(function (resultat) {
var brut = new DOMParser().parseFromString('<div>' + (resultat.value || '') + '</div>', 'text/html');
var f = feuille();
nettoyer(brut.body, f);
ajouterPage(doc, f);
});
}
function messageErreur(err) {
if (err && err.message === 'LECTEUR') { return 'Le lecteur de documents ne s\'est pas chargé. Vérifiez votre connexion puis choisissez à nouveau votre CV.'; }
if (err && err.name === 'PasswordException') { return 'Ce PDF est protégé par un mot de passe : l\'aperçu n\'est pas possible.'; }
return 'L\'aperçu de ce fichier n\'est pas possible : il est illisible ou endommagé.';
}
/* ---------- Point d'entrée ---------- */
function ouvrir(fichier) {
if (!fichier) { return false; }
var type = typeDe(fichier);
if (!type) { return false; } // format sans aperçu : on ne touche à rien
if (fichier.size > CONFIG.tailleMax) { return false; } // refusé par le widget : pas de panneau
var doc = { nom: fichier.name, pages: 0, tronque: false };
courant = doc;
document.body.classList.add('cv-charge');
pages.replaceChildren();
nomEl.textContent = fichier.name;
metaEl.textContent = taille(fichier.size);
scene.scrollTop = 0; scene.scrollLeft = 0;
montrer();
ecrireNote('Ouverture du CV…');
var rendu = type === 'pdf' ? afficherPdf(fichier, doc)
: type === 'docx' ? afficherDocx(fichier, doc)
: type === 'texte' ? afficherTexte(fichier, doc)
: afficherImage(fichier, doc);
rendu.then(function () {
if (courant !== doc) { return; }
ecrireNote('');
var detail = type === 'pdf' ? doc.pages + (doc.pages > 1 ? ' pages' : ' page')
: type === 'docx' ? 'Word'
: type === 'texte' ? 'Texte' : 'Image';
metaEl.textContent = detail + ' · ' + taille(fichier.size) +
(doc.tronque ? ' · ' + CONFIG.pagesMax + ' premières pages affichées' : '') +
(type === 'docx' ? ' · aperçu simplifié, la mise en page peut différer' : '');
}).catch(function (err) {
if (courant !== doc) { return; }
pages.replaceChildren();
ecrireNote(messageErreur(err));
});
return true;
}
function demarrer() {
construire();
// Écoute tous les champs « fichier » de la page, y compris ceux ajoutés plus tard.
document.addEventListener('change', function (e) {
var champ = e.target;
if (champ && champ.tagName === 'INPUT' && champ.type === 'file' && champ.files && champ.files[0]) {
ouvrir(champ.files[0]);
}
}, true);
}
window.ElioraCV = { ouvrir: ouvrir, fermer: fermer };
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', demarrer); }
else { demarrer(); }
})();