- 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
15 KiB
15 KiB
JavaScript Interactivity — jQuery + GSAP
Guida per aggiungere interattività e animazioni fluide ai siti web.
Setup Librerie
CDN Links
<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