diff --git a/index.html b/index.html index c7d89f7c331f0e6412fde5c659215e78468a9c4a..4782e2e9b66de1191d77710e078ccabb3392be01 100644 --- a/index.html +++ b/index.html @@ -34,6 +34,8 @@ <li><a href="/help" class="helpLink">Support</a></li> </ul> </nav> + <div class="testdiv"><button class="test">test1</button><button class="test">test2</button><button class="test">test3</button></div> + <div class="testdiv2"><button class="test2">test1</button><button class="test2">test2</button><button class="test2">test3</button></div> </header> <section class="viewContainer"> <header class="viewTitle"></header> diff --git a/package.json b/package.json index 0eaac26440b030a80f378c1a853852d29741b951..cc8d4bb13d332bd02b23301feb8a40e19df2de60 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack --mode=production", "watch": "webpack --mode=development --watch", - "start": "webpack serve --mode=development" + "start": "webpack serve --mode=development" }, "author": "Thomas Fritsch <thomas.fritsch@univ-lille.fr> (https://gitlab.univ-lille.fr/thomas.fritsch)", "homepage": "https://gitlab.univ-lille.fr/js", diff --git a/src/AboutView.js b/src/AboutView.js index 03c1bcf06f0c44c6764c14fbda4f3d43ce4238c8..e4fe79aeb8f28c337028c2402fb5a96d88b4e53a 100644 --- a/src/AboutView.js +++ b/src/AboutView.js @@ -1,5 +1,6 @@ import Router from './Router.js'; import View from './View.js'; +import $ from './lib/jqlite.js'; /** * Classe de la page "A propos" @@ -8,6 +9,7 @@ import View from './View.js'; export default class AboutView extends View { show() { super.show(); + this.element.addClass('is-loading'); // quand on affiche la page, on charge le fichier `about.html` // on pourrait optimiser ici le fonctionnement en ne le chargeant qu'une seule fois // (pour le moment à chaque fois qu'on affiche la view, une nouvelle requête AJAX est déclenchée) @@ -24,12 +26,16 @@ export default class AboutView extends View { * @see #handleButtonClick */ showFileContent(html) { - this.element.innerHTML = html; + this.element.html(html); + this.element.removeClass('is-loading'); // l'ajout d'écouteur d'événement ne peut se faire qu'une fois que la page HTML a été modifiée // (sinon le bouton n'existe pas encore) + // this.element + // .querySelector('.button') + // .addEventListener('click', event => this.handleButtonClick(event)); this.element - .querySelector('.button') - .addEventListener('click', event => this.handleButtonClick(event)); + .find('.button') + .on('click', event => this.handleButtonClick(event)); } /** * Méthode déclenchée par le click sur le bouton "Nous contacter" diff --git a/src/GameDetailView.js b/src/GameDetailView.js index 40f329099f360bc2d7ee47a34621b981ab226771..d5b63ea0b90af28a1084a903f2afc71eeae7ce93 100644 --- a/src/GameDetailView.js +++ b/src/GameDetailView.js @@ -1,6 +1,7 @@ import { API_KEY } from './config.js'; import Router from './Router.js'; import View from './View.js'; +import $ from './lib/jqlite.js'; export default class GameDetailView extends View { game; @@ -13,7 +14,8 @@ export default class GameDetailView extends View { show(slug) { super.show(); // ajout de l'état "loading" - this.element.classList.add('is-loading'); + $(this.element).addClass('is-loading'); + //this.element.classList.add('is-loading'); // création des URL de l'API pour le détail et les screenshots const detailApiUrl = `https://api.rawg.io/api/games/` + encodeURIComponent(slug), @@ -28,7 +30,7 @@ export default class GameDetailView extends View { .then(data => { this.screenshots = data; this.render(); - this.element.classList.remove('is-loading'); + $(this.element).removeClass('is-loading'); }); } @@ -51,7 +53,7 @@ export default class GameDetailView extends View { } = this.game; const releasedDate = new Date(released); // rendu dans la vue - this.element.innerHTML = /*html*/ ` + $(this.element).html(` <div class="backgroundImage"> <img src="${background_image}" /> </div> @@ -91,14 +93,20 @@ export default class GameDetailView extends View { </div> </section> <section class="description">${description}</section> - `; + `); // écoute du clic sur les liens du "fil d'Ariane" pour éviter le rechargement de page - this.element.querySelectorAll('.breadcrumb a').forEach(link => - link.addEventListener('click', event => { + $(this.element) + .find('.breadcrumb a') + .on('click', event => { event.preventDefault(); Router.navigate(event.currentTarget.getAttribute('href')); - }) - ); + }); + // this.element.querySelectorAll('.breadcrumb a').forEach(link => + // link.addEventListener('click', event => { + // event.preventDefault(); + // Router.navigate(event.currentTarget.getAttribute('href')); + // }) + // ); } } diff --git a/src/GameListView.js b/src/GameListView.js index 378783e35d7743ed74d7177d4638565db618567e..ad9f1bf82533cb6c6e80c83acb5df8beb7d10832 100644 --- a/src/GameListView.js +++ b/src/GameListView.js @@ -2,6 +2,7 @@ import { API_KEY } from './config.js'; import renderGameThumbnail from './renderGameThumbnail.js'; import Router from './Router.js'; import View from './View.js'; +import $ from './lib/jqlite.js'; export default class GameListView extends View { searchForm; @@ -10,13 +11,15 @@ export default class GameListView extends View { constructor(element) { super(element); // détection du clic sur le bouton "loupe" pour afficher/masquer le form de recherche - this.toggleSearchButton = this.element.querySelector('.toggleSearchButton'); - this.toggleSearchButton.addEventListener('click', event => + // this.toggleSearchButton = this.element.querySelector('.toggleSearchButton'); + this.toggleSearchButton = $(this.element).find('.toggleSearchButton'); + $(this.toggleSearchButton).on('click', event => this.toggleSearchForm(event) ); // détection de la soumission du formulaire de recherche - this.searchForm = this.element.querySelector('.searchForm'); - this.searchForm.addEventListener('submit', event => + //this.searchForm = this.element.querySelector('.searchForm'); + this.searchForm = $(this.element).find('.searchForm'); + $(this.searchForm).on('submit', event => this.handleSearchFormSubmit(event) ); } @@ -36,9 +39,11 @@ export default class GameListView extends View { * @param {string} ordering ordre d'affichage des résultats */ renderGameList(search = '', ordering) { - this.element.querySelector('.results').classList.add('is-loading'); - this.searchForm.querySelector('button').disabled = true; - this.searchForm.querySelector('button').setAttribute('disabled', true); + // this.element.querySelector('.results').classList.add('is-loading'); + // this.searchForm.querySelector('button').disabled = true; + // this.searchForm.querySelector('button').setAttribute('disabled', true); + $(this.element).find('.results').addClass('is-loading'); + $(this.searchForm.find('button')).attr('disabled', true); fetch( `https://api.rawg.io/api/games?search=${encodeURIComponent( search @@ -49,19 +54,28 @@ export default class GameListView extends View { // rendu de la liste des jeux let html = ''; data.results.forEach(game => (html += renderGameThumbnail(game))); - this.element.querySelector('.results').innerHTML = html; - // suppression du "loader" et réactivation du formulaire - this.element.querySelector('.results').classList.remove('is-loading'); - this.searchForm.querySelector('button').disabled = false; + // this.element.querySelector('.results').innerHTML = html; + // // suppression du "loader" et réactivation du formulaire + // this.element.querySelector('.results').classList.remove('is-loading'); + // this.searchForm.querySelector('button').disabled = false; + $(this.element).find('.results').html(html); + $(this.element).find('.results').removeClass('is-loading'); + $(this.searchForm.find('button')).removeAttr('disabled'); // détection du clic sur les vignettes de jeu // pour navigation vers la page détail (sans rechargement de page) - const gameLinks = this.element.querySelectorAll('.results > a'); - gameLinks.forEach(gameLink => - gameLink.addEventListener('click', event => { + // const gameLinks = this.element.querySelectorAll('.results > a'); + // gameLinks.forEach(gameLink => + // gameLink.addEventListener('click', event => { + // event.preventDefault(); + // Router.navigate(gameLink.getAttribute('href')); + // }) + // ); + $(this.element) + .find('.results > a') + .on('click', event => { event.preventDefault(); - Router.navigate(gameLink.getAttribute('href')); - }) - ); + Router.navigate(event.currentTarget.getAttribute('href')); + }); }); } /** @@ -69,13 +83,13 @@ export default class GameListView extends View { * @param {Event} event événement déclenché par le bouton sur lequel on a cliqué */ toggleSearchForm(event) { - const isOpened = this.searchForm.getAttribute('style') !== 'display: none;'; + const isOpened = $(this.searchForm).attr('style') !== 'display: none;'; if (!isOpened) { - this.searchForm.setAttribute('style', ''); - this.toggleSearchButton.classList.add('opened'); + $(this.searchForm).attr('style', ''); + $(this.toggleSearchButton).addClass('opened'); } else { - this.searchForm.setAttribute('style', 'display: none;'); - this.toggleSearchButton.classList.remove('opened'); + $(this.searchForm).attr('style', 'display: none;'); + $(this.toggleSearchButton).removeClass('opened'); } } /** @@ -86,8 +100,8 @@ export default class GameListView extends View { */ handleSearchFormSubmit(event) { event.preventDefault(); - const searchInput = this.searchForm.querySelector('[name=search]'), - orderingSelect = this.searchForm.querySelector('[name=ordering]'); - this.renderGameList(searchInput.value, orderingSelect.value); + const searchInput = $(this.searchForm).find('[name=search]'), + orderingSelect = $(this.searchForm).find('[name=ordering]'); + this.renderGameList(searchInput.val(), orderingSelect.val()); } } diff --git a/src/HelpView.js b/src/HelpView.js index 898adedef7fbfd2c93866c9695961c5c1b4e3a88..be02c5b5615acf4faa4d72534ffd14fd8ccb11d8 100644 --- a/src/HelpView.js +++ b/src/HelpView.js @@ -1,4 +1,5 @@ import View from './View.js'; +import $ from './lib/jqlite.js'; export default class HelpView extends View { /** @@ -9,9 +10,9 @@ export default class HelpView extends View { constructor(element) { super(element); // détection du submit du formulaire - this.element - .querySelector('.helpForm') - .addEventListener('submit', event => this.handleSubmit(event)); + $(this.element) + .find('.helpForm') + .on('submit', event => this.handleSubmit(event)); } /** @@ -23,11 +24,11 @@ export default class HelpView extends View { handleSubmit(event) { event.preventDefault(); // récupération des 2 champs du formulaire - const subjectInput = this.element.querySelector('input[name=subject]'); - const bodyTextarea = this.element.querySelector('textarea[name=body]'); + const subjectInput = $(this.element).find('input[name=subject]'); + const bodyTextarea = $(this.element).find('textarea[name=body]'); // récupération du texte saisi par l'utilisateur.rice - const subject = subjectInput.value, - body = bodyTextarea.value; + const subject = subjectInput.val(), + body = bodyTextarea.val(); // vérification des champs obligatoires if (subject === '') { alert('le champ "SUJET" est obligatoire'); @@ -42,8 +43,8 @@ export default class HelpView extends View { subject )}&body=${encodeURIComponent(body)}`; // on vide les champs - subjectInput.value = ''; - bodyTextarea.value = ''; + subjectInput.val(''); + bodyTextarea.val(''); // on aurait aussi pu faire : form.reset(); } -} \ No newline at end of file +} diff --git a/src/Router.js b/src/Router.js index 08ec59c339a526b7dbc5e1c278d6a5ed6699d30e..a6c66378a0a3568b70e80cff7a0e60fbabb6796d 100644 --- a/src/Router.js +++ b/src/Router.js @@ -1,3 +1,4 @@ +import $ from './lib/jqlite.js'; /** * Classe Router qui permet de gérer la navigation dans l'application sans rechargement de page. * (Single Page Application) @@ -23,15 +24,23 @@ export default class Router { static setMenuElement(menuElement) { this.#menuElement = menuElement; // on écoute le clic sur tous les liens du menu - const menuLinks = this.#menuElement.querySelectorAll('a'); - menuLinks.forEach(link => - link.addEventListener('click', event => { - event.preventDefault(); - // on récupère le href du lien cliqué pour déclencher navigate(...) - const linkHref = event.currentTarget.getAttribute('href'); - Router.navigate(linkHref); - }) - ); + // const menuLinks = this.#menuElement.querySelectorAll('a'); + // menuLinks.forEach(link => + // link.addEventListener('click', event => { + // event.preventDefault(); + // // on récupère le href du lien cliqué pour déclencher navigate(...) + // const linkHref = event.currentTarget.getAttribute('href'); + // Router.navigate(linkHref); + // }) + // ); + + const $menuLinks = $(this.#menuElement).find('a'); + $menuLinks.on('click', event => { + event.preventDefault(); + // on récupère le href du lien cliqué pour déclencher navigate(...) + const linkHref = $(event.currentTarget).attr('href'); + Router.navigate(linkHref); + }); } /** * Affiche la view correspondant à `path` dans le tableau `routes` @@ -70,13 +79,14 @@ export default class Router { viewParam = pathEnd; } route.view.show(viewParam); - this.titleElement.innerHTML = `<h1>${route.title}</h1>`; + $(this.titleElement).html(`<h1>${route.title}</h1>`); // Activation/désactivation des liens du menu - const previousMenuLink = this.#menuElement.querySelector('a.active'), - newMenuLink = this.#menuElement.querySelector(`a[href="${path}"]`); - previousMenuLink?.classList.remove('active'); // on retire la classe "active" du précédent menu - newMenuLink?.classList.add('active'); // on ajoute la classe CSS "active" sur le nouveau lien + const $previousMenuLink = $(this.#menuElement).find('a.active'), + $newMenuLink = $(this.#menuElement).find(`a[href="${path}"]`); + + $previousMenuLink.removeClass('active'); // on retire la classe "active" du précédent menu + $newMenuLink.addClass('active'); // on ajoute la classe CSS "active" sur le nouveau lien // History API : ajout d'une entrée dans l'historique du navigateur // pour pouvoir utiliser les boutons précédent/suivant diff --git a/src/View.js b/src/View.js index 2c0a338f58a92a3d0cee0dcebf11052f5d8058ca..6041ddbc5bdbfdd193c92c76ef25cfac0c141882 100644 --- a/src/View.js +++ b/src/View.js @@ -1,3 +1,4 @@ +import $ from './lib/jqlite.js'; /** * Classe de base des vues de notre application. * Permet d'associer une balise HTML à la vue et de l'afficher/masquer. @@ -15,12 +16,12 @@ export default class View { * Affiche la vue en lui ajoutant la classe CSS `active` */ show() { - this.element.classList.add('active'); + $(this.element).addClass('active'); } /** * Masque la vue en enlevant la classe CSS `active` */ hide() { - this.element.classList.remove('active'); + $(this.element).removeClass('active'); } } diff --git a/src/lib/JQObject.js b/src/lib/JQObject.js new file mode 100644 index 0000000000000000000000000000000000000000..72158fdeff07bac90e8c9923855295bfd023e5fd --- /dev/null +++ b/src/lib/JQObject.js @@ -0,0 +1,76 @@ +import $ from './jqlite.js'; + +export default class JQObject { + constructor(DOMElements) { + let i = 0; + DOMElements.forEach(Element => { + this[i] = Element; + i++; + }); + this.length = i; + } + + html(newHTML) { + for (let i = 0; i < this.length; i++) { + this[i].innerHTML = newHTML; + } + return this; + } + + append(newHTML) { + return this.html(this.html() + newHTML); + } + + find(querySelector) { + return $(querySelector, this[0]); + } + + addClass(className) { + for (let i = 0; i < this.length; i++) { + this[i].classList.add(className); + } + return this; + } + + removeClass(className) { + for (let i = 0; i < this.length; i++) { + this[i].classList.remove(className); + } + return this; + } + + attr(attributeName, newValue = null) { + if (newValue != null) { + for (let i = 0; i < this.length; i++) { + this[i].setAttribute(attributeName, newValue); + } + return this; + } + + return this[0].getAttribute(attributeName); + } + + removeAttr(attributeName) { + for (let i = 0; i < this.length; i++) { + this[i].removeAttribute(attributeName); + } + return this; + } + + val(newValue = null) { + if (newValue) { + for (let i = 0; i < this.length; i++) { + this[i].value = newValue; + } + return this; + } + return this[0].value; + } + + on(eventName, eventHandler) { + if (this[0]) { + this[0].addEventListener(eventName, eventHandler); + } + return this; + } +} diff --git a/src/lib/jqlite.js b/src/lib/jqlite.js new file mode 100644 index 0000000000000000000000000000000000000000..077cce546a7c4a8180b8f06b2ca2b02c18d83dad --- /dev/null +++ b/src/lib/jqlite.js @@ -0,0 +1,15 @@ +import JQObject from './JQObject.js'; + +export default function $(querySelector, element = document) { + if (typeof querySelector === 'object') { + if (querySelector instanceof JQObject) { + return querySelector; + } + querySelector = querySelector.className; + querySelector = querySelector + .split(' ') + .map(className => '.' + className) + .join(' '); + } + return new JQObject(element.querySelectorAll(querySelector)); +} diff --git a/src/main.js b/src/main.js index dd4916d4bc5db1996e0141254249989c87cf3b2a..f93fd52616d1eda496286383a3e3d74644ededc1 100644 --- a/src/main.js +++ b/src/main.js @@ -3,22 +3,31 @@ import GameDetailView from './GameDetailView.js'; import GameListView from './GameListView.js'; import HelpView from './HelpView.js'; import Router from './Router.js'; +import $ from './lib/jqlite.js'; + +$('.logo').on('click', event => { + event.preventDefault(); + Router.navigate('/', true); +}); + +$('body > footer a').on('mouseover', event => { + $(event.currentTarget).addClass('mouseover'); +}); + +$('body > footer a').on('mouseout', event => { + $(event.currentTarget).removeClass('mouseover'); +}); // Modification du footer -document.querySelector('body > footer > div:nth-of-type(2)').innerHTML += - ' / CSS inspirée de <a href="https://store.steampowered.com/">steam</a>'; +$('body > footer > div:nth-of-type(2)').append( + ' / CSS inspirée de <a href="https://store.steampowered.com/">steam</a>' +); // création des vues de notre application -const helpView = new HelpView(document.querySelector('.viewContent .help')); -const gameListView = new GameListView( - document.querySelector('.viewContent > .gameList') -); -const aboutView = new AboutView( - document.querySelector('.viewContent > .about') -); -const gameDetailView = new GameDetailView( - document.querySelector('.viewContent > .gameDetail') -); +const helpView = new HelpView($('.viewContent .help')); +const gameListView = new GameListView($('.viewContent > .gameList')); +const aboutView = new AboutView($('.viewContent > .about')); +const gameDetailView = new GameDetailView($('.viewContent > .gameDetail')); // mise en place du Router const routes = [ @@ -29,11 +38,19 @@ const routes = [ ]; Router.routes = routes; // élément dans lequel afficher le <h1> de la vue -Router.titleElement = document.querySelector('.viewTitle'); +Router.titleElement = $('.viewTitle'); // gestion des liens du menu (détection du clic et activation/désactivation) -Router.setMenuElement(document.querySelector('.mainMenu')); +Router.setMenuElement($('.mainMenu')); // chargement de la vue initiale selon l'URL demandée par l'utilisateur.rice (Deep linking) Router.navigate(window.location.pathname, true); // gestion des boutons précédent/suivant du navigateur (History API) window.onpopstate = () => Router.navigate(document.location.pathname, true); + +// console.log($('.logo span').html('<em>jquery</em>steam')); +// console.log($('a').html('<em>jquery</em>steam')); + +// import $$ from './lib/jqlite.js'; + +// $('.testdiv').find('.test').attr('test', 'hello'); +// $$('.testdiv2').find('.test2').attr('test', 'hello');