agency-skills-suite/agency-shared-references/references/js_interactivity.md
AgentePotente b289d87033 Conversione references in skill condivisa agency-shared-references
- Creata nuova skill agency-shared-references con 24 references centralizzate
- Spostate tutte le references da cartelle sparse (references/, agency-web-developer/, agency-archivist/) in un'unica posizione
- Aggiornati tutti i symlink delle 14 skills per puntare a ../agency-shared-references/references
- Aggiornati tutti i riferimenti nei SKILL.md (percorsi coerenti)
- README.md aggiornato con nuova struttura e istruzioni generiche
- INSTALL.sh semplificato con istruzioni platform-agnostic
- Eliminata cartella references/ dal root (ora centralizzata)
- Struttura più pulita e mantenibile, facile da installare su qualsiasi piattaforma
2026-03-11 00:11:03 +01:00

15 KiB

JavaScript Interactivity — jQuery + GSAP

Guida per aggiungere interattività e animazioni fluide ai siti web.


Setup Librerie

<head>
  <!-- jQuery -->
  <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
  
  <!-- GSAP -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
  
  <!-- GSAP ScrollTrigger (opzionale, per scroll animations) -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>
</head>

<body>
  <!-- Main JS -->
  <script src="js/main.js"></script>
</body>

jQuery Patterns

Document Ready

$(document).ready(function() {
  // Tutto il codice va qui dentro
  // o usa la shorthand:
});

// Shorthand (equivalente)
$(function() {
  // Codice qui
});

Mobile Menu Toggle

$(function() {
  const $menuToggle = $('.mobile-menu-toggle');
  const $navMenu = $('.nav-menu');
  const $body = $('body');
  
  $menuToggle.on('click', function() {
    $navMenu.toggleClass('is-open');
    $menuToggle.toggleClass('is-active');
    $body.toggleClass('menu-open');
    
    // Update ARIA
    const expanded = $menuToggle.attr('aria-expanded') === 'true';
    $menuToggle.attr('aria-expanded', !expanded);
  });
  
  // Close menu on link click (mobile)
  $navMenu.find('a').on('click', function() {
    if ($(window).width() < 768) {
      $navMenu.removeClass('is-open');
      $menuToggle.removeClass('is-active');
      $body.removeClass('menu-open');
      $menuToggle.attr('aria-expanded', 'false');
    }
  });
  
  // Close menu on outside click
  $(document).on('click', function(e) {
    if (!$(e.target).closest('.nav-menu, .mobile-menu-toggle').length) {
      $navMenu.removeClass('is-open');
      $menuToggle.removeClass('is-active');
      $body.removeClass('menu-open');
      $menuToggle.attr('aria-expanded', 'false');
    }
  });
});

Smooth Scroll

$(function() {
  $('a[href^="#"]').on('click', function(e) {
    const targetId = this.getAttribute('href');
    
    // Ignora link vuoti o non-anchor
    if (targetId === '#' || !targetId.startsWith('#')) return;
    
    const $target = $(targetId);
    
    if ($target.length) {
      e.preventDefault();
      
      const offsetTop = $target.offset().top;
      const headerHeight = $('header').outerHeight() || 0;
      const scrollPosition = offsetTop - headerHeight - 20;
      
      $('html, body').stop().animate({
        scrollTop: scrollPosition
      }, 800, 'easeInOutQuad');
      
      // Update URL without jumping
      history.pushState(null, null, targetId);
    }
  });
});

// Easing function (se non usi jQuery easing plugin)
$.easing.easeInOutQuad = function(x, t, b, c, d) {
  t /= d / 2;
  if (t < 1) return c / 2 * t * t + b;
  t--;
  return -c / 2 * (t * (t - 2) - 1) + b;
};

FAQ Accordion

$(function() {
  const $faqQuestions = $('.faq-question');
  
  $faqQuestions.on('click', function() {
    const $question = $(this);
    const $answer = $question.next('.faq-answer');
    const $item = $question.closest('.faq-item');
    const isActive = $question.hasClass('is-active');
    
    // Close all other items (accordion style)
    $faqQuestions.not(this).removeClass('is-active');
    $('.faq-answer').not($answer).slideUp(300);
    $('.faq-item').not($item).removeClass('is-active');
    
    // Toggle current item
    $question.toggleClass('is-active');
    $answer.slideToggle(300);
    $item.toggleClass('is-active');
    
    // Update ARIA
    const expanded = $question.attr('aria-expanded') === 'true';
    $question.attr('aria-expanded', !expanded);
  });
  
  // Keyboard accessibility
  $faqQuestions.on('keydown', function(e) {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      $(this).click();
    }
  });
});

Form Validation

$(function() {
  const $form = $('.contact-form');
  const $submitBtn = $form.find('button[type="submit"]');
  
  $form.on('submit', function(e) {
    let isValid = true;
    
    // Validate required fields
    $form.find('[required]').each(function() {
      const $field = $(this);
      const value = $field.val().trim();
      const $error = $field.siblings('.form-error');
      
      if (!value) {
        isValid = false;
        $field.addClass('is-invalid');
        if ($error.length) {
          $error.text('Questo campo è obbligatorio').show();
        } else {
          $field.after('<span class="form-error">Questo campo è obbligatorio</span>');
        }
      } else {
        $field.removeClass('is-invalid');
        $error.hide();
        
        // Email validation
        if ($field.attr('type') === 'email') {
          const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
          if (!emailRegex.test(value)) {
            isValid = false;
            $field.addClass('is-invalid');
            if ($error.length) {
              $error.text('Inserisci un\'email valida').show();
            } else {
              $field.after('<span class="form-error">Inserisci un\'email valida</span>');
            }
          }
        }
      }
    });
    
    if (!isValid) {
      e.preventDefault();
      // Scroll to first error
      const $firstError = $form.find('.is-invalid').first();
      if ($firstError.length) {
        $('html, body').animate({
          scrollTop: $firstError.offset().top - 100
        }, 500);
      }
    }
  });
  
  // Clear error on input
  $form.find('input, textarea').on('input', function() {
    $(this).removeClass('is-invalid');
    $(this).siblings('.form-error').hide();
  });
  
  // Form submission success (AJAX example with Formspree)
  $form.on('submit', function(e) {
    e.preventDefault();
    
    $submitBtn.prop('disabled', true).text('Invio in corso...');
    
    $.ajax({
      url: $form.attr('action'),
      method: 'POST',
      data: $form.serialize(),
      headers: { 'Accept': 'application/json' }
    })
    .done(function(response) {
      $form.trigger('reset');
      $submitBtn.text('Messaggio Inviato!');
      
      // Show success message
      $form.after('<div class="form-success">Grazie! Ti contatteremo presto.</div>');
      
      setTimeout(function() {
        $submitBtn.prop('disabled', false).text('Invia Messaggio');
        $('.form-success').fadeOut();
      }, 3000);
    })
    .fail(function() {
      $submitBtn.prop('disabled', false).text('Riprova');
      $form.after('<div class="form-error">Errore nell\'invio. Riprova più tardi.</div>');
    });
  });
});

Back to Top Button

$(function() {
  const $backToTop = $('.back-to-top');
  
  // Show/hide on scroll
  $(window).on('scroll', function() {
    if ($(window).scrollTop() > 300) {
      $backToTop.addClass('is-visible');
    } else {
      $backToTop.removeClass('is-visible');
    }
  });
  
  // Smooth scroll to top
  $backToTop.on('click', function(e) {
    e.preventDefault();
    $('html, body').animate({
      scrollTop: 0
    }, 800);
  });
});
<!-- HTML for back to top -->
<a href="#top" class="back-to-top" aria-label="Torna su">
  <svg><!-- icon --></svg>
</a>
.back-to-top {
  position: fixed;
  bottom: 32px;
  right: 32px;
  width: 48px;
  height: 48px;
  background: var(--color-primary);
  color: white;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  visibility: hidden;
  transition: all 0.3s ease;
  z-index: 100;
}

.back-to-top.is-visible {
  opacity: 1;
  visibility: visible;
}

.back-to-top:hover {
  background: var(--color-primary-dark);
  transform: translateY(-4px);
}

Image Lazy Loading

$(function() {
  const $lazyImages = $('img[loading="lazy"]');
  
  // Native lazy loading support check
  if ('loading' in HTMLImageElement.prototype) {
    // Browser supports native lazy loading
    $lazyImages.each(function() {
      const src = $(this).data('src');
      if (src) {
        $(this).attr('src', src);
      }
    });
  } else {
    // Fallback for browsers without support
    const imageObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          const $img = $(entry.target);
          const src = $img.data('src');
          if (src) {
            $img.attr('src', src);
            $img.on('load', function() {
              $img.addClass('is-loaded');
            });
          }
          observer.unobserve(entry.target);
        }
      });
    });
    
    $lazyImages.each(function() {
      imageObserver.observe(this);
    });
  }
});

GSAP Animations

Basic Animations

$(function() {
  // Fade in element
  gsap.from('.hero h1', {
    duration: 1,
    opacity: 0,
    y: 30,
    ease: 'power3.out'
  });
  
  // Stagger animation
  gsap.from('.service-card', {
    duration: 0.8,
    opacity: 0,
    y: 40,
    stagger: 0.15,
    ease: 'power3.out',
    delay: 0.3
  });
  
  // Multiple properties
  gsap.from('.hero .btn', {
    duration: 1,
    opacity: 0,
    y: 20,
    scale: 0.95,
    delay: 0.5,
    ease: 'back.out(1.7)'
  });
});

Timeline

$(function() {
  const tl = gsap.timeline({ defaults: { ease: 'power3.out' } });
  
  tl.from('.hero h1', {
    duration: 1,
    opacity: 0,
    y: 50
  })
  .from('.hero-sub', {
    duration: 0.8,
    opacity: 0,
    y: 30
  }, '-=0.5')
  .from('.hero .btn', {
    duration: 0.6,
    opacity: 0,
    y: 20
  }, '-=0.4')
  .from('.logo-wall', {
    duration: 1,
    opacity: 0
  }, '-=0.3');
});

ScrollTrigger Animations

// Register ScrollTrigger
gsap.registerPlugin(ScrollTrigger);

$(function() {
  // Fade in on scroll
  gsap.utils.toArray('.fade-on-scroll').forEach(function(elem) {
    gsap.to(elem, {
      scrollTrigger: {
        trigger: elem,
        start: 'top 85%',
        toggleActions: 'play none none none'
      },
      opacity: 1,
      y: 0,
      duration: 1,
      ease: 'power3.out'
    });
  });
  
  // Slide in from left
  gsap.utils.toArray('.slide-in-left').forEach(function(elem) {
    gsap.from(elem, {
      scrollTrigger: {
        trigger: elem,
        start: 'top 80%',
        toggleActions: 'play none none none'
      },
      opacity: 0,
      x: -50,
      duration: 1,
      ease: 'power3.out'
    });
  });
  
  // Slide in from right
  gsap.utils.toArray('.slide-in-right').forEach(function(elem) {
    gsap.from(elem, {
      scrollTrigger: {
        trigger: elem,
        start: 'top 80%',
        toggleActions: 'play none none none'
      },
      opacity: 0,
      x: 50,
      duration: 1,
      ease: 'power3.out'
    });
  });
  
  // Scale up
  gsap.utils.toArray('.scale-up').forEach(function(elem) {
    gsap.from(elem, {
      scrollTrigger: {
        trigger: elem,
        start: 'top 85%',
        toggleActions: 'play none none none'
      },
      opacity: 0,
      scale: 0.8,
      duration: 1,
      ease: 'back.out(1.7)'
    });
  });
  
  // Parallax effect
  gsap.to('.parallax-bg', {
    scrollTrigger: {
      trigger: '.hero',
      start: 'top top',
      end: 'bottom top',
      scrub: true
    },
    y: 100,
    ease: 'none'
  });
  
  // Pin section (sticky)
  gsap.to('.sticky-section', {
    scrollTrigger: {
      trigger: '.sticky-section',
      start: 'top top',
      end: '+=100%',
      pin: true,
      scrub: true
    }
  });
});

Hover Animations

$(function() {
  // Card hover with GSAP
  const $cards = $('.service-card');
  
  $cards.each(function() {
    const $card = $(this);
    
    $card.on('mouseenter', function() {
      gsap.to($card, {
        duration: 0.3,
        y: -8,
        boxShadow: '0 12px 32px rgba(0, 0, 0, 0.15)',
        ease: 'power2.out'
      });
    });
    
    $card.on('mouseleave', function() {
      gsap.to($card, {
        duration: 0.3,
        y: 0,
        boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
        ease: 'power2.out'
      });
    });
  });
  
  // Button hover
  const $buttons = $('.btn');
  
  $buttons.each(function() {
    const $btn = $(this);
    
    $btn.on('mouseenter', function() {
      gsap.to($btn, {
        duration: 0.2,
        scale: 1.05,
        ease: 'power2.out'
      });
    });
    
    $btn.on('mouseleave', function() {
      gsap.to($btn, {
        duration: 0.2,
        scale: 1,
        ease: 'power2.out'
      });
    });
  });
});

Complex Animation Sequence

$(function() {
  const tl = gsap.timeline({
    scrollTrigger: {
      trigger: '.feature-section',
      start: 'top 70%',
      toggleActions: 'play none none none'
    }
  });
  
  tl.from('.feature-section h2', {
    duration: 0.8,
    opacity: 0,
    y: 30,
    ease: 'power3.out'
  })
  .from('.feature-section p', {
    duration: 0.6,
    opacity: 0,
    y: 20
  }, '-=0.4')
  .from('.feature-grid .feature-card', {
    duration: 0.6,
    opacity: 0,
    y: 40,
    stagger: 0.1,
    ease: 'back.out(1.7)'
  }, '-=0.3')
  .from('.feature-section .btn', {
    duration: 0.5,
    opacity: 0,
    scale: 0.9
  }, '-=0.3');
});

Performance Best Practices

Reduce Motion

// Check for reduced motion preference
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

if (prefersReducedMotion) {
  // Disable GSAP animations
  gsap.globalTimeline.timeScale(0);
  
  // Or set duration to 0
  gsap.defaults({ duration: 0 });
}

Debounce Scroll Events

// Debounce function
function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

// Usage
$(window).on('scroll', debounce(function() {
  // Scroll logic here
}, 100));

Use CSS for Simple Animations

/* Prefer CSS transitions for simple hover effects */
.btn {
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.btn:hover {
  transform: translateY(-2px);
}

/* Use GSAP only for complex animations */

Accessibility

Respect User Preferences

// Check reduced motion
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

if (!prefersReducedMotion) {
  // Initialize animations
  initAnimations();
}

function initAnimations() {
  // GSAP animations here
}

Focus Management

// Trap focus in mobile menu
function trapFocus($element) {
  const $focusable = $element.find('a, button, input, textarea, select, [tabindex]:not([tabindex="-1"])');
  const $first = $focusable.first();
  const $last = $focusable.last();
  
  $element.on('keydown', function(e) {
    if (e.key === 'Tab') {
      if (e.shiftKey) {
        if (document.activeElement === $first[0]) {
          e.preventDefault();
          $last.focus();
        }
      } else {
        if (document.activeElement === $last[0]) {
          e.preventDefault();
          $first.focus();
        }
      }
    }
  });
}

Checklist Interattività

  • Mobile menu toggle funzionante
  • Smooth scroll per anchor links
  • FAQ accordion accessibile
  • Form validation con feedback
  • Back to top button (se pagine lunghe)
  • Lazy loading immagini
  • GSAP animations (hero, scroll)
  • Hover effects su cards/buttons
  • Respect reduced motion preference
  • Keyboard navigation testata
  • No console errors
  • Performance ottimizzata (debounce, CSS where possible)

References per agency-web-developer skill