* @license GPL2 or any later version * @since Stud.IP 3.5 */ class ActionMenu { const TEMPLATE_FILE_SINGLE = 'shared/action-menu-single.php'; const TEMPLATE_FILE_MULTIPLE = 'shared/action-menu.php'; const RENDERING_MODE_ICONS = 'icons'; const RENDERING_MODE_MENU = 'menu'; /** * Returns an instance. * * @return ActionMenu */ public static function get() { return new self(); } private $actions = []; private $attributes = []; private $condition_all = null; private $condition = true; /** * @var string $context The context for the action menu. */ protected $context = ''; /** * @var string|null $rendering_mode The forced rendering mode */ protected $rendering_mode = null; /** * Private constructur. * * @see ActionMenu::get() */ private function __construct() { $this->addCSSClass('action-menu'); } /** * Set condition for the next added item. If condition is false, * the item will not be added. * * @param bool $state State of the condition * @return ActionMenu instance to allow chaining */ public function condition($state) { $this->condition = (bool)$state; return $this; } /** * Set condition for all the next added items. If condition is false, * no items will be added. * * @param bool $state State of the condition * @return ActionMenu instance to allow chaining */ public function conditionAll($state) { $this->condition_all = $state; return $this; } /** * Checks the condition. Takes global and local (conditionAll() & * condition()) conditions into account. * * @return bool indicating whether the condition is met or not */ protected function checkCondition() { $result = $this->condition; if ($this->condition_all !== null) { $result = $result && $this->condition_all; } $this->condition = true; return $result; } /** * Adds a link to the list of actions. * * @param String|StudipLink $url Link target, eithe as string or * Stud.IP link. In the latter case, * all other parameters are ignored. * @param String|array $label Textual representation of the link * @param mixed $icon Optional icon (as Icon object) * @param array $attributes Optional attributes to add to the tag * @param mixed $index Optional index to access this link (remove for example) afterwards * @param mixed $before Optional index to insert this link before the link with given index. * @return ActionMenu instance to allow chaining */ public function addLink($url, $label = "", Icon $icon = null, array $attributes = [], $index = null, $before = null) { if ($this->checkCondition()) { if ($url instanceof StudipLink) { $action = [ 'type' => 'link', 'link' => $url->link, 'icon' => $url->icon, 'label' => $url->title, 'attributes' => $url->attributes, ]; } else { $action = [ 'type' => 'link', 'link' => $url, 'icon' => $icon, 'label' => $label, 'attributes' => $attributes, ]; } $index = $index ?: md5($action['link'] . json_encode($action['attributes'])); $action['index'] = $index; //now insert it possibly at the desired position: $before_key = null; if ($before) { $before_key = $this->getKeyForIndex($before); if ($before_key !== null) { array_splice($this->actions, $before_key, 0, $action); return $this; } } $current_key = $this->getKeyForIndex($index); if ($current_key !== null) { $this->actions[$current_key] = $action; return $this; } $this->removeLink($index); $this->actions[] = $action; } return $this; } /** * Tries to remove the link with the given index and returns true on success (else false) * @param $index : the index of the link. If the link had no special * index it's md5($url.json_encode($ttributes)). * @return bool : true if link was removed, false if index didn't exist */ public function removeLink($index) { $key = $this->getKeyForIndex($index); if ($key !== null) { unset($this->actions[$key]); return true; } return false; } /** * Adds a button to the list of actions. * * @param String $name Button name * @param String $label Textual representation of the name * @param mixed $icon Optional icon (as Icon object) * @param array $attributes Optional attributes to add to the tag * @return ActionMenu instance to allow chaining */ public function addButton($name, $label, Icon $icon = null, array $attributes = []) { if ($this->checkCondition()) { $this->actions[] = [ 'type' => 'button', 'name' => $name, 'icon' => $icon, 'label' => $label, 'attributes' => $attributes, 'index' => '' ]; } return $this; } /** * Adds a MultiPersonSearch object to the list of actions. * * @param MultiPersonSearch $mp MultiPersonSearch object * @return ActionMenu instance to allow chaining */ public function addMultiPersonSearch(MultiPersonSearch $mp) { if ($this->checkCondition()) { $this->actions[] = [ 'type' => 'multi-person-search', 'object' => $mp, 'index' => '' ]; } return $this; } /** * Adds a css classs to the root element in html. * * @param string $class Name of the css class * @return ActionMenu instance to allow chaining */ public function addCSSClass($class) { $this->addAttribute('class', $class, true); return $this; } /** * Adds an attribute to the root element in html. * * @param string $key Name of the attribute * @param string $value Value of the attribute * @param boolean $append Whether a current value should be append or not. */ public function addAttribute($key, $value, $append = false) { if (isset($this->attributes[$key]) && $append) { $this->attributes[$key] .= " {$value}"; } else { $this->attributes[$key] = $value; } } /** * Renders the action menu. If no item was added, an empty string will * be returned. If a single item was added, the item itself will be * displayed. Otherwise the whole menu will be rendered. * * @return String containing the html representation of the action menu */ public function render() { if (count($this->actions) === 0) { return ''; } $template_file = $this->getRenderingMode() === self::RENDERING_MODE_ICONS ? self::TEMPLATE_FILE_SINGLE : self::TEMPLATE_FILE_MULTIPLE; ; $template = $GLOBALS['template_factory']->open($template_file); $template->actions = array_map(function ($action) { $disabled = isset($action['attributes']['disabled']) && $action['attributes']['disabled'] !== false; if ($disabled && $action['icon']) { $action['icon'] = $action['icon']->copyWithRole(Icon::ROLE_INACTIVE); } return $action; }, $this->actions); $template->action_menu_title = $this->generateTitle(); $template->attributes = $this->attributes; return $template->render(); } /** * Magic method to render the menu as a string. * * @return String containing the html representation of the action menu * @see ActionMenu::render() */ public function __toString() { return $this->render(); } protected function getKeyForIndex($index) { foreach ($this->actions as $key => $value) { if ($value['index'] === $index) { return $key; } } return null; } /** * Sets the context for the menu. * * @param string $context The context to be set. * * @return ActionMenu The action menu instance (to allow chaining). */ public function setContext(string $context) { $this->context = $context; return $this; } /** * Forces an explicit rendering mode. * * @param string|null $mode The desired rendering mode or null for automatic detection * @return ActionMenu The action menu instance to allow chaining * @throws Exception */ public function setRenderingMode(?string $mode): ActionMenu { if ( $mode !== null && $mode !== self::RENDERING_MODE_ICONS && $mode !== self::RENDERING_MODE_MENU ) { throw new Exception("Invalid rendering mode '{$mode}'"); } $this->rendering_mode = $mode; return $this; } /** * Returns the rendering mode for this action menu. This is set by either * calling setRenderingMode or automatically determined by the configured * threshold. * * @return string */ public function getRenderingMode(): string { $rendering_mode = $this->rendering_mode; if ($rendering_mode === null) { $rendering_mode = count($this->actions) <= Config::get()->ACTION_MENU_THRESHOLD ? self::RENDERING_MODE_ICONS : self::RENDERING_MODE_MENU; } return $rendering_mode; } /** * Generates the title of the action menu, including its context, if the context has been set. * * @return string The title of the action menu. */ public function generateTitle() : string { if ($this->context) { return sprintf( _('Aktionsmenü für %s'), $this->context ); } else { return _('Aktionsmenü'); } } }