diff --git a/web/css/resolver-main.css b/web/css/resolver-main.css
index 2b6f044..7659bb2 100644
--- a/web/css/resolver-main.css
+++ b/web/css/resolver-main.css
@@ -5642,12 +5642,16 @@
}
}
.mr-download-target-list {
+ box-sizing: border-box;
position: absolute;
top: calc(100% + 4px);
left: 0;
- right: 0;
+ right: auto;
+ width: 100%;
+ min-width: 100%;
max-height: 220px;
- overflow: auto;
+ overflow-x: hidden;
+ overflow-y: auto;
display: none;
z-index: var(--mr-z-notification);
background: linear-gradient(180deg, var(--mr-panel-bg-strong) 0%, var(--mr-card-bg) 100%);
@@ -5669,11 +5673,16 @@
z-index: var(--mr-z-context-menu);
}
.mr-download-target-option {
+ box-sizing: border-box;
+ max-width: 100%;
padding: 8px 10px;
cursor: pointer;
border-bottom: 1px solid rgba(255,255,255,0.04);
color: var(--mr-text);
font-size: 12px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
.mr-download-target-option:last-child {
border-bottom: none;
diff --git a/web/resolver/actions/resolve_download_methods.js b/web/resolver/actions/resolve_download_methods.js
index cda33d1..db175bf 100644
--- a/web/resolver/actions/resolve_download_methods.js
+++ b/web/resolver/actions/resolve_download_methods.js
@@ -2054,7 +2054,7 @@ export const resolveDownloadMethods = {
sourceList.innerHTML = options
.map(option => `
${this.escapeHtml(option.label)}
`)
.join('');
- sourceList.style.display = 'block';
+ this.showDropdownList?.(sourceList, sourceSelect);
sourceList.querySelectorAll('.mr-download-target-option').forEach(optionEl => {
optionEl.addEventListener('mousedown', (event) => {
event.preventDefault();
@@ -2062,7 +2062,7 @@ export const resolveDownloadMethods = {
const label = decodeURIComponent(optionEl.dataset.label || optionEl.dataset.value || '');
this.setDropdownValue(sourceSelect, value, label);
this.setSearchSource(missing, value, container);
- sourceList.style.display = 'none';
+ this.hideDropdownList(sourceList);
});
});
};
@@ -2109,7 +2109,11 @@ export const resolveDownloadMethods = {
return `${this.escapeHtml(label)}
`;
})
.join('');
- baseList.style.display = options.length ? 'block' : 'none';
+ if (options.length) {
+ this.showDropdownList?.(baseList, baseSelect);
+ } else {
+ this.hideDropdownList(baseList);
+ }
baseList.querySelectorAll('.mr-download-target-option').forEach(optionEl => {
optionEl.addEventListener('mousedown', (event) => {
event.preventDefault();
@@ -2117,7 +2121,7 @@ export const resolveDownloadMethods = {
const label = decodeURIComponent(optionEl.dataset.label || optionEl.dataset.value || '');
this.setDropdownValue(baseSelect, value, label);
this.setSearchBaseModel(missing, value, container);
- baseList.style.display = 'none';
+ this.hideDropdownList(baseList);
});
});
};
@@ -2126,7 +2130,7 @@ export const resolveDownloadMethods = {
this.bindDropdownOutsideDismiss?.(baseList, [baseSelect], () => {
const value = this.getDropdownValue(baseSelect);
this.setDropdownValue(baseSelect, value, this.getSearchBaseModelLabel(value, missing));
- baseList.style.display = 'none';
+ this.hideDropdownList(baseList);
});
baseSelect.addEventListener('focus', () => renderBaseOptions(''));
baseSelect.addEventListener('click', () => renderBaseOptions(''));
diff --git a/web/resolver/search/download_target_methods.js b/web/resolver/search/download_target_methods.js
index a551f00..c656e64 100644
--- a/web/resolver/search/download_target_methods.js
+++ b/web/resolver/search/download_target_methods.js
@@ -1555,7 +1555,7 @@ export const downloadTargetMethods = {
if (typeof onDismiss === 'function') {
onDismiss();
} else {
- listEl.style.display = 'none';
+ this.hideDropdownList(listEl);
}
};
const handleOutsidePointer = (event) => {
@@ -1579,6 +1579,163 @@ export const downloadTargetMethods = {
window.addEventListener('mousedown', handleOutsidePointer, true);
},
+ portalDropdownList(listEl) {
+ if (!listEl) return;
+ if (listEl.id) {
+ document.querySelectorAll('.mr-download-target-list[data-ml-floating-portal="true"]').forEach(existing => {
+ if (existing !== listEl && existing.id === listEl.id) {
+ existing.remove();
+ }
+ });
+ }
+ if (listEl.dataset.mlFloatingPortal !== 'true') {
+ listEl.dataset.mlFloatingPortal = 'true';
+ document.body.appendChild(listEl);
+ }
+ listEl.classList.add('mr-download-target-floating');
+ },
+
+ cleanupFloatingDropdownPositioning(listEl) {
+ if (typeof listEl?._mlFloatingPositionCleanup === 'function') {
+ listEl._mlFloatingPositionCleanup();
+ listEl._mlFloatingPositionCleanup = null;
+ }
+ },
+
+ hideDropdownList(listEl) {
+ if (!listEl) return;
+ this.cleanupFloatingDropdownPositioning(listEl);
+ listEl.style.display = 'none';
+ },
+
+ positionFloatingDropdownList(listEl, anchorEl) {
+ if (!listEl || !anchorEl || listEl.style.display === 'none') return;
+ if (listEl.classList.contains('mr-download-folder-browser')) return;
+
+ const rect = anchorEl.getBoundingClientRect();
+ const viewportPadding = 12;
+ const viewportHeight = Math.max(240, window.innerHeight || document.documentElement.clientHeight || 0);
+ if (rect.bottom < viewportPadding || rect.top > viewportHeight - viewportPadding) {
+ this.hideDropdownList(listEl);
+ return;
+ }
+
+ const gap = 4;
+ const spaceBelow = viewportHeight - rect.bottom - viewportPadding - gap;
+ const spaceAbove = rect.top - viewportPadding - gap;
+ const openAbove = spaceBelow < 160 && spaceAbove > spaceBelow;
+ const availableHeight = Math.max(80, openAbove ? spaceAbove : spaceBelow);
+
+ listEl.style.position = 'fixed';
+ listEl.style.top = '0px';
+ listEl.style.maxHeight = `${availableHeight}px`;
+ this.fitDropdownListWidth(listEl, anchorEl);
+
+ const popupHeight = Math.min(listEl.offsetHeight || availableHeight, availableHeight);
+ const rawTop = openAbove
+ ? rect.top - popupHeight - gap
+ : rect.bottom + gap;
+ const maxTop = Math.max(viewportPadding, viewportHeight - viewportPadding - popupHeight);
+ const top = Math.min(Math.max(rawTop, viewportPadding), maxTop);
+ listEl.style.top = `${Math.round(top)}px`;
+ },
+
+ bindFloatingDropdownPositioning(listEl, anchorEl) {
+ if (!listEl || !anchorEl || listEl.classList.contains('mr-download-folder-browser')) return;
+ this.cleanupFloatingDropdownPositioning(listEl);
+
+ const updatePosition = (event) => {
+ if (event?.type === 'scroll' && event.target instanceof Node && listEl.contains(event.target)) {
+ return;
+ }
+ this.positionFloatingDropdownList(listEl, anchorEl);
+ };
+ window.addEventListener('resize', updatePosition, true);
+ window.addEventListener('scroll', updatePosition, true);
+ listEl._mlFloatingPositionCleanup = () => {
+ window.removeEventListener('resize', updatePosition, true);
+ window.removeEventListener('scroll', updatePosition, true);
+ };
+ },
+
+ showDropdownList(listEl, anchorEl = null) {
+ if (!listEl) return;
+ const anchor = anchorEl
+ || listEl.previousElementSibling
+ || listEl.parentElement?.querySelector?.('.mr-download-target-input');
+ if (!anchor) {
+ listEl.style.display = 'block';
+ return;
+ }
+
+ this.portalDropdownList(listEl);
+ listEl.style.display = 'block';
+ this.positionFloatingDropdownList(listEl, anchor);
+ this.bindFloatingDropdownPositioning(listEl, anchor);
+ requestAnimationFrame(() => this.positionFloatingDropdownList(listEl, anchor));
+ },
+
+ fitDropdownListWidth(listEl, anchorEl = null) {
+ if (!listEl || listEl.classList.contains('mr-download-folder-browser')) return;
+
+ const anchor = anchorEl
+ || listEl.previousElementSibling
+ || listEl.parentElement?.querySelector?.('.mr-download-target-input');
+ if (!anchor) return;
+
+ const anchorRect = anchor.getBoundingClientRect();
+ const inputWidth = Math.ceil(anchorRect.width || 0);
+ if (!inputWidth) return;
+
+ const viewportPadding = 12;
+ const viewportWidth = Math.max(320, window.innerWidth || document.documentElement.clientWidth || 0);
+ const maxByInput = inputWidth * 2;
+ const maxByViewport = Math.max(inputWidth, viewportWidth - viewportPadding * 2);
+ const absoluteMaxWidth = Math.max(inputWidth, Math.min(maxByInput, maxByViewport));
+
+ listEl.style.minWidth = `${inputWidth}px`;
+ listEl.style.width = `${inputWidth}px`;
+ listEl.style.maxWidth = `${absoluteMaxWidth}px`;
+
+ const listStyle = window.getComputedStyle(listEl);
+ const listChromeWidth = [
+ listStyle.borderLeftWidth,
+ listStyle.borderRightWidth,
+ listStyle.paddingLeft,
+ listStyle.paddingRight
+ ].reduce((width, value) => width + (Number.parseFloat(value) || 0), 0);
+ const optionWidths = Array.from(listEl.querySelectorAll('.mr-download-target-option')).map(option => (
+ Math.max(option.scrollWidth, option.getBoundingClientRect().width || 0)
+ ));
+ const contentWidth = Math.ceil(Math.max(inputWidth, ...optionWidths) + listChromeWidth + 2);
+ let targetWidth = Math.max(inputWidth, Math.min(contentWidth, absoluteMaxWidth));
+ const rightSpace = Math.max(inputWidth, viewportWidth - viewportPadding - anchorRect.left);
+ const leftSpace = Math.max(inputWidth, anchorRect.right - viewportPadding);
+
+ if (targetWidth > rightSpace && leftSpace > rightSpace) {
+ targetWidth = Math.min(targetWidth, leftSpace);
+ if (listEl.classList.contains('mr-download-target-floating')) {
+ listEl.style.left = `${Math.round(Math.max(viewportPadding, anchorRect.right - targetWidth))}px`;
+ listEl.style.right = 'auto';
+ } else {
+ listEl.style.left = 'auto';
+ listEl.style.right = '0';
+ }
+ } else {
+ targetWidth = Math.min(targetWidth, rightSpace);
+ if (listEl.classList.contains('mr-download-target-floating')) {
+ listEl.style.left = `${Math.round(Math.max(viewportPadding, anchorRect.left))}px`;
+ listEl.style.right = 'auto';
+ } else {
+ listEl.style.left = '0';
+ listEl.style.right = 'auto';
+ }
+ }
+
+ listEl.style.width = `${Math.round(targetWidth)}px`;
+ listEl.style.maxWidth = `${Math.round(targetWidth)}px`;
+ },
+
enableWheelScrollChaining(scrollEl) {
if (!scrollEl) return;
this.bindDropdownListPointerGuard(scrollEl);
@@ -1746,7 +1903,7 @@ export const downloadTargetMethods = {
requestAnimationFrame(positionFloatingSubfolderList);
};
- const renderOptions = (targetEl, values, onSelect) => {
+ const renderOptions = (targetEl, values, onSelect, anchorEl = null) => {
const options = values.map(value => (
typeof value === 'object'
? value
@@ -1754,7 +1911,7 @@ export const downloadTargetMethods = {
));
if (!options.length) {
targetEl.innerHTML = '';
- targetEl.style.display = 'none';
+ this.hideDropdownList(targetEl);
return;
}
@@ -1768,7 +1925,7 @@ export const downloadTargetMethods = {
})
.join('');
- targetEl.style.display = 'block';
+ this.showDropdownList(targetEl, anchorEl);
targetEl.querySelectorAll('.mr-download-target-option').forEach(option => {
option.addEventListener('mousedown', (event) => {
@@ -1777,7 +1934,7 @@ export const downloadTargetMethods = {
const label = decodeURIComponent(option.dataset.label || option.dataset.value || '');
const baseDirectory = decodeURIComponent(option.dataset.baseDirectory || '');
onSelect(value, label, baseDirectory);
- targetEl.style.display = 'none';
+ this.hideDropdownList(targetEl);
});
});
};
@@ -2080,7 +2237,7 @@ export const downloadTargetMethods = {
hideFloatingSubfolderList();
this.syncDownloadTargetFolderContext(categoryEl, subfolderEl);
this.applySuggestedDownloadSubfolder(missing, categoryEl, subfolderEl);
- });
+ }, categoryEl);
};
const populateSubfolderOptions = async (filterText = '') => {
@@ -2141,7 +2298,7 @@ export const downloadTargetMethods = {
};
this.bindDropdownOutsideDismiss(categoryListEl, [categoryEl], () => {
normalizeCategoryInput();
- categoryListEl.style.display = 'none';
+ this.hideDropdownList(categoryListEl);
});
categoryEl.addEventListener('blur', () => {
normalizeCategoryInput();