Fixing multi-level dropdown issues with window boundaries
This commit is contained in:
parent
cc76eab078
commit
e4622aee2d
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
@import "{ohif:design}/app"
|
||||
@require '{ohif:design}/app'
|
||||
|
||||
.dropdown
|
||||
cursor: default
|
||||
@ -10,10 +10,10 @@
|
||||
transform(scale(0))
|
||||
transition(transform 0.3s ease)
|
||||
|
||||
&.dropdown-menu-left
|
||||
&.dropdown-menu-left, &.origin-top-left
|
||||
transform-origin(0% 0%)
|
||||
|
||||
&.dropdown-menu-right
|
||||
&.dropdown-menu-right, &.origin-top-right
|
||||
transform-origin(100% 0%)
|
||||
|
||||
li.divider
|
||||
@ -33,21 +33,11 @@
|
||||
max-height: 18px
|
||||
vertical-align: middle
|
||||
|
||||
&.open ul.dropdown-menu
|
||||
&.open>ul.dropdown-menu
|
||||
transform(scale(1))
|
||||
|
||||
|
||||
.dropdown-submenu
|
||||
position: relative
|
||||
|
||||
&>.dropdown-menu
|
||||
top: 0
|
||||
left: 100%
|
||||
margin-top: -6px
|
||||
margin-left: -1px
|
||||
-webkit-border-radius: 0 6px 6px 6px
|
||||
-moz-border-radius: 0 6px 6px 6px
|
||||
border-radius: 0 6px 6px 6px
|
||||
|
||||
&:hover
|
||||
|
||||
@ -55,17 +45,17 @@
|
||||
display: block
|
||||
|
||||
&>a:after
|
||||
border-left-color:#FFFFFF
|
||||
border-left-color: #000000
|
||||
|
||||
&>a:after
|
||||
display: block
|
||||
float: right
|
||||
content: " "
|
||||
content: ' '
|
||||
width: 0
|
||||
height: 0
|
||||
border-color: transparent
|
||||
border-style: solid
|
||||
border-width: 5px 0 5px 5px
|
||||
border-left-color: #CCCCCC
|
||||
border-left-color: #666666
|
||||
margin-top: 5px
|
||||
margin-right: -10px
|
||||
|
||||
@ -27,9 +27,6 @@
|
||||
<span>{{item.text}}</span>
|
||||
{{/if}}
|
||||
{{/link}}
|
||||
{{#if item.items}}
|
||||
{{>dropdownFormMenu items=item.items}}
|
||||
{{/if}}
|
||||
</li>
|
||||
{{#if item.separatorAfter}}
|
||||
<li role="separator" class="divider"></li>
|
||||
|
||||
@ -23,6 +23,7 @@ label.wrapperLabel
|
||||
|
||||
& + .wrapperText
|
||||
theme('color', '$textPrimaryColor')
|
||||
cursor: auto
|
||||
|
||||
input[type=text], input[type=password]
|
||||
theme('background-color', '$uiGray')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user