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 { Meteor } from 'meteor/meteor';
|
||||||
import { Template } from 'meteor/templating';
|
import { Template } from 'meteor/templating';
|
||||||
import { Blaze } from 'meteor/blaze';
|
import { Blaze } from 'meteor/blaze';
|
||||||
|
import { ReactiveVar } from 'meteor/reactive-var';
|
||||||
import { $ } from 'meteor/jquery';
|
import { $ } from 'meteor/jquery';
|
||||||
import { OHIF } from 'meteor/ohif:core';
|
import { OHIF } from 'meteor/ohif:core';
|
||||||
|
|
||||||
@ -12,12 +13,15 @@ OHIF.mixins.dropdown = new OHIF.Mixin({
|
|||||||
composition: {
|
composition: {
|
||||||
onRendered() {
|
onRendered() {
|
||||||
const instance = Template.instance();
|
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
|
// Get the dropdown element to enable position manipulation
|
||||||
const $dropdown = instance.$('.dropdown');
|
const $dropdown = instance.$('.dropdown');
|
||||||
const dropdown = $dropdown[0];
|
const dropdown = $dropdown[0];
|
||||||
const $dropdownMenu = $dropdown.children('.dropdown-menu');
|
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)
|
// Destroy the Blaze created view (either created with template calls or with renderWithData)
|
||||||
instance.destroyView = () => {
|
instance.destroyView = () => {
|
||||||
const destroyHandle = () => {
|
const destroyHandle = () => {
|
||||||
@ -39,13 +43,37 @@ OHIF.mixins.dropdown = new OHIF.Mixin({
|
|||||||
// Destroy the view when the promise is fullfilled
|
// Destroy the view when the promise is fullfilled
|
||||||
instance.data.promise.then(instance.destroyView, instance.destroyView);
|
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
|
// Close the dropdown resolving or rejecting the promise
|
||||||
instance.close = (isResolve, result) => {
|
instance.close = (isResolve, result) => {
|
||||||
const method = instance.data[isResolve ? 'promiseResolve' : 'promiseReject'];
|
const method = instance.data[isResolve ? 'promiseResolve' : 'promiseReject'];
|
||||||
const param = result instanceof Promise ? null : result;
|
const param = result instanceof Promise ? null : result;
|
||||||
method(param);
|
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
|
// Stop here and destroy the view if no items was given
|
||||||
if (!instance.data.items.length) {
|
if (!instance.data.items.length) {
|
||||||
return instance.close(false);
|
return instance.close(false);
|
||||||
@ -70,6 +98,11 @@ OHIF.mixins.dropdown = new OHIF.Mixin({
|
|||||||
// Show the dropdown and focus the first option
|
// Show the dropdown and focus the first option
|
||||||
$dropdown.addClass('open').find('a:first').focus();
|
$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
|
// Change the dropdown position if mouse event was given
|
||||||
if (event) {
|
if (event) {
|
||||||
const position = {
|
const position = {
|
||||||
@ -78,8 +111,30 @@ OHIF.mixins.dropdown = new OHIF.Mixin({
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (centered) {
|
if (centered) {
|
||||||
|
// Center the dropdown menu based on the event mouse position
|
||||||
position.left -= $dropdownMenu.outerWidth() / 2;
|
position.left -= $dropdownMenu.outerWidth() / 2;
|
||||||
position.top -= $dropdownMenu.outerHeight() / 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');
|
$dropdownMenu.css(position).trigger('spatialChanged');
|
||||||
@ -89,9 +144,9 @@ OHIF.mixins.dropdown = new OHIF.Mixin({
|
|||||||
|
|
||||||
events: {
|
events: {
|
||||||
'click .form-action'(event, instance) {
|
'click .form-action'(event, instance) {
|
||||||
event.stopPropagation();
|
|
||||||
const $target = $(event.currentTarget);
|
const $target = $(event.currentTarget);
|
||||||
const isDisabled = $target.hasClass('disabled');
|
const isDisabled = $target.hasClass('disabled');
|
||||||
|
|
||||||
if (isDisabled) {
|
if (isDisabled) {
|
||||||
instance.close(false);
|
instance.close(false);
|
||||||
} else {
|
} else {
|
||||||
@ -100,21 +155,93 @@ OHIF.mixins.dropdown = new OHIF.Mixin({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
'click .dropdown'(event, instance) {
|
'mouseenter .form-action'(event, instance) {
|
||||||
const $target = $(event.target);
|
// Postpone the submenu opening if it's still being animated
|
||||||
if ($target.hasClass('disabled')) {
|
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.preventDefault();
|
||||||
event.stopPropagation();
|
parentInstance.closeSubmenu(true);
|
||||||
} else {
|
}
|
||||||
instance.close(false);
|
|
||||||
|
// 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) {
|
'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(() => {
|
Meteor.defer(() => {
|
||||||
const $focus = $(':focus');
|
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
|
.dropdown
|
||||||
cursor: default
|
cursor: default
|
||||||
@ -10,10 +10,10 @@
|
|||||||
transform(scale(0))
|
transform(scale(0))
|
||||||
transition(transform 0.3s ease)
|
transition(transform 0.3s ease)
|
||||||
|
|
||||||
&.dropdown-menu-left
|
&.dropdown-menu-left, &.origin-top-left
|
||||||
transform-origin(0% 0%)
|
transform-origin(0% 0%)
|
||||||
|
|
||||||
&.dropdown-menu-right
|
&.dropdown-menu-right, &.origin-top-right
|
||||||
transform-origin(100% 0%)
|
transform-origin(100% 0%)
|
||||||
|
|
||||||
li.divider
|
li.divider
|
||||||
@ -33,21 +33,11 @@
|
|||||||
max-height: 18px
|
max-height: 18px
|
||||||
vertical-align: middle
|
vertical-align: middle
|
||||||
|
|
||||||
&.open ul.dropdown-menu
|
&.open>ul.dropdown-menu
|
||||||
transform(scale(1))
|
transform(scale(1))
|
||||||
|
|
||||||
|
|
||||||
.dropdown-submenu
|
.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
|
&:hover
|
||||||
|
|
||||||
@ -55,17 +45,17 @@
|
|||||||
display: block
|
display: block
|
||||||
|
|
||||||
&>a:after
|
&>a:after
|
||||||
border-left-color:#FFFFFF
|
border-left-color: #000000
|
||||||
|
|
||||||
&>a:after
|
&>a:after
|
||||||
display: block
|
display: block
|
||||||
float: right
|
float: right
|
||||||
content: " "
|
content: ' '
|
||||||
width: 0
|
width: 0
|
||||||
height: 0
|
height: 0
|
||||||
border-color: transparent
|
border-color: transparent
|
||||||
border-style: solid
|
border-style: solid
|
||||||
border-width: 5px 0 5px 5px
|
border-width: 5px 0 5px 5px
|
||||||
border-left-color: #CCCCCC
|
border-left-color: #666666
|
||||||
margin-top: 5px
|
margin-top: 5px
|
||||||
margin-right: -10px
|
margin-right: -10px
|
||||||
|
|||||||
@ -27,9 +27,6 @@
|
|||||||
<span>{{item.text}}</span>
|
<span>{{item.text}}</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/link}}
|
{{/link}}
|
||||||
{{#if item.items}}
|
|
||||||
{{>dropdownFormMenu items=item.items}}
|
|
||||||
{{/if}}
|
|
||||||
</li>
|
</li>
|
||||||
{{#if item.separatorAfter}}
|
{{#if item.separatorAfter}}
|
||||||
<li role="separator" class="divider"></li>
|
<li role="separator" class="divider"></li>
|
||||||
|
|||||||
@ -23,6 +23,7 @@ label.wrapperLabel
|
|||||||
|
|
||||||
& + .wrapperText
|
& + .wrapperText
|
||||||
theme('color', '$textPrimaryColor')
|
theme('color', '$textPrimaryColor')
|
||||||
|
cursor: auto
|
||||||
|
|
||||||
input[type=text], input[type=password]
|
input[type=text], input[type=password]
|
||||||
theme('background-color', '$uiGray')
|
theme('background-color', '$uiGray')
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user