Fixing multi-level dropdown issues with window boundaries

This commit is contained in:
Bruno Alves de Faria 2017-03-28 15:21:03 -03:00
parent cc76eab078
commit e4622aee2d
4 changed files with 145 additions and 30 deletions

View File

@ -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);
}
});
}

View File

@ -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

View File

@ -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>

View File

@ -23,6 +23,7 @@ label.wrapperLabel
& + .wrapperText
theme('color', '$textPrimaryColor')
cursor: auto
input[type=text], input[type=password]
theme('background-color', '$uiGray')