refactor: replace inline dropdown hide logic with reusable outside-dismiss helpers
Add bindDropdownOutsideDismiss and pointer-guard utilities to downloadTargetMethods, then use them in resolveDownloadMethods to remove duplicated hide-on-blur and setTimeout-based hide patterns. This centralizes dropdown dismissal behavior and handles pointer/mouse/touch events consistently across dropdown lists.
This commit is contained in:
@@ -2067,13 +2067,8 @@ export const resolveDownloadMethods = {
|
||||
});
|
||||
};
|
||||
|
||||
const hideSourceList = () => {
|
||||
setTimeout(() => {
|
||||
sourceList.style.display = 'none';
|
||||
}, 150);
|
||||
};
|
||||
|
||||
this.enableWheelScrollChaining(sourceList);
|
||||
this.bindDropdownOutsideDismiss?.(sourceList, [sourceSelect]);
|
||||
sourceSelect.addEventListener('focus', () => renderSourceOptions());
|
||||
sourceSelect.addEventListener('click', () => renderSourceOptions());
|
||||
sourceSelect.addEventListener('keydown', (event) => {
|
||||
@@ -2082,7 +2077,6 @@ export const resolveDownloadMethods = {
|
||||
renderSourceOptions();
|
||||
}
|
||||
});
|
||||
sourceSelect.addEventListener('blur', hideSourceList);
|
||||
this.syncSearchSourceUi(missing, container);
|
||||
}
|
||||
|
||||
@@ -2128,13 +2122,12 @@ export const resolveDownloadMethods = {
|
||||
});
|
||||
};
|
||||
|
||||
const hideBaseList = () => {
|
||||
setTimeout(() => {
|
||||
baseList.style.display = 'none';
|
||||
}, 150);
|
||||
};
|
||||
|
||||
this.enableWheelScrollChaining(baseList);
|
||||
this.bindDropdownOutsideDismiss?.(baseList, [baseSelect], () => {
|
||||
const value = this.getDropdownValue(baseSelect);
|
||||
this.setDropdownValue(baseSelect, value, this.getSearchBaseModelLabel(value, missing));
|
||||
baseList.style.display = 'none';
|
||||
});
|
||||
baseSelect.addEventListener('focus', () => renderBaseOptions(''));
|
||||
baseSelect.addEventListener('click', () => renderBaseOptions(''));
|
||||
baseSelect.addEventListener('input', () => {
|
||||
@@ -2152,7 +2145,6 @@ export const resolveDownloadMethods = {
|
||||
baseSelect.addEventListener('blur', () => {
|
||||
const value = this.getDropdownValue(baseSelect);
|
||||
this.setDropdownValue(baseSelect, value, this.getSearchBaseModelLabel(value, missing));
|
||||
hideBaseList();
|
||||
});
|
||||
this.syncSearchSourceUi(missing, container);
|
||||
}
|
||||
|
||||
@@ -1482,8 +1482,107 @@ export const downloadTargetMethods = {
|
||||
};
|
||||
},
|
||||
|
||||
bindDropdownListPointerGuard(listEl) {
|
||||
if (!listEl || listEl.dataset.mlPointerGuardBound === 'true') return;
|
||||
listEl.dataset.mlPointerGuardBound = 'true';
|
||||
|
||||
let releaseTimer = null;
|
||||
const clearPointerActive = () => {
|
||||
if (releaseTimer) {
|
||||
window.clearTimeout(releaseTimer);
|
||||
releaseTimer = null;
|
||||
}
|
||||
listEl.dataset.mlPointerActive = 'false';
|
||||
};
|
||||
const markPointerActive = () => {
|
||||
if (releaseTimer) {
|
||||
window.clearTimeout(releaseTimer);
|
||||
releaseTimer = null;
|
||||
}
|
||||
listEl.dataset.mlPointerActive = 'true';
|
||||
};
|
||||
const releasePointerActive = () => {
|
||||
if (releaseTimer) {
|
||||
window.clearTimeout(releaseTimer);
|
||||
}
|
||||
releaseTimer = window.setTimeout(() => {
|
||||
clearPointerActive();
|
||||
}, 250);
|
||||
};
|
||||
const isPointerInsideList = (event) => (
|
||||
event?.target instanceof Node && listEl.contains(event.target)
|
||||
) || this.isPointerEventInsideElementBounds(event, listEl, 24);
|
||||
const clearPointerActiveIfOutside = (event) => {
|
||||
if (isPointerInsideList(event)) {
|
||||
markPointerActive();
|
||||
return;
|
||||
}
|
||||
clearPointerActive();
|
||||
};
|
||||
|
||||
window.addEventListener('pointerdown', clearPointerActiveIfOutside, true);
|
||||
window.addEventListener('mousedown', clearPointerActiveIfOutside, true);
|
||||
listEl.addEventListener('pointerdown', markPointerActive, true);
|
||||
listEl.addEventListener('mousedown', markPointerActive, true);
|
||||
window.addEventListener('pointerup', releasePointerActive, true);
|
||||
window.addEventListener('mouseup', releasePointerActive, true);
|
||||
window.addEventListener('blur', clearPointerActive, true);
|
||||
},
|
||||
|
||||
isDropdownListPointerActive(listEl) {
|
||||
return listEl?.dataset?.mlPointerActive === 'true';
|
||||
},
|
||||
|
||||
isPointerEventInsideElementBounds(event, element, tolerance = 0) {
|
||||
if (!event || !element || typeof event.clientX !== 'number' || typeof event.clientY !== 'number') {
|
||||
return false;
|
||||
}
|
||||
const rect = element.getBoundingClientRect();
|
||||
return event.clientX >= rect.left - tolerance
|
||||
&& event.clientX <= rect.right + tolerance
|
||||
&& event.clientY >= rect.top - tolerance
|
||||
&& event.clientY <= rect.bottom + tolerance;
|
||||
},
|
||||
|
||||
bindDropdownOutsideDismiss(listEl, anchorEls = [], onDismiss = null) {
|
||||
if (!listEl || listEl.dataset.mlOutsideDismissBound === 'true') return;
|
||||
listEl.dataset.mlOutsideDismissBound = 'true';
|
||||
|
||||
const anchors = Array.isArray(anchorEls)
|
||||
? anchorEls.filter(Boolean)
|
||||
: [anchorEls].filter(Boolean);
|
||||
const dismiss = () => {
|
||||
if (typeof onDismiss === 'function') {
|
||||
onDismiss();
|
||||
} else {
|
||||
listEl.style.display = 'none';
|
||||
}
|
||||
};
|
||||
const handleOutsidePointer = (event) => {
|
||||
if (listEl.style.display === 'none') return;
|
||||
const target = event?.target;
|
||||
if (!(target instanceof Node)) return;
|
||||
const isInsideList = listEl.contains(target)
|
||||
|| this.isPointerEventInsideElementBounds(event, listEl, 24);
|
||||
const isInsideAnchor = anchors.some(anchor => (
|
||||
anchor.contains(target)
|
||||
|| this.isPointerEventInsideElementBounds(event, anchor)
|
||||
));
|
||||
if (isInsideList || isInsideAnchor) {
|
||||
return;
|
||||
}
|
||||
listEl.dataset.mlPointerActive = 'false';
|
||||
dismiss();
|
||||
};
|
||||
|
||||
window.addEventListener('pointerdown', handleOutsidePointer, true);
|
||||
window.addEventListener('mousedown', handleOutsidePointer, true);
|
||||
},
|
||||
|
||||
enableWheelScrollChaining(scrollEl) {
|
||||
if (!scrollEl || scrollEl.dataset.mlWheelChainBound === 'true') return;
|
||||
if (!scrollEl) return;
|
||||
this.bindDropdownListPointerGuard(scrollEl);
|
||||
if (scrollEl.dataset.mlWheelChainBound === 'true') return;
|
||||
scrollEl.dataset.mlWheelChainBound = 'true';
|
||||
|
||||
scrollEl.addEventListener('wheel', (event) => {
|
||||
@@ -1557,6 +1656,7 @@ export const downloadTargetMethods = {
|
||||
cleanupFloatingPositioning();
|
||||
listEl.style.display = 'none';
|
||||
};
|
||||
this.bindDropdownOutsideDismiss(listEl, [subfolderEl], hideFloatingSubfolderList);
|
||||
|
||||
const clampNumber = (value, min, max) => Math.min(Math.max(value, min), max);
|
||||
|
||||
@@ -2001,16 +2101,6 @@ export const downloadTargetMethods = {
|
||||
});
|
||||
};
|
||||
|
||||
const hideList = (targetEl) => {
|
||||
setTimeout(() => {
|
||||
if (targetEl === listEl) {
|
||||
hideFloatingSubfolderList();
|
||||
} else {
|
||||
targetEl.style.display = 'none';
|
||||
}
|
||||
}, 150);
|
||||
};
|
||||
|
||||
if (categoryListEl && categoryEl.dataset.mlCategoryBound !== 'true') {
|
||||
categoryEl.dataset.mlCategoryBound = 'true';
|
||||
categoryEl.addEventListener('focus', () => populateCategoryOptions(''));
|
||||
@@ -2042,13 +2132,19 @@ export const downloadTargetMethods = {
|
||||
populateCategoryOptions(categoryEl.value);
|
||||
}
|
||||
});
|
||||
categoryEl.addEventListener('blur', () => {
|
||||
const normalizeCategoryInput = () => {
|
||||
const category = this.getDropdownValue(categoryEl);
|
||||
const knownCategories = this.getKnownDownloadCategorySet();
|
||||
if (category && knownCategories.has(this.normalizeDownloadCategory(category))) {
|
||||
this.setDropdownValue(categoryEl, category, this.getCategoryDisplayName(category));
|
||||
}
|
||||
hideList(categoryListEl);
|
||||
};
|
||||
this.bindDropdownOutsideDismiss(categoryListEl, [categoryEl], () => {
|
||||
normalizeCategoryInput();
|
||||
categoryListEl.style.display = 'none';
|
||||
});
|
||||
categoryEl.addEventListener('blur', () => {
|
||||
normalizeCategoryInput();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2068,8 +2164,6 @@ export const downloadTargetMethods = {
|
||||
populateSubfolderOptions(subfolderEl.value);
|
||||
});
|
||||
|
||||
subfolderEl.addEventListener('blur', () => hideList(listEl));
|
||||
|
||||
if (suggestBtn && suggestBtn.dataset.mlSuggestBound !== 'true') {
|
||||
suggestBtn.dataset.mlSuggestBound = 'true';
|
||||
suggestBtn.addEventListener('click', async () => {
|
||||
|
||||
Reference in New Issue
Block a user