vanduo-framework 1.1.8
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 +205 -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,936 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanduo Framework - Search Component
|
|
3
|
+
* Client-side search functionality for content pages
|
|
4
|
+
*
|
|
5
|
+
* @example Basic usage (initialize with defaults)
|
|
6
|
+
* // HTML:
|
|
7
|
+
* // <div class="doc-search">
|
|
8
|
+
* // <input type="search" class="doc-search-input" placeholder="Search...">
|
|
9
|
+
* // <div class="vd-doc-search-results"></div>
|
|
10
|
+
* // </div>
|
|
11
|
+
*
|
|
12
|
+
* @example Custom configuration
|
|
13
|
+
* var search = Search.create({
|
|
14
|
+
* containerSelector: '.my-search',
|
|
15
|
+
* contentSelector: 'article[id]',
|
|
16
|
+
* titleSelector: 'h2, h3',
|
|
17
|
+
* maxResults: 5,
|
|
18
|
+
* onSelect: function(result) {
|
|
19
|
+
* console.log('Selected:', result.title);
|
|
20
|
+
* }
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* @example With custom data source
|
|
24
|
+
* var search = Search.create({
|
|
25
|
+
* containerSelector: '.my-search',
|
|
26
|
+
* data: [
|
|
27
|
+
* { id: 'item1', title: 'First Item', content: 'Description...', category: 'Category A' },
|
|
28
|
+
* { id: 'item2', title: 'Second Item', content: 'Description...', category: 'Category B' }
|
|
29
|
+
* ]
|
|
30
|
+
* });
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
(function() {
|
|
34
|
+
'use strict';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Default configuration
|
|
38
|
+
*/
|
|
39
|
+
var DEFAULTS = {
|
|
40
|
+
// Behavior
|
|
41
|
+
minQueryLength: 2,
|
|
42
|
+
maxResults: 10,
|
|
43
|
+
debounceMs: 150,
|
|
44
|
+
highlightTag: 'mark',
|
|
45
|
+
keyboardShortcut: true, // Enable Cmd/Ctrl+K shortcut
|
|
46
|
+
|
|
47
|
+
// Selectors (for DOM-based indexing)
|
|
48
|
+
containerSelector: '.vd-doc-search',
|
|
49
|
+
inputSelector: '.vd-doc-search-input',
|
|
50
|
+
resultsSelector: '.vd-doc-search-results',
|
|
51
|
+
contentSelector: '.doc-content section[id]',
|
|
52
|
+
titleSelector: '.demo-title, h2, h3',
|
|
53
|
+
navSelector: '.doc-nav-link',
|
|
54
|
+
sectionSelector: '.doc-nav-section',
|
|
55
|
+
|
|
56
|
+
// Content extraction
|
|
57
|
+
excludeFromContent: 'pre, code, script, style',
|
|
58
|
+
maxContentLength: 500,
|
|
59
|
+
|
|
60
|
+
// Custom data source (alternative to DOM indexing)
|
|
61
|
+
data: null,
|
|
62
|
+
|
|
63
|
+
// Category icons mapping
|
|
64
|
+
categoryIcons: {
|
|
65
|
+
'getting-started': 'ph-rocket-launch',
|
|
66
|
+
'core': 'ph-cube',
|
|
67
|
+
'components': 'ph-puzzle-piece',
|
|
68
|
+
'interactive': 'ph-cursor-click',
|
|
69
|
+
'data-display': 'ph-table',
|
|
70
|
+
'feedback': 'ph-bell',
|
|
71
|
+
'meta': 'ph-info',
|
|
72
|
+
'default': 'ph-file-text'
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// Callbacks
|
|
76
|
+
onSelect: null, // function(result) - called when result is selected
|
|
77
|
+
onSearch: null, // function(query, results) - called after search
|
|
78
|
+
onOpen: null, // function() - called when results open
|
|
79
|
+
onClose: null, // function() - called when results close
|
|
80
|
+
|
|
81
|
+
// Text customization
|
|
82
|
+
emptyTitle: 'No results found',
|
|
83
|
+
emptyText: 'Try different keywords or check spelling',
|
|
84
|
+
placeholder: 'Search...'
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Search Component Factory
|
|
89
|
+
* Creates a new search instance with the given configuration
|
|
90
|
+
*
|
|
91
|
+
* @param {Object} options - Configuration options
|
|
92
|
+
* @returns {Object} Search instance
|
|
93
|
+
*/
|
|
94
|
+
function createSearch(options) {
|
|
95
|
+
var config = Object.assign({}, DEFAULTS, options || {});
|
|
96
|
+
|
|
97
|
+
// Instance state
|
|
98
|
+
var state = {
|
|
99
|
+
initialized: false,
|
|
100
|
+
index: [],
|
|
101
|
+
results: [],
|
|
102
|
+
activeIndex: -1,
|
|
103
|
+
isOpen: false,
|
|
104
|
+
query: '',
|
|
105
|
+
container: null,
|
|
106
|
+
input: null,
|
|
107
|
+
resultsContainer: null,
|
|
108
|
+
debounceTimer: null,
|
|
109
|
+
boundHandlers: {}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Initialize the search component
|
|
114
|
+
* Idempotent — safe to call more than once on the same instance.
|
|
115
|
+
* Returns the instance on success, null if required DOM elements are missing.
|
|
116
|
+
*/
|
|
117
|
+
function init() {
|
|
118
|
+
if (state.initialized) {
|
|
119
|
+
return instance;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
state.container = document.querySelector(config.containerSelector);
|
|
123
|
+
if (!state.container) {
|
|
124
|
+
state.initialized = false;
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
state.input = state.container.querySelector(config.inputSelector);
|
|
129
|
+
state.resultsContainer = state.container.querySelector(config.resultsSelector);
|
|
130
|
+
|
|
131
|
+
if (!state.input || !state.resultsContainer) {
|
|
132
|
+
state.initialized = false;
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Set placeholder if configured
|
|
137
|
+
if (config.placeholder) {
|
|
138
|
+
state.input.setAttribute('placeholder', config.placeholder);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Build search index
|
|
142
|
+
buildIndex();
|
|
143
|
+
|
|
144
|
+
// Bind events
|
|
145
|
+
bindEvents();
|
|
146
|
+
|
|
147
|
+
// Set up ARIA attributes
|
|
148
|
+
setupAria();
|
|
149
|
+
|
|
150
|
+
state.initialized = true;
|
|
151
|
+
return instance;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Build search index from DOM or custom data
|
|
156
|
+
*/
|
|
157
|
+
function buildIndex() {
|
|
158
|
+
state.index = [];
|
|
159
|
+
|
|
160
|
+
// Use custom data if provided
|
|
161
|
+
if (config.data && Array.isArray(config.data)) {
|
|
162
|
+
config.data.forEach(function(item) {
|
|
163
|
+
state.index.push({
|
|
164
|
+
id: item.id || slugify(item.title),
|
|
165
|
+
title: item.title || '',
|
|
166
|
+
category: item.category || '',
|
|
167
|
+
categorySlug: slugify(item.category || ''),
|
|
168
|
+
content: item.content || '',
|
|
169
|
+
keywords: item.keywords || extractKeywordsFromText(item.title + ' ' + item.content),
|
|
170
|
+
url: item.url || '#' + (item.id || slugify(item.title)),
|
|
171
|
+
icon: item.icon || ''
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Build from DOM
|
|
178
|
+
var sections = document.querySelectorAll(config.contentSelector);
|
|
179
|
+
var categoryMap = buildCategoryMap();
|
|
180
|
+
|
|
181
|
+
sections.forEach(function(section) {
|
|
182
|
+
var id = section.id;
|
|
183
|
+
if (!id) return;
|
|
184
|
+
|
|
185
|
+
var titleEl = section.querySelector(config.titleSelector);
|
|
186
|
+
var title = titleEl ? titleEl.textContent.replace(/v[\d.]+/g, '').trim() : id;
|
|
187
|
+
var category = categoryMap[id] || 'Documentation';
|
|
188
|
+
var content = extractContent(section);
|
|
189
|
+
var keywords = extractKeywords(section, title);
|
|
190
|
+
var iconEl = titleEl ? titleEl.querySelector('i.ph') : null;
|
|
191
|
+
var icon = '';
|
|
192
|
+
if (iconEl && iconEl.classList) {
|
|
193
|
+
for (var ci = 0; ci < iconEl.classList.length; ci++) {
|
|
194
|
+
if (iconEl.classList[ci].indexOf('ph-') === 0) {
|
|
195
|
+
icon = iconEl.classList[ci];
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
state.index.push({
|
|
202
|
+
id: id,
|
|
203
|
+
title: title,
|
|
204
|
+
category: category,
|
|
205
|
+
categorySlug: slugify(category),
|
|
206
|
+
content: content,
|
|
207
|
+
keywords: keywords,
|
|
208
|
+
url: '#' + id,
|
|
209
|
+
icon: icon
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Build a map of section IDs to their categories
|
|
216
|
+
*/
|
|
217
|
+
function buildCategoryMap() {
|
|
218
|
+
var map = {};
|
|
219
|
+
var currentCategory = 'Documentation';
|
|
220
|
+
var navItems = document.querySelectorAll(config.navSelector + ', ' + config.sectionSelector);
|
|
221
|
+
|
|
222
|
+
navItems.forEach(function(item) {
|
|
223
|
+
if (item.classList.contains('doc-nav-section')) {
|
|
224
|
+
currentCategory = item.textContent.trim();
|
|
225
|
+
} else {
|
|
226
|
+
var href = item.getAttribute('href');
|
|
227
|
+
if (href && href.startsWith('#')) {
|
|
228
|
+
var id = href.substring(1);
|
|
229
|
+
map[id] = currentCategory;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return map;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Extract searchable content from a section
|
|
239
|
+
*/
|
|
240
|
+
function extractContent(section) {
|
|
241
|
+
var clone = section.cloneNode(true);
|
|
242
|
+
|
|
243
|
+
var toRemove = clone.querySelectorAll(config.excludeFromContent);
|
|
244
|
+
toRemove.forEach(function(el) {
|
|
245
|
+
el.remove();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
var text = clone.textContent || '';
|
|
249
|
+
text = text.replace(/\s+/g, ' ').trim();
|
|
250
|
+
|
|
251
|
+
return text.substring(0, config.maxContentLength);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Extract keywords from a section
|
|
256
|
+
*/
|
|
257
|
+
function extractKeywords(section, title) {
|
|
258
|
+
var keywords = [];
|
|
259
|
+
|
|
260
|
+
// Add title words
|
|
261
|
+
title.toLowerCase().split(/\s+/).forEach(function(word) {
|
|
262
|
+
if (word.length > 2) {
|
|
263
|
+
keywords.push(word);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Add class names from code examples
|
|
268
|
+
var codeBlocks = section.querySelectorAll('code');
|
|
269
|
+
codeBlocks.forEach(function(code) {
|
|
270
|
+
var text = code.textContent || '';
|
|
271
|
+
var classMatches = text.match(/\.([\w-]+)/g);
|
|
272
|
+
if (classMatches) {
|
|
273
|
+
classMatches.forEach(function(match) {
|
|
274
|
+
keywords.push(match.substring(1).toLowerCase());
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Add data attributes
|
|
280
|
+
var dataAttrs = section.querySelectorAll('[data-tooltip], [data-modal]');
|
|
281
|
+
dataAttrs.forEach(function(el) {
|
|
282
|
+
var attrs = el.getAttributeNames().filter(function(name) {
|
|
283
|
+
return name.startsWith('data-');
|
|
284
|
+
});
|
|
285
|
+
attrs.forEach(function(attr) {
|
|
286
|
+
keywords.push(attr.replace('data-', ''));
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
return Array.from(new Set(keywords));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Extract keywords from text string
|
|
295
|
+
*/
|
|
296
|
+
function extractKeywordsFromText(text) {
|
|
297
|
+
var words = text.toLowerCase().split(/\s+/);
|
|
298
|
+
return words.filter(function(word) {
|
|
299
|
+
return word.length > 2;
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Convert string to slug
|
|
305
|
+
*/
|
|
306
|
+
function slugify(str) {
|
|
307
|
+
return str.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Bind event listeners
|
|
312
|
+
*/
|
|
313
|
+
function bindEvents() {
|
|
314
|
+
// Store bound handlers for cleanup
|
|
315
|
+
state.boundHandlers.handleInput = function(e) {
|
|
316
|
+
handleInput(e);
|
|
317
|
+
};
|
|
318
|
+
state.boundHandlers.handleFocus = function() {
|
|
319
|
+
if (state.query.length >= config.minQueryLength) {
|
|
320
|
+
open();
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
state.boundHandlers.handleKeydown = function(e) {
|
|
324
|
+
handleKeydown(e);
|
|
325
|
+
};
|
|
326
|
+
state.boundHandlers.handleOutsideClick = function(e) {
|
|
327
|
+
if (!state.container.contains(e.target)) {
|
|
328
|
+
close();
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
state.boundHandlers.handleGlobalKeydown = function(e) {
|
|
332
|
+
if (config.keyboardShortcut && (e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
333
|
+
e.preventDefault();
|
|
334
|
+
state.input.focus();
|
|
335
|
+
state.input.select();
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
state.boundHandlers.handleResultClick = function(e) {
|
|
339
|
+
var result = e.target.closest('.vd-doc-search-result');
|
|
340
|
+
if (result) {
|
|
341
|
+
var index = parseInt(result.dataset.index, 10);
|
|
342
|
+
select(index);
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// Input events
|
|
347
|
+
state.input.addEventListener('input', state.boundHandlers.handleInput);
|
|
348
|
+
state.input.addEventListener('focus', state.boundHandlers.handleFocus);
|
|
349
|
+
state.input.addEventListener('keydown', state.boundHandlers.handleKeydown);
|
|
350
|
+
|
|
351
|
+
// Close on outside click
|
|
352
|
+
document.addEventListener('click', state.boundHandlers.handleOutsideClick);
|
|
353
|
+
|
|
354
|
+
// Global keyboard shortcut
|
|
355
|
+
document.addEventListener('keydown', state.boundHandlers.handleGlobalKeydown);
|
|
356
|
+
|
|
357
|
+
// Result click handling
|
|
358
|
+
state.resultsContainer.addEventListener('click', state.boundHandlers.handleResultClick);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Unbind event listeners
|
|
363
|
+
*/
|
|
364
|
+
function unbindEvents() {
|
|
365
|
+
if (state.input) {
|
|
366
|
+
state.input.removeEventListener('input', state.boundHandlers.handleInput);
|
|
367
|
+
state.input.removeEventListener('focus', state.boundHandlers.handleFocus);
|
|
368
|
+
state.input.removeEventListener('keydown', state.boundHandlers.handleKeydown);
|
|
369
|
+
}
|
|
370
|
+
document.removeEventListener('click', state.boundHandlers.handleOutsideClick);
|
|
371
|
+
document.removeEventListener('keydown', state.boundHandlers.handleGlobalKeydown);
|
|
372
|
+
if (state.resultsContainer) {
|
|
373
|
+
state.resultsContainer.removeEventListener('click', state.boundHandlers.handleResultClick);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Set up ARIA attributes
|
|
379
|
+
*/
|
|
380
|
+
function setupAria() {
|
|
381
|
+
var resultsId = state.resultsContainer.id || 'search-results-' + Math.random().toString(36).substr(2, 9);
|
|
382
|
+
state.resultsContainer.id = resultsId;
|
|
383
|
+
|
|
384
|
+
state.input.setAttribute('role', 'combobox');
|
|
385
|
+
state.input.setAttribute('aria-autocomplete', 'list');
|
|
386
|
+
state.input.setAttribute('aria-controls', resultsId);
|
|
387
|
+
state.input.setAttribute('aria-expanded', 'false');
|
|
388
|
+
|
|
389
|
+
state.resultsContainer.setAttribute('role', 'listbox');
|
|
390
|
+
state.resultsContainer.setAttribute('aria-label', 'Search results');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Handle input changes
|
|
395
|
+
*/
|
|
396
|
+
function handleInput(e) {
|
|
397
|
+
var query = e.target.value.trim();
|
|
398
|
+
|
|
399
|
+
if (state.debounceTimer) {
|
|
400
|
+
clearTimeout(state.debounceTimer);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
state.debounceTimer = setTimeout(function() {
|
|
404
|
+
state.query = query;
|
|
405
|
+
|
|
406
|
+
if (query.length < config.minQueryLength) {
|
|
407
|
+
close();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
state.results = search(query);
|
|
412
|
+
state.activeIndex = -1;
|
|
413
|
+
render();
|
|
414
|
+
open();
|
|
415
|
+
|
|
416
|
+
// Callback
|
|
417
|
+
if (typeof config.onSearch === 'function') {
|
|
418
|
+
config.onSearch(query, state.results);
|
|
419
|
+
}
|
|
420
|
+
}, config.debounceMs);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Handle keyboard navigation
|
|
425
|
+
*/
|
|
426
|
+
function handleKeydown(e) {
|
|
427
|
+
if (!state.isOpen) {
|
|
428
|
+
if (e.key === 'ArrowDown' && state.query.length >= config.minQueryLength) {
|
|
429
|
+
e.preventDefault();
|
|
430
|
+
state.results = search(state.query);
|
|
431
|
+
render();
|
|
432
|
+
open();
|
|
433
|
+
}
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
switch (e.key) {
|
|
438
|
+
case 'ArrowDown':
|
|
439
|
+
e.preventDefault();
|
|
440
|
+
navigate(1);
|
|
441
|
+
break;
|
|
442
|
+
|
|
443
|
+
case 'ArrowUp':
|
|
444
|
+
e.preventDefault();
|
|
445
|
+
navigate(-1);
|
|
446
|
+
break;
|
|
447
|
+
|
|
448
|
+
case 'Enter':
|
|
449
|
+
e.preventDefault();
|
|
450
|
+
if (state.activeIndex >= 0) {
|
|
451
|
+
select(state.activeIndex);
|
|
452
|
+
} else if (state.results.length > 0) {
|
|
453
|
+
select(0);
|
|
454
|
+
}
|
|
455
|
+
break;
|
|
456
|
+
|
|
457
|
+
case 'Escape':
|
|
458
|
+
e.preventDefault();
|
|
459
|
+
close();
|
|
460
|
+
break;
|
|
461
|
+
|
|
462
|
+
case 'Tab':
|
|
463
|
+
close();
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Perform search
|
|
470
|
+
*/
|
|
471
|
+
function search(query) {
|
|
472
|
+
var terms = query.toLowerCase().split(/\s+/).filter(function(t) {
|
|
473
|
+
return t.length > 0;
|
|
474
|
+
});
|
|
475
|
+
var scored = [];
|
|
476
|
+
|
|
477
|
+
state.index.forEach(function(entry) {
|
|
478
|
+
var score = 0;
|
|
479
|
+
var titleLower = entry.title.toLowerCase();
|
|
480
|
+
var categoryLower = entry.category.toLowerCase();
|
|
481
|
+
var contentLower = entry.content.toLowerCase();
|
|
482
|
+
|
|
483
|
+
terms.forEach(function(term) {
|
|
484
|
+
// Title match - highest priority
|
|
485
|
+
if (titleLower.includes(term)) {
|
|
486
|
+
score += 100;
|
|
487
|
+
if (titleLower === term) {
|
|
488
|
+
score += 50;
|
|
489
|
+
} else if (titleLower.startsWith(term)) {
|
|
490
|
+
score += 25;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Category match
|
|
495
|
+
if (categoryLower.includes(term)) {
|
|
496
|
+
score += 50;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Keyword match
|
|
500
|
+
var keywordMatch = entry.keywords.some(function(k) {
|
|
501
|
+
return k.includes(term);
|
|
502
|
+
});
|
|
503
|
+
if (keywordMatch) {
|
|
504
|
+
score += 30;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Content match
|
|
508
|
+
if (contentLower.includes(term)) {
|
|
509
|
+
score += 10;
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
if (score > 0) {
|
|
514
|
+
scored.push({
|
|
515
|
+
id: entry.id,
|
|
516
|
+
title: entry.title,
|
|
517
|
+
category: entry.category,
|
|
518
|
+
categorySlug: entry.categorySlug,
|
|
519
|
+
content: entry.content,
|
|
520
|
+
url: entry.url,
|
|
521
|
+
icon: entry.icon,
|
|
522
|
+
score: score
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
scored.sort(function(a, b) {
|
|
528
|
+
return b.score - a.score;
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
return scored.slice(0, config.maxResults);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Render search results
|
|
536
|
+
*/
|
|
537
|
+
function render() {
|
|
538
|
+
if (state.results.length === 0) {
|
|
539
|
+
state.resultsContainer.innerHTML = renderEmpty();
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
var html = '<ul class="vd-doc-search-results-list" role="listbox">';
|
|
544
|
+
|
|
545
|
+
state.results.forEach(function(result, index) {
|
|
546
|
+
var isActive = index === state.activeIndex;
|
|
547
|
+
var icon = result.icon || getCategoryIcon(result.categorySlug);
|
|
548
|
+
var excerpt = getExcerpt(result.content, state.query);
|
|
549
|
+
|
|
550
|
+
html += '<li class="vd-doc-search-result' + (isActive ? ' is-active' : '') + '"' +
|
|
551
|
+
' role="option"' +
|
|
552
|
+
' id="vd-doc-search-result-' + index + '"' +
|
|
553
|
+
' data-index="' + index + '"' +
|
|
554
|
+
' data-category="' + escapeHtml(result.categorySlug) + '"' +
|
|
555
|
+
' aria-selected="' + isActive + '"' +
|
|
556
|
+
'>' +
|
|
557
|
+
'<div class="vd-doc-search-result-icon">' +
|
|
558
|
+
'<i class="ph ' + escapeHtml(icon) + '"></i>' +
|
|
559
|
+
'</div>' +
|
|
560
|
+
'<div class="vd-doc-search-result-content">' +
|
|
561
|
+
'<div class="vd-doc-search-result-title">' + highlight(result.title, state.query) + '</div>' +
|
|
562
|
+
'<div class="vd-doc-search-result-category">' + escapeHtml(result.category) + '</div>' +
|
|
563
|
+
'<div class="vd-doc-search-result-excerpt">' + highlight(excerpt, state.query) + '</div>' +
|
|
564
|
+
'</div>' +
|
|
565
|
+
'</li>';
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
html += '</ul>';
|
|
569
|
+
html += renderFooter();
|
|
570
|
+
|
|
571
|
+
state.resultsContainer.innerHTML = html;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Render empty state
|
|
576
|
+
*/
|
|
577
|
+
function renderEmpty() {
|
|
578
|
+
return '<div class="vd-doc-search-empty">' +
|
|
579
|
+
'<div class="vd-doc-search-empty-icon"><i class="ph ph-magnifying-glass"></i></div>' +
|
|
580
|
+
'<div class="vd-doc-search-empty-title">' + escapeHtml(config.emptyTitle) + '</div>' +
|
|
581
|
+
'<div class="vd-doc-search-empty-text">' + escapeHtml(config.emptyText) + '</div>' +
|
|
582
|
+
'</div>';
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Render footer with keyboard hints
|
|
587
|
+
*/
|
|
588
|
+
function renderFooter() {
|
|
589
|
+
return '<div class="vd-doc-search-footer">' +
|
|
590
|
+
'<span class="vd-doc-search-footer-item"><kbd>↑</kbd><kbd>↓</kbd> to navigate</span>' +
|
|
591
|
+
'<span class="vd-doc-search-footer-item"><kbd>↵</kbd> to select</span>' +
|
|
592
|
+
'<span class="vd-doc-search-footer-item"><kbd>esc</kbd> to close</span>' +
|
|
593
|
+
'</div>';
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Get icon for category
|
|
598
|
+
*/
|
|
599
|
+
function getCategoryIcon(categorySlug) {
|
|
600
|
+
return config.categoryIcons[categorySlug] || config.categoryIcons['default'] || 'ph-file-text';
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Get excerpt from content
|
|
605
|
+
*/
|
|
606
|
+
function getExcerpt(content, query) {
|
|
607
|
+
var terms = query.toLowerCase().split(/\s+/);
|
|
608
|
+
var contentLower = content.toLowerCase();
|
|
609
|
+
var excerptLength = 100;
|
|
610
|
+
|
|
611
|
+
var matchPos = -1;
|
|
612
|
+
for (var i = 0; i < terms.length; i++) {
|
|
613
|
+
var pos = contentLower.indexOf(terms[i]);
|
|
614
|
+
if (pos !== -1 && (matchPos === -1 || pos < matchPos)) {
|
|
615
|
+
matchPos = pos;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (matchPos === -1) {
|
|
620
|
+
return content.substring(0, excerptLength) + '...';
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
var start = Math.max(0, matchPos - 30);
|
|
624
|
+
var end = Math.min(content.length, matchPos + excerptLength);
|
|
625
|
+
var excerpt = content.substring(start, end);
|
|
626
|
+
|
|
627
|
+
if (start > 0) {
|
|
628
|
+
excerpt = '...' + excerpt;
|
|
629
|
+
}
|
|
630
|
+
if (end < content.length) {
|
|
631
|
+
excerpt = excerpt + '...';
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return excerpt;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Highlight matched terms in text
|
|
639
|
+
*/
|
|
640
|
+
function highlight(text, query) {
|
|
641
|
+
if (!query) return escapeHtml(text);
|
|
642
|
+
|
|
643
|
+
var terms = query.toLowerCase().split(/\s+/).filter(function(t) {
|
|
644
|
+
return t.length > 0;
|
|
645
|
+
});
|
|
646
|
+
var escaped = escapeHtml(text);
|
|
647
|
+
|
|
648
|
+
terms.forEach(function(term) {
|
|
649
|
+
// Skip overly long terms to prevent ReDoS
|
|
650
|
+
if (term.length > 50) return;
|
|
651
|
+
var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
|
|
652
|
+
escaped = escaped.replace(regex, '<' + config.highlightTag + '>$1</' + config.highlightTag + '>');
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
return escaped;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Escape HTML entities
|
|
660
|
+
*/
|
|
661
|
+
function escapeHtml(text) {
|
|
662
|
+
var div = document.createElement('div');
|
|
663
|
+
div.textContent = text;
|
|
664
|
+
return div.innerHTML;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Navigate results with keyboard
|
|
669
|
+
*/
|
|
670
|
+
function navigate(direction) {
|
|
671
|
+
var newIndex = state.activeIndex + direction;
|
|
672
|
+
|
|
673
|
+
if (newIndex < 0) {
|
|
674
|
+
newIndex = state.results.length - 1;
|
|
675
|
+
} else if (newIndex >= state.results.length) {
|
|
676
|
+
newIndex = 0;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
setActiveIndex(newIndex);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Set active result index
|
|
684
|
+
*/
|
|
685
|
+
function setActiveIndex(index) {
|
|
686
|
+
var prevActive = state.resultsContainer.querySelector('.vd-doc-search-result.is-active');
|
|
687
|
+
if (prevActive) {
|
|
688
|
+
prevActive.classList.remove('is-active');
|
|
689
|
+
prevActive.setAttribute('aria-selected', 'false');
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
state.activeIndex = index;
|
|
693
|
+
|
|
694
|
+
var newActive = state.resultsContainer.querySelector('[data-index="' + index + '"]');
|
|
695
|
+
if (newActive) {
|
|
696
|
+
newActive.classList.add('is-active');
|
|
697
|
+
newActive.setAttribute('aria-selected', 'true');
|
|
698
|
+
state.input.setAttribute('aria-activedescendant', 'vd-doc-search-result-' + index);
|
|
699
|
+
newActive.scrollIntoView({ block: 'nearest' });
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Select a result
|
|
705
|
+
*/
|
|
706
|
+
function select(index) {
|
|
707
|
+
var result = state.results[index];
|
|
708
|
+
if (!result) return;
|
|
709
|
+
|
|
710
|
+
// Close search
|
|
711
|
+
close();
|
|
712
|
+
state.input.value = '';
|
|
713
|
+
state.query = '';
|
|
714
|
+
|
|
715
|
+
// Custom callback
|
|
716
|
+
if (typeof config.onSelect === 'function') {
|
|
717
|
+
config.onSelect(result);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Default behavior: navigate to section
|
|
722
|
+
var section = document.querySelector(result.url);
|
|
723
|
+
if (section) {
|
|
724
|
+
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
725
|
+
window.history.pushState(null, '', result.url);
|
|
726
|
+
updateSidebarActive(result.id);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Update sidebar navigation active state
|
|
732
|
+
*/
|
|
733
|
+
function updateSidebarActive(sectionId) {
|
|
734
|
+
var navLinks = document.querySelectorAll(config.navSelector);
|
|
735
|
+
navLinks.forEach(function(link) {
|
|
736
|
+
link.classList.remove('active');
|
|
737
|
+
if (link.getAttribute('href') === '#' + sectionId) {
|
|
738
|
+
link.classList.add('active');
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Open results dropdown
|
|
745
|
+
*/
|
|
746
|
+
function open() {
|
|
747
|
+
if (state.isOpen) return;
|
|
748
|
+
|
|
749
|
+
state.isOpen = true;
|
|
750
|
+
state.resultsContainer.classList.add('is-open');
|
|
751
|
+
state.input.setAttribute('aria-expanded', 'true');
|
|
752
|
+
|
|
753
|
+
if (typeof config.onOpen === 'function') {
|
|
754
|
+
config.onOpen();
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Close results dropdown
|
|
760
|
+
*/
|
|
761
|
+
function close() {
|
|
762
|
+
if (!state.isOpen) return;
|
|
763
|
+
|
|
764
|
+
state.isOpen = false;
|
|
765
|
+
state.activeIndex = -1;
|
|
766
|
+
state.resultsContainer.classList.remove('is-open');
|
|
767
|
+
state.input.setAttribute('aria-expanded', 'false');
|
|
768
|
+
state.input.removeAttribute('aria-activedescendant');
|
|
769
|
+
|
|
770
|
+
if (typeof config.onClose === 'function') {
|
|
771
|
+
config.onClose();
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Destroy the component
|
|
777
|
+
*/
|
|
778
|
+
function destroy() {
|
|
779
|
+
unbindEvents();
|
|
780
|
+
|
|
781
|
+
state.initialized = false;
|
|
782
|
+
state.index = [];
|
|
783
|
+
state.results = [];
|
|
784
|
+
state.isOpen = false;
|
|
785
|
+
state.query = '';
|
|
786
|
+
|
|
787
|
+
if (state.debounceTimer) {
|
|
788
|
+
clearTimeout(state.debounceTimer);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (state.resultsContainer) {
|
|
792
|
+
state.resultsContainer.innerHTML = '';
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Rebuild the search index
|
|
798
|
+
*/
|
|
799
|
+
function rebuild() {
|
|
800
|
+
buildIndex();
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Update configuration
|
|
805
|
+
*/
|
|
806
|
+
function setConfig(newConfig) {
|
|
807
|
+
Object.assign(config, newConfig);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Get current configuration
|
|
812
|
+
*/
|
|
813
|
+
function getConfig() {
|
|
814
|
+
return Object.assign({}, config);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Get search index
|
|
819
|
+
*/
|
|
820
|
+
function getIndex() {
|
|
821
|
+
return state.index.slice();
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Public instance API
|
|
825
|
+
var instance = {
|
|
826
|
+
init: init,
|
|
827
|
+
destroy: destroy,
|
|
828
|
+
rebuild: rebuild,
|
|
829
|
+
search: search,
|
|
830
|
+
open: open,
|
|
831
|
+
close: close,
|
|
832
|
+
setConfig: setConfig,
|
|
833
|
+
getConfig: getConfig,
|
|
834
|
+
getIndex: getIndex
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
return instance;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Search Component (singleton for backward compatibility)
|
|
842
|
+
*/
|
|
843
|
+
var Search = {
|
|
844
|
+
// Factory method — creates and auto-initializes a new independent instance.
|
|
845
|
+
// Always returns the instance so callers retain a reference even if the
|
|
846
|
+
// DOM container is not yet available (they can retry init() later).
|
|
847
|
+
create: function(options) {
|
|
848
|
+
var instance = createSearch(options);
|
|
849
|
+
if (instance) {
|
|
850
|
+
instance.init();
|
|
851
|
+
}
|
|
852
|
+
return instance || null;
|
|
853
|
+
},
|
|
854
|
+
|
|
855
|
+
// Default instance
|
|
856
|
+
_instance: null,
|
|
857
|
+
|
|
858
|
+
// Configuration (for default instance)
|
|
859
|
+
config: Object.assign({}, DEFAULTS),
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Initialize the default search instance
|
|
863
|
+
*/
|
|
864
|
+
init: function(options) {
|
|
865
|
+
if (this._instance) {
|
|
866
|
+
this._instance.destroy();
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (options) {
|
|
870
|
+
Object.assign(this.config, options);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
this._instance = createSearch(this.config);
|
|
874
|
+
return this._instance ? this._instance.init() : null;
|
|
875
|
+
},
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Destroy the default instance
|
|
879
|
+
*/
|
|
880
|
+
destroy: function() {
|
|
881
|
+
if (this._instance) {
|
|
882
|
+
this._instance.destroy();
|
|
883
|
+
this._instance = null;
|
|
884
|
+
}
|
|
885
|
+
},
|
|
886
|
+
|
|
887
|
+
destroyAll: function() {
|
|
888
|
+
this.destroy();
|
|
889
|
+
},
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Rebuild the default instance index
|
|
893
|
+
*/
|
|
894
|
+
rebuild: function() {
|
|
895
|
+
if (this._instance) {
|
|
896
|
+
this._instance.rebuild();
|
|
897
|
+
}
|
|
898
|
+
},
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Search using the default instance
|
|
902
|
+
*/
|
|
903
|
+
search: function(query) {
|
|
904
|
+
return this._instance ? this._instance.search(query) : [];
|
|
905
|
+
},
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Open the default instance
|
|
909
|
+
*/
|
|
910
|
+
open: function() {
|
|
911
|
+
if (this._instance) {
|
|
912
|
+
this._instance.open();
|
|
913
|
+
}
|
|
914
|
+
},
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Close the default instance
|
|
918
|
+
*/
|
|
919
|
+
close: function() {
|
|
920
|
+
if (this._instance) {
|
|
921
|
+
this._instance.close();
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
// Register with Vanduo framework if available
|
|
927
|
+
if (typeof window.Vanduo !== 'undefined') {
|
|
928
|
+
window.Vanduo.register('docSearch', Search);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Expose globally (both names for compatibility)
|
|
932
|
+
window.Search = Search;
|
|
933
|
+
window.DocSearch = Search; // Backward compatibility
|
|
934
|
+
window.VanduoDocSearch = Search; // New name compatibility
|
|
935
|
+
|
|
936
|
+
})();
|