Annuaire › Code ouvert
Code publié par son auteur, en lecture seule. « Copier dans mon espace » crée un nouveau site à vous (non publié) que vous pouvez modifier librement.
js/app.js · 14.3 Ko
/* ============================================================
Fichier : js/app.js
Rôle : tout le comportement du convertisseur de monnaie.
Dépend de : index.html (les id utilisés dans getElementById
doivent exister dans la page).
Chargé par : <script src="js/app.js"> en bas de index.html.
------------------------------------------------------------
Plan du fichier :
1. Données (taux de change)
2. Stockage sûr (localStorage avec repli en mémoire)
3. Outils : lire un nombre, formater un nombre
4. Calcul de la conversion
5. Affichage (lecture/écriture du DOM)
6. Événements (clic, envoi de formulaire…)
7. Démarrage
============================================================ */
"use strict"; // Mode strict : JavaScript signale davantage d'erreurs (fautes de frappe, variables oubliées)
/* ============================================================
1. DONNÉES
============================================================ */
/**
* Taux d'exemple : combien d'Ariary vaut UNE unité de chaque devise.
* Ce sont des valeurs d'illustration, PAS le taux du jour : le site
* n'a pas Internet, donc c'est l'utilisateur qui les règle.
* On utilise l'Ariary comme « devise pivot » : toute conversion passe
* par l'Ariary (Euro → Ariary → Dollar). Ainsi, 2 taux suffisent
* au lieu de 6 couples de devises.
*/
const TAUX_PAR_DEFAUT = {
MGA: 1, // 1 Ariary vaut 1 Ariary (le pivot)
EUR: 5000, // 1 Euro ≈ 5 000 Ar (exemple)
USD: 4500 // 1 Dollar ≈ 4 500 Ar (exemple)
};
/** Les taux actuellement utilisés (copie modifiable des taux par défaut). */
let taux = { ...TAUX_PAR_DEFAUT }; // « ... » (opérateur de décomposition) copie l'objet
/** Montants proposés dans le tableau « Repères rapides » (en Ariary). */
const REPERES_ARIARY = [1000, 5000, 10000, 20000, 50000, 100000];
/** Clé sous laquelle on range les taux dans le stockage du navigateur. */
const CLE_STOCKAGE = "convertisseur-taux";
/* ============================================================
2. STOCKAGE SÛR
============================================================ */
/**
* Mémoire de secours, utilisée quand localStorage est interdit.
* PIÈGE : sur le Portail, les sites s'exécutent dans un « bac à sable » :
* le simple fait d'écrire « localStorage » lève une SecurityError.
* Il FAUT donc toujours entourer l'accès d'un try/catch.
* Dans ce cas, les taux restent en mémoire tant que la page est ouverte,
* mais sont oubliés au rechargement.
*/
let memoireDeSecours = null;
/**
* Enregistre les taux (dans localStorage si possible, sinon en mémoire).
* @param {{MGA:number, EUR:number, USD:number}} valeurs - taux à garder
* @returns {void}
*/
function sauvegarderTaux(valeurs) {
memoireDeSecours = JSON.stringify(valeurs); // JSON.stringify : objet -> texte
try {
window.localStorage.setItem(CLE_STOCKAGE, memoireDeSecours);
} catch (erreur) {
// Stockage indisponible : ce n'est pas grave, la mémoire de secours suffit.
}
}
/**
* Relit les taux sauvegardés.
* @returns {{MGA:number, EUR:number, USD:number}|null} les taux, ou null s'il n'y en a pas
*/
function chargerTaux() {
let texte = memoireDeSecours;
try {
const lu = window.localStorage.getItem(CLE_STOCKAGE);
if (lu) { texte = lu; }
} catch (erreur) {
// Stockage indisponible : on garde la mémoire de secours.
}
if (!texte) { return null; }
try {
const objet = JSON.parse(texte); // JSON.parse : texte -> objet (peut échouer si le texte est abîmé)
if (objet && objet.EUR > 0 && objet.USD > 0) {
return { MGA: 1, EUR: Number(objet.EUR), USD: Number(objet.USD) };
}
} catch (erreur) {
// Texte abîmé : on l'ignore.
}
return null;
}
/* ============================================================
3. OUTILS : LIRE ET FORMATER UN NOMBRE
============================================================ */
/**
* Transforme le texte tapé par l'utilisateur en nombre.
* Accepte « 25000 », « 25 000 », « 12,5 » et « 12.5 ».
* Pourquoi ne pas utiliser <input type="number"> ? Parce qu'en français on
* tape une virgule, et ce type de champ se comporte différemment selon les
* navigateurs. Ici, on contrôle tout.
* @param {string} texte - le texte saisi
* @returns {number|null} le nombre, ou null si le texte n'est pas un nombre valide
*/
function lireNombre(texte) {
// Retire tous les espaces (y compris insécables) : « 25 000 » -> « 25000 »
let propre = texte.replace(/[\s ]/g, "");
// Remplace la virgule décimale par un point : « 12,5 » -> « 12.5 »
propre = propre.replace(",", ".");
// Expression régulière : uniquement des chiffres, avec éventuellement un point suivi de chiffres.
// Cela refuse « abc », « -5 », « 1.2.3 » ou le texte vide.
if (!/^\d+(\.\d+)?$/.test(propre)) {
return null;
}
return Number(propre);
}
/**
* Formate un montant avec le symbole de sa devise, à la française.
* Intl.NumberFormat est l'outil intégré au navigateur pour afficher des
* nombres selon les habitudes d'une langue (espaces, virgule décimale…).
* @param {number} valeur - le montant
* @param {string} devise - "MGA", "EUR" ou "USD"
* @returns {string} le texte prêt à afficher, par exemple « 5 000,00 € »
*/
function formaterMontant(valeur, devise) {
if (devise === "MGA") {
// L'Ariary s'écrit sans décimales dans la vie courante.
const entier = new Intl.NumberFormat("fr-FR", { maximumFractionDigits: 0 });
return entier.format(Math.round(valeur)) + " Ar";
}
const format = new Intl.NumberFormat("fr-FR", {
style: "currency", // affiche le symbole de la monnaie
currency: devise // "EUR" ou "USD"
});
return format.format(valeur);
}
/**
* Formate un nombre simple (sans symbole), pour le champ des taux.
* @param {number} valeur - le nombre
* @returns {string} par exemple « 4 500 » ou « 4 512,5 »
*/
function formaterSimple(valeur) {
return new Intl.NumberFormat("fr-FR", { maximumFractionDigits: 4 }).format(valeur);
}
/* ============================================================
4. CALCUL
============================================================ */
/**
* Convertit un montant d'une devise vers une autre en passant par l'Ariary.
* Exemple : 10 € vers $ -> 10 × 5000 = 50 000 Ar -> 50 000 ÷ 4500 ≈ 11,11 $.
* @param {number} montant - le montant de départ
* @param {string} source - devise de départ ("MGA", "EUR", "USD")
* @param {string} cible - devise d'arrivée
* @returns {number} le montant converti
*/
function convertir(montant, source, cible) {
const enAriary = montant * taux[source]; // étape 1 : tout ramener en Ariary
return enAriary / taux[cible]; // étape 2 : passer de l'Ariary à la devise voulue
}
/* ============================================================
5. AFFICHAGE (DOM)
============================================================ */
/* Le DOM est l'arbre des éléments de la page. On le lit et on le modifie
depuis JavaScript. D'abord, on retrouve les éléments UNE fois pour toutes. */
const champMontant = document.getElementById("montant");
const erreurMontant = document.getElementById("erreur-montant");
const selectSource = document.getElementById("devise-source");
const selectCible = document.getElementById("devise-cible");
const boutonInverser = document.getElementById("bouton-inverser");
const zoneResultat = document.getElementById("resultat");
const formConversion = document.getElementById("formulaire-conversion");
const champTauxEur = document.getElementById("taux-eur");
const champTauxUsd = document.getElementById("taux-usd");
const erreurTaux = document.getElementById("erreur-taux");
const formTaux = document.getElementById("formulaire-taux");
const boutonReinit = document.getElementById("bouton-reinitialiser");
const corpsReperes = document.getElementById("corps-reperes");
/**
* Affiche (ou efface) un message d'erreur sous un champ.
* @param {HTMLInputElement} champ - le champ concerné (sa bordure devient rouge)
* @param {HTMLElement} zoneMessage - le paragraphe où écrire le message
* @param {string} message - le message ; chaîne vide pour effacer l'erreur
* @returns {void}
*/
function afficherErreur(champ, zoneMessage, message) {
zoneMessage.textContent = message; // textContent : écrit du TEXTE (jamais du HTML) -> plus sûr
if (message) {
champ.classList.add("invalide");
champ.setAttribute("aria-invalid", "true"); // informe les lecteurs d'écran
} else {
champ.classList.remove("invalide");
champ.removeAttribute("aria-invalid");
}
}
/**
* Affiche le résultat d'une conversion dans la zone prévue.
* @param {number} montant - montant saisi
* @param {string} source - devise de départ
* @param {string} cible - devise d'arrivée
* @returns {void}
*/
function afficherResultat(montant, source, cible) {
const resultat = convertir(montant, source, cible);
// On vide la zone puis on la remplit avec des éléments créés « à la main » :
// createElement + textContent évite d'insérer du HTML venant de l'utilisateur.
zoneResultat.textContent = "";
const ligne1 = document.createElement("span");
ligne1.textContent = formaterMontant(montant, source) + " valent";
const ligne2 = document.createElement("strong");
ligne2.textContent = formaterMontant(resultat, cible);
zoneResultat.append(ligne1, ligne2);
}
/**
* Remplit les deux champs de taux avec les valeurs actuelles.
* @returns {void}
*/
function afficherTaux() {
champTauxEur.value = formaterSimple(taux.EUR);
champTauxUsd.value = formaterSimple(taux.USD);
}
/**
* Construit le tableau « Repères rapides » (une ligne par montant en Ariary).
* @returns {void}
*/
function afficherReperes() {
corpsReperes.textContent = ""; // on repart d'un tableau vide
// forEach : exécute la fonction pour chaque élément de la liste
REPERES_ARIARY.forEach(function (ariary) {
const ligne = document.createElement("tr");
// On crée trois cellules à partir d'une petite liste de textes
[
formaterMontant(ariary, "MGA"),
formaterMontant(convertir(ariary, "MGA", "EUR"), "EUR"),
formaterMontant(convertir(ariary, "MGA", "USD"), "USD")
].forEach(function (texte) {
const cellule = document.createElement("td");
cellule.textContent = texte;
ligne.appendChild(cellule);
});
corpsReperes.appendChild(ligne);
});
}
/* ============================================================
6. ÉVÉNEMENTS
============================================================ */
/* Un événement est « quelque chose qui se passe » (clic, envoi, frappe).
addEventListener dit : « quand cela arrive, exécute cette fonction ». */
/**
* Gère l'envoi du formulaire de conversion (bouton « Convertir » ou touche Entrée).
* @param {SubmitEvent} evenement - l'événement d'envoi
* @returns {void}
*/
function surConversion(evenement) {
// Par défaut, un formulaire recharge la page : on l'empêche.
evenement.preventDefault();
const montant = lireNombre(champMontant.value);
if (montant === null) {
afficherErreur(champMontant, erreurMontant,
"Montant invalide : tapez un nombre positif, par exemple 25000 ou 12,5.");
zoneResultat.textContent = "";
champMontant.focus(); // remet le curseur dans le champ fautif
return;
}
afficherErreur(champMontant, erreurMontant, ""); // tout va bien : on efface l'erreur
afficherResultat(montant, selectSource.value, selectCible.value);
}
/**
* Échange les devises « De » et « Vers », puis relance le calcul s'il y a un montant.
* @returns {void}
*/
function surInversion() {
const ancienneSource = selectSource.value;
selectSource.value = selectCible.value;
selectCible.value = ancienneSource;
// Si un montant valide est déjà saisi, on met le résultat à jour tout de suite.
const montant = lireNombre(champMontant.value);
if (montant !== null) {
afficherResultat(montant, selectSource.value, selectCible.value);
}
}
/**
* Gère l'enregistrement de nouveaux taux.
* @param {SubmitEvent} evenement - l'événement d'envoi
* @returns {void}
*/
function surChangementTaux(evenement) {
evenement.preventDefault();
const eur = lireNombre(champTauxEur.value);
const usd = lireNombre(champTauxUsd.value);
// Un taux doit être un nombre STRICTEMENT positif (diviser par 0 donnerait « Infinity »).
const eurOk = eur !== null && eur > 0;
const usdOk = usd !== null && usd > 0;
afficherErreur(champTauxEur, erreurTaux, "");
afficherErreur(champTauxUsd, erreurTaux, "");
if (!eurOk || !usdOk) {
// On marque en rouge le(s) champ(s) fautif(s) puis on écrit le message commun.
if (!eurOk) { champTauxEur.classList.add("invalide"); }
if (!usdOk) { champTauxUsd.classList.add("invalide"); }
erreurTaux.textContent = "Chaque taux doit être un nombre supérieur à zéro (exemple : 4500).";
(eurOk ? champTauxUsd : champTauxEur).focus();
return;
}
taux = { MGA: 1, EUR: eur, USD: usd };
sauvegarderTaux(taux);
afficherTaux();
afficherReperes();
// Met à jour le résultat affiché si un montant est saisi
const montant = lireNombre(champMontant.value);
if (montant !== null) { afficherResultat(montant, selectSource.value, selectCible.value); }
erreurTaux.textContent = ""; // pas de message : tout s'est bien passé
}
/**
* Remet les taux d'exemple.
* @returns {void}
*/
function surReinitialisation() {
taux = { ...TAUX_PAR_DEFAUT };
sauvegarderTaux(taux);
afficherErreur(champTauxEur, erreurTaux, "");
afficherErreur(champTauxUsd, erreurTaux, "");
afficherTaux();
afficherReperes();
}
// On branche chaque fonction sur son événement.
formConversion.addEventListener("submit", surConversion);
boutonInverser.addEventListener("click", surInversion);
formTaux.addEventListener("submit", surChangementTaux);
boutonReinit.addEventListener("click", surReinitialisation);
/* ============================================================
7. DÉMARRAGE
============================================================ */
/**
* Prépare la page au chargement : relit d'éventuels taux sauvegardés
* puis remplit les champs et le tableau.
* @returns {void}
*/
function demarrer() {
const sauvegardes = chargerTaux();
if (sauvegardes) { taux = sauvegardes; }
afficherTaux();
afficherReperes();
}
demarrer();