Add categorized indicator dropdown with expandable Patterns submenu

- Group candlestick patterns under expandable "Patterns" category
- Regular indicators shown directly in dropdown
- Patterns submenu expands on hover with all CDL_* indicators
- Fixed submenu positioning outside scrollable container
- Tooltip shows pattern description and SVG when hovering items

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rob 2026-03-08 11:55:43 -03:00
parent 4ab7a5023d
commit 8f362a1379
1 changed files with 199 additions and 14 deletions

View File

@ -24,15 +24,24 @@
onfocus="showIndicatorDropdown()" oninput="filterIndicatorTypes()"> onfocus="showIndicatorDropdown()" oninput="filterIndicatorTypes()">
<input type="hidden" id="newi_type" value="{% if indicator_types %}{{ indicator_types[0] }}{% endif %}"> <input type="hidden" id="newi_type" value="{% if indicator_types %}{{ indicator_types[0] }}{% endif %}">
<!-- Custom dropdown --> <!-- Custom dropdown with categories -->
<div id="indicator_dropdown" class="indicator-dropdown" style="display: none;"> <div id="indicator_dropdown" class="indicator-dropdown" style="display: none;">
<!-- Regular indicators -->
{% for i_type in indicator_types %} {% for i_type in indicator_types %}
{% if not i_type.startswith('CDL_') %}
<div class="indicator-option" data-value="{{i_type}}" <div class="indicator-option" data-value="{{i_type}}"
onmouseover="showIndicatorTooltip('{{i_type}}')" onmouseover="showIndicatorTooltip('{{i_type}}'); hidePatternSubmenu();"
onclick="selectIndicatorType('{{i_type}}')"> onclick="selectIndicatorType('{{i_type}}')">
{{i_type}} {{i_type}}
</div> </div>
{% endif %}
{% endfor %} {% endfor %}
<!-- Patterns category -->
<div id="patterns_category" class="indicator-category" onmouseover="showPatternSubmenu()" onmouseout="hidePatternSubmenuDelayed()">
<span onmouseover="showCategoryTooltip()">Patterns</span>
<span class="category-arrow"></span>
</div>
</div> </div>
</div> </div>
@ -122,6 +131,19 @@
<div id="tooltip_svg" style="text-align: center;"></div> <div id="tooltip_svg" style="text-align: center;"></div>
</div> </div>
<!-- Patterns submenu - placed outside dropdown to avoid scroll issues -->
<div id="patterns_submenu" class="indicator-submenu" onmouseover="keepPatternSubmenuOpen()" onmouseout="hidePatternSubmenuDelayed()">
{% for i_type in indicator_types %}
{% if i_type.startswith('CDL_') %}
<div class="indicator-option" data-value="{{i_type}}"
onmouseover="showIndicatorTooltip('{{i_type}}')"
onclick="selectIndicatorType('{{i_type}}')">
{{i_type}}
</div>
{% endif %}
{% endfor %}
</div>
<style> <style>
.indicator-dropdown { .indicator-dropdown {
position: absolute; position: absolute;
@ -152,6 +174,45 @@
border-bottom: none; border-bottom: none;
} }
.indicator-category {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
background: #f8f8f8;
font-weight: 500;
}
.indicator-category:hover {
background-color: #3E3AF2;
color: white;
}
.category-arrow {
font-size: 10px;
margin-left: 8px;
}
.indicator-submenu {
display: none;
position: fixed;
min-width: 200px;
max-height: 300px;
overflow-y: auto;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
z-index: 10000;
}
.indicator-submenu .indicator-option {
white-space: nowrap;
}
.indicator-tooltip { .indicator-tooltip {
display: none; display: none;
position: fixed; position: fixed;
@ -334,6 +395,8 @@ const indicatorInfo = {
} }
}; };
let submenuTimeout = null;
function showIndicatorDropdown() { function showIndicatorDropdown() {
document.getElementById('indicator_dropdown').style.display = 'block'; document.getElementById('indicator_dropdown').style.display = 'block';
// Don't show tooltip until hovering over an option // Don't show tooltip until hovering over an option
@ -343,16 +406,122 @@ function hideIndicatorDropdown() {
setTimeout(() => { setTimeout(() => {
document.getElementById('indicator_dropdown').style.display = 'none'; document.getElementById('indicator_dropdown').style.display = 'none';
document.getElementById('indicator_tooltip').style.display = 'none'; document.getElementById('indicator_tooltip').style.display = 'none';
hidePatternSubmenu();
}, 200); }, 200);
} }
function showPatternSubmenu() {
if (submenuTimeout) {
clearTimeout(submenuTimeout);
submenuTimeout = null;
}
const category = document.getElementById('patterns_category');
const submenu = document.getElementById('patterns_submenu');
const categoryRect = category.getBoundingClientRect();
// Position submenu to the right of the category
submenu.style.display = 'block';
submenu.style.left = (categoryRect.right + 2) + 'px';
submenu.style.top = categoryRect.top + 'px';
// If submenu would go off right edge, position to the left instead
const submenuWidth = submenu.offsetWidth;
if (categoryRect.right + 2 + submenuWidth > window.innerWidth) {
submenu.style.left = (categoryRect.left - submenuWidth - 2) + 'px';
}
// If submenu would go off bottom, adjust top
const submenuHeight = submenu.offsetHeight;
if (categoryRect.top + submenuHeight > window.innerHeight) {
submenu.style.top = (window.innerHeight - submenuHeight - 10) + 'px';
}
}
function keepPatternSubmenuOpen() {
if (submenuTimeout) {
clearTimeout(submenuTimeout);
submenuTimeout = null;
}
}
function showCategoryTooltip() {
const tooltip = document.getElementById('indicator_tooltip');
const title = document.getElementById('tooltip_title');
const desc = document.getElementById('tooltip_description');
const svg = document.getElementById('tooltip_svg');
const submenu = document.getElementById('patterns_submenu');
title.textContent = 'Candlestick Patterns';
desc.textContent = 'Technical analysis patterns based on candlestick formations. These patterns can signal potential reversals or continuations in price trends. Hover over a pattern to see its description and visual representation.';
svg.innerHTML = '';
// Position tooltip to the right of the submenu
const submenuRect = submenu.getBoundingClientRect();
tooltip.style.display = 'block';
tooltip.style.position = 'fixed';
tooltip.style.left = (submenuRect.right + 10) + 'px';
tooltip.style.top = submenuRect.top + 'px';
const tooltipWidth = 280;
if (submenuRect.right + 10 + tooltipWidth > window.innerWidth) {
tooltip.style.left = (submenuRect.left - tooltipWidth - 10) + 'px';
}
}
function hidePatternSubmenu() {
document.getElementById('patterns_submenu').style.display = 'none';
}
function hidePatternSubmenuDelayed() {
submenuTimeout = setTimeout(() => {
hidePatternSubmenu();
}, 100);
}
function filterIndicatorTypes() { function filterIndicatorTypes() {
const search = document.getElementById('newi_type_search').value.toLowerCase(); const search = document.getElementById('newi_type_search').value.toLowerCase();
const options = document.querySelectorAll('.indicator-option'); const dropdown = document.getElementById('indicator_dropdown');
const options = dropdown.querySelectorAll('.indicator-option');
const category = dropdown.querySelector('.indicator-category');
const submenuOptions = document.querySelectorAll('#patterns_submenu .indicator-option');
let hasRegularMatch = false;
let hasPatternMatch = false;
// Filter regular indicators
options.forEach(opt => { options.forEach(opt => {
if (!opt.closest('.indicator-submenu')) {
const text = opt.textContent.toLowerCase(); const text = opt.textContent.toLowerCase();
opt.style.display = text.includes(search) ? 'block' : 'none'; const matches = text.includes(search);
opt.style.display = matches ? 'block' : 'none';
if (matches) hasRegularMatch = true;
}
}); });
// Filter pattern indicators and check if any match
submenuOptions.forEach(opt => {
const text = opt.textContent.toLowerCase();
const matches = text.includes(search);
opt.style.display = matches ? 'block' : 'none';
if (matches) hasPatternMatch = true;
});
// Show/hide the Patterns category based on search
if (category) {
if (search === '') {
// No search - show category normally
category.style.display = 'flex';
hidePatternSubmenu();
} else if (hasPatternMatch) {
// Search matches patterns - show category and expand submenu
category.style.display = 'flex';
document.getElementById('patterns_submenu').style.display = 'block';
} else {
// No pattern matches - hide category
category.style.display = 'none';
}
}
} }
function selectIndicatorType(type) { function selectIndicatorType(type) {
@ -363,12 +532,17 @@ function selectIndicatorType(type) {
} }
function showIndicatorTooltip(type) { function showIndicatorTooltip(type) {
// Clear any pending submenu hide
if (submenuTimeout) {
clearTimeout(submenuTimeout);
submenuTimeout = null;
}
const info = indicatorInfo[type]; const info = indicatorInfo[type];
const tooltip = document.getElementById('indicator_tooltip'); const tooltip = document.getElementById('indicator_tooltip');
const title = document.getElementById('tooltip_title'); const title = document.getElementById('tooltip_title');
const desc = document.getElementById('tooltip_description'); const desc = document.getElementById('tooltip_description');
const svg = document.getElementById('tooltip_svg'); const svg = document.getElementById('tooltip_svg');
const dropdown = document.getElementById('indicator_dropdown');
if (info) { if (info) {
title.textContent = type; title.textContent = type;
@ -380,28 +554,39 @@ function showIndicatorTooltip(type) {
svg.innerHTML = ''; svg.innerHTML = '';
} }
// Get dropdown position and place tooltip to the right of it // Determine which element to position relative to
const dropdownRect = dropdown.getBoundingClientRect(); let referenceElement;
if (type.startsWith('CDL_')) {
// For pattern indicators, position relative to submenu
referenceElement = document.getElementById('patterns_submenu');
} else {
// For regular indicators, position relative to dropdown
referenceElement = document.getElementById('indicator_dropdown');
}
// Position tooltip to the right of the dropdown const refRect = referenceElement.getBoundingClientRect();
// Position tooltip to the right of the reference element
tooltip.style.display = 'block'; tooltip.style.display = 'block';
tooltip.style.position = 'fixed'; tooltip.style.position = 'fixed';
tooltip.style.left = (dropdownRect.right + 10) + 'px'; tooltip.style.left = (refRect.right + 10) + 'px';
tooltip.style.top = dropdownRect.top + 'px'; tooltip.style.top = refRect.top + 'px';
// If tooltip would go off right edge of screen, put it to the left instead // If tooltip would go off right edge of screen, put it to the left instead
const tooltipWidth = 280; // matches CSS width const tooltipWidth = 280; // matches CSS width
if (dropdownRect.right + 10 + tooltipWidth > window.innerWidth) { if (refRect.right + 10 + tooltipWidth > window.innerWidth) {
tooltip.style.left = (dropdownRect.left - tooltipWidth - 10) + 'px'; tooltip.style.left = (refRect.left - tooltipWidth - 10) + 'px';
} }
} }
// Close dropdown when clicking outside // Close dropdown when clicking outside
document.addEventListener('click', function(e) { document.addEventListener('click', function(e) {
const wrapper = document.querySelector('.indicator-type-wrapper'); const wrapper = document.querySelector('.indicator-type-wrapper');
if (wrapper && !wrapper.contains(e.target)) { const submenu = document.getElementById('patterns_submenu');
if (wrapper && !wrapper.contains(e.target) && !submenu.contains(e.target)) {
document.getElementById('indicator_dropdown').style.display = 'none'; document.getElementById('indicator_dropdown').style.display = 'none';
document.getElementById('indicator_tooltip').style.display = 'none'; document.getElementById('indicator_tooltip').style.display = 'none';
document.getElementById('patterns_submenu').style.display = 'none';
} }
}); });
</script> </script>