1079 lines
38 KiB
HTML
1079 lines
38 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Tab Navigation Enhanced</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
background: #f8f9fa;
|
|
color: #1a1a1a;
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
}
|
|
|
|
main {
|
|
width: 100%;
|
|
max-width: 1200px;
|
|
background: white;
|
|
border-radius: 16px;
|
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
|
|
overflow: hidden;
|
|
}
|
|
|
|
h1 {
|
|
text-align: center;
|
|
padding: 40px 20px 20px;
|
|
font-size: 2rem;
|
|
font-weight: 600;
|
|
color: #1a1a1a;
|
|
}
|
|
|
|
.component-showcase {
|
|
padding: 20px;
|
|
}
|
|
|
|
/* Tab Container */
|
|
.tab-container {
|
|
margin-bottom: 40px;
|
|
background: white;
|
|
border-radius: 12px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Tab Header */
|
|
.tab-header {
|
|
position: relative;
|
|
background: #f8f9fa;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Tab List */
|
|
.tab-list {
|
|
display: flex;
|
|
position: relative;
|
|
overflow-x: auto;
|
|
scroll-behavior: smooth;
|
|
scrollbar-width: none;
|
|
-ms-overflow-style: none;
|
|
}
|
|
|
|
.tab-list::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
/* Scroll Indicators */
|
|
.scroll-indicator {
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 40px;
|
|
pointer-events: none;
|
|
z-index: 10;
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
.scroll-indicator.left {
|
|
left: 0;
|
|
background: linear-gradient(to right, rgba(248, 249, 250, 1), rgba(248, 249, 250, 0));
|
|
}
|
|
|
|
.scroll-indicator.right {
|
|
right: 0;
|
|
background: linear-gradient(to left, rgba(248, 249, 250, 1), rgba(248, 249, 250, 0));
|
|
}
|
|
|
|
.scroll-indicator.visible {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Tab Items */
|
|
.tab-item {
|
|
position: relative;
|
|
padding: 16px 24px;
|
|
border: none;
|
|
background: none;
|
|
color: #6b7280;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
transition: color 0.2s ease, background-color 0.2s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
user-select: none;
|
|
-webkit-tap-highlight-color: transparent;
|
|
}
|
|
|
|
.tab-item:hover {
|
|
color: #374151;
|
|
background-color: rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
.tab-item:focus {
|
|
outline: none;
|
|
box-shadow: inset 0 0 0 2px #3b82f6;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.tab-item.active {
|
|
color: #3b82f6;
|
|
}
|
|
|
|
.tab-item.loading {
|
|
color: #9ca3af;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.tab-item.dragging {
|
|
opacity: 0.5;
|
|
cursor: grabbing;
|
|
}
|
|
|
|
/* Close Button */
|
|
.tab-close {
|
|
margin-left: 8px;
|
|
padding: 2px;
|
|
background: none;
|
|
border: none;
|
|
color: #9ca3af;
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
transition: all 0.2s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.tab-close:hover {
|
|
color: #ef4444;
|
|
background: rgba(239, 68, 68, 0.1);
|
|
}
|
|
|
|
.tab-close svg {
|
|
width: 14px;
|
|
height: 14px;
|
|
}
|
|
|
|
/* Active Tab Indicator */
|
|
.tab-indicator {
|
|
position: absolute;
|
|
bottom: 0;
|
|
height: 3px;
|
|
background: #3b82f6;
|
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
border-radius: 3px 3px 0 0;
|
|
}
|
|
|
|
/* Tab Content */
|
|
.tab-content {
|
|
position: relative;
|
|
min-height: 300px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.tab-panel {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
padding: 32px;
|
|
opacity: 0;
|
|
transform: translateX(20px);
|
|
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.tab-panel.active {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
pointer-events: auto;
|
|
position: relative;
|
|
}
|
|
|
|
.tab-panel.fade-out {
|
|
transform: translateX(-20px);
|
|
}
|
|
|
|
/* Loading State */
|
|
.tab-loading {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 60px;
|
|
}
|
|
|
|
.spinner {
|
|
width: 32px;
|
|
height: 32px;
|
|
border: 3px solid #e5e7eb;
|
|
border-top-color: #3b82f6;
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* Vertical Tabs */
|
|
.tab-container.vertical {
|
|
display: flex;
|
|
}
|
|
|
|
.tab-container.vertical .tab-header {
|
|
width: 200px;
|
|
border-bottom: none;
|
|
border-right: 1px solid #e5e7eb;
|
|
}
|
|
|
|
.tab-container.vertical .tab-list {
|
|
flex-direction: column;
|
|
overflow-x: hidden;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.tab-container.vertical .tab-indicator {
|
|
width: 3px;
|
|
height: auto;
|
|
right: 0;
|
|
left: auto;
|
|
bottom: auto;
|
|
}
|
|
|
|
.tab-container.vertical .tab-content {
|
|
flex: 1;
|
|
}
|
|
|
|
/* Mobile Responsive */
|
|
@media (max-width: 640px) {
|
|
.tab-item {
|
|
padding: 12px 16px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.tab-panel {
|
|
padding: 24px 16px;
|
|
}
|
|
|
|
.tab-container.vertical {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.tab-container.vertical .tab-header {
|
|
width: 100%;
|
|
border-right: none;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
}
|
|
|
|
.tab-container.vertical .tab-list {
|
|
flex-direction: row;
|
|
}
|
|
|
|
.tab-container.vertical .tab-indicator {
|
|
width: auto;
|
|
height: 3px;
|
|
right: auto;
|
|
bottom: 0;
|
|
}
|
|
}
|
|
|
|
/* Touch Support */
|
|
.tab-list.touching {
|
|
scroll-behavior: auto;
|
|
}
|
|
|
|
/* Dropdown for Mobile */
|
|
.tab-dropdown {
|
|
display: none;
|
|
position: relative;
|
|
margin: 20px;
|
|
}
|
|
|
|
.tab-dropdown-toggle {
|
|
width: 100%;
|
|
padding: 12px 16px;
|
|
background: white;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: #374151;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.tab-dropdown-toggle:hover {
|
|
border-color: #3b82f6;
|
|
}
|
|
|
|
.tab-dropdown-menu {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
margin-top: 4px;
|
|
background: white;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
opacity: 0;
|
|
transform: translateY(-8px);
|
|
pointer-events: none;
|
|
transition: all 0.2s ease;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
z-index: 100;
|
|
}
|
|
|
|
.tab-dropdown.open .tab-dropdown-menu {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.tab-dropdown-item {
|
|
padding: 12px 16px;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s ease;
|
|
font-size: 14px;
|
|
color: #374151;
|
|
}
|
|
|
|
.tab-dropdown-item:hover {
|
|
background-color: #f3f4f6;
|
|
}
|
|
|
|
.tab-dropdown-item.active {
|
|
color: #3b82f6;
|
|
background-color: #eff6ff;
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.tab-header {
|
|
display: none;
|
|
}
|
|
|
|
.tab-dropdown {
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
/* Additional Demo Styles */
|
|
.demo-controls {
|
|
display: flex;
|
|
gap: 12px;
|
|
padding: 20px;
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
margin-bottom: 20px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.demo-button {
|
|
padding: 8px 16px;
|
|
background: white;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
color: #374151;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.demo-button:hover {
|
|
border-color: #3b82f6;
|
|
color: #3b82f6;
|
|
}
|
|
|
|
.demo-button:active {
|
|
transform: translateY(1px);
|
|
}
|
|
|
|
/* Accessibility */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
* {
|
|
animation-duration: 0.01ms !important;
|
|
animation-iteration-count: 1 !important;
|
|
transition-duration: 0.01ms !important;
|
|
scroll-behavior: auto !important;
|
|
}
|
|
}
|
|
|
|
/* Dark Mode Support */
|
|
@media (prefers-color-scheme: dark) {
|
|
body {
|
|
background: #0f0f0f;
|
|
color: #e5e7eb;
|
|
}
|
|
|
|
main {
|
|
background: #1a1a1a;
|
|
}
|
|
|
|
.tab-container {
|
|
background: #1a1a1a;
|
|
}
|
|
|
|
.tab-header {
|
|
background: #0f0f0f;
|
|
border-color: #2a2a2a;
|
|
}
|
|
|
|
.tab-item {
|
|
color: #9ca3af;
|
|
}
|
|
|
|
.tab-item:hover {
|
|
color: #e5e7eb;
|
|
background-color: rgba(255, 255, 255, 0.05);
|
|
}
|
|
|
|
.tab-item.active {
|
|
color: #60a5fa;
|
|
}
|
|
|
|
.tab-indicator {
|
|
background: #60a5fa;
|
|
}
|
|
|
|
.scroll-indicator.left {
|
|
background: linear-gradient(to right, rgba(15, 15, 15, 1), rgba(15, 15, 15, 0));
|
|
}
|
|
|
|
.scroll-indicator.right {
|
|
background: linear-gradient(to left, rgba(15, 15, 15, 1), rgba(15, 15, 15, 0));
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<h1>Tab Navigation - Enhanced</h1>
|
|
|
|
<div class="component-showcase">
|
|
<!-- Demo Controls -->
|
|
<div class="demo-controls">
|
|
<button class="demo-button" onclick="addTab()">Add Tab</button>
|
|
<button class="demo-button" onclick="toggleOrientation()">Toggle Orientation</button>
|
|
<button class="demo-button" onclick="toggleCloseable()">Toggle Closeable</button>
|
|
<button class="demo-button" onclick="simulateLoading()">Simulate Loading</button>
|
|
</div>
|
|
|
|
<!-- Horizontal Tabs Example -->
|
|
<div class="tab-container" id="mainTabs">
|
|
<div class="tab-header">
|
|
<div class="scroll-indicator left"></div>
|
|
<div class="scroll-indicator right"></div>
|
|
<div class="tab-list" role="tablist" aria-label="Main navigation">
|
|
<button class="tab-item active" role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1" tabindex="0">
|
|
Dashboard
|
|
</button>
|
|
<button class="tab-item" role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" tabindex="-1">
|
|
Analytics
|
|
</button>
|
|
<button class="tab-item" role="tab" aria-selected="false" aria-controls="panel-3" id="tab-3" tabindex="-1">
|
|
Reports
|
|
</button>
|
|
<button class="tab-item" role="tab" aria-selected="false" aria-controls="panel-4" id="tab-4" tabindex="-1">
|
|
Settings
|
|
</button>
|
|
<button class="tab-item" role="tab" aria-selected="false" aria-controls="panel-5" id="tab-5" tabindex="-1">
|
|
Team Members
|
|
</button>
|
|
<button class="tab-item" role="tab" aria-selected="false" aria-controls="panel-6" id="tab-6" tabindex="-1">
|
|
Integrations
|
|
</button>
|
|
</div>
|
|
<div class="tab-indicator"></div>
|
|
</div>
|
|
|
|
<!-- Mobile Dropdown -->
|
|
<div class="tab-dropdown" id="tabDropdown">
|
|
<button class="tab-dropdown-toggle" onclick="toggleDropdown()">
|
|
<span>Dashboard</span>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline points="6 9 12 15 18 9"></polyline>
|
|
</svg>
|
|
</button>
|
|
<div class="tab-dropdown-menu">
|
|
<div class="tab-dropdown-item active" data-tab="tab-1">Dashboard</div>
|
|
<div class="tab-dropdown-item" data-tab="tab-2">Analytics</div>
|
|
<div class="tab-dropdown-item" data-tab="tab-3">Reports</div>
|
|
<div class="tab-dropdown-item" data-tab="tab-4">Settings</div>
|
|
<div class="tab-dropdown-item" data-tab="tab-5">Team Members</div>
|
|
<div class="tab-dropdown-item" data-tab="tab-6">Integrations</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tab-content">
|
|
<div class="tab-panel active" role="tabpanel" id="panel-1" aria-labelledby="tab-1">
|
|
<h2>Dashboard Overview</h2>
|
|
<p>Welcome to your enhanced dashboard. This tab navigation system features smooth transitions, keyboard navigation, and mobile responsiveness.</p>
|
|
<p>Try using the arrow keys to navigate between tabs, or swipe on mobile devices. The active tab indicator smoothly animates to show your current position.</p>
|
|
</div>
|
|
<div class="tab-panel" role="tabpanel" id="panel-2" aria-labelledby="tab-2">
|
|
<h2>Analytics Dashboard</h2>
|
|
<p>View comprehensive analytics with real-time data updates. The tab system supports lazy loading for performance optimization.</p>
|
|
<p>Notice how the content smoothly fades in when switching tabs, providing a polished user experience.</p>
|
|
</div>
|
|
<div class="tab-panel" role="tabpanel" id="panel-3" aria-labelledby="tab-3">
|
|
<h2>Reports Center</h2>
|
|
<p>Generate and view detailed reports. The tab system includes overflow handling with scroll indicators when there are many tabs.</p>
|
|
<p>On smaller screens, tabs automatically convert to a dropdown menu for better mobile usability.</p>
|
|
</div>
|
|
<div class="tab-panel" role="tabpanel" id="panel-4" aria-labelledby="tab-4">
|
|
<h2>Settings & Preferences</h2>
|
|
<p>Customize your experience with comprehensive settings. Tabs can be made closeable and support drag-and-drop reordering.</p>
|
|
<p>The system respects user preferences like reduced motion and dark mode for accessibility.</p>
|
|
</div>
|
|
<div class="tab-panel" role="tabpanel" id="panel-5" aria-labelledby="tab-5">
|
|
<h2>Team Management</h2>
|
|
<p>Manage your team members and permissions. The enhanced tab system supports both horizontal and vertical orientations.</p>
|
|
<p>Keyboard shortcuts include Home/End keys to jump to first/last tab for power users.</p>
|
|
</div>
|
|
<div class="tab-panel" role="tabpanel" id="panel-6" aria-labelledby="tab-6">
|
|
<h2>Integration Hub</h2>
|
|
<p>Connect with third-party services seamlessly. Loading states are elegantly handled for async content.</p>
|
|
<p>The tab system is fully accessible with proper ARIA attributes and screen reader support.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
// Tab System Class
|
|
class EnhancedTabs {
|
|
constructor(container) {
|
|
this.container = container;
|
|
this.tabList = container.querySelector('.tab-list');
|
|
this.tabs = container.querySelectorAll('.tab-item');
|
|
this.panels = container.querySelectorAll('.tab-panel');
|
|
this.indicator = container.querySelector('.tab-indicator');
|
|
this.dropdown = container.querySelector('.tab-dropdown');
|
|
this.dropdownToggle = container.querySelector('.tab-dropdown-toggle span');
|
|
this.dropdownItems = container.querySelectorAll('.tab-dropdown-item');
|
|
|
|
this.activeTab = container.querySelector('.tab-item.active');
|
|
this.isVertical = container.classList.contains('vertical');
|
|
this.closeable = false;
|
|
this.touchStartX = null;
|
|
this.touchStartY = null;
|
|
this.isDragging = false;
|
|
this.draggedTab = null;
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.setupEventListeners();
|
|
this.updateIndicator();
|
|
this.checkScrollIndicators();
|
|
this.setupKeyboardNavigation();
|
|
this.setupTouchSupport();
|
|
this.setupDragAndDrop();
|
|
this.setupDropdownListeners();
|
|
}
|
|
|
|
setupEventListeners() {
|
|
this.tabs.forEach(tab => {
|
|
tab.addEventListener('click', (e) => this.handleTabClick(e));
|
|
});
|
|
|
|
this.tabList.addEventListener('scroll', () => this.checkScrollIndicators());
|
|
|
|
window.addEventListener('resize', () => {
|
|
this.updateIndicator();
|
|
this.checkScrollIndicators();
|
|
});
|
|
|
|
// Intersection Observer for lazy loading
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting && entry.target.dataset.lazyLoad) {
|
|
this.loadTabContent(entry.target);
|
|
}
|
|
});
|
|
});
|
|
|
|
this.panels.forEach(panel => observer.observe(panel));
|
|
}
|
|
|
|
setupDropdownListeners() {
|
|
if (!this.dropdown) return;
|
|
|
|
this.dropdownItems.forEach(item => {
|
|
item.addEventListener('click', () => {
|
|
const tabId = item.dataset.tab;
|
|
const tab = document.getElementById(tabId);
|
|
if (tab) {
|
|
this.activateTab(tab);
|
|
this.updateDropdown(tab);
|
|
this.closeDropdown();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Close dropdown on outside click
|
|
document.addEventListener('click', (e) => {
|
|
if (!this.dropdown.contains(e.target)) {
|
|
this.closeDropdown();
|
|
}
|
|
});
|
|
}
|
|
|
|
handleTabClick(e) {
|
|
const tab = e.target.closest('.tab-item');
|
|
if (tab && !tab.classList.contains('loading')) {
|
|
this.activateTab(tab);
|
|
}
|
|
}
|
|
|
|
activateTab(tab) {
|
|
if (tab === this.activeTab) return;
|
|
|
|
// Update tabs
|
|
this.tabs.forEach(t => {
|
|
t.classList.remove('active');
|
|
t.setAttribute('aria-selected', 'false');
|
|
t.setAttribute('tabindex', '-1');
|
|
});
|
|
|
|
tab.classList.add('active');
|
|
tab.setAttribute('aria-selected', 'true');
|
|
tab.setAttribute('tabindex', '0');
|
|
|
|
// Update panels with fade effect
|
|
const oldPanel = this.container.querySelector('.tab-panel.active');
|
|
const newPanelId = tab.getAttribute('aria-controls');
|
|
const newPanel = document.getElementById(newPanelId);
|
|
|
|
if (oldPanel && newPanel) {
|
|
oldPanel.classList.add('fade-out');
|
|
|
|
setTimeout(() => {
|
|
oldPanel.classList.remove('active', 'fade-out');
|
|
newPanel.classList.add('active');
|
|
}, 150);
|
|
}
|
|
|
|
this.activeTab = tab;
|
|
this.updateIndicator();
|
|
this.updateDropdown(tab);
|
|
|
|
// Focus management
|
|
if (!this.isDragging) {
|
|
tab.focus();
|
|
}
|
|
|
|
// Emit custom event
|
|
this.container.dispatchEvent(new CustomEvent('tabchange', {
|
|
detail: { tab, panel: newPanel }
|
|
}));
|
|
}
|
|
|
|
updateIndicator() {
|
|
if (!this.activeTab || !this.indicator) return;
|
|
|
|
const tabRect = this.activeTab.getBoundingClientRect();
|
|
const containerRect = this.tabList.getBoundingClientRect();
|
|
|
|
if (this.isVertical) {
|
|
const top = tabRect.top - containerRect.top + this.tabList.scrollTop;
|
|
this.indicator.style.transform = `translateY(${top}px)`;
|
|
this.indicator.style.height = `${tabRect.height}px`;
|
|
} else {
|
|
const left = tabRect.left - containerRect.left + this.tabList.scrollLeft;
|
|
this.indicator.style.transform = `translateX(${left}px)`;
|
|
this.indicator.style.width = `${tabRect.width}px`;
|
|
}
|
|
}
|
|
|
|
updateDropdown(tab) {
|
|
if (!this.dropdownToggle) return;
|
|
|
|
const text = tab.textContent.trim();
|
|
this.dropdownToggle.textContent = text;
|
|
|
|
this.dropdownItems.forEach(item => {
|
|
if (item.dataset.tab === tab.id) {
|
|
item.classList.add('active');
|
|
} else {
|
|
item.classList.remove('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
checkScrollIndicators() {
|
|
if (!this.tabList) return;
|
|
|
|
const scrollLeft = this.container.querySelector('.scroll-indicator.left');
|
|
const scrollRight = this.container.querySelector('.scroll-indicator.right');
|
|
|
|
if (!scrollLeft || !scrollRight) return;
|
|
|
|
const { scrollLeft: scroll, scrollWidth, clientWidth } = this.tabList;
|
|
|
|
scrollLeft.classList.toggle('visible', scroll > 0);
|
|
scrollRight.classList.toggle('visible', scroll < scrollWidth - clientWidth - 1);
|
|
}
|
|
|
|
setupKeyboardNavigation() {
|
|
this.tabList.addEventListener('keydown', (e) => {
|
|
const currentTab = document.activeElement;
|
|
if (!currentTab.classList.contains('tab-item')) return;
|
|
|
|
let nextTab;
|
|
const tabs = Array.from(this.tabs);
|
|
const currentIndex = tabs.indexOf(currentTab);
|
|
|
|
switch (e.key) {
|
|
case 'ArrowLeft':
|
|
case 'ArrowUp':
|
|
e.preventDefault();
|
|
nextTab = tabs[currentIndex - 1] || tabs[tabs.length - 1];
|
|
break;
|
|
case 'ArrowRight':
|
|
case 'ArrowDown':
|
|
e.preventDefault();
|
|
nextTab = tabs[currentIndex + 1] || tabs[0];
|
|
break;
|
|
case 'Home':
|
|
e.preventDefault();
|
|
nextTab = tabs[0];
|
|
break;
|
|
case 'End':
|
|
e.preventDefault();
|
|
nextTab = tabs[tabs.length - 1];
|
|
break;
|
|
case 'Enter':
|
|
case ' ':
|
|
e.preventDefault();
|
|
this.activateTab(currentTab);
|
|
return;
|
|
}
|
|
|
|
if (nextTab) {
|
|
nextTab.focus();
|
|
this.ensureTabVisible(nextTab);
|
|
}
|
|
});
|
|
}
|
|
|
|
ensureTabVisible(tab) {
|
|
const tabRect = tab.getBoundingClientRect();
|
|
const listRect = this.tabList.getBoundingClientRect();
|
|
|
|
if (tabRect.left < listRect.left) {
|
|
this.tabList.scrollLeft -= listRect.left - tabRect.left + 20;
|
|
} else if (tabRect.right > listRect.right) {
|
|
this.tabList.scrollLeft += tabRect.right - listRect.right + 20;
|
|
}
|
|
}
|
|
|
|
setupTouchSupport() {
|
|
let touchStartX = 0;
|
|
let touchStartY = 0;
|
|
let touchStartScrollLeft = 0;
|
|
|
|
this.tabList.addEventListener('touchstart', (e) => {
|
|
touchStartX = e.touches[0].clientX;
|
|
touchStartY = e.touches[0].clientY;
|
|
touchStartScrollLeft = this.tabList.scrollLeft;
|
|
this.tabList.classList.add('touching');
|
|
}, { passive: true });
|
|
|
|
this.tabList.addEventListener('touchmove', (e) => {
|
|
const touchEndX = e.touches[0].clientX;
|
|
const touchEndY = e.touches[0].clientY;
|
|
const diffX = touchStartX - touchEndX;
|
|
const diffY = Math.abs(touchStartY - touchEndY);
|
|
|
|
// Only scroll horizontally if the horizontal movement is greater than vertical
|
|
if (Math.abs(diffX) > diffY && !this.isVertical) {
|
|
this.tabList.scrollLeft = touchStartScrollLeft + diffX;
|
|
}
|
|
}, { passive: true });
|
|
|
|
this.tabList.addEventListener('touchend', () => {
|
|
this.tabList.classList.remove('touching');
|
|
this.checkScrollIndicators();
|
|
}, { passive: true });
|
|
|
|
// Swipe between tabs on content area
|
|
let contentTouchStartX = 0;
|
|
const contentArea = this.container.querySelector('.tab-content');
|
|
|
|
contentArea.addEventListener('touchstart', (e) => {
|
|
contentTouchStartX = e.touches[0].clientX;
|
|
}, { passive: true });
|
|
|
|
contentArea.addEventListener('touchend', (e) => {
|
|
const touchEndX = e.changedTouches[0].clientX;
|
|
const diffX = contentTouchStartX - touchEndX;
|
|
|
|
if (Math.abs(diffX) > 50) { // Minimum swipe distance
|
|
const tabs = Array.from(this.tabs);
|
|
const currentIndex = tabs.indexOf(this.activeTab);
|
|
|
|
if (diffX > 0 && currentIndex < tabs.length - 1) {
|
|
// Swipe left - next tab
|
|
this.activateTab(tabs[currentIndex + 1]);
|
|
} else if (diffX < 0 && currentIndex > 0) {
|
|
// Swipe right - previous tab
|
|
this.activateTab(tabs[currentIndex - 1]);
|
|
}
|
|
}
|
|
}, { passive: true });
|
|
}
|
|
|
|
setupDragAndDrop() {
|
|
this.tabs.forEach(tab => {
|
|
tab.draggable = true;
|
|
|
|
tab.addEventListener('dragstart', (e) => {
|
|
this.isDragging = true;
|
|
this.draggedTab = tab;
|
|
tab.classList.add('dragging');
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
e.dataTransfer.setData('text/html', tab.innerHTML);
|
|
});
|
|
|
|
tab.addEventListener('dragend', () => {
|
|
this.isDragging = false;
|
|
tab.classList.remove('dragging');
|
|
this.draggedTab = null;
|
|
this.updateIndicator();
|
|
});
|
|
|
|
tab.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
const afterElement = this.getDragAfterElement(e.clientX, e.clientY);
|
|
|
|
if (afterElement == null) {
|
|
this.tabList.appendChild(this.draggedTab);
|
|
} else {
|
|
this.tabList.insertBefore(this.draggedTab, afterElement);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
getDragAfterElement(x, y) {
|
|
const draggableElements = [...this.tabList.querySelectorAll('.tab-item:not(.dragging)')];
|
|
|
|
return draggableElements.reduce((closest, child) => {
|
|
const box = child.getBoundingClientRect();
|
|
const offset = this.isVertical ?
|
|
y - box.top - box.height / 2 :
|
|
x - box.left - box.width / 2;
|
|
|
|
if (offset < 0 && offset > closest.offset) {
|
|
return { offset: offset, element: child };
|
|
} else {
|
|
return closest;
|
|
}
|
|
}, { offset: Number.NEGATIVE_INFINITY }).element;
|
|
}
|
|
|
|
addCloseButtons() {
|
|
this.tabs.forEach((tab, index) => {
|
|
// Don't add close button to first tab
|
|
if (index === 0 || tab.querySelector('.tab-close')) return;
|
|
|
|
const closeBtn = document.createElement('button');
|
|
closeBtn.className = 'tab-close';
|
|
closeBtn.innerHTML = `
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
</svg>
|
|
`;
|
|
closeBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
this.closeTab(tab);
|
|
};
|
|
tab.appendChild(closeBtn);
|
|
});
|
|
}
|
|
|
|
removeCloseButtons() {
|
|
this.tabs.forEach(tab => {
|
|
const closeBtn = tab.querySelector('.tab-close');
|
|
if (closeBtn) closeBtn.remove();
|
|
});
|
|
}
|
|
|
|
closeTab(tab) {
|
|
const panel = document.getElementById(tab.getAttribute('aria-controls'));
|
|
const tabs = Array.from(this.tabs);
|
|
const currentIndex = tabs.indexOf(tab);
|
|
|
|
// Animate out
|
|
tab.style.width = tab.offsetWidth + 'px';
|
|
tab.style.transition = 'width 0.3s ease, opacity 0.3s ease, margin 0.3s ease';
|
|
requestAnimationFrame(() => {
|
|
tab.style.width = '0';
|
|
tab.style.opacity = '0';
|
|
tab.style.marginLeft = '0';
|
|
tab.style.marginRight = '0';
|
|
tab.style.padding = '0';
|
|
});
|
|
|
|
setTimeout(() => {
|
|
// If closing active tab, activate adjacent tab
|
|
if (tab === this.activeTab) {
|
|
const nextTab = tabs[currentIndex + 1] || tabs[currentIndex - 1];
|
|
if (nextTab) this.activateTab(nextTab);
|
|
}
|
|
|
|
tab.remove();
|
|
panel.remove();
|
|
this.updateIndicator();
|
|
this.checkScrollIndicators();
|
|
}, 300);
|
|
}
|
|
|
|
async loadTabContent(panel) {
|
|
const tab = document.getElementById(panel.getAttribute('aria-labelledby'));
|
|
tab.classList.add('loading');
|
|
|
|
// Simulate async content loading
|
|
const loadingDiv = document.createElement('div');
|
|
loadingDiv.className = 'tab-loading';
|
|
loadingDiv.innerHTML = '<div class="spinner"></div>';
|
|
panel.appendChild(loadingDiv);
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
|
|
tab.classList.remove('loading');
|
|
loadingDiv.remove();
|
|
delete panel.dataset.lazyLoad;
|
|
}
|
|
|
|
closeDropdown() {
|
|
if (this.dropdown) {
|
|
this.dropdown.classList.remove('open');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize tabs
|
|
const tabSystem = new EnhancedTabs(document.getElementById('mainTabs'));
|
|
|
|
// Demo functions
|
|
let tabCounter = 7;
|
|
|
|
function addTab() {
|
|
const tabList = document.querySelector('.tab-list');
|
|
const tabContent = document.querySelector('.tab-content');
|
|
const dropdown = document.querySelector('.tab-dropdown-menu');
|
|
|
|
// Create new tab
|
|
const newTab = document.createElement('button');
|
|
newTab.className = 'tab-item';
|
|
newTab.role = 'tab';
|
|
newTab.id = `tab-${tabCounter}`;
|
|
newTab.setAttribute('aria-selected', 'false');
|
|
newTab.setAttribute('aria-controls', `panel-${tabCounter}`);
|
|
newTab.setAttribute('tabindex', '-1');
|
|
newTab.textContent = `New Tab ${tabCounter - 6}`;
|
|
|
|
// Create new panel
|
|
const newPanel = document.createElement('div');
|
|
newPanel.className = 'tab-panel';
|
|
newPanel.role = 'tabpanel';
|
|
newPanel.id = `panel-${tabCounter}`;
|
|
newPanel.setAttribute('aria-labelledby', `tab-${tabCounter}`);
|
|
newPanel.innerHTML = `
|
|
<h2>New Tab ${tabCounter - 6}</h2>
|
|
<p>This is dynamically added content. The tab system handles new tabs seamlessly with smooth animations.</p>
|
|
`;
|
|
|
|
// Create dropdown item
|
|
if (dropdown) {
|
|
const dropdownItem = document.createElement('div');
|
|
dropdownItem.className = 'tab-dropdown-item';
|
|
dropdownItem.dataset.tab = `tab-${tabCounter}`;
|
|
dropdownItem.textContent = `New Tab ${tabCounter - 6}`;
|
|
dropdown.appendChild(dropdownItem);
|
|
}
|
|
|
|
// Animate in
|
|
newTab.style.opacity = '0';
|
|
newTab.style.transform = 'translateY(-10px)';
|
|
|
|
tabList.appendChild(newTab);
|
|
tabContent.appendChild(newPanel);
|
|
|
|
requestAnimationFrame(() => {
|
|
newTab.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
|
newTab.style.opacity = '1';
|
|
newTab.style.transform = 'translateY(0)';
|
|
});
|
|
|
|
tabCounter++;
|
|
|
|
// Reinitialize tab system
|
|
setTimeout(() => {
|
|
const container = document.getElementById('mainTabs');
|
|
window.tabSystem = new EnhancedTabs(container);
|
|
window.tabSystem.activateTab(newTab);
|
|
window.tabSystem.ensureTabVisible(newTab);
|
|
|
|
if (window.tabSystem.closeable) {
|
|
window.tabSystem.addCloseButtons();
|
|
}
|
|
}, 100);
|
|
}
|
|
|
|
function toggleOrientation() {
|
|
const container = document.getElementById('mainTabs');
|
|
container.classList.toggle('vertical');
|
|
window.tabSystem = new EnhancedTabs(container);
|
|
}
|
|
|
|
function toggleCloseable() {
|
|
window.tabSystem.closeable = !window.tabSystem.closeable;
|
|
if (window.tabSystem.closeable) {
|
|
window.tabSystem.addCloseButtons();
|
|
} else {
|
|
window.tabSystem.removeCloseButtons();
|
|
}
|
|
}
|
|
|
|
function simulateLoading() {
|
|
const activePanel = document.querySelector('.tab-panel.active');
|
|
activePanel.dataset.lazyLoad = 'true';
|
|
window.tabSystem.loadTabContent(activePanel);
|
|
}
|
|
|
|
function toggleDropdown() {
|
|
const dropdown = document.getElementById('tabDropdown');
|
|
dropdown.classList.toggle('open');
|
|
}
|
|
|
|
// Global reference
|
|
window.tabSystem = tabSystem;
|
|
</script>
|
|
</body>
|
|
</html> |