local-deep-research 0.1.26__py3-none-any.whl → 0.2.2__py3-none-any.whl
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.
- local_deep_research/__init__.py +23 -22
- local_deep_research/__main__.py +16 -0
- local_deep_research/advanced_search_system/__init__.py +7 -0
- local_deep_research/advanced_search_system/filters/__init__.py +8 -0
- local_deep_research/advanced_search_system/filters/base_filter.py +38 -0
- local_deep_research/advanced_search_system/filters/cross_engine_filter.py +200 -0
- local_deep_research/advanced_search_system/findings/base_findings.py +81 -0
- local_deep_research/advanced_search_system/findings/repository.py +452 -0
- local_deep_research/advanced_search_system/knowledge/__init__.py +1 -0
- local_deep_research/advanced_search_system/knowledge/base_knowledge.py +151 -0
- local_deep_research/advanced_search_system/knowledge/standard_knowledge.py +159 -0
- local_deep_research/advanced_search_system/questions/__init__.py +1 -0
- local_deep_research/advanced_search_system/questions/base_question.py +64 -0
- local_deep_research/advanced_search_system/questions/decomposition_question.py +445 -0
- local_deep_research/advanced_search_system/questions/standard_question.py +119 -0
- local_deep_research/advanced_search_system/repositories/__init__.py +7 -0
- local_deep_research/advanced_search_system/strategies/__init__.py +1 -0
- local_deep_research/advanced_search_system/strategies/base_strategy.py +118 -0
- local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py +450 -0
- local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py +312 -0
- local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py +270 -0
- local_deep_research/advanced_search_system/strategies/standard_strategy.py +300 -0
- local_deep_research/advanced_search_system/tools/__init__.py +1 -0
- local_deep_research/advanced_search_system/tools/base_tool.py +100 -0
- local_deep_research/advanced_search_system/tools/knowledge_tools/__init__.py +1 -0
- local_deep_research/advanced_search_system/tools/question_tools/__init__.py +1 -0
- local_deep_research/advanced_search_system/tools/search_tools/__init__.py +1 -0
- local_deep_research/api/__init__.py +5 -5
- local_deep_research/api/research_functions.py +154 -160
- local_deep_research/app.py +8 -0
- local_deep_research/citation_handler.py +25 -16
- local_deep_research/{config.py → config/config_files.py} +102 -110
- local_deep_research/config/llm_config.py +472 -0
- local_deep_research/config/search_config.py +77 -0
- local_deep_research/defaults/__init__.py +10 -5
- local_deep_research/defaults/main.toml +2 -2
- local_deep_research/defaults/search_engines.toml +60 -34
- local_deep_research/main.py +121 -19
- local_deep_research/migrate_db.py +147 -0
- local_deep_research/report_generator.py +87 -45
- local_deep_research/search_system.py +153 -283
- local_deep_research/setup_data_dir.py +35 -0
- local_deep_research/test_migration.py +178 -0
- local_deep_research/utilities/__init__.py +0 -0
- local_deep_research/utilities/db_utils.py +49 -0
- local_deep_research/{utilties → utilities}/enums.py +2 -2
- local_deep_research/{utilties → utilities}/llm_utils.py +63 -29
- local_deep_research/utilities/search_utilities.py +242 -0
- local_deep_research/{utilties → utilities}/setup_utils.py +4 -2
- local_deep_research/web/__init__.py +0 -1
- local_deep_research/web/app.py +86 -1709
- local_deep_research/web/app_factory.py +289 -0
- local_deep_research/web/database/README.md +70 -0
- local_deep_research/web/database/migrate_to_ldr_db.py +289 -0
- local_deep_research/web/database/migrations.py +447 -0
- local_deep_research/web/database/models.py +117 -0
- local_deep_research/web/database/schema_upgrade.py +107 -0
- local_deep_research/web/models/database.py +294 -0
- local_deep_research/web/models/settings.py +94 -0
- local_deep_research/web/routes/api_routes.py +559 -0
- local_deep_research/web/routes/history_routes.py +354 -0
- local_deep_research/web/routes/research_routes.py +715 -0
- local_deep_research/web/routes/settings_routes.py +1583 -0
- local_deep_research/web/services/research_service.py +947 -0
- local_deep_research/web/services/resource_service.py +149 -0
- local_deep_research/web/services/settings_manager.py +669 -0
- local_deep_research/web/services/settings_service.py +187 -0
- local_deep_research/web/services/socket_service.py +210 -0
- local_deep_research/web/static/css/custom_dropdown.css +277 -0
- local_deep_research/web/static/css/settings.css +1223 -0
- local_deep_research/web/static/css/styles.css +525 -48
- local_deep_research/web/static/js/components/custom_dropdown.js +428 -0
- local_deep_research/web/static/js/components/detail.js +348 -0
- local_deep_research/web/static/js/components/fallback/formatting.js +122 -0
- local_deep_research/web/static/js/components/fallback/ui.js +215 -0
- local_deep_research/web/static/js/components/history.js +487 -0
- local_deep_research/web/static/js/components/logpanel.js +949 -0
- local_deep_research/web/static/js/components/progress.js +1107 -0
- local_deep_research/web/static/js/components/research.js +1865 -0
- local_deep_research/web/static/js/components/results.js +766 -0
- local_deep_research/web/static/js/components/settings.js +3981 -0
- local_deep_research/web/static/js/components/settings_sync.js +106 -0
- local_deep_research/web/static/js/main.js +226 -0
- local_deep_research/web/static/js/services/api.js +253 -0
- local_deep_research/web/static/js/services/audio.js +31 -0
- local_deep_research/web/static/js/services/formatting.js +119 -0
- local_deep_research/web/static/js/services/pdf.js +622 -0
- local_deep_research/web/static/js/services/socket.js +882 -0
- local_deep_research/web/static/js/services/ui.js +546 -0
- local_deep_research/web/templates/base.html +72 -0
- local_deep_research/web/templates/components/custom_dropdown.html +47 -0
- local_deep_research/web/templates/components/log_panel.html +32 -0
- local_deep_research/web/templates/components/mobile_nav.html +22 -0
- local_deep_research/web/templates/components/settings_form.html +299 -0
- local_deep_research/web/templates/components/sidebar.html +21 -0
- local_deep_research/web/templates/pages/details.html +73 -0
- local_deep_research/web/templates/pages/history.html +51 -0
- local_deep_research/web/templates/pages/progress.html +57 -0
- local_deep_research/web/templates/pages/research.html +139 -0
- local_deep_research/web/templates/pages/results.html +59 -0
- local_deep_research/web/templates/settings_dashboard.html +78 -192
- local_deep_research/web/utils/__init__.py +0 -0
- local_deep_research/web/utils/formatters.py +76 -0
- local_deep_research/web_search_engines/engines/full_search.py +18 -16
- local_deep_research/web_search_engines/engines/meta_search_engine.py +182 -131
- local_deep_research/web_search_engines/engines/search_engine_arxiv.py +224 -139
- local_deep_research/web_search_engines/engines/search_engine_brave.py +88 -71
- local_deep_research/web_search_engines/engines/search_engine_ddg.py +48 -39
- local_deep_research/web_search_engines/engines/search_engine_github.py +415 -204
- local_deep_research/web_search_engines/engines/search_engine_google_pse.py +123 -90
- local_deep_research/web_search_engines/engines/search_engine_guardian.py +210 -157
- local_deep_research/web_search_engines/engines/search_engine_local.py +532 -369
- local_deep_research/web_search_engines/engines/search_engine_local_all.py +42 -36
- local_deep_research/web_search_engines/engines/search_engine_pubmed.py +358 -266
- local_deep_research/web_search_engines/engines/search_engine_searxng.py +212 -160
- local_deep_research/web_search_engines/engines/search_engine_semantic_scholar.py +213 -170
- local_deep_research/web_search_engines/engines/search_engine_serpapi.py +84 -68
- local_deep_research/web_search_engines/engines/search_engine_wayback.py +186 -154
- local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +115 -77
- local_deep_research/web_search_engines/search_engine_base.py +174 -99
- local_deep_research/web_search_engines/search_engine_factory.py +192 -102
- local_deep_research/web_search_engines/search_engines_config.py +22 -15
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/METADATA +177 -97
- local_deep_research-0.2.2.dist-info/RECORD +135 -0
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/WHEEL +1 -2
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/entry_points.txt +3 -0
- local_deep_research/defaults/llm_config.py +0 -338
- local_deep_research/utilties/search_utilities.py +0 -114
- local_deep_research/web/static/js/app.js +0 -3763
- local_deep_research/web/templates/api_keys_config.html +0 -82
- local_deep_research/web/templates/collections_config.html +0 -90
- local_deep_research/web/templates/index.html +0 -348
- local_deep_research/web/templates/llm_config.html +0 -120
- local_deep_research/web/templates/main_config.html +0 -89
- local_deep_research/web/templates/search_engines_config.html +0 -154
- local_deep_research/web/templates/settings.html +0 -519
- local_deep_research-0.1.26.dist-info/RECORD +0 -61
- local_deep_research-0.1.26.dist-info/top_level.txt +0 -1
- /local_deep_research/{utilties → config}/__init__.py +0 -0
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,428 @@
|
|
1
|
+
/**
|
2
|
+
* Custom Dropdown Component
|
3
|
+
*
|
4
|
+
* This module provides functionality for custom dropdown menus with filtering and keyboard navigation.
|
5
|
+
* It can be used across the application for consistent dropdown behavior.
|
6
|
+
*/
|
7
|
+
(function() {
|
8
|
+
'use strict';
|
9
|
+
|
10
|
+
// Make the setupCustomDropdown function available globally
|
11
|
+
window.setupCustomDropdown = setupCustomDropdown;
|
12
|
+
|
13
|
+
// Also export the updateDropdownOptions function
|
14
|
+
window.updateDropdownOptions = updateDropdownOptions;
|
15
|
+
|
16
|
+
// Keep a registry of inputs and their associated options functions
|
17
|
+
const dropdownRegistry = {};
|
18
|
+
|
19
|
+
/**
|
20
|
+
* Update the options for an existing dropdown without destroying it
|
21
|
+
* @param {HTMLElement} input - The input element
|
22
|
+
* @param {Array} newOptions - New options array to use [{value: string, label: string}]
|
23
|
+
*/
|
24
|
+
function updateDropdownOptions(input, newOptions) {
|
25
|
+
if (!input || !input.id) {
|
26
|
+
console.warn('Cannot update dropdown: Invalid input element');
|
27
|
+
return;
|
28
|
+
}
|
29
|
+
|
30
|
+
// Check if dropdown is registered
|
31
|
+
if (!dropdownRegistry[input.id]) {
|
32
|
+
console.warn(`Dropdown ${input.id} not found in registry, unable to update options`);
|
33
|
+
return;
|
34
|
+
}
|
35
|
+
|
36
|
+
const dropdownInfo = dropdownRegistry[input.id];
|
37
|
+
|
38
|
+
// Update the options getter function to return new options
|
39
|
+
dropdownInfo.getOptions = () => newOptions;
|
40
|
+
|
41
|
+
// If dropdown is currently open, update its content
|
42
|
+
const dropdownList = document.getElementById(`${dropdownInfo.dropdownId}-list`);
|
43
|
+
if (dropdownList && window.getComputedStyle(dropdownList).display !== 'none') {
|
44
|
+
console.log(`Dropdown ${input.id} is open, updating content in place`);
|
45
|
+
|
46
|
+
// Save scroll position
|
47
|
+
const scrollPos = dropdownList.scrollTop;
|
48
|
+
|
49
|
+
// Update dropdown content
|
50
|
+
const filteredData = dropdownInfo.getOptions();
|
51
|
+
dropdownList.innerHTML = '';
|
52
|
+
|
53
|
+
if (filteredData.length === 0) {
|
54
|
+
dropdownList.innerHTML = `<div class="custom-dropdown-no-results">${dropdownInfo.noResultsText}</div>`;
|
55
|
+
return;
|
56
|
+
}
|
57
|
+
|
58
|
+
filteredData.forEach((item, index) => {
|
59
|
+
const div = document.createElement('div');
|
60
|
+
div.className = 'custom-dropdown-item';
|
61
|
+
div.innerHTML = item.label;
|
62
|
+
div.setAttribute('data-value', item.value);
|
63
|
+
div.addEventListener('click', () => {
|
64
|
+
// Set display value
|
65
|
+
input.value = item.label;
|
66
|
+
// Update hidden input if exists
|
67
|
+
const hiddenInput = document.getElementById(`${input.id}_hidden`);
|
68
|
+
if (hiddenInput) {
|
69
|
+
hiddenInput.value = item.value;
|
70
|
+
hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
|
71
|
+
}
|
72
|
+
// Call original onSelect callback
|
73
|
+
if (dropdownInfo.onSelect) {
|
74
|
+
dropdownInfo.onSelect(item.value, item);
|
75
|
+
}
|
76
|
+
// Hide dropdown
|
77
|
+
dropdownList.style.display = 'none';
|
78
|
+
});
|
79
|
+
|
80
|
+
dropdownList.appendChild(div);
|
81
|
+
});
|
82
|
+
|
83
|
+
// Restore scroll position
|
84
|
+
dropdownList.scrollTop = scrollPos;
|
85
|
+
} else {
|
86
|
+
console.log(`Dropdown ${input.id} is closed, options will update when opened`);
|
87
|
+
}
|
88
|
+
|
89
|
+
return true;
|
90
|
+
}
|
91
|
+
|
92
|
+
/**
|
93
|
+
* Setup a custom dropdown component
|
94
|
+
* @param {HTMLElement} input - The input element
|
95
|
+
* @param {HTMLElement} dropdownList - The dropdown list element
|
96
|
+
* @param {Function} getOptions - Function that returns the current options array [{value: string, label: string}]
|
97
|
+
* @param {Function} onSelect - Callback when an item is selected (value, item) => {}
|
98
|
+
* @param {boolean} allowCustomValues - Whether to allow values not in the options list
|
99
|
+
* @param {string} noResultsText - Text to show when no results are found
|
100
|
+
*/
|
101
|
+
function setupCustomDropdown(input, dropdownList, getOptions, onSelect, allowCustomValues = false, noResultsText = 'No results found.') {
|
102
|
+
let selectedIndex = -1;
|
103
|
+
let isOpen = false;
|
104
|
+
let showAllOptions = false; // Flag to track if we should show all options
|
105
|
+
|
106
|
+
// Find the associated hidden input field
|
107
|
+
const hiddenInput = document.getElementById(`${input.id}_hidden`);
|
108
|
+
|
109
|
+
// Function to update hidden field
|
110
|
+
function updateHiddenField(value) {
|
111
|
+
if (hiddenInput) {
|
112
|
+
hiddenInput.value = value;
|
113
|
+
// Also dispatch a change event on the hidden input to trigger form handling
|
114
|
+
hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
|
115
|
+
}
|
116
|
+
}
|
117
|
+
|
118
|
+
// Function to filter options
|
119
|
+
function filterOptions(searchText, showAll = false) {
|
120
|
+
const options = getOptions();
|
121
|
+
if (showAll || !searchText.trim()) return options;
|
122
|
+
|
123
|
+
return options.filter(item =>
|
124
|
+
item.label.toLowerCase().includes(searchText.toLowerCase()) ||
|
125
|
+
item.value.toLowerCase().includes(searchText.toLowerCase())
|
126
|
+
);
|
127
|
+
}
|
128
|
+
|
129
|
+
// Function to highlight matched text
|
130
|
+
function highlightText(text, search) {
|
131
|
+
if (!search.trim() || showAllOptions) return text;
|
132
|
+
const regex = new RegExp(`(${search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
133
|
+
return text.replace(regex, '<span class="highlight">$1</span>');
|
134
|
+
}
|
135
|
+
|
136
|
+
// Function to show the dropdown
|
137
|
+
function showDropdown() {
|
138
|
+
const inputRect = input.getBoundingClientRect();
|
139
|
+
|
140
|
+
// Debug logging
|
141
|
+
console.log('Dropdown positioning:', {
|
142
|
+
inputLeft: inputRect.left,
|
143
|
+
inputBottom: inputRect.bottom,
|
144
|
+
scrollY: window.scrollY,
|
145
|
+
inputWidth: inputRect.width,
|
146
|
+
windowWidth: window.innerWidth,
|
147
|
+
windowHeight: window.innerHeight
|
148
|
+
});
|
149
|
+
|
150
|
+
// Store original parent for when we close
|
151
|
+
if (!dropdownList._originalParent) {
|
152
|
+
dropdownList._originalParent = dropdownList.parentNode;
|
153
|
+
}
|
154
|
+
|
155
|
+
// Remove from current parent
|
156
|
+
if (dropdownList.parentNode) {
|
157
|
+
dropdownList.parentNode.removeChild(dropdownList);
|
158
|
+
}
|
159
|
+
|
160
|
+
// Append directly to body
|
161
|
+
document.body.appendChild(dropdownList);
|
162
|
+
|
163
|
+
// Make dropdown visible
|
164
|
+
dropdownList.style.display = 'block';
|
165
|
+
|
166
|
+
// Add active class for dropdown.
|
167
|
+
dropdownList.classList.add('dropdown-active');
|
168
|
+
|
169
|
+
// Add dropdown-active class to body
|
170
|
+
document.body.classList.add('dropdown-active');
|
171
|
+
|
172
|
+
// Add a small offset (6px) to ensure it's visibly separated from the input
|
173
|
+
const verticalOffset = 6;
|
174
|
+
|
175
|
+
// Calculate position relative to viewport
|
176
|
+
const left = Math.min(inputRect.left, window.innerWidth - inputRect.width - 10);
|
177
|
+
const top = inputRect.bottom + window.scrollY + verticalOffset;
|
178
|
+
|
179
|
+
// Apply the calculated position
|
180
|
+
dropdownList.style.left = `${left}px`;
|
181
|
+
dropdownList.style.top = `${top}px`;
|
182
|
+
dropdownList.style.width = `${inputRect.width}px`;
|
183
|
+
|
184
|
+
// Force DOM reflow to ensure styles are applied
|
185
|
+
dropdownList.getBoundingClientRect();
|
186
|
+
|
187
|
+
// Additional styling to ensure visibility
|
188
|
+
dropdownList.style.visibility = 'visible';
|
189
|
+
dropdownList.style.opacity = '1';
|
190
|
+
|
191
|
+
// Ensure dropdown is visible by bringing it to front
|
192
|
+
dropdownList.style.zIndex = '999999';
|
193
|
+
|
194
|
+
input.setAttribute('aria-expanded', 'true');
|
195
|
+
isOpen = true;
|
196
|
+
}
|
197
|
+
|
198
|
+
// Function to hide the dropdown
|
199
|
+
function hideDropdown() {
|
200
|
+
// Get current parent
|
201
|
+
const currentParent = dropdownList.parentNode;
|
202
|
+
|
203
|
+
// Hide first
|
204
|
+
dropdownList.style.display = 'none';
|
205
|
+
// Remove active class
|
206
|
+
dropdownList.classList.remove('dropdown-active');
|
207
|
+
|
208
|
+
// Remove dropdown-active class from body
|
209
|
+
document.body.classList.remove('dropdown-active');
|
210
|
+
|
211
|
+
// Reset position styles
|
212
|
+
dropdownList.style.left = '';
|
213
|
+
dropdownList.style.top = '';
|
214
|
+
dropdownList.style.width = '';
|
215
|
+
|
216
|
+
// Move back to original parent if it exists and we're not already there
|
217
|
+
if (dropdownList._originalParent && currentParent !== dropdownList._originalParent) {
|
218
|
+
currentParent.removeChild(dropdownList);
|
219
|
+
dropdownList._originalParent.appendChild(dropdownList);
|
220
|
+
}
|
221
|
+
|
222
|
+
input.setAttribute('aria-expanded', 'false');
|
223
|
+
selectedIndex = -1;
|
224
|
+
isOpen = false;
|
225
|
+
showAllOptions = false; // Reset the flag when closing dropdown
|
226
|
+
}
|
227
|
+
|
228
|
+
// Function to update the dropdown
|
229
|
+
function updateDropdown() {
|
230
|
+
const searchText = input.value;
|
231
|
+
const filteredData = filterOptions(searchText, showAllOptions);
|
232
|
+
|
233
|
+
dropdownList.innerHTML = '';
|
234
|
+
|
235
|
+
if (filteredData.length === 0) {
|
236
|
+
dropdownList.innerHTML = `<div class="custom-dropdown-no-results">${noResultsText}</div>`;
|
237
|
+
|
238
|
+
if (allowCustomValues && searchText.trim()) {
|
239
|
+
const customOption = document.createElement('div');
|
240
|
+
customOption.className = 'custom-dropdown-footer';
|
241
|
+
customOption.textContent = `Press Enter to use "${searchText}"`;
|
242
|
+
dropdownList.appendChild(customOption);
|
243
|
+
}
|
244
|
+
|
245
|
+
return;
|
246
|
+
}
|
247
|
+
|
248
|
+
filteredData.forEach((item, index) => {
|
249
|
+
const div = document.createElement('div');
|
250
|
+
div.className = 'custom-dropdown-item';
|
251
|
+
div.innerHTML = highlightText(item.label, searchText);
|
252
|
+
div.setAttribute('data-value', item.value);
|
253
|
+
div.addEventListener('click', () => {
|
254
|
+
// Set display value
|
255
|
+
input.value = item.label;
|
256
|
+
// Update hidden input value
|
257
|
+
updateHiddenField(item.value);
|
258
|
+
// Call onSelect callback
|
259
|
+
onSelect(item.value, item);
|
260
|
+
hideDropdown();
|
261
|
+
});
|
262
|
+
|
263
|
+
if (index === selectedIndex) {
|
264
|
+
div.classList.add('active');
|
265
|
+
}
|
266
|
+
|
267
|
+
dropdownList.appendChild(div);
|
268
|
+
});
|
269
|
+
}
|
270
|
+
|
271
|
+
// Input event - filter as user types
|
272
|
+
input.addEventListener('input', () => {
|
273
|
+
showAllOptions = false; // Reset when typing
|
274
|
+
showDropdown();
|
275
|
+
updateDropdown();
|
276
|
+
selectedIndex = -1;
|
277
|
+
});
|
278
|
+
|
279
|
+
// Click event - show all options when clicking in the input
|
280
|
+
input.addEventListener('click', (e) => {
|
281
|
+
e.stopPropagation();
|
282
|
+
showAllOptions = true;
|
283
|
+
showDropdown();
|
284
|
+
updateDropdown();
|
285
|
+
});
|
286
|
+
|
287
|
+
// Focus event - show dropdown when input is focused
|
288
|
+
input.addEventListener('focus', () => {
|
289
|
+
if (!isOpen) {
|
290
|
+
showAllOptions = true; // Show all options on focus
|
291
|
+
showDropdown();
|
292
|
+
updateDropdown();
|
293
|
+
}
|
294
|
+
});
|
295
|
+
|
296
|
+
// Keyboard navigation for dropdown
|
297
|
+
input.addEventListener('keydown', (e) => {
|
298
|
+
const items = dropdownList.querySelectorAll('.custom-dropdown-item');
|
299
|
+
|
300
|
+
if (e.key === 'ArrowDown') {
|
301
|
+
e.preventDefault();
|
302
|
+
if (!isOpen) {
|
303
|
+
showAllOptions = true;
|
304
|
+
showDropdown();
|
305
|
+
updateDropdown();
|
306
|
+
selectedIndex = 0;
|
307
|
+
} else {
|
308
|
+
selectedIndex = (selectedIndex + 1) % items.length;
|
309
|
+
}
|
310
|
+
} else if (e.key === 'ArrowUp') {
|
311
|
+
e.preventDefault();
|
312
|
+
if (!isOpen) {
|
313
|
+
showAllOptions = true;
|
314
|
+
showDropdown();
|
315
|
+
updateDropdown();
|
316
|
+
selectedIndex = items.length - 1;
|
317
|
+
} else {
|
318
|
+
selectedIndex = (selectedIndex - 1 + items.length) % items.length;
|
319
|
+
}
|
320
|
+
} else if (e.key === 'Enter') {
|
321
|
+
e.preventDefault();
|
322
|
+
|
323
|
+
if (selectedIndex >= 0 && selectedIndex < items.length) {
|
324
|
+
// Select the highlighted item
|
325
|
+
const selectedItem = items[selectedIndex];
|
326
|
+
const value = selectedItem.getAttribute('data-value');
|
327
|
+
const item = getOptions().find(o => o.value === value);
|
328
|
+
// Update display value
|
329
|
+
input.value = item.label;
|
330
|
+
// Update hidden input
|
331
|
+
updateHiddenField(value);
|
332
|
+
// Call callback
|
333
|
+
onSelect(value, item);
|
334
|
+
} else if (allowCustomValues && input.value.trim()) {
|
335
|
+
// Use the custom value
|
336
|
+
const customValue = input.value.trim();
|
337
|
+
// Update hidden input with custom value
|
338
|
+
updateHiddenField(customValue);
|
339
|
+
onSelect(customValue, null);
|
340
|
+
}
|
341
|
+
hideDropdown();
|
342
|
+
} else if (e.key === 'Escape') {
|
343
|
+
e.preventDefault();
|
344
|
+
hideDropdown();
|
345
|
+
}
|
346
|
+
|
347
|
+
// Update selected item styling
|
348
|
+
items.forEach((item, index) => {
|
349
|
+
if (index === selectedIndex) {
|
350
|
+
item.classList.add('active');
|
351
|
+
// Scroll into view if necessary
|
352
|
+
if (item.offsetTop < dropdownList.scrollTop) {
|
353
|
+
dropdownList.scrollTop = item.offsetTop;
|
354
|
+
} else if (item.offsetTop + item.offsetHeight > dropdownList.scrollTop + dropdownList.offsetHeight) {
|
355
|
+
dropdownList.scrollTop = item.offsetTop + item.offsetHeight - dropdownList.offsetHeight;
|
356
|
+
}
|
357
|
+
} else {
|
358
|
+
item.classList.remove('active');
|
359
|
+
}
|
360
|
+
});
|
361
|
+
});
|
362
|
+
|
363
|
+
// Close dropdown when clicking outside
|
364
|
+
document.addEventListener('click', () => {
|
365
|
+
if (isOpen) {
|
366
|
+
hideDropdown();
|
367
|
+
}
|
368
|
+
});
|
369
|
+
|
370
|
+
// Prevent clicks in the dropdown from closing it
|
371
|
+
dropdownList.addEventListener('click', (e) => {
|
372
|
+
e.stopPropagation();
|
373
|
+
});
|
374
|
+
|
375
|
+
// Register this dropdown for future updates
|
376
|
+
if (input && input.id && dropdownList && dropdownList.id) {
|
377
|
+
const dropdownId = dropdownList.id.replace('-list', '');
|
378
|
+
console.log(`Registering dropdown: ${input.id} with list ${dropdownId}`);
|
379
|
+
|
380
|
+
dropdownRegistry[input.id] = {
|
381
|
+
getOptions: getOptions,
|
382
|
+
onSelect: onSelect,
|
383
|
+
dropdownId: dropdownId,
|
384
|
+
allowCustomValues: allowCustomValues,
|
385
|
+
noResultsText: noResultsText
|
386
|
+
};
|
387
|
+
} else {
|
388
|
+
console.warn('Cannot register dropdown: Missing input ID or dropdown list ID');
|
389
|
+
}
|
390
|
+
|
391
|
+
// Initial state
|
392
|
+
hideDropdown();
|
393
|
+
|
394
|
+
// Return functions that might be needed externally
|
395
|
+
return {
|
396
|
+
updateDropdown,
|
397
|
+
showDropdown,
|
398
|
+
hideDropdown,
|
399
|
+
setValue: (value, triggerChange = true) => {
|
400
|
+
const options = getOptions();
|
401
|
+
const matchedOption = options.find(opt => opt.value === value);
|
402
|
+
|
403
|
+
if (matchedOption) {
|
404
|
+
input.value = matchedOption.label;
|
405
|
+
} else if (allowCustomValues && value) {
|
406
|
+
input.value = value;
|
407
|
+
} else {
|
408
|
+
input.value = '';
|
409
|
+
}
|
410
|
+
|
411
|
+
if (triggerChange) {
|
412
|
+
updateHiddenField(value);
|
413
|
+
// Also call onSelect if triggerChange is true
|
414
|
+
if (matchedOption) {
|
415
|
+
onSelect(value, matchedOption);
|
416
|
+
} else {
|
417
|
+
onSelect(value, { value, label: value });
|
418
|
+
}
|
419
|
+
} else {
|
420
|
+
// Even if we don't trigger events, we should update the hidden field
|
421
|
+
if (hiddenInput) {
|
422
|
+
hiddenInput.value = value;
|
423
|
+
}
|
424
|
+
}
|
425
|
+
}
|
426
|
+
};
|
427
|
+
}
|
428
|
+
})();
|