Comment créer des cartes Apple App Store avec HTML et JS

Récemment, pour un projet au travail, je voulais créer une liste de cartes qui ressemblaient et se comportaient de la même manière que les cartes vues sur l’App Store d’Apple pour iOS. Après avoir recherché des exemples sur le Web, je n’avais pas trouvé un exemple qui ressemblait vraiment à ce que je voulais et j’ai agi; j’ai donc décidé de créer le mien, et je suis maintenant ici pour le partager avec vous tous.

L’une de mes exigences est que, lors de l’extension d’une carte, la position de la carte tapée ne change pas. Ainsi, lorsque je ferme la carte, je suis de nouveau placé dans la même position de défilement. Alors commençons!

Pour cet exemple, j'ai utilisé uniquement les bibliothèques suivantes:

  • jQuery 3.3.1
  • Bootstrap CSS 4.1.0
  • Police Awesome 4.7.0
Ce que nous allons réaliser

Je ne vais pas expliquer la partie HTML ou CSS, je vais donc vous laisser avec le balisage d’une carte et tout le CSS nécessaire aux éléments visuels de notre exemple.

HTML

  
    
      
        

CSS

*.fermer à clé {
  débordement caché;
}
.list-wrapper {
  position: absolue;
  en haut: 0;
  gauche: 0;
  à droite: 0;
  en bas: 0;
  débordement: auto;
}
.cards-list {
  position: relative;
  rembourrage: 15px 15px 20px;
  padding-bottom: calc (20px + env (fond de la zone de sécurité)); / * Pour iPhone X * /
  largeur maximale: 400px;
  marge: auto;
}
.carte {
  position: relative;
  hauteur: 45vh;
  largeur: 100%;
  border-radius: 20px;
  débordement caché;
  box-shadow: 0px 10px 30px 0px rgba (0, 0, 0, 0,1);
  transition: opacité 0,2 s facilité, boîte-ombre 0,2 s facilité;
  opacité: 1;
  marge inférieure: 40 px;
}
.card.open {
  rayon de bordure: 0;
}
.card.hover,
.card: hover {
  box-shadow: 0px 0px 10px 0px rgba (0, 0, 0, 0,1);
}
.card-content {
  position: relative;
  largeur: 100%;
  hauteur: 100%;
  arrière-plan: rgba (255, 255, 255, 0);
  débordement caché;
  transition: facilité au rayon de 0,15 s;
  border-radius: 20px;
  curseur: pointeur;
}
.card.open .card-content {
  z-index: 500! important;
  background: #fff;
  border-radius: 0px;
  curseur: par défaut;
}
.banner-holder {
  position: absolue;
  largeur: 100%;
  débordement caché;
  affichage: flex;
  align-items: centre;
  justifier-contenu: centre;
}
.banner {
  position: relative;
  hauteur: 20vh;
  largeur: 100%;
  rembourrage: 15px;
  taille du fond: couverture;
  position de fond: centre centre;
  répétition de fond: non répétée;
  couleur de fond: transparent;
  débordement caché;
  z-index: 1;
}
.card.open .banner {
  hauteur: 45vh;
  rayon de bordure: 0;
}
.content-holder {
  largeur maximale: 600px;
  rembourrage: 0;
  z-index: 1;
  hauteur: 100%;
  couleur de fond: transparent;
  débordement caché;
  position: relative;
}
.card.open .content-holder {
  débordement: auto;
}
.inner-content {
  en haut: 20vh;
  rembourrage: 20px;
  position: relative;
  couleur d'arrière-plan: #fff;
}
.card.open .inner-content {
  en haut: 40vh;
  padding-bottom: env (zone de sécurité) / * pour iPhone X * /
  bord-haut-gauche-rayon: 20px;
  border-top-right-radius: 20px;
}
.info-titulaire {
  affichage: flex;
  align-items: centre;
}
.date-category {
  text-transform: majuscule;
  taille de police: 12px;
  opacité: 0,5;
  marge inférieure: 0;
  flex: 1;
  marge: 0 1,4r 0 0;
}
.like-wrapper {
  position: relative;
  curseur: pointeur;
  padding-left: 40px;
}
.like-wrapper .fa,
.bookmark-wrapper .fa {
  opacité: 0,3;
}
.like-wrapper.btn-like .fa {
  opacité: 1;
  La couleur rouge;
}
.like-wrapper span {
  position: absolue;
  en haut: 0;
  gauche: 0;
  text-align: right;
  opacité: 0,3;
  largeur: 35px;
  hauteur de ligne: 24px;
  taille de police: 12px;
}
.bookmark-wrapper {
  marge gauche: 8px;
  largeur: 24px;
  text-align: right;
  curseur: pointeur;
}
.Titre {
  position: relative;
  marge: 10px 0 20px 0;
  poids de police: gras;
}
.la description {
  taille de police: 16px;
  hauteur de ligne: 1,5;
  opacité: 0,6;
}
.card: pas (.open) .description {
  débordement caché;
  / * Coupe le contenu sur 3 lignes lorsque la carte est réduite * /
  afficher: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  hauteur: 4.2em;
}
.close-btn {
  position: absolue;
  à droite: 15px;
  en haut: 15px;
  hauteur: 28px;
  largeur: 28px;
  hauteur de ligne: 27px;
  arrière-plan: rgba (255, 255, 255, 0,5);
  rayon de bordure: 50%;
  couleur: # 333;
  text-align: center;
  curseur: pointeur;
  opacité: 0;
  visibilité: cachée;
  transition: tous les 0,15 en douceur;
}
.card.open .close-btn {
  z-index: 600;
  opacité: 0,9;
  visibilité: visible;
}

JavaScript

Passons maintenant à la partie amusante :)

Commençons par créer notre fonction initiale et appelons-la initialiseCards (). Dans cette fonction, nous placerons ce que nous devons exécuter au chargement de la page.

fonction initialiseCards () {
  // Attache des auditeurs
  // Faites d'autres choses, nous y arriverons
}
// Exécuter la fonction en charge
initialiseCards ();

Maintenant que nous avons créé cela, créons la fonction qui va attacher nos écouteurs d’événements.

fonction attachListeners () {
  $ (document)
    .on ("click", ".card-content", function () {
      if ($ (this) .parents (". card"). hasClass ("open")) {
        revenir;
      }
      expandElement ($ (this));
    })
    .on ("click", ".card .close-btn", fonction (événement) {
      event.stopPropagation ();
      collapseElement ($ (this));
    })
    .on ('touchstart', '.card', function () {
      $ (this) .addClass ('survol');
    })
    .on ('touchend touchmove touchcancel', '.card', function () {
      $ (this) .removeClass ('survol');
    });
}

Laissez-moi vous expliquer ces quatre gestionnaires d'événements, en partant du haut:

.on ("click", ".card-content", function () {})

Cet événement se chargera de l'ouverture de chaque carte. L'instruction if empêche la fonction de s'exécuter si la carte est déjà ouverte.

.on ("click", ".card .close-btn", fonction (événement) {})

Ce deuxième événement gère la fermeture de la carte ouverte. J'ai utilisé le event.stopPropagation (); pour vous assurer que rien d’autre n’est déclenché lorsque la carte est encore en train de se fermer, car elle se base sur le minutage de l’animation et que l’événement clic peut passer par des éléments.

.on ('touchstart', '.card', function () {})
.on ('touchend touchmove touchcancel', '.card', function () {})

Les deux derniers éléments s’appliquent uniquement aux appareils mobiles et il s’agit simplement d’une belle touche d’effet visuel. Sur le Web, si vous survolez chaque carte, cela aura pour effet d’être enfoncé. Sur les appareils mobiles, le survol ne fonctionnera pas très bien. J’ajoute donc une classe qui produit le même effet. Cette classe est ajoutée sur Touchstart et supprimée sur trois événements, puis sur Touchmove et Touchcancel. N'hésitez pas à jouer avec ceux-ci.

Après avoir créé notre attachListeners (), nous l'appellerons depuis notre initialiseCards (). Juste comme ça:

fonction initialiseCards () {
  attachListeners ();
  // Faites d'autres choses, nous y arriverons
}

Ensuite, nous allons créer une fonction qui calculera la hauteur de chaque carte réduite en fonction du contenu. C'est utile car les cartes ont besoin d'une hauteur fixe, mais si vos cartes ont un contenu dynamique, vous devez pouvoir le montrer sans contrainte, c'est pourquoi je calcule la hauteur de chaque carte de manière à ce qu'elles ressemblent à leur taille le contenu à l'intérieur.

Créer un setCardHeight ()

fonction setCardHeight () {
  $ (". card") .each (fonction (index, élément) {
    var slideHeight = $ (element)
      .find (". bannière-titulaire")
      .outerHeight ();
    var containerHeight = $ (element)
      .find (". inner-content")
      .outerHeight ();
    var contentHeight = slideHeight + containerHeight;
  $ (element) .css ({
      hauteur: contentHeight
    });
  });
}

Cette fonction obtient la hauteur de la bannière et la hauteur du contenu sous la bannière et les additionne, générant la hauteur de notre carte et l’appliquant à la carte. Maintenant que nous avons cette fonction, nous allons l'appeler depuis notre initialiseCards (), comme ceci:

fonction initialiseCards () {
  attachListeners ();
  setCardHeight ();
}

Enfin, nous avons juste besoin des fonctions pour développer et réduire les cartes. Commençons par le expand.

function expandElement (elementToExpand) {
  // Ajoute la classe 'open' pour aider avec le style
  elementToExpand.parents (". card"). addClass ("open");
  // Empêche le défilement du "corps"
  $ ("body"). addClass ("lock");
  // ensemble variable
  var elementOffset = $ (". list-wrapper"). offset ();
  var elementScrollTop = $ ("body"). scrollTop ();
  var netOffset = elementOffset.top - elementScrollTop;
  var expandPosition = $ (". list-wrapper"). offset ();
  var expandTop = expandPosition.top;
  var expandLeft = expandPosition.left;
  var expandWidth = $ (". list-wrapper"). outerWidth ();
  var expandHeight = $ (". list-wrapper"). outerHeight ();
  $ (". list-wrapper"). css ({
    en haut: netOffset,
    position: "fixe",
    débordement caché",
    "z-index": "11"
  });
  // convertit l'élément développé en position fixe sans le déplacer
  elementToExpand.css ({
    top: elementToExpand.offset (). top - $ ("body"). scrollTop (),
    left: elementToExpand.offset (). left,
    height: elementToExpand.height (),
    width: elementToExpand.width (),
    "max-width": expandWidth,
    position: "fixe"
  });
  // Change la hauteur de la bannière
  var expandHeight = elementToExpand.find (". banner"). data ("height-expand");
  elementToExpand.find (". banner"). animate ({
      hauteur: ExpandHeight
    },
    expandAnimationTiming,
    "aisance"
  )
  // Change la position du contenu
  var expandPosition = elementToExpand
    .find (". inner-content")
    .data ("position-expand");
  elementToExpand.find (". inner-content"). animate ({
      en haut: expandPosition
    },
    expandAnimationTiming,
    "aisance"
  )
  // lance l'animation d'élément de développement dans le wrapper de développement
  // développe l'élément avec la classe .about-tile-bg-image
  elementToExpand.animate ({
      à gauche: expandLeft,
      en haut: expandTop,
      hauteur: expandHeight,
      largeur: expandWidth,
      "max-width": expandWidth
    },
    400, // minutage d'animation en millisecs
    "easlyoutback", // animation simplifiée
    une fonction() {
      elementToExpand.css ({
        à droite: 0,
        en bas: 0,
        largeur: "auto",
        hauteur: "auto"
      });
  elementToExpand.find (". banner-holder"). css ({
        position: "fixe"
      });
    }
  )
}

J'ai laissé quelques commentaires dans le code mais laissez-moi essayer d'expliquer ce qui se passe ici:

  1. Nous ajoutons d’abord une classe .open à la carte. C’est important, car nous gérons la plupart des choses avec CSS et c’est notre sélecteur lorsque la carte est ouverte.
  2. Nous évitons le défilement accidentel du corps.
  3. Ensuite, nous définissons la plupart de nos variables et effectuons des calculs. Dans ces variables, nous obtenons les positions et la taille de l'encapsuleur contenant les cartes. Ces valeurs constitueront les règles sur lesquelles nos cartes vont évoluer.
  4. Nous avons paramétré l'emballage pour qu'il soit corrigé de manière à ce que notre carte étendue soit contenue à l'intérieur.
  5. Ensuite, nous transformons notre carte en un élément fixe, mais en le maintenant à sa place.
  6. Nous déplaçons la position de notre contenu et augmentons la hauteur de la bannière.
  7. Enfin, nous nous animons pour développer.

Nous avons maintenant besoin de la fonction d’effondrement qui inversera fondamentalement toutes les étapes décrites ci-dessus.

fonction collapseElement (collapseButton) {
  // trouve l'élément à réduire
  var elementToCollpseParent = collapseButton.parents (". card");
  var elementToCollpse = elementToCollpseParent.find (". card-content");
  // trouver l'emplacement de l'espace réservé
  var elementToCollpsePlaceholder = elementToCollpse.parents (". card");
  var elementToCollpsePlaceholderTop =
    elementToCollpsePlaceholder.offset (). top - $ ("body"). scrollTop ();
  var elementToCollpsePlaceholderLeft = elementToCollpsePlaceholder.offset ()
    .la gauche;
  var elementToCollpsePlaceholderHeight = elementToCollpsePlaceholder.outerHeight ();
  var elementToCollpsePlaceholderWidth = elementToCollpsePlaceholder.outerWidth ();
  elementToCollpse.find (". banner-holder"). css ({
    position: "absolu"
  });
  // convertit la largeur et la hauteur en valeurs numériques
  elementToCollpse.css ({
    à droite: "auto",
    en bas: "auto",
    width: elementToCollpse.outerWidth (),
    height: elementToCollpse.outerHeight ()
  });
  $ (". list-wrapper"). css ({
    en haut: 0,
    position: "absolu",
    débordement: "auto",
    "z-index": "1"
  });
  // Change la hauteur de la bannière
  var collapsedHeight = elementToCollpse.find (". banner"). data ("height");
  elementToCollpse.find (". banner"). animate ({
      hauteur: collapsedHeight
    },
    collapsingAnimationTiming,
    "linéaire"
  )
  // Change la position du contenu
  var collapsedPosition = elementToCollpse
    .find (". inner-content")
    .data ("position");
  elementToCollpse.find (". inner-content"). animate ({
      en haut: collapsedPosition
    },
    collapsingAnimationTiming,
    "linéaire"
  )
  elementToCollpse.animate ({
    gauche: elementToCollpsePlaceholderLeft,
    en haut: elementToCollpsePlaceholderTop,
    height: elementToCollpsePlaceholderHeight,
    width: elementToCollpsePlaceholderWidth
  },
    200, // chronométrage d'animation en millisecs
    "linéaire", // animation facilitée
    une fonction() {
      // supprime la classe 'ouverte'
      elementToCollpseParent.removeClass ("open");
      elementToCollpse.css ({
        position: "relative",
        en haut: "auto",
        à gauche: "auto",
        largeur: "100%",
        hauteur: "100%"
      });
    }
  )
  // Arrête d'empêcher le défilement 'body'
  $ ("body"). removeClass ("lock");
}

Un dernier détail que je n’ai pas mentionné est que j’ai étendu les fonctions d’assouplissement par défaut de jQuery avec un effet de rebond lors de l’extension de la carte. Vous pouvez utiliser l'interface utilisateur de jQuery pour inclure un ensemble d'autres fonctions d'accélération, mais pour cet exemple, je n'avais besoin que d'une seule et j'ai donc utilisé les éléments suivants:

/ * Étend l’assouplissement d’animations jQuery * /
$ .easing = Object.assign ({}, $ .easing, {
  aisEoutBack: fonction (x, t, b, c, d, s) {
    si (s == non défini) s = 1,70158;
    retourne c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b;
  }
});

De cette façon, nous pouvons utiliser le paramètre easing du paramètre easing de la méthode .animate () lors du développement.

A ce stade, vous devriez avoir quelque chose qui ressemble et fonctionne comme mon exemple. Je vais vous laisser avec un stylo avec tout ce dont nous venons de parler.

C'est tout le monde! J'espère que vous l'apprécierez et que vous en ferez bon usage. N'hésitez pas à le copier et à le modifier selon vos besoins.

Laissez dans les commentaires vos pensées et vos modifications. J'adorerais voir votre travail talentueux.

Jusqu'à la prochaine fois, continuez à résoudre vos problèmes!