2. De Bootstrap a Bulma, y sin jQuery: Smooth Scroll, Scroll Spy y Tooltips de Redes Sociales

20 de Octubre de 2022

Este post es parte de la serie De Bootstrap a Bulma sin jQuery.

Continuando con los cambios hechos en el sitio, mostraré aquí cómo hice los cambios para el desplazamiento suave, el scroll spy, y los tooltips de redes sociales.

El desplazamiento suave (smooth scroll)

Conviene definir primeramente qué es el desplazamiento suave o smooth scroll. Se trata del efecto de transición luego de hacer clic a un enlace que redirige a otra sección de una misma página, evitando así los “saltos bruscos”.
Para los navegadores modernos, esto se resuelve fácilmente con el parámetro behavior: smooth en la función scrollIntoView():

target.scrollIntoView({ // aquí, "target" es un elemento del DOM.
    behavior: 'smooth'
});

pero, al parecer, las opciones no son soportadas en Safari, además de que quería asignarle una ecuación de easing personalizada al scroll, tal como se puede hacer con la función animate() de jQuery. Por lo tanto, manos a la obra.

Adapté una función llamada scrollToY(), que recibe como parámetros la posición actual del scroll y la distancia “destino” desde el “top”.

Como mencioné, quería un método personalizado de animación, así que tuve que hacer uso de requestAnimationFrame. Además, tuve que utilizar un shim.

requestAnimationFrame requiere de una explicación larga… Así que mejor dejo la documentación al pie 😉

window.scrollTo() recibe como parámetros las coordenadas x e y. Como no hay desplazamiento horizontal, siempre envío como 0 como primer argumento .

En los comentarios del código figuran las distintas fuentes que utilicé para hacer las adaptaciones correspondientes.

Un shim podría definirse como un “calce” o “cuña” (traducido literalmente), es decir, una adaptación para la compatibilidad de una funcionalidad en distintos entornos.

document.addEventListener('DOMContentLoaded', () => {

  // fuente1: https://jsfiddle.net/00uw1Lq9/4/
  // fuente2: https://rohankulkarni.hashnode.dev/implementing-smooth-scroll-using-javascript
  // fuente3: https://medium.com/@snowleo208/how-to-create-smooth-scroll-for-your-website-ce5b198d9d94
  // fuente final: https://codepen.io/cassadyblake/pen/gOwJyWP, desde https://stackoverflow.com/q/65908034/

  //todos los "a" con la clase "principal", que cuenten con un href que contenga un "#" (pero que no sea exactamente igual a "#")
  document.querySelectorAll('a.principal[href*="#"]:not([href="#"])').forEach(anchor => {
    anchor.addEventListener('click', function (e) {
      // hacer smooth scroll solo si es en la página principal
      if (location.pathname.replace(/^\//, '') == this.pathname.replace(/^\//, '') && location.hostname == this.hostname) {
        e.preventDefault(); //evita la acción por defecto: el autoscroll al ancla (anchor) seleccionada

        //slice(1): en este caso, logra remover el '#'. 
        target = document.querySelector(e.target.getAttribute('href').slice(1)); 
        
        if(target){
          const currentPosScroll = document.documentElement.scrollTop || document.body.scrollTop //posicion actual del scroll
          const destDistance = target.offsetTop;
          scrollToY(currentPosScroll, destDistance);
        }
      }
    });
  });
  
  opciones = {
    duracion: 1000, // Píxeles por segundo. A menor el valor, mayor la duración.
    easing: 'easeInOutExpo' //ecuación de easing
  }

  /*** Implementación de Smooth Scroll con Easing ***/
    // shim de rAF, fuente: http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ ; rAF => request Animation Frame
    window.requestAnimFrame = (function(){
      return  window.requestAnimationFrame       ||
              window.webkitRequestAnimationFrame ||
              window.mozRequestAnimationFrame    ||
              function( callback ){
                window.setTimeout(callback, 1000 / 60); // 60 fps
              };
    })();

      // Función para "scrollear" al hacer clic
    function scrollToY(posicionActual, targetY) {
      // posicionActual: la propiedad scrollY actual de la vista dentro del contenedor del scroll
      // scrollDestinoY: la propiedad scrollY target (destino) de la ventana
      // velocidad: tiempo en pixeles por segundo
      // easing: ecuación de aceleración (easing) a utilizar

      let scrollY = posicionActual,
          scrollDestinoY = targetY || 0,
          velocidad = opciones.duracion || 2000,
          easing = opciones.easing || 'easeInOutExpo', // lo dejé así por si algún día quiero agregar otra ecuación (?
          tiempoActual = 0;

      // *Para la velocidad en pixeles por segundo 
      // tiempo mín 2, tiempo máx 6 segundos
      let tiempo = Math.max(2, Math.min(Math.abs(scrollY - scrollDestinoY) / velocidad, 6));


      // fuente de la ecuación de aceleración (easing): https://github.com/danro/easing-js/blob/master/easing.js
          ecuacionesEasing = {
              easeInOutExpo: function(pos) {
                    if(pos===0) return 0;
                    if(pos===1) return 1;
                    if((pos/=0.5) < 1) return 0.5 * Math.pow(2,10 * (pos-1));
                    return 0.5 * (-Math.pow(2, -10 * --pos) + 2);
                  }
          };

      // bucle de animación
      function animarScroll() {
        tiempoActual += 1 / 60; // 60 es por los fps
          const p = tiempoActual / tiempo;
          const t = ecuacionesEasing[easing](p); // esta sintaxis representa que el valor de la clave "easing" (easing = "easeInOutExpo") en el objeto "ecuacionesEasing" es una función, y está recibiendo "p" como argumento

          if (p < 1) {
              requestAnimFrame(animarScroll);
              const nuevaPosicion = scrollY + ((scrollDestinoY - scrollY) * t);

              window.scrollTo(0, nuevaPosicion);
                    
          } else {
              window.scrollTo(0, scrollDestinoY);
          }
      }

      // llamarlo una vez para iniciarlo
      animarScroll();
    }
});

El Scrollspy

El desplazamiento espía permite resaltar el elemento de la barra de navegación en función a la posición actual de la barra de desplazamiento. En pocas palabras, y en este caso: resaltará el ítem del menú según sea la posición actual del scrollbar en la página. Ej.: si se está en la sección skills, resaltará en otro color (como “activo”) ese ítem en el menú, lo mismo para la sección blog, y las demás.

Aquí, hice uso de la API de IntersectionObserver. Se compara la sección actual con cada ítem del menú. Si coinciden, agrega la clase is-active (la cual modifica el color del texto del ítem del menú), caso contrario, remueve la clase. Los elementos observados aquí consisten en cada section de la página principal.

  const configInterseccion = {
    // umbral: 50%
      threshold: .5
  }
  
  const observer = new IntersectionObserver(entries => {

    entries.forEach(entry => {
      const elementoActual = entry.target,
            aActual = document.querySelector(`nav ul li a[href='/#${elementoActual.id}']`);
      if (entry.isIntersecting) {
        aActual.classList.add('is-active');
      } else {
        aActual.classList.remove('is-active');
      }
    });
  }, configInterseccion);

  document.querySelectorAll('section[id]').forEach((section) => {
    observer.observe(section);
  });

Los tooltips de Redes Sociales

Para reemplazar la funcionalidad de tooltip de Bootstrap (que en realidad se basa en Popper.js), utilicé una extensión de Bulma (que figura en el sitio oficial) denominada Bulma Tooltip. Su uso es bastante sencillo: basta utilizar el atributo data-tooltip para el texto a mostrar, y agregar la clase has-tooltip-<top/right/bottom/left> para establecer su ubicación.

Enlaces Útiles

Hugo Theme: "Bulma Hugo Resume", basado en  Hugo Resume