926 lines
28 KiB
JavaScript
926 lines
28 KiB
JavaScript
/**
|
||
* VIBE BRUTALISM - JavaScript Component Library
|
||
* Interactive functionality for neo-brutalist components
|
||
* Version: 2.0.0
|
||
* WCAG 2.1 AA & Section 508 Compliant
|
||
*/
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
// ============================================
|
||
// ACCESSIBILITY UTILITIES
|
||
// ============================================
|
||
|
||
const a11y = {
|
||
// Create ARIA live region for announcements
|
||
createLiveRegion() {
|
||
if (!document.getElementById('vb-live-region')) {
|
||
const liveRegion = document.createElement('div');
|
||
liveRegion.id = 'vb-live-region';
|
||
liveRegion.className = 'vb-sr-only';
|
||
liveRegion.setAttribute('aria-live', 'polite');
|
||
liveRegion.setAttribute('aria-atomic', 'true');
|
||
document.body.appendChild(liveRegion);
|
||
}
|
||
return document.getElementById('vb-live-region');
|
||
},
|
||
|
||
// Announce message to screen readers
|
||
announce(message, priority = 'polite') {
|
||
const liveRegion = this.createLiveRegion();
|
||
liveRegion.setAttribute('aria-live', priority);
|
||
liveRegion.textContent = message;
|
||
|
||
// Clear after announcement
|
||
setTimeout(() => {
|
||
liveRegion.textContent = '';
|
||
}, 1000);
|
||
},
|
||
|
||
// Trap focus within element
|
||
trapFocus(element) {
|
||
const focusableElements = element.querySelectorAll(
|
||
'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
||
);
|
||
|
||
if (focusableElements.length === 0) return null;
|
||
|
||
const firstFocusable = focusableElements[0];
|
||
const lastFocusable = focusableElements[focusableElements.length - 1];
|
||
|
||
const handler = (e) => {
|
||
if (e.key === 'Tab') {
|
||
if (e.shiftKey && document.activeElement === firstFocusable) {
|
||
e.preventDefault();
|
||
lastFocusable.focus();
|
||
} else if (!e.shiftKey && document.activeElement === lastFocusable) {
|
||
e.preventDefault();
|
||
firstFocusable.focus();
|
||
}
|
||
}
|
||
};
|
||
|
||
element.addEventListener('keydown', handler);
|
||
firstFocusable.focus();
|
||
|
||
// Return cleanup function
|
||
return () => element.removeEventListener('keydown', handler);
|
||
}
|
||
};
|
||
|
||
// ============================================
|
||
// MODAL FUNCTIONALITY (WCAG 2.1 AA)
|
||
// ============================================
|
||
|
||
class VBModal {
|
||
constructor(element) {
|
||
this.modal = element;
|
||
this.closeButtons = this.modal.querySelectorAll('.vb-modal-close, [data-vb-modal-close]');
|
||
this.previousActiveElement = null;
|
||
this.focusTrapCleanup = null;
|
||
this.escHandler = this.handleEscape.bind(this);
|
||
this.clickHandler = this.handleBackdropClick.bind(this);
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
// Set ARIA attributes
|
||
this.modal.setAttribute('role', 'dialog');
|
||
this.modal.setAttribute('aria-modal', 'true');
|
||
|
||
if (!this.modal.getAttribute('aria-labelledby')) {
|
||
const title = this.modal.querySelector('.vb-modal-title');
|
||
if (title && !title.id) {
|
||
title.id = `modal-title-${Math.random().toString(36).substr(2, 9)}`;
|
||
}
|
||
if (title) {
|
||
this.modal.setAttribute('aria-labelledby', title.id);
|
||
}
|
||
}
|
||
|
||
// Close button events
|
||
this.closeButtons.forEach(btn => {
|
||
btn.setAttribute('aria-label', 'Close dialog');
|
||
btn.addEventListener('click', () => this.close());
|
||
});
|
||
|
||
// Close on outside click
|
||
this.modal.addEventListener('click', this.clickHandler);
|
||
}
|
||
|
||
handleBackdropClick(e) {
|
||
if (e.target === this.modal) {
|
||
this.close();
|
||
}
|
||
}
|
||
|
||
handleEscape(e) {
|
||
if (e.key === 'Escape' && this.modal.classList.contains('active')) {
|
||
this.close();
|
||
}
|
||
}
|
||
|
||
open() {
|
||
this.previousActiveElement = document.activeElement;
|
||
this.modal.classList.add('active');
|
||
document.body.style.overflow = 'hidden';
|
||
|
||
// Trap focus and store cleanup function
|
||
this.focusTrapCleanup = a11y.trapFocus(this.modal);
|
||
|
||
// Add ESC key handler
|
||
document.addEventListener('keydown', this.escHandler);
|
||
|
||
a11y.announce('Dialog opened');
|
||
}
|
||
|
||
close() {
|
||
this.modal.classList.remove('active');
|
||
document.body.style.overflow = '';
|
||
|
||
// Cleanup focus trap
|
||
if (this.focusTrapCleanup) {
|
||
this.focusTrapCleanup();
|
||
this.focusTrapCleanup = null;
|
||
}
|
||
|
||
// Remove ESC key handler
|
||
document.removeEventListener('keydown', this.escHandler);
|
||
|
||
// Return focus
|
||
if (this.previousActiveElement) {
|
||
this.previousActiveElement.focus();
|
||
}
|
||
|
||
a11y.announce('Dialog closed');
|
||
}
|
||
}
|
||
|
||
// Initialize all modals
|
||
const modals = {};
|
||
document.querySelectorAll('.vb-modal').forEach(modal => {
|
||
const id = modal.id;
|
||
if (id) {
|
||
modals[id] = new VBModal(modal);
|
||
}
|
||
});
|
||
|
||
// Modal trigger buttons
|
||
document.querySelectorAll('[data-vb-modal-target]').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const targetId = btn.dataset.vbModalTarget;
|
||
if (modals[targetId]) {
|
||
modals[targetId].open();
|
||
}
|
||
});
|
||
});
|
||
|
||
// ============================================
|
||
// HAMBURGER MENU FUNCTIONALITY (WCAG 2.1 AA)
|
||
// ============================================
|
||
|
||
class VBHamburgerMenu {
|
||
constructor(navbar) {
|
||
this.navbar = navbar;
|
||
this.toggle = navbar.querySelector('.vb-navbar-toggle');
|
||
this.menu = navbar.querySelector('.vb-navbar-menu');
|
||
this.escHandler = this.handleEscape.bind(this);
|
||
|
||
if (this.toggle && this.menu) {
|
||
this.init();
|
||
}
|
||
}
|
||
|
||
init() {
|
||
// Set ARIA attributes
|
||
this.toggle.setAttribute('aria-label', 'Toggle navigation menu');
|
||
this.toggle.setAttribute('aria-expanded', 'false');
|
||
this.toggle.setAttribute('aria-controls', this.menu.id || 'navbar-menu');
|
||
|
||
if (!this.menu.id) {
|
||
this.menu.id = 'navbar-menu';
|
||
}
|
||
|
||
// Toggle on click
|
||
this.toggle.addEventListener('click', () => {
|
||
this.toggleMenu();
|
||
});
|
||
|
||
// Close on ESC - using bound handler for proper cleanup
|
||
document.addEventListener('keydown', this.escHandler);
|
||
}
|
||
|
||
handleEscape(e) {
|
||
if (e.key === 'Escape' && this.menu.classList.contains('active')) {
|
||
this.closeMenu();
|
||
this.toggle.focus();
|
||
}
|
||
}
|
||
|
||
toggleMenu() {
|
||
const isOpen = this.menu.classList.toggle('active');
|
||
this.toggle.classList.toggle('active');
|
||
this.toggle.setAttribute('aria-expanded', isOpen);
|
||
|
||
a11y.announce(isOpen ? 'Menu opened' : 'Menu closed');
|
||
}
|
||
|
||
closeMenu() {
|
||
this.menu.classList.remove('active');
|
||
this.toggle.classList.remove('active');
|
||
this.toggle.setAttribute('aria-expanded', 'false');
|
||
}
|
||
}
|
||
|
||
// Initialize hamburger menus
|
||
document.querySelectorAll('.vb-navbar').forEach(navbar => {
|
||
new VBHamburgerMenu(navbar);
|
||
});
|
||
|
||
// ============================================
|
||
// DROPDOWN FUNCTIONALITY (WCAG 2.1 AA)
|
||
// ============================================
|
||
|
||
class VBDropdown {
|
||
constructor(element) {
|
||
this.dropdown = element;
|
||
this.toggle = this.dropdown.querySelector('.vb-dropdown-toggle');
|
||
this.menu = this.dropdown.querySelector('.vb-dropdown-menu');
|
||
|
||
if (!this.toggle || !this.menu) {
|
||
console.warn('Vibe Brutalism: Dropdown requires .vb-dropdown-toggle and .vb-dropdown-menu');
|
||
return;
|
||
}
|
||
|
||
this.items = this.menu.querySelectorAll('.vb-dropdown-item');
|
||
this.outsideClickHandler = this.handleOutsideClick.bind(this);
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
// Set ARIA attributes
|
||
this.toggle.setAttribute('aria-haspopup', 'true');
|
||
this.toggle.setAttribute('aria-expanded', 'false');
|
||
this.menu.setAttribute('role', 'menu');
|
||
|
||
this.items.forEach(item => {
|
||
item.setAttribute('role', 'menuitem');
|
||
item.setAttribute('tabindex', '-1');
|
||
});
|
||
|
||
// Toggle on click
|
||
this.toggle.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
this.toggleDropdown();
|
||
});
|
||
|
||
// Keyboard navigation
|
||
this.toggle.addEventListener('keydown', (e) => {
|
||
if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
|
||
e.preventDefault();
|
||
this.open();
|
||
this.items[0]?.focus();
|
||
}
|
||
});
|
||
|
||
this.items.forEach((item, index) => {
|
||
item.addEventListener('keydown', (e) => {
|
||
if (e.key === 'ArrowDown') {
|
||
e.preventDefault();
|
||
this.items[(index + 1) % this.items.length]?.focus();
|
||
} else if (e.key === 'ArrowUp') {
|
||
e.preventDefault();
|
||
this.items[(index - 1 + this.items.length) % this.items.length]?.focus();
|
||
} else if (e.key === 'Escape') {
|
||
e.preventDefault();
|
||
this.close();
|
||
this.toggle.focus();
|
||
}
|
||
});
|
||
});
|
||
|
||
// Prevent closing when clicking inside menu
|
||
this.menu.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
});
|
||
}
|
||
|
||
handleOutsideClick() {
|
||
this.close();
|
||
}
|
||
|
||
toggleDropdown() {
|
||
const isOpen = this.dropdown.classList.toggle('active');
|
||
this.toggle.setAttribute('aria-expanded', isOpen);
|
||
|
||
if (isOpen) {
|
||
this.items.forEach(item => item.setAttribute('tabindex', '0'));
|
||
// Add outside click handler when open
|
||
setTimeout(() => {
|
||
document.addEventListener('click', this.outsideClickHandler);
|
||
}, 0);
|
||
} else {
|
||
this.items.forEach(item => item.setAttribute('tabindex', '-1'));
|
||
// Remove outside click handler when closed
|
||
document.removeEventListener('click', this.outsideClickHandler);
|
||
}
|
||
}
|
||
|
||
open() {
|
||
this.dropdown.classList.add('active');
|
||
this.toggle.setAttribute('aria-expanded', 'true');
|
||
this.items.forEach(item => item.setAttribute('tabindex', '0'));
|
||
// Add outside click handler when open
|
||
setTimeout(() => {
|
||
document.addEventListener('click', this.outsideClickHandler);
|
||
}, 0);
|
||
}
|
||
|
||
close() {
|
||
this.dropdown.classList.remove('active');
|
||
this.toggle.setAttribute('aria-expanded', 'false');
|
||
this.items.forEach(item => item.setAttribute('tabindex', '-1'));
|
||
// Remove outside click handler when closed
|
||
document.removeEventListener('click', this.outsideClickHandler);
|
||
}
|
||
}
|
||
|
||
// Initialize all dropdowns
|
||
document.querySelectorAll('.vb-dropdown').forEach(dropdown => {
|
||
new VBDropdown(dropdown);
|
||
});
|
||
|
||
// ============================================
|
||
// TABS FUNCTIONALITY (WCAG 2.1 AA)
|
||
// ============================================
|
||
|
||
class VBTabs {
|
||
constructor(element) {
|
||
this.tabs = element;
|
||
this.buttons = this.tabs.querySelectorAll('.vb-tab-button');
|
||
this.contents = this.tabs.querySelectorAll('.vb-tab-content');
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
// Set ARIA attributes
|
||
const tablist = this.tabs.querySelector('.vb-tab-list');
|
||
if (tablist) {
|
||
tablist.setAttribute('role', 'tablist');
|
||
}
|
||
|
||
this.buttons.forEach((button, index) => {
|
||
const tabId = `tab-${Math.random().toString(36).substr(2, 9)}`;
|
||
const panelId = `panel-${Math.random().toString(36).substr(2, 9)}`;
|
||
|
||
button.setAttribute('role', 'tab');
|
||
button.setAttribute('id', tabId);
|
||
button.setAttribute('aria-controls', panelId);
|
||
button.setAttribute('tabindex', '-1');
|
||
|
||
this.contents[index].setAttribute('role', 'tabpanel');
|
||
this.contents[index].setAttribute('id', panelId);
|
||
this.contents[index].setAttribute('aria-labelledby', tabId);
|
||
this.contents[index].setAttribute('tabindex', '0');
|
||
|
||
// Click event
|
||
button.addEventListener('click', () => {
|
||
this.switchTab(index);
|
||
});
|
||
|
||
// Keyboard navigation
|
||
button.addEventListener('keydown', (e) => {
|
||
let newIndex = index;
|
||
|
||
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||
e.preventDefault();
|
||
newIndex = (index + 1) % this.buttons.length;
|
||
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||
e.preventDefault();
|
||
newIndex = (index - 1 + this.buttons.length) % this.buttons.length;
|
||
} else if (e.key === 'Home') {
|
||
e.preventDefault();
|
||
newIndex = 0;
|
||
} else if (e.key === 'End') {
|
||
e.preventDefault();
|
||
newIndex = this.buttons.length - 1;
|
||
}
|
||
|
||
if (newIndex !== index) {
|
||
this.switchTab(newIndex);
|
||
this.buttons[newIndex].focus();
|
||
}
|
||
});
|
||
});
|
||
|
||
// Activate first tab by default
|
||
if (this.buttons.length > 0 && !this.tabs.querySelector('.vb-tab-button.active')) {
|
||
this.switchTab(0);
|
||
}
|
||
}
|
||
|
||
switchTab(index) {
|
||
// Remove active class and update ARIA
|
||
this.buttons.forEach((btn, i) => {
|
||
btn.classList.remove('active');
|
||
btn.setAttribute('aria-selected', 'false');
|
||
btn.setAttribute('tabindex', '-1');
|
||
});
|
||
|
||
this.contents.forEach(content => {
|
||
content.classList.remove('active');
|
||
});
|
||
|
||
// Add active class to selected button and content
|
||
this.buttons[index].classList.add('active');
|
||
this.buttons[index].setAttribute('aria-selected', 'true');
|
||
this.buttons[index].setAttribute('tabindex', '0');
|
||
this.contents[index].classList.add('active');
|
||
|
||
a11y.announce(`Tab ${index + 1} selected`);
|
||
}
|
||
}
|
||
|
||
// Initialize all tabs
|
||
document.querySelectorAll('.vb-tabs').forEach(tabs => {
|
||
new VBTabs(tabs);
|
||
});
|
||
|
||
// ============================================
|
||
// ACCORDION FUNCTIONALITY (WCAG 2.1 AA)
|
||
// ============================================
|
||
|
||
class VBAccordion {
|
||
constructor(element) {
|
||
this.accordion = element;
|
||
this.items = this.accordion.querySelectorAll('.vb-accordion-item');
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
this.items.forEach((item, index) => {
|
||
const header = item.querySelector('.vb-accordion-header');
|
||
const body = item.querySelector('.vb-accordion-body');
|
||
|
||
// Set ARIA attributes
|
||
const headerId = `accordion-header-${Math.random().toString(36).substr(2, 9)}`;
|
||
const bodyId = `accordion-body-${Math.random().toString(36).substr(2, 9)}`;
|
||
|
||
header.setAttribute('id', headerId);
|
||
header.setAttribute('aria-expanded', 'false');
|
||
header.setAttribute('aria-controls', bodyId);
|
||
|
||
body.setAttribute('id', bodyId);
|
||
body.setAttribute('role', 'region');
|
||
body.setAttribute('aria-labelledby', headerId);
|
||
|
||
// Click event
|
||
header.addEventListener('click', () => {
|
||
this.toggle(header, body);
|
||
});
|
||
|
||
// Keyboard support
|
||
header.addEventListener('keydown', (e) => {
|
||
if (e.key === 'ArrowDown') {
|
||
e.preventDefault();
|
||
const nextHeader = this.items[index + 1]?.querySelector('.vb-accordion-header');
|
||
nextHeader?.focus();
|
||
} else if (e.key === 'ArrowUp') {
|
||
e.preventDefault();
|
||
const prevHeader = this.items[index - 1]?.querySelector('.vb-accordion-header');
|
||
prevHeader?.focus();
|
||
} else if (e.key === 'Home') {
|
||
e.preventDefault();
|
||
this.items[0]?.querySelector('.vb-accordion-header')?.focus();
|
||
} else if (e.key === 'End') {
|
||
e.preventDefault();
|
||
this.items[this.items.length - 1]?.querySelector('.vb-accordion-header')?.focus();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
toggle(header, body) {
|
||
const isActive = header.classList.contains('active');
|
||
|
||
// Close all items
|
||
this.accordion.querySelectorAll('.vb-accordion-header').forEach(h => {
|
||
h.classList.remove('active');
|
||
h.setAttribute('aria-expanded', 'false');
|
||
});
|
||
this.accordion.querySelectorAll('.vb-accordion-body').forEach(b => {
|
||
b.classList.remove('active');
|
||
});
|
||
|
||
// Toggle current item
|
||
if (!isActive) {
|
||
header.classList.add('active');
|
||
header.setAttribute('aria-expanded', 'true');
|
||
body.classList.add('active');
|
||
a11y.announce('Section expanded');
|
||
} else {
|
||
a11y.announce('Section collapsed');
|
||
}
|
||
}
|
||
}
|
||
|
||
// Initialize all accordions
|
||
document.querySelectorAll('.vb-accordion').forEach(accordion => {
|
||
new VBAccordion(accordion);
|
||
});
|
||
|
||
// ============================================
|
||
// CAROUSEL FUNCTIONALITY (WCAG 2.1 AA)
|
||
// ============================================
|
||
|
||
class VBCarousel {
|
||
constructor(element) {
|
||
this.carousel = element;
|
||
this.track = this.carousel.querySelector('.vb-carousel-track');
|
||
this.items = this.carousel.querySelectorAll('.vb-carousel-item');
|
||
this.prevBtn = this.carousel.querySelector('.vb-carousel-control-prev');
|
||
this.nextBtn = this.carousel.querySelector('.vb-carousel-control-next');
|
||
this.indicators = this.carousel.querySelectorAll('.vb-carousel-indicator');
|
||
this.currentIndex = 0;
|
||
this.autoplayInterval = null;
|
||
this.touchStartX = 0;
|
||
this.touchEndX = 0;
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
if (this.items.length === 0 || !this.track) {
|
||
console.warn('Vibe Brutalism: Carousel requires .vb-carousel-track and at least one .vb-carousel-item');
|
||
return;
|
||
}
|
||
|
||
// Set ARIA attributes
|
||
this.carousel.setAttribute('role', 'region');
|
||
this.carousel.setAttribute('aria-label', 'Carousel');
|
||
this.carousel.setAttribute('aria-roledescription', 'carousel');
|
||
|
||
this.items.forEach((item, index) => {
|
||
item.setAttribute('role', 'group');
|
||
item.setAttribute('aria-roledescription', 'slide');
|
||
item.setAttribute('aria-label', `Slide ${index + 1} of ${this.items.length}`);
|
||
});
|
||
|
||
// Previous button
|
||
if (this.prevBtn) {
|
||
this.prevBtn.setAttribute('aria-label', 'Previous slide');
|
||
this.prevBtn.addEventListener('click', () => this.prev());
|
||
}
|
||
|
||
// Next button
|
||
if (this.nextBtn) {
|
||
this.nextBtn.setAttribute('aria-label', 'Next slide');
|
||
this.nextBtn.addEventListener('click', () => this.next());
|
||
}
|
||
|
||
// Indicators
|
||
this.indicators.forEach((indicator, index) => {
|
||
indicator.setAttribute('aria-label', `Go to slide ${index + 1}`);
|
||
indicator.setAttribute('role', 'button');
|
||
indicator.addEventListener('click', () => this.goTo(index));
|
||
});
|
||
|
||
// Keyboard navigation
|
||
this.carousel.addEventListener('keydown', (e) => {
|
||
if (e.key === 'ArrowLeft') {
|
||
e.preventDefault();
|
||
this.prev();
|
||
} else if (e.key === 'ArrowRight') {
|
||
e.preventDefault();
|
||
this.next();
|
||
}
|
||
});
|
||
|
||
// Pause autoplay on hover/focus
|
||
this.carousel.addEventListener('mouseenter', () => this.pauseAutoplay());
|
||
this.carousel.addEventListener('mouseleave', () => this.startAutoplay());
|
||
this.carousel.addEventListener('focusin', () => this.pauseAutoplay());
|
||
this.carousel.addEventListener('focusout', () => this.startAutoplay());
|
||
|
||
// Touch/Swipe support
|
||
this.carousel.addEventListener('touchstart', (e) => {
|
||
this.touchStartX = e.changedTouches[0].screenX;
|
||
}, { passive: true });
|
||
|
||
this.carousel.addEventListener('touchend', (e) => {
|
||
this.touchEndX = e.changedTouches[0].screenX;
|
||
this.handleSwipe();
|
||
}, { passive: true });
|
||
|
||
// Initialize
|
||
this.goTo(0);
|
||
|
||
// Start autoplay if enabled
|
||
if (this.carousel.dataset.autoplay === 'true') {
|
||
this.startAutoplay();
|
||
}
|
||
}
|
||
|
||
handleSwipe() {
|
||
const swipeThreshold = 50;
|
||
const diff = this.touchStartX - this.touchEndX;
|
||
|
||
if (Math.abs(diff) > swipeThreshold) {
|
||
if (diff > 0) {
|
||
// Swipe left - go to next
|
||
this.next();
|
||
} else {
|
||
// Swipe right - go to previous
|
||
this.prev();
|
||
}
|
||
}
|
||
}
|
||
|
||
goTo(index) {
|
||
this.currentIndex = index;
|
||
const offset = -index * 100;
|
||
this.track.style.transform = `translateX(${offset}%)`;
|
||
|
||
// Update indicators
|
||
this.indicators.forEach((indicator, i) => {
|
||
indicator.classList.toggle('active', i === index);
|
||
indicator.setAttribute('aria-current', i === index ? 'true' : 'false');
|
||
});
|
||
|
||
// Update items
|
||
this.items.forEach((item, i) => {
|
||
item.setAttribute('aria-hidden', i !== index);
|
||
});
|
||
|
||
a11y.announce(`Slide ${index + 1} of ${this.items.length}`);
|
||
}
|
||
|
||
next() {
|
||
const nextIndex = (this.currentIndex + 1) % this.items.length;
|
||
this.goTo(nextIndex);
|
||
}
|
||
|
||
prev() {
|
||
const prevIndex = (this.currentIndex - 1 + this.items.length) % this.items.length;
|
||
this.goTo(prevIndex);
|
||
}
|
||
|
||
startAutoplay() {
|
||
const interval = parseInt(this.carousel.dataset.interval) || 5000;
|
||
this.autoplayInterval = setInterval(() => this.next(), interval);
|
||
}
|
||
|
||
pauseAutoplay() {
|
||
if (this.autoplayInterval) {
|
||
clearInterval(this.autoplayInterval);
|
||
this.autoplayInterval = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Initialize all carousels
|
||
const carousels = {};
|
||
document.querySelectorAll('.vb-carousel').forEach(carousel => {
|
||
const id = carousel.id || `carousel-${Math.random().toString(36).substr(2, 9)}`;
|
||
carousel.id = id;
|
||
carousels[id] = new VBCarousel(carousel);
|
||
});
|
||
|
||
// ============================================
|
||
// TOAST NOTIFICATIONS (WCAG 2.1 AA)
|
||
// ============================================
|
||
|
||
class VBToastManager {
|
||
constructor() {
|
||
this.container = null;
|
||
this.toasts = [];
|
||
}
|
||
|
||
getContainer(position = 'top-right') {
|
||
const containerId = `vb-toast-container-${position}`;
|
||
let container = document.getElementById(containerId);
|
||
|
||
if (!container) {
|
||
container = document.createElement('div');
|
||
container.id = containerId;
|
||
container.className = `vb-toast-container ${position}`;
|
||
container.setAttribute('aria-live', 'polite');
|
||
container.setAttribute('aria-atomic', 'false');
|
||
document.body.appendChild(container);
|
||
}
|
||
|
||
return container;
|
||
}
|
||
|
||
show(message, type = 'info', duration = 5000, position = 'top-right') {
|
||
const container = this.getContainer(position);
|
||
|
||
const toast = document.createElement('div');
|
||
toast.className = `vb-toast vb-toast-${type}`;
|
||
toast.setAttribute('role', 'status');
|
||
toast.setAttribute('aria-live', 'polite');
|
||
|
||
// Icon
|
||
const icons = {
|
||
success: '✓',
|
||
warning: '⚠',
|
||
danger: '✕',
|
||
info: 'ℹ'
|
||
};
|
||
|
||
toast.innerHTML = `
|
||
<span class="vb-toast-icon" aria-hidden="true">${icons[type] || icons.info}</span>
|
||
<div class="vb-toast-content">${message}</div>
|
||
<button class="vb-toast-close" aria-label="Close notification">×</button>
|
||
`;
|
||
|
||
// Close button
|
||
const closeBtn = toast.querySelector('.vb-toast-close');
|
||
closeBtn.addEventListener('click', () => this.remove(toast));
|
||
|
||
container.appendChild(toast);
|
||
this.toasts.push(toast);
|
||
|
||
// Announce to screen readers
|
||
a11y.announce(message, 'polite');
|
||
|
||
// Auto remove
|
||
if (duration > 0) {
|
||
setTimeout(() => this.remove(toast), duration);
|
||
}
|
||
|
||
return toast;
|
||
}
|
||
|
||
remove(toast) {
|
||
toast.classList.add('removing');
|
||
setTimeout(() => {
|
||
toast.remove();
|
||
const index = this.toasts.indexOf(toast);
|
||
if (index > -1) {
|
||
this.toasts.splice(index, 1);
|
||
}
|
||
}, 300);
|
||
}
|
||
}
|
||
|
||
const toastManager = new VBToastManager();
|
||
|
||
// ============================================
|
||
// SNACKBAR FUNCTIONALITY (WCAG 2.1 AA)
|
||
// ============================================
|
||
|
||
class VBSnackbar {
|
||
constructor() {
|
||
this.snackbar = null;
|
||
this.timeout = null;
|
||
}
|
||
|
||
show(message, actionText = null, actionCallback = null, duration = 5000) {
|
||
// Remove existing snackbar
|
||
if (this.snackbar) {
|
||
this.hide();
|
||
}
|
||
|
||
// Create snackbar
|
||
this.snackbar = document.createElement('div');
|
||
this.snackbar.className = 'vb-snackbar';
|
||
this.snackbar.setAttribute('role', 'status');
|
||
this.snackbar.setAttribute('aria-live', 'polite');
|
||
|
||
let html = `<div class="vb-snackbar-message">${message}</div>`;
|
||
|
||
if (actionText && actionCallback) {
|
||
html += `<button class="vb-snackbar-action">${actionText}</button>`;
|
||
}
|
||
|
||
html += `<button class="vb-snackbar-close" aria-label="Close">×</button>`;
|
||
|
||
this.snackbar.innerHTML = html;
|
||
|
||
// Action button
|
||
if (actionText && actionCallback) {
|
||
const actionBtn = this.snackbar.querySelector('.vb-snackbar-action');
|
||
actionBtn.addEventListener('click', () => {
|
||
actionCallback();
|
||
this.hide();
|
||
});
|
||
}
|
||
|
||
// Close button
|
||
const closeBtn = this.snackbar.querySelector('.vb-snackbar-close');
|
||
closeBtn.addEventListener('click', () => this.hide());
|
||
|
||
document.body.appendChild(this.snackbar);
|
||
|
||
// Show with animation
|
||
setTimeout(() => {
|
||
this.snackbar.classList.add('show');
|
||
}, 10);
|
||
|
||
// Announce to screen readers
|
||
a11y.announce(message, 'polite');
|
||
|
||
// Auto hide
|
||
if (duration > 0) {
|
||
this.timeout = setTimeout(() => this.hide(), duration);
|
||
}
|
||
}
|
||
|
||
hide() {
|
||
if (this.snackbar) {
|
||
this.snackbar.classList.remove('show');
|
||
setTimeout(() => {
|
||
this.snackbar?.remove();
|
||
this.snackbar = null;
|
||
}, 300);
|
||
}
|
||
|
||
if (this.timeout) {
|
||
clearTimeout(this.timeout);
|
||
this.timeout = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
const snackbar = new VBSnackbar();
|
||
|
||
// ============================================
|
||
// ALERT CLOSE FUNCTIONALITY
|
||
// ============================================
|
||
|
||
document.querySelectorAll('[data-vb-alert-close]').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const alert = btn.closest('.vb-alert');
|
||
if (alert) {
|
||
alert.style.opacity = '0';
|
||
setTimeout(() => {
|
||
alert.remove();
|
||
}, 300);
|
||
}
|
||
});
|
||
});
|
||
|
||
// ============================================
|
||
// PROGRESS BAR
|
||
// ============================================
|
||
|
||
window.VBSetProgress = function(elementId, percentage) {
|
||
const progressBar = document.querySelector(`#${elementId} .vb-progress-bar`);
|
||
if (progressBar) {
|
||
progressBar.style.width = `${percentage}%`;
|
||
progressBar.textContent = `${percentage}%`;
|
||
progressBar.setAttribute('aria-valuenow', percentage);
|
||
a11y.announce(`Progress: ${percentage}%`);
|
||
}
|
||
};
|
||
|
||
// ============================================
|
||
// FORM VALIDATION
|
||
// ============================================
|
||
|
||
window.VBValidateForm = function(formElement) {
|
||
const inputs = formElement.querySelectorAll('.vb-input[required], .vb-textarea[required]');
|
||
let isValid = true;
|
||
|
||
inputs.forEach(input => {
|
||
if (!input.value.trim()) {
|
||
input.style.borderColor = 'var(--vb-danger)';
|
||
input.setAttribute('aria-invalid', 'true');
|
||
isValid = false;
|
||
} else {
|
||
input.style.borderColor = 'var(--vb-black)';
|
||
input.setAttribute('aria-invalid', 'false');
|
||
}
|
||
});
|
||
|
||
return isValid;
|
||
};
|
||
|
||
// ============================================
|
||
// GLOBAL API
|
||
// ============================================
|
||
|
||
window.VB = {
|
||
modals,
|
||
carousels,
|
||
Toast: (message, type, duration, position) => toastManager.show(message, type, duration, position),
|
||
Snackbar: (message, actionText, actionCallback, duration) => snackbar.show(message, actionText, actionCallback, duration),
|
||
SetProgress: window.VBSetProgress,
|
||
ValidateForm: window.VBValidateForm,
|
||
announce: (message, priority) => a11y.announce(message, priority)
|
||
};
|
||
|
||
// Initialize accessibility features
|
||
if (document.body) {
|
||
a11y.createLiveRegion();
|
||
console.log('🎨 Vibe Brutalism v2.0.0 initialized with WCAG 2.1 AA compliance!');
|
||
} else {
|
||
console.warn('Vibe Brutalism: document.body not available. Retrying on DOMContentLoaded...');
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
a11y.createLiveRegion();
|
||
console.log('🎨 Vibe Brutalism v2.0.0 initialized with WCAG 2.1 AA compliance!');
|
||
});
|
||
}
|
||
})();
|