* @package Combodo\iTop\Application\UI\Base\Component\PopoverMenu * @internal * @since 3.0.0 */ class PopoverMenu extends UIBlock { // Overloaded constants public const BLOCK_CODE = 'ibo-popover-menu'; public const DEFAULT_HTML_TEMPLATE_REL_PATH = 'base/components/popover-menu/layout'; public const DEFAULT_JS_TEMPLATE_REL_PATH = 'base/components/popover-menu/layout'; public const DEFAULT_JS_FILES_REL_PATH = [ '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 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; /** * PopoverMenu constructor. * * @param string|null $sId */ public function __construct(?string $sId = null) { 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 = []; } /** * @param string $sSelector * * @return $this * @uses static::$sTogglerJSSelector */ public function SetTogglerJSSelector(string $sSelector) { $this->sTogglerJSSelector = $sSelector; return $this; } /** * Shortcut to avoid passing the '#' in static::SetTogglerJSSelector(). * * @param string $sId * * @return $this */ public function SetTogglerFromId(string $sId) { $this->SetTogglerJSSelector('#'.$sId); return $this; } /** * Shortcut to get the toggler JS selector directly from the block * * @param \Combodo\iTop\Application\UI\Base\iUIBlock $oBlock * * @return $this */ public function SetTogglerFromBlock(iUIBlock $oBlock) { $this->SetTogglerFromId($oBlock->GetId()); return $this; } /** * @return string * @uses static::$sTogglerJSSelector */ public function GetTogglerJSSelector(): string { return $this->sTogglerJSSelector; } /** * @return bool * @uses static::$sTogglerJSSelector */ public function HasToggler(): bool { return !empty($this->sTogglerJSSelector); } /** * @return $this * @uses static::$bAddVisualHintToToggler */ public function AddVisualHintToToggler() { $this->bAddVisualHintToToggler = true; return $this; } /** * @return bool * @uses static::$bAddVisualHintToToggler */ public function HasVisualHintToAddToToggler(): bool { 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. * * @param string $sId * * @return $this */ public function AddSection(string $sId) { if (false === $this->HasSection($sId)) { $this->aSections[$sId] = [ 'aItems' => [], ]; } return $this; } /** * Remove the $sId section. * Note: If the section does not exist, we silently proceed anyway. * * @param string $sId * * @return $this * @throws \Exception */ public function RemoveSection(string $sId) { if (true === $this->HasSection($sId)) { unset($this->aSections[$sId]); } return $this; } /** * Return true if the $sId section exists * * @param string $sId * * @return bool */ public function HasSection(string $sId): bool { return array_key_exists($sId, $this->aSections); } /** * Clear the $sId section from all its items. * * @param string $sId * * @return $this * @throws \Exception */ public function ClearSection(string $sId) { if (false === $this->HasSection($sId)) { throw new Exception('Could not clear section "'.$sId.'" as it does not exist in the "'.$this->GetId().'" menu'); } $this->aSections[$sId]['aItems'] = []; return $this; } /** * Return the sections * * @return array */ public function GetSections(): array { return $this->aSections; } /** * @return bool Whether there are some sections, even if they have no items. * @uses static::$aSections */ public function HasSections(): bool { return !empty($this->aSections); } /** * Add the $oItem in the $sSectionId. If an item with the same ID already exists it will be overwritten. * * @param string $sSectionId * @param \Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenuItem\PopoverMenuItem $oItem * * @return $this * @throws \Exception */ public function AddItem(string $sSectionId, PopoverMenuItem $oItem) { if (false === $this->HasSection($sSectionId)) { $this->AddSection($sSectionId); } $this->aSections[$sSectionId]['aItems'][$oItem->GetId()] = $oItem; return $this; } /** * Remove the $sItemId from the $sSectionId. * Note: If the item is not in the section, we proceed silently. * * @param string $sSectionId * @param string $sItemId * * @return $this * @throws \Exception */ public function RemoveItem(string $sSectionId, string $sItemId) { if (false === $this->HasSection($sSectionId)) { throw new Exception('Could not remove en item from the "'.$sSectionId.'" as it does not seem to exist in the "'.$this->GetId().'" menu.'); } if (array_key_exists($sItemId, $this->aSections[$sSectionId]['aItems'])) { unset($this->aSections[$sSectionId]['aItems'][$sItemId]); } return $this; } /** * Add all $aItems to the $sSectionId after the existing items * * @param string $sSectionId * @param PopoverMenuItem[] $aItems * * @return $this * @throws \Exception */ public function AddItems(string $sSectionId, array $aItems) { foreach ($aItems as $oItem) { $this->AddItem($sSectionId, $oItem); } return $this; } /** * Set all $aItems at once in the $sSectionId, overwriting all existing. * * @param string $sSectionId * @param PopoverMenuItem[] $aItems * * @return $this * @throws \Exception */ public function SetItems(string $sSectionId, array $aItems) { if (false === $this->HasSection($sSectionId)) { throw new Exception('Could not set items to the "'.$sSectionId.'" section has it does not seem to exist in the "'.$this->GetId().'" menu.'); } $this->aSections[$sSectionId]['aItems'] = $aItems; return $this; } /** * @return bool Whether there is at least 1 section with some items * @uses static::$aSections */ public function HasItems(): bool { $bResult = false; foreach ($this->GetSections() as $sId => $aData) { if (!empty($aData['aItems'])) { $bResult = true; break; } } return $bResult; } /** * @inheritDoc */ public function GetSubBlocks(): array { $aSubBlocks = []; foreach ($this->aSections as $sSectionId => $aSectionData) { foreach ($aSectionData['aItems'] as $sItemId => $oItem) { $aSubBlocks[$sItemId] = $oItem; } } return $aSubBlocks; } }