TP : Développement d’une Mini‐Application Web Mobile avec
JavaScript
Projet : Application de Gestion de Tâches ﴾Todo List﴿
Vous allez développer une application de gestion de tâches complète, fonctionnant sur mobile et perme‐
ttant de :
• Ajouter des tâches avec une priorité
• Marquer les tâches comme terminées
• Supprimer des tâches
• Filtrer les tâches ﴾toutes, actives, terminées﴿
• Sauvegarder les données localement
• Fonctionner hors ligne
Exercice 1 : Structure de Base ﴾30 minutes﴿
1.1 Création de la Structure HTML
Créez un fichier [Link] avec la structure suivante :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF‐8">
<meta name="viewport" content="width=device‐width, initial‐scale=1.0, maximum‐scale=1.0, u
<meta name="theme‐color" content="#667eea">
<meta name="apple‐mobile‐web‐app‐capable" content="yes">
<meta name="apple‐mobile‐web‐app‐status‐bar‐style" content="default">
<title>Mes Tâches</title>
<link rel="stylesheet" href="[Link]">
</head>
<body>
<div class="app‐container">
<!‐‐ En‐tête ‐‐>
<header class="app‐header">
<h1>� Mes Tâches</h1>
<p class="date" id="date‐actuelle"></p>
</header>
<!‐‐ Formulaire d'ajout ‐‐>
<form id="formulaire‐tache" class="formulaire‐ajout">
<div class="champ‐saisie">
<input
type="text"
id="nouvelle‐tache"
placeholder="Ajouter une nouvelle tâche..."
autocomplete="off"
required
>
</div>
<div class="options‐tache">
<select id="priorite">
<option value="basse">Priorité basse</option>
<option value="moyenne" selected>Priorité moyenne</option>
<option value="haute">Priorité haute</option>
1
</select>
<button type="submit" class="btn‐ajouter">
Ajouter
</button>
</div>
</form>
<!‐‐ Filtres ‐‐>
<div class="filtres" id="filtres">
<button class="filtre actif" data‐filtre="toutes">Toutes</button>
<button class="filtre" data‐filtre="actives">Actives</button>
<button class="filtre" data‐filtre="terminees">Terminées</button>
</div>
<!‐‐ Liste des tâches ‐‐>
<main class="liste‐taches‐container">
<ul id="liste‐taches" class="liste‐taches">
<!‐‐ Les tâches seront ajoutées ici dynamiquement ‐‐>
</ul>
<p id="message‐vide" class="message‐vide">
Aucune tâche pour le moment. Ajoutez‐en une !
</p>
</main>
<!‐‐ Statistiques ‐‐>
<footer class="app‐footer">
<div class="stats">
<span id="compteur‐actives">0 tâche(s) restante(s)</span>
</div>
<button id="btn‐supprimer‐terminees" class="btn‐supprimer‐terminees">
Supprimer terminées
</button>
</footer>
</div>
<script src="[Link]"></script>
</body>
</html>
1.2 Questions de Compréhension
Q1. Quel est le rôle de la balise <meta name="viewport"> pour les applications mobiles ?
Q2. Pourquoi utilise‐t‐on l’attribut data‐filtre sur les boutons de filtre ?
Q3. Expliquez l’intérêt de l’attribut autocomplete="off" sur le champ de saisie.
Exercice 2 : Styles CSS Responsive ﴾30 minutes﴿
2.1 Création des Styles
Créez un fichier [Link] :
/* Variables CSS */
:root {
‐‐couleur‐primaire: #667eea;
‐‐couleur‐primaire‐sombre: #5a6fd6;
2
‐‐couleur‐succes: #48bb78;
‐‐couleur‐danger: #f56565;
‐‐couleur‐attention: #ed8936;
‐‐couleur‐fond: #f7fafc;
‐‐couleur‐carte: #ffffff;
‐‐couleur‐texte: #2d3748;
‐‐couleur‐texte‐secondaire: #718096;
‐‐ombre‐legere: 0 2px 4px rgba(0,0,0,0.1);
‐‐ombre‐moyenne: 0 4px 6px rgba(0,0,0,0.1);
‐‐rayon‐bordure: 12px;
‐‐transition: all 0.3s ease;
}
/* Reset et base */
* {
margin: 0;
padding: 0;
box‐sizing: border‐box;
}
body {
font‐family: ‐apple‐system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans‐se
background: linear‐gradient(135deg, var(‐‐couleur‐primaire) 0%, #764ba2 100%);
min‐height: 100vh;
color: var(‐‐couleur‐texte);
line‐height: 1.6;
}
/* Container principal */
.app‐container {
max‐width: 500px;
margin: 0 auto;
min‐height: 100vh;
background‐color: var(‐‐couleur‐fond);
display: flex;
flex‐direction: column;
}
/* En‐tête */
.app‐header {
background: linear‐gradient(135deg, var(‐‐couleur‐primaire) 0%, #764ba2 100%);
color: white;
padding: 24px 20px;
text‐align: center;
}
.app‐header h1 {
font‐size: 1.75rem;
font‐weight: 700;
margin‐bottom: 4px;
}
.app‐header .date {
font‐size: 0.9rem;
opacity: 0.9;
}
/* Formulaire d'ajout */
3
.formulaire‐ajout {
padding: 20px;
background‐color: var(‐‐couleur‐carte);
box‐shadow: var(‐‐ombre‐legere);
}
.champ‐saisie input {
width: 100%;
padding: 14px 16px;
font‐size: 1rem;
border: 2px solid #e2e8f0;
border‐radius: var(‐‐rayon‐bordure);
transition: var(‐‐transition);
background‐color: var(‐‐couleur‐fond);
}
.champ‐saisie input:focus {
outline: none;
border‐color: var(‐‐couleur‐primaire);
box‐shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
}
.options‐tache {
display: flex;
gap: 10px;
margin‐top: 12px;
}
.options‐tache select {
flex: 1;
padding: 12px;
font‐size: 0.9rem;
border: 2px solid #e2e8f0;
border‐radius: var(‐‐rayon‐bordure);
background‐color: white;
cursor: pointer;
}
.btn‐ajouter {
padding: 12px 24px;
font‐size: 1rem;
font‐weight: 600;
color: white;
background: linear‐gradient(135deg, var(‐‐couleur‐primaire) 0%, #764ba2 100%);
border: none;
border‐radius: var(‐‐rayon‐bordure);
cursor: pointer;
transition: var(‐‐transition);
}
.btn‐ajouter:hover {
transform: translateY(‐2px);
box‐shadow: var(‐‐ombre‐moyenne);
}
.btn‐ajouter:active {
transform: translateY(0);
}
4
/* Filtres */
.filtres {
display: flex;
padding: 12px 20px;
gap: 8px;
background‐color: var(‐‐couleur‐carte);
border‐bottom: 1px solid #e2e8f0;
}
.filtre {
flex: 1;
padding: 10px;
font‐size: 0.85rem;
font‐weight: 500;
color: var(‐‐couleur‐texte‐secondaire);
background‐color: transparent;
border: 2px solid transparent;
border‐radius: 8px;
cursor: pointer;
transition: var(‐‐transition);
}
.filtre:hover {
background‐color: var(‐‐couleur‐fond);
}
.[Link] {
color: var(‐‐couleur‐primaire);
background‐color: rgba(102, 126, 234, 0.1);
border‐color: var(‐‐couleur‐primaire);
}
/* Liste des tâches */
.liste‐taches‐container {
flex: 1;
padding: 16px 20px;
overflow‐y: auto;
}
.liste‐taches {
list‐style: none;
}
.tache {
display: flex;
align‐items: center;
padding: 16px;
margin‐bottom: 12px;
background‐color: var(‐‐couleur‐carte);
border‐radius: var(‐‐rayon‐bordure);
box‐shadow: var(‐‐ombre‐legere);
transition: var(‐‐transition);
animation: apparition 0.3s ease;
}
@keyframes apparition {
from {
5
opacity: 0;
transform: translateY(‐10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.tache:hover {
box‐shadow: var(‐‐ombre‐moyenne);
}
.[Link] {
opacity: 0.6;
}
.[Link] .tache‐texte {
text‐decoration: line‐through;
color: var(‐‐couleur‐texte‐secondaire);
}
/* Checkbox personnalisée */
.tache‐checkbox {
width: 24px;
height: 24px;
margin‐right: 14px;
border: 2px solid #cbd5e0;
border‐radius: 50%;
cursor: pointer;
display: flex;
align‐items: center;
justify‐content: center;
transition: var(‐‐transition);
flex‐shrink: 0;
}
.tache‐checkbox:hover {
border‐color: var(‐‐couleur‐primaire);
}
.[Link] .tache‐checkbox {
background‐color: var(‐‐couleur‐succes);
border‐color: var(‐‐couleur‐succes);
}
.[Link] .tache‐checkbox::after {
content: '�';
color: white;
font‐size: 14px;
font‐weight: bold;
}
/* Contenu de la tâche */
.tache‐contenu {
flex: 1;
min‐width: 0;
}
6
.tache‐texte {
font‐size: 1rem;
word‐wrap: break‐word;
}
.tache‐meta {
display: flex;
gap: 8px;
margin‐top: 4px;
font‐size: 0.75rem;
}
/* Badges de priorité */
.badge‐priorite {
padding: 2px 8px;
border‐radius: 12px;
font‐weight: 500;
text‐transform: uppercase;
font‐size: 0.65rem;
}
.badge‐[Link] {
background‐color: rgba(245, 101, 101, 0.15);
color: var(‐‐couleur‐danger);
}
.badge‐[Link] {
background‐color: rgba(237, 137, 54, 0.15);
color: var(‐‐couleur‐attention);
}
.badge‐[Link] {
background‐color: rgba(72, 187, 120, 0.15);
color: var(‐‐couleur‐succes);
}
/* Bouton supprimer */
.btn‐supprimer {
width: 32px;
height: 32px;
border: none;
background‐color: transparent;
color: var(‐‐couleur‐texte‐secondaire);
font‐size: 1.25rem;
cursor: pointer;
border‐radius: 50%;
transition: var(‐‐transition);
display: flex;
align‐items: center;
justify‐content: center;
opacity: 0;
}
.tache:hover .btn‐supprimer {
opacity: 1;
}
7
.btn‐supprimer:hover {
background‐color: rgba(245, 101, 101, 0.15);
color: var(‐‐couleur‐danger);
}
/* Message vide */
.message‐vide {
text‐align: center;
padding: 40px 20px;
color: var(‐‐couleur‐texte‐secondaire);
font‐size: 1rem;
}
.message‐[Link] {
display: none;
}
/* Footer */
.app‐footer {
display: flex;
justify‐content: space‐between;
align‐items: center;
padding: 16px 20px;
background‐color: var(‐‐couleur‐carte);
border‐top: 1px solid #e2e8f0;
}
.stats {
font‐size: 0.9rem;
color: var(‐‐couleur‐texte‐secondaire);
}
.btn‐supprimer‐terminees {
padding: 8px 16px;
font‐size: 0.85rem;
color: var(‐‐couleur‐danger);
background‐color: transparent;
border: 1px solid var(‐‐couleur‐danger);
border‐radius: 8px;
cursor: pointer;
transition: var(‐‐transition);
}
.btn‐supprimer‐terminees:hover {
background‐color: var(‐‐couleur‐danger);
color: white;
}
/* Media queries pour tablettes et desktop */
@media (min‐width: 768px) {
body {
padding: 40px 20px;
}
.app‐container {
border‐radius: 24px;
box‐shadow: 0 25px 50px ‐12px rgba(0, 0, 0, 0.25);
min‐height: auto;
8
max‐height: 90vh;
}
.app‐header {
border‐radius: 24px 24px 0 0;
}
}
/* Support du mode sombre */
@media (prefers‐color‐scheme: dark) {
:root {
‐‐couleur‐fond: #1a202c;
‐‐couleur‐carte: #2d3748;
‐‐couleur‐texte: #e2e8f0;
‐‐couleur‐texte‐secondaire: #a0aec0;
}
.champ‐saisie input,
.options‐tache select {
background‐color: var(‐‐couleur‐carte);
border‐color: #4a5568;
color: var(‐‐couleur‐texte);
}
.filtres,
.app‐footer {
border‐color: #4a5568;
}
}
2.2 Questions de Compréhension
Q4. Expliquez le rôle des variables CSS ﴾custom properties﴿ dans cette feuille de styles.
Q5. Comment fonctionne la media query prefers‐color‐scheme: dark ?
Q6. Pourquoi utilise‐t‐on flex: 1 sur .tache‐contenu ?
Exercice 3 : Logique JavaScript de Base ﴾45 minutes﴿
3.1 Initialisation et Structure
Créez un fichier [Link] et implémentez la structure de base :
/**
* Application de gestion de tâches
* Structure modulaire avec pattern IIFE
*/
const App = (function() {
'use strict';
// ========================================
// CONFIGURATION ET ÉTAT
// ========================================
const CONFIG = {
9
CLE_STOCKAGE: 'mes‐taches',
ANIMATION_DELAI: 300
};
let taches = [];
let filtreActuel = 'toutes';
// ========================================
// SÉLECTEURS DOM
// ========================================
const elements = {
formulaire: null,
champNouvelleTache: null,
selectPriorite: null,
listeTaches: null,
messageVide: null,
compteurActives: null,
btnSupprimerTerminees: null,
conteneurFiltres: null,
dateActuelle: null
};
// ========================================
// FONCTIONS UTILITAIRES
// ========================================
/**
* Génère un identifiant unique
* @returns {string} Identifiant unique
*/
function genererID() {
return [Link]().toString(36) + [Link]().toString(36).substr(2);
}
/**
* Formate une date pour l'affichage
* @param {Date} date ‐ Date à formater
* @returns {string} Date formatée
*/
function formaterDate(date) {
const options = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
};
return new [Link]('fr‐FR', options).format(date);
}
/**
* Formate une heure pour l'affichage
* @param {Date} date ‐ Date à formater
* @returns {string} Heure formatée
*/
function formaterHeure(date) {
return new [Link]('fr‐FR', {
hour: '2‐digit',
10
minute: '2‐digit'
}).format(date);
}
// ========================================
// GESTION DU STOCKAGE LOCAL
// ========================================
/**
* Sauvegarde les tâches dans le localStorage
*/
function sauvegarderTaches() {
// À COMPLÉTER ‐ Exercice 3.2
}
/**
* Charge les tâches depuis le localStorage
* @returns {Array} Tableau des tâches
*/
function chargerTaches() {
// À COMPLÉTER ‐ Exercice 3.2
}
// ========================================
// GESTION DES TÂCHES
// ========================================
/**
* Crée une nouvelle tâche
* @param {string} texte ‐ Texte de la tâche
* @param {string} priorite ‐ Niveau de priorité
* @returns {Object} Objet tâche
*/
function creerTache(texte, priorite) {
return {
id: genererID(),
texte: [Link](),
priorite: priorite,
terminee: false,
dateCreation: new Date().toISOString()
};
}
/**
* Ajoute une nouvelle tâche
* @param {string} texte ‐ Texte de la tâche
* @param {string} priorite ‐ Niveau de priorité
*/
function ajouterTache(texte, priorite) {
// À COMPLÉTER ‐ Exercice 3.3
}
/**
* Supprime une tâche par son ID
* @param {string} id ‐ Identifiant de la tâche
*/
function supprimerTache(id) {
// À COMPLÉTER ‐ Exercice 3.3
11
}
/**
* Bascule l'état terminé d'une tâche
* @param {string} id ‐ Identifiant de la tâche
*/
function basculerTache(id) {
// À COMPLÉTER ‐ Exercice 3.3
}
/**
* Supprime toutes les tâches terminées
*/
function supprimerTachesTerminees() {
// À COMPLÉTER ‐ Exercice 3.3
}
// ========================================
// RENDU DE L'INTERFACE
// ========================================
/**
* Crée l'élément HTML d'une tâche
* @param {Object} tache ‐ Objet tâche
* @returns {HTMLElement} Élément li
*/
function creerElementTache(tache) {
// À COMPLÉTER ‐ Exercice 3.4
}
/**
* Met à jour l'affichage de la liste des tâches
*/
function mettreAJourAffichage() {
// À COMPLÉTER ‐ Exercice 3.4
}
/**
* Met à jour le compteur de tâches actives
*/
function mettreAJourCompteur() {
// À COMPLÉTER ‐ Exercice 3.4
}
// ========================================
// GESTION DES ÉVÉNEMENTS
// ========================================
/**
* Configure les écouteurs d'événements
*/
function configurerEvenements() {
// À COMPLÉTER ‐ Exercice 3.5
}
// ========================================
// INITIALISATION
// ========================================
12
/**
* Initialise l'application
*/
function initialiser() {
// Récupérer les éléments du DOM
[Link] = [Link]('formulaire‐tache');
[Link] = [Link]('nouvelle‐tache');
[Link] = [Link]('priorite');
[Link] = [Link]('liste‐taches');
[Link] = [Link]('message‐vide');
[Link] = [Link]('compteur‐actives');
[Link] = [Link]('btn‐supprimer‐terminees');
[Link] = [Link]('filtres');
[Link] = [Link]('date‐actuelle');
// Afficher la date actuelle
[Link] = formaterDate(new Date());
// Charger les tâches existantes
taches = chargerTaches();
// Configurer les événements
configurerEvenements();
// Afficher les tâches
mettreAJourAffichage();
[Link]('Application initialisée avec succès');
}
// ========================================
// INTERFACE PUBLIQUE
// ========================================
return {
init: initialiser
};
})();
// Démarrer l'application quand le DOM est prêt
[Link]('DOMContentLoaded', [Link]);
3.2 Implémentez le Stockage Local
Complétez les fonctions sauvegarderTaches() et chargerTaches() :
/**
* Sauvegarde les tâches dans le localStorage
*/
function sauvegarderTaches() {
try {
[Link](CONFIG.CLE_STOCKAGE, [Link](taches));
} catch (erreur) {
[Link]('Erreur de sauvegarde:', erreur);
}
}
13
/**
* Charge les tâches depuis le localStorage
* @returns {Array} Tableau des tâches
*/
function chargerTaches() {
try {
const donnees = [Link](CONFIG.CLE_STOCKAGE);
return donnees ? [Link](donnees) : [];
} catch (erreur) {
[Link]('Erreur de chargement:', erreur);
return [];
}
}
3.3 Implémentez les Fonctions de Gestion des Tâches
Complétez les fonctions suivantes :
/**
* Ajoute une nouvelle tâche
*/
function ajouterTache(texte, priorite) {
if (![Link]()) {
return;
}
const nouvelleTache = creerTache(texte, priorite);
[Link](nouvelleTache); // Ajouter au début
sauvegarderTaches();
mettreAJourAffichage();
}
/**
* Supprime une tâche par son ID
*/
function supprimerTache(id) {
taches = [Link](tache => [Link] !== id);
sauvegarderTaches();
mettreAJourAffichage();
}
/**
* Bascule l'état terminé d'une tâche
*/
function basculerTache(id) {
const tache = [Link](t => [Link] === id);
if (tache) {
[Link] = ![Link];
sauvegarderTaches();
mettreAJourAffichage();
}
}
/**
* Supprime toutes les tâches terminées
*/
function supprimerTachesTerminees() {
14
taches = [Link](tache => ![Link]);
sauvegarderTaches();
mettreAJourAffichage();
}
3.4 Implémentez le Rendu de l’Interface
/**
* Crée l'élément HTML d'une tâche
*/
function creerElementTache(tache) {
const li = [Link]('li');
[Link] = `tache ${[Link] ? 'terminee' : ''}`;
[Link] = [Link];
const dateCreation = new Date([Link]);
[Link] = `
<div class="tache‐checkbox" role="checkbox" aria‐checked="${[Link]}" tabindex=
<div class="tache‐contenu">
<span class="tache‐texte">${echapperHTML([Link])}</span>
<div class="tache‐meta">
<span class="badge‐priorite ${[Link]}">${[Link]}</span>
<span class="tache‐heure">${formaterHeure(dateCreation)}</span>
</div>
</div>
<button class="btn‐supprimer" aria‐label="Supprimer la tâche">×</button>
`;
return li;
}
/**
* Échappe le HTML pour éviter les failles XSS
*/
function echapperHTML(texte) {
const div = [Link]('div');
[Link] = texte;
return [Link];
}
/**
* Met à jour l'affichage de la liste des tâches
*/
function mettreAJourAffichage() {
// Filtrer les tâches selon le filtre actuel
let tachesFiltrees = taches;
if (filtreActuel === 'actives') {
tachesFiltrees = [Link](t => ![Link]);
} else if (filtreActuel === 'terminees') {
tachesFiltrees = [Link](t => [Link]);
}
// Vider la liste
[Link] = '';
15
// Ajouter les tâches filtrées
[Link](tache => {
const element = creerElementTache(tache);
[Link](element);
});
// Afficher/masquer le message vide
if ([Link] === 0) {
[Link]('cache');
} else {
[Link]('cache');
}
// Mettre à jour le compteur
mettreAJourCompteur();
}
/**
* Met à jour le compteur de tâches actives
*/
function mettreAJourCompteur() {
const nombreActives = [Link](t => ![Link]).length;
[Link] = `${nombreActives} tâche(s) restante(s)`;
}
3.5 Implémentez la Gestion des Événements
/**
* Configure les écouteurs d'événements
*/
function configurerEvenements() {
// Soumission du formulaire
[Link]('submit', function(event) {
[Link]();
const texte = [Link];
const priorite = [Link];
ajouterTache(texte, priorite);
// Réinitialiser le formulaire
[Link] = '';
[Link]();
});
// Délégation d'événements pour la liste des tâches
[Link]('click', function(event) {
const tacheElement = [Link]('.tache');
if (!tacheElement) return;
const id = [Link];
// Clic sur le checkbox
if ([Link]('tache‐checkbox')) {
basculerTache(id);
}
16
// Clic sur le bouton supprimer
if ([Link]('btn‐supprimer')) {
supprimerTache(id);
}
});
// Support clavier pour le checkbox
[Link]('keydown', function(event) {
if ([Link] === 'Enter' || [Link] === ' ') {
if ([Link]('tache‐checkbox')) {
[Link]();
const tacheElement = [Link]('.tache');
if (tacheElement) {
basculerTache([Link]);
}
}
}
});
// Filtres
[Link]('click', function(event) {
if ([Link]('filtre')) {
// Mettre à jour l'état actif
[Link]('.filtre').forEach(btn => {
[Link]('actif');
});
[Link]('actif');
// Appliquer le filtre
filtreActuel = [Link];
mettreAJourAffichage();
}
});
// Supprimer les tâches terminées
[Link]('click', function() {
if ([Link](t => [Link])) {
supprimerTachesTerminees();
}
});
}
3.6 Questions de Compréhension
Q7. Expliquez pourquoi on utilise [Link]() lors de la soumission du formulaire.
Q8. Quel est l’avantage d’utiliser la délégation d’événements pour la liste des tâches ?
Q9. Pourquoi la fonction echapperHTML() est‐elle importante pour la sécurité ?
Exercice 4 : Fonctionnalités Avancées ﴾45 minutes﴿
4.1 Ajout du Support Tactile ﴾Swipe pour Supprimer﴿
Ajoutez ces fonctions au fichier [Link] :
17
/**
* Configure les gestes tactiles
*/
function configurerGesturesTactiles() {
let touchStartX = 0;
let touchStartY = 0;
let elementGlisse = null;
[Link]('touchstart', function(event) {
const tacheElement = [Link]('.tache');
if (!tacheElement) return;
touchStartX = [Link][0].clientX;
touchStartY = [Link][0].clientY;
elementGlisse = tacheElement;
}, { passive: true });
[Link]('touchmove', function(event) {
if (!elementGlisse) return;
const touchX = [Link][0].clientX;
const touchY = [Link][0].clientY;
const diffX = touchX ‐ touchStartX;
const diffY = touchY ‐ touchStartY;
// Vérifier que c'est un mouvement horizontal
if ([Link](diffX) > [Link](diffY) && [Link](diffX) > 10) {
[Link]();
// Limiter le déplacement
const deplacement = [Link](‐100, [Link](0, diffX));
[Link] = `translateX(${deplacement}px)`;
[Link] = 'none';
}
}, { passive: false });
[Link]('touchend', function(event) {
if (!elementGlisse) return;
const touchEndX = [Link][0].clientX;
const diffX = touchEndX ‐ touchStartX;
// Si glissement suffisant vers la gauche, supprimer
if (diffX < ‐80) {
const id = [Link];
[Link] = 'transform 0.3s ease, opacity 0.3s ease';
[Link] = 'translateX(‐100%)';
[Link] = '0';
setTimeout(() => {
supprimerTache(id);
}, 300);
} else {
// Remettre en place
[Link] = 'transform 0.3s ease';
[Link] = 'translateX(0)';
}
18
elementGlisse = null;
});
}
N’oubliez pas d’appeler cette fonction dans initialiser() :
function initialiser() {
// ... code existant ...
// Configurer les gestes tactiles
configurerGesturesTactiles();
}
4.2 Ajout des Notifications
/**
* Demande la permission pour les notifications
*/
async function demanderPermissionNotifications() {
if ('Notification' in window) {
const permission = await [Link]();
return permission === 'granted';
}
return false;
}
/**
* Envoie une notification
*/
function envoyerNotification(titre, corps) {
if ([Link] === 'granted') {
new Notification(titre, {
body: corps,
icon: '�',
vibrate: [200, 100, 200]
});
}
}
// Modifier ajouterTache pour inclure une notification
function ajouterTache(texte, priorite) {
if (![Link]()) {
return;
}
const nouvelleTache = creerTache(texte, priorite);
[Link](nouvelleTache);
sauvegarderTaches();
mettreAJourAffichage();
// Vibration sur mobile
if ('vibrate' in navigator) {
[Link](50);
}
}
19
4.3 Mode Hors Ligne
Créez un fichier [Link] à la racine du projet :
const CACHE_NAME = 'todo‐app‐v1';
const urlsToCache = [
'/',
'/[Link]',
'/[Link]',
'/[Link]'
];
// Installation
[Link]('install', event => {
[Link](
[Link](CACHE_NAME)
.then(cache => {
[Link]('Cache ouvert');
return [Link](urlsToCache);
})
);
});
// Activation
[Link]('activate', event => {
[Link](
[Link]().then(cacheNames => {
return [Link](
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => [Link](name))
);
})
);
});
// Interception des requêtes
[Link]('fetch', event => {
[Link](
[Link]([Link])
.then(response => {
if (response) {
return response;
}
return fetch([Link]);
})
);
});
Ajoutez l’enregistrement du Service Worker dans [Link] :
// À ajouter à la fin du fichier, après le DOMContentLoaded
if ('serviceWorker' in navigator) {
[Link]('load', () => {
[Link]('/[Link]')
.then(registration => {
[Link]('Service Worker enregistré:', [Link]);
})
.catch(error => {
[Link]('Échec de l\'enregistrement du Service Worker:', error);
20
});
});
}
4.4 Questions de Compréhension
Q10. Expliquez le fonctionnement de la détection de swipe dans le code tactile.
Q11. Pourquoi utilise‐t‐on { passive: false } pour l’événement touchmove mais { passive: true
} pour touchstart ?
Q12. Quel est l’avantage du Service Worker pour une application mobile ?
Exercice 5 : Test et Débogage ﴾30 minutes﴿
5.1 Tests Manuels
Effectuez les tests suivants et notez les résultats :
Test Description Résultat
T1 Ajouter une tâche avec du texte valide
T2 Tenter d’ajouter une tâche vide
T3 Marquer une tâche comme terminée
T4 Supprimer une tâche
T5 Filtrer par tâches actives
T6 Filtrer par tâches terminées
T7 Supprimer toutes les tâches terminées
T8 Recharger la page et vérifier la persistance
T9 Tester le swipe pour supprimer ﴾mobile﴿
T10 Tester en mode hors ligne
5.2 Ajout de Tests Automatisés
Ajoutez ces fonctions de test à la fin de [Link] :
/**
* Module de tests
*/
const Tests = (function() {
let testsReussis = 0;
let testsEchoues = 0;
function assertEqual(obtenu, attendu, description) {
if ([Link](obtenu) === [Link](attendu)) {
[Link](`� ${description}`);
testsReussis++;
} else {
[Link](`� ${description}`);
[Link](` Attendu: ${[Link](attendu)}`);
[Link](` Obtenu: ${[Link](obtenu)}`);
testsEchoues++;
}
21
}
function assertTrue(condition, description) {
assertEqual(condition, true, description);
}
function assertFalse(condition, description) {
assertEqual(condition, false, description);
}
function executerTests() {
[Link]('Exécution des tests');
// Test de genererID
const id1 = genererID();
const id2 = genererID();
assertTrue(typeof id1 === 'string', 'genererID retourne une chaîne');
assertTrue(id1 !== id2, 'genererID génère des IDs uniques');
// Test de creerTache
const tache = creerTache('Test', 'haute');
assertTrue([Link] === 'Test', 'creerTache définit le texte');
assertTrue([Link] === 'haute', 'creerTache définit la priorité');
assertFalse([Link], 'creerTache initialise terminee à false');
// Test de echapperHTML
const htmlDangereux = '<script>alert("xss")</script>';
const htmlEchappe = echapperHTML(htmlDangereux);
assertFalse([Link]('<script>'), 'echapperHTML neutralise les scripts');
[Link]();
[Link](`\nRésultats: ${testsReussis} réussis, ${testsEchoues} échoués`);
}
return {
executer: executerTests
};
})();
// Décommenter pour exécuter les tests
// [Link]();
5.3 Débogage avec les DevTools
Ouvrez les DevTools de votre navigateur ﴾F12﴿ et effectuez les actions suivantes :
1. Console : Vérifiez qu’il n’y a pas d’erreurs
2. Application > Local Storage : Vérifiez que les tâches sont bien sauvegardées
3. Application > Service Workers : Vérifiez que le SW est enregistré
4. Network : Simulez le mode hors ligne et testez l’application
5. Performance : Enregistrez une session et identifiez les goulots d’étranglement
Exercice 6 : Bonus ‐ Améliorations ﴾Optionnel﴿
Si vous avez terminé les exercices précédents, implémentez une ou plusieurs de ces améliorations :
22
6.1 Édition des Tâches
Permettez de modifier le texte d’une tâche en double‐cliquant dessus.
6.2 Réorganisation par Glisser‐Déposer
Implémentez la possibilité de réordonner les tâches par glisser‐déposer.
6.3 Export/Import des Données
Ajoutez des boutons pour exporter les tâches en JSON et les importer.
6.4 Thème Personnalisable
Ajoutez un sélecteur de thème avec plusieurs couleurs au choix.
Ressources Complémentaires
• MDN Web Docs ‐ JavaScript
• MDN Web Docs ‐ DOM
• MDN Web Docs ‐ Touch Events
• MDN Web Docs ‐ Service Workers
• [Link] ‐ Progressive Web Apps
23