diff --git a/Packages/ohif-core/client/components/base/mixins/dropdown.js b/Packages/ohif-core/client/components/base/mixins/dropdown.js index 4a66959be..631a42b7e 100644 --- a/Packages/ohif-core/client/components/base/mixins/dropdown.js +++ b/Packages/ohif-core/client/components/base/mixins/dropdown.js @@ -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); } }); } diff --git a/Packages/ohif-core/client/components/bootstrap/dropdown/dropdown.styl b/Packages/ohif-core/client/components/bootstrap/dropdown/dropdown.styl index 76010918a..4d90fdc6b 100644 --- a/Packages/ohif-core/client/components/bootstrap/dropdown/dropdown.styl +++ b/Packages/ohif-core/client/components/bootstrap/dropdown/dropdown.styl @@ -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 diff --git a/Packages/ohif-core/client/components/bootstrap/dropdown/form.html b/Packages/ohif-core/client/components/bootstrap/dropdown/form.html index d9163a9ec..ac7d668d9 100644 --- a/Packages/ohif-core/client/components/bootstrap/dropdown/form.html +++ b/Packages/ohif-core/client/components/bootstrap/dropdown/form.html @@ -27,9 +27,6 @@ {{item.text}} {{/if}} {{/link}} - {{#if item.items}} - {{>dropdownFormMenu items=item.items}} - {{/if}} {{#if item.separatorAfter}} diff --git a/Packages/ohif-design/styles/common/form.styl b/Packages/ohif-design/styles/common/form.styl index 31fe235b6..1ed73ed10 100644 --- a/Packages/ohif-design/styles/common/form.styl +++ b/Packages/ohif-design/styles/common/form.styl @@ -23,6 +23,7 @@ label.wrapperLabel & + .wrapperText theme('color', '$textPrimaryColor') + cursor: auto input[type=text], input[type=password] theme('background-color', '$uiGray')