From 65ed5b3fce1be7f27f45b50abb7dc5625acaa2de Mon Sep 17 00:00:00 2001 From: Molkobain Date: Mon, 22 Mar 2021 13:21:31 +0100 Subject: [PATCH] Popover menu: Refactor to remove the necessity of coupling JS and PHP code to instantiate it correctly --- application/dashboard.class.inc.php | 27 +-- application/displayblock.class.inc.php | 55 ++---- css/backoffice/components/_newsroom-menu.scss | 1 - js/components/newsroom-menu.js | 38 ++-- js/components/popover-menu.js | 175 ++++++++++++++-- js/layouts/activity-panel/activity-panel.js | 3 - js/layouts/navigation-menu.js | 19 -- .../NewsroomMenu/NewsroomMenuFactory.php | 9 +- .../Component/PopoverMenu/PopoverMenu.php | 186 +++++++++++++++++- .../PopoverMenu/PopoverMenuFactory.php | 5 +- .../CaseLogEntryForm/CaseLogEntryForm.php | 12 +- .../CaseLogEntryFormFactory.php | 6 +- .../components/popover-menu/layout.js.twig | 6 + .../caselog-entry-form/layout.html.twig | 4 +- 14 files changed, 409 insertions(+), 137 deletions(-) diff --git a/application/dashboard.class.inc.php b/application/dashboard.class.inc.php index 4e31c401f..1867f9d0d 100644 --- a/application/dashboard.class.inc.php +++ b/application/dashboard.class.inc.php @@ -1123,24 +1123,9 @@ JS } else { $oToolbar = $oDashboard->GetToolbar(); } - $oActionButton = ButtonUIBlockFactory::MakeIconAction('fas fa-ellipsis-v', Dict::S($sName), $sName, '', false, $sMenuTogglerId); - $oActionButton->AddCSSClasses(['ibo-top-bar--toolbar-dashboard-menu-toggler', 'ibo-action-button']); - $oActionButton->SetJsCode(<<AddCSSClass('ibo-top-bar--toolbar-dashboard-menu-toggler') + ->AddCSSClass('ibo-action-button'); $oToolbar->AddSubBlock($oActionButton); @@ -1161,7 +1146,11 @@ JS utils::GetPopupMenuItems($oPage, iPopupMenuExtension::MENU_DASHBOARD_ACTIONS, $this, $aActions); - $oToolbar->AddSubBlock($oPage->GetPopoverMenu($sPopoverMenuId, $aActions)); + $oActionsMenu = $oPage->GetPopoverMenu($sPopoverMenuId, $aActions) + ->SetTogglerJSSelector("#$sMenuTogglerId"); + + $oToolbar->AddSubBlock($oActionButton) + ->AddSubBlock($oActionsMenu); $sReloadURL = $this->GetReloadURL(); $oPage->add_script( diff --git a/application/displayblock.class.inc.php b/application/displayblock.class.inc.php index 29fad472d..7d74f3082 100644 --- a/application/displayblock.class.inc.php +++ b/application/displayblock.class.inc.php @@ -11,6 +11,7 @@ use Combodo\iTop\Application\UI\Base\Component\Dashlet\DashletFactory; use Combodo\iTop\Application\UI\Base\Component\DataTable\DataTableUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Html\Html; use Combodo\iTop\Application\UI\Base\Component\Pill\PillFactory; +use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenu; use Combodo\iTop\Application\UI\Base\Component\Toolbar\Separator\ToolbarSeparatorUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Toolbar\ToolbarUIBlockFactory; use Combodo\iTop\Application\UI\Base\iUIBlock; @@ -1805,9 +1806,9 @@ class MenuBlock extends DisplayBlock $oSet = new CMDBObjectSet($this->m_oFilter); $sRefreshAction = $aExtraParams['sRefreshAction'] ?? ''; - /** @var $aRegularActions Any action other than a transition */ + /** @var array $aRegularActions Any action other than a transition */ $aRegularActions = []; - /** @var $aTransitionActions Only transitions */ + /** @var array $aTransitionActions Only transitions */ $aTransitionActions = []; if ((!isset($aExtraParams['selection_mode']) || $aExtraParams['selection_mode'] == "") && $this->m_sStyle != 'listInObject') { $oAppContext = new ApplicationContext(); @@ -2201,27 +2202,14 @@ class MenuBlock extends DisplayBlock $sName = 'UI:Menu:Transitions'; } $oActionButton = ButtonUIBlockFactory::MakeIconAction('fas fa-map-signs', Dict::S($sName), $sName, '', false, $sTransitionActionsMenuTogglerId) - ->AddCSSClasses(['ibo-action-button', 'ibo-transition-action-button']) - ->SetJsCode(<<AddCSSClasses(['ibo-action-button', 'ibo-transition-action-button']); + + $oTransitionActionsMenu = $oPage->GetPopoverMenu($sTransitionActionsPopoverMenuId, $aTransitionActions) + ->SetTogglerJSSelector("#$sTransitionActionsMenuTogglerId") + ->AddVisualHintToToggler(); - // TODO 3.0.0: Try to handle the JS above in a nicer place or through block options $oActionsToolbar->AddSubBlock($oActionButton) - ->AddSubBlock($oPage->GetPopoverMenu($sTransitionActionsPopoverMenuId, $aTransitionActions)); + ->AddSubBlock($oTransitionActionsMenu); } // Separator between transitions and regulars @@ -2294,27 +2282,14 @@ JS $sName = 'UI:Menu:Actions'; } $oActionButton = ButtonUIBlockFactory::MakeIconAction('fas fa-ellipsis-v', Dict::S($sName), $sName, '', false, $sRegularActionsMenuTogglerId) - ->AddCSSClasses(['ibo-action-button', 'ibo-regular-action-button']) - ->SetJsCode(<<AddCSSClasses(['ibo-action-button', 'ibo-regular-action-button']); + + $oRegularActionsMenu = $oPage->GetPopoverMenu($sRegularActionsPopoverMenuId, $aRegularActions) + ->SetTogglerJSSelector("#$sRegularActionsMenuTogglerId") + ->SetContainer(PopoverMenu::ENUM_CONTAINER_BODY); - // TODO 3.0.0: Try to handle the JS above in a nicer place or through block options $oActionsToolbar->AddSubBlock($oActionButton) - ->AddSubBlock($oPage->GetPopoverMenu($sRegularActionsPopoverMenuId, $aRegularActions)); + ->AddSubBlock($oRegularActionsMenu); } } diff --git a/css/backoffice/components/_newsroom-menu.scss b/css/backoffice/components/_newsroom-menu.scss index 7e2554c0e..ca3931088 100644 --- a/css/backoffice/components/_newsroom-menu.scss +++ b/css/backoffice/components/_newsroom-menu.scss @@ -103,7 +103,6 @@ img.ibo-navigation-menu--notifications--item--image:not([src=""]) ~ i.ibo-naviga } .ibo-navigation-menu--notifications-show-all-multiple ~ .ibo-popover-menu{ - bottom: 0; .ibo-navigation-menu--notifications--item--new-message-indicator{ display: inline-block; margin-right: $ibo-navigation-menu--notifications-show-all-multiple--ibo-popover-menu--indicator--margin-right; diff --git a/js/components/newsroom-menu.js b/js/components/newsroom-menu.js index 21deacb28..9d567e760 100644 --- a/js/components/newsroom-menu.js +++ b/js/components/newsroom-menu.js @@ -40,19 +40,20 @@ $(function() _initializePopoverMenu: function() { var me = this; + + // Important: For now, the popover menu is manually instantiated even though the PHP NewsroomMenu class inherits PopoverMenu because the jQuery widget doesn't. We might refactor this in the future. $(me.element).popover_menu({'toggler': this.js_selectors.menu_toggler}); - $(this.js_selectors.menu_toggler).on('click', function(oEvent) { + $(this.js_selectors.menu_toggler).on('click', function (oEvent) { var oEventTarget = $(oEvent.target); var aEventTargetPos = oEventTarget.position(); var aEventTargetOffset = oEventTarget.offset(); - $iHeight = Math.abs(aEventTargetOffset.top - 100); + $iHeight = Math.abs(aEventTargetOffset.top-100); $(me.element).css({ - 'max-height': $iHeight + 'px', - 'top': (aEventTargetPos.top + parseInt(oEventTarget.css('marginTop'), 10) - Math.min($(me.element).height(), $iHeight)) + 'px', - 'left': (aEventTargetPos.left + parseInt(oEventTarget.css('marginLeft'), 10) + oEventTarget.width()) + 'px', + 'max-height': $iHeight+'px', + 'top': (aEventTargetPos.top+parseInt(oEventTarget.css('marginTop'), 10)-Math.min($(me.element).height(), $iHeight))+'px', + 'left': (aEventTargetPos.left+parseInt(oEventTarget.css('marginLeft'), 10)+oEventTarget.width())+'px', }); - $(me.element).popover_menu("togglePopup"); }); this.element.addClass(this.css_classes.newsroom_menu); $(this.js_selectors.menu_toggler).addClass('ibo-is-loaded'); @@ -214,7 +215,7 @@ $(function() }, _buildNoMessageItem: function() { - return '
' + Dict.S(this.options.labels.no_notification) + + return '
' + Dict.S(this.options.labels.no_notification) + '
' + this.options.no_message_icon + '
'; }, _buildSingleShowAllMessagesItem: function() @@ -269,10 +270,10 @@ $(function() sMessageSection += sNoMessageItem; } sMessageSection += '
'; - + if (this.options.providers.length == 1) { - var SingleShowAllMessagesItem = this._buildSingleShowAllMessagesItem(); + var SingleShowAllMessagesItem = this._buildSingleShowAllMessagesItem(); sShowAllMessagesSection += SingleShowAllMessagesItem; sShowAllMessagesSection += '' } @@ -303,19 +304,14 @@ $(function() // Add class to show there is no messages $(this.js_selectors.menu_toggler).addClass(this.css_classes.empty); } - - if (this.options.providers.length != 1) - { - var oElem = $('[data-role="ibo-navigation-menu--notifications-show-all-multiple"]~[data-role="ibo-popover-menu"]'); - oElem.popover_menu({'toggler': '[data-role="ibo-navigation-menu--notifications-show-all-multiple"]'}); - $('[data-role="ibo-navigation-menu--notifications-show-all-multiple"]').on('click', function(oEvent){ - var oEventTarget = $(oEvent.target); - var aEventTargetPos = oEventTarget.position(); - oElem.css({ - 'left': (aEventTargetPos.left + parseInt(oEventTarget.css('marginLeft'), 10) + oEventTarget.width()) + 'px' - }); - oElem.popover_menu("togglePopup"); + if (this.options.providers.length != 1) { + var oElem = $('[data-role="ibo-navigation-menu--notifications-show-all-multiple"]~[data-role="ibo-popover-menu"]'); + oElem.popover_menu({ + 'toggler': '[data-role="ibo-navigation-menu--notifications-show-all-multiple"]', + 'position': { + 'horizontal': "(oTargetPos.left+parseInt(oTargetElem.css('marginLeft'), 10)+(oTargetElem.outerWidth() / 2)-(oElem.outerWidth() / 2))+'px'", + }, }); } diff --git a/js/components/popover-menu.js b/js/components/popover-menu.js index 46c145a3c..68f71b586 100644 --- a/js/components/popover-menu.js +++ b/js/components/popover-menu.js @@ -26,7 +26,26 @@ $(function() // default options options: { + // Valid JS selector of the DOM element toggling the menu on click toggler: '', + // Container element of the menu. Can be either 'parent' (default, better performance) or 'body' (use it if the menu gets cut by the hidden overflow of its parent). + container: 'parent', + // Position of the menu, relative to a DOM target element. Default target is 'toggler', but any valid JS selector is also accepted + position: { + // DOM element used to compute the menu relative position from. Value be 'toggler' to use the 'toggler' option or any valid JS selector. + target: 'toggler', + // Relative vertical position of the menu from the target. Value can be 'below' or 'above' for the menu to be strictly below/above the target, + // or a JS expression to be evaluated that must return pixels (eg. (oTargetPos.top + oTarget.outerHeight(true)) + 'px') + vertical: 'below', + // Relative horizontal position of the menu from the target. Value can be 'align_inner_left' or 'align_inner_right' for the menu to be aligned with the target border, + // or a JS expression to be evaluated that must return pixels (eg. (oTargetPos.left + oTarget.outerWidth(true) - popover.width()) + 'px') + // JS vars that can be used in the expression: + // - oElem + // - oTargetElem + // - oTargetPos + horizontal: 'align_inner_right', + }, + add_visual_hint_to_toggler: false }, css_classes: { @@ -41,12 +60,22 @@ $(function() // the constructor _create: function () { - this._bindEvents(); - this._closePopup(); + // Consistency checks + // - When target position set to 'toggler', ensure that a toggler is indeed set + if (('toggler' === this.options.position.target) && (false === this._hasToggler())) { + CombodoJSConsole.Error('Could not instantiate menu as the position target is set to "toggler" but no toggler set'); + } + // Build markup if (true === this.options.add_visual_hint_to_toggler) { this._addVisualHintToToggler(); } + if ('body' === this.options.container) { + this.element.appendTo($('body')); + } + + this._bindEvents(); + this._closePopup(); }, // events bound via _bind are removed automatically // revert other modifications here @@ -56,6 +85,27 @@ $(function() const me = this; const oBodyElem = $('body'); + // Toggler + if (true === this._hasToggler()) { + oBodyElem.find(this.options.toggler).on('click', function (oEvent) { + me._onTogglerClick(oEvent); + }); + } + + // Force menu to close on scroll when it is positioned on the body, otherwise it will not follow it's target and it will look buggy. + // Also, we decided not to update to position during scroll for to avoid performance drop. + if ('body' === this.options.container) { + // Important: This event is not bind using jQuery but the native method so we can set the "passive" option to minimize performance drops + // as the 'scroll' event is extremely CPU consuming. + // TODO 3.0.0: Make it work, event seems not to be triggered on user scroll + // window.addEventListener('scroll', function () { + // me._onBodyScroll(); + // }, { + // passive: true + // }) + } + + // Menu items this.element.find(this.js_selectors.item).on('click', function (oEvent) { me._closePopup(); }); @@ -67,6 +117,61 @@ $(function() }, // Events callbacks + _onTogglerClick: function (oEvent) { + // Avoid anchor / link default behavior + oEvent.preventDefault(); + + // Only recompute position when the menu is closed and about to be opened + if (false === this._isOpened()) { + const oTargetElem = ('toggler' === this.options.position.target) ? $(this.options.toggler) : $(this.options.position.target); + const oTargetPos = ('parent' === this.options.container) ? oTargetElem.position() : oTargetElem.offset(); + + let oNextCSSPosition = { + 'z-index': 1, + }; + const sVerticalPosExp = this.options.position.vertical; + const sHorizontalPosExp = this.options.position.horizontal; + + // Position referential + if ('body' === this.options.container) { + oNextCSSPosition['position'] = 'fixed'; + oNextCSSPosition['z-index'] = 30; // 30 to be above #ibo-page-container (10) and #ibo-navigation-menu (20) + } + + // Vertical + if ('below' === sVerticalPosExp) { + oNextCSSPosition['top'] = (oTargetPos.top+oTargetElem.outerHeight())+'px'; + } else if ('above' === sVerticalPosExp) { + oNextCSSPosition['top'] = (oTargetPos.top-this.element.outerHeight())+'px'; + } else { + let oTmpFunction = eval('(oElem, oTargetElem, oTargetPos) => '+sVerticalPosExp); + oNextCSSPosition['top'] = oTmpFunction(this.element, oTargetElem, oTargetPos); + } + + // Horizontal + if ('align_inner_left' === sHorizontalPosExp) { + oNextCSSPosition['left'] = (oTargetPos.left)+'px'; + } else if ('align_outer_left' === sHorizontalPosExp) { + oNextCSSPosition['left'] = (oTargetPos.left-this.element.width())+'px'; + } else if ('align_inner_right' === sHorizontalPosExp) { + oNextCSSPosition['left'] = (oTargetPos.left+oTargetElem.outerWidth(true)-this.element.width())+'px'; + } else if ('align_outer_right' === sHorizontalPosExp) { + oNextCSSPosition['left'] = (oTargetPos.left+oTargetElem.outerWidth(true))+'px'; + } else { + let oTmpFunction = eval('(oElem, oTargetElem, oTargetPos) => '+sHorizontalPosExp); + oNextCSSPosition['left'] = oTmpFunction(this.element, oTargetElem, oTargetPos); + } + + this.element.css(oNextCSSPosition); + } + + this.togglePopup(); + }, + _onBodyScroll: function () { + if (true === this._isOpened()) { + this._closePopup(); + } + }, _onBodyClick: function (oEvent) { if ($(oEvent.target.closest(this.js_selectors.menu)).length === 0 && $(oEvent.target.closest(this.options.toggler)).length === 0) { this._closePopup(); @@ -74,6 +179,21 @@ $(function() }, // Methods + /** + * @return {boolean} True if there is a toggler selector for the popover menu + * @private + */ + _hasToggler: function () { + if (('' === this.options.toggler) || (null === this.options.toggler)) { + return false; + } + + if ($(this.options.toggler).length === 0) { + return false; + } + + return true; + }, /** * Add a visual hint (caret) on the toggler * @@ -81,40 +201,59 @@ $(function() * @private */ _addVisualHintToToggler: function () { - if ('' === this.options.toggler) { + if (false === this._hasToggler()) { return false; } - const oTogglerElem = $(this.options.toggler); - if (oTogglerElem.length === 0) { - return false; - } - - oTogglerElem.append($(``)); + $(this.options.toggler).append($(``)); return true; }, + /** + * @return {boolean} True if the menu is currently opened + * @private + */ + _isOpened: function () { + return this.element.hasClass(this.css_classes.opened); + }, + /** + * Open the menu + * @return {void} + * @private + */ _openPopup: function () { this.element.addClass(this.css_classes.opened); }, + /** + * Close the menu + * @return {void} + * @private + */ _closePopup: function () { this.element.removeClass(this.css_classes.opened); }, + /** + * @api + * @return {void} + */ openPopup: function () { this._openPopup(); }, - closePopup: function() - { + /** + * @api + * @return {void} + */ + closePopup: function () { this._closePopup(); }, - togglePopup: function() - { - if(this.element.hasClass(this.css_classes.opened)) - { + /** + * @api + * @return {void} + */ + togglePopup: function () { + if (this.element.hasClass(this.css_classes.opened)) { this._closePopup(); - } - else - { + } else { this._openPopup(); } }, diff --git a/js/layouts/activity-panel/activity-panel.js b/js/layouts/activity-panel/activity-panel.js index 49da7d182..c57139cb2 100644 --- a/js/layouts/activity-panel/activity-panel.js +++ b/js/layouts/activity-panel/activity-panel.js @@ -134,9 +134,6 @@ $(function() this._ReformatDateTimes(); this._PrepareEntriesSubmitConfirmationDialog(); - // TODO 3.0.0: Modify PopoverMenu so we can pass it the ID of the block triggering the open/close - //$(this.element).find(this.js_selectors.send_choices_picker).popover_menu({toggler: this.js_selectors.send_button}); - this.element.trigger('ready.activity_panel.itop'); }, // events bound via _bind are removed automatically diff --git a/js/layouts/navigation-menu.js b/js/layouts/navigation-menu.js index 57572c25b..1fbd8fda9 100644 --- a/js/layouts/navigation-menu.js +++ b/js/layouts/navigation-menu.js @@ -100,11 +100,6 @@ $(function() this.element.find(this.js_selectors.menu_filter_hint_close).on('click', function (oEvent) { me._onFilterHintCloseClick(oEvent); }); - - // User info - this.element.find(this.js_selectors.user_menu_toggler).on('click', function (oEvent) { - me._onUserMenuTogglerClick(oEvent); - }); }, // Events callbacks @@ -195,20 +190,6 @@ $(function() SetUserPreference('navigation_menu.show_filter_hint', false, true); }, - _onUserMenuTogglerClick: function(oEvent) - { - // Avoid anchor glitch - oEvent.preventDefault(); - var oEventTarget = $(oEvent.target); - var aEventTargetPos = oEventTarget.position(); - - $(this.js_selectors.user_menu_container).css({ - 'top': (aEventTargetPos.top + parseInt(oEventTarget.css('marginTop'), 10) - $(this.js_selectors.user_menu).height()) + 'px', - 'left': (aEventTargetPos.left + parseInt(oEventTarget.css('marginLeft'), 10) + oEventTarget.width()) + 'px' - }); - $(this.js_selectors.user_menu).popover_menu('togglePopup'); - }, - // Methods _checkIfClickShouldCloseDrawer: function(oEvent) { diff --git a/sources/application/UI/Base/Component/PopoverMenu/NewsroomMenu/NewsroomMenuFactory.php b/sources/application/UI/Base/Component/PopoverMenu/NewsroomMenu/NewsroomMenuFactory.php index 376432b15..1b11b869d 100644 --- a/sources/application/UI/Base/Component/PopoverMenu/NewsroomMenu/NewsroomMenuFactory.php +++ b/sources/application/UI/Base/Component/PopoverMenu/NewsroomMenu/NewsroomMenuFactory.php @@ -20,7 +20,6 @@ namespace Combodo\iTop\Application\UI\Base\Component\PopoverMenu\NewsroomMenu; use appUserPreferences; -use Dict; use MetaModel; use UserRights; use utils; @@ -44,10 +43,10 @@ class NewsroomMenuFactory */ public static function MakeNewsroomMenuForNavigationMenu() { - $oMenu = new NewsroomMenu('ibo-navigation-menu--notifications-menu'); - $oMenu->SetParams(static::PrepareParametersForNewsroomMenu()); - - return $oMenu; + $oMenu = new NewsroomMenu('ibo-navigation-menu--notifications-menu'); + $oMenu->SetParams(static::PrepareParametersForNewsroomMenu()); + + return $oMenu; } /** diff --git a/sources/application/UI/Base/Component/PopoverMenu/PopoverMenu.php b/sources/application/UI/Base/Component/PopoverMenu/PopoverMenu.php index 988e2c10f..97a7c3077 100644 --- a/sources/application/UI/Base/Component/PopoverMenu/PopoverMenu.php +++ b/sources/application/UI/Base/Component/PopoverMenu/PopoverMenu.php @@ -42,11 +42,103 @@ class PopoverMenu extends UIBlock 'js/components/popover-menu.js', ]; + // Specific constants + /** @see static::$sContainer */ + public const ENUM_CONTAINER_BODY = 'body'; + /** @see static::$sContainer */ + public const ENUM_CONTAINER_PARENT = 'parent'; + /** @see static::$sTargetForPositionJSSelector */ + public const ENUM_TARGET_FOR_POSITION_TOGGLER = 'toggler'; + /** @see static::$sVerticalPosition */ + public const ENUM_VERTICAL_POSITION_ABOVE = 'above'; + /** @see static::$sVerticalPosition */ + public const ENUM_VERTICAL_POSITION_BELOW = 'below'; + /** + * @see static::$sHorizontalPosition + * + * +--------+ + * | Target | + * +--------+-----------+ + * | | + * | Menu | + * | | + * +--------------------+ + */ + public const ENUM_HORIZONTAL_POSITION_ALIGN_INNER_LEFT = 'align_inner_left'; + /** + * @see static::$sHorizontalPosition + * + * +--------+ + * | Target | + * +--------------------+--------+ + * | | + * | Menu | + * | | + * +--------------------+ + */ + public const ENUM_HORIZONTAL_POSITION_ALIGN_OUTER_LEFT = 'align_outer_left'; + /** + * @see static::$sHorizontalPosition + * + * +--------+ + * | Target | + * +-----------+--------+ + * | | + * | Menu | + * | | + * +--------------------+ + */ + public const ENUM_HORIZONTAL_POSITION_ALIGN_INNER_RIGHT = 'align_inner_right'; + /** + * @see static::$sHorizontalPosition + * + * +--------+ + * | Target | + * +--------+--------------------+ + * | | + * | Menu | + * | | + * +--------------------+ + */ + public const ENUM_HORIZONTAL_POSITION_ALIGN_OUTER_RIGHT = 'align_outer_right'; + + /** @see static::$sContainer */ + public const DEFAULT_CONTAINER = self::ENUM_CONTAINER_PARENT; + /** @see static::$sTargetForPositionJSSelector */ + public const DEFAULT_TARGET_FOR_POSITION = self::ENUM_TARGET_FOR_POSITION_TOGGLER; + /** @see static::$sVerticalPosition */ + public const DEFAULT_VERTICAL_POSITION = self::ENUM_VERTICAL_POSITION_BELOW; + /** @see static::$sHorizontalPosition */ + public const DEFAULT_HORIZONTAL_POSITION = self::ENUM_HORIZONTAL_POSITION_ALIGN_INNER_RIGHT; + /** @var string JS selector for the DOM element that should trigger the menu open/close */ protected $sTogglerJSSelector; /** @var bool Whether the menu should add a visual hint (caret down) on the toggler to help the user understand that clicking on the toggler won't do something right away, but will open a menu instead */ protected $bAddVisualHintToToggler; - /** @var array $aSections */ + /** @var string Container element of the menu. Can be either: + * * static::ENUM_CONTAINER_PARENT (default, better performance) + * * static::ENUM_CONTAINER_BODY (use it if the menu gets cut by the hidden overflow of its parent) + */ + protected $sContainer; + /** + * @var string JS selector for the DOM element the menu should be positioned relatively to. + * * static::ENUM_TARGET_FOR_POSITION_TOGGLER (default, a shortcut pointing to the toggler) + * * A JS selector + */ + protected $sTargetForPositionJSSelector; + /** @var string Relative vertical position of the menu from the target. Value can be: + * * static::ENUM_VERTICAL_POSITION_BELOW for the menu to be directly below the target + * * static::ENUM_VERTICAL_POSITION_ABOVE for the menu to be directly above the target + * * A JS expression to be evaluated that must return pixels (eg. (oTargetPos.top + oTarget.outerHeight(true)) + 'px') + */ + protected $sVerticalPosition; + /** @var string Relative horizontal position of the menu from the target. Value can be: + * * static::ENUM_HORIZONTAL_POSITION_ALIGN_INNER_LEFT for the menu to be aligned with the target's left side + * * static::ENUM_HORIZONTAL_POSITION_ALIGN_INNER_RIGHT for the menu to be aligned with the target's right side + * * A JS expression to be evaluated that must return pixels (eg. (oTargetPos.left + oTarget.outerWidth(true) - popover.width()) + 'px') + */ + protected $sHorizontalPosition; + /** @var array */ protected $aSections; /** @@ -59,6 +151,10 @@ class PopoverMenu extends UIBlock parent::__construct($sId); $this->sTogglerJSSelector = ''; $this->bAddVisualHintToToggler = false; + $this->sContainer = static::DEFAULT_CONTAINER; + $this->sTargetForPositionJSSelector = static::DEFAULT_TARGET_FOR_POSITION; + $this->sVerticalPosition = static::DEFAULT_VERTICAL_POSITION; + $this->sHorizontalPosition = static::DEFAULT_HORIZONTAL_POSITION; $this->aSections = []; } @@ -113,6 +209,94 @@ class PopoverMenu extends UIBlock return $this->bAddVisualHintToToggler; } + /** + * @param string $sContainer + * + * @return $this + * @uses static::$sContainer + */ + public function SetContainer(string $sContainer) + { + $this->sContainer = $sContainer; + + return $this; + } + + /** + * @return string + * @uses static::$sContainer + */ + public function GetContainer(): string + { + return $this->sContainer; + } + + /** + * @param string $sJSSelector + * + * @return $this + * @uses static::$sTargetForPositionJSSelector + */ + public function SetTargetForPositionJSSelector(string $sJSSelector) + { + $this->sTargetForPositionJSSelector = $sJSSelector; + + return $this; + } + + /** + * @return string + * @uses static::$sTargetForPositionJSSelector + */ + public function GetTargetForPositionJSSelector(): string + { + return $this->sTargetForPositionJSSelector; + } + + /** + * @param string $sPosition + * + * @return $this + * @uses static::$sVerticalPosition + */ + public function SetVerticalPosition(string $sPosition) + { + $this->sVerticalPosition = $sPosition; + + return $this; + } + + /** + * @return string + * @uses static::$sVerticalPosition + */ + public function GetVerticalPosition(): string + { + return $this->sVerticalPosition; + } + + /** + * @param string $sPosition + * + * @return $this + * @uses static::$sHorizontalPosition + */ + public function SetHorizontalPosition(string $sPosition) + { + $this->sHorizontalPosition = $sPosition; + + return $this; + } + + /** + * @return string + * @uses static::$sHorizontalPosition + */ + public function GetHorizontalPosition(): string + { + return $this->sHorizontalPosition; + } + /** * Add a section $sId if not already existing. * Important: It does not reset the section. diff --git a/sources/application/UI/Base/Component/PopoverMenu/PopoverMenuFactory.php b/sources/application/UI/Base/Component/PopoverMenu/PopoverMenuFactory.php index 4886be66d..190df72df 100644 --- a/sources/application/UI/Base/Component/PopoverMenu/PopoverMenuFactory.php +++ b/sources/application/UI/Base/Component/PopoverMenu/PopoverMenuFactory.php @@ -49,7 +49,10 @@ class PopoverMenuFactory public static function MakeUserMenuForNavigationMenu() { $oMenu = new PopoverMenu('ibo-navigation-menu--user-menu'); - $oMenu->SetTogglerJSSelector('[data-role="ibo-navigation-menu--user-menu--toggler"]'); + $oMenu->SetTogglerJSSelector('[data-role="ibo-navigation-menu--user-menu--toggler"]') + ->SetContainer(PopoverMenu::ENUM_CONTAINER_BODY) + ->SetHorizontalPosition(PopoverMenu::ENUM_HORIZONTAL_POSITION_ALIGN_OUTER_RIGHT) + ->SetVerticalPosition(PopoverMenu::ENUM_VERTICAL_POSITION_ABOVE); // Allowed portals $aAllowedPortalsItems = static::PrepareAllowedPortalsItemsForUserMenu(); diff --git a/sources/application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryForm.php b/sources/application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryForm.php index 2f9f26e30..c72e1b685 100644 --- a/sources/application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryForm.php +++ b/sources/application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryForm.php @@ -299,18 +299,18 @@ class CaseLogEntryForm extends UIContentBlock $aSubBlocks = []; $aSubBlocks[$this->GetTextInput()->GetId()] = $this->GetTextInput(); - foreach ($this->GetExtraActionButtons() as $oExtraActionButton) - { + foreach ($this->GetExtraActionButtons() as $oExtraActionButton) { $aSubBlocks[$oExtraActionButton->GetId()] = $oExtraActionButton; } - foreach ($this->GetMainActionButtons() as $oMainActionButton) - { + foreach ($this->GetMainActionButtons() as $oMainActionButton) { $aSubBlocks[$oMainActionButton->GetId()] = $oMainActionButton; } - $aSubBlocks[$this->GetSendButtonPopoverMenu()->GetId()] = $this->GetSendButtonPopoverMenu(); - + if ($this->HasSendButtonPopoverMenu()) { + $aSubBlocks[$this->GetSendButtonPopoverMenu()->GetId()] = $this->GetSendButtonPopoverMenu(); + } + return $aSubBlocks; } diff --git a/sources/application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryFormFactory.php b/sources/application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryFormFactory.php index 82dd0e4c4..e82c83cdb 100644 --- a/sources/application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryFormFactory.php +++ b/sources/application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryFormFactory.php @@ -37,8 +37,10 @@ class CaseLogEntryFormFactory $oCaseLogEntryForm = new CaseLogEntryForm($oObject, $sCaseLogAttCode); $oCaseLogEntryForm->SetSubmitModeFromHostObjectMode($sObjectMode) ->AddMainActionButtons(static::PrepareCancelButton()) - ->AddMainActionButtons(static::PrepareSaveButton()) - ->SetSendButtonPopoverMenu(static::PrepareSendActionSelectionPopoverMenu($oObject, $sCaseLogAttCode)); + ->AddMainActionButtons(static::PrepareSaveButton()); + + // TODO 3.0.0: Will be fixed by N°3649 +// ->SetSendButtonPopoverMenu(static::PrepareSendActionSelectionPopoverMenu($oObject, $sCaseLogAttCode)); return $oCaseLogEntryForm; } diff --git a/templates/base/components/popover-menu/layout.js.twig b/templates/base/components/popover-menu/layout.js.twig index 54169a62d..333f6fbb2 100644 --- a/templates/base/components/popover-menu/layout.js.twig +++ b/templates/base/components/popover-menu/layout.js.twig @@ -1,4 +1,10 @@ $('#{{ oUIBlock.GetId() }}').popover_menu({ toggler: {{ oUIBlock.GetTogglerJSSelector()|json_encode|raw }}, + container: {{ oUIBlock.GetContainer()|json_encode|raw }}, + position: { + target: {{ oUIBlock.GetTargetForPositionJSSelector()|json_encode|raw }}, + vertical: {{ oUIBlock.GetVerticalPosition()|json_encode|raw }}, + horizontal: {{ oUIBlock.GetHorizontalPosition()|json_encode|raw }}, + }, add_visual_hint_to_toggler: {{ oUIBlock.HasVisualHintToAddToToggler()|var_export }} }); \ No newline at end of file diff --git a/templates/base/layouts/activity-panel/caselog-entry-form/layout.html.twig b/templates/base/layouts/activity-panel/caselog-entry-form/layout.html.twig index 8cde8d0f8..74a8a410c 100644 --- a/templates/base/layouts/activity-panel/caselog-entry-form/layout.html.twig +++ b/templates/base/layouts/activity-panel/caselog-entry-form/layout.html.twig @@ -17,7 +17,9 @@ {% for FormActionButton in oUIBlock.GetMainActionButtons() %} {{ render_block(FormActionButton, {aPage: aPage}) }} {% endfor %} - {{ render_block(oUIBlock.GetSendButtonPopoverMenu(), {aPage: aPage}) }} + {% if oUIBlock.HasSendButtonPopoverMenu() %} + {{ render_block(oUIBlock.GetSendButtonPopoverMenu(), {aPage: aPage}) }} + {% endif %}