Fixing multi-level dropdown issues with window boundaries
This commit is contained in:
1 parent
cc76eab078
commit
e4622aee2d
4 files changed
+145
-30
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Blaze } from 'meteor/blaze';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { $ } from 'meteor/jquery';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
@@ -12,12 +13,15 @@ OHIF.mixins.dropdown = new OHIF.Mixin({
|
||||
composition: {
|
||||
onRendered() {
|
||||
const instance = Template.instance();
|
||||
const { event, centered, marginTop } = instance.data.options;
|
||||
const { event, centered, marginTop, $parentLi, reactiveClose } = instance.data.options;
|
||||
// Get the dropdown element to enable position manipulation
|
||||
const $dropdown = instance.$('.dropdown');
|
||||
const dropdown = $dropdown[0];
|
||||
const $dropdownMenu = $dropdown.children('.dropdown-menu');
|
||||
|
||||
// Set a opening state to the component
|
||||
instance.opening = true;
|
||||
|
||||
// Destroy the Blaze created view (either created with template calls or with renderWithData)
|
||||
instance.destroyView = () => {
|
||||
const destroyHandle = () => {
|
||||
@@ -39,13 +43,37 @@ OHIF.mixins.dropdown = new OHIF.Mixin({
|
||||
// Destroy the view when the promise is fullfilled
|
||||
instance.data.promise.then(instance.destroyView, instance.destroyView);
|
||||
|
||||
// Close submenu if exists
|
||||
instance.closeSubmenu = focusSelf => {
|
||||
if (instance.reactiveClose) {
|
||||
if (focusSelf) {
|
||||
$(instance.lastSubmenu).focus();
|
||||
}
|
||||
|
||||
instance.reactiveClose.set(true);
|
||||
delete instance.reactiveClose;
|
||||
delete instance.lastSubmenu;
|
||||
}
|
||||
};
|
||||
|
||||
// Close the dropdown resolving or rejecting the promise
|
||||
instance.close = (isResolve, result) => {
|
||||
const method = instance.data[isResolve ? 'promiseResolve' : 'promiseReject'];
|
||||
const param = result instanceof Promise ? null : result;
|
||||
method(param);
|
||||
instance.closeSubmenu(false);
|
||||
instance.closed = true;
|
||||
};
|
||||
|
||||
// Close the dropdown if the reactiveClose suffered changes
|
||||
if (reactiveClose) {
|
||||
instance.autorun(() => {
|
||||
const isClosed = reactiveClose.get();
|
||||
if (!isClosed) return;
|
||||
instance.close(false);
|
||||
});
|
||||
}
|
||||
|
||||
// Stop here and destroy the view if no items was given
|
||||
if (!instance.data.items.length) {
|
||||
return instance.close(false);
|
||||
@@ -70,6 +98,11 @@ OHIF.mixins.dropdown = new OHIF.Mixin({
|
||||
// Show the dropdown and focus the first option
|
||||
$dropdown.addClass('open').find('a:first').focus();
|
||||
|
||||
// Add a handler to change the opening state
|
||||
$dropdownMenu.one('transitionend', event => {
|
||||
instance.opening = false;
|
||||
});
|
||||
|
||||
// Change the dropdown position if mouse event was given
|
||||
if (event) {
|
||||
const position = {
|
||||
@@ -78,8 +111,30 @@ OHIF.mixins.dropdown = new OHIF.Mixin({
|
||||
};
|
||||
|
||||
if (centered) {
|
||||
// Center the dropdown menu based on the event mouse position
|
||||
position.left -= $dropdownMenu.outerWidth() / 2;
|
||||
position.top -= $dropdownMenu.outerHeight() / 2;
|
||||
} else if ($parentLi) {
|
||||
// Change the dropdown menu position based on the parent menu item
|
||||
const $parentDm = $parentLi.closest('.dropdown-menu');
|
||||
const pdmOffset = $parentDm.offset();
|
||||
const pdmWidth = $parentDm.outerWidth();
|
||||
const pliOffset = $parentLi.offset();
|
||||
const dmWidth = $dropdownMenu.outerWidth();
|
||||
Object.assign(position, {
|
||||
left: pdmWidth + pdmOffset.left,
|
||||
top: pliOffset.top
|
||||
});
|
||||
|
||||
// Check if the element position is going beyond the window right boundary
|
||||
let rightToLeft = false;
|
||||
if (position.left < pdmOffset.left + pdmWidth || position.left > document.body.clientWidth - dmWidth) {
|
||||
position.left -= pdmWidth + $dropdownMenu.outerWidth();
|
||||
rightToLeft = true;
|
||||
}
|
||||
|
||||
const menuClass = rightToLeft ? 'origin-top-right' : 'origin-top-left';
|
||||
$dropdownMenu.addClass(menuClass);
|
||||
}
|
||||
|
||||
$dropdownMenu.css(position).trigger('spatialChanged');
|
||||
@@ -89,9 +144,9 @@ OHIF.mixins.dropdown = new OHIF.Mixin({
|
||||
|
||||
events: {
|
||||
'click .form-action'(event, instance) {
|
||||
event.stopPropagation();
|
||||
const $target = $(event.currentTarget);
|
||||
const isDisabled = $target.hasClass('disabled');
|
||||
|
||||
if (isDisabled) {
|
||||
instance.close(false);
|
||||
} else {
|
||||
@@ -100,21 +155,93 @@ OHIF.mixins.dropdown = new OHIF.Mixin({
|
||||
}
|
||||
},
|
||||
|
||||
'click .dropdown'(event, instance) {
|
||||
const $target = $(event.target);
|
||||
if ($target.hasClass('disabled')) {
|
||||
'mouseenter .form-action'(event, instance) {
|
||||
// Postpone the submenu opening if it's still being animated
|
||||
if (instance.opening) {
|
||||
instance.$('.dropdown-menu').one('transitionend', event => {
|
||||
$(event.currentTarget).trigger('mouseenter');
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop here if dropdown is already closed or event was triggered in child elements
|
||||
if (instance.closed || event.target !== event.currentTarget) return;
|
||||
|
||||
// Close the submenu if a sibling element was hovered
|
||||
if (instance.lastSubmenu && instance.lastSubmenu !== event.currentTarget) {
|
||||
instance.closeSubmenu(true);
|
||||
}
|
||||
|
||||
// Stop here if submenu is already opened for the current menu item
|
||||
if (!this.items || instance.lastSubmenu === event.currentTarget) return;
|
||||
|
||||
// Close the current opened submenu (if exists) in order to open another one
|
||||
instance.closeSubmenu(true);
|
||||
|
||||
// Set the reactive closing elementcontroller and the last submenu element trigger
|
||||
const reactiveClose = new ReactiveVar(false);
|
||||
instance.reactiveClose = reactiveClose;
|
||||
instance.lastSubmenu = event.currentTarget;
|
||||
|
||||
// Get the triggering li element and render the submenu
|
||||
const $parentLi = $(event.currentTarget).closest('li');
|
||||
OHIF.ui.showDropdown(this.items, {
|
||||
event,
|
||||
reactiveClose: instance.reactiveClose,
|
||||
$parentLi,
|
||||
parentInstance: instance
|
||||
}).then(instance.data.promiseResolve).catch(() => {});
|
||||
},
|
||||
|
||||
'keydown .form-action'(event, instance) {
|
||||
const key = event.which;
|
||||
|
||||
// Close the submenu if LEFT, BACKSPACE or ESC key was pressed
|
||||
const { parentInstance } = instance.data.options;
|
||||
if (parentInstance && (key === 37 || key === 8 || key === 27)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
} else {
|
||||
instance.close(false);
|
||||
parentInstance.closeSubmenu(true);
|
||||
}
|
||||
|
||||
// Stop here if it's not a submenu trigger
|
||||
if (!this.items) return;
|
||||
|
||||
// Open the submenu if RIGHT, ENTER or SPACE key was pressed
|
||||
if (key === 39 || key === 13 || key === 32) {
|
||||
event.preventDefault();
|
||||
$(event.currentTarget).trigger('mouseenter');
|
||||
}
|
||||
},
|
||||
|
||||
'blur .dropdown'(event, instance) {
|
||||
// Stop here if it's closed or if it's a submenu not being closed
|
||||
if (instance.closed) return;
|
||||
if (instance.reactiveClose && !instance.reactiveClose.get()) return;
|
||||
|
||||
// Postpone the execution to enable getting the focused element
|
||||
Meteor.defer(() => {
|
||||
const $focus = $(':focus');
|
||||
if (!$focus.length || !$.contains(event.currentTarget, $focus[0])) {
|
||||
instance.close(false);
|
||||
|
||||
// Iterate over all parent dropdowns and check if one of them has the focus
|
||||
let hasFocus = false;
|
||||
let currentInstance = instance;
|
||||
do {
|
||||
if ($.contains(currentInstance.$('.dropdown')[0], $focus[0])) {
|
||||
hasFocus = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!currentInstance.data.options.parentInstance) {
|
||||
break;
|
||||
} else {
|
||||
currentInstance = currentInstance.data.options.parentInstance;
|
||||
}
|
||||
} while (currentInstance);
|
||||
|
||||
// Close all dropdown levels if it lost the focus
|
||||
if (!hasFocus) {
|
||||
currentInstance.close(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user