initial commit
This commit is contained in:
925
vibe-brutalism.js
Normal file
925
vibe-brutalism.js
Normal file
@@ -0,0 +1,925 @@
|
||||
/**
|
||||
* 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!');
|
||||
});
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user