docyard 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -1
- data/LICENSE.vscode-icons +42 -0
- data/README.md +86 -23
- data/lib/docyard/asset_handler.rb +33 -0
- data/lib/docyard/build/asset_bundler.rb +139 -0
- data/lib/docyard/build/file_copier.rb +105 -0
- data/lib/docyard/build/sitemap_generator.rb +57 -0
- data/lib/docyard/build/static_generator.rb +141 -0
- data/lib/docyard/builder.rb +104 -0
- data/lib/docyard/cli.rb +19 -0
- data/lib/docyard/components/base_processor.rb +24 -0
- data/lib/docyard/components/callout_processor.rb +121 -0
- data/lib/docyard/components/code_block_processor.rb +55 -0
- data/lib/docyard/components/code_detector.rb +59 -0
- data/lib/docyard/components/icon_detector.rb +57 -0
- data/lib/docyard/components/icon_processor.rb +51 -0
- data/lib/docyard/components/registry.rb +34 -0
- data/lib/docyard/components/table_wrapper_processor.rb +18 -0
- data/lib/docyard/components/tabs_parser.rb +60 -0
- data/lib/docyard/components/tabs_processor.rb +44 -0
- data/lib/docyard/config/validator.rb +171 -0
- data/lib/docyard/config.rb +135 -0
- data/lib/docyard/constants.rb +5 -0
- data/lib/docyard/icons/LICENSE.phosphor +21 -0
- data/lib/docyard/icons/file_types.rb +92 -0
- data/lib/docyard/icons/phosphor.rb +64 -0
- data/lib/docyard/icons.rb +40 -0
- data/lib/docyard/initializer.rb +93 -9
- data/lib/docyard/language_mapping.rb +52 -0
- data/lib/docyard/markdown.rb +27 -3
- data/lib/docyard/preview_server.rb +72 -0
- data/lib/docyard/rack_application.rb +77 -8
- data/lib/docyard/renderer.rb +56 -9
- data/lib/docyard/server.rb +5 -2
- data/lib/docyard/sidebar/config_parser.rb +180 -0
- data/lib/docyard/sidebar/item.rb +58 -0
- data/lib/docyard/sidebar/renderer.rb +33 -6
- data/lib/docyard/sidebar_builder.rb +54 -2
- data/lib/docyard/templates/assets/css/code.css +150 -2
- data/lib/docyard/templates/assets/css/components/callout.css +169 -0
- data/lib/docyard/templates/assets/css/components/code-block.css +196 -0
- data/lib/docyard/templates/assets/css/components/icon.css +16 -0
- data/lib/docyard/templates/assets/css/components/logo.css +44 -0
- data/lib/docyard/templates/assets/css/{components.css → components/navigation.css} +111 -53
- data/lib/docyard/templates/assets/css/components/tabs.css +299 -0
- data/lib/docyard/templates/assets/css/components/theme-toggle.css +69 -0
- data/lib/docyard/templates/assets/css/layout.css +14 -4
- data/lib/docyard/templates/assets/css/markdown.css +27 -17
- data/lib/docyard/templates/assets/css/reset.css +4 -0
- data/lib/docyard/templates/assets/css/variables.css +94 -3
- data/lib/docyard/templates/assets/favicon.svg +16 -0
- data/lib/docyard/templates/assets/js/components/code-block.js +162 -0
- data/lib/docyard/templates/assets/js/components/navigation.js +221 -0
- data/lib/docyard/templates/assets/js/components/tabs.js +338 -0
- data/lib/docyard/templates/assets/js/theme.js +12 -179
- data/lib/docyard/templates/assets/logo-dark.svg +4 -0
- data/lib/docyard/templates/assets/logo.svg +12 -0
- data/lib/docyard/templates/config/docyard.yml.erb +42 -0
- data/lib/docyard/templates/layouts/default.html.erb +32 -4
- data/lib/docyard/templates/markdown/getting-started/installation.md.erb +46 -12
- data/lib/docyard/templates/markdown/guides/configuration.md.erb +202 -0
- data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +247 -0
- data/lib/docyard/templates/markdown/index.md.erb +55 -59
- data/lib/docyard/templates/partials/_callout.html.erb +11 -0
- data/lib/docyard/templates/partials/_code_block.html.erb +6 -0
- data/lib/docyard/templates/partials/_icon.html.erb +1 -0
- data/lib/docyard/templates/partials/_icon_file_extension.html.erb +1 -0
- data/lib/docyard/templates/partials/_nav_group.html.erb +10 -4
- data/lib/docyard/templates/partials/_nav_leaf.html.erb +9 -1
- data/lib/docyard/templates/partials/_tabs.html.erb +40 -0
- data/lib/docyard/templates/partials/_theme_toggle.html.erb +13 -0
- data/lib/docyard/version.rb +1 -1
- data/lib/docyard.rb +8 -0
- metadata +91 -7
- data/lib/docyard/templates/markdown/core-concepts/file-structure.md.erb +0 -61
- data/lib/docyard/templates/markdown/core-concepts/markdown.md.erb +0 -90
- data/lib/docyard/templates/markdown/getting-started/introduction.md.erb +0 -30
- data/lib/docyard/templates/markdown/getting-started/quick-start.md.erb +0 -56
- data/lib/docyard/templates/partials/_icons.html.erb +0 -11
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeBlockManager - Manages code block copy functionality
|
|
3
|
+
*
|
|
4
|
+
* @class CodeBlockManager
|
|
5
|
+
*/
|
|
6
|
+
class CodeBlockManager {
|
|
7
|
+
/**
|
|
8
|
+
* Create a CodeBlockManager instance
|
|
9
|
+
* @param {HTMLElement} container - The .docyard-code-block container element
|
|
10
|
+
*/
|
|
11
|
+
constructor(container) {
|
|
12
|
+
if (!container) return;
|
|
13
|
+
|
|
14
|
+
this.container = container;
|
|
15
|
+
this.copyButton = container.querySelector('.docyard-code-block__copy');
|
|
16
|
+
this.codeText = this.copyButton?.getAttribute('data-code') || '';
|
|
17
|
+
|
|
18
|
+
this.originalIcon = this.copyButton?.innerHTML || '';
|
|
19
|
+
|
|
20
|
+
this.checkIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z"/></svg>';
|
|
21
|
+
|
|
22
|
+
this.handleCopy = this.handleCopy.bind(this);
|
|
23
|
+
|
|
24
|
+
this.init();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Initialize the code block component
|
|
29
|
+
*/
|
|
30
|
+
init() {
|
|
31
|
+
if (!this.copyButton) return;
|
|
32
|
+
|
|
33
|
+
this.copyButton.addEventListener('click', this.handleCopy);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Handle copy button click
|
|
38
|
+
*/
|
|
39
|
+
async handleCopy() {
|
|
40
|
+
try {
|
|
41
|
+
await this.copyToClipboard(this.codeText);
|
|
42
|
+
this.showSuccess();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.warn('Failed to copy code:', error);
|
|
45
|
+
this.showError();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Copy text to clipboard
|
|
51
|
+
* @param {string} text - Text to copy
|
|
52
|
+
* @returns {Promise<void>}
|
|
53
|
+
*/
|
|
54
|
+
async copyToClipboard(text) {
|
|
55
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
56
|
+
await navigator.clipboard.writeText(text);
|
|
57
|
+
} else {
|
|
58
|
+
this.fallbackCopy(text);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Fallback copy method for older browsers
|
|
64
|
+
* @param {string} text - Text to copy
|
|
65
|
+
*/
|
|
66
|
+
fallbackCopy(text) {
|
|
67
|
+
const textArea = document.createElement('textarea');
|
|
68
|
+
textArea.value = text;
|
|
69
|
+
textArea.style.position = 'fixed';
|
|
70
|
+
textArea.style.left = '-999999px';
|
|
71
|
+
textArea.style.top = '-999999px';
|
|
72
|
+
document.body.appendChild(textArea);
|
|
73
|
+
textArea.focus();
|
|
74
|
+
textArea.select();
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
document.execCommand('copy');
|
|
78
|
+
textArea.remove();
|
|
79
|
+
} catch (error) {
|
|
80
|
+
textArea.remove();
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Show success state
|
|
87
|
+
*/
|
|
88
|
+
showSuccess() {
|
|
89
|
+
this.copyButton.classList.add('is-success');
|
|
90
|
+
this.copyButton.setAttribute('aria-label', 'Copied to clipboard!');
|
|
91
|
+
|
|
92
|
+
this.copyButton.innerHTML = this.checkIcon;
|
|
93
|
+
|
|
94
|
+
if (this.resetTimeout) {
|
|
95
|
+
clearTimeout(this.resetTimeout);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.resetTimeout = setTimeout(() => {
|
|
99
|
+
this.resetState();
|
|
100
|
+
}, 2000);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Show error state
|
|
105
|
+
*/
|
|
106
|
+
showError() {
|
|
107
|
+
this.copyButton.classList.add('is-error');
|
|
108
|
+
this.copyButton.setAttribute('aria-label', 'Failed to copy');
|
|
109
|
+
|
|
110
|
+
if (this.resetTimeout) {
|
|
111
|
+
clearTimeout(this.resetTimeout);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
this.resetTimeout = setTimeout(() => {
|
|
115
|
+
this.resetState();
|
|
116
|
+
}, 2000);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Reset button to default state
|
|
121
|
+
*/
|
|
122
|
+
resetState() {
|
|
123
|
+
this.copyButton.classList.remove('is-success', 'is-error');
|
|
124
|
+
this.copyButton.setAttribute('aria-label', 'Copy code to clipboard');
|
|
125
|
+
|
|
126
|
+
this.copyButton.innerHTML = this.originalIcon;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Cleanup - remove event listeners
|
|
131
|
+
*/
|
|
132
|
+
destroy() {
|
|
133
|
+
if (this.copyButton) {
|
|
134
|
+
this.copyButton.removeEventListener('click', this.handleCopy);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (this.resetTimeout) {
|
|
138
|
+
clearTimeout(this.resetTimeout);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Auto-initialize all code blocks on page load
|
|
145
|
+
*/
|
|
146
|
+
function initializeCodeBlocks() {
|
|
147
|
+
const codeBlocks = document.querySelectorAll('.docyard-code-block');
|
|
148
|
+
|
|
149
|
+
codeBlocks.forEach(container => {
|
|
150
|
+
new CodeBlockManager(container);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (document.readyState === 'loading') {
|
|
155
|
+
document.addEventListener('DOMContentLoaded', initializeCodeBlocks);
|
|
156
|
+
} else {
|
|
157
|
+
initializeCodeBlocks();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
161
|
+
module.exports = { CodeBlockManager };
|
|
162
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Docyard Navigation JavaScript
|
|
2
|
+
// Handles sidebar navigation, mobile menu, accordion groups, and scroll behavior
|
|
3
|
+
|
|
4
|
+
(function() {
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
function initMobileMenu() {
|
|
8
|
+
const toggle = document.querySelector('.secondary-header-menu');
|
|
9
|
+
const sidebar = document.querySelector('.sidebar');
|
|
10
|
+
const overlay = document.querySelector('.mobile-overlay');
|
|
11
|
+
|
|
12
|
+
if (!toggle || !sidebar || !overlay) return;
|
|
13
|
+
|
|
14
|
+
function openMenu() {
|
|
15
|
+
sidebar.classList.add('is-open');
|
|
16
|
+
overlay.classList.add('is-visible');
|
|
17
|
+
toggle.setAttribute('aria-expanded', 'true');
|
|
18
|
+
document.body.style.overflow = 'hidden';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function closeMenu() {
|
|
22
|
+
sidebar.classList.remove('is-open');
|
|
23
|
+
overlay.classList.remove('is-visible');
|
|
24
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
25
|
+
document.body.style.overflow = '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function toggleMenu() {
|
|
29
|
+
if (sidebar.classList.contains('is-open')) {
|
|
30
|
+
closeMenu();
|
|
31
|
+
} else {
|
|
32
|
+
openMenu();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
toggle.addEventListener('click', toggleMenu);
|
|
37
|
+
overlay.addEventListener('click', closeMenu);
|
|
38
|
+
|
|
39
|
+
document.addEventListener('keydown', function(e) {
|
|
40
|
+
if (e.key === 'Escape' && sidebar.classList.contains('is-open')) {
|
|
41
|
+
closeMenu();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
sidebar.querySelectorAll('a').forEach(function(link) {
|
|
46
|
+
link.addEventListener('click', closeMenu);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function initAccordion() {
|
|
51
|
+
const toggles = document.querySelectorAll('.nav-group-toggle');
|
|
52
|
+
|
|
53
|
+
toggles.forEach(function(toggle) {
|
|
54
|
+
toggle.addEventListener('click', function() {
|
|
55
|
+
const expanded = toggle.getAttribute('aria-expanded') === 'true';
|
|
56
|
+
const children = toggle.nextElementSibling;
|
|
57
|
+
|
|
58
|
+
if (!children || !children.classList.contains('nav-group-children')) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
toggle.setAttribute('aria-expanded', !expanded);
|
|
63
|
+
children.classList.toggle('collapsed');
|
|
64
|
+
|
|
65
|
+
if (expanded) {
|
|
66
|
+
children.style.maxHeight = '0';
|
|
67
|
+
} else {
|
|
68
|
+
children.style.maxHeight = children.scrollHeight + 'px';
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
document.querySelectorAll('.nav-group-children.collapsed').forEach(function(el) {
|
|
74
|
+
el.style.maxHeight = '0';
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function expandActiveGroups() {
|
|
79
|
+
const activeLinks = document.querySelectorAll('a.active');
|
|
80
|
+
|
|
81
|
+
activeLinks.forEach(function(activeLink) {
|
|
82
|
+
let parent = activeLink.closest('.nav-group-children');
|
|
83
|
+
|
|
84
|
+
while (parent) {
|
|
85
|
+
if (parent.classList.contains('nav-group-children')) {
|
|
86
|
+
parent.classList.remove('collapsed');
|
|
87
|
+
|
|
88
|
+
const toggle = parent.previousElementSibling;
|
|
89
|
+
if (toggle && toggle.classList.contains('nav-group-toggle')) {
|
|
90
|
+
toggle.setAttribute('aria-expanded', 'true');
|
|
91
|
+
|
|
92
|
+
parent.style.maxHeight = 'none';
|
|
93
|
+
const height = parent.scrollHeight;
|
|
94
|
+
parent.style.maxHeight = height + 'px';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
parent = parent.parentElement?.closest('.nav-group-children');
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function initSidebarScroll() {
|
|
104
|
+
const scrollContainer = document.querySelector('.sidebar nav');
|
|
105
|
+
if (!scrollContainer) return;
|
|
106
|
+
|
|
107
|
+
const STORAGE_KEY = 'docyard_sidebar_scroll';
|
|
108
|
+
const savedPosition = sessionStorage.getItem(STORAGE_KEY);
|
|
109
|
+
|
|
110
|
+
if (savedPosition) {
|
|
111
|
+
const position = parseInt(savedPosition, 10);
|
|
112
|
+
scrollContainer.scrollTop = position;
|
|
113
|
+
|
|
114
|
+
setTimeout(function() {
|
|
115
|
+
scrollContainer.scrollTop = position;
|
|
116
|
+
}, 100);
|
|
117
|
+
} else {
|
|
118
|
+
const activeLink = scrollContainer.querySelector('a.active');
|
|
119
|
+
if (activeLink) {
|
|
120
|
+
setTimeout(function() {
|
|
121
|
+
activeLink.scrollIntoView({
|
|
122
|
+
behavior: 'instant',
|
|
123
|
+
block: 'center'
|
|
124
|
+
});
|
|
125
|
+
}, 50);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
scrollContainer.querySelectorAll('a').forEach(function(link) {
|
|
130
|
+
link.addEventListener('click', function() {
|
|
131
|
+
sessionStorage.setItem(STORAGE_KEY, scrollContainer.scrollTop);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
let scrollTimeout;
|
|
136
|
+
scrollContainer.addEventListener('scroll', function() {
|
|
137
|
+
clearTimeout(scrollTimeout);
|
|
138
|
+
scrollTimeout = setTimeout(function() {
|
|
139
|
+
sessionStorage.setItem(STORAGE_KEY, scrollContainer.scrollTop);
|
|
140
|
+
}, 150);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const logo = document.querySelector('.header-logo');
|
|
144
|
+
if (logo) {
|
|
145
|
+
logo.addEventListener('click', function() {
|
|
146
|
+
sessionStorage.removeItem(STORAGE_KEY);
|
|
147
|
+
scrollContainer.scrollTop = 0;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function initScrollBehavior() {
|
|
153
|
+
const header = document.querySelector('.header');
|
|
154
|
+
const secondaryHeader = document.querySelector('.secondary-header');
|
|
155
|
+
|
|
156
|
+
if (!header || !secondaryHeader) return;
|
|
157
|
+
|
|
158
|
+
let lastScrollTop = 0;
|
|
159
|
+
let ticking = false;
|
|
160
|
+
|
|
161
|
+
function isMobile() {
|
|
162
|
+
return window.innerWidth <= 1024;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function updateHeaders() {
|
|
166
|
+
if (!isMobile()) {
|
|
167
|
+
header.classList.remove('hide-on-scroll');
|
|
168
|
+
secondaryHeader.classList.remove('shift-up');
|
|
169
|
+
ticking = false;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
174
|
+
|
|
175
|
+
if (scrollTop > lastScrollTop && scrollTop > 100) {
|
|
176
|
+
header.classList.add('hide-on-scroll');
|
|
177
|
+
secondaryHeader.classList.add('shift-up');
|
|
178
|
+
} else if (scrollTop < lastScrollTop) {
|
|
179
|
+
header.classList.remove('hide-on-scroll');
|
|
180
|
+
secondaryHeader.classList.remove('shift-up');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
lastScrollTop = scrollTop <= 0 ? 0 : scrollTop;
|
|
184
|
+
ticking = false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
window.addEventListener('scroll', function() {
|
|
188
|
+
if (!ticking) {
|
|
189
|
+
window.requestAnimationFrame(updateHeaders);
|
|
190
|
+
ticking = true;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
window.addEventListener('resize', function() {
|
|
195
|
+
if (!isMobile()) {
|
|
196
|
+
header.classList.remove('hide-on-scroll');
|
|
197
|
+
secondaryHeader.classList.remove('shift-up');
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if ('scrollRestoration' in history) {
|
|
203
|
+
history.scrollRestoration = 'manual';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (document.readyState === 'loading') {
|
|
207
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
208
|
+
initMobileMenu();
|
|
209
|
+
initAccordion();
|
|
210
|
+
expandActiveGroups();
|
|
211
|
+
initSidebarScroll();
|
|
212
|
+
initScrollBehavior();
|
|
213
|
+
});
|
|
214
|
+
} else {
|
|
215
|
+
initMobileMenu();
|
|
216
|
+
initAccordion();
|
|
217
|
+
expandActiveGroups();
|
|
218
|
+
initSidebarScroll();
|
|
219
|
+
initScrollBehavior();
|
|
220
|
+
}
|
|
221
|
+
})();
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TabsManager - Manages tab component interactions
|
|
3
|
+
*
|
|
4
|
+
* @class TabsManager
|
|
5
|
+
*/
|
|
6
|
+
class TabsManager {
|
|
7
|
+
/**
|
|
8
|
+
* Create a TabsManager instance
|
|
9
|
+
* @param {HTMLElement} container - The .docyard-tabs container element
|
|
10
|
+
*/
|
|
11
|
+
constructor(container) {
|
|
12
|
+
if (!container) return;
|
|
13
|
+
|
|
14
|
+
this.container = container;
|
|
15
|
+
this.tabListWrapper = container.querySelector('.docyard-tabs__list-wrapper');
|
|
16
|
+
this.tabList = container.querySelector('[role="tablist"]');
|
|
17
|
+
this.tabs = Array.from(container.querySelectorAll('[role="tab"]'));
|
|
18
|
+
this.panels = Array.from(container.querySelectorAll('[role="tabpanel"]'));
|
|
19
|
+
this.indicator = container.querySelector('.docyard-tabs__indicator');
|
|
20
|
+
this.activeIndex = 0;
|
|
21
|
+
this.groupId = container.getAttribute('data-tabs');
|
|
22
|
+
|
|
23
|
+
this.handleTabClick = this.handleTabClick.bind(this);
|
|
24
|
+
this.handleKeyDown = this.handleKeyDown.bind(this);
|
|
25
|
+
this.handleResize = this.handleResize.bind(this);
|
|
26
|
+
this.handleScroll = this.handleScroll.bind(this);
|
|
27
|
+
|
|
28
|
+
this.init();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Initialize the tabs component
|
|
33
|
+
*/
|
|
34
|
+
init() {
|
|
35
|
+
if (!this.tabList || this.tabs.length === 0 || this.panels.length === 0) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.createScrollIndicators();
|
|
40
|
+
this.loadPreference();
|
|
41
|
+
this.attachEventListeners();
|
|
42
|
+
this.activateTab(this.activeIndex, false);
|
|
43
|
+
this.updateIndicator();
|
|
44
|
+
this.updateScrollIndicators();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create scroll indicator elements
|
|
49
|
+
*/
|
|
50
|
+
createScrollIndicators() {
|
|
51
|
+
if (!this.tabListWrapper || !this.tabList) return;
|
|
52
|
+
|
|
53
|
+
this.leftIndicator = document.createElement('div');
|
|
54
|
+
this.leftIndicator.className = 'docyard-tabs__scroll-indicator docyard-tabs__scroll-indicator--left';
|
|
55
|
+
this.tabListWrapper.insertBefore(this.leftIndicator, this.tabList);
|
|
56
|
+
|
|
57
|
+
this.rightIndicator = document.createElement('div');
|
|
58
|
+
this.rightIndicator.className = 'docyard-tabs__scroll-indicator docyard-tabs__scroll-indicator--right';
|
|
59
|
+
this.tabListWrapper.appendChild(this.rightIndicator);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Attach all event listeners
|
|
64
|
+
*/
|
|
65
|
+
attachEventListeners() {
|
|
66
|
+
this.tabs.forEach((tab, index) => {
|
|
67
|
+
tab.addEventListener('click', () => this.handleTabClick(index));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
this.tabList.addEventListener('keydown', this.handleKeyDown);
|
|
71
|
+
|
|
72
|
+
this.tabList.addEventListener('scroll', this.handleScroll);
|
|
73
|
+
|
|
74
|
+
window.addEventListener('resize', this.handleResize);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Handle tab click
|
|
79
|
+
* @param {number} index - Index of clicked tab
|
|
80
|
+
*/
|
|
81
|
+
handleTabClick(index) {
|
|
82
|
+
if (index === this.activeIndex) return;
|
|
83
|
+
|
|
84
|
+
this.activateTab(index, true);
|
|
85
|
+
this.savePreference(index);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Handle keyboard navigation
|
|
90
|
+
* @param {KeyboardEvent} event - Keyboard event
|
|
91
|
+
*/
|
|
92
|
+
handleKeyDown(event) {
|
|
93
|
+
const { key } = event;
|
|
94
|
+
|
|
95
|
+
if (key === 'ArrowLeft' || key === 'ArrowRight') {
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
|
|
98
|
+
if (key === 'ArrowLeft') {
|
|
99
|
+
this.activatePreviousTab();
|
|
100
|
+
} else {
|
|
101
|
+
this.activateNextTab();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.tabs[this.activeIndex].focus();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (key === 'Home') {
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
this.activateTab(0, true);
|
|
110
|
+
this.tabs[0].focus();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (key === 'End') {
|
|
114
|
+
event.preventDefault();
|
|
115
|
+
const lastIndex = this.tabs.length - 1;
|
|
116
|
+
this.activateTab(lastIndex, true);
|
|
117
|
+
this.tabs[lastIndex].focus();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Handle scroll event - update scroll indicators
|
|
123
|
+
*/
|
|
124
|
+
handleScroll() {
|
|
125
|
+
if (this.scrollTimeout) {
|
|
126
|
+
cancelAnimationFrame(this.scrollTimeout);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.scrollTimeout = requestAnimationFrame(() => {
|
|
130
|
+
this.updateScrollIndicators();
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Handle window resize - update indicator and scroll indicators
|
|
136
|
+
*/
|
|
137
|
+
handleResize() {
|
|
138
|
+
if (this.resizeTimeout) {
|
|
139
|
+
cancelAnimationFrame(this.resizeTimeout);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this.resizeTimeout = requestAnimationFrame(() => {
|
|
143
|
+
this.updateIndicator(false);
|
|
144
|
+
this.updateScrollIndicators();
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Activate a specific tab
|
|
150
|
+
* @param {number} index - Index of tab to activate
|
|
151
|
+
* @param {boolean} animate - Whether to animate the transition
|
|
152
|
+
*/
|
|
153
|
+
activateTab(index, animate = true) {
|
|
154
|
+
if (index < 0 || index >= this.tabs.length) return;
|
|
155
|
+
|
|
156
|
+
const previousIndex = this.activeIndex;
|
|
157
|
+
this.activeIndex = index;
|
|
158
|
+
|
|
159
|
+
this.tabs.forEach((tab, i) => {
|
|
160
|
+
const isActive = i === index;
|
|
161
|
+
tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
|
162
|
+
tab.setAttribute('tabindex', isActive ? '0' : '-1');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
this.panels.forEach((panel, i) => {
|
|
166
|
+
const isActive = i === index;
|
|
167
|
+
panel.setAttribute('aria-hidden', isActive ? 'false' : 'true');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
this.updateIndicator(animate);
|
|
171
|
+
|
|
172
|
+
if (previousIndex !== index) {
|
|
173
|
+
this.savePreference(index);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Activate the next tab (wraps around)
|
|
179
|
+
*/
|
|
180
|
+
activateNextTab() {
|
|
181
|
+
const nextIndex = (this.activeIndex + 1) % this.tabs.length;
|
|
182
|
+
this.activateTab(nextIndex, true);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Activate the previous tab (wraps around)
|
|
187
|
+
*/
|
|
188
|
+
activatePreviousTab() {
|
|
189
|
+
const prevIndex = (this.activeIndex - 1 + this.tabs.length) % this.tabs.length;
|
|
190
|
+
this.activateTab(prevIndex, true);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Update the visual indicator position
|
|
195
|
+
* @param {boolean} animate - Whether to animate the transition
|
|
196
|
+
*/
|
|
197
|
+
updateIndicator(animate = true) {
|
|
198
|
+
if (!this.indicator || !this.tabs[this.activeIndex]) return;
|
|
199
|
+
|
|
200
|
+
const activeTab = this.tabs[this.activeIndex];
|
|
201
|
+
const tabListRect = this.tabList.getBoundingClientRect();
|
|
202
|
+
const activeTabRect = activeTab.getBoundingClientRect();
|
|
203
|
+
|
|
204
|
+
const left = activeTabRect.left - tabListRect.left + this.tabList.scrollLeft;
|
|
205
|
+
const width = activeTabRect.width;
|
|
206
|
+
|
|
207
|
+
this.indicator.style.width = `${width}px`;
|
|
208
|
+
this.indicator.style.transform = `translateX(${left}px)`;
|
|
209
|
+
|
|
210
|
+
if (!animate) {
|
|
211
|
+
this.indicator.style.transition = 'none';
|
|
212
|
+
void this.indicator.offsetWidth;
|
|
213
|
+
this.indicator.style.transition = '';
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Update scroll indicators visibility based on scroll position
|
|
219
|
+
*/
|
|
220
|
+
updateScrollIndicators() {
|
|
221
|
+
if (!this.tabList || !this.leftIndicator || !this.rightIndicator) return;
|
|
222
|
+
|
|
223
|
+
const { scrollLeft, scrollWidth, clientWidth } = this.tabList;
|
|
224
|
+
const hasOverflow = scrollWidth > clientWidth;
|
|
225
|
+
|
|
226
|
+
if (!hasOverflow) {
|
|
227
|
+
this.leftIndicator.classList.remove('is-visible');
|
|
228
|
+
this.rightIndicator.classList.remove('is-visible');
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const canScrollLeft = scrollLeft > 5;
|
|
233
|
+
if (canScrollLeft) {
|
|
234
|
+
this.leftIndicator.classList.add('is-visible');
|
|
235
|
+
} else {
|
|
236
|
+
this.leftIndicator.classList.remove('is-visible');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const canScrollRight = scrollLeft < scrollWidth - clientWidth - 5;
|
|
240
|
+
if (canScrollRight) {
|
|
241
|
+
this.rightIndicator.classList.add('is-visible');
|
|
242
|
+
} else {
|
|
243
|
+
this.rightIndicator.classList.remove('is-visible');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Load user preference from localStorage
|
|
249
|
+
*/
|
|
250
|
+
loadPreference() {
|
|
251
|
+
try {
|
|
252
|
+
const preferredTab = localStorage.getItem('docyard-preferred-pm');
|
|
253
|
+
if (!preferredTab) return;
|
|
254
|
+
|
|
255
|
+
const index = this.tabs.findIndex(tab =>
|
|
256
|
+
tab.textContent.trim().toLowerCase() === preferredTab.toLowerCase()
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
if (index !== -1) {
|
|
260
|
+
this.activeIndex = index;
|
|
261
|
+
}
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.warn('Could not load tab preference:', error);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Save user preference to localStorage
|
|
269
|
+
* @param {number} index - Index of active tab
|
|
270
|
+
*/
|
|
271
|
+
savePreference(index) {
|
|
272
|
+
if (index < 0 || index >= this.tabs.length) return;
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const tabName = this.tabs[index].textContent.trim().toLowerCase();
|
|
276
|
+
localStorage.setItem('docyard-preferred-pm', tabName);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
console.warn('Could not save tab preference:', error);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Activate tab by name
|
|
284
|
+
* @param {string} name - Name of tab to activate
|
|
285
|
+
*/
|
|
286
|
+
activateTabByName(name) {
|
|
287
|
+
const index = this.tabs.findIndex(tab =>
|
|
288
|
+
tab.textContent.trim().toLowerCase() === name.toLowerCase()
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
if (index !== -1) {
|
|
292
|
+
this.activateTab(index, true);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Cleanup - remove event listeners
|
|
298
|
+
*/
|
|
299
|
+
destroy() {
|
|
300
|
+
this.tabs.forEach((tab, index) => {
|
|
301
|
+
tab.removeEventListener('click', () => this.handleTabClick(index));
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
this.tabList.removeEventListener('keydown', this.handleKeyDown);
|
|
305
|
+
this.tabList.removeEventListener('scroll', this.handleScroll);
|
|
306
|
+
window.removeEventListener('resize', this.handleResize);
|
|
307
|
+
|
|
308
|
+
if (this.resizeTimeout) {
|
|
309
|
+
cancelAnimationFrame(this.resizeTimeout);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (this.scrollTimeout) {
|
|
313
|
+
cancelAnimationFrame(this.scrollTimeout);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Auto-initialize all tabs on page load
|
|
320
|
+
*/
|
|
321
|
+
function initializeTabs() {
|
|
322
|
+
const tabsContainers = document.querySelectorAll('.docyard-tabs');
|
|
323
|
+
|
|
324
|
+
tabsContainers.forEach(container => {
|
|
325
|
+
new TabsManager(container);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Initialize on DOM ready
|
|
330
|
+
if (document.readyState === 'loading') {
|
|
331
|
+
document.addEventListener('DOMContentLoaded', initializeTabs);
|
|
332
|
+
} else {
|
|
333
|
+
initializeTabs();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
337
|
+
module.exports = { TabsManager };
|
|
338
|
+
}
|