vanduo-framework 1.1.8-docs-update
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.
- package/LICENSE +35 -0
- package/README.md +216 -0
- package/css/components/alerts.css +224 -0
- package/css/components/avatar.css +275 -0
- package/css/components/badges.css +230 -0
- package/css/components/breadcrumbs.css +146 -0
- package/css/components/button-group.css +82 -0
- package/css/components/buttons.css +530 -0
- package/css/components/cards.css +304 -0
- package/css/components/chips.css +259 -0
- package/css/components/code-snippet.css +555 -0
- package/css/components/collapsible.css +267 -0
- package/css/components/collections.css +253 -0
- package/css/components/doc-search.css +464 -0
- package/css/components/doc-tabs.css +38 -0
- package/css/components/draggable.css +317 -0
- package/css/components/dropdown.css +266 -0
- package/css/components/footer.css +375 -0
- package/css/components/forms.css +1774 -0
- package/css/components/image-box.css +279 -0
- package/css/components/modals.css +285 -0
- package/css/components/navbar.css +530 -0
- package/css/components/pagination.css +186 -0
- package/css/components/preloader.css +340 -0
- package/css/components/progress.css +107 -0
- package/css/components/sidenav.css +301 -0
- package/css/components/skeleton.css +241 -0
- package/css/components/spinner.css +144 -0
- package/css/components/tabs.css +327 -0
- package/css/components/theme-customizer.css +835 -0
- package/css/components/toast.css +357 -0
- package/css/components/tooltips.css +270 -0
- package/css/core/colors.css +1017 -0
- package/css/core/fonts.css +266 -0
- package/css/core/grid.css +1699 -0
- package/css/core/helpers.css +2202 -0
- package/css/core/reset.css +128 -0
- package/css/core/tokens.css +213 -0
- package/css/core/typography.css +405 -0
- package/css/core/vd-aliases.css +47 -0
- package/css/effects/parallax.css +113 -0
- package/css/icons/icons-all.css +23 -0
- package/css/icons/icons.css +25 -0
- package/css/utilities/media.css +167 -0
- package/css/utilities/print.css +111 -0
- package/css/utilities/shadow.css +243 -0
- package/css/utilities/table.css +381 -0
- package/css/utilities/transforms.css +71 -0
- package/css/utilities/transitions.css +87 -0
- package/css/vanduo.css +80 -0
- package/dist/build-info.json +6 -0
- package/dist/fonts/fira-sans/fira-sans-bold.woff2 +0 -0
- package/dist/fonts/fira-sans/fira-sans-medium.woff2 +0 -0
- package/dist/fonts/fira-sans/fira-sans-regular.woff2 +0 -0
- package/dist/fonts/ibm-plex/ibm-plex-sans-bold.woff2 +0 -0
- package/dist/fonts/ibm-plex/ibm-plex-sans-medium.woff2 +0 -0
- package/dist/fonts/ibm-plex/ibm-plex-sans-regular.woff2 +0 -0
- package/dist/fonts/inter/inter-bold.woff2 +0 -0
- package/dist/fonts/inter/inter-medium.woff2 +0 -0
- package/dist/fonts/inter/inter-regular.woff2 +0 -0
- package/dist/fonts/inter/inter-semibold.woff2 +0 -0
- package/dist/fonts/jetbrains-mono/jetbrains-mono-bold.woff2 +0 -0
- package/dist/fonts/jetbrains-mono/jetbrains-mono-regular.woff2 +0 -0
- package/dist/fonts/open-sans/open-sans-bold.woff2 +0 -0
- package/dist/fonts/open-sans/open-sans-medium.woff2 +0 -0
- package/dist/fonts/open-sans/open-sans-regular.woff2 +0 -0
- package/dist/fonts/rubik/rubik-bold.woff2 +0 -0
- package/dist/fonts/rubik/rubik-medium.woff2 +0 -0
- package/dist/fonts/rubik/rubik-regular.woff2 +0 -0
- package/dist/fonts/source-sans/source-sans-bold.woff2 +0 -0
- package/dist/fonts/source-sans/source-sans-regular.woff2 +0 -0
- package/dist/fonts/source-sans/source-sans-semibold.woff2 +0 -0
- package/dist/fonts/titillium-web/titillium-web-bold.woff2 +0 -0
- package/dist/fonts/titillium-web/titillium-web-regular.woff2 +0 -0
- package/dist/fonts/titillium-web/titillium-web-semibold.woff2 +0 -0
- package/dist/fonts/ubuntu/ubuntu-bold.woff2 +0 -0
- package/dist/fonts/ubuntu/ubuntu-medium.woff2 +0 -0
- package/dist/fonts/ubuntu/ubuntu-regular.woff2 +0 -0
- package/dist/icons/phosphor/LICENSE +21 -0
- package/dist/icons/phosphor/bold/Phosphor-Bold.ttf +0 -0
- package/dist/icons/phosphor/bold/Phosphor-Bold.woff +0 -0
- package/dist/icons/phosphor/bold/Phosphor-Bold.woff2 +0 -0
- package/dist/icons/phosphor/bold/style.css +4627 -0
- package/dist/icons/phosphor/duotone/Phosphor-Duotone.ttf +0 -0
- package/dist/icons/phosphor/duotone/Phosphor-Duotone.woff +0 -0
- package/dist/icons/phosphor/duotone/Phosphor-Duotone.woff2 +0 -0
- package/dist/icons/phosphor/duotone/style.css +12115 -0
- package/dist/icons/phosphor/fill/Phosphor-Fill.ttf +0 -0
- package/dist/icons/phosphor/fill/Phosphor-Fill.woff +0 -0
- package/dist/icons/phosphor/fill/Phosphor-Fill.woff2 +0 -0
- package/dist/icons/phosphor/fill/style.css +4627 -0
- package/dist/icons/phosphor/light/Phosphor-Light.ttf +0 -0
- package/dist/icons/phosphor/light/Phosphor-Light.woff +0 -0
- package/dist/icons/phosphor/light/Phosphor-Light.woff2 +0 -0
- package/dist/icons/phosphor/light/style.css +4627 -0
- package/dist/icons/phosphor/regular/Phosphor.ttf +0 -0
- package/dist/icons/phosphor/regular/Phosphor.woff +0 -0
- package/dist/icons/phosphor/regular/Phosphor.woff2 +0 -0
- package/dist/icons/phosphor/regular/style.css +4627 -0
- package/dist/icons/phosphor/thin/Phosphor-Thin.ttf +0 -0
- package/dist/icons/phosphor/thin/Phosphor-Thin.woff +0 -0
- package/dist/icons/phosphor/thin/Phosphor-Thin.woff2 +0 -0
- package/dist/icons/phosphor/thin/style.css +4627 -0
- package/dist/vanduo.cjs.js +5569 -0
- package/dist/vanduo.cjs.js.map +7 -0
- package/dist/vanduo.cjs.min.js +48 -0
- package/dist/vanduo.cjs.min.js.map +7 -0
- package/dist/vanduo.css +60666 -0
- package/dist/vanduo.css.map +1 -0
- package/dist/vanduo.esm.js +5548 -0
- package/dist/vanduo.esm.js.map +7 -0
- package/dist/vanduo.esm.min.js +48 -0
- package/dist/vanduo.esm.min.js.map +7 -0
- package/dist/vanduo.js +5545 -0
- package/dist/vanduo.js.map +7 -0
- package/dist/vanduo.min.css +2 -0
- package/dist/vanduo.min.css.map +1 -0
- package/dist/vanduo.min.js +48 -0
- package/dist/vanduo.min.js.map +7 -0
- package/fonts/fira-sans/fira-sans-bold.woff2 +0 -0
- package/fonts/fira-sans/fira-sans-medium.woff2 +0 -0
- package/fonts/fira-sans/fira-sans-regular.woff2 +0 -0
- package/fonts/ibm-plex/ibm-plex-sans-bold.woff2 +0 -0
- package/fonts/ibm-plex/ibm-plex-sans-medium.woff2 +0 -0
- package/fonts/ibm-plex/ibm-plex-sans-regular.woff2 +0 -0
- package/fonts/inter/inter-bold.woff2 +0 -0
- package/fonts/inter/inter-medium.woff2 +0 -0
- package/fonts/inter/inter-regular.woff2 +0 -0
- package/fonts/inter/inter-semibold.woff2 +0 -0
- package/fonts/jetbrains-mono/jetbrains-mono-bold.woff2 +0 -0
- package/fonts/jetbrains-mono/jetbrains-mono-regular.woff2 +0 -0
- package/fonts/open-sans/open-sans-bold.woff2 +0 -0
- package/fonts/open-sans/open-sans-medium.woff2 +0 -0
- package/fonts/open-sans/open-sans-regular.woff2 +0 -0
- package/fonts/rubik/rubik-bold.woff2 +0 -0
- package/fonts/rubik/rubik-medium.woff2 +0 -0
- package/fonts/rubik/rubik-regular.woff2 +0 -0
- package/fonts/source-sans/source-sans-bold.woff2 +0 -0
- package/fonts/source-sans/source-sans-regular.woff2 +0 -0
- package/fonts/source-sans/source-sans-semibold.woff2 +0 -0
- package/fonts/titillium-web/titillium-web-bold.woff2 +0 -0
- package/fonts/titillium-web/titillium-web-regular.woff2 +0 -0
- package/fonts/titillium-web/titillium-web-semibold.woff2 +0 -0
- package/fonts/ubuntu/ubuntu-bold.woff2 +0 -0
- package/fonts/ubuntu/ubuntu-medium.woff2 +0 -0
- package/fonts/ubuntu/ubuntu-regular.woff2 +0 -0
- package/icons/phosphor/LICENSE +21 -0
- package/icons/phosphor/bold/Phosphor-Bold.ttf +0 -0
- package/icons/phosphor/bold/Phosphor-Bold.woff +0 -0
- package/icons/phosphor/bold/Phosphor-Bold.woff2 +0 -0
- package/icons/phosphor/bold/style.css +4627 -0
- package/icons/phosphor/duotone/Phosphor-Duotone.ttf +0 -0
- package/icons/phosphor/duotone/Phosphor-Duotone.woff +0 -0
- package/icons/phosphor/duotone/Phosphor-Duotone.woff2 +0 -0
- package/icons/phosphor/duotone/style.css +12115 -0
- package/icons/phosphor/fill/Phosphor-Fill.ttf +0 -0
- package/icons/phosphor/fill/Phosphor-Fill.woff +0 -0
- package/icons/phosphor/fill/Phosphor-Fill.woff2 +0 -0
- package/icons/phosphor/fill/style.css +4627 -0
- package/icons/phosphor/light/Phosphor-Light.ttf +0 -0
- package/icons/phosphor/light/Phosphor-Light.woff +0 -0
- package/icons/phosphor/light/Phosphor-Light.woff2 +0 -0
- package/icons/phosphor/light/style.css +4627 -0
- package/icons/phosphor/regular/Phosphor.ttf +0 -0
- package/icons/phosphor/regular/Phosphor.woff +0 -0
- package/icons/phosphor/regular/Phosphor.woff2 +0 -0
- package/icons/phosphor/regular/style.css +4627 -0
- package/icons/phosphor/thin/Phosphor-Thin.ttf +0 -0
- package/icons/phosphor/thin/Phosphor-Thin.woff +0 -0
- package/icons/phosphor/thin/Phosphor-Thin.woff2 +0 -0
- package/icons/phosphor/thin/style.css +4627 -0
- package/js/components/code-snippet.js +639 -0
- package/js/components/collapsible.js +226 -0
- package/js/components/doc-search.js +936 -0
- package/js/components/draggable.js +725 -0
- package/js/components/dropdown.js +362 -0
- package/js/components/font-switcher.js +253 -0
- package/js/components/grid.js +279 -0
- package/js/components/image-box.js +372 -0
- package/js/components/modals.js +367 -0
- package/js/components/navbar.js +264 -0
- package/js/components/pagination.js +286 -0
- package/js/components/parallax.js +216 -0
- package/js/components/preloader.js +183 -0
- package/js/components/select.js +444 -0
- package/js/components/sidenav.js +303 -0
- package/js/components/tabs.js +303 -0
- package/js/components/theme-customizer.js +784 -0
- package/js/components/theme-switcher.js +183 -0
- package/js/components/toast.js +343 -0
- package/js/components/tooltips.js +306 -0
- package/js/index.js +52 -0
- package/js/utils/helpers.js +306 -0
- package/js/utils/lifecycle.js +135 -0
- package/js/vanduo.js +120 -0
- package/package.json +78 -0
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanduo Framework - Code Snippet Component
|
|
3
|
+
* Copyable code blocks with tabs, syntax highlighting, and HTML extraction
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function () {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Code Snippet Component
|
|
11
|
+
*/
|
|
12
|
+
const CodeSnippet = {
|
|
13
|
+
_snippetIdCounter: 0,
|
|
14
|
+
|
|
15
|
+
getSnippetInstanceId: function (snippet) {
|
|
16
|
+
if (snippet.dataset.codeSnippetId) {
|
|
17
|
+
return snippet.dataset.codeSnippetId;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const baseId = (snippet.id || '').trim();
|
|
21
|
+
if (baseId) {
|
|
22
|
+
snippet.dataset.codeSnippetId = `snippet-${baseId}`;
|
|
23
|
+
return snippet.dataset.codeSnippetId;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this._snippetIdCounter += 1;
|
|
27
|
+
snippet.dataset.codeSnippetId = `snippet-auto-${this._snippetIdCounter}`;
|
|
28
|
+
return snippet.dataset.codeSnippetId;
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
addListener: function (snippet, target, event, handler) {
|
|
32
|
+
if (!target) return;
|
|
33
|
+
target.addEventListener(event, handler);
|
|
34
|
+
if (!snippet._codeSnippetCleanup) {
|
|
35
|
+
snippet._codeSnippetCleanup = [];
|
|
36
|
+
}
|
|
37
|
+
snippet._codeSnippetCleanup.push(() => target.removeEventListener(event, handler));
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Initialize all code snippet components
|
|
42
|
+
*/
|
|
43
|
+
init: function () {
|
|
44
|
+
const snippets = document.querySelectorAll('.vd-code-snippet');
|
|
45
|
+
|
|
46
|
+
snippets.forEach(snippet => {
|
|
47
|
+
if (!snippet.dataset.initialized) {
|
|
48
|
+
this.initSnippet(snippet);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Initialize a single code snippet
|
|
55
|
+
* @param {HTMLElement} snippet - Code snippet container element
|
|
56
|
+
*/
|
|
57
|
+
initSnippet: function (snippet) {
|
|
58
|
+
snippet.dataset.initialized = 'true';
|
|
59
|
+
snippet._codeSnippetCleanup = [];
|
|
60
|
+
|
|
61
|
+
// Handle collapsible toggle
|
|
62
|
+
const toggle = snippet.querySelector('.vd-code-snippet-toggle');
|
|
63
|
+
const content = snippet.querySelector('.vd-code-snippet-content');
|
|
64
|
+
|
|
65
|
+
if (toggle && content) {
|
|
66
|
+
this.initCollapsible(snippet, toggle, content);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Handle tabs
|
|
70
|
+
const tabs = snippet.querySelectorAll('.vd-code-snippet-tab');
|
|
71
|
+
const panes = snippet.querySelectorAll('.vd-code-snippet-pane');
|
|
72
|
+
|
|
73
|
+
if (tabs.length > 0) {
|
|
74
|
+
this.initTabs(snippet, tabs, panes);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Handle copy button
|
|
78
|
+
const copyBtn = snippet.querySelector('.vd-code-snippet-copy');
|
|
79
|
+
if (copyBtn) {
|
|
80
|
+
this.initCopyButton(snippet, copyBtn);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Handle HTML extraction
|
|
84
|
+
const extractPanes = snippet.querySelectorAll('[data-extract]');
|
|
85
|
+
extractPanes.forEach(pane => {
|
|
86
|
+
this.extractHtml(pane);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Handle line numbers
|
|
90
|
+
const lineNumberPanes = snippet.querySelectorAll('.has-line-numbers');
|
|
91
|
+
lineNumberPanes.forEach(pane => {
|
|
92
|
+
this.addLineNumbers(pane);
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Initialize collapsible functionality
|
|
98
|
+
* @param {HTMLElement} snippet - Code snippet container
|
|
99
|
+
* @param {HTMLElement} toggle - Toggle button
|
|
100
|
+
* @param {HTMLElement} content - Collapsible content
|
|
101
|
+
*/
|
|
102
|
+
initCollapsible: function (snippet, toggle, content) {
|
|
103
|
+
// Set initial state
|
|
104
|
+
const isExpanded = snippet.dataset.expanded === 'true';
|
|
105
|
+
toggle.setAttribute('aria-expanded', isExpanded);
|
|
106
|
+
content.dataset.visible = isExpanded;
|
|
107
|
+
|
|
108
|
+
this.addListener(snippet, toggle, 'click', () => {
|
|
109
|
+
const expanded = snippet.dataset.expanded === 'true';
|
|
110
|
+
snippet.dataset.expanded = !expanded;
|
|
111
|
+
toggle.setAttribute('aria-expanded', !expanded);
|
|
112
|
+
content.dataset.visible = !expanded;
|
|
113
|
+
|
|
114
|
+
// Extract HTML on first expand if needed
|
|
115
|
+
if (!expanded) {
|
|
116
|
+
const extractPanes = content.querySelectorAll('[data-extract]:not([data-extracted])');
|
|
117
|
+
extractPanes.forEach(pane => {
|
|
118
|
+
this.extractHtml(pane);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Dispatch event
|
|
123
|
+
const event = new CustomEvent('codesnippet:toggle', {
|
|
124
|
+
bubbles: true,
|
|
125
|
+
detail: { snippet, expanded: !expanded }
|
|
126
|
+
});
|
|
127
|
+
snippet.dispatchEvent(event);
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Initialize tab functionality
|
|
133
|
+
* @param {HTMLElement} snippet - Code snippet container
|
|
134
|
+
* @param {NodeList} tabs - Tab buttons
|
|
135
|
+
* @param {NodeList} panes - Code panes
|
|
136
|
+
*/
|
|
137
|
+
initTabs: function (snippet, tabs, panes) {
|
|
138
|
+
const snippetId = this.getSnippetInstanceId(snippet);
|
|
139
|
+
|
|
140
|
+
// Set up ARIA attributes
|
|
141
|
+
const tabList = snippet.querySelector('.vd-code-snippet-tabs');
|
|
142
|
+
if (tabList) {
|
|
143
|
+
tabList.setAttribute('role', 'tablist');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
tabs.forEach((tab, index) => {
|
|
147
|
+
const lang = tab.dataset.lang;
|
|
148
|
+
const isActive = tab.classList.contains('is-active');
|
|
149
|
+
|
|
150
|
+
// Set ARIA attributes
|
|
151
|
+
tab.setAttribute('role', 'tab');
|
|
152
|
+
tab.setAttribute('aria-selected', isActive);
|
|
153
|
+
tab.setAttribute('tabindex', isActive ? '0' : '-1');
|
|
154
|
+
tab.id = tab.id || `code-tab-${snippetId}-${lang || 'tab'}-${index}`;
|
|
155
|
+
|
|
156
|
+
// Find corresponding pane
|
|
157
|
+
const pane = snippet.querySelector(`.vd-code-snippet-pane[data-lang="${lang}"]`);
|
|
158
|
+
if (pane) {
|
|
159
|
+
pane.id = pane.id || `code-pane-${snippetId}-${lang || 'pane'}-${index}`;
|
|
160
|
+
pane.setAttribute('role', 'tabpanel');
|
|
161
|
+
tab.setAttribute('aria-controls', pane.id);
|
|
162
|
+
pane.setAttribute('aria-labelledby', tab.id);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Click handler
|
|
166
|
+
this.addListener(snippet, tab, 'click', () => {
|
|
167
|
+
this.switchTab(snippet, tab, tabs, panes);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Keyboard navigation
|
|
171
|
+
this.addListener(snippet, tab, 'keydown', (e) => {
|
|
172
|
+
this.handleTabKeydown(e, snippet, tabs, panes);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Switch to a specific tab
|
|
179
|
+
* @param {HTMLElement} snippet - Code snippet container
|
|
180
|
+
* @param {HTMLElement} activeTab - Tab to activate
|
|
181
|
+
* @param {NodeList} tabs - All tab buttons
|
|
182
|
+
* @param {NodeList} panes - All code panes
|
|
183
|
+
*/
|
|
184
|
+
switchTab: function (snippet, activeTab, tabs, panes) {
|
|
185
|
+
const lang = activeTab.dataset.lang;
|
|
186
|
+
|
|
187
|
+
// Deactivate all tabs
|
|
188
|
+
tabs.forEach(tab => {
|
|
189
|
+
tab.classList.remove('is-active');
|
|
190
|
+
tab.setAttribute('aria-selected', 'false');
|
|
191
|
+
tab.setAttribute('tabindex', '-1');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Hide all panes
|
|
195
|
+
panes.forEach(pane => {
|
|
196
|
+
pane.classList.remove('is-active');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Activate selected tab
|
|
200
|
+
activeTab.classList.add('is-active');
|
|
201
|
+
activeTab.setAttribute('aria-selected', 'true');
|
|
202
|
+
activeTab.setAttribute('tabindex', '0');
|
|
203
|
+
|
|
204
|
+
// Show corresponding pane
|
|
205
|
+
const activePane = snippet.querySelector(`.vd-code-snippet-pane[data-lang="${lang}"]`);
|
|
206
|
+
if (activePane) {
|
|
207
|
+
activePane.classList.add('is-active');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Dispatch event
|
|
211
|
+
const event = new CustomEvent('codesnippet:tabchange', {
|
|
212
|
+
bubbles: true,
|
|
213
|
+
detail: { snippet, tab: activeTab, lang }
|
|
214
|
+
});
|
|
215
|
+
snippet.dispatchEvent(event);
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Handle keyboard navigation for tabs
|
|
220
|
+
* @param {KeyboardEvent} e - Keyboard event
|
|
221
|
+
* @param {HTMLElement} snippet - Code snippet container
|
|
222
|
+
* @param {NodeList} tabs - All tab buttons
|
|
223
|
+
* @param {NodeList} panes - All code panes
|
|
224
|
+
*/
|
|
225
|
+
handleTabKeydown: function (e, snippet, tabs, panes) {
|
|
226
|
+
const tabArray = Array.from(tabs);
|
|
227
|
+
const currentIndex = tabArray.indexOf(e.target);
|
|
228
|
+
let newIndex;
|
|
229
|
+
|
|
230
|
+
switch (e.key) {
|
|
231
|
+
case 'ArrowLeft':
|
|
232
|
+
e.preventDefault();
|
|
233
|
+
newIndex = currentIndex > 0 ? currentIndex - 1 : tabArray.length - 1;
|
|
234
|
+
break;
|
|
235
|
+
case 'ArrowRight':
|
|
236
|
+
e.preventDefault();
|
|
237
|
+
newIndex = currentIndex < tabArray.length - 1 ? currentIndex + 1 : 0;
|
|
238
|
+
break;
|
|
239
|
+
case 'Home':
|
|
240
|
+
e.preventDefault();
|
|
241
|
+
newIndex = 0;
|
|
242
|
+
break;
|
|
243
|
+
case 'End':
|
|
244
|
+
e.preventDefault();
|
|
245
|
+
newIndex = tabArray.length - 1;
|
|
246
|
+
break;
|
|
247
|
+
default:
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (newIndex !== currentIndex) {
|
|
252
|
+
tabArray[newIndex].focus();
|
|
253
|
+
this.switchTab(snippet, tabArray[newIndex], tabs, panes);
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Initialize copy button
|
|
259
|
+
* @param {HTMLElement} snippet - Code snippet container
|
|
260
|
+
* @param {HTMLElement} copyBtn - Copy button element
|
|
261
|
+
*/
|
|
262
|
+
initCopyButton: function (snippet, copyBtn) {
|
|
263
|
+
this.addListener(snippet, copyBtn, 'click', async () => {
|
|
264
|
+
await this.copyCode(snippet, copyBtn);
|
|
265
|
+
});
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Copy code to clipboard
|
|
270
|
+
* @param {HTMLElement} snippet - Code snippet container
|
|
271
|
+
* @param {HTMLElement} copyBtn - Copy button element
|
|
272
|
+
*/
|
|
273
|
+
copyCode: async function (snippet, copyBtn) {
|
|
274
|
+
const activePane = snippet.querySelector('.vd-code-snippet-pane.is-active') ||
|
|
275
|
+
snippet.querySelector('.vd-code-snippet-pane');
|
|
276
|
+
|
|
277
|
+
if (!activePane) {
|
|
278
|
+
console.warn('CodeSnippet: No code pane found');
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const codeElement = activePane.querySelector('code') || activePane;
|
|
283
|
+
const code = codeElement.textContent;
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
await navigator.clipboard.writeText(code);
|
|
287
|
+
this.showCopyFeedback(copyBtn, true);
|
|
288
|
+
} catch (_err) {
|
|
289
|
+
// Fallback for older browsers
|
|
290
|
+
const success = this.fallbackCopy(code);
|
|
291
|
+
this.showCopyFeedback(copyBtn, success);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Dispatch event
|
|
295
|
+
const event = new CustomEvent('codesnippet:copy', {
|
|
296
|
+
bubbles: true,
|
|
297
|
+
detail: { snippet, code, success: true }
|
|
298
|
+
});
|
|
299
|
+
snippet.dispatchEvent(event);
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Fallback copy method for older browsers
|
|
304
|
+
* @param {string} text - Text to copy
|
|
305
|
+
* @returns {boolean} Success status
|
|
306
|
+
*/
|
|
307
|
+
fallbackCopy: function (text) {
|
|
308
|
+
const textarea = document.createElement('textarea');
|
|
309
|
+
textarea.value = text;
|
|
310
|
+
textarea.style.position = 'fixed';
|
|
311
|
+
textarea.style.left = '-9999px';
|
|
312
|
+
textarea.style.top = '-9999px';
|
|
313
|
+
document.body.appendChild(textarea);
|
|
314
|
+
textarea.focus();
|
|
315
|
+
textarea.select();
|
|
316
|
+
|
|
317
|
+
let success = false;
|
|
318
|
+
try {
|
|
319
|
+
success = document.execCommand('copy');
|
|
320
|
+
} catch (err) {
|
|
321
|
+
console.warn('CodeSnippet: Fallback copy failed', err);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
document.body.removeChild(textarea);
|
|
325
|
+
return success;
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Show copy feedback
|
|
330
|
+
* @param {HTMLElement} copyBtn - Copy button element
|
|
331
|
+
* @param {boolean} success - Whether copy was successful
|
|
332
|
+
*/
|
|
333
|
+
showCopyFeedback: function (copyBtn, success) {
|
|
334
|
+
if (success) {
|
|
335
|
+
copyBtn.classList.add('is-copied');
|
|
336
|
+
|
|
337
|
+
// Announce to screen readers
|
|
338
|
+
const announcement = document.createElement('span');
|
|
339
|
+
announcement.setAttribute('role', 'status');
|
|
340
|
+
announcement.setAttribute('aria-live', 'polite');
|
|
341
|
+
announcement.className = 'sr-only';
|
|
342
|
+
announcement.textContent = 'Code copied to clipboard';
|
|
343
|
+
copyBtn.appendChild(announcement);
|
|
344
|
+
|
|
345
|
+
setTimeout(() => {
|
|
346
|
+
copyBtn.classList.remove('is-copied');
|
|
347
|
+
if (announcement.parentNode) {
|
|
348
|
+
announcement.parentNode.removeChild(announcement);
|
|
349
|
+
}
|
|
350
|
+
}, 2000);
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Extract HTML from a demo element
|
|
356
|
+
* @param {HTMLElement} pane - Code pane with data-extract attribute
|
|
357
|
+
*/
|
|
358
|
+
extractHtml: function (pane) {
|
|
359
|
+
const selector = pane.dataset.extract;
|
|
360
|
+
if (!selector) return;
|
|
361
|
+
|
|
362
|
+
const source = document.querySelector(selector);
|
|
363
|
+
if (!source) {
|
|
364
|
+
console.warn(`CodeSnippet: Source element not found: ${selector}`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Get inner HTML
|
|
369
|
+
let html = source.innerHTML;
|
|
370
|
+
|
|
371
|
+
// Format the HTML
|
|
372
|
+
html = this.formatHtml(html);
|
|
373
|
+
|
|
374
|
+
// Escape for display
|
|
375
|
+
html = this.escapeHtml(html);
|
|
376
|
+
|
|
377
|
+
// Apply syntax highlighting
|
|
378
|
+
html = this.highlightHtml(html);
|
|
379
|
+
|
|
380
|
+
// Set content
|
|
381
|
+
pane.innerHTML = '<code>' + html + '</code>';
|
|
382
|
+
pane.dataset.extracted = 'true';
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Format HTML with proper indentation
|
|
387
|
+
* @param {string} html - Raw HTML string
|
|
388
|
+
* @returns {string} Formatted HTML
|
|
389
|
+
*/
|
|
390
|
+
formatHtml: function (html) {
|
|
391
|
+
// Remove leading/trailing whitespace
|
|
392
|
+
html = html.trim();
|
|
393
|
+
|
|
394
|
+
// Simple formatting: normalize whitespace
|
|
395
|
+
// Split by tags, then rejoin with proper indentation
|
|
396
|
+
const lines = html.split('\n');
|
|
397
|
+
let indent = 0;
|
|
398
|
+
const indentSize = 2;
|
|
399
|
+
const formattedLines = [];
|
|
400
|
+
|
|
401
|
+
lines.forEach(line => {
|
|
402
|
+
line = line.trim();
|
|
403
|
+
if (!line) return;
|
|
404
|
+
|
|
405
|
+
// Check for closing tags at start
|
|
406
|
+
if (line.match(/^<\/\w/)) {
|
|
407
|
+
indent = Math.max(0, indent - indentSize);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
formattedLines.push(' '.repeat(indent) + line);
|
|
411
|
+
|
|
412
|
+
// Check for opening tags (not self-closing)
|
|
413
|
+
// Use short fixed-length regex + indexOf to prevent ReDoS
|
|
414
|
+
const hasOpenTag = /<[a-zA-Z]/.test(line);
|
|
415
|
+
const isSelfClosing = line.includes('/>');
|
|
416
|
+
if (hasOpenTag && !isSelfClosing) {
|
|
417
|
+
// Don't indent for void elements
|
|
418
|
+
if (!line.match(/<(br|hr|img|input|meta|link|area|base|col|embed|param|source|track|wbr)/i)) {
|
|
419
|
+
// Only indent if not also closing on same line
|
|
420
|
+
if (!line.match(/<\/\w+>$/)) {
|
|
421
|
+
indent += indentSize;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
return formattedLines.join('\n');
|
|
428
|
+
},
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Escape HTML entities for display
|
|
432
|
+
* @param {string} html - HTML string
|
|
433
|
+
* @returns {string} Escaped HTML
|
|
434
|
+
*/
|
|
435
|
+
escapeHtml: function (html) {
|
|
436
|
+
const div = document.createElement('div');
|
|
437
|
+
div.textContent = html;
|
|
438
|
+
return div.innerHTML;
|
|
439
|
+
},
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Apply syntax highlighting to HTML
|
|
443
|
+
* @param {string} html - Escaped HTML string
|
|
444
|
+
* @returns {string} HTML with syntax highlighting spans
|
|
445
|
+
*/
|
|
446
|
+
highlightHtml: function (html) {
|
|
447
|
+
// Highlight HTML tags
|
|
448
|
+
html = html.replace(/(<\/?)([\w-]+)/g, '$1<span class="code-tag">$2</span>');
|
|
449
|
+
|
|
450
|
+
// Highlight attributes
|
|
451
|
+
html = html.replace(/([\w-]+)(=)("|')/g, '<span class="code-attr">$1</span>$2$3');
|
|
452
|
+
|
|
453
|
+
// Highlight attribute values (strings)
|
|
454
|
+
html = html.replace(/("|')([^&]*)("|')/g, '$1<span class="code-string">$2</span>$3');
|
|
455
|
+
|
|
456
|
+
// Highlight comments
|
|
457
|
+
html = html.replace(/(<!--)(.*?)(-->)/g, '<span class="code-comment">$1$2$3</span>');
|
|
458
|
+
|
|
459
|
+
return html;
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Apply syntax highlighting to CSS
|
|
464
|
+
* @param {string} css - CSS string
|
|
465
|
+
* @returns {string} CSS with syntax highlighting spans
|
|
466
|
+
*/
|
|
467
|
+
highlightCss: function (css) {
|
|
468
|
+
// Highlight selectors — use non-backtracking bounded pattern
|
|
469
|
+
css = css.replace(/([.#]?[a-zA-Z][a-zA-Z0-9_-]{0,200})(\s*\{)/g, '<span class="code-selector">$1</span>$2');
|
|
470
|
+
|
|
471
|
+
// Highlight properties — use non-backtracking bounded pattern
|
|
472
|
+
css = css.replace(/([a-zA-Z][a-zA-Z0-9_-]{0,200})(\s*:)/g, '<span class="code-property">$1</span>$2');
|
|
473
|
+
|
|
474
|
+
// Highlight values
|
|
475
|
+
css = css.replace(/:\s*([^;{}]+)(;)/g, ': <span class="code-value">$1</span>$2');
|
|
476
|
+
|
|
477
|
+
// Highlight units
|
|
478
|
+
css = css.replace(/(\d+)(px|rem|em|%|vh|vw|deg|s|ms)/g, '<span class="code-number">$1</span><span class="code-unit">$2</span>');
|
|
479
|
+
|
|
480
|
+
// Highlight comments
|
|
481
|
+
css = css.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="code-comment">$1</span>');
|
|
482
|
+
|
|
483
|
+
return css;
|
|
484
|
+
},
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Apply syntax highlighting to JavaScript
|
|
488
|
+
* @param {string} js - JavaScript string
|
|
489
|
+
* @returns {string} JS with syntax highlighting spans
|
|
490
|
+
*/
|
|
491
|
+
highlightJs: function (js) {
|
|
492
|
+
// Highlight keywords
|
|
493
|
+
const keywords = ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'switch', 'case', 'break', 'continue', 'new', 'this', 'class', 'extends', 'import', 'export', 'default', 'async', 'await', 'try', 'catch', 'throw', 'typeof', 'instanceof'];
|
|
494
|
+
keywords.forEach(kw => {
|
|
495
|
+
const regex = new RegExp(`\\b(${kw})\\b`, 'g');
|
|
496
|
+
js = js.replace(regex, '<span class="code-keyword">$1</span>');
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// Highlight strings (limit to 10 000 chars to prevent polynomial backtracking)
|
|
500
|
+
js = js.replace(/('(?:[^'\\]|\\.){0,10000}'|"(?:[^"\\]|\\.){0,10000}"|`(?:[^`\\]|\\.){0,10000}`)/g, '<span class="code-string">$1</span>');
|
|
501
|
+
|
|
502
|
+
// Highlight numbers
|
|
503
|
+
js = js.replace(/\b(\d+\.?\d*)\b/g, '<span class="code-number">$1</span>');
|
|
504
|
+
|
|
505
|
+
// Highlight function calls
|
|
506
|
+
js = js.replace(/\b([\w]+)(\s*\()/g, '<span class="code-function">$1</span>$2');
|
|
507
|
+
|
|
508
|
+
// Highlight comments
|
|
509
|
+
js = js.replace(/(\/\/.*$)/gm, '<span class="code-comment">$1</span>');
|
|
510
|
+
js = js.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="code-comment">$1</span>');
|
|
511
|
+
|
|
512
|
+
return js;
|
|
513
|
+
},
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Add line numbers to a code pane
|
|
517
|
+
* @param {HTMLElement} pane - Code pane element
|
|
518
|
+
*/
|
|
519
|
+
addLineNumbers: function (pane) {
|
|
520
|
+
const code = pane.querySelector('code');
|
|
521
|
+
if (!code) return;
|
|
522
|
+
|
|
523
|
+
const lines = code.innerHTML.split('\n');
|
|
524
|
+
const lineCount = lines.length;
|
|
525
|
+
|
|
526
|
+
// Create line numbers container
|
|
527
|
+
const lineNumbers = document.createElement('div');
|
|
528
|
+
lineNumbers.className = 'vd-code-snippet-line-numbers';
|
|
529
|
+
lineNumbers.setAttribute('aria-hidden', 'true');
|
|
530
|
+
|
|
531
|
+
for (let i = 1; i <= lineCount; i++) {
|
|
532
|
+
const lineNum = document.createElement('span');
|
|
533
|
+
lineNum.textContent = i;
|
|
534
|
+
lineNumbers.appendChild(lineNum);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Wrap code content
|
|
538
|
+
const codeWrapper = document.createElement('div');
|
|
539
|
+
codeWrapper.className = 'vd-code-snippet-code';
|
|
540
|
+
codeWrapper.innerHTML = code.outerHTML;
|
|
541
|
+
|
|
542
|
+
// Replace code with new structure
|
|
543
|
+
code.parentNode.removeChild(code);
|
|
544
|
+
pane.appendChild(lineNumbers);
|
|
545
|
+
pane.appendChild(codeWrapper);
|
|
546
|
+
},
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Programmatically expand a code snippet
|
|
550
|
+
* @param {string|HTMLElement} snippet - Snippet selector or element
|
|
551
|
+
*/
|
|
552
|
+
expand: function (snippet) {
|
|
553
|
+
if (typeof snippet === 'string') {
|
|
554
|
+
snippet = document.querySelector(snippet);
|
|
555
|
+
}
|
|
556
|
+
if (!snippet) return;
|
|
557
|
+
|
|
558
|
+
snippet.dataset.expanded = 'true';
|
|
559
|
+
const toggle = snippet.querySelector('.vd-code-snippet-toggle');
|
|
560
|
+
const content = snippet.querySelector('.vd-code-snippet-content');
|
|
561
|
+
|
|
562
|
+
if (toggle) toggle.setAttribute('aria-expanded', 'true');
|
|
563
|
+
if (content) content.dataset.visible = 'true';
|
|
564
|
+
},
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Programmatically collapse a code snippet
|
|
568
|
+
* @param {string|HTMLElement} snippet - Snippet selector or element
|
|
569
|
+
*/
|
|
570
|
+
collapse: function (snippet) {
|
|
571
|
+
if (typeof snippet === 'string') {
|
|
572
|
+
snippet = document.querySelector(snippet);
|
|
573
|
+
}
|
|
574
|
+
if (!snippet) return;
|
|
575
|
+
|
|
576
|
+
snippet.dataset.expanded = 'false';
|
|
577
|
+
const toggle = snippet.querySelector('.vd-code-snippet-toggle');
|
|
578
|
+
const content = snippet.querySelector('.vd-code-snippet-content');
|
|
579
|
+
|
|
580
|
+
if (toggle) toggle.setAttribute('aria-expanded', 'false');
|
|
581
|
+
if (content) content.dataset.visible = 'false';
|
|
582
|
+
},
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Programmatically switch to a specific language tab
|
|
586
|
+
* @param {string|HTMLElement} snippet - Snippet selector or element
|
|
587
|
+
* @param {string} lang - Language to switch to (html, css, js)
|
|
588
|
+
*/
|
|
589
|
+
showLang: function (snippet, lang) {
|
|
590
|
+
if (typeof snippet === 'string') {
|
|
591
|
+
snippet = document.querySelector(snippet);
|
|
592
|
+
}
|
|
593
|
+
if (!snippet) return;
|
|
594
|
+
|
|
595
|
+
const tab = snippet.querySelector(`.vd-code-snippet-tab[data-lang="${lang}"]`);
|
|
596
|
+
const tabs = snippet.querySelectorAll('.vd-code-snippet-tab');
|
|
597
|
+
const panes = snippet.querySelectorAll('.vd-code-snippet-pane');
|
|
598
|
+
|
|
599
|
+
if (tab) {
|
|
600
|
+
this.switchTab(snippet, tab, tabs, panes);
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Destroy a code snippet instance and clean up listeners
|
|
606
|
+
* @param {string|HTMLElement} snippet - Snippet selector or element
|
|
607
|
+
*/
|
|
608
|
+
destroy: function (snippet) {
|
|
609
|
+
if (typeof snippet === 'string') {
|
|
610
|
+
snippet = document.querySelector(snippet);
|
|
611
|
+
}
|
|
612
|
+
if (!snippet) return;
|
|
613
|
+
|
|
614
|
+
if (snippet._codeSnippetCleanup) {
|
|
615
|
+
snippet._codeSnippetCleanup.forEach(fn => fn());
|
|
616
|
+
delete snippet._codeSnippetCleanup;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
delete snippet.dataset.initialized;
|
|
620
|
+
},
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Destroy all code snippet instances
|
|
624
|
+
*/
|
|
625
|
+
destroyAll: function () {
|
|
626
|
+
const snippets = document.querySelectorAll('.vd-code-snippet[data-initialized="true"]');
|
|
627
|
+
snippets.forEach(snippet => this.destroy(snippet));
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
// Register with Vanduo framework if available
|
|
632
|
+
if (typeof window.Vanduo !== 'undefined') {
|
|
633
|
+
window.Vanduo.register('codeSnippet', CodeSnippet);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Also expose globally for convenience
|
|
637
|
+
window.CodeSnippet = CodeSnippet;
|
|
638
|
+
|
|
639
|
+
})();
|