- New skill for building static websites from copy + design system - HTML semantic with accessibility + SEO best practices - CSS layout with Bootstrap or custom support - JS interactivity with jQuery + GSAP for smooth animations - 3 reference files: html_semantics.md, css_layout.md, js_interactivity.md - Updated README with new skill (13 total) and references (23 total) - Workflow updated: agency-ux-copy → agency-web-developer → agency-seo No overlap with existing skills: - agency-visual-generator: generates images (PNG/webp) for social/YouTube - agency-publisher: publishes content via webhook - agency-web-developer: builds complete static websites (HTML/CSS/JS)
730 lines
15 KiB
Markdown
730 lines
15 KiB
Markdown
# JavaScript Interactivity — jQuery + GSAP
|
|
|
|
Guida per aggiungere interattività e animazioni fluide ai siti web.
|
|
|
|
---
|
|
|
|
## Setup Librerie
|
|
|
|
### CDN Links
|
|
|
|
```html
|
|
<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
|
|
|
|
```javascript
|
|
$(document).ready(function() {
|
|
// Tutto il codice va qui dentro
|
|
// o usa la shorthand:
|
|
});
|
|
|
|
// Shorthand (equivalente)
|
|
$(function() {
|
|
// Codice qui
|
|
});
|
|
```
|
|
|
|
### Mobile Menu Toggle
|
|
|
|
```javascript
|
|
$(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
|
|
|
|
```javascript
|
|
$(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
|
|
|
|
```javascript
|
|
$(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
|
|
|
|
```javascript
|
|
$(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
|
|
|
|
```javascript
|
|
$(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
|
|
<!-- HTML for back to top -->
|
|
<a href="#top" class="back-to-top" aria-label="Torna su">
|
|
<svg><!-- icon --></svg>
|
|
</a>
|
|
```
|
|
|
|
```css
|
|
.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
|
|
|
|
```javascript
|
|
$(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
|
|
|
|
```javascript
|
|
$(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
|
|
|
|
```javascript
|
|
$(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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
$(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
|
|
|
|
```javascript
|
|
$(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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```css
|
|
/* 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
|
|
|
|
```javascript
|
|
// Check reduced motion
|
|
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
|
|
if (!prefersReducedMotion) {
|
|
// Initialize animations
|
|
initAnimations();
|
|
}
|
|
|
|
function initAnimations() {
|
|
// GSAP animations here
|
|
}
|
|
```
|
|
|
|
### Focus Management
|
|
|
|
```javascript
|
|
// 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_
|